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
smart_tests/__init__.py
ADDED
|
File without changes
|
smart_tests/__main__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import importlib.util
|
|
3
|
+
import sys
|
|
4
|
+
from glob import glob
|
|
5
|
+
from os.path import basename, dirname, join
|
|
6
|
+
|
|
7
|
+
from smart_tests.app import Application
|
|
8
|
+
from smart_tests.args4p.command import Group
|
|
9
|
+
from smart_tests.commands.compare import compare
|
|
10
|
+
from smart_tests.commands.detect_flakes import detect_flakes
|
|
11
|
+
from smart_tests.commands.inspect import inspect
|
|
12
|
+
from smart_tests.commands.record import record
|
|
13
|
+
from smart_tests.commands.stats import stats
|
|
14
|
+
from smart_tests.commands.subset import subset
|
|
15
|
+
from smart_tests.commands.verify import verify
|
|
16
|
+
|
|
17
|
+
cli = Group(name="cli", callback=Application)
|
|
18
|
+
cli.add_command(record)
|
|
19
|
+
cli.add_command(subset)
|
|
20
|
+
# TODO: main.add_command(split_subset)
|
|
21
|
+
cli.add_command(verify)
|
|
22
|
+
cli.add_command(inspect)
|
|
23
|
+
cli.add_command(stats)
|
|
24
|
+
cli.add_command(compare)
|
|
25
|
+
cli.add_command(detect_flakes)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_test_runners():
|
|
29
|
+
# load all test runners
|
|
30
|
+
for f in glob(join(dirname(__file__), 'test_runners', "*.py")):
|
|
31
|
+
f = basename(f)[:-3]
|
|
32
|
+
if f == '__init__':
|
|
33
|
+
continue
|
|
34
|
+
importlib.import_module(f'smart_tests.test_runners.{f}')
|
|
35
|
+
|
|
36
|
+
# load all plugins. Here we do a bit of command line parsing ourselves,
|
|
37
|
+
# because the command line could look something like `smart-tests record tests myprofile --plugins ...
|
|
38
|
+
plugin_dir = None
|
|
39
|
+
if "--plugins" in sys.argv:
|
|
40
|
+
idx = sys.argv.index("--plugins")
|
|
41
|
+
if idx + 1 < len(sys.argv):
|
|
42
|
+
plugin_dir = sys.argv[idx + 1]
|
|
43
|
+
|
|
44
|
+
if plugin_dir:
|
|
45
|
+
for f in glob(join(plugin_dir, '*.py')):
|
|
46
|
+
spec = importlib.util.spec_from_file_location(
|
|
47
|
+
f"smart_tests.plugins.{basename(f)[:-3]}", f)
|
|
48
|
+
plugin = importlib.util.module_from_spec(spec)
|
|
49
|
+
spec.loader.exec_module(plugin)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_load_test_runners()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
cli.main()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == '__main__':
|
|
60
|
+
main()
|
smart_tests/app.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Object representing the most global state possible, which represents a single invocation of CLI
|
|
2
|
+
# Currently it's used to keep global configurations.
|
|
3
|
+
#
|
|
4
|
+
# From command implementations, this is available via dependency injection
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
import smart_tests.args4p.typer as typer
|
|
13
|
+
from smart_tests.utils import logger
|
|
14
|
+
from smart_tests.utils.env_keys import SKIP_CERT_VERIFICATION
|
|
15
|
+
from smart_tests.version import __version__
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Application:
|
|
19
|
+
# Group commands that take the CLI profile as a sub-command shall set this parameter
|
|
20
|
+
test_runner: str | None = None
|
|
21
|
+
|
|
22
|
+
# this maps to the main entry point of the CLI command
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
log_level: Annotated[str, typer.Option(
|
|
26
|
+
help="Set logger's log level (CRITICAL, ERROR, WARNING, AUDIT, INFO, DEBUG)."
|
|
27
|
+
)] = logger.LOG_LEVEL_DEFAULT_STR,
|
|
28
|
+
plugin_dir: Annotated[str | None, typer.Option(
|
|
29
|
+
"--plugins",
|
|
30
|
+
help="Directory to load plugins from"
|
|
31
|
+
)] = None,
|
|
32
|
+
dry_run: Annotated[bool, typer.Option(
|
|
33
|
+
help="Dry-run mode. No data is sent to the server. However, sometimes "
|
|
34
|
+
"GET requests without payload data or side effects could be sent."
|
|
35
|
+
"note: Since the dry run log is output together with the AUDIT log, "
|
|
36
|
+
"even if the log-level is set to warning or higher, the log level will "
|
|
37
|
+
"be forced to be set to AUDIT."
|
|
38
|
+
)] = False,
|
|
39
|
+
skip_cert_verification: Annotated[bool, typer.Option(
|
|
40
|
+
help="Skip the SSL certificate check. This lets you bypass system setup issues "
|
|
41
|
+
"like CERTIFICATE_VERIFY_FAILED, at the expense of vulnerability against "
|
|
42
|
+
"a possible man-in-the-middle attack. Use it as an escape hatch, but with caution."
|
|
43
|
+
)] = False,
|
|
44
|
+
version: Annotated[bool, typer.Option(
|
|
45
|
+
"--version", help="Show version and exit"
|
|
46
|
+
)] = False,
|
|
47
|
+
):
|
|
48
|
+
if version:
|
|
49
|
+
click.echo(f"smart-tests-cli {__version__}")
|
|
50
|
+
raise typer.Exit(0)
|
|
51
|
+
|
|
52
|
+
level = logger.get_log_level(log_level)
|
|
53
|
+
# In the case of dry-run, it is forced to set the level below the AUDIT.
|
|
54
|
+
# This is because the dry-run log will be output along with the audit log.
|
|
55
|
+
if dry_run and level > logger.LOG_LEVEL_AUDIT:
|
|
56
|
+
level = logger.LOG_LEVEL_AUDIT
|
|
57
|
+
logging.basicConfig(level=level)
|
|
58
|
+
|
|
59
|
+
# plugin_dir is processed earlier. If we do it here, it's too late
|
|
60
|
+
|
|
61
|
+
# Dry run mode. This command is used by customers to inspect data we'd send to our server,
|
|
62
|
+
# but without actually doing so.
|
|
63
|
+
self.dry_run = dry_run
|
|
64
|
+
|
|
65
|
+
if not skip_cert_verification:
|
|
66
|
+
skip_cert_verification = (os.environ.get(SKIP_CERT_VERIFICATION) is not None)
|
|
67
|
+
self.skip_cert_verification = skip_cert_verification
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# User guide
|
|
2
|
+
|
|
3
|
+
## Option/argument annotations
|
|
4
|
+
|
|
5
|
+
To control how args4p passes option/argument values to your function, you can either use the `@args4p.option` or `@args4p.argument` decorators, or use the `Option` and `Argument` in parameter annotations.
|
|
6
|
+
|
|
7
|
+
@args4p.option('-v', 'verbose', ...)
|
|
8
|
+
@args4p.command
|
|
9
|
+
def foo(verbose: bool, ...):
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
import smart_tests.args4p.typer as typer
|
|
13
|
+
|
|
14
|
+
@args4p.command
|
|
15
|
+
def foo(verbose: Annotated[bool, typer.Option('-v', ...)
|
|
16
|
+
|
|
17
|
+
The only difference between those two variants is that the former requires the parameter name be passed as string, while the latter takes that from the actual parameter declaration.
|
|
18
|
+
|
|
19
|
+
These invocations take the following parameters:
|
|
20
|
+
|
|
21
|
+
**\*param_decls: List[str]**: The first portion of the invocation is a var-arg of strings, and they designate the names of options. When used as a decorator, this list must be followed by the parameter name itself.
|
|
22
|
+
|
|
23
|
+
If just the parameter name is given but no option names are given, the option name is generated from the parameter name.
|
|
24
|
+
|
|
25
|
+
# this creates the --verbose option
|
|
26
|
+
@args4p.option("verbose")
|
|
27
|
+
def f (verbose: bool):
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
# same
|
|
31
|
+
def f (verbose: Annotated[bool, args4p.Option()]):
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
Arguments would take the parameter name if used as a decorator, or nothing if used as annotation.
|
|
35
|
+
|
|
36
|
+
**help**: Human readable description of the option, used to render the help message
|
|
37
|
+
|
|
38
|
+
**type**: Specify the type of the option/argument. The type is individual, meaning even if you allow the option/argument to be specified multiple times, the type of option/argument is always that of a single value (e.g., `int`, not `List[int]`)
|
|
39
|
+
|
|
40
|
+
You can also specify any `Callable` that takes a `str` and produces a value of your desired type. This is handy for defining custom types.
|
|
41
|
+
|
|
42
|
+
If this parameter is omitted, the type is inferred from the type annotation of the function parameter, with the following massaging:
|
|
43
|
+
|
|
44
|
+
- if the type is optional, such as `str|None` or `Optional[str]`, the type is inferred as the non-None type (e.g., `str` in this case)
|
|
45
|
+
|
|
46
|
+
**default**: Default value if the option/argument is not provided. bool options are treated as having `False` as the default.
|
|
47
|
+
|
|
48
|
+
**required**: Whether the option/argument is required. Default is `False`.
|
|
49
|
+
|
|
50
|
+
**metavar**: User-friendly name for the option value place holder, used in help messages.
|
|
51
|
+
|
|
52
|
+
**multiple**: Whether the option/argument can be specified multiple times. Default is `False`. If true, the parameter type must be a list type (e.g. `List[str]`)
|
|
53
|
+
|
|
54
|
+
**hidden**: If true, this option is hidden from help messages.
|
|
55
|
+
|
|
56
|
+
## Group (sub-commands)
|
|
57
|
+
This library encourages organizing a complex CLI through sub-commands,
|
|
58
|
+
ala `git`, like this:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
@args4p.group
|
|
62
|
+
@args4p.option('-v', 'verbose', ...)
|
|
63
|
+
def cli(verbose: bool):
|
|
64
|
+
return {'verbose': verbose}
|
|
65
|
+
|
|
66
|
+
@cli.command # NOT args4p.command
|
|
67
|
+
@args4p.argument('name')
|
|
68
|
+
def subcmd1(parent, name):
|
|
69
|
+
if parent.verbose:
|
|
70
|
+
print(f"Hello {name}")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Sub-commands receive the result of the parent command as its first argument. This allows you to pass options to the parent command and access them in the sub-command.
|
|
74
|
+
|
|
75
|
+
A particularly useful idiom is to create a group out of a class initializer, like this:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
class App:
|
|
79
|
+
def __init__(self, verbose: Annotated[bool,Option('-v')]):
|
|
80
|
+
self.verbose = verbose
|
|
81
|
+
|
|
82
|
+
cli=Group(App)
|
|
83
|
+
|
|
84
|
+
@cli.command # NOT args4p.command
|
|
85
|
+
@args4p.argument('name')
|
|
86
|
+
def subcmd1(app: App, name):
|
|
87
|
+
if app.verbose:
|
|
88
|
+
print(f"Hello {name}")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
(Note that decorators won't work with this idiom, so you have to use annotations.)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
Alternatively, use `Group.add_command` to add a top-level command as a sub-command to a group.
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
@args4p.command
|
|
98
|
+
def subcmd2(...):
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
cli.add_command(subcmd2)
|
|
102
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Callable, TypeVar
|
|
2
|
+
|
|
3
|
+
F = TypeVar("F", bound=Callable)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def decorator(func: F) -> F:
|
|
7
|
+
"""
|
|
8
|
+
Functions marked with this decorator are meant to be used as decorators themselves.
|
|
9
|
+
"""
|
|
10
|
+
return func
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
from .decorators import argument, command, group, option # noqa: F401, E402
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Any, Callable, Optional
|
|
2
|
+
|
|
3
|
+
from .exceptions import BadCmdLineException
|
|
4
|
+
from .option import NO_DEFAULT
|
|
5
|
+
from .parameter import Parameter, normalize_type
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Argument(Parameter):
|
|
9
|
+
clazz = "argument"
|
|
10
|
+
|
|
11
|
+
def __init__(self, name: Optional[str], type: type | Callable[..., Any] | None = None, multiple: bool = False,
|
|
12
|
+
required: bool = True, metavar: str | None = None, help: str | None = None, default: Any = NO_DEFAULT):
|
|
13
|
+
self.name = name # type: ignore[assignment] # once properly constructed, name is never None
|
|
14
|
+
self.type = normalize_type(type)
|
|
15
|
+
self.multiple = multiple
|
|
16
|
+
self.required = required
|
|
17
|
+
self.metavar = metavar
|
|
18
|
+
self.help = help
|
|
19
|
+
self.default = default
|
|
20
|
+
|
|
21
|
+
def append(self, existing: Any, arg: str):
|
|
22
|
+
'''
|
|
23
|
+
Given the current value 'existing' that represents the present value to invoke the user function with,
|
|
24
|
+
this method is called when another argument 'arg' is consumed from the command line to create the updated
|
|
25
|
+
value that replaces 'existing'.
|
|
26
|
+
'''
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
v = self.type(arg)
|
|
30
|
+
except ValueError as e:
|
|
31
|
+
raise BadCmdLineException(f"Invalid value '{arg}' for argument '{self.name}'") from e
|
|
32
|
+
|
|
33
|
+
if self.multiple:
|
|
34
|
+
if existing is None:
|
|
35
|
+
existing = []
|
|
36
|
+
existing.append(v)
|
|
37
|
+
return existing
|
|
38
|
+
else:
|
|
39
|
+
return v
|
|
40
|
+
|
|
41
|
+
def attach_to_command(self, command): # typing command makes reference circular
|
|
42
|
+
super().attach_to_command(command)
|
|
43
|
+
|
|
44
|
+
if self.metavar is None:
|
|
45
|
+
self.metavar = str(self.name).upper()
|