atex 0.8__py3-none-any.whl → 0.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atex/cli/fmf.py +73 -23
- atex/cli/libvirt.py +127 -0
- atex/cli/testingfarm.py +12 -0
- atex/connection/__init__.py +13 -11
- atex/connection/podman.py +63 -0
- atex/connection/ssh.py +31 -33
- atex/executor/executor.py +131 -107
- atex/executor/reporter.py +66 -71
- atex/executor/scripts.py +9 -3
- atex/executor/testcontrol.py +43 -30
- atex/fmf.py +94 -74
- atex/orchestrator/__init__.py +3 -2
- atex/orchestrator/aggregator.py +63 -58
- atex/orchestrator/orchestrator.py +194 -133
- atex/provision/__init__.py +11 -11
- atex/provision/libvirt/__init__.py +2 -24
- atex/provision/libvirt/libvirt.py +465 -0
- atex/provision/libvirt/locking.py +168 -0
- atex/provision/libvirt/setup-libvirt.sh +21 -1
- atex/provision/podman/__init__.py +1 -0
- atex/provision/podman/podman.py +274 -0
- atex/provision/testingfarm/api.py +69 -26
- atex/provision/testingfarm/testingfarm.py +29 -31
- atex/util/libvirt.py +18 -0
- atex/util/log.py +23 -8
- atex/util/named_mapping.py +158 -0
- atex/util/threads.py +64 -20
- {atex-0.8.dist-info → atex-0.9.dist-info}/METADATA +27 -46
- atex-0.9.dist-info/RECORD +43 -0
- atex/provision/podman/README +0 -59
- atex/provision/podman/host_container.sh +0 -74
- atex-0.8.dist-info/RECORD +0 -37
- {atex-0.8.dist-info → atex-0.9.dist-info}/WHEEL +0 -0
- {atex-0.8.dist-info → atex-0.9.dist-info}/entry_points.txt +0 -0
- {atex-0.8.dist-info → atex-0.9.dist-info}/licenses/COPYING.txt +0 -0
atex/executor/executor.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import enum
|
|
3
|
+
import time
|
|
2
4
|
import select
|
|
3
5
|
import threading
|
|
4
6
|
import contextlib
|
|
@@ -27,10 +29,12 @@ class Executor:
|
|
|
27
29
|
tests_data = atex.fmf.FMFTests(tests_repo, "/plans/default")
|
|
28
30
|
|
|
29
31
|
with Executor(tests_data, conn) as e:
|
|
30
|
-
e.upload_tests(
|
|
31
|
-
e.
|
|
32
|
-
|
|
32
|
+
e.upload_tests()
|
|
33
|
+
e.plan_prepare()
|
|
34
|
+
Path("output_here").mkdir()
|
|
35
|
+
e.run_test("/some/test", "output_here")
|
|
33
36
|
e.run_test(...)
|
|
37
|
+
e.plan_finish()
|
|
34
38
|
|
|
35
39
|
One Executor instance may be used to run multiple tests sequentially.
|
|
36
40
|
In addition, multiple Executor instances can run in parallel on the same
|
|
@@ -39,27 +43,31 @@ class Executor:
|
|
|
39
43
|
conn.cmd(["mkdir", "-p", "/shared"])
|
|
40
44
|
|
|
41
45
|
with Executor(tests_data, conn, state_dir="/shared") as e:
|
|
42
|
-
e.upload_tests(
|
|
43
|
-
e.
|
|
46
|
+
e.upload_tests()
|
|
47
|
+
e.plan_prepare()
|
|
44
48
|
|
|
45
49
|
# in parallel (ie. threading or multiprocessing)
|
|
46
50
|
with Executor(tests_data, unique_conn, state_dir="/shared") as e:
|
|
47
51
|
e.run_test(...)
|
|
48
52
|
"""
|
|
49
53
|
|
|
50
|
-
def __init__(self, fmf_tests, connection, *, state_dir=None):
|
|
54
|
+
def __init__(self, fmf_tests, connection, *, env=None, state_dir=None):
|
|
51
55
|
"""
|
|
52
56
|
'fmf_tests' is a class FMFTests instance with (discovered) tests.
|
|
53
57
|
|
|
54
58
|
'connection' is a class Connection instance, already fully connected.
|
|
55
59
|
|
|
60
|
+
'env' is a dict of extra environment variables to pass to the
|
|
61
|
+
plan prepare/finish scripts and to all tests.
|
|
62
|
+
|
|
56
63
|
'state_dir' is a string or Path specifying path on the remote system for
|
|
57
64
|
storing additional data, such as tests, execution wrappers, temporary
|
|
58
65
|
plan-exported variables, etc. If left as None, a tmpdir is used.
|
|
59
66
|
"""
|
|
60
67
|
self.lock = threading.RLock()
|
|
61
|
-
self.conn = connection
|
|
62
68
|
self.fmf_tests = fmf_tests
|
|
69
|
+
self.conn = connection
|
|
70
|
+
self.env = env or {}
|
|
63
71
|
self.state_dir = state_dir
|
|
64
72
|
self.work_dir = None
|
|
65
73
|
self.tests_dir = None
|
|
@@ -96,6 +104,9 @@ class Executor:
|
|
|
96
104
|
# use the tmpdir as work_dir, avoid extra mkdir over conn
|
|
97
105
|
self.work_dir = tmp_dir
|
|
98
106
|
|
|
107
|
+
# create / truncate the TMT_PLAN_ENVIRONMENT_FILE
|
|
108
|
+
self.conn.cmd(("truncate", "-s", "0", self.plan_env_file), check=True)
|
|
109
|
+
|
|
99
110
|
def cleanup(self):
|
|
100
111
|
with self.lock:
|
|
101
112
|
work_dir = self.work_dir
|
|
@@ -109,8 +120,12 @@ class Executor:
|
|
|
109
120
|
self.plan_env_file = None
|
|
110
121
|
|
|
111
122
|
def __enter__(self):
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
try:
|
|
124
|
+
self.setup()
|
|
125
|
+
return self
|
|
126
|
+
except Exception:
|
|
127
|
+
self.cleanup()
|
|
128
|
+
raise
|
|
114
129
|
|
|
115
130
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
116
131
|
self.cleanup()
|
|
@@ -131,13 +146,32 @@ class Executor:
|
|
|
131
146
|
f"remote:{self.tests_dir}",
|
|
132
147
|
)
|
|
133
148
|
|
|
134
|
-
def
|
|
149
|
+
def _run_prepare_scripts(self, scripts):
|
|
150
|
+
# make envionment for 'prepare' scripts
|
|
151
|
+
env = {
|
|
152
|
+
**self.fmf_tests.plan_env,
|
|
153
|
+
**self.env,
|
|
154
|
+
"TMT_PLAN_ENVIRONMENT_FILE": self.plan_env_file,
|
|
155
|
+
}
|
|
156
|
+
env_args = (f"{k}={v}" for k, v in env.items())
|
|
157
|
+
# run the scripts
|
|
158
|
+
for script in scripts:
|
|
159
|
+
self.conn.cmd(
|
|
160
|
+
("env", *env_args, "bash"),
|
|
161
|
+
input=script,
|
|
162
|
+
text=True,
|
|
163
|
+
check=True,
|
|
164
|
+
stdout=None if util.in_debug_mode() else subprocess.DEVNULL,
|
|
165
|
+
stderr=subprocess.STDOUT,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def plan_prepare(self):
|
|
135
169
|
"""
|
|
136
170
|
Install packages and run scripts extracted from a TMT plan by a FMFTests
|
|
137
171
|
instance given during class initialization.
|
|
138
172
|
|
|
139
|
-
Also
|
|
140
|
-
|
|
173
|
+
Also run additional scripts specified under the 'prepare' step inside
|
|
174
|
+
the fmf metadata of a plan.
|
|
141
175
|
"""
|
|
142
176
|
# install packages from the plan
|
|
143
177
|
if self.fmf_tests.prepare_pkgs:
|
|
@@ -151,65 +185,74 @@ class Executor:
|
|
|
151
185
|
stderr=subprocess.STDOUT,
|
|
152
186
|
)
|
|
153
187
|
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
env["TMT_PLAN_ENVIRONMENT_FILE"] = self.plan_env_file
|
|
158
|
-
env_args = (f"{k}={v}" for k, v in env.items())
|
|
188
|
+
# run 'prepare' scripts from the plan
|
|
189
|
+
if scripts := self.fmf_tests.prepare_scripts:
|
|
190
|
+
self._run_prepare_scripts(scripts)
|
|
159
191
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
stdout=None if util.in_debug_mode() else subprocess.DEVNULL,
|
|
168
|
-
stderr=subprocess.STDOUT,
|
|
169
|
-
)
|
|
192
|
+
def plan_finish(self):
|
|
193
|
+
"""
|
|
194
|
+
Run any scripts specified under the 'finish' step inside
|
|
195
|
+
the fmf metadata of a plan.
|
|
196
|
+
"""
|
|
197
|
+
if scripts := self.fmf_tests.finish_scripts:
|
|
198
|
+
self._run_prepare_scripts(scripts)
|
|
170
199
|
|
|
171
|
-
|
|
200
|
+
class State(enum.Enum):
|
|
201
|
+
STARTING_TEST = enum.auto()
|
|
202
|
+
READING_CONTROL = enum.auto()
|
|
203
|
+
WAITING_FOR_EXIT = enum.auto()
|
|
204
|
+
RECONNECTING = enum.auto()
|
|
205
|
+
|
|
206
|
+
def run_test(self, test_name, output_dir, *, env=None):
|
|
172
207
|
"""
|
|
173
208
|
Run one test on the remote system.
|
|
174
209
|
|
|
175
210
|
'test_name' is a string with test name.
|
|
176
211
|
|
|
177
|
-
'
|
|
178
|
-
|
|
179
|
-
|
|
212
|
+
'output_dir' is a destination dir (string or Path) for results reported
|
|
213
|
+
and files uploaded by the test. Results are always stored in a line-JSON
|
|
214
|
+
format in a file named 'results', files are always uploaded to directory
|
|
215
|
+
named 'files', both inside 'output_dir'.
|
|
216
|
+
The path for 'output_dir' must already exist and be an empty directory
|
|
217
|
+
(ie. typically a tmpdir).
|
|
180
218
|
|
|
181
219
|
'env' is a dict of extra environment variables to pass to the test.
|
|
182
220
|
|
|
183
221
|
Returns an integer exit code of the test script.
|
|
184
222
|
"""
|
|
223
|
+
output_dir = Path(output_dir)
|
|
185
224
|
test_data = self.fmf_tests.tests[test_name]
|
|
186
225
|
|
|
187
|
-
# start with fmf-plan-defined environment
|
|
188
|
-
env_vars = self.fmf_tests.plan_env.copy()
|
|
189
|
-
# append fmf-test-defined environment into it
|
|
190
|
-
for item in fmf.listlike(test_data, "environment"):
|
|
191
|
-
env_vars.update(item)
|
|
192
|
-
# append additional variables typically exported by tmt
|
|
193
|
-
env_vars["TMT_PLAN_ENVIRONMENT_FILE"] = self.plan_env_file
|
|
194
|
-
env_vars["TMT_TEST_NAME"] = test_name
|
|
195
|
-
env_vars["ATEX_TEST_NAME"] = test_name
|
|
196
|
-
# append variables given to this function call
|
|
197
|
-
if env:
|
|
198
|
-
env_vars.update(env)
|
|
199
|
-
|
|
200
226
|
# run a setup script, preparing wrapper + test scripts
|
|
201
227
|
setup_script = scripts.test_setup(
|
|
202
228
|
test=scripts.Test(test_name, test_data, self.fmf_tests.test_dirs[test_name]),
|
|
203
229
|
tests_dir=self.tests_dir,
|
|
204
230
|
wrapper_exec=f"{self.work_dir}/wrapper.sh",
|
|
205
231
|
test_exec=f"{self.work_dir}/test.sh",
|
|
232
|
+
test_yaml=f"{self.work_dir}/metadata.yaml",
|
|
206
233
|
)
|
|
207
234
|
self.conn.cmd(("bash",), input=setup_script, text=True, check=True)
|
|
208
235
|
|
|
236
|
+
# start with fmf-plan-defined environment
|
|
237
|
+
env_vars = {
|
|
238
|
+
**self.fmf_tests.plan_env,
|
|
239
|
+
"TMT_PLAN_ENVIRONMENT_FILE": self.plan_env_file,
|
|
240
|
+
"TMT_TEST_NAME": test_name,
|
|
241
|
+
"TMT_TEST_METADATA": f"{self.work_dir}/metadata.yaml",
|
|
242
|
+
}
|
|
243
|
+
# append fmf-test-defined environment into it
|
|
244
|
+
for item in fmf.listlike(test_data, "environment"):
|
|
245
|
+
env_vars.update(item)
|
|
246
|
+
# append the Executor-wide environment passed to __init__()
|
|
247
|
+
env_vars.update(self.env)
|
|
248
|
+
# append variables given to this function call
|
|
249
|
+
if env:
|
|
250
|
+
env_vars.update(env)
|
|
251
|
+
|
|
209
252
|
with contextlib.ExitStack() as stack:
|
|
210
|
-
reporter = stack.enter_context(Reporter(
|
|
211
|
-
testout_fd = stack.enter_context(reporter.open_tmpfile())
|
|
253
|
+
reporter = stack.enter_context(Reporter(output_dir, "results", "files"))
|
|
212
254
|
duration = Duration(test_data.get("duration", "5m"))
|
|
255
|
+
control = testcontrol.TestControl(reporter=reporter, duration=duration)
|
|
213
256
|
|
|
214
257
|
test_proc = None
|
|
215
258
|
control_fd = None
|
|
@@ -223,23 +266,19 @@ class Executor:
|
|
|
223
266
|
test_proc.wait()
|
|
224
267
|
raise TestAbortedError(msg) from None
|
|
225
268
|
|
|
269
|
+
exception = None
|
|
270
|
+
|
|
226
271
|
try:
|
|
227
|
-
|
|
228
|
-
state = "starting_test"
|
|
272
|
+
state = self.State.STARTING_TEST
|
|
229
273
|
while not duration.out_of_time():
|
|
230
274
|
with self.lock:
|
|
231
275
|
if self.cancelled:
|
|
232
276
|
abort("cancel requested")
|
|
233
277
|
|
|
234
|
-
if state ==
|
|
278
|
+
if state == self.State.STARTING_TEST:
|
|
235
279
|
control_fd, pipe_w = os.pipe()
|
|
236
280
|
os.set_blocking(control_fd, False)
|
|
237
|
-
control
|
|
238
|
-
control_fd=control_fd,
|
|
239
|
-
reporter=reporter,
|
|
240
|
-
duration=duration,
|
|
241
|
-
testout_fd=testout_fd,
|
|
242
|
-
)
|
|
281
|
+
control.reassign(control_fd)
|
|
243
282
|
# reconnect/reboot count (for compatibility)
|
|
244
283
|
env_vars["TMT_REBOOT_COUNT"] = str(reconnects)
|
|
245
284
|
env_vars["TMT_TEST_RESTART_COUNT"] = str(reconnects)
|
|
@@ -249,13 +288,13 @@ class Executor:
|
|
|
249
288
|
test_proc = self.conn.cmd(
|
|
250
289
|
("env", *env_args, f"{self.work_dir}/wrapper.sh"),
|
|
251
290
|
stdout=pipe_w,
|
|
252
|
-
stderr=
|
|
291
|
+
stderr=reporter.testout_fobj.fileno(),
|
|
253
292
|
func=util.subprocess_Popen,
|
|
254
293
|
)
|
|
255
294
|
os.close(pipe_w)
|
|
256
|
-
state =
|
|
295
|
+
state = self.State.READING_CONTROL
|
|
257
296
|
|
|
258
|
-
elif state ==
|
|
297
|
+
elif state == self.State.READING_CONTROL:
|
|
259
298
|
rlist, _, xlist = select.select((control_fd,), (), (control_fd,), 0.1)
|
|
260
299
|
if xlist:
|
|
261
300
|
abort(f"got exceptional condition on control_fd {control_fd}")
|
|
@@ -264,9 +303,9 @@ class Executor:
|
|
|
264
303
|
if control.eof:
|
|
265
304
|
os.close(control_fd)
|
|
266
305
|
control_fd = None
|
|
267
|
-
state =
|
|
306
|
+
state = self.State.WAITING_FOR_EXIT
|
|
268
307
|
|
|
269
|
-
elif state ==
|
|
308
|
+
elif state == self.State.WAITING_FOR_EXIT:
|
|
270
309
|
# control stream is EOF and it has nothing for us to read,
|
|
271
310
|
# we're now just waiting for proc to cleanly terminate
|
|
272
311
|
try:
|
|
@@ -279,7 +318,7 @@ class Executor:
|
|
|
279
318
|
self.conn.disconnect()
|
|
280
319
|
# if reconnect was requested, do so, otherwise abort
|
|
281
320
|
if control.reconnect:
|
|
282
|
-
state =
|
|
321
|
+
state = self.State.RECONNECTING
|
|
283
322
|
if control.reconnect != "always":
|
|
284
323
|
control.reconnect = None
|
|
285
324
|
else:
|
|
@@ -291,13 +330,18 @@ class Executor:
|
|
|
291
330
|
except subprocess.TimeoutExpired:
|
|
292
331
|
pass
|
|
293
332
|
|
|
294
|
-
elif state ==
|
|
333
|
+
elif state == self.State.RECONNECTING:
|
|
295
334
|
try:
|
|
296
335
|
self.conn.connect(block=False)
|
|
297
336
|
reconnects += 1
|
|
298
|
-
state =
|
|
337
|
+
state = self.State.STARTING_TEST
|
|
299
338
|
except BlockingIOError:
|
|
300
339
|
pass
|
|
340
|
+
except ConnectionError:
|
|
341
|
+
# can happen when ie. ssh is connecting over a LocalForward port,
|
|
342
|
+
# causing 'read: Connection reset by peer' instead of timeout
|
|
343
|
+
# - just retry again after a short delay
|
|
344
|
+
time.sleep(0.5)
|
|
301
345
|
|
|
302
346
|
else:
|
|
303
347
|
raise AssertionError("reached unexpected state")
|
|
@@ -305,12 +349,19 @@ class Executor:
|
|
|
305
349
|
else:
|
|
306
350
|
abort("test duration timeout reached")
|
|
307
351
|
|
|
308
|
-
# testing successful
|
|
352
|
+
# testing successful
|
|
309
353
|
|
|
310
354
|
# test wrapper hasn't provided exitcode
|
|
311
355
|
if control.exit_code is None:
|
|
312
356
|
abort("exitcode not reported, wrapper bug?")
|
|
313
357
|
|
|
358
|
+
return control.exit_code
|
|
359
|
+
|
|
360
|
+
except Exception as e:
|
|
361
|
+
exception = e
|
|
362
|
+
raise
|
|
363
|
+
|
|
364
|
+
finally:
|
|
314
365
|
# partial results that were never reported
|
|
315
366
|
if control.partial_results:
|
|
316
367
|
for result in control.partial_results.values():
|
|
@@ -320,59 +371,32 @@ class Executor:
|
|
|
320
371
|
control.nameless_result_seen = True
|
|
321
372
|
if testout := result.get("testout"):
|
|
322
373
|
try:
|
|
323
|
-
reporter.
|
|
374
|
+
reporter.link_testout(testout, name)
|
|
324
375
|
except FileExistsError:
|
|
325
376
|
raise testcontrol.BadReportJSONError(
|
|
326
377
|
f"file '{testout}' already exists",
|
|
327
378
|
) from None
|
|
328
379
|
reporter.report(result)
|
|
329
380
|
|
|
330
|
-
#
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
381
|
+
# if an unexpected infrastructure-related exception happened
|
|
382
|
+
if exception:
|
|
383
|
+
try:
|
|
384
|
+
reporter.link_testout("output.txt")
|
|
385
|
+
except FileExistsError:
|
|
386
|
+
pass
|
|
335
387
|
reporter.report({
|
|
336
|
-
"status": "
|
|
388
|
+
"status": "infra",
|
|
389
|
+
"note": repr(exception),
|
|
337
390
|
"testout": "output.txt",
|
|
338
391
|
})
|
|
339
392
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
except Exception:
|
|
343
|
-
# if the test hasn't reported a result for itself, but still
|
|
344
|
-
# managed to break something, provide at least the default log
|
|
345
|
-
# for manual investigation - otherwise test output disappears
|
|
346
|
-
if not control.nameless_result_seen:
|
|
393
|
+
# if the test hasn't reported a result for itself
|
|
394
|
+
elif not control.nameless_result_seen:
|
|
347
395
|
try:
|
|
348
|
-
reporter.
|
|
349
|
-
reporter.report({
|
|
350
|
-
"status": "infra",
|
|
351
|
-
"testout": "output.txt",
|
|
352
|
-
})
|
|
353
|
-
# in case outout.txt exists as a directory
|
|
396
|
+
reporter.link_testout("output.txt")
|
|
354
397
|
except FileExistsError:
|
|
355
398
|
pass
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
# info.name for info in _pkgutil.iter_modules(__spec__.submodule_search_locations)
|
|
361
|
-
#]
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
#import importlib as _importlib
|
|
365
|
-
#import pkgutil as _pkgutil
|
|
366
|
-
#
|
|
367
|
-
#
|
|
368
|
-
#def __dir__():
|
|
369
|
-
# return __all__
|
|
370
|
-
#
|
|
371
|
-
#
|
|
372
|
-
## lazily import submodules
|
|
373
|
-
#def __getattr__(attr):
|
|
374
|
-
# # importing a module known to exist
|
|
375
|
-
# if attr in __all__:
|
|
376
|
-
# return _importlib.import_module(f".{attr}", __name__)
|
|
377
|
-
# else:
|
|
378
|
-
# raise AttributeError(f"module '{__name__}' has no attribute '{attr}'")
|
|
399
|
+
reporter.report({
|
|
400
|
+
"status": "pass" if control.exit_code == 0 else "fail",
|
|
401
|
+
"testout": "output.txt",
|
|
402
|
+
})
|
atex/executor/reporter.py
CHANGED
|
@@ -1,70 +1,72 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import json
|
|
3
|
-
import ctypes
|
|
4
|
-
import ctypes.util
|
|
5
|
-
import contextlib
|
|
6
3
|
from pathlib import Path
|
|
7
4
|
|
|
8
5
|
from .. import util
|
|
9
6
|
|
|
10
7
|
|
|
11
|
-
libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
|
|
12
|
-
|
|
13
|
-
# int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, int flags)
|
|
14
|
-
libc.linkat.argtypes = (
|
|
15
|
-
ctypes.c_int,
|
|
16
|
-
ctypes.c_char_p,
|
|
17
|
-
ctypes.c_int,
|
|
18
|
-
ctypes.c_char_p,
|
|
19
|
-
ctypes.c_int,
|
|
20
|
-
)
|
|
21
|
-
libc.linkat.restype = ctypes.c_int
|
|
22
|
-
|
|
23
|
-
# fcntl.h:#define AT_EMPTY_PATH 0x1000 /* Allow empty relative pathname */
|
|
24
|
-
AT_EMPTY_PATH = 0x1000
|
|
25
|
-
|
|
26
|
-
# fcntl.h:#define AT_FDCWD -100 /* Special value used to indicate
|
|
27
|
-
AT_FDCWD = -100
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def linkat(*args):
|
|
31
|
-
if (ret := libc.linkat(*args)) == -1:
|
|
32
|
-
errno = ctypes.get_errno()
|
|
33
|
-
raise OSError(errno, os.strerror(errno))
|
|
34
|
-
return ret
|
|
35
|
-
|
|
36
|
-
|
|
37
8
|
class Reporter:
|
|
38
9
|
"""
|
|
39
10
|
Collects reported results (in a format specified by RESULTS.md) for
|
|
40
11
|
a specific test, storing them persistently.
|
|
41
12
|
"""
|
|
42
13
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
14
|
+
# internal name, stored inside 'output_dir' and hardlinked to
|
|
15
|
+
# 'testout'-JSON-key-specified result entries; deleted on exit
|
|
16
|
+
TESTOUT = "testout.temp"
|
|
46
17
|
|
|
47
|
-
|
|
18
|
+
def __init__(self, output_dir, results_file, files_dir):
|
|
48
19
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
self.json_fobj = None
|
|
20
|
+
'output_dir' is a destination dir (string or Path) for results reported
|
|
21
|
+
and files uploaded.
|
|
52
22
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
23
|
+
'results_file' is a file name inside 'output_dir' the results will be
|
|
24
|
+
reported into.
|
|
25
|
+
|
|
26
|
+
'files_dir' is a dir name inside 'output_dir' any files will be
|
|
27
|
+
uploaded to.
|
|
28
|
+
"""
|
|
29
|
+
output_dir = Path(output_dir)
|
|
30
|
+
self.testout_file = output_dir / self.TESTOUT
|
|
31
|
+
self.results_file = output_dir / results_file
|
|
32
|
+
self.files_dir = output_dir / files_dir
|
|
33
|
+
self.output_dir = output_dir
|
|
34
|
+
self.results_fobj = None
|
|
35
|
+
self.testout_fobj = None
|
|
36
|
+
|
|
37
|
+
def start(self):
|
|
38
|
+
if self.testout_file.exists():
|
|
39
|
+
raise FileExistsError(f"{self.testout_file} already exists")
|
|
40
|
+
self.testout_fobj = open(self.testout_file, "wb")
|
|
41
|
+
|
|
42
|
+
if self.results_file.exists():
|
|
43
|
+
raise FileExistsError(f"{self.results_file} already exists")
|
|
44
|
+
self.results_fobj = open(self.results_file, "w", newline="\n")
|
|
57
45
|
|
|
58
46
|
if self.files_dir.exists():
|
|
59
47
|
raise FileExistsError(f"{self.files_dir} already exists")
|
|
60
48
|
self.files_dir.mkdir()
|
|
61
49
|
|
|
62
|
-
|
|
50
|
+
def stop(self):
|
|
51
|
+
if self.results_fobj:
|
|
52
|
+
self.results_fobj.close()
|
|
53
|
+
self.results_fobj = None
|
|
54
|
+
|
|
55
|
+
if self.testout_fobj:
|
|
56
|
+
self.testout_fobj.close()
|
|
57
|
+
self.testout_fobj = None
|
|
58
|
+
Path(self.testout_file).unlink()
|
|
59
|
+
|
|
60
|
+
def __enter__(self):
|
|
61
|
+
try:
|
|
62
|
+
self.start()
|
|
63
|
+
return self
|
|
64
|
+
except Exception:
|
|
65
|
+
self.stop()
|
|
66
|
+
raise
|
|
63
67
|
|
|
64
68
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
65
|
-
|
|
66
|
-
self.json_fobj.close()
|
|
67
|
-
self.json_fobj = None
|
|
69
|
+
self.stop()
|
|
68
70
|
|
|
69
71
|
def report(self, result_line):
|
|
70
72
|
"""
|
|
@@ -72,35 +74,28 @@ class Reporter:
|
|
|
72
74
|
|
|
73
75
|
'result_line' is a dict in the format specified by RESULTS.md.
|
|
74
76
|
"""
|
|
75
|
-
json.dump(result_line, self.
|
|
76
|
-
self.
|
|
77
|
-
self.
|
|
77
|
+
json.dump(result_line, self.results_fobj, indent=None)
|
|
78
|
+
self.results_fobj.write("\n")
|
|
79
|
+
self.results_fobj.flush()
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
flags = open_mode | os.O_TMPFILE
|
|
86
|
-
fd = os.open(self.files_dir, flags, 0o644)
|
|
87
|
-
try:
|
|
88
|
-
yield fd
|
|
89
|
-
finally:
|
|
90
|
-
os.close(fd)
|
|
81
|
+
def _dest_path(self, file_name, result_name=None):
|
|
82
|
+
result_name = util.normalize_path(result_name) if result_name else "."
|
|
83
|
+
# /path/to/files_dir / path/to/subtest / path/to/file.log
|
|
84
|
+
file_path = self.files_dir / result_name / util.normalize_path(file_name)
|
|
85
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
return file_path
|
|
91
87
|
|
|
92
|
-
def
|
|
88
|
+
def open_file(self, file_name, result_name=None, mode="wb"):
|
|
93
89
|
"""
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
This function can be called multiple times with the same 'fd', and
|
|
98
|
-
does not close or otherwise alter the descriptor.
|
|
90
|
+
Open a file named 'file_name' in a directory relevant to 'result_name'.
|
|
91
|
+
Returns an opened file-like object that can be used in a context manager
|
|
92
|
+
just like with regular open().
|
|
99
93
|
|
|
100
|
-
If 'result_name' is not given,
|
|
94
|
+
If 'result_name' (typically a subtest) is not given, open the file
|
|
95
|
+
for the test (name) itself.
|
|
101
96
|
"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
return open(self._dest_path(file_name, result_name), mode)
|
|
98
|
+
|
|
99
|
+
def link_testout(self, file_name, result_name=None):
|
|
100
|
+
# TODO: docstring
|
|
101
|
+
os.link(self.testout_file, self._dest_path(file_name, result_name))
|
atex/executor/scripts.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import collections
|
|
2
|
+
import yaml
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from .. import util, fmf
|
|
@@ -102,7 +103,7 @@ def _install_packages(pkgs, extra_opts=None):
|
|
|
102
103
|
""") # noqa: E501
|
|
103
104
|
|
|
104
105
|
|
|
105
|
-
def test_setup(*, test, wrapper_exec, test_exec, **kwargs):
|
|
106
|
+
def test_setup(*, test, wrapper_exec, test_exec, test_yaml, **kwargs):
|
|
106
107
|
"""
|
|
107
108
|
Generate a bash script that should prepare the remote end for test
|
|
108
109
|
execution.
|
|
@@ -111,12 +112,12 @@ def test_setup(*, test, wrapper_exec, test_exec, **kwargs):
|
|
|
111
112
|
scripts: a test script (contents of 'test' from FMF) and a wrapper script
|
|
112
113
|
to run the test script.
|
|
113
114
|
|
|
115
|
+
'test' is a class Test instance.
|
|
116
|
+
|
|
114
117
|
'wrapper_exec' is the remote path where the wrapper script should be put.
|
|
115
118
|
|
|
116
119
|
'test_exec' is the remote path where the test script should be put.
|
|
117
120
|
|
|
118
|
-
'test' is a class Test instance.
|
|
119
|
-
|
|
120
121
|
Any 'kwargs' are passed to test_wrapper().
|
|
121
122
|
"""
|
|
122
123
|
out = "#!/bin/bash\n"
|
|
@@ -134,6 +135,11 @@ def test_setup(*, test, wrapper_exec, test_exec, **kwargs):
|
|
|
134
135
|
if recommend := list(fmf.test_pkg_requires(test.data, "recommend")):
|
|
135
136
|
out += _install_packages(recommend, ("--skip-broken",)) + "\n"
|
|
136
137
|
|
|
138
|
+
# write out test data
|
|
139
|
+
out += f"cat > '{test_yaml}' <<'ATEX_SETUP_EOF'\n"
|
|
140
|
+
out += yaml.dump(test.data).rstrip("\n") # don't rely on trailing \n
|
|
141
|
+
out += "\nATEX_SETUP_EOF\n"
|
|
142
|
+
|
|
137
143
|
# make the wrapper script
|
|
138
144
|
out += f"cat > '{wrapper_exec}' <<'ATEX_SETUP_EOF'\n"
|
|
139
145
|
out += test_wrapper(
|