| 1 | // vtest build: !self_sandboxed_packaging? && !sanitized_job? |
| 2 | import os |
| 3 | import term |
| 4 | import v.util.diff |
| 5 | import v.util.vtest |
| 6 | import time |
| 7 | import runtime |
| 8 | import benchmark |
| 9 | |
| 10 | const 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 | |
| 15 | const 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 | |
| 22 | const 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 | |
| 28 | const skip_on_ci_musl = [ |
| 29 | 'vlib/v/tests/skip_unused/gg_code.vv', |
| 30 | ] |
| 31 | |
| 32 | const vexe = os.getenv('VEXE') |
| 33 | |
| 34 | @[markused] |
| 35 | const turn_off_vcolors = os.setenv('VCOLORS', 'never', true) |
| 36 | |
| 37 | const 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: |
| 41 | const turn_on_normal_test_runner = os.setenv('VTEST_RUNNER', 'normal', true) |
| 42 | |
| 43 | const should_autofix = os.getenv('VAUTOFIX') != '' |
| 44 | |
| 45 | const is_silent = $if silent ? { true } $else { false } |
| 46 | |
| 47 | const github_job = os.getenv('GITHUB_JOB') |
| 48 | |
| 49 | const should_show_details = !is_silent && github_job == '' |
| 50 | |
| 51 | const v_ci_ubuntu_musl = os.getenv('V_CI_UBUNTU_MUSL').len > 0 |
| 52 | |
| 53 | const v_ci_musl = os.getenv('V_CI_MUSL').len > 0 |
| 54 | |
| 55 | const v_ci_cstrict = os.getenv('V_CI_CSTRICT').len > 0 |
| 56 | |
| 57 | struct TaskDescription { |
| 58 | vexe string |
| 59 | evars string |
| 60 | dir string |
| 61 | voptions string |
| 62 | result_extension string |
| 63 | path string |
| 64 | mut: |
| 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 | |
| 77 | struct Tasks { |
| 78 | vexe string |
| 79 | parallel_jobs int // 0 is using VJOBS, anything else is an override |
| 80 | label string |
| 81 | mut: |
| 82 | show_cmd bool |
| 83 | all []TaskDescription |
| 84 | } |
| 85 | |
| 86 | fn 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 | |
| 193 | fn (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 | |
| 198 | fn (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 | |
| 203 | fn (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 | |
| 221 | fn 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 |
| 226 | fn (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 |
| 344 | fn 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 | |
| 373 | fn (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 |
| 381 | fn (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 | |
| 409 | fn 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 | |
| 418 | fn 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 | |
| 426 | fn 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 | |
| 434 | fn 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 | |
| 449 | fn 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 | |
| 461 | fn get_max_ntries() int { |
| 462 | return if v_ci_musl { 3 } else { 1 } |
| 463 | } |
| 464 | |