Skip to content

Commit da5810b

Browse files
committed
tests
1 parent 062bafa commit da5810b

File tree

5 files changed

+560
-0
lines changed

5 files changed

+560
-0
lines changed

Shiny.Spatial.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<Project Path="src/Shiny.Spatial/Shiny.Spatial.csproj" />
1616
<Project Path="tests/Shiny.Spatial.Tests/Shiny.Spatial.Tests.csproj" />
1717
<Project Path="src\Shiny.Spatial.Geofencing\Shiny.Spatial.Geofencing.csproj" />
18+
<Project Path="tests/Shiny.Spatial.Geofencing.Tests/Shiny.Spatial.Geofencing.Tests.csproj" />
1819
<Project Path="tests/Shiny.Spatial.Benchmarks/Shiny.Spatial.Benchmarks.csproj" />
1920
<Project Path="tools/Shiny.Spatial.DatabaseSeeder/Shiny.Spatial.DatabaseSeeder.csproj" />
2021
<Folder Name="/Solution/databases/">
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>$(BaseTargetFramework)</TargetFramework>
5+
<IsPackable>false</IsPackable>
6+
<IsTestProject>true</IsTestProject>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
11+
<PackageReference Include="xunit"/>
12+
<PackageReference Include="xunit.runner.visualstudio">
13+
<PrivateAssets>all</PrivateAssets>
14+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
15+
</PackageReference>
16+
<PackageReference Include="Shouldly"/>
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="..\..\src\Shiny.Spatial.Geofencing\Shiny.Spatial.Geofencing.csproj"/>
21+
</ItemGroup>
22+
23+
</Project>
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
using Microsoft.Extensions.Logging.Abstractions;
2+
using Shouldly;
3+
using Shiny.Locations;
4+
using Shiny.Spatial.Database;
5+
using Shiny.Spatial.Geofencing;
6+
using Shiny.Spatial.Geometry;
7+
using Xunit;
8+
9+
namespace Shiny.Spatial.Geofencing.Tests;
10+
11+
public class TestGeofenceDelegate : ISpatialGeofenceDelegate
12+
{
13+
public List<SpatialRegionChange> Changes { get; } = [];
14+
15+
public Task OnRegionChanged(SpatialRegionChange change)
16+
{
17+
Changes.Add(change);
18+
return Task.CompletedTask;
19+
}
20+
}
21+
22+
public class TestableSpatialGpsDelegate : SpatialGpsDelegate
23+
{
24+
public TestableSpatialGpsDelegate(
25+
ISpatialGeofenceDelegate geofenceDelegate,
26+
SpatialMonitorConfig config
27+
) : base(NullLogger<SpatialGpsDelegate>.Instance, geofenceDelegate, config)
28+
{
29+
}
30+
31+
public Task SimulateGpsReading(GpsReading reading) => OnGpsReading(reading);
32+
}
33+
34+
public class SpatialGpsDelegateTests : IDisposable
35+
{
36+
readonly List<string> _tempFiles = [];
37+
38+
public void Dispose()
39+
{
40+
foreach (var f in _tempFiles)
41+
{
42+
try { File.Delete(f); } catch { }
43+
}
44+
}
45+
46+
string CreateTestDatabase(string tableName, params SpatialFeature[] features)
47+
{
48+
var path = Path.Combine(Path.GetTempPath(), $"geofence_test_{Guid.NewGuid()}.db");
49+
_tempFiles.Add(path);
50+
51+
using var db = new SpatialDatabase(path);
52+
var table = db.CreateTable(tableName, CoordinateSystem.Wgs84,
53+
new PropertyDefinition("name", PropertyType.Text));
54+
55+
foreach (var feature in features)
56+
table.Insert(feature);
57+
58+
return path;
59+
}
60+
61+
static SpatialFeature MakePolygonRegion(string name, double minX, double minY, double maxX, double maxY)
62+
{
63+
return new SpatialFeature(new Polygon([
64+
new Coordinate(minX, minY),
65+
new Coordinate(maxX, minY),
66+
new Coordinate(maxX, maxY),
67+
new Coordinate(minX, maxY),
68+
new Coordinate(minX, minY)
69+
]))
70+
{
71+
Properties = { ["name"] = name }
72+
};
73+
}
74+
75+
static GpsReading MakeReading(double longitude, double latitude)
76+
{
77+
return new GpsReading(
78+
new Position(latitude, longitude),
79+
0d, // positionAccuracy
80+
DateTimeOffset.UtcNow, // timestamp
81+
0d, 0d, 0d, 0d, 0d, // altitude, heading, headingAccuracy, speed, speedAccuracy
82+
0, // satellites
83+
false // isFromMockProvider
84+
);
85+
}
86+
87+
[Fact]
88+
public async Task Enter_Region_Fires_Change()
89+
{
90+
var regionA = MakePolygonRegion("Region A", -10, -10, 10, 10);
91+
var dbPath = CreateTestDatabase("zones", regionA);
92+
93+
var testDelegate = new TestGeofenceDelegate();
94+
var config = new SpatialMonitorConfig().Add(dbPath, "zones");
95+
96+
using var gpsDelegate = new TestableSpatialGpsDelegate(testDelegate, config);
97+
98+
await gpsDelegate.SimulateGpsReading(MakeReading(0, 0));
99+
100+
testDelegate.Changes.Count.ShouldBe(1);
101+
testDelegate.Changes[0].PreviousRegion.ShouldBeNull();
102+
testDelegate.Changes[0].CurrentRegion.ShouldNotBeNull();
103+
testDelegate.Changes[0].CurrentRegion!.Properties["name"].ShouldBe("Region A");
104+
testDelegate.Changes[0].TableName.ShouldBe("zones");
105+
}
106+
107+
[Fact]
108+
public async Task Exit_Region_Fires_Change()
109+
{
110+
var regionA = MakePolygonRegion("Region A", -10, -10, 10, 10);
111+
var dbPath = CreateTestDatabase("zones", regionA);
112+
113+
var testDelegate = new TestGeofenceDelegate();
114+
var config = new SpatialMonitorConfig().Add(dbPath, "zones");
115+
116+
using var gpsDelegate = new TestableSpatialGpsDelegate(testDelegate, config);
117+
118+
// Enter region
119+
await gpsDelegate.SimulateGpsReading(MakeReading(0, 0));
120+
// Exit region
121+
await gpsDelegate.SimulateGpsReading(MakeReading(50, 50));
122+
123+
testDelegate.Changes.Count.ShouldBe(2);
124+
125+
// First change: enter
126+
testDelegate.Changes[0].PreviousRegion.ShouldBeNull();
127+
testDelegate.Changes[0].CurrentRegion.ShouldNotBeNull();
128+
129+
// Second change: exit
130+
testDelegate.Changes[1].PreviousRegion.ShouldNotBeNull();
131+
testDelegate.Changes[1].PreviousRegion!.Properties["name"].ShouldBe("Region A");
132+
testDelegate.Changes[1].CurrentRegion.ShouldBeNull();
133+
}
134+
135+
[Fact]
136+
public async Task Move_Between_Regions_Fires_Change()
137+
{
138+
var regionA = MakePolygonRegion("Region A", -10, -10, 0, 0);
139+
var regionB = MakePolygonRegion("Region B", 10, 10, 20, 20);
140+
var dbPath = CreateTestDatabase("zones", regionA, regionB);
141+
142+
var testDelegate = new TestGeofenceDelegate();
143+
var config = new SpatialMonitorConfig().Add(dbPath, "zones");
144+
145+
using var gpsDelegate = new TestableSpatialGpsDelegate(testDelegate, config);
146+
147+
// Enter region A
148+
await gpsDelegate.SimulateGpsReading(MakeReading(-5, -5));
149+
// Move to region B
150+
await gpsDelegate.SimulateGpsReading(MakeReading(15, 15));
151+
152+
testDelegate.Changes.Count.ShouldBe(2);
153+
testDelegate.Changes[0].CurrentRegion!.Properties["name"].ShouldBe("Region A");
154+
testDelegate.Changes[1].PreviousRegion!.Properties["name"].ShouldBe("Region A");
155+
testDelegate.Changes[1].CurrentRegion!.Properties["name"].ShouldBe("Region B");
156+
}
157+
158+
[Fact]
159+
public async Task Stay_In_Same_Region_No_Change()
160+
{
161+
var regionA = MakePolygonRegion("Region A", -10, -10, 10, 10);
162+
var dbPath = CreateTestDatabase("zones", regionA);
163+
164+
var testDelegate = new TestGeofenceDelegate();
165+
var config = new SpatialMonitorConfig().Add(dbPath, "zones");
166+
167+
using var gpsDelegate = new TestableSpatialGpsDelegate(testDelegate, config);
168+
169+
await gpsDelegate.SimulateGpsReading(MakeReading(0, 0));
170+
await gpsDelegate.SimulateGpsReading(MakeReading(1, 1));
171+
await gpsDelegate.SimulateGpsReading(MakeReading(2, 2));
172+
173+
// Only 1 change: the initial enter
174+
testDelegate.Changes.Count.ShouldBe(1);
175+
}
176+
177+
[Fact]
178+
public async Task Stay_Outside_All_Regions_No_Change()
179+
{
180+
var regionA = MakePolygonRegion("Region A", -10, -10, 10, 10);
181+
var dbPath = CreateTestDatabase("zones", regionA);
182+
183+
var testDelegate = new TestGeofenceDelegate();
184+
var config = new SpatialMonitorConfig().Add(dbPath, "zones");
185+
186+
using var gpsDelegate = new TestableSpatialGpsDelegate(testDelegate, config);
187+
188+
await gpsDelegate.SimulateGpsReading(MakeReading(50, 50));
189+
await gpsDelegate.SimulateGpsReading(MakeReading(60, 60));
190+
191+
testDelegate.Changes.Count.ShouldBe(0);
192+
}
193+
194+
[Fact]
195+
public async Task Multiple_Tables_Independent_Tracking()
196+
{
197+
var city = MakePolygonRegion("Denver", -105, 39, -104, 40);
198+
var state = MakePolygonRegion("Colorado", -109, 37, -102, 41);
199+
200+
var cityDbPath = CreateTestDatabase("cities", city);
201+
var stateDbPath = CreateTestDatabase("states", state);
202+
203+
var testDelegate = new TestGeofenceDelegate();
204+
var config = new SpatialMonitorConfig()
205+
.Add(cityDbPath, "cities")
206+
.Add(stateDbPath, "states");
207+
208+
using var gpsDelegate = new TestableSpatialGpsDelegate(testDelegate, config);
209+
210+
// Inside both Denver and Colorado
211+
await gpsDelegate.SimulateGpsReading(MakeReading(-104.5, 39.5));
212+
213+
testDelegate.Changes.Count.ShouldBe(2);
214+
testDelegate.Changes.ShouldContain(c => c.TableName == "cities");
215+
testDelegate.Changes.ShouldContain(c => c.TableName == "states");
216+
}
217+
218+
[Fact]
219+
public async Task Multiple_Tables_Exit_One_Stay_In_Other()
220+
{
221+
var city = MakePolygonRegion("Denver", -105, 39, -104, 40);
222+
var state = MakePolygonRegion("Colorado", -109, 37, -102, 41);
223+
224+
var cityDbPath = CreateTestDatabase("cities", city);
225+
var stateDbPath = CreateTestDatabase("states", state);
226+
227+
var testDelegate = new TestGeofenceDelegate();
228+
var config = new SpatialMonitorConfig()
229+
.Add(cityDbPath, "cities")
230+
.Add(stateDbPath, "states");
231+
232+
using var gpsDelegate = new TestableSpatialGpsDelegate(testDelegate, config);
233+
234+
// Inside Denver and Colorado
235+
await gpsDelegate.SimulateGpsReading(MakeReading(-104.5, 39.5));
236+
testDelegate.Changes.Clear();
237+
238+
// Move outside Denver, still in Colorado
239+
await gpsDelegate.SimulateGpsReading(MakeReading(-106, 38));
240+
241+
testDelegate.Changes.Count.ShouldBe(1);
242+
testDelegate.Changes[0].TableName.ShouldBe("cities");
243+
testDelegate.Changes[0].PreviousRegion.ShouldNotBeNull();
244+
testDelegate.Changes[0].CurrentRegion.ShouldBeNull();
245+
}
246+
247+
[Fact]
248+
public async Task Same_Database_Different_Tables()
249+
{
250+
var path = Path.Combine(Path.GetTempPath(), $"geofence_test_{Guid.NewGuid()}.db");
251+
_tempFiles.Add(path);
252+
253+
using (var db = new SpatialDatabase(path))
254+
{
255+
var citiesTable = db.CreateTable("cities", CoordinateSystem.Wgs84,
256+
new PropertyDefinition("name", PropertyType.Text));
257+
citiesTable.Insert(MakePolygonRegion("City A", -10, -10, 10, 10));
258+
259+
var zonesTable = db.CreateTable("zones", CoordinateSystem.Wgs84,
260+
new PropertyDefinition("name", PropertyType.Text));
261+
zonesTable.Insert(MakePolygonRegion("Zone 1", -20, -20, 20, 20));
262+
}
263+
264+
var testDelegate = new TestGeofenceDelegate();
265+
var config = new SpatialMonitorConfig()
266+
.Add(path, "cities")
267+
.Add(path, "zones");
268+
269+
using var gpsDelegate = new TestableSpatialGpsDelegate(testDelegate, config);
270+
271+
await gpsDelegate.SimulateGpsReading(MakeReading(0, 0));
272+
273+
testDelegate.Changes.Count.ShouldBe(2);
274+
}
275+
276+
[Fact]
277+
public async Task Dispose_Allows_Multiple_Calls()
278+
{
279+
var regionA = MakePolygonRegion("A", -10, -10, 10, 10);
280+
var dbPath = CreateTestDatabase("zones", regionA);
281+
282+
var testDelegate = new TestGeofenceDelegate();
283+
var config = new SpatialMonitorConfig().Add(dbPath, "zones");
284+
285+
var gpsDelegate = new TestableSpatialGpsDelegate(testDelegate, config);
286+
287+
// Trigger table loading
288+
await gpsDelegate.SimulateGpsReading(MakeReading(0, 0));
289+
290+
// Multiple dispose should not throw
291+
gpsDelegate.Dispose();
292+
gpsDelegate.Dispose();
293+
}
294+
295+
[Fact]
296+
public async Task Region_Change_Includes_Correct_Table_Name()
297+
{
298+
var region = MakePolygonRegion("Test Region", -10, -10, 10, 10);
299+
var dbPath = CreateTestDatabase("my_custom_table", region);
300+
301+
var testDelegate = new TestGeofenceDelegate();
302+
var config = new SpatialMonitorConfig().Add(dbPath, "my_custom_table");
303+
304+
using var gpsDelegate = new TestableSpatialGpsDelegate(testDelegate, config);
305+
306+
await gpsDelegate.SimulateGpsReading(MakeReading(0, 0));
307+
308+
testDelegate.Changes[0].TableName.ShouldBe("my_custom_table");
309+
}
310+
311+
[Fact]
312+
public async Task Enter_Exit_Reenter_Same_Region()
313+
{
314+
var region = MakePolygonRegion("Region A", -10, -10, 10, 10);
315+
var dbPath = CreateTestDatabase("zones", region);
316+
317+
var testDelegate = new TestGeofenceDelegate();
318+
var config = new SpatialMonitorConfig().Add(dbPath, "zones");
319+
320+
using var gpsDelegate = new TestableSpatialGpsDelegate(testDelegate, config);
321+
322+
// Enter
323+
await gpsDelegate.SimulateGpsReading(MakeReading(0, 0));
324+
// Exit
325+
await gpsDelegate.SimulateGpsReading(MakeReading(50, 50));
326+
// Re-enter
327+
await gpsDelegate.SimulateGpsReading(MakeReading(5, 5));
328+
329+
testDelegate.Changes.Count.ShouldBe(3);
330+
331+
// Enter
332+
testDelegate.Changes[0].PreviousRegion.ShouldBeNull();
333+
testDelegate.Changes[0].CurrentRegion.ShouldNotBeNull();
334+
335+
// Exit
336+
testDelegate.Changes[1].PreviousRegion.ShouldNotBeNull();
337+
testDelegate.Changes[1].CurrentRegion.ShouldBeNull();
338+
339+
// Re-enter
340+
testDelegate.Changes[2].PreviousRegion.ShouldBeNull();
341+
testDelegate.Changes[2].CurrentRegion.ShouldNotBeNull();
342+
}
343+
}

0 commit comments

Comments
 (0)