Summary
go test -count=N with N > 1 cannot pass on the proxy package. Two tests assert on
process-global metrics counters that they themselves increment, so the second run in the same
process reads the first run's residue and the assertions fail. This is on main today
(51fcd91), independent of any open PR.
Reproduce
$ go test -race -count=2 -run "TestExtractEconomicsAreExported|TestExpandUnresolvedSeriesRender" ./proxy/
--- FAIL: TestExtractEconomicsAreExported (0.00s)
promexport_components_test.go:78: missing from /metrics:
cg_extract_calls_total{outcome="avoided"} 1
promexport_components_test.go:78: missing from /metrics:
cg_extract_calls_total{outcome="suppressed"} 1
promexport_components_test.go:78: missing from /metrics:
cg_extract_gate_declines_total{reason="gate:not_worth_it"} 1
--- FAIL: TestExpandUnresolvedSeriesRender (0.00s)
promexport_coverage_test.go:162: /metrics is missing the line "cg_expand_unresolved_total{reason=\"malformed\"} 0"
promexport_coverage_test.go:162: /metrics is missing the line "cg_expand_unresolved_total{reason=\"missing\"} 0"
FAIL
-count=1 (what CI runs, via make cover) is green, which is why this has stayed invisible.
Cause
Neither test's state lives in the metrics.NewAggregator() it constructs.
-
proxy/promexport_components_test.go:63 — TestExtractEconomicsAreExported.
metrics.RecordExtractionSuppressed / RecordExtractionCacheLookup write through
metrics.xFor(component), which reads and lazily fills the package-global registry
xReg (metrics/extract.go:112). The agg passed to New is not consulted. Run 2 finds the
run-1 counters still there, renders ... 2, and the exact-string strings.Contains(body, "... 1")
assertions miss.
-
proxy/promexport_coverage_test.go:155 — TestExpandUnresolvedSeriesRender. The test's
whole point is that both reasons render at zero before anything breaks, then it increments
them itself to prove the values move. The values come from expand.Unresolved()
(proxy/promexport.go:478), also process-global — deliberately so, per the comment there,
because sourcing them off Snapshot would export a hard-wired 0 forever. So the 0 assertion
is only true on the first run in a process.
The production behaviour is correct in both cases: a proxy is one process with one lifetime, and
process-global counters are the right shape for it. The defect is that the tests read that
global state without owning it.
Why it matters beyond tidiness
-count=N is the tool for shaking out a flake or a race, and it is exactly what a reviewer reaches
for on a concurrency change — #191's own write-up leans on -count=20 and -count=200 runs. On
the proxy package that instrument is unavailable: any -count>1 run fails for reasons unrelated
to whatever is being investigated, so a real intermittent failure is hidden in noise that looks
identical to it. Found while re-running #191's suite at -count=2, where these two failures had to
be chased down and cleared before the actual change could be trusted.
Severity: low impact on shipped behaviour, moderate on the ability to diagnose concurrency bugs.
Fix options
A. Reset the globals per test (smallest). Export test-only resets — metrics.ResetExtractionForTest()
and an equivalent for expand's unresolved counters — and call them at the top of each test (or via
t.Cleanup). Two or three lines per test, no production change. Downside: a test-only export on a
production package, and it stays a footgun for the next test that touches these counters — nothing
makes the requirement visible.
B. Assert relative, not absolute. Read the counter before acting and assert on the delta.
Keeps the globals private and is repeat-safe by construction. Downside: TestExpandUnresolvedSeriesRender
genuinely wants to assert the literal 0 line — that a zero-valued series is rendered at all
rather than omitted (Grafana's "No data" reading as healthy). A delta assertion cannot express
that, so this one needs a reset regardless.
C. Make the counters injectable. Give xReg and expand's counters a per-instance home reachable
from the Options/aggregator the handler already takes, with the global as the default. This is the
structurally right answer and makes the whole family testable without ceremony — and it is a
production refactor across metrics/extract.go and the expand path, well beyond the size of the
problem being solved.
Recommendation: A for expand (the zero-render assertion needs it), B where a delta says the same
thing — then -count=N works on the package without a production refactor. Happy to take a
different call.
Not fixed here
Reported rather than patched: it is a main defect found while reviewing a branch, so it belongs on
its own branch off main rather than riding along in #191, whose diff is test synchronisation.
Assisted-By: Claude Opus 5 (1M context) noreply@anthropic.com
Correction to the base SHA (this issue originally said 3fabf51). The default branch on the
remote is 51fcd91. 3fabf51 is a stale ancestor of it — a local branch in one checkout that was
78 commits behind — and both tests named here were added after it (TestExtractEconomicsAreExported
by d9e2f24/#83, TestExpandUnresolvedSeriesRender by 3ebc65d/#155), so they do not exist in that
tree at all. The reproduction above is unaffected:
it was run against a tree built from the remote default-branch ref, which resolved to 51fcd91,
plus independently against #191's head 999e1bf, whose parent is also 51fcd91. Only the SHA quoted
in the text was wrong. Caught by the reviewer on #191.
(Amended: the note above first said "ten commits" and attributed both tests to #184. Both figures
were mine and both were wrong — the gap is 78 commits (git rev-list --count 3fabf51..51fcd91), and
the tests came from #83 and #155. The "ten" was a truncated git log | head read as the whole range;
the #184 attribution was an inference I never checked. Corrected inline above; the conclusion is
unchanged, since both files are absent at 3fabf51 either way. Caught by the reviewer on #191.)
Summary
go test -count=NwithN > 1cannot pass on theproxypackage. Two tests assert onprocess-global metrics counters that they themselves increment, so the second run in the same
process reads the first run's residue and the assertions fail. This is on
maintoday(
51fcd91), independent of any open PR.Reproduce
-count=1(what CI runs, viamake cover) is green, which is why this has stayed invisible.Cause
Neither test's state lives in the
metrics.NewAggregator()it constructs.proxy/promexport_components_test.go:63—TestExtractEconomicsAreExported.metrics.RecordExtractionSuppressed/RecordExtractionCacheLookupwrite throughmetrics.xFor(component), which reads and lazily fills the package-global registryxReg(metrics/extract.go:112). Theaggpassed toNewis not consulted. Run 2 finds therun-1 counters still there, renders
... 2, and the exact-stringstrings.Contains(body, "... 1")assertions miss.
proxy/promexport_coverage_test.go:155—TestExpandUnresolvedSeriesRender. The test'swhole point is that both reasons render at zero before anything breaks, then it increments
them itself to prove the values move. The values come from
expand.Unresolved()(
proxy/promexport.go:478), also process-global — deliberately so, per the comment there,because sourcing them off
Snapshotwould export a hard-wired 0 forever. So the0assertionis only true on the first run in a process.
The production behaviour is correct in both cases: a proxy is one process with one lifetime, and
process-global counters are the right shape for it. The defect is that the tests read that
global state without owning it.
Why it matters beyond tidiness
-count=Nis the tool for shaking out a flake or a race, and it is exactly what a reviewer reachesfor on a concurrency change — #191's own write-up leans on
-count=20and-count=200runs. Onthe
proxypackage that instrument is unavailable: any-count>1run fails for reasons unrelatedto whatever is being investigated, so a real intermittent failure is hidden in noise that looks
identical to it. Found while re-running #191's suite at
-count=2, where these two failures had tobe chased down and cleared before the actual change could be trusted.
Severity: low impact on shipped behaviour, moderate on the ability to diagnose concurrency bugs.
Fix options
A. Reset the globals per test (smallest). Export test-only resets —
metrics.ResetExtractionForTest()and an equivalent for expand's unresolved counters — and call them at the top of each test (or via
t.Cleanup). Two or three lines per test, no production change. Downside: a test-only export on aproduction package, and it stays a footgun for the next test that touches these counters — nothing
makes the requirement visible.
B. Assert relative, not absolute. Read the counter before acting and assert on the delta.
Keeps the globals private and is repeat-safe by construction. Downside:
TestExpandUnresolvedSeriesRendergenuinely wants to assert the literal
0line — that a zero-valued series is rendered at allrather than omitted (Grafana's "No data" reading as healthy). A delta assertion cannot express
that, so this one needs a reset regardless.
C. Make the counters injectable. Give
xRegand expand's counters a per-instance home reachablefrom the
Options/aggregator the handler already takes, with the global as the default. This is thestructurally right answer and makes the whole family testable without ceremony — and it is a
production refactor across
metrics/extract.goand the expand path, well beyond the size of theproblem being solved.
Recommendation: A for expand (the zero-render assertion needs it), B where a delta says the same
thing — then
-count=Nworks on the package without a production refactor. Happy to take adifferent call.
Not fixed here
Reported rather than patched: it is a
maindefect found while reviewing a branch, so it belongs onits own branch off
mainrather than riding along in #191, whose diff is test synchronisation.Assisted-By: Claude Opus 5 (1M context) noreply@anthropic.com
Correction to the base SHA (this issue originally said
3fabf51). The default branch on theremote is
51fcd91.3fabf51is a stale ancestor of it — a local branch in one checkout that was78 commits behind — and both tests named here were added after it (
TestExtractEconomicsAreExportedby
d9e2f24/#83,TestExpandUnresolvedSeriesRenderby3ebc65d/#155), so they do not exist in thattree at all. The reproduction above is unaffected:
it was run against a tree built from the remote default-branch ref, which resolved to
51fcd91,plus independently against #191's head
999e1bf, whose parent is also51fcd91. Only the SHA quotedin the text was wrong. Caught by the reviewer on #191.
(Amended: the note above first said "ten commits" and attributed both tests to #184. Both figures
were mine and both were wrong — the gap is 78 commits (
git rev-list --count 3fabf51..51fcd91), andthe tests came from #83 and #155. The "ten" was a truncated
git log | headread as the whole range;the #184 attribution was an inference I never checked. Corrected inline above; the conclusion is
unchanged, since both files are absent at
3fabf51either way. Caught by the reviewer on #191.)