atex 0.13__py3-none-any.whl → 0.15__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/executor/__init__.py CHANGED
@@ -2,21 +2,18 @@ class ExecutorError(Exception):
2
2
  """
3
3
  Raised by class Executor.
4
4
  """
5
- pass
6
5
 
7
6
 
8
7
  class TestSetupError(ExecutorError):
9
8
  """
10
9
  Raised when the preparation for test execution (ie. pkg install) fails.
11
10
  """
12
- pass
13
11
 
14
12
 
15
13
  class TestAbortedError(ExecutorError):
16
14
  """
17
15
  Raised when an infrastructure-related issue happened while running a test.
18
16
  """
19
- pass
20
17
 
21
18
 
22
19
  from . import testcontrol # noqa: F401, E402
atex/executor/duration.py CHANGED
@@ -10,7 +10,7 @@ class Duration:
10
10
 
11
11
  def __init__(self, fmf_duration):
12
12
  """
13
- 'fmf_duration' is the string specified as 'duration' in FMF metadata.
13
+ - `fmf_duration` is the string specified as 'duration' in FMF metadata.
14
14
  """
15
15
  duration = self._fmf_to_seconds(fmf_duration)
16
16
  self.end = time.monotonic() + duration
atex/executor/executor.py CHANGED
@@ -46,16 +46,18 @@ class Executor:
46
46
 
47
47
  def __init__(self, fmf_tests, connection, *, env=None, state_dir=None):
48
48
  """
49
- 'fmf_tests' is a class FMFTests instance with (discovered) tests.
49
+ - `fmf_tests` is a class FMFTests instance with (discovered) tests.
50
50
 
51
- 'connection' is a class Connection instance, already fully connected.
51
+ - `connection` is a class Connection instance, already fully connected.
52
52
 
53
- 'env' is a dict of extra environment variables to pass to the
54
- plan prepare/finish scripts and to all tests.
53
+ - `env` is a dict of extra environment variables to pass to the
54
+ plan prepare/finish scripts and to all tests.
55
55
 
56
- 'state_dir' is a string or Path specifying path on the remote system for
57
- storing additional data, such as tests, execution wrappers, temporary
58
- plan-exported variables, etc. If left as None, a tmpdir is used.
56
+ - `state_dir` is a string or Path specifying path on the remote system
57
+ for storing additional data, such as tests, execution wrappers,
58
+ temporary plan-exported variables, etc.
59
+
60
+ If left as `None`, a tmpdir is used.
59
61
  """
60
62
  self.lock = threading.RLock()
61
63
  self.fmf_tests = fmf_tests
@@ -130,7 +132,7 @@ class Executor:
130
132
  def upload_tests(self):
131
133
  """
132
134
  Upload a directory of all tests, the location of which was provided to
133
- __init__() inside 'fmf_tests', to the remote host.
135
+ `__init__()` inside `fmf_tests`, to the remote host.
134
136
  """
135
137
  self.conn.rsync(
136
138
  "-r", "--delete", "--exclude=.git/",
@@ -199,16 +201,19 @@ class Executor:
199
201
  """
200
202
  Run one test on the remote system.
201
203
 
202
- 'test_name' is a string with test name.
204
+ - `test_name` is a string with test name.
205
+
206
+ - `output_dir` is a destination dir (string or Path) for results reported
207
+ and files uploaded by the test.
208
+
209
+ Results are always stored in a line-JSON format in a file named
210
+ `results`, files are always uploaded to directory named `files`,
211
+ both inside `output_dir`.
203
212
 
204
- 'output_dir' is a destination dir (string or Path) for results reported
205
- and files uploaded by the test. Results are always stored in a line-JSON
206
- format in a file named 'results', files are always uploaded to directory
207
- named 'files', both inside 'output_dir'.
208
- The path for 'output_dir' must already exist and be an empty directory
209
- (ie. typically a tmpdir).
213
+ The path for `output_dir` must already exist and be an empty directory
214
+ (ie. typically a tmpdir).
210
215
 
211
- 'env' is a dict of extra environment variables to pass to the test.
216
+ - `env` is a dict of extra environment variables to pass to the test.
212
217
 
213
218
  Returns an integer exit code of the test script.
214
219
  """
@@ -280,22 +285,26 @@ class Executor:
280
285
  abort("cancel requested")
281
286
 
282
287
  if state == self.State.STARTING_TEST:
283
- control_fd, pipe_w = os.pipe()
284
- os.set_blocking(control_fd, False)
285
- control.reassign(control_fd)
286
288
  # reconnect/reboot count (for compatibility)
287
289
  env_vars["TMT_REBOOT_COUNT"] = str(reconnects)
288
290
  env_vars["TMT_TEST_RESTART_COUNT"] = str(reconnects)
289
- # run the test in the background, letting it log output directly to
290
- # an opened file (we don't handle it, cmd client sends it to kernel)
291
291
  env_args = (f"{k}={v}" for k, v in env_vars.items())
292
- test_proc = self.conn.cmd(
293
- ("env", *env_args, f"{self.work_dir}/wrapper.sh"),
294
- stdout=pipe_w,
295
- stderr=reporter.testout_fobj.fileno(),
296
- func=util.subprocess_Popen,
297
- )
298
- os.close(pipe_w)
292
+ try:
293
+ # open a pipe for test control
294
+ control_fd, pipe_w = os.pipe()
295
+ os.set_blocking(control_fd, False)
296
+ control.reassign(control_fd)
297
+ # run the test in the background, letting it log output directly to
298
+ # an opened file (we don't handle it, cmd client sends it to kernel)
299
+ with reporter.open_testout() as testout_fd:
300
+ test_proc = self.conn.cmd(
301
+ ("env", *env_args, f"{self.work_dir}/wrapper.sh"),
302
+ stdout=pipe_w,
303
+ stderr=testout_fd,
304
+ func=util.subprocess_Popen,
305
+ )
306
+ finally:
307
+ os.close(pipe_w)
299
308
  state = self.State.READING_CONTROL
300
309
 
301
310
  elif state == self.State.READING_CONTROL:
@@ -304,7 +313,7 @@ class Executor:
304
313
  abort(f"got exceptional condition on control_fd {control_fd}")
305
314
  elif rlist:
306
315
  control.process()
307
- if control.eof:
316
+ if control.eof or control.disconnect_received:
308
317
  os.close(control_fd)
309
318
  control_fd = None
310
319
  state = self.State.WAITING_FOR_EXIT
@@ -320,11 +329,16 @@ class Executor:
320
329
  else:
321
330
  # unexpected error happened (crash, disconnect, etc.)
322
331
  self.conn.disconnect()
323
- # if reconnect was requested, do so, otherwise abort
324
- if control.reconnect:
332
+ # if there was a test control parser running
333
+ if control.in_progress:
334
+ abort(
335
+ f"{str(control.in_progress)} was running while test "
336
+ f"wrapper unexpectedly exited with {code}",
337
+ )
338
+ # if test control disconnect was intentional, try to reconnect
339
+ if control.disconnect_received:
325
340
  state = self.State.RECONNECTING
326
- if control.reconnect != "always":
327
- control.reconnect = None
341
+ control.disconnect_received = False
328
342
  else:
329
343
  abort(
330
344
  f"test wrapper unexpectedly exited with {code} and "
atex/executor/reporter.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import json
3
+ import contextlib
3
4
  from pathlib import Path
4
5
 
5
6
  from .. import util
@@ -17,32 +18,30 @@ class Reporter:
17
18
 
18
19
  def __init__(self, output_dir, results_file, files_dir):
19
20
  """
20
- 'output_dir' is a destination dir (string or Path) for results reported
21
- and files uploaded.
21
+ - `output_dir` is a destination dir (string or Path) for results
22
+ reported and files uploaded.
22
23
 
23
- 'results_file' is a file name inside 'output_dir' the results will be
24
- reported into.
24
+ - `results_file` is a file name inside `output_dir` the results
25
+ will be reported into.
25
26
 
26
- 'files_dir' is a dir name inside 'output_dir' any files will be
27
- uploaded to.
27
+ - `files_dir` is a dir name inside `output_dir` any files will be
28
+ uploaded to.
28
29
  """
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
30
+ self.output_dir = Path(output_dir)
31
+ self.results_file = self.output_dir / results_file
34
32
  self.results_fobj = None
35
- self.testout_fobj = None
33
+ self.files_dir = self.output_dir / files_dir
34
+ self.testout_file = self.output_dir / self.TESTOUT
36
35
 
37
36
  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
37
  if self.results_file.exists():
43
38
  raise FileExistsError(f"{self.results_file} already exists")
44
39
  self.results_fobj = open(self.results_file, "w", newline="\n")
45
40
 
41
+ if self.testout_file.exists():
42
+ raise FileExistsError(f"{self.testout_file} already exists")
43
+ self.testout_file.touch()
44
+
46
45
  if self.files_dir.exists():
47
46
  raise FileExistsError(f"{self.files_dir} already exists")
48
47
  self.files_dir.mkdir()
@@ -51,11 +50,7 @@ class Reporter:
51
50
  if self.results_fobj:
52
51
  self.results_fobj.close()
53
52
  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()
53
+ self.testout_file.unlink(missing_ok=True)
59
54
 
60
55
  def __enter__(self):
61
56
  try:
@@ -72,7 +67,7 @@ class Reporter:
72
67
  """
73
68
  Persistently record a test result.
74
69
 
75
- 'result_line' is a dict in the format specified by RESULTS.md.
70
+ - `result_line` is a dict in the format specified by RESULTS.md.
76
71
  """
77
72
  json.dump(result_line, self.results_fobj, indent=None)
78
73
  self.results_fobj.write("\n")
@@ -85,15 +80,32 @@ class Reporter:
85
80
  file_path.parent.mkdir(parents=True, exist_ok=True)
86
81
  return file_path
87
82
 
88
- def open_fd(self, file_name, mode, result_name=None):
83
+ @contextlib.contextmanager
84
+ def open_file(self, file_name, mode, result_name=None):
89
85
  """
90
- Open a file named 'file_name' in a directory relevant to 'result_name'.
91
- Returns an opened file descriptor that can be closed with os.close().
86
+ Open a file named `file_name` in a directory relevant to `result_name`.
87
+ Yields an opened file descriptor (as integer) as a Context Manager.
92
88
 
93
- If 'result_name' (typically a subtest) is not given, open the file
89
+ If `result_name` (typically a subtest) is not given, open the file
94
90
  for the test (name) itself.
95
91
  """
96
- return os.open(self._dest_path(file_name, result_name), mode)
92
+ fd = os.open(self._dest_path(file_name, result_name), mode)
93
+ try:
94
+ yield fd
95
+ finally:
96
+ os.close(fd)
97
+
98
+ @contextlib.contextmanager
99
+ def open_testout(self):
100
+ """
101
+ Open a file named after self.TESTOUT inside self.output_dir.
102
+ Yields an opened file descriptor (as integer) as a Context Manager.
103
+ """
104
+ fd = os.open(self.testout_file, os.O_WRONLY | os.O_CREAT | os.O_APPEND)
105
+ try:
106
+ yield fd
107
+ finally:
108
+ os.close(fd)
97
109
 
98
110
  def link_testout(self, file_name, result_name=None):
99
111
  # TODO: docstring
atex/executor/scripts.py CHANGED
@@ -34,12 +34,12 @@ def test_wrapper(*, test, tests_dir, test_exec):
34
34
  is considered as test output and any unintended environment changes
35
35
  will impact the test itself.
36
36
 
37
- 'test' is a class Test instance.
37
+ - `test` is a class Test instance.
38
38
 
39
- 'test_dir' is a remote directory (repository) of all the tests,
40
- a.k.a. FMF metadata root.
39
+ - `test_dir` is a remote directory (repository) of all the tests,
40
+ a.k.a. FMF metadata root.
41
41
 
42
- 'test_exec' is a remote path to the actual test to run.
42
+ - `test_exec` is a remote path to the actual test to run.
43
43
  """
44
44
  out = "#!/bin/bash\n"
45
45
 
@@ -104,13 +104,13 @@ def test_setup(*, test, wrapper_exec, test_exec, test_yaml, **kwargs):
104
104
  scripts: a test script (contents of 'test' from FMF) and a wrapper script
105
105
  to run the test script.
106
106
 
107
- 'test' is a class Test instance.
107
+ - `test` is a class Test instance.
108
108
 
109
- 'wrapper_exec' is the remote path where the wrapper script should be put.
109
+ - `wrapper_exec` is the remote path where the wrapper script should be put.
110
110
 
111
- 'test_exec' is the remote path where the test script should be put.
111
+ - `test_exec` is the remote path where the test script should be put.
112
112
 
113
- Any 'kwargs' are passed to test_wrapper().
113
+ Any `kwargs` are passed to `test_wrapper()`.
114
114
  """
115
115
  out = "#!/bin/bash\n"
116
116
 
@@ -1,8 +1,9 @@
1
1
  import os
2
- import collections
3
2
  import json
3
+ import logging
4
+ import collections
4
5
 
5
- from .. import util
6
+ logger = logging.getLogger("atex.executor.testcontrol")
6
7
 
7
8
 
8
9
  class BufferFullError(Exception):
@@ -12,21 +13,22 @@ class BufferFullError(Exception):
12
13
  class NonblockLineReader:
13
14
  """
14
15
  Kind of like io.BufferedReader but capable of reading from non-blocking
15
- sources (both O_NONBLOCK sockets and os.set_blocking(False) descriptors),
16
- re-assembling full lines from (potentially) multiple read() calls.
16
+ sources (both `O_NONBLOCK` sockets and `os.set_blocking(False)`
17
+ descriptors), re-assembling full lines from (potentially) multiple
18
+ `read()` calls.
17
19
 
18
20
  It also takes a file descriptor (not a file-like object) and takes extra
19
21
  care to read one-byte-at-a-time to not read (and buffer) more data from the
20
22
  source descriptor, allowing it to be used for in-kernel move, such as via
21
- os.sendfile() or os.splice().
23
+ `os.sendfile()` or `os.splice()`.
22
24
  """
23
25
 
24
26
  def __init__(self, src, maxlen=4096):
25
27
  """
26
- 'src' is an opened file descriptor (integer).
28
+ - `src` is an opened file descriptor (integer).
27
29
 
28
- 'maxlen' is a maximum potential line length, incl. the newline
29
- character - if reached, a BufferFullError is raised.
30
+ - `maxlen` is a maximum potential line length, incl. the newline
31
+ character - if reached, a BufferFullError is raised.
30
32
  """
31
33
  self.src = src
32
34
  self.eof = False
@@ -35,7 +37,7 @@ class NonblockLineReader:
35
37
 
36
38
  def readline(self):
37
39
  r"""
38
- Read a line and return it, without the '\n' terminating character,
40
+ Read a line and return it, without the `\n` terminating character,
39
41
  clearing the internal buffer upon return.
40
42
 
41
43
  Returns None if nothing could be read (BlockingIOError) or if EOF
@@ -77,7 +79,6 @@ class BadControlError(Exception):
77
79
  such as invalid syntax, unknown control word, or bad or unexpected data for
78
80
  any given control word.
79
81
  """
80
- pass
81
82
 
82
83
 
83
84
  class BadReportJSONError(BadControlError):
@@ -86,7 +87,6 @@ class BadReportJSONError(BadControlError):
86
87
  the TEST_CONROL.md specification when passing JSON data to the 'result'
87
88
  control word.
88
89
  """
89
- pass
90
90
 
91
91
 
92
92
  class TestControl:
@@ -97,12 +97,12 @@ class TestControl:
97
97
 
98
98
  def __init__(self, *, reporter, duration, control_fd=None):
99
99
  """
100
- 'control_fd' is a non-blocking file descriptor to be read.
100
+ - `control_fd` is a non-blocking file descriptor to be read.
101
101
 
102
- 'reporter' is an instance of class Reporter all the results
103
- and uploaded files will be written to.
102
+ - `reporter` is an instance of class Reporter all the results
103
+ and uploaded files will be written to.
104
104
 
105
- 'duration' is a class Duration instance.
105
+ - `duration` is a class Duration instance.
106
106
  """
107
107
  self.reporter = reporter
108
108
  self.duration = duration
@@ -116,7 +116,7 @@ class TestControl:
116
116
  self.in_progress = None
117
117
  self.partial_results = collections.defaultdict(dict)
118
118
  self.exit_code = None
119
- self.reconnect = None
119
+ self.disconnect_received = False
120
120
  self.nameless_result_seen = False
121
121
 
122
122
  def reassign(self, new_fd):
@@ -138,7 +138,7 @@ class TestControl:
138
138
  Read from the control file descriptor and potentially perform any
139
139
  appropriate action based on commands read from the test.
140
140
 
141
- Returns True if there is more data expected, False otherwise
141
+ Returns `True` if there is more data expected, `False` otherwise
142
142
  (when the control file descriptor reached EOF).
143
143
  """
144
144
  # if a parser operation is in progress, continue calling it,
@@ -156,7 +156,7 @@ class TestControl:
156
156
  except BufferFullError as e:
157
157
  raise BadControlError(str(e)) from None
158
158
 
159
- util.extradebug(f"control line: {line} // eof: {self.stream.eof}")
159
+ logger.debug(f"control line: {line} // eof: {self.stream.eof}")
160
160
 
161
161
  if self.stream.eof:
162
162
  self.eof = True
@@ -176,8 +176,10 @@ class TestControl:
176
176
  parser = self._parser_duration(arg)
177
177
  elif word == "exitcode":
178
178
  parser = self._parser_exitcode(arg)
179
- elif word == "reconnect":
180
- parser = self._parser_reconnect(arg)
179
+ elif word == "disconnect":
180
+ parser = self._parser_disconnect(arg)
181
+ elif word == "noop":
182
+ parser = self._parser_noop(arg)
181
183
  else:
182
184
  raise BadControlError(f"unknown control word: {word}")
183
185
 
@@ -191,8 +193,8 @@ class TestControl:
191
193
  @classmethod
192
194
  def _merge(cls, dst, src):
193
195
  """
194
- Merge a 'src' dict into 'dst', using the rules described by
195
- TEST_CONTROL.md for 'Partial results'.
196
+ Merge a `src` dict into `dst`, using the rules described by
197
+ TEST_CONTROL.md for "Partial results".
196
198
  """
197
199
  for key, value in src.items():
198
200
  # delete existing if new value is None (JSON null)
@@ -240,7 +242,7 @@ class TestControl:
240
242
  yield
241
243
  continue
242
244
  if chunk == b"":
243
- raise BadControlError("EOF when reading data")
245
+ raise BadControlError(f"EOF when reading data, got so far: {json_data}")
244
246
  json_data += chunk
245
247
  json_length -= len(chunk)
246
248
  yield
@@ -267,8 +269,7 @@ class TestControl:
267
269
  except ValueError as e:
268
270
  raise BadReportJSONError(f"file entry {file_name} length: {str(e)}") from None
269
271
 
270
- fd = self.reporter.open_fd(file_name, os.O_WRONLY | os.O_CREAT, name)
271
- try:
272
+ with self.reporter.open_file(file_name, os.O_WRONLY | os.O_CREAT, name) as fd:
272
273
  # Linux can't do splice(2) on O_APPEND fds, so we open it above
273
274
  # as O_WRONLY and just seek to the end, simulating append
274
275
  os.lseek(fd, 0, os.SEEK_END)
@@ -290,8 +291,6 @@ class TestControl:
290
291
  raise BadControlError("EOF when reading data")
291
292
  file_length -= written
292
293
  yield
293
- finally:
294
- os.close(fd)
295
294
 
296
295
  # either store partial result + return,
297
296
  # or load previous partial result and merge into it
@@ -326,6 +325,18 @@ class TestControl:
326
325
  except FileExistsError:
327
326
  raise BadReportJSONError(f"file '{testout}' already exists") from None
328
327
 
328
+ # deduplicate file names
329
+ # - appending to files (multiple identical file 'name' in one result, or
330
+ # using partial:true) can create large amounts of files:[] entries,
331
+ # so simply add up all their lengths and specify each file 'name' once
332
+ if files := result.get("files"):
333
+ counter = collections.Counter()
334
+ for entry in files:
335
+ counter[entry["name"]] += entry["length"]
336
+ result["files"] = tuple(
337
+ {"name": name, "length": length} for name, length in counter.items()
338
+ )
339
+
329
340
  self.reporter.report(result)
330
341
 
331
342
  def _parser_duration(self, arg):
@@ -359,13 +370,14 @@ class TestControl:
359
370
  if False:
360
371
  yield
361
372
 
362
- def _parser_reconnect(self, arg):
363
- if not arg:
364
- self.reconnect = "once"
365
- elif arg == "always":
366
- self.reconnect = "always"
367
- else:
368
- raise BadControlError(f"unknown reconnect arg: {arg}")
373
+ def _parser_disconnect(self, _):
374
+ self.disconnect_received = True
375
+ # pretend to be a generator
376
+ if False:
377
+ yield
378
+
379
+ @staticmethod
380
+ def _parser_noop(_):
369
381
  # pretend to be a generator
370
382
  if False:
371
383
  yield
atex/fmf.py CHANGED
@@ -12,11 +12,15 @@ def listlike(data, key):
12
12
  defined as a dict or a list.
13
13
 
14
14
  This is needed because many fmf metadata keys can be used either as
15
+
15
16
  some_key: 123
17
+
16
18
  or as lists via YAML syntax
19
+
17
20
  some_key:
18
21
  - 123
19
22
  - 456
23
+
20
24
  and, for simplicity, we want to always deal with lists (iterables).
21
25
  """
22
26
  if value := data.get(key):
@@ -39,32 +43,34 @@ class FMFTests:
39
43
  context=None,
40
44
  ):
41
45
  """
42
- 'fmf_tree' is filesystem path somewhere inside fmf metadata tree,
43
- or a root fmf.Tree instance.
46
+ - `fmf_tree` is filesystem path somewhere inside fmf metadata tree,
47
+ or a root fmf.Tree instance.
44
48
 
45
- 'plan_name' is fmf identifier (like /some/thing) of a tmt plan
46
- to use for discovering tests. If None, a dummy (empty) plan is used.
49
+ - `plan_name` is fmf identifier (like `/some/thing`) of a tmt plan
50
+ to use for discovering tests. If None, a dummy (empty) plan is used.
47
51
 
48
- 'names', 'filters', 'conditions' and 'exclude' (all tuple/list)
49
- are fmf tree filters (resolved by the fmf module), overriding any
50
- existing tree filters in the plan's discover phase specifies, where:
52
+ - `names`, `filters`, `conditions` and `exclude` (all tuple/list)
53
+ are fmf tree filters (resolved by the fmf module), overriding any
54
+ existing tree filters in the plan's discover phase specifies, where:
51
55
 
52
- 'names' are test regexes like ["/some/test", "/another/test"]
56
+ - `names` are test regexes like `["/some/test", "/another/test"]`.
53
57
 
54
- 'filters' are fmf-style filter expressions, as documented on
55
- https://fmf.readthedocs.io/en/stable/modules.html#fmf.filter
58
+ - `filters` are fmf-style filter expressions, as documented on
59
+ https://fmf.readthedocs.io/en/stable/modules.html#fmf.filter
56
60
 
57
- 'conditions' are python expressions whose namespace locals()
58
- are set up to be a dictionary of the fmf tree. When any of the
59
- expressions returns True, the tree is returned, ie.
60
- ["environment['FOO'] == 'BAR'"]
61
- ["'enabled' not in locals() or enabled"]
62
- Note that KeyError is silently ignored and treated as False.
61
+ - `conditions` are python expressions whose namespace `locals()`
62
+ are set up to be a dictionary of the fmf tree. When any of the
63
+ expressions returns `True`, the tree is returned, ie.
63
64
 
64
- 'excludes' are test regexes to exclude, format same as 'names'
65
+ ["environment['FOO'] == 'BAR'"]
66
+ ["'enabled' not in locals() or enabled"]
65
67
 
66
- 'context' is a dict like {'distro': 'rhel-9.6'} used for additional
67
- adjustment of the discovered fmf metadata.
68
+ Note that KeyError is silently ignored and treated as `False`.
69
+
70
+ - `excludes` are test regexes to exclude, format same as `names`.
71
+
72
+ - `context` is a dict like `{'distro': 'rhel-9.6'}` used for additional
73
+ adjustment of the discovered fmf metadata.
68
74
  """
69
75
  # list of packages to install, as extracted from plan
70
76
  self.prepare_pkgs = []
@@ -187,8 +193,8 @@ class FMFTests:
187
193
 
188
194
  def test_pkg_requires(data, key="require"):
189
195
  """
190
- Yield RPM package names specified by test 'data' (fmf metadata dict)
191
- in the metadata 'key' (require or recommend), ignoring any non-RPM-package
196
+ Yield RPM package names specified by test `data` (fmf metadata dict)
197
+ in the metadata `key` (require or recommend), ignoring any non-RPM-package
192
198
  requires/recommends.
193
199
  """
194
200
  for entry in listlike(data, key):
@@ -204,7 +210,7 @@ def test_pkg_requires(data, key="require"):
204
210
  def all_pkg_requires(fmf_tests, key="require"):
205
211
  """
206
212
  Yield RPM package names from the plan and all tests discovered by
207
- a class FMFTests instance 'fmf_tests', ignoring any non-RPM-package
213
+ a class FMFTests instance `fmf_tests`, ignoring any non-RPM-package
208
214
  requires/recommends.
209
215
  """
210
216
  # use a set to avoid duplicates
@@ -213,25 +219,3 @@ def all_pkg_requires(fmf_tests, key="require"):
213
219
  for data in fmf_tests.tests.values():
214
220
  pkgs.update(test_pkg_requires(data, key))
215
221
  yield from pkgs
216
-
217
-
218
- # Some extra notes for fmf.prune() arguments:
219
- #
220
- # Set 'names' to filter by a list of fmf node names, ie.
221
- # ['/some/test', '/another/test']
222
- #
223
- # Set 'filters' to filter by a list of fmf-style filter expressions, see
224
- # https://fmf.readthedocs.io/en/stable/modules.html#fmf.filter
225
- #
226
- # Set 'conditions' to filter by a list of python expressions whose namespace
227
- # locals() are set up to be a dictionary of the tree. When any of the
228
- # expressions returns True, the tree is returned, ie.
229
- # ['environment["FOO"] == "BAR"']
230
- # ['"enabled" not in locals() or enabled']
231
- # Note that KeyError is silently ignored and treated as False.
232
- #
233
- # Set 'context' to a dictionary to post-process the tree metadata with
234
- # adjust expressions (that may be present in a tree) using the specified
235
- # context. Any other filters are applied afterwards to allow modification
236
- # of tree metadata by the adjust expressions. Ie.
237
- # {'distro': 'rhel-9.6.0', 'arch': 'x86_64'}