blob: eb14ef22842d3e2272d46a209fb1c16174822e33 [file] [edit]
#!/usr/bin/env python3
# Copyright (C) 2026 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
import contextlib
import io
import os
import re
import shutil
import sys
import tempfile
import unittest
from unittest import mock
import yaml
import runner
class TestParseMetadata(unittest.TestCase):
def test_basic_metadata(self):
contents = """\
/*---
description: A test
features: [SharedArrayBuffer]
flags: [noStrict]
negative:
type: TypeError
phase: parse
includes: [testTypedArray.js]
---*/
var x = 1;
"""
meta = runner._parse_metadata(contents, "test.js")
self.assertEqual(meta["description"], "A test")
self.assertEqual(meta["features"], ["SharedArrayBuffer"])
self.assertEqual(meta["flags"], ["noStrict"])
self.assertEqual(meta["negative"]["type"], "TypeError")
self.assertEqual(meta["includes"], ["testTypedArray.js"])
def test_no_metadata(self):
self.assertEqual(runner._parse_metadata("var x = 1;", "test.js"), {})
def test_empty_metadata(self):
contents = "/*---\n---*/"
self.assertEqual(runner._parse_metadata(contents, "test.js"), {})
def test_malformed_yaml(self):
contents = "/*---\n: : : [invalid\n---*/"
meta = runner._parse_metadata(contents, "test.js")
self.assertEqual(meta, {})
def test_multiple_features(self):
contents = """\
/*---
features: [Atomics, SharedArrayBuffer, TypedArray]
---*/
"""
meta = runner._parse_metadata(contents, "test.js")
self.assertEqual(
meta["features"], ["Atomics", "SharedArrayBuffer", "TypedArray"]
)
class TestParseError(unittest.TestCase):
def test_none_input(self):
self.assertIsNone(runner._parse_error(None))
def test_empty_input(self):
self.assertIsNone(runner._parse_error(""))
def test_exception_format(self):
output = "Exception: TypeError: undefined is not an object"
self.assertEqual(
runner._parse_error(output),
"TypeError: undefined is not an object",
)
def test_exception_multiline(self):
output = "some preamble\nException: RangeError: invalid array length\nmore stuff"
self.assertEqual(
runner._parse_error(output), "RangeError: invalid array length"
)
def test_no_exception_returns_first_line(self):
output = "something went wrong\nsecond line"
self.assertEqual(runner._parse_error(output), "something went wrong")
class TestDetectPlatform(unittest.TestCase):
@mock.patch("runner.PlatformInfo")
def test_macos_debug(self, mock_platform_info):
mock_platform_info.return_value = mock.Mock(
os_name="mac", os_version="15.4.1"
)
info = runner._detect_platform(release=False, sanitizer=None)
self.assertEqual(info["os"], "macOS")
self.assertEqual(info["os_version"], "15.4.1")
self.assertEqual(info["build"], "debug")
self.assertEqual(info["sanitizer"], "")
@mock.patch("runner.PlatformInfo")
def test_release_with_sanitizer(self, mock_platform_info):
mock_platform_info.return_value = mock.Mock(
os_name="mac", os_version="15.0"
)
info = runner._detect_platform(release=True, sanitizer="asan")
self.assertEqual(info["build"], "release")
self.assertEqual(info["sanitizer"], "asan")
@mock.patch("runner.PlatformInfo")
def test_linux(self, mock_platform_info):
mock_platform_info.return_value = mock.Mock(
os_name="linux", os_version="6.1.0"
)
info = runner._detect_platform(release=False, sanitizer=None)
self.assertEqual(info["os"], "Linux")
self.assertEqual(info["os_version"], "6.1.0")
class TestEvaluateCondition(unittest.TestCase):
PLATFORM = {
"os": "macOS",
"os_version": "15.4.1",
"build": "debug",
"sanitizer": "asan",
}
def test_os_match(self):
self.assertTrue(
runner._evaluate_condition({"os": "macOS"}, self.PLATFORM)
)
def test_os_mismatch(self):
self.assertFalse(
runner._evaluate_condition({"os": "Linux"}, self.PLATFORM)
)
def test_os_case_insensitive(self):
self.assertTrue(
runner._evaluate_condition({"os": "macos"}, self.PLATFORM)
)
def test_os_version(self):
self.assertTrue(
runner._evaluate_condition(
{"os_version": ">=15.0"}, self.PLATFORM
)
)
self.assertFalse(
runner._evaluate_condition(
{"os_version": "<15.0"}, self.PLATFORM
)
)
def test_os_version_missing(self):
plat = dict(self.PLATFORM, os_version="")
self.assertFalse(
runner._evaluate_condition({"os_version": ">=15.0"}, plat)
)
def test_os_version_invalid(self):
plat = dict(self.PLATFORM, os_version="not-a-version")
self.assertFalse(
runner._evaluate_condition({"os_version": ">=15.0"}, plat)
)
def test_build_match(self):
self.assertTrue(
runner._evaluate_condition({"build": "debug"}, self.PLATFORM)
)
self.assertFalse(
runner._evaluate_condition({"build": "release"}, self.PLATFORM)
)
def test_sanitizer_match(self):
self.assertTrue(
runner._evaluate_condition({"sanitizer": "asan"}, self.PLATFORM)
)
self.assertFalse(
runner._evaluate_condition({"sanitizer": "tsan"}, self.PLATFORM)
)
def test_multiple_conditions_all_must_match(self):
self.assertTrue(
runner._evaluate_condition(
{"os": "macOS", "build": "debug"}, self.PLATFORM
)
)
self.assertFalse(
runner._evaluate_condition(
{"os": "macOS", "build": "release"}, self.PLATFORM
)
)
def test_empty_condition(self):
self.assertTrue(runner._evaluate_condition({}, self.PLATFORM))
class TestShouldSkip(unittest.TestCase):
PLATFORM = {
"os": "macOS",
"os_version": "15.4.1",
"build": "debug",
"sanitizer": "",
}
def _skip(self, rel_path, metadata=None, config=None, skip_set=None,
skip_paths=None, skip_features=None, filter_features=None,
conditional_skips=None):
return runner._should_skip(
rel_path,
metadata or {},
config or {},
skip_set or set(),
skip_paths or [],
skip_features or set(),
filter_features or set(),
self.PLATFORM,
conditional_skips or [],
)
def test_skip_by_file(self):
self.assertTrue(
self._skip(
"test/foo.js",
config={"skip": {"files": ["test/foo.js"]}},
skip_set={"test/foo.js"},
)
)
def test_skip_by_path_pattern(self):
self.assertTrue(
self._skip(
"test/annexB/foo.js",
config={"skip": {"paths": ["annexB"]}},
skip_paths=["annexB"],
)
)
def test_no_skip_without_config(self):
self.assertFalse(self._skip("test/foo.js"))
def test_skip_by_feature(self):
self.assertTrue(
self._skip(
"test/foo.js",
metadata={"features": ["Atomics"]},
config={"skip": {"features": ["Atomics"]}},
skip_features={"Atomics"},
)
)
def test_filter_features_overrides_skip(self):
self.assertFalse(
self._skip(
"test/foo.js",
metadata={"features": ["Atomics"]},
config={"skip": {"features": ["Atomics"]}},
skip_features={"Atomics"},
filter_features={"Atomics"},
)
)
def test_filter_features_skips_non_matching(self):
self.assertTrue(
self._skip(
"test/foo.js",
metadata={"features": ["Proxy"]},
config={"skip": {}},
filter_features={"Atomics"},
)
)
def test_filter_features_without_config(self):
self.assertTrue(
self._skip(
"test/foo.js",
metadata={"features": ["Proxy"]},
filter_features={"Atomics"},
)
)
self.assertFalse(
self._skip(
"test/foo.js",
metadata={"features": ["Atomics"]},
filter_features={"Atomics"},
)
)
def test_conditional_skip_by_file(self):
conds = [{"if": {"os": "macOS"}, "files": ["test/foo.js"]}]
self.assertTrue(
self._skip("test/foo.js", conditional_skips=conds)
)
def test_conditional_skip_not_matching_platform(self):
conds = [{"if": {"os": "Linux"}, "files": ["test/foo.js"]}]
self.assertFalse(
self._skip("test/foo.js", conditional_skips=conds)
)
def test_conditional_skip_by_path(self):
conds = [{"if": {"build": "debug"}, "paths": ["staging/"]}]
self.assertTrue(
self._skip("test/staging/foo.js", conditional_skips=conds)
)
def test_conditional_skip_by_feature(self):
conds = [
{"if": {"sanitizer": "asan"}, "features": ["SharedArrayBuffer"]}
]
plat = dict(self.PLATFORM, sanitizer="asan")
self.assertTrue(
runner._should_skip(
"test/foo.js",
{"features": ["SharedArrayBuffer"]},
{},
set(),
[],
set(),
set(),
plat,
conds,
)
)
def test_conditional_skip_feature_overridden_by_filter(self):
conds = [
{"if": {"sanitizer": "asan"}, "features": ["SharedArrayBuffer"]}
]
plat = dict(self.PLATFORM, sanitizer="asan")
self.assertFalse(
runner._should_skip(
"test/foo.js",
{"features": ["SharedArrayBuffer"]},
{},
set(),
[],
set(),
{"SharedArrayBuffer"},
plat,
conds,
)
)
class TestGetScenarios(unittest.TestCase):
def test_no_flags(self):
self.assertEqual(
runner._get_scenarios([]), ["strict mode", "default"]
)
def test_none_flags(self):
self.assertEqual(
runner._get_scenarios(None), ["strict mode", "default"]
)
def test_raw(self):
self.assertEqual(runner._get_scenarios(["raw"]), ["raw"])
def test_no_strict(self):
self.assertEqual(runner._get_scenarios(["noStrict"]), ["default"])
def test_only_strict(self):
self.assertEqual(
runner._get_scenarios(["onlyStrict"]), ["strict mode"]
)
def test_module(self):
self.assertEqual(runner._get_scenarios(["module"]), ["module"])
def test_unrecognized_flag(self):
self.assertEqual(
runner._get_scenarios(["async"]), ["strict mode", "default"]
)
def test_raw_takes_priority(self):
self.assertEqual(
runner._get_scenarios(["raw", "noStrict"]), ["raw"]
)
class TestGetFeatureFlags(unittest.TestCase):
def test_basic_feature_flag(self):
config = {"flags": {"SharedArrayBuffer": "useSharedArrayBuffer"}}
metadata = {"features": ["SharedArrayBuffer"]}
flags = runner._get_feature_flags(config, metadata)
self.assertEqual(flags, ["--useSharedArrayBuffer=1"])
def test_multiple_features(self):
config = {
"flags": {
"SharedArrayBuffer": "useSharedArrayBuffer",
"Atomics": "useAtomics",
}
}
metadata = {"features": ["SharedArrayBuffer", "Atomics"]}
flags = runner._get_feature_flags(config, metadata)
self.assertEqual(
flags, ["--useSharedArrayBuffer=1", "--useAtomics=1"]
)
def test_unmapped_feature(self):
config = {"flags": {"SharedArrayBuffer": "useSharedArrayBuffer"}}
metadata = {"features": ["Proxy"]}
self.assertEqual(runner._get_feature_flags(config, metadata), [])
def test_no_features(self):
config = {"flags": {"SharedArrayBuffer": "useSharedArrayBuffer"}}
self.assertEqual(runner._get_feature_flags(config, {}), [])
def test_no_config(self):
self.assertEqual(runner._get_feature_flags({}, {"features": ["X"]}), [])
def test_can_block_is_false(self):
metadata = {"flags": ["CanBlockIsFalse"]}
flags = runner._get_feature_flags({}, metadata)
self.assertEqual(flags, ["--can-block-is-false"])
def test_can_block_with_features(self):
config = {"flags": {"SharedArrayBuffer": "useSharedArrayBuffer"}}
metadata = {
"features": ["SharedArrayBuffer"],
"flags": ["CanBlockIsFalse"],
}
flags = runner._get_feature_flags(config, metadata)
self.assertEqual(
flags, ["--useSharedArrayBuffer=1", "--can-block-is-false"]
)
class TestClassifyResults(unittest.TestCase):
def _result(self, path="test/a.js", mode="default", passed=True,
error=None, is_crash=False, skipped=False, exit_code=0):
return {
"path": path,
"mode": mode,
"passed": passed,
"error": error,
"is_crash": is_crash,
"skipped": skipped,
"exit_code": exit_code,
"time": 0.1,
}
def test_expected_pass(self):
r = self._result(passed=True)
classified = runner._classify_results([r], {})
self.assertEqual(classified[0]["result"], "expected_pass")
def test_expected_pass_no_expectations(self):
r = self._result(passed=True)
classified = runner._classify_results([r], None)
self.assertEqual(classified[0]["result"], "expected_pass")
def test_unexpected_pass(self):
r = self._result(passed=True)
expectations = {"test/a.js": {"default": 3}}
classified = runner._classify_results([r], expectations)
self.assertEqual(classified[0]["result"], "unexpected_pass")
def test_expected_fail_matching_exit_code(self):
r = self._result(passed=False, exit_code=3)
expectations = {"test/a.js": {"default": 3}}
classified = runner._classify_results([r], expectations)
self.assertEqual(classified[0]["result"], "expected_fail")
def test_unexpected_fail_different_exit_code(self):
r = self._result(passed=False, exit_code=139)
expectations = {"test/a.js": {"default": 3}}
classified = runner._classify_results([r], expectations)
self.assertEqual(classified[0]["result"], "unexpected_fail")
def test_unexpected_fail_no_expectations(self):
r = self._result(passed=False, exit_code=3)
classified = runner._classify_results([r], None)
self.assertEqual(classified[0]["result"], "unexpected_fail")
def test_unexpected_fail_no_entry(self):
r = self._result(passed=False, exit_code=3)
classified = runner._classify_results([r], {})
self.assertEqual(classified[0]["result"], "unexpected_fail")
def test_crash_can_match_expectation(self):
# With exit-code-based expectations, a crash with a recorded exit
# code (e.g. 139 for SIGSEGV) is no longer "always unexpected".
r = self._result(passed=False, exit_code=139, is_crash=True)
expectations = {"test/a.js": {"default": 139}}
classified = runner._classify_results([r], expectations)
self.assertEqual(classified[0]["result"], "expected_fail")
def test_skipped(self):
r = self._result(skipped=True)
r["mode"] = "skip"
classified = runner._classify_results([r], {})
self.assertEqual(classified[0]["result"], "skip")
def test_strict_mode_independent(self):
r_default = self._result(mode="default", passed=True)
r_strict = self._result(
mode="strict mode", passed=False, exit_code=3
)
expectations = {"test/a.js": {"strict mode": 3}}
classified = runner._classify_results(
[r_default, r_strict], expectations
)
self.assertEqual(classified[0]["result"], "expected_pass")
self.assertEqual(classified[1]["result"], "expected_fail")
def test_pass_with_expected_zero_is_expected(self):
# An expected_exit of 0 is unusual (we only record fails) but the
# comparison should still work. The test passed, so since there is
# an expectation entry it counts as an unexpected_pass.
r = self._result(passed=True, exit_code=0)
expectations = {"test/a.js": {"default": 0}}
classified = runner._classify_results([r], expectations)
self.assertEqual(classified[0]["result"], "unexpected_pass")
class TestCompileHarness(unittest.TestCase):
def test_concatenates_files(self):
with tempfile.TemporaryDirectory() as d:
with open(os.path.join(d, "a.js"), "w") as f:
f.write("var a = 1;\n")
with open(os.path.join(d, "b.js"), "w") as f:
f.write("var b = 2;\n")
result = runner._compile_harness(["a.js", "b.js"], d)
self.assertEqual(result, "var a = 1;\nvar b = 2;\n")
class TestCompileIncludes(unittest.TestCase):
def test_creates_temp_file(self):
with tempfile.TemporaryDirectory() as harness_dir:
with open(os.path.join(harness_dir, "inc.js"), "w") as f:
f.write("// include\n")
with tempfile.TemporaryDirectory() as temp_dir:
path = runner._compile_includes(
["inc.js"], harness_dir, temp_dir
)
self.assertIsNotNone(path)
with open(path) as f:
self.assertEqual(f.read(), "// include\n")
def test_missing_include_returns_none(self):
with tempfile.TemporaryDirectory() as harness_dir:
with tempfile.TemporaryDirectory() as temp_dir:
path = runner._compile_includes(
["nonexistent.js"], harness_dir, temp_dir
)
self.assertIsNone(path)
def test_multiple_includes(self):
with tempfile.TemporaryDirectory() as harness_dir:
with open(os.path.join(harness_dir, "a.js"), "w") as f:
f.write("A")
with open(os.path.join(harness_dir, "b.js"), "w") as f:
f.write("B")
with tempfile.TemporaryDirectory() as temp_dir:
path = runner._compile_includes(
["a.js", "b.js"], harness_dir, temp_dir
)
with open(path) as f:
self.assertEqual(f.read(), "AB")
class TestDiscoverTests(unittest.TestCase):
def test_finds_js_files(self):
with tempfile.TemporaryDirectory() as d:
test_dir = os.path.join(d, "test")
os.makedirs(os.path.join(test_dir, "sub"))
for name in ["a.js", "b.JS", "c.txt", "d_FIXTURE.js"]:
with open(os.path.join(test_dir, "sub", name), "w") as f:
f.write("")
files = runner._discover_tests(d, ["test"])
basenames = [os.path.basename(f) for f in files]
self.assertIn("a.js", basenames)
self.assertIn("b.JS", basenames)
self.assertNotIn("c.txt", basenames)
self.assertNotIn("d_FIXTURE.js", basenames)
def test_missing_directory(self):
with tempfile.TemporaryDirectory() as d:
files = runner._discover_tests(d, ["nonexistent"])
self.assertEqual(files, [])
def test_sorted_output(self):
with tempfile.TemporaryDirectory() as d:
test_dir = os.path.join(d, "test")
os.makedirs(test_dir)
for name in ["c.js", "a.js", "b.js"]:
with open(os.path.join(test_dir, name), "w") as f:
f.write("")
files = runner._discover_tests(d, ["test"])
basenames = [os.path.basename(f) for f in files]
self.assertEqual(basenames, ["a.js", "b.js", "c.js"])
class TestLoadImportFile(unittest.TestCase):
def test_parses_added_and_modified(self):
with tempfile.TemporaryDirectory() as d:
with open(os.path.join(d, "latest-changes-summary.txt"), "w") as f:
f.write("A\ttest/foo.js\n")
f.write("M\ttest/bar.js\n")
f.write("D\ttest/deleted.js\n")
f.write("A\tsrc/other.js\n")
files = runner._load_import_file(d)
self.assertEqual(len(files), 2)
self.assertTrue(files[0].endswith("test/foo.js"))
self.assertTrue(files[1].endswith("test/bar.js"))
class TestSaveExpectations(unittest.TestCase):
def _make_result(self, path, mode, result, exit_code=0, skipped=False):
return {
"path": path,
"mode": mode,
"result": result,
"exit_code": exit_code,
"skipped": skipped,
}
def test_full_save(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml",
delete=False) as f:
fname = f.name
try:
results = [
self._make_result("test/a.js", "default", "expected_fail", 3),
self._make_result("test/a.js", "strict mode", "expected_pass"),
self._make_result("test/b.js", "default", "unexpected_fail", 139),
]
runner._save_expectations(fname, results, running_all=True)
import yaml
with open(fname) as f:
saved = yaml.safe_load(f)
self.assertIn("test/a.js", saved)
self.assertEqual(saved["test/a.js"]["default"], 3)
self.assertNotIn("strict mode", saved["test/a.js"])
self.assertIn("test/b.js", saved)
self.assertEqual(saved["test/b.js"]["default"], 139)
finally:
os.unlink(fname)
def test_documents_exit_codes(self):
import yaml
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml",
delete=False) as f:
fname = f.name
try:
results = [
self._make_result("test/a.js", "default", "expected_fail", 3),
]
# The header explains the exit codes to anyone editing the file, and
# rewriting the file must not accumulate copies of it.
runner._save_expectations(fname, results, running_all=True)
runner._save_expectations(fname, results, running_all=True)
with open(fname) as f:
text = f.read()
self.assertTrue(text.startswith("# Expected test262 failures"))
self.assertEqual(text.count("# Expected test262 failures"), 1)
self.assertEqual(yaml.safe_load(text), {"test/a.js": {"default": 3}})
finally:
os.unlink(fname)
def test_incremental_preserves_unrun(self):
import yaml
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml",
delete=False) as f:
yaml.dump({
"test/old.js": {"default": 3},
"test/a.js": {"default": 3, "strict mode": 139},
}, f)
fname = f.name
try:
results = [
self._make_result("test/a.js", "default", "expected_pass"),
]
runner._save_expectations(fname, results, running_all=False)
with open(fname) as f:
saved = yaml.safe_load(f)
self.assertIn("test/old.js", saved)
self.assertEqual(saved["test/old.js"]["default"], 3)
self.assertIn("test/a.js", saved)
self.assertEqual(saved["test/a.js"]["strict mode"], 139)
self.assertNotIn("default", saved["test/a.js"])
finally:
os.unlink(fname)
def test_incremental_adds_new_failure(self):
import yaml
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml",
delete=False) as f:
yaml.dump({"test/old.js": {"default": 3}}, f)
fname = f.name
try:
results = [
self._make_result("test/new.js", "default",
"unexpected_fail", 3),
]
runner._save_expectations(fname, results, running_all=False)
with open(fname) as f:
saved = yaml.safe_load(f)
self.assertIn("test/old.js", saved)
self.assertIn("test/new.js", saved)
self.assertEqual(saved["test/new.js"]["default"], 3)
finally:
os.unlink(fname)
def test_incremental_removes_now_passing(self):
import yaml
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml",
delete=False) as f:
yaml.dump({
"test/a.js": {"default": 3},
}, f)
fname = f.name
try:
results = [
self._make_result("test/a.js", "default", "expected_pass"),
]
runner._save_expectations(fname, results, running_all=False)
with open(fname) as f:
saved = yaml.safe_load(f)
self.assertFalse(saved)
finally:
os.unlink(fname)
def test_incremental_skipped_not_counted_as_run(self):
import yaml
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml",
delete=False) as f:
yaml.dump({
"test/a.js": {"default": 3},
}, f)
fname = f.name
try:
results = [
self._make_result("test/a.js", "skip", "skip", skipped=True),
]
runner._save_expectations(fname, results, running_all=False)
with open(fname) as f:
saved = yaml.safe_load(f)
self.assertIn("test/a.js", saved)
self.assertEqual(saved["test/a.js"]["default"], 3)
finally:
os.unlink(fname)
class TestRunJsc(unittest.TestCase):
def _base_env(self):
return {"DYLD_FRAMEWORK_PATH": "/path/to/build", "TZ": "PST"}
@mock.patch("runner.subprocess.run")
def test_pass_returns_none_output(self, mock_run):
mock_run.return_value = mock.Mock(returncode=0, stdout="some debug output\n")
code, output, _ = runner._run_jsc(
"/path/to/jsc", "/test.js", "default", {}, [],
"/harness.js", None, None, None, self._base_env(),
)
self.assertEqual(code, 0)
self.assertIsNone(output)
@mock.patch("runner.subprocess.run")
def test_failure_returns_output(self, mock_run):
mock_run.return_value = mock.Mock(
returncode=3,
stdout="Exception: TypeError: undefined is not an object\n",
)
code, output, _ = runner._run_jsc(
"/path/to/jsc", "/test.js", "default", {}, [],
"/harness.js", None, None, None, self._base_env(),
)
self.assertEqual(code, 3)
self.assertIn("TypeError", output)
@mock.patch("runner.subprocess.run")
def test_uses_env_and_fork_exec(self, mock_run):
mock_run.return_value = mock.Mock(returncode=0, stdout="")
runner._run_jsc(
"/path/to/jsc", "/test.js", "default", {}, [],
"/harness.js", None, None, None, self._base_env(),
)
kwargs = mock_run.call_args[1]
# shell=False keeps us off /bin/sh
self.assertFalse(kwargs.get("shell", False))
# env is passed directly
self.assertEqual(kwargs["env"]["DYLD_FRAMEWORK_PATH"], "/path/to/build")
self.assertEqual(kwargs["env"]["TZ"], "PST")
# stderr redirected to stdout, matching 2>&1
self.assertEqual(kwargs["stderr"], runner.subprocess.STDOUT)
@mock.patch("runner.subprocess.run")
def test_strict_mode_uses_strict_file(self, mock_run):
mock_run.return_value = mock.Mock(returncode=0, stdout="")
runner._run_jsc(
"/path/to/jsc", "/test.js", "strict mode", {}, [],
"/harness.js", None, None, None, self._base_env(),
)
cmd = mock_run.call_args[0][0]
self.assertTrue(any(a.startswith("--strict-file=") for a in cmd))
@mock.patch("runner.subprocess.run")
def test_module_uses_module_file(self, mock_run):
mock_run.return_value = mock.Mock(returncode=0, stdout="")
runner._run_jsc(
"/path/to/jsc", "/test.js", "module", {}, [],
"/harness.js", None, None, None, self._base_env(),
)
cmd = mock_run.call_args[0][0]
self.assertTrue(any(a.startswith("--module-file=") for a in cmd))
@mock.patch("runner.subprocess.run")
def test_raw_skips_harness(self, mock_run):
mock_run.return_value = mock.Mock(returncode=0, stdout="")
runner._run_jsc(
"/path/to/jsc", "/test.js", "raw", {}, [],
"/harness.js", "/async.js", None, None, self._base_env(),
)
cmd = mock_run.call_args[0][0]
self.assertNotIn("/harness.js", cmd)
self.assertNotIn("/async.js", cmd)
@mock.patch("runner.subprocess.run")
def test_async_includes_async_harness(self, mock_run):
mock_run.return_value = mock.Mock(returncode=0, stdout="")
runner._run_jsc(
"/path/to/jsc", "/test.js", "default",
{"flags": ["async"]}, [],
"/harness.js", "/async.js", None, None, self._base_env(),
)
cmd = mock_run.call_args[0][0]
self.assertIn("--test262-async", cmd)
self.assertIn("/async.js", cmd)
@mock.patch("runner.subprocess.run")
def test_negative_exception(self, mock_run):
mock_run.return_value = mock.Mock(returncode=0, stdout="")
runner._run_jsc(
"/path/to/jsc", "/test.js", "default",
{"negative": {"type": "SyntaxError"}}, [],
"/harness.js", None, None, None, self._base_env(),
)
cmd = mock_run.call_args[0][0]
self.assertIn("--exception=SyntaxError", cmd)
@mock.patch("runner.subprocess.run")
def test_watchdog_timeout(self, mock_run):
mock_run.return_value = mock.Mock(returncode=0, stdout="")
runner._run_jsc(
"/path/to/jsc", "/test.js", "default", {}, [],
"/harness.js", None, None, 10000, self._base_env(),
)
cmd = mock_run.call_args[0][0]
self.assertIn("--watchdog=10000", cmd)
@mock.patch("runner.subprocess.run")
def test_feature_flags_in_command(self, mock_run):
mock_run.return_value = mock.Mock(returncode=0, stdout="")
runner._run_jsc(
"/path/to/jsc", "/test.js", "default", {},
["--useSharedArrayBuffer=1"],
"/harness.js", None, None, None, self._base_env(),
)
cmd = mock_run.call_args[0][0]
self.assertIn("--useSharedArrayBuffer=1", cmd)
@mock.patch("runner.subprocess.run")
def test_timeout_exception(self, mock_run):
mock_run.side_effect = runner.subprocess.TimeoutExpired(cmd="jsc", timeout=300)
code, output, _ = runner._run_jsc(
"/path/to/jsc", "/test.js", "default", {}, [],
"/harness.js", None, None, None, self._base_env(),
)
self.assertEqual(code, 1)
self.assertEqual(output, "Timeout")
@mock.patch("runner.subprocess.run")
def test_no_dyld_without_darwin(self, mock_run):
mock_run.return_value = mock.Mock(returncode=0, stdout="")
runner._run_jsc(
"/path/to/jsc", "/test.js", "default", {}, [],
"/harness.js", None, None, None, {"TZ": "PST"},
)
env = mock_run.call_args[1]["env"]
self.assertNotIn("DYLD_FRAMEWORK_PATH", env)
self.assertEqual(env["TZ"], "PST")
@mock.patch("runner.subprocess.run")
def test_signal_exit_code_normalized(self, mock_run):
# Python reports signal kills as negative returncodes; the runner
# should normalize -11 (SIGSEGV) to 139 (128+11) so that the shell-
# style `exit_code & 0x7f` crash detection matches.
mock_run.return_value = mock.Mock(returncode=-11, stdout="crashy")
code, _, _ = runner._run_jsc(
"/path/to/jsc", "/test.js", "default", {}, [],
"/harness.js", None, None, None, self._base_env(),
)
self.assertEqual(code, 139)
class TestCrashDetection(unittest.TestCase):
"""Verify the exit_code & 0x7f signal extraction logic used in _process_test_file."""
def test_exit_zero_no_crash(self):
self.assertEqual(0 & 0x7f, 0)
def test_exit_three_special_cased(self):
signal = 3 & 0x7f
self.assertEqual(signal, 3)
if signal == 3:
signal = 0
self.assertEqual(signal, 0)
def test_sigsegv_detected(self):
# Shell reports 128 + 11 = 139 for SIGSEGV
signal = 139 & 0x7f
self.assertEqual(signal, 11)
self.assertNotEqual(signal, 0)
def test_sigabrt_detected(self):
# Shell reports 128 + 6 = 134 for SIGABRT
signal = 134 & 0x7f
self.assertEqual(signal, 6)
def test_sigbus_detected(self):
# Shell reports 128 + 10 = 138 for SIGBUS
signal = 138 & 0x7f
self.assertEqual(signal, 10)
class TestSaveResults(unittest.TestCase):
def test_round_trip(self):
import yaml
results = [
{
"path": "test/a.js",
"mode": "default",
"time": 0.5,
"result": "expected_pass",
"error": None,
"output": None,
"features": ["Proxy"],
},
{
"path": "test/b.js",
"mode": "strict mode",
"time": 1.2,
"result": "unexpected_fail",
"error": "TypeError: x",
"output": "Exception: TypeError: x",
"features": [],
},
]
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml",
delete=False) as f:
fname = f.name
try:
runner._save_results(fname, results)
with open(fname) as f:
loaded = yaml.safe_load(f)
self.assertEqual(len(loaded), 2)
self.assertEqual(loaded[0]["path"], "test/a.js")
self.assertEqual(loaded[0]["result"], "expected_pass")
self.assertNotIn("error", loaded[0])
self.assertNotIn("output", loaded[0])
self.assertEqual(loaded[1]["error"], "TypeError: x")
self.assertEqual(loaded[1]["output"], "Exception: TypeError: x")
finally:
os.unlink(fname)
class TestFindJsc(unittest.TestCase):
def test_explicit_path(self):
with tempfile.NamedTemporaryFile(delete=False) as f:
fname = f.name
try:
result = runner._find_jsc(fname, release=False)
self.assertEqual(result, os.path.abspath(fname))
finally:
os.unlink(fname)
def test_explicit_path_missing(self):
with self.assertRaises(SystemExit):
runner._find_jsc("/nonexistent/jsc", release=False)
def test_port_build_dir(self):
with tempfile.TemporaryDirectory() as d:
bin_dir = os.path.join(d, "WPE", "Release", "bin")
os.makedirs(bin_dir)
jsc_path = os.path.join(bin_dir, "jsc")
open(jsc_path, "w").close()
with mock.patch.dict(os.environ, {"WEBKIT_OUTPUTDIR": d}):
result = runner._find_jsc(None, release=True, port="wpe")
self.assertEqual(result, jsc_path)
def test_port_build_dir_missing(self):
with tempfile.TemporaryDirectory() as d:
with mock.patch.dict(os.environ, {"WEBKIT_OUTPUTDIR": d}):
with self.assertRaises(SystemExit):
runner._find_jsc(None, release=True, port="gtk")
def test_port_does_not_fall_back_to_path(self):
with tempfile.TemporaryDirectory() as d:
with mock.patch.dict(os.environ, {"WEBKIT_OUTPUTDIR": d}):
with mock.patch("runner.subprocess.run") as run_mock:
with self.assertRaises(SystemExit):
runner._find_jsc(None, release=True, port="jsc-only")
run_mock.assert_not_called()
class TestTestFileRegex(unittest.TestCase):
def test_matches_js(self):
self.assertIsNotNone(runner._TEST_FILE_RE.search("test.js"))
self.assertIsNotNone(runner._TEST_FILE_RE.search("test.JS"))
def test_rejects_fixture(self):
self.assertIsNone(runner._TEST_FILE_RE.search("test_FIXTURE.js"))
def test_rejects_non_js(self):
self.assertIsNone(runner._TEST_FILE_RE.search("test.py"))
self.assertIsNone(runner._TEST_FILE_RE.search("test.txt"))
class TestParseArgs(unittest.TestCase):
def _parse(self, *argv):
with mock.patch.object(sys, "argv", ["test262-runner", *argv]):
return runner._parse_args()
def test_build_config_optional_defaults_to_release(self):
# The original runner did not require --release/--debug and defaulted to
# Release; --stats and --jsc must work without a build flag.
self.assertTrue(self._parse().release)
self.assertTrue(self._parse("--stats").release)
self.assertTrue(self._parse("--jsc", "/path/to/jsc").release)
def test_explicit_release(self):
self.assertTrue(self._parse("--release").release)
def test_explicit_debug(self):
self.assertFalse(self._parse("--debug").release)
def test_release_and_debug_are_mutually_exclusive(self):
with contextlib.redirect_stderr(io.StringIO()):
with self.assertRaises(SystemExit):
self._parse("--release", "--debug")
# Stands in for jsc. Picks its outcome from the name of the test file it is
# handed, so one mock covers every case below. The exit codes match jsc's:
# 3 is EXIT_EXCEPTION, 139 is death by SIGSEGV as reported through the shell.
_MOCK_JSC = """\
#!/usr/bin/env python3
import sys
target = sys.argv[-1]
if "crashing" in target:
sys.stdout.write("Exception: Test262: This test crashes.")
sys.exit(139)
if "failing" in target:
sys.stdout.write("Exception: Test262: This test fails.")
sys.exit(3)
sys.exit(0)
"""
class TestEndToEnd(unittest.TestCase):
"""Drives main() end to end against a mock jsc.
Ported from Tools/Scripts/webkitperl/test262_unittest, which covered the
Perl runner the same way and was removed along with it.
"""
TEST_FILES = ("passing.js", "failing.js", "crashing.js")
def setUp(self):
self.tmp = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, self.tmp, True)
self.test262_dir = os.path.join(self.tmp, "test262")
harness_dir = os.path.join(self.test262_dir, "harness")
tests_dir = os.path.join(self.test262_dir, "test")
os.makedirs(harness_dir)
os.makedirs(tests_dir)
for name in ("sta.js", "assert.js", "doneprintHandle.js"):
with open(os.path.join(harness_dir, name), "w") as f:
f.write("// mock harness\n")
for name in self.TEST_FILES:
with open(os.path.join(tests_dir, name), "w") as f:
f.write("// mock test\n")
self.jsc = os.path.join(self.tmp, "mock-jsc")
with open(self.jsc, "w") as f:
f.write(_MOCK_JSC)
os.chmod(self.jsc, 0o755)
# main() writes test262-results/ into the working directory.
self.addCleanup(os.chdir, os.getcwd())
os.chdir(self.tmp)
def _write_yaml(self, name, contents):
path = os.path.join(self.tmp, name)
with open(path, "w") as f:
yaml.dump(contents, f)
return path
def _run(self, *extra_args, child_processes=("-p", "1")):
argv = [
"test262-runner",
*child_processes,
"--release",
"--no-progress",
"--jsc", self.jsc,
"-t", self.test262_dir,
] + list(extra_args)
out = io.StringIO()
exit_code = 0
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(out):
with mock.patch.object(sys, "argv", argv):
try:
runner.main()
except SystemExit as e:
exit_code = e.code or 0
return exit_code, out.getvalue()
def test_failure_without_expectations(self):
code, out = self._run("-i", "-x", "-o", "test/failing.js")
self.assertEqual(code, 1)
self.assertEqual(out.count("! NEW FAIL"), 2)
def test_pass_without_expectations(self):
code, out = self._run("-i", "-x", "-o", "test/passing.js")
self.assertEqual(code, 0)
self.assertEqual(out.count("! NEW FAIL"), 0)
def test_newly_failing(self):
expectations = self._write_yaml(
"expectations.yaml", {"test/unrelated.js": {"default": 3}}
)
code, out = self._run("-i", "-e", expectations, "-o", "test/failing.js")
self.assertEqual(code, 1)
self.assertEqual(out.count("! NEW FAIL"), 2)
def test_newly_passing(self):
expectations = self._write_yaml(
"expectations.yaml",
{"test/passing.js": {"default": 3, "strict mode": 3}},
)
code, out = self._run("-i", "-e", expectations, "-o", "test/passing.js")
self.assertEqual(code, 0)
self.assertEqual(out.count("NEW PASS"), 2)
self.assertIn("2 tests newly pass", out)
def test_expected_failure_is_not_new(self):
expectations = self._write_yaml(
"expectations.yaml",
{"test/failing.js": {"default": 3, "strict mode": 3}},
)
code, out = self._run("-i", "-e", expectations, "-o", "test/failing.js")
self.assertEqual(code, 0)
self.assertEqual(out.count("! NEW FAIL"), 0)
def test_crash_where_failure_was_expected_is_new(self):
# A test that used to throw and now crashes is still a regression, even
# though it failed before and fails now.
expectations = self._write_yaml(
"expectations.yaml",
{"test/crashing.js": {"default": 3, "strict mode": 3}},
)
code, out = self._run("-i", "-e", expectations, "-o", "test/crashing.js")
self.assertEqual(code, 1)
self.assertEqual(out.count("! NEW FAIL"), 2)
def test_test_only_accepts_a_single_file(self):
code, out = self._run("-i", "-x", "-o", "test/passing.js")
self.assertEqual(code, 0)
self.assertIn("2 tests run", out)
def test_no_tests_found_is_an_error(self):
code, out = self._run("-i", "-x", "-o", "test/does-not-exist")
self.assertEqual(code, 1)
self.assertIn("No test files found.", out)
def test_skipped_files_reports_newly_passing(self):
config = self._write_yaml(
"config.yaml", {"skip": {"files": ["test/passing.js"]}}
)
code, out = self._run("-c", config, "-S")
self.assertIn("2 tests newly pass", out)
self.assertIn("PASS test/passing.js", out)
def test_skipped_files_does_not_report_expected_failures(self):
config = self._write_yaml(
"config.yaml", {"skip": {"files": ["test/failing.js"]}}
)
code, out = self._run("-c", config, "-S")
self.assertEqual(out.count("! NEW FAIL"), 0)
def test_save_records_exit_codes(self):
expectations = os.path.join(self.tmp, "saved.yaml")
self._run("-i", "-x", "--save", "-e", expectations)
with open(expectations) as f:
saved = yaml.safe_load(f)
self.assertEqual(
saved,
{
"test/crashing.js": {"default": 139, "strict mode": 139},
"test/failing.js": {"default": 3, "strict mode": 3},
},
)
def _child_processes(self, number_of_processors, *extra_args):
with mock.patch.dict(os.environ, {"NUMBER_OF_PROCESSORS": number_of_processors}):
code, out = self._run("-i", "-x", "-o", "test/passing.js", *extra_args, child_processes=())
self.assertEqual(code, 0)
match = re.search(r"^Child Processes: (\d+)$", out, re.MULTILINE)
self.assertIsNotNone(match, out)
return int(match.group(1))
def test_number_of_processors_is_honored(self):
# Linux CI bots cap the parallelism of each container with this env var. Ensure it is honored.
self.assertEqual(self._child_processes("3"), 3)
self.assertEqual(self._child_processes("5"), 5)
def test_child_processes_option_overrides_number_of_processors(self):
self.assertEqual(self._child_processes("3", "-p", "1"), 1)
if __name__ == "__main__":
unittest.main()