diff --git a/crates/rune/src/cli/tests.rs b/crates/rune/src/cli/tests.rs index 7c2dc3427..ddc26b80d 100644 --- a/crates/rune/src/cli/tests.rs +++ b/crates/rune/src/cli/tests.rs @@ -13,7 +13,9 @@ use crate::cli::naming::Naming; use crate::compile::{ItemBuf, FileSourceLoader}; use crate::modules::capture_io::CaptureIo; use crate::runtime::{Value, Vm, VmError, VmResult, UnitFn}; +use crate::doc::TestParams; use crate::{Hash, Sources, Unit, Diagnostics, Source}; +use crate::termcolor::{WriteColor, ColorSpec, Color}; #[derive(Parser, Debug, Clone)] pub(super) struct Flags { @@ -66,9 +68,11 @@ pub(super) async fn run<'p, I>( where I: IntoIterator>, { + let colors = Colors::new(); + let start = Instant::now(); - let mut failures = 0usize; + let mut build_errors = 0usize; let mut executed = 0usize; let capture = crate::modules::capture_io::CaptureIo::new(); @@ -78,8 +82,6 @@ where let mut cases = Vec::new(); let mut naming = Naming::default(); - let mut build_error = false; - let mut include_std = false; for opt in &flags.options { @@ -126,7 +128,7 @@ where diagnostics.emit(&mut io.stdout.lock(), &sources)?; if diagnostics.has_error() || flags.warnings_are_errors && diagnostics.has_warning() { - build_error = true; + build_errors = build_errors.wrapping_add(1); continue; } @@ -136,7 +138,7 @@ where doc_visitors.push(doc_visitor); for (hash, item) in functions.into_functions() { - cases.push(TestCase::new(hash, item, unit.clone(), sources.clone())); + cases.push(TestCase::new(hash, item, unit.clone(), sources.clone(), TestParams::default())); } } @@ -174,7 +176,7 @@ where diagnostics.emit(&mut io.stdout.lock(), &sources)?; if diagnostics.has_error() || flags.warnings_are_errors && diagnostics.has_warning() { - build_error = true; + build_errors = build_errors.wrapping_add(1); continue; } @@ -186,25 +188,39 @@ where bail!("Compiling source did not result in a function at offset 0"); }; - cases.push(TestCase::new(hash, test.item.clone(), unit.clone(), sources.clone())); + cases.push(TestCase::new(hash, test.item.clone(), unit.clone(), sources.clone(), test.params)); } } let runtime = Arc::new(context.runtime()); + let mut failed = Vec::new(); - for case in &mut cases { - let mut vm = Vm::new(runtime.clone(), case.unit.clone()); - - executed = executed.wrapping_add(1); + let total = cases.len(); - let success = case.execute(io, &mut vm, flags.quiet, Some(&capture)).await?; + for mut case in cases { + executed = executed.wrapping_add(1); - if !success { - failures = failures.wrapping_add(1); + let mut vm = Vm::new(runtime.clone(), case.unit.clone()); + case.execute(&mut vm, &capture).await?; - if flags.fail_fast { - break; + if case.outcome.is_ok() { + if flags.quiet { + write!(io.stdout, ".")?; + } else { + case.emit(io, &colors)?; } + + continue; + } + + if flags.quiet { + write!(io.stdout, "f")?; + } + + failed.push(case); + + if flags.fail_fast { + break; } } @@ -212,22 +228,25 @@ where writeln!(io.stdout)?; } - for case in &cases { - case.emit(io)?; + let failures = failed.len(); + + for case in failed { + case.emit(io, &colors)?; } let elapsed = start.elapsed(); writeln!( io.stdout, - "Executed {} tests with {} failures ({} skipped) in {:.3} seconds", + "Executed {} tests with {} failures ({} skipped, {} build errors) in {:.3} seconds", executed, failures, - cases.len() - executed, + total - executed, + build_errors, elapsed.as_secs_f64() )?; - if !build_error && failures == 0 { + if build_errors == 0 && failures == 0 { Ok(ExitCode::Success) } else { Ok(ExitCode::Failure) @@ -235,10 +254,18 @@ where } #[derive(Debug)] -enum FailureReason { - Crash(VmError), - ReturnedNone, - ReturnedErr { output: Box<[u8]>, error: Value }, +enum Outcome { + Ok, + Panic(VmError), + ExpectedPanic, + None, + Err(Value), +} + +impl Outcome { + fn is_ok(&self) -> bool { + matches!(self, Outcome::Ok) + } } struct TestCase { @@ -246,116 +273,122 @@ struct TestCase { item: ItemBuf, unit: Arc, sources: Arc, - outcome: Option, - buf: Vec, + params: TestParams, + outcome: Outcome, + output: Vec, } impl TestCase { - fn new(hash: Hash, item: ItemBuf, unit: Arc, sources: Arc) -> Self { + fn new(hash: Hash, item: ItemBuf, unit: Arc, sources: Arc, params: TestParams) -> Self { Self { hash, item, unit, sources, - outcome: None, - buf: Vec::new(), + params, + outcome: Outcome::Ok, + output: Vec::new(), } } async fn execute( &mut self, - io: &mut Io<'_>, vm: &mut Vm, - quiet: bool, - capture_io: Option<&CaptureIo>, - ) -> Result { - if !quiet { - write!(io.stdout, "{} ", self.item)?; - } - + capture_io: &CaptureIo, + ) -> Result<()> { let result = match vm.execute(self.hash, ()) { Ok(mut execution) => execution.async_complete().await, Err(err) => VmResult::Err(err), }; - if let Some(capture_io) = capture_io { - let _ = capture_io.drain_into(&mut self.buf); - } + capture_io.drain_into(&mut self.output)?; self.outcome = match result { VmResult::Ok(v) => match v { Value::Result(result) => match result.take()? { - Ok(..) => None, - Err(error) => Some(FailureReason::ReturnedErr { - output: self.buf.as_slice().into(), - error, - }), + Ok(..) => Outcome::Ok, + Err(error) => Outcome::Err(error), }, Value::Option(option) => match *option.borrow_ref()? { - Some(..) => None, - None => Some(FailureReason::ReturnedNone), + Some(..) => Outcome::Ok, + None => Outcome::None, }, - _ => None, + _ => Outcome::Ok, }, - VmResult::Err(e) => Some(FailureReason::Crash(e)), + VmResult::Err(e) => { + Outcome::Panic(e) + } }; - if quiet { - match &self.outcome { - Some(FailureReason::Crash(..)) => { - writeln!(io.stdout, "F")?; - } - Some(FailureReason::ReturnedErr { .. }) => { - write!(io.stdout, "f")?; - } - Some(FailureReason::ReturnedNone { .. }) => { - write!(io.stdout, "n")?; - } - None => { - write!(io.stdout, ".")?; - } - } - } else { - match &self.outcome { - Some(FailureReason::Crash(..)) => { - writeln!(io.stdout, "failed")?; - } - Some(FailureReason::ReturnedErr { .. }) => { - writeln!(io.stdout, "returned error")?; - } - Some(FailureReason::ReturnedNone { .. }) => { - writeln!(io.stdout, "returned none")?; - } - None => { - writeln!(io.stdout, "passed")?; - } + if self.params.should_panic { + if matches!(self.outcome, Outcome::Panic(..)) { + self.outcome = Outcome::Ok; + } else { + self.outcome = Outcome::ExpectedPanic; } } - self.buf.clear(); - Ok(self.outcome.is_none()) + Ok(()) } - fn emit(&self, io: &mut Io<'_>) -> Result<()> { - if let Some(outcome) = &self.outcome { - match outcome { - FailureReason::Crash(err) => { - writeln!(io.stdout, "----------------------------------------")?; - writeln!(io.stdout, "Test: {}\n", self.item)?; - err.emit(io.stdout, &self.sources)?; - } - FailureReason::ReturnedNone { .. } => {} - FailureReason::ReturnedErr { output, error, .. } => { - writeln!(io.stdout, "----------------------------------------")?; - writeln!(io.stdout, "Test: {}\n", self.item)?; - writeln!(io.stdout, "Error: {:?}\n", error)?; - writeln!(io.stdout, "-- output --")?; - io.stdout.write_all(output)?; - writeln!(io.stdout, "-- end of output --")?; - } + fn emit(self, io: &mut Io<'_>, colors: &Colors) -> Result<()> { + write!(io.stdout, "Test: {}: ", self.item)?; + + match &self.outcome { + Outcome::Panic(error) => { + io.stdout.set_color(&colors.error)?; + writeln!(io.stdout, "Panicked")?; + io.stdout.reset()?; + + error.emit(io.stdout, &self.sources)?; + } + Outcome::ExpectedPanic => { + io.stdout.set_color(&colors.error)?; + writeln!(io.stdout, "Expected panic because of `should_panic`, but ran without issue")?; + io.stdout.reset()?; + } + Outcome::Err(error) => { + io.stdout.set_color(&colors.error)?; + write!(io.stdout, "Returned Err: ")?; + io.stdout.reset()?; + writeln!(io.stdout, "{:?}", error)?; + } + Outcome::None => { + io.stdout.set_color(&colors.error)?; + writeln!(io.stdout, "Returned None")?; + io.stdout.reset()?; + } + Outcome::Ok => { + io.stdout.set_color(&colors.passed)?; + writeln!(io.stdout, "Ok")?; + io.stdout.reset()?; } } + if !self.outcome.is_ok() && !self.output.is_empty() { + writeln!(io.stdout, "-- output --")?; + io.stdout.write_all(&self.output)?; + writeln!(io.stdout, "-- end of output --")?; + } + Ok(()) } } + +struct Colors { + error: ColorSpec, + passed: ColorSpec, +} + +impl Colors { + fn new() -> Self { + let mut this = Self { + error: ColorSpec::new(), + passed: ColorSpec::new(), + }; + + this.error.set_fg(Some(Color::Red)); + this.passed.set_fg(Some(Color::Green)); + this + } +} diff --git a/crates/rune/src/doc.rs b/crates/rune/src/doc.rs index e9968b20f..15ed3135f 100644 --- a/crates/rune/src/doc.rs +++ b/crates/rune/src/doc.rs @@ -4,7 +4,7 @@ mod context; pub(self) use self::context::Context; mod artifacts; -pub(crate) use self::artifacts::Artifacts; +pub(crate) use self::artifacts::{TestParams, Artifacts}; mod templating; diff --git a/crates/rune/src/doc/artifacts.rs b/crates/rune/src/doc/artifacts.rs index 0e8febce4..bde199436 100644 --- a/crates/rune/src/doc/artifacts.rs +++ b/crates/rune/src/doc/artifacts.rs @@ -16,6 +16,8 @@ use anyhow::{Context as _, Error, Result}; pub(crate) struct TestParams { /// If the test should not run. pub(crate) no_run: bool, + /// If the test should panic. + pub(crate) should_panic: bool, } /// A discovered test. diff --git a/crates/rune/src/doc/build/markdown.rs b/crates/rune/src/doc/build/markdown.rs index 2df12aa42..1ec992ab7 100644 --- a/crates/rune/src/doc/build/markdown.rs +++ b/crates/rune/src/doc/build/markdown.rs @@ -267,6 +267,10 @@ where params.no_run = true; continue; } + "should_panic" => { + params.should_panic = true; + continue; + } RUNE_TOKEN => { (RUNE_TOKEN, RUST_TOKEN, true) } diff --git a/crates/rune/src/modules/option.rs b/crates/rune/src/modules/option.rs index e522f70d7..c042b10e0 100644 --- a/crates/rune/src/modules/option.rs +++ b/crates/rune/src/modules/option.rs @@ -21,6 +21,7 @@ pub fn module() -> Result { module.associated_function("transpose", transpose_impl)?; module.associated_function("unwrap", unwrap_impl)?; module.associated_function("unwrap_or", Option::::unwrap_or)?; + module.associated_function("ok_or", Option::::ok_or::)?; module.function_meta(unwrap_or_else)?; module.associated_function(Protocol::INTO_ITER, option_iter)?; Ok(module) diff --git a/crates/rune/src/modules/result.rs b/crates/rune/src/modules/result.rs index 96f0dd100..675ba901e 100644 --- a/crates/rune/src/modules/result.rs +++ b/crates/rune/src/modules/result.rs @@ -25,9 +25,9 @@ pub fn module() -> Result { module.associated_function("is_err", is_err)?; module.associated_function("unwrap", unwrap_impl)?; module.associated_function("unwrap_or", Result::::unwrap_or)?; - module.associated_function("expect", expect_impl)?; - module.associated_function("and_then", and_then_impl)?; - module.associated_function("map", map_impl)?; + module.function_meta(expect)?; + module.function_meta(and_then)?; + module.function_meta(map)?; Ok(module) } @@ -58,24 +58,88 @@ fn unwrap_impl(result: Result) -> VmResult { } } -fn expect_impl(result: Result, message: &str) -> VmResult { +/// Returns the contained [`Ok`] value, consuming the `self` value. +/// +/// Because this function may panic, its use is generally discouraged. Instead, +/// prefer to use pattern matching and handle the [`Err`] case explicitly, or +/// call [`unwrap_or`], [`unwrap_or_else`], or [`unwrap_or_default`]. +/// +/// [`unwrap_or`]: Result::unwrap_or +/// [`unwrap_or_else`]: Result::unwrap_or_else +/// [`unwrap_or_default`]: Result::unwrap_or_default +/// +/// # Panics +/// +/// Panics if the value is an [`Err`], with a panic message including the passed +/// message, and the content of the [`Err`]. +/// +/// # Examples +/// +/// ```rune,should_panic +/// let x = Err("emergency failure"); +/// x.expect("Testing expect"); // panics with `Testing expect: emergency failure` +/// ``` +/// +/// # Recommended Message Style +/// +/// We recommend that `expect` messages are used to describe the reason you +/// _expect_ the `Result` should be `Ok`. If you're having trouble remembering +/// how to phrase expect error messages remember to focus on the word "should" +/// as in "env variable should be set by blah" or "the given binary should be +/// available and executable by the current user". +#[rune::function(instance)] +fn expect(result: Result, message: &str) -> VmResult { match result { Ok(value) => VmResult::Ok(value), Err(err) => VmResult::err(Panic::msg(format_args!("{}: {:?}", message, err))), } } -fn and_then_impl(this: &Result, then: Function) -> VmResult> { +/// Calls `op` if the result is [`Ok`], otherwise returns the [`Err`] value of `self`. +/// +/// This function can be used for control flow based on `Result` values. +/// +/// # Examples +/// +/// ```rune +/// fn sq_then_to_string(x) { +/// x.checked_mul(x).ok_or("overflowed") +/// } +/// +/// assert_eq!(Ok(2).and_then(sq_then_to_string), Ok(4)); +/// assert_eq!(Ok(i64::MAX).and_then(sq_then_to_string), Err("overflowed")); +/// assert_eq!(Err("not a number").and_then(sq_then_to_string), Err("not a number")); +/// ``` +#[rune::function(instance)] +fn and_then(this: &Result, op: Function) -> VmResult> { match this { - // No need to clone v, passing the same reference forward - Ok(v) => VmResult::Ok(vm_try!(then.call::<_, _>((v,)))), + Ok(v) => VmResult::Ok(vm_try!(op.call::<_, _>((v,)))), Err(e) => VmResult::Ok(Err(e.clone())), } } -fn map_impl(this: &Result, then: Function) -> VmResult> { +/// Maps a `Result` to `Result` by applying a function to a +/// contained [`Ok`] value, leaving an [`Err`] value untouched. +/// +/// This function can be used to compose the results of two functions. +/// +/// # Examples +/// +/// Print the numbers on each line of a string multiplied by two. +/// +/// ```rune +/// let lines = ["1", "2", "3", "4"]; +/// let out = []; +/// +/// for num in lines { +/// out.push(i64::parse(num).map(|i| i * 2)?); +/// } +/// +/// assert_eq!(out, [2, 4, 6, 8]); +/// ``` +#[rune::function(instance)] +fn map(this: &Result, then: Function) -> VmResult> { match this { - // No need to clone v, passing the same reference forward Ok(v) => VmResult::Ok(Ok(vm_try!(then.call::<_, _>((v,))))), Err(e) => VmResult::Ok(Err(e.clone())), }