vxx / vlib / v / gen / c / coutput_test.v
1028 lines · 970 sloc · 38.26 KB · 106e6ec1141d9a4382a13e40641aadee6313c717
Raw
1// vtest build: !self_sandboxed_packaging? && !sanitized_job?
2import os
3import time
4import term
5import v.util.diff
6import v.util.vtest
7
8const vexe = @VEXE
9
10const vroot = os.real_path(@VMODROOT)
11
12const local_tdata_path = 'vlib/v/gen/c/testdata'
13
14const testdata_folder = os.real_path(os.join_path(vroot, local_tdata_path))
15
16const show_compilation_output = os.getenv('VTEST_SHOW_COMPILATION_OUTPUT').int() == 1
17
18const user_os = os.user_os()
19
20const gcc_path = os.find_abs_path_of_executable('gcc') or { '' }
21
22fn mm(s string) string {
23 return term.colorize(term.magenta, s)
24}
25
26fn mj(input ...string) string {
27 return mm(input.filter(it.len > 0).join(' '))
28}
29
30fn test_out_files() {
31 os.chdir(vroot) or {}
32 output_path := os.join_path(os.vtmp_dir(), 'coutput_outs')
33 os.mkdir_all(output_path)!
34 defer {
35 os.rmdir_all(output_path) or {}
36 }
37 files := os.ls(testdata_folder) or { [] }
38 tests := files.filter(it.ends_with('.out'))
39 if tests.len == 0 {
40 eprintln('no `.out` tests found in ${testdata_folder}')
41 return
42 }
43 mut total_errors := 0
44 mut total_oks := 0
45 mut total_oks_panic := 0
46 mut total_skips := 0
47 paths := vtest.filter_vtest_only(tests, basepath: testdata_folder).sorted()
48 println(term.colorize(term.green,
49 '> testing whether ${paths.len} .out files in ${local_tdata_path} match:'))
50 for out_path in paths {
51 basename, path, relpath, out_relpath := target2paths(out_path, '.out')
52 if should_skip(relpath) {
53 total_skips++
54 continue
55 }
56 pexe := os.join_path(output_path, '${basename}.exe')
57 //
58 file_options := get_file_options(path)
59 if user_os == 'windows' && file_options.vflags.contains('-cc clang') && gcc_path.len > 0
60 && github_job.contains('gcc') {
61 eprintln('> skipping ${relpath} on gcc-windows, since it requires clang')
62 total_skips++
63 continue
64 }
65 alloptions := '-o ${os.quoted_path(pexe)} ${file_options.vflags}'
66 label := mj('v', file_options.vflags, 'run', relpath) + ' == ${mm(out_relpath)} '
67 //
68 compile_cmd := '${os.quoted_path(vexe)} ${alloptions} ${os.quoted_path(path)}'
69 sw_compile := time.new_stopwatch()
70 compilation := os.execute(compile_cmd)
71 compile_ms := sw_compile.elapsed().milliseconds()
72 ensure_compilation_succeeded(compilation, compile_cmd)
73 //
74 sw_run := time.new_stopwatch()
75 res := os.execute(os.quoted_path(pexe))
76 run_ms := sw_run.elapsed().milliseconds()
77 //
78 if res.exit_code < 0 {
79 println('${term.red('FAIL')} C:${compile_ms:6}ms, R:${run_ms:2}ms ${label}')
80 println(' run crashed with exit code: ${res.exit_code}')
81 if res.output.len > 0 {
82 println(res.output)
83 }
84 total_errors++
85 continue
86 }
87 mut found := res.output.trim_right('\r\n').replace('\r\n', '\n')
88 mut expected := os.read_file(out_path)!
89 expected = expected.trim_right('\r\n').replace('\r\n', '\n')
90 if expected.contains('================ V panic ================') {
91 // panic include backtraces and absolute file paths, so can't do char by char comparison
92 n_found := normalize_panic_message(found, vroot)
93 n_expected := normalize_panic_message(expected, vroot)
94 if found.contains('================ V panic ================') {
95 if n_found.starts_with(n_expected) {
96 vprintln('${term.green('OK (panic)')} C:${compile_ms:6}ms, R:${run_ms:2}ms ${label}')
97 total_oks_panic++
98 continue
99 } else {
100 // Both have panics, but there was a difference...
101 // Pass the normalized strings for further reporting.
102 // There is no point in comparing the backtraces too.
103 found = n_found
104 expected = n_expected
105 }
106 }
107 }
108 if expected != found {
109 println('${term.red('FAIL')} C:${compile_ms:6}ms, R:${run_ms:2}ms ${label}')
110 if diff_ := diff.compare_text(expected, found) {
111 println(term.header('difference:', '-'))
112 println(diff_)
113 } else {
114 println(term.header('expected:', '-'))
115 println(expected)
116 println(term.header('found:', '-'))
117 println(found)
118 }
119 println(term.h_divider('-'))
120 total_errors++
121 } else {
122 vprintln('${term.green('OK ')} C:${compile_ms:6}ms, R:${run_ms:2}ms ${label}')
123 total_oks++
124 }
125 }
126 println('>>> Summary for test_out_files: files: ${paths.len}, oks: ${total_oks}, ok panics: ${total_oks_panic}, skipped: ${total_skips}, error: ${total_errors} .')
127 assert total_errors == 0
128}
129
130fn test_c_must_have_files() {
131 os.chdir(vroot) or {}
132 output_path := os.join_path(os.vtmp_dir(), 'coutput_c_must_haves')
133 os.mkdir_all(output_path)!
134 defer {
135 os.rmdir_all(output_path) or {}
136 }
137 files := os.ls(testdata_folder) or { [] }
138 tests := files.filter(it.ends_with('.c.must_have'))
139 if tests.len == 0 {
140 eprintln('no `.c.must_have` files found in ${testdata_folder}')
141 return
142 }
143 paths := vtest.filter_vtest_only(tests, basepath: testdata_folder).sorted()
144 mut total_errors := 0
145 mut total_oks := 0
146 mut total_oks_panic := 0
147 mut total_skips := 0
148 mut failed_descriptions := []string{cap: paths.len}
149 println(term.colorize(term.green,
150 '> testing whether all line patterns in ${paths.len} `.c.must_have` files in ${local_tdata_path} match:'))
151 for must_have_path in paths {
152 basename, path, relpath, must_have_relpath := target2paths(must_have_path, '.c.must_have')
153 if should_skip(relpath) {
154 total_skips++
155 continue
156 }
157 file_options := get_file_options(path)
158 alloptions := '-o - ${file_options.vflags}'
159 mut description := mj('v', alloptions, relpath) + ' matches ${mm(must_have_relpath)} '
160 cmd := '${os.quoted_path(vexe)} ${alloptions} ${os.quoted_path(path)}'
161 sw_compile := time.new_stopwatch()
162 compilation := os.execute(cmd)
163 compile_ms := sw_compile.elapsed().milliseconds()
164 ensure_compilation_succeeded(compilation, cmd)
165 expected_lines := os.read_lines(must_have_path) or { [] }
166 generated_c_lines := compilation.output.split_into_lines()
167 mut nmatches := 0
168 mut failed_patterns := []string{}
169 for idx_expected_line, eline in expected_lines {
170 if does_line_match_one_of_generated_lines(eline, generated_c_lines) {
171 nmatches++
172 // eprintln('> testing: ${must_have_path} has line: ${eline}')
173 } else {
174 failed_patterns << eline
175 description += '\n failed pattern: `${eline}`'
176 println('${term.red('FAIL')} C:${compile_ms:5}ms ${description}')
177 eprintln('${must_have_path}:${idx_expected_line + 1}: expected match error:')
178 eprintln('`${cmd}` did NOT produce expected line:')
179 eprintln(term.colorize(term.red, eline))
180 if description !in failed_descriptions {
181 failed_descriptions << description
182 }
183 total_errors++
184 continue
185 }
186 }
187 if nmatches == expected_lines.len {
188 vprintln('${term.green('OK ')} C:${compile_ms:5}ms ${description}')
189 total_oks++
190 } else {
191 if show_compilation_output {
192 eprintln('> ALL lines:')
193 eprintln(compilation.output)
194 }
195 eprintln('--------- failed patterns: -------------------------------------------')
196 for fpattern in failed_patterns {
197 eprintln(fpattern)
198 }
199 eprintln('----------------------------------------------------------------------')
200 }
201 }
202 if failed_descriptions.len > 0 {
203 eprintln('--------- failed commands: -------------------------------------------')
204 for fd in failed_descriptions {
205 eprintln(' > ${fd}')
206 }
207 eprintln('----------------------------------------------------------------------')
208 }
209 println('>>> Summary for test_c_must_have_files: files: ${paths.len}, oks: ${total_oks}, ok panics: ${total_oks_panic}, skipped: ${total_skips}, error: ${total_errors} .')
210 assert total_errors == 0
211}
212
213fn test_or_block_err_var_collision_does_not_emit_self_referential_err() {
214 os.chdir(vroot) or {}
215 path := os.join_path(testdata_folder, 'or_block_err_var_collision.vv')
216 cmd := '${os.quoted_path(vexe)} -o - ${os.quoted_path(path)}'
217 compilation := os.execute(cmd)
218 ensure_compilation_succeeded(compilation, cmd)
219 assert !compilation.output.contains('IError err = err.err;')
220 mut source_err_tmp := ''
221 mut has_visible_or_block_err := false
222 for line in compilation.output.split_into_lines() {
223 trimmed := line.trim_space()
224 if trimmed.starts_with('IError _t') && trimmed.ends_with(' = err.err;') {
225 source_err_tmp = trimmed.all_after('IError ').all_before(' = err.err;')
226 }
227 if trimmed.starts_with('IError _t') && trimmed.contains('.err;') {
228 err_tmp := trimmed.all_after('IError ').all_before(' = ')
229 if compilation.output.contains('IError err = ${err_tmp};') {
230 has_visible_or_block_err = true
231 }
232 }
233 }
234 assert source_err_tmp != ''
235 assert !compilation.output.contains('IError err = ${source_err_tmp};')
236 assert has_visible_or_block_err
237}
238
239fn test_main_error_propagation_panic_branches_do_not_fall_through() {
240 os.chdir(vroot) or {}
241 test_dir := os.join_path(os.vtmp_dir(), 'main_error_propagation_panic_tail_${os.getpid()}')
242 os.mkdir_all(test_dir)!
243 defer {
244 os.rmdir_all(test_dir) or {}
245 }
246 source_path := os.join_path(test_dir, 'main_error_propagation_panic_tail.v')
247 os.write_file(source_path,
248 ['module main', '', 'fn fail_result() ! {', "\treturn error('new error')", '}', '', 'fn fail_option() ?int {', '\treturn none', '}', '', 'fn defer_result() ! {', '\tdefer {', "\t\tprintln('result deferred')", '\t}', '\tfail_result()!', '}', '', 'fn defer_option() ?int {', '\tdefer {', "\t\tprintln('option deferred')", '\t}', '\treturn fail_option()', '}', '', 'fn main() {', '\tif arguments().len > 1000 {', '\t\tdefer_option()?', '\t}', '\tdefer_result()!', '}'].join('\n') +
249 '\n')!
250 cmd := '${os.quoted_path(vexe)} -o - ${os.quoted_path(source_path)}'
251 compilation := os.execute(cmd)
252 ensure_compilation_succeeded(compilation, cmd)
253 for panic_call in [
254 'builtin__panic_result_not_set(IError_name_table[',
255 'builtin__panic_option_not_set( IError_name_table[',
256 ] {
257 assert compilation.output.contains(panic_call)
258 branch_tail := compilation.output.all_after(panic_call).all_before('}')
259 // The panic helper is `@[noreturn]`, so the branch is closed off with
260 // `VUNREACHABLE();` (matching the other panic sites) instead of dead code
261 // after the noreturn call.
262 assert branch_tail.contains('VUNREACHABLE();')
263 }
264}
265
266fn test_imported_empty_interface_concat_does_not_emit_noop_array_cast_helper() {
267 os.chdir(vroot) or {}
268 path := os.join_path(vroot,
269 'vlib/v/tests/modules/interface_array_concat_from_another_module/main_test.v')
270 symbol := '__v_array_to_interface_array__Array_interface_array_concat_from_another_module__mod__Value__to__Array_interface_array_concat_from_another_module__mod__Value'
271 cmd := '${os.quoted_path(vexe)} -o - ${os.quoted_path(path)}'
272 compilation := os.execute(cmd)
273 ensure_compilation_succeeded(compilation, cmd)
274 assert !compilation.output.contains(symbol)
275}
276
277fn test_comptime_for_empty_attrs_does_not_emit_new_array_calls() {
278 os.chdir(vroot) or {}
279 path := os.join_path(testdata_folder, 'comptime_for_empty_attrs_inline_init.vv')
280 cmd := '${os.quoted_path(vexe)} -o - ${os.quoted_path(path)}'
281 compilation := os.execute(cmd)
282 ensure_compilation_succeeded(compilation, cmd)
283 // regression for https://github.com/vlang/v/issues/27274
284 assert !compilation.output.contains('.attrs = builtin____new_array_with_default(0, 0, sizeof(string), 0)')
285 assert !compilation.output.contains('.attributes = builtin____new_array_with_default(0, 0, sizeof(VAttribute), 0)')
286 assert !compilation.output.contains('.args = builtin____new_array_with_default(0, 0, sizeof(FunctionParam), 0)')
287}
288
289fn test_array_push_no_bounds_checking_keeps_max_len_panics() {
290 os.chdir(vroot) or {}
291 test_source := os.join_path(os.vtmp_dir(), 'coutput_array_push_no_bounds_checking.vv')
292 source_lines := [
293 'module main',
294 '',
295 'fn main() {',
296 '\tmut names := []string{}',
297 "\tnames << 'alpha'",
298 '\tmut scores := []int{}',
299 '\tscores << 1',
300 '\tprintln(names.len + scores.len)',
301 '}',
302 ]
303 os.write_file(test_source, source_lines.join('\n') + '\n')!
304 defer {
305 os.rm(test_source) or {}
306 }
307 cmd := '${os.quoted_path(vexe)} -prod -no-bounds-checking -o - ${os.quoted_path(test_source)}'
308 compilation := os.execute(cmd)
309 ensure_compilation_succeeded(compilation, cmd)
310 assert compilation.output.contains('VV_LOC void builtin__array_push(array* a, voidptr val) {')
311 assert compilation.output.contains('VV_LOC void builtin__array_push_noscan(array* a, voidptr val) {')
312 assert !compilation.output.contains('array.push: negative len')
313 assert compilation.output.contains('array.push: len bigger than max_int')
314 assert !compilation.output.contains('array.push_noscan: negative len')
315 assert compilation.output.contains('array.push_noscan: len bigger than max_int')
316 || compilation.output.contains('builtin__array_push(a, val);')
317}
318
319fn test_windows_sharedlive_string_interpolation_in_ternary_does_not_emit_inline_tmp_decl() {
320 os.chdir(vroot) or {}
321 test_source := os.join_path(os.vtmp_dir(), 'coutput_live_windows_ternary_str_intp.vv')
322 os.write_file(test_source,
323 "module main\n\n@[live]\nfn foo(ok bool, name string) string {\n\treturn if ok { 'Hello, \${name}!' } else { '\${u32(7)}' }\n}\n\nfn main() {\n\tprintln(foo(true, 'V'))\n}\n")!
324 defer {
325 os.rm(test_source) or {}
326 }
327 cmd := '${os.quoted_path(vexe)} -o - -os windows -sharedlive ${os.quoted_path(test_source)}'
328 compilation := os.execute(cmd)
329 ensure_compilation_succeeded(compilation, cmd)
330 mut normalized := compilation.output.replace('\t', ' ').replace('\n', ' ')
331 for normalized.contains(' ') {
332 normalized = normalized.replace(' ', ' ')
333 }
334 assert !normalized.contains('? ( string _t')
335 assert compilation.output.contains('builtin__str_intp')
336}
337
338fn test_simple_string_interpolation_does_not_emit_str_intp_runtime() {
339 os.chdir(vroot) or {}
340 test_source := os.join_path(os.vtmp_dir(), 'coutput_simple_interpolation_no_str_intp.vv')
341 os.write_file(test_source,
342 "module main\n\nimport time\n\nfn main() {\n\tt := time.now()\n\tprintln('elapsed \${time.since(t)}')\n}\n")!
343 defer {
344 os.rm(test_source) or {}
345 }
346 cmd := '${os.quoted_path(vexe)} -o - ${os.quoted_path(test_source)}'
347 compilation := os.execute(cmd)
348 ensure_compilation_succeeded(compilation, cmd)
349 assert !compilation.output.contains('builtin__str_intp')
350 assert !compilation.output.contains('StrIntpData')
351}
352
353fn test_auto_str_float_array_still_emits_str_intp_runtime() {
354 os.chdir(vroot) or {}
355 test_source := os.join_path(os.vtmp_dir(), 'coutput_float_array_str_intp.vv')
356 os.write_file(test_source, 'module main\n\nfn main() {\n\tprintln([f32(1.2), f32(3.4)])\n}\n')!
357 defer {
358 os.rm(test_source) or {}
359 }
360 cmd := '${os.quoted_path(vexe)} -o - ${os.quoted_path(test_source)}'
361 compilation := os.execute(cmd)
362 ensure_compilation_succeeded(compilation, cmd)
363 assert compilation.output.contains('builtin__str_intp')
364 assert compilation.output.contains('StrIntpData')
365}
366
367fn test_windows_tcc_atomic_postfix_uses_interlocked_helpers() {
368 os.chdir(vroot) or {}
369 cc := windows_tcc_ccompiler_for_coutput_test()
370 if cc == '' {
371 eprintln('> skipping ${@FN} since tcc is not available on windows')
372 return
373 }
374 test_source := os.join_path(os.vtmp_dir(), 'coutput_windows_tcc_atomic_postfix.vv')
375 os.write_file(test_source, 'module main
376
377struct App {
378mut:
379 idx atomic int
380}
381
382fn (mut app App) bump() {
383 app.idx++
384}
385
386fn main() {
387 mut app := App{}
388 app.bump()
389}
390')!
391 defer {
392 os.rm(test_source) or {}
393 }
394 cmd := '${os.quoted_path(vexe)} -o - -os windows -cc ${cc} ${os.quoted_path(test_source)}'
395 compilation := os.execute(cmd)
396 ensure_compilation_succeeded(compilation, cmd)
397 assert compilation.output.contains('thirdparty/stdatomic/win/atomic.h')
398 assert compilation.output.contains('InterlockedExchangeAdd')
399 assert !compilation.output.contains('__atomic_fetch_add')
400}
401
402fn test_windows_tcc_boehm_prod_does_not_emit_gc_remove_roots() {
403 os.chdir(vroot) or {}
404 cc := windows_tcc_ccompiler_for_coutput_test()
405 if cc == '' {
406 eprintln('> skipping ${@FN} since tcc is not available on windows')
407 return
408 }
409 test_source := os.join_path(os.vtmp_dir(), 'coutput_windows_tcc_boehm_scope_pin.vv')
410 os.write_file(test_source, "module main
411
412fn use_value(value string) {
413 println(value)
414}
415
416fn main() {
417 values := ['alpha', 'beta']
418 use_value(values[0])
419}
420")!
421 defer {
422 os.rm(test_source) or {}
423 }
424 cmd := '${os.quoted_path(vexe)} -o - -os windows -cc ${cc} -prod ${os.quoted_path(test_source)}'
425 compilation := os.execute(cmd)
426 ensure_compilation_succeeded(compilation, cmd)
427 assert !compilation.output.contains('GC_remove_roots(')
428}
429
430fn windows_tcc_ccompiler_for_coutput_test() string {
431 if user_os != 'windows' {
432 return 'x86_64-w64-mingw32-tcc'
433 }
434 bundled_tcc := os.join_path(vroot, 'thirdparty', 'tcc', 'tcc.exe')
435 if os.is_file(bundled_tcc) && os.is_executable(bundled_tcc) {
436 return os.quoted_path(bundled_tcc)
437 }
438 if os.find_abs_path_of_executable('tcc') or { '' } != '' {
439 return 'tcc'
440 }
441 return ''
442}
443
444fn test_no_main_exports_initialize_windows_runtime() {
445 os.chdir(vroot) or {}
446 test_source := os.join_path(os.vtmp_dir(), 'coutput_no_main_export_windows_init.vv')
447 os.write_file(test_source,
448 "module no_main\n\n@[export: 'v_sdl_app_quit']\npub fn app_quit() {}\n")!
449 defer {
450 os.rm(test_source) or {}
451 }
452 cmd := '${os.quoted_path(vexe)} -o - -os windows ${os.quoted_path(test_source)}'
453 compilation := os.execute(cmd)
454 ensure_compilation_succeeded(compilation, cmd)
455 generated_c_lines := compilation.output.split_into_lines()
456 expected_lines := [
457 'static void _vno_main_init_caller(void);',
458 'static void _vno_main_cleanup_caller(void);',
459 'void v_sdl_app_quit(void) {',
460 '_vno_main_init_caller();',
461 'void _vinit(int ___argc, voidptr ___argv) {',
462 'static bool once = false; if (once) {return;} once = true;',
463 'void _vcleanup(void) {',
464 'static void _vno_main_cleanup_caller(void) {',
465 'static void _vno_main_init_caller(void) {',
466 'con_valid = AttachConsole(ATTACH_PARENT_PROCESS);',
467 'err = freopen_s(&res_fp, "NUL", "w", stdout);',
468 '_vinit(0,0);',
469 'atexit(_vno_main_cleanup_caller);',
470 ]
471 for expected_line in expected_lines {
472 assert does_line_match_one_of_generated_lines(expected_line, generated_c_lines)
473 }
474}
475
476fn test_coverage_output_checks_counter_file_open() {
477 os.chdir(vroot) or {}
478 test_source := os.join_path(os.vtmp_dir(), 'coutput_coverage_file_guard.vv')
479 os.write_file(test_source, 'fn main() {\n\tprintln("coverage")\n}\n')!
480 defer {
481 os.rm(test_source) or {}
482 }
483 coverage_dir := os.join_path(os.vtmp_dir(), 'coutput_coverage')
484 cmd := '${os.quoted_path(vexe)} -o - -coverage ${os.quoted_path(coverage_dir)} ${os.quoted_path(test_source)}'
485 compilation := os.execute(cmd)
486 ensure_compilation_succeeded(compilation, cmd)
487 assert compilation.output.contains('FILE *fp = fopen(cov_filename, "wb+");')
488 assert compilation.output.contains('if (fp == NULL) { return; }')
489 assert compilation.output.contains('nsecs = ts.tv_nsec;')
490 assert !compilation.output.contains('\nsecs = ts.tv_nsec;')
491}
492
493fn test_c_fallback_decl_uses_module_wide_c_includes() {
494 os.chdir(vroot) or {}
495 test_source := os.join_path(os.vtmp_dir(), 'coutput_module_c_include')
496 os.rmdir_all(test_source) or {}
497 os.mkdir_all(test_source)!
498 defer {
499 os.rmdir_all(test_source) or {}
500 }
501 header_path := os.join_path(test_source, 'c_header_decl.h')
502 os.write_file(header_path, 'int c_header_decl(const char* input);\n')!
503 header_include_path := header_path.replace('\\', '/')
504 os.write_file(os.join_path(test_source, 'include.v'), 'module main
505
506#include "${header_include_path}"
507')!
508 os.write_file(os.join_path(test_source, 'decl.v'), "module main
509
510fn C.c_header_decl(input &char) int
511
512fn main() {
513 C.c_header_decl(c'text')
514}
515")!
516 cmd := '${os.quoted_path(vexe)} -o - ${os.quoted_path(test_source)}'
517 compilation := os.execute(cmd)
518 ensure_compilation_succeeded(compilation, cmd)
519 assert !compilation.output.contains('extern int c_header_decl(')
520}
521
522fn test_c_fallback_decl_uses_c_helper_submodule_includes() {
523 test_source := os.join_path(os.vtmp_dir(), 'coutput_c_helper_include')
524 module_path := os.join_path(test_source, 'coutput_sdl')
525 c_module_path := os.join_path(module_path, 'c')
526 os.rmdir_all(test_source) or {}
527 os.mkdir_all(c_module_path)!
528 defer {
529 os.rmdir_all(test_source) or {}
530 }
531 header_path := os.join_path(c_module_path, 'c_helper_decl.h')
532 os.write_file(header_path,
533 ['#include <stdbool.h>', 'typedef enum { false_value, true_value } foreign_bool;', 'foreign_bool c_helper_decl(void);'].join('\n') +
534 '\n')!
535 header_include_path := header_path.replace('\\', '/')
536 os.write_file(os.join_path(c_module_path, 'c.c.v'), 'module c
537
538pub const used_import = 1
539
540#include "${header_include_path}"
541')!
542 os.write_file(os.join_path(module_path, 'coutput_sdl.v'), 'module coutput_sdl
543
544import coutput_sdl.c
545
546pub const used_import = c.used_import
547')!
548 os.write_file(os.join_path(module_path, 'atomic.c.v'), 'module coutput_sdl
549
550fn C.c_helper_decl() bool
551
552pub fn call() {
553 _ = C.c_helper_decl()
554}
555')!
556 old_wd := os.getwd()
557 os.chdir(test_source) or {}
558 defer {
559 os.chdir(old_wd) or {}
560 }
561 cmd := '${os.quoted_path(vexe)} -shared -o - coutput_sdl'
562 compilation := os.execute(cmd)
563 ensure_compilation_succeeded(compilation, cmd)
564 assert compilation.output.contains('#include "${header_include_path}"')
565 assert !compilation.output.contains('extern bool c_helper_decl(')
566}
567
568fn test_user_defined_windows_dllmain_disables_generated_entrypoint() {
569 os.chdir(vroot) or {}
570 test_source := os.join_path(os.vtmp_dir(), 'coutput_user_defined_windows_dllmain.vv')
571 os.write_file(test_source,
572 ['module test', '', 'pub type C.DWORD = u32', 'pub type C.LPVOID = voidptr', '', 'fn C._vinit_caller()', 'fn C._vcleanup_caller()', '', "@[export: 'library_answer']", 'pub fn library_answer() int {', '\treturn 42', '}', '', "@[export: 'DllMain']", 'pub fn dll_main(hinst C.HINSTANCE, reason C.DWORD, reserved C.LPVOID) C.BOOL {', '\t_ = hinst', '\t_ = reserved', '\tif reason == C.DWORD(1) {', '\t\tC._vinit_caller()', '\t} else if reason == C.DWORD(0) {', '\t\tC._vcleanup_caller()', '\t}', '\treturn 1', '}'].join('\n') +
573 '\n')!
574 defer {
575 os.rm(test_source) or {}
576 }
577 cmd := '${os.quoted_path(vexe)} -o - -os windows -shared -gc boehm ${os.quoted_path(test_source)}'
578 compilation := os.execute(cmd)
579 ensure_compilation_succeeded(compilation, cmd)
580 assert compilation.output.contains('void _vinit_caller() {')
581 assert compilation.output.contains('GC_set_pages_executable(0);')
582 // The shared-library GC tuning (issue #27555) must stay guarded, so loading
583 // the library into an already-GC-initialized host does not clobber the host's
584 // process-wide free-space divisor (its local GC_INIT() would be a no-op).
585 assert compilation.output.contains('if (!GC_is_init_called()) {')
586 assert compilation.output.contains('GC_INIT();')
587 assert compilation.output.contains('DllMain(')
588 assert compilation.output.contains('_vinit_caller();')
589 assert compilation.output.contains('_vcleanup_caller();')
590 assert !compilation.output.contains('switch (fdwReason)')
591 assert !compilation.output.contains('case DLL_PROCESS_ATTACH')
592}
593
594fn test_array_sort_with_compare_uses_stable_sort_adapters() {
595 os.chdir(vroot) or {}
596 test_source := os.join_path(os.vtmp_dir(), 'coutput_array_sort_with_compare_stable_sort.vv')
597 source_lines := [
598 'module main',
599 '',
600 'struct Foo {',
601 '\tx int',
602 '}',
603 '',
604 'fn by_x(a &Foo, b &Foo) int {',
605 '\treturn a.x - b.x',
606 '}',
607 '',
608 'fn main() {',
609 '\tmut xs := [Foo{ x: 2 }, Foo{ x: 1 }]',
610 '\txs.sort_with_compare(by_x)',
611 '\tmut ys := [Foo{ x: 2 }, Foo{ x: 1 }]!',
612 '\tys.sort_with_compare(by_x)',
613 '\tmut zs := [Foo{ x: 2 }, Foo{ x: 1 }]',
614 '\tzs.sort(a.x < b.x)',
615 '}',
616 ]
617 os.write_file(test_source, source_lines.join('\n') + '\n')!
618 defer {
619 os.rm(test_source) or {}
620 }
621 cmd := '${os.quoted_path(vexe)} -o - ${os.quoted_path(test_source)}'
622 compilation := os.execute(cmd)
623 ensure_compilation_succeeded(compilation, cmd)
624 mut normalized := compilation.output.replace('\t', ' ').replace('\n', ' ')
625 for normalized.contains(' ') {
626 normalized = normalized.replace(' ', ' ')
627 }
628 assert normalized.contains('int main__by_x_qsort_adapter(const void* a, const void* b) { return main__by_x((main__Foo*)a, (main__Foo*)b); }')
629 assert normalized.contains('if (xs.len > 0) { v_stable_sort(xs.data, xs.len, xs.element_size, main__by_x_qsort_adapter); }')
630 assert normalized.contains('v_stable_sort(&ys, 2, sizeof(main__Foo), main__by_x_qsort_adapter);')
631 assert normalized.contains('_qsort_adapter(const void* a, const void* b) { return compare_')
632 assert normalized.contains('v_stable_sort(zs.data, zs.len, zs.element_size, compare_')
633 assert normalized.contains('_qsort_adapter);')
634}
635
636fn test_array_sort_expression_key_avoids_sanitized_name_collisions() {
637 os.chdir(vroot) or {}
638 test_dir := os.join_path(os.vtmp_dir(), 'coutput_array_sort_expr_collision_${os.getpid()}')
639 os.mkdir_all(test_dir)!
640 defer {
641 os.rmdir_all(test_dir) or {}
642 }
643 os.write_file(os.join_path(test_dir, 'main.v'),
644 ['module main', '', 'struct Inner {', '\tbar int', '}', '', 'struct Item {', '\tfoo Inner', '\tfoo_bar int', '\tlabel string', '}', '', 'fn main() {', '\tprintln(sort_by_nested())', '\tprintln(sort_by_flat())', '}'].join('\n') +
645 '\n')!
646 os.write_file(os.join_path(test_dir, 'nested.v'),
647 ['module main', '', 'fn sort_by_nested() string {', "\tmut items := [Item{ foo: Inner{ bar: 2 }, foo_bar: 1, label: 'nested-wrong' }, Item{ foo: Inner{ bar: 1 }, foo_bar: 2, label: 'nested-ok' }]", '\titems.sort(a.foo.bar < b.foo.bar)', '\treturn items[0].label', '}'].join('\n') +
648 '\n')!
649 os.write_file(os.join_path(test_dir, 'flat.v'),
650 ['module main', '', 'fn sort_by_flat() string {', "\tmut items := [Item{ foo: Inner{ bar: 1 }, foo_bar: 2, label: 'flat-wrong' }, Item{ foo: Inner{ bar: 2 }, foo_bar: 1, label: 'flat-ok' }]", '\titems.sort(a.foo_bar < b.foo_bar)', '\treturn items[0].label', '}'].join('\n') +
651 '\n')!
652 pexe := os.join_path(test_dir, 'sort_expr_collision')
653 cmd := '${os.quoted_path(vexe)} -o ${os.quoted_path(pexe)} ${os.quoted_path(test_dir)}'
654 compilation := os.execute(cmd)
655 ensure_compilation_succeeded(compilation, cmd)
656 res := os.execute(os.quoted_path(pexe))
657 assert res.exit_code == 0
658 assert res.output.trim_space().replace('\r\n', '\n') == 'nested-ok\nflat-ok'
659}
660
661// generated_c_symbol_with_prefix extracts one generated C symbol from compiler output.
662fn generated_c_symbol_with_prefix(output string, prefix string) string {
663 idx := output.index(prefix) or { return '' }
664 rest := output[idx..]
665 mut end := 0
666 for end < rest.len && (rest[end].is_letter() || rest[end].is_digit() || rest[end] == `_`) {
667 end++
668 }
669 return rest[..end]
670}
671
672fn test_auxiliary_c_symbols_use_stable_type_hashes() {
673 $if windows {
674 $if tinyc {
675 eprintln('> skipping ${@FN} on windows-tcc, since deep GC scope pins (and thus their keepalive helpers) are intentionally not emitted there: the bundled Windows tcc libgc archive lacks GC_remove_roots()')
676 return
677 }
678 }
679 os.chdir(vroot) or {}
680 test_dir := os.join_path(os.vtmp_dir(), 'coutput_stable_type_symbol_hash_${os.getpid()}')
681 dir_a := os.join_path(test_dir, 'a')
682 dir_b := os.join_path(test_dir, 'b')
683 os.mkdir_all(dir_a)!
684 os.mkdir_all(dir_b)!
685 defer {
686 os.rmdir_all(test_dir) or {}
687 }
688 source_lines := [
689 'module main',
690 '',
691 'struct Item {',
692 '\tname string',
693 '\trank int',
694 '}',
695 '',
696 'fn main() {',
697 "\tmut items := [Item{'b', 2}, Item{'a', 1}, Item{'c', 3}]",
698 '\titems.sort(a.rank < b.rank)',
699 '\tmut nested := [items]',
700 '\tprintln("\${nested[0][0].name}")',
701 '}',
702 ]
703 source := source_lines.join('\n') + '\n'
704 path_a := os.join_path(dir_a, 'main.v')
705 path_b := os.join_path(dir_b, 'main.v')
706 os.write_file(path_a, source)!
707 os.write_file(path_b, source)!
708 cmd_a := '${os.quoted_path(vexe)} -prod -gc boehm_full_opt -o - ${os.quoted_path(path_a)}'
709 cmd_b := '${os.quoted_path(vexe)} -prod -gc boehm_full_opt -o - ${os.quoted_path(path_b)}'
710 compilation_a := os.execute(cmd_a)
711 compilation_b := os.execute(cmd_b)
712 ensure_compilation_succeeded(compilation_a, cmd_a)
713 ensure_compilation_succeeded(compilation_b, cmd_b)
714 compare_a := generated_c_symbol_with_prefix(compilation_a.output, 'compare_')
715 compare_b := generated_c_symbol_with_prefix(compilation_b.output, 'compare_')
716 keepalive_a := generated_c_symbol_with_prefix(compilation_a.output,
717 '__v_boehm_collect_keepalive_')
718 keepalive_b := generated_c_symbol_with_prefix(compilation_b.output,
719 '__v_boehm_collect_keepalive_')
720 assert compare_a != ''
721 assert keepalive_a != ''
722 assert compare_a == compare_b
723 assert keepalive_a == keepalive_b
724}
725
726fn test_veb_implicit_ctx_alias_uses_user_context_name() {
727 os.chdir(vroot) or {}
728 test_source := os.join_path(os.vtmp_dir(), 'coutput_veb_implicit_ctx_alias.vv')
729 os.write_file(test_source,
730 ['module main', '', 'import veb', '', 'struct App {}', '', 'struct Context {', '\tveb.Context', '}', '', 'fn (app App) nested(mut ctx Context) veb.Result {', "\treturn ctx.text('nested')", '}', '', 'fn (app App) log(_ Context) {', "\tprintln('hi')", '}', '', 'fn (app App) index(mut c Context) veb.Result {', '\tapp.log(c)', '\treturn app.nested()', '}', '', 'fn main() {', '\tmut app := App{}', '\tmut ctx := Context{}', '\t_ = app.index(mut ctx)', '}'].join('\n') +
731 '\n')!
732 defer {
733 os.rm(test_source) or {}
734 }
735 cmd := '${os.quoted_path(vexe)} -gc boehm_full_opt -o - ${os.quoted_path(test_source)}'
736 compilation := os.execute(cmd)
737 ensure_compilation_succeeded(compilation, cmd)
738 mut normalized := compilation.output.replace('\t', ' ').replace('\n', ' ')
739 for normalized.contains(' ') {
740 normalized = normalized.replace(' ', ' ')
741 }
742 assert normalized.contains('veb__Result main__App_index(main__App app, main__Context* c) { main__App_log(app, *c); GC_reachable_here(&c); return main__App_nested(app, c); }')
743}
744
745fn test_veb_implicit_ctx_alias_on_context_receiver_tmpl_not_found() {
746 if github_job.contains('musl') {
747 eprintln('> skipping ${@FN} on ${github_job}, since `-gc boehm_full_opt` links `__pthread_unregister_cancel`, which musl does not provide')
748 return
749 }
750 os.chdir(vroot) or {}
751 test_dir := os.join_path(os.vtmp_dir(), 'coutput_veb_context_receiver_tmpl_not_found')
752 os.rmdir_all(test_dir) or {}
753 os.mkdir_all(os.join_path(test_dir, 'web'))!
754 test_source := os.join_path(test_dir, 'main.v')
755 os.write_file(os.join_path(test_dir, 'web', 'notfound.html'), '<h1>@ctx.req.url</h1>\n')!
756 os.write_file(test_source,
757 ['module main', '', 'import veb', '', 'pub struct Context {', '\tveb.Context', '}', '', 'pub struct App {}', '', 'pub fn (mut c Context) not_found() veb.Result {', '\tc.res.set_status(.not_found)', "\treturn c.html(\$tmpl('web/notfound.html'))", '}', '', 'fn main() {', '\tmut app := App{}', '\tveb.run[App, Context](mut app, 8080)', '}'].join('\n') +
758 '\n')!
759 defer {
760 os.rmdir_all(test_dir) or {}
761 }
762 test_exe := os.join_path(test_dir, 'app')
763 compile_cmd := '${os.quoted_path(vexe)} -gc boehm_full_opt -o ${os.quoted_path(test_exe)} ${os.quoted_path(test_source)}'
764 ensure_compilation_succeeded(os.execute(compile_cmd), compile_cmd)
765 c_cmd := '${os.quoted_path(vexe)} -gc boehm_full_opt -o - ${os.quoted_path(test_source)}'
766 compilation := os.execute(c_cmd)
767 ensure_compilation_succeeded(compilation, c_cmd)
768 not_found_start := 'veb__Result main__Context_not_found(main__Context* c) {'
769 assert compilation.output.contains(not_found_start)
770 not_found_body :=
771 compilation.output.all_after(not_found_start).all_before('VV_LOC void main__main')
772 assert !not_found_body.contains('GC_reachable_here(&ctx);')
773 assert not_found_body.contains('GC_reachable_here(&c);')
774 assert not_found_body.contains('return veb__Context_html(&c->Context, _tmpl_res_')
775}
776
777fn test_veb_template_scope_gc_pin_does_not_escape_loop_var() {
778 if github_job.contains('musl') {
779 eprintln('> skipping ${@FN} on ${github_job}, since `-gc boehm_full_opt` links `__pthread_unregister_cancel`, which musl does not provide')
780 return
781 }
782 os.chdir(vroot) or {}
783 test_dir := os.join_path(os.vtmp_dir(), 'coutput_veb_template_scope_gc_pin')
784 os.rmdir_all(test_dir) or {}
785 os.mkdir_all(os.join_path(test_dir, 'templates'))!
786 template_lines := [
787 '<div class="tree-path">',
788 ' @if is_top_directory',
789 ' @for i, p in ctx.parts',
790 ' <a href="/@repo.user_name/@{ctx.make_path(branch_name, i)}">@p</a>',
791 ' @end',
792 ' @end',
793 ' @if is_repo_watcher',
794 ' <span>@watcher_count</span>',
795 ' @end',
796 '</div>',
797 ]
798 os.write_file(os.join_path(test_dir, 'templates', 'tree.html'),
799 template_lines.join('\n') + '\n')!
800 test_source := os.join_path(test_dir, 'main.v')
801 source_lines := [
802 'module main',
803 '',
804 'import veb',
805 '',
806 'pub struct Context {',
807 '\tveb.Context',
808 'pub mut:',
809 '\tparts []string',
810 '}',
811 '',
812 'pub struct App {}',
813 '',
814 'pub struct Repo {',
815 '\tuser_name string',
816 '}',
817 '',
818 'pub fn (ctx &Context) make_path(branch_name string, i int) string {',
819 '\treturn branch_name + i.str()',
820 '}',
821 '',
822 'pub fn (mut app App) index(mut ctx Context) veb.Result {',
823 "\trepo := Repo{ user_name: 'gitly' }",
824 "\tbranch_name := 'master'",
825 '\tis_top_directory := true',
826 '\tis_repo_watcher := false',
827 '\twatcher_count := 0',
828 "\treturn \$veb.html('templates/tree.html')",
829 '}',
830 '',
831 'fn main() {',
832 '\tmut app := App{}',
833 "\tmut ctx := Context{ parts: ['src'] }",
834 '\t_ = app.index(mut ctx)',
835 '}',
836 ]
837 os.write_file(test_source, source_lines.join('\n') + '\n')!
838 defer {
839 os.rmdir_all(test_dir) or {}
840 }
841 test_exe := os.join_path(test_dir, 'app')
842 compile_cmd := '${os.quoted_path(vexe)} -gc boehm_full_opt -o ${os.quoted_path(test_exe)} ${os.quoted_path(test_source)}'
843 ensure_compilation_succeeded(os.execute(compile_cmd), compile_cmd)
844 c_cmd := '${os.quoted_path(vexe)} -gc boehm_full_opt -o - ${os.quoted_path(test_source)}'
845 compilation := os.execute(c_cmd)
846 ensure_compilation_succeeded(compilation, c_cmd)
847 index_start := 'veb__Result main__App_index(main__App* app, main__Context* ctx) {'
848 assert compilation.output.contains(index_start)
849 index_body := compilation.output.all_after(index_start).all_before('VV_LOC void main__main')
850 assert index_body.contains('string p =')
851 assert !index_body.contains('GC_reachable_here(&p);')
852 assert index_body.contains('veb__Context_html(&ctx->Context, _tmpl_res_')
853}
854
855fn does_line_match_one_of_generated_lines(line string, generated_c_lines []string) bool {
856 for cline in generated_c_lines {
857 if line == cline {
858 return true
859 }
860 if cline.contains(line) {
861 return true
862 }
863 }
864 return false
865}
866
867fn normalize_panic_message(message string, vroot string) string {
868 mut msg := message.all_before('=========================================')
869 // change windows to nix path
870 s := vroot.replace(os.path_separator, '/')
871 msg = msg.replace(s + '/', '')
872 msg = msg.trim_space()
873 return msg
874}
875
876fn vroot_relative(opath string) string {
877 nvroot := vroot.replace(os.path_separator, '/') + '/'
878 npath := opath.replace(os.path_separator, '/')
879 return npath.replace(nvroot, '')
880}
881
882fn ensure_compilation_succeeded(compilation os.Result, cmd string) {
883 if compilation.exit_code < 0 {
884 eprintln('> cmd exit_code < 0, cmd: ${cmd}')
885 panic(compilation.output)
886 }
887 if compilation.exit_code != 0 {
888 eprintln('> cmd exit_code != 0, cmd: ${cmd}')
889 panic('compilation failed: ${compilation.output}')
890 }
891}
892
893fn target2paths(target_path string, postfix string) (string, string, string, string) {
894 basename := os.file_name(target_path).replace(postfix, '')
895 target_dir := os.dir(target_path)
896 path := os.join_path(target_dir, '${basename}.vv')
897 relpath := vroot_relative(path)
898 target_relpath := vroot_relative(target_path)
899 return basename, path, relpath, target_relpath
900}
901
902struct FileOptions {
903mut:
904 vflags string
905}
906
907pub fn get_file_options(file string) FileOptions {
908 mut res := FileOptions{}
909 lines := os.read_lines(file) or { [] }
910 for line in lines {
911 if line.starts_with('// vtest vflags:') {
912 res.vflags = line.all_after(':').trim_space()
913 }
914 }
915 return res
916}
917
918const github_job = os.getenv('GITHUB_JOB')
919
920fn should_skip(relpath string) bool {
921 if github_job.contains('musl') && relpath.ends_with('autofree_sql_or_block.vv') {
922 eprintln('> skipping ${relpath} on ${github_job}, since it uses db.sqlite, and its headers are not available to the C compiler in that environment')
923 return true
924 }
925 if github_job.contains('musl') && (relpath.ends_with('print_boehm_leak.vv')
926 || relpath.ends_with('scope_cleanup_boehm_leak.vv')
927 || relpath.ends_with('gc_debugger_linux.vv')
928 || relpath.ends_with('heap_struct_init_order.vv')) {
929 eprintln('> skipping ${relpath} on ${github_job}, since gc related tests are not compatible with `-gc none`')
930 return true
931 }
932 if github_job.contains('musl')
933 && relpath.ends_with('nested_aggregate_boehm_prod_keep_alive_nix.vv') {
934 eprintln('> skipping ${relpath} on ${github_job}, since `-prod -gc boehm_full_opt` pulls in `getcontext`, which musl does not provide')
935 return true
936 }
937 if user_os == 'windows' {
938 if relpath.contains('_nix.vv') {
939 eprintln('> skipping ${relpath} on windows')
940 return true
941 }
942 $if tinyc {
943 if relpath.ends_with('boehm_keepalive_c_union_tag.vv') {
944 eprintln('> skipping ${relpath} on windows-tcc, since deep GC scope pins (and thus their keepalive helpers) are intentionally not emitted there: the bundled Windows tcc libgc archive lacks GC_remove_roots()')
945 return true
946 }
947 }
948 $if !msvc {
949 if relpath.contains('_msvc_windows.vv') {
950 eprintln('> skipping ${relpath} on !msvc')
951 return true
952 }
953 }
954 $if !gcc {
955 if relpath.contains('_gcc_windows.vv') {
956 eprintln('> skipping ${relpath} on !gcc')
957 return true
958 }
959 }
960 $if msvc {
961 if relpath.contains('_not_msvc_windows.vv') {
962 eprintln('> skipping ${relpath} on msvc')
963 return true
964 }
965 if relpath.ends_with('cross_printfn_v_malloc.vv') {
966 eprintln('> skipping ${relpath} on msvc, since -cross -printfn does not emit a runnable executable')
967 return true
968 }
969 if relpath.contains('asm_') {
970 eprintln('> skipping ${relpath} on msvc, since it uses gcc-style inline asm')
971 return true
972 }
973 }
974 } else {
975 if relpath.contains('_windows.vv') {
976 eprintln('> skipping ${relpath} on !windows')
977 return true
978 }
979 }
980 if relpath.contains('freestanding_') {
981 $if !amd64 {
982 // https://github.com/vlang/v/issues/23397
983 eprintln('> skipping ${relpath} on != amd64')
984 return true
985 }
986 if user_os != 'linux' {
987 eprintln('> skipping ${relpath} on != linux')
988 return true
989 }
990 if gcc_path == '' {
991 eprintln('> skipping ${relpath} since it needs gcc, which is not detected')
992 return true
993 }
994 }
995 if user_os == 'macos' {
996 $if arm64 {
997 if relpath.ends_with('spawn_stack_nix.vv') {
998 eprintln('> skipping ${relpath} on macOS arm64, since i386 linking is unavailable')
999 return true
1000 }
1001 }
1002 }
1003 if gcc_path == '' {
1004 test_path := os.join_path(vroot, relpath)
1005 file_options := get_file_options(test_path)
1006 if file_options.vflags.contains('-cc gcc') {
1007 eprintln('> skipping ${relpath} since its vflags require gcc, which is not detected')
1008 return true
1009 }
1010 }
1011 if github_job in ['tcc-windows', 'msvc-windows'] {
1012 test_path := os.join_path(vroot, relpath)
1013 file_options := get_file_options(test_path)
1014 if file_options.vflags.contains('-cc clang') {
1015 // The Windows runner's clang toolchain produces V binaries that
1016 // crash at startup on this CI; the test is still exercised by
1017 // the macOS/Linux jobs.
1018 eprintln('> skipping ${relpath} on ${github_job}, since `-cc clang` produces unstable binaries on this runner')
1019 return true
1020 }
1021 }
1022 return false
1023}
1024
1025@[if !silent ?]
1026fn vprintln(msg string) {
1027 println(msg)
1028}
1029