Skip to content

Commit 40fcefb

Browse files
authored
Merge pull request #43 from ma-sadeghi/fix_ipynb_bug_redo
Fix ipynb bug [REDO]
2 parents 30a4f2a + 57af61c commit 40fcefb

File tree

5 files changed

+155
-0
lines changed

5 files changed

+155
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77
### Added
88
- PR template
99
- Test against 3.10
10+
- Compatibility with IPython Notebooks
1011

1112
## [0.5.0] - 2021-11-09
1213
### Added

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ Lists the slowest tests based on the information stored in the test durations fi
6161
## Interactions with other pytest plugins
6262
* [`pytest-random-order`](https://github.com/jbasko/pytest-random-order): ⚠️ The **default settings** of that plugin (setting only `--random-order` to activate it) are **incompatible** with `pytest-split`. Test selection in the groups happens after randomization, potentially causing some tests to be selected in several groups and others not at all. Instead, a global random seed needs to be computed before running the tests (for example using `$RANDOM` from the shell) and that single seed then needs to be used for all groups by setting the `--random-order-seed` option.
6363

64+
* [`nbval`](https://github.com/computationalmodelling/nbval): `pytest-split` could, in principle, break up a single IPython Notebook into different test groups. This most likely causes broken up pieces to fail (for the very least, package `import`s are usually done at Cell 1, and so, any broken up piece that doesn't contain Cell 1 will certainly fail). To avoid this, after splitting step is done, test groups are reorganized based on a simple algorithm illustrated in the following cartoon:
65+
66+
![image](https://user-images.githubusercontent.com/14086031/145830494-07afcaf0-5a0f-4817-b9ee-f84a459652a8.png)
67+
68+
where the letters (A to E) refer to individual IPython Notebooks, and the numbers refer to the corresponding cell number.
69+
6470
## Splitting algorithms
6571
The plugin supports multiple algorithms to split tests into groups.
6672
Each algorithm makes different tradeoffs, but generally `least_duration` should give more balanced groups.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from typing import TYPE_CHECKING
2+
3+
if TYPE_CHECKING:
4+
from typing import List
5+
6+
from pytest_split.algorithms import TestGroup
7+
8+
9+
def ensure_ipynb_compatibility(group: "TestGroup", items: list) -> None:
10+
"""
11+
Ensures that group doesn't contain partial IPy notebook cells.
12+
13+
``pytest-split`` might, in principle, break up the cells of an
14+
IPython notebook into different test groups, in which case the tests
15+
most likely fail (for starters, libraries are imported in Cell 0, so
16+
all subsequent calls to the imported libraries in the following cells
17+
will raise ``NameError``).
18+
19+
"""
20+
if not group.selected or not _is_ipy_notebook(group.selected[0].nodeid):
21+
return
22+
23+
item_node_ids = [item.nodeid for item in items]
24+
25+
# Deal with broken up notebooks at the beginning of the test group
26+
first = group.selected[0].nodeid
27+
siblings = _find_sibiling_ipynb_cells(first, item_node_ids)
28+
if first != siblings[0]:
29+
for item in list(group.selected):
30+
if item.nodeid in siblings:
31+
group.deselected.append(item)
32+
group.selected.remove(item)
33+
34+
if not group.selected or not _is_ipy_notebook(group.selected[-1].nodeid):
35+
return
36+
37+
# Deal with broken up notebooks at the end of the test group
38+
last = group.selected[-1].nodeid
39+
siblings = _find_sibiling_ipynb_cells(last, item_node_ids)
40+
if last != siblings[-1]:
41+
for item in list(group.deselected):
42+
if item.nodeid in siblings:
43+
group.deselected.remove(item)
44+
group.selected.append(item)
45+
46+
47+
def _find_sibiling_ipynb_cells(
48+
ipynb_node_id: str, item_node_ids: "List[str]"
49+
) -> "List[str]":
50+
"""
51+
Returns all sibiling IPyNb cells given an IPyNb cell nodeid.
52+
"""
53+
fpath = ipynb_node_id.split("::")[0]
54+
return [item for item in item_node_ids if fpath in item]
55+
56+
57+
def _is_ipy_notebook(node_id: str) -> bool:
58+
"""
59+
Returns True if node_id is an IPython notebook, otherwise False.
60+
"""
61+
fpath = node_id.split("::")[0]
62+
return fpath.endswith(".ipynb")

src/pytest_split/plugin.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from _pytest.reports import TestReport
88

99
from pytest_split import algorithms
10+
from pytest_split.ipynb_compatibility import ensure_ipynb_compatibility
1011

1112
if TYPE_CHECKING:
1213
from typing import Dict, List, Optional, Union
@@ -16,6 +17,7 @@
1617
from _pytest.config.argparsing import Parser
1718
from _pytest.main import ExitCode
1819

20+
1921
# Ugly hack for freezegun compatibility: https://github.com/spulec/freezegun/issues/286
2022
STORE_DURATIONS_SETUP_AND_TEARDOWN_THRESHOLD = 60 * 10 # seconds
2123

@@ -165,6 +167,8 @@ def pytest_collection_modifyitems(
165167
groups = algo(splits, items, self.cached_durations)
166168
group = groups[group_idx - 1]
167169

170+
ensure_ipynb_compatibility(group, items)
171+
168172
items[:] = group.selected
169173
config.hook.pytest_deselected(items=group.deselected)
170174

tests/test_ipynb.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from collections import namedtuple
2+
3+
import pytest
4+
5+
from pytest_split.algorithms import Algorithms
6+
from pytest_split.ipynb_compatibility import ensure_ipynb_compatibility
7+
8+
item = namedtuple("item", "nodeid")
9+
10+
11+
class TestIPyNb:
12+
@pytest.mark.parametrize("algo_name", ["duration_based_chunks"])
13+
def test_ensure_ipynb_compatibility(self, algo_name):
14+
durations = {
15+
"temp/nbs/test_1.ipynb::Cell 0": 1,
16+
"temp/nbs/test_1.ipynb::Cell 1": 1,
17+
"temp/nbs/test_1.ipynb::Cell 2": 1,
18+
"temp/nbs/test_2.ipynb::Cell 0": 3,
19+
"temp/nbs/test_2.ipynb::Cell 1": 5,
20+
"temp/nbs/test_2.ipynb::Cell 2": 1,
21+
"temp/nbs/test_2.ipynb::Cell 3": 4,
22+
"temp/nbs/test_3.ipynb::Cell 0": 5,
23+
"temp/nbs/test_3.ipynb::Cell 1": 1,
24+
"temp/nbs/test_3.ipynb::Cell 2": 1,
25+
"temp/nbs/test_3.ipynb::Cell 3": 2,
26+
"temp/nbs/test_3.ipynb::Cell 4": 1,
27+
"temp/nbs/test_4.ipynb::Cell 0": 1,
28+
"temp/nbs/test_4.ipynb::Cell 1": 1,
29+
"temp/nbs/test_4.ipynb::Cell 2": 3,
30+
}
31+
items = [item(x) for x in durations.keys()]
32+
algo = Algorithms[algo_name].value
33+
groups = algo(splits=3, items=items, durations=durations)
34+
35+
assert groups[0].selected == [
36+
item(nodeid="temp/nbs/test_1.ipynb::Cell 0"),
37+
item(nodeid="temp/nbs/test_1.ipynb::Cell 1"),
38+
item(nodeid="temp/nbs/test_1.ipynb::Cell 2"),
39+
item(nodeid="temp/nbs/test_2.ipynb::Cell 0"),
40+
item(nodeid="temp/nbs/test_2.ipynb::Cell 1"),
41+
]
42+
assert groups[1].selected == [
43+
item(nodeid="temp/nbs/test_2.ipynb::Cell 2"),
44+
item(nodeid="temp/nbs/test_2.ipynb::Cell 3"),
45+
item(nodeid="temp/nbs/test_3.ipynb::Cell 0"),
46+
item(nodeid="temp/nbs/test_3.ipynb::Cell 1"),
47+
]
48+
assert groups[2].selected == [
49+
item(nodeid="temp/nbs/test_3.ipynb::Cell 2"),
50+
item(nodeid="temp/nbs/test_3.ipynb::Cell 3"),
51+
item(nodeid="temp/nbs/test_3.ipynb::Cell 4"),
52+
item(nodeid="temp/nbs/test_4.ipynb::Cell 0"),
53+
item(nodeid="temp/nbs/test_4.ipynb::Cell 1"),
54+
item(nodeid="temp/nbs/test_4.ipynb::Cell 2"),
55+
]
56+
57+
ensure_ipynb_compatibility(groups[0], items)
58+
assert groups[0].selected == [
59+
item(nodeid="temp/nbs/test_1.ipynb::Cell 0"),
60+
item(nodeid="temp/nbs/test_1.ipynb::Cell 1"),
61+
item(nodeid="temp/nbs/test_1.ipynb::Cell 2"),
62+
item(nodeid="temp/nbs/test_2.ipynb::Cell 0"),
63+
item(nodeid="temp/nbs/test_2.ipynb::Cell 1"),
64+
item(nodeid="temp/nbs/test_2.ipynb::Cell 2"),
65+
item(nodeid="temp/nbs/test_2.ipynb::Cell 3"),
66+
]
67+
68+
ensure_ipynb_compatibility(groups[1], items)
69+
assert groups[1].selected == [
70+
item(nodeid="temp/nbs/test_3.ipynb::Cell 0"),
71+
item(nodeid="temp/nbs/test_3.ipynb::Cell 1"),
72+
item(nodeid="temp/nbs/test_3.ipynb::Cell 2"),
73+
item(nodeid="temp/nbs/test_3.ipynb::Cell 3"),
74+
item(nodeid="temp/nbs/test_3.ipynb::Cell 4"),
75+
]
76+
77+
ensure_ipynb_compatibility(groups[2], items)
78+
assert groups[2].selected == [
79+
item(nodeid="temp/nbs/test_4.ipynb::Cell 0"),
80+
item(nodeid="temp/nbs/test_4.ipynb::Cell 1"),
81+
item(nodeid="temp/nbs/test_4.ipynb::Cell 2"),
82+
]

0 commit comments

Comments
 (0)