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
479 changes: 461 additions & 18 deletions readme.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,18 @@
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)..\OsmfEula.txt" Pack="true" PackagePath="" Visible="false" />
</ItemGroup>
<!--
Native-asset packages (notably SkiaSharp 4.148+) ship large native debug-symbol (.pdb) files under
runtimes/<rid>/native/ — ~85-90 MB each, for every RID. They are never needed at runtime but balloon
packed .NET tools by well over 100 MB (the geoconvert and geoconvert-app tool nupkgs each jumped to
~180 MB after the SkiaSharp 3.x -> 4.148 bump). Strip them from any project that publishes; the
target is inert for library packs, which don't run the publish pipeline. Native runtime libs
themselves are kept, so cross-platform tools still work on every OS.
-->
<Target Name="TrimNativeSymbols" AfterTargets="ComputeFilesToPublish">
<ItemGroup>
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)"
Condition="'%(Extension)' == '.pdb' and $([System.String]::Copy('%(RelativePath)').Contains('native'))" />
</ItemGroup>
</Target>
</Project>
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<PackageVersion Include="Verify" Version="31.20.0" />
<PackageVersion Include="Verify.TUnit" Version="31.20.0" />
<PackageVersion Include="Verify.DiffPlex" Version="3.2.0" />
<PackageVersion Include="Verify.WinForms" Version="4.0.0" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="Parquet.Net" Version="6.0.3" />
<PackageVersion Include="MapBundle" Version="1.0.0" />
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions src/GeoConvert.App.Tests/DiffRenderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace GeoConvert.App.Tests;

public class DiffRenderTests
{
// A small fixed pixel size keeps the snapshots compact and deterministic.
static RenderSettings Settings() =>
new() { MaxDimension = 0, Width = 400, Height = 0 };

[Test]
public Task Overlay() =>
VerifyDiff(DiffMode.Overlay);

[Test]
public Task SideBySide() =>
VerifyDiff(DiffMode.SideBySide);

static Task VerifyDiff(DiffMode mode)
{
var png = MapDiff.Render(SampleMaps.A(), SampleMaps.B(), Settings(), mode, MapDiff.DefaultColorA, MapDiff.DefaultColorB);
return Verify(Images.DecodePng(png)).UseParameters(mode);
}
}
19 changes: 19 additions & 0 deletions src/GeoConvert.App.Tests/DiffSummaryTests.Summarize.verified.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Map A: a.geojson
Features: 3
Layers: 1
Bounds: 0, 0 .. 20, 12
Geometry: Polygon 2, LineString 1
Properties: name, pop

Map B: b.geojson
Features: 2
Layers: 1
Bounds: 1, 1 .. 16, 11
Geometry: Polygon 1, Point 1
Properties: iso, name, pop

Differences:
Features: -1
Geometry: LineString -1, Point +1, Polygon -1
Properties only in A: (none)
Properties only in B: iso
8 changes: 8 additions & 0 deletions src/GeoConvert.App.Tests/DiffSummaryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace GeoConvert.App.Tests;

public class DiffSummaryTests
{
[Test]
public Task Summarize() =>
Verify(MapDiff.Summarize("a.geojson", SampleMaps.A(), "b.geojson", SampleMaps.B()));
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions src/GeoConvert.App.Tests/FormsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace GeoConvert.App.Tests;

[NotInParallel]
public class FormsTests
{
// Each window is snapshotted at 100% and 150% scale so DPI-only layout breaks (fixed-pixel sizes that
// don't scale with the font, as the diff window's input rows once did) are caught.
[Test]
[Arguments(100)]
[Arguments(150)]
public Task MainWindow(int dpiPercent) =>
// The whole main window: menu, the "no map loaded" bar, the (empty) preview and the fixed-width
// options column on the right.
Verify(WinFormsSnapshot.Render(() => new MainForm(SeededSettings(), null), 1000, 680, dpiPercent / 100f))
.UseParameters(dpiPercent);

[Test]
[Arguments(100)]
[Arguments(150)]
public Task DiffWindow(int dpiPercent) =>
// The empty compare window: the two file pickers, the mode/projection/colour toolbar, and the
// (empty) preview / summary panes.
Verify(WinFormsSnapshot.Render(() => new DiffForm(), 1000, 680, dpiPercent / 100f))
.UseParameters(dpiPercent);

[Test]
[Arguments(100)]
[Arguments(150)]
public Task About(int dpiPercent) =>
// The (auto-sizing) About dialog: title, description, the clickable project link and OK button.
Verify(WinFormsSnapshot.Render(() => new AboutForm(), 420, 220, dpiPercent / 100f))
.UseParameters(dpiPercent);

static SettingsManager SeededSettings()
{
// Pre-mark the first-run prompt as shown so the briefly-shown MainForm doesn't pop the (blocking)
// association MessageBox. A throwaway temp path keeps it away from the real user settings.
var manager = new SettingsManager(Path.Combine(Path.GetTempPath(), "GeoConvert.App.Tests", "settings.json"));
manager.Write(new() { AssociationsPrompted = true });
return manager;
}
}
26 changes: 26 additions & 0 deletions src/GeoConvert.App.Tests/GeoConvert.App.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<!-- A '-windows' TFM (plus UseWindowsForms) so WinForms and Verify.WinForms are available; the app
it tests is referenced at its net11.0 build. Test projects don't pack, so no tool-packaging hack
is needed here (unlike the app itself). -->
<TargetFramework>net11.0-windows</TargetFramework>
<OutputType>Exe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<!-- WinForms snapshot tests are Windows-only by nature, like the app they test (which also NoWarns
this). Suppresses the platform-compatibility analyzer for the WinForms calls. -->
<NoWarn>$(NoWarn);CA1416</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ProjectFiles" />
<PackageReference Include="TUnit" />
<PackageReference Include="Verify.TUnit" />
<PackageReference Include="Verify.WinForms" />
<PackageReference Include="ProjectDefaults" PrivateAssets="all" />
<ProjectReference Include="..\GeoConvert.App\GeoConvert.App.csproj" />
</ItemGroup>

</Project>
5 changes: 5 additions & 0 deletions src/GeoConvert.App.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
global using System.Drawing.Imaging;
global using GeoConvert;
global using GeoConvert.App;
global using TUnit.Core;
global using static VerifyTUnit.Verifier;
22 changes: 22 additions & 0 deletions src/GeoConvert.App.Tests/ModuleInitializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
public static class ModuleInitializer
{
[ModuleInitializer]
public static void Init()
{
// Verify.WinForms snapshots WinForms UI as images. Its Control converter renders via a live
// message loop, which deadlocks headless, so the tests render each control to a Bitmap on an STA
// thread (see WinFormsSnapshot) and snapshot that. This converter writes a Bitmap out as a PNG so
// Verify treats it as an image; SSIM comparison then tolerates the small machine-dependent pixel
// differences inherent in rendering WinForms to a bitmap.
VerifyWinForms.Initialize();
VerifierSettings.UseSsimForPng();
VerifierSettings.RegisterFileConverter<Bitmap>(
(bitmap, _) =>
{
var stream = new MemoryStream();
bitmap.Save(stream, ImageFormat.Png);
stream.Position = 0;
return new(null, "png", stream, null);
});
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions src/GeoConvert.App.Tests/OptionsPanelTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace GeoConvert.App.Tests;

// WinForms rendering wants a single UI thread, so keep these off the parallel runner.
[NotInParallel]
public class OptionsPanelTests
{
// Snapshotted at 100% and 150% scale so DPI-only layout breaks (fixed-pixel sizes that don't scale
// with the font) are caught, not just the 96-DPI layout.
[Test]
[Arguments(100)]
[Arguments(150)]
public Task Kml(int dpiPercent) =>
// A plain vector format: the always-on Projection radios plus the Output, Simplify and
// format-note sections (no image options).
Verify(WinFormsSnapshot.Render(() => Panel(GeoFormat.Kml), 480, 560, dpiPercent / 100f))
.UseParameters(dpiPercent);

[Test]
[Arguments(100)]
[Arguments(150)]
public Task Png(int dpiPercent) =>
// The image formats reveal the full render-options section (projection, strokes, labels, colours)
// plus the PNG sub-section, so this covers most of the options UI and its show/hide logic.
Verify(WinFormsSnapshot.Render(() => Panel(GeoFormat.Png), 480, 1320, dpiPercent / 100f))
.UseParameters(dpiPercent);

static OptionsPanel Panel(GeoFormat format)
{
var panel = new OptionsPanel(new(), new(), new(), new());
panel.SelectFormat(format);
return panel;
}
}
41 changes: 41 additions & 0 deletions src/GeoConvert.App.Tests/SampleMaps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace GeoConvert.App.Tests;

/// <summary>Small, deterministic in-memory maps used across the snapshot tests.</summary>
static class SampleMaps
{
// Two overlapping squares, a triangle and a line — enough geometry to exercise the renderer, the
// diff overlay and the structural summary without depending on any external data file.
public static FeatureCollection A() =>
[
new Feature(
new Polygon([[new(0, 0), new(10, 0), new(10, 10), new(0, 10), new(0, 0)]]),
Props(("name", "Square"), ("pop", 100L))),
new Feature(
new Polygon([[new(12, 0), new(20, 0), new(16, 8), new(12, 0)]]),
Props(("name", "Tri"))),
new Feature(
new LineString([new(0, 12), new(20, 12)]),
Props(("name", "Road"))),
];

public static FeatureCollection B() =>
[
new Feature(
new Polygon([[new(1, 1), new(11, 1), new(11, 11), new(1, 11), new(1, 1)]]),
Props(("name", "Square"), ("pop", 100L), ("iso", "SQ"))),
new Feature(
new Point(new(16, 4)),
Props(("name", "Dot"), ("iso", "DT"))),
];

static IDictionary<string, object?> Props(params (string Key, object? Value)[] pairs)
{
var properties = new Dictionary<string, object?>();
foreach (var (key, value) in pairs)
{
properties[key] = value;
}

return properties;
}
}
106 changes: 106 additions & 0 deletions src/GeoConvert.App.Tests/WinFormsSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
namespace GeoConvert.App.Tests;

/// <summary>
/// Renders a WinForms control to a <see cref="Bitmap"/> for Verify to snapshot. WinForms requires an STA
/// thread, so the control is created, laid out and drawn on a dedicated one; the resulting bitmap is
/// thread-agnostic and handed back. Forms are briefly shown off-screen so their <c>OnLoad</c> (e.g. the
/// diff window's splitter layout) runs before the draw; plain controls just get a handle and a layout
/// pass. Nothing is verified here — the caller passes the bitmap to Verify.
/// <para>
/// A <c>scale</c> &gt; 1 simulates a higher display DPI: WinForms applies DPI scaling through
/// <see cref="Control.Scale(SizeF)"/>, so calling it directly reproduces the same scaling behaviour
/// (including the class of bug where fixed-pixel sizes don't scale while font-sized controls do) without
/// needing an actual high-DPI monitor. scale 1.25 == 120 DPI / 125%, scale 1.5 == 144 DPI / 150%.
/// </para>
/// </summary>
static class WinFormsSnapshot
{
static bool stylesEnabled;

public static Bitmap Render(Func<Control> factory, int width, int height, float scale = 1f)
{
Bitmap? result = null;
Exception? failure = null;

var thread = new Thread(() =>
{
try
{
EnsureStyles();
using var control = factory();
if (control is Form form)
{
// Off-screen + off-taskbar so the brief show is invisible; Show() raises OnLoad/OnShown
// so docked/split layouts settle before the draw.
form.StartPosition = FormStartPosition.Manual;
form.ShowInTaskbar = false;
form.Location = new(-5000, -5000);
// An AutoSize form sizes itself to its content — forcing a size would clip it.
if (!form.AutoSize)
{
form.Size = new(width, height);
}

Rescale(form, scale);
form.Show();
Application.DoEvents();
result = Draw(form);
form.Close();
}
else
{
control.Size = new(width, height);
_ = control.Handle;
Rescale(control, scale);
control.PerformLayout();
Application.DoEvents();
result = Draw(control);
}
}
catch (Exception exception)
{
failure = exception;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.IsBackground = true;
thread.Start();
thread.Join();

if (failure != null)
{
throw failure;
}

return result!;
}

static void Rescale(Control control, float scale)
{
if (scale != 1f)
{
control.Scale(new SizeF(scale, scale));
}
}

static Bitmap Draw(Control control)
{
var bounds = control.ClientRectangle;
var bitmap = new Bitmap(Math.Max(1, bounds.Width), Math.Max(1, bounds.Height));
control.DrawToBitmap(bitmap, bounds);
return bitmap;
}

static void EnsureStyles()
{
if (stylesEnabled)
{
return;
}

// Match the real app's themed rendering (ApplicationConfiguration.Initialize does this).
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
stylesEnabled = true;
}
}
Loading
Loading