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();