Files
Artemis/CLAUDE.md
T

125 lines
7.2 KiB
Markdown

# Artemis
Artemis is an RGB lighting app: C# on .NET 10, Avalonia for the UI, SkiaSharp for drawing and RGB.NET for talking to devices.
The solution is `src/Artemis.sln`. `Artemis.Core` holds profiles, layers, brushes, effects and the render pipeline, the
`Artemis.UI.*` projects are the app itself.
## Building and testing
```bash
dotnet build src/Artemis.sln
dotnet test --project src/Artemis.Core.Tests
dotnet test --project src/Artemis.UI.Shared.Tests
```
The tests use xUnit v3 on Microsoft.Testing.Platform (switched on by `global.json` in the repo root), so use
`dotnet test --project ...` as shown above. The whole suite runs in a few seconds. To run part of it, pass a filter, for
example `dotnet test --project src/Artemis.Core.Tests --filter-class "*TimelineTests"`.
Pull requests to `master` run the tests on Windows and Linux (`.github/workflows/pr.yml`).
## Rules
- Changes to `Artemis.Core` come with tests in `Artemis.Core.Tests`: a test that fails without the change and passes
with it for bug fixes, tests covering the new behaviour for features.
- `dotnet test --project src/Artemis.Core.Tests` must pass before you consider the work done.
- Tests run one at a time on purpose (see `TestAssembly.cs`), Artemis.Core keeps brush and effect registrations in
static state that tests would otherwise share.
## The render harness
Most tests render real profiles through the same pipeline the app uses and check the colors that end up on each LED.
- `Infrastructure/Rendering/RenderHarness.cs` sets up fake devices from RGB.NET layout files, creates profiles, layers,
folders and effects, and renders frames. No services or plugins are started and the harness controls time: a frame is
only rendered when a test asks for one, after moving time forward by a given amount.
- `Infrastructure/Rendering/RenderTestBase.cs` is the base class for tests on a single ANSI keyboard. It gives you
`Harness`, `Keyboard`, `Led(LedId)`, `Leds(...)`, `TopLetterRow`, and `Render(profile)` / `RenderTimeline(profile, ...)`
which also take care of saving the render output described below. Tests that need other devices or render scales
create their own `RenderHarness` (see `Rendering/SmallLedTests.cs`).
- The test brushes and effects under `Infrastructure/Plugins` are simple, predictable stand-ins for real plugins.
- Compare colors with `ShouldBeColor` (`Infrastructure/ColorAssertions.cs`), it allows a small difference per channel
(2 by default) since drawing and sampling can be off by a little.
A typical test:
```csharp
public class LayerBrushTests : RenderTestBase
{
[Fact]
public void SolidBrush_DefaultsToItsDefaultValue()
{
Profile profile = Harness.CreateProfile();
Harness.AddLayer<SolidColorTestBrush>(profile.GetRootFolder(), [Led(LedId.Keyboard_A)]);
using RenderFrame frame = Render(profile);
frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red);
}
}
```
### Render output
When a render test fails, what it rendered is written to
`src/Artemis.Core.Tests/render-output/<namespace>/<class>/<test>/`, for example
`render-output/Profiles/LayerBrushTests/SolidBrush_DefaultsToItsDefaultValue/`. Passing tests write nothing, unless you
set the environment variable `ARTEMIS_RENDER_OUTPUT=always`. The folder is ignored by git.
A single frame produces:
- `canvas.png`: the raw canvas the profile was drawn on
- `leds.png`: every LED drawn at its position, filled with the color it received
- `leds.json`: the color of every LED by LED ID, as hex (`#RRGGBB`, or `#AARRGGBB` when not fully opaque)
A timeline produces `canvas-strip.png`, `leds-strip.png` (all frames stacked top to bottom) and `timeline.json`.
When working out why a render test failed, read the JSON first: it holds the exact color each LED received, which is
more reliable than judging colors from the PNGs. Use the PNGs to get an idea of shapes and positions.
In CI the same files are attached to the test report, which is uploaded as the `test-results-<OS>` artifact of the run.
## Testing without rendering
Not everything needs the render harness. These pieces live under `src/Artemis.Core.Tests/Infrastructure`:
- `Plugins/TestPlugin.cs` hosts test plugin features (brushes, effects, modules, node providers) without plugin
loading. `AddFeature` enables a feature the way Artemis does and disposing the plugin disables them again, which also
removes their registrations from Core's static stores.
- `Plugins/Modules/TestModule.cs` is a module with a `TestDataModel`: numbers, text, a color, an enum, a nested data
model, lists and data model events. Adding it to a `TestPlugin` registers the data model, so data model paths can find
it both directly and when loaded from storage. See `DataModels/DataModelPathTests.cs`.
- `Plugins/Nodes/TestNodeProvider.cs` registers the test nodes (`TestBooleanNode`, `TestNumericNode`), so scripts using
them can be saved and loaded. Create nodes with `AddNode<T>(script)` rather than `new`, otherwise they can't be loaded
again. See `VisualScripting/NodeScriptTests.cs`.
- `Services/ServiceHarness.cs` registers the Artemis.Core services like the app does, backed by a new database in a
temporary folder and logging to the test output. `Resolve<T>()` also works for protected services such as
`ISettingsService`. Anything working on `Constants.DataFolder` or `Constants.PluginsFolder` directly (loading plugins,
the web server) still points at your real Artemis folders, so don't call it. See `Services/ServiceHarnessTests.cs`.
## Code coverage
```bash
dotnet test --project src/Artemis.Core.Tests -- --coverage --coverage-output-format cobertura --coverage-settings "$PWD/src/Artemis.Core.Tests/coverage.settings.xml"
```
`coverage.settings.xml` leaves out the database migrations Entity Framework generates in `Artemis.Storage/Migrations`.
The path to it has to be absolute, with a relative path the test run fails without saying why. `$PWD` works in bash and
PowerShell when you run the command from the repo root.
## Profile editor tests
`src/Artemis.UI.Shared.Tests` covers the profile editor logic in `Artemis.UI.Shared/Services/ProfileEditor`: the
`ProfileEditorService` and its undo/redo commands. Changes there come with tests in this project, and it must pass too.
- It references `Artemis.Core.Tests` to reuse the render harness, test plugins and fake devices, and has the same
setup (xUnit v3, tests one at a time).
- `Infrastructure/ProfileEditorTestBase.cs` builds on `RenderTestBase` and adds a `Profile`, a `Service` backed by the
simple test services in `Infrastructure/Services` (they record what the editor asks of them), `StartEditing()`,
`Undo()`/`Redo()` and `Snapshot()`. `Snapshot()` returns the profile as Artemis would save it, compare the snapshot
from before a command with the one after undoing it to check that undo restored everything.
- No Avalonia app or dispatcher is needed: under a test runner ReactiveUI runs commands right away on the calling
thread. One thing to know: a command that throws inside `ExecuteCommand` is also passed to ReactiveUI's
`RxApp.DefaultExceptionHandler`, which ends the test run unless the test replaces it before editing starts (see
`ProfileEditorHistoryTests.ExecuteCommand_ThatFails_DoesNotEndUpInTheHistory`).