Skip to content

Commit 79364c9

Browse files
gayanWkohsuke
authored andcommitted
Add karma, and ng test runners
1 parent 852ae4a commit 79364c9

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()
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
{
2+
"events": [
3+
{
4+
"type": "case",
5+
"testPath": [
6+
{
7+
"type": "file",
8+
"name": "foo/bar/zot.spec.ts"
9+
},
10+
{
11+
"type": "testcase",
12+
"name": "foo/bar/zot.spec.ts should feed the monkey"
13+
}
14+
],
15+
"duration": 0.092,
16+
"status": 1,
17+
"stdout": "",
18+
"stderr": "",
19+
"data": null
20+
},
21+
{
22+
"type": "case",
23+
"testPath": [
24+
{
25+
"type": "file",
26+
"name": "foo/bar/zot.spec.ts"
27+
},
28+
{
29+
"type": "testcase",
30+
"name": "foo/bar/zot.spec.ts should fetch the toy"
31+
}
32+
],
33+
"duration": 0.027,
34+
"status": 1,
35+
"stdout": "",
36+
"stderr": "",
37+
"data": null
38+
},
39+
{
40+
"type": "case",
41+
"testPath": [
42+
{
43+
"type": "file",
44+
"name": "foo/bar/zot.spec.ts"
45+
},
46+
{
47+
"type": "testcase",
48+
"name": "foo/bar/zot.spec.ts should fetch the toyForRecord - record found"
49+
}
50+
],
51+
"duration": 0.028,
52+
"status": 1,
53+
"stdout": "",
54+
"stderr": "",
55+
"data": null
56+
},
57+
{
58+
"type": "case",
59+
"testPath": [
60+
{
61+
"type": "file",
62+
"name": "foo/bar/zot.spec.ts"
63+
},
64+
{
65+
"type": "testcase",
66+
"name": "foo/bar/zot.spec.ts should fetch the toyForRecord - record not found"
67+
}
68+
],
69+
"duration": 0.027,
70+
"status": 1,
71+
"stdout": "",
72+
"stderr": "",
73+
"data": null
74+
},
75+
{
76+
"type": "case",
77+
"testPath": [
78+
{
79+
"type": "file",
80+
"name": "foo/bar/zot.spec.ts"
81+
},
82+
{
83+
"type": "testcase",
84+
"name": "foo/bar/zot.spec.ts should fetch the toyForRecord - error in response"
85+
}
86+
],
87+
"duration": 0.033,
88+
"status": 1,
89+
"stdout": "",
90+
"stderr": "",
91+
"data": null
92+
},
93+
{
94+
"type": "case",
95+
"testPath": [
96+
{
97+
"type": "file",
98+
"name": "foo/bar/zot.spec.ts"
99+
},
100+
{
101+
"type": "testcase",
102+
"name": "foo/bar/zot.spec.ts should throw a ball"
103+
}
104+
],
105+
"duration": 0.026,
106+
"status": 1,
107+
"stdout": "",
108+
"stderr": "",
109+
"data": null
110+
},
111+
{
112+
"type": "case",
113+
"testPath": [
114+
{
115+
"type": "file",
116+
"name": "foo/bar/zot.spec.ts"
117+
},
118+
{
119+
"type": "testcase",
120+
"name": "foo/bar/zot.spec.ts should be nice to the zookeeper"
121+
}
122+
],
123+
"duration": 0.024,
124+
"status": 1,
125+
"stdout": "",
126+
"stderr": "",
127+
"data": null
128+
},
129+
{
130+
"type": "case",
131+
"testPath": [
132+
{
133+
"type": "file",
134+
"name": "foo/bar/zot.spec.ts"
135+
},
136+
{
137+
"type": "testcase",
138+
"name": "foo/bar/zot.spec.ts should like a banana or two"
139+
}
140+
],
141+
"duration": 0.024,
142+
"status": 1,
143+
"stdout": "",
144+
"stderr": "",
145+
"data": null
146+
},
147+
{
148+
"type": "case",
149+
"testPath": [
150+
{
151+
"type": "file",
152+
"name": "foo/bar/zot.spec.ts"
153+
},
154+
{
155+
"type": "testcase",
156+
"name": "foo/bar/zot.spec.ts should fall back to an apple if banana is not available"
157+
}
158+
],
159+
"duration": 0.025,
160+
"status": 1,
161+
"stdout": "",
162+
"stderr": "",
163+
"data": null
164+
},
165+
{
166+
"type": "case",
167+
"testPath": [
168+
{
169+
"type": "file",
170+
"name": "foo/bar/zot.spec.ts"
171+
},
172+
{
173+
"type": "testcase",
174+
"name": "foo/bar/zot.spec.ts should accept oranges if apple is not available"
175+
}
176+
],
177+
"duration": 0.032,
178+
"status": 1,
179+
"stdout": "",
180+
"stderr": "",
181+
"data": null
182+
}
183+
],
184+
"testRunner": "karma",
185+
"group": "",
186+
"noBuild": false,
187+
"flavors": [],
188+
"testSuite": ""
189+
}

0 commit comments

Comments
 (0)