Skip to content

Commit e8b15bd

Browse files
authored
Merge pull request #1199 from cloudbees-oss/v1-sync
V1 sync
2 parents 14eee43 + 7743e3b commit e8b15bd

File tree

24 files changed

+1070
-84
lines changed

24 files changed

+1070
-84
lines changed

smart_tests/commands/verify.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
import click
88

99
import smart_tests.args4p.typer as typer
10-
from smart_tests.utils.commands import Command
11-
from smart_tests.utils.env_keys import TOKEN_KEY
1210
from smart_tests.utils.tracking import Tracking, TrackingClient
1311

1412
from .. import args4p
1513
from ..app import Application
16-
from ..utils.authentication import get_org_workspace
14+
from ..utils.authentication import ensure_org_workspace, get_org_workspace
15+
from ..utils.commands import Command
16+
from ..utils.env_keys import TOKEN_KEY
1717
from ..utils.java import get_java_command
1818
from ..utils.smart_tests_client import SmartTestsClient
1919
from ..utils.typer_types import emoji
@@ -82,18 +82,8 @@ def verify(app_instance: Application):
8282
click.echo("Java command: " + repr(java))
8383
click.echo("smart-tests version: " + repr(version))
8484

85-
if org is None or workspace is None:
86-
msg = (
87-
"Could not identify Smart Tests organization/workspace. "
88-
"Please confirm if you set SMART_TESTS_TOKEN or SMART_TESTS_ORGANIZATION and SMART_TESTS_WORKSPACE "
89-
"environment variables"
90-
)
91-
tracking_client.send_error_event(
92-
event_name=Tracking.ErrorEvent.INTERNAL_CLI_ERROR,
93-
stack_trace=msg
94-
)
95-
click.secho(msg, fg='red', err=True)
96-
raise typer.Exit(1)
85+
# raise an error here after we print out the basic diagnostics if LAUNCHABLE_TOKEN is not set.
86+
ensure_org_workspace()
9787

9888
try:
9989
res = client.request("get", "verification")

smart_tests/test_runners/file.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,9 @@
22
# The most bare-bone versions of the test runner support
33
#
44

5-
from ..commands.subset import Subset
65
from . import smart_tests
76

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-
7+
subset = smart_tests.CommonSubsetImpls(__name__).scan_stdin()
188
record_tests = smart_tests.CommonRecordTestImpls(__name__).file_profile_report_files()
199
smart_tests.CommonDetectFlakesImpls(__name__).detect_flakes()
2010
# split_subset = launchable.CommonSplitSubsetImpls(__name__).split_subset()

smart_tests/test_runners/jasmine.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,7 @@ def record_tests(
2727
client.run()
2828

2929

30-
@smart_tests.subset
31-
def subset(client):
32-
# read lines as test file names
33-
for t in client.stdin():
34-
client.test_path(t.rstrip("\n"))
35-
36-
client.run()
30+
subset = smart_tests.CommonSubsetImpls(__name__).scan_stdin()
3731

3832

3933
class JSONReportParser:

smart_tests/test_runners/karma.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# This runner only supports recording tests
2+
# For subsetting, use 'ng' test runner instead
3+
# It's possible to use 'karma' runner for recording, and 'ng' runner for subsetting, for the same test session
4+
import json
5+
from typing import Annotated, Dict, Generator, List
6+
7+
import click
8+
9+
from ..args4p import typer
10+
from ..commands.record.case_event import CaseEvent
11+
from ..testpath import TestPath
12+
from . import smart_tests
13+
14+
15+
@smart_tests.subset
16+
def subset(client, _with: Annotated[str | None, typer.Option(
17+
'--with', help="Specify 'ng' to use the Angular test runner for subsetting")] = None, ):
18+
# TODO: implement the --with ng option
19+
20+
# read lines as test file names
21+
for t in client.stdin():
22+
client.test_path(t.rstrip("\n"))
23+
24+
client.run()
25+
26+
27+
@smart_tests.record.tests
28+
def record_tests(client,
29+
reports: Annotated[List[str], typer.Argument(multiple=True, help="Test report files to process")],
30+
):
31+
client.parse_func = JSONReportParser(client).parse_func
32+
33+
for r in reports:
34+
client.report(r)
35+
36+
client.run()
37+
38+
39+
class JSONReportParser:
40+
"""
41+
Sample Karma report format:
42+
{
43+
"browsers": {...},
44+
"result": {
45+
"24461741": [
46+
{
47+
"fullName": "path/to/spec.ts should do something",
48+
"description": "should do something",
49+
"id": "spec0",
50+
"log": [],
51+
"skipped": false,
52+
"disabled": false,
53+
"pending": false,
54+
"success": true,
55+
"suite": [
56+
"path/to/spec.ts"
57+
],
58+
"time": 92,
59+
"executedExpectationsCount": 1,
60+
"passedExpectations": [...],
61+
"properties": null
62+
}
63+
]
64+
},
65+
"summary": {...}
66+
}
67+
"""
68+
69+
def __init__(self, client):
70+
self.client = client
71+
72+
def parse_func(self, report_file: str) -> Generator[Dict, None, None]: # type: ignore
73+
data: Dict
74+
with open(report_file, 'r') as json_file:
75+
try:
76+
data = json.load(json_file)
77+
except Exception:
78+
click.echo(
79+
click.style("Error: Failed to load Json report file: {}".format(report_file), fg='red'), err=True)
80+
return
81+
82+
if not self._validate_report_format(data):
83+
click.echo(
84+
"Error: {} does not appear to be valid Karma report format. "
85+
"Make sure you are using karma-json-reporter or a compatible reporter.".format(
86+
report_file), err=True)
87+
return
88+
89+
results = data.get("result", {})
90+
for browser_id, specs in results.items():
91+
if isinstance(specs, list):
92+
for event in self._parse_specs(specs):
93+
yield event
94+
95+
def _validate_report_format(self, data: Dict) -> bool:
96+
if not isinstance(data, dict):
97+
return False
98+
99+
if "result" not in data:
100+
return False
101+
102+
results = data.get("result", {})
103+
if not isinstance(results, dict):
104+
return False
105+
106+
for browser_id, specs in results.items():
107+
if not isinstance(specs, list):
108+
return False
109+
110+
for spec in specs:
111+
if not isinstance(spec, dict):
112+
return False
113+
# Check for required fields
114+
if "suite" not in spec or "time" not in spec:
115+
return False
116+
# Field suite should have at least one element (filename)
117+
suite = spec.get("suite", [])
118+
if not isinstance(suite, list) or len(suite) == 0:
119+
return False
120+
121+
return True
122+
123+
def _parse_specs(self, specs: List[Dict]) -> List[Dict]:
124+
events: List[Dict] = []
125+
126+
for spec in specs:
127+
# TODO:
128+
# In NextWorld, test filepaths are included in the suite tag
129+
# But generally in a Karma test report, a suite tag can be any string
130+
# For the time being let's get filepaths from the suite tag,
131+
# until we find a standard way to include filepaths in the test reports
132+
suite = spec.get("suite", [])
133+
filename = suite[0] if suite else ""
134+
135+
test_path: TestPath = [
136+
self.client.make_file_path_component(filename),
137+
{"type": "testcase", "name": spec.get("fullName", spec.get("description", ""))}
138+
]
139+
140+
duration_msec = spec.get("time", 0)
141+
status = self._case_event_status_from_spec(spec)
142+
stderr = self._parse_stderr(spec)
143+
144+
events.append(CaseEvent.create(
145+
test_path=test_path,
146+
duration_secs=duration_msec / 1000 if duration_msec else 0,
147+
status=status,
148+
stderr=stderr
149+
))
150+
151+
return events
152+
153+
def _case_event_status_from_spec(self, spec: Dict) -> int:
154+
if spec.get("skipped", False) or spec.get("disabled", False) or spec.get("pending", False):
155+
return CaseEvent.TEST_SKIPPED
156+
157+
if spec.get("success", False):
158+
return CaseEvent.TEST_PASSED
159+
else:
160+
return CaseEvent.TEST_FAILED
161+
162+
def _parse_stderr(self, spec: Dict) -> str:
163+
log_messages = spec.get("log", [])
164+
if not log_messages:
165+
return ""
166+
167+
return "\n".join(str(msg) for msg in log_messages if msg)

smart_tests/test_runners/ng.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from . import smart_tests
2+
3+
4+
@smart_tests.subset
5+
def subset(client):
6+
"""
7+
Input format example:
8+
src/app/feature/feature.component.spec.ts
9+
src/app/service/service.service.spec.ts
10+
11+
Output format: --include=<path> format that can be passed to ng test
12+
Example:
13+
--include=src/app/feature/feature.component.spec.ts --include=src/app/service/service.service.spec.ts
14+
"""
15+
for t in client.stdin():
16+
path = t.strip()
17+
if path:
18+
client.test_path(path)
19+
20+
client.formatter = lambda x: "--include={}".format(x[0]['name'])
21+
client.separator = " "
22+
client.run()

smart_tests/test_runners/playwright.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from ..args4p.exceptions import BadCmdLineException
1414
from ..commands.record.case_event import CaseEvent, CaseEventGenerator
1515
from ..commands.record.tests import RecordTests
16-
from ..commands.subset import Subset
1716
from ..testpath import TestPath
1817
from . import smart_tests
1918

@@ -63,13 +62,7 @@ def path_builder(case: TestCase, suite: TestSuite,
6362
client.run()
6463

6564

66-
@smart_tests.subset
67-
def subset(client: Subset):
68-
# read lines as test file names
69-
for t in client.stdin():
70-
client.test_path(t.rstrip("\n"))
71-
72-
client.run()
65+
subset = smart_tests.CommonSubsetImpls(__name__).scan_stdin()
7366

7467

7568
class JSONReportParser:

smart_tests/test_runners/prove.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
from ..args4p.exceptions import BadCmdLineException
99
from ..commands.record.tests import RecordTests
10-
from ..commands.subset import Subset
1110
from ..testpath import TestPath
1211
from . import smart_tests
1312

@@ -19,13 +18,7 @@ def remove_leading_number_and_dash(input_string: str) -> str:
1918
return result
2019

2120

22-
@smart_tests.subset
23-
def subset(client: Subset):
24-
# read lines as test file names
25-
for t in client.stdin():
26-
client.test_path(t.rstrip("\n"))
27-
28-
client.run()
21+
subset = smart_tests.CommonSubsetImpls(__name__).scan_stdin()
2922

3023

3124
@smart_tests.record.tests

smart_tests/test_runners/smart_tests.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,27 @@ class CommonSubsetImpls:
7575
def __init__(self, module_name):
7676
self.cmdname = cmdname(module_name)
7777

78-
def scan_files(self, pattern):
78+
def scan_stdin(self):
79+
"""
80+
Historical implementation of the files profile that's also used elsewhere.
81+
Reads test one line at a time from stdin. Consider this implementation deprecated.
82+
Newer test runners are advised to use scan_files() without the pattern argument.
83+
"""
84+
def subset(client):
85+
# read lines as test file names
86+
for t in client.stdin():
87+
client.test_path(t.rstrip("\n"))
88+
client.run()
89+
90+
return wrap(subset, subset_cmd, self.cmdname)
91+
92+
def scan_files(self, pattern=None):
7993
"""
8094
Suitable for test runners that use files as unit of tests where file names follow a naming pattern.
8195
8296
:param pattern: file masks that identify test files, such as '*_spec.rb'
97+
for test runners that do not have natural file naming conventions, pass in None,
98+
so that the implementation will refuse to accept directories.
8399
"""
84100
def subset(
85101
client: Subset,
@@ -92,6 +108,8 @@ def subset(
92108
# client type: Optimize in def lauchable.commands.subset.subset
93109
def parse(fname: str):
94110
if os.path.isdir(fname):
111+
if pattern is None:
112+
raise click.UsageError(f'{fname} is a directory, but expecting a file or GLOB')
95113
client.scan(fname, '**/' + pattern)
96114
elif fname == '@-':
97115
# read stdin

smart_tests/test_runners/vitest.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import smart_tests.args4p.typer as typer
55

66
from ..commands.record.tests import RecordTests
7-
from ..commands.subset import Subset
87
from . import smart_tests
98

109

@@ -40,10 +39,4 @@ def parse_func(report: str) -> ET.ElementTree:
4039
smart_tests.CommonRecordTestImpls.load_report_files(client=client, source_roots=reports)
4140

4241

43-
@smart_tests.subset
44-
def subset(client: Subset):
45-
# read lines as test file names
46-
for t in client.stdin():
47-
client.test_path(t.rstrip("\n"))
48-
49-
client.run()
42+
subset = smart_tests.CommonSubsetImpls(__name__).scan_stdin()

smart_tests/utils/authentication.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@
1010

1111

1212
def get_org_workspace():
13+
'''
14+
Returns (org,ws) tuple from LAUNCHABLE_TOKEN, or (None,None) if not found.
15+
Use ensure_org_workspace() if this is supposed to be an error condition
16+
'''
1317
token = get_token()
1418
if token:
1519
try:
1620
_, user, _ = token.split(":", 2)
1721
org, workspace = user.split("/", 1)
1822
return org, workspace
1923
except ValueError:
20-
return None, None
24+
click.secho("Invalid value in LAUNCHABLE_TOKEN environment variable.", fg="red")
25+
raise typer.Exit(1)
2126

2227
return os.getenv(ORGANIZATION_KEY), os.getenv(WORKSPACE_KEY)
2328

0 commit comments

Comments
 (0)