Skip to content

Commit

Permalink
Map local build path during WSL run (#729)
Browse files Browse the repository at this point in the history
  • Loading branch information
JanKrivanek authored Jan 29, 2023
1 parent ced30c2 commit 3aefe4c
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 15 deletions.
Binary file added docs/TestExplorerEnvironments.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,26 @@ Verify follows [Semantic Versioning](https://semver.org/). The same applies for
Snapshot changes do not trigger a major version change to avoid causing [Diamond dependency](https://en.wikipedia.org/wiki/Dependency_hell#Problems) issues for downstream extensions.


## Unit testing inside virtualized environment

Unit tests referencing `Verify` (including unit tests within this repository as well as any other code referencing `Verify`) can be run and debugged on a local virtualized environment supported by [Visual Studio Remote Testing](https://learn.microsoft.com/en-us/visualstudio/test/remote-testing?view=vs-2022).
Initial configurations have been added for `WSL` and net 7.0 linux docker via `testenvironments.json` (for third party code, the file needs to be copied or recreated next to the `.sln` solution file for solution to leverage the functionality).
Upon opening the Tests Explorer the advanced environments are available in the GUI:

![TestExplorerEnvironments](/docs/TestExplorerEnvironments.png)

This readme will not discuss definitive list of details for proper setup of the environments instead refer the following information sources and warn about particular gotchas:

* WSL runs
* Install [WSL](https://learn.microsoft.com/en-us/windows/wsl/about).
* Install a [distribution](https://aka.ms/wslstore).
* [Install .NET Runtime](https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu)
* Docker runs
* Install [Docker Desktop](https://www.docker.com/products/docker-desktop/)
* First run of docker scenario might need elevation ([Test project does not reference any .NET NuGet adapter](https://developercommunity.visualstudio.com/t/test-project-does-not-reference-any-net-nuget-adap/1311698) error)
* Third party test runners might not support this feature. Use [Visual Studio Test Explorer](https://learn.microsoft.com/en-us/visualstudio/test/run-unit-tests-with-test-explorer).


## Media

* [Compare object values in xUnit C# with Verify - Pierre Belin](https://goatreview.com/compare-object-values-xunit-csharp-verify/)
Expand Down
20 changes: 20 additions & 0 deletions readme.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,26 @@ Verify follows [Semantic Versioning](https://semver.org/). The same applies for
Snapshot changes do not trigger a major version change to avoid causing [Diamond dependency](https://en.wikipedia.org/wiki/Dependency_hell#Problems) issues for downstream extensions.


## Unit testing inside virtualized environment

Unit tests referencing `Verify` (including unit tests within this repository as well as any other code referencing `Verify`) can be run and debugged on a local virtualized environment supported by [Visual Studio Remote Testing](https://learn.microsoft.com/en-us/visualstudio/test/remote-testing?view=vs-2022).
Initial configurations have been added for `WSL` and net 7.0 linux docker via `testenvironments.json` (for third party code, the file needs to be copied or recreated next to the `.sln` solution file for solution to leverage the functionality).
Upon opening the Tests Explorer the advanced environments are available in the GUI:

![TestExplorerEnvironments](/docs/TestExplorerEnvironments.png)

This readme will not discuss definitive list of details for proper setup of the environments instead refer the following information sources and warn about particular gotchas:

* WSL runs
* Install [WSL](https://learn.microsoft.com/en-us/windows/wsl/about).
* Install a [distribution](https://aka.ms/wslstore).
* [Install .NET Runtime](https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu)
* Docker runs
* Install [Docker Desktop](https://www.docker.com/products/docker-desktop/)
* First run of docker scenario might need elevation ([Test project does not reference any .NET NuGet adapter](https://developercommunity.visualstudio.com/t/test-project-does-not-reference-any-net-nuget-adap/1311698) error)
* Third party test runners might not support this feature. Use [Visual Studio Test Explorer](https://learn.microsoft.com/en-us/visualstudio/test/run-unit-tests-with-test-explorer).


## Media

* [Compare object values in xUnit C# with Verify - Pierre Belin](https://goatreview.com/compare-object-values-xunit-csharp-verify/)
Expand Down
1 change: 1 addition & 0 deletions src/Verify.Expecto/Verifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ static InnerVerifier GetVerifier(VerifySettings settings, string sourceFile, str
settings.UseUniqueDirectory();
}

sourceFile = IoHelpers.GetMappedBuildPath(sourceFile) ?? sourceFile;
var fileName = Path.GetFileNameWithoutExtension(sourceFile);

var pathInfo = GetPathInfo(sourceFile, fileName, methodName);
Expand Down
7 changes: 4 additions & 3 deletions src/Verify.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
global.json = global.json
mdsnippets.json = mdsnippets.json
..\readme.source.md = ..\readme.source.md
testenvironments.json = testenvironments.json
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Verify", "Verify\Verify.csproj", "{A017D3FD-7DA5-4DF4-A169-72DE4AF76048}"
Expand Down Expand Up @@ -72,11 +73,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NullComparer", "NullCompare
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoVerifyTests", "AutoVerifyTests\AutoVerifyTests.csproj", "{6E3F9E6E-72D9-4D40-90EE-933B4B1C26DD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SingleTfmTests", "SingleTfmTests\SingleTfmTests.csproj", "{63CFAA91-CEC2-4D21-BEEE-3EB970776E0E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SingleTfmTests", "SingleTfmTests\SingleTfmTests.csproj", "{63CFAA91-CEC2-4D21-BEEE-3EB970776E0E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SplitUniqueDirectoryMode", "SplitUniqueDirectoryMode\SplitUniqueDirectoryMode.csproj", "{02684D47-1A14-4D1A-9B26-387E3F45FEFC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SplitUniqueDirectoryMode", "SplitUniqueDirectoryMode\SplitUniqueDirectoryMode.csproj", "{02684D47-1A14-4D1A-9B26-387E3F45FEFC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SortedJsonTest", "SortedJsonTest\SortedJsonTest.csproj", "{DEC5A717-0914-4798-B68C-F848B6205B64}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SortedJsonTest", "SortedJsonTest\SortedJsonTest.csproj", "{DEC5A717-0914-4798-B68C-F848B6205B64}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NugetUsage", "NugetUsage", "{55ADD9F3-FD0D-4A98-8723-1348FF605944}"
EndProject
Expand Down
26 changes: 18 additions & 8 deletions src/Verify/DerivePaths/AttributeReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,47 @@ public static string GetProjectDirectory() =>
GetProjectDirectory(Assembly.GetCallingAssembly());

public static string GetProjectDirectory(Assembly assembly) =>
GetValue(assembly, "Verify.ProjectDirectory");
GetValue(assembly, "Verify.ProjectDirectory", true);

public static bool TryGetProjectDirectory([NotNullWhen(true)] out string? projectDirectory) =>
TryGetProjectDirectory(Assembly.GetCallingAssembly(), out projectDirectory);

public static bool TryGetProjectDirectory(Assembly assembly, [NotNullWhen(true)] out string? projectDirectory) =>
TryGetValue(assembly, "Verify.ProjectDirectory", out projectDirectory);
TryGetValue(assembly, "Verify.ProjectDirectory", out projectDirectory, true);

public static string GetSolutionDirectory() =>
GetSolutionDirectory(Assembly.GetCallingAssembly());

public static string GetSolutionDirectory(Assembly assembly) =>
GetValue(assembly, "Verify.SolutionDirectory");
GetValue(assembly, "Verify.SolutionDirectory", true);

public static bool TryGetSolutionDirectory([NotNullWhen(true)] out string? solutionDirectory) =>
TryGetSolutionDirectory(Assembly.GetCallingAssembly(), out solutionDirectory);
TryGetSolutionDirectory(Assembly.GetCallingAssembly(), true, out solutionDirectory);

public static bool TryGetSolutionDirectory(Assembly assembly, [NotNullWhen(true)] out string? solutionDirectory) =>
TryGetValue(assembly, "Verify.SolutionDirectory", out solutionDirectory);
TryGetSolutionDirectory(assembly, true, out solutionDirectory);

static bool TryGetValue(Assembly assembly, string key, [NotNullWhen(true)] out string? value)
internal static bool TryGetSolutionDirectory(bool mapPathForVirtualizedRun, [NotNullWhen(true)] out string? solutionDirectory) =>
TryGetSolutionDirectory(Assembly.GetCallingAssembly(), mapPathForVirtualizedRun, out solutionDirectory);

internal static bool TryGetSolutionDirectory(Assembly assembly, bool mapPathForVirtualizedRun, [NotNullWhen(true)] out string? solutionDirectory) =>
TryGetValue(assembly, "Verify.SolutionDirectory", out solutionDirectory, mapPathForVirtualizedRun);

static bool TryGetValue(Assembly assembly, string key, [NotNullWhen(true)] out string? value, bool isSourcePath = false)
{
value = assembly.GetCustomAttributes<AssemblyMetadataAttribute>()
.SingleOrDefault(_ => _.Key == key)
?.Value;
if (isSourcePath)
{
value = value == null ? null : IoHelpers.GetMappedBuildPath(value, assembly);
}
return value is not null;
}

static string GetValue(Assembly assembly, string key)
static string GetValue(Assembly assembly, string key, bool isSourcePath = false)
{
if (TryGetValue(assembly, key, out var value))
if (TryGetValue(assembly, key, out var value, isSourcePath))
{
return value;
}
Expand Down
5 changes: 2 additions & 3 deletions src/Verify/DerivePaths/PathInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,9 @@ internal static PathInfo DeriveDefault(
Type type,
MethodInfo method) =>
new(
directory: Path.GetDirectoryName(sourceFile)!,
directory: IoHelpers.GetDirectoryName(sourceFile)!,
typeName: type.NameWithParent(),
methodName: method.Name);

#endregion

internal static PathInfo DeriveDefault(
Expand All @@ -40,7 +39,7 @@ internal static PathInfo DeriveDefault(
string typeName,
string methodName) =>
new(
directory: Path.GetDirectoryName(sourceFile)!,
directory: IoHelpers.GetDirectoryName(sourceFile)!,
typeName: typeName,
methodName: methodName);
}
19 changes: 19 additions & 0 deletions src/Verify/IoHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,25 @@ public static string GetRelativePath(string directory, string file)

}

static VirtualizedRunHelper? virtualizedRunHelper;
static ConcurrentDictionary<Assembly, VirtualizedRunHelper> virtualizedRunHelpers = new();

internal static void MapPathsForCallingAssembly(Assembly assembly) =>
virtualizedRunHelper = GetForAssembly(assembly);

internal static string? GetMappedBuildPath(string? path, Assembly? assembly = null)
{
VirtualizedRunHelper? helper = assembly != null ? GetForAssembly(assembly) : virtualizedRunHelper;

return helper == null ? path : helper.GetMappedBuildPath(path);
}

private static VirtualizedRunHelper GetForAssembly(Assembly assembly) =>
virtualizedRunHelpers.GetOrAdd(assembly, a => new(a));

public static string? GetDirectoryName(string? path) =>
Path.GetDirectoryName(GetMappedBuildPath(path));

#if NET5_0_OR_GREATER || NETCOREAPP3_0_OR_GREATER

public static async Task<StringBuilder> ReadStringBuilderWithFixedLines(string path)
Expand Down
1 change: 1 addition & 0 deletions src/Verify/TargetAssembly.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public static void Assign(Assembly assembly)
}

Namer.UseAssembly(assembly);
IoHelpers.MapPathsForCallingAssembly(assembly);
ProjectDir = AttributeReader.GetProjectDirectory(assembly);
AttributeReader.TryGetSolutionDirectory(assembly, out var solutionDir);
SolutionDir = solutionDir;
Expand Down
3 changes: 2 additions & 1 deletion src/Verify/Verifier/InnerVerifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ static string GetUniquenessVerified(string sharedUniqueness, Namer namer)

static string ResolveDirectory(string sourceFile, VerifySettings settings, PathInfo pathInfo)
{
var sourceFileDirectory = Path.GetDirectoryName(sourceFile)!;
var sourceFileDirectory = IoHelpers.GetDirectoryName(sourceFile)!;
var pathInfoDirectory = pathInfo.Directory;
if (ContinuousTestingDetector.IsNCrunch)
{
Expand All @@ -234,6 +234,7 @@ static string ResolveDirectory(string sourceFile, VerifySettings settings, PathI
}

var settingsOrInfoDirectory = settings.Directory ?? pathInfoDirectory;
settingsOrInfoDirectory = IoHelpers.GetMappedBuildPath(settingsOrInfoDirectory);

if (settingsOrInfoDirectory is null)
{
Expand Down
115 changes: 115 additions & 0 deletions src/Verify/VirtualizedRunHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
class VirtualizedRunHelper
{
// e.g. WSL or docker run (https://github.com/VerifyTests/Verify#unit-testing-inside-virtualized-environment)
readonly bool appearsToBeLocalVirtualizedRun;
readonly string originalCodeBaseRootAbsolute = string.Empty;
readonly string mappedCodeBaseRootAbsolute = string.Empty;
static readonly char[] Separators = { '\\', '/' };

public VirtualizedRunHelper(Assembly userAssembly)
{
var originalCodeBaseRoot = AttributeReader.TryGetSolutionDirectory(userAssembly, false, out var solutionDir)
? solutionDir
: AttributeReader.GetProjectDirectory(userAssembly);
var appearsToBeBuiltOnDifferentPlatform =
!string.IsNullOrEmpty(originalCodeBaseRoot) &&
!originalCodeBaseRoot.Contains(Path.DirectorySeparatorChar) &&
originalCodeBaseRoot.Contains("\\");

if (!appearsToBeBuiltOnDifferentPlatform)
{
return;
}

string currentDir = Environment.CurrentDirectory;
string currentDirRelativeToAppRoot = currentDir.TrimStart(Separators);

// WSL paths mount to /mnt/<drive>/...
// docker testing mounts to /mnt/approot/...
if (!TryRemoveDirFromStartOfPath(ref currentDirRelativeToAppRoot) ||
!TryRemoveDirFromStartOfPath(ref currentDirRelativeToAppRoot))
{
return;
}

//remove the drive info from the code root
var mappedCodeBaseRootRelative = originalCodeBaseRoot.Replace('\\', '/');
if (!TryRemoveDirFromStartOfPath(ref mappedCodeBaseRootRelative))
{
return;
}

// Move through the code base dir and try to see if it seems to be basePath for currentDir
while (!currentDirRelativeToAppRoot.StartsWith(mappedCodeBaseRootRelative, StringComparison.CurrentCultureIgnoreCase))
{
//no more dirs in code base path - no match found - bail out.
if (!TryRemoveDirFromStartOfPath(ref mappedCodeBaseRootRelative))
{
return;
}
}

mappedCodeBaseRootAbsolute = currentDir[..^currentDirRelativeToAppRoot.Length];

// the start of paths to be mapped
originalCodeBaseRootAbsolute = originalCodeBaseRoot[..^mappedCodeBaseRootRelative.Length];

var testMappedPath = Path.Combine(
mappedCodeBaseRootAbsolute,
originalCodeBaseRoot[originalCodeBaseRootAbsolute.Length..].Replace('\\', '/'));

if (PathExists(testMappedPath))
{
appearsToBeLocalVirtualizedRun = true;
}
}

public string? GetMappedBuildPath(string? path)
{
if (path == null ||
string.IsNullOrEmpty(path) ||
!appearsToBeLocalVirtualizedRun)
{
return path;
}

if (!path.StartsWith(originalCodeBaseRootAbsolute, StringComparison.CurrentCultureIgnoreCase))
{
return path;
}

var mappedPathRelative = path[originalCodeBaseRootAbsolute.Length..].Replace('\\', '/');

var mappedPath = Path.Combine(mappedCodeBaseRootAbsolute, mappedPathRelative);

if (PathExists(mappedPath))
{
return mappedPath;
}

return path;
}

static bool TryRemoveDirFromStartOfPath(ref string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}

path = path.TrimStart(Separators);

int nextSeparatorIdx = path.IndexOfAny(Separators);
if (nextSeparatorIdx <= 0 || nextSeparatorIdx == path.Length - 1)
{
return false;
}

path = path[(nextSeparatorIdx + 1)..];

return !string.IsNullOrWhiteSpace(path);
}

static bool PathExists(string path) =>
File.Exists(path) || Directory.Exists(path);
}
15 changes: 15 additions & 0 deletions src/testenvironments.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": "1",
"environments": [
{
"name": "WSL-Ubuntu",
"type": "wsl",
"wslDistribution": "Ubuntu"
},
{
"name": "docker dotnet 7.0",
"type": "docker",
"dockerImage": "mcr.microsoft.com/dotnet/sdk:7.0"
}
]
}

0 comments on commit 3aefe4c

Please sign in to comment.