Skip to content

Commit 9b106d2

Browse files
committed
feat(provider-tck): emit a machine-readable conformance report
Setting PROVIDER_TCK_REPORT_DIR makes each suite write its run to <dir>/<name>.json against the report schema in the specification repository (open-feature/spec#425, part of open-feature/spec#424). Go is the language that needs this first. godog counts a capability-gated skip in its passed tally, so a run that skipped five of twenty-nine scenarios prints "29 scenarios (29 passed)". Appendix F is unambiguous that a scenario skipped for an undeclared capability is reported as skipped with the reason and never as passed, and the harness does say so in a separate log line -- but the headline number still says something false, and the number is what gets read. pytest and jest-cucumber report skips correctly, so this is the runner's property rather than the suite's design. The report does not fix godog's summary. It makes the summary stop mattering, by recording every scenario's outcome individually so a consumer can check the rule instead of trusting the runner to have applied it. The same run now reports twenty-four passed and five not-declared, each with its reason. Identity comes from revision.go, generated by sync_assets.go beside the embedded artifacts. Generating both in one command is what keeps them honest: the CI check regenerates and fails on any difference, so a revision disagreeing with the artifacts beside it cannot be committed. The check is widened to cover the generated file, which it would otherwise have missed. Both the commit and the tree hash are recorded, the tree because it identifies the artifacts alone -- unchanged by unrelated edits elsewhere in the specification, so two runs of identical artifacts agree even when pinned to different commits, and checkable because `git rev-parse <commit>:specification/assets/provider-tck` reproduces it. Two smaller decisions. The provider is identified by the name it reports through its own metadata, with Config.Name recorded as the configuration, because Config.Name is chosen to read well in a failure message -- "flagd-rpc" -- and a provider with two materially different modes produces two reports that are not interchangeable. And how the backend was driven is read through an optional interface rather than a new BackendControl method, so that adding it breaks no existing implementation and a control that does not implement it simply omits the field. Emission is opt-in through the environment rather than through Config so that producing a report is a property of the run and not of the code: CI sets it, a local run does not, and no adopter changes a line to publish one. The tests assert the property that motivated the work -- that no scenario the capability gate stopped is ever reported as passed, and that every scenario is accounted for exactly once, since a report that silently omitted what it skipped would satisfy the first half while still misleading a reader. That test earned its place immediately: it caught this emitter recording every skipped scenario twice, the second time as passed, because godog does not deliver the before hook's ErrSkip to the after hook. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
1 parent 95586a3 commit 9b106d2

8 files changed

Lines changed: 726 additions & 2 deletions

File tree

tools/provider-tck/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,62 @@ provider in a domain replaces and shuts down the previous one; a fresh domain pe
434434
leave every provider of the suite registered and running, which for a provider holding a network
435435
connection means leaking one connection per scenario.
436436

437+
## Conformance reports
438+
439+
Set `PROVIDER_TCK_REPORT_DIR` and each suite writes a machine-readable report of its run to
440+
`<dir>/<name>.json`, conforming to the [report schema][report-schema] in the specification.
441+
442+
```console
443+
$ PROVIDER_TCK_REPORT_DIR=./reports go test ./...
444+
$ jq '.scenarios | group_by(.outcome) | map({(.[0].outcome): length}) | add' reports/in-memory.json
445+
{
446+
"passed": 24,
447+
"not-declared": 5
448+
}
449+
```
450+
451+
It is an environment variable rather than a `Config` field so that emitting a report is a property
452+
of the run and not of the code: CI sets it, a developer running the suite locally does not, and no
453+
adopter changes a line to publish one. Unset means no report, which is not an error. Several suites
454+
in one test binary each write their own file, so flagd's two resolvers do not collide.
455+
456+
### Why this exists in Go before the other languages
457+
458+
Because Go is the language that needs it most. godog counts a capability-gated skip in its **passed**
459+
tally:
460+
461+
```
462+
29 scenarios (29 passed)
463+
```
464+
465+
Five of those twenty-nine did not run. Appendix F is unambiguous that a scenario skipped for an
466+
undeclared capability is reported as skipped with the reason and *never* as passed, and the harness
467+
does say so in a separate log line — but the headline number still says something false, and a
468+
number is what gets read. pytest and jest-cucumber both report skips correctly, so this is a property
469+
of the runner rather than of the suite's design.
470+
471+
The report does not fix godog's summary. It makes the summary stop mattering, by recording the
472+
outcome of every scenario individually so that a consumer can check the rule instead of trusting the
473+
runner to have applied it. `reports/in-memory.json` above accounts for all twenty-nine scenarios and
474+
calls five of them `not-declared`, each with the reason.
475+
476+
### What identifies a report
477+
478+
`tck.specRevision` and `tck.assetsTree` come from [`revision.go`](./pkg/tck/revision.go), which
479+
`sync_assets.go` generates from the submodule alongside the embedded artifacts. Generating both in
480+
the same command is what keeps them honest: `make provider-tck-assets-check` regenerates and fails on
481+
any difference, so a revision that disagrees with the artifacts beside it cannot be committed.
482+
483+
The tree hash is carried as well as the commit because it identifies the artifacts alone. It is
484+
unchanged by unrelated edits elsewhere in the specification, so two runs that executed identical
485+
artifacts report the same value even when pinned to different commits — and it is checkable, since
486+
`git rev-parse <specRevision>:specification/assets/provider-tck` must reproduce it.
487+
488+
`provider.name` is what the provider reports through its own metadata, not `Config.Name`.
489+
`Config.Name` is chosen to read well in a failure message — `flagd-rpc` — which makes it the
490+
*configuration*, and it is reported as such. One provider with two materially different modes
491+
produces two reports that are not interchangeable.
492+
437493
## The self-tests
438494

439495
Three suites run against providers from the SDK itself. They need no Docker and finish in
@@ -522,6 +578,7 @@ the SDK's provider rather than reimplementing it — every resolution decision i
522578
[control-api]: https://github.com/open-feature/spec/blob/main/specification/assets/provider-tck/openapi/control-api.yaml
523579
[numeric-coercion-adr]: https://github.com/open-feature/flagd/blob/main/docs/architecture-decisions/numeric-coercion.md
524580
[reinit-fix]: https://github.com/open-feature/spec/commit/fc99d5ace4da472a5fea0595fa4db8034bbbc769
581+
[report-schema]: https://github.com/open-feature/spec/blob/main/specification/assets/provider-tck/report/conformance-report.schema.json
525582
[req-223]: https://github.com/open-feature/spec/blob/main/specification/sections/02-providers.md#requirement-223
526583
[req-224]: https://github.com/open-feature/spec/blob/main/specification/sections/02-providers.md#requirement-224
527584
[req-225]: https://github.com/open-feature/spec/blob/main/specification/sections/02-providers.md#requirement-225

tools/provider-tck/pkg/tck/inprocess.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,10 @@ func (c *InProcessControl) ChangeFlag(context.Context) error {
313313

314314
return c.current.UpdateFlag(ChangingFlagKey, changingFlag(c.changingVariant))
315315
}
316+
317+
// ControlAPI reports how this backend was driven.
318+
//
319+
// "in-process" is the narrow allowance the schema makes for a provider with no
320+
// backend. A report claiming it for a provider that has one should be treated
321+
// with suspicion, which is precisely why it is recorded rather than assumed.
322+
func (c *InProcessControl) ControlAPI() string { return "in-process" }

0 commit comments

Comments
 (0)