1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-11 16:42:21 -05:00
This commit is contained in:
Marvin Hagemeister 2024-10-10 00:01:59 +02:00
parent 5214346993
commit e4dd440a4d
5 changed files with 311 additions and 138 deletions

View file

@ -7,6 +7,9 @@ import { escapeName, withPermissions } from "ext:cli/40_test_common.js";
const {
op_register_test_step,
op_register_test,
op_register_test_group,
op_test_group_pop,
op_register_test_group_lifecycle,
op_test_event_step_result_failed,
op_test_event_step_result_ignored,
op_test_event_step_result_ok,
@ -27,7 +30,6 @@ const {
} = primordials;
import { setExitHandler } from "ext:runtime/30_os.js";
import console from "node:console";
// Capture `Deno` global so that users deleting or mangling it, won't
// have impact on our sanitizers.
@ -499,6 +501,12 @@ function createTestContext(desc) {
};
}
/** @type { only: boolean[], ignore: boolean[] } */
const bddStack = {
only: [],
ignore: [],
};
/**
* Wrap a user test function in one which returns a structured result.
* @template T {Function}
@ -519,31 +527,6 @@ function wrapTest(desc) {
globalThis.Deno.test = test;
/**
* @typedef {{ name: string, kind: "group" | "test", ignore: boolean, children: BddItem[], fn: null | (() => any), parent: BddItem | null, beforeAll: null | (() => void | Promise<void>), afterAll: null | (() => void | Promise<void>), beforeEach: null | (() => void | Promise<void>), afterEach: null | (() => void | Promise<void>) }} BddItem
*/
/** @type {BddItem} */
const BDD_ROOT = {
name: "__<root>__",
kind: "group",
ignore: false,
only: false,
fn: null,
children: [],
parent: null,
beforeAll: null,
beforeEach: null,
afterAll: null,
afterEach: null,
};
/** @type {BddItem[]} */
const bddStack = [BDD_ROOT];
/** @type {BddItem[]} */
const onlys = [];
/**
* @param {string} name
* @param {fn: () => any} fn
@ -551,23 +534,41 @@ const onlys = [];
* @param {boolean} only
*/
function itInner(name, fn, ignore, only) {
const parent = bddStack.at(-1);
/** @type {BddItem} */
const item = {
kind: "test",
name,
fn,
// No-op if we're not running in `deno test` subcommand.
if (typeof op_register_test !== "function") {
return;
}
if (cachedOrigin == undefined) {
cachedOrigin = op_test_get_origin();
}
const location = core.currentUserCallSite();
const sanitizeOps = false;
const sanitizeResources = false;
const testFn = async () => {
if (ignore) return "ignored";
try {
await fn();
return "ok";
} catch (error) {
return { failed: { jsError: core.destructureError(error) } };
}
};
op_register_test(
testFn,
escapeName(name),
ignore,
only,
children: [],
parent,
beforeAll: null,
beforeEach: null,
afterAll: null,
afterEach: null,
};
if (only) onlys.push(item);
parent.children.push(item);
sanitizeOps,
sanitizeResources,
location.fileName,
location.lineNumber,
location.columnNumber,
registerTestIdRetBufU8,
);
}
/**
@ -600,28 +601,16 @@ it.skip = it.ignore;
* @param {boolean} only
*/
function describeInner(name, fn, ignore, only) {
const parent = bddStack.at(-1);
/** @type {BddItem} */
const item = {
name,
kind: "group",
fn: null,
ignore,
only,
children: [],
parent,
beforeAll: null,
beforeEach: null,
afterAll: null,
afterEach: null,
};
if (only) onlys.push(item);
parent.children.push(item);
bddStack.push(item);
// No-op if we're not running in `deno test` subcommand.
if (typeof op_register_test !== "function") {
return;
}
op_register_test_group(name, ignore, only);
try {
fn();
} finally {
bddStack.pop();
op_test_group_pop();
}
}
@ -648,29 +637,65 @@ describe.ignore = (name, fn) => {
};
describe.skip = describe.ignore;
// Keep in sync on the rust side
const BEFORE_ALL = 1;
const BEFORE_EACH = 2;
const AFTER_ALL = 3;
const AFTER_EACH = 4;
/**
* @param {() => any} fn
*/
function beforeAll(fn) {
bddStack.at(-1).beforeAll = fn;
const location = core.currentUserCallSite();
op_register_test_group_lifecycle(
BEFORE_ALL,
fn,
location.fileName,
location.lineNumber,
location.columnNumber,
);
}
/**
* @param {() => any} fn
*/
function afterAll(fn) {
bddStack.at(-1).afterAll = fn;
const location = core.currentUserCallSite();
op_register_test_group_lifecycle(
AFTER_ALL,
fn,
location.fileName,
location.lineNumber,
location.columnNumber,
);
}
/**
* @param {() => any} fn
*/
function beforeEach(fn) {
bddStack.at(-1).beforeEach = fn;
const location = core.currentUserCallSite();
op_register_test_group_lifecycle(
BEFORE_EACH,
fn,
location.fileName,
location.lineNumber,
location.columnNumber,
);
}
/**
* @param {() => any} fn
*/
function afterEach(fn) {
bddStack.at(-1).afterEach = fn;
const location = core.currentUserCallSite();
op_register_test_group_lifecycle(
AFTER_EACH,
fn,
location.fileName,
location.lineNumber,
location.columnNumber,
);
}
globalThis.before = beforeAll;
@ -681,66 +706,3 @@ globalThis.beforeEach = beforeEach;
globalThis.afterEach = afterEach;
globalThis.it = it;
globalThis.describe = describe;
/**
* @param {BddItem} root
*/
async function runBddTests(root) {
if (onlys.length > 0) {
// TODO
return;
}
await runGroup(root);
console.log({ onlys });
}
/**
* @param {BddItem} item
*/
function getLabel(item) {
let name = item.name;
let tmp = item.parent;
while (tmp !== null && tmp !== BDD_ROOT) {
name = `${tmp.name} > ${name}`;
tmp = tmp.parent;
}
return name;
}
/**
* @param {BddItem} group
*/
async function runGroup(group) {
await group.beforeAll?.();
for (let i = 0; i < group.children.length; i++) {
await group.beforeEach?.();
const child = group.children[i];
if (child.kind === "test") {
const name = getLabel(child);
console.log("running:", name);
await child.fn();
} else {
await runGroup(child);
}
await group.afterEach?.();
}
await group.afterAll?.();
}
// Check if we're running the `deno test` command
if (typeof op_register_test === "function") {
// Wait a tick and check if there are any bdd tests to run
setTimeout(async () => {
if (bddStack.length > 0) {
await runBddTests(BDD_ROOT);
}
}, 0);
}

View file

@ -5,6 +5,8 @@ use crate::tools::test::TestDescription;
use crate::tools::test::TestEvent;
use crate::tools::test::TestEventSender;
use crate::tools::test::TestFailure;
use crate::tools::test::TestGroup;
use crate::tools::test::TestGroupLifecycleFn;
use crate::tools::test::TestLocation;
use crate::tools::test::TestStepDescription;
use crate::tools::test::TestStepResult;
@ -28,6 +30,9 @@ deno_core::extension!(deno_test,
op_restore_test_permissions,
op_register_test,
op_register_test_step,
op_register_test_group,
op_test_group_pop,
op_register_test_group_lifecycle,
op_test_get_origin,
op_test_event_step_wait,
op_test_event_step_result_ok,
@ -39,7 +44,7 @@ deno_core::extension!(deno_test,
},
state = |state, options| {
state.put(options.sender);
state.put(TestContainer::default());
state.put(TestContainer::new());
},
);
@ -87,6 +92,7 @@ pub fn op_restore_test_permissions(
}
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
static NEXT_GROUP_ID: AtomicUsize = AtomicUsize::new(1);
#[allow(clippy::too_many_arguments)]
#[op2]
@ -113,6 +119,7 @@ fn op_register_test(
let origin = state.borrow::<ModuleSpecifier>().to_string();
let description = TestDescription {
id,
parent_id: 0,
name,
ignore,
only,
@ -131,6 +138,76 @@ fn op_register_test(
Ok(())
}
#[op2(fast)]
fn op_register_test_group(
state: &mut OpState,
#[string] name: String,
ignore: bool,
only: bool,
) -> Result<(), AnyError> {
let id = NEXT_GROUP_ID.fetch_add(1, Ordering::SeqCst);
let container = state.borrow_mut::<TestContainer>();
let group = TestGroup {
id,
parent_id: 0,
name,
ignore,
only,
children: vec![],
after_all: None,
after_each: None,
before_all: None,
before_each: None,
};
container.register_group(group);
Ok(())
}
#[op2(fast)]
fn op_test_group_pop(state: &mut OpState) -> Result<(), AnyError> {
let container = state.borrow_mut::<TestContainer>();
container.group_pop();
Ok(())
}
#[allow(clippy::too_many_arguments)]
#[op2]
fn op_register_test_group_lifecycle(
state: &mut OpState,
#[smi] kind: u32,
#[global] function: v8::Global<v8::Function>,
#[string] file_name: String,
#[smi] line_number: u32,
#[smi] column_number: u32,
) -> Result<(), AnyError> {
let container = state.borrow_mut::<TestContainer>();
let lifecycle = TestGroupLifecycleFn {
function,
location: TestLocation {
column_number,
line_number,
file_name,
},
};
// Keep in sync with the JS side
if let Some(last_id) = container.stack.last() {
let last: &mut TestGroup =
container.groups.get_mut::<usize>(*last_id).unwrap();
match kind {
1 => last.before_all = Some(lifecycle),
2 => last.before_each = Some(lifecycle),
3 => last.after_all = Some(lifecycle),
4 => last.after_each = Some(lifecycle),
_ => panic!("Unknown test group lifecycle kind"),
}
}
Ok(())
}
#[op2]
#[string]
fn op_test_get_origin(state: &mut OpState) -> String {

View file

@ -222,23 +222,87 @@ pub struct TestLocation {
}
#[derive(Default)]
pub(crate) struct TestContainer(
TestDescriptions,
Vec<v8::Global<v8::Function>>,
);
pub(crate) struct TestContainer {
has_tests: bool,
pub has_only: bool,
pub groups: Vec<TestGroup>,
pub stack: Vec<usize>,
pub tests: (TestDescriptions, Vec<v8::Global<v8::Function>>),
}
impl TestContainer {
pub fn new() -> Self {
let root = TestGroup {
id: 0,
parent_id: 0,
children: vec![],
ignore: false,
name: "<root>".to_string(),
only: false,
after_all: None,
after_each: None,
before_all: None,
before_each: None,
};
let stack = vec![root.id];
Self {
has_tests: false,
groups: vec![root],
has_only: false,
stack,
tests: (
TestDescriptions {
..Default::default()
},
vec![],
),
}
}
pub fn register(
&mut self,
description: TestDescription,
mut description: TestDescription,
function: v8::Global<v8::Function>,
) {
self.0.tests.insert(description.id, description);
self.1.push(function)
self.has_tests = true;
if description.only {
self.has_only = true
}
if let Some(last_id) = self.stack.last() {
description.parent_id = *last_id;
let last: &mut TestGroup =
self.groups.get_mut::<usize>(*last_id).unwrap();
last.children.push(TestGroupChild::Test(description.id));
}
self.tests.0.tests.insert(description.id, description);
self.tests.1.push(function);
}
pub fn register_group(&mut self, mut group: TestGroup) {
if let Some(last_id) = self.stack.last() {
group.parent_id = *last_id;
let last: &mut TestGroup =
self.groups.get_mut::<usize>(*last_id).unwrap();
last.children.push(TestGroupChild::Group(group.id));
}
self.stack.push(group.id);
self.groups.push(group);
}
pub fn group_pop(&mut self) {
self.stack.pop();
}
pub fn is_empty(&self) -> bool {
self.1.is_empty()
self.has_tests
}
}
@ -270,6 +334,7 @@ impl<'a> IntoIterator for &'a TestDescriptions {
#[serde(rename_all = "camelCase")]
pub struct TestDescription {
pub id: usize,
pub parent_id: usize,
pub name: String,
pub ignore: bool,
pub only: bool,
@ -300,6 +365,32 @@ impl From<&TestDescription> for TestFailureDescription {
}
}
#[derive(Debug, Clone)]
pub struct TestGroupLifecycleFn {
pub function: v8::Global<v8::Function>,
pub location: TestLocation,
}
#[derive(Debug, Default, Clone)]
pub struct TestGroup {
pub id: usize,
pub parent_id: usize,
pub name: String,
pub ignore: bool,
pub only: bool,
pub children: Vec<TestGroupChild>,
pub before_all: Option<TestGroupLifecycleFn>,
pub before_each: Option<TestGroupLifecycleFn>,
pub after_all: Option<TestGroupLifecycleFn>,
pub after_each: Option<TestGroupLifecycleFn>,
}
#[derive(Debug, Clone)]
pub enum TestGroupChild {
Group(usize),
Test(usize),
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct TestFailureFormatOptions {
pub hide_stacktraces: bool,
@ -788,6 +879,12 @@ pub fn send_test_event(
)
}
enum TestRunItem {
Lifecycle(usize),
Group(usize),
Test(usize),
}
pub async fn run_tests_for_worker(
worker: &mut MainWorker,
specifier: &ModuleSpecifier,
@ -796,10 +893,22 @@ pub async fn run_tests_for_worker(
) -> Result<(), AnyError> {
let state_rc = worker.js_runtime.op_state();
// Take whatever tests have been registered
let TestContainer(tests, test_functions) =
let tc =
std::mem::take(&mut *state_rc.borrow_mut().borrow_mut::<TestContainer>());
let tests: Arc<TestDescriptions> = tests.into();
eprintln!("{:#?}", tc.stack);
let to_run: Vec<TestRunItem> = vec![];
if let Some(seed) = options.shuffle {
// tests_to_run.shuffle(&mut SmallRng::seed_from_u64(seed));
}
// FILTER
eprintln!("sorted, {:#?}", tc.stack);
let test_functions = tc.tests.1;
let tests: Arc<TestDescriptions> = tc.tests.0.into();
send_test_event(&state_rc, TestEvent::Register(tests.clone()))?;
let res = run_tests_for_worker_inner(
worker,

View file

@ -142,6 +142,28 @@ interface PerformanceMeasureOptions {
end?: string | number;
}
interface BddTestFunction {
(name: string, fn: () => void | Promise<void>): void;
only(name: string, fn: () => void | Promise<void>): void;
ignore(name: string, fn: () => void | Promise<void>): void;
skip(name: string, fn: () => void | Promise<void>): void;
}
declare const it: BddTestFunction;
interface BddTestGroup {
(name: string, fn: () => void | Promise<void>): void;
only(name: string, fn: () => void | Promise<void>): void;
ignore(name: string, fn: () => void | Promise<void>): void;
skip(name: string, fn: () => void | Promise<void>): void;
}
declare const describe: BddTestGroup;
type BddLifecycleFn = (fn: () => unknown) => void;
declare const beforeAll: BddLifecycleFn;
declare const beforeEach: BddLifecycleFn;
declare const afterAll: BddLifecycleFn;
declare const afterEach: BddLifecycleFn;
/** The global namespace where Deno specific, non-standard APIs are located. */
declare namespace Deno {
/** A set of error constructors that are raised by Deno APIs.

View file

@ -483,6 +483,9 @@ const NOT_IMPORTED_OPS = [
"op_restore_test_permissions",
"op_register_test_step",
"op_register_test",
"op_register_test_group",
"op_test_group_pop",
"op_register_test_group_lifecycle",
"op_test_get_origin",
"op_pledge_test_permissions",