You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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). Unset means no report,
which is the default and is not an error.
An environment variable rather than a TckConfig field, so that emitting 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. Several suites in one pytest
session each write their own file, so flagd's two resolvers would not collide.
The load-bearing part is the per-scenario list. Appendix F requires that a
scenario skipped for an undeclared capability is reported as skipped with the
reason and never as passed, and nothing downstream can check that against a
summary line. Recording every scenario's outcome individually makes the rule
checkable by the consumer instead of dependent on the runner. It is also
required to be complete, because a document that quietly dropped what it skipped
would satisfy the letter of the rule and still mislead whoever read it.
pytest, unlike godog, reports a skip honestly -- so the interesting divergence
here is elsewhere. The one scenario the Python SDK cannot satisfy is marked
xfail, so the run finishes green; the provider still did not satisfy it, and the
document says failed with the reason. An expected failure is a recorded
deviation, not an excused one. Scenarios are therefore enumerated at collection
and resolved at the end of the session rather than as fixtures run, which is
also what keeps a scenario skipped by a marker -- whose fixtures never run at
all -- from vanishing from the document.
Identity comes from spec_revision.json, generated by hatch_build_sync.py beside
the copied assets and force-included into the wheel. It has to be captured at
build time: the submodule that knows the answer is not in the distribution, so
an installed copy has nothing left to ask. A build that cannot reach git -- an
unpacked sdist -- warns and records "unknown" rather than inventing a commit.
Both the commit and the tree hash are recorded, the tree because it identifies
the assets alone: unchanged by unrelated edits elsewhere in the specification,
so two runs of identical assets 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 TckConfig.name recorded as the configuration, because
TckConfig.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 off an optional
control_api property rather than added to the BackendControl protocol, so that
adding it leaves every existing control complete and one that stays quiet simply
omits the field.
The tests assert the two properties a consumer is entitled to assume -- that no
scenario the capability gate stopped is ever reported as passed, and that every
collected scenario appears exactly once, counted against pytest's own collection
rather than against a number written down beside it.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
It is an environment variable rather than a `TckConfig` field so that emitting a report is a property
499
+
of the *run* and not of the code: CI sets it, a developer running the suite locally does not, and no
500
+
adopter changes a line to publish one. Unset means no report, which is not an error. Several suites
501
+
in one pytest session each write their own file, so flagd's two resolvers would not collide.
502
+
503
+
### Why every scenario is listed
504
+
505
+
Appendix F requires that a scenario skipped for an undeclared capability is reported as skipped
506
+
**with the reason** and never as passed. A consumer cannot check that against a summary line, so the
507
+
report records the outcome of *every* scenario individually — and is required to be complete, because
508
+
a document that quietly dropped what it skipped would satisfy the letter of the rule and still
509
+
mislead whoever read it.
510
+
511
+
Which also means the report is not a transcription of pytest's summary. The run above finishes green:
512
+
the one scenario the Python SDK cannot satisfy is marked `xfail` (finding 1), so pytest counts it as
513
+
expected and exits zero. The provider still did not satisfy it, and the document says `failed` with
514
+
the reason — an expected failure is a recorded deviation, not an excused one.
515
+
516
+
Four outcomes rather than two, because "did not run" is not one thing:
517
+
518
+
| Outcome | Means |
519
+
| --- | --- |
520
+
|`passed`| the scenario ran and passed |
521
+
|`failed`| the scenario ran and failed, including a known deviation marked `xfail`|
522
+
|`not-declared`| skipped because the provider did not declare a capability the scenario is tagged with |
523
+
|`not-applicable`| skipped for any other reason — a marker an adopter applied, a step calling `pytest.skip`|
524
+
525
+
### What identifies a report
526
+
527
+
`tck.specRevision` and `tck.assetsTree` come from `spec_revision.json`, which `hatch_build_sync.py`
528
+
generates from the submodule alongside the copied assets. It has to be captured at build time: the
529
+
submodule is not in the wheel, so an installed copy has nothing left to ask. A build that cannot
530
+
reach git — an unpacked sdist, say — warns and records `unknown` rather than inventing a commit.
531
+
532
+
The tree hash is carried as well as the commit because it identifies the assets alone. It is
533
+
unchanged by unrelated edits elsewhere in the specification, so two runs that executed identical
534
+
assets report the same value even when pinned to different commits — and it is checkable, since
535
+
`git rev-parse <specRevision>:specification/assets/provider-tck` must reproduce it.
536
+
537
+
`provider.name` is what the provider reports through its own metadata, not `TckConfig.name`.
538
+
`TckConfig.name` is chosen to read well in a failure message — `flagd-rpc` — which makes it the
539
+
*configuration*, and it is reported as such. One provider with two materially different modes
540
+
produces two reports that are not interchangeable.
541
+
542
+
`backend.controlApi` is read off an optional `control_api` property on your `BackendControl`,
543
+
returning `"http"` or `"in-process"`. It is not a member of the protocol: adding one would make every
544
+
existing control incomplete for the sake of one string, and a control that stays quiet simply omits
545
+
the field.
546
+
481
547
## The self-tests
482
548
483
549
| Suite | Subject | Why |
@@ -489,14 +555,15 @@ This mirrors what `openfeature-flagd-api-testkit` already does for the flagd tes
489
555
|`test_declaration`| what a `TckConfig` claims | none of it is observable in a pass or a fail, so nothing else would catch it |
490
556
|`test_extensions`| an adopter's own scenarios | an extension runs inside the canonical suite, changes nothing for an adopter who has none, and cannot take a canonical scenario's identity |
491
557
|`test_http_control`|`HttpControl`| the `/reset` fallback, the disconnect bookkeeping and the control-API it reports, against a stubbed control API |
558
+
|`test_report`| the conformance report | checks the two properties a consumer is entitled to assume |
492
559
493
560
```
494
-
163 passed, 35 skipped, 2 xfailed
561
+
PLACEHOLDER passed, 27 skipped, 2 xfailed
495
562
```
496
563
497
-
No Docker and no network beyond loopback. The conformance suites take under a second;
498
-
`test_extensions`takes most of the rest, because the properties it checks are properties of a whole
499
-
pytest session and it runs a generated adoption in a subprocess to check them.
564
+
No Docker and no network beyond loopback. The conformance suites take under a second;`test_report`
565
+
and `test_extensions`take most of the rest, because the properties they check are properties of a
566
+
whole pytest session and they run generated adoptions in subprocesses to check them.
500
567
501
568
Neither in-memory suite declares `@lifecycle`, so the six lifecycle scenarios — three about
502
569
initialisation, three about shutdown — are skipped in both. That is the point: with no backend to
@@ -521,7 +588,14 @@ both. Both declare `@variants`, since an in-memory flag set is keyed by variant
521
588
stack and discovering its mapped ports is still each adopter's own code. Abstracting that from a
522
589
single example tends to produce the wrong abstraction; it should wait for a second adopter.
523
590
-**Caching, hooks and flag metadata** are not covered.
524
-
591
+
-**A report cannot name a Scenario Outline row portably.** Every row of an outline shares one
592
+
scenario name, and the report schema has nowhere to put the row, so several entries would be
593
+
indistinguishable — including, here, one that differs in outcome from its siblings. This
594
+
implementation qualifies the name with pytest's example id (`... [boolean-flag-Integer-1]`), which
595
+
is unambiguous but is not what another language would produce for the same row. Raised on
0 commit comments