diff --git a/src/DiffEngine.Tests/DiffRunnerTests.cs b/src/DiffEngine.Tests/DiffRunnerTests.cs index ed9acacf..b278bc8f 100644 --- a/src/DiffEngine.Tests/DiffRunnerTests.cs +++ b/src/DiffEngine.Tests/DiffRunnerTests.cs @@ -175,10 +175,14 @@ public async Task LaunchAndKill() await WaitForRunning(true); await Assert.That(IsRunning()).IsTrue(); await Assert.That(ProcessCleanup.IsRunning(command)).IsTrue(); + + using var launched = OpenLaunched(); DiffRunner.Kill(file1, file2); + await WaitForRunning(false); await Assert.That(IsRunning()).IsFalse(); await Assert.That(ProcessCleanup.IsRunning(command)).IsFalse(); + await AssertTerminated(launched); } [Test] @@ -192,27 +196,61 @@ public async Task LaunchAndKillAsync() await WaitForRunning(true); await Assert.That(IsRunning()).IsTrue(); await Assert.That(ProcessCleanup.IsRunning(command)).IsTrue(); + + using var launched = OpenLaunched(); DiffRunner.Kill(file1, file2); + await WaitForRunning(false); await Assert.That(IsRunning()).IsFalse(); await Assert.That(ProcessCleanup.IsRunning(command)).IsFalse(); + await AssertTerminated(launched); + } + + /// + /// Opens a handle on the process the launch just started, before anything can kill it. + /// + /// Process.GetProcessById holds no OS handle of its own, and a handle opened after the process + /// has gone cannot report how it went. Touching Handle here is what makes the exit code + /// readable afterwards. + /// + /// + Process OpenLaunched() + { + var match = ProcessCleanup.FindAll().Single(_ => _.Command == Expected); + var process = Process.GetProcessById(match.Process); + _ = process.Handle; + return process; + } + + /// + /// That the process was killed, rather than that it is merely gone. + /// + /// The distinction is the whole point of this assertion. FakeDiffTool sleeps for five seconds + /// and then exits on its own, and WaitForRunning polls for ten, so "no longer running" is + /// true whether the kill worked or did nothing at all - these tests passed with + /// ProcessCleanup.Kill short circuited to a bare return. The exit code tells them apart: + /// WindowsProcess.TryTerminateProcess passes -1 to TerminateProcess, and a FakeDiffTool that + /// ran out its sleep returns 0. + /// + /// + static async Task AssertTerminated(Process process) + { + await Assert.That(process.WaitForExit(5000)).IsTrue(); + await Assert.That(process.ExitCode).IsNotEqualTo(0); } // Match this test's exact command, not any FakeDiffTool: DiffEngineTray.Tests // runs concurrently in the same CI job and launches its own FakeDiffTool // instances, which a machine-wide substring scan would see. - bool IsRunning() - { - var expected = command; - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - expected = expected.Replace("\"", ""); - } + string Expected => + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? command + : command.Replace("\"", ""); - return ProcessCleanup + bool IsRunning() => + ProcessCleanup .FindAll() - .Any(_ => _.Command == expected); - } + .Any(_ => _.Command == Expected); // Process spawn and kill are asynchronous, so poll instead of guessing with a // fixed sleep. Also used at test start: the previous test's kill may still be