review

review patchsets using your default editor
Log | Files | Refs

analyser_test.odin (12534B)


      1 package analyser
      2 
      3 import "core:fmt"
      4 import "core:os"
      5 import "core:path/filepath"
      6 import "core:strings"
      7 import "core:testing"
      8 import "jm:sh"
      9 
     10 import "../change"
     11 import "../finding"
     12 import "../tree"
     13 
     14 @(test)
     15 parse_go_build_reads_the_errors :: proc(t: ^testing.T) {
     16 	out := "{\"ImportPath\":\"probe\",\"Action\":\"build-output\",\"Output\":\"# probe\\n\"}\n{\"ImportPath\":\"probe\",\"Action\":\"build-output\",\"Output\":\"./a.go:3:15: undefined: helper\\n\"}\n{\"ImportPath\":\"probe\",\"Action\":\"build-fail\"}\n"
     17 	got := parse_go_build("/tree", out, context.temp_allocator)
     18 	testing.expect_value(t, len(got), 1)
     19 	if len(got) == 1 {
     20 		testing.expect_value(t, got[0].file, "a.go")
     21 		testing.expect_value(t, got[0].line, 3)
     22 		testing.expect(t, got[0].fault)
     23 		testing.expect_value(t, got[0].message, "undefined: helper")
     24 	}
     25 }
     26 
     27 @(test)
     28 parse_go_vet_reads_each_analyser :: proc(t: ^testing.T) {
     29 	out := "# probe\n{\n\t\"probe\": {\n\t\t\"printf\": [{\"posn\": \"/tree/a.go:6:14\", \"message\": \"wrong type\"}],\n\t\t\"shadow\": [{\"posn\": \"/tree/b.go:9:2\", \"message\": \"declaration of err shadows\"}]\n\t}\n}\n# other\nvet: other/x.go:3: undefined: y\n"
     30 	got := parse_go_vet("/tree", out, context.temp_allocator)
     31 	testing.expect_value(t, len(got), 2)
     32 	by := make(map[string]Diagnostic, context.temp_allocator)
     33 	for d in got {
     34 		by[d.code] = d
     35 	}
     36 	testing.expect_value(t, by["printf"].file, "a.go")
     37 	testing.expect_value(t, by["printf"].line, 6)
     38 	testing.expect_value(t, by["printf"].severity, finding.Severity.Must_Fix)
     39 	testing.expect_value(t, by["shadow"].severity, finding.Severity.Consider)
     40 	Case :: struct {
     41 		name: string,
     42 		want: finding.Severity,
     43 	}
     44 	for c in ([]Case{{"printf", .Must_Fix}, {"nilness", .Must_Fix}, {"shadow", .Consider}, {"unusedwrite", .Consider}, {"rangeint", .Note}, {"stringscut", .Note}}) {
     45 		testing.expectf(t, vet_severity(c.name) == c.want, "%s: %v", c.name, vet_severity(c.name))
     46 	}
     47 	chunks := split_json_objects(
     48 		"# a\n{\"x\": {\"y\": \"}\"}}\nprose {not\n{\"z\": 1}\n",
     49 		context.temp_allocator,
     50 	)
     51 	testing.expect_value(t, len(chunks), 2)
     52 	if len(chunks) == 2 {
     53 		testing.expect_value(t, chunks[0], `{"x": {"y": "}"}}`)
     54 		testing.expect_value(t, chunks[1], `{"z": 1}`)
     55 	}
     56 	file, line := split_position("/tree/a.go:6:14")
     57 	testing.expect_value(t, file, "/tree/a.go")
     58 	testing.expect_value(t, line, 6)
     59 	file, line = split_position("a.go:9")
     60 	testing.expect_value(t, file, "a.go")
     61 	testing.expect_value(t, line, 9)
     62 }
     63 
     64 @(test)
     65 parse_odin_tells_vet_from_type_errors :: proc(t: ^testing.T) {
     66 	out := `{"error_count": 3, "errors": [
     67 	 {"type": "error", "pos": {"file": "/tree/lib/a.odin", "line": 6}, "msgs": ["'unused' declared but not used"]},
     68 	 {"type": "warning", "pos": {"file": "/tree/lib/a.odin", "line": 4}, "msgs": ["Syntax Error: With '-strict-style' the attached brace style (1TBS) is enforced"]},
     69 	 {"type": "error", "pos": {"file": "/tree/lib/a.odin", "line": 9}, "msgs": ["Undeclared name: foo"]}
     70 	]}`
     71 	got := parse_odin("/tree", out, context.temp_allocator)
     72 	testing.expect_value(t, len(got), 3)
     73 	if len(got) == 3 {
     74 		testing.expect_value(t, got[0].code, "vet")
     75 		testing.expect_value(t, got[0].severity, finding.Severity.Consider)
     76 		testing.expect(t, !got[0].fault)
     77 		testing.expect_value(t, got[1].code, "style")
     78 		testing.expect_value(t, got[1].severity, finding.Severity.Note)
     79 		testing.expect_value(t, got[2].code, "")
     80 		testing.expect(t, got[2].fault)
     81 		testing.expect_value(t, got[2].file, "lib/a.odin")
     82 	}
     83 }
     84 
     85 @(test)
     86 parse_tsc_ruff_mypy_cargo_semgrep :: proc(t: ^testing.T) {
     87 	ts := parse_tsc(
     88 		"/tree",
     89 		"/tree/web",
     90 		"src/a.ts(12,5): error TS2322: Type 'string' is not assignable to type 'number'.\nnoise\n",
     91 		context.temp_allocator,
     92 	)
     93 	testing.expect_value(t, len(ts), 1)
     94 	if len(ts) == 1 {
     95 		testing.expect_value(t, ts[0].file, "web/src/a.ts")
     96 		testing.expect_value(t, ts[0].line, 12)
     97 		testing.expect_value(t, ts[0].code, "TS2322")
     98 		testing.expect(t, ts[0].fault)
     99 	}
    100 	rf, rerr := parse_ruff(
    101 		"/tree",
    102 		`[{"code":"F401","message":"os imported but unused","filename":"/tree/a.py","location":{"row":1,"column":8}},
    103 	{"code":"E501","message":"Line too long","filename":"/tree/a.py","location":{"row":9,"column":89}}]`,
    104 		context.temp_allocator,
    105 	)
    106 	testing.expect_value(t, rerr, "")
    107 	testing.expect_value(t, len(rf), 2)
    108 	if len(rf) == 2 {
    109 		testing.expect_value(t, rf[0].severity, finding.Severity.Consider)
    110 		testing.expect_value(t, rf[0].file, "a.py")
    111 		testing.expect_value(t, rf[1].severity, finding.Severity.Note)
    112 	}
    113 	_, prose := parse_ruff("/tree", "not json", context.temp_allocator)
    114 	testing.expect(t, prose != "", "prose was read as a report")
    115 	my := parse_mypy(
    116 		"/tree",
    117 		`{"file": "a.py", "line": 4, "column": 4, "message": "Incompatible return value type", "hint": null, "code": "return-value", "severity": "error"}
    118 {"file": "a.py", "line": 4, "column": 4, "message": "See the docs", "hint": null, "code": "return-value", "severity": "note"}
    119 `,
    120 		context.temp_allocator,
    121 	)
    122 	testing.expect_value(t, len(my), 2)
    123 	if len(my) == 2 {
    124 		testing.expect_value(t, my[0].severity, finding.Severity.Must_Fix)
    125 		testing.expect_value(t, my[0].code, "return-value")
    126 		testing.expect_value(t, my[1].severity, finding.Severity.Note)
    127 	}
    128 	cg := parse_cargo(
    129 		"/tree",
    130 		"/tree/crate",
    131 		`{"reason":"compiler-artifact","target":{}}
    132 {"reason":"compiler-message","message":{"level":"error","message":"cannot find value x","code":{"code":"E0425"},"spans":[{"file_name":"src/main.rs","line_start":3,"is_primary":false},{"file_name":"src/main.rs","line_start":4,"is_primary":true}]}}
    133 {"reason":"compiler-message","message":{"level":"warning","message":"unused variable","code":{"code":"unused_variables"},"spans":[{"file_name":"src/lib.rs","line_start":7,"is_primary":true}]}}
    134 {"reason":"compiler-message","message":{"level":"note","message":"aborting","code":null,"spans":[]}}
    135 `,
    136 		context.temp_allocator,
    137 	)
    138 	testing.expect_value(t, len(cg), 2)
    139 	if len(cg) == 2 {
    140 		testing.expect_value(t, cg[0].file, "crate/src/main.rs")
    141 		testing.expect_value(t, cg[0].line, 4)
    142 		testing.expect_value(t, cg[0].code, "E0425")
    143 		testing.expect(t, cg[0].fault)
    144 		testing.expect_value(t, cg[1].severity, finding.Severity.Consider)
    145 		testing.expect(t, !cg[1].fault)
    146 	}
    147 	sg, serr := parse_semgrep(
    148 		"/tree",
    149 		`{"results":[
    150 	 {"check_id":"go.lang.security.audit.crypto.math_random","path":"a.go","start":{"line":12},"extra":{"message":"math/rand is not secure\n","severity":"WARNING"}},
    151 	 {"check_id":"python.lang.best-practice.open-never-closed","path":"/tree/b.py","start":{"line":3},"extra":{"message":"file never closed","severity":"ERROR"}},
    152 	 {"check_id":"generic.note","path":"c.js","start":{"line":1},"extra":{"message":"fyi","severity":"INFO"}}
    153 	],"errors":[]}`,
    154 		context.temp_allocator,
    155 	)
    156 	testing.expect_value(t, serr, "")
    157 	testing.expect_value(t, len(sg), 3)
    158 	if len(sg) == 3 {
    159 		testing.expect_value(t, sg[0].severity, finding.Severity.Consider)
    160 		testing.expect_value(t, sg[0].message, "math/rand is not secure")
    161 		testing.expect_value(t, sg[1].severity, finding.Severity.Must_Fix)
    162 		testing.expect_value(t, sg[1].file, "b.py")
    163 		testing.expect_value(t, sg[2].severity, finding.Severity.Note)
    164 	}
    165 	sc := parse_staticcheck(
    166 		"/tree",
    167 		`{"code":"SA4006","location":{"file":"/tree/a.go","line":5},"message":"never used"}
    168 {"code":"compile","location":{"file":"/tree/a.go","line":1},"message":"broken"}
    169 {"code":"ST1000","location":{"file":"/tree/a.go","line":1},"message":"style"}
    170 `,
    171 		context.temp_allocator,
    172 	)
    173 	testing.expect_value(t, len(sc), 2)
    174 	if len(sc) == 2 {
    175 		testing.expect_value(t, sc[0].severity, finding.Severity.Must_Fix)
    176 		testing.expect_value(t, sc[1].severity, finding.Severity.Note)
    177 	}
    178 	testing.expect_value(t, staticcheck_severity("S1002"), finding.Severity.Consider)
    179 	testing.expect_value(t, staticcheck_severity("U1000"), finding.Severity.Consider)
    180 }
    181 
    182 @(test)
    183 a_run_past_the_timeout_is_not_waited_for :: proc(t: ^testing.T) {
    184 	saved := timeout_seconds_override
    185 	timeout_seconds_override = 1
    186 	defer timeout_seconds_override = saved
    187 	_, _, err := execute_both(".", "sleep", {"3"}, context.temp_allocator)
    188 	testing.expect(t, strings.contains(err, "not waited for"), err)
    189 	out, _, ok_err := execute_both(".", "echo", {"quick"}, context.temp_allocator)
    190 	testing.expect_value(t, ok_err, "")
    191 	testing.expect_value(t, out, "quick\n")
    192 }
    193 
    194 @(test)
    195 collections_resolve_against_the_repository :: proc(t: ^testing.T) {
    196 	temp := os.temp_directory(context.temp_allocator) or_else ""
    197 	scratch, err := os.make_directory_temp(temp, "review-ols-*", context.temp_allocator)
    198 	testing.expect(t, err == nil)
    199 	defer os.remove_all(scratch)
    200 	testing.expect(
    201 		t,
    202 		os.write_entire_file(
    203 			join(scratch, "ols.json"),
    204 			transmute([]byte)string(
    205 				`{"collections":[{"name":"jm","path":"../jm"},{"name":"abs","path":"/opt/abs"},{"name":""}]}`,
    206 			),
    207 		) ==
    208 		nil,
    209 	)
    210 	got := collections(scratch, "/repo", context.temp_allocator)
    211 	testing.expect_value(
    212 		t,
    213 		fmt.tprint(got),
    214 		`["-collection:jm=/jm", "-collection:abs=/opt/abs"]`,
    215 	)
    216 	testing.expect_value(t, len(collections("/nowhere", "/repo", context.temp_allocator)), 0)
    217 }
    218 
    219 @(test)
    220 nearest_walks_up :: proc(t: ^testing.T) {
    221 	temp := os.temp_directory(context.temp_allocator) or_else ""
    222 	root, err := os.make_directory_temp(temp, "review-analyser-*", context.temp_allocator)
    223 	testing.expect(t, err == nil)
    224 	defer os.remove_all(root)
    225 	testing.expect(t, os.make_directory_all(join(root, "web/src/deep")) == nil)
    226 	testing.expect(
    227 		t,
    228 		os.write_entire_file(join(root, "web/tsconfig.json"), transmute([]byte)string("{}")) ==
    229 		nil,
    230 	)
    231 	testing.expect_value(t, nearest(root, "web/src/deep", "tsconfig.json"), "web/tsconfig.json")
    232 	testing.expect_value(t, nearest(root, "web/src/deep", "Cargo.toml"), "")
    233 	testing.expect_value(t, relative("/tree", "/tree/a/b.go"), "a/b.go")
    234 	testing.expect_value(t, relative("/tree", "./a.go"), "a.go")
    235 	testing.expect_value(t, relative("/tree", "/elsewhere/a.go"), "/elsewhere/a.go")
    236 	testing.expect_value(t, dir_of("a.go"), ".")
    237 	testing.expect_value(t, dir_of("x/y/a.go"), "x/y")
    238 
    239 	for path in ([]string{"a", "sidecar/govet", "sidecar/gofront"}) {
    240 		testing.expect(t, os.make_directory_all(join(root, path)) == nil)
    241 	}
    242 	for path in ([]string{"go.mod", "sidecar/govet/go.mod"}) {
    243 		testing.expect(
    244 			t,
    245 			os.write_entire_file(join(root, path), transmute([]byte)string("module x")) == nil,
    246 		)
    247 	}
    248 	pkgs := go_packages(
    249 		root,
    250 		{"a/a.go", "sidecar/govet/main.go", "sidecar/gofront/main.go", "vendor/x/x.go"},
    251 	)
    252 	testing.expect_value(t, fmt.tprint(pkgs), `["./a", "./sidecar/gofront"]`)
    253 }
    254 
    255 // go_fixture is a module with one package: a printf fault on line 6 that
    256 // vet sees, and nothing the compiler minds.
    257 go_fixture :: proc(t: ^testing.T) -> (root: string, ok: bool) {
    258 	temp := os.temp_directory(context.temp_allocator) or_else ""
    259 	scratch, err := os.make_directory_temp(temp, "review-govet-*", context.temp_allocator)
    260 	if err != nil {
    261 		testing.fail_now(t, "no scratch directory")
    262 	}
    263 	root = scratch
    264 	git := proc(root: string, args: ..string) -> bool {
    265 		argv := make([dynamic]string, context.temp_allocator)
    266 		append(
    267 			&argv,
    268 			"git",
    269 			"-c",
    270 			"user.email=t@t",
    271 			"-c",
    272 			"user.name=t",
    273 			"-c",
    274 			"commit.gpgsign=false",
    275 		)
    276 		append(&argv, ..args)
    277 		return sh.exec(argv[:], {dir = root}, context.temp_allocator).ok
    278 	}
    279 	ok = git(root, "init", "-q")
    280 	ok &&=
    281 		os.write_entire_file(
    282 			join(root, "go.mod"),
    283 			transmute([]byte)string("module probe\n\ngo 1.27.0\n"),
    284 		) ==
    285 		nil
    286 	ok &&=
    287 		os.write_entire_file(
    288 			join(root, "a.go"),
    289 			transmute([]byte)string(
    290 				"package probe\n\nimport \"fmt\"\n\nfunc F() {\n\tfmt.Printf(\"%d\", \"s\")\n}\n",
    291 			),
    292 		) ==
    293 		nil
    294 	ok &&= git(root, "add", "go.mod", "a.go")
    295 	return root, ok
    296 }
    297 
    298 @(test)
    299 go_vet_reads_the_change :: proc(t: ^testing.T) {
    300 	if !on_path("go") {
    301 		testing.fail_now(t, "go is not on the path")
    302 	}
    303 	root, made := go_fixture(t)
    304 	testing.expect(t, made)
    305 	defer os.remove_all(root)
    306 	c, gathered := change.gather("", root, context.temp_allocator)
    307 	testing.expect(t, gathered)
    308 	tr, at_ok := tree.at(root, "", context.temp_allocator)
    309 	testing.expect(t, at_ok)
    310 	findings := check(&c, tr, false, {go_build, go_vet}, context.temp_allocator)
    311 	testing.expect_value(t, len(findings), 1)
    312 	if len(findings) == 1 {
    313 		testing.expect_value(t, findings[0].rule, "go-vet/printf")
    314 		testing.expect_value(t, findings[0].file, "a.go")
    315 		testing.expect_value(t, findings[0].line, 6)
    316 		testing.expect_value(t, findings[0].severity, finding.Severity.Must_Fix)
    317 		testing.expect(t, findings[0].verified)
    318 	}
    319 	_ = fmt.tprint(filepath.SEPARATOR)
    320 }