v2 / vlib / v / compiler_errors_test.v
463 lines · 427 sloc · 14.85 KB · e6b78d063953a662a2c9746735674e2f40152508
Raw
1// vtest build: !self_sandboxed_packaging? && !sanitized_job?
2import os
3import term
4import v.util.diff
5import v.util.vtest
6import time
7import runtime
8import benchmark
9
10const skip_files = [
11 'non_existing.vv', // minimize commit diff churn, do not remove
12 'vlib/v/checker/tests/var_duplicate_const.vv', // produces non-deterministic C error output
13]
14
15const skip_on_cstrict = [
16 'vlib/v/checker/tests/missing_c_lib_header_1.vv',
17 'vlib/v/checker/tests/missing_c_lib_header_with_explanation_2.vv',
18 'vlib/v/checker/tests/comptime_value_d_in_include_errors.vv',
19 'vlib/v/checker/tests/missing_shader_header_1.vv',
20]
21
22const skip_on_ubuntu_musl = [
23 'vlib/v/checker/tests/orm_op_with_option_and_none.vv',
24 'vlib/v/checker/tests/orm_unused_var.vv',
25 'vlib/v/tests/skip_unused/gg_code.vv',
26]
27
28const skip_on_ci_musl = [
29 'vlib/v/tests/skip_unused/gg_code.vv',
30]
31
32const vexe = os.getenv('VEXE')
33
34@[markused]
35const turn_off_vcolors = os.setenv('VCOLORS', 'never', true)
36
37const show_cmd = os.getenv('VTEST_SHOW_CMD') != ''
38
39// This is needed, because some of the .vv files are tests, and we do need stable
40// output from them, that can be compared against their .out files:
41const turn_on_normal_test_runner = os.setenv('VTEST_RUNNER', 'normal', true)
42
43const should_autofix = os.getenv('VAUTOFIX') != ''
44
45const is_silent = $if silent ? { true } $else { false }
46
47const github_job = os.getenv('GITHUB_JOB')
48
49const should_show_details = !is_silent && github_job == ''
50
51const v_ci_ubuntu_musl = os.getenv('V_CI_UBUNTU_MUSL').len > 0
52
53const v_ci_musl = os.getenv('V_CI_MUSL').len > 0
54
55const v_ci_cstrict = os.getenv('V_CI_CSTRICT').len > 0
56
57struct TaskDescription {
58 vexe string
59 evars string
60 dir string
61 voptions string
62 result_extension string
63 path string
64mut:
65 is_error bool
66 is_skipped bool
67 is_module bool
68 expected string
69 expected_out_path string
70 found___ string
71 took time.Duration
72 cli_cmd string
73 ntries int
74 max_ntries int = 1
75}
76
77struct Tasks {
78 vexe string
79 parallel_jobs int // 0 is using VJOBS, anything else is an override
80 label string
81mut:
82 show_cmd bool
83 all []TaskDescription
84}
85
86fn test_all() {
87 vroot := os.dir(vexe)
88 os.chdir(vroot) or {}
89 checker_dir := 'vlib/v/checker/tests'
90 checker_with_check_option_dir := 'vlib/v/checker/tests/with_check_option'
91 parser_dir := 'vlib/v/parser/tests'
92 scanner_dir := 'vlib/v/scanner/tests'
93 module_dir := '${checker_dir}/modules'
94 global_dir := '${checker_dir}/globals'
95 global_run_dir := '${checker_dir}/globals_run'
96 run_dir := '${checker_dir}/run'
97 su_dir := 'vlib/v/tests/skip_unused'
98 no_closures_dir := 'vlib/v/tests/no_closures'
99 js_checker_tests := ['index_expr_implicit_int_downcast_err.vv',
100 'js_number_requires_explicit_cast.vv']
101 disable_explicit_mutability_tests := ['disable_explicit_mutability.vv']
102
103 checker_tests := get_tests_in_dir(checker_dir, false).filter(!it.contains('with_check_option')
104 && it !in js_checker_tests && it !in disable_explicit_mutability_tests)
105 parser_tests := get_tests_in_dir(parser_dir, false)
106 scanner_tests := get_tests_in_dir(scanner_dir, false)
107 global_tests := get_tests_in_dir(global_dir, false)
108 global_run_tests := get_tests_in_dir(global_run_dir, false)
109 module_tests := get_tests_in_dir(module_dir, true)
110 run_tests := get_tests_in_dir(run_dir, false)
111 su_dir_tests := get_tests_in_dir(su_dir, false)
112 no_closures_tests := get_tests_in_dir(no_closures_dir, false)
113 checker_with_check_option_tests := get_tests_in_dir(checker_with_check_option_dir, false)
114
115 if os.user_os() == 'linux' {
116 mut su_tasks := Tasks{
117 vexe: vexe
118 parallel_jobs: 1
119 label: '-skip-unused tests'
120 }
121 su_tasks.add('', su_dir, ' run ', '.run.out', su_dir_tests, false)
122 su_tasks.run()
123 }
124
125 if github_job == 'ubuntu-tcc' {
126 // This is done with tcc only, because the error output is compiler specific.
127 // Note: the tasks should be run serially, since they depend on
128 // setting and using environment variables.
129 mut cte_tasks := Tasks{
130 vexe: vexe
131 parallel_jobs: 1
132 label: 'comptime env tests'
133 }
134 cte_dir := '${checker_dir}/comptime_env'
135 files := get_tests_in_dir(cte_dir, false)
136 cte_tasks.add('', cte_dir, '-no-retry-compilation run', '.run.out', files, false)
137 cte_tasks.add_evars('VAR=/usr/include', '', cte_dir, '-no-retry-compilation run',
138 '.var.run.out', ['using_comptime_env.vv'], false)
139 cte_tasks.add_evars('VAR=/opt/invalid/path', '', cte_dir, '-no-retry-compilation run',
140 '.var_invalid.run.out', ['using_comptime_env.vv'], false)
141 cte_tasks.run()
142 }
143 mut ct_tasks := Tasks{
144 vexe: vexe
145 parallel_jobs: 1
146 label: 'comptime define tests'
147 }
148 ct_tasks.add_checked_run('-d mysymbol run', '.mysymbol.run.out', [
149 'custom_comptime_define_error.vv',
150 ])
151 ct_tasks.add_checked_run('-d mydebug run', '.mydebug.run.out', [
152 'custom_comptime_define_if_flag.vv',
153 ])
154 ct_tasks.add_checked_run('-d nodebug run', '.nodebug.run.out', [
155 'custom_comptime_define_if_flag.vv',
156 ])
157 ct_tasks.add_checked_run('run', '.run.out', ['custom_comptime_define_if_debug.vv'])
158 ct_tasks.add_checked_run('-g run', '.g.run.out', [
159 'custom_comptime_define_if_debug.vv',
160 ])
161 ct_tasks.add_checked_run('-cg run', '.cg.run.out', [
162 'custom_comptime_define_if_debug.vv',
163 ])
164 ct_tasks.add_checked_run('-d debug run', '.debug.run.out', [
165 'custom_comptime_define_if_debug.vv',
166 ])
167 ct_tasks.add_checked_run('-d debug -d bar run', '.debug.bar.run.out', [
168 'custom_comptime_define_if_debug.vv',
169 ])
170 ct_tasks.run()
171
172 mut tasks := Tasks{
173 vexe: vexe
174 label: 'all tests'
175 }
176 tasks.add('', parser_dir, '', '.out', parser_tests, false)
177 tasks.add('', checker_dir, '', '.out', checker_tests, false)
178 tasks.add('', checker_dir, '-b js', '.js.out', js_checker_tests, false)
179 tasks.add('', scanner_dir, '', '.out', scanner_tests, false)
180 tasks.add('', checker_dir, '-enable-globals run', '.run.out', ['globals_error.vv'], false)
181 tasks.add('', global_run_dir, '-enable-globals run', '.run.out', global_run_tests, false)
182 tasks.add('', global_dir, '-enable-globals', '.out', global_tests, false)
183 tasks.add('', module_dir, '-prod run', '.out', module_tests, true)
184 tasks.add('', run_dir, 'run', '.run.out', run_tests, false)
185 tasks.add('', checker_dir, '-disable-explicit-mutability run',
186 '.disable_explicit_mutability.run.out', disable_explicit_mutability_tests, false)
187 tasks.add('', checker_with_check_option_dir, '-check', '.out', checker_with_check_option_tests,
188 false)
189 tasks.add('', no_closures_dir, '-no-closures run', '.out', no_closures_tests, false)
190 tasks.run()
191}
192
193fn (mut tasks Tasks) add_checked_run(voptions string, result_extension string, tests []string) {
194 checker_dir := 'vlib/v/checker/tests'
195 tasks.add('', checker_dir, voptions, result_extension, tests, false)
196}
197
198fn (mut tasks Tasks) add(custom_vexe string, dir string, voptions string, result_extension string, tests []string,
199 is_module bool) {
200 tasks.add_evars('', custom_vexe, dir, voptions, result_extension, tests, is_module)
201}
202
203fn (mut tasks Tasks) add_evars(evars string, custom_vexe string, dir string, voptions string, result_extension string,
204 tests []string, is_module bool) {
205 max_ntries := get_max_ntries()
206 paths := vtest.filter_vtest_only(tests, basepath: dir)
207 for path in paths {
208 tasks.all << TaskDescription{
209 evars: evars
210 vexe: if custom_vexe != '' { custom_vexe } else { tasks.vexe }
211 dir: dir
212 voptions: voptions
213 result_extension: result_extension
214 path: path
215 is_module: is_module
216 max_ntries: max_ntries
217 }
218 }
219}
220
221fn bstep_message(mut bench benchmark.Benchmark, label string, msg string, sduration time.Duration) string {
222 return bench.step_message_with_label_and_duration(label, msg, sduration)
223}
224
225// process an array of tasks in parallel, using no more than vjobs worker threads
226fn (mut tasks Tasks) run() {
227 if tasks.all.len == 0 {
228 return
229 }
230 tasks.show_cmd = show_cmd
231 vjobs := if tasks.parallel_jobs > 0 { tasks.parallel_jobs } else { runtime.nr_jobs() }
232 mut bench := benchmark.new_benchmark()
233 bench.set_total_expected_steps(tasks.all.len)
234 mut work := chan TaskDescription{cap: tasks.all.len}
235 mut results := chan TaskDescription{cap: tasks.all.len}
236 mut m_skip_files := skip_files.clone()
237 if v_ci_ubuntu_musl {
238 m_skip_files << skip_on_ubuntu_musl
239 }
240 if v_ci_musl {
241 m_skip_files << skip_on_ci_musl
242 }
243 if v_ci_cstrict {
244 m_skip_files << skip_on_cstrict
245 }
246 $if noskip ? {
247 m_skip_files = []
248 }
249 $if tinyc {
250 // Note: tcc does not support __has_include, so the detection mechanism
251 // used for the other compilers does not work. It still provides a
252 // cleaner error message, than a generic C error, but without the explanation.
253 m_skip_files << 'vlib/v/checker/tests/missing_c_lib_header_1.vv'
254 m_skip_files << 'vlib/v/checker/tests/missing_c_lib_header_with_explanation_2.vv'
255 m_skip_files << 'vlib/v/checker/tests/comptime_value_d_in_include_errors.vv'
256 m_skip_files << 'vlib/v/checker/tests/missing_shader_header_1.vv'
257 }
258 $if msvc {
259 m_skip_files << 'vlib/v/checker/tests/asm_alias_does_not_exist.vv'
260 m_skip_files << 'vlib/v/checker/tests/asm_immutable_err.vv'
261 // TODO: investigate why MSVC regressed
262 m_skip_files << 'vlib/v/checker/tests/missing_c_lib_header_1.vv'
263 m_skip_files << 'vlib/v/checker/tests/missing_c_lib_header_with_explanation_2.vv'
264 m_skip_files << 'vlib/v/checker/tests/comptime_value_d_in_include_errors.vv'
265 m_skip_files << 'vlib/v/checker/tests/missing_shader_header_1.vv'
266 }
267 $if windows {
268 m_skip_files << 'vlib/v/checker/tests/invalid_utf8_string.vv'
269 m_skip_files << 'vlib/v/checker/tests/modules/deprecated_module'
270 }
271 for i in 0 .. tasks.all.len {
272 if tasks.all[i].path in m_skip_files {
273 tasks.all[i].is_skipped = true
274 }
275 work <- tasks.all[i]
276 }
277 work.close()
278 for _ in 0 .. vjobs {
279 spawn work_processor(work, results)
280 }
281 if should_show_details {
282 println('')
283 }
284 mut line_can_be_erased := true
285 mut total_errors := 0
286 for _ in 0 .. tasks.all.len {
287 mut task := TaskDescription{}
288 task = <-results
289 bench.step()
290 if task.is_skipped {
291 bench.skip()
292 if should_show_details {
293 eprintln(bstep_message(mut bench, benchmark.b_skip, task.path, task.took))
294 line_can_be_erased = false
295 }
296 continue
297 }
298 if task.is_error {
299 total_errors++
300 bench.fail()
301 eprintln(bstep_message(mut bench, benchmark.b_fail, task.path, task.took))
302 println('============')
303 println('failed cmd: ${task.cli_cmd}')
304 println('expected_out_path: ${task.expected_out_path}')
305 println('============')
306 println('expected (len: ${task.expected.len:5}, hash: ${task.expected.hash()}):')
307 println(task.expected)
308 println('============')
309 println('found (len: ${task.found___.len:5}, hash: ${task.found___.hash()}):')
310 println(task.found___)
311 println('============\n')
312 diff_content(task.expected, task.found___)
313 line_can_be_erased = false
314 } else {
315 bench.ok()
316 assert true
317 if tasks.show_cmd {
318 eprintln(bstep_message(mut bench, benchmark.b_ok, '${task.cli_cmd}', task.took))
319 line_can_be_erased = true
320 } else {
321 if should_show_details {
322 // local mode:
323 if line_can_be_erased {
324 term.clear_previous_line()
325 }
326 println(bstep_message(mut bench, benchmark.b_ok, task.path, task.took))
327 line_can_be_erased = true
328 }
329 }
330 }
331 }
332 bench.stop()
333 if should_show_details {
334 eprintln(term.h_divider('-'))
335 }
336 eprintln(bench.total_message(tasks.label))
337 if total_errors != 0 {
338 exit(1)
339 }
340}
341
342// a single worker thread spends its time getting work from the `work` channel,
343// processing the task, and then putting the task in the `results` channel
344fn work_processor(work chan TaskDescription, results chan TaskDescription) {
345 for {
346 mut task := <-work or { break }
347 mut i := 0
348 for i = 1; i <= task.max_ntries; i++ {
349 // reset the .is_error flag, from the potential previous retries, otherwise it can
350 // be set on the first retry, all the next retries can succeed, and the task will
351 // be still considered failed, with a very puzzling non difference reported.
352 task.is_error = false
353 sw := time.new_stopwatch()
354 task.execute()
355 task.took = sw.elapsed()
356 cli_cmd := task.get_cli_cmd()
357 if !task.is_error {
358 if i > 1 {
359 eprintln('> succeeded after ${i:3}/${task.max_ntries} retries, doing `${cli_cmd}`')
360 }
361 break
362 }
363 eprintln('> failed ${i:3}/${task.max_ntries} times, doing `${cli_cmd}`')
364 if i <= task.max_ntries {
365 time.sleep(100 * time.millisecond)
366 }
367 }
368 task.ntries = i
369 results <- task
370 }
371}
372
373fn (mut task TaskDescription) get_cli_cmd() string {
374 program := task.path
375 cmd_prefix := if task.evars.len > 0 { '${task.evars} ' } else { '' }
376 cli_cmd := '${cmd_prefix}${os.quoted_path(task.vexe)} ${task.voptions} ${os.quoted_path(program)}'
377 return cli_cmd
378}
379
380// actual processing; Note: no output is done here at all
381fn (mut task TaskDescription) execute() {
382 if task.is_skipped {
383 return
384 }
385 cli_cmd := task.get_cli_cmd()
386 res := os.execute(cli_cmd)
387 expected_out_path := task.path.replace('.vv', '') + task.result_extension
388 task.expected_out_path = expected_out_path
389 task.cli_cmd = cli_cmd
390 if should_autofix && !os.exists(expected_out_path) {
391 os.create(expected_out_path) or { panic(err) }
392 }
393 mut expected := os.read_file(expected_out_path) or { panic(err) }
394 task.expected = clean_line_endings(expected)
395 task.found___ = clean_line_endings(res.output)
396 $if windows {
397 if task.is_module {
398 task.found___ = task.found___.replace_once('\\', '/')
399 }
400 }
401 if task.expected != task.found___ {
402 task.is_error = true
403 if should_autofix {
404 os.write_file(expected_out_path, res.output) or { panic(err) }
405 }
406 }
407}
408
409fn clean_line_endings(s string) string {
410 mut res := s.trim_space()
411 res = res.replace(' \n', '\n')
412 res = res.replace(' \r\n', '\n')
413 res = res.replace('\r\n', '\n')
414 res = res.trim('\n')
415 return res
416}
417
418fn chunks(s string, chunk_size int) string {
419 mut res := []string{}
420 for i := 0; i < s.len; i += chunk_size {
421 res << s#[i..i + chunk_size]
422 }
423 return res.join('\n')
424}
425
426fn chunka(s []u8, chunk_size int) string {
427 mut res := []string{}
428 for i := 0; i < s.len; i += chunk_size {
429 res << s#[i..i + chunk_size].str()
430 }
431 return res.join('\n')
432}
433
434fn diff_content(expected string, found string) {
435 println(term.bold(term.yellow('diff: ')))
436 if diff_ := diff.compare_text(expected, found) {
437 println(diff_)
438 } else {
439 println('>>>> `${err}`; dumping bytes instead...')
440 println('expected bytes:\n${chunka(expected.bytes(), 25)}')
441 println(' found bytes:\n${chunka(found.bytes(), 25)}')
442 println('============')
443 println(' expected hex:\n${chunks(expected.hex(), 80)}')
444 println(' found hex:\n${chunks(found.hex(), 80)}')
445 }
446 println('============\n')
447}
448
449fn get_tests_in_dir(dir string, is_module bool) []string {
450 files := os.ls(dir) or { panic(err) }
451 mut tests := files.clone()
452 if !is_module {
453 tests = files.filter(it.ends_with('.vv'))
454 } else {
455 tests = files.filter(!it.ends_with('.out'))
456 }
457 tests.sort()
458 return tests
459}
460
461fn get_max_ntries() int {
462 return if v_ci_musl { 3 } else { 1 }
463}
464