diff --git a/docs/diff-tool.custom.md b/docs/diff-tool.custom.md index 073068bc..c49438b5 100644 --- a/docs/diff-tool.custom.md +++ b/docs/diff-tool.custom.md @@ -53,7 +53,7 @@ New tools are added to the top of the order, the last tool added will resolve be ```cs await DiffRunner.LaunchAsync(tempFile, targetFile); ``` -snippet source | anchor +snippet source | anchor Alternatively the instance returned from `AddTool*` can be used to explicitly launch that tool. diff --git a/readme.md b/readme.md index 26b66af5..09bdd229 100644 --- a/readme.md +++ b/readme.md @@ -102,7 +102,7 @@ A tool can be launched using the following: ```cs await DiffRunner.LaunchAsync(tempFile, targetFile); ``` -snippet source | anchor +snippet source | anchor Note that this method will respect the above [difference behavior](/docs/diff-tool.md#detected-difference-behavior) in terms of Auto refresh and MDI behaviors. @@ -117,7 +117,7 @@ A tool can be closed using the following: ```cs DiffRunner.Kill(file1, file2); ``` -snippet source | anchor +snippet source | anchor Note that this method will respect the above [difference behavior](/docs/diff-tool.md#detected-difference-behavior) in terms of MDI behavior. diff --git a/src/DiffEngine.Tests/DiffRunnerTests.cs b/src/DiffEngine.Tests/DiffRunnerTests.cs index 5a0af685..ed9acacf 100644 --- a/src/DiffEngine.Tests/DiffRunnerTests.cs +++ b/src/DiffEngine.Tests/DiffRunnerTests.cs @@ -7,7 +7,29 @@ public class DiffRunnerTests static string SourceDirectory { get; } = Path.GetDirectoryName(GetSourceFile())!; static string GetSourceFile([CallerFilePath] string path = "") => path; - static ResolvedTool tool; + // Launching registers a real pending move of file1 over file2 with whatever owns the queue on + // this machine, usually the developer's tray. Accepting it moves file1, and discarding it + // deletes file1 and then its directory. Run against the source directory that quietly destroys + // the checked in fixtures, so the tests get copies instead. + // One directory for the whole class, because IsRunning matches the exact command string and + // WaitForRunning is also used at test start to wait out the previous test's kill, both of which + // want the same paths across tests. Fresh per run, so a FakeDiffTool left behind by a crashed + // earlier run cannot match this run's command. + static string TempDirectory { get; } = Path.Combine( + Path.GetTempPath(), + "DiffEngine.DiffRunnerTests", + Guid.NewGuid().ToString("N")); + + [After(Class)] + public static void DeleteTempDirectory() + { + if (Directory.Exists(TempDirectory)) + { + Directory.Delete(TempDirectory, true); + } + } + + static ResolvedTool? tool; string file2; string file1; string command; @@ -216,13 +238,26 @@ async Task WaitForRunning(bool expected) public DiffRunnerTests() { - file1 = Path.Combine(SourceDirectory, "DiffRunner.file1.txt"); - file2 = Path.Combine(SourceDirectory, "DiffRunner.file2.txt"); - command = tool.BuildCommand(file1, file2); + file1 = CopyFixture("DiffRunner.file1.txt"); + file2 = CopyFixture("DiffRunner.file2.txt"); + command = Tool.BuildCommand(file1, file2); + } + + // Per test rather than per class: a discard deletes the temp file and its directory, so the + // copies have to be put back for the test that follows. + static string CopyFixture(string name) + { + Directory.CreateDirectory(TempDirectory); + var target = Path.Combine(TempDirectory, name); + File.Copy(Path.Combine(SourceDirectory, name), target, true); + return target; } - static DiffRunnerTests() => - tool = DiffTools.AddTool( + // Resolved on first use rather than in a type initializer. [After(Class)] runs even when every + // test here is skipped for the OS, so initializing the type has to be safe everywhere, and this + // reaches FakeDiffTool, which is only built for Windows and macOS. + static ResolvedTool Tool => + tool ??= DiffTools.AddTool( name: "FakeDiffTool", autoRefresh: true, isMdi: false, diff --git a/src/DiffEngine.Tests/FakeDiffTool.cs b/src/DiffEngine.Tests/FakeDiffTool.cs index 9b059434..0387cfe2 100644 --- a/src/DiffEngine.Tests/FakeDiffTool.cs +++ b/src/DiffEngine.Tests/FakeDiffTool.cs @@ -16,6 +16,6 @@ static FakeDiffTool() return; } - throw new(); + throw new($"FakeDiffTool is only built for Windows and macOS. OS: {RuntimeInformation.OSDescription}"); } } \ No newline at end of file diff --git a/src/DiffEngine.Tests/ModuleInitializer.cs b/src/DiffEngine.Tests/ModuleInitializer.cs index b9205d43..6d65819b 100644 --- a/src/DiffEngine.Tests/ModuleInitializer.cs +++ b/src/DiffEngine.Tests/ModuleInitializer.cs @@ -1,4 +1,10 @@ -public static class ModuleInitializer +// DiffEngineTray is the obsolete public shim, but its IsRunning is still where the tray check lives. +#pragma warning disable CS0618 + +using System.Net; +using System.Net.Sockets; + +public static class ModuleInitializer { [ModuleInitializer] public static void Initialize() @@ -6,5 +12,33 @@ public static void Initialize() FileExtensions.AddTextFileConvention(_ => _.EndsWith(".txtConvention".AsSpan())); Logging.Enable(); DiffRunner.Disabled = false; + DetachFromPendingFileSurfaces(); + } + + /// + /// Launching sends a real pending move to whatever owns the queue on this machine. On a + /// developer box that is the tray, started at login, and an accept or discard from it kills the + /// diff tool process DiffRunnerTests is asserting on. Being inconclusive when a tray is running + /// would mean those tests never run locally, so cut both routes instead: no tray, and a viewer + /// port nothing is listening on. + /// + /// A failed move send is the end of the road in PendingFiles.AddMove, and only a delete + /// launches a viewer, which nothing here adds. So the moves these tests produce go nowhere and + /// no process outside the test can see them. + /// + /// + /// The cost is that the real piper send is not covered from here. That belongs with a tray + /// under test control, which is what DiffEngineTray.Tests/DiffRunnerCanKillTest does. + /// + /// + static void DetachFromPendingFileSurfaces() + { + DiffEngineTray.IsRunning = false; + + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint) listener.LocalEndpoint).Port; + listener.Stop(); + Environment.SetEnvironmentVariable(ViewerClient.PortVariable, port.ToString()); } } diff --git a/src/DiffEngine.Tests/ViewerProtocolTests.cs b/src/DiffEngine.Tests/ViewerProtocolTests.cs index be9513af..38ef40a2 100644 --- a/src/DiffEngine.Tests/ViewerProtocolTests.cs +++ b/src/DiffEngine.Tests/ViewerProtocolTests.cs @@ -504,6 +504,15 @@ public async Task AListingWithNoWindowCommandSaysNothing() await Assert.That(parsed!.Window).IsNull(); } + /// + /// The client's three second default is what a real caller uses to decide the owner has died. + /// The tests below are about what the owner answers rather than how fast, and CI starts six + /// test assemblies at once on a two core runner, where an answer arriving on a scheduled task + /// has twice missed that deadline. keeps the + /// default, because being answered inside it while another exchange is held is the point there. + /// + static readonly TimeSpan underLoad = TimeSpan.FromSeconds(30); + /// /// Bind, serve and exchange for real. The one test here that is not pure string work, because /// the async socket calls take a different path on the frameworks without a token overload. @@ -516,7 +525,7 @@ public async Task AnOwnerAnswersAClient() using var cancel = new CancelSource(); var listening = server.Listen(_ => ViewerResponse.Success($"heard {_.Verb}"), cancel.Token); - var sent = ViewerClient.TrySend(new(ViewerVerb.List), out var response, server.Port); + var sent = ViewerClient.TrySend(new(ViewerVerb.List), out var response, server.Port, underLoad); await Assert.That(sent).IsTrue(); await Assert.That(response!.Ok).IsTrue(); @@ -590,7 +599,7 @@ public async Task AThrowingHandlerAnswersAnError() using var cancel = new CancelSource(); var listening = server.Listen(_ => throw new("the handler is broken"), cancel.Token); - var sent = ViewerClient.TrySend(new(ViewerVerb.List), out var response, server.Port); + var sent = ViewerClient.TrySend(new(ViewerVerb.List), out var response, server.Port, underLoad); await Assert.That(sent).IsTrue(); await Assert.That(response!.Ok).IsFalse();