review

review patchsets using your default editor
Log | Files | Refs

check_test.odin (41277B)


      1 package check
      2 
      3 import "core:fmt"
      4 import "core:os"
      5 import "core:path/filepath"
      6 import "core:slice"
      7 import "core:strings"
      8 import "core:testing"
      9 import "jm:sh"
     10 
     11 import "../change"
     12 import "../finding"
     13 import "../frontend"
     14 import "../tree"
     15 import "../txt"
     16 
     17 
     18 // dyn is a dynamic array over the items given, for a change built in a
     19 // test.
     20 dyn :: proc(items: []$T) -> [dynamic]T {
     21 	out := make([dynamic]T, context.temp_allocator)
     22 	append(&out, ..items)
     23 	return out
     24 }
     25 
     26 // Pair is one case of a table-driven test.
     27 Pair :: struct($K, $V: typeid) {
     28 	key:   K,
     29 	value: V,
     30 }
     31 
     32 // coupling is a history in which one file changes with one partner.
     33 coupling :: proc(
     34 	a: string,
     35 	a_commits: int,
     36 	b: string,
     37 	b_commits: int,
     38 	shared: int,
     39 ) -> change.Temporal {
     40 	t := change.Temporal {
     41 		commits  = make(map[string]int, context.temp_allocator),
     42 		partners = make(map[string][]change.Partner, context.temp_allocator),
     43 	}
     44 	t.commits[a] = a_commits
     45 	t.commits[b] = b_commits
     46 	partners := make([]change.Partner, 1, context.temp_allocator)
     47 	partners[0] = {b, shared}
     48 	t.partners[a] = partners
     49 	return t
     50 }
     51 
     52 // only keeps the findings whose rule opens with the prefix.
     53 only :: proc(findings: []finding.Finding, prefix: string) -> []finding.Finding {
     54 	out := make([dynamic]finding.Finding, context.temp_allocator)
     55 	for f in findings {
     56 		if strings.has_prefix(f.rule, prefix) {
     57 			append(&out, f)
     58 		}
     59 	}
     60 	return out[:]
     61 }
     62 
     63 rules_of :: proc(findings: []finding.Finding) -> string {
     64 	names := make([dynamic]string, context.temp_allocator)
     65 	for f in findings {
     66 		append(&names, f.rule)
     67 	}
     68 	return strings.join(names[:], ",", context.temp_allocator)
     69 }
     70 
     71 // over runs every check over a change built in the test, with no
     72 // repository behind it.
     73 over :: proc(c: ^change.Change) -> []finding.Finding {
     74 	return run(Scope{c = c}, context.temp_allocator)
     75 }
     76 
     77 // added renders a diff adding the lines to one file, in the shape gather
     78 // produces.
     79 added :: proc(file: string, lines: ..string) -> string {
     80 	b := strings.builder_make(context.temp_allocator)
     81 	fmt.sbprintf(&b, "--- /dev/null\n+++ b/%s\n@@ -0,0 +1,%d @@\n", file, len(lines))
     82 	for l in lines {
     83 		fmt.sbprintf(&b, "+%s\n", l)
     84 	}
     85 	return strings.to_string(b)
     86 }
     87 
     88 licence_line :: "Copyright 2026 Example Corp. All rights reserved. Licensed under the Apache License, Version 2.0.\n"
     89 
     90 good_message :: `review: say which findings the source dismissed
     91 
     92 A dismissal answered the finding above it and was then thrown away with
     93 the finding, so a reading that met a third of the change reported less
     94 than it read. Kept beside the finding they answered, a dismissal is
     95 visible where a silent one was not, and --verbose names the reason it
     96 gave rather than leaving a gap in the list for nobody to explain.
     97 
     98 The window a dismissal covers stays as written: a few lines either side
     99 of the comment, because narrowing it waits on a case that shows the
    100 narrowing losing a finding it should have kept.`
    101 
    102 @(test)
    103 low_entropy_is_reported :: proc(t: ^testing.T) {
    104 	c := change.Change {
    105 		message = strings.repeat("asdf asdf ", 6, context.temp_allocator),
    106 	}
    107 	findings := over(&c)
    108 	testing.expect_value(t, rules_of(findings), "message-low-entropy")
    109 	if len(findings) == 1 {
    110 		f := findings[0]
    111 		testing.expect_value(t, f.job, "static")
    112 		testing.expect_value(t, f.severity, finding.Severity.Must_Fix)
    113 		testing.expect(t, f.verified)
    114 		for want in ([]string{"2.3", "Shannon", "3.2", "phrase repeated"}) {
    115 			testing.expectf(
    116 				t,
    117 				strings.contains(f.message, want),
    118 				"%q missing from: %s",
    119 				want,
    120 				f.message,
    121 			)
    122 		}
    123 	}
    124 }
    125 
    126 @(test)
    127 boilerplate_is_reported :: proc(t: ^testing.T) {
    128 	c := change.Change {
    129 		message = strings.repeat(licence_line, 9, context.temp_allocator),
    130 	}
    131 	findings := over(&c)
    132 	testing.expect_value(t, rules_of(findings), "message-boilerplate")
    133 	if len(findings) == 1 {
    134 		for want in ([]string{"compresses", "20%", "pasted"}) {
    135 			testing.expectf(
    136 				t,
    137 				strings.contains(findings[0].message, want),
    138 				"%q missing from: %s",
    139 				want,
    140 				findings[0].message,
    141 			)
    142 		}
    143 	}
    144 	both := change.Change {
    145 		message = strings.repeat("asdf asdf asdf asdf asdf asdf\n", 20, context.temp_allocator),
    146 	}
    147 	testing.expect_value(t, rules_of(over(&both)), "message-low-entropy,message-boilerplate")
    148 }
    149 
    150 @(test)
    151 a_message_that_says_something_is_silent :: proc(t: ^testing.T) {
    152 	c := change.Change {
    153 		message = good_message,
    154 	}
    155 	testing.expect_value(t, rules_of(over(&c)), "")
    156 	for msg in ([]string{"wip", strings.repeat(licence_line, 4, context.temp_allocator)}) {
    157 		short := change.Change {
    158 			message = msg,
    159 		}
    160 		testing.expectf(t, len(over(&short)) == 0, "%q: got %v", msg, over(&short))
    161 	}
    162 }
    163 
    164 // common_history is a history in which fix and build are common and
    165 // nothing else is.
    166 common_history :: proc(extra: ..string) -> []string {
    167 	h := make([dynamic]string, context.temp_allocator)
    168 	for _ in 0 ..< 60 {
    169 		append(&h, "fix the failing build", "build: fix the build", "fix build")
    170 	}
    171 	for _ in 0 ..< 40 {
    172 		append(&h, ..extra)
    173 	}
    174 	return h[:]
    175 }
    176 
    177 @(test)
    178 common_words_are_reported :: proc(t: ^testing.T) {
    179 	c := change.Change {
    180 		message = "fix build",
    181 		history = common_history(),
    182 		files   = {"auth_service.go"},
    183 		diff    = "+type JWTParser struct{}\n",
    184 	}
    185 	findings := over(&c)
    186 	testing.expect_value(t, rules_of(findings), "message-common-words")
    187 	if len(findings) == 1 {
    188 		for want in ([]string{"fix", "build", "180", "names nothing"}) {
    189 			testing.expectf(
    190 				t,
    191 				strings.contains(findings[0].message, want),
    192 				"%q missing from: %s",
    193 				want,
    194 				findings[0].message,
    195 			)
    196 		}
    197 	}
    198 }
    199 
    200 @(test)
    201 common_words_are_spared :: proc(t: ^testing.T) {
    202 	// A word the history has not used; the change named; a version
    203 	// number; a thin history; git's own subjects.
    204 	unused := change.Change {
    205 		message = "fix the parser deadlock",
    206 		history = common_history(),
    207 		files   = {"auth_service.go"},
    208 	}
    209 	testing.expect_value(t, rules_of(over(&unused)), "")
    210 	named := change.Change {
    211 		message = "update parser",
    212 		history = common_history("update the parser", "parser: update"),
    213 		files   = {"parser.go"},
    214 		diff    = "+func Parse() {}\n",
    215 	}
    216 	testing.expect_value(t, rules_of(over(&named)), "")
    217 	version := change.Change {
    218 		message = "bump to 2.0.26",
    219 		history = common_history("bump version"),
    220 		files   = {"version.go"},
    221 	}
    222 	testing.expect_value(t, rules_of(over(&version)), "")
    223 	thin := change.Change {
    224 		message = "fix build",
    225 		history = common_history()[:99],
    226 		files   = {"auth_service.go"},
    227 	}
    228 	testing.expect_value(t, rules_of(over(&thin)), "")
    229 	for msg in ([]string{"Merge branch 'main' into dev", "Squashed 'vendor/x/' content from branch main"}) {
    230 		merge := change.Change {
    231 			message = msg,
    232 			history = common_history(),
    233 		}
    234 		testing.expectf(t, len(over(&merge)) == 0, "%q: got %v", msg, over(&merge))
    235 	}
    236 }
    237 
    238 @(test)
    239 venting_is_reported :: proc(t: ^testing.T) {
    240 	for msg in ([]string{"whoops", "oops, missed a comma", "damn, ran the formatter with spaces instead of tabs", "WHOOPS, left the debug print in"}) {
    241 		c := change.Change {
    242 			message = msg,
    243 		}
    244 		findings := over(&c)
    245 		testing.expectf(
    246 			t,
    247 			rules_of(findings) == "message-frustration",
    248 			"%q: got %v",
    249 			msg,
    250 			findings,
    251 		)
    252 		if len(findings) == 1 {
    253 			testing.expect(t, strings.contains(findings[0].message, "exclamation"))
    254 		}
    255 	}
    256 	for msg in ([]string{"Change >> behaviour in LLVM to prevent stupid UB", "Respect TERM=dumb in the test runner", "Implement dumb PtrMap", "Temporarily fix the syscall (eventually to be replaced)", "finally fixed the flaky resize test"}) {
    257 		c := change.Change {
    258 			message = msg,
    259 		}
    260 		testing.expectf(
    261 			t,
    262 			len(only(over(&c), "message-frustration")) == 0,
    263 			"%q: got %v",
    264 			msg,
    265 			over(&c),
    266 		)
    267 	}
    268 }
    269 
    270 @(test)
    271 mood_is_checked_and_spared :: proc(t: ^testing.T) {
    272 	for msg in ([]string{"Added readme.", "Fixing the build", "This fixes the crash", "i can't spell", "web: solarized palette at artifact-page contrast", "deps: upgraded the webp"}) {
    273 		c := change.Change {
    274 			message = msg,
    275 		}
    276 		findings := over(&c)
    277 		testing.expectf(
    278 			t,
    279 			rules_of(findings) == "message-not-imperative",
    280 			"%q: got %v",
    281 			msg,
    282 			findings,
    283 		)
    284 		if len(findings) == 1 {
    285 			testing.expect(t, strings.contains(findings[0].message, "imperative mood"))
    286 		}
    287 	}
    288 	for msg in ([]string{"review: measure the message without a model", "icnsify: read icons out of a Windows binary", "Add caching for responses", "Bump to 2.0.x", "review: an eval set, and the criteria tuned against it", "docs: the vocabulary, the voices section, and the three providers", "Speed up the build", "Merge branch 'main' into dev", "Squashed 'vendor/x/' content from branch main"}) {
    289 		c := change.Change {
    290 			message = msg,
    291 		}
    292 		testing.expectf(t, len(over(&c)) == 0, "%q: got %v", msg, over(&c))
    293 	}
    294 }
    295 
    296 varied_words :: proc(n: int) -> string {
    297 	words := make([dynamic]string, context.temp_allocator)
    298 	for i in 0 ..< n {
    299 		append(&words, fmt.tprintf("word%d", i))
    300 	}
    301 	return strings.join(words[:], " ", context.temp_allocator)
    302 }
    303 
    304 @(test)
    305 body_is_owed_spared_and_capped :: proc(t: ^testing.T) {
    306 	owed := change.Change {
    307 		message = "feat: add the thing",
    308 		diff    = strings.repeat("+line\n", 51, context.temp_allocator),
    309 	}
    310 	findings := over(&owed)
    311 	testing.expect_value(t, rules_of(findings), "message-no-body")
    312 	if len(findings) == 1 {
    313 		testing.expect(t, strings.contains(findings[0].message, "carries no body"))
    314 	}
    315 	Case :: struct {
    316 		diff, subject, body: string,
    317 	}
    318 	for c in ([]Case{{strings.repeat("+line\n", 10, context.temp_allocator), "feat: add the thing", ""}, {strings.repeat("+line\n", 500, context.temp_allocator), "feat: add the thing", "one two three four five"}, {strings.repeat("+line\n", 500, context.temp_allocator), "feat: add the thing", varied_words(150)}, {strings.repeat("+line\n", 500, context.temp_allocator), "Merge branch 'main'", ""}, {strings.concatenate({strings.repeat("+x\n", 30, context.temp_allocator), strings.repeat("-x\n", 30, context.temp_allocator)}, context.temp_allocator), "format: run gofmt", ""}}) {
    319 		msg := c.subject
    320 		if c.body != "" {
    321 			msg = strings.concatenate({c.subject, "\n\n", c.body}, context.temp_allocator)
    322 		}
    323 		spared := change.Change {
    324 			message = msg,
    325 			diff    = c.diff,
    326 		}
    327 		testing.expectf(t, len(over(&spared)) == 0, "%q: got %v", c.subject, over(&spared))
    328 	}
    329 	capped := change.Change {
    330 		message = strings.concatenate(
    331 			{"feat: add the thing\n\n", varied_words(151)},
    332 			context.temp_allocator,
    333 		),
    334 		diff    = strings.repeat("+line\n", 10, context.temp_allocator),
    335 	}
    336 	testing.expect_value(t, rules_of(over(&capped)), "message-long-body")
    337 }
    338 
    339 @(test)
    340 temporal_is_reported_and_spared :: proc(t: ^testing.T) {
    341 	coupled := coupling("login.go", 10, "session.go", 9, 9)
    342 	c := change.Change {
    343 		files    = {"login.go"},
    344 		temporal = coupled,
    345 	}
    346 	findings := over(&c)
    347 	testing.expect_value(t, rules_of(findings), "history-coupled-file")
    348 	if len(findings) == 1 {
    349 		for want in ([]string{"login.go", "session.go", "9 of the 10"}) {
    350 			testing.expectf(
    351 				t,
    352 				strings.contains(findings[0].message, want),
    353 				"%q missing from: %s",
    354 				want,
    355 				findings[0].message,
    356 			)
    357 		}
    358 		testing.expect_value(t, findings[0].file, "login.go")
    359 	}
    360 	touched := change.Change {
    361 		files    = {"login.go", "session.go"},
    362 		temporal = coupled,
    363 	}
    364 	testing.expect_value(t, rules_of(over(&touched)), "")
    365 	weak := change.Change {
    366 		files    = {"a.go"},
    367 		temporal = coupling("a.go", 10, "b.go", 10, 5),
    368 	}
    369 	testing.expect_value(t, rules_of(over(&weak)), "")
    370 	thin := change.Change {
    371 		files    = {"a.go"},
    372 		temporal = coupling("a.go", 3, "b.go", 3, 3),
    373 	}
    374 	testing.expect_value(t, rules_of(over(&thin)), "")
    375 }
    376 
    377 @(test)
    378 brand_shaped_words_are_not_identifiers :: proc(t: ^testing.T) {
    379 	for p in ([]Pair(string, bool){{"gRPC", true}, {"iOS", true}, {"macOS", true}, {"eBay", true}, {"getID", false}, {"parseConfig", false}, {"readURL", false}, {"id", false}}) {
    380 		testing.expectf(t, brand_shaped(p.key) == p.value, "%s: %v", p.key, brand_shaped(p.key))
    381 	}
    382 	names := find_all(
    383 		identifier_shaped,
    384 		"x: add `load_settings` and parseConfig() for docs/notes.md",
    385 	)
    386 	testing.expect_value(
    387 		t,
    388 		fmt.tprint(names),
    389 		"[\"`load_settings`\", \"parseConfig\", \"docs/notes.md\"]",
    390 	)
    391 }
    392 
    393 @(test)
    394 suppression_added_is_reported :: proc(t: ^testing.T) {
    395 	// The ignore comment is assembled at runtime so this file does not
    396 	// add a dismissal of its own.
    397 	diff := strings.concatenate(
    398 		{
    399 			"--- a/ico.go\n+++ b/ico.go\n@@ -1,3 +1,5 @@\n package ico\n+//review:",
    400 			"ignore cannot-fail the test can fail\n+//review:",
    401 			"ignore <rule> <why>\n func f() {}\n",
    402 		},
    403 		context.temp_allocator,
    404 	)
    405 	c := change.Change {
    406 		diff = diff,
    407 	}
    408 	findings := collect(Scope{c = &c}, check_suppression_added, context.temp_allocator)
    409 	testing.expect_value(t, len(findings), 1)
    410 	if len(findings) == 1 {
    411 		f := findings[0]
    412 		testing.expect_value(t, f.severity, finding.Severity.Must_Fix)
    413 		testing.expect(t, strings.contains(f.message, "ico.go:2 (cannot-fail)"))
    414 		testing.expect(
    415 			t,
    416 			!strings.contains(f.message, "<rule>"),
    417 			"counted a documented placeholder",
    418 		)
    419 		testing.expect_value(t, f.file, "")
    420 	}
    421 	prose := change.Change {
    422 		diff = strings.concatenate(
    423 			{
    424 				"--- a/readme.md\n+++ b/readme.md\n@@ -1,3 +1,4 @@\n # review\n+`//review:",
    425 				"ignore <rule> <why>`\n done\n",
    426 			},
    427 			context.temp_allocator,
    428 		),
    429 	}
    430 	testing.expect_value(
    431 		t,
    432 		len(collect(Scope{c = &prose}, check_suppression_added, context.temp_allocator)),
    433 		0,
    434 	)
    435 	both := change.Change {
    436 		diff = strings.concatenate(
    437 			{
    438 				"--- a/x_test.go\n+++ b/x_test.go\n@@ -1,2 +1,2 @@\n package x\n+//review:",
    439 				"ignore all tidy\n-func TestA(t *testing.T) {}\n",
    440 			},
    441 			context.temp_allocator,
    442 		),
    443 	}
    444 	got := over(&both)
    445 	testing.expect_value(t, rules_of(got), "suppression-added,test-deleted")
    446 	for f in got {
    447 		testing.expect_value(t, f.severity, finding.Severity.Must_Fix)
    448 	}
    449 }
    450 
    451 @(test)
    452 deleted_tests_are_reported :: proc(t: ^testing.T) {
    453 	c := change.Change {
    454 		diff = "--- a/ico_test.go\n+++ b/ico_test.go\n@@ -1,5 +1,4 @@\n package ico\n-func TestAssemble(t *testing.T) {}\n-func TestWrite(t *testing.T) {}\n+func TestAssembleIcons(t *testing.T) {}\n func f() {}\n",
    455 	}
    456 	findings := collect(Scope{c = &c}, check_deleted_tests, context.temp_allocator)
    457 	testing.expect_value(t, len(findings), 1)
    458 	if len(findings) == 1 {
    459 		f := findings[0]
    460 		testing.expect_value(t, f.rule, "test-deleted")
    461 		testing.expect_value(t, f.file, "ico_test.go")
    462 		testing.expect(t, strings.contains(f.message, "TestWrite"))
    463 		testing.expect(
    464 			t,
    465 			!strings.contains(f.message, "TestAssemble"),
    466 			"the rename was counted as a deletion",
    467 		)
    468 		testing.expect(t, strings.contains(f.fix, "test-deleted"))
    469 	}
    470 	js := change.Change {
    471 		diff = "--- a/web/app.test.ts\n+++ b/web/app.test.ts\n@@ -1,4 +1 @@\n import { it } from \"testing\";\n-it.skip(\"parses icons\", () => {});\n-describe(\"loads\", () => {});\n+export {};\n",
    472 	}
    473 	got := collect(Scope{c = &js}, check_deleted_tests, context.temp_allocator)
    474 	testing.expect_value(t, len(got), 1)
    475 	if len(got) == 1 {
    476 		testing.expect(t, strings.contains(got[0].message, "parses icons"))
    477 		testing.expect(t, strings.contains(got[0].message, "loads"))
    478 	}
    479 	outside := change.Change {
    480 		diff = "--- a/ico.go\n+++ b/ico.go\n@@ -1,3 +1,2 @@\n package ico\n-func TestWrite(w io.Writer) {}\n-func helper() {}\n+func helper() {}\n",
    481 	}
    482 	testing.expect_value(
    483 		t,
    484 		len(collect(Scope{c = &outside}, check_deleted_tests, context.temp_allocator)),
    485 		0,
    486 	)
    487 	inside := change.Change {
    488 		diff = "--- a/ico_test.go\n+++ b/ico_test.go\n@@ -1,3 +1,2 @@\n package ico\n-func TestWrite(t *testing.T) {}\n-func helper() {}\n+func helper() {}\n",
    489 	}
    490 	testing.expect_value(
    491 		t,
    492 		len(collect(Scope{c = &inside}, check_deleted_tests, context.temp_allocator)),
    493 		1,
    494 	)
    495 }
    496 
    497 @(test)
    498 covered_spares_a_rename :: proc(t: ^testing.T) {
    499 	testing.expect(t, covered("TestParseIcons", {"TestParseIconsV2", "TestWrite"}))
    500 	testing.expect(t, !covered("TestParseIcons", {"TestParse", "TestWrite"}))
    501 	testing.expect(t, !covered("TestV2", {"TestV2"}))
    502 	for p in ([]Pair(string, string){{`Deno.test("deno string", () => {`, "deno string"}, {`Deno.test.ignore("deno ignored", () => {`, "deno ignored"}, {`  test.skip("bun skipped", () => {`, "bun skipped"}, {`it.only("focused", () => {`, "focused"}, {`test('node plain', { timeout: 5 }, () => {`, "node plain"}, {`test.concurrent.only("both", async () => {`, "both"}, {`describe("suite", () => {`, "suite"}, {`Deno.test({ name: "object", fn() {`, ""}, {`const test = 1;`, ""}, {`func TestX(t *testing.T) {`, "TestX"}, {`async def test_it(self):`, "test_it"}}) {
    503 		testing.expectf(
    504 			t,
    505 			removed_test_name(p.key) == p.value,
    506 			"%q: got %q, want %q",
    507 			p.key,
    508 			removed_test_name(p.key),
    509 			p.value,
    510 		)
    511 	}
    512 	for path in ([]string{"ico_test.go", "web/app.test.ts", "web/app.spec.js", "tests/x.py", "test_x.py"}) {
    513 		testing.expectf(t, is_test_file(path), "%s is a test file", path)
    514 	}
    515 	for path in ([]string{"ico.go", "web/app.ts", "src/lib.rs"}) {
    516 		testing.expectf(t, !is_test_file(path), "%s is not a test file", path)
    517 	}
    518 }
    519 
    520 @(test)
    521 names_are_measured :: proc(t: ^testing.T) {
    522 	context.allocator = context.temp_allocator
    523 	Stutter :: struct {
    524 		s:     change.Symbol,
    525 		fires: bool,
    526 	}
    527 	for c in ([]Stutter{{{name = "IcoEntry", pkg = "ico", exported = true, file = "ico/ico.go"}, true}, {{name = "exe_kind", pkg = "exe", exported = true, file = "exe/exe.odin"}, true}, {{name = "Time", pkg = "time", exported = true, file = "time/time.go"}, false}, {{name = "Entry", pkg = "ico", exported = true, file = "ico/ico.go"}, false}, {{name = "icoEntry", pkg = "ico", exported = false, file = "ico/ico.go"}, false}, {{name = "MainLoop", pkg = "main", exported = true, file = "main.go"}, false}, {{name = "Iconic", pkg = "ico", exported = true, file = "ico/ico.go"}, false}}) {
    528 		out := make([dynamic]finding.Finding, context.temp_allocator)
    529 		stutter(c.s, &out)
    530 		testing.expectf(t, (len(out) == 1) == c.fires, "%s in %s: %v", c.s.name, c.s.pkg, out)
    531 	}
    532 	for c in ([]Stutter{{{name = "len", file = "x.go"}, true}, {{name = "url", file = "x.go"}, true}, {{name = "Promise", file = "x.ts"}, true}, {{name = "render", file = "x.go"}, false}, {{name = "len", file = "x.ts"}, false}, {{name = "Promise", file = "x.go"}, false}}) {
    533 		out := make([dynamic]finding.Finding, context.temp_allocator)
    534 		shadow(c.s, &out)
    535 		testing.expectf(t, (len(out) == 1) == c.fires, "%s in %s: %v", c.s.name, c.s.file, out)
    536 	}
    537 	for p in ([]Pair(string, bool){{"loadCfg", true}, {"user_mgr", true}, {"BtnLabel", true}, {"msgCount", false}, {"parseURL", false}, {"ctx", false}, {"configure", false}}) {
    538 		out := make([dynamic]finding.Finding, context.temp_allocator)
    539 		abbreviated(change.Symbol{name = p.key, file = "x.go"}, &out)
    540 		testing.expectf(t, (len(out) == 1) == p.value, "%s: %v", p.key, out)
    541 	}
    542 	c := change.Change {
    543 		symbols = dyn(
    544 			[]change.Symbol {
    545 				{name = "IcoEntry", pkg = "ico", exported = true, file = "ico/ico.go", line = 3},
    546 				{name = "cfg", file = "ico/ico.go", line = 9},
    547 				{name = "cfgForTests", file = "ico/ico_test.go", line = 4},
    548 			},
    549 		),
    550 	}
    551 	testing.expect_value(
    552 		t,
    553 		rules_of(collect(Scope{c = &c}, check_names, context.temp_allocator)),
    554 		"no-stutter,abbreviation",
    555 	)
    556 	testing.expect_value(
    557 		t,
    558 		fmt.tprint(txt.split_words("parseHTTPRequest_now", context.temp_allocator)),
    559 		`["parse", "H", "T", "T", "P", "Request", "now"]`,
    560 	)
    561 }
    562 
    563 @(test)
    564 leftovers_are_reported :: proc(t: ^testing.T) {
    565 	Debug :: struct {
    566 		file, line: string,
    567 		severity:   finding.Severity,
    568 		fires:      bool,
    569 	}
    570 	for c in ([]Debug{{"a.ts", "  debugger;", .Consider, true}, {"a.tsx", "  console.log(x)", .Note, true}, {"a.py", "breakpoint()", .Consider, true}, {"a.rs", "let y = dbg!(x);", .Consider, true}, {"a.go", "spew.Dump(x)", .Consider, true}, {"a.go", `fmt.Println("DEBUG", x)`, .Consider, true}, {"a.go", `fmt.Println("done")`, .Note, false}, {"a.go", "debugger := newDebugger()", .Note, false}, {"a.md", "  debugger;", .Note, false}}) {
    571 		ch := change.Change {
    572 			diff = added(c.file, c.line),
    573 		}
    574 		got := collect(Scope{c = &ch}, check_debug_leftovers, context.temp_allocator)
    575 		testing.expectf(t, (len(got) == 1) == c.fires, "%s %q: %v", c.file, c.line, got)
    576 		if len(got) == 1 && c.fires {
    577 			testing.expect_value(t, got[0].severity, c.severity)
    578 		}
    579 	}
    580 	todos := change.Change {
    581 		comments = dyn(
    582 			[]change.Located {
    583 				{text = "TODO handle the empty case", file = "a.go", line = 1},
    584 				{text = "TODO(jack) handle the empty case", file = "a.go", line = 2},
    585 				{text = "FIXME see #42", file = "a.go", line = 3},
    586 				{text = "HACK until PROJ-12 lands", file = "a.go", line = 4},
    587 				{text = "the todo list is rendered here", file = "a.go", line = 5},
    588 			},
    589 		),
    590 	}
    591 	got := collect(Scope{c = &todos}, check_todos, context.temp_allocator)
    592 	testing.expect_value(t, len(got), 1)
    593 	if len(got) == 1 {
    594 		testing.expect_value(t, got[0].line, 1)
    595 	}
    596 	commented := change.Change {
    597 		comments = dyn(
    598 			[]change.Located {
    599 				{text = "x := parse(input);", file = "a.go", line = 1},
    600 				{text = "returns the name (see below)", file = "a.go", line = 5},
    601 				{text = "if err != nil {", file = "a.go", line = 10},
    602 				{text = "return err", file = "a.go", line = 11},
    603 				{text = "}", file = "a.go", line = 12},
    604 				{text = "for the record:", file = "a.go", line = 20},
    605 				{text = "go:generate stringer -type=Kind", file = "a.go", line = 30},
    606 			},
    607 		),
    608 	}
    609 	code := collect(Scope{c = &commented}, check_commented_code, context.temp_allocator)
    610 	testing.expect_value(t, len(code), 2)
    611 	if len(code) == 2 {
    612 		testing.expect_value(t, code[0].line, 1)
    613 		testing.expect_value(t, code[1].line, 10)
    614 	}
    615 	Swallowed :: struct {
    616 		file:  string,
    617 		lines: []string,
    618 		fires: bool,
    619 	}
    620 	for c in ([]Swallowed{{"a.go", {"_ = err"}, true}, {"a.go", {"_, err := f()", "if err != nil {", "\treturn err", "}"}, false}, {"a.ts", {"try { f() } catch (e) {}"}, true}, {"a.ts", {"} catch (e) {", "}"}, true}, {"a.ts", {"} catch (e) {", "  log(e)", "}"}, false}, {"a.js", {"p.catch(() => {})"}, true}, {"a.py", {"except ValueError:", "    pass"}, true}, {"a.py", {"except ValueError: pass"}, true}, {"a.py", {"except ValueError:", "    raise"}, false}}) {
    621 		ch := change.Change {
    622 			diff = added(c.file, ..c.lines),
    623 		}
    624 		found := collect(Scope{c = &ch}, check_swallowed_errors, context.temp_allocator)
    625 		testing.expectf(t, (len(found) == 1) == c.fires, "%s %v: %v", c.file, c.lines, found)
    626 	}
    627 }
    628 
    629 // body is a Go function long enough to compare, built from a name and
    630 // the names of the two values it works on.
    631 body :: proc(name, a, b: string) -> string {
    632 	return fmt.tprintf(
    633 		`func %s(%s []int, %s int) int {
    634 	total := 0
    635 	for _, v := range %s {
    636 		if v > %s {
    637 			total += v
    638 		} else if v == %s {
    639 			total -= v
    640 		} else {
    641 			total++
    642 		}
    643 	}
    644 	if total < 0 {
    645 		return -total
    646 	}
    647 	if total > 1000 {
    648 		return 1000
    649 	}
    650 	for i := 0; i < len(%s); i++ {
    651 		total += i * 2
    652 	}
    653 	return total
    654 }`,
    655 		name,
    656 		a,
    657 		b,
    658 		a,
    659 		b,
    660 		b,
    661 		a,
    662 	)
    663 }
    664 
    665 // small is a wrapper of the shape every wrapper has, too short for a match
    666 // in shape to mean anything, though long enough for an exact copy to.
    667 small :: proc(name, kind: string) -> string {
    668 	return fmt.tprintf(
    669 		`func %s(raw string) (%s, error) {
    670 	var r %s
    671 	if err := json.Unmarshal([]byte(raw), &r); err != nil {
    672 		return %s{}, fmt.Errorf("reading the answer: %%w", err)
    673 	}
    674 	return r, nil
    675 }`,
    676 		name,
    677 		kind,
    678 		kind,
    679 		kind,
    680 	)
    681 }
    682 
    683 @(test)
    684 clones_are_found :: proc(t: ^testing.T) {
    685 	exact := change.Change {
    686 		symbols = dyn(
    687 			[]change.Symbol {
    688 				{
    689 					name = "sumAbove",
    690 					kind = "func",
    691 					file = "b.go",
    692 					line = 10,
    693 					body = body("sumAbove", "xs", "floor"),
    694 				},
    695 			},
    696 		),
    697 		index   = {
    698 			{
    699 				name = "sumAll",
    700 				kind = "func",
    701 				file = "a.go",
    702 				line = 3,
    703 				body = body("sumAll", "xs", "floor"),
    704 			},
    705 		},
    706 	}
    707 	got := collect(Scope{c = &exact}, check_clones, context.temp_allocator)
    708 	testing.expect_value(t, len(got), 1)
    709 	if len(got) == 1 {
    710 		testing.expect_value(t, got[0].rule, "duplicate-body")
    711 		testing.expect_value(t, got[0].severity, finding.Severity.Must_Fix)
    712 		testing.expect(t, strings.contains(got[0].message, "a.go:3"))
    713 		testing.expect_value(t, got[0].symbol, "sumAbove")
    714 	}
    715 	shape := change.Change {
    716 		symbols = dyn(
    717 			[]change.Symbol {
    718 				{
    719 					name = "sumAbove",
    720 					kind = "func",
    721 					file = "b.go",
    722 					line = 10,
    723 					body = body("sumAbove", "rows", "limit"),
    724 				},
    725 			},
    726 		),
    727 		index   = {
    728 			{
    729 				name = "sumAll",
    730 				kind = "func",
    731 				file = "a.go",
    732 				line = 3,
    733 				body = body("sumAll", "xs", "floor"),
    734 			},
    735 		},
    736 	}
    737 	got = collect(Scope{c = &shape}, check_clones, context.temp_allocator)
    738 	testing.expect_value(t, len(got), 1)
    739 	if len(got) == 1 {
    740 		testing.expect_value(t, got[0].severity, finding.Severity.Consider)
    741 	}
    742 	twice := change.Change {
    743 		symbols = dyn(
    744 			[]change.Symbol {
    745 				{
    746 					name = "one",
    747 					kind = "func",
    748 					file = "a.go",
    749 					line = 3,
    750 					body = body("one", "xs", "floor"),
    751 				},
    752 				{
    753 					name = "two",
    754 					kind = "func",
    755 					file = "a.go",
    756 					line = 30,
    757 					body = body("two", "xs", "floor"),
    758 				},
    759 			},
    760 		),
    761 	}
    762 	got = collect(Scope{c = &twice}, check_clones, context.temp_allocator)
    763 	testing.expect_value(t, len(got), 1)
    764 	if len(got) == 1 {
    765 		testing.expect_value(t, got[0].symbol, "two")
    766 	}
    767 	small_or_different := change.Change {
    768 		symbols = dyn(
    769 			[]change.Symbol {
    770 				{
    771 					name = "Size",
    772 					kind = "func",
    773 					file = "b.go",
    774 					line = 10,
    775 					body = "func (e Entry) Size() int { return e.size }",
    776 				},
    777 				{
    778 					name = "other",
    779 					kind = "func",
    780 					file = "b.go",
    781 					line = 20,
    782 					body = strings.concatenate(
    783 						{body("other", "xs", "floor"), "\n// and more\nvar _ = 1"},
    784 						context.temp_allocator,
    785 					),
    786 				},
    787 			},
    788 		),
    789 		index   = {
    790 			{
    791 				name = "Len",
    792 				kind = "func",
    793 				file = "a.go",
    794 				line = 3,
    795 				body = "func (e Entry) Len() int { return e.size }",
    796 			},
    797 			{
    798 				name = "sumAll",
    799 				kind = "func",
    800 				file = "a.go",
    801 				line = 3,
    802 				body = body("sumAll", "xs", "floor"),
    803 			},
    804 		},
    805 	}
    806 	testing.expect_value(
    807 		t,
    808 		len(collect(Scope{c = &small_or_different}, check_clones, context.temp_allocator)),
    809 		0,
    810 	)
    811 	wrapper := change.Change {
    812 		symbols = dyn(
    813 			[]change.Symbol {
    814 				{
    815 					name = "decodeB",
    816 					kind = "func",
    817 					file = "b.go",
    818 					line = 10,
    819 					body = small("decodeB", "verdicts"),
    820 				},
    821 			},
    822 		),
    823 		index   = {
    824 			{
    825 				name = "decodeA",
    826 				kind = "func",
    827 				file = "a.go",
    828 				line = 3,
    829 				body = small("decodeA", "reported"),
    830 			},
    831 		},
    832 	}
    833 	testing.expect_value(
    834 		t,
    835 		len(collect(Scope{c = &wrapper}, check_clones, context.temp_allocator)),
    836 		0,
    837 	)
    838 	wrapper.symbols[0].body = small("decodeB", "reported")
    839 	copied := collect(Scope{c = &wrapper}, check_clones, context.temp_allocator)
    840 	testing.expect_value(t, len(copied), 1)
    841 	if len(copied) == 1 {
    842 		testing.expect_value(t, copied[0].severity, finding.Severity.Must_Fix)
    843 	}
    844 }
    845 
    846 @(test)
    847 normalise_reads_through_comments_and_space :: proc(t: ^testing.T) {
    848 	a := normalise(body("f", "xs", "n"), "f", "go", context.temp_allocator)
    849 	with_comment, _ := strings.replace_all(
    850 		body("f", "xs", "n"),
    851 		"total := 0",
    852 		"total := 0 // start\n\n",
    853 		context.temp_allocator,
    854 	)
    855 	b := normalise(with_comment, "f", "go", context.temp_allocator)
    856 	testing.expect_value(t, a.exact, b.exact)
    857 	testing.expect(t, strings.contains(a.exact, "NAME"))
    858 	testing.expect(t, !strings.contains(a.exact, " f "))
    859 	testing.expect(t, strings.contains(a.structural, "for ID , ID := range ID"))
    860 	toks := lexemes(`x := a.b(0x1F, 2.5, "s\"t", 'c') // c`, context.temp_allocator)
    861 	testing.expect_value(
    862 		t,
    863 		fmt.tprint(toks),
    864 		`["x", ":=", "a", ".", "b", "(", "0x1F", ",", "2.5", ",", "\"s\\\"t\"", ",", "'c'", ")", "/", "/", "c"]`,
    865 	)
    866 	testing.expect_value(
    867 		t,
    868 		strip_comments("a // b\n  # c\nd /* e */ f \"//x\"", context.temp_allocator),
    869 		"a  \n   \nd   f \"//x\"",
    870 	)
    871 }
    872 
    873 @(test)
    874 shape_prose_and_formatting_are_measured :: proc(t: ^testing.T) {
    875 	deep := "func f() {\n\tif a {\n\t\tfor b {\n\t\t\tif c {\n\t\t\t\tswitch d {\n\t\t\t\tcase 1:\n\t\t\t\t\tif e {\n\t\t\t\t\t\tif g { x := \"{\" }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}"
    876 	testing.expect_value(t, nesting(deep), 6)
    877 	testing.expect_value(t, nesting("func f() { return 1 }"), 0)
    878 	testing.expect_value(t, line_count("a\nb\nc"), 3)
    879 	c := change.Change {
    880 		symbols = dyn(
    881 			[]change.Symbol{{name = "f", kind = "func", file = "a.go", line = 1, body = deep}},
    882 		),
    883 	}
    884 	testing.expect_value(
    885 		t,
    886 		rules_of(collect(Scope{c = &c}, check_shape, context.temp_allocator)),
    887 		"nesting-too-deep",
    888 	)
    889 	long := change.Change {
    890 		symbols = dyn(
    891 			[]change.Symbol {
    892 				{
    893 					name = "g",
    894 					kind = "func",
    895 					file = "a.go",
    896 					line = 1,
    897 					body = strings.repeat("x\n", 151, context.temp_allocator),
    898 				},
    899 			},
    900 		),
    901 	}
    902 	testing.expect_value(
    903 		t,
    904 		rules_of(collect(Scope{c = &long}, check_shape, context.temp_allocator)),
    905 		"function-too-long",
    906 	)
    907 
    908 	testing.expect(
    909 		t,
    910 		restates(change.Located{text = "parse the input", below = "func parseInput(x) {\nreturn"}),
    911 	)
    912 	testing.expect(
    913 		t,
    914 		!restates(
    915 			change.Located{text = "parse the input carefully", below = "func parseInput(x) {"},
    916 		),
    917 	)
    918 	testing.expect(t, !restates(change.Located{text = "parse", below = "func parse() {"}))
    919 	testing.expect(
    920 		t,
    921 		!restates(change.Located{text = "TODO parse the input", below = "func parseInput() {"}),
    922 	)
    923 	testing.expect(
    924 		t,
    925 		directive("go:generate x") && directive("see https://x") && !directive("plain prose"),
    926 	)
    927 	prose := change.Change {
    928 		comments = dyn(
    929 			[]change.Located {
    930 				{
    931 					text = "parse the input",
    932 					file = "a.go",
    933 					line = 3,
    934 					below = "func parseInput(x) {",
    935 				},
    936 			},
    937 		),
    938 	}
    939 	testing.expect_value(
    940 		t,
    941 		rules_of(collect(Scope{c = &prose}, check_restating, context.temp_allocator)),
    942 		"comment-restates-code",
    943 	)
    944 
    945 	mixed := change.Change {
    946 		changed    = 100,
    947 		whitespace = 60,
    948 	}
    949 	testing.expect_value(
    950 		t,
    951 		rules_of(collect(Scope{c = &mixed}, check_formatting, context.temp_allocator)),
    952 		"formatting-mixed-in",
    953 	)
    954 	pure := change.Change {
    955 		changed    = 100,
    956 		whitespace = 95,
    957 	}
    958 	testing.expect_value(
    959 		t,
    960 		rules_of(collect(Scope{c = &pure}, check_formatting, context.temp_allocator)),
    961 		"",
    962 	)
    963 	little := change.Change {
    964 		changed    = 30,
    965 		whitespace = 10,
    966 	}
    967 	testing.expect_value(
    968 		t,
    969 		rules_of(collect(Scope{c = &little}, check_formatting, context.temp_allocator)),
    970 		"",
    971 	)
    972 }
    973 
    974 @(test)
    975 tests_are_measured :: proc(t: ^testing.T) {
    976 	testing.expect(
    977 		t,
    978 		assertless(
    979 			change.Function {
    980 				name = "TestX",
    981 				file = "a_test.go",
    982 				body = "func TestX(t *testing.T) {\n\tf()\n}",
    983 			},
    984 		),
    985 	)
    986 	testing.expect(
    987 		t,
    988 		!assertless(
    989 			change.Function {
    990 				name = "TestX",
    991 				file = "a_test.go",
    992 				body = "func TestX(tc *testing.T) {\n\ttc.Fatal(1)\n}",
    993 			},
    994 		),
    995 	)
    996 	testing.expect(
    997 		t,
    998 		!assertless(
    999 			change.Function {
   1000 				name = "TestX",
   1001 				file = "a_test.go",
   1002 				body = "func TestX(t *testing.T) {\n\thelper(t, 1)\n}",
   1003 			},
   1004 		),
   1005 	)
   1006 	testing.expect(
   1007 		t,
   1008 		!assertless(
   1009 			change.Function {
   1010 				name = "BenchmarkX",
   1011 				file = "a_test.go",
   1012 				body = "func BenchmarkX(b *testing.B) {}",
   1013 			},
   1014 		),
   1015 	)
   1016 	testing.expect(
   1017 		t,
   1018 		assertless(
   1019 			change.Function {
   1020 				name = "reads",
   1021 				file = "a.test.ts",
   1022 				body = "test('reads', () => { f() })",
   1023 			},
   1024 		),
   1025 	)
   1026 	testing.expect(
   1027 		t,
   1028 		!assertless(
   1029 			change.Function {
   1030 				name = "reads",
   1031 				file = "a.test.ts",
   1032 				body = "test('reads', () => { expect(f()).toBe(1) })",
   1033 			},
   1034 		),
   1035 	)
   1036 	testing.expect(
   1037 		t,
   1038 		assertless(
   1039 			change.Function {
   1040 				name = "x",
   1041 				file = "a_test.odin",
   1042 				body = "x :: proc(t: ^testing.T) { f() }",
   1043 			},
   1044 		),
   1045 	)
   1046 	testing.expect(
   1047 		t,
   1048 		!assertless(
   1049 			change.Function {
   1050 				name = "x",
   1051 				file = "a_test.odin",
   1052 				body = "x :: proc(t: ^testing.T) { testing.expect(t, f()) }",
   1053 			},
   1054 		),
   1055 	)
   1056 	for p in ([]Pair(string, string){{"assert.True(t, true)", "a constant"}, {"require.NoError(t, nil)", "a constant"}, {"if got != got {", "a value against itself"}, {"assert 1 == 1", "a constant"}, {"self.assertTrue(True)", "a constant"}, {"expect(true).toBe(true)", "a constant"}, {"expect(1).toEqual(1)", "a constant"}, {"expect(x.name).toBe(x.name)", "a value against itself"}, {"assert!(true);", "a constant"}, {"assert_eq!(2, 2);", "a constant"}, {"testing.expect(t, true)", "a constant"}, {"assert_eq!(a, a);", "a value against itself"}, {"assert.Equal(t, x, x)", "a value against itself"}, {"assert.Equal(t, got, want)", ""}, {"if got != want {", ""}, {"// assert.True(t, true) is what not to write", ""}, {"expect(x).toBe(y)", ""}}) {
   1057 		testing.expectf(
   1058 			t,
   1059 			tautological(p.key) == p.value,
   1060 			"%q: got %q, want %q",
   1061 			p.key,
   1062 			tautological(p.key),
   1063 			p.value,
   1064 		)
   1065 	}
   1066 	c := change.Change {
   1067 		tests = dyn(
   1068 			[]change.Function {
   1069 				{
   1070 					name = "TestX",
   1071 					file = "x_test.go",
   1072 					line = 10,
   1073 					body = "func TestX(t *testing.T) {\n\tassert.True(t, true)\n}",
   1074 				},
   1075 				{
   1076 					name = "TestY",
   1077 					file = "x_test.go",
   1078 					line = 20,
   1079 					body = "func TestY(t *testing.T) {\n\tf()\n}",
   1080 				},
   1081 			},
   1082 		),
   1083 	}
   1084 	got := over(&c)
   1085 	testing.expect_value(t, rules_of(got), "test-no-assertion,assertion-always-true")
   1086 	if len(got) == 2 {
   1087 		testing.expect_value(t, got[1].line, 11)
   1088 		testing.expect_value(t, got[1].symbol, "TestX")
   1089 	}
   1090 }
   1091 
   1092 @(test)
   1093 references_are_searched_as_whole_words :: proc(t: ^testing.T) {
   1094 	sources := make(map[string][]byte, context.temp_allocator)
   1095 	sources["a.go"] = transmute([]byte)string("package x\n\nfunc Waiting() int { return 2 }\n")
   1096 	sources["b.go"] = transmute([]byte)string("var _ = WaitingRoom\n")
   1097 	testing.expect(
   1098 		t,
   1099 		!referenced(change.Symbol{name = "Waiting", file = "a.go", line = 3}, sources),
   1100 		"a prefix of another word counted",
   1101 	)
   1102 	sources["c.go"] = transmute([]byte)string("var _ = Waiting()\n")
   1103 	testing.expect(
   1104 		t,
   1105 		referenced(change.Symbol{name = "Waiting", file = "a.go", line = 3}, sources),
   1106 		"a call was not counted",
   1107 	)
   1108 	sources["c.go"] = transmute([]byte)string("// Waiting is documented here\n")
   1109 	testing.expect(
   1110 		t,
   1111 		!referenced(change.Symbol{name = "Waiting", file = "a.go", line = 3}, sources),
   1112 		"a comment counted",
   1113 	)
   1114 
   1115 	testing.expect(
   1116 		t,
   1117 		called_by_the_runtime(change.Symbol{name = "MarshalJSON", kind = "func", file = "a.go"}),
   1118 	)
   1119 	testing.expect(
   1120 		t,
   1121 		called_by_the_runtime(
   1122 			change.Symbol {
   1123 				name = "DllGetClassObject",
   1124 				kind = "func",
   1125 				file = "a.go",
   1126 				doc = "DllGetClassObject answers COM.\n\nexport DllGetClassObject",
   1127 			},
   1128 		),
   1129 	)
   1130 	testing.expect(
   1131 		t,
   1132 		!called_by_the_runtime(
   1133 			change.Symbol {
   1134 				name = "DllInstall",
   1135 				kind = "func",
   1136 				file = "a.go",
   1137 				doc = "export DllGetClassObject",
   1138 			},
   1139 		),
   1140 	)
   1141 	testing.expect(
   1142 		t,
   1143 		!called_by_the_runtime(change.Symbol{name = "String", kind = "value", file = "a.go"}),
   1144 	)
   1145 	desc, ok := describe("go-vet/nilness")
   1146 	testing.expect(t, ok && strings.contains(desc, "go vet"))
   1147 	_, ok = describe("no-such-rule")
   1148 	testing.expect(t, !ok)
   1149 	testing.expect(t, strings.contains(catalogue(context.temp_allocator), "`test-deleted`"))
   1150 }
   1151 
   1152 // new_repo makes a repository with one commit holding go.mod, x.go and
   1153 // x_test.go, for the checks that read the tree.
   1154 new_repo :: proc(t: ^testing.T) -> (root: string, ok: bool) {
   1155 	temp := os.temp_directory(context.temp_allocator) or_else ""
   1156 	scratch, err := os.make_directory_temp(temp, "review-check-*", context.temp_allocator)
   1157 	if err != nil {
   1158 		testing.fail_now(t, "no scratch directory")
   1159 	}
   1160 	root = scratch
   1161 	ok = git(root, "init", "-q")
   1162 	ok &&= write(root, "go.mod", "module x\n\ngo 1.27.0\n")
   1163 	ok &&= write(root, "x.go", "package x\n")
   1164 	ok &&= write(root, "x_test.go", "package x\n")
   1165 	ok &&= git(root, "add", "go.mod", "x.go", "x_test.go")
   1166 	ok &&= git(root, "commit", "-q", "-m", "first")
   1167 	return root, ok
   1168 }
   1169 
   1170 git :: proc(root: string, args: ..string) -> bool {
   1171 	argv := make([dynamic]string, context.temp_allocator)
   1172 	append(&argv, "git", "-c", "user.email=t@t", "-c", "user.name=t", "-c", "commit.gpgsign=false")
   1173 	append(&argv, ..args)
   1174 	return sh.exec(argv[:], {dir = root}, context.temp_allocator).ok
   1175 }
   1176 
   1177 write :: proc(root, name, src: string) -> bool {
   1178 	path := filepath.join({root, name}, context.temp_allocator) or_else name
   1179 	return os.write_entire_file(path, transmute([]byte)src) == nil
   1180 }
   1181 
   1182 // staged gathers the staged change of a repository and reads it, for a
   1183 // check that needs the whole tree.
   1184 staged :: proc(t: ^testing.T, root: string) -> (c: ^change.Change, s: Scope) {
   1185 	c = new(change.Change, context.temp_allocator)
   1186 	gathered: bool
   1187 	c^, gathered = change.gather("", root, context.temp_allocator)
   1188 	testing.expect(t, gathered, "gather")
   1189 	tr, at_ok := tree.at(root, "", context.temp_allocator)
   1190 	testing.expect(t, at_ok)
   1191 	change.read(c, tr, context.temp_allocator)
   1192 	c.index, _ = change.index(tr, context.temp_allocator)
   1193 	return c, scope_of(c, tr, context.temp_allocator)
   1194 }
   1195 
   1196 @(test)
   1197 unreferenced_reads_the_whole_tree :: proc(t: ^testing.T) {
   1198 	if !frontend.installed(.Go) {
   1199 		testing.fail_now(t, "review-go is not on the path")
   1200 	}
   1201 	root, made := new_repo(t)
   1202 	testing.expect(t, made)
   1203 	defer os.remove_all(root)
   1204 	testing.expect(
   1205 		t,
   1206 		write(
   1207 			root,
   1208 			"x.go",
   1209 			"package x\n\n// Used is called from the template below.\nfunc Used() int { return 1 }\n\n// Waiting is called from nowhere.\nfunc Waiting() int { return 2 }\n\nfunc lonely() int { return 3 }\n",
   1210 		),
   1211 	)
   1212 	testing.expect(t, write(root, "page.tmpl", "{{ Used }}\n"))
   1213 	testing.expect(t, git(root, "add", "x.go", "page.tmpl"))
   1214 	_, s := staged(t, root)
   1215 	got := collect(s, check_unreferenced, context.temp_allocator)
   1216 	testing.expect_value(t, len(got), 1)
   1217 	if len(got) == 1 {
   1218 		testing.expect_value(t, got[0].rule, "new-symbol-unreferenced")
   1219 		testing.expect_value(t, got[0].file, "x.go")
   1220 		testing.expect(t, strings.contains(got[0].message, "Waiting"))
   1221 		testing.expect(t, strings.contains(got[0].message, "lonely"))
   1222 		testing.expect(
   1223 			t,
   1224 			!strings.contains(got[0].message, "Used"),
   1225 			"a name the template uses was reported",
   1226 		)
   1227 	}
   1228 }
   1229 
   1230 @(test)
   1231 code_without_tests_is_reported_where_tests_are_kept :: proc(t: ^testing.T) {
   1232 	if !frontend.installed(.Go) {
   1233 		testing.fail_now(t, "review-go is not on the path")
   1234 	}
   1235 	root, made := new_repo(t)
   1236 	testing.expect(t, made)
   1237 	defer os.remove_all(root)
   1238 	parts := make([dynamic]string, context.temp_allocator)
   1239 	append(&parts, "package x\n\n")
   1240 	for i in 0 ..< 60 {
   1241 		append(&parts, fmt.tprintf("var v%d = %d\n", i, i))
   1242 	}
   1243 	testing.expect(t, write(root, "x.go", strings.concatenate(parts[:], context.temp_allocator)))
   1244 	testing.expect(t, git(root, "add", "x.go"))
   1245 	c, s := staged(t, root)
   1246 	c.message = "x: add sixty variables"
   1247 	got := over_scope(s)
   1248 	testing.expect_value(t, rules_of(only(got, "code-without")), "code-without-tests")
   1249 	testing.expect_value(t, rules_of(only(got, "message-")), "message-no-body")
   1250 	names := only(got, "message-names")
   1251 	testing.expect_value(t, len(names), 0)
   1252 
   1253 	testing.expect(t, write(root, "x_test.go", "package x\n\n// touched\n"))
   1254 	testing.expect(t, git(root, "add", "x_test.go"))
   1255 	_, again := staged(t, root)
   1256 	testing.expect_value(
   1257 		t,
   1258 		len(collect(again, check_code_without_tests, context.temp_allocator)),
   1259 		0,
   1260 	)
   1261 }
   1262 
   1263 over_scope :: proc(s: Scope) -> []finding.Finding {
   1264 	return run(s, context.temp_allocator)
   1265 }
   1266 
   1267 @(test)
   1268 dismissals_are_counted_per_rule :: proc(t: ^testing.T) {
   1269 	context.allocator = context.temp_allocator
   1270 	sources := make(map[string][]byte)
   1271 	sources["a.go"] = transmute([]byte)strings.concatenate(
   1272 		{
   1273 			"package a\n//review:",
   1274 			"ignore no-shadow the loop's\nvar x = 1\n// review:",
   1275 			"ignore no-shadow\n",
   1276 		},
   1277 	)
   1278 	sources["b_test.go"] = transmute([]byte)strings.concatenate(
   1279 		{"const fixture = \"//review:", "ignore cannot-fail it can\\nconst b = 6\"\n"},
   1280 	)
   1281 	sources["readme.md"] = transmute([]byte)strings.concatenate(
   1282 		{"`//review:", "ignore <rule> <why>`\n"},
   1283 	)
   1284 	out := dismissals(Scope{sources = sources})
   1285 	testing.expect(t, strings.has_prefix(out, "no-shadow                    2\n"), out)
   1286 	testing.expect(t, strings.contains(out, "    a.go:2  the loop's\n"), out)
   1287 	testing.expect(t, strings.contains(out, "    a.go:4  no reason given\n"), out)
   1288 	testing.expect(
   1289 		t,
   1290 		strings.contains(out, "cannot-fail                  1\n    b_test.go:1  it can\n"),
   1291 		out,
   1292 	)
   1293 	testing.expect(t, !strings.contains(out, "<rule>"), out)
   1294 	testing.expect_value(t, dismissals(Scope{}), "no dismissals in the tree\n")
   1295 }
   1296 
   1297 @(test)
   1298 names_unknown_reads_the_tree :: proc(t: ^testing.T) {
   1299 	if !frontend.installed(.Go) {
   1300 		testing.fail_now(t, "review-go is not on the path")
   1301 	}
   1302 	root, made := new_repo(t)
   1303 	testing.expect(t, made)
   1304 	defer os.remove_all(root)
   1305 	testing.expect(
   1306 		t,
   1307 		write(root, "x.go", "package x\n\nfunc readConfig() {}\n\nfunc parseFlags() {}\n"),
   1308 	)
   1309 	testing.expect(
   1310 		t,
   1311 		os.make_directory_all(filepath.join({root, "docs"}, context.temp_allocator) or_else "") ==
   1312 		nil,
   1313 	)
   1314 	testing.expect(t, write(root, "docs/notes.md", "notes\n"))
   1315 	testing.expect(t, git(root, "add", "x.go", "docs/notes.md"))
   1316 	c, s := staged(t, root)
   1317 	Case :: struct {
   1318 		message: string,
   1319 		fires:   bool,
   1320 	}
   1321 	for k in ([]Case{{"x: add parseFlags beside readConfig", false}, {"x: add parseConfig()", true}, {"x: add `load_settings` for docs/notes.md", true}, {"x: touch docs/notes.md", false}, {"x: make the reader faster", false}, {"x: see https://example.com/parseConfig", false}}) {
   1322 		c.message = k.message
   1323 		got := collect(s, check_names_unknown, context.temp_allocator)
   1324 		testing.expectf(t, (len(got) == 1) == k.fires, "%q: got %v", k.message, got)
   1325 	}
   1326 	_ = slice.contains([]int{1}, 1)
   1327 }