smart-tests-cli 2.0.0__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.
- smart_tests/__init__.py +0 -0
- smart_tests/__main__.py +60 -0
- smart_tests/app.py +67 -0
- smart_tests/args4p/README.md +102 -0
- smart_tests/args4p/__init__.py +13 -0
- smart_tests/args4p/argument.py +45 -0
- smart_tests/args4p/command.py +593 -0
- smart_tests/args4p/converters/__init__.py +75 -0
- smart_tests/args4p/decorators.py +98 -0
- smart_tests/args4p/exceptions.py +12 -0
- smart_tests/args4p/option.py +85 -0
- smart_tests/args4p/parameter.py +84 -0
- smart_tests/args4p/typer/__init__.py +42 -0
- smart_tests/commands/__init__.py +0 -0
- smart_tests/commands/compare/__init__.py +11 -0
- smart_tests/commands/compare/subsets.py +58 -0
- smart_tests/commands/detect_flakes.py +105 -0
- smart_tests/commands/inspect/__init__.py +13 -0
- smart_tests/commands/inspect/model.py +52 -0
- smart_tests/commands/inspect/subset.py +138 -0
- smart_tests/commands/record/__init__.py +19 -0
- smart_tests/commands/record/attachment.py +38 -0
- smart_tests/commands/record/build.py +356 -0
- smart_tests/commands/record/case_event.py +190 -0
- smart_tests/commands/record/commit.py +157 -0
- smart_tests/commands/record/session.py +120 -0
- smart_tests/commands/record/tests.py +498 -0
- smart_tests/commands/stats/__init__.py +11 -0
- smart_tests/commands/stats/test_sessions.py +45 -0
- smart_tests/commands/subset.py +567 -0
- smart_tests/commands/test_path_writer.py +51 -0
- smart_tests/commands/verify.py +153 -0
- smart_tests/jar/exe_deploy.jar +0 -0
- smart_tests/plugins/__init__.py +0 -0
- smart_tests/test_runners/__init__.py +0 -0
- smart_tests/test_runners/adb.py +24 -0
- smart_tests/test_runners/ant.py +35 -0
- smart_tests/test_runners/bazel.py +103 -0
- smart_tests/test_runners/behave.py +62 -0
- smart_tests/test_runners/codeceptjs.py +33 -0
- smart_tests/test_runners/ctest.py +164 -0
- smart_tests/test_runners/cts.py +189 -0
- smart_tests/test_runners/cucumber.py +451 -0
- smart_tests/test_runners/cypress.py +46 -0
- smart_tests/test_runners/dotnet.py +106 -0
- smart_tests/test_runners/file.py +20 -0
- smart_tests/test_runners/flutter.py +251 -0
- smart_tests/test_runners/go_test.py +99 -0
- smart_tests/test_runners/googletest.py +34 -0
- smart_tests/test_runners/gradle.py +96 -0
- smart_tests/test_runners/jest.py +52 -0
- smart_tests/test_runners/maven.py +149 -0
- smart_tests/test_runners/minitest.py +40 -0
- smart_tests/test_runners/nunit.py +190 -0
- smart_tests/test_runners/playwright.py +252 -0
- smart_tests/test_runners/prove.py +74 -0
- smart_tests/test_runners/pytest.py +358 -0
- smart_tests/test_runners/raw.py +238 -0
- smart_tests/test_runners/robot.py +125 -0
- smart_tests/test_runners/rspec.py +5 -0
- smart_tests/test_runners/smart_tests.py +235 -0
- smart_tests/test_runners/vitest.py +49 -0
- smart_tests/test_runners/xctest.py +79 -0
- smart_tests/testpath.py +154 -0
- smart_tests/utils/__init__.py +0 -0
- smart_tests/utils/authentication.py +78 -0
- smart_tests/utils/ci_provider.py +7 -0
- smart_tests/utils/commands.py +14 -0
- smart_tests/utils/commit_ingester.py +59 -0
- smart_tests/utils/common_tz.py +12 -0
- smart_tests/utils/edit_distance.py +11 -0
- smart_tests/utils/env_keys.py +19 -0
- smart_tests/utils/exceptions.py +34 -0
- smart_tests/utils/fail_fast_mode.py +99 -0
- smart_tests/utils/file_name_pattern.py +4 -0
- smart_tests/utils/git_log_parser.py +53 -0
- smart_tests/utils/glob.py +44 -0
- smart_tests/utils/gzipgen.py +46 -0
- smart_tests/utils/http_client.py +169 -0
- smart_tests/utils/java.py +61 -0
- smart_tests/utils/link.py +149 -0
- smart_tests/utils/logger.py +53 -0
- smart_tests/utils/no_build.py +2 -0
- smart_tests/utils/sax.py +119 -0
- smart_tests/utils/session.py +73 -0
- smart_tests/utils/smart_tests_client.py +134 -0
- smart_tests/utils/subprocess.py +12 -0
- smart_tests/utils/tracking.py +95 -0
- smart_tests/utils/typer_types.py +241 -0
- smart_tests/version.py +7 -0
- smart_tests_cli-2.0.0.dist-info/METADATA +168 -0
- smart_tests_cli-2.0.0.dist-info/RECORD +96 -0
- smart_tests_cli-2.0.0.dist-info/WHEEL +5 -0
- smart_tests_cli-2.0.0.dist-info/entry_points.txt +2 -0
- smart_tests_cli-2.0.0.dist-info/licenses/LICENSE.txt +202 -0
- smart_tests_cli-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import os
|
|
3
|
+
from typing import Annotated, List
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
import smart_tests.args4p.typer as typer
|
|
8
|
+
from smart_tests.commands.record.tests import RecordTests
|
|
9
|
+
from smart_tests.commands.subset import Subset
|
|
10
|
+
from smart_tests.test_runners import smart_tests
|
|
11
|
+
from smart_tests.test_runners.nunit import nunit_parse_func
|
|
12
|
+
from smart_tests.testpath import TestPath
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# main subset logic
|
|
16
|
+
def do_subset(client: Subset, bare):
|
|
17
|
+
if bare:
|
|
18
|
+
separator = "\n"
|
|
19
|
+
prefix = ""
|
|
20
|
+
else:
|
|
21
|
+
# LEGACY: we recommend the bare mode with native NUnit integration
|
|
22
|
+
# ref: https://github.com/Microsoft/vstest-docs/blob/main/docs/filter.md
|
|
23
|
+
separator = "|"
|
|
24
|
+
prefix = "FullyQualifiedName="
|
|
25
|
+
|
|
26
|
+
if client.is_output_exclusion_rules:
|
|
27
|
+
separator = "&"
|
|
28
|
+
prefix = "FullyQualifiedName!="
|
|
29
|
+
|
|
30
|
+
def formatter(test_path: TestPath):
|
|
31
|
+
paths = []
|
|
32
|
+
|
|
33
|
+
for path in test_path:
|
|
34
|
+
t = path.get("type", "")
|
|
35
|
+
if t == 'Assembly':
|
|
36
|
+
continue
|
|
37
|
+
if t == 'ParameterizedMethod':
|
|
38
|
+
# For parameterized test, we get something like
|
|
39
|
+
# Assembly=calc.dll#TestSuite=SomeNamespace#TestSuite=TestClassName#ParameterizedMethod=DivideTest#TestCase=DivideTest(1,3)
|
|
40
|
+
# see record_test_result.json as an example.
|
|
41
|
+
continue
|
|
42
|
+
paths.append(path.get("name", ""))
|
|
43
|
+
|
|
44
|
+
return prefix + ".".join(paths)
|
|
45
|
+
|
|
46
|
+
def exclusion_output_handler(subset_tests: List[TestPath], rest_tests: List[TestPath]):
|
|
47
|
+
if client.rest:
|
|
48
|
+
with open(client.rest, "w+", encoding="utf-8") as fp:
|
|
49
|
+
fp.write(client.separator.join(formatter(t) for t in subset_tests))
|
|
50
|
+
|
|
51
|
+
click.echo(client.separator.join(formatter(t) for t in rest_tests))
|
|
52
|
+
|
|
53
|
+
client.separator = separator
|
|
54
|
+
client.formatter = formatter
|
|
55
|
+
client.exclusion_output_handler = exclusion_output_handler
|
|
56
|
+
client.run()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@smart_tests.subset
|
|
60
|
+
def subset(
|
|
61
|
+
client: Subset,
|
|
62
|
+
bare: Annotated[bool, typer.Option(
|
|
63
|
+
"--bare",
|
|
64
|
+
help="outputs class names alone"
|
|
65
|
+
)] = False,
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
Alpha: Supports only Zero Input Subsetting
|
|
69
|
+
"""
|
|
70
|
+
if not client.is_get_tests_from_previous_sessions:
|
|
71
|
+
click.secho(
|
|
72
|
+
"The dotnet profile only supports Zero Input Subsetting.\nMake sure to use "
|
|
73
|
+
"`--get-tests-from-previous-sessions` option",
|
|
74
|
+
fg='red',
|
|
75
|
+
err=True)
|
|
76
|
+
raise typer.Exit(1)
|
|
77
|
+
|
|
78
|
+
do_subset(client, bare)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@smart_tests.record.tests
|
|
82
|
+
def record_tests(
|
|
83
|
+
client: RecordTests,
|
|
84
|
+
files: Annotated[List[str], typer.Argument(
|
|
85
|
+
multiple=True,
|
|
86
|
+
help="Test report files to process"
|
|
87
|
+
)],
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
Alpha: Supports only NUnit report formats.
|
|
91
|
+
"""
|
|
92
|
+
for file in files:
|
|
93
|
+
match = False
|
|
94
|
+
for t in glob.iglob(file, recursive=True):
|
|
95
|
+
match = True
|
|
96
|
+
if os.path.isdir(t):
|
|
97
|
+
client.scan(t, "*.xml")
|
|
98
|
+
else:
|
|
99
|
+
client.report(t)
|
|
100
|
+
if not match:
|
|
101
|
+
click.echo(f"No matches found: {file}", err=True)
|
|
102
|
+
|
|
103
|
+
# Note: we support only Nunit test report format now.
|
|
104
|
+
# If we need to support another format e.g) JUnit, trc, then we'll add a option
|
|
105
|
+
client.parse_func = nunit_parse_func
|
|
106
|
+
client.run()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#
|
|
2
|
+
# The most bare-bone versions of the test runner support
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
from ..commands.subset import Subset
|
|
6
|
+
from . import smart_tests
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@smart_tests.subset
|
|
10
|
+
def subset(client: Subset):
|
|
11
|
+
# read lines as test file names
|
|
12
|
+
for t in client.stdin():
|
|
13
|
+
client.test_path(t.rstrip("\n"))
|
|
14
|
+
|
|
15
|
+
client.run()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
record_tests = smart_tests.CommonRecordTestImpls(__name__).file_profile_report_files()
|
|
19
|
+
smart_tests.CommonDetectFlakesImpls(__name__).detect_flakes()
|
|
20
|
+
# split_subset = launchable.CommonSplitSubsetImpls(__name__).split_subset()
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pathlib
|
|
3
|
+
from typing import Annotated, Dict, List
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
import smart_tests.args4p.typer as typer
|
|
8
|
+
from smart_tests.commands.record.case_event import CaseEvent, CaseEventGenerator
|
|
9
|
+
from smart_tests.testpath import FilePathNormalizer
|
|
10
|
+
|
|
11
|
+
from ..commands.record.tests import RecordTests
|
|
12
|
+
from . import smart_tests
|
|
13
|
+
|
|
14
|
+
FLUTTER_FILE_EXT = "_test.dart"
|
|
15
|
+
|
|
16
|
+
FLUTTER_TEST_RESULT_SUCCESS = "success"
|
|
17
|
+
FLUTTER_TEST_RESULT_FAILURE = "error"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestCase:
|
|
21
|
+
def __init__(self, id: int, name: str, is_skipped: bool = False):
|
|
22
|
+
self._id: int = id
|
|
23
|
+
self._name: str = name
|
|
24
|
+
self._is_skipped: bool = is_skipped
|
|
25
|
+
self._status: str = ""
|
|
26
|
+
self._stdout: str = ""
|
|
27
|
+
self._stderr: str = ""
|
|
28
|
+
self._duration_sec: float = 0
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def id(self):
|
|
32
|
+
return self._id
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def name(self):
|
|
36
|
+
return self._name
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def status(self) -> int: # status code see: case_event.py
|
|
40
|
+
if self._is_skipped:
|
|
41
|
+
return CaseEvent.TEST_SKIPPED
|
|
42
|
+
elif self._status == FLUTTER_TEST_RESULT_SUCCESS:
|
|
43
|
+
return CaseEvent.TEST_PASSED
|
|
44
|
+
elif self._status == FLUTTER_TEST_RESULT_FAILURE:
|
|
45
|
+
return CaseEvent.TEST_FAILED
|
|
46
|
+
|
|
47
|
+
# safe fallback
|
|
48
|
+
return CaseEvent.TEST_PASSED
|
|
49
|
+
|
|
50
|
+
@status.setter
|
|
51
|
+
def status(self, status: str):
|
|
52
|
+
self._status = status
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def duration(self) -> float:
|
|
56
|
+
return self._duration_sec
|
|
57
|
+
|
|
58
|
+
@duration.setter
|
|
59
|
+
def duration(self, duration_sec: float):
|
|
60
|
+
self._duration_sec = duration_sec
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def stdout(self) -> str:
|
|
64
|
+
return self._stdout
|
|
65
|
+
|
|
66
|
+
@stdout.setter
|
|
67
|
+
def stdout(self, stdout: str):
|
|
68
|
+
self._stdout = stdout
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def stderr(self) -> str:
|
|
72
|
+
return self._stderr
|
|
73
|
+
|
|
74
|
+
@stderr.setter
|
|
75
|
+
def stderr(self, stderr: str):
|
|
76
|
+
self._stderr = stderr
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestSuite:
|
|
80
|
+
def __init__(self, id: int, platform: str, path: str):
|
|
81
|
+
self._id = id
|
|
82
|
+
self._platform = platform
|
|
83
|
+
self._path = path
|
|
84
|
+
self._test_cases: Dict[int, TestCase] = {}
|
|
85
|
+
|
|
86
|
+
def _get_test_case(self, id: int) -> TestCase | None:
|
|
87
|
+
return self._test_cases.get(id)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ReportParser:
|
|
91
|
+
def __init__(self, file_path_normalizer: FilePathNormalizer):
|
|
92
|
+
self.file_path_normalizer = file_path_normalizer
|
|
93
|
+
self._suites: Dict[int, TestSuite] = {}
|
|
94
|
+
|
|
95
|
+
def _get_suite(self, suite_id: int) -> TestSuite | None:
|
|
96
|
+
return self._suites.get(suite_id)
|
|
97
|
+
|
|
98
|
+
def _get_test(self, test_id: int) -> TestCase | None:
|
|
99
|
+
if test_id is None:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
for s in self._suites.values():
|
|
103
|
+
c = s._get_test_case(test_id)
|
|
104
|
+
if c is not None:
|
|
105
|
+
return c
|
|
106
|
+
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def _events(self) -> List:
|
|
110
|
+
events = []
|
|
111
|
+
for s in self._suites.values():
|
|
112
|
+
for c in s._test_cases.values():
|
|
113
|
+
events.append(CaseEvent.create(
|
|
114
|
+
test_path=[
|
|
115
|
+
{"type": "file", "name": pathlib.Path(self.file_path_normalizer.relativize(s._path)).as_posix()},
|
|
116
|
+
{"type": "testcase", "name": c.name}],
|
|
117
|
+
duration_secs=c.duration,
|
|
118
|
+
status=c.status,
|
|
119
|
+
stdout=c.stdout,
|
|
120
|
+
stderr=c.stderr,
|
|
121
|
+
))
|
|
122
|
+
|
|
123
|
+
return events
|
|
124
|
+
|
|
125
|
+
def parse_func(self, report_file: str) -> CaseEventGenerator:
|
|
126
|
+
# TODO: Support cases that include information about `flutter pub get`
|
|
127
|
+
# see detail: https://github.com/launchableinc/examples/actions/runs/11884312142/job/33112309450
|
|
128
|
+
if not pathlib.Path(report_file).exists():
|
|
129
|
+
click.secho(f"Error: Report file not found: {report_file}", fg='red', err=True)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
with open(report_file, "r") as ndjson:
|
|
133
|
+
try:
|
|
134
|
+
for j in ndjson:
|
|
135
|
+
if not j.strip():
|
|
136
|
+
continue
|
|
137
|
+
try:
|
|
138
|
+
data = json.loads(j)
|
|
139
|
+
self._parse_json(data)
|
|
140
|
+
except json.JSONDecodeError:
|
|
141
|
+
click.secho(
|
|
142
|
+
f"Error: Invalid JSON format: {j}. Skip load this line",
|
|
143
|
+
fg='yellow',
|
|
144
|
+
err=True)
|
|
145
|
+
continue
|
|
146
|
+
except Exception as e:
|
|
147
|
+
click.secho(
|
|
148
|
+
f"Error: Failed to parse the report file: {report_file} : {e}", fg='red', err=True)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
for event in self._events():
|
|
152
|
+
yield event
|
|
153
|
+
|
|
154
|
+
def _parse_json(self, data: Dict):
|
|
155
|
+
if not isinstance(data, Dict):
|
|
156
|
+
# Note: array sometimes comes in but won't use it
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
data_type = data.get("type")
|
|
160
|
+
if data_type is None:
|
|
161
|
+
return
|
|
162
|
+
elif data_type == "suite":
|
|
163
|
+
suite_data = data.get("suite")
|
|
164
|
+
if suite_data is None:
|
|
165
|
+
# it's might be invalid suite data
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
suite_id = suite_data.get("id")
|
|
169
|
+
if self._get_suite(suite_data.get("id")) is not None:
|
|
170
|
+
# already recorded
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
self._suites[suite_id] = TestSuite(suite_id, suite_data.get("platform"), suite_data.get("path"))
|
|
174
|
+
elif data_type == "testStart":
|
|
175
|
+
test_data = data.get("test")
|
|
176
|
+
|
|
177
|
+
if test_data is None:
|
|
178
|
+
# it's might be invalid test data
|
|
179
|
+
return
|
|
180
|
+
if test_data.get("line") is None:
|
|
181
|
+
# Still set up test case, should skip
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
suite_id = test_data.get("suiteID")
|
|
185
|
+
suite = self._get_suite(suite_id)
|
|
186
|
+
|
|
187
|
+
if suite_id is None or suite is None:
|
|
188
|
+
click.secho(
|
|
189
|
+
f"Warning: Cannot find a parent test suite(id: {suite_id}). So won't send test result of "
|
|
190
|
+
f"{test_data.get('name')}", fg='yellow', err=True)
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
test_id = test_data.get("id")
|
|
194
|
+
test = self._get_test(test_id)
|
|
195
|
+
if test is not None:
|
|
196
|
+
# already recorded
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
name = test_data.get("name")
|
|
200
|
+
metadata = test_data.get("metadata", {})
|
|
201
|
+
is_skipped = metadata.get("skip", False)
|
|
202
|
+
suite._test_cases[test_id] = TestCase(test_id, name, is_skipped)
|
|
203
|
+
|
|
204
|
+
elif data_type == "testDone":
|
|
205
|
+
test_id = data.get("testID", 0)
|
|
206
|
+
test = self._get_test(test_id)
|
|
207
|
+
|
|
208
|
+
if test is None:
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
test.status = data.get("result", "success") # safe fallback
|
|
212
|
+
duration_msec = data.get("time", 0)
|
|
213
|
+
test.duration = duration_msec / 1000 # to sec
|
|
214
|
+
|
|
215
|
+
elif data_type == "error":
|
|
216
|
+
test_id = data.get("testID", 0)
|
|
217
|
+
test = self._get_test(test_id)
|
|
218
|
+
if test is None:
|
|
219
|
+
click.secho(
|
|
220
|
+
f"Warning: Cannot find a test (id: {test_id}). So we skip update stderr", fg='yellow',
|
|
221
|
+
err=True)
|
|
222
|
+
return
|
|
223
|
+
test.stderr += ("\n" if test.stderr else "") + data.get("error", "")
|
|
224
|
+
|
|
225
|
+
elif data_type == "print":
|
|
226
|
+
# It's difficult to identify the "Retry" case because Flutter reports it with the same test ID
|
|
227
|
+
# So we won't handle it at the moment.
|
|
228
|
+
test_id = data.get("testID", 0)
|
|
229
|
+
test = self._get_test(test_id)
|
|
230
|
+
if test is None:
|
|
231
|
+
click.secho(
|
|
232
|
+
f"Warning: Cannot find a test (id: {test_id}). So we skip update stdout", fg='yellow',
|
|
233
|
+
err=True)
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
test.stdout += ("\n" if test.stdout else "") + data.get("message", "")
|
|
237
|
+
|
|
238
|
+
else:
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@smart_tests.record.tests
|
|
243
|
+
def record_tests(client: RecordTests,
|
|
244
|
+
reports: Annotated[List[str], typer.Argument(required=True, multiple=True)]):
|
|
245
|
+
file_path_normalizer = FilePathNormalizer(base_path=client.base_path, no_base_path_inference=client.no_base_path_inference)
|
|
246
|
+
client.parse_func = ReportParser(file_path_normalizer).parse_func
|
|
247
|
+
|
|
248
|
+
smart_tests.CommonRecordTestImpls.load_report_files(client=client, source_roots=reports)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
subset = smart_tests.CommonSubsetImpls(__name__).scan_files('*.dart')
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from typing import Annotated, Dict, List
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from junitparser import TestCase, TestSuite # type: ignore
|
|
8
|
+
|
|
9
|
+
import smart_tests.args4p.typer as typer
|
|
10
|
+
|
|
11
|
+
from ..commands.record.tests import RecordTests
|
|
12
|
+
from ..commands.subset import Subset
|
|
13
|
+
from ..testpath import TestPath
|
|
14
|
+
from ..utils.logger import Logger
|
|
15
|
+
from . import smart_tests
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@smart_tests.subset
|
|
19
|
+
def subset(client: Subset):
|
|
20
|
+
logger = Logger()
|
|
21
|
+
|
|
22
|
+
# NOTE: This should be using package name + test function name to specify
|
|
23
|
+
# which tests to run. However, the initial integration is created so that we
|
|
24
|
+
# only specify the test function names. This can result in matching some
|
|
25
|
+
# extra tests in multiple packages. However, in order to keep the initial
|
|
26
|
+
# way of the integration, we cannot change this. Try to do the best.
|
|
27
|
+
test_cases = []
|
|
28
|
+
pattern = re.compile('\\s+')
|
|
29
|
+
for line in client.stdin():
|
|
30
|
+
if ' ' not in line:
|
|
31
|
+
test_cases.append(line.strip('\n'))
|
|
32
|
+
else:
|
|
33
|
+
parts = pattern.split(line)
|
|
34
|
+
if len(parts) >= 2:
|
|
35
|
+
package = parts[1].split('/')[-1]
|
|
36
|
+
for test_case in test_cases:
|
|
37
|
+
client.test_path([{'type': 'class', 'name': package}, {
|
|
38
|
+
'type': 'testcase', 'name': test_case}])
|
|
39
|
+
else:
|
|
40
|
+
logger.warning("Cannot extract the package from the input. This may result in missing some tests.")
|
|
41
|
+
test_cases = []
|
|
42
|
+
client.formatter = lambda x: f"^{x[1]['name']}$"
|
|
43
|
+
client.separator = '|'
|
|
44
|
+
client.run()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@smart_tests.record.tests
|
|
48
|
+
def record_tests(
|
|
49
|
+
client: RecordTests,
|
|
50
|
+
source_roots: Annotated[List[str], typer.Argument(
|
|
51
|
+
multiple=True,
|
|
52
|
+
help="Source root directories or files to process"
|
|
53
|
+
)],
|
|
54
|
+
):
|
|
55
|
+
for root in source_roots:
|
|
56
|
+
match = False
|
|
57
|
+
for t in glob.iglob(root, recursive=True):
|
|
58
|
+
match = True
|
|
59
|
+
if os.path.isdir(t):
|
|
60
|
+
client.scan(t, "*.xml")
|
|
61
|
+
else:
|
|
62
|
+
client.report(t)
|
|
63
|
+
|
|
64
|
+
if not match:
|
|
65
|
+
click.echo(f"No matches found: {root}", err=True)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
default_path_builder = client.path_builder
|
|
69
|
+
|
|
70
|
+
def path_builder(case: TestCase, suite: TestSuite, report_file: str) -> TestPath:
|
|
71
|
+
tp = default_path_builder(case, suite, report_file)
|
|
72
|
+
for tpc in tp:
|
|
73
|
+
if 'type' in tpc and 'name' in tpc:
|
|
74
|
+
if tpc['type'] == 'class':
|
|
75
|
+
# go-junit-report v2 reports full package name. go-junit-report
|
|
76
|
+
# v1 reports only the last component of the package name. In
|
|
77
|
+
# order to make this backward compatible, we align this to v1.
|
|
78
|
+
tpc['name'] = tpc['name'].split('/')[-1]
|
|
79
|
+
if tpc['type'] == 'testcase' and '/' in tpc['name']:
|
|
80
|
+
# go-junit-report produces test with subtests like `<TEST NAME?/<SUBTEST NAME>`
|
|
81
|
+
# But `go test -list` command doesn't include subtest names
|
|
82
|
+
# So, we split the name by '/' and save testcase and subtest
|
|
83
|
+
names = tpc['name'].split('/')
|
|
84
|
+
tpc['name'] = names[0]
|
|
85
|
+
tp.append({
|
|
86
|
+
'type': 'subtest',
|
|
87
|
+
'name': '/'.join(names[1:]) if len(names) > 1 else ''
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return tp
|
|
91
|
+
|
|
92
|
+
client.path_builder = path_builder
|
|
93
|
+
client.run()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def format_same_bin(s: str) -> List[Dict[str, str]]:
|
|
97
|
+
t = s.split(".")
|
|
98
|
+
return [{"type": "class", "name": t[0]},
|
|
99
|
+
{"type": "testcase", "name": t[1]}]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from ..commands.subset import Subset
|
|
4
|
+
from ..testpath import TestPath
|
|
5
|
+
from . import smart_tests
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def make_test_path(cls, case) -> TestPath:
|
|
9
|
+
return [{'type': 'class', 'name': cls}, {'type': 'testcase', 'name': case}]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@smart_tests.subset
|
|
13
|
+
def subset(client: Subset):
|
|
14
|
+
cls = ''
|
|
15
|
+
class_pattern = re.compile(r'^([^\.]+)\.')
|
|
16
|
+
case_pattern = re.compile(r'^ ([^ ]+)')
|
|
17
|
+
for label in map(str.rstrip, client.stdin()):
|
|
18
|
+
# handle Google Test's --gtest_list_tests output
|
|
19
|
+
# FooTest.
|
|
20
|
+
# Bar
|
|
21
|
+
# Baz
|
|
22
|
+
gtest_class = class_pattern.match(label)
|
|
23
|
+
if gtest_class:
|
|
24
|
+
cls = gtest_class.group(1)
|
|
25
|
+
gtest_case = case_pattern.match(label)
|
|
26
|
+
if gtest_case and cls:
|
|
27
|
+
case = gtest_case.group(1)
|
|
28
|
+
client.test_path(make_test_path(cls, case))
|
|
29
|
+
|
|
30
|
+
client.formatter = lambda x: x[0]['name'] + "." + x[1]['name']
|
|
31
|
+
client.run()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
record_tests = smart_tests.CommonRecordTestImpls(__name__).report_files()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Annotated, List
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
import smart_tests.args4p.typer as typer
|
|
7
|
+
from smart_tests.utils.java import junit5_nested_class_path_builder
|
|
8
|
+
|
|
9
|
+
from ..args4p.exceptions import BadCmdLineException
|
|
10
|
+
from ..commands.record.tests import RecordTests
|
|
11
|
+
from ..commands.subset import Subset
|
|
12
|
+
from ..utils.file_name_pattern import jvm_test_pattern
|
|
13
|
+
from . import smart_tests
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@smart_tests.subset
|
|
17
|
+
def subset(
|
|
18
|
+
client: Subset,
|
|
19
|
+
source_roots: Annotated[List[str] | None, typer.Argument(
|
|
20
|
+
multiple=True,
|
|
21
|
+
required=False,
|
|
22
|
+
help="Source root directories to scan for tests"
|
|
23
|
+
)] = None,
|
|
24
|
+
bare: Annotated[bool, typer.Option(
|
|
25
|
+
"--bare",
|
|
26
|
+
help="outputs class names alone"
|
|
27
|
+
)] = False,
|
|
28
|
+
):
|
|
29
|
+
def file2test(f: str):
|
|
30
|
+
if jvm_test_pattern.match(f):
|
|
31
|
+
f = f[:f.rindex('.')] # remove extension
|
|
32
|
+
# directory -> package name conversion
|
|
33
|
+
cls_name = f.replace(os.path.sep, '.')
|
|
34
|
+
return [{"type": "class", "name": cls_name}]
|
|
35
|
+
else:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
# Handle None source_roots - convert to empty list
|
|
39
|
+
if source_roots is None:
|
|
40
|
+
source_roots = []
|
|
41
|
+
|
|
42
|
+
if client.is_get_tests_from_previous_sessions:
|
|
43
|
+
if len(source_roots) != 0:
|
|
44
|
+
click.secho(
|
|
45
|
+
"Warning: SOURCE_ROOTS are ignored when --get-tests-from-previous-sessions is used",
|
|
46
|
+
fg='yellow', err=True)
|
|
47
|
+
# Always set to empty list when getting tests from previous sessions
|
|
48
|
+
source_roots = []
|
|
49
|
+
else:
|
|
50
|
+
if len(source_roots) == 0:
|
|
51
|
+
raise BadCmdLineException("Error: Missing argument 'SOURCE_ROOTS...'")
|
|
52
|
+
|
|
53
|
+
# Only scan if we have source roots
|
|
54
|
+
for root in source_roots:
|
|
55
|
+
client.scan(root, '**/*', file2test)
|
|
56
|
+
|
|
57
|
+
def exclusion_output_handler(subset_tests, rest_tests):
|
|
58
|
+
if client.rest:
|
|
59
|
+
with open(client.rest, "w+", encoding="utf-8") as fp:
|
|
60
|
+
if not bare and len(rest_tests) == 0:
|
|
61
|
+
# This prevents the CLI output to be evaled as an empty
|
|
62
|
+
# string argument.
|
|
63
|
+
fp.write('-PdummyPlaceHolder')
|
|
64
|
+
else:
|
|
65
|
+
fp.write(client.separator.join(client.formatter(t) for t in rest_tests))
|
|
66
|
+
|
|
67
|
+
classes = [to_class_file(tp[0]['name']) for tp in rest_tests]
|
|
68
|
+
if bare:
|
|
69
|
+
click.echo(','.join(classes))
|
|
70
|
+
else:
|
|
71
|
+
click.echo('-PexcludeTests=' + (','.join(classes)))
|
|
72
|
+
client.exclusion_output_handler = exclusion_output_handler
|
|
73
|
+
|
|
74
|
+
if bare:
|
|
75
|
+
client.formatter = lambda x: x[0]['name']
|
|
76
|
+
else:
|
|
77
|
+
client.formatter = lambda x: f"--tests {x[0]['name']}"
|
|
78
|
+
client.separator = ' '
|
|
79
|
+
|
|
80
|
+
client.run()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def to_class_file(class_name: str):
|
|
84
|
+
return class_name.replace('.', '/') + '.class'
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@smart_tests.record.tests
|
|
88
|
+
def record_tests(
|
|
89
|
+
client: RecordTests,
|
|
90
|
+
reports: Annotated[List[str], typer.Argument(
|
|
91
|
+
multiple=True,
|
|
92
|
+
help="Test report files to process"
|
|
93
|
+
)],
|
|
94
|
+
):
|
|
95
|
+
client.path_builder = junit5_nested_class_path_builder(client.path_builder)
|
|
96
|
+
smart_tests.CommonRecordTestImpls.load_report_files(client=client, source_roots=reports)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Annotated, List
|
|
2
|
+
|
|
3
|
+
from junitparser import TestCase, TestSuite # type: ignore
|
|
4
|
+
|
|
5
|
+
import smart_tests.args4p.typer as typer
|
|
6
|
+
from smart_tests.testpath import TestPath
|
|
7
|
+
|
|
8
|
+
from ..args4p.exceptions import BadCmdLineException
|
|
9
|
+
from ..commands.record.tests import RecordTests
|
|
10
|
+
from ..commands.subset import Subset
|
|
11
|
+
from . import smart_tests
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def path_builder(case: TestCase, suite: TestSuite, report_file: str) -> TestPath:
|
|
15
|
+
test_path = []
|
|
16
|
+
if suite.name:
|
|
17
|
+
test_path.append({"type": "file", "name": suite.name})
|
|
18
|
+
|
|
19
|
+
if case.classname:
|
|
20
|
+
test_path.append({"type": "class", "name": case.classname})
|
|
21
|
+
|
|
22
|
+
if case.name:
|
|
23
|
+
test_path.append({"type": "testcase", "name": case.name})
|
|
24
|
+
|
|
25
|
+
return test_path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@smart_tests.record.tests
|
|
29
|
+
def record_tests(
|
|
30
|
+
client: RecordTests,
|
|
31
|
+
reports: Annotated[List[str], typer.Argument(
|
|
32
|
+
multiple=True,
|
|
33
|
+
help="Test report files to process"
|
|
34
|
+
)],
|
|
35
|
+
):
|
|
36
|
+
for r in reports:
|
|
37
|
+
client.report(r)
|
|
38
|
+
|
|
39
|
+
client.path_builder = path_builder
|
|
40
|
+
client.run()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@smart_tests.subset
|
|
44
|
+
def subset(client: Subset):
|
|
45
|
+
if client.base_path is None:
|
|
46
|
+
raise BadCmdLineException("Please specify base path")
|
|
47
|
+
|
|
48
|
+
for line in client.stdin():
|
|
49
|
+
if len(line.strip()) and not line.startswith(">"):
|
|
50
|
+
client.test_path(line.rstrip("\n"))
|
|
51
|
+
|
|
52
|
+
client.run()
|