rastrillo / fiesta Public

Close the promise gates' loopholes: empty values, re-registered properties, redefined keyframes, names in old markup

Review of the promise gates found four ways a 1.x rule could change a
1.0 page and still pass TestEveryNewRuleRequiresNewMarkup, each where
no photograph looks (a 700 to 750px media query, or an animation
still() switches off), and one test that Task 5's own markup would
break.

- An empty value read as a new name. [rst-page=""] became the key
  rst-page~=, which nothing styles, so it passed as new markup while
  matching 1.0's bare <main rst-page>. Attribute selectors are now read
  by what they match: a bare or empty value needs only the attribute,
  a value needs every one of its tokens, and ~= with an empty or
  spaced value is refused, since it matches nothing. A class is read as
  the attribute it spells, so .rst-job and [rst-job] are one name.
- A registration of a property 1.0 already uses passed as new.
  @property --rst-sp-4 { inherits: false; initial-value: 48px } was
  absent from 1.0.0's at-rules, yet it changes 1.0's spacing. Any
  custom property 1.0.0 declares, reads or registers is now old, for a
  registration and for a :root value alike.
- An empty redefinition left no record. @keyframes rst-spin {} has no
  declarations, so neither gate saw it, though it replaces 1.0's
  spinner on every page. The reader now records each descriptor and
  keyframes at-rule by name, empty ones included, and a definition
  beyond 1.0.0's count of that name is refused.
- A name 1.0's CSS never styles is not a name 1.0 markup never
  carries. rst-job is an unstyled hook around every 1.0 progress bar,
  so [rst-job] [rst-job-bar] passed through it. The baseline
  vocabulary now also holds every rst- attribute, value token and
  class in 1.0.0's 163 frozen example documents (layouts included) and
  the coverage gate's documented hooks and qualifiers.
- TestTheHeaderLiftsOnlyTheSchemeMenu wrapped the live standalone
  example in a footer row and required one rst-page-foot, so the day
  Task 5 puts that row in the example it would fail before checking
  anything. It now rebuilds the body as the menu alone inside exactly
  one footer row, whatever the example already wraps it in.

The gate's body moves into refusedAdditions so
TestTheNewRuleGateHasNoKnownHoles can run it over 1.0.0's tokens.css with one rule appended.
Red before the fix (the four holes passed), green after; four rules
that only style new markup (a new @keyframes rst-…, a new
@property --rst-…, a new :root --rst-… value, [rst-page-foot]
[rst-dropdown]) pass both times, so it cannot pass by refusing
everything. TestOnlyNewMarkupPassesTheGrammar gains [rst-page=""],
[rst-page="  "] and [rst-dropdown~=""], red before and green after,
and .rst-page[rst-row], refused both times.

The same four rules appended to the tree's css/tokens.css in a scratch
copy: TestEveryNewRuleRequiresNewMarkup passed each before the fix and
names each after. The header test, run with the live standalone
example already wrapped in a footer row: red before ("no longer has the
<body> lines this test wraps"), green after, and still red after
against a schemeMenu that lifts the whole body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 8d58daa8f3cfec5ea6b7cced9b1aa0deed136450 parent a7a1129
3 files changed, +289 −42
  • check/baseline_test.go +222 −31
  • check/selectors_test.go +49 −6
  • internal/gallery/gallery_test.go +18 −5

CI failed — run details

CI log
=== gofmt ===
=== no-rastrillo ===
mkdir -p "/var/lib/amadan-runner/w4/repos/rastrillo__fiesta/wt/fiesta-1-1/.build/tmp"
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
go: downloading go1.26.10 (linux/arm64)
ok  	amadan.net/rastrillo/fiesta	0.932s
=== size ===
go test ./internal/build -run '^TestTheOneLinkIsUnder25KiBGzipped$' -count=1 -v
=== RUN   TestTheOneLinkIsUnder25KiBGzipped
    build_test.go:138: fiesta-plain.css: 152856 bytes, 22303 bytes gzipped (21.8 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta-signal.css: 152951 bytes, 22414 bytes gzipped (21.9 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta.css: 155770 bytes, 23018 bytes gzipped (22.5 KiB of the 25.0 KiB limit)
--- PASS: TestTheOneLinkIsUnder25KiBGzipped (0.02s)
PASS
ok  	amadan.net/rastrillo/fiesta/internal/build	0.025s
=== history ===
history: 12 commits; release tags: v1.0.0
=== suite ===
--- shard 0 of 48 ---
==> shard 0 of 48: make root
go build ./...
go vet ./...
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh root 'internal/gallery@3 cmd/fiesta@2 internal/www@2'
ok  	amadan.net/rastrillo/fiesta	4.227s
ok  	amadan.net/rastrillo/fiesta/check	8.299s
ok  	amadan.net/rastrillo/fiesta/example	0.265s
?   	amadan.net/rastrillo/fiesta/hack/seed/english	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/axe	0.268s
ok  	amadan.net/rastrillo/fiesta/internal/build	1.757s
ok  	amadan.net/rastrillo/fiesta/internal/codeview	0.024s
?   	amadan.net/rastrillo/fiesta/internal/harness	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/markup	0.004s
ok  	amadan.net/rastrillo/fiesta/internal/nodetest	0.687s
ok  	amadan.net/rastrillo/fiesta/internal/site	4.261s
ok  	amadan.net/rastrillo/fiesta/internal/tokens	0.027s
suite: root took 41s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m22.680000s 0m4.410000s
--- shard 1 of 48 ---
==> shard 1 of 48: make test.internal.gallery~1of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~1of3'
gotest: slice 1 of 3 of ./internal/gallery/ runs 35 of 105 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	71.473s
suite: test.internal.gallery~1of3 took 94s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m52.260000s 0m1.800000s
--- shard 2 of 48 ---
==> shard 2 of 48: make test.internal.gallery~2of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~2of3'
gotest: slice 2 of 3 of ./internal/gallery/ runs 35 of 105 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	68.792s
suite: test.internal.gallery~2of3 took 91s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m50.990000s 0m1.850000s
--- shard 3 of 48 ---
==> shard 3 of 48: make test.internal.gallery~3of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~3of3'
gotest: slice 3 of 3 of ./internal/gallery/ runs 35 of 105 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	65.509s
suite: test.internal.gallery~3of3 took 88s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m48.030000s 0m1.970000s
--- shard 4 of 48 ---
==> shard 4 of 48: make test.cmd.fiesta~1of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'cmd.fiesta~1of2'
gotest: slice 1 of 2 of ./cmd/fiesta/ runs 7 of 12 test tests
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	56.554s
suite: test.cmd.fiesta~1of2 took 77s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.040000s 0m2.800000s
--- shard 5 of 48 ---
==> shard 5 of 48: make test.cmd.fiesta~2of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'cmd.fiesta~2of2'
gotest: slice 2 of 2 of ./cmd/fiesta/ runs 5 of 12 test tests
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	58.580s
suite: test.cmd.fiesta~2of2 took 80s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m38.160000s 0m2.900000s
--- shard 6 of 48 ---
==> shard 6 of 48: make test.internal.www~1of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.www~1of2'
gotest: slice 1 of 2 of ./internal/www/ runs 11 of 25 test tests
ok  	amadan.net/rastrillo/fiesta/internal/www	66.098s
suite: test.internal.www~1of2 took 95s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m0.790000s 0m3.630000s
--- shard 7 of 48 ---
==> shard 7 of 48: make test.internal.www~2of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.www~2of2'
gotest: slice 2 of 2 of ./internal/www/ runs 14 of 25 test tests
ok  	amadan.net/rastrillo/fiesta/internal/www	62.456s
suite: test.internal.www~2of2 took 91s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m57.390000s 0m3.380000s
--- shard 8 of 48 ---
==> shard 8 of 48: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
go: downloading honnef.co/go/tools v0.8.2
go: downloading golang.org/x/tools v0.51.0
go: downloading github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c
go: downloading golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678
go: downloading golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
suite: staticcheck took 45s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m44.870000s 0m5.850000s
--- shard 9 of 48 ---
==> shard 9 of 48: make govulncheck
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...
go: downloading golang.org/x/vuln v1.8.0
go: downloading golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518
go: downloading golang.org/x/tools v0.50.0
go: downloading golang.org/x/mod v0.41.0
go: downloading golang.org/x/sync v0.23.0
=== Symbol Results ===

No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 2 vulnerabilities in packages you import and 5
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
suite: govulncheck took 35s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.510000s 0m2.450000s
--- shard 10 of 48 ---
==> shard 10 of 48: make gitleaks
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
go: downloading github.com/zricethezav/gitleaks/v8 v8.30.1
go: downloading github.com/spf13/cobra v1.9.1
go: downloading github.com/rs/zerolog v1.33.0
go: downloading github.com/spf13/viper v1.19.0
go: downloading golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
go: downloading github.com/BobuSumisu/aho-corasick v1.0.3
go: downloading github.com/hashicorp/go-version v1.7.0
go: downloading github.com/charmbracelet/lipgloss v0.5.0
go: downloading github.com/fatih/semgroup v1.2.0
go: downloading github.com/gitleaks/go-gitdiff v0.9.1
go: downloading github.com/Masterminds/sprig/v3 v3.3.0
go: downloading github.com/mholt/archives v0.1.2
go: downloading github.com/h2non/filetype v1.1.3
go: downloading golang.org/x/sync v0.11.0
go: downloading github.com/spf13/pflag v1.0.6
go: downloading golang.org/x/text v0.22.0
go: downloading github.com/STARRY-S/zip v0.2.1
go: downloading github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3
go: downloading github.com/bodgit/sevenzip v1.6.0
go: downloading github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
go: downloading github.com/klauspost/compress v1.17.11
go: downloading github.com/klauspost/pgzip v1.2.6
go: downloading github.com/minio/minlz v1.0.0
go: downloading github.com/nwaples/rardecode/v2 v2.1.0
go: downloading github.com/pierrec/lz4/v4 v4.1.21
go: downloading github.com/sorairolake/lzip-go v0.3.5
go: downloading github.com/therootcompany/xz v1.0.1
go: downloading github.com/ulikunitz/xz v0.5.12
go: downloading github.com/spf13/cast v1.7.1
go: downloading github.com/subosito/gotenv v1.6.0
go: downloading github.com/mitchellh/mapstructure v1.5.0
go: downloading github.com/fsnotify/fsnotify v1.8.0
go: downloading github.com/sagikazarmark/slog-shim v0.1.0
go: downloading github.com/spf13/afero v1.12.0
go: downloading github.com/hashicorp/hcl v1.0.0
go: downloading gopkg.in/ini.v1 v1.67.0
go: downloading github.com/magiconair/properties v1.8.9
go: downloading github.com/pelletier/go-toml/v2 v2.2.3
go: downloading gopkg.in/yaml.v3 v3.0.1
go: downloading dario.cat/mergo v1.0.1
go: downloading github.com/Masterminds/goutils v1.1.1
go: downloading github.com/Masterminds/semver/v3 v3.3.0
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/huandu/xstrings v1.5.0
go: downloading github.com/mitchellh/copystructure v1.2.0
go: downloading github.com/shopspring/decimal v1.4.0
go: downloading golang.org/x/crypto v0.35.0
go: downloading github.com/lucasb-eyer/go-colorful v1.2.0
go: downloading github.com/mattn/go-runewidth v0.0.14
go: downloading github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68
go: downloading github.com/muesli/termenv v0.15.1
go: downloading github.com/mattn/go-colorable v0.1.14
go: downloading github.com/bodgit/plumbing v1.3.0
go: downloading github.com/bodgit/windows v1.0.1
go: downloading github.com/hashicorp/go-multierror v1.1.1
go: downloading go4.org v0.0.0-20230225012048-214862532bf5
go: downloading github.com/mitchellh/reflectwalk v1.0.2
go: downloading golang.org/x/sys v0.30.0
go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/hashicorp/errwrap v1.1.0
go: downloading github.com/rivo/uniseg v0.2.0
go: downloading github.com/aymanbagabas/go-osc52/v2 v2.0.1
12:27AM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
12:27AM INF 12 commits scanned.
12:27AM INF scanned ~8185321 bytes (8.19 MB) in 2.54s
12:27AM INF no leaks found
suite: gitleaks took 38s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m30.450000s 0m3.810000s
--- shard 11 of 48 ---
==> shard 11 of 48: make browser.internal.harness
./hack/gotest.sh browser 'internal.harness'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: ./internal/harness/ runs its 10 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/harness	5.850s
suite: browser.internal.harness took 37s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.980000s 0m4.000000s
--- shard 12 of 48 ---
==> shard 12 of 48: make browser.check~1of13
./hack/gotest.sh browser 'check~1of13'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 1 of 13 of ./check/ runs 14 of 204 browser tests
ok  	amadan.net/rastrillo/fiesta/check	81.431s
suite: browser.check~1of13 took 118s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m58.930000s 0m16.000000s
--- shard 13 of 48 ---
==> shard 13 of 48: make browser.check~2of13
./hack/gotest.sh browser 'check~2of13'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 2 of 13 of ./check/ runs 15 of 204 browser tests
--- FAIL: TestEveryOneOhExampleRendersAsItDid4of4 (62.70s)
    oneoh_browser_test.go:318: 1.0.0's api-tokens/just-created-the-secret-shown-once at 390px, left to right, light, screen renders differently under the tree's CSS: pixel 372,18 differs by 1
FAIL
FAIL	amadan.net/rastrillo/fiesta/check	83.152s
FAIL
make[1]: *** [Makefile:234: browser.check~2of13] Error 1
suite: browser.check~2of13 took 120s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m59.930000s 0m15.900000s
make: *** [Makefile:76: suite] Error 2

amadan: step suite shard 13 of 48 failed: exit status 2
--- shard 14 of 48 ---
==> shard 14 of 48: make browser.check~3of13
./hack/gotest.sh browser 'check~3of13'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 3 of 13 of ./check/ runs 15 of 204 browser tests
ok  	amadan.net/rastrillo/fiesta/check	82.449s
suite: browser.check~3of13 took 120s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m57.430000s 0m15.430000s
--- shard 15 of 48 ---
==> shard 15 of 48: make browser.check~4of13
./hack/gotest.sh browser 'check~4of13'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 4 of 13 of ./check/ runs 15 of 204 browser tests
ok  	amadan.net/rastrillo/fiesta/check	84.597s
suite: browser.check~4of13 took 122s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m1.260000s 0m17.860000s
--- shard 16 of 48 ---
==> shard 16 of 48: make browser.check~5of13
./hack/gotest.sh browser 'check~5of13'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 5 of 13 of ./check/ runs 15 of 204 browser tests
ok  	amadan.net/rastrillo/fiesta/check	83.687s
suite: browser.check~5of13 took 121s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m0.760000s 0m14.250000s
--- shard 17 of 48 ---
==> shard 17 of 48: make browser.check~6of13
./hack/gotest.sh browser 'check~6of13'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 6 of 13 of ./check/ runs 16 of 204 browser tests
ok  	amadan.net/rastrillo/fiesta/check	87.247s
suite: browser.check~6of13 took 125s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m58.790000s 0m16.000000s
--- shard 18 of 48 ---
==> shard 18 of 48: make browser.check~7of13
./hack/gotest.sh browser 'check~7of13'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 7 of 13 of ./check/ runs 16 of 204 browser tests
ok  	amadan.net/rastrillo/fiesta/check	87.082s
suite: browser.check~7of13 took 124s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m3.360000s 0m14.870000s
--- shard 19 of 48 ---
==> shard 19 of 48: make browser.check~8of13
./hack/gotest.sh browser 'check~8of13'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 8 of 13 of ./check/ runs 16 of 204 browser tests
ok  	amadan.net/rastrillo/fiesta/check	82.310s
suite: browser.check~8of13 took 119s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m54.410000s 0m15.860000s
--- shard 20 of 48 ---
==> shard 20 of 48: make browser.check~9of13
./hack/gotest.sh browser 'check~9of13'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 9 of 13 of ./check/ runs 16 of 204 browser tests
ok  	amadan.net/rastrillo/fiesta/check	85.953s
suite: browser.check~9of13 took 124s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m55.840000s 0m15.580000s
--- shard 21 of 48 ---
==> shard 21 of 48: make browser.check~10of13
./hack/gotest.sh browser 'check~10of13'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 10 of 13 of ./check/ runs 16 of 204 browser tests
ok  	amadan.net/rastrillo/fiesta/check	91.809s
suite: browser.check~10of13 took 129s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m7.040000s 0m14.640000s
--- shard 22 of 48 ---
==> shard 22 of 48: make browser.check~11of13
./hack/gotest.sh browser 'check~11of13'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 11 of 13 of ./check/ runs 16 of 204 browser tests
ok  	amadan.net/rastrillo/fiesta/check	85.789s
suite: browser.check~11of13 took 123s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m2.770000s 0m19.850000s
--- shard 23 of 48 ---
==> shard 23 of 48: make browser.check~12of13
./hack/gotest.sh browser 'check~12of13'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 12 of 13 of ./check/ runs 17 of 204 browser tests
ok  	amadan.net/rastrillo/fiesta/check	81.025s
suite: browser.check~12of13 took 118s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m48.540000s 0m12.560000s
--- shard 24 of 48 ---
==> shard 24 of 48: make browser.check~13of13
./hack/gotest.sh browser 'check~13of13'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 13 of 13 of ./check/ runs 17 of 204 browser tests
ok  	amadan.net/rastrillo/fiesta/check	79.733s
suite: browser.check~13of13 took 117s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.750000s 0m12.320000s
--- shard 25 of 48 ---
==> shard 25 of 48: make browser.internal.gallery~1of18
./hack/gotest.sh browser 'internal.gallery~1of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	35.345s
suite: browser.internal.gallery~1of18 took 71s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m31.240000s 0m5.290000s
--- shard 26 of 48 ---
==> shard 26 of 48: make browser.internal.gallery~2of18
./hack/gotest.sh browser 'internal.gallery~2of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	51.567s
suite: browser.internal.gallery~2of18 took 88s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.040000s 0m7.000000s
--- shard 27 of 48 ---
==> shard 27 of 48: make browser.internal.gallery~3of18
./hack/gotest.sh browser 'internal.gallery~3of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	59.916s
suite: browser.internal.gallery~3of18 took 96s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.040000s 0m7.050000s
--- shard 28 of 48 ---
==> shard 28 of 48: make browser.internal.gallery~4of18
./hack/gotest.sh browser 'internal.gallery~4of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	59.090s
suite: browser.internal.gallery~4of18 took 95s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.980000s 0m8.600000s
--- shard 29 of 48 ---
==> shard 29 of 48: make browser.internal.gallery~5of18
./hack/gotest.sh browser 'internal.gallery~5of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 5 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	61.804s
suite: browser.internal.gallery~5of18 took 98s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m33.840000s 0m7.010000s
--- shard 30 of 48 ---
==> shard 30 of 48: make browser.internal.gallery~6of18
./hack/gotest.sh browser 'internal.gallery~6of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 6 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	54.971s
suite: browser.internal.gallery~6of18 took 91s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m34.720000s 0m6.790000s
--- shard 31 of 48 ---
==> shard 31 of 48: make browser.internal.gallery~7of18
./hack/gotest.sh browser 'internal.gallery~7of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 7 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	55.075s
suite: browser.internal.gallery~7of18 took 91s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m30.850000s 0m5.020000s
--- shard 32 of 48 ---
==> shard 32 of 48: make browser.internal.gallery~8of18
./hack/gotest.sh browser 'internal.gallery~8of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 8 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	59.409s
suite: browser.internal.gallery~8of18 took 96s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m34.250000s 0m7.360000s
--- shard 33 of 48 ---
==> shard 33 of 48: make browser.internal.gallery~9of18
./hack/gotest.sh browser 'internal.gallery~9of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 9 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	66.201s
suite: browser.internal.gallery~9of18 took 102s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m42.480000s 0m10.300000s
--- shard 34 of 48 ---
==> shard 34 of 48: make browser.internal.gallery~10of18
./hack/gotest.sh browser 'internal.gallery~10of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 10 of 18 of ./internal/gallery/ runs 2 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	61.213s
suite: browser.internal.gallery~10of18 took 97s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m32.960000s 0m5.440000s
--- shard 35 of 48 ---
==> shard 35 of 48: make browser.internal.gallery~11of18
./hack/gotest.sh browser 'internal.gallery~11of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 11 of 18 of ./internal/gallery/ runs 2 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	62.681s
suite: browser.internal.gallery~11of18 took 99s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m32.670000s 0m5.590000s
--- shard 36 of 48 ---
==> shard 36 of 48: make browser.internal.gallery~12of18
./hack/gotest.sh browser 'internal.gallery~12of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 12 of 18 of ./internal/gallery/ runs 2 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	63.632s
suite: browser.internal.gallery~12of18 took 99s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m38.550000s 0m7.990000s
--- shard 37 of 48 ---
==> shard 37 of 48: make browser.internal.gallery~13of18
./hack/gotest.sh browser 'internal.gallery~13of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 13 of 18 of ./internal/gallery/ runs 3 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	68.168s
suite: browser.internal.gallery~13of18 took 104s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.310000s 0m7.870000s
--- shard 38 of 48 ---
==> shard 38 of 48: make browser.internal.gallery~14of18
./hack/gotest.sh browser 'internal.gallery~14of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 14 of 18 of ./internal/gallery/ runs 4 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	67.593s
suite: browser.internal.gallery~14of18 took 104s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.830000s 0m7.930000s
--- shard 39 of 48 ---
==> shard 39 of 48: make browser.internal.gallery~15of18
./hack/gotest.sh browser 'internal.gallery~15of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 15 of 18 of ./internal/gallery/ runs 4 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	67.140s
suite: browser.internal.gallery~15of18 took 104s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.120000s 0m8.200000s
--- shard 40 of 48 ---
==> shard 40 of 48: make browser.internal.gallery~16of18
./hack/gotest.sh browser 'internal.gallery~16of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 16 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	65.034s
suite: browser.internal.gallery~16of18 took 101s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.320000s 0m7.070000s
--- shard 41 of 48 ---
==> shard 41 of 48: make browser.internal.gallery~17of18
./hack/gotest.sh browser 'internal.gallery~17of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 17 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	68.315s
suite: browser.internal.gallery~17of18 took 105s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.040000s 0m8.350000s
--- shard 42 of 48 ---
==> shard 42 of 48: make browser.internal.gallery~18of18
./hack/gotest.sh browser 'internal.gallery~18of18'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 18 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	68.112s
suite: browser.internal.gallery~18of18 took 104s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.730000s 0m7.840000s
--- shard 43 of 48 ---
==> shard 43 of 48: make browser.internal.gallery.history
./hack/gotest.sh browser 'internal.gallery.history'
gotest: on ip-172-31-22-36, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: ./internal/gallery/history/ runs its 3 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery/history	35.111s
suite: browser.internal.gallery.history took 67s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m39.890000s 0m4.770000s
--- shard 44 of 48 ---
==> shard 44 of 48: make browser.internal.www~1of4
./hack/gotest.sh browser 'internal.www~1of4'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 4 of ./internal/www/ runs 4 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	70.327s
suite: browser.internal.www~1of4 took 104s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m50.430000s 0m4.970000s
--- shard 45 of 48 ---
==> shard 45 of 48: make browser.internal.www~2of4
./hack/gotest.sh browser 'internal.www~2of4'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 4 of ./internal/www/ runs 6 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	69.349s
suite: browser.internal.www~2of4 took 103s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m4.740000s 0m5.620000s
--- shard 46 of 48 ---
==> shard 46 of 48: make browser.internal.www~3of4
./hack/gotest.sh browser 'internal.www~3of4'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 4 of ./internal/www/ runs 6 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	82.223s
suite: browser.internal.www~3of4 took 116s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m11.480000s 0m5.960000s
--- shard 47 of 48 ---
==> shard 47 of 48: make browser.internal.www~4of4
./hack/gotest.sh browser 'internal.www~4of4'
gotest: on ip-172-31-22-36, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 4 of ./internal/www/ runs 6 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	78.047s
suite: browser.internal.www~4of4 took 112s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
1m5.170000s 0m5.910000s

amadan: job failed at step suite shard 13 of 48 — its output is above, not at the tail of this log
diff --git a/check/baseline_test.go b/check/baseline_test.go
index 9aae64b..dae5747 100644
--- a/check/baseline_test.go
+++ b/check/baseline_test.go
@@ -6,9 +6,14 @@ import (
"os"
"path/filepath"
"reflect"
+ "regexp"
"sort"
"strings"
"testing"
+
+ xhtml "golang.org/x/net/html"
+
+ "amadan.net/rastrillo/fiesta/internal/markup"
)
// testdata/seed/ is 1.0.0's CSS and examples, copied from the tag
@@ -117,16 +122,27 @@ var (
// error naming it, so a file that grows one fails here rather than
// being half read.
func declarationsOf(file, css string) ([]declaration, error) {
+ decls, _, err := readStylesheet(file, css)
+ return decls, err
+}
+
+// readStylesheet is declarationsOf with the identity of every
+// descriptor and keyframes at-rule as well, in source order
+// ("@keyframes rst-spin", "@property --rst-press"), empty ones
+// included: an empty @keyframes rst-spin {} says nothing a declaration
+// could record, and still replaces 1.0's spinner on every page.
+func readStylesheet(file, css string) ([]declaration, []string, error) {
r := cssReader{file: file}
if err := r.rules(stripCSSComments(css), nil); err != nil {
- return nil, fmt.Errorf("%s: %w", file, err)
+ return nil, nil, fmt.Errorf("%s: %w", file, err)
}
- return r.out, nil
+ return r.out, r.atRules, nil
}
type cssReader struct {
- file string
- out []declaration
+ file string
+ out []declaration
+ atRules []string
}
// cssItem is one statement of a rule list: its prelude, and its block
@@ -164,12 +180,14 @@ func (r *cssReader) rules(src string, context []string) error {
return err
}
case descriptorRules[name]:
+ r.atRules = append(r.atRules, prelude)
decls, err := cssDeclarations(it.block)
if err != nil {
return fmt.Errorf("%s: %w", prelude, err)
}
r.add(inner, []string{""}, decls)
case name == "keyframes":
+ r.atRules = append(r.atRules, prelude)
frames, err := cssItems(it.block)
if err != nil {
return err
@@ -342,7 +360,7 @@ func cutImportant(value string) (string, bool) {
// readDeclarations reads one stylesheet from disk, failing the test if
// it is missing, empty or unreadable as declarations.
-func readDeclarations(t *testing.T, path, name string) []declaration {
+func readDeclarations(t *testing.T, path, name string) ([]declaration, []string, string) {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
@@ -351,22 +369,31 @@ func readDeclarations(t *testing.T, path, name string) []declaration {
if len(b) == 0 {
t.Fatalf("%s is empty; an empty stylesheet holds nothing", name)
}
- decls, err := declarationsOf(name, string(b))
+ decls, atRules := parseDeclarations(t, name, string(b))
+ return decls, atRules, string(b)
+}
+
+// parseDeclarations is declarationsOf failing the test on an error or
+// on a stylesheet that reads as nothing.
+func parseDeclarations(t *testing.T, name, css string) ([]declaration, []string) {
+ t.Helper()
+ decls, atRules, err := readStylesheet(name, css)
if err != nil {
t.Fatal(err)
}
if len(decls) == 0 {
t.Fatalf("%s reads as no declarations at all", name)
}
- return decls
+ return decls, atRules
}
// sideBySide is one stylesheet as 1.0.0 shipped it and as the tree has
// it, for each of the five, in a fixed order.
type sideBySide struct {
- src string // the path under css/
- was, is []declaration
- wasBytes string
+ src string // the path under css/
+ was, is []declaration
+ wasAt, isAt []string // readStylesheet's at-rule identities
+ wasBytes string
}
func everyStylesheet(t *testing.T) []sideBySide {
@@ -374,10 +401,9 @@ func everyStylesheet(t *testing.T) []sideBySide {
var out []sideBySide
for _, src := range sortedKeys(baseline) {
seed := baseline[src]
- was := readDeclarations(t, filepath.Join("testdata", "seed", seed), "check/testdata/seed/"+seed)
- is := readDeclarations(t, filepath.Join("..", "css", filepath.FromSlash(src)), "css/"+src)
- b, _ := os.ReadFile(filepath.Join("testdata", "seed", seed))
- out = append(out, sideBySide{src: src, was: was, is: is, wasBytes: string(b)})
+ was, wasAt, wasBytes := readDeclarations(t, filepath.Join("testdata", "seed", seed), "check/testdata/seed/"+seed)
+ is, isAt, _ := readDeclarations(t, filepath.Join("..", "css", filepath.FromSlash(src)), "css/"+src)
+ out = append(out, sideBySide{src: src, was: was, is: is, wasAt: wasAt, isAt: isAt, wasBytes: wasBytes})
}
return out
}
@@ -490,9 +516,14 @@ func TestEveryOneOhDeclarationStillApplies(t *testing.T) {
}
}
-// vocabularyOfBaseline is every name 1.0.0's five stylesheets style:
-// each rst- attribute, attribute and value, and class. A new rule
-// needs a name outside it to be sure no 1.0 page carries it.
+// vocabularyOfBaseline is every name 1.0 markup may carry: each rst-
+// attribute, attribute and value token, and class that 1.0.0's five
+// stylesheets style, that 1.0.0's frozen examples write (an attribute
+// can be in 1.0 markup with no rule of its own, as rst-job is around
+// every progress bar), and that the coverage gate names as hooks or
+// qualifiers. A new rule needs a name outside it to be sure no 1.0
+// page carries it; a name only 1.0's CSS was missing would let
+// [rst-job] [rst-job-bar] restyle every 1.0 job.
func vocabularyOfBaseline(t *testing.T, files []sideBySide) map[string]bool {
t.Helper()
var css []string
@@ -505,9 +536,79 @@ func vocabularyOfBaseline(t *testing.T, files []sideBySide) map[string]bool {
names[class] = true
}
}
+ for name := range unstyledByDesign {
+ names[name] = true
+ }
+ for name := range qualifiedOnly {
+ names[name] = true
+ }
+ root := filepath.Join("testdata", "seed", "examples")
+ pages := 0
+ err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
+ if err != nil || d.IsDir() || filepath.Ext(path) != ".html" {
+ return err
+ }
+ b, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ pages++
+ markupNames(string(b), names)
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if pages < 163 {
+ t.Fatalf("read %d of 1.0.0's example documents under %s, want 163", pages, root)
+ }
return names
}
+// markupNames adds every rst- name one document's tags carry: each
+// attribute, each token of its value, and each rst- class, read also
+// as the attribute it spells.
+func markupNames(doc string, names map[string]bool) {
+ z := xhtml.NewTokenizer(strings.NewReader(doc))
+ for {
+ switch z.Next() {
+ case xhtml.ErrorToken:
+ return
+ case xhtml.StartTagToken, xhtml.SelfClosingTagToken:
+ _, more := z.TagName()
+ for more {
+ var k, v []byte
+ k, v, more = z.TagAttr()
+ key, value := string(k), string(v)
+ switch {
+ case strings.HasPrefix(key, "rst-"):
+ names[key] = true
+ for _, token := range strings.Fields(value) {
+ names[key+"~="+token] = true
+ }
+ case key == "class":
+ for _, class := range strings.Fields(value) {
+ if !strings.HasPrefix(class, "rst-") {
+ continue
+ }
+ names[class] = true
+ if name, variant, ok := markup.Attribute(class); ok {
+ names[name] = true
+ if variant != "" {
+ names[name+"~="+variant] = true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+// customProperty is a custom property's name wherever it is written:
+// declared, read through var(), or registered.
+var customProperty = regexp.MustCompile(`--[A-Za-z0-9_-]+`)
+
// TestEveryNewRuleRequiresNewMarkup: everything 1.x adds to the five
// stylesheets styles only markup 1.0 never wrote, so a 1.0 page is
// drawn as it was. The 1.0 declarations are matched first, under
@@ -524,14 +625,39 @@ func vocabularyOfBaseline(t *testing.T, files []sideBySide) map[string]bool {
//
// Every newOnOldMarkup entry is used.
func TestEveryNewRuleRequiresNewMarkup(t *testing.T) {
- files := everyStylesheet(t)
+ for _, why := range refusedAdditions(t, everyStylesheet(t)) {
+ t.Error(why)
+ }
+}
+
+// refusedAdditions is TestEveryNewRuleRequiresNewMarkup over any five
+// stylesheets side by side: one message for each record the gate
+// refuses, and for each newOnOldMarkup entry no record used.
+func refusedAdditions(t *testing.T, files []sideBySide) []string {
+ t.Helper()
+ var refused []string
names := vocabularyOfBaseline(t, files)
- oldProps, oldAtRules := map[string]bool{}, map[string]bool{}
+ // oldProps is every custom property 1.0.0 declares, reads or
+ // registers: a 1.x registration or :root value for one changes
+ // what 1.0's own rules compute (an inherits: false on --rst-sp-4
+ // would reset 1.0's spacing), so only a name 1.0 never wrote is new.
+ oldProps, oldAtRules := map[string]bool{}, map[string]int{}
+ for _, f := range files {
+ for _, p := range customProperty.FindAllString(stripCSSComments(f.wasBytes), -1) {
+ oldProps[p] = true
+ }
+ for _, at := range f.wasAt {
+ oldAtRules[at]++
+ }
+ }
+ newAtRules := map[string]int{}
for _, f := range files {
- for _, d := range f.was {
- oldProps[d.Property] = true
- for _, at := range strings.Split(d.Context, " > ") {
- oldAtRules[at] = true
+ for _, at := range f.isAt {
+ if newAtRules[at]++; newAtRules[at] <= oldAtRules[at] {
+ continue
+ }
+ if why := newAtRuleRefused(at, oldProps, oldAtRules); why != "" {
+ refused = append(refused, fmt.Sprintf("css/%s: %s %s", f.src, at, why))
}
}
}
@@ -572,30 +698,95 @@ func TestEveryNewRuleRequiresNewMarkup(t *testing.T) {
continue
}
if why := newRecordRefused(d, names, oldProps, oldAtRules, exceptions, used); why != "" {
- t.Errorf("css/%s: %s is new and %s", f.src, d, why)
+ refused = append(refused, fmt.Sprintf("css/%s: %s is new and %s", f.src, d, why))
}
}
}
for _, sel := range sortedKeys(newOnOldMarkup) {
if !used[sel] {
- t.Errorf("newOnOldMarkup[%q] is used by no new declaration; an exception for nothing is a hole waiting to be used", sel)
+ refused = append(refused, fmt.Sprintf("newOnOldMarkup[%q] is used by no new declaration; an exception for nothing is a hole waiting to be used", sel))
+ }
+ }
+ return refused
+}
+
+// TestTheNewRuleGateHasNoKnownHoles holds refusedAdditions to the ways
+// past it a review found, each a rule appended to 1.0.0's own
+// tokens.css that changes 1.0 pages where no photograph looks: a value
+// selector that matches 1.0's bare attribute, a registration that
+// changes a property 1.0 already uses, an empty redefinition of a 1.0
+// animation (which still() hides from every photograph), and a new
+// name that only an ancestor 1.0 markup already carries supplies. The
+// last four are additions the gate must let through, so the test
+// cannot pass by refusing everything.
+func TestTheNewRuleGateHasNoKnownHoles(t *testing.T) {
+ for _, c := range []struct {
+ css, names string // the rule appended, and what a refusal names
+ refused bool
+ }{
+ {`@media (min-width: 700px) and (max-width: 750px) { [rst-page=""] { padding-inline: 3rem; } }`, `[rst-page=""]`, true},
+ {`@property --rst-sp-4 { syntax: "<length>"; inherits: false; initial-value: 48px; }`, "@property --rst-sp-4", true},
+ {`@keyframes rst-spin {}`, "@keyframes rst-spin", true},
+ {`@media (min-width: 700px) and (max-width: 750px) { [rst-job] [rst-job-bar] { inline-size: 1px; } }`, "[rst-job] [rst-job-bar]", true},
+ {`@keyframes rst-gate-control { to { opacity: 1; } }`, "@keyframes rst-gate-control", false},
+ {`@property --rst-gate-control { syntax: "<length>"; inherits: false; initial-value: 0px; }`, "@property --rst-gate-control", false},
+ {`:root { --rst-gate-control: 1px; }`, "--rst-gate-control", false},
+ {`[rst-page-foot] [rst-dropdown] { margin: 0; }`, "[rst-page-foot] [rst-dropdown]", false},
+ } {
+ files := everyStylesheet(t)
+ for i := range files {
+ if files[i].src == "tokens.css" {
+ files[i].is, files[i].isAt = parseDeclarations(t, "css/tokens.css", files[i].wasBytes+"\n"+c.css+"\n")
+ }
+ }
+ named := false
+ for _, why := range refusedAdditions(t, files) {
+ if strings.Contains(why, c.names) {
+ named = true
+ t.Logf("refused: %s", why)
+ }
+ }
+ switch {
+ case c.refused && !named:
+ t.Errorf("appending %s to 1.0.0's tokens.css passes the gate; it changes 1.0 pages", c.css)
+ case !c.refused && named:
+ t.Errorf("appending %s to 1.0.0's tokens.css is refused; it styles only markup 1.0 never wrote", c.css)
+ }
+ }
+}
+
+// newAtRuleRefused says why one descriptor or keyframes at-rule 1.0.0
+// does not define is not allowed, or "" when it is. Only a new
+// @keyframes rst-… or a registration of a --rst-… property 1.0 never
+// wrote can add nothing to a 1.0 page; a second definition of one 1.0
+// has replaces it, empty or not.
+func newAtRuleRefused(at string, oldProps map[string]bool, oldAtRules map[string]int) string {
+ switch {
+ case oldAtRules[at] > 0:
+ return "is a second definition of one 1.0.0 has; the later one wins on every 1.0 page"
+ case strings.HasPrefix(at, "@keyframes rst-"):
+ return ""
+ case strings.HasPrefix(at, "@property --rst-"):
+ if p := strings.TrimPrefix(at, "@property "); oldProps[p] {
+ return "is a registration of " + p + ", which 1.0.0 already uses; registering it changes how 1.0's rules compute it"
}
+ return ""
}
+ return "is not a new @keyframes rst-… or @property --rst-…, the only at-rules that add nothing to a 1.0 page"
}
// newRecordRefused says why one record 1.0.0 did not make is not
// allowed, or "" when it is.
-func newRecordRefused(d declaration, names, oldProps, oldAtRules map[string]bool, exceptions map[string]complexSel, used map[string]bool) string {
+func newRecordRefused(d declaration, names, oldProps map[string]bool, oldAtRules map[string]int, exceptions map[string]complexSel, used map[string]bool) string {
contexts := strings.Split(d.Context, " > ")
last := contexts[len(contexts)-1]
if d.Selector == "" || strings.HasPrefix(last, "@keyframes ") {
- newName := strings.HasPrefix(last, "@keyframes rst-") || strings.HasPrefix(last, "@property --rst-")
- if !newName {
- return fmt.Sprintf("belongs to %s, which is not a new @keyframes rst-… or @property --rst-…", last)
- }
- if oldAtRules[last] {
+ if oldAtRules[last] > 0 {
return fmt.Sprintf("belongs to %s, which 1.0.0 already has; a 1.0 page using it would animate or register differently", last)
}
+ if why := newAtRuleRefused(last, oldProps, oldAtRules); why != "" {
+ return "belongs to " + last + ", and that at-rule " + why
+ }
return ""
}
if d.Selector == ":root" && strings.HasPrefix(d.Property, "--rst-") && !oldProps[d.Property] {
diff --git a/check/selectors_test.go b/check/selectors_test.go
index 66a311f..0152d49 100644
--- a/check/selectors_test.go
+++ b/check/selectors_test.go
@@ -6,6 +6,8 @@ import (
"sort"
"strings"
"testing"
+
+ "amadan.net/rastrillo/fiesta/internal/markup"
)
// The grammar every rule 1.x adds is written in, read by a parser
@@ -243,6 +245,11 @@ func (p *selParser) attr() (attrSel, error) {
return a, p.errorf("%q in an attribute selector is outside the grammar", p.s[p.i:])
}
p.i++
+ // ~= takes one token: an empty or spaced value matches nothing at
+ // all, so the selector could only be a mistake.
+ if a.Op == "~=" && (a.Value == "" || len(strings.Fields(a.Value)) != 1 || strings.TrimSpace(a.Value) != a.Value) {
+ return a, p.errorf("[%s~=%q] matches nothing", a.Name, a.Value)
+ }
return a, nil
}
@@ -335,16 +342,12 @@ func requiresNewMarkup(sel string, baseline map[string]bool) (bool, string) {
func requiresNew(c complexSel, baseline map[string]bool, partial *[]string) bool {
for _, cp := range c.Compounds {
for _, class := range cp.Classes {
- if strings.HasPrefix(class, "rst-") && !baseline[class] && !baseline["."+class] {
+ if strings.HasPrefix(class, "rst-") && newClass(class, baseline) {
return true
}
}
for _, a := range cp.Attrs {
- key := a.Name
- if a.Op != "" {
- key += "~=" + a.Value
- }
- if strings.HasPrefix(a.Name, "rst-") && !baseline[key] {
+ if strings.HasPrefix(a.Name, "rst-") && newAttr(a, baseline) {
return true
}
}
@@ -367,6 +370,42 @@ func requiresNew(c complexSel, baseline map[string]bool, partial *[]string) bool
return false
}
+// newAttr reports whether an attribute selector needs markup 1.0 never
+// wrote, read by what it matches rather than how it is spelt: a bare
+// [a] and an empty [a=""] need only the attribute, which <main rst-page>
+// carries; a value needs every one of its tokens, so [a="v"] is new
+// exactly when [a~="v"] is.
+func newAttr(a attrSel, baseline map[string]bool) bool {
+ tokens := strings.Fields(a.Value)
+ if a.Op == "" || len(tokens) == 0 {
+ return !baseline[a.Name]
+ }
+ for _, token := range tokens {
+ if !baseline[a.Name+"~="+token] {
+ return true
+ }
+ }
+ return false
+}
+
+// newClass reports whether a class needs markup 1.0 never wrote. The
+// class is read as the attribute it spells (markup.Attribute), so
+// .rst-job and [rst-job] are one name, and a 1.0 page that writes
+// either carries it.
+func newClass(class string, baseline map[string]bool) bool {
+ if baseline[class] || baseline["."+class] {
+ return false
+ }
+ name, variant, ok := markup.Attribute(class)
+ switch {
+ case !ok:
+ return true
+ case variant == "":
+ return !baseline[name]
+ }
+ return !baseline[name+"~="+variant]
+}
+
// relativeText writes a parsed alternative back as a selector, for a
// reason a reader can find in the CSS.
func relativeText(c complexSel) string {
@@ -440,6 +479,10 @@ func TestOnlyNewMarkupPassesTheGrammar(t *testing.T) {
{"[rst-page]:not([rst-seg-tabs] > button)", false, "positively requires"},
{"* [rst-snippet]", false, "`*`"},
{"[rst-page]:has(> [rst-snippet], > [rst-row])", false, "> [rst-row]"},
+ {`[rst-page=""]`, false, "positively requires"},
+ {`[rst-page=" "]`, false, "positively requires"},
+ {`[rst-dropdown~=""]`, false, "matches nothing"},
+ {`.rst-page[rst-row]`, false, "positively requires"},
{"[rst-snippet]", true, ""},
{"[rst-page-foot]:not(:has(> *))", true, ""},
{`[rst-row]:has(> [rst-status~="inline"]) > [rst-status]`, true, ""},
diff --git a/internal/gallery/gallery_test.go b/internal/gallery/gallery_test.go
index 9a0231e..6ce9f6e 100644
--- a/internal/gallery/gallery_test.go
+++ b/internal/gallery/gallery_test.go
@@ -453,15 +453,28 @@ func TestHighlightColoursPassAgainstTheCodeBackground(t *testing.T) {
// header lifts the menu and not the row around it. A lifted
// <footer rst-page-foot> would put the page's foot row inside the
// gallery's top bar, styled as a foot. The tree is fiesta.FS() with the
-// standalone state's body wrapped in that row, rendered for Day in
-// English, which is all the header needs.
+// standalone state's body rebuilt as its menu alone inside one footer
+// row, whatever row the live example already shows it in, rendered for
+// Day in English, which is all the header needs.
func TestTheHeaderLiftsOnlyTheSchemeMenu(t *testing.T) {
fsys := copyFS(t, fiesta.FS())
const file = "examples/scheme-menu/standalone.html"
doc := string(fsys[file].Data)
- wrapped := strings.Replace(strings.Replace(doc, "<body>\n", "<body>\n<footer rst-page-foot>\n", 1), "\n</body>", "\n</footer>\n</body>", 1)
- if strings.Count(wrapped, "rst-page-foot") != 1 || !strings.Contains(wrapped, "</footer>\n</body>") {
- t.Fatalf("%s no longer has the <body> lines this test wraps", file)
+ body, err := example.Markup(fsys, "scheme-menu", "standalone")
+ if err != nil {
+ t.Fatal(err)
+ }
+ scheme, err := example.Element(body, "rst-scheme")
+ if err != nil {
+ t.Fatal(err)
+ }
+ open, end := strings.Index(doc, "<body>"), strings.Index(doc, "</body>")
+ if open < 0 || end < open {
+ t.Fatalf("%s has no <body> and </body> to rebuild between", file)
+ }
+ wrapped := doc[:open] + "<body>\n<footer rst-page-foot>\n" + scheme + "\n</footer>\n" + doc[end:]
+ if n := strings.Count(wrapped, "rst-page-foot"); n != 1 {
+ t.Fatalf("the rebuilt %s carries rst-page-foot %d times, want once: the menu itself holds a footer row", file, n)
}
fsys[file] = &fstest.MapFile{Data: []byte(wrapped)}
s := newTestSite(t, fsys, "")