diff --git a/cli/tests/integration/repl_tests.rs b/cli/tests/integration/repl_tests.rs index 5335cf964bf2b1..a6524f7181128d 100644 --- a/cli/tests/integration/repl_tests.rs +++ b/cli/tests/integration/repl_tests.rs @@ -871,6 +871,48 @@ fn repl_with_quiet_flag() { assert!(err.is_empty()); } +#[test] +fn repl_unit_tests() { + util::with_pty(&["repl"], |mut console| { + console.write_line( + "\ + console.log('Hello from outside of test!'); \ + Deno.test('test1', async (t) => { \ + console.log('Hello from inside of test!'); \ + await t.step('step1', () => {}); \ + }); \ + Deno.test('test2', () => { \ + throw new Error('some message'); \ + }); \ + console.log('Hello again from outside of test!'); \ + ", + ); + + console.expect("Hello from outside of test!"); + console.expect("Hello again from outside of test!"); + // FIXME(nayeemrmn): REPL unit tests don't support output capturing. + console.expect("Hello from inside of test!"); + console.expect("test1 ..."); + console.expect(" step1 ... ok ("); + console.expect("test1 ... ok ("); + console.expect("test2 ... FAILED ("); + console.expect(" ERRORS "); + console.expect("test2 => :7:6"); + console.expect("error: Error: some message"); + console.expect(" at :8:9"); + console.expect(" FAILURES "); + console.expect("test2 => :7:6"); + console.expect("FAILED | 1 passed (1 step) | 1 failed ("); + console.expect("undefined"); + + console.write_line("Deno.test('test2', () => {});"); + + console.expect("test2 ... ok ("); + console.expect("ok | 1 passed | 0 failed ("); + console.expect("undefined"); + }); +} + #[test] fn npm_packages() { let mut env_vars = util::env_vars_for_npm_tests(); diff --git a/cli/tests/testdata/jupyter/integration_test.ipynb b/cli/tests/testdata/jupyter/integration_test.ipynb index c1b31724ca36c4..25d55e88c8fb98 100644 --- a/cli/tests/testdata/jupyter/integration_test.ipynb +++ b/cli/tests/testdata/jupyter/integration_test.ipynb @@ -628,6 +628,58 @@ "console.table([1, 2, 3])" ] }, + { + "cell_type": "markdown", + "id": "9f38f1eb", + "metadata": {}, + "source": [ + "## Unit Tests With `Deno.test()`" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "b33808fd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "passing test ... \u001b[0m\u001b[32mok\u001b[0m \u001b[0m\u001b[38;5;245m(1ms)\u001b[0m\n", + "passing test with steps ...\n", + " step 1 ... \u001b[0m\u001b[32mok\u001b[0m \u001b[0m\u001b[38;5;245m(0ms)\u001b[0m\n", + " step 2 ... \u001b[0m\u001b[32mok\u001b[0m \u001b[0m\u001b[38;5;245m(0ms)\u001b[0m\n", + "passing test with steps ... \u001b[0m\u001b[32mok\u001b[0m \u001b[0m\u001b[38;5;245m(1ms)\u001b[0m\n", + "failing test ... \u001b[0m\u001b[31mFAILED\u001b[0m \u001b[0m\u001b[38;5;245m(1ms)\u001b[0m\n", + "\n", + "\u001b[0m\u001b[1m\u001b[37m\u001b[41m ERRORS \u001b[0m\n", + "\n", + "failing test \u001b[0m\u001b[38;5;245m=> :7:6\u001b[0m\n", + "\u001b[0m\u001b[1m\u001b[31merror\u001b[0m: Error: some message\n", + " at \u001b[0m\u001b[36m\u001b[0m:\u001b[0m\u001b[33m8\u001b[0m:\u001b[0m\u001b[33m9\u001b[0m\n", + "\n", + "\u001b[0m\u001b[1m\u001b[37m\u001b[41m FAILURES \u001b[0m\n", + "\n", + "failing test \u001b[0m\u001b[38;5;245m=> :7:6\u001b[0m\n", + "\n", + "\u001b[0m\u001b[31mFAILED\u001b[0m | 2 passed (2 steps) | 1 failed \u001b[0m\u001b[38;5;245m(0ms)\u001b[0m\n" + ] + } + ], + "source": [ + "Deno.test(\"passing test\", () => {});\n", + "\n", + "Deno.test(\"passing test with steps\", async (t) => {\n", + " await t.step(\"step 1\", () => {});\n", + " await t.step(\"step 2\", () => {});\n", + "});\n", + "\n", + "Deno.test(\"failing test\", () => {\n", + " throw new Error(\"some message\");\n", + "});\n" + ] + }, { "cell_type": "markdown", "id": "8822eed9-a801-4c1b-81c0-00e4ff180f40", diff --git a/cli/tools/jupyter/mod.rs b/cli/tools/jupyter/mod.rs index fb0860e368b70a..62b298352691d0 100644 --- a/cli/tools/jupyter/mod.rs +++ b/cli/tools/jupyter/mod.rs @@ -3,6 +3,7 @@ use crate::args::Flags; use crate::args::JupyterFlags; use crate::ops; +use crate::tools::jupyter::server::StdioMsg; use crate::tools::repl; use crate::util::logger; use crate::CliFactory; @@ -12,9 +13,17 @@ use deno_core::located_script_name; use deno_core::resolve_url_or_path; use deno_core::serde::Deserialize; use deno_core::serde_json; +use deno_runtime::deno_io::Stdio; +use deno_runtime::deno_io::StdioPipe; use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsContainer; use tokio::sync::mpsc; +use tokio::sync::mpsc::unbounded_channel; +use tokio::sync::mpsc::UnboundedSender; + +use super::test::reporters::PrettyTestReporter; +use super::test::TestEvent; +use super::test::TestEventSender; mod install; pub(crate) mod jupyter_msg; @@ -71,13 +80,25 @@ pub async fn kernel( connection_filepath ) })?; - + let (test_event_sender, test_event_receiver) = + unbounded_channel::(); + let test_event_sender = TestEventSender::new(test_event_sender); + let stdout = StdioPipe::File(test_event_sender.stdout()); + let stderr = StdioPipe::File(test_event_sender.stderr()); let mut worker = worker_factory .create_custom_worker( main_module.clone(), permissions, - vec![ops::jupyter::deno_jupyter::init_ops(stdio_tx)], - Default::default(), + vec![ + ops::jupyter::deno_jupyter::init_ops(stdio_tx.clone()), + ops::testing::deno_test::init_ops(test_event_sender.clone()), + ], + // FIXME(nayeemrmn): Test output capturing currently doesn't work. + Stdio { + stdin: StdioPipe::Inherit, + stdout, + stderr, + }, ) .await?; worker.setup_repl().await?; @@ -86,9 +107,35 @@ pub async fn kernel( "Deno[Deno.internal].enableJupyter();", )?; let worker = worker.into_main_worker(); - let repl_session = - repl::ReplSession::initialize(cli_options, npm_resolver, resolver, worker) - .await?; + let mut repl_session = repl::ReplSession::initialize( + cli_options, + npm_resolver, + resolver, + worker, + main_module, + test_event_sender, + test_event_receiver, + ) + .await?; + struct TestWriter(UnboundedSender); + impl std::io::Write for TestWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self + .0 + .send(StdioMsg::Stdout(String::from_utf8_lossy(buf).into_owned())) + .ok(); + Ok(buf.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + repl_session.set_test_reporter_factory(Box::new(move || { + Box::new( + PrettyTestReporter::new(false, true, false, true) + .with_writer(Box::new(TestWriter(stdio_tx.clone()))), + ) + })); server::JupyterServer::start(spec, stdio_rx, repl_session).await?; diff --git a/cli/tools/repl/mod.rs b/cli/tools/repl/mod.rs index a1e741dfddf430..c25dc00c6c7c04 100644 --- a/cli/tools/repl/mod.rs +++ b/cli/tools/repl/mod.rs @@ -12,6 +12,7 @@ use deno_core::unsync::spawn_blocking; use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsContainer; use rustyline::error::ReadlineError; +use tokio::sync::mpsc::unbounded_channel; pub(crate) mod cdp; mod channel; @@ -28,6 +29,9 @@ pub use session::EvaluationOutput; pub use session::ReplSession; pub use session::REPL_INTERNALS_NAME; +use super::test::TestEvent; +use super::test::TestEventSender; + #[allow(clippy::await_holding_refcell_ref)] async fn read_line_and_poll( repl_session: &mut ReplSession, @@ -114,15 +118,31 @@ pub async fn run(flags: Flags, repl_flags: ReplFlags) -> Result { .deno_dir() .ok() .and_then(|dir| dir.repl_history_file_path()); - + let (test_event_sender, test_event_receiver) = + unbounded_channel::(); + let test_event_sender = TestEventSender::new(test_event_sender); let mut worker = worker_factory - .create_main_worker(main_module, permissions) + .create_custom_worker( + main_module.clone(), + permissions, + vec![crate::ops::testing::deno_test::init_ops( + test_event_sender.clone(), + )], + Default::default(), + ) .await?; worker.setup_repl().await?; let worker = worker.into_main_worker(); - let mut repl_session = - ReplSession::initialize(cli_options, npm_resolver, resolver, worker) - .await?; + let mut repl_session = ReplSession::initialize( + cli_options, + npm_resolver, + resolver, + worker, + main_module, + test_event_sender, + test_event_receiver, + ) + .await?; let mut rustyline_channel = rustyline_channel(); let helper = EditorHelper { diff --git a/cli/tools/repl/session.rs b/cli/tools/repl/session.rs index f833fbf5d259cb..338a253d21b598 100644 --- a/cli/tools/repl/session.rs +++ b/cli/tools/repl/session.rs @@ -9,6 +9,13 @@ use crate::colors; use crate::lsp::ReplLanguageServer; use crate::npm::CliNpmResolver; use crate::resolver::CliGraphResolver; +use crate::tools::test::report_tests; +use crate::tools::test::reporters::PrettyTestReporter; +use crate::tools::test::reporters::TestReporter; +use crate::tools::test::run_tests_for_worker; +use crate::tools::test::worker_has_tests; +use crate::tools::test::TestEvent; +use crate::tools::test::TestEventSender; use deno_ast::swc::ast as swc_ast; use deno_ast::swc::visit::noop_visit_type; @@ -23,6 +30,7 @@ use deno_core::futures::FutureExt; use deno_core::futures::StreamExt; use deno_core::serde_json; use deno_core::serde_json::Value; +use deno_core::unsync::spawn; use deno_core::LocalInspectorSession; use deno_graph::source::Resolver; use deno_runtime::worker::MainWorker; @@ -131,6 +139,11 @@ pub struct ReplSession { pub language_server: ReplLanguageServer, pub notifications: Rc>>, referrer: ModuleSpecifier, + main_module: ModuleSpecifier, + test_reporter_factory: Box Box>, + test_event_sender: TestEventSender, + /// This is only optional because it's temporarily taken when evaluating. + test_event_receiver: Option>, } impl ReplSession { @@ -139,6 +152,9 @@ impl ReplSession { npm_resolver: Arc, resolver: Arc, mut worker: MainWorker, + main_module: ModuleSpecifier, + test_event_sender: TestEventSender, + test_event_receiver: tokio::sync::mpsc::UnboundedReceiver, ) -> Result { let language_server = ReplLanguageServer::new_initialized().await?; let mut session = worker.create_inspector_session().await; @@ -189,6 +205,12 @@ impl ReplSession { language_server, referrer, notifications: Rc::new(RefCell::new(notification_rx)), + test_reporter_factory: Box::new(|| { + Box::new(PrettyTestReporter::new(false, true, false, true)) + }), + main_module, + test_event_sender, + test_event_receiver: Some(test_event_receiver), }; // inject prelude @@ -197,6 +219,13 @@ impl ReplSession { Ok(repl_session) } + pub fn set_test_reporter_factory( + &mut self, + f: Box Box>, + ) { + self.test_reporter_factory = f; + } + pub async fn closing(&mut self) -> Result { let closed = self .evaluate_expression("(this.closed)") @@ -325,7 +354,7 @@ impl ReplSession { // If that fails, we retry it without wrapping in parens letting the error bubble up to the // user if it is still an error. - if wrapped_line != line + let result = if wrapped_line != line && (evaluate_response.is_err() || evaluate_response .as_ref() @@ -337,7 +366,29 @@ impl ReplSession { self.evaluate_ts_expression(line).await } else { evaluate_response + }; + + if worker_has_tests(&mut self.worker) { + let report_tests_handle = spawn(report_tests( + self.test_event_receiver.take().unwrap(), + (self.test_reporter_factory)(), + )); + run_tests_for_worker( + &mut self.worker, + &self.main_module, + &Default::default(), + &Default::default(), + ) + .await + .unwrap(); + self + .test_event_sender + .send(TestEvent::ForceEndReport) + .unwrap(); + self.test_event_receiver = Some(report_tests_handle.await.unwrap().1); } + + result } async fn set_last_thrown_error( diff --git a/cli/tools/test/mod.rs b/cli/tools/test/mod.rs index 66e3a587002e4e..b3aadc1e71c77b 100644 --- a/cli/tools/test/mod.rs +++ b/cli/tools/test/mod.rs @@ -473,6 +473,12 @@ pub async fn test_specifier( Ok(()) } +pub fn worker_has_tests(worker: &mut MainWorker) -> bool { + let state_rc = worker.js_runtime.op_state(); + let state = state_rc.borrow(); + !state.borrow::().0.is_empty() +} + pub async fn run_tests_for_worker( worker: &mut MainWorker, specifier: &ModuleSpecifier, diff --git a/cli/tools/test/reporters/common.rs b/cli/tools/test/reporters/common.rs index 889110057e3012..aa92c7ecddcbd2 100644 --- a/cli/tools/test/reporters/common.rs +++ b/cli/tools/test/reporters/common.rs @@ -215,7 +215,7 @@ pub(super) fn report_summary( writeln!( writer, - "\n{} | {} {}\n", + "\n{} | {} {}", status, summary_result, colors::gray(format!("({})", display::human_elapsed(elapsed.as_millis()))), diff --git a/cli/tools/test/reporters/dot.rs b/cli/tools/test/reporters/dot.rs index cb005b29780468..eba07a332a033e 100644 --- a/cli/tools/test/reporters/dot.rs +++ b/cli/tools/test/reporters/dot.rs @@ -190,6 +190,7 @@ impl TestReporter for DotTestReporter { &self.summary, elapsed, ); + println!(); } fn report_sigint( diff --git a/cli/tools/test/reporters/pretty.rs b/cli/tools/test/reporters/pretty.rs index c3b61c66ccfdef..c09c4cd2303cb3 100644 --- a/cli/tools/test/reporters/pretty.rs +++ b/cli/tools/test/reporters/pretty.rs @@ -43,6 +43,10 @@ impl PrettyTestReporter { } } + pub fn with_writer(self, writer: Box) -> Self { + Self { writer, ..self } + } + fn force_report_wait(&mut self, description: &TestDescription) { if !self.in_new_line { writeln!(&mut self.writer).unwrap(); @@ -368,6 +372,9 @@ impl TestReporter for PrettyTestReporter { _test_steps: &IndexMap, ) { common::report_summary(&mut self.writer, &self.cwd, &self.summary, elapsed); + if !self.repl { + writeln!(&mut self.writer).unwrap(); + } self.in_new_line = true; }