diff --git a/Cargo.toml b/Cargo.toml index e5a8968..5dacbe1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ shell-escape = "0.1" strip-ansi-escapes = "0.1" subprocess = "0.2.7" tempfile = "3.8" +thiserror = "1.0" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } unicode_categories = "0.1" diff --git a/selftest/complex/missing-language/missing-language.md b/selftest/complex/missing-language/missing-language.md index 830260a..4cff31e 100644 --- a/selftest/complex/missing-language/missing-language.md +++ b/selftest/complex/missing-language/missing-language.md @@ -2,11 +2,10 @@ ```scrut $ $SCRUT_BIN test --match-markdown "*.mdtest" "$TESTDIR"/missing-language.mdtest 2>&1 -* parse test from "*missing-language.mdtest" with markdown parser (glob) +* Failed to parse test from "*missing-language.mdtest" with markdown parser (glob) Caused by: Code block starting at line 2 is missing language specifier. Use ```scrut to make this block a Scrut test, or any other language to make Scrut skip this block. -* (glob?) [1] ``` @@ -14,10 +13,9 @@ Caused by: ```scrut $ $SCRUT_BIN test --match-markdown "*.mdtest" "$TESTDIR"/missing-language-empty-block.mdtest 2>&1 -* parse test from "*missing-language-empty-block.mdtest" with markdown parser (glob) +* Failed to parse test from "*missing-language-empty-block.mdtest" with markdown parser (glob) Caused by: Code block starting at line 2 is missing language specifier. Use ```scrut to make this block a Scrut test, or any other language to make Scrut skip this block. -* (glob?) [1] ``` diff --git a/src/bin/commands/test.rs b/src/bin/commands/test.rs index 9da1b93..f663b30 100644 --- a/src/bin/commands/test.rs +++ b/src/bin/commands/test.rs @@ -6,7 +6,6 @@ */ use std::collections::BTreeMap; -use std::fmt::Display; use std::io::stdout; use std::io::IsTerminal; use std::path::Path; @@ -47,15 +46,10 @@ use crate::utils::make_executor; use crate::utils::FileParser; use crate::utils::TestEnvironment; -#[derive(Debug, Clone)] +#[derive(Debug, thiserror::Error)] +#[error("validation failed")] pub struct ValidationFailedError; -impl Display for ValidationFailedError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "validation failed") - } -} - /// Run tests from files or directories #[derive(Debug, ClapParser)] pub struct Args { diff --git a/src/bin/fb_main.rs b/src/bin/fb_main.rs new file mode 100644 index 0000000..01c210a --- /dev/null +++ b/src/bin/fb_main.rs @@ -0,0 +1,23 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +extern crate scrut; + +mod commands; +mod fb_main_common; +mod utils; + +use anyhow::Result; +use cli::ExitCode; +use fb_main_common::main_impl; +use fb_main_common::Args; +use fbinit::FacebookInit; + +#[cli::main("scrut", error_logging)] +pub fn main(fb: FacebookInit, args: Args) -> Result { + main_impl(fb, args) +} diff --git a/src/bin/fb_main_common.rs b/src/bin/fb_main_common.rs new file mode 100644 index 0000000..9d61fd5 --- /dev/null +++ b/src/bin/fb_main_common.rs @@ -0,0 +1,58 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use anyhow::anyhow; +use anyhow::Result; +use clap::Parser; +use cli::ExitCode; +use fbinit::FacebookInit; +use scrut::parsers::markdown::MarkdownParserError; +use tracing::error; + +use crate::commands::root::Commands; +use crate::commands::root::GlobalParameters; +use crate::commands::test::ValidationFailedError; + +#[derive(Debug, Parser)] +#[clap( + about = "A testing toolkit to scrutinize CLI applications", + author = "clifoundation" +)] +pub struct Args { + #[clap(subcommand)] + commands: Commands, + + #[clap(flatten)] + global: GlobalParameters, +} + +/// Implemented here because it's only used in `fb-main*.rs`, and so +/// there's little point in exposing it to the OSS version. +pub fn is_user_error(err: &anyhow::Error) -> bool { + if let Some(MarkdownParserError::MissingLanguageSpecifier { .. }) = + err.downcast_ref::() + { + return true; + } + false +} + +pub fn main_impl(_fb: FacebookInit, args: Args) -> Result { + if let Err(err) = args.commands.run() { + if err.downcast_ref::().is_some() { + return Ok(ExitCode::from(50)); + } + if is_user_error(&err) { + error!("{:?}", err); + Ok(ExitCode::from(1)) + } else { + Err(anyhow!(err)) + } + } else { + Ok(ExitCode::SUCCESS) + } +} diff --git a/src/bin/fb_main_nodeps.rs b/src/bin/fb_main_nodeps.rs new file mode 100644 index 0000000..823ea07 --- /dev/null +++ b/src/bin/fb_main_nodeps.rs @@ -0,0 +1,23 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +extern crate scrut; + +mod commands; +mod fb_main_common; +mod utils; + +use anyhow::Result; +use cli::ExitCode; +use fb_main_common::main_impl; +use fb_main_common::Args; +use fbinit::FacebookInit; + +#[cli::main("scrut", usage_logging = false)] +pub fn main(fb: FacebookInit, args: Args) -> Result { + main_impl(fb, args) +} diff --git a/src/bin/utils/file_parser.rs b/src/bin/utils/file_parser.rs index ac8b220..e070d35 100644 --- a/src/bin/utils/file_parser.rs +++ b/src/bin/utils/file_parser.rs @@ -72,7 +72,7 @@ impl<'a> FileParser<'a> { let (parser_type, parser) = self.parser(&test_file_path, cram_compat)?; let (config, testcases) = parser.parse(&test_file_content).with_context(|| { format!( - "parse {} from {:?} with {} parser", + "Failed to parse {} from {:?} with {} parser", name, &test_file_path, parser_type ) })?; diff --git a/src/parsers/markdown.rs b/src/parsers/markdown.rs index 376dd1c..185c30c 100644 --- a/src/parsers/markdown.rs +++ b/src/parsers/markdown.rs @@ -30,6 +30,14 @@ lazy_static! { pub const DEFAULT_MARKDOWN_LANGUAGES: &[&str] = &["scrut"]; +#[derive(Debug, thiserror::Error)] +pub enum MarkdownParserError { + #[error( + "Code block starting at line {line} is missing language specifier. Use ```scrut to make this block a Scrut test, or any other language to make Scrut skip this block." + )] + MissingLanguageSpecifier { line: usize }, +} + /// A parser for Cram `.t` files, which reads [`crate::testcase::TestCase`]s /// that are encoded in the form: /// @@ -102,10 +110,9 @@ impl Parser for MarkdownParser { lines: _, } => { if language.is_empty() { - anyhow::bail!( - "Code block starting at line {} is missing language specifier. Use ```scrut to make this block a Scrut test, or any other language to make Scrut skip this block.", - starting_line_number - ); + anyhow::bail!(MarkdownParserError::MissingLanguageSpecifier { + line: starting_line_number, + }); } } MarkdownToken::TestCodeBlock {