Skip to content

Commit 29dc97a

Browse files
committed
Add coverage.runsettings and unit tests
Update GitHub Actions to use a coverage.runsettings file and add that runsettings (cobertura format, excludes for source-generated files and attributes). Annotate JsonSerializerContext partials with [ExcludeFromCodeCoverage] and add the necessary using directives to avoid counting generated serializers in coverage. Add a number of unit tests: Recorder CLI width/height parsing and defaults, BrowserFinder candidate/resolve behaviors and InstalledBinariesPath handling, and BrowserPool/BrowserLease stress and option tests.
1 parent 2aa4b10 commit 29dc97a

10 files changed

Lines changed: 405 additions & 1 deletion

File tree

.github/workflows/motus-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
--logger "trx;LogFilePrefix=results"
4949
--collect:"XPlat Code Coverage"
5050
--results-directory TestResults
51-
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura
51+
--settings coverage.runsettings
5252
5353
- name: Upload test results
5454
if: always()

coverage.runsettings

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<RunSettings>
3+
<DataCollectionRunSettings>
4+
<DataCollectors>
5+
<DataCollector friendlyName="XPlat Code Coverage">
6+
<Configuration>
7+
<Format>cobertura</Format>
8+
9+
<!-- Exclude source-generated protocol domains and the Blazor visual runner -->
10+
<Exclude>
11+
[Motus]Motus.Protocol.*,
12+
[Motus.Runner]*
13+
</Exclude>
14+
15+
<!-- Exclude types decorated with these attributes -->
16+
<ExcludeByAttribute>
17+
ExcludeFromCodeCoverage,CompilerGenerated
18+
</ExcludeByAttribute>
19+
20+
<!-- Exclude source-generated files from the Roslyn generators -->
21+
<ExcludeByFile>
22+
**/*.g.cs
23+
</ExcludeByFile>
24+
</Configuration>
25+
</DataCollector>
26+
</DataCollectors>
27+
</DataCollectionRunSettings>
28+
</RunSettings>

src/Motus.Recorder/ActionCapture/RecorderJsonContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using System.Text.Json.Serialization;
23

34
namespace Motus.Recorder.ActionCapture;
@@ -15,4 +16,5 @@ internal sealed record RecorderDialogClosedEvent(bool Result, string UserInput);
1516
[JsonSourceGenerationOptions(
1617
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
1718
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
19+
[ExcludeFromCodeCoverage]
1820
internal sealed partial class RecorderJsonContext : JsonSerializerContext;

src/Motus/Config/MotusConfigJsonContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using System.Text.Json.Serialization;
23

34
namespace Motus;
@@ -15,4 +16,5 @@ namespace Motus;
1516
[JsonSourceGenerationOptions(
1617
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
1718
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
19+
[ExcludeFromCodeCoverage]
1820
internal sealed partial class MotusConfigJsonContext : JsonSerializerContext;

src/Motus/Network/HarJsonContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using System.Text.Json.Serialization;
23

34
namespace Motus;
@@ -16,4 +17,5 @@ namespace Motus;
1617
[JsonSourceGenerationOptions(
1718
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
1819
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
20+
[ExcludeFromCodeCoverage]
1921
internal sealed partial class HarJsonContext : JsonSerializerContext;

src/Motus/Protocol/CdpJsonContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using System.Text.Json;
23
using System.Text.Json.Serialization;
34

@@ -218,6 +219,7 @@ namespace Motus;
218219
[JsonSourceGenerationOptions(
219220
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
220221
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
222+
[ExcludeFromCodeCoverage]
221223
internal sealed partial class CdpJsonContext : JsonSerializerContext;
222224

223225
// ============================================================================

src/Motus/Transport/BiDi/BiDiJsonContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using System.Text.Json;
23
using System.Text.Json.Serialization;
34

@@ -236,4 +237,5 @@ internal sealed record BiDiNetworkHeader(
236237
[JsonSourceGenerationOptions(
237238
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
238239
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
240+
[ExcludeFromCodeCoverage]
239241
internal sealed partial class BiDiJsonContext : JsonSerializerContext;

tests/Motus.Cli.Tests/Commands/RecordCommandTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,50 @@ public void Parse_AllOptions_NoErrors()
4343
var result = Cmd.Parse("--output test.cs --framework nunit --class-name MyTest --method-name DoStuff --namespace My.Ns");
4444
Assert.AreEqual(0, result.Errors.Count);
4545
}
46+
47+
[TestMethod]
48+
public void Parse_WidthOption_NoErrors()
49+
{
50+
var result = Cmd.Parse("--width 1920");
51+
Assert.AreEqual(0, result.Errors.Count);
52+
}
53+
54+
[TestMethod]
55+
public void Parse_HeightOption_NoErrors()
56+
{
57+
var result = Cmd.Parse("--height 1080");
58+
Assert.AreEqual(0, result.Errors.Count);
59+
}
60+
61+
[TestMethod]
62+
public void Parse_ViewportOptions_NoErrors()
63+
{
64+
var result = Cmd.Parse("--width 1920 --height 1080");
65+
Assert.AreEqual(0, result.Errors.Count);
66+
}
67+
68+
[TestMethod]
69+
public void Parse_AllOptionsWithViewport_NoErrors()
70+
{
71+
var result = Cmd.Parse("--output test.cs --framework nunit --width 1440 --height 900 --class-name MyTest --method-name DoStuff --namespace My.Ns");
72+
Assert.AreEqual(0, result.Errors.Count);
73+
}
74+
75+
[TestMethod]
76+
public void Parse_WidthDefault_Is1024()
77+
{
78+
var result = Cmd.Parse("");
79+
var widthOpt = (Option<int>)Cmd.Options.First(o => o.Name.Contains("width"));
80+
var value = result.GetValue(widthOpt);
81+
Assert.AreEqual(1024, value);
82+
}
83+
84+
[TestMethod]
85+
public void Parse_HeightDefault_Is768()
86+
{
87+
var result = Cmd.Parse("");
88+
var heightOpt = (Option<int>)Cmd.Options.First(o => o.Name.Contains("height"));
89+
var value = result.GetValue(heightOpt);
90+
Assert.AreEqual(768, value);
91+
}
4692
}

tests/Motus.Tests/Browser/BrowserFinderTests.cs

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,223 @@ public void CandidatesForChannel_Firefox_DoesNotOverlapChrome()
9494

9595
CollectionAssert.AreNotEqual(chrome.ToList(), firefox.ToList());
9696
}
97+
98+
[TestMethod]
99+
public void CandidatesForChannel_Edge_ReturnsNonEmptyList()
100+
{
101+
var candidates = BrowserFinder.CandidatesForChannel(BrowserChannel.Edge);
102+
103+
Assert.IsTrue(candidates.Count > 0, "Should return at least one Edge candidate path.");
104+
}
105+
106+
[TestMethod]
107+
public void CandidatesForChannel_Chromium_ReturnsNonEmptyList()
108+
{
109+
var candidates = BrowserFinder.CandidatesForChannel(BrowserChannel.Chromium);
110+
111+
Assert.IsTrue(candidates.Count > 0, "Should return at least one Chromium candidate path.");
112+
}
113+
114+
[TestMethod]
115+
public void CandidatesForChannel_Edge_IncludesInstalledBinariesPath_WhenSet()
116+
{
117+
var originalPath = BrowserFinder.InstalledBinariesPath;
118+
try
119+
{
120+
BrowserFinder.InstalledBinariesPath = "/opt/motus/browsers";
121+
var candidates = BrowserFinder.CandidatesForChannel(BrowserChannel.Edge);
122+
123+
Assert.IsTrue(candidates.Any(c => c.StartsWith("/opt/motus/browsers")),
124+
"Should include installed binaries path for Edge when set.");
125+
Assert.IsTrue(candidates.Any(c => c.Contains("msedge")),
126+
"Edge candidate should contain 'msedge' in the path.");
127+
}
128+
finally
129+
{
130+
BrowserFinder.InstalledBinariesPath = originalPath;
131+
}
132+
}
133+
134+
[TestMethod]
135+
public void CandidatesForChannel_Chromium_IncludesInstalledBinariesPath_WhenSet()
136+
{
137+
var originalPath = BrowserFinder.InstalledBinariesPath;
138+
try
139+
{
140+
BrowserFinder.InstalledBinariesPath = "/opt/motus/browsers";
141+
var candidates = BrowserFinder.CandidatesForChannel(BrowserChannel.Chromium);
142+
143+
Assert.IsTrue(candidates.Any(c => c.StartsWith("/opt/motus/browsers")),
144+
"Should include installed binaries path for Chromium when set.");
145+
Assert.IsTrue(candidates.Any(c => c.Contains("chromium")),
146+
"Chromium candidate should contain 'chromium' in the path.");
147+
}
148+
finally
149+
{
150+
BrowserFinder.InstalledBinariesPath = originalPath;
151+
}
152+
}
153+
154+
[TestMethod]
155+
public void CandidatesForChannel_AllChannels_DoNotOverlap()
156+
{
157+
var chrome = BrowserFinder.CandidatesForChannel(BrowserChannel.Chrome);
158+
var edge = BrowserFinder.CandidatesForChannel(BrowserChannel.Edge);
159+
var chromium = BrowserFinder.CandidatesForChannel(BrowserChannel.Chromium);
160+
var firefox = BrowserFinder.CandidatesForChannel(BrowserChannel.Firefox);
161+
162+
// With InstalledBinariesPath cleared, platform-specific paths should be distinct
163+
var originalPath = BrowserFinder.InstalledBinariesPath;
164+
try
165+
{
166+
BrowserFinder.InstalledBinariesPath = null;
167+
chrome = BrowserFinder.CandidatesForChannel(BrowserChannel.Chrome);
168+
edge = BrowserFinder.CandidatesForChannel(BrowserChannel.Edge);
169+
chromium = BrowserFinder.CandidatesForChannel(BrowserChannel.Chromium);
170+
firefox = BrowserFinder.CandidatesForChannel(BrowserChannel.Firefox);
171+
172+
var all = chrome.Concat(edge).Concat(chromium).Concat(firefox).ToList();
173+
var distinct = all.Distinct().ToList();
174+
Assert.AreEqual(distinct.Count, all.Count, "Candidate paths across channels should be unique.");
175+
}
176+
finally
177+
{
178+
BrowserFinder.InstalledBinariesPath = originalPath;
179+
}
180+
}
181+
182+
[TestMethod]
183+
public void Resolve_WithChannel_ExercisesChannelPath()
184+
{
185+
var originalPath = BrowserFinder.InstalledBinariesPath;
186+
try
187+
{
188+
BrowserFinder.InstalledBinariesPath = "/nonexistent/motus/browsers";
189+
try
190+
{
191+
var result = BrowserFinder.Resolve(channel: BrowserChannel.Firefox, executablePath: null);
192+
// If we reach here, a real Firefox is installed at a system path
193+
Assert.IsTrue(File.Exists(result));
194+
}
195+
catch (FileNotFoundException)
196+
{
197+
// Expected when no Firefox is installed
198+
}
199+
}
200+
finally
201+
{
202+
BrowserFinder.InstalledBinariesPath = originalPath;
203+
}
204+
}
205+
206+
[TestMethod]
207+
public void Resolve_WithChannel_UsesExistingBinary()
208+
{
209+
var existingFile = typeof(BrowserFinderTests).Assembly.Location;
210+
var originalPath = BrowserFinder.InstalledBinariesPath;
211+
try
212+
{
213+
// Point InstalledBinariesPath to the directory containing the test assembly,
214+
// and rename expectation to match the binary name pattern
215+
var dir = Path.GetDirectoryName(existingFile)!;
216+
217+
// Create a temp file named "chrome" (or "chrome.exe" on Windows) in a temp dir
218+
var tempDir = Path.Combine(Path.GetTempPath(), $"motus-test-{Guid.NewGuid():N}");
219+
Directory.CreateDirectory(tempDir);
220+
var fakeBrowser = Path.Combine(tempDir,
221+
OperatingSystem.IsWindows() ? "chrome.exe" : "chrome");
222+
File.WriteAllText(fakeBrowser, "fake");
223+
224+
BrowserFinder.InstalledBinariesPath = tempDir;
225+
var result = BrowserFinder.Resolve(channel: BrowserChannel.Chrome, executablePath: null);
226+
227+
Assert.AreEqual(fakeBrowser, result);
228+
229+
// Cleanup
230+
File.Delete(fakeBrowser);
231+
Directory.Delete(tempDir);
232+
}
233+
finally
234+
{
235+
BrowserFinder.InstalledBinariesPath = originalPath;
236+
}
237+
}
238+
239+
[TestMethod]
240+
public void Resolve_AutoDetect_FindsFirstAvailableBrowser()
241+
{
242+
var originalPath = BrowserFinder.InstalledBinariesPath;
243+
try
244+
{
245+
var tempDir = Path.Combine(Path.GetTempPath(), $"motus-test-{Guid.NewGuid():N}");
246+
Directory.CreateDirectory(tempDir);
247+
var fakeBrowser = Path.Combine(tempDir,
248+
OperatingSystem.IsWindows() ? "chrome.exe" : "chrome");
249+
File.WriteAllText(fakeBrowser, "fake");
250+
251+
BrowserFinder.InstalledBinariesPath = tempDir;
252+
// channel: null, executablePath: null triggers auto-detect
253+
var result = BrowserFinder.Resolve(channel: null, executablePath: null);
254+
255+
Assert.AreEqual(fakeBrowser, result, "Auto-detect should find Chrome first.");
256+
257+
File.Delete(fakeBrowser);
258+
Directory.Delete(tempDir);
259+
}
260+
finally
261+
{
262+
BrowserFinder.InstalledBinariesPath = originalPath;
263+
}
264+
}
265+
266+
[TestMethod]
267+
public void Resolve_AutoDetect_ExercisesAutoDetectPath()
268+
{
269+
var originalPath = BrowserFinder.InstalledBinariesPath;
270+
try
271+
{
272+
var tempDir = Path.Combine(Path.GetTempPath(), $"motus-test-{Guid.NewGuid():N}");
273+
Directory.CreateDirectory(tempDir);
274+
275+
BrowserFinder.InstalledBinariesPath = tempDir;
276+
try
277+
{
278+
var result = BrowserFinder.Resolve(channel: null, executablePath: null);
279+
// If we reach here, a real browser is installed at a system path
280+
Assert.IsTrue(File.Exists(result));
281+
}
282+
catch (FileNotFoundException)
283+
{
284+
// Expected when no browser is installed at system paths
285+
}
286+
finally
287+
{
288+
Directory.Delete(tempDir);
289+
}
290+
}
291+
finally
292+
{
293+
BrowserFinder.InstalledBinariesPath = originalPath;
294+
}
295+
}
296+
297+
[TestMethod]
298+
public void InstalledBinariesPath_Null_DoesNotPrependPath()
299+
{
300+
var originalPath = BrowserFinder.InstalledBinariesPath;
301+
try
302+
{
303+
BrowserFinder.InstalledBinariesPath = null;
304+
var candidates = BrowserFinder.CandidatesForChannel(BrowserChannel.Chrome);
305+
306+
// All candidates should be platform-specific system paths, not prepended
307+
Assert.IsTrue(candidates.All(c => !string.IsNullOrWhiteSpace(c)));
308+
}
309+
finally
310+
{
311+
BrowserFinder.InstalledBinariesPath = originalPath;
312+
}
313+
}
97314
}
98315

99316
[TestClass]

0 commit comments

Comments
 (0)