Skip to content

Commit c94bc9e

Browse files
tomasaschanclaude
andcommitted
v2/conformance: add POSIX/GNU suite + divergence oracle
Add the second conformance promise (POSIX/GNU argument syntax) alongside the vendored stdlib flag suite, plus the machinery to reconcile the two when they conflict. - gnuposix_test.go: hand-written suite keyed to the POSIX Utility Syntax Guidelines (1-14) and the GNU long-option extensions. No reusable Go-native POSIX corpus exists to vendor, so each case cites the rule it checks. - divergences.json: single source of truth cataloguing the intentional differences between pflag and the stdlib flag package. POSIX wins, so the listed stdlib tests are *expected* to fail; each entry names them and the rule that overrides them. Categories: posix-overrides-stdlib, pflag-design-differs, pflag-omits-gnu-feature, not-implemented-yet. - hack/oracle: reads `go test -json` and gates CI on the catalogue rather than raw pass/fail: green iff failures match the documented set exactly (catches regressions and silently-resolved divergences). The top-level "status" field tolerates a build failure while "bootstrapping" so CI is green during v2 build-out; "not-implemented-yet" entries are lenient (tolerated when failing, retirable when they start passing). - internal/divergence: catalogue types + parsing, shared by the manifest test and the oracle (no build tag, no v2 dependency). CI conformance-v2 job now pipes the suite through the oracle and drops continue-on-error; it is green now (bootstrapping) and tightens to enforcing once v2 compiles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f78167e commit c94bc9e

10 files changed

Lines changed: 948 additions & 21 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,9 @@ jobs:
6969
# run: go test -race -v -shuffle=on ./...
7070
run: go test -race -v ./...
7171
conformance-v2:
72-
name: Conformance v2 (stdlib flag drop-in)
72+
name: Conformance v2 (flag drop-in + POSIX/GNU)
7373
runs-on: ubuntu-latest
7474
timeout-minutes: 20
75-
# The conformance suite runs the standard library's own flag tests against
76-
# pflag v2 (see v2/conformance/README.md). It is expected to be red until v2
77-
# implements the flag API, so it must not gate merges yet. Remove
78-
# continue-on-error once the suite passes to make it a required check.
79-
continue-on-error: true
8075
defaults:
8176
run:
8277
working-directory: ./v2
@@ -91,5 +86,11 @@ jobs:
9186
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
9287
with:
9388
go-version: ${{ matrix.go }}
94-
- name: Conformance test
95-
run: go test -tags conformance -race -v ./conformance/...
89+
# The conformance suites (see v2/conformance/README.md) run the stdlib
90+
# flag tests and the POSIX/GNU syntax tests against pflag v2. The two specs
91+
# conflict by design (POSIX wins), so the oracle gates on the documented
92+
# divergence catalogue rather than on a raw pass/fail: green iff failures
93+
# match v2/conformance/divergences.json exactly. NOTE: this is expected to
94+
# be red until v2 implements the flag API.
95+
- name: Conformance (oracle gates on the divergence catalogue)
96+
run: go test -tags conformance -json ./conformance | go run ./conformance/hack/oracle

v2/conformance/README.md

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,67 @@
1-
# Standard-library `flag` conformance suite
1+
# Conformance suites
22

3-
pflag's headline promise is that it is a **drop-in replacement** for the Go
4-
standard library's `flag` package. This directory runs `flag`'s *own* test suite
5-
against pflag v2 to keep that promise honest: pflag v2 is dot-imported as `flag`,
6-
and anything that fails to compile or fails to pass is, by definition, a gap in
7-
drop-in compatibility.
3+
pflag makes two compatibility promises, and this directory keeps both honest
4+
with executable specs that run against pflag v2:
5+
6+
1. **Drop-in replacement for the standard library `flag` package.** We run
7+
`flag`'s *own* test suite against pflag v2 (dot-imported as `flag`) —
8+
`flag_go*_test.go`. See [Standard-library `flag`](#standard-library-flag-suite).
9+
2. **POSIX/GNU command-line argument syntax.** A hand-written suite keyed to the
10+
POSIX Utility Syntax Guidelines and GNU extensions — `gnuposix_test.go`. See
11+
[POSIX/GNU argument syntax](#posixgnu-argument-syntax-suite).
12+
13+
Anything that fails to compile or fails to pass is, by definition, a gap.
14+
15+
## POSIX wins, and the oracle gates on it
16+
17+
The two specs genuinely conflict in a few places — single-dash `-int` is the flag
18+
`int` to the stdlib, but the cluster `-i -n -t` to POSIX. **That conflict is the
19+
whole reason pflag exists, and POSIX wins.** So some vendored stdlib tests are
20+
*expected* to fail, and the stdlib suite can never be all-green.
21+
22+
The success criterion is therefore not "everything passes" but:
23+
24+
> The suite builds and runs, and the set of failing tests matches the documented
25+
> divergence catalogue **exactly** — no undocumented failure (a regression) and
26+
> no documented test that quietly started passing (a divergence to retire).
27+
28+
That catalogue is [`divergences.json`](divergences.json) — the single source of
29+
truth, listing each conflict, the POSIX Guideline / GNU rule that decides it, and
30+
the exact tests it makes fail (categories: `posix-overrides-stdlib`,
31+
`pflag-design-differs`, `pflag-omits-gnu-feature`). The oracle in
32+
[`hack/oracle`](hack/oracle) reads `go test -json` and enforces the criterion
33+
above; CI gates on it:
34+
35+
```sh
36+
go test -tags conformance -json ./conformance | go run ./conformance/hack/oracle
37+
```
38+
39+
The oracle tells you exactly what to do when it's red: fix a regression, add a
40+
newly-discovered conflict to the catalogue, or retire one that no longer applies.
41+
That is how the catalogue gets **calibrated** against the real implementation —
42+
the current entries are a best-effort seed.
43+
44+
`go test ./...` (no tag) validates that `divergences.json` is well-formed
45+
(`TestDivergenceManifest`) without needing v2 to compile.
46+
47+
### Lifecycle: building v2 with a green CI
48+
49+
The catalogue's top-level `status` and the `not-implemented-yet` category let the
50+
job stay green while v2 is built out, tightening as it matures:
51+
52+
| Phase | `status` | catalogue | oracle |
53+
| --- | --- | --- | --- |
54+
| v2 empty (now) | `bootstrapping` | permanent divergences only | build failure tolerated → **green** |
55+
| v2 compiles, partial | `enforcing` | add `not-implemented-yet` entries for tests that fail only because a feature is missing | build required; those failures tolerated → **green** |
56+
| v2 complete | `enforcing` | `not-implemented-yet` burned down to empty | only permanent divergences remain |
57+
58+
`not-implemented-yet` is lenient: a listed test may fail, be skipped, or not run.
59+
When one starts **passing**, the oracle stays green and just lists it as
60+
*retirable* — delete the entry. (A *permanent* divergence that passes is the
61+
opposite: a hard failure, because it never should.) So implementing a feature
62+
never turns CI red; forgetting to categorise a new failure does.
63+
64+
# Standard-library `flag` suite
865

966
## Running it
1067

@@ -65,6 +122,11 @@ primary `conformance` package.
65122
| `doc.go` | hand-written | package doc; keeps the dir buildable with no tags; holds the `//go:generate` directive |
66123
| `flag_go<ver>_test.go` | generated from that version's `flag_test.go` | **do not edit** — regenerate with `sync.sh` |
67124
| `harness_test.go` | hand-written | reimplements the stdlib's internal `export_test.go` helpers (`ResetForTesting`, `DefaultUsage`) against v2's public API, plus the version guard |
125+
| `gnuposix_test.go` | hand-written | the POSIX/GNU argument-syntax suite |
126+
| `divergences.json` | hand-written | the catalogue of intentional differences (single source of truth) |
127+
| `divergences.go` / `divergences_test.go` | hand-written | embed + validate the catalogue (untagged, runs in normal `go test`) |
128+
| `internal/divergence/` | hand-written | catalogue types + parsing, shared by the test and the oracle (no build tag, no v2 dependency) |
129+
| `hack/oracle/` | hand-written | the CI gate: compares `go test -json` against the catalogue |
68130
| `internal/testenv/testenv.go` | hand-written | minimal stand-in for the stdlib-internal `internal/testenv` (only `MustHaveExec` / `Executable`) |
69131
| `hack/sync.sh` | hand-written | regenerates the vendored copies |
70132

@@ -79,6 +141,34 @@ edits, which the script applies and nothing else:
79141
- inject a one-line `init()` after the imports registering the version with the guard
80142
- prepend the build tags + a "generated" banner
81143

144+
# POSIX/GNU argument syntax suite
145+
146+
`gnuposix_test.go` checks pflag's *other* promise: compatibility with the POSIX
147+
Utility Syntax Guidelines and the GNU long-option extensions. Each test cites the
148+
rule it covers — `Gn` for a numbered POSIX Guideline, or "GNU" for an extension:
149+
150+
| Area | Rules covered |
151+
| --- | --- |
152+
| Short options | preceded by `-` (G4), single alphanumeric name (G3), clustering (G5), separate or attached arg (G6), cluster ending in an arg-taking option (G5) |
153+
| Long options (GNU) | `--name`, dashes in names, `--name=value`, `--name value` |
154+
| Special tokens | `--` ends options (G10), lone `-` is an operand (G13) |
155+
| Ordering | GNU interspersing (default) vs strict POSIX stop-at-first-operand (G9), order-independence and repetition (G11) |
156+
157+
### Why hand-written instead of vendored?
158+
159+
Unlike the stdlib `flag` suite, there is **no reusable Go-native POSIX
160+
conformance corpus to vendor**. The authoritative sources are prose — the
161+
[Open Group Utility Conventions ch. 12][posix] (Guidelines 1–14) and
162+
[GNU Argument Syntax][gnu] — and the machine-runnable suites (glibc/gnulib
163+
`tst-getopt*.c`) are C, coupled to the C `getopt`/optstring API, so they mostly
164+
exercise C-isms pflag does not share. The tests are therefore authored directly
165+
from the guidelines. They are shaped as *parse → normalized result*, so if a
166+
shared cross-language corpus ever appears we can expose a tiny CLI to drive it
167+
without restructuring.
168+
169+
[posix]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html
170+
[gnu]: https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
171+
82172
For a version not matching the local toolchain, `sync.sh` fetches `flag_test.go`
83173
from the matching Go release branch on GitHub.
84174

v2/conformance/divergences.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package conformance
2+
3+
import (
4+
_ "embed"
5+
6+
"github.com/spf13/pflag/v2/conformance/internal/divergence"
7+
)
8+
9+
//go:embed divergences.json
10+
var manifestJSON []byte
11+
12+
// Manifest returns the parsed divergence catalogue (divergences.json): the
13+
// documented, intentional differences between pflag v2 and the stdlib flag
14+
// package. The conformance oracle (hack/oracle) uses it to decide which test
15+
// failures are expected.
16+
func Manifest() (divergence.Manifest, error) {
17+
return divergence.Parse(manifestJSON)
18+
}

v2/conformance/divergences.json

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{
2+
"_comment": "Catalogue of intentional differences between pflag v2 and the standard library flag package. Single source of truth for both the human-readable docs and the conformance oracle (hack/oracle). Each entry lists the conformance tests it is expected to make fail. POSIX wins. This catalogue is a best-effort seed and MUST be calibrated against the real v2 implementation: the oracle reports any undocumented failure (a regression) and any permanent divergence that unexpectedly passes (to remove). Categories: posix-overrides-stdlib | pflag-design-differs | pflag-omits-gnu-feature | not-implemented-yet (temporary, burned down as v2 is built). The 'status' field gates build failures: 'bootstrapping' tolerates the suite not building yet (CI green during build-out); flip to 'enforcing' once v2 compiles.",
3+
"status": "bootstrapping",
4+
"divergences": [
5+
{
6+
"category": "posix-overrides-stdlib",
7+
"topic": "single-dash tokens are short-option clusters; long names need the GNU \"--\" prefix",
8+
"stdlib": "A single dash introduces a (possibly multi-character) flag name: \"-bool\", \"-int 22\", \"-help\".",
9+
"pflag": "A single dash introduces one or more single-character options, so \"-int\" is the cluster -i -n -t; multi-character names require \"--\".",
10+
"refs": "POSIX Guidelines 3, 4, 5, 14; GNU long-option extension",
11+
"affectedTests": [
12+
"TestParse",
13+
"TestFlagSetParse",
14+
"TestUserDefined",
15+
"TestUserDefinedFunc",
16+
"TestUserDefinedBool",
17+
"TestHelp",
18+
"TestExitCode"
19+
]
20+
},
21+
{
22+
"category": "pflag-design-differs",
23+
"topic": "usage / PrintDefaults output uses the \"-s, --long\" layout",
24+
"stdlib": "PrintDefaults emits \" -A\\tfor ...\" (single dash, tab-aligned) and the tests compare it byte-for-byte.",
25+
"pflag": "Help reflects POSIX/GNU spelling (short and long forms together); the exact byte layout differs from the stdlib.",
26+
"refs": "GNU usage convention",
27+
"affectedTests": [
28+
"TestPrintDefaults",
29+
"TestUserDefinedBoolUsage"
30+
]
31+
},
32+
{
33+
"category": "pflag-design-differs",
34+
"topic": "parse-error and usage message text differs from the stdlib",
35+
"stdlib": "Errors read e.g. \"flag provided but not defined: -i\" and \"invalid value ... parse error\".",
36+
"pflag": "pflag has its own error wording, so byte/substring comparisons against the stdlib text do not hold.",
37+
"refs": "implementation detail (not mandated by POSIX)",
38+
"affectedTests": [
39+
"TestUsageOutput",
40+
"TestParseError"
41+
]
42+
},
43+
{
44+
"category": "pflag-design-differs",
45+
"topic": "no \"flag set before being defined\" panic",
46+
"stdlib": "Defining a flag after Set was called for that name panics with \"flag X set at ... before being defined\".",
47+
"pflag": "pflag does not track set-before-define; Set on an unknown flag returns an error and defining later does not panic.",
48+
"refs": "implementation detail (not mandated by POSIX)",
49+
"affectedTests": [
50+
"TestDefineAfterSet"
51+
]
52+
},
53+
{
54+
"category": "pflag-design-differs",
55+
"topic": "flag-name validation panics with different messages",
56+
"stdlib": "Var panics with \"flag \\\"-foo\\\" begins with -\" / \"flag \\\"foo=bar\\\" contains =\".",
57+
"pflag": "pflag validates names differently and does not produce the same panic messages.",
58+
"refs": "implementation detail (not mandated by POSIX)",
59+
"affectedTests": [
60+
"TestInvalidFlags"
61+
]
62+
},
63+
{
64+
"category": "pflag-omits-gnu-feature",
65+
"topic": "unambiguous long-option abbreviation",
66+
"stdlib": "n/a",
67+
"pflag": "GNU accepts \"--verb\" for \"--verbose\" when unambiguous; pflag requires the full long name.",
68+
"refs": "GNU abbreviation rule",
69+
"affectedTests": [
70+
"TestLongOptionAbbreviation"
71+
]
72+
}
73+
]
74+
}

v2/conformance/divergences_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package conformance
2+
3+
// This is an untagged, in-package test so it runs in the normal `go test ./...`
4+
// (it does not need v2 to compile). It validates the divergence catalogue
5+
// (divergences.json) and prints it under -v. The catalogue is the single source
6+
// of truth shared with the conformance oracle (hack/oracle); the oracle uses it
7+
// to treat exactly these test failures as expected when gating CI.
8+
9+
import "testing"
10+
11+
func TestDivergenceManifest(t *testing.T) {
12+
m, err := Manifest()
13+
if err != nil {
14+
t.Fatal(err)
15+
}
16+
if err := m.Validate(); err != nil {
17+
t.Fatalf("divergences.json is invalid: %v", err)
18+
}
19+
for _, d := range m.Divergences {
20+
t.Logf("[%s] %s\n stdlib: %s\n pflag : %s\n refs : %s\n affected: %v",
21+
d.Category, d.Topic, d.Stdlib, d.Pflag, d.Refs, d.AffectedTests)
22+
}
23+
}

v2/conformance/doc.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
// Package conformance runs the Go standard library's own flag package test
2-
// suite against pflag v2 to verify the drop-in-replacement promise.
1+
// Package conformance verifies pflag v2's two compatibility promises:
32
//
4-
// The actual tests are vendored, build-tagged copies of the stdlib
5-
// flag_test.go (one per supported Go version) and only compile under the
6-
// "conformance" build tag, so they do not affect the normal `go test ./...`
7-
// run while v2 is still being built out. See README.md for details and run
8-
// them with:
3+
// - Drop-in replacement for the Go standard library flag package. Tested by
4+
// vendored, build-tagged copies of the stdlib flag_test.go, one per
5+
// supported Go version (flag_go*_test.go).
6+
// - POSIX/GNU command-line argument syntax. Tested by a hand-written suite
7+
// keyed to the POSIX Utility Syntax Guidelines and GNU extensions
8+
// (gnuposix_test.go).
9+
//
10+
// The two specs genuinely conflict in a few places — that is why pflag exists.
11+
// POSIX wins; those conflicts are catalogued in divergences.json and enforced by
12+
// the oracle (hack/oracle), which gates CI: the suite is green iff the failing
13+
// tests match the catalogue exactly.
14+
//
15+
// Everything here only compiles under the "conformance" build tag, so it does
16+
// not affect the normal `go test ./...` run while v2 is still being built out.
17+
// See README.md for details and run them with:
918
//
1019
// go test -tags conformance ./conformance/...
1120
//

0 commit comments

Comments
 (0)