summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNayeem Rahman <nayeemrmn99@gmail.com>2023-10-05 21:45:11 +0100
committerGitHub <noreply@github.com>2023-10-05 21:45:11 +0100
commitac464ead49c9f676d7117baa2ab06433e132e188 (patch)
tree646d0674bb8d585478d3520b61785c7d453044e3
parent820e93e3e742a22f010f3200312039c50ae0d63a (diff)
feat(jupyter): support Deno.test() (#20778)
-rw-r--r--cli/tests/integration/repl_tests.rs42
-rw-r--r--cli/tests/testdata/jupyter/integration_test.ipynb52
-rw-r--r--cli/tools/jupyter/mod.rs59
-rw-r--r--cli/tools/repl/mod.rs30
-rw-r--r--cli/tools/repl/session.rs53
-rw-r--r--cli/tools/test/mod.rs6
-rw-r--r--cli/tools/test/reporters/common.rs2
-rw-r--r--cli/tools/test/reporters/dot.rs1
-rw-r--r--cli/tools/test/reporters/pretty.rs7
9 files changed, 239 insertions, 13 deletions
diff --git a/cli/tests/integration/repl_tests.rs b/cli/tests/integration/repl_tests.rs
index 5335cf964..a6524f718 100644
--- a/cli/tests/integration/repl_tests.rs
+++ b/cli/tests/integration/repl_tests.rs
@@ -872,6 +872,48 @@ fn repl_with_quiet_flag() {
}
#[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 => <anonymous>:7:6");
+ console.expect("error: Error: some message");
+ console.expect(" at <anonymous>:8:9");
+ console.expect(" FAILURES ");
+ console.expect("test2 => <anonymous>: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();
env_vars.push(("NO_COLOR".to_owned(), "1".to_owned()));
diff --git a/cli/tests/testdata/jupyter/integration_test.ipynb b/cli/tests/testdata/jupyter/integration_test.ipynb
index c1b31724c..25d55e88c 100644
--- a/cli/tests/testdata/jupyter/integration_test.ipynb
+++ b/cli/tests/testdata/jupyter/integration_test.ipynb
@@ -630,6 +630,58 @@
},
{
"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=> <anonymous>:7:6\u001b[0m\n",
+ "\u001b[0m\u001b[1m\u001b[31merror\u001b[0m: Error: some message\n",
+ " at \u001b[0m\u001b[36m<anonymous>\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=> <anonymous>: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",
"metadata": {},
"source": [
diff --git a/cli/tools/jupyter/mod.rs b/cli/tools/jupyter/mod.rs
index fb0860e36..62b298352 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::<TestEvent>();
+ 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<StdioMsg>);
+ impl std::io::Write for TestWriter {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+ 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 a1e741dfd..c25dc00c6 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<i32, AnyError> {
.deno_dir()
.ok()
.and_then(|dir| dir.repl_history_file_path());
-
+ let (test_event_sender, test_event_receiver) =
+ unbounded_channel::<TestEvent>();
+ 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 f833fbf5d..338a253d2 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<RefCell<UnboundedReceiver<Value>>>,
referrer: ModuleSpecifier,
+ main_module: ModuleSpecifier,
+ test_reporter_factory: Box<dyn Fn() -> Box<dyn TestReporter>>,
+ test_event_sender: TestEventSender,
+ /// This is only optional because it's temporarily taken when evaluating.
+ test_event_receiver: Option<tokio::sync::mpsc::UnboundedReceiver<TestEvent>>,
}
impl ReplSession {
@@ -139,6 +152,9 @@ impl ReplSession {
npm_resolver: Arc<dyn CliNpmResolver>,
resolver: Arc<CliGraphResolver>,
mut worker: MainWorker,
+ main_module: ModuleSpecifier,
+ test_event_sender: TestEventSender,
+ test_event_receiver: tokio::sync::mpsc::UnboundedReceiver<TestEvent>,
) -> Result<Self, AnyError> {
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<dyn Fn() -> Box<dyn TestReporter>>,
+ ) {
+ self.test_reporter_factory = f;
+ }
+
pub async fn closing(&mut self) -> Result<bool, AnyError> {
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 66e3a5870..b3aadc1e7 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::<ops::testing::TestContainer>().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 889110057..aa92c7ecd 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 cb005b297..eba07a332 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 c3b61c66c..c09c4cd23 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<dyn std::io::Write>) -> 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<usize, TestStepDescription>,
) {
common::report_summary(&mut self.writer, &self.cwd, &self.summary, elapsed);
+ if !self.repl {
+ writeln!(&mut self.writer).unwrap();
+ }
self.in_new_line = true;
}