| 1 | module main |
| 2 | |
| 3 | import os |
| 4 | import os.cmdline |
| 5 | import testing |
| 6 | import v.pref |
| 7 | |
| 8 | struct Context { |
| 9 | mut: |
| 10 | verbose bool |
| 11 | fail_fast bool |
| 12 | run_only []string |
| 13 | } |
| 14 | |
| 15 | fn main() { |
| 16 | args := os.args.clone() |
| 17 | if os.args.last() == 'test' { |
| 18 | show_usage() |
| 19 | return |
| 20 | } |
| 21 | args_to_executable := args[1..] |
| 22 | mut args_before := cmdline.options_before(args_to_executable, ['test']) |
| 23 | mut args_after := cmdline.options_after(args_to_executable, ['test']) |
| 24 | mut ctx := Context{} |
| 25 | ctx.fail_fast = extract_flag_bool('-fail-fast', mut args_after, testing.fail_fast) |
| 26 | ctx.verbose = extract_flag_bool('-v', mut args_after, false) |
| 27 | ctx.run_only = extract_flag_string_array('-run-only', mut args_after, testing.test_only_fn) |
| 28 | os.setenv('VTEST_ONLY_FN', ctx.run_only.join(','), true) |
| 29 | if args_after == ['v'] { |
| 30 | eprintln('`v test v` has been deprecated.') |
| 31 | eprintln('Use `v test-all` instead.') |
| 32 | exit(1) |
| 33 | } |
| 34 | backend_pos := args_before.index('-b') |
| 35 | backend := if backend_pos == -1 { '.c' } else { args_before[backend_pos + 1] } |
| 36 | |
| 37 | mut ts := testing.new_test_session(args_before.join(' '), true) |
| 38 | ts.exec_mode = .compile_and_run |
| 39 | ts.fail_fast = ctx.fail_fast |
| 40 | for targ in args_after { |
| 41 | if os.is_dir(targ) { |
| 42 | // Fetch all tests from the directory |
| 43 | files, skip_files := ctx.should_test_dir(targ.trim_right(os.path_separator), backend) |
| 44 | ts.files << files |
| 45 | ts.skip_files << skip_files |
| 46 | continue |
| 47 | } else if os.exists(targ) { |
| 48 | match ctx.should_test(targ, backend) { |
| 49 | .test { |
| 50 | ts.files << targ |
| 51 | continue |
| 52 | } |
| 53 | .skip { |
| 54 | if ctx.run_only.len > 0 { |
| 55 | continue |
| 56 | } |
| 57 | ts.files << targ |
| 58 | ts.skip_files << os.abs_path(targ) |
| 59 | continue |
| 60 | } |
| 61 | .ignore {} |
| 62 | } |
| 63 | } else { |
| 64 | eprintln('\nUnrecognized test file `${targ}`.\n `v test` can only be used with folders and/or _test.v files.\n') |
| 65 | show_usage() |
| 66 | exit(1) |
| 67 | } |
| 68 | } |
| 69 | ts.session_start('Testing...') |
| 70 | ts.test() |
| 71 | ts.session_stop('all V _test.v files') |
| 72 | if ts.failed_cmds.len > 0 { |
| 73 | exit(1) |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | fn show_usage() { |
| 78 | println('Usage:') |
| 79 | println(' A)') |
| 80 | println(' v test folder/ : run all v tests in the given folder.') |
| 81 | println(' v -stats test folder/ : the same, but print more stats.') |
| 82 | println(' B)') |
| 83 | println(' v test file_test.v : run test functions in a given test file.') |
| 84 | println(' v -stats test file_test.v : as above, but with more stats.') |
| 85 | println(' Note: you can also give many and mixed folder/ file_test.v arguments after `v test` .') |
| 86 | println('') |
| 87 | } |
| 88 | |
| 89 | pub fn (mut ctx Context) should_test_dir(path string, backend string) ([]string, []string) { // return is (files, skip_files) |
| 90 | mut files := os.ls(path) or { return []string{}, []string{} } |
| 91 | mut local_path_separator := os.path_separator |
| 92 | if path.ends_with(os.path_separator) { |
| 93 | local_path_separator = '' |
| 94 | } |
| 95 | mut res_files := []string{} |
| 96 | mut skip_files := []string{} |
| 97 | for file in files { |
| 98 | p := path + local_path_separator + file |
| 99 | if os.is_dir(p) && !os.is_link(p) { |
| 100 | if file == 'testdata' { |
| 101 | continue |
| 102 | } |
| 103 | ret_files, ret_skip_files := ctx.should_test_dir(p, backend) |
| 104 | res_files << ret_files |
| 105 | skip_files << ret_skip_files |
| 106 | } else if os.exists(p) { |
| 107 | match ctx.should_test(p, backend) { |
| 108 | .test { |
| 109 | res_files << p |
| 110 | } |
| 111 | .skip { |
| 112 | if ctx.run_only.len > 0 { |
| 113 | continue |
| 114 | } |
| 115 | res_files << p |
| 116 | skip_files << os.abs_path(p) |
| 117 | } |
| 118 | .ignore {} |
| 119 | } |
| 120 | } |
| 121 | } |
| 122 | return res_files, skip_files |
| 123 | } |
| 124 | |
| 125 | enum ShouldTestStatus { |
| 126 | test // do test, print OK or FAIL, depending on if it passes |
| 127 | skip // print SKIP for the test |
| 128 | ignore // just ignore the file, so it will not be printed at all in the list of tests |
| 129 | } |
| 130 | |
| 131 | fn (mut ctx Context) should_test(path string, backend string) ShouldTestStatus { |
| 132 | if path.ends_with('_test.v') { |
| 133 | return ctx.should_test_when_it_contains_matching_fns(path, backend) |
| 134 | } |
| 135 | if path.ends_with('_test.c.v') { |
| 136 | return ctx.should_test_when_it_contains_matching_fns(path, backend) |
| 137 | } |
| 138 | if path.ends_with('_test.js.v') { |
| 139 | if testing.is_node_present { |
| 140 | return ctx.should_test_when_it_contains_matching_fns(path, backend) |
| 141 | } |
| 142 | return .skip |
| 143 | } |
| 144 | // `_test.vv2` files are v2-only integration tests. They are full V programs |
| 145 | // (with `main()`) that exercise v2-specific syntax; the test runner routes |
| 146 | // them through the v2 binary instead of v1. Honor `-run-only` so targeted |
| 147 | // runs do not pull in unrelated vv2 tests. |
| 148 | if path.ends_with('_test.vv2') { |
| 149 | return ctx.should_test_when_it_contains_matching_fns(path, backend) |
| 150 | } |
| 151 | if path.ends_with('.v') && path.count('.') == 2 { |
| 152 | if !path.all_before_last('.v').all_before_last('.').ends_with('_test') { |
| 153 | return .ignore |
| 154 | } |
| 155 | backend_arg := path.all_before_last('.v').all_after_last('.') |
| 156 | arch := pref.arch_from_string(backend_arg) or { pref.Arch._auto } |
| 157 | if arch == pref.get_host_arch() { |
| 158 | return ctx.should_test_when_it_contains_matching_fns(path, backend) |
| 159 | } else if arch == ._auto { |
| 160 | if backend_arg == 'c' { // .c.v |
| 161 | return if backend == 'c' { |
| 162 | ctx.should_test_when_it_contains_matching_fns(path, backend) |
| 163 | } else { |
| 164 | ShouldTestStatus.skip |
| 165 | } |
| 166 | } |
| 167 | if backend_arg == 'js' { |
| 168 | return if backend == 'js' { |
| 169 | ctx.should_test_when_it_contains_matching_fns(path, backend) |
| 170 | } else { |
| 171 | ShouldTestStatus.skip |
| 172 | } |
| 173 | } |
| 174 | } else { |
| 175 | return .skip |
| 176 | } |
| 177 | } |
| 178 | return .ignore |
| 179 | } |
| 180 | |
| 181 | fn (mut ctx Context) should_test_when_it_contains_matching_fns(path string, _backend string) ShouldTestStatus { |
| 182 | if ctx.run_only.len == 0 { |
| 183 | // no filters set, so just compile and test |
| 184 | return .test |
| 185 | } |
| 186 | lines := os.read_lines(path) or { return .ignore } |
| 187 | for line in lines { |
| 188 | if line.match_glob('fn test_*') || line.match_glob('pub fn test_*') { |
| 189 | tname := line.replace_each(['pub fn ', '', 'fn ', '']).all_before('(') |
| 190 | for pattern in ctx.run_only { |
| 191 | mut pat := pattern.clone() |
| 192 | if pat.contains('.') { |
| 193 | pat = pat.all_after_last('.') |
| 194 | } |
| 195 | if tname.match_glob(pat) { |
| 196 | if ctx.verbose { |
| 197 | println('> compiling path: ${path}, since test fn `${tname}` matches glob pattern `${pat}`') |
| 198 | } |
| 199 | return .test |
| 200 | } |
| 201 | } |
| 202 | } |
| 203 | } |
| 204 | return .ignore |
| 205 | } |
| 206 | |
| 207 | fn extract_flag_bool(flag_name string, mut after []string, flag_default bool) bool { |
| 208 | mut res := flag_default |
| 209 | orig_after := |
| 210 | after.clone() // workaround for after.filter() codegen bug, when `mut after []string` |
| 211 | matches_after := orig_after.filter(it != flag_name) |
| 212 | if matches_after.len < after.len { |
| 213 | after = matches_after.clone() |
| 214 | res = true |
| 215 | } |
| 216 | return res |
| 217 | } |
| 218 | |
| 219 | fn extract_flag_string_array(flag_name string, mut after []string, flag_default []string) []string { |
| 220 | mut res := flag_default.clone() |
| 221 | mut found := after.index(flag_name) |
| 222 | if found > -1 { |
| 223 | if found + 1 < after.len { |
| 224 | res = after[found + 1].split_any(',') |
| 225 | after.delete(found) |
| 226 | } |
| 227 | after.delete(found) |
| 228 | } |
| 229 | return res |
| 230 | } |
| 231 | |