Skip to content

Commit 4bd11ba

Browse files
committed
Add karma, and ng test runners
1 parent 852ae4a commit 4bd11ba

File tree

7 files changed

+780
-0
lines changed

7 files changed

+780
-0
lines changed

launchable/test_runners/karma.py

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

launchable/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 launchable
2+
3+
4+
@launchable.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()

0 commit comments

Comments
 (0)