tests.odin (5995B)
1 package check 2 3 // A test with no assertion in it is the plainest shape of a test that 4 // cannot fail, and the shape is visible without reading what the test 5 // means. Whether an assertion that is there asserts anything is the tests 6 // job's, except for the tautologies a pattern can see. 7 8 import "core:fmt" 9 import "core:strings" 10 11 import "../change" 12 import "../finding" 13 14 // go_test_param reads the name a Go test gives its testing.T, so that a 15 // test naming it tc or tt is read by the name it uses. 16 go_test_param :: `^func \w+\((\w+) \*testing\.T\)` 17 18 // check_test_assertions reports an added or altered test whose body 19 // asserts nothing: no failing call, no subtest, no helper handed the test. 20 check_test_assertions :: proc(s: Scope, out: ^[dynamic]finding.Finding) { 21 for t in s.c.tests { 22 if !assertless(t) { 23 continue 24 } 25 append( 26 out, 27 static( 28 "test-no-assertion", 29 .Consider, 30 fmt.aprintf( 31 "%s asserts nothing: no call in its body can fail it, so it passes whatever the code does, and can only fail by crashing", 32 t.name, 33 ), 34 "assert the value the test exists to check, or remove the test", 35 file = t.file, 36 line = t.line, 37 symbol = t.name, 38 ), 39 ) 40 } 41 } 42 43 // assertless is whether a test body holds nothing that could fail it, in 44 // the shapes the tool's languages assert in. 45 assertless :: proc(t: change.Function) -> bool { 46 body := t.body 47 switch { 48 case strings.has_suffix(t.file, ".go"): 49 if strings.has_prefix(t.name, "Benchmark") || 50 strings.has_prefix(t.name, "Fuzz") || 51 strings.has_prefix(t.name, "Example") || 52 t.name == "TestMain" { 53 return false 54 } 55 param := "t" 56 if m, ok := capture(go_test_param, subject_of(body)); ok { 57 param = m[1] 58 } 59 for shape in ([]string{".Error", ".Fatal", ".Fail", ".Run(", ".Skip"}) { 60 if strings.contains( 61 body, 62 strings.concatenate({param, shape}, context.temp_allocator), 63 ) { 64 return false 65 } 66 } 67 for shape in ([]string{"panic(", "require.", "assert.", "is."}) { 68 if strings.contains(body, shape) { 69 return false 70 } 71 } 72 // A test that hands its testing.T to a helper may assert through 73 // it, and the helper is not here to read. 74 return !matches(fmt.tprintf(`[(,]\s*%s\s*[,)]`, param), body) 75 case grammar_of(t.file) != "": 76 for shape in ([]string{"expect(", "expect.", "assert", "should", "toThrow", "fail(", ".rejects", ".resolves", "throw "}) { 77 if strings.contains(body, shape) { 78 return false 79 } 80 } 81 return true 82 case strings.has_suffix(t.file, ".odin"): 83 for shape in ([]string{"testing.expect", "testing.fail", "expect(", "expectf(", "expect_value(", "assert(", "panic("}) { 84 if strings.contains(body, shape) { 85 return false 86 } 87 } 88 return true 89 case strings.has_suffix(t.file, ".py"): 90 for shape in ([]string{"assert ", "assert(", "self.assert", "pytest.raises", "pytest.fail", "raise ", ".assert_"}) { 91 if strings.contains(body, shape) { 92 return false 93 } 94 } 95 return true 96 case strings.has_suffix(t.file, ".rs"): 97 for shape in ([]string{"assert!", "assert_eq!", "assert_ne!", "panic!", "unwrap()", "expect(", "?;", "should_panic"}) { 98 if strings.contains(body, shape) { 99 return false 100 } 101 } 102 return true 103 } 104 return false 105 } 106 107 // tautologies are the assertion shapes that hold whatever the code does: 108 // a literal true asserted, two literals compared. They are what a test 109 // reaches for once a rule says a test must assert. 110 @(private = "file") 111 tautologies := []string { 112 `\b(?:assert|require)\.(?:True|NoError|Nil|Empty)\(\s*\w+\s*,\s*(?:true|nil)\s*\)`, 113 `\bassert\s+(?:True|1|"[^"]+"|'[^']+')\s*(?:,|$)`, 114 `\bassert(?:True|Is)\(\s*True\s*[,)]`, 115 `\bexpect\(\s*true\s*\)\.(?:toBe\(\s*true\s*\)|toBeTruthy\(\))`, 116 `\bassert!\(\s*true\s*\)`, 117 `\b(?:testing\.)?expect\(\s*\w+\s*,\s*true\s*\)`, 118 `\bexpect\(\s*(-?\d+|"[^"]*"|'[^']*')\s*\)\.(?:toBe|toEqual|toStrictEqual)\(\s*(-?\d+|"[^"]*"|'[^']*')\s*\)`, 119 `\bassert_eq!\(\s*(-?\d+|"[^"]*")\s*,\s*(-?\d+|"[^"]*")\s*\)`, 120 `\b(?:assert|require)\.Equal\(\s*\w+\s*,\s*(-?\d+|"[^"]*")\s*,\s*(-?\d+|"[^"]*")\s*\)`, 121 `\bassert\s+(-?\d+|"[^"]*"|'[^']*')\s*==\s*(-?\d+|"[^"]*"|'[^']*')`, 122 } 123 124 // self_compare matches an expression compared with itself, in the shapes 125 // the languages assert with: x == x, expect(x).toBe(x), assert_eq!(x, x), 126 // assert.Equal(t, x, x). 127 @(private = "file") 128 self_compare := []string { 129 `\b([\w.]+(?:\([^()]*\))?)\s*(?:==|!=)\s*([\w.]+(?:\([^()]*\))?)`, 130 `\bexpect\(\s*([^()]+)\s*\)\.(?:toBe|toEqual|toStrictEqual)\(\s*([^()]+)\s*\)`, 131 `\bassert_(?:eq|ne)!\(\s*(.+?)\s*,\s*(.+?)\s*\);?\s*$`, 132 `\b(?:assert|require)\.(?:Equal|NotEqual)\(\s*\w+\s*,\s*(.+?)\s*,\s*(.+?)\s*\)\s*$`, 133 } 134 135 // check_tautologies reports an assertion in an added or altered test that 136 // holds whatever the code does. 137 check_tautologies :: proc(s: Scope, out: ^[dynamic]finding.Finding) { 138 for t in s.c.tests { 139 rest := t.body 140 i := 0 141 for line in strings.split_lines_iterator(&rest) { 142 if why := tautological(line); why != "" { 143 append( 144 out, 145 static( 146 "assertion-always-true", 147 .Must_Fix, 148 fmt.aprintf( 149 "%s asserts %s: %s; the assertion holds whatever the code does, so the test cannot fail on it", 150 t.name, 151 why, 152 strings.trim_space(line), 153 ), 154 "assert the value the code produced against the value it should have", 155 file = t.file, 156 line = t.line + i, 157 symbol = t.name, 158 ), 159 ) 160 } 161 i += 1 162 } 163 } 164 } 165 166 // tautological says what is tautological about an assertion line, or 167 // nothing. 168 tautological :: proc(line: string) -> string { 169 trimmed := strings.trim_space(line) 170 if trimmed == "" || strings.has_prefix(trimmed, "//") || strings.has_prefix(trimmed, "#") { 171 return "" 172 } 173 for pattern in tautologies { 174 if matches(pattern, trimmed) { 175 return "a constant" 176 } 177 } 178 for pattern in self_compare { 179 if m, ok := capture(pattern, trimmed); 180 ok && len(m) > 2 && strings.trim_space(m[1]) == strings.trim_space(m[2]) { 181 return "a value against itself" 182 } 183 } 184 return "" 185 }