v / cmd / tools / modules / testing / common.v
1176 lines · 1074 sloc · 36.83 KB · 712a7493d966be2e24bcf74ccd45e6adf14d64bf
Raw
1module testing
2
3import os
4import os.cmdline
5import semver
6import time
7import term
8import benchmark
9import sync
10import sync.pool
11import v.pref
12import v.util.vtest
13import v.util.vflags
14import runtime
15import rand
16import strings
17import v.build_constraint
18
19pub const max_header_len = get_max_header_len()
20
21pub const host_os = pref.get_host_os()
22
23pub const github_job = os.getenv('GITHUB_JOB')
24
25pub const runner_os = os.getenv('RUNNER_OS') // GitHub runner OS
26
27pub const keep_session = os.getenv('VTEST_KEEP_SESSION') == '1'
28
29pub const show_cmd = os.getenv('VTEST_SHOW_CMD') == '1'
30
31pub const show_start = os.getenv('VTEST_SHOW_START') == '1'
32
33pub const show_longest_by_runtime = os.getenv('VTEST_SHOW_LONGEST_BY_RUNTIME').int()
34pub const show_longest_by_comptime = os.getenv('VTEST_SHOW_LONGEST_BY_COMPTIME').int()
35pub const show_longest_by_totaltime = os.getenv('VTEST_SHOW_LONGEST_BY_TOTALTIME').int()
36
37pub const is_ci = os.getenv('CI') != '' || os.getenv('GITHUB_JOB') != ''
38
39pub const hide_skips = os.getenv('VTEST_HIDE_SKIP') == '1'
40 || (is_ci && os.getenv('VTEST_HIDE_SKIP') != '0')
41
42pub const hide_oks = os.getenv('VTEST_HIDE_OK') == '1'
43 || (is_ci && os.getenv('VTEST_HIDE_OK') != '0')
44
45pub const fail_fast = os.getenv('VTEST_FAIL_FAST') == '1'
46
47pub const fail_flaky = os.getenv('VTEST_FAIL_FLAKY') == '1'
48
49pub const test_only = os.getenv('VTEST_ONLY').split_any(',')
50
51pub const test_only_fn = os.getenv('VTEST_ONLY_FN').split_any(',')
52
53// TODO: this !!!*reliably*!!! fails compilation of `v cmd/tools/vbuild-examples.v` with a cgen error, without `-no-parallel`:
54// pub const fail_retry_delay_ms = os.getenv_opt('VTEST_FAIL_RETRY_DELAY_MS') or { '500' }.int() * time.millisecond
55// Note, it works with `-no-parallel`, and it works when that whole expr is inside a function, like below:
56pub const fail_retry_delay_ms = get_fail_retry_delay_ms()
57
58pub const pkgcmd = get_pkgcmd()
59
60pub const is_node_present = os.execute('node --version').exit_code == 0
61
62pub const is_go_present = os.execute('go version').exit_code == 0
63
64pub const is_ruby_present = os.execute('ruby --version').exit_code == 0
65 && os.execute('${pkgcmd} ruby --libs').exit_code == 0
66
67pub const is_python_present = os.execute('python --version').exit_code == 0
68 && os.execute('${pkgcmd} python3 --libs').exit_code == 0
69
70pub const is_sqlite3_present = get_present_sqlite()
71
72pub const all_processes = get_all_processes()
73
74pub const header_bytes_to_search_for_module_main = 500
75
76pub const separator = '-'.repeat(max_header_len) + '\n'
77
78pub const max_compilation_retries = get_max_compilation_retries()
79
80const c_error_bug_report_disabled_env = 'V_C_ERROR_BUG_REPORT_DISABLED'
81
82fn get_max_compilation_retries() int {
83 return os.getenv_opt('VTEST_MAX_COMPILATION_RETRIES') or { '3' }.int()
84}
85
86fn get_fail_retry_delay_ms() time.Duration {
87 return os.getenv_opt('VTEST_FAIL_RETRY_DELAY_MS') or { '500' }.int() * time.millisecond
88}
89
90fn get_pkgcmd() string {
91 for cmd in ['pkgconf', 'pkg-config'] {
92 if os.execute('${cmd} --version').exit_code == 0 {
93 return cmd
94 }
95 }
96 return 'false'
97}
98
99fn get_present_sqlite() bool {
100 if os.user_os() == 'windows' {
101 return os.exists(@VEXEROOT + '/thirdparty/sqlite/sqlite3.c')
102 }
103 return os.execute('sqlite3 --version').exit_code == 0
104 && os.execute('${pkgcmd} sqlite3 --libs').exit_code == 0
105}
106
107fn get_all_processes() []string {
108 $if windows {
109 // TODO
110 return []
111 } $else {
112 return os.execute('ps ax').output.split_any('\r\n')
113 }
114}
115
116pub enum ActionMode {
117 compile
118 compile_and_run
119}
120
121pub struct TestSession {
122pub mut:
123 files []string
124 skip_files []string
125 vexe string
126 vroot string
127 vtmp_dir string
128 vargs string
129 fail_fast bool
130 benchmark benchmark.Benchmark
131 rm_binaries bool = true
132 build_tools bool // builds only executables in cmd/tools; used by `v build-tools'
133 silent_mode bool
134 show_stats bool
135 show_asserts bool
136 progress_mode bool
137 root_relative bool // used by CI runs, so that the output is stable everywhere
138 nmessages chan LogMessage // many publishers, single consumer/printer
139 nmessage_idx int // currently printed message index
140 failed_cmds shared []string
141 reporter Reporter = Reporter(NormalReporter{})
142 hash string // used as part of the name of the temporary directory created for tests, to ease cleanup
143
144 exec_mode ActionMode = .compile // .compile_and_run only for `v test`
145
146 build_environment build_constraint.Environment // see the documentation in v.build_constraint
147 custom_defines []string // for adding custom defines, known only to the individual runners
148}
149
150pub fn (mut ts TestSession) add_failed_cmd(cmd string) {
151 lock ts.failed_cmds {
152 ts.failed_cmds << cmd
153 }
154}
155
156pub fn (mut ts TestSession) show_list_of_failed_tests() {
157 rlock ts.failed_cmds {
158 ts.reporter.list_of_failed_commands(ts.failed_cmds)
159 }
160}
161
162struct MessageThreadContext {
163mut:
164 file string
165 flow_id string
166}
167
168fn (mut ts TestSession) append_message(kind MessageKind, msg string, mtc MessageThreadContext) {
169 ts.nmessages <- LogMessage{
170 file: mtc.file
171 flow_id: mtc.flow_id
172 message: msg
173 kind: kind
174 when: time.now()
175 }
176}
177
178fn (mut ts TestSession) append_message_with_duration(kind MessageKind, msg string, d time.Duration, mtc MessageThreadContext) {
179 ts.nmessages <- LogMessage{
180 file: mtc.file
181 flow_id: mtc.flow_id
182 message: msg
183 kind: kind
184 when: time.now()
185 took: d
186 }
187}
188
189pub fn (mut ts TestSession) session_start(message string) {
190 ts.reporter.session_start(message, mut ts)
191}
192
193pub fn (mut ts TestSession) session_stop(message string) {
194 ts.reporter.session_stop(message, mut ts)
195}
196
197pub fn (mut ts TestSession) print_messages() {
198 mut test_idx := 0
199 mut print_msg_time := time.new_stopwatch()
200 for {
201 // get a message from the channel of messages to be printed:
202 mut rmessage := <-ts.nmessages
203 ts.nmessage_idx++
204
205 // first sent *all events* to the output reporter, so it can then process them however it wants:
206 ts.reporter.report(ts.nmessage_idx, rmessage)
207
208 if rmessage.kind in [.cmd_begin, .cmd_end, .compile_begin] {
209 // The following events, are sent before the test framework has determined,
210 // what the full completion status is. They can also be repeated multiple times,
211 // for tests that are flaky and need repeating.
212 continue
213 }
214 if rmessage.kind == .compile_end {
215 if rmessage.message.trim_space().len == 0 {
216 continue
217 }
218 if ts.progress_mode {
219 ts.reporter.update_last_line_and_move_to_next(ts.nmessage_idx, '')
220 }
221 if rmessage.message.ends_with('\n') {
222 eprint(rmessage.message)
223 } else {
224 eprintln(rmessage.message)
225 }
226 continue
227 }
228 if rmessage.kind == .sentinel {
229 // a sentinel for stopping the printing thread
230 if !ts.silent_mode && ts.progress_mode {
231 ts.reporter.report_stop()
232 }
233 return
234 }
235 if rmessage.kind in [.stats_output, .stats_error] {
236 mut msg := rmessage.message
237 if msg != '' && !msg.ends_with('\n') {
238 msg += '\n'
239 }
240 if rmessage.kind == .stats_error {
241 eprint(msg)
242 flush_stderr()
243 } else {
244 print(msg)
245 flush_stdout()
246 }
247 continue
248 }
249 if rmessage.kind != .info {
250 // info events can also be repeated, and should be ignored when determining
251 // the total order of the current test file, in the following replacements:
252 test_idx++
253 }
254 msg := rmessage.message.replace_each([
255 'TMP1',
256 '${test_idx:1d}',
257 'TMP2',
258 '${test_idx:2d}',
259 'TMP3',
260 '${test_idx:3d}',
261 'TMP4',
262 '${test_idx:4d}',
263 ])
264 is_ok := rmessage.kind == .ok
265 //
266 time_passed := print_msg_time.elapsed().seconds()
267 if time_passed > 10 && ts.silent_mode && is_ok {
268 // Even if OK tests are suppressed,
269 // show *at least* 1 result every 10 seconds,
270 // otherwise the CI can seem stuck ...
271 ts.reporter.progress(ts.nmessage_idx, msg)
272 print_msg_time.restart()
273 continue
274 }
275 if ts.progress_mode {
276 if is_ok && !ts.silent_mode {
277 ts.reporter.update_last_line(ts.nmessage_idx, msg)
278 } else {
279 ts.reporter.update_last_line_and_move_to_next(ts.nmessage_idx, msg)
280 }
281 continue
282 }
283 if !ts.silent_mode || !is_ok {
284 // normal expanded mode, or failures in -silent mode
285 ts.reporter.message(ts.nmessage_idx, msg)
286 continue
287 }
288 }
289}
290
291pub fn (mut ts TestSession) execute(cmd string, mtc MessageThreadContext) os.Result {
292 if show_cmd {
293 ts.append_message(.info, '> execute cmd: ${cmd}', mtc)
294 }
295 return os.execute(cmd)
296}
297
298pub fn (mut ts TestSession) system(cmd string, mtc MessageThreadContext) int {
299 if show_cmd {
300 ts.append_message(.info, '> system cmd: ${cmd}', mtc)
301 }
302 return os.system(cmd)
303}
304
305pub fn new_test_session(_vargs string, will_compile bool) TestSession {
306 os.setenv(c_error_bug_report_disabled_env, '1', true)
307 mut skip_files := []string{}
308 vexe := pref.vexe_path()
309 vroot := os.dir(vexe)
310 if will_compile {
311 if runner_os != 'Linux' || !github_job.starts_with('tcc-') {
312 if !os.exists('/usr/local/include/wkhtmltox/pdf.h') {
313 skip_files << 'examples/c_interop_wkhtmltopdf.v' // needs installation of wkhtmltopdf from https://github.com/wkhtmltopdf/packaging/releases
314 }
315 }
316 }
317 if os.user_os() == 'windows' {
318 skip_files << windows_disabled_fasthttp_veb_tests(vroot)
319 }
320 skip_files = skip_files.map(os.abs_path)
321 vargs := _vargs.replace('-progress', '')
322 hash := '${sync.thread_id().hex()}_${rand.ulid()}'
323 new_vtmp_dir := setup_new_vtmp_folder(hash)
324 if term.can_show_color_on_stderr() {
325 os.setenv('VCOLORS', 'always', true)
326 }
327 mut ts := TestSession{
328 vexe: vexe
329 vroot: vroot
330 skip_files: skip_files
331 fail_fast: fail_fast
332 show_stats: '-stats' in vargs.split(' ')
333 show_asserts: '-show-asserts' in vargs.split(' ')
334 vargs: vargs
335 vtmp_dir: new_vtmp_dir
336 hash: hash
337 silent_mode: _vargs.contains('-silent')
338 progress_mode: _vargs.contains('-progress')
339 }
340 if keep_session {
341 ts.rm_binaries = false
342 println('> vtmp_dir: ${new_vtmp_dir}')
343 }
344
345 ts.handle_test_runner_option()
346 return ts
347}
348
349fn windows_disabled_fasthttp_veb_tests(vroot string) []string {
350 mut files := []string{}
351 for dir in [
352 os.join_path(vroot, 'vlib', 'fasthttp'),
353 os.join_path(vroot, 'vlib', 'veb'),
354 ] {
355 if !os.is_dir(dir) {
356 continue
357 }
358 os.walk(dir, fn [mut files] (path string) {
359 if path.ends_with('_test.v') || path.ends_with('_test.c.v')
360 || path.ends_with('_test.js.v') {
361 files << path
362 }
363 })
364 }
365 session_app_test := os.join_path(vroot, 'vlib', 'x', 'sessions', 'tests', 'session_app_test.v')
366 if os.exists(session_app_test) {
367 files << session_app_test
368 }
369 return files
370}
371
372fn (mut ts TestSession) handle_test_runner_option() {
373 test_runner := cmdline.option(os.args, '-test-runner', 'normal')
374 if test_runner !in pref.supported_test_runners {
375 eprintln('v test: `-test-runner ${test_runner}` is not using one of the supported test runners: ${pref.supported_test_runners_list()}')
376 }
377 test_runner_implementation_file := os.join_path(ts.vroot,
378 'cmd/tools/modules/testing/output_${test_runner}.v')
379 if !os.exists(test_runner_implementation_file) {
380 eprintln('v test: using `-test-runner ${test_runner}` needs ${test_runner_implementation_file} to exist, and contain a valid testing.Reporter implementation for that runner. See `cmd/tools/modules/testing/output_dump.v` for an example.')
381 exit(1)
382 }
383 match test_runner {
384 'normal' {
385 // default, nothing to do
386 }
387 'dump' {
388 ts.reporter = DumpReporter{}
389 }
390 'teamcity' {
391 ts.reporter = TeamcityReporter{}
392 }
393 else {
394 dump('just set ts.reporter to an instance of your own struct here')
395 }
396 }
397}
398
399pub fn (mut ts TestSession) init() {
400 ts.files.sort()
401 ts.benchmark = benchmark.new_benchmark_no_cstep()
402}
403
404pub fn (mut ts TestSession) add(file string) {
405 ts.files << file
406}
407
408pub fn (mut ts TestSession) test() {
409 unbuffer_stdout()
410 // Ensure that .tmp.c files generated from compiling _test.v files,
411 // are easy to delete at the end, *without* affecting the existing ones.
412 current_wd := os.getwd()
413 if current_wd == os.wd_at_startup && current_wd == ts.vroot {
414 ts.root_relative = true
415 }
416
417 ts.init()
418 mut remaining_files := []string{}
419 for dot_relative_file in ts.files {
420 file := os.real_path(dot_relative_file)
421 if ts.build_tools && dot_relative_file.ends_with('_test.v') {
422 continue
423 }
424 // Skip OS-specific tests if we are not running that OS
425 // Special case for android_outside_termux because of its
426 // underscores
427 if file.ends_with('_android_outside_termux_test.v') {
428 if !host_os.is_target_of('android_outside_termux') {
429 remaining_files << dot_relative_file
430 ts.skip_files << file
431 continue
432 }
433 }
434 os_target := file.all_before_last('_test.v').all_after_last('_')
435 if !host_os.is_target_of(os_target) {
436 remaining_files << dot_relative_file
437 ts.skip_files << file
438 continue
439 }
440 remaining_files << dot_relative_file
441 }
442 remaining_files = vtest.filter_vtest_only(remaining_files, fix_slashes: false)
443 ts.files = remaining_files
444 ts.benchmark.set_total_expected_steps(remaining_files.len)
445 mut njobs := runtime.nr_jobs()
446 if remaining_files.len < njobs {
447 njobs = remaining_files.len
448 }
449 ts.benchmark.njobs = njobs
450 mut pool_of_test_runners := pool.new_pool_processor(callback: worker_trunner)
451 // ensure that the nmessages queue/channel, has enough capacity for handling many messages across threads, without blocking
452 ts.nmessages = chan LogMessage{cap: 10000}
453 ts.nmessage_idx = 0
454 printing_thread := spawn ts.print_messages()
455 pool_of_test_runners.set_shared_context(ts)
456 ts.reporter.worker_threads_start(remaining_files, mut ts)
457
458 ts.setup_build_environment()
459
460 // all the testing happens here:
461 pool_of_test_runners.work_on_pointers(unsafe { remaining_files.pointers() })
462
463 ts.benchmark.stop()
464 ts.append_message(.sentinel, '', MessageThreadContext{ flow_id: '-1' }) // send the sentinel
465 printing_thread.wait()
466 ts.reporter.worker_threads_finish(mut ts)
467 ts.reporter.divider()
468 ts.show_list_of_failed_tests()
469
470 // cleanup the session folder, if everything was ok:
471 if ts.benchmark.nfail == 0 {
472 if ts.rm_binaries {
473 os.rmdir_all(ts.vtmp_dir) or {}
474 }
475 }
476 if os.ls(ts.vtmp_dir) or { [] }.len == 0 {
477 os.rmdir_all(ts.vtmp_dir) or {}
478 }
479}
480
481fn worker_trunner(mut p pool.PoolProcessor, idx int, thread_id int) voidptr {
482 mut ts := unsafe { &TestSession(p.get_shared_context()) }
483 if ts.fail_fast {
484 if ts.failed_cmds.len > 0 {
485 return pool.no_result
486 }
487 }
488 // tls_bench is used to format the step messages/timings
489 mut tls_bench := unsafe { &benchmark.Benchmark(p.get_thread_context(idx)) }
490 if isnil(tls_bench) {
491 tls_bench = benchmark.new_benchmark_pointer()
492 tls_bench.set_total_expected_steps(ts.benchmark.nexpected_steps)
493 p.set_thread_context(idx, tls_bench)
494 }
495 tls_bench.no_cstep = true
496 tls_bench.njobs = ts.benchmark.njobs
497 abs_path := os.real_path(p.get_item[string](idx))
498 mut relative_file := abs_path
499 mut cmd_options :=
500 vflags.tokenize_to_args(ts.vargs) // make sure that `'-W -silent'` becomes `['-W', '-silent']`, while keeping quoted spaces intact
501 mut run_js := false
502
503 is_fmt := ts.vargs.contains('fmt')
504 is_vet := ts.vargs.contains('vet')
505 produces_file_output := !(is_fmt || is_vet)
506
507 if relative_file.ends_with('.js.v') {
508 if produces_file_output {
509 cmd_options << ' -b js'
510 run_js = true
511 }
512 }
513
514 if relative_file.ends_with('.c.v') {
515 if produces_file_output {
516 cmd_options << ' -b c'
517 run_js = false
518 }
519 }
520
521 if relative_file.contains('global') && !is_fmt {
522 cmd_options << ' -enable-globals'
523 }
524 if ts.root_relative {
525 relative_file = relative_file.replace_once(ts.vroot + os.path_separator, '')
526 }
527 normalised_relative_file := relative_file.replace('\\', '/')
528
529 file := abs_path
530 mtc := MessageThreadContext{
531 file: file
532 flow_id: thread_id.str()
533 }
534
535 // Ensure that the generated binaries will be stored in an *unique*, fresh, and per test folder,
536 // inside the common session temporary folder, used for all the tests.
537 // This is done to provide a clean working environment, for each test, that will not contain
538 // files from other tests, and will make sure that tests with the same name, can be compiled
539 // inside their own folders, without name conflicts (and without locking issues on windows,
540 // where an executable is not writable, if it is running).
541 // Note, that the common session temporary folder ts.vtmp_dir,
542 // will be removed after all tests are done.
543 test_id := '${idx}_${thread_id}'
544 mut test_folder_path := os.join_path(ts.vtmp_dir, test_id)
545 if ts.build_tools {
546 // `v build-tools`, produce all executables in the same session folder, so that they can be copied later:
547 test_folder_path = ts.vtmp_dir
548 } else {
549 os.mkdir_all(test_folder_path) or {}
550 }
551 fname := os.file_name(file)
552 // There are test files ending with `_test.v`, `_test.c.v` and `_test.js.v`.
553 mut fname_without_extension := fname.all_before_last('.v')
554 if fname_without_extension.ends_with('.c') {
555 fname_without_extension = fname_without_extension.all_before_last('.c')
556 }
557 generated_binary_fname := if os.user_os() == 'windows' && !run_js {
558 fname_without_extension + '.exe'
559 } else {
560 fname_without_extension
561 }
562 mut details := get_test_details(file)
563 if details.vflags != '' && !is_fmt {
564 cmd_options << details.vflags
565 }
566
567 reproduce_options := cmd_options.clone()
568 generated_binary_fpath := os.join_path_single(test_folder_path, generated_binary_fname)
569 if produces_file_output {
570 if ts.rm_binaries {
571 os.rm(generated_binary_fpath) or {}
572 }
573 cmd_options << ' -o ${os.quoted_path(generated_binary_fpath)}'
574 }
575 defer {
576 if produces_file_output && ts.rm_binaries {
577 os.rmdir_all(test_folder_path) or {}
578 }
579 }
580
581 mut skip_running := '-skip-running'
582 if ts.show_stats {
583 skip_running = ''
584 }
585 compile_options := cmd_options.filter(it != '-silent')
586 mut compile_vexe := ts.vexe
587 mut compile_args := '${skip_running} ${compile_options.join(' ')}'
588 mut reproduce_vexe := ts.vexe
589 mut reproduce_args := reproduce_options.join(' ')
590 // `_test.vv2` files are v2-only integration tests: full V programs that
591 // exercise v2-specific syntax. Compile them with the v2 binary instead of
592 // v1, forwarding only flags that v2 recognizes — v2 errors on unknown
593 // flags, so v1-specific options must be stripped. Preserving `-d <name>`,
594 // `-b`/`-backend`, `-cc`, `-stats`, etc. keeps `v test -d feature ...`
595 // and per-file `// vtest vflags` working for conditional code paths.
596 is_vv2 := relative_file.ends_with('_test.vv2')
597 if is_vv2 {
598 mut v2_bin := os.join_path(ts.vroot, 'cmd', 'v2', 'v2')
599 $if windows {
600 v2_bin += '.exe'
601 }
602 if !os.is_executable(v2_bin) {
603 ts.append_message(.info, 'SKIP ${relative_file}: v2 binary not built. Run: ${os.quoted_path(ts.vexe)} -o ${os.quoted_path(v2_bin)} ${os.quoted_path(os.join_path(ts.vroot,
604 'cmd', 'v2', 'v2.v'))}', mtc)
605 ts.benchmark.skip()
606 tls_bench.skip()
607 return pool.no_result
608 }
609 compile_vexe = v2_bin
610 compile_args = filter_args_for_v2(compile_options)
611 // Reproduction command must invoke v2 too, otherwise the suggested
612 // rerun fails immediately on v2-only syntax.
613 reproduce_vexe = v2_bin
614 reproduce_args = filter_args_for_v2(reproduce_options)
615 }
616 reproduce_cmd := '${os.quoted_path(reproduce_vexe)} ${reproduce_args} ${os.quoted_path(file)}'
617 cmd := '${os.quoted_path(compile_vexe)} ${compile_args} ${os.quoted_path(file)}'
618 run_cmd := if run_js {
619 'node ${os.quoted_path(generated_binary_fpath)}'
620 } else {
621 os.quoted_path(generated_binary_fpath)
622 }
623 mut should_be_built := true
624 if details.vbuild != '' {
625 should_be_built = ts.build_environment.eval(details.vbuild) or {
626 eprintln('${file}:${details.vbuild_line}:17: error during parsing the `// v test build` expression `${details.vbuild}`: ${err}')
627 false
628 }
629 $if trace_should_be_built ? {
630 eprintln('${file} has specific build constraint: `${details.vbuild}` => should_be_built: `${should_be_built}`')
631 eprintln('> env facts: ${ts.build_environment.facts}')
632 eprintln('> env defines: ${ts.build_environment.defines}')
633 }
634 }
635
636 ts.benchmark.step()
637 tls_bench.step()
638 if produces_file_output && !ts.build_tools && (!should_be_built || abs_path in ts.skip_files) {
639 ts.benchmark.skip()
640 tls_bench.skip()
641 if !hide_skips {
642 ts.append_message(.skip, tls_bench.step_message_with_label_and_duration(benchmark.b_skip,
643 normalised_relative_file, 0,
644 preparation: 1 * time.microsecond
645 ), mtc)
646 }
647 return pool.no_result
648 }
649 mut compile_cmd_duration := time.Duration(0)
650 mut cmd_duration := time.Duration(0)
651 if ts.show_stats {
652 ts.append_message(.cmd_begin, cmd, mtc)
653 d_cmd := time.new_stopwatch()
654 mut res := ts.execute(cmd, mtc)
655 mut status := res.exit_code
656 if res.output != '' {
657 output_kind := if status == 0 { MessageKind.stats_output } else { .stats_error }
658 ts.append_message(output_kind, res.output, mtc)
659 }
660
661 cmd_duration = d_cmd.elapsed()
662 ts.append_message_with_duration(.cmd_end, '', cmd_duration, mtc)
663
664 if status != 0 {
665 os.setenv('VTEST_RETRY_MAX', '${details.retry}', true)
666 for retry := 1; retry <= details.retry; retry++ {
667 if !details.hide_retries {
668 ts.append_message(.info,
669 ' [stats] retrying ${retry}/${details.retry} of ${relative_file} ; known flaky: ${details.flaky} ...',
670 mtc)
671 }
672 os.setenv('VTEST_RETRY', '${retry}', true)
673 ts.append_message(.cmd_begin, cmd, mtc)
674 d_cmd_2 := time.new_stopwatch()
675 retry_res := ts.execute(cmd, mtc)
676 status = retry_res.exit_code
677 if retry_res.output != '' {
678 output_kind := if status == 0 { MessageKind.stats_output } else { .stats_error }
679 ts.append_message(output_kind, retry_res.output, mtc)
680 }
681 cmd_duration = d_cmd_2.elapsed()
682 ts.append_message_with_duration(.cmd_end, '', cmd_duration, mtc)
683
684 if status == 0 {
685 unsafe {
686 goto test_passed_system
687 }
688 }
689 time.sleep(fail_retry_delay_ms)
690 }
691 if details.flaky && !fail_flaky {
692 ts.append_message(.info,
693 ' *FAILURE* of the known flaky test file ${relative_file} is ignored, since VTEST_FAIL_FLAKY is 0 . Retry count: ${details.retry} .\ncmd: ${cmd}',
694 mtc)
695 unsafe {
696 goto test_passed_system
697 }
698 }
699 // most probably compiler error
700 if res.output.contains(': error: ') {
701 ts.append_message(.cannot_compile, 'Cannot compile file ${file}', mtc)
702 }
703 ts.benchmark.fail()
704 tls_bench.fail()
705 ts.add_failed_cmd(reproduce_cmd)
706 return pool.no_result
707 }
708 } else {
709 if show_start {
710 ts.append_message(.info, ' starting ${relative_file} ...', mtc)
711 }
712 ts.append_message(.compile_begin, cmd, mtc)
713 compile_d_cmd := time.new_stopwatch()
714 mut compile_r := os.Result{}
715 for cretry in 0 .. max_compilation_retries {
716 compile_r = ts.execute(cmd, mtc)
717 compile_cmd_duration = compile_d_cmd.elapsed()
718 // eprintln('>>>> cretry: ${cretry} | compile_r.exit_code: ${compile_r.exit_code} | compile_cmd_duration: ${compile_cmd_duration:8} | file: ${normalised_relative_file}')
719 if compile_r.exit_code == 0 {
720 break
721 }
722 random_sleep_ms(50, 100 * cretry)
723 }
724 ts.append_message_with_duration(.compile_end, compile_r.output, compile_cmd_duration, mtc)
725 if compile_r.exit_code != 0 {
726 ts.benchmark.fail()
727 tls_bench.fail()
728 ts.append_message_with_duration(.fail, tls_bench.step_message_with_label_and_duration(benchmark.b_fail,
729 '${normalised_relative_file}\n>> compilation failed:\n${compile_r.output}',
730 cmd_duration,
731 preparation: compile_cmd_duration
732 ), cmd_duration, mtc)
733 ts.add_failed_cmd(reproduce_cmd)
734 return pool.no_result
735 }
736 tls_bench.step_restart()
737 ts.benchmark.step_restart()
738 if ts.exec_mode == .compile {
739 unsafe {
740 goto test_passed_execute
741 }
742 }
743 //
744 mut retry := 1
745 mut failure_output := strings.new_builder(1024)
746 ts.append_message(.cmd_begin, run_cmd, mtc)
747 d_cmd := time.new_stopwatch()
748 mut r := ts.execute(run_cmd, mtc)
749 cmd_duration = d_cmd.elapsed()
750 ts.append_message_with_duration(.cmd_end, r.output, cmd_duration, mtc)
751 if ts.show_asserts && r.exit_code == 0 {
752 println(r.output.split_into_lines().filter(it.contains(' assert')).join('\n'))
753 }
754 if r.exit_code != 0 {
755 mut trimmed_output := r.output.trim_space()
756 if trimmed_output.len == 0 {
757 // retry running at least 1 more time, to avoid CI false positives as much as possible
758 details.retry++
759 }
760 if details.retry != 0 {
761 failure_output.write_string(separator)
762 failure_output.writeln(' retry: 0 ; max_retry: ${details.retry} ; r.exit_code: ${r.exit_code} ; trimmed_output.len: ${trimmed_output.len}')
763 }
764 failure_output.writeln(trimmed_output)
765 os.setenv('VTEST_RETRY_MAX', '${details.retry}', true)
766 for retry = 1; retry <= details.retry; retry++ {
767 if !details.hide_retries {
768 ts.append_message(.info,
769 ' retrying ${retry}/${details.retry} of ${relative_file} ; known flaky: ${details.flaky} ...',
770 mtc)
771 }
772 os.setenv('VTEST_RETRY', '${retry}', true)
773 ts.append_message(.cmd_begin, run_cmd, mtc)
774 d_cmd_2 := time.new_stopwatch()
775 r = ts.execute(run_cmd, mtc)
776 cmd_duration = d_cmd_2.elapsed()
777 ts.append_message_with_duration(.cmd_end, r.output, cmd_duration, mtc)
778
779 if r.exit_code == 0 {
780 unsafe {
781 goto test_passed_execute
782 }
783 }
784 trimmed_output = r.output.trim_space()
785 failure_output.write_string(separator)
786 failure_output.writeln(' retry: ${retry} ; max_retry: ${details.retry} ; r.exit_code: ${r.exit_code} ; trimmed_output.len: ${trimmed_output.len}')
787 failure_output.writeln(trimmed_output)
788 time.sleep(fail_retry_delay_ms)
789 }
790 full_failure_output := failure_output.str().trim_space()
791 if details.flaky && !fail_flaky {
792 ts.append_message(.info, '>>> flaky failures so far:', mtc)
793 for line in full_failure_output.split_into_lines() {
794 ts.append_message(.info, '>>>>>> ${line}', mtc)
795 }
796 ts.append_message(.info,
797 ' *FAILURE* of the known flaky test file ${relative_file} is ignored, since VTEST_FAIL_FLAKY is 0 . Retry count: ${details.retry} .\n comp_cmd: ${cmd}\n run_cmd: ${run_cmd}',
798 mtc)
799 unsafe {
800 goto test_passed_execute
801 }
802 }
803 ts.benchmark.fail()
804 tls_bench.fail()
805 cmd_duration = d_cmd.elapsed() - (fail_retry_delay_ms * details.retry)
806 ts.append_message_with_duration(.fail, tls_bench.step_message_with_label_and_duration(benchmark.b_fail,
807 '${normalised_relative_file}\n${full_failure_output}', cmd_duration,
808 preparation: compile_cmd_duration
809 ), cmd_duration, mtc)
810 ts.add_failed_cmd(reproduce_cmd)
811 return pool.no_result
812 }
813 }
814 test_passed_system:
815 test_passed_execute:
816 ts.benchmark.ok()
817 tls_bench.ok()
818 if !hide_oks {
819 ts.append_message_with_duration(.ok, tls_bench.step_message_with_label_and_duration(benchmark.b_ok,
820 normalised_relative_file, cmd_duration,
821 preparation: compile_cmd_duration
822 ), cmd_duration, mtc)
823 }
824 return pool.no_result
825}
826
827pub fn vlib_should_be_present(parent_dir string) {
828 vlib_dir := os.join_path_single(parent_dir, 'vlib')
829 if !os.is_dir(vlib_dir) {
830 eprintln('${vlib_dir} is missing, it must be next to the V executable')
831 exit(1)
832 }
833}
834
835pub fn prepare_test_session(zargs string, folder string, oskipped []string, main_label string) TestSession {
836 vexe := pref.vexe_path()
837 parent_dir := os.dir(vexe)
838 nparent_dir := parent_dir.replace('\\', '/')
839 vlib_should_be_present(parent_dir)
840 vargs := zargs.replace(vexe, '')
841 eheader(main_label)
842 if vargs.len > 0 {
843 eprintln('v compiler args: "${vargs}"')
844 }
845 mut session := new_test_session(vargs, true)
846 files := os.walk_ext(os.join_path_single(parent_dir, folder), '.v')
847 mut mains := []string{}
848 mut skipped := oskipped.clone()
849 next_file: for f in files {
850 fnormalised := f.replace('\\', '/')
851 // Note: a `testdata` folder, is the preferred name of a folder, containing V code,
852 // that you *do not want* the test framework to find incidentally for various reasons,
853 // for example module import tests, or subtests, that are compiled/run by other parent tests
854 // in specific configurations, etc.
855 if fnormalised.contains('testdata/') || fnormalised.contains('modules/')
856 || fnormalised.contains('preludes/') {
857 continue
858 }
859 $if windows {
860 // skip process/command examples on windows. TODO: remove the need for this, fix os.Command
861 if fnormalised.ends_with('examples/process/command.v') {
862 skipped << fnormalised.replace(nparent_dir + '/', '')
863 continue
864 }
865 }
866 c := os.read_file(fnormalised) or { panic(err) }
867 start := c#[0..header_bytes_to_search_for_module_main]
868 if start.contains('module ') {
869 modname := start.all_after('module ').all_before('\n')
870 if modname !in ['main', 'no_main'] {
871 skipped << fnormalised.replace(nparent_dir + '/', '')
872 continue next_file
873 }
874 }
875 for skip_prefix in oskipped {
876 skip_folder := skip_prefix + '/'
877 if fnormalised.starts_with(skip_folder) {
878 continue next_file
879 }
880 }
881 mains << fnormalised
882 }
883 session.files << mains
884 session.skip_files << skipped
885 return session
886}
887
888pub type FnTestSetupCb = fn (mut session TestSession)
889
890pub fn v_build_failing_skipped(zargs string, folder string, oskipped []string, cb FnTestSetupCb) bool {
891 main_label := 'Building ${folder} ...'
892 finish_label := 'building ${folder}'
893 mut session := prepare_test_session(zargs, folder, oskipped, main_label)
894 cb(mut session)
895 session.test()
896 eprintln(session.benchmark.total_message(finish_label))
897 return session.failed_cmds.len > 0
898}
899
900pub fn build_v_cmd_failed(cmd string) bool {
901 res := os.execute(cmd)
902 if res.exit_code < 0 {
903 return true
904 }
905 if res.exit_code != 0 {
906 eprintln('')
907 eprintln(res.output)
908 return true
909 }
910 return false
911}
912
913pub fn building_any_v_binaries_failed() bool {
914 eheader('Building V binaries...')
915 eprintln('VFLAGS is: "' + os.getenv('VFLAGS') + '"')
916 vexe := pref.vexe_path()
917 parent_dir := os.dir(vexe)
918 vlib_should_be_present(parent_dir)
919 os.chdir(parent_dir) or { panic(err) }
920 mut failed := false
921 v_build_commands := ['${vexe} -o v_g -g cmd/v',
922 '${vexe} -o v_prod_g -prod -g cmd/v', '${vexe} -o v_cg -cg cmd/v',
923 '${vexe} -o v_prod_cg -prod -cg cmd/v', '${vexe} -o v_prod -prod cmd/v']
924 mut bmark := benchmark.new_benchmark()
925 for cmd in v_build_commands {
926 bmark.step()
927 if build_v_cmd_failed(cmd) {
928 bmark.fail()
929 failed = true
930 eprintln(bmark.step_message_fail('command: ${cmd} . See details above ^^^^^^^'))
931 eprintln('')
932 continue
933 }
934 bmark.ok()
935 if !hide_oks {
936 eprintln(bmark.step_message_ok('command: ${cmd}'))
937 }
938 }
939 bmark.stop()
940 h_divider()
941 eprintln(bmark.total_message('building v binaries'))
942 return failed
943}
944
945pub fn h_divider() {
946 eprintln(term.h_divider('-')#[..max_header_len])
947}
948
949// filter_args_for_v2 returns a command-line string containing only the flags
950// the v2 compiler accepts (`vlib/v2/pref/pref.v`). v2 errors on any unknown
951// flag, so this is used when forwarding `v test` options to v2 for
952// `_test.vv2` files. Keep these lists in sync with v2's pref validator.
953fn filter_args_for_v2(compile_options []string) string {
954 v2_value_flags := ['-backend', '-b', '-o', '-output', '-arch', '-printfn', '-gc', '-d', '-hot-fn',
955 '-cc']
956 v2_bool_flags := ['--debug', '--verbose', '-v', '--skip-genv', '--skip-builtin', '--skip-imports',
957 '--skip-type-check', '--no-parallel', '-nocache', '--nocache', '-nomarkused', '--nomarkused',
958 '-showcc', '--showcc', '-stats', '--stats', '-print-parsed-files', '--print-parsed-files',
959 '-keepc', '--profile-alloc', '-profile-alloc', '-enable-globals', '--enable-globals',
960 '-shared', '--shared', '-O0', '--single-backend', '-single-backend', '-prod', '-prealloc',
961 '-ownership']
962 tokens := vflags.tokenize_to_args(compile_options.join(' '))
963 mut out := []string{}
964 mut i := 0
965 for i < tokens.len {
966 t := tokens[i]
967 if t in v2_value_flags {
968 if i + 1 < tokens.len {
969 out << t
970 out << os.quoted_path(tokens[i + 1])
971 i += 2
972 continue
973 }
974 } else if t in v2_bool_flags {
975 out << t
976 }
977 i++
978 }
979 return out.join(' ')
980}
981
982// setup_new_vtmp_folder creates a new nested folder inside VTMP, then resets VTMP to it,
983// so that V programs/tests will write their temporary files to new location.
984// The new nested folder, and its contents, will get removed after all tests/programs succeed.
985pub fn setup_new_vtmp_folder(hash string) string {
986 new_vtmp_dir := os.join_path(os.vtmp_dir(), 'tsession_${hash}')
987 os.mkdir_all(new_vtmp_dir) or { panic(err) }
988 os.setenv('VTMP', new_vtmp_dir, true)
989 return new_vtmp_dir
990}
991
992pub struct TestDetails {
993pub mut:
994 retry int
995 flaky bool // when flaky tests fail, the whole run is still considered successful, unless VTEST_FAIL_FLAKY is 1
996 //
997 hide_retries bool // when true, all retry tries are silent; used by `vlib/v/tests/retry_test.v`
998 vbuild string // could be `!(windows && tinyc)`
999 vbuild_line int // for more precise error reporting, if the `vbuild` expression is incorrect
1000 vflags string // custom compilation flags for the test (enables for example: `// vtest vflags: -w`, for tests that have known warnings, but should still pass with -W)
1001}
1002
1003pub fn get_test_details(file string) TestDetails {
1004 mut res := TestDetails{}
1005 if !os.is_file(file) {
1006 return res
1007 }
1008 lines := os.read_lines(file) or { [] }
1009 for idx, line in lines {
1010 if line.starts_with('// vtest retry:') {
1011 res.retry = line.all_after(':').trim_space().int()
1012 }
1013 if line.starts_with('// vtest flaky:') {
1014 res.flaky = line.all_after(':').trim_space().bool()
1015 }
1016 if line.starts_with('// vtest build:') {
1017 res.vbuild = line.all_after(':').trim_space()
1018 res.vbuild_line = idx + 1
1019 }
1020 if line.starts_with('// vtest vflags:') {
1021 res.vflags = line.all_after(':').trim_space()
1022 }
1023 if line.starts_with('// vtest hide_retries') {
1024 res.hide_retries = true
1025 }
1026 }
1027 return res
1028}
1029
1030pub fn find_started_process(pname string) !string {
1031 for line in all_processes {
1032 if line.contains(pname) {
1033 return line
1034 }
1035 }
1036 return error('could not find process matching ${pname}')
1037}
1038
1039fn limited_header(msg string) string {
1040 return term.header_left(msg, '-')#[..max_header_len]
1041}
1042
1043pub fn eheader(msg string) {
1044 eprintln(limited_header(msg))
1045}
1046
1047pub fn header(msg string) {
1048 println(limited_header(msg))
1049 flush_stdout()
1050}
1051
1052fn random_sleep_ms(_ int, _ int) {
1053 time.sleep((50 + rand.intn(50) or { 0 }) * time.millisecond)
1054}
1055
1056fn get_max_header_len() int {
1057 maximum := 140
1058 cols, _ := term.get_terminal_size()
1059 if cols > maximum {
1060 return maximum
1061 }
1062 return cols
1063}
1064
1065fn check_openssl_present() bool {
1066 if github_job.ends_with('-windows') {
1067 // TODO: investigate the https://github.com/vlang/v/actions/runs/18590919000/job/53005499130 failure in more details
1068 return false
1069 }
1070 $if openbsd {
1071 return os.execute('eopenssl35 --version').exit_code == 0
1072 && os.execute('${pkgcmd} eopenssl35 --libs').exit_code == 0
1073 } $else {
1074 return os.execute('openssl --version').exit_code == 0
1075 && os.execute('${pkgcmd} openssl --libs').exit_code == 0
1076 }
1077}
1078
1079fn check_modern_openssl_present() bool {
1080 if !is_openssl_present {
1081 return false
1082 }
1083 mut version_cmd := 'openssl version'
1084 $if openbsd {
1085 version_cmd = 'eopenssl35 version'
1086 }
1087 res := os.execute(version_cmd)
1088 if res.exit_code != 0 {
1089 return false
1090 }
1091 line := res.output.trim_space()
1092 if !line.starts_with('OpenSSL ') {
1093 return false
1094 }
1095 version := semver.coerce(line) or { return false }
1096 return version.satisfies('>=3.5.0')
1097}
1098
1099pub const is_openssl_present = check_openssl_present()
1100pub const is_modern_openssl_present = check_modern_openssl_present()
1101
1102// is_started_mysqld is true, when the test runner determines that there is a running mysql server
1103pub const is_started_mysqld = find_started_process('mysqld') or { '' }
1104
1105// is_started_postgres is true, when the test runner determines that there is a running postgres server
1106pub const is_started_postgres = find_started_process('postgres') or { '' }
1107
1108// is_started_mssql is true, when the test runner determines that there is a running sql server
1109pub const is_started_mssql = find_started_process('sqlservr') or { '' }
1110
1111// is_started_redis is true, when the test runner determines that there is a running redis server
1112pub const is_started_redis = find_started_process('redis-server') or { '' }
1113
1114pub fn (mut ts TestSession) setup_build_environment() {
1115 facts, mut defines := pref.get_build_facts_and_defines()
1116 // add the runtime information, that the test runner has already determined by checking once:
1117 if github_job.starts_with('sanitize-') {
1118 defines << 'sanitized_job'
1119 }
1120 if is_started_mysqld != '' {
1121 defines << 'started_mysqld'
1122 }
1123 if is_started_postgres != '' {
1124 defines << 'started_postgres'
1125 }
1126 if is_started_mssql != '' {
1127 defines << 'started_mssql'
1128 }
1129 if is_started_redis != '' {
1130 defines << 'started_redis'
1131 }
1132 if is_node_present {
1133 defines << 'present_node'
1134 }
1135 if is_python_present {
1136 defines << 'present_python'
1137 }
1138 if is_ruby_present {
1139 defines << 'present_ruby'
1140 }
1141 if is_go_present {
1142 defines << 'present_go'
1143 }
1144 if is_sqlite3_present {
1145 defines << 'present_sqlite3'
1146 }
1147 if is_openssl_present {
1148 defines << 'present_openssl'
1149 }
1150 if is_modern_openssl_present {
1151 defines << 'has_modern_openssl'
1152 }
1153
1154 // detect the linux distribution as well when possible:
1155 if os.is_file('/etc/os-release') {
1156 mut distro_kind := ''
1157 if lines := os.read_lines('/etc/os-release') {
1158 for line in lines {
1159 if line.starts_with('ID=') {
1160 distro_kind = line.all_after('ID=')
1161 break
1162 }
1163 }
1164 }
1165 if distro_kind != '' {
1166 defines << 'os_id_${distro_kind}' // os_id_alpine, os_id_freebsd, os_id_ubuntu, os_id_debian etc
1167 }
1168 }
1169
1170 defines << ts.custom_defines
1171 $if trace_vbuild ? {
1172 eprintln('>>> testing.get_build_environment facts: ${facts}')
1173 eprintln('>>> testing.get_build_environment defines: ${defines}')
1174 }
1175 ts.build_environment = build_constraint.new_environment(facts, defines)
1176}
1177