Skip to content

Trace a C# repo

TL;DR

Read the repo's .cs files with csharp-file and its xUnit tests with xunit. A file links to its design element with an Implements: line in its first type's /// comment. A test names the test case it runs, or what it verifies, at the start of its DisplayName, as a string literal. Then run qmstrace check, and read a test run's verdicts from the TRX file dotnet test --logger trx writes with qmstrace results.

Steps

  1. Declare the two item types in the repo's qmstrace.toml — components from the source files, automated tests from the test files. This is the fixture project's C# agent:

    # This product repo's TRACE CONFIGURATION — a C# agent (ADR-0004). It holds code and automated
    # tests only: the requirements, the design and the test cases are in the project's ledger.
    
    version = 1
    ledger  = "../ledger"
    
    [schema]                            # by path, as repos side by side read it
    path = "../ledger/qmstrace.schema.toml"
    
    [items.component]                   # ID = the file's path
    extractor = "csharp-file"
    files     = ["src/**/*.cs"]
    
    [items.automated_test]              # ID = path::Class::Method; the probes are not traced
    extractor = "xunit"
    files     = ["tests/Agent.Tests/**/*Tests.cs"]
    

    The type the tests are read into must carry exactly one link in the project's schema — verifies — because a DisplayName names an ID, not a link. A schema that gives it two is refused, and says so. qmstrace init on a repo with no configuration still writes the Python starter (#38), and a pytest.ini that registers the verifies marker: edit [items] as above, and delete the pytest.ini.

  2. Mark each component in its first type's leading doc comment. A field line, with XML tags around it if you like; the same line in any other comment is not read, and is reported:

    namespace Agent;
    
    /// <summary>Refuses a call that carries no valid key, before any handler sees it.</summary>
    /// <remarks>
    /// Implements: DSN-SVC-01
    /// </remarks>
    public static class KeyCheck
    {
        /// <summary>Whether a call carrying <paramref name="key"/> may go on.</summary>
        public static bool Admits(string? key, string expected) =>
            !string.IsNullOrEmpty(key) && key == expected;
    }
    
  3. Start each test's DisplayName with its ID. In a project with a Test Specification, that is the test case the test runs; several tests may run one. A [Theory] is one automated test, its rows its runs:

    using FluentAssertions;
    
    namespace Agent.Tests.Unit;
    
    /// <summary>The agent's side of the key check.</summary>
    public class KeyCheckTests
    {
        [Fact(DisplayName = "TC-SVC-01.1 a call without a key is refused")]
        public void Admits_NoKey_IsFalse()
        {
            KeyCheck.Admits(null, "the-key").Should().BeFalse();
        }
    
        [Theory(DisplayName = "TC-SVC-01.2 the key check decides before any handler")]
        [InlineData("the-key", true)]
        [InlineData("a-wrong-key", false)]
        public void Admits_AnyKey_DecidesAlone(string key, bool admitted)
        {
            KeyCheck.Admits(key, "the-key").Should().Be(admitted);
        }
    }
    

    The ID ends where an ID in prose ends: TC-SVC-01.1/O03 … and TC-SVC-01.1: … both link TC-SVC-01.1. A letter, digit, hyphen or dotted number straight after it makes it no ID — TC-SVC-01.1a … is reported as V14. The level comes from the folder the file sits in — Unit/ is the level unit.

  4. Check.

    qmstrace check --strict
    
  5. Read a test run's verdicts. Write a TRX file, then read it as a separate command, so a failing run does not skip it:

    dotnet test tests/Agent.Tests --logger "trx;LogFileName=report.trx" --results-directory .
    qmstrace results report.trx
    

    A run is matched by its class and method, never by its DisplayName, so rewording a test keeps its results and renaming its method does not. Each row of a [Theory] is a run of one automated test. Nothing of the file's build paths, machine or user names, or output is shown. In this run one [Fact] was skipped and one [Theory] row failed:

    report: report.trx
      3 XML entries · 3 reported runs (distinct full identities)
      matched: 3 runs of 2 automated tests · scanned: 2 automated tests · unmatched: 0 run identities
      matched against the current working tree — report commit NOT VERIFIED
      a partial parameter set does not prove every possible variant ran
      baselines: judged from reachable local tags — remote-tag completeness NOT VERIFIED; qmstrace never fetches
      baselines: no matching reachable local tag — nothing frozen locally
      change requests: ../ledger/change-requests (1 readable; 0 malformed) — current working tree, not pinned
    
    requirements — verdicts along verifies links, from automated tests directly or through test cases:
      REQ-SVC-01  skip      via TC-SVC-01.1 · 1 automated tests · 1 runs — A call carries a key · requirement, in sibling ledger — not pinned · ../ledger/docs/req/service-srs.md:5
        via TC-SVC-01.1  skip · 1 automated tests · verifies REQ-SVC-01 — A call without a valid key is refused
          skip      tests/Agent.Tests/Unit/KeyCheckTests.cs::KeyCheckTests::Admits_NoKey_IsFalse · 1 runs
            skip: TC-SVC-01.1 a call without a key is refused (NotExecuted)
              NotExecuted: the key store is offline
    
    test cases that reach no requirement — no requirement verdict inferred:
      TC-SVC-01.2  fail · 1 automated tests · verifies DSN-SVC-01 — The key check runs before any handler
        fail      tests/Agent.Tests/Unit/KeyCheckTests.cs::KeyCheckTests::Admits_AnyKey_DecidesAlone · 2 runs
          fail: TC-SVC-01.2 the key check decides before any handler(key: "a-wrong-key", admitted: True) (Failed)
            Failed: Expected KeyCheck.Admits(key, "the-key") to be True, but found False.
    
    result: 2 matched automated tests · 3 matched runs · 0 unmatched run identities · exit 0 (informational) — report commit NOT VERIFIED; this repo's tests only
    

    With several test projects, write one TRX per project, each with its own name, and give results all of them. They are read as one; a run in two files keeps its worst verdict:

    dotnet test tests/Agent.Tests --logger "trx;LogFileName=unit.trx" --results-directory TestResults
    dotnet test tests/Agent.IntegrationTests --logger "trx;LogFileName=integration.trx" --results-directory TestResults
    qmstrace results TestResults/*.trx
    

    Here, two runs of the agent's tests read together:

    reports: 2
      TestResults/first.trx · 3 XML entries
      TestResults/second.trx · 3 XML entries
      6 XML entries · 4 reported runs (distinct full identities)
      matched: 4 runs of 2 automated tests · scanned: 2 automated tests · unmatched: 0 run identities
      matched against the current working tree — report commit NOT VERIFIED
      a partial parameter set does not prove every possible variant ran
      baselines: judged from reachable local tags — remote-tag completeness NOT VERIFIED; qmstrace never fetches
      baselines: no matching reachable local tag — nothing frozen locally
      change requests: ../ledger/change-requests (1 readable; 0 malformed) — current working tree, not pinned
      2 duplicate XML entries combined by full identity; retry attempts are not inferred
    
    requirements — verdicts along verifies links, from automated tests directly or through test cases:
      REQ-SVC-01  skip      via TC-SVC-01.1 · 1 automated tests · 1 runs — A call carries a key · requirement, in sibling ledger — not pinned · ../ledger/docs/req/service-srs.md:5
        via TC-SVC-01.1  skip · 1 automated tests · verifies REQ-SVC-01 — A call without a valid key is refused
          skip      tests/Agent.Tests/Unit/KeyCheckTests.cs::KeyCheckTests::Admits_NoKey_IsFalse · 1 runs
            skip: TC-SVC-01.1 a call without a key is refused (NotExecuted) · reported in TestResults/first.trx, TestResults/second.trx
              NotExecuted: the key store is offline
    
    test cases that reach no requirement — no requirement verdict inferred:
      TC-SVC-01.2  fail · 1 automated tests · verifies DSN-SVC-01 — The key check runs before any handler
        fail      tests/Agent.Tests/Unit/KeyCheckTests.cs::KeyCheckTests::Admits_AnyKey_DecidesAlone · 3 runs
          fail: TC-SVC-01.2 the key check decides before any handler(key: "a-wrong-key", admitted: True) (Failed) · reported in TestResults/second.trx
            Failed: Expected KeyCheck.Admits(key, "the-key") to be True, but found False.
    
    result: 2 matched automated tests · 4 matched runs · 0 unmatched run identities · exit 0 (informational) — report commit NOT VERIFIED; this repo's tests only
    

    One LogFileName for a whole solution overwrites

    Given a solution and a fixed LogFileName, every project writes the same file and only the last one's results remain, without warning. qmstrace cannot see the overwrite; the header's entry count per report is where the shortfall shows.

What is not read

Each of these is reported as V15, never skipped:

Written Why it is not read
DisplayName = Id + "…", $"{Id} …", a constant not a string literal: the tool reads source, it does not evaluate it
[Trait("verifies", "REQ-…")] a trait never reaches a test report, so it cannot carry the trace
[SkippableFact], any attribute derived from FactAttribute not xUnit's own: its base class is in another file
a test inside #if … it may never be built

A test attribute in a comment is a stray: it links nothing, and the gap it leaves names it.