use std::fmt::Debug;
use std::sync::atomic::{AtomicBool, Ordering};
use color_eyre::eyre::{self, ContextCompat};
use typst::diag::Warned;
use typst::model::Document as TypstDocument;
use typst::syntax::Source;
use tytanic_core::doc::compare::Strategy;
use tytanic_core::doc::render::Origin;
use tytanic_core::doc::{compile, Document};
use tytanic_core::project::Project;
use tytanic_core::test::{Kind, Suite, SuiteResult, Test, TestResult, TestResultKind};
use crate::cli::TestFailure;
use crate::report::Reporter;
use crate::world::SystemWorld;
use crate::DEFAULT_OPTIMIZE_OPTIONS;
#[derive(Debug, Clone)]
pub enum Action {
Run {
strategy: Option<Strategy>,
export: bool,
origin: Origin,
},
Update {
export: bool,
origin: Origin,
},
}
#[derive(Debug, Clone)]
pub struct RunnerConfig<'c> {
pub promote_warnings: bool,
pub optimize: bool,
pub fail_fast: bool,
pub pixel_per_pt: f32,
pub action: Action,
pub cancellation: &'c AtomicBool,
}
pub struct Runner<'c, 'p> {
pub project: &'p Project,
pub suite: &'p Suite,
pub world: &'p SystemWorld,
pub result: SuiteResult,
pub config: RunnerConfig<'c>,
}
impl<'c, 'p> Runner<'c, 'p> {
pub fn new(
project: &'p Project,
suite: &'p Suite,
world: &'p SystemWorld,
config: RunnerConfig<'c>,
) -> Self {
Self {
project,
result: SuiteResult::new(suite),
suite,
world,
config,
}
}
pub fn test<'s>(&'s self, test: &'p Test) -> TestRunner<'c, 's, 'p> {
TestRunner {
project_runner: self,
test,
result: TestResult::new(),
}
}
pub fn run_inner(&mut self, reporter: &Reporter) -> eyre::Result<()> {
reporter.report_status(&self.result)?;
for (id, test) in self.suite.matched() {
if self.config.cancellation.load(Ordering::SeqCst) {
return Ok(());
}
let result = self.test(test).run()?;
reporter.clear_status()?;
match result.kind() {
Some(
TestResultKind::FailedCompilation { .. } | TestResultKind::FailedComparison(..),
) => {
reporter.report_test_fail(test, &result, true)?;
if self.config.fail_fast {
self.result.set_test_result(id.clone(), result);
return Ok(());
}
}
Some(TestResultKind::PassedCompilation | TestResultKind::PassedComparison) => {
reporter.report_test_pass(test, result.duration(), result.warnings())?;
}
_ => unreachable!(),
}
reporter.report_status(&self.result)?;
self.result.set_test_result(id.clone(), result);
}
reporter.clear_status()?;
Ok(())
}
pub fn run(mut self, reporter: &Reporter) -> eyre::Result<SuiteResult> {
self.result.start();
reporter.report_start(&self.result)?;
let res = self.run_inner(reporter);
self.result.end();
reporter.report_end(&self.result)?;
res?;
Ok(self.result)
}
}
pub struct TestRunner<'c, 's, 'p> {
project_runner: &'s Runner<'c, 'p>,
test: &'p Test,
result: TestResult,
}
impl TestRunner<'_, '_, '_> {
fn run_inner(&mut self) -> eyre::Result<()> {
let paths = self.project_runner.project.paths();
match self.project_runner.config.action {
Action::Run {
strategy,
export,
origin,
} => {
let output = self.load_out_src()?;
let output = self.compile_out_doc(output)?;
let output = self.render_out_doc(output)?;
if export {
self.export_out_doc(&output)?;
}
match self.test.kind() {
Kind::Ephemeral => {
let reference = self.load_ref_src()?;
let reference = self.compile_ref_doc(reference)?;
let reference = self.render_ref_doc(reference)?;
if export {
self.export_ref_doc(&reference)?;
let diff = self.render_diff_doc(&output, &reference, origin)?;
self.export_diff_doc(&diff)?;
}
if let Some(strategy) = strategy {
if let Err(err) = self.compare(&output, &reference, strategy) {
eyre::bail!(err);
}
}
}
Kind::Persistent => {
let reference = self.load_ref_doc()?;
if export {
let diff = self.render_diff_doc(&output, &reference, origin)?;
self.export_diff_doc(&diff)?;
}
if let Some(strategy) = strategy {
if let Err(err) = self.compare(&output, &reference, strategy) {
eyre::bail!(err);
}
}
}
Kind::CompileOnly => {}
}
}
Action::Update { export, origin } => match self.test.kind() {
Kind::Ephemeral => {
let output = self.load_out_src()?;
let output = self.compile_out_doc(output)?;
let output = self.render_out_doc(output)?;
if export {
self.export_out_doc(&output)?;
}
}
Kind::Persistent => {
let output = self.load_out_src()?;
let output = self.compile_out_doc(output)?;
let output = self.render_out_doc(output)?;
self.test.create_reference_documents(
paths,
&output,
self.project_runner
.config
.optimize
.then_some(&*DEFAULT_OPTIMIZE_OPTIONS),
)?;
if export {
let reference = self.load_ref_doc()?;
self.export_out_doc(&reference)?;
let diff = self.render_diff_doc(&output, &reference, origin)?;
self.export_diff_doc(&diff)?;
}
}
Kind::CompileOnly => eyre::bail!("attempted to update compile-only test"),
},
}
Ok(())
}
pub fn run(mut self) -> eyre::Result<TestResult> {
self.result.start();
self.prepare()?;
let res = self.run_inner();
self.cleanup()?;
self.result.end();
if let Err(err) = res {
if !err.chain().any(|s| s.is::<TestFailure>()) {
eyre::bail!(err);
}
}
Ok(self.result)
}
pub fn prepare(&mut self) -> eyre::Result<()> {
tracing::trace!(test = ?self.test.id(), "clearing temporary directories");
self.test
.create_temporary_directories(self.project_runner.project.paths())?;
Ok(())
}
pub fn cleanup(&mut self) -> eyre::Result<()> {
Ok(())
}
pub fn load_out_src(&mut self) -> eyre::Result<Source> {
tracing::trace!(test = ?self.test.id(), "loading output source");
Ok(self.test.load_source(self.project_runner.project.paths())?)
}
pub fn load_ref_src(&mut self) -> eyre::Result<Source> {
tracing::trace!(test = ?self.test.id(), "loading reference source");
if !self.test.kind().is_ephemeral() {
eyre::bail!("attempted to load reference source for non-ephemeral test");
}
self.test
.load_reference_source(self.project_runner.project.paths())?
.wrap_err_with(|| format!("couldn't load reference source for test {}", self.test.id()))
}
pub fn load_ref_doc(&mut self) -> eyre::Result<Document> {
tracing::trace!(test = ?self.test.id(), "loading reference document");
if !self.test.kind().is_persistent() {
eyre::bail!("attempted to load reference source for non-persistent test");
}
self.test
.load_reference_documents(self.project_runner.project.paths())?
.wrap_err_with(|| {
format!(
"couldn't load reference document for test {}",
self.test.id()
)
})
}
pub fn render_out_doc(&mut self, doc: TypstDocument) -> eyre::Result<Document> {
tracing::trace!(test = ?self.test.id(), "rendering output document");
Ok(Document::render(
doc,
self.project_runner.config.pixel_per_pt,
))
}
pub fn render_ref_doc(&mut self, doc: TypstDocument) -> eyre::Result<Document> {
tracing::trace!(test = ?self.test.id(), "rendering reference document");
if !self.test.kind().is_ephemeral() {
eyre::bail!("attempted to render reference for non-ephemeral test");
}
Ok(Document::render(
doc,
self.project_runner.config.pixel_per_pt,
))
}
pub fn render_diff_doc(
&mut self,
output: &Document,
reference: &Document,
origin: Origin,
) -> eyre::Result<Document> {
tracing::trace!(test = ?self.test.id(), "rendering difference document");
if self.test.kind().is_compile_only() {
eyre::bail!("attempted to render difference document for compile-only test");
}
Ok(Document::render_diff(reference, output, origin))
}
pub fn compile_out_doc(&mut self, output: Source) -> eyre::Result<TypstDocument> {
tracing::trace!(test = ?self.test.id(), "compiling output document");
self.compile_inner(output, false)
}
pub fn compile_ref_doc(&mut self, reference: Source) -> eyre::Result<TypstDocument> {
tracing::trace!(test = ?self.test.id(), "compiling reference document");
if self.test.kind().is_compile_only() {
eyre::bail!("attempted to compile reference for compile-only test");
}
self.compile_inner(reference, true)
}
fn compile_inner(&mut self, source: Source, is_reference: bool) -> eyre::Result<TypstDocument> {
let Warned { output, warnings } = compile::compile(
source,
self.project_runner.world,
self.project_runner.config.promote_warnings,
);
if warnings.is_empty() {
self.result.set_warnings(warnings);
}
let doc = match output {
Ok(doc) => {
self.result.set_passed_compilation();
doc
}
Err(err) => {
if is_reference {
self.result.set_failed_reference_compilation(err);
} else {
self.result.set_failed_test_compilation(err);
}
eyre::bail!(TestFailure);
}
};
Ok(doc)
}
pub fn export_ref_doc(&mut self, reference: &Document) -> eyre::Result<()> {
tracing::trace!(test = ?self.test.id(), "saving reference document");
if !self.test.kind().is_ephemeral() {
eyre::bail!("attempted to save reference document for non-ephemeral test");
}
reference.save(
self.project_runner
.project
.paths()
.test_ref_dir(self.test.id()),
None,
)?;
Ok(())
}
pub fn export_out_doc(&mut self, output: &Document) -> eyre::Result<()> {
tracing::trace!(test = ?self.test.id(), "saving output document");
output.save(
self.project_runner
.project
.paths()
.test_out_dir(self.test.id()),
None,
)?;
Ok(())
}
pub fn export_diff_doc(&mut self, doc: &Document) -> eyre::Result<()> {
tracing::trace!(test = ?self.test.id(), "saving difference document");
if self.test.kind().is_compile_only() {
eyre::bail!("attempted to save difference document for compile-only test");
}
doc.save(
self.project_runner
.project
.paths()
.test_diff_dir(self.test.id()),
None,
)?;
Ok(())
}
pub fn compare(
&mut self,
output: &Document,
reference: &Document,
strategy: Strategy,
) -> eyre::Result<()> {
tracing::trace!(test = ?self.test.id(), "comparing");
if self.test.kind().is_compile_only() {
eyre::bail!("attempted to compare compile-only test");
}
if let Err(error) = Document::compare(output, reference, strategy) {
self.result.set_failed_comparison(error);
eyre::bail!(TestFailure);
}
self.result.set_passed_comparison();
Ok(())
}
}