v / vlib / toml / tests / toml_rs_test.v
321 lines · 292 sloc · 10.45 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1import os
2import toml
3import toml.ast
4import x.json2
5
6const hide_oks = os.getenv('VTEST_HIDE_OK') == '1'
7const no_jq = os.getenv('VNO_JQ') == '1'
8
9// Instructions for developers:
10// The actual tests and data can be obtained by doing:
11// `git clone -n https://github.com/toml-rs/toml.git vlib/toml/tests/testdata/toml_rs`
12// `git -C vlib/toml/tests/testdata/toml_rs reset --hard 9bd454c`
13// See also the CI toml tests
14// Kept for easier handling of future updates to the tests
15const valid_exceptions = [
16 'valid/example-v0.3.0.toml',
17 'valid/example-v0.4.0.toml',
18 'valid/datetime-truncate.toml', // Not considered valid since RFC 3339 doesn't permit > 6 ms digits ??
19]
20const invalid_exceptions = []string{}
21
22const valid_value_exceptions = [
23 // These have correct values, and should've passed, but the format of arrays is *mixed* in the JSON ??
24 'valid/datetime-truncate.toml',
25 'valid/example2.toml',
26 'valid/example-v0.4.0.toml',
27 'valid/example-v0.3.0.toml',
28]
29
30// These have correct values, and should've passed as-is, but the format of arrays changes in the JSON ??
31// We account for that here
32const use_type_2_arrays = [
33 'valid/table-array-implicit.toml',
34 'valid/table-array-many.toml',
35 'valid/table-array-one.toml',
36 'valid/table-array-nest.toml',
37 'valid/table-array-nest-no-keys.toml',
38]
39const tests_folder = os.join_path('crates', 'test-suite', 'tests')
40const jq = os.find_abs_path_of_executable('jq') or { '' }
41const compare_work_dir_root = os.join_path(os.vtmp_dir(), 'toml_toml_rs')
42// From: https://stackoverflow.com/a/38266731/1904615
43const jq_normalize = r'# Apply f to composite entities recursively using keys[], and to atoms
44def sorted_walk(f):
45 . as $in
46 | if type == "object" then
47 reduce keys[] as $key
48 ( {}; . + { ($key): ($in[$key] | sorted_walk(f)) } ) | f
49 elif type == "array" then map( sorted_walk(f) ) | f
50 else f
51 end;
52
53def normalize: sorted_walk(if type == "array" then sort else . end);
54
55normalize'
56
57fn run(args []string) !string {
58 res := os.execute(args.join(' '))
59 if res.exit_code != 0 {
60 return error('${args[0]} failed with return code ${res.exit_code}.\n${res.output}')
61 }
62 return res.output
63}
64
65// test_toml_rs_toml_rs run though 'testdata/toml_rs/crates/test-suite/tests/*' if found.
66fn test_toml_rs_toml_rs() {
67 eprintln('> running ${@LOCATION}')
68 test_root := '${@VEXEROOT}/vlib/toml/tests/testdata/toml_rs/crates/test-suite/tests'
69 if os.is_dir(test_root) {
70 valid_test_files := os.walk_ext(os.join_path(test_root, 'valid'), '.toml')
71 invalid_test_files := os.walk_ext(os.join_path(test_root, 'invalid'), '.toml')
72 println('Testing ${valid_test_files.len} valid TOML files...')
73 mut valid := 0
74 mut e := 0
75 for i, valid_test_file in valid_test_files {
76 mut relative := valid_test_file.all_after(tests_folder).trim_left(os.path_separator)
77 $if windows {
78 relative = relative.replace('/', '\\')
79 }
80
81 if relative in valid_exceptions {
82 e++
83 idx := valid_exceptions.index(relative) + 1
84 println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...')
85 continue
86 }
87 if !hide_oks {
88 println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...')
89 }
90 toml_doc := toml.parse_file(valid_test_file)!
91 valid++
92 }
93 println('${valid}/${valid_test_files.len} TOML files were parsed correctly')
94 if valid_exceptions.len > 0 {
95 println('TODO Skipped parsing of ${e} valid TOML files...')
96 }
97
98 // If the command-line tool `jq` is installed, value tests can be run as well.
99 if jq != '' && !no_jq {
100 println('Testing value output of ${valid_test_files.len} valid TOML files using "${jq}"...')
101
102 if os.exists(compare_work_dir_root) {
103 os.rmdir_all(compare_work_dir_root)!
104 }
105 os.mkdir_all(compare_work_dir_root)!
106
107 jq_normalize_path := os.join_path(compare_work_dir_root, 'normalize.jq')
108 os.write_file(jq_normalize_path, jq_normalize)!
109
110 valid = 0
111 e = 0
112 for i, valid_test_file in valid_test_files {
113 mut relative := valid_test_file.all_after(tests_folder).trim_left(os.path_separator)
114 $if windows {
115 relative = relative.replace('/', '\\')
116 }
117 if !os.exists(valid_test_file.all_before_last('.') + '.json') {
118 println('N/A [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...')
119 continue
120 }
121 // Skip the file if we know it can't be parsed or we know that the value retrieval needs work.
122 if relative in valid_exceptions {
123 e++
124 idx := valid_exceptions.index(relative) + 1
125 println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...')
126 continue
127 }
128 if relative in valid_value_exceptions {
129 e++
130 idx := valid_value_exceptions.index(relative) + 1
131 println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID VALUE EXCEPTION [${idx}/${valid_value_exceptions.len}]...')
132 continue
133 }
134
135 if !hide_oks {
136 println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...')
137 }
138 toml_doc := toml.parse_file(valid_test_file)!
139
140 v_toml_json_path := os.join_path(compare_work_dir_root,
141
142 os.file_name(valid_test_file).all_before_last('.') + '.v.json')
143 toml_rs_toml_json_path := os.join_path(compare_work_dir_root,
144
145 os.file_name(valid_test_file).all_before_last('.') + '.json')
146
147 mut array_type := 1
148 if relative in use_type_2_arrays {
149 array_type = 2
150 }
151
152 os.write_file(v_toml_json_path, to_toml_rs(toml_doc.ast.table, array_type))!
153
154 toml_rs_json := os.read_file(valid_test_file.all_before_last('.') + '.json')!
155
156 os.write_file(toml_rs_toml_json_path, toml_rs_json)!
157
158 v_normalized_json := run([jq, '-S', '-f "${jq_normalize_path}"', v_toml_json_path]) or {
159 contents := os.read_file(v_toml_json_path)!
160 panic(err.msg() + '\n${contents}')
161 }
162 toml_rs_normalized_json := run([jq, '-S', '-f "${jq_normalize_path}"',
163 toml_rs_toml_json_path]) or {
164 contents := os.read_file(v_toml_json_path)!
165 panic(err.msg() + '\n${contents}')
166 }
167
168 assert toml_rs_normalized_json == v_normalized_json
169
170 valid++
171 }
172 println('${valid}/${valid_test_files.len} TOML files were parsed correctly and value checked')
173 if valid_value_exceptions.len > 0 {
174 println('TODO Skipped value checks of ${e} valid TOML files...')
175 }
176 }
177
178 println('Testing ${invalid_test_files.len} invalid TOML files...')
179 mut invalid := 0
180 e = 0
181 for i, invalid_test_file in invalid_test_files {
182 mut relative := invalid_test_file.all_after(tests_folder).trim_left(os.path_separator)
183 $if windows {
184 relative = relative.replace('/', '\\')
185 }
186 if relative in invalid_exceptions {
187 e++
188 idx := invalid_exceptions.index(relative) + 1
189 println('SKIP [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}" INVALID EXCEPTION [${idx}/${invalid_exceptions.len}]...')
190 continue
191 }
192
193 if !hide_oks {
194 println('OK [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}"...')
195 }
196 if toml_doc := toml.parse_file(invalid_test_file) {
197 content_that_should_have_failed := os.read_file(invalid_test_file)!
198 println(' This TOML should have failed:\n${'-'.repeat(40)}\n${content_that_should_have_failed}\n${'-'.repeat(40)}')
199 assert false
200 } else {
201 if !hide_oks {
202 println(' ${err.msg()}')
203 }
204 assert true
205 }
206 invalid++
207 }
208 println('${invalid}/${invalid_test_files.len} TOML files were parsed correctly')
209 if invalid_exceptions.len > 0 {
210 println('TODO Skipped parsing of ${invalid_exceptions.len} invalid TOML files...')
211 }
212 } else {
213 println('No test data directory found in "${test_root}"')
214 assert true
215 }
216}
217
218// to_toml_rs_time
219fn to_toml_rs_time(time_str string) string {
220 if time_str.contains('.') {
221 date_and_time := time_str.all_before('.')
222 mut ms := time_str.all_after('.')
223 z := if ms.contains('Z') { 'Z' } else { '' }
224 ms = ms.replace('Z', '')
225 if ms.len > 3 {
226 ms = ms[..3]
227 }
228 return date_and_time + '.' + ms + z
229 } else {
230 return time_str
231 }
232}
233
234// to_toml_rs returns an toml_rs compatible json string converted from the `value` ast.Value.
235fn to_toml_rs(value ast.Value, array_type int) string {
236 match value {
237 ast.Quoted {
238 json_text := json2.Any(value.text).json_str()
239 return '{ "type": "string", "value": ${json_text} }'
240 }
241 ast.DateTime {
242 // Normalization for json
243 mut json_text := json2.Any(value.text).json_str().to_upper().replace(' ', 'T')
244 typ := if json_text.ends_with('Z"') || json_text.all_after('T').contains('-')
245 || json_text.all_after('T').contains('+') {
246 'datetime'
247 } else {
248 'datetime-local'
249 }
250 // NOTE test suite inconsistency.
251 // It seems it's implementation specific how time and
252 // date-time values are represented in detail. For now we follow the toml-lang format
253 // that expands to 6 digits which is also a valid RFC 3339 representation.
254 json_text = to_toml_rs_time(json_text[1..json_text.len - 1])
255 return '{ "type": "${typ}", "value": "${json_text}" }'
256 }
257 ast.Date {
258 json_text := json2.Any(value.text).json_str()
259 return '{ "type": "date", "value": ${json_text} }'
260 }
261 ast.Time {
262 mut json_text := json2.Any(value.text).json_str()
263 json_text = to_toml_rs_time(json_text[1..json_text.len - 1])
264 return '{ "type": "time", "value": "${json_text}" }'
265 }
266 ast.Bool {
267 json_text := json2.Any(value.text.bool()).json_str()
268 return '{ "type": "bool", "value": "${json_text}" }'
269 }
270 ast.Null {
271 json_text := json2.Any(value.text).json_str()
272 return '{ "type": "null", "value": ${json_text} }'
273 }
274 ast.Number {
275 text := value.text
276 if text.contains('inf') || text.contains('nan') {
277 return '{ "type": "float", "value": ${value.text} }'
278 }
279 if !text.starts_with('0x') && (text.contains('.') || text.to_lower().contains('e')) {
280 mut val := ''
281 if text.to_lower().contains('e') && !text.contains('-') {
282 val = '${value.f64():.1f}'
283 } else {
284 val = '${value.f64()}'
285 }
286 return '{ "type": "float", "value": "${val}" }'
287 }
288 return '{ "type": "integer", "value": "${value.i64()}" }'
289 }
290 map[string]ast.Value {
291 mut str := '{ '
292 for key, val in value {
293 json_key := json2.Any(key).json_str()
294 str += ' ${json_key}: ${to_toml_rs(val, array_type)},'
295 }
296 str = str.trim_right(',')
297 str += ' }'
298 return str
299 }
300 []ast.Value {
301 mut str := ''
302 if array_type == 1 {
303 str = '{ "type": "array", "value": [ '
304 } else {
305 str = '[ '
306 }
307 for val in value {
308 str += ' ${to_toml_rs(val, array_type)},'
309 }
310 str = str.trim_right(',')
311 if array_type == 1 {
312 str += ' ] }\n'
313 } else {
314 str += ' ]\n'
315 }
316 return str
317 }
318 }
319
320 return '<error>'
321}
322