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,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional, Type
|
|
4
|
+
|
|
5
|
+
from . import decorator
|
|
6
|
+
from .argument import Argument
|
|
7
|
+
from .command import Command, Group
|
|
8
|
+
from .exceptions import BadConfigException
|
|
9
|
+
from .option import NO_DEFAULT, Option
|
|
10
|
+
from .parameter import Parameter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _command(
|
|
14
|
+
name: Optional[str] = None,
|
|
15
|
+
help: Optional[str] = None,
|
|
16
|
+
cls: Type[Command] = Command,
|
|
17
|
+
):
|
|
18
|
+
def decorator(f: Callable) -> Command:
|
|
19
|
+
return cls(name=name, help=help, callback=f)
|
|
20
|
+
|
|
21
|
+
return decorator
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@decorator
|
|
25
|
+
def command(name: Optional[str] = None, help: Optional[str] = None) -> Callable[..., Command]:
|
|
26
|
+
return _command(name, help, Command)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@decorator
|
|
30
|
+
def group(name: Optional[str] = None, help: Optional[str] = None) -> Callable[..., Group]:
|
|
31
|
+
return _command(name, help, Group) # type: ignore
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@decorator
|
|
35
|
+
def option(
|
|
36
|
+
*param_decls: str,
|
|
37
|
+
help: str | None = None, type: type | Callable | None = None, default: Any = NO_DEFAULT, required: bool = False,
|
|
38
|
+
metavar: str | None = None, multiple: bool = False, hidden: bool = False
|
|
39
|
+
) -> Callable:
|
|
40
|
+
'''
|
|
41
|
+
See README.md for usage
|
|
42
|
+
'''
|
|
43
|
+
|
|
44
|
+
def decorator(f: Callable) -> Callable:
|
|
45
|
+
if len(param_decls) == 0:
|
|
46
|
+
raise BadConfigException("Variable name is required")
|
|
47
|
+
|
|
48
|
+
variable_name = param_decls[-1]
|
|
49
|
+
if len(param_decls) == 1:
|
|
50
|
+
option_names = [f"--{variable_name.replace('_', '-')}"]
|
|
51
|
+
else:
|
|
52
|
+
option_names = list(param_decls[:-1])
|
|
53
|
+
|
|
54
|
+
o = Option(
|
|
55
|
+
name=variable_name,
|
|
56
|
+
option_names=option_names,
|
|
57
|
+
help=help,
|
|
58
|
+
type=type,
|
|
59
|
+
default=default,
|
|
60
|
+
required=required,
|
|
61
|
+
metavar=metavar,
|
|
62
|
+
multiple=multiple,
|
|
63
|
+
hidden=hidden)
|
|
64
|
+
|
|
65
|
+
return _attach(f, o)
|
|
66
|
+
|
|
67
|
+
return decorator
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@decorator
|
|
71
|
+
def argument(
|
|
72
|
+
name: str,
|
|
73
|
+
type: type | Callable = str,
|
|
74
|
+
multiple: bool = False,
|
|
75
|
+
required: bool = True,
|
|
76
|
+
metavar: str | None = None,
|
|
77
|
+
help: str | None = None,
|
|
78
|
+
default: Any = NO_DEFAULT
|
|
79
|
+
) -> Callable:
|
|
80
|
+
'''
|
|
81
|
+
See README.md for usage
|
|
82
|
+
'''
|
|
83
|
+
a = Argument(name=name, type=type, multiple=multiple, required=required, metavar=metavar, help=help, default=default)
|
|
84
|
+
return lambda f: _attach(f, a)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _attach(f: Callable[..., Any], param: Parameter):
|
|
88
|
+
# depending on whether a command annotation comes before/after parameter annotations, 'f' might be
|
|
89
|
+
# a naked user-defined function or a Command instance
|
|
90
|
+
if isinstance(f, Command):
|
|
91
|
+
f.add_param(param, True)
|
|
92
|
+
else:
|
|
93
|
+
if not hasattr(f, "__args4p_params__"):
|
|
94
|
+
f.__args4p_params__ = [] # type: ignore
|
|
95
|
+
|
|
96
|
+
f.__args4p_params__.append(param) # type: ignore
|
|
97
|
+
|
|
98
|
+
return f
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class BadCmdLineException(Exception):
|
|
2
|
+
'''
|
|
3
|
+
Indicates that arguments given by the user are invalid.
|
|
4
|
+
|
|
5
|
+
The message gets printed, and the CLI exists with non-zero.
|
|
6
|
+
'''
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BadConfigException(Exception):
|
|
10
|
+
'''
|
|
11
|
+
Indicates that the option/command/argument declarations are invalid
|
|
12
|
+
'''
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from typing import Any, Callable, Optional
|
|
2
|
+
|
|
3
|
+
from .exceptions import BadCmdLineException
|
|
4
|
+
from .parameter import Parameter, normalize_type
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NoDefault:
|
|
8
|
+
'''
|
|
9
|
+
If there's no default value configured for option/argument, we use `NO_DEFAULT`.
|
|
10
|
+
In contrast, `None` is a valid and very typical default value.
|
|
11
|
+
'''
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
NO_DEFAULT = NoDefault()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Option(Parameter):
|
|
19
|
+
clazz = "option"
|
|
20
|
+
|
|
21
|
+
hidden: bool
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self, name: Optional[str],
|
|
25
|
+
option_names: list[str],
|
|
26
|
+
help: str | None = None,
|
|
27
|
+
type: type | Callable[..., Any] | None = str,
|
|
28
|
+
default: Any = NO_DEFAULT,
|
|
29
|
+
required: bool = False,
|
|
30
|
+
metavar: str | None = None,
|
|
31
|
+
multiple: bool = False,
|
|
32
|
+
hidden: bool = False):
|
|
33
|
+
self.name = name # type: ignore[assignment] # once properly constructed, name is never None
|
|
34
|
+
self.option_names = option_names
|
|
35
|
+
self.help = help
|
|
36
|
+
self.type = normalize_type(type)
|
|
37
|
+
self.default = default
|
|
38
|
+
self.required = required
|
|
39
|
+
self.metavar = metavar
|
|
40
|
+
self.multiple = multiple
|
|
41
|
+
self.hidden = hidden
|
|
42
|
+
|
|
43
|
+
def append(self, existing: Any, option_name: str, args): # args is ArgList, but typing it creates a circular import
|
|
44
|
+
'''
|
|
45
|
+
Given the current value 'existing' that represents the present value to invoke the user function with,
|
|
46
|
+
this method is called when this option was specified as 'option_name' on the command line.
|
|
47
|
+
'args' is pointing at the next argument after 'option_name', which may be the value for this option.
|
|
48
|
+
'''
|
|
49
|
+
|
|
50
|
+
if self.type == bool or self.type == Optional[bool]:
|
|
51
|
+
v = True
|
|
52
|
+
else:
|
|
53
|
+
v = args.eat(option_name)
|
|
54
|
+
try:
|
|
55
|
+
v = self.type(v)
|
|
56
|
+
except ValueError as e:
|
|
57
|
+
raise BadCmdLineException(f"Invalid value '{v}' for option '{option_name}': {str(e)}") from e
|
|
58
|
+
|
|
59
|
+
if self.multiple:
|
|
60
|
+
if existing is None:
|
|
61
|
+
existing = []
|
|
62
|
+
existing.append(v)
|
|
63
|
+
return existing
|
|
64
|
+
else:
|
|
65
|
+
return v
|
|
66
|
+
|
|
67
|
+
def attach_to_command(self, command): # typing command makes reference circular
|
|
68
|
+
super().attach_to_command(command)
|
|
69
|
+
|
|
70
|
+
if self.metavar is None:
|
|
71
|
+
def type_name(t):
|
|
72
|
+
if hasattr(t, "__name__"):
|
|
73
|
+
return t.__name__
|
|
74
|
+
else:
|
|
75
|
+
return str(t)
|
|
76
|
+
self.metavar = type_name(self.type).upper()
|
|
77
|
+
if self.type == bool and self.default is NO_DEFAULT:
|
|
78
|
+
# if the flag is absent, bind the value to False, or else the function signature requires a defalut value,
|
|
79
|
+
# which is silly
|
|
80
|
+
self.default = False
|
|
81
|
+
|
|
82
|
+
def __repr__(self):
|
|
83
|
+
return (f"Option(name={self.name!r}, option_names={self.option_names!r}, help={self.help!r}, "
|
|
84
|
+
f"type={self.type.__name__!r}, default={self.default!r}, required={self.required!r}, "
|
|
85
|
+
f"metavar={self.metavar!r}, many={self.multiple!r})")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import types
|
|
3
|
+
from typing import Annotated, Any, Callable, Optional, Union, get_args, get_origin
|
|
4
|
+
|
|
5
|
+
from smart_tests.args4p.exceptions import BadConfigException
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def to_type(p: inspect.Parameter) -> Optional[type]:
|
|
9
|
+
'''
|
|
10
|
+
Given output from inspect.signature, extract the type annotation.
|
|
11
|
+
'''
|
|
12
|
+
annotation = p.annotation
|
|
13
|
+
if annotation == inspect.Parameter.empty:
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
# we expect a List[something] and we want to extract 'something'
|
|
17
|
+
|
|
18
|
+
origin = get_origin(annotation)
|
|
19
|
+
if origin is Annotated:
|
|
20
|
+
return get_args(annotation)[0]
|
|
21
|
+
|
|
22
|
+
return annotation
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize_type(t) -> type:
|
|
26
|
+
if isinstance(t, types.UnionType) or get_origin(t) is Union:
|
|
27
|
+
# x|None is a common typing of a parameter that confuses args4p.
|
|
28
|
+
# we want to normalize it to just x
|
|
29
|
+
# Not sure when UnionType is used and when Union is used, but they both seem to appear
|
|
30
|
+
args = get_args(t)
|
|
31
|
+
if len(args) == 2 and args[1] is type(None):
|
|
32
|
+
return args[0]
|
|
33
|
+
|
|
34
|
+
return t
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Parameter:
|
|
38
|
+
'''
|
|
39
|
+
Common parts of Argument and Option
|
|
40
|
+
'''
|
|
41
|
+
|
|
42
|
+
# the name of the argument, used as the variable name in the user function
|
|
43
|
+
# when created from typer.Option or typer.Argument, this is not set until attached to a command
|
|
44
|
+
name: str
|
|
45
|
+
|
|
46
|
+
multiple: bool # True if this argument can appear multiple times
|
|
47
|
+
required: bool # True if this argument is required
|
|
48
|
+
metavar: str | None # the name to use in help messages for the argument value
|
|
49
|
+
help: str | None # the help message for this argument
|
|
50
|
+
default: Any # the default value if the argument/option is not provided
|
|
51
|
+
clazz: str # "argument" or "option"
|
|
52
|
+
|
|
53
|
+
# convert the string argument to a value.
|
|
54
|
+
# For multiple=True, this is the type of each individual value.
|
|
55
|
+
# 'type' object itself, like 'int' is a convenient callable to do just that
|
|
56
|
+
type: type | Callable
|
|
57
|
+
|
|
58
|
+
def attach_to_command(self, command): # typing command makes reference circular
|
|
59
|
+
def error(msg: str):
|
|
60
|
+
raise BadConfigException(
|
|
61
|
+
f"{msg} in function '{command.callback.__name__}': "
|
|
62
|
+
f"{inspect.getsourcefile(command.callback)}:{inspect.getsourcelines(command.callback)[1]}")
|
|
63
|
+
|
|
64
|
+
for name, param in inspect.signature(command.callback).parameters.items():
|
|
65
|
+
if name == self.name:
|
|
66
|
+
# we found the parameter that matches the name
|
|
67
|
+
if self.type is None:
|
|
68
|
+
def infer_type() -> type:
|
|
69
|
+
t = normalize_type(to_type(param))
|
|
70
|
+
if t is None:
|
|
71
|
+
raise error(f"Type annotation is missing on parameter '{name}'")
|
|
72
|
+
if self.multiple:
|
|
73
|
+
# we expect a List[something] and we want to extract 'something'
|
|
74
|
+
if get_origin(t) is list:
|
|
75
|
+
return get_args(t)[0]
|
|
76
|
+
raise error(f"multiple=True requires a List[T] type annotation with parameter '{name}'")
|
|
77
|
+
else:
|
|
78
|
+
return t
|
|
79
|
+
|
|
80
|
+
self.type = infer_type()
|
|
81
|
+
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
raise error(f"No parameter named '{self.name}' found")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# This package defines Typer-style annotation based option/command declarations
|
|
2
|
+
from typing import Any, Callable
|
|
3
|
+
|
|
4
|
+
from ..argument import Argument as _Argument
|
|
5
|
+
from ..option import NO_DEFAULT
|
|
6
|
+
from ..option import Option as _Option
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def Option(
|
|
10
|
+
*option_names: str,
|
|
11
|
+
help: str | None = None, type: type | Callable | None = None, default: Any = NO_DEFAULT, required: bool = False,
|
|
12
|
+
metavar: str | None = None, multiple: bool = False, hidden: bool = False
|
|
13
|
+
) -> _Option:
|
|
14
|
+
'''
|
|
15
|
+
See README.md for usage
|
|
16
|
+
'''
|
|
17
|
+
|
|
18
|
+
return _Option(name=None, option_names=list(option_names), help=help, type=type,
|
|
19
|
+
default=default, required=required, metavar=metavar, multiple=multiple, hidden=hidden)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def Argument(
|
|
23
|
+
type: type | Callable = str,
|
|
24
|
+
multiple: bool = False,
|
|
25
|
+
required: bool = True,
|
|
26
|
+
metavar: str | None = None,
|
|
27
|
+
help: str | None = None,
|
|
28
|
+
default: Any = NO_DEFAULT
|
|
29
|
+
) -> _Argument:
|
|
30
|
+
'''
|
|
31
|
+
See README.md for usage
|
|
32
|
+
'''
|
|
33
|
+
return _Argument(name=None, type=type, multiple=multiple, required=required, metavar=metavar, help=help, default=default)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Exit(Exception):
|
|
37
|
+
'''
|
|
38
|
+
Raise this exception to exit the CLI with the given exit code
|
|
39
|
+
'''
|
|
40
|
+
|
|
41
|
+
def __init__(self, code: int):
|
|
42
|
+
self.code = code
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated, List, Tuple, Union
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from tabulate import tabulate
|
|
6
|
+
|
|
7
|
+
import smart_tests.args4p.typer as typer
|
|
8
|
+
from smart_tests import args4p
|
|
9
|
+
from smart_tests.app import Application
|
|
10
|
+
from smart_tests.args4p.converters import path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@args4p.command()
|
|
14
|
+
def subsets(
|
|
15
|
+
app: Application,
|
|
16
|
+
file_before: Annotated[Path, typer.Argument(type=path(exists=True), help="First subset file to compare")],
|
|
17
|
+
file_after: Annotated[Path, typer.Argument(type=path(exists=True), help="Second subset file to compare")]
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Compare two subset files and display changes in test order positions
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Read files and map test paths to their indices
|
|
24
|
+
with open(file_before, 'r') as f:
|
|
25
|
+
before_tests = f.read().splitlines()
|
|
26
|
+
before_index_map = {test: idx for idx, test in enumerate(before_tests)}
|
|
27
|
+
|
|
28
|
+
with open(file_after, 'r') as f:
|
|
29
|
+
after_tests = f.read().splitlines()
|
|
30
|
+
after_index_map = {test: idx for idx, test in enumerate(after_tests)}
|
|
31
|
+
|
|
32
|
+
# List of tuples representing test order changes (before, after, diff, test)
|
|
33
|
+
rows: List[Tuple[Union[int, str], Union[int, str], Union[int, str], str]] = []
|
|
34
|
+
|
|
35
|
+
# Calculate order difference and add each test in file_after to changes
|
|
36
|
+
for after_idx, test in enumerate(after_tests):
|
|
37
|
+
if test in before_index_map:
|
|
38
|
+
before_idx = before_index_map[test]
|
|
39
|
+
diff = after_idx - before_idx
|
|
40
|
+
rows.append((before_idx + 1, after_idx + 1, diff, test))
|
|
41
|
+
else:
|
|
42
|
+
rows.append(('-', after_idx + 1, 'NEW', test))
|
|
43
|
+
|
|
44
|
+
# Add all deleted tests to changes
|
|
45
|
+
for before_idx, test in enumerate(before_tests):
|
|
46
|
+
if test not in after_index_map:
|
|
47
|
+
rows.append((before_idx + 1, '-', 'DELETED', test))
|
|
48
|
+
|
|
49
|
+
# Sort changes by the order diff
|
|
50
|
+
rows.sort(key=lambda x: (0 if isinstance(x[2], str) else 1, x[2]))
|
|
51
|
+
|
|
52
|
+
# Display results in a tabular format
|
|
53
|
+
headers = ["Before", "After", "After - Before", "Test"]
|
|
54
|
+
tabular_data = [
|
|
55
|
+
(before, after, f"{diff:+}" if isinstance(diff, int) else diff, test)
|
|
56
|
+
for before, after, diff, test in rows
|
|
57
|
+
]
|
|
58
|
+
click.echo_via_pager(tabulate(tabular_data, headers=headers, tablefmt="github"))
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
import smart_tests.args4p.typer as typer
|
|
8
|
+
from smart_tests.app import Application
|
|
9
|
+
from smart_tests.args4p.command import Group
|
|
10
|
+
from smart_tests.commands.test_path_writer import TestPathWriter
|
|
11
|
+
from smart_tests.testpath import unparse_test_path
|
|
12
|
+
from smart_tests.utils.commands import Command
|
|
13
|
+
from smart_tests.utils.env_keys import REPORT_ERROR_KEY
|
|
14
|
+
from smart_tests.utils.exceptions import print_error_and_die
|
|
15
|
+
from smart_tests.utils.session import get_session
|
|
16
|
+
from smart_tests.utils.smart_tests_client import SmartTestsClient
|
|
17
|
+
from smart_tests.utils.tracking import Tracking, TrackingClient
|
|
18
|
+
from smart_tests.utils.typer_types import ignorable_error
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DetectFlakesRetryThreshold(str, Enum):
|
|
22
|
+
LOW = "LOW"
|
|
23
|
+
MEDIUM = "MEDIUM"
|
|
24
|
+
HIGH = "HIGH"
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def from_str(value: str) -> "DetectFlakesRetryThreshold":
|
|
28
|
+
for member in DetectFlakesRetryThreshold:
|
|
29
|
+
if member.value.lower() == value.lower():
|
|
30
|
+
return member
|
|
31
|
+
raise ValueError(f"Invalid value for DetectFlakesRetryThreshold: {value}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DetectFlakes(TestPathWriter):
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
app: Application,
|
|
38
|
+
session: Annotated[str, typer.Option(
|
|
39
|
+
"--session",
|
|
40
|
+
help="In the format builds/<build-name>/test_sessions/<test-session-id>",
|
|
41
|
+
metavar="SESSION",
|
|
42
|
+
required=True
|
|
43
|
+
)],
|
|
44
|
+
retry_threshold: Annotated[DetectFlakesRetryThreshold, typer.Option(
|
|
45
|
+
"--retry-threshold",
|
|
46
|
+
help="Thoroughness of how \"flake\" is detected",
|
|
47
|
+
type=DetectFlakesRetryThreshold.from_str,
|
|
48
|
+
metavar="low|medium|high"
|
|
49
|
+
)] = DetectFlakesRetryThreshold.MEDIUM,
|
|
50
|
+
test_runner: Annotated[str | None, typer.Argument()] = None,
|
|
51
|
+
):
|
|
52
|
+
super().__init__(app)
|
|
53
|
+
|
|
54
|
+
app.test_runner = test_runner
|
|
55
|
+
self.tracking_client = TrackingClient(Command.DETECT_FLAKE, app=app)
|
|
56
|
+
self.client = SmartTestsClient(app=app, tracking_client=self.tracking_client)
|
|
57
|
+
|
|
58
|
+
self.session = session
|
|
59
|
+
self.test_session = None
|
|
60
|
+
try:
|
|
61
|
+
self.test_session = get_session(client=self.client, session=session)
|
|
62
|
+
except ValueError as e:
|
|
63
|
+
print_error_and_die(msg=str(e), tracking_client=self.tracking_client, event=Tracking.ErrorEvent.USER_ERROR)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
if os.getenv(REPORT_ERROR_KEY):
|
|
66
|
+
raise e
|
|
67
|
+
else:
|
|
68
|
+
click.echo(ignorable_error(e), err=True)
|
|
69
|
+
|
|
70
|
+
if self.test_session is None:
|
|
71
|
+
raise typer.Exit(0) # bail out
|
|
72
|
+
|
|
73
|
+
self.retry_threshold = retry_threshold
|
|
74
|
+
|
|
75
|
+
def run(self):
|
|
76
|
+
test_paths = []
|
|
77
|
+
try:
|
|
78
|
+
res = self.client.request(
|
|
79
|
+
"get",
|
|
80
|
+
"detect-flake",
|
|
81
|
+
params={
|
|
82
|
+
"confidence": self.retry_threshold.value.upper(),
|
|
83
|
+
"session-id": os.path.basename(self.session),
|
|
84
|
+
"test-runner": self.app.test_runner,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
res.raise_for_status()
|
|
88
|
+
test_paths = res.json().get("testPaths", [])
|
|
89
|
+
if test_paths:
|
|
90
|
+
self.print(test_paths)
|
|
91
|
+
click.echo("Trying to retry the following tests:", err=True)
|
|
92
|
+
for detail in res.json().get("testDetails", []):
|
|
93
|
+
click.echo(f"{detail.get('reason'): {unparse_test_path(detail.get('fullTestPath'))}}", err=True)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
self.tracking_client.send_error_event(
|
|
96
|
+
event_name=Tracking.ErrorEvent.INTERNAL_CLI_ERROR,
|
|
97
|
+
stack_trace=str(e),
|
|
98
|
+
)
|
|
99
|
+
if os.getenv(REPORT_ERROR_KEY):
|
|
100
|
+
raise e
|
|
101
|
+
else:
|
|
102
|
+
click.echo(ignorable_error(e), err=True)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
detect_flakes = Group(name="detect-flakes", callback=DetectFlakes, help="Detect flaky tests")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from ... import args4p
|
|
2
|
+
from ...app import Application
|
|
3
|
+
from .model import model
|
|
4
|
+
from .subset import subset
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@args4p.group(help="Inspect test and subset data")
|
|
8
|
+
def inspect(app: Application):
|
|
9
|
+
return app
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
inspect.add_command(model)
|
|
13
|
+
inspect.add_command(subset)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from requests import Response
|
|
8
|
+
from tabulate import tabulate
|
|
9
|
+
|
|
10
|
+
from smart_tests import args4p
|
|
11
|
+
from smart_tests.app import Application
|
|
12
|
+
from smart_tests.args4p import typer
|
|
13
|
+
from smart_tests.utils.smart_tests_client import SmartTestsClient
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@args4p.command()
|
|
17
|
+
def model(
|
|
18
|
+
app: Application,
|
|
19
|
+
is_json_format: Annotated[bool, typer.Option(
|
|
20
|
+
'--json',
|
|
21
|
+
help="display JSON format"
|
|
22
|
+
)] = False):
|
|
23
|
+
client = SmartTestsClient(app=app)
|
|
24
|
+
try:
|
|
25
|
+
res: Response = client.request("get", "model-metadata")
|
|
26
|
+
|
|
27
|
+
if res.status_code == HTTPStatus.NOT_FOUND:
|
|
28
|
+
click.echo(click.style(
|
|
29
|
+
"Model metadata currently not available for this workspace.", 'yellow'), err=True)
|
|
30
|
+
sys.exit()
|
|
31
|
+
|
|
32
|
+
res.raise_for_status()
|
|
33
|
+
|
|
34
|
+
if is_json_format:
|
|
35
|
+
display_as_json(res)
|
|
36
|
+
else:
|
|
37
|
+
display_as_table(res)
|
|
38
|
+
|
|
39
|
+
except Exception as e:
|
|
40
|
+
client.print_exception_and_recover(e, "Warning: failed to inspect model")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def display_as_json(res: Response):
|
|
44
|
+
res_json = res.json()
|
|
45
|
+
click.echo(json.dumps(res_json, indent=2))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def display_as_table(res: Response):
|
|
49
|
+
headers = ["Metadata", "Value"]
|
|
50
|
+
res_json = res.json()
|
|
51
|
+
rows = [["Training Cutoff Test Session ID", res_json['training_cutoff_test_session_id']]]
|
|
52
|
+
click.echo(tabulate(rows, headers, tablefmt="github"))
|