|
10 | 10 | import pytest |
11 | 11 |
|
12 | 12 | _implicit_markers = {'default'} |
13 | | -_extended_implicit_markers = _implicit_markers.union({'solver'}) |
| 13 | +_category_markers = {'solver', 'writer'} |
| 14 | +_extended_implicit_markers = _implicit_markers.union(_category_markers) |
14 | 15 |
|
15 | 16 |
|
16 | | -def pytest_collection_modifyitems(items): |
17 | | - """ |
18 | | - This method will mark any unmarked tests with the implicit marker ('default') |
| 17 | +def pytest_configure(config): |
| 18 | + # If the user specified "--solver" or "--writer", then add that |
| 19 | + # logic to the marker expression |
| 20 | + markexpr = config.option.markexpr |
| 21 | + for cat in _category_markers: |
| 22 | + opt = config.getoption('--' + cat) |
| 23 | + if opt: |
| 24 | + if markexpr: |
| 25 | + markexpr = f"({markexpr}) and " |
| 26 | + markexpr += f"{cat}(id='{opt}')" |
| 27 | + # If the user didn't specify a marker expression, then we will |
| 28 | + # select all "default" tests. |
| 29 | + if not markexpr: |
| 30 | + markexpr = 'default' |
| 31 | + config.option.markexpr = markexpr |
19 | 32 |
|
20 | | - """ |
21 | | - for item in items: |
22 | | - try: |
23 | | - next(item.iter_markers()) |
24 | | - except StopIteration: |
25 | | - for marker in _implicit_markers: |
26 | | - item.add_marker(getattr(pytest.mark, marker)) |
27 | 33 |
|
| 34 | +def pytest_itemcollected(item): |
| 35 | + """Standardize all Pyomo test markers. |
| 36 | +
|
| 37 | + This callback ensures that all unmarked tests, along with all tests |
| 38 | + that are only marked by category markers (e.g., "solver" or |
| 39 | + "writer"), are also marked with the default (implicit) markers |
| 40 | + (currently just "default"). |
| 41 | +
|
| 42 | + About category markers |
| 43 | + ---------------------- |
| 44 | +
|
| 45 | + We have historically supported "category markers":: |
| 46 | +
|
| 47 | + @pytest.mark.solver("highs") |
| 48 | +
|
| 49 | + Unfortunately, pytest doesn't allow for building marker |
| 50 | + expressions (e.g., for "-m") based on the marker.args. We will |
| 51 | + map the positional argument (for pytest.mark.solver and |
| 52 | + pytest.mark.writer) to the keyword argument "id". This will allow |
| 53 | + querying against specific solver interfaces in marker expressions |
| 54 | + with:: |
| 55 | +
|
| 56 | + solver(id='highs') |
| 57 | +
|
| 58 | + We will take this opportunity to also set a keyword argument for |
| 59 | + the solver/writer "vendor" (defined as the id up to the first |
| 60 | + underscore). This will allow running "all Gurobi tests" |
| 61 | + (including, e.g., lp, direct, and persistent) with:: |
| 62 | +
|
| 63 | + -m solver(vendor='gurobi') |
| 64 | +
|
| 65 | + As with all pytest markers, these can be combined into more complex |
| 66 | + "marker expressions" using ``and``, ``or``, ``not``, and ``()``. |
28 | 67 |
|
29 | | -def pytest_runtest_setup(item): |
30 | | - """ |
31 | | - This method overrides pytest's default behavior for marked tests. |
32 | | -
|
33 | | - The logic below follows this flow: |
34 | | - 1) Did the user ask for a specific solver using the '--solver' flag? |
35 | | - If so: Add skip statements to any test NOT labeled with the |
36 | | - requested solver category. |
37 | | - 2) Did the user ask for a specific marker using the '-m' flag? |
38 | | - If so: Return to pytest's default behavior. |
39 | | - 3) If the user requested no specific solver or marker, look at each |
40 | | - test for the following: |
41 | | - a) If unmarked, run the test |
42 | | - b) If marked with implicit_markers, run the test |
43 | | - c) If marked "solver" and NOT any explicit marker, run the test |
44 | | - OTHERWISE: Skip the test. |
45 | | - In other words - we want to run unmarked, implicit, and solver tests as |
46 | | - the default mode; but if solver tests are also marked with an explicit |
47 | | - category (e.g., "expensive"), we will skip them. |
48 | 68 | """ |
49 | | - solvernames = [mark.args[0] for mark in item.iter_markers(name="solver")] |
50 | | - solveroption = item.config.getoption("--solver") |
51 | | - markeroption = item.config.getoption("-m") |
52 | | - item_markers = set(mark.name for mark in item.iter_markers()) |
53 | | - if solveroption: |
54 | | - if solveroption not in solvernames: |
55 | | - pytest.skip("SKIPPED: Test not marked {!r}".format(solveroption)) |
56 | | - return |
57 | | - elif markeroption: |
| 69 | + markers = list(item.iter_markers()) |
| 70 | + if not markers: |
| 71 | + # No markers; add the implicit (default) markers |
| 72 | + for marker in _implicit_markers: |
| 73 | + item.add_marker(getattr(pytest.mark, marker)) |
58 | 74 | return |
59 | | - elif item_markers: |
60 | | - if not _implicit_markers.issubset(item_markers) and not item_markers.issubset( |
61 | | - _extended_implicit_markers |
62 | | - ): |
63 | | - pytest.skip('SKIPPED: Only running default, solver, and unmarked tests.') |
| 75 | + |
| 76 | + marker_set = {mark.name for mark in markers} |
| 77 | + # If the item is only marked by extended implicit markers (e.g., |
| 78 | + # solver and/or writer), then make sure it is also marked by all |
| 79 | + # implicit markers (i.e., "default") |
| 80 | + if marker_set.issubset(_extended_implicit_markers): |
| 81 | + for marker in _implicit_markers - marker_set: |
| 82 | + item.add_marker(getattr(pytest.mark, marker)) |
| 83 | + |
| 84 | + # Map any "category" markers (solver or writer) positional arguments |
| 85 | + # to the id keyword, and ensure the 'vendor' keyword is populated |
| 86 | + for mark in markers: |
| 87 | + if mark.name not in _category_markers: |
| 88 | + continue |
| 89 | + if mark.args: |
| 90 | + (_id,) = mark.args |
| 91 | + mark.kwargs['id'] = _id |
| 92 | + if 'vendor' not in mark.kwargs: |
| 93 | + mark.kwargs['vendor'] = mark.kwargs['id'].split("_")[0] |
64 | 94 |
|
65 | 95 |
|
66 | 96 | def pytest_addoption(parser): |
67 | 97 | """ |
68 | | - Add another parser option to specify suite of solver tests to run |
| 98 | + Add parser options as shorthand for running tests marked by specific |
| 99 | + solvers or writers. |
69 | 100 | """ |
70 | 101 | parser.addoption( |
71 | 102 | "--solver", |
72 | 103 | action="store", |
73 | 104 | metavar="SOLVER", |
74 | 105 | help="Run tests matching the requested SOLVER.", |
75 | 106 | ) |
76 | | - |
77 | | - |
78 | | -def pytest_configure(config): |
79 | | - """ |
80 | | - Register additional solver marker, as applicable. |
81 | | - This stops pytest from printing a warning about unregistered solver options. |
82 | | - """ |
83 | | - config.addinivalue_line( |
84 | | - "markers", "solver(name): mark test to run the named solver" |
| 107 | + parser.addoption( |
| 108 | + "--writer", |
| 109 | + action="store", |
| 110 | + metavar="WRITER", |
| 111 | + help="Run tests matching the requested WRITER.", |
85 | 112 | ) |
0 commit comments