Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ Accepting through the owner rather than here is what keeps one writer per source

Staged snapshots live in the intermediate (`obj`) directory of the test project, so, as with [recorded pairings](#recorded-pairings), the working directory has to contain `obj`.

A staged patch names the source file its snapshot belongs to, which is a separate question from where it was staged, so two kinds are passed over rather than listed. One whose source file is outside the working directory, since reviewing a directory does not reach out of it, which is the rule the queue already gets. And one whose source file has gone, since the literal is located by searching that file, so nothing can be accepted into it. `obj` keeps what a run staged until something clears it, and a deleted test leaves patches of the second kind behind.

### Snapshots that cannot be accepted

A snapshot that refuses says why, and does not stop the rest of the run being processed. Two cases:
Expand Down
8 changes: 4 additions & 4 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="DiffEngine" Version="20.3.1" />
<PackageVersion Include="DiffEngine" Version="20.5.0" />
<PackageVersion Include="DiffPlex" Version="1.9.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.12" />
<PackageVersion Include="Spectre.Console" Version="0.57.2" />
Expand All @@ -14,9 +14,9 @@
<PackageVersion Include="Spectre.Console.Testing" Version="0.57.2" />
<PackageVersion Include="Spectre.IO.Testing" Version="0.23.0" />
<PackageVersion Include="Spectre.Verify.Extensions" Version="28.16.0" />
<PackageVersion Include="Verify.ExceptionParsing" Version="33.1.0" />
<PackageVersion Include="Verify" Version="33.1.0" />
<PackageVersion Include="Verify.XunitV3" Version="33.1.0" />
<PackageVersion Include="Verify.ExceptionParsing" Version="33.1.3" />
<PackageVersion Include="Verify" Version="33.1.3" />
<PackageVersion Include="Verify.XunitV3" Version="33.1.3" />
<PackageVersion Include="xunit.v3" Version="4.0.1" />
<PackageVersion Include="MinVer" PrivateAssets="All" Version="8.0.0" />
<PackageVersion Include="Roslynator.Analyzers" Version="5.0.0" />
Expand Down
91 changes: 76 additions & 15 deletions src/Verify.Terminal.IntegrationTests/Harness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,21 @@ public Harness(string name)
public DirectoryPath Directory { get; }

// Verify writes maps to this test project's obj directory, but a real run scans a root that
// contains obj. So copy this scenario's maps under the harness directory to match that layout.
// Returns how many were copied, so a test can assert the map path is actually set up rather than
// silently falling back.
// contains obj. So this scenario's maps are moved under the harness directory to match that
// layout. Returns how many were moved, so a test can assert the map path is actually set up
// rather than silently falling back.
//
// Moved rather than copied: the paths in them are this scenario's temp directory, which goes
// when it does, so a copy leaves a map naming nothing behind in obj for every scenario ever run.
public int PublishMaps()
{
var copied = 0;
var moved = 0;
var source = System.IO.Path.Combine(
AttributeReader.GetIntermediateDirectory(typeof(Harness).Assembly),
"VerifyReceived");
if (!System.IO.Directory.Exists(source))
{
return copied;
return moved;
}

var target = System.IO.Path.Combine(_directory, "obj", "VerifyReceived");
Expand All @@ -67,12 +70,12 @@ public int PublishMaps()
if (lines.Length > 0 &&
lines[0].StartsWith(_directory, StringComparison.OrdinalIgnoreCase))
{
File.Copy(file, System.IO.Path.Combine(target, System.IO.Path.GetFileName(file)), true);
copied++;
File.Move(file, System.IO.Path.Combine(target, System.IO.Path.GetFileName(file)), true);
moved++;
}
}

return copied;
return moved;
}

public VerifySettings CreateSettings()
Expand Down Expand Up @@ -121,18 +124,22 @@ public string ReadSource() =>
File.ReadAllText(System.IO.Path.Combine(_directory, "SampleTests.cs"));

// Verify stages inline snapshots in this test project's obj directory, but a real run scans a
// root that contains obj. So copy this scenario's staged files under the harness directory to
// match that layout. Returns how many were copied, so a test can assert that staging really
// root that contains obj. So this scenario's staged files are moved under the harness directory
// to match that layout. Returns how many were moved, so a test can assert that staging really
// happened rather than silently finding nothing.
//
// Moved rather than copied, for the reason PublishMaps is: these patches name a source file in
// this scenario's temp directory, so a copy leaves a trio naming a file that has gone behind in
// obj, once per scenario per run, for whatever scans this project next.
public int PublishInline()
{
var copied = 0;
var moved = 0;
var source = System.IO.Path.Combine(
AttributeReader.GetIntermediateDirectory(typeof(Harness).Assembly),
InlineSnapshotFinder.StagingDirectoryName);
if (!System.IO.Directory.Exists(source))
{
return copied;
return moved;
}

var target = System.IO.Path.Combine(_directory, "obj", InlineSnapshotFinder.StagingDirectoryName);
Expand All @@ -151,16 +158,16 @@ public int PublishInline()
var stem = System.IO.Path.GetFileNameWithoutExtension(patch);
foreach (var file in System.IO.Directory.GetFiles(source, $"{stem}.*"))
{
File.Copy(
File.Move(
file,
System.IO.Path.Combine(target, System.IO.Path.GetFileName(file)),
true);
}

copied++;
moved++;
}

return copied;
return moved;
}

// What the harness directory holds once PublishInline has run, which is what a scan reads and
Expand Down Expand Up @@ -255,6 +262,8 @@ public void Dispose()
System.Environment.SetEnvironmentVariable(DiffEngine.DiffRunner.InlineViewerVariable, _inlineViewer);
System.Environment.SetEnvironmentVariable(MaxInstancesVariable, _maxInstances);

ClearIntermediate();

try
{
System.IO.Directory.Delete(_directory, recursive: true);
Expand All @@ -264,4 +273,56 @@ public void Dispose()
// Best effort cleanup of the temp directory.
}
}

// Verify writes into this test project's obj as it runs: a map for every received file it
// leaves, and a staged trio for an inline snapshot nothing owned. What a scenario published was
// moved, but one that published nothing, or wrote more afterwards, leaves the rest naming a
// temp directory that goes with this harness. Taken here rather than left to accumulate a
// run's worth per run, for whatever scans this project next.
private void ClearIntermediate()
{
var intermediate = AttributeReader.GetIntermediateDirectory(typeof(Harness).Assembly);

// A map holds the received path on its first line, so the file itself says whose it is.
ClearNaming(
System.IO.Path.Combine(intermediate, "VerifyReceived"),
"*",
_ => System.IO.Path.GetFileName(_));

// Only the patch of a staged trio names the source; the two texts beside it are snapshot
// content, and are found by the name they share with it.
ClearNaming(
System.IO.Path.Combine(intermediate, InlineSnapshotFinder.StagingDirectoryName),
"*.inlinepatch",
_ => $"{System.IO.Path.GetFileNameWithoutExtension(_)}.*");
}

private void ClearNaming(string directory, string pattern, Func<string, string> companions)
{
if (!System.IO.Directory.Exists(directory))
{
return;
}

foreach (var file in System.IO.Directory.GetFiles(directory, pattern))
{
try
{
if (!File.ReadAllText(file).Contains(_directory, StringComparison.OrdinalIgnoreCase))
{
continue;
}

foreach (var owned in System.IO.Directory.GetFiles(directory, companions(file)))
{
File.Delete(owned);
}
}
catch (Exception exception)
when (exception is IOException or UnauthorizedAccessException)
{
// Best effort: a file left here is noise in obj rather than a failed scenario.
}
}
}
}
25 changes: 25 additions & 0 deletions src/Verify.Terminal.Tests/InlineSnapshotFinderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,31 @@ public void Should_Flag_A_Conflicted_Queued_Snapshot()
result.Conflict.ShouldBe("net8.0 / net10.0");
}

[Fact]
public void Should_Ignore_A_Staged_Snapshot_Outside_The_Root()
{
// Staging sits under a project's obj, but the patch inside it names wherever the source
// was. Reviewing a directory must not reach out of it, which is the rule the queue gets.
var fileSystem = CreateFileSystem();
InlineTestData.Stage(
fileSystem,
InlineTestData.Patch("new snapshot", source: "/Other/src/SampleTests.cs"));

Find(fileSystem).ShouldBeEmpty();
}

[Fact]
public void Should_Ignore_A_Staged_Snapshot_Whose_Source_Is_Gone()
{
// The literal is located by searching the source file, so a patch naming one that has gone
// can never apply. Offering it is offering something no accept can take.
var fileSystem = CreateFileSystem();
InlineTestData.Stage(fileSystem, InlineTestData.Patch("new snapshot"));
fileSystem.GetFile(InlineTestData.SourceFile).Delete();

Find(fileSystem).ShouldBeEmpty();
}

[Fact]
public void Should_Ignore_Queued_Snapshots_Outside_The_Root()
{
Expand Down
48 changes: 17 additions & 31 deletions src/Verify.Terminal.Tests/InlineSnapshotManagerTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Verify.Terminal.Tests;
namespace Verify.Terminal.Tests;

public sealed class InlineSnapshotManagerTests
{
Expand Down Expand Up @@ -77,10 +77,10 @@ public Task Sample() =>

var fileSystem = CreateFileSystem();
var patch = InlineTestData.Patch("new snapshot", line: 4, source: source.Path);
InlineTestData.Stage(fileSystem, patch);
var staged = InlineTestData.Stage(fileSystem, patch);

var queue = new FakeInlineQueueOwner { AcceptOutcome = InlineAcceptOutcome.Unknown };
var snapshot = new InlineSnapshot(patch, isQueued: true, Staged(fileSystem, patch));
var snapshot = new InlineSnapshot(patch, isQueued: true, [staged]);

Create(queue, fileSystem).Accept(snapshot).Succeeded.ShouldBeTrue();

Expand All @@ -95,10 +95,10 @@ public void Should_Clear_The_Staged_Files_When_The_Owner_Accepts()
{
var fileSystem = CreateFileSystem();
var patch = InlineTestData.Patch("new snapshot");
InlineTestData.Stage(fileSystem, patch);
var staged = InlineTestData.Stage(fileSystem, patch);

var queue = new FakeInlineQueueOwner { AcceptOutcome = InlineAcceptOutcome.Accepted };
var snapshot = new InlineSnapshot(patch, isQueued: true, Staged(fileSystem, patch));
var snapshot = new InlineSnapshot(patch, isQueued: true, [staged]);

Create(queue, fileSystem).Accept(snapshot).Succeeded.ShouldBeTrue();

Expand All @@ -119,9 +119,9 @@ public Task Sample() =>

var fileSystem = CreateFileSystem();
var patch = InlineTestData.Patch("new snapshot", line: 4, source: source.Path);
InlineTestData.Stage(fileSystem, patch);
var staged = InlineTestData.Stage(fileSystem, patch);

var snapshot = new InlineSnapshot(patch, isQueued: false, Staged(fileSystem, patch));
var snapshot = new InlineSnapshot(patch, isQueued: false, [staged]);

Create(fileSystem: fileSystem).Accept(snapshot).Succeeded.ShouldBeTrue();

Expand All @@ -146,9 +146,9 @@ public Task Sample() =>

var fileSystem = CreateFileSystem();
var patch = InlineTestData.Patch("new snapshot", line: 4, source: source.Path);
InlineTestData.Stage(fileSystem, patch);
var staged = InlineTestData.Stage(fileSystem, patch);

var snapshot = new InlineSnapshot(patch, isQueued: false, Staged(fileSystem, patch));
var snapshot = new InlineSnapshot(patch, isQueued: false, [staged]);

var result = Create(fileSystem: fileSystem).Accept(snapshot);

Expand Down Expand Up @@ -211,7 +211,7 @@ public void Should_Treat_A_Discard_The_Owner_Could_Not_Be_Asked_About_As_Done()
// staging is cleared and the reject stands rather than reporting a failure it cannot back.
var fileSystem = CreateFileSystem();
var patch = InlineTestData.Patch("new snapshot");
InlineTestData.Stage(fileSystem, patch);
var staged = InlineTestData.Stage(fileSystem, patch);

var queue = new FakeInlineQueueOwner
{
Expand All @@ -220,7 +220,7 @@ public void Should_Treat_A_Discard_The_Owner_Could_Not_Be_Asked_About_As_Done()
StillPendingResult = null,
};

var snapshot = new InlineSnapshot(patch, isQueued: true, Staged(fileSystem, patch));
var snapshot = new InlineSnapshot(patch, isQueued: true, [staged]);

Create(queue, fileSystem).Reject(snapshot).Succeeded.ShouldBeTrue();

Expand All @@ -232,9 +232,9 @@ public void Should_Clear_The_Staged_Files_On_Reject()
{
var fileSystem = CreateFileSystem();
var patch = InlineTestData.Patch("new snapshot");
InlineTestData.Stage(fileSystem, patch);
var staged = InlineTestData.Stage(fileSystem, patch);

var snapshot = new InlineSnapshot(patch, isQueued: false, Staged(fileSystem, patch));
var snapshot = new InlineSnapshot(patch, isQueued: false, [staged]);

Create(fileSystem: fileSystem).Reject(snapshot).Succeeded.ShouldBeTrue();

Expand All @@ -249,24 +249,10 @@ private static InlineSnapshotManager Create(
FakeFileSystem? fileSystem = null) =>
new(fileSystem ?? CreateFileSystem(), queue ?? new FakeInlineQueueOwner());

// The staged trio as the finder builds it, for a test that starts from the manager instead.
private static IReadOnlyList<StagedInline> Staged(FakeFileSystem fileSystem, InlinePatch patch)
{
var globber = new Globber(fileSystem, new FakeEnvironment(PlatformFamily.Linux));
var finder = new InlineSnapshotFinder(
globber,
new FakeEnvironment(PlatformFamily.Linux),
fileSystem,
new FakeInlineQueueOwner());

// Through the finder rather than by hand, so the manager is handed what it is handed in a
// real run: the patch as it was read back off disk, beside the files it was read from.
return finder
.Find(new("/Working"))
.SelectMany(_ => _.Staged)
.ToList();
}

// The staged trio comes from InlineTestData, which writes it and hands back what a scan would
// report. Not through the finder: the sources below are real files in a temp directory, so an
// accept has something to rewrite, and a scan declines a patch whose source is outside the root
// it was pointed at.
private static IReadOnlyList<string> StagedFiles(FakeFileSystem fileSystem)
{
var globber = new Globber(fileSystem, new FakeEnvironment(PlatformFamily.Linux));
Expand Down
23 changes: 20 additions & 3 deletions src/Verify.Terminal.Tests/Utilities/InlineTestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,32 @@ public static InlinePatch Patch(
};

// Verify names the staged files `{type}.{method}.{hash}.{runtime}`, and writes the two texts
// beside the patch.
public static void Stage(
// beside the patch. Returns the trio as a scan would report it, for a test that starts from
// the manager rather than from a scan.
public static StagedInline Stage(
FakeFileSystem fileSystem,
InlinePatch patch,
string runtime = "DotNet10_0",
bool withTexts = true)
{
var name = $"SampleTests.Sample.a1b2c3d4.{runtime}";

// The source the patch names, which a run stages beside rather than instead of: the
// snapshot it holds is a literal in that file, and an accept rewrites it there.
if (!fileSystem.Exist(new FilePath(patch.SourceFile)))
{
fileSystem
.CreateFile(new FilePath(patch.SourceFile))
.SetTextContent("// the test this snapshot belongs to");
}

fileSystem
.CreateFile($"{StagingDirectory}/{name}.inlinepatch")
.SetTextContent(InlinePatchFile.Build(patch));

if (!withTexts)
{
return;
return new(patch, new($"{StagingDirectory}/{name}.inlinepatch"), null, null, runtime);
}

fileSystem
Expand All @@ -45,5 +55,12 @@ public static void Stage(
fileSystem
.CreateFile($"{StagingDirectory}/{name}.expected.txt")
.SetTextContent(patch.OriginalValue ?? string.Empty);

return new(
patch,
new($"{StagingDirectory}/{name}.inlinepatch"),
new($"{StagingDirectory}/{name}.received.txt"),
new($"{StagingDirectory}/{name}.expected.txt"),
runtime);
}
}
Loading
Loading