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