All checks were successful
ci / fast (linux) (push) Successful in 6m19s
Add function property support to the JS engine (fn.prop = val, fn.prop, fn.method()) enabling the Test262 assert harness pattern. Implement Test262 execution mode in the JS262 harness with simplified sta.js/assert.js shims. Vendor 50 real tc39/test262 tests via curation script (35 pass, 15 known_fail). Engine changes: - JsObject: add Debug impl and has() method for presence checks - JsFunction: add properties field (Rc-shared JsObject) - Interpreter: handle function property get/set/call/compound-assign/update with user-property-over-builtin precedence Harness changes: - Add Test262 variant to Js262Mode with relaxed manifest validation - Implement run_test262_case with harness prepending and error classification - Create sta.js (Test262Error shim) and assert.js (assert.sameValue etc.) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
628 lines
21 KiB
Rust
628 lines
21 KiB
Rust
use crate::types::{ExpectedError, Js262Case, Js262Mode, Js262Outcome, Js262Status, Step};
|
|
use js::{JsEngine, JsError, OutputSink};
|
|
use std::fs;
|
|
use std::sync::{Arc, Mutex};
|
|
use web_api::scheduling::Clock;
|
|
use web_api::WebApiFacade;
|
|
|
|
const TEST262_STA: &str = include_str!("../external/js262/test262/harness/sta.js");
|
|
const TEST262_ASSERT: &str = include_str!("../external/js262/test262/harness/assert.js");
|
|
|
|
/// A capture sink that can be shared between the engine and the test runner.
|
|
struct SharedCaptureSink {
|
|
lines: Arc<Mutex<Vec<String>>>,
|
|
}
|
|
|
|
impl SharedCaptureSink {
|
|
fn new(lines: Arc<Mutex<Vec<String>>>) -> Self {
|
|
Self { lines }
|
|
}
|
|
}
|
|
|
|
impl OutputSink for SharedCaptureSink {
|
|
fn write_line(&mut self, message: &str) {
|
|
self.lines.lock().unwrap().push(message.to_string());
|
|
}
|
|
}
|
|
|
|
pub fn run_case(case: &Js262Case) -> Js262Outcome {
|
|
if let Js262Status::Skip { reason } = &case.status {
|
|
return Js262Outcome::Skipped {
|
|
reason: reason.clone(),
|
|
};
|
|
}
|
|
|
|
let script = match fs::read_to_string(&case.input) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
return Js262Outcome::Fail {
|
|
reason: format!("failed to read input {}: {e}", case.input.display()),
|
|
};
|
|
}
|
|
};
|
|
|
|
match case.mode {
|
|
Js262Mode::Script => run_script_case(case, &script),
|
|
Js262Mode::Dom => run_dom_case(case, &script),
|
|
Js262Mode::Test262 => run_test262_case(case, &script),
|
|
}
|
|
}
|
|
|
|
fn run_script_case(case: &Js262Case, script: &str) -> Js262Outcome {
|
|
let captured = Arc::new(Mutex::new(Vec::new()));
|
|
let sink = SharedCaptureSink::new(Arc::clone(&captured));
|
|
let mut engine = JsEngine::with_output(Box::new(sink));
|
|
engine.prime_runtime().unwrap();
|
|
|
|
let result = engine.execute(script);
|
|
|
|
if let Some(expected_error) = case.expected_error {
|
|
return match result {
|
|
Ok(_) => Js262Outcome::Fail {
|
|
reason: format!("expected {:?} error but script succeeded", expected_error),
|
|
},
|
|
Err(err) => {
|
|
if error_matches_class(&err, expected_error) {
|
|
Js262Outcome::Pass
|
|
} else {
|
|
Js262Outcome::Fail {
|
|
reason: format!("expected {:?} error but got: {err}", expected_error),
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Output comparison
|
|
if let Err(err) = result {
|
|
return Js262Outcome::Fail {
|
|
reason: format!("script failed: {err}"),
|
|
};
|
|
}
|
|
|
|
compare_output_from_captured(case, &captured)
|
|
}
|
|
|
|
fn run_dom_case(case: &Js262Case, script: &str) -> Js262Outcome {
|
|
let captured = Arc::new(Mutex::new(Vec::new()));
|
|
let sink = SharedCaptureSink::new(Arc::clone(&captured));
|
|
let engine = JsEngine::with_output(Box::new(sink));
|
|
let mut facade = WebApiFacade::new_with_engine(engine, Clock::new_virtual());
|
|
|
|
// Set up minimal DOM: root > html > body#body > div#target
|
|
{
|
|
let doc = facade.document_mut();
|
|
let root = doc.root().unwrap();
|
|
let html = doc.create_element("html");
|
|
doc.append_child(root, html);
|
|
let body = doc.create_element("body");
|
|
doc.set_attribute(body, "id", "body");
|
|
doc.append_child(html, body);
|
|
let target = doc.create_element("div");
|
|
doc.set_attribute(target, "id", "target");
|
|
doc.append_child(body, target);
|
|
}
|
|
|
|
if let Err(e) = facade.bootstrap() {
|
|
return Js262Outcome::Fail {
|
|
reason: format!("bootstrap failed: {e}"),
|
|
};
|
|
}
|
|
|
|
if let Err(e) = facade.execute_script(script) {
|
|
if let Some(expected_error) = case.expected_error {
|
|
// Check error class via string matching since SharedResult wraps JsError
|
|
let err_str = e.to_string();
|
|
let class_matches = match expected_error {
|
|
ExpectedError::Parse => err_str.contains("ParseError"),
|
|
ExpectedError::Reference => err_str.contains("ReferenceError"),
|
|
ExpectedError::Type => err_str.contains("TypeError"),
|
|
ExpectedError::Runtime => err_str.contains("RuntimeError"),
|
|
};
|
|
return if class_matches {
|
|
Js262Outcome::Pass
|
|
} else {
|
|
Js262Outcome::Fail {
|
|
reason: format!("expected {:?} error but got: {err_str}", expected_error),
|
|
}
|
|
};
|
|
}
|
|
return Js262Outcome::Fail {
|
|
reason: format!("script execution failed: {e}"),
|
|
};
|
|
}
|
|
|
|
// Execute steps
|
|
for step in &case.steps {
|
|
match step {
|
|
Step::Tick => {
|
|
if let Err(e) = facade.tick() {
|
|
return Js262Outcome::Fail {
|
|
reason: format!("tick failed: {e}"),
|
|
};
|
|
}
|
|
}
|
|
Step::AdvanceMs(ms) => {
|
|
facade.task_queue_mut().clock_mut().advance(*ms);
|
|
}
|
|
Step::DispatchClick(element_id) => {
|
|
let node_id = facade
|
|
.document()
|
|
.get_element_by_id(element_id)
|
|
.unwrap_or_else(|| {
|
|
panic!("dispatch_click: element with id '{element_id}' not found in DOM")
|
|
});
|
|
if let Err(e) = facade.dispatch_click(node_id) {
|
|
return Js262Outcome::Fail {
|
|
reason: format!("dispatch_click failed: {e}"),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compare output
|
|
compare_output_from_captured(case, &captured)
|
|
}
|
|
|
|
fn compare_output_from_captured(
|
|
case: &Js262Case,
|
|
captured: &Arc<Mutex<Vec<String>>>,
|
|
) -> Js262Outcome {
|
|
let expected_path = match &case.expected_output {
|
|
Some(p) => p,
|
|
None => {
|
|
// No expected output — pass if we got here without errors
|
|
return Js262Outcome::Pass;
|
|
}
|
|
};
|
|
|
|
let expected = match fs::read_to_string(expected_path) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
return Js262Outcome::Fail {
|
|
reason: format!(
|
|
"failed to read expected output {}: {e}",
|
|
expected_path.display()
|
|
),
|
|
};
|
|
}
|
|
};
|
|
|
|
let lines = captured.lock().unwrap();
|
|
let actual = lines.join("\n");
|
|
let actual = if actual.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!("{actual}\n")
|
|
};
|
|
|
|
let expected = normalize_output(&expected);
|
|
let actual = normalize_output(&actual);
|
|
|
|
if actual == expected {
|
|
Js262Outcome::Pass
|
|
} else {
|
|
Js262Outcome::Fail {
|
|
reason: format!(
|
|
"output mismatch for '{}':\n--- expected ---\n{expected}--- actual ---\n{actual}",
|
|
case.id
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn normalize_output(s: &str) -> String {
|
|
let mut out = String::new();
|
|
for line in s.replace("\r\n", "\n").lines() {
|
|
out.push_str(line.trim_end());
|
|
out.push('\n');
|
|
}
|
|
out
|
|
}
|
|
|
|
fn error_matches_class(err: &JsError, expected: ExpectedError) -> bool {
|
|
match expected {
|
|
ExpectedError::Parse => err.is_parse_error(),
|
|
ExpectedError::Reference => err.is_reference_error(),
|
|
ExpectedError::Type => err.is_type_error(),
|
|
ExpectedError::Runtime => err.is_runtime_error(),
|
|
}
|
|
}
|
|
|
|
fn is_test262_assertion_error(err: &JsError) -> bool {
|
|
if let JsError::UncaughtThrow { message } = err {
|
|
message.starts_with("Test262Error:")
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn run_test262_case(case: &Js262Case, script: &str) -> Js262Outcome {
|
|
// Prepend harness files: sta.js first, then assert.js, then the test script
|
|
let combined = format!("{TEST262_STA}\n{TEST262_ASSERT}\n{script}");
|
|
|
|
let captured = Arc::new(Mutex::new(Vec::new()));
|
|
let sink = SharedCaptureSink::new(Arc::clone(&captured));
|
|
let mut engine = JsEngine::with_output(Box::new(sink));
|
|
engine.prime_runtime().unwrap();
|
|
|
|
let result = engine.execute(&combined);
|
|
|
|
if let Some(expected_error) = case.expected_error {
|
|
// Negative test: we expect a specific error class
|
|
return match result {
|
|
Ok(_) => Js262Outcome::Fail {
|
|
reason: format!(
|
|
"negative test expected {:?} error but script succeeded",
|
|
expected_error
|
|
),
|
|
},
|
|
Err(ref err) if is_test262_assertion_error(err) => Js262Outcome::Fail {
|
|
reason: format!(
|
|
"negative test got Test262 assertion error instead of {:?}: {err}",
|
|
expected_error
|
|
),
|
|
},
|
|
Err(ref err) => {
|
|
if error_matches_class(err, expected_error) {
|
|
Js262Outcome::Pass
|
|
} else {
|
|
Js262Outcome::Fail {
|
|
reason: format!(
|
|
"negative test expected {:?} error but got: {err}",
|
|
expected_error
|
|
),
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Positive test: script must complete without error
|
|
match result {
|
|
Ok(_) => Js262Outcome::Pass,
|
|
Err(ref err) if is_test262_assertion_error(err) => Js262Outcome::Fail {
|
|
reason: format!("assertion failed: {err}"),
|
|
},
|
|
Err(err) => Js262Outcome::Fail {
|
|
reason: format!("unexpected error: {err}"),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn enforce_case_policy(case: &Js262Case, outcome: Js262Outcome) -> Result<(), String> {
|
|
match (&case.status, outcome) {
|
|
(Js262Status::Pass, Js262Outcome::Pass) => Ok(()),
|
|
(Js262Status::Pass, Js262Outcome::Fail { reason }) => Err(reason),
|
|
(Js262Status::Pass, Js262Outcome::Skipped { reason }) => {
|
|
Err(format!("case '{}' unexpectedly skipped: {reason}", case.id))
|
|
}
|
|
(Js262Status::KnownFail { .. }, Js262Outcome::Fail { .. }) => Ok(()),
|
|
(Js262Status::KnownFail { .. }, Js262Outcome::Pass) => Err(format!(
|
|
"case '{}' is marked known_fail but now passes; promote it to pass in manifest",
|
|
case.id
|
|
)),
|
|
(Js262Status::KnownFail { .. }, Js262Outcome::Skipped { reason }) => Err(format!(
|
|
"case '{}' marked known_fail unexpectedly skipped: {reason}",
|
|
case.id
|
|
)),
|
|
(Js262Status::Skip { .. }, Js262Outcome::Skipped { .. }) => Ok(()),
|
|
(Js262Status::Skip { .. }, Js262Outcome::Pass) => Err(format!(
|
|
"case '{}' marked skip but unexpectedly passed",
|
|
case.id
|
|
)),
|
|
(Js262Status::Skip { .. }, Js262Outcome::Fail { reason }) => Err(format!(
|
|
"case '{}' marked skip but unexpectedly executed and failed: {reason}",
|
|
case.id
|
|
)),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::types::{ExpectedError, Js262Mode, Js262Status};
|
|
use std::io::Write;
|
|
|
|
/// Build a minimal `Js262Case` backed by a temporary JS file containing `script`.
|
|
fn dom_case_with_error(
|
|
script: &str,
|
|
expected_error: ExpectedError,
|
|
) -> (Js262Case, tempfile::NamedTempFile) {
|
|
let mut tmp = tempfile::NamedTempFile::new().unwrap();
|
|
tmp.write_all(script.as_bytes()).unwrap();
|
|
let case = Js262Case {
|
|
id: "test-dom-error".into(),
|
|
input: tmp.path().to_path_buf(),
|
|
mode: Js262Mode::Dom,
|
|
expected_output: None,
|
|
expected_error: Some(expected_error),
|
|
status: Js262Status::Pass,
|
|
feature: "errors".into(),
|
|
steps: vec![],
|
|
flags: vec![],
|
|
};
|
|
(case, tmp)
|
|
}
|
|
|
|
// --- DOM-mode expected_error: correct class → Pass ---
|
|
|
|
#[test]
|
|
fn dom_mode_reference_error_with_matching_class_is_pass() {
|
|
// A script that throws a ReferenceError when expected_error = Reference
|
|
// must produce Js262Outcome::Pass.
|
|
let (case, _tmp) = dom_case_with_error("undeclaredVar;", ExpectedError::Reference);
|
|
let outcome = run_case(&case);
|
|
assert!(
|
|
matches!(outcome, Js262Outcome::Pass),
|
|
"expected Pass for matching ReferenceError, got: {outcome:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_mode_type_error_with_matching_class_is_pass() {
|
|
// A script that throws a TypeError (const reassignment) when expected_error
|
|
// = Type must produce Js262Outcome::Pass.
|
|
let (case, _tmp) = dom_case_with_error("const c = 1; c = 2;", ExpectedError::Type);
|
|
let outcome = run_case(&case);
|
|
assert!(
|
|
matches!(outcome, Js262Outcome::Pass),
|
|
"expected Pass for matching TypeError, got: {outcome:?}"
|
|
);
|
|
}
|
|
|
|
// --- DOM-mode expected_error: wrong class → Fail (not vacuous Pass) ---
|
|
|
|
#[test]
|
|
fn dom_mode_wrong_error_class_produces_fail_not_pass() {
|
|
// A script that throws a ReferenceError but expected_error = Type must
|
|
// produce Js262Outcome::Fail. The old vacuous-pass bug would have
|
|
// returned Pass here.
|
|
let (case, _tmp) = dom_case_with_error("undeclaredVar;", ExpectedError::Type);
|
|
let outcome = run_case(&case);
|
|
assert!(
|
|
matches!(outcome, Js262Outcome::Fail { .. }),
|
|
"expected Fail when error class does not match, got: {outcome:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_mode_wrong_error_class_fail_reason_mentions_expected_and_actual() {
|
|
// The Fail reason must identify both the expected class and the actual
|
|
// error so a developer can diagnose the mismatch.
|
|
let (case, _tmp) = dom_case_with_error("undeclaredVar;", ExpectedError::Type);
|
|
let outcome = run_case(&case);
|
|
match outcome {
|
|
Js262Outcome::Fail { reason } => {
|
|
assert!(
|
|
reason.contains("Type"),
|
|
"reason should mention expected 'Type', got: {reason}"
|
|
);
|
|
assert!(
|
|
reason.contains("ReferenceError"),
|
|
"reason should mention actual 'ReferenceError', got: {reason}"
|
|
);
|
|
}
|
|
other => panic!("expected Fail, got: {other:?}"),
|
|
}
|
|
}
|
|
|
|
// --- DOM-mode: no expected_error and script succeeds → Pass ---
|
|
|
|
#[test]
|
|
fn dom_mode_no_expected_error_succeeds() {
|
|
// A script with no expected_error that succeeds must produce Pass.
|
|
let mut tmp = tempfile::NamedTempFile::new().unwrap();
|
|
tmp.write_all(b"var x = 1;").unwrap();
|
|
let case = Js262Case {
|
|
id: "test-dom-ok".into(),
|
|
input: tmp.path().to_path_buf(),
|
|
mode: Js262Mode::Dom,
|
|
expected_output: None,
|
|
expected_error: None,
|
|
status: Js262Status::Pass,
|
|
feature: "basics".into(),
|
|
steps: vec![],
|
|
flags: vec![],
|
|
};
|
|
let outcome = run_case(&case);
|
|
assert!(
|
|
matches!(outcome, Js262Outcome::Pass),
|
|
"expected Pass for succeeding DOM script, got: {outcome:?}"
|
|
);
|
|
}
|
|
|
|
// --- enforce_case_policy: all branches ---
|
|
|
|
#[test]
|
|
fn enforce_case_policy_pass_status_pass_outcome_is_ok() {
|
|
let case = Js262Case {
|
|
id: "x".into(),
|
|
input: std::path::PathBuf::new(),
|
|
mode: Js262Mode::Script,
|
|
expected_output: None,
|
|
expected_error: None,
|
|
status: Js262Status::Pass,
|
|
feature: "f".into(),
|
|
steps: vec![],
|
|
flags: vec![],
|
|
};
|
|
assert!(enforce_case_policy(&case, Js262Outcome::Pass).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn enforce_case_policy_pass_status_fail_outcome_is_err() {
|
|
let case = Js262Case {
|
|
id: "x".into(),
|
|
input: std::path::PathBuf::new(),
|
|
mode: Js262Mode::Script,
|
|
expected_output: None,
|
|
expected_error: None,
|
|
status: Js262Status::Pass,
|
|
feature: "f".into(),
|
|
steps: vec![],
|
|
flags: vec![],
|
|
};
|
|
assert!(enforce_case_policy(
|
|
&case,
|
|
Js262Outcome::Fail {
|
|
reason: "oops".into()
|
|
}
|
|
)
|
|
.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn enforce_case_policy_known_fail_status_fail_outcome_is_ok() {
|
|
let case = Js262Case {
|
|
id: "x".into(),
|
|
input: std::path::PathBuf::new(),
|
|
mode: Js262Mode::Script,
|
|
expected_output: None,
|
|
expected_error: None,
|
|
status: Js262Status::KnownFail {
|
|
reason: "not yet".into(),
|
|
},
|
|
feature: "f".into(),
|
|
steps: vec![],
|
|
flags: vec![],
|
|
};
|
|
assert!(enforce_case_policy(
|
|
&case,
|
|
Js262Outcome::Fail {
|
|
reason: "still broken".into()
|
|
}
|
|
)
|
|
.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn enforce_case_policy_known_fail_status_pass_outcome_is_err() {
|
|
let case = Js262Case {
|
|
id: "x".into(),
|
|
input: std::path::PathBuf::new(),
|
|
mode: Js262Mode::Script,
|
|
expected_output: None,
|
|
expected_error: None,
|
|
status: Js262Status::KnownFail {
|
|
reason: "not yet".into(),
|
|
},
|
|
feature: "f".into(),
|
|
steps: vec![],
|
|
flags: vec![],
|
|
};
|
|
let err = enforce_case_policy(&case, Js262Outcome::Pass).unwrap_err();
|
|
assert!(
|
|
err.contains("promote"),
|
|
"error should mention promoting the case, got: {err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn enforce_case_policy_skip_status_skipped_outcome_is_ok() {
|
|
let case = Js262Case {
|
|
id: "x".into(),
|
|
input: std::path::PathBuf::new(),
|
|
mode: Js262Mode::Script,
|
|
expected_output: None,
|
|
expected_error: None,
|
|
status: Js262Status::Skip {
|
|
reason: "needs feature".into(),
|
|
},
|
|
feature: "f".into(),
|
|
steps: vec![],
|
|
flags: vec![],
|
|
};
|
|
assert!(enforce_case_policy(
|
|
&case,
|
|
Js262Outcome::Skipped {
|
|
reason: "needs feature".into()
|
|
}
|
|
)
|
|
.is_ok());
|
|
}
|
|
|
|
// --- Test262 runner tests ---
|
|
|
|
fn test262_case(
|
|
script: &str,
|
|
expected_error: Option<ExpectedError>,
|
|
) -> (Js262Case, tempfile::NamedTempFile) {
|
|
let mut tmp = tempfile::NamedTempFile::new().unwrap();
|
|
tmp.write_all(script.as_bytes()).unwrap();
|
|
let case = Js262Case {
|
|
id: "test-t262".into(),
|
|
input: tmp.path().to_path_buf(),
|
|
mode: Js262Mode::Test262,
|
|
expected_output: None,
|
|
expected_error,
|
|
status: Js262Status::Pass,
|
|
feature: "test".into(),
|
|
steps: vec![],
|
|
flags: vec![],
|
|
};
|
|
(case, tmp)
|
|
}
|
|
|
|
#[test]
|
|
fn test262_positive_pass() {
|
|
let (case, _tmp) = test262_case("assert.sameValue(1, 1);", None);
|
|
let outcome = run_case(&case);
|
|
assert!(
|
|
matches!(outcome, Js262Outcome::Pass),
|
|
"expected Pass, got: {outcome:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test262_positive_assertion_failure() {
|
|
let (case, _tmp) = test262_case("assert.sameValue(1, 2);", None);
|
|
let outcome = run_case(&case);
|
|
assert!(
|
|
matches!(outcome, Js262Outcome::Fail { .. }),
|
|
"expected Fail, got: {outcome:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test262_positive_unexpected_error() {
|
|
let (case, _tmp) = test262_case("undeclaredVar;", None);
|
|
let outcome = run_case(&case);
|
|
assert!(
|
|
matches!(outcome, Js262Outcome::Fail { .. }),
|
|
"expected Fail for unexpected error, got: {outcome:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test262_negative_matching_error() {
|
|
let (case, _tmp) = test262_case("undeclaredVar;", Some(ExpectedError::Reference));
|
|
let outcome = run_case(&case);
|
|
assert!(
|
|
matches!(outcome, Js262Outcome::Pass),
|
|
"expected Pass for matching error class, got: {outcome:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test262_negative_wrong_error() {
|
|
let (case, _tmp) = test262_case("undeclaredVar;", Some(ExpectedError::Type));
|
|
let outcome = run_case(&case);
|
|
assert!(
|
|
matches!(outcome, Js262Outcome::Fail { .. }),
|
|
"expected Fail for wrong error class, got: {outcome:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test262_negative_no_error() {
|
|
let (case, _tmp) = test262_case("var x = 1;", Some(ExpectedError::Reference));
|
|
let outcome = run_case(&case);
|
|
assert!(
|
|
matches!(outcome, Js262Outcome::Fail { .. }),
|
|
"expected Fail when negative test doesn't throw, got: {outcome:?}"
|
|
);
|
|
}
|
|
}
|