Add assert expression (#1845)

This commit is contained in:
Elizaveta Demina 2024-05-15 04:55:32 +03:00 committed by GitHub
parent e11684008e
commit 9aea3e679b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 258 additions and 103 deletions

View File

@ -83,6 +83,7 @@ module : 'mod' '?'? NAME string?
boolean : ':=' ('true' | 'false') boolean : ':=' ('true' | 'false')
expression : 'if' condition '{' expression '}' 'else' '{' expression '}' expression : 'if' condition '{' expression '}' 'else' '{' expression '}'
| 'assert' '(' condition ',' expression ')'
| value '/' expression | value '/' expression
| value '+' expression | value '+' expression
| value | value

View File

@ -54,25 +54,17 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src> { fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src> {
match expression { match expression {
Expression::Variable { name } => { Expression::Assert {
let variable = name.lexeme(); condition: Condition {
if self.evaluated.contains(variable) { lhs,
Ok(()) rhs,
} else if self.stack.contains(&variable) { operator: _,
self.stack.push(variable); },
Err( error,
self.assignments[variable] } => {
.name self.resolve_expression(lhs)?;
.error(CircularVariableDependency { self.resolve_expression(rhs)?;
variable, self.resolve_expression(error)
circle: self.stack.clone(),
}),
)
} else if self.assignments.contains_key(variable) {
self.resolve_assignment(variable)
} else {
Err(name.token.error(UndefinedVariable { variable }))
}
} }
Expression::Call { thunk } => match thunk { Expression::Call { thunk } => match thunk {
Thunk::Nullary { .. } => Ok(()), Thunk::Nullary { .. } => Ok(()),
@ -111,15 +103,12 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
self.resolve_expression(lhs)?; self.resolve_expression(lhs)?;
self.resolve_expression(rhs) self.resolve_expression(rhs)
} }
Expression::Join { lhs, rhs } => {
if let Some(lhs) = lhs {
self.resolve_expression(lhs)?;
}
self.resolve_expression(rhs)
}
Expression::Conditional { Expression::Conditional {
condition: Condition {
lhs, lhs,
rhs, rhs,
operator: _,
},
then, then,
otherwise, otherwise,
.. ..
@ -129,8 +118,34 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
self.resolve_expression(then)?; self.resolve_expression(then)?;
self.resolve_expression(otherwise) self.resolve_expression(otherwise)
} }
Expression::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()),
Expression::Group { contents } => self.resolve_expression(contents), Expression::Group { contents } => self.resolve_expression(contents),
Expression::Join { lhs, rhs } => {
if let Some(lhs) = lhs {
self.resolve_expression(lhs)?;
}
self.resolve_expression(rhs)
}
Expression::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()),
Expression::Variable { name } => {
let variable = name.lexeme();
if self.evaluated.contains(variable) {
Ok(())
} else if self.stack.contains(&variable) {
self.stack.push(variable);
Err(
self.assignments[variable]
.name
.error(CircularVariableDependency {
variable,
circle: self.stack.clone(),
}),
)
} else if self.assignments.contains_key(variable) {
self.resolve_assignment(variable)
} else {
Err(name.token.error(UndefinedVariable { variable }))
}
}
} }
} }
} }

27
src/condition.rs Normal file
View File

@ -0,0 +1,27 @@
use super::*;
#[derive(PartialEq, Debug, Clone)]
pub(crate) struct Condition<'src> {
pub(crate) lhs: Box<Expression<'src>>,
pub(crate) rhs: Box<Expression<'src>>,
pub(crate) operator: ConditionalOperator,
}
impl<'src> Display for Condition<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
write!(f, "{} {} {}", self.lhs, self.operator, self.rhs)
}
}
impl<'src> Serialize for Condition<'src> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(None)?;
seq.serialize_element(&self.operator.to_string())?;
seq.serialize_element(&self.lhs)?;
seq.serialize_element(&self.rhs)?;
seq.end()
}
}

View File

@ -13,6 +13,9 @@ pub(crate) enum Error<'src> {
min: usize, min: usize,
max: usize, max: usize,
}, },
Assert {
message: String,
},
Backtick { Backtick {
token: Token<'src>, token: Token<'src>,
output_error: OutputError, output_error: OutputError,
@ -256,6 +259,9 @@ impl<'src> ColorDisplay for Error<'src> {
write!(f, "Recipe `{recipe}` got {found} {count} but takes at most {max}")?; write!(f, "Recipe `{recipe}` got {found} {count} but takes at most {max}")?;
} }
} }
Assert { message }=> {
write!(f, "Assert failed: {message}")?;
}
Backtick { output_error, .. } => match output_error { Backtick { output_error, .. } => match output_error {
OutputError::Code(code) => write!(f, "Backtick failed with exit code {code}")?, OutputError::Code(code) => write!(f, "Backtick failed with exit code {code}")?,
OutputError::Signal(signal) => write!(f, "Backtick was terminated by signal {signal}")?, OutputError::Signal(signal) => write!(f, "Backtick was terminated by signal {signal}")?,

View File

@ -171,22 +171,11 @@ impl<'src, 'run> Evaluator<'src, 'run> {
Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(rhs)?) Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(rhs)?)
} }
Expression::Conditional { Expression::Conditional {
lhs, condition,
rhs,
then, then,
otherwise, otherwise,
operator,
} => { } => {
let lhs_value = self.evaluate_expression(lhs)?; if self.evaluate_condition(condition)? {
let rhs_value = self.evaluate_expression(rhs)?;
let condition = match operator {
ConditionalOperator::Equality => lhs_value == rhs_value,
ConditionalOperator::Inequality => lhs_value != rhs_value,
ConditionalOperator::RegexMatch => Regex::new(&rhs_value)
.map_err(|source| Error::RegexCompile { source })?
.is_match(&lhs_value),
};
if condition {
self.evaluate_expression(then) self.evaluate_expression(then)
} else { } else {
self.evaluate_expression(otherwise) self.evaluate_expression(otherwise)
@ -198,8 +187,30 @@ impl<'src, 'run> Evaluator<'src, 'run> {
lhs: Some(lhs), lhs: Some(lhs),
rhs, rhs,
} => Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?), } => Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?),
Expression::Assert { condition, error } => {
if self.evaluate_condition(condition)? {
Ok(String::new())
} else {
Err(Error::Assert {
message: self.evaluate_expression(error)?,
})
} }
} }
}
}
fn evaluate_condition(&mut self, condition: &Condition<'src>) -> RunResult<'src, bool> {
let lhs_value = self.evaluate_expression(&condition.lhs)?;
let rhs_value = self.evaluate_expression(&condition.rhs)?;
let condition = match condition.operator {
ConditionalOperator::Equality => lhs_value == rhs_value,
ConditionalOperator::Inequality => lhs_value != rhs_value,
ConditionalOperator::RegexMatch => Regex::new(&rhs_value)
.map_err(|source| Error::RegexCompile { source })?
.is_match(&lhs_value),
};
Ok(condition)
}
fn run_backtick(&self, raw: &str, token: &Token<'src>) -> RunResult<'src, String> { fn run_backtick(&self, raw: &str, token: &Token<'src>) -> RunResult<'src, String> {
let mut cmd = self.settings.shell_command(self.config); let mut cmd = self.settings.shell_command(self.config);

View File

@ -8,6 +8,11 @@ use super::*;
/// The parser parses both values and expressions into `Expression`s. /// The parser parses both values and expressions into `Expression`s.
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub(crate) enum Expression<'src> { pub(crate) enum Expression<'src> {
/// `assert(condition, error)`
Assert {
condition: Condition<'src>,
error: Box<Expression<'src>>,
},
/// `contents` /// `contents`
Backtick { Backtick {
contents: String, contents: String,
@ -20,13 +25,11 @@ pub(crate) enum Expression<'src> {
lhs: Box<Expression<'src>>, lhs: Box<Expression<'src>>,
rhs: Box<Expression<'src>>, rhs: Box<Expression<'src>>,
}, },
/// `if lhs == rhs { then } else { otherwise }` /// `if condition { then } else { otherwise }`
Conditional { Conditional {
lhs: Box<Expression<'src>>, condition: Condition<'src>,
rhs: Box<Expression<'src>>,
then: Box<Expression<'src>>, then: Box<Expression<'src>>,
otherwise: Box<Expression<'src>>, otherwise: Box<Expression<'src>>,
operator: ConditionalOperator,
}, },
/// `(contents)` /// `(contents)`
Group { contents: Box<Expression<'src>> }, Group { contents: Box<Expression<'src>> },
@ -50,6 +53,7 @@ impl<'src> Expression<'src> {
impl<'src> Display for Expression<'src> { impl<'src> Display for Expression<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
match self { match self {
Expression::Assert { condition, error } => write!(f, "assert({condition}, {error})"),
Expression::Backtick { token, .. } => write!(f, "{}", token.lexeme()), Expression::Backtick { token, .. } => write!(f, "{}", token.lexeme()),
Expression::Join { lhs: None, rhs } => write!(f, "/ {rhs}"), Expression::Join { lhs: None, rhs } => write!(f, "/ {rhs}"),
Expression::Join { Expression::Join {
@ -58,15 +62,10 @@ impl<'src> Display for Expression<'src> {
} => write!(f, "{lhs} / {rhs}"), } => write!(f, "{lhs} / {rhs}"),
Expression::Concatenation { lhs, rhs } => write!(f, "{lhs} + {rhs}"), Expression::Concatenation { lhs, rhs } => write!(f, "{lhs} + {rhs}"),
Expression::Conditional { Expression::Conditional {
lhs, condition,
rhs,
then, then,
otherwise, otherwise,
operator, } => write!(f, "if {condition} {{ {then} }} else {{ {otherwise} }}"),
} => write!(
f,
"if {lhs} {operator} {rhs} {{ {then} }} else {{ {otherwise} }}"
),
Expression::StringLiteral { string_literal } => write!(f, "{string_literal}"), Expression::StringLiteral { string_literal } => write!(f, "{string_literal}"),
Expression::Variable { name } => write!(f, "{}", name.lexeme()), Expression::Variable { name } => write!(f, "{}", name.lexeme()),
Expression::Call { thunk } => write!(f, "{thunk}"), Expression::Call { thunk } => write!(f, "{thunk}"),
@ -81,6 +80,13 @@ impl<'src> Serialize for Expression<'src> {
S: Serializer, S: Serializer,
{ {
match self { match self {
Self::Assert { condition, error } => {
let mut seq: <S as Serializer>::SerializeSeq = serializer.serialize_seq(None)?;
seq.serialize_element("assert")?;
seq.serialize_element(condition)?;
seq.serialize_element(error)?;
seq.end()
}
Self::Backtick { contents, .. } => { Self::Backtick { contents, .. } => {
let mut seq = serializer.serialize_seq(None)?; let mut seq = serializer.serialize_seq(None)?;
seq.serialize_element("evaluate")?; seq.serialize_element("evaluate")?;
@ -103,17 +109,13 @@ impl<'src> Serialize for Expression<'src> {
seq.end() seq.end()
} }
Self::Conditional { Self::Conditional {
lhs, condition,
rhs,
then, then,
otherwise, otherwise,
operator,
} => { } => {
let mut seq = serializer.serialize_seq(None)?; let mut seq = serializer.serialize_seq(None)?;
seq.serialize_element("if")?; seq.serialize_element("if")?;
seq.serialize_element(&operator.to_string())?; seq.serialize_element(condition)?;
seq.serialize_element(lhs)?;
seq.serialize_element(rhs)?;
seq.serialize_element(then)?; seq.serialize_element(then)?;
seq.serialize_element(otherwise)?; seq.serialize_element(otherwise)?;
seq.end() seq.end()

View File

@ -6,6 +6,7 @@ pub(crate) enum Keyword {
Alias, Alias,
AllowDuplicateRecipes, AllowDuplicateRecipes,
AllowDuplicateVariables, AllowDuplicateVariables,
Assert,
DotenvFilename, DotenvFilename,
DotenvLoad, DotenvLoad,
DotenvPath, DotenvPath,

View File

@ -19,15 +19,16 @@ pub(crate) use {
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding, assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding,
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation, color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation,
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler, compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError, condition::Condition, conditional_operator::ConditionalOperator, config::Config,
count::Count, delimiter::Delimiter, dependency::Dependency, dump_format::DumpFormat, config_error::ConfigError, count::Count, delimiter::Delimiter, dependency::Dependency,
enclosure::Enclosure, error::Error, evaluator::Evaluator, expression::Expression, dump_format::DumpFormat, enclosure::Enclosure, error::Error, evaluator::Evaluator,
fragment::Fragment, function::Function, function_context::FunctionContext, expression::Expression, fragment::Fragment, function::Function,
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item, function_context::FunctionContext, interrupt_guard::InterruptGuard,
justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List, interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, keyed::Keyed,
load_dotenv::load_dotenv, loader::Loader, name::Name, namepath::Namepath, ordinal::Ordinal, keyword::Keyword, lexer::Lexer, line::Line, list::List, load_dotenv::load_dotenv,
output::output, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind, loader::Loader, name::Name, namepath::Namepath, ordinal::Ordinal, output::output,
parser::Parser, platform::Platform, platform_interface::PlatformInterface, position::Position, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind, parser::Parser,
platform::Platform, platform_interface::PlatformInterface, position::Position,
positional::Positional, ran::Ran, range_ext::RangeExt, recipe::Recipe, positional::Positional, ran::Ran, range_ext::RangeExt, recipe::Recipe,
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, scope::Scope, search::Search, recipe_context::RecipeContext, recipe_resolver::RecipeResolver, scope::Scope, search::Search,
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting, search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
@ -124,6 +125,7 @@ mod compile_error;
mod compile_error_kind; mod compile_error_kind;
mod compiler; mod compiler;
mod completions; mod completions;
mod condition;
mod conditional_operator; mod conditional_operator;
mod config; mod config;
mod config_error; mod config_error;

View File

@ -83,13 +83,19 @@ impl<'src> Node<'src> for Assignment<'src> {
impl<'src> Node<'src> for Expression<'src> { impl<'src> Node<'src> for Expression<'src> {
fn tree(&self) -> Tree<'src> { fn tree(&self) -> Tree<'src> {
match self { match self {
Expression::Assert {
condition: Condition { lhs, rhs, operator },
error,
} => Tree::atom(Keyword::Assert.lexeme())
.push(lhs.tree())
.push(operator.to_string())
.push(rhs.tree())
.push(error.tree()),
Expression::Concatenation { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()), Expression::Concatenation { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()),
Expression::Conditional { Expression::Conditional {
lhs, condition: Condition { lhs, rhs, operator },
rhs,
then, then,
otherwise, otherwise,
operator,
} => { } => {
let mut tree = Tree::atom(Keyword::If.lexeme()); let mut tree = Tree::atom(Keyword::If.lexeme());
tree.push_mut(lhs.tree()); tree.push_mut(lhs.tree());

View File

@ -504,18 +504,7 @@ impl<'run, 'src> Parser<'run, 'src> {
/// Parse a conditional, e.g. `if a == b { "foo" } else { "bar" }` /// Parse a conditional, e.g. `if a == b { "foo" } else { "bar" }`
fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> { fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> {
let lhs = self.parse_expression()?; let condition = self.parse_condition()?;
let operator = if self.accepted(BangEquals)? {
ConditionalOperator::Inequality
} else if self.accepted(EqualsTilde)? {
ConditionalOperator::RegexMatch
} else {
self.expect(EqualsEquals)?;
ConditionalOperator::Equality
};
let rhs = self.parse_expression()?;
self.expect(BraceL)?; self.expect(BraceL)?;
@ -535,10 +524,26 @@ impl<'run, 'src> Parser<'run, 'src> {
}; };
Ok(Expression::Conditional { Ok(Expression::Conditional {
lhs: Box::new(lhs), condition,
rhs: Box::new(rhs),
then: Box::new(then), then: Box::new(then),
otherwise: Box::new(otherwise), otherwise: Box::new(otherwise),
})
}
fn parse_condition(&mut self) -> CompileResult<'src, Condition<'src>> {
let lhs = self.parse_expression()?;
let operator = if self.accepted(BangEquals)? {
ConditionalOperator::Inequality
} else if self.accepted(EqualsTilde)? {
ConditionalOperator::RegexMatch
} else {
self.expect(EqualsEquals)?;
ConditionalOperator::Equality
};
let rhs = self.parse_expression()?;
Ok(Condition {
lhs: Box::new(lhs),
rhs: Box::new(rhs),
operator, operator,
}) })
} }
@ -564,9 +569,16 @@ impl<'run, 'src> Parser<'run, 'src> {
if contents.starts_with("#!") { if contents.starts_with("#!") {
return Err(next.error(CompileErrorKind::BacktickShebang)); return Err(next.error(CompileErrorKind::BacktickShebang));
} }
Ok(Expression::Backtick { contents, token }) Ok(Expression::Backtick { contents, token })
} else if self.next_is(Identifier) { } else if self.next_is(Identifier) {
if self.accepted_keyword(Keyword::Assert)? {
self.expect(ParenL)?;
let condition = self.parse_condition()?;
self.expect(Comma)?;
let error = Box::new(self.parse_expression()?);
self.expect(ParenR)?;
Ok(Expression::Assert { condition, error })
} else {
let name = self.parse_name()?; let name = self.parse_name()?;
if self.next_is(ParenL) { if self.next_is(ParenL) {
@ -577,6 +589,7 @@ impl<'run, 'src> Parser<'run, 'src> {
} else { } else {
Ok(Expression::Variable { name }) Ok(Expression::Variable { name })
} }
}
} else if self.next_is(ParenL) { } else if self.next_is(ParenL) {
self.presume(ParenL)?; self.presume(ParenL)?;
let contents = Box::new(self.parse_expression()?); let contents = Box::new(self.parse_expression()?);
@ -2103,6 +2116,18 @@ mod tests {
tree: (justfile (mod ? foo "some/file/path.txt")), tree: (justfile (mod ? foo "some/file/path.txt")),
} }
test! {
name: assert,
text: "a := assert(foo == \"bar\", \"error\")",
tree: (justfile (assignment a (assert foo == "bar" "error"))),
}
test! {
name: assert_conditional_condition,
text: "foo := assert(if a != b { c } else { d } == \"abc\", \"error\")",
tree: (justfile (assignment foo (assert (if a != b c d) == "abc" "error"))),
}
error! { error! {
name: alias_syntax_multiple_rhs, name: alias_syntax_multiple_rhs,
input: "alias foo := bar baz", input: "alias foo := bar baz",

View File

@ -19,9 +19,9 @@ use {
mod full { mod full {
pub(crate) use crate::{ pub(crate) use crate::{
assignment::Assignment, conditional_operator::ConditionalOperator, dependency::Dependency, assignment::Assignment, condition::Condition, conditional_operator::ConditionalOperator,
expression::Expression, fragment::Fragment, justfile::Justfile, line::Line, dependency::Dependency, expression::Expression, fragment::Fragment, justfile::Justfile,
parameter::Parameter, parameter_kind::ParameterKind, recipe::Recipe, thunk::Thunk, line::Line, parameter::Parameter, parameter_kind::ParameterKind, recipe::Recipe, thunk::Thunk,
}; };
} }
@ -183,6 +183,10 @@ impl Assignment {
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
pub enum Expression { pub enum Expression {
Assert {
condition: Condition,
error: Box<Expression>,
},
Backtick { Backtick {
command: String, command: String,
}, },
@ -217,6 +221,17 @@ impl Expression {
fn new(expression: &full::Expression) -> Expression { fn new(expression: &full::Expression) -> Expression {
use full::Expression::*; use full::Expression::*;
match expression { match expression {
Assert {
condition: full::Condition { lhs, rhs, operator },
error,
} => Expression::Assert {
condition: Condition {
lhs: Box::new(Expression::new(lhs)),
rhs: Box::new(Expression::new(rhs)),
operator: ConditionalOperator::new(*operator),
},
error: Box::new(Expression::new(error)),
},
Backtick { contents, .. } => Expression::Backtick { Backtick { contents, .. } => Expression::Backtick {
command: (*contents).clone(), command: (*contents).clone(),
}, },
@ -284,10 +299,8 @@ impl Expression {
rhs: Box::new(Expression::new(rhs)), rhs: Box::new(Expression::new(rhs)),
}, },
Conditional { Conditional {
lhs, condition: full::Condition { lhs, rhs, operator },
operator,
otherwise, otherwise,
rhs,
then, then,
} => Expression::Conditional { } => Expression::Conditional {
lhs: Box::new(Expression::new(lhs)), lhs: Box::new(Expression::new(lhs)),
@ -307,6 +320,13 @@ impl Expression {
} }
} }
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
pub struct Condition {
lhs: Box<Expression>,
rhs: Box<Expression>,
operator: ConditionalOperator,
}
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
pub enum ConditionalOperator { pub enum ConditionalOperator {
Equality, Equality,

View File

@ -49,11 +49,14 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
} }
}, },
Expression::Conditional { Expression::Conditional {
condition:
Condition {
lhs, lhs,
rhs, rhs,
operator: _,
},
then, then,
otherwise, otherwise,
..
} => { } => {
self.stack.push(otherwise); self.stack.push(otherwise);
self.stack.push(then); self.stack.push(then);
@ -74,6 +77,19 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
Expression::Group { contents } => { Expression::Group { contents } => {
self.stack.push(contents); self.stack.push(contents);
} }
Expression::Assert {
condition:
Condition {
lhs,
rhs,
operator: _,
},
error,
} => {
self.stack.push(error);
self.stack.push(rhs);
self.stack.push(lhs);
}
} }
} }
} }

22
tests/assertions.rs Normal file
View File

@ -0,0 +1,22 @@
use super::*;
test! {
name: assert_pass,
justfile: "
foo:
{{ assert('a' == 'a', 'error message') }}
",
stdout: "",
stderr: "",
}
test! {
name: assert_fail,
justfile: "
foo:
{{ assert('a' != 'a', 'error message') }}
",
stdout: "",
stderr: "error: Assert failed: error message\n",
status: EXIT_FAILURE,
}

View File

@ -262,7 +262,7 @@ fn dependency_argument() {
["concatenate", "a", "b"], ["concatenate", "a", "b"],
["evaluate", "echo"], ["evaluate", "echo"],
["variable", "x"], ["variable", "x"],
["if", "==", "a", "b", "c", "d"], ["if", ["==", "a", "b"], "c", "d"],
["call", "arch"], ["call", "arch"],
["call", "env_var", "foo"], ["call", "env_var", "foo"],
["call", "join", "a", "b"], ["call", "join", "a", "b"],

View File

@ -36,6 +36,7 @@ mod allow_duplicate_recipes;
mod allow_duplicate_variables; mod allow_duplicate_variables;
mod assert_stdout; mod assert_stdout;
mod assert_success; mod assert_success;
mod assertions;
mod attributes; mod attributes;
mod backticks; mod backticks;
mod byte_order_mark; mod byte_order_mark;