pytest-split 0.8.2__tar.gz → 0.10.0__tar.gz

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.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2021 Jerry Pussinen
1
+ Copyright (c) 2024 Jerry Pussinen
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -1,25 +1,25 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytest-split
3
- Version: 0.8.2
3
+ Version: 0.10.0
4
4
  Summary: Pytest plugin which splits the test suite to equally sized sub suites based on test execution time.
5
5
  Home-page: https://jerry-git.github.io/pytest-split
6
6
  License: MIT
7
7
  Keywords: pytest,plugin,split,tests
8
8
  Author: Jerry Pussinen
9
9
  Author-email: jerry.pussinen@gmail.com
10
- Requires-Python: >=3.7.1,<4.0
10
+ Requires-Python: >=3.8.1,<4.0
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.8
18
17
  Classifier: Programming Language :: Python :: 3.9
19
18
  Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
22
- Classifier: Programming Language :: Python :: 3.7
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.8
23
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
24
  Classifier: Typing :: Typed
25
25
  Requires-Dist: pytest (>=5,<9)
@@ -125,7 +125,7 @@ The `least_duration` algorithm walks the list of tests and assigns each test to
125
125
  * Clone this repository
126
126
  * Requirements:
127
127
  * [Poetry](https://python-poetry.org/)
128
- * Python 3.7+
128
+ * Python 3.8+
129
129
  * Create a virtual environment and install the dependencies
130
130
 
131
131
  ```sh
@@ -146,9 +146,8 @@ pytest
146
146
 
147
147
  ### Documentation
148
148
 
149
- The documentation is automatically generated from the content of the [docs directory](./docs) and from the docstrings
150
- of the public signatures of the source code. The documentation is updated and published as a [Github project page
151
- ](https://pages.github.com/) automatically as part each release.
149
+ The documentation is automatically generated from the content of the [docs directory](https://github.com/jerry-git/pytest-split/tree/master/docs) and from the docstrings
150
+ of the public signatures of the source code. The documentation is updated and published as a [Github Pages page](https://pages.github.com/) automatically as part each release.
152
151
 
153
152
  ### Releasing
154
153
 
@@ -162,7 +161,7 @@ Find the draft release from the
162
161
 
163
162
  ### Pre-commit
164
163
 
165
- Pre-commit hooks run all the auto-formatters (e.g. `black`), linters (e.g. `mypy`, `ruff`), and other quality
164
+ Pre-commit hooks run all the auto-formatting (`ruff format`), linters (e.g. `ruff` and `mypy`), and other quality
166
165
  checks to make sure the changeset is in good shape before a commit/push happens.
167
166
 
168
167
  You can install the hooks with (runs for each commit):
@@ -96,7 +96,7 @@ The `least_duration` algorithm walks the list of tests and assigns each test to
96
96
  * Clone this repository
97
97
  * Requirements:
98
98
  * [Poetry](https://python-poetry.org/)
99
- * Python 3.7+
99
+ * Python 3.8+
100
100
  * Create a virtual environment and install the dependencies
101
101
 
102
102
  ```sh
@@ -117,9 +117,8 @@ pytest
117
117
 
118
118
  ### Documentation
119
119
 
120
- The documentation is automatically generated from the content of the [docs directory](./docs) and from the docstrings
121
- of the public signatures of the source code. The documentation is updated and published as a [Github project page
122
- ](https://pages.github.com/) automatically as part each release.
120
+ The documentation is automatically generated from the content of the [docs directory](https://github.com/jerry-git/pytest-split/tree/master/docs) and from the docstrings
121
+ of the public signatures of the source code. The documentation is updated and published as a [Github Pages page](https://pages.github.com/) automatically as part each release.
123
122
 
124
123
  ### Releasing
125
124
 
@@ -133,7 +132,7 @@ Find the draft release from the
133
132
 
134
133
  ### Pre-commit
135
134
 
136
- Pre-commit hooks run all the auto-formatters (e.g. `black`), linters (e.g. `mypy`, `ruff`), and other quality
135
+ Pre-commit hooks run all the auto-formatting (`ruff format`), linters (e.g. `ruff` and `mypy`), and other quality
137
136
  checks to make sure the changeset is in good shape before a commit/push happens.
138
137
 
139
138
  You can install the hooks with (runs for each commit):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pytest-split"
3
- version = "0.8.2"
3
+ version = "0.10.0"
4
4
  description = "Pytest plugin which splits the test suite to equally sized sub suites based on test execution time."
5
5
  authors = [
6
6
  "Jerry Pussinen <jerry.pussinen@gmail.com>",
@@ -18,27 +18,26 @@ classifiers = [
18
18
  "Operating System :: OS Independent",
19
19
  "Programming Language :: Python",
20
20
  "Programming Language :: Python :: 3",
21
- "Programming Language :: Python :: 3.7",
22
21
  "Programming Language :: Python :: 3.8",
23
22
  "Programming Language :: Python :: 3.9",
24
23
  "Programming Language :: Python :: 3.10",
25
24
  "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
26
27
  "Topic :: Software Development :: Libraries :: Python Modules",
27
28
  "Typing :: Typed",
28
29
  ]
29
30
 
30
31
  keywords = ['pytest', 'plugin', 'split', 'tests']
31
32
  packages = [{ include = 'pytest_split', from = 'src' }]
32
- include = ['LICENCE']
33
33
 
34
34
 
35
35
  [tool.poetry.dependencies]
36
- python = ">=3.7.1, <4.0"
36
+ python = ">=3.8.1, <4.0"
37
37
  pytest = "^5 | ^6 | ^7 | ^8"
38
38
 
39
39
 
40
40
  [tool.poetry.dev-dependencies]
41
- black = "*"
42
41
  importlib-metadata = "==4.11.*"
43
42
  mkdocstrings = {version = ">=0.18", extras = ["python"]}
44
43
  mkdocs-material = "*"
@@ -49,7 +48,7 @@ pytest = "*"
49
48
  pytest-github-actions-annotate-failures = "*"
50
49
  pytest-cov = "*"
51
50
  python-kacl = "*"
52
- ruff = "*"
51
+ ruff = ">=0.2.0"
53
52
 
54
53
  [build-system]
55
54
  requires = ["poetry-core>=1.0.0"]
@@ -81,22 +80,26 @@ exclude_lines = [
81
80
  ]
82
81
 
83
82
  [tool.mypy]
83
+ # This is the global mypy configuration.
84
+ # Avoid changing this!
85
+ strict = true # See all the enabled flags `mypy --help | grep -A 10 'Strict mode'`
84
86
  disallow_any_unimported = true
85
- disallow_untyped_defs = true
86
- no_implicit_optional = true
87
- strict_equality = true
88
- warn_unused_ignores = true
89
- warn_redundant_casts = true
90
- warn_return_any = true
91
- check_untyped_defs = true
92
- show_error_codes = true
93
87
 
94
88
  [[tool.mypy.overrides]]
95
89
  module = ['tests.*']
96
90
  disallow_untyped_defs = false
97
91
  strict_equality = false
92
+ disallow_untyped_calls = false
93
+
98
94
 
99
95
  [tool.ruff]
96
+ target-version = "py38" # The lowest supported version
97
+
98
+ [tool.ruff.lint]
99
+ # By default, enable all the lint rules.
100
+ # Add to the ignore list below if you don't want some rules.
101
+ # If you need some ignores for certain modules, see tool.ruff.lint.per-file-ignores below.
102
+ # For individual ignore cases, prefer inline `# noqa`s within the code.
100
103
  select = ["ALL"]
101
104
  ignore = [
102
105
  "ANN", # Type hints related, let mypy handle these.
@@ -112,5 +115,20 @@ ignore = [
112
115
  "TRY003", # "Avoid specifying long messages outside the exception class"
113
116
  "FA100", # "Missing `from __future__ import annotations` ..." pytest-split supports a range of Python versions
114
117
  ]
115
- line-length = 88
116
- target-version = "py37"
118
+
119
+ [tool.ruff.lint.per-file-ignores]
120
+ "tests/**" = [
121
+ "S101", # "Use of `assert` detected"
122
+ "ARG", # "Unused function argument". Fixtures are often unused.
123
+ "S105", # "Possible hardcoded password".
124
+ ]
125
+
126
+ [tool.ruff.lint.mccabe]
127
+ max-complexity = 10
128
+
129
+ [tool.ruff.lint.pep8-naming]
130
+ classmethod-decorators = [
131
+ "classmethod",
132
+ "pydantic.validator",
133
+ "pydantic.root_validator",
134
+ ]
@@ -0,0 +1,188 @@
1
+ import enum
2
+ import heapq
3
+ from abc import ABC, abstractmethod
4
+ from operator import itemgetter
5
+ from typing import TYPE_CHECKING, NamedTuple
6
+
7
+ if TYPE_CHECKING:
8
+ from typing import Dict, List, Tuple
9
+
10
+ from _pytest import nodes
11
+
12
+
13
+ class TestGroup(NamedTuple):
14
+ selected: "List[nodes.Item]"
15
+ deselected: "List[nodes.Item]"
16
+ duration: float
17
+
18
+
19
+ class AlgorithmBase(ABC):
20
+ """Abstract base class for the algorithm implementations."""
21
+
22
+ @abstractmethod
23
+ def __call__(
24
+ self, splits: int, items: "List[nodes.Item]", durations: "Dict[str, float]"
25
+ ) -> "List[TestGroup]":
26
+ pass
27
+
28
+ def __hash__(self) -> int:
29
+ return hash(self.__class__.__name__)
30
+
31
+ def __eq__(self, other: object) -> bool:
32
+ if not isinstance(other, AlgorithmBase):
33
+ return NotImplemented
34
+ return self.__class__.__name__ == other.__class__.__name__
35
+
36
+
37
+ class LeastDurationAlgorithm(AlgorithmBase):
38
+ """
39
+ Split tests into groups by runtime.
40
+ It walks the test items, starting with the test with largest duration.
41
+ It assigns the test with the largest runtime to the group with the smallest duration sum.
42
+
43
+ The algorithm sorts the items by their duration. Since the sorting algorithm is stable, ties will be broken by
44
+ maintaining the original order of items. It is therefore important that the order of items be identical on all nodes
45
+ that use this plugin. Due to issue #25 this might not always be the case.
46
+
47
+ :param splits: How many groups we're splitting in.
48
+ :param items: Test items passed down by Pytest.
49
+ :param durations: Our cached test runtimes. Assumes contains timings only of relevant tests
50
+ :return:
51
+ List of groups
52
+ """
53
+
54
+ def __call__(
55
+ self, splits: int, items: "List[nodes.Item]", durations: "Dict[str, float]"
56
+ ) -> "List[TestGroup]":
57
+ items_with_durations = _get_items_with_durations(items, durations)
58
+
59
+ # add index of item in list
60
+ items_with_durations_indexed = [
61
+ (*tup, i) for i, tup in enumerate(items_with_durations)
62
+ ]
63
+
64
+ # Sort by name to ensure it's always the same order
65
+ items_with_durations_indexed = sorted(
66
+ items_with_durations_indexed, key=lambda tup: str(tup[0])
67
+ )
68
+
69
+ # sort in ascending order
70
+ sorted_items_with_durations = sorted(
71
+ items_with_durations_indexed, key=lambda tup: tup[1], reverse=True
72
+ )
73
+
74
+ selected: List[List[Tuple[nodes.Item, int]]] = [[] for _ in range(splits)]
75
+ deselected: List[List[nodes.Item]] = [[] for _ in range(splits)]
76
+ duration: List[float] = [0 for _ in range(splits)]
77
+
78
+ # create a heap of the form (summed_durations, group_index)
79
+ heap: List[Tuple[float, int]] = [(0, i) for i in range(splits)]
80
+ heapq.heapify(heap)
81
+ for item, item_duration, original_index in sorted_items_with_durations:
82
+ # get group with smallest sum
83
+ summed_durations, group_idx = heapq.heappop(heap)
84
+ new_group_durations = summed_durations + item_duration
85
+
86
+ # store assignment
87
+ selected[group_idx].append((item, original_index))
88
+ duration[group_idx] = new_group_durations
89
+ for i in range(splits):
90
+ if i != group_idx:
91
+ deselected[i].append(item)
92
+
93
+ # store new duration - in case of ties it sorts by the group_idx
94
+ heapq.heappush(heap, (new_group_durations, group_idx))
95
+
96
+ groups = []
97
+ for i in range(splits):
98
+ # sort the items by their original index to maintain relative ordering
99
+ # we don't care about the order of deselected items
100
+ s = [
101
+ item
102
+ for item, original_index in sorted(selected[i], key=lambda tup: tup[1])
103
+ ]
104
+ group = TestGroup(
105
+ selected=s, deselected=deselected[i], duration=duration[i]
106
+ )
107
+ groups.append(group)
108
+ return groups
109
+
110
+
111
+ class DurationBasedChunksAlgorithm(AlgorithmBase):
112
+ """
113
+ Split tests into groups by runtime.
114
+ Ensures tests are split into non-overlapping groups.
115
+ The original list of test items is split into groups by finding boundary indices i_0, i_1, i_2
116
+ and creating group_1 = items[0:i_0], group_2 = items[i_0, i_1], group_3 = items[i_1, i_2], ...
117
+
118
+ :param splits: How many groups we're splitting in.
119
+ :param items: Test items passed down by Pytest.
120
+ :param durations: Our cached test runtimes. Assumes contains timings only of relevant tests
121
+ :return: List of TestGroup
122
+ """
123
+
124
+ def __call__(
125
+ self, splits: int, items: "List[nodes.Item]", durations: "Dict[str, float]"
126
+ ) -> "List[TestGroup]":
127
+ items_with_durations = _get_items_with_durations(items, durations)
128
+ time_per_group = sum(map(itemgetter(1), items_with_durations)) / splits
129
+
130
+ selected: List[List[nodes.Item]] = [[] for i in range(splits)]
131
+ deselected: List[List[nodes.Item]] = [[] for i in range(splits)]
132
+ duration: List[float] = [0 for i in range(splits)]
133
+
134
+ group_idx = 0
135
+ for item, item_duration in items_with_durations:
136
+ if duration[group_idx] >= time_per_group:
137
+ group_idx += 1
138
+
139
+ selected[group_idx].append(item)
140
+ for i in range(splits):
141
+ if i != group_idx:
142
+ deselected[i].append(item)
143
+ duration[group_idx] += item_duration
144
+
145
+ return [
146
+ TestGroup(
147
+ selected=selected[i], deselected=deselected[i], duration=duration[i]
148
+ )
149
+ for i in range(splits)
150
+ ]
151
+
152
+
153
+ def _get_items_with_durations(
154
+ items: "List[nodes.Item]", durations: "Dict[str, float]"
155
+ ) -> "List[Tuple[nodes.Item, float]]":
156
+ durations = _remove_irrelevant_durations(items, durations)
157
+ avg_duration_per_test = _get_avg_duration_per_test(durations)
158
+ items_with_durations = [
159
+ (item, durations.get(item.nodeid, avg_duration_per_test)) for item in items
160
+ ]
161
+ return items_with_durations
162
+
163
+
164
+ def _get_avg_duration_per_test(durations: "Dict[str, float]") -> float:
165
+ if durations:
166
+ avg_duration_per_test = sum(durations.values()) / len(durations)
167
+ else:
168
+ # If there are no durations, give every test the same arbitrary value
169
+ avg_duration_per_test = 1
170
+ return avg_duration_per_test
171
+
172
+
173
+ def _remove_irrelevant_durations(
174
+ items: "List[nodes.Item]", durations: "Dict[str, float]"
175
+ ) -> "Dict[str, float]":
176
+ # Filtering down durations to relevant ones ensures the avg isn't skewed by irrelevant data
177
+ test_ids = [item.nodeid for item in items]
178
+ durations = {name: durations[name] for name in test_ids if name in durations}
179
+ return durations
180
+
181
+
182
+ class Algorithms(enum.Enum):
183
+ duration_based_chunks = DurationBasedChunksAlgorithm()
184
+ least_duration = LeastDurationAlgorithm()
185
+
186
+ @staticmethod
187
+ def names() -> "List[str]":
188
+ return [x.name for x in Algorithms]
@@ -6,7 +6,7 @@ if TYPE_CHECKING:
6
6
  from pytest_split.algorithms import TestGroup
7
7
 
8
8
 
9
- def ensure_ipynb_compatibility(group: "TestGroup", items: list) -> None:
9
+ def ensure_ipynb_compatibility(group: "TestGroup", items: list) -> None: # type: ignore[type-arg]
10
10
  """
11
11
  Ensures that group doesn't contain partial IPy notebook cells.
12
12
 
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15
15
  from _pytest import nodes
16
16
  from _pytest.config import Config
17
17
  from _pytest.config.argparsing import Parser
18
- from _pytest.main import ExitCode
18
+ from _pytest.main import ExitCode # type: ignore[attr-defined]
19
19
 
20
20
 
21
21
  # Ugly hack for freezegun compatibility: https://github.com/spulec/freezegun/issues/286
@@ -193,9 +193,9 @@ class PytestSplitCachePlugin(Base):
193
193
  https://github.com/pytest-dev/pytest/blob/main/src/_pytest/main.py#L308
194
194
  """
195
195
  terminal_reporter = self.config.pluginmanager.get_plugin("terminalreporter")
196
- test_durations: "Dict[str, float]" = {}
196
+ test_durations: Dict[str, float] = {}
197
197
 
198
- for test_reports in terminal_reporter.stats.values():
198
+ for test_reports in terminal_reporter.stats.values(): # type: ignore[union-attr]
199
199
  for test_report in test_reports:
200
200
  if isinstance(test_report, TestReport):
201
201
  # These ifs be removed after this is solved: # https://github.com/spulec/freezegun/issues/286
@@ -224,8 +224,6 @@ class PytestSplitCachePlugin(Base):
224
224
  json.dump(self.cached_durations, f, sort_keys=True, indent=4)
225
225
 
226
226
  message = self.writer.markup(
227
- "\n\n[pytest-split] Stored test durations in {}".format(
228
- self.config.option.durations_path
229
- )
227
+ f"\n\n[pytest-split] Stored test durations in {self.config.option.durations_path}"
230
228
  )
231
229
  self.writer.line(message)
@@ -1,162 +0,0 @@
1
- import enum
2
- import functools
3
- import heapq
4
- from operator import itemgetter
5
- from typing import TYPE_CHECKING, NamedTuple
6
-
7
- if TYPE_CHECKING:
8
- from typing import Dict, List, Tuple
9
-
10
- from _pytest import nodes
11
-
12
-
13
- class TestGroup(NamedTuple):
14
- selected: "List[nodes.Item]"
15
- deselected: "List[nodes.Item]"
16
- duration: float
17
-
18
-
19
- def least_duration(
20
- splits: int, items: "List[nodes.Item]", durations: "Dict[str, float]"
21
- ) -> "List[TestGroup]":
22
- """
23
- Split tests into groups by runtime.
24
- It walks the test items, starting with the test with largest duration.
25
- It assigns the test with the largest runtime to the group with the smallest duration sum.
26
-
27
- The algorithm sorts the items by their duration. Since the sorting algorithm is stable, ties will be broken by
28
- maintaining the original order of items. It is therefore important that the order of items be identical on all nodes
29
- that use this plugin. Due to issue #25 this might not always be the case.
30
-
31
- :param splits: How many groups we're splitting in.
32
- :param items: Test items passed down by Pytest.
33
- :param durations: Our cached test runtimes. Assumes contains timings only of relevant tests
34
- :return:
35
- List of groups
36
- """
37
- items_with_durations = _get_items_with_durations(items, durations)
38
-
39
- # add index of item in list
40
- items_with_durations_indexed = [
41
- (*tup, i) for i, tup in enumerate(items_with_durations)
42
- ]
43
-
44
- # Sort by name to ensure it's always the same order
45
- items_with_durations_indexed = sorted(
46
- items_with_durations_indexed, key=lambda tup: str(tup[0])
47
- )
48
-
49
- # sort in ascending order
50
- sorted_items_with_durations = sorted(
51
- items_with_durations_indexed, key=lambda tup: tup[1], reverse=True
52
- )
53
-
54
- selected: "List[List[Tuple[nodes.Item, int]]]" = [[] for _ in range(splits)]
55
- deselected: "List[List[nodes.Item]]" = [[] for _ in range(splits)]
56
- duration: "List[float]" = [0 for _ in range(splits)]
57
-
58
- # create a heap of the form (summed_durations, group_index)
59
- heap: "List[Tuple[float, int]]" = [(0, i) for i in range(splits)]
60
- heapq.heapify(heap)
61
- for item, item_duration, original_index in sorted_items_with_durations:
62
- # get group with smallest sum
63
- summed_durations, group_idx = heapq.heappop(heap)
64
- new_group_durations = summed_durations + item_duration
65
-
66
- # store assignment
67
- selected[group_idx].append((item, original_index))
68
- duration[group_idx] = new_group_durations
69
- for i in range(splits):
70
- if i != group_idx:
71
- deselected[i].append(item)
72
-
73
- # store new duration - in case of ties it sorts by the group_idx
74
- heapq.heappush(heap, (new_group_durations, group_idx))
75
-
76
- groups = []
77
- for i in range(splits):
78
- # sort the items by their original index to maintain relative ordering
79
- # we don't care about the order of deselected items
80
- s = [
81
- item for item, original_index in sorted(selected[i], key=lambda tup: tup[1])
82
- ]
83
- group = TestGroup(selected=s, deselected=deselected[i], duration=duration[i])
84
- groups.append(group)
85
- return groups
86
-
87
-
88
- def duration_based_chunks(
89
- splits: int, items: "List[nodes.Item]", durations: "Dict[str, float]"
90
- ) -> "List[TestGroup]":
91
- """
92
- Split tests into groups by runtime.
93
- Ensures tests are split into non-overlapping groups.
94
- The original list of test items is split into groups by finding boundary indices i_0, i_1, i_2
95
- and creating group_1 = items[0:i_0], group_2 = items[i_0, i_1], group_3 = items[i_1, i_2], ...
96
-
97
- :param splits: How many groups we're splitting in.
98
- :param items: Test items passed down by Pytest.
99
- :param durations: Our cached test runtimes. Assumes contains timings only of relevant tests
100
- :return: List of TestGroup
101
- """
102
- items_with_durations = _get_items_with_durations(items, durations)
103
- time_per_group = sum(map(itemgetter(1), items_with_durations)) / splits
104
-
105
- selected: "List[List[nodes.Item]]" = [[] for i in range(splits)]
106
- deselected: "List[List[nodes.Item]]" = [[] for i in range(splits)]
107
- duration: "List[float]" = [0 for i in range(splits)]
108
-
109
- group_idx = 0
110
- for item, item_duration in items_with_durations:
111
- if duration[group_idx] >= time_per_group:
112
- group_idx += 1
113
-
114
- selected[group_idx].append(item)
115
- for i in range(splits):
116
- if i != group_idx:
117
- deselected[i].append(item)
118
- duration[group_idx] += item_duration
119
-
120
- return [
121
- TestGroup(selected=selected[i], deselected=deselected[i], duration=duration[i])
122
- for i in range(splits)
123
- ]
124
-
125
-
126
- def _get_items_with_durations(
127
- items: "List[nodes.Item]", durations: "Dict[str, float]"
128
- ) -> "List[Tuple[nodes.Item, float]]":
129
- durations = _remove_irrelevant_durations(items, durations)
130
- avg_duration_per_test = _get_avg_duration_per_test(durations)
131
- items_with_durations = [
132
- (item, durations.get(item.nodeid, avg_duration_per_test)) for item in items
133
- ]
134
- return items_with_durations
135
-
136
-
137
- def _get_avg_duration_per_test(durations: "Dict[str, float]") -> float:
138
- if durations:
139
- avg_duration_per_test = sum(durations.values()) / len(durations)
140
- else:
141
- # If there are no durations, give every test the same arbitrary value
142
- avg_duration_per_test = 1
143
- return avg_duration_per_test
144
-
145
-
146
- def _remove_irrelevant_durations(
147
- items: "List[nodes.Item]", durations: "Dict[str, float]"
148
- ) -> "Dict[str, float]":
149
- # Filtering down durations to relevant ones ensures the avg isn't skewed by irrelevant data
150
- test_ids = [item.nodeid for item in items]
151
- durations = {name: durations[name] for name in test_ids if name in durations}
152
- return durations
153
-
154
-
155
- class Algorithms(enum.Enum):
156
- # values have to wrapped inside functools to avoid them being considered method definitions
157
- duration_based_chunks = functools.partial(duration_based_chunks)
158
- least_duration = functools.partial(least_duration)
159
-
160
- @staticmethod
161
- def names() -> "List[str]":
162
- return [x.name for x in Algorithms]