Skip to content

Commit d165af1

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 228b85e commit d165af1

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
@@ -461,6 +461,62 @@ provider in a domain replaces and shuts down the previous one; a fresh domain pe
461461
leave every provider of the suite registered and running, which for a provider holding a network
462462
connection means leaking one connection per scenario.
463463

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

466522
Three suites run against providers from the SDK itself. They need no Docker and finish in
@@ -574,6 +630,7 @@ capability rather than to relax the assertion.
574630
[control-api]: https://github.com/open-feature/spec/blob/main/specification/assets/provider-tck/openapi/control-api.yaml
575631
[numeric-coercion-adr]: https://github.com/open-feature/flagd/blob/main/docs/architecture-decisions/numeric-coercion.md
576632
[reinit-fix]: https://github.com/open-feature/spec/commit/fc99d5ace4da472a5fea0595fa4db8034bbbc769
633+
[report-schema]: https://github.com/open-feature/spec/blob/main/specification/assets/provider-tck/report/conformance-report.schema.json
577634
[req-147]: https://github.com/open-feature/spec/blob/main/specification/sections/01-flag-evaluation.md#requirement-147
578635
[req-223]: https://github.com/open-feature/spec/blob/main/specification/sections/02-providers.md#requirement-223
579636
[req-224]: https://github.com/open-feature/spec/blob/main/specification/sections/02-providers.md#requirement-224

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)