/
live_test.v
202 lines (181 loc) · 7.18 KB
/
live_test.v
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import os
import time
// vtest flaky: true
// vtest retry: 4
/*
The goal of this test, is to simulate a developer, that has run a program, compiled with -live flag.
It does so by writing a new generated program containing a [live] fn pmessage() string {...} function,
(that program is in `vlib/v/live/live_test_template.vv`)
then runs the generated program at the start *in the background*,
waits some time, so that the program could run a few iterations, then modifies its source
(simulates a developer that has saved a new version of the program source),
then it waits some more, modifies it again and saves it once more.
On each modification, the running program, should detect that its source code has changed,
and recompile a shared library, which it then it should load, and thus modify its own
behavior at runtime (the pmessage function).
If everything works fine, the output of the generated program would have changed at least 1-2 times,
which then is detected by the test program (the histogram checks).
Since this test program is sensitive to coordination (or lack of) of several processes,
it tries to sidestep the coordination issue by polling the file system for the existence
of files, ORIGINAL.txt ... STOP.txt , which are appended to by the generated program.
Note: That approach of monitoring the state of the running generated program, is clearly not ideal,
but sidesteps the issue of coordinating processes through IPC or stdin/stdout in hopefully
not very flaky way.
TODO: Cleanup this when/if v has better process control/communication primitives.
*/
const vexe = os.getenv('VEXE')
const vtmp_folder = os.join_path(os.vtmp_dir(), 'live_tests')
const main_source_file = os.join_path(vtmp_folder, 'main.v')
const tmp_file = os.join_path(vtmp_folder, 'mymodule', 'generated_live_module.tmp')
const source_file = os.join_path(vtmp_folder, 'mymodule', 'mymodule.v')
const genexe_file = os.join_path(vtmp_folder, 'generated_live_program')
const output_file = os.join_path(vtmp_folder, 'generated_live_program.output.txt')
const res_original_file = os.join_path(vtmp_folder, 'ORIGINAL.txt')
const res_changed_file = os.join_path(vtmp_folder, 'CHANGED.txt')
const res_another_file = os.join_path(vtmp_folder, 'ANOTHER.txt')
const res_stop_file = os.join_path(vtmp_folder, 'STOP.txt')
const live_program_source = get_source_template()
fn get_source_template() string {
src := os.read_file(os.join_path(os.dir(@FILE), 'live_test_template.vv')) or { panic(err) }
return src.replace('#OUTPUT_FILE#', output_file)
}
fn atomic_write_source(source string) {
// Note: here wrtiting is done in 2 steps, since os.write_file can take some time,
// during which the file will be modified, but it will still be not completely written.
// The os.mv after that, guarantees that the reloader will see a complete valid V program.
os.write_file(tmp_file, source) or { panic(err) }
os.mv(tmp_file, source_file) or { panic(err) }
}
//
fn testsuite_begin() {
os.rmdir_all(vtmp_folder) or {}
os.mkdir_all(vtmp_folder) or {}
os.mkdir_all(os.join_path(vtmp_folder, 'mymodule'))!
os.write_file(os.join_path(vtmp_folder, 'v.mod'), '')!
os.write_file(os.join_path(vtmp_folder, 'main.v'), '
import mymodule
fn main() {
mymodule.mymain()
}
')!
if os.user_os() !in ['linux', 'solaris'] && os.getenv('FORCE_LIVE_TEST').len == 0 {
eprintln('Testing the runtime behaviour of -live mode,')
eprintln('is reliable only on Linux/macOS for now.')
eprintln('You can still do it by setting FORCE_LIVE_TEST=1 .')
exit(0)
}
atomic_write_source(live_program_source)
// os.system('tree $vtmp_folder') exit(1)
spawn watchdog()
}
fn watchdog() {
// This thread will automatically exit the live_test.v process, if it gets stuck.
// On the Github CI, especially on the sanitized jobs, that are super slow, this allows
// the job as a whole to continue, because the V test framework will restart live_test.v
// a few times, and then it will stop.
// Previusly, it could not do that, because if the process itself takes say a few hours,
// when the CI job gets reprioritized, the whole Github job will get cancelled, when it
// reaches its own timeout (which is 3 hours).
// Note, that usually `v vlib/v/live/live_test.v` does not take too long - it takes
// ~4 seconds, even on an i3, with tcc, ~12 seconds with clang, and ~15 seconds with gcc,
// so the *5 minutes* period, allows plenty of time for the process to finish normally.
sw := time.new_stopwatch()
for {
elapsed_time_in_seconds := sw.elapsed().seconds()
dump(elapsed_time_in_seconds)
if elapsed_time_in_seconds > 5 * 60 {
exit(3)
}
time.sleep(1 * time.second)
}
}
@[debuglivetest]
fn vprintln(s string) {
eprintln(s)
}
fn testsuite_end() {
// os.system('tree $vtmp_folder') exit(1)
vprintln('source: ${source_file}')
vprintln('output: ${output_file}')
vprintln('---------------------------------------------------------------------------')
output_lines := os.read_lines(output_file) or {
panic('could not read ${output_file}, error: ${err}')
}
mut histogram := map[string]int{}
for line in output_lines {
histogram[line] = histogram[line] + 1
}
for k, v in histogram {
eprintln('> found ${v:5d} times: ${k}')
}
vprintln('---------------------------------------------------------------------------')
assert histogram['START'] > 0
assert histogram['ORIGINAL'] > 0
assert histogram['CHANGED'] + histogram['ANOTHER'] > 0
// assert histogram['END'] > 0
os.rmdir_all(vtmp_folder) or {}
}
fn change_source(new string) {
time.sleep(100 * time.millisecond)
vprintln('> change ORIGINAL to: ${new}')
atomic_write_source(live_program_source.replace('ORIGINAL', new))
wait_for_file(new)
}
fn wait_for_file(new string) {
time.sleep(100 * time.millisecond)
expected_file := os.join_path(vtmp_folder, new + '.txt')
eprintln('waiting for ${expected_file} ...')
// os.system('tree $vtmp_folder')
max_wait_cycles := os.getenv_opt('WAIT_CYCLES') or { '1' }.int()
for i := 0; i <= max_wait_cycles; i++ {
if i % 25 == 0 {
vprintln(' checking ${i:-10d} for ${expected_file} ...')
}
if os.exists(expected_file) {
assert true
vprintln('> done.')
time.sleep(100 * time.millisecond)
break
}
time.sleep(5 * time.millisecond)
}
}
fn setup_cycles_environment() {
mut max_live_cycles := 1000
mut max_wait_cycles := 400
if os.user_os() == 'macos' {
// max_live_cycles *= 5
// max_wait_cycles *= 5
}
os.setenv('LIVE_CYCLES', '${max_live_cycles}', true)
os.setenv('WAIT_CYCLES', '${max_wait_cycles}', true)
}
//
fn test_live_program_can_be_compiled() {
setup_cycles_environment()
eprintln('Compiling...')
compile_cmd := '${os.quoted_path(vexe)} -cg -keepc -nocolor -live -o ${os.quoted_path(genexe_file)} ${os.quoted_path(main_source_file)}'
eprintln('> compile_cmd: ${compile_cmd}')
os.system(compile_cmd)
//
cmd := '${os.quoted_path(genexe_file)} > /dev/null &'
eprintln('Running with: ${cmd}')
res := os.system(cmd)
assert res == 0
eprintln('... running in the background')
wait_for_file('ORIGINAL')
}
fn test_live_program_can_be_changed_1() {
change_source('CHANGED')
assert true
}
fn test_live_program_can_be_changed_2() {
change_source('ANOTHER')
assert true
}
fn test_live_program_can_be_changed_3() {
change_source('STOP')
change_source('STOP')
change_source('STOP')
assert true
}