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,190 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Any, Callable, Dict, Generator
|
|
4
|
+
|
|
5
|
+
import dateutil.parser
|
|
6
|
+
from dateutil.tz import tzlocal
|
|
7
|
+
from junitparser import Error, Failure, IntAttr, Skipped, TestCase, TestSuite
|
|
8
|
+
|
|
9
|
+
from smart_tests.utils.common_tz import COMMON_TIMEZONES # type: ignore
|
|
10
|
+
|
|
11
|
+
from ...testpath import FilePathNormalizer, TestPath
|
|
12
|
+
|
|
13
|
+
POSSIBLE_RESULTS = (Failure, Error, Skipped)
|
|
14
|
+
|
|
15
|
+
CaseEventType = Dict[str, str]
|
|
16
|
+
CaseEventGenerator = Generator[CaseEventType, None, None]
|
|
17
|
+
|
|
18
|
+
# function that computes TestPath from a test case
|
|
19
|
+
# The 3rd argument is the report file path
|
|
20
|
+
TestPathBuilder = Callable[[TestCase, TestSuite, str], TestPath]
|
|
21
|
+
|
|
22
|
+
DataBuilder = Callable[[TestCase], Dict[str, Any] | None]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CaseEvent:
|
|
26
|
+
EVENT_TYPE = "case"
|
|
27
|
+
TEST_SKIPPED = 2
|
|
28
|
+
TEST_PASSED = 1
|
|
29
|
+
TEST_FAILED = 0
|
|
30
|
+
|
|
31
|
+
STATUS_MAP = {
|
|
32
|
+
'TEST_SKIPPED': TEST_SKIPPED,
|
|
33
|
+
'TEST_PASSED': TEST_PASSED,
|
|
34
|
+
'TEST_FAILED': TEST_FAILED,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def default_path_builder(
|
|
39
|
+
file_path_normalizer: FilePathNormalizer) -> TestPathBuilder:
|
|
40
|
+
"""
|
|
41
|
+
Obtains a default TestPathBuilder that uses a base directory to relativize the file name
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def f(case: TestCase, suite: TestSuite, report_file: str) -> TestPath:
|
|
45
|
+
classname = case._elem.attrib.get("classname") or suite._elem.attrib.get("classname")
|
|
46
|
+
filepath = case._elem.attrib.get("file") or suite._elem.attrib.get("filepath")
|
|
47
|
+
if filepath:
|
|
48
|
+
filepath = file_path_normalizer.relativize(filepath)
|
|
49
|
+
|
|
50
|
+
test_path = []
|
|
51
|
+
if filepath:
|
|
52
|
+
test_path.append({"type": "file", "name": filepath})
|
|
53
|
+
if classname:
|
|
54
|
+
test_path.append({"type": "class", "name": classname})
|
|
55
|
+
if case.name:
|
|
56
|
+
test_path.append({"type": "testcase", "name": case._elem.attrib.get("name")})
|
|
57
|
+
return test_path
|
|
58
|
+
|
|
59
|
+
return f
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def default_data_builder() -> DataBuilder:
|
|
63
|
+
def f(case: TestCase):
|
|
64
|
+
"""
|
|
65
|
+
case for:
|
|
66
|
+
<testcase ... file="tests/commands/inspect/test_tests.py" line="133">
|
|
67
|
+
</testcase>
|
|
68
|
+
"""
|
|
69
|
+
metadata = MetadataTestCase.fromelem(case)
|
|
70
|
+
if metadata and metadata.line is not None:
|
|
71
|
+
return {
|
|
72
|
+
"lineNumber": metadata.line
|
|
73
|
+
}
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
return f
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_case_and_suite(
|
|
80
|
+
cls,
|
|
81
|
+
path_builder: TestPathBuilder,
|
|
82
|
+
case: TestCase,
|
|
83
|
+
suite: TestSuite,
|
|
84
|
+
report_file: str,
|
|
85
|
+
data_builder: DataBuilder
|
|
86
|
+
) -> Dict:
|
|
87
|
+
"Builds a JSON representation of CaseEvent from JUnitPaser objects"
|
|
88
|
+
|
|
89
|
+
# TODO: reconsider the initial value of the status.
|
|
90
|
+
status = CaseEvent.TEST_PASSED
|
|
91
|
+
for r in case.result:
|
|
92
|
+
if any(isinstance(r, s) for s in (Failure, Error)):
|
|
93
|
+
status = CaseEvent.TEST_FAILED
|
|
94
|
+
break
|
|
95
|
+
elif isinstance(r, Skipped):
|
|
96
|
+
status = CaseEvent.TEST_SKIPPED
|
|
97
|
+
|
|
98
|
+
def path_canonicalizer(test_path: TestPath) -> TestPath:
|
|
99
|
+
if sys.platform == "win32":
|
|
100
|
+
for p in test_path:
|
|
101
|
+
p['name'] = p['name'].replace("\\", "/")
|
|
102
|
+
|
|
103
|
+
return test_path
|
|
104
|
+
|
|
105
|
+
def stdout(case: TestCase) -> str:
|
|
106
|
+
"""
|
|
107
|
+
case for:
|
|
108
|
+
<testcase>
|
|
109
|
+
<system-out>...</system-out>
|
|
110
|
+
</testcase>
|
|
111
|
+
"""
|
|
112
|
+
if case.system_out is not None:
|
|
113
|
+
return case.system_out
|
|
114
|
+
|
|
115
|
+
return ""
|
|
116
|
+
|
|
117
|
+
def stderr(case: TestCase) -> str:
|
|
118
|
+
"""
|
|
119
|
+
case for:
|
|
120
|
+
<testcase>
|
|
121
|
+
<system-err>...</system-err>
|
|
122
|
+
</testcase>
|
|
123
|
+
"""
|
|
124
|
+
if case.system_err is not None:
|
|
125
|
+
return case.system_err
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
case for:
|
|
129
|
+
<testcase>
|
|
130
|
+
<failure message="...">...</failure>
|
|
131
|
+
</testcase>
|
|
132
|
+
"""
|
|
133
|
+
stderr = ""
|
|
134
|
+
for result in case.result:
|
|
135
|
+
if type(result) in POSSIBLE_RESULTS:
|
|
136
|
+
# Since the `message` property is a summary of the `text` property,
|
|
137
|
+
# we should attempt to retrieve the `text` property first in order to obtain a detailed log.
|
|
138
|
+
if result.text:
|
|
139
|
+
stderr = stderr + result.text
|
|
140
|
+
elif result.message:
|
|
141
|
+
stderr = stderr + result.message + "\n"
|
|
142
|
+
|
|
143
|
+
return stderr
|
|
144
|
+
|
|
145
|
+
return CaseEvent.create(
|
|
146
|
+
test_path=path_canonicalizer(path_builder(case, suite, report_file)),
|
|
147
|
+
duration_secs=case.time,
|
|
148
|
+
status=status,
|
|
149
|
+
stdout=stdout(case),
|
|
150
|
+
stderr=stderr(case),
|
|
151
|
+
timestamp=suite.timestamp,
|
|
152
|
+
data=data_builder(case),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def create(cls, test_path: TestPath, duration_secs: float, status,
|
|
157
|
+
stdout: str | None = None, stderr: str | None = None,
|
|
158
|
+
timestamp: str | None = None, data: Dict | None = None) -> Dict:
|
|
159
|
+
def _timestamp(ts: str | None = None):
|
|
160
|
+
if ts is None:
|
|
161
|
+
return datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
162
|
+
try:
|
|
163
|
+
date = dateutil.parser.parse(timestr=ts, tzinfos=COMMON_TIMEZONES)
|
|
164
|
+
if date.tzinfo is None:
|
|
165
|
+
return date.replace(tzinfo=tzlocal()).isoformat()
|
|
166
|
+
return date.isoformat()
|
|
167
|
+
except Exception:
|
|
168
|
+
return datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
Builds a JSON representation of CaseEvent from arbitrary set of values
|
|
172
|
+
|
|
173
|
+
status: TEST_FAILED or TEST_PASSED
|
|
174
|
+
timestamp: ISO-8601 formatted date
|
|
175
|
+
data: arbitrary data to be submitted to the server. reserved for future enhancement.
|
|
176
|
+
"""
|
|
177
|
+
return {
|
|
178
|
+
"type": cls.EVENT_TYPE,
|
|
179
|
+
"testPath": test_path,
|
|
180
|
+
"duration": duration_secs if duration_secs and duration_secs >= 0.0 else 0.0,
|
|
181
|
+
"status": status,
|
|
182
|
+
"stdout": stdout or "",
|
|
183
|
+
"stderr": stderr or "",
|
|
184
|
+
"createdAt": _timestamp(timestamp),
|
|
185
|
+
"data": data
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class MetadataTestCase(TestCase):
|
|
190
|
+
line = IntAttr()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Annotated, List
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
import smart_tests.args4p.typer as typer
|
|
10
|
+
from smart_tests.utils.smart_tests_client import SmartTestsClient
|
|
11
|
+
from smart_tests.utils.tracking import Tracking, TrackingClient
|
|
12
|
+
|
|
13
|
+
from ... import args4p
|
|
14
|
+
from ...app import Application
|
|
15
|
+
from ...utils.commands import Command
|
|
16
|
+
from ...utils.commit_ingester import upload_commits
|
|
17
|
+
from ...utils.env_keys import COMMIT_TIMEOUT, REPORT_ERROR_KEY
|
|
18
|
+
from ...utils.fail_fast_mode import set_fail_fast_mode, warn_and_exit_if_fail_fast_mode
|
|
19
|
+
from ...utils.git_log_parser import parse_git_log
|
|
20
|
+
from ...utils.http_client import get_base_url
|
|
21
|
+
from ...utils.java import cygpath, get_java_command
|
|
22
|
+
from ...utils.logger import LOG_LEVEL_AUDIT, Logger
|
|
23
|
+
|
|
24
|
+
jar_file_path = os.path.normpath(os.path.join(os.path.dirname(__file__), "../../jar/exe_deploy.jar"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@args4p.command(help="Record commit information")
|
|
28
|
+
def commit(
|
|
29
|
+
app: Application,
|
|
30
|
+
name: Annotated[str | None, typer.Option(
|
|
31
|
+
help="repository name"
|
|
32
|
+
)] = None,
|
|
33
|
+
source: Annotated[str, typer.Option(
|
|
34
|
+
help="repository path"
|
|
35
|
+
)] = os.getcwd(),
|
|
36
|
+
executable: Annotated[str, typer.Option(
|
|
37
|
+
help="[Obsolete] it was to specify how to perform commit collection but has been removed",
|
|
38
|
+
hidden=True
|
|
39
|
+
)] = "jar",
|
|
40
|
+
max_days: Annotated[int, typer.Option(
|
|
41
|
+
help="the maximum number of days to collect commits retroactively"
|
|
42
|
+
)] = 30,
|
|
43
|
+
import_git_log_output: Annotated[str | None, typer.Option(
|
|
44
|
+
help="import from the git-log output"
|
|
45
|
+
)] = None,
|
|
46
|
+
):
|
|
47
|
+
if executable == 'docker':
|
|
48
|
+
click.echo("--executable docker is no longer supported", err=True)
|
|
49
|
+
raise typer.Exit(1)
|
|
50
|
+
|
|
51
|
+
tracking_client = TrackingClient(Command.COMMIT, app=app)
|
|
52
|
+
client = SmartTestsClient(tracking_client=tracking_client, app=app)
|
|
53
|
+
set_fail_fast_mode(client.is_fail_fast_mode())
|
|
54
|
+
|
|
55
|
+
if import_git_log_output:
|
|
56
|
+
_import_git_log(import_git_log_output, app)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Commit messages are not collected in the default.
|
|
60
|
+
is_collect_message = False
|
|
61
|
+
is_collect_files = False
|
|
62
|
+
try:
|
|
63
|
+
res = client.request("get", "commits/collect/options")
|
|
64
|
+
res.raise_for_status()
|
|
65
|
+
is_collect_message = res.json().get("commitMessage", False)
|
|
66
|
+
is_collect_files = res.json().get("files", False)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
tracking_client.send_error_event(
|
|
69
|
+
event_name=Tracking.ErrorEvent.INTERNAL_CLI_ERROR,
|
|
70
|
+
stack_trace=str(e),
|
|
71
|
+
api="commits/options",
|
|
72
|
+
)
|
|
73
|
+
client.print_exception_and_recover(e)
|
|
74
|
+
|
|
75
|
+
cwd = os.path.abspath(source)
|
|
76
|
+
if not name:
|
|
77
|
+
name = os.path.basename(cwd)
|
|
78
|
+
try:
|
|
79
|
+
exec_jar(name, cwd, max_days, app, is_collect_message, is_collect_files)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
if os.getenv(REPORT_ERROR_KEY):
|
|
82
|
+
raise e
|
|
83
|
+
else:
|
|
84
|
+
warn_and_exit_if_fail_fast_mode(
|
|
85
|
+
"Couldn't get commit history from `{}`. Do you run command root of git-controlled directory? "
|
|
86
|
+
"If not, please set a directory use by --source option.\nerror: {}".format(cwd, e))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def exec_jar(name: str, source: str, max_days: int, app: Application, is_collect_message: bool, is_collect_files: bool):
|
|
90
|
+
java = get_java_command()
|
|
91
|
+
|
|
92
|
+
if not java:
|
|
93
|
+
sys.exit("You need to install Java")
|
|
94
|
+
|
|
95
|
+
base_url = get_base_url()
|
|
96
|
+
|
|
97
|
+
# using subprocess.check_out with shell=False and a list of command to prevent vulnerability
|
|
98
|
+
# https://knowledge-base.secureflag.com/vulnerabilities/code_injection/os_command_injection_python.html
|
|
99
|
+
command = [java]
|
|
100
|
+
command.extend(_build_proxy_option(os.getenv("HTTPS_PROXY")))
|
|
101
|
+
command.extend([
|
|
102
|
+
"-jar",
|
|
103
|
+
cygpath(jar_file_path),
|
|
104
|
+
"-endpoint",
|
|
105
|
+
f"{base_url}/intake/",
|
|
106
|
+
"-max-days",
|
|
107
|
+
str(max_days)
|
|
108
|
+
])
|
|
109
|
+
|
|
110
|
+
if Logger().logger.isEnabledFor(LOG_LEVEL_AUDIT):
|
|
111
|
+
command.append("-audit")
|
|
112
|
+
if app.dry_run:
|
|
113
|
+
command.append("-dry-run")
|
|
114
|
+
if app.skip_cert_verification:
|
|
115
|
+
command.append("-skip-cert-verification")
|
|
116
|
+
if is_collect_message:
|
|
117
|
+
command.append("-commit-message")
|
|
118
|
+
if is_collect_files:
|
|
119
|
+
command.append("-files")
|
|
120
|
+
if os.getenv(COMMIT_TIMEOUT):
|
|
121
|
+
command.append("-enable-timeout")
|
|
122
|
+
command.append(name)
|
|
123
|
+
command.append(cygpath(source))
|
|
124
|
+
|
|
125
|
+
subprocess.run(
|
|
126
|
+
command,
|
|
127
|
+
check=True,
|
|
128
|
+
shell=False,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _import_git_log(output_file: str, app: Application):
|
|
133
|
+
try:
|
|
134
|
+
with open(output_file) as fp:
|
|
135
|
+
commits = parse_git_log(fp)
|
|
136
|
+
upload_commits(commits, app)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
if os.getenv(REPORT_ERROR_KEY):
|
|
139
|
+
raise e
|
|
140
|
+
else:
|
|
141
|
+
warn_and_exit_if_fail_fast_mode("Failed to import the git-log output\n error: {}".format(e))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _build_proxy_option(https_proxy: str | None) -> List[str]:
|
|
145
|
+
if not https_proxy:
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
if not (https_proxy.startswith("https://") or https_proxy.startswith("http://")):
|
|
149
|
+
https_proxy = "https://" + https_proxy
|
|
150
|
+
proxy_url = urlparse(https_proxy)
|
|
151
|
+
|
|
152
|
+
options = []
|
|
153
|
+
if proxy_url.hostname:
|
|
154
|
+
options.append(f"-Dhttps.proxyHost={proxy_url.hostname}")
|
|
155
|
+
if proxy_url.port:
|
|
156
|
+
options.append(f"-Dhttps.proxyPort={proxy_url.port}")
|
|
157
|
+
return options
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from typing import Annotated, List
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
import smart_tests.args4p.typer as typer
|
|
10
|
+
from smart_tests import args4p
|
|
11
|
+
from smart_tests.app import Application
|
|
12
|
+
from smart_tests.utils.commands import Command
|
|
13
|
+
from smart_tests.utils.exceptions import print_error_and_die
|
|
14
|
+
from smart_tests.utils.fail_fast_mode import set_fail_fast_mode
|
|
15
|
+
from smart_tests.utils.link import capture_links
|
|
16
|
+
from smart_tests.utils.no_build import NO_BUILD_BUILD_NAME
|
|
17
|
+
from smart_tests.utils.smart_tests_client import SmartTestsClient
|
|
18
|
+
from smart_tests.utils.tracking import Tracking, TrackingClient
|
|
19
|
+
from smart_tests.utils.typer_types import KeyValue, parse_key_value, validate_datetime_with_tz
|
|
20
|
+
|
|
21
|
+
TEST_SESSION_NAME_RULE = re.compile("^[a-zA-Z0-9][a-zA-Z0-9_.-]*$")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@args4p.command(help="Record session information")
|
|
25
|
+
def session(
|
|
26
|
+
app: Application,
|
|
27
|
+
build_name: Annotated[str, typer.Option(
|
|
28
|
+
"--build",
|
|
29
|
+
help="build name",
|
|
30
|
+
required=True
|
|
31
|
+
)],
|
|
32
|
+
test_suite: Annotated[str, typer.Option(
|
|
33
|
+
"--test-suite",
|
|
34
|
+
help="Set test suite name. A test suite is a collection of test sessions. Setting a test suite allows you to "
|
|
35
|
+
"manage data over test sessions and lineages.",
|
|
36
|
+
required=True
|
|
37
|
+
)],
|
|
38
|
+
flavors: Annotated[List[KeyValue], typer.Option(
|
|
39
|
+
"--flavor",
|
|
40
|
+
help="flavors",
|
|
41
|
+
multiple=True,
|
|
42
|
+
metavar="KEY=VALUE",
|
|
43
|
+
type=parse_key_value
|
|
44
|
+
)] = [],
|
|
45
|
+
is_observation: Annotated[bool, typer.Option(
|
|
46
|
+
"--observation",
|
|
47
|
+
help="enable observation mode"
|
|
48
|
+
)] = False,
|
|
49
|
+
links: Annotated[List[KeyValue], typer.Option(
|
|
50
|
+
"--link",
|
|
51
|
+
help="Set external link of a title and url",
|
|
52
|
+
multiple=True,
|
|
53
|
+
type=parse_key_value,
|
|
54
|
+
)] = [],
|
|
55
|
+
is_no_build: Annotated[bool, typer.Option(
|
|
56
|
+
"--no-build",
|
|
57
|
+
help="If you want to only send test reports, please use this option"
|
|
58
|
+
)] = False,
|
|
59
|
+
timestamp: Annotated[str | None, typer.Option(
|
|
60
|
+
help="Used to overwrite the session time when importing historical data. Note: Format must be "
|
|
61
|
+
"`YYYY-MM-DDThh:mm:ssTZD` or `YYYY-MM-DDThh:mm:ss` (local timezone applied)"
|
|
62
|
+
)] = None,
|
|
63
|
+
):
|
|
64
|
+
|
|
65
|
+
# Validate and convert timestamp if provided
|
|
66
|
+
parsed_timestamp = None
|
|
67
|
+
if timestamp:
|
|
68
|
+
parsed_timestamp = validate_datetime_with_tz(timestamp)
|
|
69
|
+
|
|
70
|
+
tracking_client = TrackingClient(Command.RECORD_SESSION, app=app)
|
|
71
|
+
client = SmartTestsClient(app=app, tracking_client=tracking_client)
|
|
72
|
+
set_fail_fast_mode(client.is_fail_fast_mode())
|
|
73
|
+
|
|
74
|
+
if not is_no_build and not build_name:
|
|
75
|
+
print_error_and_die("Missing option '--build'", tracking_client, Tracking.ErrorEvent.USER_ERROR)
|
|
76
|
+
|
|
77
|
+
if is_no_build and build_name:
|
|
78
|
+
print_error_and_die("Cannot use --build option with --no-build option", tracking_client, Tracking.ErrorEvent.USER_ERROR)
|
|
79
|
+
|
|
80
|
+
if is_no_build:
|
|
81
|
+
build_name = NO_BUILD_BUILD_NAME
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
payload = {
|
|
85
|
+
"flavors": dict([(f.key, f.value) for f in flavors]),
|
|
86
|
+
"isObservation": is_observation,
|
|
87
|
+
"noBuild": is_no_build,
|
|
88
|
+
"testSuite": test_suite,
|
|
89
|
+
"timestamp": parsed_timestamp.isoformat() if parsed_timestamp else None,
|
|
90
|
+
"links": capture_links(link_options=links, env=os.environ)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
sub_path = f"builds/{build_name}/test_sessions"
|
|
94
|
+
res = client.request("post", sub_path, payload=payload)
|
|
95
|
+
|
|
96
|
+
if res.status_code == HTTPStatus.NOT_FOUND:
|
|
97
|
+
msg = f"Build {build_name} was not found." \
|
|
98
|
+
f"Make sure to run `launchable record build --build {build_name}` before you run this command."
|
|
99
|
+
tracking_client.send_error_event(
|
|
100
|
+
event_name=Tracking.ErrorEvent.INTERNAL_CLI_ERROR,
|
|
101
|
+
stack_trace=msg,
|
|
102
|
+
)
|
|
103
|
+
click.secho(msg, fg='yellow', err=True)
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
|
|
106
|
+
res.raise_for_status()
|
|
107
|
+
|
|
108
|
+
session_id = res.json().get('id', None)
|
|
109
|
+
if is_no_build:
|
|
110
|
+
build_name = res.json().get("buildNumber", "")
|
|
111
|
+
assert build_name is not None
|
|
112
|
+
|
|
113
|
+
click.echo(f"{sub_path}/{session_id}", nl=False)
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
tracking_client.send_error_event(
|
|
117
|
+
event_name=Tracking.ErrorEvent.INTERNAL_CLI_ERROR,
|
|
118
|
+
stack_trace=str(e),
|
|
119
|
+
)
|
|
120
|
+
client.print_exception_and_recover(e)
|