pytest-flakefighters 0.2.3__py3-none-any.whl → 0.3.1__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.
- _version.py +2 -2
- pytest_flakefighters/config.py +90 -0
- pytest_flakefighters/database_management.py +68 -2
- pytest_flakefighters/flakefighters/deflaker.py +6 -7
- pytest_flakefighters/flakefighters/traceback_matching.py +8 -14
- pytest_flakefighters/main.py +75 -95
- pytest_flakefighters/plugin.py +191 -2
- {pytest_flakefighters-0.2.3.dist-info → pytest_flakefighters-0.3.1.dist-info}/METADATA +10 -6
- pytest_flakefighters-0.3.1.dist-info/RECORD +19 -0
- {pytest_flakefighters-0.2.3.dist-info → pytest_flakefighters-0.3.1.dist-info}/WHEEL +1 -1
- pytest_flakefighters-0.2.3.dist-info/RECORD +0 -18
- {pytest_flakefighters-0.2.3.dist-info → pytest_flakefighters-0.3.1.dist-info}/entry_points.txt +0 -0
- {pytest_flakefighters-0.2.3.dist-info → pytest_flakefighters-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {pytest_flakefighters-0.2.3.dist-info → pytest_flakefighters-0.3.1.dist-info}/top_level.txt +0 -0
_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.3.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 3, 1)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines all the configuration options in a dictionary.
|
|
3
|
+
Keys should be of the form `(--long-name, -L)` or just `(--long-name,)`.
|
|
4
|
+
Options can then be specified on the commandline as `--long-name` or in a configuration file as `long_name`.
|
|
5
|
+
Options specified on the commandline will override those specified in configuration files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from pytest_flakefighters.rerun_strategies import All, FlakyFailure, PreviouslyFlaky
|
|
11
|
+
|
|
12
|
+
rerun_strategies = {"ALL": All, "FLAKY_FAILURE": FlakyFailure, "PREVIOUSLY_FLAKY": PreviouslyFlaky}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
options = {
|
|
16
|
+
("--root",): {
|
|
17
|
+
"dest": "root",
|
|
18
|
+
"action": "store",
|
|
19
|
+
"default": os.getcwd(),
|
|
20
|
+
"help": "The root directory of the project. Defaults to the current working directory.",
|
|
21
|
+
},
|
|
22
|
+
("--suppress-flaky-failures-exit-code",): {
|
|
23
|
+
"dest": "suppress_flaky",
|
|
24
|
+
"action": "store_true",
|
|
25
|
+
"default": False,
|
|
26
|
+
"help": "Return OK exit code if the only failures are flaky failures.",
|
|
27
|
+
},
|
|
28
|
+
("--no-save",): {
|
|
29
|
+
"action": "store_true",
|
|
30
|
+
"default": False,
|
|
31
|
+
"help": "Do not save this run to the database of previous flakefighters runs.",
|
|
32
|
+
},
|
|
33
|
+
("--function-coverage",): {
|
|
34
|
+
"action": "store_true",
|
|
35
|
+
"default": False,
|
|
36
|
+
"help": "Use function-level coverage instead of line coverage.",
|
|
37
|
+
},
|
|
38
|
+
("--load-max-runs", "-M"): {
|
|
39
|
+
"action": "store",
|
|
40
|
+
"default": None,
|
|
41
|
+
"help": "The maximum number of previous runs to consider.",
|
|
42
|
+
},
|
|
43
|
+
("--database-url", "-D"): {
|
|
44
|
+
"action": "store",
|
|
45
|
+
"default": "sqlite:///flakefighters.db",
|
|
46
|
+
"help": "The database URL. Defaults to 'flakefighters.db' in current working directory.",
|
|
47
|
+
},
|
|
48
|
+
("--store-max-runs",): {
|
|
49
|
+
"action": "store",
|
|
50
|
+
"default": None,
|
|
51
|
+
"type": int,
|
|
52
|
+
"help": "The maximum number of previous flakefighters runs to store. Default is to store all.",
|
|
53
|
+
},
|
|
54
|
+
("--max-reruns",): {
|
|
55
|
+
"action": "store",
|
|
56
|
+
"default": 0,
|
|
57
|
+
"type": int,
|
|
58
|
+
"help": "The maximum number of times to rerun tests. "
|
|
59
|
+
"By default, only failing tests marked as flaky will be rerun. "
|
|
60
|
+
"This can be changed with the --rerun-strategy parameter.",
|
|
61
|
+
},
|
|
62
|
+
("--rerun-strategy",): {
|
|
63
|
+
"action": "store",
|
|
64
|
+
"type": str,
|
|
65
|
+
"choices": list(rerun_strategies),
|
|
66
|
+
"default": "FLAKY_FAILURE",
|
|
67
|
+
"help": "The strategy used to determine which tests to rerun. Supported options are:\n "
|
|
68
|
+
+ "\n ".join(f"{name} - {strat.help()}" for name, strat in rerun_strategies.items()),
|
|
69
|
+
},
|
|
70
|
+
("--time-immemorial",): {
|
|
71
|
+
"action": "store",
|
|
72
|
+
"default": None,
|
|
73
|
+
"help": "How long to store flakefighters runs for, specified as `days:hours:minutes`. "
|
|
74
|
+
"E.g. to store tests for one week, use 7:0:0.",
|
|
75
|
+
},
|
|
76
|
+
("--display-outcomes", "-O"): {
|
|
77
|
+
"action": "store",
|
|
78
|
+
"type": int,
|
|
79
|
+
"nargs": "?", # Allows 0 or 1 arguments
|
|
80
|
+
"const": 0, # Value used if -O is present but no value is provided
|
|
81
|
+
"default": 0, # Value used if -O is not present at all
|
|
82
|
+
"help": "Display historical test outcomes of the specified number of previous runs."
|
|
83
|
+
"If no value is specified, then display only the current verdict.",
|
|
84
|
+
},
|
|
85
|
+
("--display-verdicts",): {
|
|
86
|
+
"action": "store_true",
|
|
87
|
+
"default": False,
|
|
88
|
+
"help": "Display the flaky classification verdicts alongside test outcomes.",
|
|
89
|
+
},
|
|
90
|
+
}
|
|
@@ -34,10 +34,12 @@ from sqlalchemy.orm import (
|
|
|
34
34
|
class Base(DeclarativeBase):
|
|
35
35
|
"""
|
|
36
36
|
Declarative base class for data objects.
|
|
37
|
+
|
|
38
|
+
:ivar id: Unique autoincrementing ID for the object.
|
|
37
39
|
"""
|
|
38
40
|
|
|
39
41
|
id: Mapped[int] = Column(Integer, primary_key=True) # pylint: disable=C0103
|
|
40
|
-
#
|
|
42
|
+
# Explicitly flag that we don't want pytest to collect our Test, TestExecution, etc. classes.
|
|
41
43
|
__test__ = False # pylint: disable=C0103
|
|
42
44
|
|
|
43
45
|
@declared_attr
|
|
@@ -49,8 +51,16 @@ class Base(DeclarativeBase):
|
|
|
49
51
|
class Run(Base):
|
|
50
52
|
"""
|
|
51
53
|
Class to store attributes of a flakefighters run.
|
|
54
|
+
:ivar start_time: The time the test run was begun.
|
|
55
|
+
:ivar created_at: The time the entry was added to the database.
|
|
56
|
+
This is not necessarily equivalent to start_time if the test suite took a long time to run or
|
|
57
|
+
if the entry was migrated from a separate database.
|
|
58
|
+
:ivar root: The root directory of the project.
|
|
59
|
+
:ivar tests: The test suite.
|
|
60
|
+
:ivar active_flakefighters: The flakefighters that are active on the run.
|
|
52
61
|
"""
|
|
53
62
|
|
|
63
|
+
start_time = Column(DateTime)
|
|
54
64
|
created_at = Column(DateTime, default=func.now())
|
|
55
65
|
root: Mapped[str] = Column(String)
|
|
56
66
|
tests = relationship("Test", backref="run", lazy="subquery", cascade="all, delete", passive_deletes=True)
|
|
@@ -63,6 +73,10 @@ class Run(Base):
|
|
|
63
73
|
class ActiveFlakeFighter(Base):
|
|
64
74
|
"""
|
|
65
75
|
Store relevant information about the active flakefighters.
|
|
76
|
+
|
|
77
|
+
:ivar run_id: Foreign key of the related run.
|
|
78
|
+
:ivar name: Class name of the flakefighter.
|
|
79
|
+
:ivar params: The parameterss of the flakefighter.
|
|
66
80
|
"""
|
|
67
81
|
|
|
68
82
|
run_id: Mapped[int] = Column(Integer, ForeignKey("run.id"), nullable=False)
|
|
@@ -74,6 +88,17 @@ class ActiveFlakeFighter(Base):
|
|
|
74
88
|
class Test(Base):
|
|
75
89
|
"""
|
|
76
90
|
Class to store attributes of a test case.
|
|
91
|
+
|
|
92
|
+
:ivar run_id: Foreign key of the related run.
|
|
93
|
+
:ivar fspath: File system path of the file containing the test definition.
|
|
94
|
+
:ivar line_no: Line number of the test definition.
|
|
95
|
+
:ivar name: Name of the test case.
|
|
96
|
+
:ivar skipped: Boolean true if the test was skipped, else false.
|
|
97
|
+
:ivar executions: List of execution attempts.
|
|
98
|
+
:ivar flakefighter_results: List of test-level flakefighter results.
|
|
99
|
+
|
|
100
|
+
.. note::
|
|
101
|
+
Execution-level flakefighter results will be stored inside the individual TestExecution objects
|
|
77
102
|
"""
|
|
78
103
|
|
|
79
104
|
run_id: Mapped[int] = Column(Integer, ForeignKey("run.id"), nullable=False)
|
|
@@ -104,6 +129,16 @@ class Test(Base):
|
|
|
104
129
|
class TestExecution(Base): # pylint: disable=R0902
|
|
105
130
|
"""
|
|
106
131
|
Class to store attributes of a test outcome.
|
|
132
|
+
|
|
133
|
+
:ivar test_id: Foreign key of the related test.
|
|
134
|
+
:ivar outcome: Outcome of the test. One of "passed", "failed", or "skipped".
|
|
135
|
+
:ivar stdout: The captured stdout string.
|
|
136
|
+
:ivar stedrr: The captured stderr string.
|
|
137
|
+
:ivar start_time: The start time of the test.
|
|
138
|
+
:ivar end_time: The end time of the test.
|
|
139
|
+
:ivar coverage: The line coverage of the test.
|
|
140
|
+
:ivar flakefighter_results: The execution-level flakefighter results.
|
|
141
|
+
:ivar exception: The exception associated with the test if one was thrown.
|
|
107
142
|
"""
|
|
108
143
|
|
|
109
144
|
__tablename__ = "test_execution"
|
|
@@ -140,6 +175,10 @@ class TestExecution(Base): # pylint: disable=R0902
|
|
|
140
175
|
class TestException(Base): # pylint: disable=R0902
|
|
141
176
|
"""
|
|
142
177
|
Class to store information about the exceptions that cause tests to fail.
|
|
178
|
+
|
|
179
|
+
:ivar execution_id: Foreign key of the related execution.
|
|
180
|
+
:ivar name: Name of the exception.
|
|
181
|
+
:traceback: The full stack of traceback entries.
|
|
143
182
|
"""
|
|
144
183
|
|
|
145
184
|
__tablename__ = "test_exception"
|
|
@@ -155,6 +194,13 @@ class TestException(Base): # pylint: disable=R0902
|
|
|
155
194
|
class TracebackEntry(Base): # pylint: disable=R0902
|
|
156
195
|
"""
|
|
157
196
|
Class to store attributes of entries in the stack trace.
|
|
197
|
+
|
|
198
|
+
:ivar exception_id: Foreign key of the related exception.
|
|
199
|
+
:ivar path: Filepath of the source file.
|
|
200
|
+
:ivar lineno: Line number of the executed statement.
|
|
201
|
+
:ivar colno: Column number of the executed statement.
|
|
202
|
+
:ivar statement: The executed statement.
|
|
203
|
+
:ivar source: The surrounding source code.
|
|
158
204
|
"""
|
|
159
205
|
|
|
160
206
|
exception_id: Mapped[int] = Column(Integer, ForeignKey("test_exception.id"), nullable=False)
|
|
@@ -169,6 +215,11 @@ class TracebackEntry(Base): # pylint: disable=R0902
|
|
|
169
215
|
class FlakefighterResult(Base): # pylint: disable=R0902
|
|
170
216
|
"""
|
|
171
217
|
Class to store flakefighter results.
|
|
218
|
+
|
|
219
|
+
:ivar test_execution_id: Foreign key of the related test execution. Should not be set if test_id is present.
|
|
220
|
+
:ivar test_id: Foreign key of the related test. Should not be set if test_execution_id is present.
|
|
221
|
+
:ivar name: Name of the flakefighter.
|
|
222
|
+
:ivar flaky: Boolean true if the test (execution) was classified as flaky.
|
|
172
223
|
"""
|
|
173
224
|
|
|
174
225
|
__tablename__ = "flakefighter_result"
|
|
@@ -182,10 +233,25 @@ class FlakefighterResult(Base): # pylint: disable=R0902
|
|
|
182
233
|
CheckConstraint("NOT (test_execution_id IS NULL AND test_id IS NULL)", name="check_test_id_not_null"),
|
|
183
234
|
)
|
|
184
235
|
|
|
236
|
+
@property
|
|
237
|
+
def classification(self):
|
|
238
|
+
"""
|
|
239
|
+
Return the classification as a string.
|
|
240
|
+
"flaky" if the test was classified as flaky, else "genuine".
|
|
241
|
+
"""
|
|
242
|
+
return "flaky" if self.flaky else "genuine"
|
|
243
|
+
|
|
185
244
|
|
|
186
245
|
class Database:
|
|
187
246
|
"""
|
|
188
247
|
Class to handle database setup and interaction.
|
|
248
|
+
|
|
249
|
+
:ivar engine: The database engine.
|
|
250
|
+
:ivar store_max_runs: The maximum number of previous runs that should be stored. If the database exceeds this size,
|
|
251
|
+
older runs will be pruned to make space for newer ones.
|
|
252
|
+
:ivar time_immemorial: Time before which runs should not be considered. Runs before this date will be pruned when
|
|
253
|
+
saving new runs.
|
|
254
|
+
:ivar previous_runs: List of previous flakefighter runs with most recent first.
|
|
189
255
|
"""
|
|
190
256
|
|
|
191
257
|
def __init__(
|
|
@@ -230,4 +296,4 @@ class Database:
|
|
|
230
296
|
:param limit: The maximum number of runs to return (these will be most recent runs).
|
|
231
297
|
"""
|
|
232
298
|
with Session(self.engine) as session:
|
|
233
|
-
return session.scalars(select(Run).order_by(desc(Run.
|
|
299
|
+
return session.scalars(select(Run).order_by(desc(Run.start_time)).limit(limit)).all()
|
|
@@ -68,7 +68,10 @@ class DeFlaker(FlakeFighter):
|
|
|
68
68
|
self.method_declarations = {}
|
|
69
69
|
for file, lines in self.lines_changed.items():
|
|
70
70
|
with open(file) as f:
|
|
71
|
-
|
|
71
|
+
try:
|
|
72
|
+
tree = ast.parse(f.read())
|
|
73
|
+
except SyntaxError:
|
|
74
|
+
continue
|
|
72
75
|
|
|
73
76
|
self.method_declarations[file] = [
|
|
74
77
|
node.lineno
|
|
@@ -133,9 +136,5 @@ class DeFlaker(FlakeFighter):
|
|
|
133
136
|
:param run: Run object representing the pytest run, with tests accessible through run.tests.
|
|
134
137
|
"""
|
|
135
138
|
for test in run.tests:
|
|
136
|
-
test.
|
|
137
|
-
|
|
138
|
-
name=self.__class__.__name__,
|
|
139
|
-
flaky=any(self._flaky_execution(execution) for execution in test.executions),
|
|
140
|
-
)
|
|
141
|
-
)
|
|
139
|
+
for execution in test.executions:
|
|
140
|
+
self.flaky_test_live(execution)
|
|
@@ -49,7 +49,7 @@ class TracebackMatching(FlakeFighter):
|
|
|
49
49
|
Convert the key parameters into a dictionary so that the object can be replicated.
|
|
50
50
|
:return A dictionary of the parameters used to create the object.
|
|
51
51
|
"""
|
|
52
|
-
return {"root": self.root}
|
|
52
|
+
return {"run_live": self.run_live, "root": self.root}
|
|
53
53
|
|
|
54
54
|
def _flaky_execution(self, execution, previous_executions) -> bool:
|
|
55
55
|
"""
|
|
@@ -66,15 +66,13 @@ class TracebackMatching(FlakeFighter):
|
|
|
66
66
|
]
|
|
67
67
|
return any(e == current_traceback for e in previous_executions)
|
|
68
68
|
|
|
69
|
-
def previous_flaky_executions(self, runs: list[Run]
|
|
69
|
+
def previous_flaky_executions(self, runs: list[Run]) -> list:
|
|
70
70
|
"""
|
|
71
71
|
Extract the relevant information from previous flaky executions and collapse into a single list.
|
|
72
72
|
:param runs: The runs to consider. Defaults to self.previous_runs.
|
|
73
73
|
:return: List containing the relative path, line number, column number, and code statement of all previous
|
|
74
74
|
test executions.
|
|
75
75
|
"""
|
|
76
|
-
if runs is None:
|
|
77
|
-
runs = self.previous_runs
|
|
78
76
|
return [
|
|
79
77
|
[
|
|
80
78
|
(os.path.relpath(elem.path, run.root), elem.lineno, elem.colno, elem.statement)
|
|
@@ -87,17 +85,20 @@ class TracebackMatching(FlakeFighter):
|
|
|
87
85
|
if execution.exception
|
|
88
86
|
]
|
|
89
87
|
|
|
90
|
-
def flaky_test_live(self, execution: TestExecution):
|
|
88
|
+
def flaky_test_live(self, execution: TestExecution, previous_runs: list[Run] = None):
|
|
91
89
|
"""
|
|
92
90
|
Classify executions as flaky if they have the same failure logs as a flaky execution.
|
|
93
91
|
:param execution: Test execution to consider.
|
|
92
|
+
:param previous_runs: The previous runs to which the execution will be compared.
|
|
94
93
|
"""
|
|
94
|
+
if previous_runs is None:
|
|
95
|
+
previous_runs = self.previous_runs
|
|
95
96
|
execution.flakefighter_results.append(
|
|
96
97
|
FlakefighterResult(
|
|
97
98
|
name=self.__class__.__name__,
|
|
98
99
|
flaky=self._flaky_execution(
|
|
99
100
|
execution,
|
|
100
|
-
self.previous_flaky_executions(),
|
|
101
|
+
self.previous_flaky_executions(previous_runs),
|
|
101
102
|
),
|
|
102
103
|
)
|
|
103
104
|
)
|
|
@@ -109,14 +110,7 @@ class TracebackMatching(FlakeFighter):
|
|
|
109
110
|
"""
|
|
110
111
|
for test in run.tests:
|
|
111
112
|
for execution in test.executions:
|
|
112
|
-
execution.
|
|
113
|
-
FlakefighterResult(
|
|
114
|
-
name=self.__class__.__name__,
|
|
115
|
-
flaky=self._flaky_execution(
|
|
116
|
-
execution, self.previous_flaky_executions(self.previous_runs + [run])
|
|
117
|
-
),
|
|
118
|
-
)
|
|
119
|
-
)
|
|
113
|
+
self.flaky_test_live(execution, self.previous_runs + [run])
|
|
120
114
|
|
|
121
115
|
|
|
122
116
|
class CosineSimilarity(TracebackMatching):
|
pytest_flakefighters/main.py
CHANGED
|
@@ -3,13 +3,13 @@ This module adds all the FlakeFighter configuration options to pytest.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
import os
|
|
7
6
|
from importlib.metadata import entry_points
|
|
8
7
|
|
|
9
8
|
import coverage
|
|
10
9
|
import pytest
|
|
11
10
|
import yaml
|
|
12
11
|
|
|
12
|
+
from pytest_flakefighters.config import options
|
|
13
13
|
from pytest_flakefighters.database_management import Database
|
|
14
14
|
from pytest_flakefighters.flakefighters.deflaker import DeFlaker
|
|
15
15
|
from pytest_flakefighters.function_coverage import Profiler
|
|
@@ -35,79 +35,45 @@ def pytest_addoption(parser: pytest.Parser):
|
|
|
35
35
|
Add extra pytest options.
|
|
36
36
|
:param parser: The argument parser.
|
|
37
37
|
"""
|
|
38
|
+
# Allows users to specify flakefighter configurations in their pyproject.toml file without pytest throwing out
|
|
39
|
+
# "unknown configuration option" warnings
|
|
40
|
+
parser.addini("pytest_flakefighters", type="args", help="Configuration for the pytest-flakefighters extension")
|
|
41
|
+
|
|
42
|
+
def datatype(details):
|
|
43
|
+
if "type" not in details:
|
|
44
|
+
return None
|
|
45
|
+
if details["type"] is str:
|
|
46
|
+
return "string"
|
|
47
|
+
return str(details["type"].__name__)
|
|
48
|
+
|
|
38
49
|
group = parser.getgroup("flakefighters")
|
|
39
|
-
|
|
40
|
-
"--
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"--load-max-runs",
|
|
67
|
-
"-M",
|
|
68
|
-
action="store",
|
|
69
|
-
default=None,
|
|
70
|
-
help="The maximum number of previous runs to consider.",
|
|
71
|
-
)
|
|
72
|
-
group.addoption(
|
|
73
|
-
"--database-url",
|
|
74
|
-
"-D",
|
|
75
|
-
action="store",
|
|
76
|
-
default="sqlite:///flakefighters.db",
|
|
77
|
-
help="The database URL. Defaults to 'flakefighters.db' in current working directory.",
|
|
78
|
-
)
|
|
79
|
-
group.addoption(
|
|
80
|
-
"--store-max-runs",
|
|
81
|
-
action="store",
|
|
82
|
-
default=None,
|
|
83
|
-
type=int,
|
|
84
|
-
help="The maximum number of previous flakefighters runs to store. Default is to store all.",
|
|
85
|
-
)
|
|
86
|
-
group.addoption(
|
|
87
|
-
"--max-reruns",
|
|
88
|
-
action="store",
|
|
89
|
-
default=0,
|
|
90
|
-
type=int,
|
|
91
|
-
help="The maximum number of times to rerun tests. "
|
|
92
|
-
"By default, only failing tests marked as flaky will be rerun. "
|
|
93
|
-
"This can be changed with the --rerun-strategy parameter.",
|
|
94
|
-
)
|
|
95
|
-
group.addoption(
|
|
96
|
-
"--rerun-strategy",
|
|
97
|
-
action="store",
|
|
98
|
-
type=str,
|
|
99
|
-
choices=list(rerun_strategies),
|
|
100
|
-
default="FLAKY_FAILURE",
|
|
101
|
-
help="The strategy used to determine which tests to rerun. Supported options are:\n "
|
|
102
|
-
+ "\n ".join(f"{name} - {strat.help()}" for name, strat in rerun_strategies.items()),
|
|
103
|
-
)
|
|
104
|
-
group.addoption(
|
|
105
|
-
"--time-immemorial",
|
|
106
|
-
action="store",
|
|
107
|
-
default=None,
|
|
108
|
-
help="How long to store flakefighters runs for, specified as `days:hours:minutes`. "
|
|
109
|
-
"E.g. to store tests for one week, use 7:0:0.",
|
|
110
|
-
)
|
|
50
|
+
for name, details in options.items():
|
|
51
|
+
# Add a commandline option with short name if provided, e.g. "--custom-option"
|
|
52
|
+
# We need the default to be None here so that we can test if the user has provided it
|
|
53
|
+
group.addoption(*name, **(details | {"default": None}))
|
|
54
|
+
# Add configuration file option with no "--" and "-" replaced by "_"
|
|
55
|
+
parser.addini(
|
|
56
|
+
name[0][2:].replace("-", "_"),
|
|
57
|
+
help=details["help"],
|
|
58
|
+
default=details.get("default"),
|
|
59
|
+
type=datatype(details),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_config_value(config, name):
|
|
64
|
+
"""
|
|
65
|
+
Get the configuration value.
|
|
66
|
+
Options specified on the commandline will override those specified in configuration files.
|
|
67
|
+
If neither is specified, the default value specified in `options.py` will be used.
|
|
68
|
+
"""
|
|
69
|
+
cli_val = config.getoption(name)
|
|
70
|
+
if cli_val is not None:
|
|
71
|
+
return cli_val
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
return config.getini(name)
|
|
75
|
+
except ValueError:
|
|
76
|
+
return None
|
|
111
77
|
|
|
112
78
|
|
|
113
79
|
def pytest_configure(config: pytest.Config):
|
|
@@ -116,52 +82,66 @@ def pytest_configure(config: pytest.Config):
|
|
|
116
82
|
:param config: The config options.
|
|
117
83
|
"""
|
|
118
84
|
database = Database(
|
|
119
|
-
config
|
|
120
|
-
config
|
|
121
|
-
config
|
|
122
|
-
config
|
|
85
|
+
get_config_value(config, "database_url"),
|
|
86
|
+
get_config_value(config, "load_max_runs"),
|
|
87
|
+
get_config_value(config, "store_max_runs"),
|
|
88
|
+
get_config_value(config, "time_immemorial"),
|
|
123
89
|
)
|
|
124
90
|
|
|
125
|
-
if config
|
|
91
|
+
if get_config_value(config, "function_coverage"):
|
|
126
92
|
cov = Profiler()
|
|
127
93
|
else:
|
|
128
94
|
cov = coverage.Coverage()
|
|
129
95
|
|
|
130
|
-
algorithms = entry_points(group="pytest_flakefighters")
|
|
96
|
+
algorithms = {ff.name: ff for ff in entry_points(group="pytest_flakefighters")}
|
|
131
97
|
flakefighter_configs = config.inicfg.get("pytest_flakefighters")
|
|
132
98
|
|
|
133
99
|
flakefighters = []
|
|
134
100
|
if flakefighter_configs is not None:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
101
|
+
# Can't measure coverage since the branch taken depends on the python version
|
|
102
|
+
if isinstance(flakefighter_configs, str): # pragma: no cover
|
|
103
|
+
flakefighter_configs = yaml.safe_load(flakefighter_configs) # pragma: no cover
|
|
104
|
+
elif hasattr(flakefighter_configs, "value"): # pragma: no cover
|
|
105
|
+
flakefighter_configs = yaml.safe_load(flakefighter_configs.value) # pragma: no cover
|
|
106
|
+
else: # pragma: no cover
|
|
107
|
+
raise TypeError(f"Unexpected type for config: {type(flakefighter_configs)}") # pragma: no cover
|
|
108
|
+
for module, classes in flakefighter_configs["flakefighters"].items():
|
|
109
|
+
for class_name, params in classes.items():
|
|
110
|
+
if class_name in algorithms:
|
|
111
|
+
flakefighters.append(
|
|
112
|
+
algorithms[class_name]
|
|
113
|
+
.load()
|
|
114
|
+
.from_config(
|
|
115
|
+
{k: get_config_value(config, k) for k in vars(config.option)}
|
|
116
|
+
| {"database": database}
|
|
117
|
+
| params
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"Could not load flakefighter {module}:{class_name}. Did you register its entry point?"
|
|
146
123
|
)
|
|
147
|
-
)
|
|
148
124
|
|
|
149
125
|
else:
|
|
150
126
|
logger.warning("No flakefighters specified. Using basic DeFlaker only.")
|
|
151
127
|
flakefighters.append(
|
|
152
128
|
DeFlaker(
|
|
153
129
|
run_live=True,
|
|
154
|
-
root=config
|
|
130
|
+
root=get_config_value(config, "root"),
|
|
155
131
|
)
|
|
156
132
|
)
|
|
157
133
|
|
|
158
134
|
config.pluginmanager.register(
|
|
159
135
|
FlakeFighterPlugin(
|
|
160
|
-
root=config
|
|
136
|
+
root=get_config_value(config, "root"),
|
|
161
137
|
database=database,
|
|
162
138
|
cov=cov,
|
|
163
139
|
flakefighters=flakefighters,
|
|
164
|
-
rerun_strategy=rerun_strategy(
|
|
165
|
-
|
|
140
|
+
rerun_strategy=rerun_strategy(
|
|
141
|
+
get_config_value(config, "rerun_strategy"), get_config_value(config, "max_reruns"), database=database
|
|
142
|
+
),
|
|
143
|
+
save_run=not get_config_value(config, "no_save"),
|
|
144
|
+
display_outcomes=get_config_value(config, "display_outcomes"),
|
|
145
|
+
display_verdicts=get_config_value(config, "display_verdicts"),
|
|
166
146
|
)
|
|
167
147
|
)
|
pytest_flakefighters/plugin.py
CHANGED
|
@@ -5,6 +5,7 @@ This module implements the DeFlaker algorithm [Bell et al. 10.1145/3180155.31801
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from typing import Union
|
|
8
|
+
from xml.etree import ElementTree as ET
|
|
8
9
|
|
|
9
10
|
import coverage
|
|
10
11
|
import pytest
|
|
@@ -49,6 +50,8 @@ class FlakeFighterPlugin: # pylint: disable=R0902
|
|
|
49
50
|
flakefighters: list[FlakeFighter],
|
|
50
51
|
save_run: bool = True,
|
|
51
52
|
rerun_strategy: RerunStrategy = RerunStrategy.FLAKY_FAILURE,
|
|
53
|
+
display_outcomes: int = 0,
|
|
54
|
+
display_verdicts: bool = False,
|
|
52
55
|
):
|
|
53
56
|
self.root = root
|
|
54
57
|
self.database = database
|
|
@@ -56,12 +59,16 @@ class FlakeFighterPlugin: # pylint: disable=R0902
|
|
|
56
59
|
self.flakefighters = flakefighters
|
|
57
60
|
self.save_run = save_run
|
|
58
61
|
self.rerun_strategy = rerun_strategy
|
|
62
|
+
self.test_reports = {}
|
|
63
|
+
self.display_verdicts = display_verdicts
|
|
64
|
+
self.display_outcomes = display_outcomes
|
|
59
65
|
|
|
60
66
|
self.run = Run( # pylint: disable=E1123
|
|
61
67
|
root=root,
|
|
62
68
|
active_flakefighters=[
|
|
63
69
|
ActiveFlakeFighter(name=f.__class__.__name__, params=f.params()) for f in flakefighters
|
|
64
70
|
],
|
|
71
|
+
start_time=datetime.now(),
|
|
65
72
|
)
|
|
66
73
|
|
|
67
74
|
def pytest_sessionstart(self, session: pytest.Session): # pylint: disable=unused-argument
|
|
@@ -176,9 +183,54 @@ class FlakeFighterPlugin: # pylint: disable=R0902
|
|
|
176
183
|
test.executions.append(test_execution)
|
|
177
184
|
for ff in filter(lambda ff: ff.run_live, self.flakefighters):
|
|
178
185
|
ff.flaky_test_live(test_execution)
|
|
186
|
+
self.test_reports[item.nodeid] = report
|
|
179
187
|
report.flaky = any(result.flaky for result in test_execution.flakefighter_results)
|
|
188
|
+
# Limited pytest-json support
|
|
189
|
+
report.stage_metadata = {
|
|
190
|
+
"executions": [
|
|
191
|
+
{
|
|
192
|
+
"start_time": x.start_time.isoformat(),
|
|
193
|
+
"end_time": x.end_time.isoformat(),
|
|
194
|
+
"outcome": test_execution.outcome,
|
|
195
|
+
"flakefighter_results": {r.name: r.classification for r in x.flakefighter_results},
|
|
196
|
+
}
|
|
197
|
+
for x in test.executions
|
|
198
|
+
],
|
|
199
|
+
}
|
|
200
|
+
# html
|
|
201
|
+
if hasattr(report, "extras"):
|
|
202
|
+
report.extras.append(
|
|
203
|
+
{
|
|
204
|
+
"content": f"""
|
|
205
|
+
<h4>Flakefighter Results</h4>
|
|
206
|
+
<div id="ff-{report.nodeid.replace("::", "_")}"></div>
|
|
207
|
+
<table style="width:100%"><tbody><tr>"""
|
|
208
|
+
+ "".join(
|
|
209
|
+
[
|
|
210
|
+
f"""
|
|
211
|
+
<td>
|
|
212
|
+
<p><strong>Start time:</strong> {execution.start_time}</p>
|
|
213
|
+
<p><strong>End time:</strong> {execution.end_time}</p>
|
|
214
|
+
<p><strong>Outcome:</strong> {execution.outcome}</p>
|
|
215
|
+
<p><strong>Flakefighter Results:</strong></p>
|
|
216
|
+
<ul>
|
|
217
|
+
{''.join(['<li><strong>'+result.name+':</strong> '+result.classification+'</li>'
|
|
218
|
+
for result in execution.flakefighter_results])}
|
|
219
|
+
</ul>
|
|
220
|
+
</td>
|
|
221
|
+
"""
|
|
222
|
+
for execution in test.executions
|
|
223
|
+
]
|
|
224
|
+
)
|
|
225
|
+
+ "</tr></tbody></table>",
|
|
226
|
+
"extension": "html",
|
|
227
|
+
"format_type": "html",
|
|
228
|
+
"mime_type": "text/html",
|
|
229
|
+
}
|
|
230
|
+
)
|
|
180
231
|
if item.execution_count <= self.rerun_strategy.max_reruns and self.rerun_strategy.rerun(report):
|
|
181
232
|
break # trigger rerun
|
|
233
|
+
|
|
182
234
|
item.ihook.pytest_runtest_logreport(report=report)
|
|
183
235
|
else:
|
|
184
236
|
break # Skip further reruns
|
|
@@ -199,6 +251,128 @@ class FlakeFighterPlugin: # pylint: disable=R0902
|
|
|
199
251
|
return report.outcome, "F", ("FLAKY", {"yellow": True})
|
|
200
252
|
return None
|
|
201
253
|
|
|
254
|
+
@pytest.hookimpl(hookwrapper=True)
|
|
255
|
+
def pytest_runtestloop(self, session: pytest.Session): # pylint: disable=unused-argument
|
|
256
|
+
"""
|
|
257
|
+
Run postprocessing flakefighters.
|
|
258
|
+
:param session: The pytest session object.
|
|
259
|
+
"""
|
|
260
|
+
yield
|
|
261
|
+
for ff in filter(lambda ff: not ff.run_live, self.flakefighters):
|
|
262
|
+
ff.flaky_tests_post(self.run)
|
|
263
|
+
for test in self.run.tests:
|
|
264
|
+
if test.name in self.test_reports:
|
|
265
|
+
self.test_reports[test.name].flakefighter_results = {
|
|
266
|
+
r.name: r.classification for r in test.flakefighter_results
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@pytest.hookimpl(optionalhook=True)
|
|
270
|
+
def pytest_json_modifyreport(self, json_report: dict):
|
|
271
|
+
"""
|
|
272
|
+
Add flakefighter results to the pytest-json-report report.
|
|
273
|
+
|
|
274
|
+
:param json_report: The report dict.
|
|
275
|
+
"""
|
|
276
|
+
for t in json_report.get("tests", []):
|
|
277
|
+
t["call"]["metadata"] = self.test_reports[t["nodeid"]].stage_metadata
|
|
278
|
+
|
|
279
|
+
t["metadata"] = t.get("metadata", {}) | {
|
|
280
|
+
"flakefighter_results": self.test_reports[t["nodeid"]].flakefighter_results
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
def build_outcome_string(self, test: Test) -> str:
|
|
284
|
+
"""
|
|
285
|
+
Construct a string to represent previous flakefighter outcomes for a given test and its associated executions.
|
|
286
|
+
|
|
287
|
+
:param test: The test case.
|
|
288
|
+
"""
|
|
289
|
+
result_string = []
|
|
290
|
+
if test.flakefighter_results:
|
|
291
|
+
if self.display_verdicts:
|
|
292
|
+
result_string.append(
|
|
293
|
+
"Overall\n" + "\n".join(f" {f.name}: {f.classification}" for f in test.flakefighter_results) + "\n"
|
|
294
|
+
)
|
|
295
|
+
for i, execution in enumerate(test.executions):
|
|
296
|
+
if execution.flakefighter_results:
|
|
297
|
+
if self.display_verdicts:
|
|
298
|
+
result_string.append(
|
|
299
|
+
f"Execution {i}: {execution.outcome}\n"
|
|
300
|
+
+ "\n".join(
|
|
301
|
+
f" {f.name}: {'flaky' if f.flaky else 'genuine'}" for f in execution.flakefighter_results
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
result_string.append(f"Execution {i}: {execution.outcome}")
|
|
306
|
+
return "\n".join(result_string)
|
|
307
|
+
|
|
308
|
+
def modify_xml(self, xml_path: str):
|
|
309
|
+
"""
|
|
310
|
+
Modify the JUnitXML file to add the flakefighter results for each test.
|
|
311
|
+
|
|
312
|
+
:param xml_path: The path of the saved XML file.
|
|
313
|
+
"""
|
|
314
|
+
tree = ET.parse(xml_path)
|
|
315
|
+
for testsuite in tree.getroot().findall("testsuite"):
|
|
316
|
+
for testcase in testsuite.findall("testcase"):
|
|
317
|
+
splitname = testcase.get("classname").split(".")
|
|
318
|
+
splitname[0] = f"{splitname[0]}.py"
|
|
319
|
+
nodeid = "::".join(splitname + [testcase.get("name")])
|
|
320
|
+
flakefighter_results = ET.SubElement(testcase, "flakefighterresults")
|
|
321
|
+
if nodeid in self.test_reports:
|
|
322
|
+
for execution in self.test_reports[nodeid].stage_metadata["executions"]:
|
|
323
|
+
execution_results = ET.Element(
|
|
324
|
+
"execution",
|
|
325
|
+
{
|
|
326
|
+
"outcome": execution["outcome"],
|
|
327
|
+
"starttime": execution["start_time"],
|
|
328
|
+
"endtime": execution["end_time"],
|
|
329
|
+
},
|
|
330
|
+
)
|
|
331
|
+
flakefighter_results.append(execution_results)
|
|
332
|
+
for name, classification in execution["flakefighter_results"].items():
|
|
333
|
+
ET.SubElement(execution_results, name).text = classification
|
|
334
|
+
test_results = ET.SubElement(flakefighter_results, "test")
|
|
335
|
+
for name, classification in self.test_reports[nodeid].flakefighter_results.items():
|
|
336
|
+
ET.SubElement(test_results, name).text = classification
|
|
337
|
+
|
|
338
|
+
tree.write(xml_path)
|
|
339
|
+
|
|
340
|
+
@pytest.hookimpl(optionalhook=True)
|
|
341
|
+
def pytest_html_results_summary(
|
|
342
|
+
self, prefix: list, summary: list, postfix: list
|
|
343
|
+
): # pylint: disable=unused-argument
|
|
344
|
+
"""
|
|
345
|
+
Add the test-level flakefighter results.
|
|
346
|
+
:param prefix: The prefix content. UNUSED.
|
|
347
|
+
:param summary: The summary content. UNUSED.
|
|
348
|
+
:param postfix: The postfix content.
|
|
349
|
+
"""
|
|
350
|
+
postfix.extend(
|
|
351
|
+
[
|
|
352
|
+
"<h2>Test-level flakefighter results</h2>",
|
|
353
|
+
"<table>",
|
|
354
|
+
"<thead><tr><td>Test</td><td>Flakefighter results</td></tr></thead>",
|
|
355
|
+
"<tbody>",
|
|
356
|
+
]
|
|
357
|
+
+ [
|
|
358
|
+
f"<tr><td>{nodeid}</td><td>"
|
|
359
|
+
+ "".join(
|
|
360
|
+
[
|
|
361
|
+
f"""<ul>
|
|
362
|
+
{''.join(['<li><strong>'+result['name']+':</strong> '+result['classification']+'</li>'
|
|
363
|
+
for result in report.flakefighter_results])}
|
|
364
|
+
</ul>"""
|
|
365
|
+
]
|
|
366
|
+
)
|
|
367
|
+
+ "</td></tr>"
|
|
368
|
+
for nodeid, report in self.test_reports.items()
|
|
369
|
+
]
|
|
370
|
+
+ [
|
|
371
|
+
"</tbody>",
|
|
372
|
+
"</table>",
|
|
373
|
+
]
|
|
374
|
+
)
|
|
375
|
+
|
|
202
376
|
def pytest_sessionfinish(
|
|
203
377
|
self,
|
|
204
378
|
session: pytest.Session,
|
|
@@ -208,8 +382,23 @@ class FlakeFighterPlugin: # pylint: disable=R0902
|
|
|
208
382
|
:param session: The pytest session object.
|
|
209
383
|
:param exitstatus: The status which pytest will return to the system.
|
|
210
384
|
"""
|
|
211
|
-
|
|
212
|
-
|
|
385
|
+
|
|
386
|
+
if session.config.option.xmlpath:
|
|
387
|
+
self.modify_xml(session.config.option.xmlpath)
|
|
388
|
+
|
|
389
|
+
runs = [self.run]
|
|
390
|
+
if self.display_outcomes:
|
|
391
|
+
runs += self.database.load_runs(self.display_outcomes)
|
|
392
|
+
if self.display_verdicts or self.display_outcomes:
|
|
393
|
+
for run in runs:
|
|
394
|
+
for test in run.tests:
|
|
395
|
+
if test.name in self.test_reports:
|
|
396
|
+
self.test_reports[test.name].sections.append(
|
|
397
|
+
(
|
|
398
|
+
f"Flakefighter Verdicts {run.start_time if run != self.run else '(Current)'}",
|
|
399
|
+
self.build_outcome_string(test),
|
|
400
|
+
)
|
|
401
|
+
)
|
|
213
402
|
|
|
214
403
|
genuine_failure_observed = any(
|
|
215
404
|
not test.flaky for test in self.run.tests if any(e.outcome != "passed" for e in test.executions)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-flakefighters
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Pytest plugin implementing flaky test failure detection and classification.
|
|
5
5
|
Author: TestFLARE Team
|
|
6
6
|
Project-URL: Documentation, https://pytest-flakefighters.readthedocs.io
|
|
@@ -38,22 +38,26 @@ Requires-Dist: psycopg2>2.9; extra == "pg"
|
|
|
38
38
|
Dynamic: license-file
|
|
39
39
|
|
|
40
40
|
# Pytest FlakeFighters
|
|
41
|
-
### Pytest plugin implementing flaky test failure detection and classification.
|
|
42
41
|
|
|
43
42
|
[](https://www.repostatus.org/#active)
|
|
44
43
|
[](https://pypi.org/project/pytest-flakefighters)
|
|
45
44
|
[](https://pypi.org/project/pytest-flakefighters)
|
|
46
45
|

|
|
47
46
|
[](https://codecov.io/gh/test-flare/pytest-flakefighters)
|
|
48
|
-
[](https://pytest-flakefighters.readthedocs.io/en/latest/?badge=latest)
|
|
49
48
|

|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
### Pytest plugin implementing flaky test failure detection and classification.
|
|
51
|
+
Read more about flaky tests [here](https://docs.pytest.org/en/stable/explanation/flaky.html).
|
|
52
52
|
|
|
53
53
|
## Features
|
|
54
54
|
|
|
55
|
-
-
|
|
56
|
-
|
|
55
|
+
- Implements the [DeFlaker algorithm](http://www.deflaker.org/get-rid-of-your-flakes/) for pytest
|
|
56
|
+
- Implements two traceback-matching classifiers from [Alshammari et al. (2024)](https://doi.org/10.1109/ICST60714.2024.00031).
|
|
57
|
+
- Implements a novel coverage-independence classifier that classifies tests as flaky if they fail independently of passing test cases that exercise overlapping code.
|
|
58
|
+
- Optionally rerun flaky failures
|
|
59
|
+
- Output results to JSON, HTML, or JUnitXML
|
|
60
|
+
- Save test outcome history to a remote or local database
|
|
57
61
|
|
|
58
62
|
## Installation
|
|
59
63
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
_version.py,sha256=gGLpQUQx-ty9SEy9PYw9OgJWWzJLBnCpfJOfzL7SjlI,704
|
|
2
|
+
pytest_flakefighters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
pytest_flakefighters/config.py,sha256=NBqbonht0O4L_P3hV_g__LiD6Fo5aGIau7D84hBA5kE,3460
|
|
4
|
+
pytest_flakefighters/database_management.py,sha256=FZTDYG9uVlPY2j874I_YDFQ9kuNXhm4P9fL3ckrg8MY,10427
|
|
5
|
+
pytest_flakefighters/function_coverage.py,sha256=IgzWHMFgkVrc68ZjQ1o27tGxKa8WvcMcGW8RWnpFLVw,2187
|
|
6
|
+
pytest_flakefighters/main.py,sha256=QxhaCdrbJmMNgekUDRsLIwNDhnd_vaGQ56wpbYKcV1U,5541
|
|
7
|
+
pytest_flakefighters/plugin.py,sha256=enB2bXXE76Swpn7aHrb2j8fx2oCHi2bcQAx9kLWrTOc,17489
|
|
8
|
+
pytest_flakefighters/rerun_strategies.py,sha256=YI-spvm0dhB-gGM9IEgDz19NHvzD_XesZ8lDcBDGSJM,2536
|
|
9
|
+
pytest_flakefighters/flakefighters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
pytest_flakefighters/flakefighters/abstract_flakefighter.py,sha256=bchRwoNzQaFhSknT9wKDpbf3EiReHPsHsDhm5qi8y_o,1953
|
|
11
|
+
pytest_flakefighters/flakefighters/coverage_independence.py,sha256=5swoY1jOaaGFlZNX6PdejBFVd7Gpi3zsaW5W6toIzk8,4480
|
|
12
|
+
pytest_flakefighters/flakefighters/deflaker.py,sha256=eqqfhvKrxYIbbtm_pdqTw2AwmcHo5vBh3HfqyscusdU,5911
|
|
13
|
+
pytest_flakefighters/flakefighters/traceback_matching.py,sha256=L-3vVdB9aTjGHrsCAh9R8L51YQoSfRUfhMioIywoJqM,6808
|
|
14
|
+
pytest_flakefighters-0.3.1.dist-info/licenses/LICENSE,sha256=tTzR2CWQMPOp-mQIQqi0cTRkaogeBUmW06blQsBLdQg,1082
|
|
15
|
+
pytest_flakefighters-0.3.1.dist-info/METADATA,sha256=qJ0VwrCDLgYhWnZASbHn17hivEt1XJVeszRfB49Ko6I,6044
|
|
16
|
+
pytest_flakefighters-0.3.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
17
|
+
pytest_flakefighters-0.3.1.dist-info/entry_points.txt,sha256=xockvN1AszN2XqaET77JDIRdOafgm3DdvOtHgVw-aDU,424
|
|
18
|
+
pytest_flakefighters-0.3.1.dist-info/top_level.txt,sha256=bWwe0xVZ_l2CP8KSnztW6FafZKQZ7zUTMa1U32geY28,30
|
|
19
|
+
pytest_flakefighters-0.3.1.dist-info/RECORD,,
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
_version.py,sha256=kBRz0P2plw1eVdIpt70W6m1LMbEIhLY3RyOfVGdubaI,704
|
|
2
|
-
pytest_flakefighters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
pytest_flakefighters/database_management.py,sha256=mSdrP4enD6_oT941y5KAP-Jae84FihVJEvqCUFPhrss,7105
|
|
4
|
-
pytest_flakefighters/function_coverage.py,sha256=IgzWHMFgkVrc68ZjQ1o27tGxKa8WvcMcGW8RWnpFLVw,2187
|
|
5
|
-
pytest_flakefighters/main.py,sha256=N0yGSGA-ZLtsKRCtSCIdnw7RPVEtLnXUhnftQGHjujA,5430
|
|
6
|
-
pytest_flakefighters/plugin.py,sha256=kdagObAK93ab6-5lckwkBR846RPspxXen4qlEcsAjB8,8866
|
|
7
|
-
pytest_flakefighters/rerun_strategies.py,sha256=YI-spvm0dhB-gGM9IEgDz19NHvzD_XesZ8lDcBDGSJM,2536
|
|
8
|
-
pytest_flakefighters/flakefighters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
pytest_flakefighters/flakefighters/abstract_flakefighter.py,sha256=bchRwoNzQaFhSknT9wKDpbf3EiReHPsHsDhm5qi8y_o,1953
|
|
10
|
-
pytest_flakefighters/flakefighters/coverage_independence.py,sha256=5swoY1jOaaGFlZNX6PdejBFVd7Gpi3zsaW5W6toIzk8,4480
|
|
11
|
-
pytest_flakefighters/flakefighters/deflaker.py,sha256=GjmAA5i7YFJKbNaheWihqUqyu9_z2NZwqy6U--ztj9c,5989
|
|
12
|
-
pytest_flakefighters/flakefighters/traceback_matching.py,sha256=LheOEqCvrBDrZJm4RK4ZFFcebWs4ggP7qRiUPg8HQpM,6926
|
|
13
|
-
pytest_flakefighters-0.2.3.dist-info/licenses/LICENSE,sha256=tTzR2CWQMPOp-mQIQqi0cTRkaogeBUmW06blQsBLdQg,1082
|
|
14
|
-
pytest_flakefighters-0.2.3.dist-info/METADATA,sha256=k4LadBPIqRWtTIkjOtj1R_KQE8_zUh7ldLPvcETfvOI,5507
|
|
15
|
-
pytest_flakefighters-0.2.3.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
16
|
-
pytest_flakefighters-0.2.3.dist-info/entry_points.txt,sha256=xockvN1AszN2XqaET77JDIRdOafgm3DdvOtHgVw-aDU,424
|
|
17
|
-
pytest_flakefighters-0.2.3.dist-info/top_level.txt,sha256=bWwe0xVZ_l2CP8KSnztW6FafZKQZ7zUTMa1U32geY28,30
|
|
18
|
-
pytest_flakefighters-0.2.3.dist-info/RECORD,,
|
{pytest_flakefighters-0.2.3.dist-info → pytest_flakefighters-0.3.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{pytest_flakefighters-0.2.3.dist-info → pytest_flakefighters-0.3.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|