mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
refactor(transform_conformance): add driver (#4969)
This commit is contained in:
parent
37b9b0e21d
commit
4fdf26dac8
9 changed files with 129 additions and 197 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
|
@ -1877,15 +1877,8 @@ name = "oxc_transform_conformance"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"oxc_allocator",
|
"oxc",
|
||||||
"oxc_ast",
|
|
||||||
"oxc_codegen",
|
|
||||||
"oxc_diagnostics",
|
|
||||||
"oxc_parser",
|
|
||||||
"oxc_semantic",
|
|
||||||
"oxc_span",
|
|
||||||
"oxc_tasks_common",
|
"oxc_tasks_common",
|
||||||
"oxc_transformer",
|
|
||||||
"pico-args",
|
"pico-args",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ pub trait CompilerInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compress_options(&self) -> Option<CompressOptions> {
|
fn compress_options(&self) -> Option<CompressOptions> {
|
||||||
Some(CompressOptions::all_true())
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn codegen_options(&self) -> Option<CodegenOptions> {
|
fn codegen_options(&self) -> Option<CodegenOptions> {
|
||||||
|
|
@ -69,6 +69,10 @@ pub trait CompilerInterface {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_semantic_error(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn after_parse(&mut self, _parser_return: &mut ParserReturn) -> ControlFlow<()> {
|
fn after_parse(&mut self, _parser_return: &mut ParserReturn) -> ControlFlow<()> {
|
||||||
ControlFlow::Continue(())
|
ControlFlow::Continue(())
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +176,7 @@ pub trait CompilerInterface {
|
||||||
source_path: &Path,
|
source_path: &Path,
|
||||||
) -> SemanticBuilderReturn<'a> {
|
) -> SemanticBuilderReturn<'a> {
|
||||||
SemanticBuilder::new(source_text, source_type)
|
SemanticBuilder::new(source_text, source_type)
|
||||||
.with_check_syntax_error(true)
|
.with_check_syntax_error(self.check_semantic_error())
|
||||||
.build_module_record(source_path.to_path_buf(), program)
|
.build_module_record(source_path.to_path_buf(), program)
|
||||||
.build(program)
|
.build(program)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,8 @@ test = false
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
oxc_ast = { workspace = true }
|
oxc = { workspace = true, features = ["full"] }
|
||||||
oxc_span = { workspace = true }
|
|
||||||
oxc_allocator = { workspace = true }
|
|
||||||
oxc_parser = { workspace = true }
|
|
||||||
oxc_codegen = { workspace = true }
|
|
||||||
oxc_semantic = { workspace = true }
|
|
||||||
oxc_transformer = { workspace = true }
|
|
||||||
oxc_tasks_common = { workspace = true }
|
oxc_tasks_common = { workspace = true }
|
||||||
oxc_diagnostics = { workspace = true }
|
|
||||||
|
|
||||||
walkdir = { workspace = true }
|
walkdir = { workspace = true }
|
||||||
pico-args = { workspace = true }
|
pico-args = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
commit: 12619ffe
|
commit: 12619ffe
|
||||||
|
|
||||||
Passed: 466/953
|
Passed: 487/953
|
||||||
|
|
||||||
# All Passed:
|
# All Passed:
|
||||||
* babel-plugin-transform-optional-catch-binding
|
* babel-plugin-transform-optional-catch-binding
|
||||||
|
|
@ -465,37 +465,16 @@ Passed: 466/953
|
||||||
* opts/optimizeConstEnums/input.ts
|
* opts/optimizeConstEnums/input.ts
|
||||||
* opts/rewriteImportExtensions/input.ts
|
* opts/rewriteImportExtensions/input.ts
|
||||||
|
|
||||||
# babel-plugin-transform-typescript (109/151)
|
# babel-plugin-transform-typescript (130/151)
|
||||||
* class/accessor-allowDeclareFields-false/input.ts
|
* class/accessor-allowDeclareFields-false/input.ts
|
||||||
* class/accessor-allowDeclareFields-true/input.ts
|
* class/accessor-allowDeclareFields-true/input.ts
|
||||||
* enum/mix-references/input.ts
|
* enum/mix-references/input.ts
|
||||||
* enum/ts5.0-const-foldable/input.ts
|
* enum/ts5.0-const-foldable/input.ts
|
||||||
* exports/declared-types/input.ts
|
* exports/declared-types/input.ts
|
||||||
* exports/export-import=/input.ts
|
|
||||||
* exports/interface/input.ts
|
* exports/interface/input.ts
|
||||||
* imports/elide-type-referenced-in-imports-equal-no/input.ts
|
|
||||||
* imports/import=-module/input.ts
|
|
||||||
* imports/only-remove-type-imports/input.ts
|
* imports/only-remove-type-imports/input.ts
|
||||||
* imports/type-only-export-specifier-2/input.ts
|
* imports/type-only-export-specifier-2/input.ts
|
||||||
* imports/type-only-import-specifier-4/input.ts
|
* imports/type-only-import-specifier-4/input.ts
|
||||||
* namespace/alias/input.ts
|
|
||||||
* namespace/clobber-class/input.ts
|
|
||||||
* namespace/clobber-enum/input.ts
|
|
||||||
* namespace/clobber-export/input.ts
|
|
||||||
* namespace/contentious-names/input.ts
|
|
||||||
* namespace/declare/input.ts
|
|
||||||
* namespace/declare-global-nested-namespace/input.ts
|
|
||||||
* namespace/empty-removed/input.ts
|
|
||||||
* namespace/export/input.ts
|
|
||||||
* namespace/module-nested/input.ts
|
|
||||||
* namespace/module-nested-export/input.ts
|
|
||||||
* namespace/multiple/input.ts
|
|
||||||
* namespace/mutable-fail/input.ts
|
|
||||||
* namespace/nested/input.ts
|
|
||||||
* namespace/nested-namespace/input.ts
|
|
||||||
* namespace/nested-shorthand/input.ts
|
|
||||||
* namespace/same-name/input.ts
|
|
||||||
* namespace/undeclared/input.ts
|
|
||||||
* optimize-const-enums/custom-values/input.ts
|
* optimize-const-enums/custom-values/input.ts
|
||||||
* optimize-const-enums/custom-values-exported/input.ts
|
* optimize-const-enums/custom-values-exported/input.ts
|
||||||
* optimize-const-enums/declare/input.ts
|
* optimize-const-enums/declare/input.ts
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
commit: 12619ffe
|
commit: 12619ffe
|
||||||
|
|
||||||
Passed: 28/35
|
Passed: 31/35
|
||||||
|
|
||||||
# All Passed:
|
# All Passed:
|
||||||
* babel-plugin-transform-optional-catch-binding
|
* babel-plugin-transform-optional-catch-binding
|
||||||
|
* babel-plugin-transform-typescript
|
||||||
|
|
||||||
|
|
||||||
# babel-plugin-transform-typescript (4/7)
|
|
||||||
* computed-constant-value/input.ts
|
|
||||||
* enum-member-reference/input.ts
|
|
||||||
* export-elimination/input.ts
|
|
||||||
|
|
||||||
# babel-plugin-transform-react-jsx (23/27)
|
# babel-plugin-transform-react-jsx (23/27)
|
||||||
* refresh/can-handle-implicit-arrow-returns/input.jsx
|
* refresh/can-handle-implicit-arrow-returns/input.jsx
|
||||||
* refresh/registers-identifiers-used-in-jsx-at-definition-site/input.jsx
|
* refresh/registers-identifiers-used-in-jsx-at-definition-site/input.jsx
|
||||||
|
|
|
||||||
94
tasks/transform_conformance/src/driver.rs
Normal file
94
tasks/transform_conformance/src/driver.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
use std::{mem, ops::ControlFlow, path::Path};
|
||||||
|
|
||||||
|
use oxc::{
|
||||||
|
ast::ast::Program,
|
||||||
|
diagnostics::OxcDiagnostic,
|
||||||
|
semantic::{post_transform_checker::PostTransformChecker, SemanticBuilderReturn},
|
||||||
|
span::SourceType,
|
||||||
|
transformer::{TransformOptions, TransformerReturn},
|
||||||
|
CompilerInterface,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Driver {
|
||||||
|
options: TransformOptions,
|
||||||
|
printed: String,
|
||||||
|
errors: Vec<OxcDiagnostic>,
|
||||||
|
check_semantic: bool,
|
||||||
|
checker: PostTransformChecker,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompilerInterface for Driver {
|
||||||
|
fn transform_options(&self) -> Option<TransformOptions> {
|
||||||
|
Some(self.options.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_semantic_error(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_errors(&mut self, errors: Vec<OxcDiagnostic>) {
|
||||||
|
self.errors.extend(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn after_codegen(&mut self, printed: String) {
|
||||||
|
self.printed = printed;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn after_semantic(
|
||||||
|
&mut self,
|
||||||
|
program: &mut Program<'_>,
|
||||||
|
_semantic_return: &mut SemanticBuilderReturn,
|
||||||
|
) -> ControlFlow<()> {
|
||||||
|
if self.check_semantic {
|
||||||
|
if let Some(errors) = self.checker.before_transform(program) {
|
||||||
|
self.errors.extend(errors);
|
||||||
|
return ControlFlow::Break(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ControlFlow::Continue(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn after_transform(
|
||||||
|
&mut self,
|
||||||
|
program: &mut Program<'_>,
|
||||||
|
transformer_return: &mut TransformerReturn,
|
||||||
|
) -> ControlFlow<()> {
|
||||||
|
if self.check_semantic {
|
||||||
|
if let Some(errors) = self.checker.after_transform(
|
||||||
|
&transformer_return.symbols,
|
||||||
|
&transformer_return.scopes,
|
||||||
|
program,
|
||||||
|
) {
|
||||||
|
self.errors.extend(errors);
|
||||||
|
return ControlFlow::Break(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ControlFlow::Continue(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Driver {
|
||||||
|
pub fn new(options: TransformOptions) -> Self {
|
||||||
|
Self {
|
||||||
|
options,
|
||||||
|
printed: String::new(),
|
||||||
|
errors: vec![],
|
||||||
|
check_semantic: false,
|
||||||
|
checker: PostTransformChecker::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute(
|
||||||
|
&mut self,
|
||||||
|
source_text: &str,
|
||||||
|
source_type: SourceType,
|
||||||
|
source_path: &Path,
|
||||||
|
) -> Result<String, Vec<OxcDiagnostic>> {
|
||||||
|
self.compile(source_text, source_type, source_path);
|
||||||
|
if self.errors.is_empty() {
|
||||||
|
Ok(mem::take(&mut self.printed))
|
||||||
|
} else {
|
||||||
|
Err(mem::take(&mut self.errors))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ use oxc_tasks_common::{normalize_path, project_root, Snapshot};
|
||||||
use test_case::TestCaseKind;
|
use test_case::TestCaseKind;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
mod semantic;
|
mod driver;
|
||||||
mod test_case;
|
mod test_case;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
use oxc_ast::{
|
|
||||||
ast::{BindingIdentifier, ExportSpecifier, ImportSpecifier, ModuleExportName, Program},
|
|
||||||
visit::walk::walk_import_specifier,
|
|
||||||
Visit,
|
|
||||||
};
|
|
||||||
use oxc_semantic::{ScopeTree, SymbolTable};
|
|
||||||
|
|
||||||
pub struct SemanticTester {
|
|
||||||
scopes: ScopeTree,
|
|
||||||
symbols: SymbolTable,
|
|
||||||
errors: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SemanticTester {
|
|
||||||
pub fn new(scopes: ScopeTree, symbols: SymbolTable) -> Self {
|
|
||||||
Self { scopes, symbols, errors: Vec::new() }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn test(mut self, program: &Program) -> Vec<String> {
|
|
||||||
self.visit_program(program);
|
|
||||||
self.errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Visit<'a> for SemanticTester {
|
|
||||||
fn visit_binding_identifier(&mut self, it: &BindingIdentifier<'a>) {
|
|
||||||
let symbol_id = it.symbol_id.get();
|
|
||||||
if let Some(symbol_id) = symbol_id {
|
|
||||||
if self.symbols.get_flag(symbol_id).is_empty() {
|
|
||||||
self.errors.push(format!(
|
|
||||||
"Expect SymbolFlags for BindingIdentifier({}) to not be empty",
|
|
||||||
it.name
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if !self.scopes.has_binding(self.symbols.get_scope_id(symbol_id), &it.name) {
|
|
||||||
self.errors.push(format!(
|
|
||||||
"Cannot find BindingIdentifier({}) in the Scope corresponding to the Symbol",
|
|
||||||
it.name
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.errors.push(format!("Expect BindingIdentifier({}) to have a symbol_id", it.name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn visit_identifier_reference(&mut self, it: &oxc_ast::ast::IdentifierReference<'a>) {
|
|
||||||
if let Some(reference_id) = it.reference_id.get() {
|
|
||||||
let reference = self.symbols.get_reference(reference_id);
|
|
||||||
if reference.flag().is_empty() {
|
|
||||||
self.errors.push(format!(
|
|
||||||
"Expect ReferenceFlags for IdentifierReference({}) to not be empty",
|
|
||||||
it.name
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.errors
|
|
||||||
.push(format!("Expect IdentifierReference({}) to have a reference_id", it.name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn visit_import_specifier(&mut self, it: &ImportSpecifier<'a>) {
|
|
||||||
let symbol_id = it.local.symbol_id.get();
|
|
||||||
if let Some(symbol_id) = symbol_id {
|
|
||||||
if !self.symbols.get_flag(symbol_id).is_import() {
|
|
||||||
self.errors.push(format!(
|
|
||||||
"Expect SymbolFlags for ImportSpecifier({}) should contain SymbolFlags::Import",
|
|
||||||
it.local.name
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
walk_import_specifier(self, it);
|
|
||||||
}
|
|
||||||
fn visit_export_specifier(&mut self, it: &ExportSpecifier<'a>) {
|
|
||||||
if let ModuleExportName::IdentifierReference(ident) = &it.local {
|
|
||||||
let reference_id = ident.reference_id.get();
|
|
||||||
if let Some(symbol_id) = reference_id
|
|
||||||
.and_then(|reference_id| self.symbols.get_reference(reference_id).symbol_id())
|
|
||||||
{
|
|
||||||
if self.symbols.get_flag(symbol_id).is_empty() {
|
|
||||||
self.errors.push(format!(
|
|
||||||
"Expect SymbolFlags for ExportSpecifier({}) should contain SymbolFlags::Import",
|
|
||||||
it.local
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,19 +3,18 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use oxc_allocator::Allocator;
|
use oxc::allocator::Allocator;
|
||||||
use oxc_codegen::CodeGenerator;
|
use oxc::codegen::CodeGenerator;
|
||||||
use oxc_diagnostics::{Error, OxcDiagnostic};
|
use oxc::diagnostics::{Error, OxcDiagnostic};
|
||||||
use oxc_parser::Parser;
|
use oxc::parser::Parser;
|
||||||
use oxc_span::{SourceType, VALID_EXTENSIONS};
|
use oxc::span::{SourceType, VALID_EXTENSIONS};
|
||||||
|
use oxc::transformer::{BabelOptions, TransformOptions};
|
||||||
use oxc_tasks_common::{normalize_path, print_diff_in_terminal};
|
use oxc_tasks_common::{normalize_path, print_diff_in_terminal};
|
||||||
use oxc_transformer::{BabelOptions, TransformOptions, Transformer};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::{PLUGINS_NOT_SUPPORTED_YET, SKIP_TESTS},
|
constants::{PLUGINS_NOT_SUPPORTED_YET, SKIP_TESTS},
|
||||||
fixture_root, packages_root,
|
driver::Driver,
|
||||||
semantic::SemanticTester,
|
fixture_root, packages_root, TestRunnerEnv,
|
||||||
TestRunnerEnv,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -158,7 +157,6 @@ pub trait TestCase {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let allocator = Allocator::default();
|
|
||||||
let source_text = fs::read_to_string(path).unwrap();
|
let source_text = fs::read_to_string(path).unwrap();
|
||||||
|
|
||||||
// Some babel test cases have a js extension, but contain typescript code.
|
// Some babel test cases have a js extension, but contain typescript code.
|
||||||
|
|
@ -171,22 +169,7 @@ pub trait TestCase {
|
||||||
source_type = source_type.with_typescript(true);
|
source_type = source_type.with_typescript(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ret = Parser::new(&allocator, &source_text, source_type).parse();
|
Driver::new(transform_options.clone()).execute(&source_text, source_type, path)
|
||||||
let mut program = ret.program;
|
|
||||||
let result = Transformer::new(
|
|
||||||
&allocator,
|
|
||||||
path,
|
|
||||||
source_type,
|
|
||||||
&source_text,
|
|
||||||
ret.trivias.clone(),
|
|
||||||
transform_options.clone(),
|
|
||||||
)
|
|
||||||
.build(&mut program);
|
|
||||||
if result.errors.is_empty() {
|
|
||||||
Ok(CodeGenerator::new().build(&program).source_text)
|
|
||||||
} else {
|
|
||||||
Err(result.errors)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,45 +237,26 @@ impl TestCase for ConformanceTestCase {
|
||||||
|
|
||||||
let mut transformed_code = String::new();
|
let mut transformed_code = String::new();
|
||||||
let mut actual_errors = String::new();
|
let mut actual_errors = String::new();
|
||||||
let mut semantic_errors = Vec::default();
|
|
||||||
|
|
||||||
let transform_options = match self.transform_options() {
|
let transform_options = match self.transform_options() {
|
||||||
Ok(transform_options) => {
|
Ok(transform_options) => {
|
||||||
let ret = Parser::new(&allocator, &input, source_type).parse();
|
match Driver::new(transform_options.clone()).execute(
|
||||||
if ret.errors.is_empty() {
|
|
||||||
let mut program = ret.program;
|
|
||||||
let transformer = Transformer::new(
|
|
||||||
&allocator,
|
|
||||||
&self.path,
|
|
||||||
source_type,
|
|
||||||
&input,
|
&input,
|
||||||
ret.trivias.clone(),
|
source_type,
|
||||||
transform_options.clone(),
|
&self.path,
|
||||||
);
|
) {
|
||||||
let ret = transformer.build(&mut program);
|
Ok(printed) => {
|
||||||
|
transformed_code = printed;
|
||||||
semantic_errors = SemanticTester::new(ret.scopes, ret.symbols).test(&program);
|
|
||||||
|
|
||||||
if ret.errors.is_empty() {
|
|
||||||
transformed_code = CodeGenerator::new().build(&program).source_text;
|
|
||||||
} else {
|
|
||||||
let error = ret
|
|
||||||
.errors
|
|
||||||
.into_iter()
|
|
||||||
.map(|e| Error::from(e).to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n");
|
|
||||||
actual_errors = get_babel_error(&error);
|
|
||||||
}
|
}
|
||||||
} else {
|
Err(errors) => {
|
||||||
let error = ret
|
let error = errors
|
||||||
.errors
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|err| err.to_string())
|
.map(|err| err.to_string())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
actual_errors = get_babel_error(&error);
|
actual_errors = get_babel_error(&error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Some(transform_options.clone())
|
Some(transform_options.clone())
|
||||||
}
|
}
|
||||||
Err(json_err) => {
|
Err(json_err) => {
|
||||||
|
|
@ -319,9 +283,8 @@ impl TestCase for ConformanceTestCase {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let passed = semantic_errors.is_empty()
|
let passed =
|
||||||
&& (transformed_code == output
|
transformed_code == output || (!output.is_empty() && actual_errors.contains(&output));
|
||||||
|| (!output.is_empty() && actual_errors.contains(&output)));
|
|
||||||
|
|
||||||
if filtered {
|
if filtered {
|
||||||
println!("Options:");
|
println!("Options:");
|
||||||
|
|
@ -350,10 +313,6 @@ impl TestCase for ConformanceTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !semantic_errors.is_empty() {
|
|
||||||
println!("\nSemantic Errors:\n\n{}\n", semantic_errors.join("\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Passed: {passed}");
|
println!("Passed: {passed}");
|
||||||
}
|
}
|
||||||
passed
|
passed
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue