review

review patchsets using your default editor
Log | Files | Refs

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 }