use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::{env, io};
use chrono::{DateTime, Utc};
use clap::ColorChoice;
use color_eyre::eyre;
use color_eyre::eyre::WrapErr;
use termcolor::Color;
use thiserror::Error;
use tytanic_core::config::{Config, ConfigLayer};
use tytanic_core::project::Project;
use tytanic_core::test::{Id, Suite};
use tytanic_core::test_set::{self, eval, Error as TestSetError, TestSet};
use crate::kit;
use crate::ui::{self, Ui};
use crate::world::SystemWorld;
pub mod add;
pub mod list;
pub mod remove;
pub mod run;
pub mod status;
pub mod update;
pub mod util;
pub static CANCELLED: AtomicBool = AtomicBool::new(false);
const ENV_PATH_SEP: char = if cfg!(windows) { ';' } else { ':' };
pub const EXIT_OK: u8 = 0;
pub const EXIT_TEST_FAILURE: u8 = 1;
pub const EXIT_OPERATION_FAILURE: u8 = 2;
pub const EXIT_ERROR: u8 = 3;
#[derive(Debug, Error)]
#[error("an operation failed")]
pub struct OperationFailure;
#[derive(Debug, Error)]
#[error("one or more test failed")]
pub struct TestFailure;
pub struct Context<'a> {
pub args: &'a Args,
pub ui: &'a Ui,
}
impl<'a> Context<'a> {
pub fn new(args: &'a Args, ui: &'a Ui) -> Self {
Self { args, ui }
}
}
impl Context<'_> {
pub fn error_aborted(&self) -> io::Result<()> {
self.ui.error_with(|w| writeln!(w, "Operation aborted"))
}
pub fn error_root_not_found(&self, root: &Path) -> io::Result<()> {
self.ui
.error_with(|w| writeln!(w, "Root '{}' not found", root.display()))
}
pub fn error_no_project(&self) -> io::Result<()> {
self.ui.error_hinted_with(
|w| writeln!(w, "Must be in a typst project"),
|w| {
write!(w, "You can pass the project root using ")?;
ui::write_colored(w, Color::Cyan, |w| write!(w, "--root <path>"))?;
writeln!(w)
},
)
}
pub fn error_test_set_failure(&self, error: TestSetError) -> io::Result<()> {
self.ui.error_with(|w| {
writeln!(
w,
"Couldn't parse or evaluate test set expression:\n{error:?}",
)
})
}
pub fn error_test_already_exists(&self, id: &Id) -> io::Result<()> {
self.ui.error_with(|w| {
write!(w, "Test ")?;
ui::write_test_id(w, id)?;
writeln!(w, " already exists")
})
}
pub fn error_no_tests(&self) -> io::Result<()> {
self.ui.error("Matched no tests")
}
pub fn error_too_many_tests(&self, expr: &str) -> io::Result<()> {
self.ui.error_hinted_with(
|w| writeln!(w, "Matched more than one test"),
|w| {
write!(w, "use '")?;
ui::write_colored(w, Color::Cyan, |w| write!(w, "all:"))?;
writeln!(w, "{expr}' to confirm using all tests")
},
)
}
pub fn error_nested_tests(&self) -> io::Result<()> {
self.ui.error_hinted_with(
|w| writeln!(w, "Found nested tests"),
|w| {
writeln!(w, "This is no longer supported")?;
write!(w, "You can run ")?;
ui::write_colored(w, Color::Cyan, |w| write!(w, "tt util migrate"))?;
writeln!(w, " to automatically fix the tests")
},
)
}
pub fn run(&mut self) -> eyre::Result<()> {
self.args.cmd.run(self)
}
}
impl Context<'_> {
pub fn root(&self) -> eyre::Result<PathBuf> {
Ok(match &self.args.global.root {
Some(root) => {
if !root.try_exists()? {
self.error_root_not_found(root)?;
eyre::bail!(OperationFailure);
}
root.canonicalize()?
}
None => env::current_dir().wrap_err("reading PWD")?,
})
}
pub fn config(&self) -> eyre::Result<Config> {
let mut config = Config::new(None);
config.user = ConfigLayer::collect_user()?;
Ok(config)
}
pub fn project(&self) -> eyre::Result<Project> {
let root = self.root()?;
let Some(project) = Project::discover(root, self.args.global.root.is_some())? else {
self.error_no_project()?;
eyre::bail!(OperationFailure);
};
Ok(project)
}
pub fn test_set(&self, filter: &FilterArgs) -> eyre::Result<TestSet> {
if !filter.tests.is_empty() {
let mut tests = filter
.tests
.iter()
.map(|test| eval::Set::built_in_pattern(test_set::Pat::Exact(test.into())));
let a = tests.next();
let b = tests.next();
let set = a
.and_then(|a| b.map(|b| (a, b)))
.map(|(a, b)| eval::Set::built_in_union(a, b, tests))
.unwrap_or_default();
Ok(TestSet::new(eval::Context::empty(), set))
} else {
let ctx = eval::Context::with_built_ins();
let mut set = match TestSet::parse_and_evaluate(ctx, &filter.expression) {
Ok(set) => set,
Err(err) => {
self.error_test_set_failure(err)?;
eyre::bail!(OperationFailure);
}
};
if !filter.no_implicit_skip {
set.add_implicit_skip();
}
Ok(set)
}
}
pub fn collect_tests(&self, project: &Project, set: &TestSet) -> eyre::Result<Suite> {
if !util::migrate::collect_old_structure(project.paths(), "self")?.is_empty() {
self.error_nested_tests()?;
eyre::bail!(OperationFailure);
}
let suite = Suite::collect(project.paths(), set)?;
Ok(suite)
}
pub fn collect_all_tests(&self, project: &Project) -> eyre::Result<Suite> {
let suite = Suite::collect(
project.paths(),
&TestSet::new(eval::Context::empty(), eval::Set::built_in_all()),
)?;
Ok(suite)
}
pub fn world(&self, compile: &CompileArgs) -> eyre::Result<SystemWorld> {
kit::world(
self.root()?,
&self.args.global.fonts,
&self.args.global.package,
compile,
)
}
}
macro_rules! ansi {
($s:expr; b) => {
concat!("\x1B[1m", $s, "\x1B[0m")
};
($s:expr; u) => {
concat!("\x1B[4m", $s, "\x1B[0m")
};
($s:expr;) => {
$s
};
($s:expr; $first:ident $( + $rest:tt)*) => {
ansi!(ansi!($s; $($rest)*); $first)
};
}
#[rustfmt::skip]
static AFTER_LONG_ABOUT: &str = concat!(
ansi!("Exit Codes:\n"; u + b),
" ", ansi!("0"; b), " Success\n",
" ", ansi!("1"; b), " At least one test failed\n",
" ", ansi!("2"; b), " The requested operation failed\n",
" ", ansi!("3"; b), " An unexpected error occurred",
);
#[derive(clap::Args, Debug, Clone)]
pub struct GlobalArgs {
#[arg(long, short, env = "TYPST_ROOT", global = true)]
pub root: Option<PathBuf>,
#[arg(long, short, global = true)]
pub jobs: Option<usize>,
#[command(flatten, next_help_heading = "Font Options")]
pub fonts: FontArgs,
#[command(flatten, next_help_heading = "Package Options")]
pub package: PackageArgs,
#[command(flatten, next_help_heading = "Output Options")]
pub output: OutputArgs,
}
#[derive(clap::Args, Debug, Clone)]
pub struct FilterArgs {
#[allow(rustdoc::bare_urls)]
#[arg(short, long, default_value = "all()")]
pub expression: String,
#[arg(short = 'S', long)]
pub no_implicit_skip: bool,
#[arg(required = false, conflicts_with = "expression")]
pub tests: Vec<String>,
}
fn parse_source_date_epoch(raw: &str) -> Result<DateTime<Utc>, String> {
let timestamp: i64 = raw
.parse()
.map_err(|err| format!("timestamp must be decimal integer ({err})"))?;
DateTime::from_timestamp(timestamp, 0).ok_or_else(|| "timestamp out of range".to_string())
}
#[derive(clap::Args, Debug, Clone)]
pub struct CompileArgs {
#[arg(
long,
env = "SOURCE_DATE_EPOCH",
value_name = "UNIX_TIMESTAMP",
value_parser = parse_source_date_epoch,
global = true,
)]
pub creation_timestamp: Option<DateTime<Utc>>,
#[arg(long, global = true)]
pub promote_warnings: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, clap::ValueEnum)]
pub enum Direction {
Ltr,
Rtl,
}
#[derive(clap::Args, Debug, Clone)]
pub struct RenderArgs {
#[arg(long, visible_alias = "dir", global = true)]
pub direction: Option<Direction>,
#[arg(long, visible_alias = "ppi", default_value_t = 144.0, global = true)]
pub pixel_per_inch: f32,
}
#[derive(clap::Args, Debug, Clone)]
pub struct ExportArgs {
#[command(flatten)]
pub render: RenderArgs,
#[arg(long, global = true)]
pub no_save_temporary: bool,
#[arg(long, global = true)]
pub no_optimize_references: bool,
}
#[derive(clap::Args, Debug, Clone)]
pub struct CompareArgs {
#[arg(long, default_value_t = 0, global = true)]
pub max_delta: u8,
#[arg(long, default_value_t = 0, global = true)]
pub max_deviations: usize,
}
#[derive(clap::Args, Debug, Clone)]
pub struct RunArgs {
#[arg(long, global = true)]
pub no_fail_fast: bool,
}
#[derive(clap::Args, Debug, Clone)]
pub struct FontArgs {
#[arg(long, global = true)]
pub ignore_system_fonts: bool,
#[arg(
long = "font-path",
env = "TYPST_FONT_PATHS",
value_name = "DIR",
value_delimiter = ENV_PATH_SEP,
global = true,
)]
pub font_paths: Vec<PathBuf>,
}
#[derive(clap::Args, Debug, Clone)]
pub struct PackageArgs {
#[clap(long, env = "TYPST_PACKAGE_PATH", value_name = "DIR")]
pub package_path: Option<PathBuf>,
#[clap(long, env = "TYPST_PACKAGE_CACHE_PATH", value_name = "DIR")]
pub package_cache_path: Option<PathBuf>,
#[clap(long, visible_alias = "cert", env = "TYPST_CERT")]
pub certificate: Option<PathBuf>,
}
#[derive(clap::Args, Debug, Clone)]
pub struct OutputArgs {
#[clap(
long,
value_name = "WHEN",
require_equals = true,
num_args = 0..=1,
default_value = "auto",
default_missing_value = "always",
global = true,
)]
pub color: ColorChoice,
#[arg(long, short, action = clap::ArgAction::Count, global = true)]
pub verbose: u8,
}
#[derive(clap::Parser, Debug, Clone)]
#[command(version, after_long_help = AFTER_LONG_ABOUT)]
pub struct Args {
#[command(flatten)]
pub global: GlobalArgs,
#[command(subcommand)]
pub cmd: Command,
}
#[derive(clap::Subcommand, Debug, Clone)]
pub enum Command {
#[command(visible_alias = "st")]
Status(status::Args),
#[command(visible_alias = "ls")]
List(list::Args),
#[command(visible_alias = "r")]
Run(run::Args),
#[command()]
Update(update::Args),
#[command()]
Add(add::Args),
#[command(visible_alias = "rm")]
Remove(remove::Args),
#[command()]
Util(util::Args),
}
impl Command {
pub fn run(&self, ctx: &mut Context) -> eyre::Result<()> {
match self {
Command::Add(args) => add::run(ctx, args),
Command::Remove(args) => remove::run(ctx, args),
Command::Status(args) => status::run(ctx, args),
Command::List(args) => list::run(ctx, args),
Command::Update(args) => update::run(ctx, args),
Command::Run(args) => run::run(ctx, args),
Command::Util(args) => args.cmd.run(ctx),
}
}
}