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
20 changes: 20 additions & 0 deletions src/Verify.Terminal.IntegrationTests/Harness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,26 @@ public IReadOnlyList<string> ReceivedFileNames() =>
.Select(_ => System.IO.Path.GetFileName(_))
.ToList();

// Writes a received map by hand, in the same shape Verify writes them, for a scenario that needs
// a map naming something this harness did not produce.
public void WriteMap(string received, string verified)
{
var target = System.IO.Path.Combine(_directory, "obj", "VerifyReceived");
System.IO.Directory.CreateDirectory(target);
File.WriteAllLines(
System.IO.Path.Combine(target, $"{Guid.NewGuid():N}.txt"),
[received, verified]);
}

// UseUniqueDirectory in split mode puts the marker in the directory name and names the files
// inside after their targets, so none of them are visible to a scan for `*.received.*` files.
public IReadOnlyList<string> SplitReceivedFiles() =>
System.IO.Directory
.GetDirectories(_directory, "*.received")
.SelectMany(_ => System.IO.Directory.GetFiles(_, "*", SearchOption.AllDirectories))
.Order(StringComparer.Ordinal)
.ToList();

// Runs the real SnapshotFinder (real globber, real filesystem) over the temp directory.
public Snapshot FindSingle() => FindFileSnapshots().Single();

Expand Down
146 changes: 146 additions & 0 deletions src/Verify.Terminal.IntegrationTests/SplitModeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
namespace Verify.Terminal.IntegrationTests;

// UseUniqueDirectory in split mode names the directory `{prefix}.received` and the files inside it
// after their targets, so no file name carries a `.received.` marker. A scan for `**/*.received.*`
// finds nothing, and the tool reported no pending snapshots at all for such a project.
//
// Verify records the pair for every received file it leaves on disk, split mode included, so the
// maps are what make these findable.
public class SplitModeTests : IntegrationTestBase
{
[Fact]
public async Task SingleTarget_IsDetectedAndAccepts()
{
using var harness = new Harness(nameof(SingleTarget_IsDetectedAndAccepts));

var settings = SplitSettings(harness, "Single");
await ProduceReceived(settings);

// The marker is on the directory, not the file, which is the whole reason this needed fixing.
var received = harness.SplitReceivedFiles().ShouldHaveSingleItem();
System.IO.Path.GetFileName(received).ShouldNotContain(".received");
harness.ReceivedFileNames().ShouldBeEmpty();

harness.PublishMaps().ShouldBeGreaterThan(0);

var snapshot = harness.FindSingle();
snapshot.Received.FullPath.ShouldBe(Normalize(received));

// The verified file goes in the sibling `.verified` directory under the same name.
snapshot.Verified.FullPath.ShouldEndWith(
$".verified/{System.IO.Path.GetFileName(received)}");

Harness.Accept(snapshot).Succeeded.ShouldBeTrue();
(await Verifies(settings)).ShouldBeTrue();
}

// A test with several targets produces one file per target inside the one directory, so the
// scan has to yield all of them rather than the directory once.
[Fact]
public async Task MultipleTargets_AreAllDetected()
{
using var harness = new Harness(nameof(MultipleTargets_AreAllDetected));

var settings = SplitSettings(harness, "Targets");
var exception = await Record.ExceptionAsync(
async () => await Verifier.Verify(
new List<Target>
{
new("txt", "first", "one"),
new("txt", "second", "two")
},
settings));
exception.ShouldNotBeNull();

harness.SplitReceivedFiles().Count.ShouldBe(2);
harness.PublishMaps().ShouldBeGreaterThan(0);

harness.FindFileSnapshots().Count.ShouldBe(2);
}

// Both conventions in one tree. Split mode is additive: the flat received file is still found
// the way it always was.
[Fact]
public async Task AlongsideAFlatSnapshot_BothAreDetected()
{
using var harness = new Harness(nameof(AlongsideAFlatSnapshot_BothAreDetected));

await ProduceReceived(SplitSettings(harness, "Split"));

var flat = harness.CreateSettings();
flat.UseTypeName("N");
flat.UseMethodName("Flat");
await ProduceReceived(flat);

harness.SplitReceivedFiles().ShouldHaveSingleItem();
harness.ReceivedFileNames().ShouldHaveSingleItem();
harness.PublishMaps().ShouldBeGreaterThan(0);

harness.FindFileSnapshots().Count.ShouldBe(2);
}

// Split mode names every file after its target, so the file name alone says nothing about which
// test is under review. The directory holding it is what does.
[Fact]
public async Task Header_NamesTheDirectory()
{
using var harness = new Harness(nameof(Header_NamesTheDirectory));

await ProduceReceived(SplitSettings(harness, "Header"));
harness.PublishMaps().ShouldBeGreaterThan(0);

var header = harness.FindSingle().Headers[0];

System.IO.Path.GetFileName(header.Path).ShouldBe("target.txt");
header.Path.ShouldStartWith("N.Header");
header.Path.ShouldContain(".received/");
}

// A map records absolute paths and a test can send its snapshots anywhere, so a map found under
// the scanned root can name a file outside it. Reviewing a directory must not reach out of it.
[Fact]
public async Task MappedOutsideTheRoot_IsNotDetected()
{
using var harness = new Harness(nameof(MappedOutsideTheRoot_IsNotDetected));
using var outside = new Harness($"{nameof(MappedOutsideTheRoot_IsNotDetected)}-outside");

// A real received file, in a directory the scan below never looks at.
await ProduceReceived(SplitSettings(outside, "Outside"));
var stray = outside.SplitReceivedFiles().ShouldHaveSingleItem();

// Its map, published under the root that is scanned.
harness.WriteMap(stray, stray.Replace(".received", ".verified", StringComparison.Ordinal));

harness.FindFileSnapshots().ShouldBeEmpty();
}

// Without a map there is nothing naming the pair, and the file name says nothing either. The
// scan yields nothing rather than guessing, which is the same contract the flat fallback has:
// never invent a pairing it cannot support.
[Fact]
public async Task WithoutMap_IsNotDetected()
{
using var harness = new Harness(nameof(WithoutMap_IsNotDetected));

await ProduceReceived(SplitSettings(harness, "NoMap"));

harness.SplitReceivedFiles().ShouldHaveSingleItem();

harness.FindFileSnapshots().ShouldBeEmpty();
}

private static VerifySettings SplitSettings(Harness harness, string method)
{
var settings = harness.CreateSettings();
settings.UseTypeName("N");
settings.UseMethodName(method);
settings.UseUniqueDirectory();
settings.UseSplitModeForUniqueDirectory();
return settings;
}

// Spectre.IO reports paths with forward slashes, so a path read off the file system has to be
// put in the same form before comparing.
private static string Normalize(string path) =>
new FilePath(path).FullPath;
}
22 changes: 20 additions & 2 deletions src/Verify.Terminal/Snapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public IReadOnlyList<SnapshotHeader> Headers
{
get
{
var received = new SnapshotHeader(Received.GetFilename().FullPath);
var received = new SnapshotHeader(Describe(Received));

// The verified name is only worth a line of its own when it is not the one the received
// name reads as, which is exactly when the snapshot was rerouted.
Expand All @@ -21,10 +21,28 @@ public IReadOnlyList<SnapshotHeader> Headers
return [received];
}

return [received, new(Verified.GetFilename().FullPath, "(rerouted)")];
return [received, new(Describe(Verified), "(rerouted)")];
}
}

// A split mode snapshot is named after its target, and the test it belongs to is the directory
// holding it, so the file name alone does not say which snapshot is under review. Every split
// mode snapshot in a run would otherwise read the same, commonly `target.txt`.
private static string Describe(FilePath path)
{
var directory = path.GetDirectory().FullPath;
var separator = directory.LastIndexOf('/');
var directoryName = separator < 0 ? directory : directory[(separator + 1)..];

if (directoryName.EndsWith(".received", StringComparison.Ordinal) ||
directoryName.EndsWith(".verified", StringComparison.Ordinal))
{
return $"{directoryName}/{path.GetFilename().FullPath}";
}

return path.GetFilename().FullPath;
}

public Snapshot(FilePath received)
{
Received = received.NotNull();
Expand Down
74 changes: 74 additions & 0 deletions src/Verify.Terminal/SnapshotFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ public sealed class SnapshotFinder
private static readonly StringComparison _pathComparison =
OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;

private static readonly StringComparer _pathComparer =
StringComparer.FromComparison(_pathComparison);

// The marker Verify puts on a split mode snapshot directory, in place of one on the file name.
private const string _splitMarker = ".received";

private readonly IGlobber _globber;
private readonly IEnvironment _environment;

Expand Down Expand Up @@ -39,15 +45,83 @@ public ISet<Snapshot> Find(DirectoryPath? root = null)
.ToDictionary(_ => _.Key, _ => _.ToList(), StringComparer.Ordinal);

var result = new HashSet<Snapshot>();
var found = new HashSet<string>(_pathComparer);
foreach (var received in Match(root, "**/*.received.*", "received"))
{
var (verifiedPath, isRerouted) = GetVerified(received, maps, verifiedByDirectory);
result.Add(new(received.Path, verifiedPath, isRerouted));
found.Add(received.Path.FullPath);
}

AddMappedOnly(root, maps, found, result);

return result;
}

// `UseUniqueDirectory` in split mode names the directory `{prefix}.received` and the files inside
// it after their targets, so nothing in a file name says received and the glob above cannot see
// any of them. Verify records the pair for every received file it leaves on disk, split mode
// included, so the maps are the only thing that can find these, and the only thing that can say
// which verified file each belongs to.
//
// Any received file the glob did find is skipped here, so this only ever adds what a file name
// could not express. Without a map such a snapshot stays invisible: the name carries nothing to
// pair on, and inventing a pairing is worse than reporting nothing.
private static void AddMappedOnly(
DirectoryPath root,
ReceivedMaps maps,
HashSet<string> found,
HashSet<Snapshot> result)
{
foreach (var pair in maps.Pairs)
{
var received = new FilePath(pair.Received);
if (!found.Add(received.FullPath))
{
continue;
}

// A map records absolute paths, and a test can send its snapshots anywhere, so a map
// found under this root can still name a file outside it. Reviewing a subdirectory must
// not reach out of it.
if (!IsUnder(root, received) ||
IsInlineStaging(received))
{
continue;
}

var verified = new FilePath(pair.Verified);
result.Add(new(received, verified, IsRerouted(received, verified)));
}
}

private static bool IsUnder(DirectoryPath root, FilePath path) =>
path.FullPath.StartsWith($"{root.FullPath}/", _pathComparison);

// Display only: what puts "(rerouted)" beside a snapshot whose verified name is not the one its
// received name reads as. A split mode directory carries the received uniqueness that its
// verified twin drops, so the two names differ whenever the test asked for any.
private static bool IsRerouted(FilePath received, FilePath verified)
{
var literal = SplitLiteralVerified(received);
return literal is null ||
!literal.Equals(verified.FullPath, _pathComparison);
}

// The verified path a split mode received path reads as, ie. the same file in the sibling
// `.verified` directory. Null when the path is not in a split mode directory at all.
private static string? SplitLiteralVerified(FilePath received)
{
var directory = received.GetDirectory().FullPath;
if (!directory.EndsWith(_splitMarker, StringComparison.Ordinal))
{
return null;
}

var verifiedDirectory = $"{directory[..^_splitMarker.Length]}.verified";
return $"{verifiedDirectory}/{received.GetFilename().FullPath}";
}

private IEnumerable<ParsedName> Match(DirectoryPath root, string pattern, string marker) =>
_globber
.Match(pattern, new() { Root = root })
Expand Down
Loading