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
15 changes: 14 additions & 1 deletion claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,20 @@ the comment there about not caching "nothing staged" asks for.
the core's, and the question a locked file asks is the head's form (`LockedFilesHandler`).
`IssueLauncher.Declined` declines in the core, which has no toolkit to ask with, and the head
hands it `IssueQuestion`'s MessageBox first thing in Main. The core is signed and named in
DiffEngine's `InternalsVisibleTo`, and names the head and the tests in its own
DiffEngine's `InternalsVisibleTo`, and names the head and both test projects in its own
- The core's tests are `DiffEngineTray.Core.Tests` (net10.0), which runs in the unix CI job as
well as on Windows. `DiffEngineTray.Tests` keeps what needs the head: the menu, the forms, the
hot keys, `LockedFilesHandler`, `DiffToolLauncher` and `TrayViewerSyncTest`. The helpers both
use (`RecordingTracker`, `FakeViewer`, `FileLockUtils` and the rest) live with the core's tests
and are linked into the head's. What stays Windows only is marked `[RunOn(OS.Windows)]` with
the reason in its summary, and is of three kinds: `ProcessEx` and `FileLockKiller`, a lock held
by a Windows PowerShell process, and Windows' file semantics, under which a file open with no
sharing or marked read-only cannot be moved over or deleted. On Unix a rename or an unlink
takes no notice of either. Test data is written as the platform roots a path
(`OwnedInlineHostTest.Repo`): a Windows path read on Linux is one file name of the whole
string, so every entry had the same name, and a viewer at `C:\tools\DiffEngineViewer.exe` was
not the viewer. `DebugReportTests` writes `{Directory}\` for the separator on every platform,
so one snapshot holds for all of them
- `PiperServer` - TCP server (localhost, 3492) receiving move/delete payloads from DiffEngine.
Deliberately a second listener beside the viewer protocol, not debt: its format is frozen
(every stable DiffEngine embeds PiperClient, pinned in test projects while the tray updates
Expand Down
8 changes: 4 additions & 4 deletions docs/tray.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ The case it exists for is a test suite that needs the launch to happen but does
"ProcessId":1000
}
```
<sup><a href='/src/DiffEngineTray.Tests/PiperTest.MoveJson.verified.txt#L1-L9' title='Snippet source file'>snippet source</a> | <a href='#snippet-PiperTest.MoveJson.verified.txt' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/DiffEngineTray.Core.Tests/PiperTest.MoveJson.verified.txt#L1-L9' title='Snippet source file'>snippet source</a> | <a href='#snippet-PiperTest.MoveJson.verified.txt' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand All @@ -216,7 +216,7 @@ The case it exists for is a test suite that needs the launch to happen but does
"File":"theFilePath"
}
```
<sup><a href='/src/DiffEngineTray.Tests/PiperTest.DeleteJson.verified.txt#L1-L4' title='Snippet source file'>snippet source</a> | <a href='#snippet-PiperTest.DeleteJson.verified.txt' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/DiffEngineTray.Core.Tests/PiperTest.DeleteJson.verified.txt#L1-L4' title='Snippet source file'>snippet source</a> | <a href='#snippet-PiperTest.DeleteJson.verified.txt' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand All @@ -238,7 +238,7 @@ A move or a delete of a file that was [derived from a document](/docs/viewer.md#
"Source":"theSourceTempFilePath"
}
```
<sup><a href='/src/DiffEngineTray.Tests/PiperTest.MoveWithSourceJson.verified.txt#L1-L10' title='Snippet source file'>snippet source</a> | <a href='#snippet-PiperTest.MoveWithSourceJson.verified.txt' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/DiffEngineTray.Core.Tests/PiperTest.MoveWithSourceJson.verified.txt#L1-L10' title='Snippet source file'>snippet source</a> | <a href='#snippet-PiperTest.MoveWithSourceJson.verified.txt' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

<!-- snippet: PiperTest.DeleteWithSourceJson.verified.txt -->
Expand All @@ -250,7 +250,7 @@ A move or a delete of a file that was [derived from a document](/docs/viewer.md#
"Source":"theSourceTempFilePath"
}
```
<sup><a href='/src/DiffEngineTray.Tests/PiperTest.DeleteWithSourceJson.verified.txt#L1-L5' title='Snippet source file'>snippet source</a> | <a href='#snippet-PiperTest.DeleteWithSourceJson.verified.txt' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/DiffEngineTray.Core.Tests/PiperTest.DeleteWithSourceJson.verified.txt#L1-L5' title='Snippet source file'>snippet source</a> | <a href='#snippet-PiperTest.DeleteWithSourceJson.verified.txt' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

The tray tracks such a file as it tracks any other, so it is listed in the menu and taken by an accept-all. The property is only passed on to the viewer, which is what folds the file beneath its document. A tray older than the property ignores it.
Expand Down
4 changes: 4 additions & 0 deletions src/DiffEngine.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@
<Project Path="DiffEngineTray.Core/DiffEngineTray.Core.csproj">
<BuildType Solution="Release-NotWindows|*" Project="Release" />
</Project>
<!-- Its tests, which run everywhere too. -->
<Project Path="DiffEngineTray.Core.Tests/DiffEngineTray.Core.Tests.csproj">
<BuildType Solution="Release-NotWindows|*" Project="Release" />
</Project>
<Project Path="FakeDiffTool/FakeDiffTool.csproj">
<BuildType Solution="Release-NotWindows|*" Project="Release" />
<!--
Expand Down
1 change: 1 addition & 0 deletions src/DiffEngine/InternalsVisibleTo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
[assembly: InternalsVisibleTo("DiffEngineTray, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
[assembly: InternalsVisibleTo("DiffEngineTray.Core, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
[assembly: InternalsVisibleTo("DiffEngineTray.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
[assembly: InternalsVisibleTo("DiffEngineTray.Core.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
[assembly: InternalsVisibleTo("DiffEngineViewer.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
[assembly: InternalsVisibleTo("DiffEngine.Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Moves (2)
Target: {Directory}\Sample.Test.verified.pdf (missing)
Extension: pdf
Group: <null>
Exe: C:\tools\DiffEngineViewer.exe
Exe: {Directory}\DiffEngineViewer.exe
Arguments: --diff
CanKill: False
KillLockingProcess: False
Expand All @@ -30,7 +30,7 @@ Moves (2)
Extension: png
Group: <null>
DerivedFrom: {Directory}\Sample.Test.received.pdf (exists)
Exe: C:\tools\DiffEngineViewer.exe
Exe: {Directory}\DiffEngineViewer.exe
Arguments: --diff
CanKill: False
KillLockingProcess: False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ none

Snapshots (1)
-------------
[1] SampleTests.cs:42
[1] sampletests.cs:42
Key: {Directory}\sampletests.cs|42
Source: {Directory}\sampletests.cs (exists)
Group: <null>
Status: <null>
SourceFile: {Directory}\SampleTests.cs
SourceFile: {Directory}\sampletests.cs
LineHint: 42
Mode: Set
OriginalExpression: "old"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public DebugReportTests()
File.WriteAllText(received, "");

settings = new();
settings.AddScrubber(_ => _.Replace(directory, "{Directory}"));
settings.AddScrubber(_ => ScrubDirectory(_, directory));
// Read when the scrubber runs, not when it is added: every test points at an ephemeral
// port so that a viewer running on this machine is not talked to, and FakeViewer moves it
// again for the tests that want one
Expand Down Expand Up @@ -81,7 +81,9 @@ public async Task Derived()
await File.WriteAllTextAsync(document, "");
await File.WriteAllTextAsync(page, "");
await File.WriteAllTextAsync(stale, "");
const string viewer = @"C:\tools\DiffEngineViewer.exe";
// A path as this platform writes one, so its file name is the viewer's and the moves are
// open: on Linux all of C:\tools\DiffEngineViewer.exe is one file name
var viewer = Path.Combine(directory, "DiffEngineViewer.exe");
tracker.AddMove(
document,
Path.Combine(directory, "Sample.Test.verified.pdf"),
Expand Down Expand Up @@ -114,10 +116,13 @@ public async Task Owned()
host.Start();
// Ephemeral, so a viewer or tray running on this machine is not in the way
settings.AddScrubber(_ => _.Replace(host.Port.ToString(), "{Port}"));
// The key a patch is filed under is case folded, so the directory shows up twice
settings.AddScrubber(_ => _.Replace(directory.ToLowerInvariant(), "{Directory}"));
// The key a patch is filed under is case folded on Windows and macOS, so the directory
// shows up twice there
settings.AddScrubber(_ => ScrubDirectory(_, directory.ToLowerInvariant()));

var source = Path.Combine(directory, "SampleTests.cs");
// In lower case, so the key reads the same where it is case folded and on Linux, where it
// is not
var source = Path.Combine(directory, "sampletests.cs");
await File.WriteAllTextAsync(source, "");
var patch = new InlinePatch(source, 42, "\"old\"", "line one\nline two")
{
Expand All @@ -134,6 +139,17 @@ public async Task Owned()
await Verify(DebugReport.Build(tracker, now), settings);
}

/// <summary>
/// The directory, and the separator after it as Windows writes one, so the report reads the
/// same on every platform and one snapshot holds for all of them. One scrubber for both, in
/// that order: as two, a later one ran first and took the directory from in front of the
/// separator.
/// </summary>
static void ScrubDirectory(StringBuilder builder, string path) =>
builder
.Replace(path + Path.DirectorySeparatorChar, @"{Directory}\")
.Replace(path, "{Directory}");

/// <summary>
/// Owning the queue means a window can be asked for, and this test wants none. Reporting one
/// as already up means none is ever started.
Expand Down
25 changes: 25 additions & 0 deletions src/DiffEngineTray.Core.Tests/DiffEngineTray.Core.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--
The tests of the tray's core, on every platform, where DiffEngineTray.Tests is net10.0-windows
and has the head's WinForms. The helpers both use live here, and that project links them.
-->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<NoWarn>$(NoWarn);CA1416;NU1902</NoWarn>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\DiffEngine.Tests\MachineSettings.cs" Link="MachineSettings.cs" />
<Using Include="DiffEngine" />
<PackageReference Include="Argon" />
<PackageReference Include="Verify.TUnit" />
<PackageReference Include="EmptyFiles" />
<PackageReference Include="SimpleInfoName" />
<PackageReference Include="Verify" />
<PackageReference Include="ProjectDefaults" PrivateAssets="all" />
<ProjectReference Include="..\DiffEngineTray.Core\DiffEngineTray.Core.csproj" />
<ProjectReference Include="..\DiffEngine\DiffEngine.csproj" />
<PackageReference Include="TUnit" />
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/// <summary>
/// Windows only: FileLockKiller asks the restart manager who holds a file, and the lock is held by
/// a Windows PowerShell process. A Unix head that kills lockers will need a way of its own to ask.
/// </summary>
[RunOn(TUnit.Core.Enums.OS.Windows)]
public class FileLockKillerTest
{
[Test]
Expand Down
13 changes: 13 additions & 0 deletions src/DiffEngineTray.Core.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
global using System.Diagnostics;
global using System.Net;
global using System.Net.Sockets;
global using System.Security.AccessControl;
global using System.Security.Principal;
global using EmptyFiles;

[assembly: ParallelLimiter<SingleThreadedLimit>]

public class SingleThreadedLimit : TUnit.Core.Interfaces.IParallelLimit
{
public int Limit => 1;
}
50 changes: 50 additions & 0 deletions src/DiffEngineTray.Core.Tests/ModuleInitializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
public static class ModuleInitializer
{
[ModuleInitializer]
public static void Initialize()
{
DeclineToOpenAnIssue();
MachineSettings.Ignore();
KeepEnvironmentWritesInProcess();
PointAtAClosedPort();
}

/// <summary>
/// The tray follows an error it did not expect by offering to open an issue on GitHub, and a
/// yes opens a browser. A test that reaches the offer is declined here, with nobody asked, and
/// what it reached is kept for the tests that are about an error being reported.
/// </summary>
static void DeclineToOpenAnIssue() =>
IssueLauncher.Declined = text =>
{
IssuesAsked.Enqueue(text);
return true;
};

/// <summary>
/// Every question <see cref="DeclineToOpenAnIssue"/> has declined, oldest first.
/// </summary>
internal static ConcurrentQueue<string> IssuesAsked { get; } = new();

/// <summary>
/// Tests must not write the user environment of the machine running them. The test projects
/// run as parallel processes over the one registry key, so a capture in one and a restore in
/// the other race, and the value that loses is gone.
/// </summary>
static void KeepEnvironmentWritesInProcess() =>
EnvironmentHelper.Set = EnvironmentHelper.SetProcessOnly;

/// <summary>
/// The tray asks the viewer for pending snapshots. Without this a DiffEngineViewer running on
/// the developer's machine would answer, and tests that expect nothing pending would see its
/// queue. FakeViewer overrides this for the tests that do want a viewer.
/// </summary>
static void PointAtAClosedPort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint) listener.LocalEndpoint).Port;
listener.Stop();
Environment.SetEnvironmentVariable(ViewerClient.PortVariable, port.ToString());
}
}
Loading
Loading