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] } // this giant mess because closures are not implemented |
36 | |
37 | mut ts := testing.new_test_session(args_before.join(' '), true) |
38 | ts.fail_fast = ctx.fail_fast |
39 | for targ in args_after { |
40 | if os.is_dir(targ) { |
41 | // Fetch all tests from the directory |
42 | files, skip_files := ctx.should_test_dir(targ.trim_right(os.path_separator), |
43 | 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 << 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 << 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('mysql_orm_test.v') { |
133 | testing.find_started_process('mysqld') or { return .skip } |
134 | } |
135 | if path.ends_with('mysql_test.v') { |
136 | testing.find_started_process('mysqld') or { return .skip } |
137 | } |
138 | if path.ends_with('pg_orm_test.v') { |
139 | testing.find_started_process('postgres') or { return .skip } |
140 | } |
141 | if path.ends_with('onecontext_test.v') { |
142 | return .skip |
143 | } |
144 | $if tinyc { |
145 | if path.ends_with('naked_attr_test.amd64.v') { |
146 | return .skip |
147 | } |
148 | } |
149 | if path.ends_with('_test.v') { |
150 | return ctx.should_test_when_it_contains_matching_fns(path, backend) |
151 | } |
152 | if path.ends_with('_test.js.v') { |
153 | if testing.is_node_present { |
154 | return ctx.should_test_when_it_contains_matching_fns(path, backend) |
155 | } |
156 | return .skip |
157 | } |
158 | if path.ends_with('.v') && path.count('.') == 2 { |
159 | if !path.all_before_last('.v').all_before_last('.').ends_with('_test') { |
160 | return .ignore |
161 | } |
162 | backend_arg := path.all_before_last('.v').all_after_last('.') |
163 | arch := pref.arch_from_string(backend_arg) or { pref.Arch._auto } |
164 | if arch == pref.get_host_arch() { |
165 | return ctx.should_test_when_it_contains_matching_fns(path, backend) |
166 | } else if arch == ._auto { |
167 | if backend_arg == 'c' { // .c.v |
168 | return if backend == 'c' { |
169 | ctx.should_test_when_it_contains_matching_fns(path, backend) |
170 | } else { |
171 | ShouldTestStatus.skip |
172 | } |
173 | } |
174 | if backend_arg == 'js' { |
175 | return if backend == 'js' { |
176 | ctx.should_test_when_it_contains_matching_fns(path, backend) |
177 | } else { |
178 | ShouldTestStatus.skip |
179 | } |
180 | } |
181 | } else { |
182 | return .skip |
183 | } |
184 | } |
185 | return .ignore |
186 | } |
187 | |
188 | fn (mut ctx Context) should_test_when_it_contains_matching_fns(path string, backend string) ShouldTestStatus { |
189 | if ctx.run_only.len == 0 { |
190 | // no filters set, so just compile and test |
191 | return .test |
192 | } |
193 | lines := os.read_lines(path) or { return .ignore } |
194 | for line in lines { |
195 | if line.match_glob('fn test_*') || line.match_glob('pub fn test_*') { |
196 | tname := line.replace_each(['pub fn ', '', 'fn ', '']).all_before('(') |
197 | for pattern in ctx.run_only { |
198 | mut pat := pattern.clone() |
199 | if pat.contains('.') { |
200 | pat = pat.all_after_last('.') |
201 | } |
202 | if tname.match_glob(pat) { |
203 | if ctx.verbose { |
204 | println('> compiling path: ${path}, since test fn `${tname}` matches glob pattern `${pat}`') |
205 | } |
206 | return .test |
207 | } |
208 | } |
209 | } |
210 | } |
211 | return .ignore |
212 | } |
213 | |
214 | fn extract_flag_bool(flag_name string, mut after []string, flag_default bool) bool { |
215 | mut res := flag_default |
216 | orig_after := after.clone() // workaround for after.filter() codegen bug, when `mut after []string` |
217 | matches_after := orig_after.filter(it != flag_name) |
218 | if matches_after.len < after.len { |
219 | after = matches_after.clone() |
220 | res = true |
221 | } |
222 | return res |
223 | } |
224 | |
225 | fn extract_flag_string_array(flag_name string, mut after []string, flag_default []string) []string { |
226 | mut res := flag_default.clone() |
227 | mut found := after.index(flag_name) |
228 | if found > -1 { |
229 | if found + 1 < after.len { |
230 | res = after[found + 1].split_any(',') |
231 | after.delete(found) |
232 | } |
233 | after.delete(found) |
234 | } |
235 | return res |
236 | } |