19
19
import tempfile
20
20
import warnings
21
21
from pathlib import Path
22
- from typing import Optional # noqa: F401 needed for typing.
22
+ from typing import List , Optional # noqa: F401 needed for typing.
23
+
24
+ from _pytest .config import Config as PytestConfig
25
+ from _pytest .config .argparsing import Parser as PytestParser
26
+ from _pytest .fixtures import SubRequest
27
+ from _pytest .mark import MarkDecorator # noqa: F401 used for typing
23
28
24
29
import pytest
25
30
32
37
from .workflow import Workflow , WorkflowQueue
33
38
34
39
35
- def pytest_addoption (parser ):
40
+ def pytest_addoption (parser : PytestParser ):
36
41
parser .addoption (
37
42
"--keep-workflow-wd" ,
38
43
action = "store_true" ,
@@ -82,7 +87,7 @@ def pytest_collect_file(path, parent):
82
87
return None
83
88
84
89
85
- def pytest_configure (config ):
90
+ def pytest_configure (config : PytestConfig ):
86
91
"""This runs before tests start and adds values to the config."""
87
92
# We need to add a workflow queue to some central variable. Instead of
88
93
# using a global variable we add a value to the config.
@@ -92,6 +97,14 @@ def pytest_configure(config):
92
97
workflow_queue = WorkflowQueue ()
93
98
setattr (config , "workflow_queue" , workflow_queue )
94
99
100
+ # Save which workflows are run and which are not.
101
+ executed_workflows = [] # type: List[str]
102
+ setattr (config , "executed_workflows" , executed_workflows )
103
+
104
+ # Save workflow for cleanup in this var.
105
+ workflow_cleanup_dirs = [] # type: List[str]
106
+ setattr (config , "workflow_cleanup_dirs" , workflow_cleanup_dirs )
107
+
95
108
# When multiple workflows are started they should all be set in the same
96
109
# temporary directory
97
110
# Running in a temporary directory will prevent the project repository
@@ -110,13 +123,13 @@ def pytest_configure(config):
110
123
# So this is why the native pytest `tmpdir` fixture is not used.
111
124
112
125
basetemp = config .getoption ("basetemp" )
113
- workflow_dir = (
126
+ workflow_temp_dir = (
114
127
Path (basetemp ) if basetemp is not None
115
128
else Path (tempfile .mkdtemp (prefix = "pytest_workflow_" )))
116
- setattr (config , "workflow_dir " , workflow_dir )
129
+ setattr (config , "workflow_temp_dir " , workflow_temp_dir )
117
130
118
131
119
- def pytest_collection (session ):
132
+ def pytest_collection (session : pytest . Session ):
120
133
"""This function is started at the beginning of collection"""
121
134
# pylint: disable=unused-argument
122
135
# needed for pytest
@@ -129,13 +142,81 @@ def pytest_collection(session):
129
142
print ()
130
143
131
144
132
- def pytest_runtestloop (session ):
145
+ def pytest_collection_modifyitems (config : PytestConfig ,
146
+ items : List [pytest .Item ]):
147
+ """Here we skip all tests related to workflows that are not executed"""
148
+
149
+ for item in items :
150
+ marker = item .get_closest_marker (
151
+ name = "workflow" ) # type: Optional[MarkDecorator] # noqa: E501
152
+
153
+ if marker is None :
154
+ continue
155
+
156
+ if 'name' in marker .kwargs :
157
+ workflow_name = marker .kwargs ['name' ]
158
+ # If name key is not defined use the first arg.
159
+ elif 'name' not in marker .kwargs and len (marker .args ) >= 1 :
160
+ workflow_name = marker .args [0 ]
161
+ # Make sure a name attribute is added anyway for the
162
+ # fixture lookup.
163
+ marker .kwargs ['name' ] = workflow_name
164
+ else :
165
+ # If we raise an error here a number of things will happen:
166
+ # + Pytest will crash. Giving a lot of INTERNAL ERROR lines
167
+ # + No tests will run
168
+ # + No workflows will be started
169
+ # + All the threads that are waiting for a workflow to
170
+ # finish will wait indefinitely. Causing an infinite
171
+ # hang. Pytest will never finish.
172
+ # Therefore we do not crash here, but raise a warning.
173
+ item .warn (pytest .PytestWarning (
174
+ "A workflow name should be defined in the "
175
+ "workflow marker of {0}" .format (item .nodeid )))
176
+ # Go on with the next item.
177
+ continue
178
+
179
+ if workflow_name not in config .executed_workflows :
180
+ skip_marker = pytest .mark .skip (
181
+ reason = "'{0}' has not run." .format (workflow_name ))
182
+ item .add_marker (skip_marker )
183
+
184
+
185
+ def pytest_runtestloop (session : pytest .Session ):
133
186
"""This runs after collection, but before the tests."""
134
187
session .config .workflow_queue .process (
135
188
session .config .getoption ("workflow_threads" )
136
189
)
137
190
138
191
192
+ def pytest_sessionfinish (session : pytest .Session ):
193
+ if not session .config .getoption ("keep_workflow_wd" ):
194
+ for tempdir in session .config .workflow_cleanup_dirs :
195
+ shutil .rmtree (str (tempdir ))
196
+
197
+
198
+ @pytest .fixture ()
199
+ def workflow_dir (request : SubRequest ):
200
+ """Returns the workflow_dir of the workflow named in the mark. This fixture
201
+ is only provided for tests that are marked with the workflow mark."""
202
+
203
+ # request.node refers to the node that has the mark. This is a pytest.Node
204
+ marker = request .node .get_closest_marker (name = "workflow" )
205
+
206
+ if marker is not None :
207
+ workflow_temp_dir = request .config .workflow_temp_dir
208
+ try :
209
+ workflow_name = marker .kwargs ['name' ]
210
+ except KeyError :
211
+ raise TypeError (
212
+ "A workflow name should be defined in the "
213
+ "workflow marker of {0}" .format (request .node .nodeid ))
214
+ return workflow_temp_dir / Path (replace_whitespace (workflow_name ))
215
+ else :
216
+ raise ValueError ("workflow_dir can only be requested in tests marked"
217
+ " with the workflow mark." )
218
+
219
+
139
220
class YamlFile (pytest .File ):
140
221
"""
141
222
This class collects YAML files and turns them into test items.
@@ -162,8 +243,6 @@ class WorkflowTestsCollector(pytest.Collector):
162
243
def __init__ (self , workflow_test : WorkflowTest , parent : pytest .Collector ):
163
244
self .workflow_test = workflow_test
164
245
super ().__init__ (workflow_test .name , parent = parent )
165
- # Tempdir is stored for cleanup with teardown().
166
- self .tempdir = None # type: Optional[Path]
167
246
168
247
# Attach tags to this node for easier workflow selection
169
248
self .tags = [self .workflow_test .name ] + self .workflow_test .tags
@@ -185,28 +264,34 @@ def queue_workflow(self):
185
264
This is shorter than using pytest's terminal reporter.
186
265
"""
187
266
188
- self . tempdir = (self .config .workflow_dir /
189
- Path (replace_whitespace (self .name , '_' )))
267
+ tempdir = (self .config .workflow_temp_dir /
268
+ Path (replace_whitespace (self .name , '_' )))
190
269
191
270
# Remove the tempdir if it exists. This is needed for shutil.copytree
192
271
# to work properly.
193
- if self . tempdir .exists ():
272
+ if tempdir .exists ():
194
273
warnings .warn (
195
- "'{0}' already exists. Deleting ..." .format (self . tempdir ))
196
- shutil .rmtree (str (self . tempdir ))
274
+ "'{0}' already exists. Deleting ..." .format (tempdir ))
275
+ shutil .rmtree (str (tempdir ))
197
276
198
277
# Copy the project directory to the temporary directory using pytest's
199
278
# rootdir.
200
- shutil .copytree (str (self .config .rootdir ), str (self . tempdir ))
279
+ shutil .copytree (str (self .config .rootdir ), str (tempdir ))
201
280
202
281
# Create a workflow and make sure it runs in the tempdir
203
282
workflow = Workflow (command = self .workflow_test .command ,
204
- cwd = self . tempdir ,
283
+ cwd = tempdir ,
205
284
name = self .workflow_test .name )
206
285
207
286
# Add the workflow to the workflow queue.
208
287
self .config .workflow_queue .put (workflow )
209
288
289
+ # Add the tempdir to the removal queue. We do not use a teardown method
290
+ # because this will remove the tempdir right after all the tests from
291
+ # this node have finished. If custom tests are defined this should not
292
+ # happen. The removal queue is processed just before pytest finishes
293
+ # and all tests have run.
294
+ self .config .workflow_cleanup_dirs .append (tempdir )
210
295
return workflow
211
296
212
297
def collect (self ):
@@ -222,6 +307,9 @@ def collect(self):
222
307
if not (set (self .config .getoption ("workflow_tags" )
223
308
).issubset (set (self .tags ))):
224
309
return []
310
+ else :
311
+ # If we run the workflow, save this for reference later.
312
+ self .config .executed_workflows .append (self .workflow_test .name )
225
313
226
314
# This creates a workflow that is queued for processing after the
227
315
# collection phase.
@@ -253,13 +341,6 @@ def collect(self):
253
341
254
342
return tests
255
343
256
- def teardown (self ):
257
- """This function is executed after all tests from this collector have
258
- finished. It is used to cleanup the tempdir."""
259
- if (not self .config .getoption ("keep_workflow_wd" )
260
- and self .tempdir is not None ):
261
- shutil .rmtree (str (self .tempdir ))
262
-
263
344
264
345
class ExitCodeTest (pytest .Item ):
265
346
def __init__ (self , parent : pytest .Collector ,
0 commit comments