Darp.Fairino.Testing
0.8.0
See the version list below for details.
dotnet add package Darp.Fairino.Testing --version 0.8.0
NuGet\Install-Package Darp.Fairino.Testing -Version 0.8.0
<PackageReference Include="Darp.Fairino.Testing" Version="0.8.0" />
<PackageVersion Include="Darp.Fairino.Testing" Version="0.8.0" />
<PackageReference Include="Darp.Fairino.Testing" />
paket add Darp.Fairino.Testing --version 0.8.0
#r "nuget: Darp.Fairino.Testing, 0.8.0"
#:package Darp.Fairino.Testing@0.8.0
#addin nuget:?package=Darp.Fairino.Testing&version=0.8.0
#tool nuget:?package=Darp.Fairino.Testing&version=0.8.0
Darp.Fairino.Testing
An in-memory Fairino robot controller for testing code written against
Darp.Fairino. A real FairinoRobot connects to a
FakeFairinoController instead of a network address, so the code under test runs unchanged.
The fake knows the channels of a robot, not its commands. It records what arrives on each channel, and the test says what comes back. It is no simulator: a command changes nothing, and there are no joints, no kinematics and no I/O behind it.
Install
dotnet add package Darp.Fairino.Testing
The package depends on exactly the Darp.Fairino version it was built with. Update both together.
Usage
using Darp.Fairino;
using Darp.Fairino.Testing;
var controller = new FakeFairinoController();
await using FairinoRobot robot = await controller.ConnectRobotAsync();
await robot.SetDOAsync(id: 1, status: 1, smooth: 0, block: 1);
controller.Commands.ShouldHaveSingleItem().ShouldBe(new FakeCommand("SetDO", [1, 1, 0, 1]));
Any command is recorded and answered with success and no values. That needs no setup, also for a command the
SDK does not have and that you send with robot.SendCommandAsync(...).
ConnectRobotAsync returns with Commands empty: the two commands of its connect are not listed. CreateRobot
returns a robot that is not connected yet; the commands of a connect the test does itself are listed. Both take an
optional logger, a callback that configures the robot and FakeRecoveryTimings, which set how the robot recovers
a lost state connection. With dependency injection, register the fake after the robot:
services.AddFairinoRobot().AddFakeFairinoController(controller);
Commands that return a value
A command that returns a value has to be told what comes back. Unscripted, its call fails with an SdkError
that names the method and says so:
controller.AnswerNext("GetWeldingCurrent", FakeResponse.Values(182.5)); // once; the values of the controller
controller.AnswerNext("GetInverseKin", joints); // once; the result itself
controller.Handle("GetInverseKin", _ => joints); // every time
controller.Handle("GetSysVarValue", command => FakeResponse.Values(command.Arguments[0]));
controller.FailNext("MoveC", FairinoErrorCode.TheRobotIsNotEnabled); // once
The answers and failures queued for a method are used first, in the order they were queued. Then its handler answers, and without one the command succeeds with no values.
| Answer | What the call returns |
|---|---|
FakeResponse.Values(values…) |
What its decoder reads from the values, as from those of a controller. |
A result, FakeResponse.Result(…) |
The result as it is. The call must read that type. |
FakeResponse.Error(code, message) |
A RobotError with the code and the message the SDK creates for it. |
FakeResponse.Failure(message) |
An SdkError with the message. |
Values are in the units of the controller, so joints are six numbers in degrees:
FakeResponse.Values([.. joints.ToControllerJointsInDegrees()]). A result is returned without decoding, in the
units of the SDK.
The SDK asks the controller for kinematics before some moves: MoveLAsync(pose, …) sends GetInverseKin and
MoveJAsync(joints, tool, user) sends GetForwardKin. Script those, or call the overloads that take both the
joints and the pose.
An unscripted command is told from one without a result by the decoder of its call, which rejects an answer
without values. A raw SendCommandAsync that returns the values as they are accepts it and returns no values.
A handler that takes the cancellation token of the call can wait, which keeps a move in progress:
var finished = new TaskCompletionSource();
controller.Handle("MoveJ", async (_, cancellationToken) =>
{
await finished.Task.WaitAsync(cancellationToken);
return FakeResponse.Values();
});
Task<Result<Success, FairinoError>> move = robot.MoveJAsync(joints, pose, tool: 0, user: 0); // pending
finished.SetResult();
await move;
The call also ends when it is cancelled, the robot disconnects or the controller becomes unreachable. A handler
that throws or returns null fails its call with an SdkError that names the method.
What the robot sent
controller.Commands lists every XML-RPC call, oldest first, with the arguments as the SDK passed them: joints
are a JointPos in radians, a pose is a Euclidean3d in metres, a whole number is an integer and a
floating-point number a double. MoveL, MoveC and Circle have one argument, an RpcValue.Flat(...) list.
The other channels are recorded the same way:
| Record | An entry |
|---|---|
UdpFrames |
A UDP frame: its Text as it was sent, and its Count, CommandId and Content. |
FileTransfers |
An upload or a download: Kind, the Content, and Succeeded. |
StateConnections |
A realtime-state connection: the configured Fields and Period, IsStarted, IsOpen. |
Links |
The link of one ConnectAsync: its Ip, IsDisposed and WasUsedAfterDisposal. |
A state connection and a link stay listed after they were closed, so a recovery is one more entry in
StateConnections; StateConnectionAttempts also counts the attempts that failed. ClearRecords() empties
Commands, UdpFrames and FileTransfers.
controller.Records lists the commands, UDP frames and file transfers together, in the order they arrived. A
FakeFileTransfer compares by value like a FakeCommand, so a test can say that a file went between two
commands:
await robot.LuaUploadAsync("pick.lua", "-- pick");
controller.Records.ShouldBe([
new FakeCommand("FileUpload", [0, "pick.lua"]),
new FakeFileTransfer(FakeFileTransferKind.Upload, "-- pick"u8),
new FakeCommand("LuaUpLoadUpdate", ["pick.lua"]),
]);
An upload is recorded with its bytes. The file channel carries no name: the name of a file is an argument of
the FileUpload or FileDownload command in Commands. A download delivers the bytes the test queued, and
fails with an SdkError when none are queued:
controller.QueueDownload("-- pick"u8);
await robot.LuaDownloadAsync("pick.lua", directory);
Realtime states
A state is what the test sets. The fake sends the states a robot configured once per configured period, and a field that was never set as zeros.
var time = new FakeTimeProvider();
var controller = new FakeFairinoController(time);
await using FairinoRobot robot = await controller.ConnectRobotAsync(
new FairinoRobotStateConfiguration([FairinoRobotStates.MotionDone], TimeSpan.FromMilliseconds(8))
);
Task<Result<Success, FairinoError>> done = robot.WaitForMotionDoneAsync();
controller.SetState(FairinoRobotStates.MotionDone, 1);
await controller.PublishStateAsync();
await done;
SetState takes the value as the SDK returns it and sends it as the controller does, so a JointPos goes out in
degrees. For anything that cannot express, such as a field with a parser of your own, SetStateBytes(field, bytes) takes the bytes on the
wire. A field you define yourself is configured and sent like a field of FairinoRobotStates.
The stream is paced by the clock of the controller, which the robots it creates share; pass a FakeTimeProvider
to control it. PublishState() sends a frame now without advancing the clock. PublishStateAsync() also waits
until every robot has processed the frame: by then it is the robot's last state, and every wait that was started
before, such as GetNextStateAsync or WaitForMotionDoneAsync, was offered it. The observers of
WhenStateReceived may see it a moment later.
Faults
controller.IsReachable = false; // commands fail, the recovery keeps retrying
controller.DropStateConnection(); // the robot recovers the connection
controller.RefuseStateConnections("The port is closed."); // only that fails, until AcceptStateConnections()
controller.FailNextUdpSend("The socket is gone."); // once; the frame does not arrive
controller.FailNextFileTransfer("The transfer broke."); // once; listed in FileTransfers as failed
using (controller.HoldUdpSends()) { … } // UDP sends stay pending until the hold is disposed
The faults of a channel fail with an SdkError with the message given. While the controller is not reachable
nothing reaches it: nothing is recorded, no queued answer, download or fault is used, and a UDP send succeeds
into the void.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Aardvark.Base (>= 5.3.27)
- Darp.Fairino (= 0.8.0)
- Darp.Results (>= 1.9.0)
- Dunet (>= 1.16.2)
- Microsoft.Extensions.DependencyInjection (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Http (>= 10.0.8)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.8)
- Serilog (>= 4.4.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.