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.
Files changed (96) hide show
  1. smart_tests/__init__.py +0 -0
  2. smart_tests/__main__.py +60 -0
  3. smart_tests/app.py +67 -0
  4. smart_tests/args4p/README.md +102 -0
  5. smart_tests/args4p/__init__.py +13 -0
  6. smart_tests/args4p/argument.py +45 -0
  7. smart_tests/args4p/command.py +593 -0
  8. smart_tests/args4p/converters/__init__.py +75 -0
  9. smart_tests/args4p/decorators.py +98 -0
  10. smart_tests/args4p/exceptions.py +12 -0
  11. smart_tests/args4p/option.py +85 -0
  12. smart_tests/args4p/parameter.py +84 -0
  13. smart_tests/args4p/typer/__init__.py +42 -0
  14. smart_tests/commands/__init__.py +0 -0
  15. smart_tests/commands/compare/__init__.py +11 -0
  16. smart_tests/commands/compare/subsets.py +58 -0
  17. smart_tests/commands/detect_flakes.py +105 -0
  18. smart_tests/commands/inspect/__init__.py +13 -0
  19. smart_tests/commands/inspect/model.py +52 -0
  20. smart_tests/commands/inspect/subset.py +138 -0
  21. smart_tests/commands/record/__init__.py +19 -0
  22. smart_tests/commands/record/attachment.py +38 -0
  23. smart_tests/commands/record/build.py +356 -0
  24. smart_tests/commands/record/case_event.py +190 -0
  25. smart_tests/commands/record/commit.py +157 -0
  26. smart_tests/commands/record/session.py +120 -0
  27. smart_tests/commands/record/tests.py +498 -0
  28. smart_tests/commands/stats/__init__.py +11 -0
  29. smart_tests/commands/stats/test_sessions.py +45 -0
  30. smart_tests/commands/subset.py +567 -0
  31. smart_tests/commands/test_path_writer.py +51 -0
  32. smart_tests/commands/verify.py +153 -0
  33. smart_tests/jar/exe_deploy.jar +0 -0
  34. smart_tests/plugins/__init__.py +0 -0
  35. smart_tests/test_runners/__init__.py +0 -0
  36. smart_tests/test_runners/adb.py +24 -0
  37. smart_tests/test_runners/ant.py +35 -0
  38. smart_tests/test_runners/bazel.py +103 -0
  39. smart_tests/test_runners/behave.py +62 -0
  40. smart_tests/test_runners/codeceptjs.py +33 -0
  41. smart_tests/test_runners/ctest.py +164 -0
  42. smart_tests/test_runners/cts.py +189 -0
  43. smart_tests/test_runners/cucumber.py +451 -0
  44. smart_tests/test_runners/cypress.py +46 -0
  45. smart_tests/test_runners/dotnet.py +106 -0
  46. smart_tests/test_runners/file.py +20 -0
  47. smart_tests/test_runners/flutter.py +251 -0
  48. smart_tests/test_runners/go_test.py +99 -0
  49. smart_tests/test_runners/googletest.py +34 -0
  50. smart_tests/test_runners/gradle.py +96 -0
  51. smart_tests/test_runners/jest.py +52 -0
  52. smart_tests/test_runners/maven.py +149 -0
  53. smart_tests/test_runners/minitest.py +40 -0
  54. smart_tests/test_runners/nunit.py +190 -0
  55. smart_tests/test_runners/playwright.py +252 -0
  56. smart_tests/test_runners/prove.py +74 -0
  57. smart_tests/test_runners/pytest.py +358 -0
  58. smart_tests/test_runners/raw.py +238 -0
  59. smart_tests/test_runners/robot.py +125 -0
  60. smart_tests/test_runners/rspec.py +5 -0
  61. smart_tests/test_runners/smart_tests.py +235 -0
  62. smart_tests/test_runners/vitest.py +49 -0
  63. smart_tests/test_runners/xctest.py +79 -0
  64. smart_tests/testpath.py +154 -0
  65. smart_tests/utils/__init__.py +0 -0
  66. smart_tests/utils/authentication.py +78 -0
  67. smart_tests/utils/ci_provider.py +7 -0
  68. smart_tests/utils/commands.py +14 -0
  69. smart_tests/utils/commit_ingester.py +59 -0
  70. smart_tests/utils/common_tz.py +12 -0
  71. smart_tests/utils/edit_distance.py +11 -0
  72. smart_tests/utils/env_keys.py +19 -0
  73. smart_tests/utils/exceptions.py +34 -0
  74. smart_tests/utils/fail_fast_mode.py +99 -0
  75. smart_tests/utils/file_name_pattern.py +4 -0
  76. smart_tests/utils/git_log_parser.py +53 -0
  77. smart_tests/utils/glob.py +44 -0
  78. smart_tests/utils/gzipgen.py +46 -0
  79. smart_tests/utils/http_client.py +169 -0
  80. smart_tests/utils/java.py +61 -0
  81. smart_tests/utils/link.py +149 -0
  82. smart_tests/utils/logger.py +53 -0
  83. smart_tests/utils/no_build.py +2 -0
  84. smart_tests/utils/sax.py +119 -0
  85. smart_tests/utils/session.py +73 -0
  86. smart_tests/utils/smart_tests_client.py +134 -0
  87. smart_tests/utils/subprocess.py +12 -0
  88. smart_tests/utils/tracking.py +95 -0
  89. smart_tests/utils/typer_types.py +241 -0
  90. smart_tests/version.py +7 -0
  91. smart_tests_cli-2.0.0.dist-info/METADATA +168 -0
  92. smart_tests_cli-2.0.0.dist-info/RECORD +96 -0
  93. smart_tests_cli-2.0.0.dist-info/WHEEL +5 -0
  94. smart_tests_cli-2.0.0.dist-info/entry_points.txt +2 -0
  95. smart_tests_cli-2.0.0.dist-info/licenses/LICENSE.txt +202 -0
  96. 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)