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 }