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
112 changes: 109 additions & 3 deletions src/Daqifi.Core.Tests/Firmware/FirmwareUpdateServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,112 @@ [new HidDeviceInfo(0x04D8, 0x003C, "path-1", "SN-1", "DAQiFi Bootloader")]
Assert.Equal(100, terminalProgress.PercentComplete);
}

[Theory]
[InlineData(0)]
[InlineData(-1)]
public async Task ExecuteWithRetryAsync_WithNonPositiveMaxAttempts_ThrowsAndNeverRunsTheAction(int maxAttempts)
{
// The retry helper is shared plumbing for flash-critical steps (erase, program,
// verify). A caller asking for zero attempts must fail loudly rather than return
// a silent success that skipped the operation entirely.
var context = new FirmwareUpdateContext(
new object(),
NullLogger<FirmwareUpdateService>.Instance,
new FirmwareUpdateServiceOptions());

var actionRan = false;

var ex = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
() => context.ExecuteWithRetryAsync(
"erase flash",
maxAttempts,
TimeSpan.Zero,
_ =>
{
actionRan = true;
return Task.CompletedTask;
},
_ => true,
CancellationToken.None));

Assert.Equal("maxAttempts", ex.ParamName);
Assert.False(actionRan);
}

[Fact]
public async Task ExecuteWithRetryAsync_WithSingleAttempt_RunsTheActionExactlyOnce()
{
// Guards the boundary: 1 is valid and must not be caught by the new check.
var context = new FirmwareUpdateContext(
new object(),
NullLogger<FirmwareUpdateService>.Instance,
new FirmwareUpdateServiceOptions());

var runCount = 0;

await context.ExecuteWithRetryAsync(
"erase flash",
1,
TimeSpan.Zero,
_ =>
{
runCount++;
return Task.CompletedTask;
},
_ => true,
CancellationToken.None);

Assert.Equal(1, runCount);
}

[Fact]
public async Task UpdateFirmwareAsync_RaisesStateChangedWithTheServiceAsSender()
{
// The state machine lives in an internal collaborator, so the event's
// sender is forwarded rather than being an implicit `this`. Subscribers
// that key off sender (e.g. a UI tracking several services) must keep
// seeing the public service instance.
var device = new FakeStreamingDevice("COM3");
var hidTransport = new FakeHidTransport();
hidTransport.EnqueueRead([0x01, 0x10]); // version
hidTransport.EnqueueRead([0x01, 0x02]); // erase ack
hidTransport.EnqueueRead([0x01, 0x03]); // program ack 1
hidTransport.EnqueueRead([0x01, 0x03]); // program ack 2
hidTransport.EnqueueRead([0xCD, 0xAB]); // READ_CRC response → decodes to 0xABCD (match)

var enumerator = new FakeHidDeviceEnumerator([
Array.Empty<HidDeviceInfo>(),
[new HidDeviceInfo(0x04D8, 0x003C, "path-1", "SN-1", "DAQiFi Bootloader")]
]);

var service = new FirmwareUpdateService(
hidTransport,
new FakeFirmwareDownloadService(),
new FakeExternalProcessRunner(),
NullLogger<FirmwareUpdateService>.Instance,
new FakeBootloaderProtocol(
[[0xA1, 0x01], [0xA1, 0x02]],
crcRegions: [new FlashCrcRegion(0x9D000000, 256, 0xABCD)]),
enumerator,
CreateFastOptions());

var senders = new List<object?>();
service.StateChanged += (sender, _) => senders.Add(sender);

var hexPath = CreateTempFile();
try
{
await service.UpdateFirmwareAsync(device, hexPath);
}
finally
{
File.Delete(hexPath);
}

Assert.NotEmpty(senders);
Assert.All(senders, sender => Assert.Same(service, sender));
}

[Fact]
public async Task UpdateFirmwareAsync_WhenFlashCrcMatches_VerifiesViaReadCrcAndCompletes()
{
Expand Down Expand Up @@ -1785,7 +1891,7 @@ public async Task UpdateWifiModuleAsync_FiresBridgeActivationCallbackAtWincPromp
[Fact]
public void WifiFlashProgressParser_IgnoresImageBuildPercentAndAdvancesAcrossDeviceFlashPhases()
{
var parser = new FirmwareUpdateService.WifiFlashProgressParser();
var parser = new WifiFlashProgressParser();

// The local image-build phase reaches 100% per region; those must NOT move the bar,
// otherwise the monotonic max latches before the on-device flash even starts.
Expand Down Expand Up @@ -1818,7 +1924,7 @@ public void WifiFlashProgressParser_IgnoresImageBuildPercentAndAdvancesAcrossDev
[Fact]
public void WifiFlashProgressParser_MeasuresFromRangeBase_ForNonZeroVerifyRange()
{
var parser = new FirmwareUpdateService.WifiFlashProgressParser();
var parser = new WifiFlashProgressParser();

// Range base 0x40000 (span 0x40000). Absolute block addresses must be measured relative to
// the base; otherwise the first block saturates the fraction to ~100% immediately.
Expand All @@ -1840,7 +1946,7 @@ public void WifiFlashProgressParser_MeasuresFromRangeBase_ForNonZeroVerifyRange(
[Fact]
public void WifiFlashProgressParser_NeverMovesBackward_WhenAddressesResetBetweenPhases()
{
var parser = new FirmwareUpdateService.WifiFlashProgressParser();
var parser = new WifiFlashProgressParser();

parser.Observe("begin write operation");
var writeEnd = parser.Observe(" 0x060000:[wwwwwwww] 0x068000:[wwwwwwww] 0x070000:[wwwwwwww] 0x078000:[wwwwwwww]");
Expand Down
Loading