Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions test/fixtures/tracker_id_baseline.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Tracker ids already present in lib/.
# Read by the "no lib/ file gains a tracker id" test in
# test/statifier_examples/tracker_id_test.exs, which defines the id shape.
# One line per file and id: the file, the id, and how many times the file
# carries it, tab-separated. A new id is refused, not added here; a line is
# lowered or deleted when an id leaves its file.
lib/statifier_examples/application.ex se-1cl 1
lib/statifier_examples/card_auth.ex se-0u1 1
lib/statifier_examples/card_auth.ex se-2ox 1
lib/statifier_examples/card_auth.ex se-bv9 1
lib/statifier_examples/card_auth.ex se-ihm 1
lib/statifier_examples/card_auth/legacy_check.ex se-bv9 1
lib/statifier_examples/charts.ex se-06z 2
lib/statifier_examples/charts.ex se-4dt 3
lib/statifier_examples/charts.ex se-6ag 1
lib/statifier_examples/charts/async_calls.ex se-d74 1
lib/statifier_examples/charts/durable.ex se-29d 1
lib/statifier_examples/charts/durable.ex se-4dt 3
lib/statifier_examples/charts/durable.ex se-6ag 2
lib/statifier_examples/charts/durable.ex se-cqr 1
lib/statifier_examples/charts/durable.ex se-j87 1
lib/statifier_examples/charts/durable.ex se-w4i 2
lib/statifier_examples/charts/durable.ex sp-2yx 1
lib/statifier_examples/charts/execution.ex se-0ay 1
lib/statifier_examples/charts/execution.ex se-29d 1
lib/statifier_examples/charts/execution.ex se-b2f 1
lib/statifier_examples/charts/fan_out.ex sb-kha0 1
lib/statifier_examples/charts/fan_out.ex se-j87 1
lib/statifier_examples/charts/fixture.ex se-06z 1
lib/statifier_examples/charts/step.ex se-06z 1
lib/statifier_examples/charts/subchart.ex se-4dt 1
lib/statifier_examples/charts/subchart.ex se-6ag 1
lib/statifier_examples/charts/subchart.ex st-pvpz 1
lib/statifier_examples/charts/sync_adapter.ex se-4dt 2
lib/statifier_examples/documents.ex se-1cl 1
lib/statifier_examples/persistence.ex se-1tro 1
lib/statifier_examples/persistence.ex se-6ag 1
lib/statifier_examples/persistence.ex se-j87 1
lib/statifier_examples/persistence.ex sp-11w 1
lib/statifier_examples/signup.ex st-pvpz 1
lib/statifier_examples/signup/handlers.ex se-dyo 1
lib/statifier_examples/signup/invites.ex se-j87 1
lib/statifier_examples/signup/journey.ex se-19h 1
lib/statifier_examples/signup/journey.ex se-e68 2
lib/statifier_examples/signup/journey.ex se-w4i 1
lib/statifier_examples/signup/promotion.ex se-j87 1
lib/statifier_examples/signup/screens.ex se-19h 1
lib/statifier_examples/signup/screens.ex se-e68 1
lib/statifier_examples/signup/signup_step.ex se-1mc 1
lib/statifier_examples_web/components/signup_elements.ex se-19h 1
lib/statifier_examples_web/live/editor_live.ex se-1cl 1
lib/statifier_examples_web/live/signup_screens_live.ex se-19h 2
150 changes: 150 additions & 0 deletions test/statifier_examples/tracker_id_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
defmodule StatifierExamples.TrackerIdTest do
@moduledoc """
No file under `lib/` gains a tracker id.

A tracker id names an issue in one of the family's trackers: a repository
prefix, a dash, and three or four base36 characters (`se-` then `06z`,
say). In a moduledoc or a comment it points a reader at an issue instead
of saying what the code does and why, and the issue text is not shipped
with the code. Write the substance instead.

## Mechanism: a committed baseline

`lib/` already carries such ids. Each one is counted in the committed
baseline, `test/fixtures/tracker_id_baseline.txt`: per file, the id and
how many times the file carries it. A count above the baseline fails,
naming the file and the id; a count below it fails too, naming the
baseline line to lower or delete, so an id that leaves a file cannot come
back unseen. An existing occurrence that moves within its file changes no
count. Like `StatifierExamples.PrivateIdTest`, it needs no git history and
runs the same locally and in CI.

## What it does not catch

* An id of a prefix missing from `@prefixes`: a tracker added to the
family later is enforced only once its prefix is listed there.
* An id that spells one of `@css_words`: those are the CSS class names
in `lib/` that share the shape (`sr-only`), excluded by name.
* An author who adds a baseline line beside a new id. The baseline's
own header says a new id is refused, not added there, and a reviewer
reads the diff.
"""
use ExUnit.Case, async: true

# Every tracker prefix in the family, one per repository.
@prefixes ~w(ece enc eq ots pts px rd rs sb sd se sob sp sr st sui)

# CSS class names in lib/ with a tracker id's shape. A class name is an
# English word where an id has minted characters, so it is excluded by
# name; everything else of the shape is an id.
@css_words ~w(only)

@tracker_id Regex.compile!(
"\\b(?:" <>
Enum.join(@prefixes, "|") <>
")-(?!(?:" <> Enum.join(@css_words, "|") <> ")\\b)[a-z0-9]{3,4}\\b"
)

@baseline "test/fixtures/tracker_id_baseline.txt"

defp repo_path(relative), do: __DIR__ |> Path.join("../../#{relative}") |> Path.expand()

# Every file under lib/, globbed at run time rather than listed, so a file
# added later is enforced the moment it exists.
defp lib_files do
root = repo_path("")

root
|> Path.join("lib/**/*")
|> Path.wildcard()
|> Enum.filter(&File.regular?/1)
|> Enum.map(&Path.relative_to(&1, root))
|> Enum.sort()
end

defp tracker_ids(text), do: @tracker_id |> Regex.scan(text) |> Enum.map(&hd/1)

defp tracker_id_counts(files) do
for file <- files,
{id, count} <-
file |> repo_path() |> File.read!() |> tracker_ids() |> Enum.frequencies(),
into: %{},
do: {{file, id}, count}
end

defp read_baseline do
@baseline
|> repo_path()
|> File.read!()
|> String.split("\n", trim: true)
|> Enum.reject(&String.starts_with?(&1, "#"))
|> Map.new(fn line ->
[file, id, count] = String.split(line, "\t")
{{file, id}, String.to_integer(count)}
end)
end

# Every invented id below is assembled from two halves at run time, so
# this file's own source carries none of them.
defp invented(prefix, rest), do: prefix <> rest

# sabotage: drop "only" from @css_words -> red on the `sr-only` negative;
# shorten the id shape to three characters -> red on a four-character
# positive (both run 2026-09-30).
test "the tracker-id pattern matches each prefix's id shape and no class name" do
positives =
for prefix <- @prefixes, rest <- ["zz9", "zz9q"] do
id = invented(prefix <> "-", rest)
{"filed as #{id} last week", id}
end ++
[
{"(#{invented("se-", "zz9")}.2)", invented("se-", "zz9")},
{"Bead `#{invented("sb-", "zz9q")}` builds it", invented("sb-", "zz9q")},
{"# is a second one (#{invented("sp-", "zz9")}).", invented("sp-", "zz9")}
]

for {text, id} <- positives, do: assert(tracker_ids(text) == [id], text)

negatives = [
~s(<span class="sr-only">Actions</span>),
~s(<div class="sb-editor myapp-plan">),
"`--sb-block-accent` and `--sb-block-accent-tint`",
~s(@accent_token "--sb-accent-myapp"),
"the first-run path",
"a post-1998 engine",
"released as v0.32.0",
"ADR-0005 part (iii)"
]

for text <- negatives, do: assert(tracker_ids(text) == [], text)
end

# sabotage: plant an invented tracker id in charts/fixture.ex's moduledoc
# -> red, naming the file and the id; add a second copy of an id
# documents.ex already carries -> red ("2 now, 1 before"); delete the id
# from charts/fan_out.ex's moduledoc -> red on the second assertion
# (all three run 2026-09-30).
test "no lib/ file gains a tracker id" do
baseline = read_baseline()
current = tracker_id_counts(lib_files())

added =
for {{file, id} = key, count} <- current,
count > Map.get(baseline, key, 0),
do: "#{file}: #{id} (#{count} now, #{Map.get(baseline, key, 0)} before)"

gone =
for {{file, id} = key, before} <- baseline,
Map.get(current, key, 0) < before,
do: "#{file}\t#{id}\t#{before}"

assert added == [],
"new tracker ids in lib/ - write the substance instead:\n" <>
Enum.join(Enum.sort(added), "\n")

assert gone == [],
"ids removed since #{@baseline} was written - lower or delete " <>
"these lines there so the id cannot come back unseen:\n" <>
Enum.join(Enum.sort(gone), "\n")
end
end
Loading