Skip to content

Commit 866baf4

Browse files
authored
Add GenerateAll command to bulk generate tests (#368)
1 parent c61bb28 commit 866baf4

1 file changed

Lines changed: 204 additions & 27 deletions

File tree

lib/generate.vim

Lines changed: 204 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"
22
" This Vim script fetches the canonical test data for an
33
" exercise from GitHub and converts it to a Vader file.
4+
" It also provides a bulk command to convert the canonical
5+
" test data for all implemented practice exercises.
46
"
57
" :source %
68
" :Generate word-count
9+
" :GenerateAll
710
"
811

912
if get(g:, 'loaded_netrwPlugin') != 0
@@ -14,30 +17,110 @@ elseif !exists('*json_decode')
1417
finish
1518
endif
1619

20+
" Capture script location at load time for GenerateAll
21+
let s:script_dir = expand('<sfile>:p:h')
22+
let s:root_dir = fnamemodify(s:script_dir, ':h')
23+
1724
function! s:data_url(slug) abort
1825
return printf('https://raw.githubusercontent.com/exercism/problem-specifications/master/exercises/%s/canonical-data.json', a:slug)
1926
endfunction
2027

21-
function! s:generate_header(data)
22-
call append(0, [
23-
\ '"',
24-
\ '" Version: '. a:data.version,
25-
\ '"',
26-
\ ])
28+
function! s:get_practice_exercises() abort
29+
let slugs = glob(s:get_practice_dir() . '*', 0, 1)
30+
return sort(map(slugs, 'fnamemodify(v:val, ":t")'))
31+
endfunction
32+
33+
function! s:get_exercise_dir(slug) abort
34+
return s:get_practice_dir() . a:slug
35+
endfunction
36+
37+
function! s:get_practice_dir() abort
38+
return s:root_dir . '/exercises/practice/'
39+
endfunction
40+
41+
function! s:get_test_path(slug) abort
42+
return s:get_exercise_dir(a:slug) . '/' . s:exercise_to_vader(a:slug)
43+
endfunction
44+
45+
function! s:exercise_to_vader(slug) abort
46+
return substitute(a:slug, '-', '_', 'g') . '.vader'
47+
endfunction
48+
49+
function! s:get_test_toml(slug) abort
50+
return s:get_exercise_dir(a:slug) . '/.meta/tests.toml'
51+
endfunction
52+
53+
function! s:parse_tests_toml(toml_path) abort
54+
let config = {}
55+
let current_uuid = ''
56+
for line in readfile(a:toml_path)
57+
let uuid_match = matchstr(line, '^\[\zs.*\ze\]$')
58+
if !empty(uuid_match)
59+
let current_uuid = uuid_match
60+
let config[current_uuid] = {}
61+
continue
62+
endif
63+
64+
if !empty(current_uuid)
65+
let kv_match = matchlist(line, '^\(\w\+\)\s*=\s*\(.*\)$')
66+
if !empty(kv_match)
67+
let k = kv_match[1]
68+
let v = kv_match[2]
69+
let v = trim(v)
70+
71+
if v ==# 'false'
72+
let config[current_uuid][k] = v:false
73+
elseif v ==# 'true'
74+
let config[current_uuid][k] = v:true
75+
else
76+
let config[current_uuid][k] = substitute(v, '^"\|"$', '', 'g')
77+
endif
78+
endif
79+
endif
80+
endfor
81+
82+
return config
83+
endfunction
84+
85+
function! s:get_excluded_uuids(test_config) abort
86+
let excluded = []
87+
for [uuid, props] in items(a:test_config)
88+
if has_key(props, 'include') && props.include ==# v:false
89+
call add(excluded, uuid)
90+
endif
91+
if has_key(props, 'reimplements')
92+
call add(excluded, props.reimplements)
93+
endif
94+
endfor
95+
return excluded
96+
endfunction
97+
98+
function! s:filter_test_cases(cases, excluded_uuids) abort
99+
let filtered = []
100+
for test in a:cases
101+
if has_key(test, 'uuid') && index(a:excluded_uuids, test.uuid) != -1
102+
continue
103+
endif
104+
let new_test = copy(test)
105+
if has_key(test, 'cases')
106+
let new_test.cases = s:filter_test_cases(test.cases, a:excluded_uuids)
107+
if empty(new_test.cases)
108+
continue
109+
endif
110+
endif
111+
call add(filtered, new_test)
112+
endfor
113+
return filtered
27114
endfunction
28115

29116
function! s:generate_variable(name, value)
30-
let value = a:value
31-
if type(a:value) == type('')
32-
let value = '"'. value .'"'
33-
endif
34-
call append(line('$'), printf(' let g:%s = %s', a:name, value))
117+
call append(line('$'), printf(' let g:%s = %s', a:name, string(a:value)))
35118
endfunction
36119

37120
function! s:generate_assert(test, arguments) abort
38121
let funcname = toupper(a:test.property[0]) . a:test.property[1:]
39122

40-
if type(a:test.expected) == type({}) && has_key(a:test.expected, 'error')
123+
if type(a:test.expected) ==# type({}) && has_key(a:test.expected, 'error')
41124
call s:generate_variable('expected', a:test.expected.error)
42125
call append(line('$'),
43126
\ printf(' AssertThrows call %s(%s)', funcname, join(a:arguments, ', ')))
@@ -51,12 +134,16 @@ function! s:generate_assert(test, arguments) abort
51134
call append(line('$'), '')
52135
endfunction
53136

54-
function! s:generate_tests(tests) abort
137+
function! s:generate_tests(tests, ...) abort
138+
let is_top_level = a:0 ==# 0 ? 1 : 0
55139
for test in a:tests
56140
if has_key(test, 'cases')
57-
call s:generate_tests(test.cases)
141+
call s:generate_tests(test.cases, 0)
58142
else
59143
let arguments = []
144+
if line('$') > 1 && getline(line('$')) !=# ''
145+
call append(line('$'), '')
146+
endif
60147
call append(line('$'), printf('Execute (%s):', test.description))
61148
for [arg, val] in sort(items(test.input))
62149
call s:generate_variable(arg, val)
@@ -66,50 +153,140 @@ function! s:generate_tests(tests) abort
66153
endif
67154
endfor
68155

69-
if empty(getline(line('$')))
156+
if is_top_level && empty(getline(line('$')))
70157
silent $delete _
71158
endif
72159
endfunction
73160

74161
function! s:replace_types() abort
75-
silent %substitute/v:true/1/eg
76-
silent %substitute/v:false/0/eg
77-
silent %substitute/v:null/''/eg
162+
silent %substitute/['"]v:true['"]/1/eg
163+
silent %substitute/['"]v:false['"]/0/eg
164+
silent %substitute/['"]v:null['"]/v:null/eg
78165
endfunction
79166

80-
function! s:generate(slug) abort
167+
function! s:generate(slug, ...) abort
168+
let output_path = a:0 > 0 ? a:1 : ''
169+
81170
execute 'silent edit' s:data_url(a:slug)
82171
if getline(1) ==# '404: Not Found'
83172
silent bwipeout!
173+
if !empty(output_path)
174+
throw '404: Not Found'
175+
endif
84176
redraw!
85-
echomsg '404: Not Found'
177+
echohl WarningMsg
178+
echomsg 'Skipped: No canonical data available for ' . a:slug
179+
echohl None
86180
return
87-
elseif line2byte('$') == -1
181+
elseif line2byte('$') ==# -1
88182
silent bwipeout!
89-
echomsg 'Got empty buffer. Have you disabled the netrw plugin?'
183+
if !empty(output_path)
184+
throw 'Got empty buffer. Have you disabled the netrw plugin?'
185+
endif
186+
echohl WarningMsg
187+
echomsg 'Skipped: Got empty buffer for ' . a:slug . '. Have you disabled the netrw plugin?'
188+
echohl None
90189
return
91190
endif
92-
%yank x
191+
silent %yank x
192+
193+
let json_text = substitute(@x, '\%x00', '', 'g')
194+
93195
try
94-
let data = json_decode(substitute(@x, '\\', '\\\\', 'g'))
196+
let data = json_decode(substitute(json_text, '\\\\', '\\\\\\\\', 'g'))
95197
catch
198+
if !empty(output_path)
199+
silent bwipeout!
200+
throw 'JSON decoding failed: ' . v:exception
201+
endif
96202
redraw
97203
echohl ErrorMsg
98204
echomsg 'JSON decoding failed.'
99205
echomsg 'Trying again without backslash escaping.'
100206
echomsg 'Check escaping in the generated tests!'
101207
echohl None
102208
call input('[press any key]')
103-
let data = json_decode(@x)
209+
let data = json_decode(json_text)
104210
endtry
105211
bwipeout!
212+
213+
let tests_toml = s:get_test_toml(a:slug)
214+
if filereadable(tests_toml)
215+
let test_config = s:parse_tests_toml(tests_toml)
216+
let excluded_uuids = s:get_excluded_uuids(test_config)
217+
if !empty(excluded_uuids)
218+
let data.cases = s:filter_test_cases(data.cases, excluded_uuids)
219+
endif
220+
endif
221+
106222
enew!
107223
setfiletype vader
108-
call s:generate_header(data)
109224
call s:generate_tests(data.cases)
110225
call s:replace_types()
111-
set nomodified
226+
227+
if getline(1) ==# ''
228+
1delete _
229+
endif
230+
231+
if !empty(output_path)
232+
execute 'silent write! ' . fnameescape(output_path)
233+
silent bwipeout!
234+
else
235+
set nomodified
236+
redraw!
237+
endif
238+
endfunction
239+
240+
function! s:generate_all() abort
241+
let exercises = s:get_practice_exercises()
242+
let total = len(exercises)
243+
let generated = 0
244+
let skipped = []
245+
let failed = []
246+
247+
redraw!
248+
echo 'Regenerating tests for ' . total . ' exercises...'
249+
250+
for [idx, slug] in items(exercises)
251+
let output_path = s:get_test_path(slug)
252+
redraw!
253+
echo printf('[%d/%d] Generating %s...', idx + 1, total, slug)
254+
255+
try
256+
call s:generate(slug, output_path)
257+
let generated += 1
258+
catch
259+
if v:exception =~# '404' || v:exception =~# 'empty buffer'
260+
call add(skipped, slug)
261+
else
262+
call add(failed, {'slug': slug, 'error': v:exception})
263+
endif
264+
endtry
265+
endfor
266+
112267
redraw!
268+
echohl MoreMsg
269+
echomsg printf('Successfully generated %d/%d tests', generated, total)
270+
echohl None
271+
272+
if !empty(skipped)
273+
echohl WarningMsg
274+
echomsg printf('Skipped %d exercises (no canonical data available):', len(skipped))
275+
echohl None
276+
for slug in skipped
277+
echomsg ' - ' . slug
278+
endfor
279+
endif
280+
281+
if !empty(failed)
282+
echohl ErrorMsg
283+
echomsg printf('Failed to generate %d tests:', len(failed))
284+
echohl None
285+
for item in failed
286+
echomsg ' - ' . item.slug . ': ' . item.error
287+
endfor
288+
endif
113289
endfunction
114290

115291
command! -nargs=1 Generate call s:generate(<f-args>)
292+
command! GenerateAll call s:generate_all()

0 commit comments

Comments
 (0)