diff --git a/launchable/test_runners/jasmine.py b/launchable/test_runners/jasmine.py new file mode 100644 index 000000000..8a9e19f3c --- /dev/null +++ b/launchable/test_runners/jasmine.py @@ -0,0 +1,159 @@ +import json +from typing import Dict, Generator, List + +import click + +from ..commands.record.case_event import CaseEvent +from ..testpath import TestPath +from . import launchable + + +@click.argument('reports', required=True, nargs=-1) +@launchable.record.tests +def record_tests(client, reports): + client.parse_func = JSONReportParser(client).parse_func + + for r in reports: + client.report(r) + + client.run() + + +@launchable.subset +def subset(client): + # read lines as test file names + for t in client.stdin(): + client.test_path(t.rstrip("\n")) + + client.run() + + +split_subset = launchable.CommonSplitSubsetImpls(__name__).split_subset() + + +class JSONReportParser: + """ + Sample report format: + { + "suite1": { + "id": "suite1", + "description": "Player", + "fullName": "Player", + "parentSuiteId": null, + "filename": "/path/to/spec/PlayerSpec.js", + "failedExpectations": [], + "deprecationWarnings": [], + "duration": 3, + "properties": null, + "status": "passed", + "specs": [ + { + "id": "spec0", + "description": "should be able to play a Song", + "fullName": "Player should be able to play a Song", + "parentSuiteId": "suite1", + "filename": "/path/to/spec/PlayerSpec.js", + "failedExpectations": [], + "passedExpectations": [...], + "deprecationWarnings": [], + "pendingReason": "", + "duration": 1, + "properties": null, + "debugLogs": null, + "status": "passed" + } + ] + } + } + """ + + def __init__(self, client): + self.client = client + + def parse_func(self, report_file: str) -> Generator[Dict, None, None]: # type: ignore + data: Dict[str, Dict] + with open(report_file, 'r') as json_file: + try: + data = json.load(json_file) + except Exception: + click.echo( + click.style("Error: Failed to load Json report file: {}".format(report_file), fg='red'), err=True) + return + + if not self._validate_report_format(data): + click.echo( + "Error: {} does not appear to be valid format. " + "Make sure you are using Jasmine >= v4.6.0 and jasmine-json-test-reporter as the reporter.".format( + report_file), err=True) + return + + # If validation passes, parse the suites + for suite_id, suite in data.items(): + for event in self._parse_suite(suite): + yield event + + def _validate_report_format(self, data: Dict) -> bool: + for suite in data.values(): + if not isinstance(suite, dict): + return False + + if "filename" not in suite or "specs" not in suite: + return False + + specs = suite.get("specs", []) + for spec in specs: + if not isinstance(spec, dict): + return False + if "status" not in spec or "duration" not in spec: + return False + + return True + + def _parse_suite(self, suite: Dict) -> List[Dict]: + events: List[Dict] = [] + + filename = suite.get("filename", "") + specs = suite.get("specs", []) + for spec in specs: + test_path: TestPath = [ + self.client.make_file_path_component(filename), + {"type": "testcase", "name": spec.get("fullName", spec.get("description", ""))} + ] + + duration_msec = spec.get("duration", 0) + status = self._case_event_status_from_str(spec.get("status", "")) + stderr = self._parse_stderr(spec) + + events.append(CaseEvent.create( + test_path=test_path, + duration_secs=duration_msec / 1000 if duration_msec else 0, # convert msec to sec + status=status, + stderr=stderr + )) + + return events + + def _case_event_status_from_str(self, status_str: str) -> int: + if status_str == "passed": + return CaseEvent.TEST_PASSED + elif status_str == "failed": + return CaseEvent.TEST_FAILED + else: + return CaseEvent.TEST_SKIPPED + + def _parse_stderr(self, spec: Dict) -> str: + failed_expectations = spec.get("failedExpectations", []) + if not failed_expectations: + return "" + + error_messages = [] + for expectation in failed_expectations: + message = expectation.get("message", "") + stack = expectation.get("stack", "") + + if message: + error_messages.append(message) + if stack: + error_messages.append(stack) + + return "\n".join(error_messages) diff --git a/tests/data/jasmine/jasmine-test-results.json b/tests/data/jasmine/jasmine-test-results.json new file mode 100644 index 000000000..34888dab5 --- /dev/null +++ b/tests/data/jasmine/jasmine-test-results.json @@ -0,0 +1,159 @@ +{ + "suite2": { + "id": "suite2", + "description": "when song has been paused", + "fullName": "Player when song has been paused", + "parentSuiteId": "suite1", + "filename": "spec/jasmine_examples/PlayerSpec.js", + "failedExpectations": [], + "deprecationWarnings": [], + "duration": 2, + "properties": null, + "status": "passed", + "specs": [ + { + "id": "spec1", + "description": "should indicate that the song is currently paused", + "fullName": "Player when song has been paused should indicate that the song is currently paused", + "parentSuiteId": "suite2", + "filename": "spec/jasmine_examples/PlayerSpec.js", + "failedExpectations": [], + "passedExpectations": [ + { + "matcherName": "toBeFalsy", + "message": "Passed.", + "stack": "", + "passed": true + }, + { + "matcherName": "toBePlaying", + "message": "Passed.", + "stack": "", + "passed": true + } + ], + "deprecationWarnings": [], + "pendingReason": "", + "duration": 1, + "properties": null, + "debugLogs": null, + "status": "passed" + }, + { + "id": "spec2", + "description": "should be possible to resume", + "fullName": "Player when song has been paused should be possible to resume", + "parentSuiteId": "suite2", + "filename": "spec/jasmine_examples/PlayerSpec.js", + "failedExpectations": [ + { + "matcherName": "toBeFalsy", + "message": "Expected true to be falsy.", + "stack": " at \n at UserContext. (spec/jasmine_examples/PlayerSpec.js:37:29)\n at ", + "passed": false, + "expected": [], + "actual": true + } + ], + "passedExpectations": [ + { + "matcherName": "toEqual", + "message": "Passed.", + "stack": "", + "passed": true + } + ], + "deprecationWarnings": [], + "pendingReason": "", + "duration": 1, + "properties": null, + "debugLogs": null, + "status": "failed" + } + ] + }, + "suite1": { + "id": "suite1", + "description": "Player", + "fullName": "Player", + "parentSuiteId": null, + "filename": "spec/jasmine_examples/PlayerSpec.js", + "failedExpectations": [], + "deprecationWarnings": [], + "duration": 2, + "properties": null, + "status": "passed", + "specs": [ + { + "id": "spec0", + "description": "should be able to play a Song", + "fullName": "Player should be able to play a Song", + "parentSuiteId": "suite1", + "filename": "spec/jasmine_examples/PlayerSpec.js", + "failedExpectations": [], + "passedExpectations": [ + { + "matcherName": "toEqual", + "message": "Passed.", + "stack": "", + "passed": true + }, + { + "matcherName": "toBePlaying", + "message": "Passed.", + "stack": "", + "passed": true + } + ], + "deprecationWarnings": [], + "pendingReason": "", + "duration": 0, + "properties": null, + "debugLogs": null, + "status": "passed" + } + ] + }, + "suite3": { + "id": "suite3", + "description": "User", + "fullName": "User", + "parentSuiteId": null, + "filename": "spec/jasmine_examples/UserSpec.js", + "failedExpectations": [], + "deprecationWarnings": [], + "duration": 0, + "properties": null, + "status": "passed", + "specs": [ + { + "id": "spec3", + "description": "should be able to play a Song", + "fullName": "User should be able to play a Song", + "parentSuiteId": "suite3", + "filename": "spec/jasmine_examples/UserSpec.js", + "failedExpectations": [], + "passedExpectations": [ + { + "matcherName": "toEqual", + "message": "Passed.", + "stack": "", + "passed": true + }, + { + "matcherName": "toBePlaying", + "message": "Passed.", + "stack": "", + "passed": true + } + ], + "deprecationWarnings": [], + "pendingReason": "", + "duration": 0, + "properties": null, + "debugLogs": null, + "status": "passed" + } + ] + } +} diff --git a/tests/data/jasmine/record_test_result.json b/tests/data/jasmine/record_test_result.json new file mode 100644 index 000000000..52a2d4639 --- /dev/null +++ b/tests/data/jasmine/record_test_result.json @@ -0,0 +1,81 @@ +{ + "events": [ + { + "type": "case", + "testPath": [ + { + "type": "file", + "name": "spec/jasmine_examples/PlayerSpec.js" + }, + { + "type": "testcase", + "name": "Player when song has been paused should indicate that the song is currently paused" + } + ], + "duration": 0.001, + "status": 1, + "stdout": "", + "stderr": "", + "data": null + }, + { + "type": "case", + "testPath": [ + { + "type": "file", + "name": "spec/jasmine_examples/PlayerSpec.js" + }, + { + "type": "testcase", + "name": "Player when song has been paused should be possible to resume" + } + ], + "duration": 0.001, + "status": 0, + "stdout": "", + "stderr": "Expected true to be falsy.\n at \n at UserContext. (spec/jasmine_examples/PlayerSpec.js:37:29)\n at ", + "data": null + }, + { + "type": "case", + "testPath": [ + { + "type": "file", + "name": "spec/jasmine_examples/PlayerSpec.js" + }, + { + "type": "testcase", + "name": "Player should be able to play a Song" + } + ], + "duration": 0.0, + "status": 1, + "stdout": "", + "stderr": "", + "data": null + }, + { + "type": "case", + "testPath": [ + { + "type": "file", + "name": "spec/jasmine_examples/UserSpec.js" + }, + { + "type": "testcase", + "name": "User should be able to play a Song" + } + ], + "duration": 0.0, + "status": 1, + "stdout": "", + "stderr": "", + "data": null + } + ], + "testRunner": "jasmine", + "group": "", + "noBuild": false, + "flavors": [], + "testSuite": "" +} diff --git a/tests/data/jasmine/subset_payload.json b/tests/data/jasmine/subset_payload.json new file mode 100644 index 000000000..85c8e183f --- /dev/null +++ b/tests/data/jasmine/subset_payload.json @@ -0,0 +1,16 @@ +{ + "testPaths": [ + [ + { "type": "file", "name": "spec/jasmine_examples/PlayerSpec.js" } + ], + [ + { "type": "file", "name": "spec/jasmine_examples/UserSpec.js" } + ] + ], + "testRunner": "jasmine", + "goal": {"type": "subset-by-percentage", "percentage": 0.1}, + "ignoreNewTests": false, + "session": { "id": "16" }, + "getTestsFromGuess": false, + "getTestsFromPreviousSessions": false +} diff --git a/tests/test_runners/test_jasmine.py b/tests/test_runners/test_jasmine.py new file mode 100644 index 000000000..2a49d07ad --- /dev/null +++ b/tests/test_runners/test_jasmine.py @@ -0,0 +1,32 @@ +import os +from unittest import mock + +import responses # type: ignore + +from launchable.utils.session import write_build +from tests.cli_test_case import CliTestCase + + +class JasmineTest(CliTestCase): + @responses.activate + @mock.patch.dict(os.environ, + {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) + def test_record_test_json(self): + result = self.cli('record', 'tests', '--session', self.session, + 'jasmine', str(self.test_files_dir.joinpath("jasmine-test-results.json"))) + + self.assert_success(result) + self.assert_record_tests_payload('record_test_result.json') + + @responses.activate + @mock.patch.dict(os.environ, + {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) + def test_subset(self): + write_build(self.build_name) + + subset_input = """spec/jasmine_examples/PlayerSpec.js +spec/jasmine_examples/UserSpec.js +""" + result = self.cli('subset', '--target', '10%', 'jasmine', input=subset_input) + self.assert_success(result) + self.assert_subset_payload('subset_payload.json')