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
7 changes: 7 additions & 0 deletions .github/workflows/sonar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ name: SonarCloud
# https://sonarcloud.io/project/settings?id=Ktechen_Trackify -> Analysis Method
# after which every run here is accepted.
#
# !! THAT HAS NOT HAPPENED YET (`sonar.autoscan.enabled=true`), so every run of this workflow to date
# has failed at `Sonar end` and NONE of the /d: settings below have ever taken effect — the coverage
# report is produced and never consumed, and the scope exclusions are inert. What actually scopes the
# published results today is .sonarcloud.properties, the file automatic analysis reads. Keep its
# exclusion list identical to the one here, or a scope change silently does nothing. See R-9 in
# docs/arc42/11-risks-and-technical-debt.md.
#
# Scope note: the scanner for .NET only sees projects that are built between `begin` and `end`. As in
# ci.yml the Uno app (Source/Trackify, five heads) is not buildable on a Linux runner, so the
# analysed scope here is the shared core + CLI + tests.
Expand Down
18 changes: 15 additions & 3 deletions .sonarcloud.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@
#
# The CI scan in .github/workflows/sonar.yml is the real analysis (it builds the projects and uploads
# coverage) and carries its own /d:sonar.* settings; automatic analysis has to be switched off for that
# scan to be accepted, at which point this file is simply ignored. It is kept so the exclusion below
# also applies while automatic analysis is still the active mode.
# scan to be accepted, at which point this file is simply ignored. It is kept because automatic
# analysis is still the active mode (`sonar.autoscan.enabled=true` on the project), so today this file
# — not the workflow — is what actually scopes the published results. Keep the two exclusion lists
# identical; anything only in the workflow has no effect until the mode is switched.
#
# docs/ is the vendored `lego-ble-wireless-protocol-docs` submodule: third-party LEGO protocol
# documentation (HTML/JS) that we neither own nor maintain.
sonar.exclusions=docs/**
#
# Source/Trackify/**/*.cs is the Uno app. Automatic analysis never builds, so the C# analyser sees it
# without a compilation: the CommunityToolkit.Mvvm source generator has not run and the WinUI/Uno
# references are unresolved. Every member that reads an [ObservableProperty]-generated property, and
# every IValueConverter implementation, therefore looks like it uses no instance state — 57 bogus
# S2325 "make it static" findings (uncompilable if applied), plus S8970 "nullable warnings are
# disabled here" although Directory.Build.props sets <Nullable>enable</Nullable>. The app is not
# analysable in either mode — the CI scan can't build its five heads on a Linux runner either, and
# excludes the same path — so exclude it rather than leave unfixable findings open.
# Only the .cs is excluded: the WasmScripts JS and any XML still get analysed.
sonar.exclusions=docs/**,Source/Trackify/**/*.cs
6 changes: 4 additions & 2 deletions Source/Trackify.Application/Lego/LwpAddressingMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public static string FormatMacAddress(ulong address)

/// <summary>Parses "AA:BB:CC:DD:EE:FF" (or '-' separated) back into a 48-bit address.</summary>
public static ulong ParseMacAddress(string mac)
// Explicit separator array: Split(':', '-') also binds to the (char, int count) overload.
=> mac.Split([':', '-'])
// Separator array *and* explicit options on purpose: Split(':', '-') binds to the
// (char, int count) overload instead ('-' converts to int), and only the non-params
// (char[], StringSplitOptions) overload takes the array without S3878 calling it redundant.
=> mac.Split([':', '-'], StringSplitOptions.None)
.Aggregate<string?, ulong>(0, (current, part) => (current << 8) | Convert.ToByte(part, 16));
}
34 changes: 34 additions & 0 deletions Test/Trackify.Tests/Application/LwpAddressingMappingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Trackify.Application.Lego;

namespace Trackify.Tests.Application;

public class LwpAddressingMappingTests
{
[Theory]
[InlineData("90:84:2B:4E:5B:96")]
[InlineData("90-84-2B-4E-5B-96")]
public void Parses_both_separators_to_the_same_address(string mac)
=> Assert.Equal(0x90842B4E5B96UL, LwpAddressingMapping.ParseMacAddress(mac));

// Guards the deliberate Split([':', '-'], StringSplitOptions.None) overload choice: the shorter
// Split(':', '-') binds to (char separator, int count) instead — '-' converts to int 45 — which
// silently splits on ':' only and leaves the '-' form as one unparsable 17-character token.
[Fact]
public void Round_trips_through_the_formatter()
{
const ulong address = 0x0011AAFF7788UL;
var formatted = LwpAddressingMapping.FormatMacAddress(address);

Assert.Equal("00:11:AA:FF:77:88", formatted);
Assert.Equal(address, LwpAddressingMapping.ParseMacAddress(formatted));
Assert.Equal(address, LwpAddressingMapping.ParseMacAddress(formatted.Replace(':', '-')));
}

[Fact]
public void Maps_the_rgb_led_port_per_hub_model()
{
Assert.Equal((byte)50, LwpAddressingMapping.RgbLedPortFor(HubType.PoweredUpHub));
Assert.Equal((byte)17, LwpAddressingMapping.RgbLedPortFor(HubType.DuploTrainHub));
Assert.Null(LwpAddressingMapping.RgbLedPortFor(HubType.WeDo2SmartHub));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ namespace Trackify.Tests.Architecture;
/// </summary>
public class LayerTrainDependencyTests
{
private const string Domain = "Trackify.Domain";
// No "Trackify.Domain" constant: Domain is the innermost layer, so every other layer is allowed
// to depend on it and no assertion below ever names it.
private const string Application = "Trackify.Application";
private const string Infrastructure = "Trackify.Infrastructure";
private const string Cli = "Trackify.Cli";
Expand Down
2 changes: 1 addition & 1 deletion Test/Trackify.Tests/Domain/SpeedFunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public void TryCompile_accepts_a_valid_formula()
{
Assert.True(SpeedFunction.TryCompile("1-(1-x)^2", out var fn));
Assert.NotNull(fn);
Assert.Equal(0.0, fn!(0), 3);
Assert.Equal(0.0, fn(0), 3);
Assert.Equal(1.0, fn(1), 3);
}

Expand Down
25 changes: 25 additions & 0 deletions docs/arc42/07-deployment-view.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,28 @@ dotnet run --project Source/Trackify/Trackify.csproj -f net10.0-desktop # laun
Buildable heads outside macOS: `net10.0-android`, `net10.0-desktop`, `net10.0-browserwasm`,
`net10.0-windows10.0.19041.0`. Real BLE behaviour is confirmed on a device and on a Pi — it cannot be
exercised in CI.

**Checking SonarCloud's C# rules locally.** The scan needs a `SONAR_TOKEN` and only runs in CI, so a
fix to a Sonar finding is otherwise unverifiable before pushing — and while
[R-9](11-risks-and-technical-debt.md) stands, the published findings are not trustworthy anyway. The
same rules can be run offline by injecting the analyzer package without touching any repo file: write

```xml
<Project><ItemGroup>
<PackageReference Include="SonarAnalyzer.CSharp" VersionOverride="10.31.0.145097" PrivateAssets="all" />
</ItemGroup></Project>
```

to a scratch file and pass it as `CustomAfterMicrosoftCommonProps`:

```bash
dotnet build Test/Trackify.Tests/Trackify.Tests.csproj -t:Rebuild \
-p:CustomAfterMicrosoftCommonProps=<path> \
-p:TreatWarningsAsErrors=false -p:EnforceCodeStyleInBuild=false
```

Building the **test** project covers the whole analysed scope, since it references the core libs and
the CLI. `VersionOverride` bypasses Central Package Management; the two overrides stop warnings-as-errors
from aborting the build before every rule has reported. Keep the version in step with the
`sonar.cs.analyzer.dotnet.pluginVersion` the server reports. Non-C# rules (docker, XML, YAML,
JavaScript) have no local equivalent — reason about those from the rule description.
28 changes: 28 additions & 0 deletions docs/arc42/11-risks-and-technical-debt.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,34 @@ have no hub address recorded yet will be merged into one local row.
- **Realistic trigger:** trains created in the app before discovery has run, then synced from a Pi.
- **Fix if it bites:** drop the name fallback and require a hub identity for matching.

### R-9: `sonar.yml` has never published an analysis (high impact, needs a one-off admin action)

The project still has **Automatic Analysis** enabled server-side (`sonar.autoscan.enabled=true`), and
the two modes are mutually exclusive: every `sonarscanner end` step is rejected with *"You are running
CI analysis while Automatic Analysis is enabled"*, so the workflow has failed on **every** run to date
while the `begin` and test steps pass. Three consequences, all of them easy to misread:

- **Coverage is not measured at all.** The OpenCover report is produced and never consumed, so the
coverage metric and its badge come from nothing.
- **Every `/d:sonar.*` setting in `sonar.yml` is inert**, including the scope exclusions. What actually
scopes the published results is [`.sonarcloud.properties`](../../.sonarcloud.properties), the file
Automatic Analysis reads — which is why the two exclusion lists must be kept identical.
- **The published C# findings are analysed without a compilation.** Source generators have not run and
package references are unresolved, so the results are unreliable in exactly the places that depend on
either — most visibly the Uno app, where every read of a `[ObservableProperty]`-generated property
and every `IValueConverter` member drew a bogus `S2325` "make it static", and `S8970` claimed nullable
warnings were disabled although `Directory.Build.props` sets `<Nullable>enable</Nullable>`. Applying
those "fixes" would not compile. The app is excluded for this reason.

- **Mitigation today:** C# rules are reproduced locally against the same analyzer version by injecting
`SonarAnalyzer.CSharp` through `CustomAfterMicrosoftCommonProps` —
see [§7.6](07-deployment-view.md#76-build-and-delivery-pipeline).
- **Fix:** a project admin turns Automatic Analysis off once, at
[Project Settings → Analysis Method](https://sonarcloud.io/project/settings?id=Ktechen_Trackify).
Nothing in this repository can do it — it is a server-side setting, and a `sonar-project.properties`
file (sometimes suggested) is rejected outright by the scanner for .NET. After the switch the CI scan
is accepted, coverage starts reporting, and the workflow's own exclusions take over.

## 11.2 Technical debt

| # | Debt | Where | Cost of leaving it |
Expand Down
Loading