tests/
├── DIF.Core.Tests/ 28 unit tests
│ ├── DiagnosticCodesTaskTests.cs
│ ├── GovernorGainTaskTests.cs
│ ├── PidDataTests.cs
│ ├── TaskResultTests.cs
│ └── VsgTaskTests.cs
│
├── DIF.Integration.Tests/ 14 integration tests
│ ├── EcmSimulatorTests.cs
│ ├── MessageRecorderPlayerTests.cs
│ └── SimulatorTransportTests.cs
│
└── DIF.Transport.Tests/ (empty - for future RP1210 tests)
Total: 42 passing tests (28 unit + 14 integration)
# Run all tests
dotnet test /mnt/d/source/repos/ddec/dif-maui
# Run specific test project
dotnet test /mnt/d/source/repos/ddec/dif-maui/tests/DIF.Core.Tests
dotnet test /mnt/d/source/repos/ddec/dif-maui/tests/DIF.Integration.Tests
# Run with verbose output
dotnet test /mnt/d/source/repos/ddec/dif-maui --verbosity normal
# Run specific test class
dotnet test /mnt/d/source/repos/ddec/dif-maui --filter "FullyQualifiedName~GovernorGainTaskTests"
# Run specific test method
dotnet test /mnt/d/source/repos/ddec/dif-maui --filter "FullyQualifiedName~ProcessResponse_SetGain_AcknowledgesSuccess"
# Run all tests
dotnet test D:\source\repos\ddec\dif-maui
# Run from Visual Studio
# Test Explorer → Run All
Note: The DIF.Maui project requires the maui-windows workload and will be skipped during test runs on Linux.
PidDataTests - Data model conversion pipeline
TaskResultTests - Task result factory methods
TaskResult.Succeeded() propertiesTaskResult.TimedOut() propertiesTaskResult.Cancelled() propertiesTaskResult.Failed() propertiesStatus140.GetMessage() for all status codesStatus195.GetMessage() for all status codesTaskStatusCodes constant valuesGovernorGainTaskTests - Governor gain operations
VsgTaskTests - Variable Speed Governor operations
DiagnosticCodesTaskTests - Fault code operations
SimulatorTransportTests - Transport layer
EcmSimulatorTests - Simulator behavior
MessageRecorderPlayerTests - Recording/playback
The simulator runs in-process without any hardware. It implements IEcmTransport so it can be used anywhere a real transport would be.
// Create simulator with custom state
var state = new EcmState
{
Rpm = 1200f,
CoolantTempOut = 190f,
OilPressure = 55f,
ActiveRating = 1
};
var simulator = new EcmSimulator(state);
var transport = new SimulatorTransport();
await transport.ConnectWithSimulatorAsync(simulator);
// Now use transport like any IEcmTransport
var result = await transport.ReadMessageAsync();
Simulate timeout (hardware not responding):
simulator.State.SimulateTimeouts = true;
Simulate slow response:
simulator.State.ResponseDelayMs = 500; // 500ms delay
Reject password:
simulator.State.AcceptPassword = false;
Set fault codes:
simulator.State.ActiveCodes.Add(new FaultCode
{
SidOrPid = 100,
Fmi = 3,
IsActive = true,
Description = "Oil pressure low"
});
Engine stopped (no broadcasts):
simulator.State.IsRunning = false;
The simulator maps EDMS names to state properties:
| EDMS Name | Property | Units |
|---|---|---|
| EC1_RPM | Rpm | RPM |
| EC1_BVOLT | BatteryVoltage | Volts |
| EC1_CLTO | CoolantTempOut | Deg F |
| EC1_GOP | OilPressure | PSI |
| EC1_GOT | OilTemperature | Deg F |
| EC1_FUEL_RATE | FuelRate | gal/hr |
| EC1_IMP | IntakeManifoldPressure | PSI |
| EC1_IMT | IntakeManifoldTemp | Deg F |
| EC1_AAT | AmbientAirTemp | Deg F |
| EC1_ATI | AirTempInlet | Deg F |
| EC1_BARO | BarometricPressure | PSI |
| EC1_THRPC | ThrottlePosition | % |
| EC1_PEL | PercentEngineLoad | % |
| EC1_PW | PulseWidth | ms |
| EC1_FTI | FuelTempInlet | Deg F |
| EC1_CLEVEL | CoolantLevel | % |
| EC1_ENG_HRS | EngineHours | hours |
| EC1_TOTAL_FUEL | TotalFuel | gallons |
| EC1_TUR_SPEED | TurboSpeed | RPM |
Use the DIF.MessageRecorder console tool in the SC lab:
# Build the recorder
dotnet build tools/DIF.MessageRecorder
# Run with RP1210 transport (lab only)
DIF.MessageRecorder --driver MCOM32NT --output session_2025_01.bin
// Replay a recorded session
var player = new MessagePlayer("session_2025_01.bin");
await player.ConnectAsync(new TransportConfig(TransportType.Simulator));
// Use player as IEcmTransport - messages replay with original timing
var result = await player.ReadMessageAsync();
Each recorded message:
[timestamp_ms : uint32] Milliseconds since recording start
[direction : byte] 0=inbound (ECM→App), 1=outbound (App→ECM)
[length : uint16] Number of data bytes
[data : byte[]] Raw message bytes
using DIF.Core.Tasks;
using DIF.Transport;
using Xunit;
public class MyNewTaskTests
{
[Fact]
public async Task SendRequest_BuildsCorrectMessage()
{
// Arrange
var task = new MyTask(parameters);
var transport = new SimulatorTransport();
await transport.ConnectAsync(new TransportConfig(TransportType.Simulator));
// Act
bool sent = await task.SendRequestAsync(transport, J1587Constants.MidMaster);
// Assert
Assert.True(sent);
}
[Fact]
public void ProcessResponse_HandlesAcknowledgment()
{
// Arrange
var task = new MyTask(parameters);
byte[] response = { /* expected response bytes */ };
// Act
bool handled = task.ProcessResponse(pid, J1587Constants.MidMaster, response);
// Assert
Assert.True(handled);
Assert.True(task.IsComplete);
Assert.NotNull(task.Result);
Assert.True(task.Result.Success);
}
}
using DIF.Simulator;
using DIF.Transport;
using Xunit;
public class MyIntegrationTests
{
[Fact]
public async Task FullRoundTrip_WithSimulator()
{
// Arrange
var state = new EcmState { /* configure state */ };
var simulator = new EcmSimulator(state);
var transport = new SimulatorTransport();
await transport.ConnectWithSimulatorAsync(simulator);
// Act - send request and read response
await transport.SendRawMessageAsync(requestBytes);
var result = await transport.ReadMessageAsync();
// Assert
Assert.True(result.Success);
Assert.Equal(expectedMid, result.Mid);
// Verify response data
}
}
[MethodUnderTest]_[Condition]_[ExpectedResult]
Examples:
ProcessResponse_SetGain_AcknowledgesSuccessSendRequest_WithInvalidPid_ReturnsFalseExecuteSequence_PasswordRejected_ReturnsFailure