Skip to content

Commit 793ba1e

Browse files
pmeemsCopilot
andauthored
✅ test(activex-host): expand automated test coverage (#59)
- Add automated checks for layer loading, navigation, errors, resizing, clearing, and rendering backends. - Document build and manual testing in the C# host README. - Record late-bound COM error handling in testing memory. - Update the root README and testing guide. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ed4edb5 commit 793ba1e

5 files changed

Lines changed: 321 additions & 29 deletions

File tree

‎.ai/memory/testing/testing.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ ActiveX host checks require Windows and an interactive desktop; they are not
1919
currently part of CI. The rendering performance baseline is opt-in and should
2020
be run in Release when collecting meaningful timings.
2121

22+
The C# host calls ActiveX methods late-bound. An invalid layer ID surfaces as
23+
`.NET ArgumentException` with the COM error text and `E_INVALIDARG` HRESULT.
24+
The host assertion accepts this mapping as well as `COMException`; see
25+
`samples/CSharp/ActiveXTestHost/MainForm.cs`.
26+
2227
## References
2328

2429
- [Testing guide](../../../docs/TESTING.md)

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ For additional configurations, dependency features, and troubleshooting, see
8888
| `tests/MapWinGIS.Core.Tests/` | Core C++ unit tests (GoogleTest) |
8989
| `tests/MapWinGIS.ActiveX.Tests/` | ActiveX and COM-hosting C++ unit tests (GoogleTest) |
9090
| `samples/Cpp/` | Buildable native C++ examples |
91+
| `samples/CSharp/ActiveXTestHost/` | WinForms host for manual and automated ActiveX testing |
9192
| `build/` | Shared MSBuild props, vcpkg triplets and scripts |
9293
| `docs/` | Build, architecture, coding, GDAL integration, porting and testing documentation |
9394

‎docs/TESTING.md‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,18 @@ pwsh -File samples\CSharp\ActiveXTestHost\Run-ActiveXTestHost.ps1 -Configuration
6060
The launcher makes no COM registration changes. Automated mode creates a
6161
visible window and runs the UI message loop, checks the `CoreInitialized`
6262
automation property, loads a generated two-polygon GeoJSON dataset through the
63-
control, resizes the host, samples rendered polygon and background pixels for
64-
both the GDI preference and the default (OpenGL-first) path, recreates the
65-
control, and reloads the layer. It calls the original `LoadVectorDataset`
63+
control, resizes the host, and samples rendered polygon and background pixels
64+
for GDI and a requested OpenGL path. The OpenGL request can use GDI when
65+
OpenGL is unavailable or the scene is unsupported. The host recreates the
66+
control and reloads the layer. It calls the original `LoadVectorDataset`
6667
method and reads `LayerLoaded` and the absolute `FeatureCount` on each fresh
67-
control. It also loads two layers through `LoadVectorDatasetEx`, checks
68-
their IDs and feature-count deltas, zooms to each, pans, and clears them.
69-
It makes sure that navigation preserves the counts and that clearing removes
70-
the layers from the control and the list. It writes individual outcomes to
68+
control. It also checks failed dataset loads, loads two layers through
69+
`LoadVectorDatasetEx`, and checks their IDs, counts, and feature-count deltas.
70+
It exercises map fitting, per-layer fitting, zoom in and out, pan, cursor
71+
anchored zoom, navigation after resizing, invalid layer IDs, and navigation
72+
with no layers. It clears and reloads layers and makes sure that failed loads
73+
and navigation do not change layer or feature counts. The host writes
74+
individual outcomes to
7175
`bin\Debug\x64\ActiveXTestHost-results.json`. Pixels are sampled from the host
7276
window's own composed content (`PrintWindow` with `PW_RENDERFULLCONTENT`), not
7377
from the screen, so another window on top or a lost foreground race does not
@@ -81,6 +85,8 @@ are not currently part of CI.
8185
The host executable accepts `--automated`, `--results <path>`, and
8286
`--timeout-seconds <5-300>`. The PowerShell launcher supplies these options
8387
and starts the host without changing COM registration.
88+
For build and manual-use instructions, see the
89+
[ActiveX Test Host README](../samples/CSharp/ActiveXTestHost/README.md).
8490

8591
## Rendering performance baseline
8692

‎samples/CSharp/ActiveXTestHost/MainForm.cs‎

Lines changed: 246 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ internal sealed class MainForm : Form
1212
{
1313
private const string ProgId = "MapWinGIS6.Control.6";
1414
private const int ColorWindow = 5;
15+
private const int HResultFail = unchecked((int)0x80004005);
16+
private const int HResultInvalidArgument = unchecked((int)0x80070057);
17+
private const int HResultDispatchException = unchecked((int)0x80020009);
1518
private const double ZoomStep = 1.5;
1619
private static readonly Color MapFillColor = Color.FromArgb(61, 122, 169);
1720

@@ -240,8 +243,9 @@ private async Task RunAutomatedAsync()
240243
RefreshStatus();
241244
});
242245
RecordCheck("Load vector layer", LoadTestLayer);
246+
RecordCheck("Invalid dataset handling", VerifyInvalidDatasetHandling);
243247
await RecordCheckAsync("GDAL COM event sinks", VerifyGdalEventSinksAsync);
244-
RecordCheck("CPU backend preference and status", VerifyCpuBackendSelection);
248+
RecordCheck("Rendering backends", VerifyRenderingBackendsWhereSupported);
245249

246250
RecordCheck("Resize", VerifyResize);
247251
await RecordCheckAsync("Repaint", VerifyRepaintAsync);
@@ -254,7 +258,16 @@ private async Task RunAutomatedAsync()
254258
});
255259
RecordCheck("Reload vector layer", LoadTestLayer);
256260
await RecordCheckAsync("Recreated repaint", VerifyRepaintAsync);
257-
RecordCheck("Layer loading, navigation and clear", VerifyLayerOperations);
261+
RecordCheck("Multiple datasets and stable layer IDs", VerifyMultipleDatasetLoading);
262+
RecordCheck("Invalid layer ID handling", VerifyInvalidLayerIdHandling);
263+
RecordCheck("Zoom to individual layers", VerifyZoomToLayers);
264+
RecordCheck("Zoom in and zoom out", VerifyZoomInAndOut);
265+
RecordCheck("Pan", VerifyPan);
266+
RecordCheck("ZoomAt", VerifyZoomAt);
267+
RecordCheck("Zoom to map", VerifyFitToMap);
268+
RecordCheck("Navigation after resizing", VerifyNavigationAfterResize);
269+
RecordCheck("Clear and reload layers", VerifyClearAndReload);
270+
RecordCheck("Navigation on an empty map", VerifyEmptyMapNavigation);
258271
RecordCheck("Interactive load rendering status", VerifyInteractiveLoadStatus);
259272
RecordCheck("Final dispose", VerifyDispose);
260273

@@ -609,7 +622,32 @@ private void ClearLoadedLayers()
609622
RefreshStatus();
610623
}
611624

612-
private void VerifyLayerOperations()
625+
private void VerifyInvalidDatasetHandling()
626+
{
627+
dynamic automation = GetAutomation();
628+
int previousLayerCount = (int)automation.LayerCount;
629+
long previousFeatureCount = (long)automation.FeatureCount;
630+
long previousLayerId = (long)automation.GetLayerId(0);
631+
string missingPath = Path.Combine(_testDatasetDirectory, "missing.geojson");
632+
633+
ExpectComFailure(
634+
() =>
635+
{
636+
_ = automation.LoadVectorDatasetEx(missingPath);
637+
},
638+
HResultFail,
639+
"loading a missing dataset");
640+
641+
if ((int)automation.LayerCount != previousLayerCount ||
642+
(long)automation.FeatureCount != previousFeatureCount ||
643+
(long)automation.GetLayerId(0) != previousLayerId)
644+
{
645+
throw new InvalidOperationException(
646+
"A failed dataset load changed the existing layers or feature count.");
647+
}
648+
}
649+
650+
private void VerifyMultipleDatasetLoading()
613651
{
614652
ClearLoadedLayers();
615653
List<string> warnings = [];
@@ -620,36 +658,218 @@ private void VerifyLayerOperations()
620658
throw new InvalidOperationException(string.Join(Environment.NewLine, warnings));
621659
}
622660

623-
dynamic automation = _host!.AutomationObject!;
661+
dynamic automation = GetAutomation();
624662
if (first.Id <= 0 || second.Id <= 0 || first.Id == second.Id ||
625663
first.FeatureCount != 2 || second.FeatureCount != 2 ||
664+
(int)automation.LayerCount != 2 || (long)automation.FeatureCount != 4 ||
626665
(long)automation.GetLayerId(0) != first.Id ||
627666
(long)automation.GetLayerId(1) != second.Id ||
628667
_layerList.Items.Count != 2)
629668
{
630669
throw new InvalidOperationException("The two loaded layers have inconsistent IDs or feature counts.");
631670
}
671+
}
632672

633-
automation.ZoomToLayer(first.Id);
634-
automation.ZoomToLayer(second.Id);
635-
automation.Pan(1.0, -1.0);
636-
RepaintHost();
637-
if (!(bool)automation.LayerLoaded || (int)automation.LayerCount != 2 ||
638-
(long)automation.FeatureCount != 4)
673+
private void VerifyInvalidLayerIdHandling()
674+
{
675+
dynamic automation = GetAutomation();
676+
ExpectComFailure(
677+
() => automation.ZoomToLayer(long.MaxValue),
678+
HResultInvalidArgument,
679+
"zooming to an unknown layer ID",
680+
HResultDispatchException);
681+
VerifyLayerTotals(2, 4);
682+
}
683+
684+
private void VerifyZoomToLayers()
685+
{
686+
dynamic automation = GetAutomation();
687+
for (int index = 0; index < _layerList.Items.Count; index++)
688+
{
689+
LoadedLayer layer = GetLoadedLayer(index);
690+
automation.ZoomToLayer(layer.Id);
691+
RepaintHost();
692+
}
693+
694+
VerifyLayerTotals(2, 4);
695+
}
696+
697+
private void VerifyZoomInAndOut()
698+
{
699+
VerifyNavigation(automation =>
700+
{
701+
automation.ZoomIn(ZoomStep);
702+
automation.ZoomOut(ZoomStep);
703+
});
704+
}
705+
706+
private void VerifyPan()
707+
{
708+
VerifyNavigation(automation => automation.Pan(1.0, -1.0));
709+
}
710+
711+
private void VerifyZoomAt()
712+
{
713+
ActiveXHostControl host = _host ??
714+
throw new InvalidOperationException("The ActiveX control has not been created.");
715+
VerifyNavigation(automation =>
716+
{
717+
automation.ZoomAt(host.Width / 2.0, host.Height / 2.0, ZoomStep);
718+
});
719+
}
720+
721+
private void VerifyFitToMap()
722+
{
723+
VerifyNavigation(automation => automation.FitToMap());
724+
}
725+
726+
private void VerifyNavigationAfterResize()
727+
{
728+
if (_host is null)
729+
{
730+
throw new InvalidOperationException("The ActiveX control has not been created.");
731+
}
732+
733+
ActiveXHostControl host = _host;
734+
ClientSize = new Size(900, 600);
735+
PerformLayout();
736+
Application.DoEvents();
737+
if (host.Bounds != _surfacePanel.ClientRectangle)
738+
{
739+
throw new InvalidOperationException(
740+
$"The hosted surface did not follow the resized panel. Control={host.Bounds}, panel={_surfacePanel.ClientRectangle}.");
741+
}
742+
743+
VerifyNavigation(automation =>
744+
{
745+
automation.ZoomIn(ZoomStep);
746+
automation.Pan(1.0, -1.0);
747+
automation.ZoomAt(host.Width / 2.0, host.Height / 2.0, ZoomStep);
748+
automation.FitToMap();
749+
});
750+
}
751+
752+
private void VerifyClearAndReload()
753+
{
754+
long previousLayerId = GetLoadedLayer(_layerList.Items.Count - 1).Id;
755+
ClearLoadedLayers();
756+
List<string> warnings = [];
757+
LoadedLayer reloadedLayer = AddVectorDataset(TestDatasetPath, warnings);
758+
if (warnings.Count > 0)
759+
{
760+
throw new InvalidOperationException(string.Join(Environment.NewLine, warnings));
761+
}
762+
763+
if (reloadedLayer.Id <= previousLayerId ||
764+
reloadedLayer.FeatureCount != 2 ||
765+
(int)GetAutomation().LayerCount != 1 ||
766+
(long)GetAutomation().FeatureCount != 2 ||
767+
_layerList.Items.Count != 1)
639768
{
640-
throw new InvalidOperationException("Navigation changed the loaded layer or feature counts.");
769+
throw new InvalidOperationException(
770+
"Reloading after clearing did not create a new stable layer with the expected feature count.");
641771
}
772+
}
642773

774+
private void VerifyEmptyMapNavigation()
775+
{
643776
ClearLoadedLayers();
644-
if (_layerList.Items.Count != 0)
777+
VerifyNavigation(
778+
automation =>
779+
{
780+
automation.FitToMap();
781+
automation.ZoomIn(ZoomStep);
782+
automation.ZoomOut(ZoomStep);
783+
automation.Pan(1.0, -1.0);
784+
automation.ZoomAt(0.0, 0.0, ZoomStep);
785+
},
786+
expectedLayerCount: 0,
787+
expectedFeatureCount: 0);
788+
}
789+
790+
private dynamic GetAutomation()
791+
{
792+
if (_host?.AutomationObject is not object automationObject)
793+
{
794+
throw new InvalidOperationException("The ActiveX automation object is unavailable.");
795+
}
796+
797+
return automationObject;
798+
}
799+
800+
private LoadedLayer GetLoadedLayer(int index)
801+
{
802+
if (index < 0 || index >= _layerList.Items.Count ||
803+
_layerList.Items[index].Tag is not LoadedLayer layer)
804+
{
805+
throw new InvalidOperationException($"No loaded layer exists at index {index}.");
806+
}
807+
808+
return layer;
809+
}
810+
811+
private void VerifyNavigation(
812+
Action<dynamic> operation,
813+
int expectedLayerCount = 2,
814+
long expectedFeatureCount = 4)
815+
{
816+
dynamic automation = GetAutomation();
817+
operation(automation);
818+
RepaintHost();
819+
RefreshStatus();
820+
VerifyLayerTotals(expectedLayerCount, expectedFeatureCount);
821+
}
822+
823+
private void VerifyLayerTotals(int expectedLayerCount, long expectedFeatureCount)
824+
{
825+
dynamic automation = GetAutomation();
826+
if ((int)automation.LayerCount != expectedLayerCount ||
827+
(long)automation.FeatureCount != expectedFeatureCount ||
828+
(bool)automation.LayerLoaded != (expectedLayerCount > 0))
829+
{
830+
throw new InvalidOperationException(
831+
$"Expected {expectedLayerCount} layers and {expectedFeatureCount} features after the operation.");
832+
}
833+
}
834+
835+
private static void ExpectComFailure(
836+
Action operation,
837+
int expectedHResult,
838+
string operationName,
839+
int? alternateHResult = null)
840+
{
841+
try
645842
{
646-
throw new InvalidOperationException("The layer list was not cleared.");
843+
operation();
647844
}
845+
catch (Exception exception)
846+
{
847+
Exception failure = exception.GetBaseException();
848+
if (failure is not COMException and not ArgumentException)
849+
{
850+
throw new InvalidOperationException(
851+
$"The {operationName} threw {failure.GetType().Name}: {failure.Message}");
852+
}
853+
854+
if (failure.HResult == expectedHResult ||
855+
failure.HResult == alternateHResult)
856+
{
857+
return;
858+
}
859+
860+
throw new InvalidOperationException(
861+
$"The {operationName} returned HRESULT 0x{failure.HResult:x8}, expected 0x{expectedHResult:x8}" +
862+
(alternateHResult.HasValue ? $" or 0x{alternateHResult:x8}." : ".") +
863+
$" COM error: {failure.Message}");
864+
}
865+
866+
throw new InvalidOperationException($"The {operationName} unexpectedly succeeded.");
648867
}
649868

650869
private void VerifyInteractiveLoadStatus()
651870
{
652-
dynamic automation = _host!.AutomationObject!;
871+
ClearLoadedLayers();
872+
dynamic automation = GetAutomation();
653873
string previousPreference = automation.RenderingBackendPreference;
654874
try
655875
{
@@ -786,14 +1006,9 @@ private void VerifyCoreInitialized(string activation)
7861006
}
7871007
}
7881008

789-
private void VerifyCpuBackendSelection()
1009+
private void VerifyRenderingBackendsWhereSupported()
7901010
{
791-
if (_host?.AutomationObject is not object automationObject)
792-
{
793-
throw new InvalidOperationException("The ActiveX automation object is unavailable.");
794-
}
795-
796-
dynamic automation = automationObject;
1011+
dynamic automation = GetAutomation();
7971012
string previousPreference = automation.RenderingBackendPreference;
7981013
try
7991014
{
@@ -810,10 +1025,19 @@ private void VerifyCpuBackendSelection()
8101025
}
8111026

8121027
VerifyMapPixels();
1028+
1029+
automation.RenderingBackendPreference = "OpenGL";
1030+
RepaintHost();
1031+
string requestedBackend = automation.RenderingBackend;
1032+
if (requestedBackend is not "GDI" and not "OpenGL")
1033+
{
1034+
throw new InvalidOperationException(
1035+
$"The control reported an unknown rendering backend '{requestedBackend}'.");
1036+
}
1037+
VerifyMapPixels();
8131038
}
8141039
finally
8151040
{
816-
// Later resize/repaint checks must exercise the default backend path.
8171041
automation.RenderingBackendPreference = previousPreference;
8181042
RepaintHost();
8191043
}

0 commit comments

Comments
 (0)