Skip to content

feat(cli): add xping doctor — diagnose why a report is empty, wrong, or missing #138

Description

@xping-admin

Problem

Setup is where people fall off, and every failure mode looks identical from the outside: dotnet xping report says nothing useful and the developer has no way to find out why.

Today the only diagnostic is ReportCommand.ReportUnavailable, which prints one of two messages — "No Xping local store found" or "No runs recorded yet in <path>" — both followed by "Run your tests once with the Xping SDK installed, then try again." That advice is correct for exactly one of the causes below and useless for the rest:

  • The SDK package is installed but tracking was never turned on (no [assembly: TestFramework(...)], no [XpingTrack], no XpingTestBase), so runs were never recorded.
  • NUnit/MSTest wiring is present but the teardown is missing, so sessions are collected and never flushed (documented at docs/troubleshooting/common-issues.md §4).
  • XPING_ENABLED=false or XPING_MODE=Disabled in the shell that ran the tests.
  • The store resolved to the user-profile fallback instead of the repo root, because the checkout was read-only (LocalStorePathResolver.ResolveGetProfileFallback). The developer is looking in .xping/ and the data is somewhere else entirely.
  • XPING_LOCAL_STORE is set and points elsewhere.
  • Runs exist but there are fewer than MinimumSessionsToReport (5), so the window is real and the findings list is legitimately empty.
  • Runs exist but all of them are older than DefaultWindowDays (14), so the default window is empty while the store is not.
  • The store holds several assemblies and auto-scoping picked the newest, so the suite the developer cares about isn't in the report.
  • The recorded runs came from an SDK old enough that the finding they're looking for cannot be produced — e.g. retry attempt numbers were not recorded before xUnit adapter never records a retry attempt number, so RetryMasked is silent #115/MSTest adapter never records a retry attempt number, so RetryMasked is silent #116, so RetryMasked is silent no matter how flaky the suite is.

Every one of these is mechanically detectable. None of them is currently reported.

Proposed Solution

Add xping doctor — a read-only command that inspects the store, the environment and (best-effort) the test projects, and prints one line per check with the fix inline.

$ dotnet xping doctor

Store
  ok    .xping resolved from repository root (/Users/x/repo/.xping)
  ok    writable
  ok    47 runs · 3.1 MB · newest 4 minutes ago
  warn  9 runs evicted by retention in the last 7 days (MaxRuns 50)
        A window of 20 needs runs to survive; raise MaxRuns or run `xping report --runs 12`.

History
  ok    Checkout.Tests · 44 runs · 412 tests
  warn  Legacy.Tests · 3 runs · below the 5-run floor, no findings will be emitted
  fail  every run is older than 14 days; the default window is empty
        Run your tests, or widen the window: `xping report --since 2026-06-01`.

SDK
  ok    CLI 1.0.0-rc.5
  warn  newest run recorded by SDK 1.0.0-rc.2
        RetryMasked findings need rc.4 or later. Update Xping.Sdk.XUnit.

Configuration
  warn  XPING_SAMPLINGRATE=0.5 in this shell
        Half of all executions go unrecorded; every rate in the report is wrong.
  ok    XPING_ENABLED unset (default: true)
  ok    local mode — no API key set, nothing is uploaded

Projects
  ok    SampleApp.XUnit — Xping.Sdk.XUnit + [assembly: TestFramework(...)]
  fail  SampleApp.NUnit — Xping.Sdk.NUnit referenced, no [XpingTrack] found
        Add `[assembly: XpingTrack]`. See https://docs.xping.io/getting-started/quickstart-nunit.html

2 failed · 3 warnings · 8 ok

The checks

Store — resolution and health.

Check Detail
Which resolution rule won XPING_LOCAL_STORE / repo root (.git, *.sln, *.slnx) / profile fallback. Naming the branch is the whole point: the profile fallback is silent today and is the single most confusing outcome.
Writable Reuse LocalStorePathResolver.IsWritable's probe.
Runs, size, age spread, per assembly Same data xping where prints; call it, don't reimplement it.
Unreadable sessions Already surfaced as WindowResult.UnreadableSessions; doctor should name the files.
Retention pressure MaxRuns 50 / MaxBytes 50 MB / MaxAge 30d. Warn when eviction is outpacing the analysis window.
Store tracked by git .xping/.gitignore (*) is written automatically now, but a store committed before that shipped stays tracked. git ls-files --error-unmatch .xping answers it.

History — why the report is empty or thin. This is the highest-value group.

Check Threshold
Sessions below the reporting floor LocalAnalysisConstants.MinimumSessionsToReport (5)
Per-test executions below the floor MinimumExecutionsToReport (5) — already counted as AnalysisResult.ExcludedLowEvidence, never explained
Every run outside the default window DefaultWindowDays (14) / DefaultWindowSessions (20)
More than one assembly in the store Auto-scoping covers the newest only; name the others
Sessions with zero executions Adapter loaded, collecting nothing — the "wiring present, tracking off" signature
All runs recorded in CI Local history is empty even though the store isn't

SDK — version skew. TestSession.SdkVersion is recorded on every session, so doctor can compare what wrote the history against the CLI reading it, and name the findings a stale adapter cannot produce.

Configuration — read the XPING_* surface and report what will change the outcome: ENABLED, MODE, SAMPLINGRATE (< 1.0 means the rates are wrong), CAPTURESTACKTRACES (off degrades failure signatures, which weakens SharedFailure clustering and the "N failure modes" counts), LOCAL_STORE, NO_BANNER, and the half-configured cloud case — APIKEY without PROJECTID or the reverse.

Projects — best-effort static detection, grep-level, warnings only. For each test project found near the store root: which adapter package is referenced, and whether the required wiring marker is present.

Behaviour

  • Read-only. Never writes, never mutates the store. A --fix mode is a separate decision, not this issue.
  • No network, ever, by default. The local path promises no network calls without an API key, and doctor must not be the command that breaks it. Any cloud connectivity probe to upload.xping.io goes behind an explicit --check-cloud flag.
  • --format json alongside the rendered form, same envelope discipline as report — versioned, stdout only, warnings to stderr.
  • Honours --directory, --ascii, --no-color, NO_COLOR exactly as report does, resolved through OutputCapabilities.Resolve.
  • Exit codes, consistent with report: 0 no failures (warnings allowed) · 1 at least one fail · 2 doctor itself could not run.
  • report points at it. ReportUnavailable's generic "Run your tests once with the Xping SDK installed" is replaced by "Run xping doctor to find out why."

Known constraint: the CLI's environment is not the test host's

doctor reads the XPING_* variables of its own process. The developer may have run dotnet test from a different shell, from an IDE test runner with its own environment, or in CI. Configuration findings must therefore be phrased as "in this shell" and, wherever possible, cross-checked against what the sessions actually recorded — LocalSessionProperties.Mode travels on every session and is authoritative about the mode the run really used. Where the two disagree, that disagreement is itself the most useful thing doctor can print.

Out of scope

  • --fix / interactive repair.
  • Cloud connectivity diagnosis beyond the opt-in reachability probe.
  • Replacing xping where — it stays, and doctor reuses its data.

Open questions

  1. Should doctor run automatically when report exits 2, or only be pointed at? Pointing is safer: a diagnostic that runs itself unbidden is how a tool gets noisy.
  2. How hard should project detection try? Walking every *.csproj under the repo root is cheap but noisy in a large solution; restricting to projects that have written sessions misses precisely the ones that never worked, which are the interesting case.
  3. Does the retention-pressure check need real eviction history, which is not recorded today, or can it be inferred from the age/count spread alone?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions