1use std::fmt;
2use std::io::Write;
3use std::mem::take;
4use std::slice;
5use std::sync::Arc;
6use std::time::Instant;
7
8use anyhow::{Context, Result};
9
10use crate::alloc::fmt::TryWrite;
11use crate::alloc::prelude::*;
12use crate::cli::naming::Naming;
13use crate::cli::visitor;
14use crate::cli::{
15 AssetKind, Color, CommandBase, Config, Entry, EntryPoint, ExitCode, Io, Options, SharedFlags,
16 Stream,
17};
18use crate::compile::FileSourceLoader;
19use crate::doc::{TestKind, TestParams};
20use crate::modules::capture_io::CaptureIo;
21use crate::runtime::{Repr, Value, Vm, VmError, VmResult};
22use crate::{Diagnostics, Hash, Item, ItemBuf, Source, Sources, TypeHash, Unit};
23
24mod cli {
25 use std::string::String;
26 use std::vec::Vec;
27
28 use clap::Parser;
29
30 #[derive(Parser, Debug, Clone)]
31 #[command(rename_all = "kebab-case")]
32 pub struct Flags {
33 #[arg(long)]
35 pub warnings_are_errors: bool,
36 #[arg(long, short = 'q')]
38 pub quiet: bool,
39 #[arg(long)]
41 pub fail_fast: bool,
42 #[arg(long)]
45 pub skip_lib_tests: bool,
46 pub filters: Vec<String>,
48 }
49}
50
51pub(super) use cli::Flags;
52
53impl CommandBase for Flags {
54 #[inline]
55 fn is_debug(&self) -> bool {
56 true
57 }
58
59 #[inline]
60 fn is_workspace(&self, kind: AssetKind) -> bool {
61 matches!(kind, AssetKind::Test)
62 }
63
64 #[inline]
65 fn describe(&self) -> &str {
66 "Testing"
67 }
68
69 #[inline]
70 fn propagate(&mut self, c: &mut Config, _: &mut SharedFlags) {
71 c.test = true;
72 }
73}
74
75enum BatchKind {
76 LibTests,
77 DocTests,
78 ContextDocTests,
79}
80
81impl fmt::Display for BatchKind {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 Self::LibTests => write!(f, "lib tests"),
85 Self::DocTests => write!(f, "doc tests"),
86 Self::ContextDocTests => write!(f, "context doc tests"),
87 }
88 }
89}
90
91struct Batch<'a> {
92 kind: BatchKind,
93 entry: Option<EntryPoint<'a>>,
94 cases: Vec<TestCase>,
95}
96
97pub(super) async fn run<'p, I>(
99 io: &mut Io<'_>,
100 c: &Config,
101 flags: &Flags,
102 shared: &SharedFlags,
103 options: &Options,
104 entry: &mut Entry<'_>,
105 entries: I,
106) -> anyhow::Result<ExitCode>
107where
108 I: IntoIterator<Item = EntryPoint<'p>>,
109{
110 let start = Instant::now();
111
112 let mut executed = 0usize;
113 let mut skipped = 0usize;
114 let mut build_errors = 0usize;
115 let mut skipped_entries = 0usize;
116 let mut collected = Vec::new();
117
118 let capture = crate::modules::capture_io::CaptureIo::new();
119 let context = shared.context(entry, c, Some(&capture))?;
120
121 let mut batches = Vec::new();
122 let mut naming = Naming::default();
123 let mut name = String::new();
124
125 let mut filter = |item: &Item| -> Result<bool> {
126 if flags.filters.is_empty() {
127 return Ok(false);
128 }
129
130 name.clear();
131
132 write!(name, "{item}")?;
133
134 if !flags.filters.iter().any(|f| name.contains(f.as_str())) {
135 return Ok(true);
136 }
137
138 Ok(false)
139 };
140
141 for e in entries {
142 if flags.skip_lib_tests {
143 continue;
144 }
145
146 let mut options = options.clone();
147
148 if e.is_argument() {
149 options.function_body = true;
150 }
151
152 let item = naming.item(&e)?;
153
154 let mut sources = Sources::new();
155
156 let source = match Source::from_path(e.path()) {
157 Ok(source) => source,
158 Err(error) => return Err(error).context(e.path().display().try_to_string()?),
159 };
160
161 sources.insert(source)?;
162
163 let mut diagnostics = if shared.warnings || flags.warnings_are_errors {
164 Diagnostics::new()
165 } else {
166 Diagnostics::without_warnings()
167 };
168
169 let mut doc_visitor = crate::doc::Visitor::new(&item)?;
170 let mut functions = visitor::FunctionVisitor::new(visitor::Attribute::Test);
171 let mut source_loader = FileSourceLoader::new();
172
173 let unit = crate::prepare(&mut sources)
174 .with_context(&context)
175 .with_diagnostics(&mut diagnostics)
176 .with_options(&options)
177 .with_visitor(&mut doc_visitor)?
178 .with_visitor(&mut functions)?
179 .with_source_loader(&mut source_loader)
180 .build();
181
182 if diagnostics.has_error() || flags.warnings_are_errors && diagnostics.has_warning() {
183 build_errors = build_errors.wrapping_add(1);
184 collected.try_push((diagnostics, sources))?;
185 continue;
186 }
187
188 diagnostics.emit(&mut io.stdout.lock(), &sources)?;
189
190 let unit = Arc::new(unit?);
191 let sources = Arc::new(sources);
192
193 let mut cases = Vec::new();
194
195 for (hash, item) in functions.into_functions() {
196 let filtered = filter(&item)?;
197
198 cases.try_push(TestCase::new(
199 hash,
200 item,
201 TestKind::Free,
202 unit.clone(),
203 sources.clone(),
204 TestParams::default(),
205 filtered,
206 ))?;
207 }
208
209 batches.try_push(Batch {
210 kind: BatchKind::LibTests,
211 entry: Some(e.try_clone()?),
212 cases,
213 })?;
214
215 let mut artifacts = crate::doc::Artifacts::without_assets();
216 crate::doc::build("root", &mut artifacts, None, slice::from_ref(&doc_visitor))?;
217
218 if !c.filtered {
219 let cases = populate_doc_tests(
220 io,
221 artifacts,
222 shared,
223 flags,
224 &options,
225 &context,
226 &mut build_errors,
227 &mut skipped_entries,
228 &mut collected,
229 &mut filter,
230 )?;
231
232 batches.try_push(Batch {
233 kind: BatchKind::DocTests,
234 entry: Some(e),
235 cases,
236 })?;
237 }
238 }
239
240 let mut artifacts = crate::doc::Artifacts::without_assets();
241 crate::doc::build("root", &mut artifacts, Some(&context), &[])?;
242
243 if !c.filtered {
244 let cases = populate_doc_tests(
245 io,
246 artifacts,
247 shared,
248 flags,
249 options,
250 &context,
251 &mut build_errors,
252 &mut skipped_entries,
253 &mut collected,
254 &mut filter,
255 )?;
256
257 batches.try_push(Batch {
258 kind: BatchKind::ContextDocTests,
259 entry: None,
260 cases,
261 })?;
262 }
263
264 let runtime = Arc::new(context.runtime()?);
265 let mut failed = Vec::new();
266
267 for batch in batches {
268 if batch.cases.is_empty() {
269 continue;
270 }
271
272 let all_ignored = batch
273 .cases
274 .iter()
275 .all(|case| case.filtered || case.params.no_run);
276
277 let mut section = None;
278
279 if shared.verbose {
280 if all_ignored {
281 section = Some(("Ignoring", Color::Ignore));
282 } else {
283 section = Some(("Running", Color::Highlight));
284 }
285 }
286
287 if let Some((title, color)) = section {
288 let mut section = io.section(title, Stream::Stdout, color)?;
289
290 if !flags.quiet && !all_ignored {
291 section.append(format_args!(" {} {}", batch.cases.len(), batch.kind))?;
292
293 if let Some(entry) = batch.entry {
294 section.append(format_args!(" from {entry}"))?;
295 }
296 }
297
298 section.close()?;
299 }
300
301 for mut case in batch.cases {
302 if case.filtered {
303 skipped = skipped.wrapping_add(1);
304 continue;
305 }
306
307 if case.params.no_run {
308 continue;
309 }
310
311 let mut vm = Vm::new(runtime.clone(), case.unit.clone());
312 case.execute(&mut vm, &capture).await?;
313 executed = executed.wrapping_add(1);
314
315 if case.outcome.is_ok() {
316 if flags.quiet {
317 write!(io.stdout, ".")?;
318 } else {
319 case.emit(io)?;
320 }
321
322 continue;
323 }
324
325 if flags.quiet {
326 write!(io.stdout, "f")?;
327 }
328
329 failed.try_push(case)?;
330
331 if flags.fail_fast {
332 break;
333 }
334 }
335 }
336
337 if flags.quiet {
338 writeln!(io.stdout)?;
339 }
340
341 let failures = failed.len();
342
343 for (diagnostics, sources) in collected {
344 diagnostics.emit(&mut io.stdout.lock(), &sources)?;
345 }
346
347 for case in failed {
348 case.emit(io)?;
349 }
350
351 let elapsed = start.elapsed();
352
353 let mut section = io.section("Executed", Stream::Stdout, Color::Highlight)?;
354
355 section.append(format_args!(" {executed} tests"))?;
356
357 let any = failures > 0 || build_errors > 0 || skipped > 0 || skipped_entries > 0;
358
359 if any {
360 section.append(" with")?;
361
362 let mut first = true;
363
364 let mut emit = |color: Color, count: usize, singular: &str, plural: &str| {
365 if count == 0 {
366 return Ok::<_, anyhow::Error>(());
367 }
368
369 if !take(&mut first) {
370 section.append(", ")?;
371 } else {
372 section.append(" ")?;
373 }
374
375 let what = if count == 1 { singular } else { plural };
376
377 section.append(format_args!("{count} "))?;
378 section.append_with(what, color)?;
379 Ok::<_, anyhow::Error>(())
380 };
381
382 emit(Color::Error, failures, "failure", "failures")?;
383 emit(Color::Error, build_errors, "build error", "build errors")?;
384 emit(Color::Ignore, skipped, "filtered", "filtered")?;
385 emit(
386 Color::Ignore,
387 skipped_entries,
388 "filtered entries",
389 "filtered entries",
390 )?;
391 }
392
393 writeln!(io.stdout, " in {:.3} seconds", elapsed.as_secs_f64())?;
394
395 if build_errors == 0 && failures == 0 {
396 Ok(ExitCode::Success)
397 } else {
398 Ok(ExitCode::Failure)
399 }
400}
401
402fn populate_doc_tests(
403 io: &mut Io,
404 artifacts: crate::doc::Artifacts,
405 shared: &SharedFlags,
406 flags: &Flags,
407 options: &Options,
408 context: &crate::Context,
409 build_errors: &mut usize,
410 skipped_entries: &mut usize,
411 collected: &mut Vec<(Diagnostics, Sources)>,
412 filter: &mut dyn FnMut(&Item) -> Result<bool>,
413) -> Result<Vec<TestCase>> {
414 let mut cases = Vec::new();
415
416 for test in artifacts.tests() {
417 if !options.test_std && test.item.as_crate() == Some("std") || test.params.ignore {
418 continue;
419 }
420
421 let is_filtered = filter(&test.item)?;
422
423 if is_filtered {
424 *skipped_entries = skipped_entries.wrapping_add(1);
425 continue;
426 }
427
428 let mut sources = Sources::new();
429
430 let source = Source::new(test.item.try_to_string()?, &test.content)?;
431 sources.insert(source)?;
432
433 let mut diagnostics = if shared.warnings || flags.warnings_are_errors {
434 Diagnostics::new()
435 } else {
436 Diagnostics::without_warnings()
437 };
438
439 let mut source_loader = FileSourceLoader::new();
440
441 let mut options = options.clone();
442 options.function_body = true;
443
444 let unit = crate::prepare(&mut sources)
445 .with_context(context)
446 .with_diagnostics(&mut diagnostics)
447 .with_options(&options)
448 .with_source_loader(&mut source_loader)
449 .build();
450
451 if diagnostics.has_error() || flags.warnings_are_errors && diagnostics.has_warning() {
452 *build_errors = build_errors.wrapping_add(1);
453 collected.try_push((diagnostics, sources))?;
454 continue;
455 }
456
457 diagnostics.emit(&mut io.stdout.lock(), &sources)?;
458
459 if !test.params.no_run {
460 let unit = Arc::new(unit?);
461 let sources = Arc::new(sources);
462
463 cases.try_push(TestCase::new(
464 Hash::EMPTY,
465 test.item.try_clone()?,
466 test.kind,
467 unit.clone(),
468 sources.clone(),
469 test.params,
470 is_filtered,
471 ))?;
472 }
473 }
474
475 Ok(cases)
476}
477
478#[derive(Debug)]
479enum Outcome {
480 Ok,
481 Panic(VmError),
482 ExpectedPanic,
483 None,
484 Err(Value),
485}
486
487impl Outcome {
488 fn is_ok(&self) -> bool {
489 matches!(self, Outcome::Ok)
490 }
491}
492
493struct TestCase {
494 hash: Hash,
495 item: ItemBuf,
496 kind: TestKind,
497 unit: Arc<Unit>,
498 sources: Arc<Sources>,
499 params: TestParams,
500 outcome: Outcome,
501 output: Vec<u8>,
502 filtered: bool,
503}
504
505impl TestCase {
506 fn new(
507 hash: Hash,
508 item: ItemBuf,
509 kind: TestKind,
510 unit: Arc<Unit>,
511 sources: Arc<Sources>,
512 params: TestParams,
513 filtered: bool,
514 ) -> Self {
515 Self {
516 hash,
517 item,
518 kind,
519 unit,
520 sources,
521 params,
522 outcome: Outcome::Ok,
523 output: Vec::new(),
524 filtered,
525 }
526 }
527
528 async fn execute(&mut self, vm: &mut Vm, capture_io: &CaptureIo) -> Result<()> {
529 let result = match vm.execute(self.hash, ()) {
530 Ok(mut execution) => execution.async_complete().await,
531 Err(err) => VmResult::Err(err),
532 };
533
534 capture_io.drain_into(&mut self.output)?;
535
536 self.outcome = match result {
537 VmResult::Ok(v) => match v.as_ref() {
538 Repr::Any(value) => match value.type_hash() {
539 Result::<Value, Value>::HASH => {
540 let result = value.borrow_ref::<Result<Value, Value>>()?;
541
542 match &*result {
543 Ok(..) => Outcome::Ok,
544 Err(error) => Outcome::Err(error.clone()),
545 }
546 }
547 Option::<Value>::HASH => {
548 let option = value.borrow_ref::<Option<Value>>()?;
549
550 match &*option {
551 Some(..) => Outcome::Ok,
552 None => Outcome::None,
553 }
554 }
555 _ => Outcome::Ok,
556 },
557 _ => Outcome::Ok,
558 },
559 VmResult::Err(e) => Outcome::Panic(e),
560 };
561
562 if self.params.should_panic {
563 if matches!(self.outcome, Outcome::Panic(..)) {
564 self.outcome = Outcome::Ok;
565 } else {
566 self.outcome = Outcome::ExpectedPanic;
567 }
568 }
569
570 Ok(())
571 }
572
573 fn emit(self, io: &mut Io<'_>) -> Result<()> {
574 let mut section = io.section("Test", Stream::Stdout, Color::Highlight)?;
575
576 match self.kind {
577 TestKind::Free => {
578 section.append(format_args!(" {}: ", self.item))?;
579 }
580 TestKind::Protocol(protocol) => {
581 section.append(format_args!(" {}", self.item))?;
582 section.append_with(format_args!(" {}: ", protocol.name), Color::Important)?;
583 }
584 }
585
586 let mut emitted = None;
587
588 match &self.outcome {
589 Outcome::Panic(error) => {
590 section.error("errored")?;
591 emitted = Some(error);
592 }
593 Outcome::ExpectedPanic => {
594 section.error("expected panic because of `should_panic`, but ran without issue")?;
595 }
596 Outcome::Err(error) => {
597 section.error("err: ")?;
598 section.append(format_args!("{error:?}"))?;
599 }
600 Outcome::None => {
601 section.error("returned none")?;
602 }
603 Outcome::Ok => {
604 section.passed("ok")?;
605 }
606 }
607
608 section.close()?;
609
610 if let Some(error) = emitted {
611 error.emit(io.stdout, &self.sources)?;
612 }
613
614 if !self.outcome.is_ok() && !self.output.is_empty() {
615 writeln!(io.stdout, "-- output --")?;
616 io.stdout.write_all(&self.output)?;
617 writeln!(io.stdout, "-- end of output --")?;
618 }
619
620 Ok(())
621 }
622}