Allow arbitrary expressions as default arguments (#400)

This commit is contained in:
Casey Rodarmor 2019-04-11 23:58:08 -07:00 committed by GitHub
parent 12f9428695
commit fe0a6c252c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 505 additions and 170 deletions

View File

@ -63,6 +63,7 @@ value : NAME '(' arguments? ')'
| RAW_STRING
| BACKTICK
| NAME
| '(' expression ')'
arguments : expression ',' arguments
| expression ','?
@ -70,8 +71,7 @@ arguments : expression ',' arguments
recipe : '@'? NAME parameter* ('+' parameter)? ':' dependencies? body?
parameter : NAME
| NAME '=' STRING
| NAME '=' RAW_STRING
| NAME '=' value
dependencies : NAME+

View File

@ -480,7 +480,9 @@ cd my-awesome-project && make
Parameters may have default values:
```make
test target tests='all':
default = 'all'
test target tests=default:
@echo 'Testing {{target}}:{{tests}}...'
./test --tests {{tests}} {{target}}
```
@ -501,6 +503,15 @@ Testing server:unit...
./test --tests unit server
```
Default values may be arbitrary expressions, but concatenations must be parenthesized:
```make
arch = "wasm"
test triple=(arch + "-unknown-unknown"):
./test {{triple}}
```
The last parameter of a recipe may be variadic, indicated with a `+` before the argument name:
```make

View File

@ -1,5 +1,6 @@
use crate::common::*;
#[derive(Debug)]
pub struct Alias<'a> {
pub name: &'a str,
pub target: &'a str,

View File

@ -83,7 +83,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
Ok(())
}
fn evaluate_expression(
pub fn evaluate_expression(
&mut self,
expression: &Expression<'a>,
arguments: &BTreeMap<&str, Cow<str>>,
@ -120,7 +120,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
};
evaluate_function(token, name, &context, &call_arguments)
}
Expression::String { ref cooked_string } => Ok(cooked_string.cooked.clone()),
Expression::String { ref cooked_string } => Ok(cooked_string.cooked.to_string()),
Expression::Backtick { raw, ref token } => {
if self.dry_run {
Ok(format!("`{}`", raw))
@ -131,6 +131,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
Expression::Concatination { ref lhs, ref rhs } => {
Ok(self.evaluate_expression(lhs, arguments)? + &self.evaluate_expression(rhs, arguments)?)
}
Expression::Group { ref expression } => self.evaluate_expression(&expression, arguments),
}
}

View File

@ -56,7 +56,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
}
fn resolve_expression(&mut self, expression: &Expression<'a>) -> CompilationResult<'a, ()> {
match *expression {
match expression {
Expression::Variable { name, ref token } => {
if self.evaluated.contains(name) {
return Ok(());
@ -83,6 +83,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
self.resolve_expression(rhs)?;
}
Expression::String { .. } | Expression::Backtick { .. } => {}
Expression::Group { expression } => self.resolve_expression(expression)?,
}
Ok(())
}

View File

@ -3,7 +3,7 @@ use crate::common::*;
#[derive(PartialEq, Debug)]
pub struct CookedString<'a> {
pub raw: &'a str,
pub cooked: String,
pub cooked: Cow<'a, str>,
}
impl<'a> CookedString<'a> {
@ -12,7 +12,7 @@ impl<'a> CookedString<'a> {
if let TokenKind::RawString = token.kind {
Ok(CookedString {
cooked: raw.to_string(),
cooked: Cow::Borrowed(raw),
raw,
})
} else if let TokenKind::StringToken = token.kind {
@ -41,7 +41,10 @@ impl<'a> CookedString<'a> {
}
cooked.push(c);
}
Ok(CookedString { raw, cooked })
Ok(CookedString {
raw,
cooked: Cow::Owned(cooked),
})
} else {
Err(token.error(CompilationErrorKind::Internal {
message: "cook_string() called on non-string token".to_string(),
@ -49,3 +52,12 @@ impl<'a> CookedString<'a> {
}
}
}
impl<'a> Display for CookedString<'a> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self.cooked {
Cow::Borrowed(raw) => write!(f, "'{}'", raw),
Cow::Owned(_) => write!(f, "\"{}\"", self.raw),
}
}
}

View File

@ -22,6 +22,9 @@ pub enum Expression<'a> {
name: &'a str,
token: Token<'a>,
},
Group {
expression: Box<Expression<'a>>,
},
}
impl<'a> Expression<'a> {
@ -39,7 +42,7 @@ impl<'a> Display for Expression<'a> {
match *self {
Expression::Backtick { raw, .. } => write!(f, "`{}`", raw)?,
Expression::Concatination { ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?,
Expression::String { ref cooked_string } => write!(f, "\"{}\"", cooked_string.raw)?,
Expression::String { ref cooked_string } => write!(f, "{}", cooked_string)?,
Expression::Variable { name, .. } => write!(f, "{}", name)?,
Expression::Call {
name,
@ -56,6 +59,7 @@ impl<'a> Display for Expression<'a> {
}
write!(f, ")")?;
}
Expression::Group { ref expression } => write!(f, "({})", expression)?,
}
Ok(())
}
@ -71,15 +75,19 @@ impl<'a> Iterator for Variables<'a> {
fn next(&mut self) -> Option<&'a Token<'a>> {
match self.stack.pop() {
None
| Some(&Expression::String { .. })
| Some(&Expression::Backtick { .. })
| Some(&Expression::Call { .. }) => None,
Some(&Expression::Variable { ref token, .. }) => Some(token),
Some(&Expression::Concatination { ref lhs, ref rhs }) => {
| Some(Expression::String { .. })
| Some(Expression::Backtick { .. })
| Some(Expression::Call { .. }) => None,
Some(Expression::Variable { token, .. }) => Some(token),
Some(Expression::Concatination { lhs, rhs }) => {
self.stack.push(lhs);
self.stack.push(rhs);
self.next()
}
Some(Expression::Group { expression }) => {
self.stack.push(expression);
self.next()
}
}
}
}
@ -94,19 +102,21 @@ impl<'a> Iterator for Functions<'a> {
fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop() {
None
| Some(&Expression::String { .. })
| Some(&Expression::Backtick { .. })
| Some(&Expression::Variable { .. }) => None,
Some(&Expression::Call {
ref token,
ref arguments,
..
| Some(Expression::String { .. })
| Some(Expression::Backtick { .. })
| Some(Expression::Variable { .. }) => None,
Some(Expression::Call {
token, arguments, ..
}) => Some((token, arguments.len())),
Some(&Expression::Concatination { ref lhs, ref rhs }) => {
Some(Expression::Concatination { lhs, rhs }) => {
self.stack.push(lhs);
self.stack.push(rhs);
self.next()
}
Some(Expression::Group { expression }) => {
self.stack.push(expression);
self.next()
}
}
}
}

View File

@ -1,5 +1,6 @@
use crate::common::*;
#[derive(Debug)]
pub struct Justfile<'a> {
pub recipes: BTreeMap<&'a str, Recipe<'a>>,
pub assignments: BTreeMap<&'a str, Expression<'a>>,

View File

@ -617,6 +617,12 @@ c: b
"#$#$.",
}
summary_test! {
multiple_recipes,
"a:\n foo\nb:",
"N:$>^_$<N:.",
}
error_test! {
name: tokenize_space_then_tab,
input: "a:

View File

@ -2,7 +2,7 @@ use crate::common::*;
#[derive(PartialEq, Debug)]
pub struct Parameter<'a> {
pub default: Option<String>,
pub default: Option<Expression<'a>>,
pub name: &'a str,
pub token: Token<'a>,
pub variadic: bool,
@ -16,11 +16,7 @@ impl<'a> Display for Parameter<'a> {
}
write!(f, "{}", color.parameter().paint(self.name))?;
if let Some(ref default) = self.default {
let escaped = default
.chars()
.flat_map(char::escape_default)
.collect::<String>();;
write!(f, r#"='{}'"#, color.string().paint(&escaped))?;
write!(f, "={}", color.string().paint(&default.to_string()))?;
}
Ok(())
}

View File

@ -49,15 +49,6 @@ impl<'a> Parser<'a> {
}
}
fn accept_any(&mut self, kinds: &[TokenKind]) -> Option<Token<'a>> {
for kind in kinds {
if self.peek(*kind) {
return self.tokens.next();
}
}
None
}
fn accepted(&mut self, kind: TokenKind) -> bool {
self.accept(kind).is_some()
}
@ -137,12 +128,7 @@ impl<'a> Parser<'a> {
let default;
if self.accepted(Equals) {
if let Some(string) = self.accept_any(&[StringToken, RawString]) {
default = Some(CookedString::new(&string)?.cooked);
} else {
let unexpected = self.tokens.next().unwrap();
return Err(self.unexpected_token(&unexpected, &[StringToken, RawString]));
}
default = Some(self.value()?);
} else {
default = None
}
@ -243,6 +229,10 @@ impl<'a> Parser<'a> {
}
}
while lines.last().map(Vec::is_empty).unwrap_or(false) {
lines.pop();
}
self.recipes.insert(
name.lexeme,
Recipe {
@ -262,9 +252,10 @@ impl<'a> Parser<'a> {
Ok(())
}
fn expression(&mut self) -> CompilationResult<'a, Expression<'a>> {
fn value(&mut self) -> CompilationResult<'a, Expression<'a>> {
let first = self.tokens.next().unwrap();
let lhs = match first.kind {
match first.kind {
Name => {
if self.peek(ParenL) {
if let Some(token) = self.expect(ParenL) {
@ -274,30 +265,46 @@ impl<'a> Parser<'a> {
if let Some(token) = self.expect(ParenR) {
return Err(self.unexpected_token(&token, &[Name, StringToken, ParenR]));
}
Expression::Call {
Ok(Expression::Call {
name: first.lexeme,
token: first,
arguments,
}
})
} else {
Expression::Variable {
Ok(Expression::Variable {
name: first.lexeme,
token: first,
})
}
}
}
Backtick => Expression::Backtick {
Backtick => Ok(Expression::Backtick {
raw: &first.lexeme[1..first.lexeme.len() - 1],
token: first,
},
RawString | StringToken => Expression::String {
}),
RawString | StringToken => Ok(Expression::String {
cooked_string: CookedString::new(&first)?,
},
_ => return Err(self.unexpected_token(&first, &[Name, StringToken])),
};
}),
ParenL => {
let expression = self.expression()?;
if let Some(token) = self.expect(ParenR) {
return Err(self.unexpected_token(&token, &[ParenR]));
}
Ok(Expression::Group {
expression: Box::new(expression),
})
}
_ => Err(self.unexpected_token(&first, &[Name, StringToken])),
}
}
fn expression(&mut self) -> CompilationResult<'a, Expression<'a>> {
let lhs = self.value()?;
if self.accepted(Plus) {
let rhs = self.expression()?;
Ok(Expression::Concatination {
lhs: Box::new(lhs),
rhs: Box::new(rhs),
@ -463,6 +470,8 @@ impl<'a> Parser<'a> {
}));
}
AssignmentResolver::resolve_assignments(&self.assignments, &self.assignment_tokens)?;
RecipeResolver::resolve_recipes(&self.recipes, &self.assignments, self.text)?;
for recipe in self.recipes.values() {
@ -486,8 +495,6 @@ impl<'a> Parser<'a> {
AliasResolver::resolve_aliases(&self.aliases, &self.recipes, &self.alias_tokens)?;
AssignmentResolver::resolve_assignments(&self.assignments, &self.assignment_tokens)?;
Ok(Justfile {
recipes: self.recipes,
assignments: self.assignments,
@ -513,9 +520,17 @@ mod test {
let actual = format!("{:#}", justfile);
if actual != expected {
println!("got:\n\"{}\"\n", actual);
println!("\texpected:\n\"{}\"", expected);
println!("expected:\n\"{}\"", expected);
assert_eq!(actual, expected);
}
println!("Re-parsing...");
let reparsed = parse_success(&actual);
let redumped = format!("{:#}", reparsed);
if redumped != actual {
println!("reparsed:\n\"{}\"\n", redumped);
println!("expected:\n\"{}\"", actual);
assert_eq!(redumped, actual);
}
}
};
}
@ -539,7 +554,18 @@ foo a="b\t":
"#,
r#"foo a='b\t':"#,
r#"foo a="b\t":"#,
}
summary_test! {
parse_multiple,
r#"
a:
b:
"#,
r#"a:
b:"#,
}
summary_test! {
@ -561,7 +587,7 @@ foo +a="Hello":
"#,
r#"foo +a='Hello':"#,
r#"foo +a="Hello":"#,
}
summary_test! {
@ -572,7 +598,7 @@ foo a='b\t':
"#,
r#"foo a='b\\t':"#,
r#"foo a='b\t':"#,
}
summary_test! {
@ -671,7 +697,7 @@ install:
\t\treturn
\tfi
",
"practicum = \"hello\"
"practicum = 'hello'
install:
#!/bin/sh
@ -765,10 +791,76 @@ x = env_var('foo',)
a:
{{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#,
r#"x = env_var("foo")
r#"x = env_var('foo')
a:
{{env_var_or_default("foo" + "bar", "baz")}} {{env_var(env_var("baz"))}}"#,
{{env_var_or_default('foo' + 'bar', 'baz')}} {{env_var(env_var("baz"))}}"#,
}
summary_test! {
parameter_default_string,
r#"
f x="abc":
"#,
r#"f x="abc":"#,
}
summary_test! {
parameter_default_raw_string,
r#"
f x='abc':
"#,
r#"f x='abc':"#,
}
summary_test! {
parameter_default_backtick,
r#"
f x=`echo hello`:
"#,
r#"f x=`echo hello`:"#,
}
summary_test! {
parameter_default_concatination_string,
r#"
f x=(`echo hello` + "foo"):
"#,
r#"f x=(`echo hello` + "foo"):"#,
}
summary_test! {
parameter_default_concatination_variable,
r#"
x = "10"
f y=(`echo hello` + x) +z="foo":
"#,
r#"x = "10"
f y=(`echo hello` + x) +z="foo":"#,
}
summary_test! {
parameter_default_multiple,
r#"
x = "10"
f y=(`echo hello` + x) +z=("foo" + "bar"):
"#,
r#"x = "10"
f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
}
summary_test! {
concatination_in_group,
"x = ('0' + '1')",
"x = ('0' + '1')",
}
summary_test! {
string_in_group,
"x = ('0' )",
"x = ('0')",
}
compilation_error_test! {
@ -848,7 +940,7 @@ a:
line: 0,
column: 10,
width: Some(1),
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Eol},
kind: UnexpectedToken{expected: vec![Name, StringToken], found: Eol},
}
compilation_error_test! {
@ -858,27 +950,7 @@ a:
line: 0,
column: 10,
width: Some(0),
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Eof},
}
compilation_error_test! {
name: missing_default_colon,
input: "hello arg=:",
index: 10,
line: 0,
column: 10,
width: Some(1),
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Colon},
}
compilation_error_test! {
name: missing_default_backtick,
input: "hello arg=`hello`",
index: 10,
line: 0,
column: 10,
width: Some(7),
kind: UnexpectedToken{expected: vec![StringToken, RawString], found: Backtick},
kind: UnexpectedToken{expected: vec![Name, StringToken], found: Eof},
}
compilation_error_test! {
@ -1065,4 +1137,33 @@ a:
parse_success(&justfile);
}
}
#[test]
fn empty_recipe_lines() {
let text = "a:";
let justfile = parse_success(&text);
assert_eq!(justfile.recipes["a"].lines.len(), 0);
}
#[test]
fn simple_recipe_lines() {
let text = "a:\n foo";
let justfile = parse_success(&text);
assert_eq!(justfile.recipes["a"].lines.len(), 1);
}
#[test]
fn complex_recipe_lines() {
let text = "a:
foo
b:
";
let justfile = parse_success(&text);
assert_eq!(justfile.recipes["a"].lines.len(), 1);
}
}

View File

@ -86,11 +86,24 @@ impl<'a> Recipe<'a> {
let mut argument_map = BTreeMap::new();
let mut evaluator = AssignmentEvaluator {
assignments: &empty(),
dry_run: configuration.dry_run,
evaluated: empty(),
invocation_directory: context.invocation_directory,
overrides: &empty(),
quiet: configuration.quiet,
scope: &context.scope,
shell: configuration.shell,
dotenv,
exports,
};
let mut rest = arguments;
for parameter in &self.parameters {
let value = if rest.is_empty() {
match parameter.default {
Some(ref default) => Cow::Borrowed(default.as_str()),
Some(ref default) => Cow::Owned(evaluator.evaluate_expression(default, &empty())?),
None => {
return Err(RuntimeError::Internal {
message: "missing parameter without default".to_string(),
@ -109,19 +122,6 @@ impl<'a> Recipe<'a> {
argument_map.insert(parameter.name, value);
}
let mut evaluator = AssignmentEvaluator {
assignments: &empty(),
dry_run: configuration.dry_run,
evaluated: empty(),
invocation_directory: context.invocation_directory,
overrides: &empty(),
quiet: configuration.quiet,
scope: &context.scope,
shell: configuration.shell,
dotenv,
exports,
};
if self.shebang {
let mut evaluated_lines = vec![];
for line in &self.lines {

View File

@ -2,11 +2,23 @@ use crate::common::*;
use CompilationErrorKind::*;
// There are borrow issues here that seems too difficult to solve.
// The errors derived from the variable token has too short a lifetime,
// so we create a new error from its contents, which do live long
// enough.
//
// I suspect the solution here is to give recipes, pieces, and expressions
// two lifetime parameters instead of one, with one being the lifetime
// of the struct, and the second being the lifetime of the tokens
// that it contains.
pub struct RecipeResolver<'a: 'b, 'b> {
stack: Vec<&'a str>,
seen: BTreeSet<&'a str>,
resolved: BTreeSet<&'a str>,
recipes: &'b BTreeMap<&'a str, Recipe<'a>>,
assignments: &'b BTreeMap<&'a str, Expression<'a>>,
text: &'a str,
}
impl<'a, 'b> RecipeResolver<'a, 'b> {
@ -19,6 +31,8 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
seen: empty(),
stack: empty(),
resolved: empty(),
assignments,
text,
recipes,
};
@ -27,38 +41,56 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
resolver.seen = empty();
}
// There are borrow issues here that seems too difficult to solve.
// The errors derived from the variable token has too short a lifetime,
// so we create a new error from its contents, which do live long
// enough.
//
// I suspect the solution here is to give recipes, pieces, and expressions
// two lifetime parameters instead of one, with one being the lifetime
// of the struct, and the second being the lifetime of the tokens
// that it contains.
for recipe in recipes.values() {
for parameter in &recipe.parameters {
if let Some(expression) = &parameter.default {
for (function, argc) in expression.functions() {
resolver.resolve_function(function, argc)?;
}
for variable in expression.variables() {
resolver.resolve_variable(variable, &[])?;
}
}
}
for line in &recipe.lines {
for fragment in line {
if let Fragment::Expression { ref expression, .. } = *fragment {
for (function, argc) in expression.functions() {
if let Err(error) = resolve_function(function, argc) {
return Err(CompilationError {
resolver.resolve_function(function, argc)?;
}
for variable in expression.variables() {
resolver.resolve_variable(variable, &recipe.parameters)?;
}
}
}
}
}
Ok(())
}
fn resolve_function(&self, function: &Token, argc: usize) -> CompilationResult<'a, ()> {
resolve_function(function, argc).map_err(|error| CompilationError {
index: error.index,
line: error.line,
column: error.column,
width: error.width,
kind: UnknownFunction {
function: &text[error.index..error.index + error.width.unwrap()],
function: &self.text[error.index..error.index + error.width.unwrap()],
},
text,
});
text: self.text,
})
}
}
for variable in expression.variables() {
fn resolve_variable(
&self,
variable: &Token,
parameters: &[Parameter],
) -> CompilationResult<'a, ()> {
let name = variable.lexeme;
let undefined = !assignments.contains_key(name)
&& !recipe.parameters.iter().any(|p| p.name == name);
let undefined =
!self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name == name);
if undefined {
let error = variable.error(UndefinedVariable { variable: name });
return Err(CompilationError {
@ -67,16 +99,11 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
column: error.column,
width: error.width,
kind: UndefinedVariable {
variable: &text[error.index..error.index + error.width.unwrap()],
variable: &self.text[error.index..error.index + error.width.unwrap()],
},
text,
text: self.text,
});
}
}
}
}
}
}
Ok(())
}
@ -186,4 +213,24 @@ mod test {
width: Some(3),
kind: UnknownFunction{function: "bar"},
}
compilation_error_test! {
name: unknown_function_in_default,
input: "a f=baz():",
index: 4,
line: 0,
column: 4,
width: Some(3),
kind: UnknownFunction{function: "baz"},
}
compilation_error_test! {
name: unknown_variable_in_default,
input: "a f=foo:",
index: 4,
line: 0,
column: 4,
width: Some(3),
kind: UndefinedVariable{variable: "foo"},
}
}

View File

@ -18,7 +18,7 @@ use std::{
path::Path,
};
use crate::{expression, fragment, justfile::Justfile, parser::Parser, recipe};
use crate::{expression, fragment, justfile::Justfile, parameter, parser::Parser, recipe};
pub fn summary(path: impl AsRef<Path>) -> Result<Result<Summary, String>, io::Error> {
let path = path.as_ref();
@ -46,7 +46,7 @@ impl Summary {
for alias in justfile.aliases.values() {
aliases
.entry(alias.target)
.or_insert(Vec::new())
.or_insert_with(Vec::new)
.push(alias.name.to_string());
}
@ -57,7 +57,7 @@ impl Summary {
.map(|(name, recipe)| {
(
name.to_string(),
Recipe::new(recipe, aliases.remove(name).unwrap_or(Vec::new())),
Recipe::new(recipe, aliases.remove(name).unwrap_or_default()),
)
})
.collect(),
@ -83,6 +83,7 @@ pub struct Recipe {
pub private: bool,
pub quiet: bool,
pub shebang: bool,
pub parameters: Vec<Parameter>,
}
impl Recipe {
@ -93,11 +94,31 @@ impl Recipe {
quiet: recipe.quiet,
dependencies: recipe.dependencies.into_iter().map(str::to_owned).collect(),
lines: recipe.lines.into_iter().map(Line::new).collect(),
parameters: recipe.parameters.into_iter().map(Parameter::new).collect(),
aliases,
}
}
}
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
pub struct Parameter {
pub variadic: bool,
pub name: String,
pub default: Option<Expression>,
}
impl Parameter {
fn new(parameter: parameter::Parameter) -> Parameter {
Parameter {
variadic: parameter.variadic,
name: parameter.name.to_owned(),
default: parameter
.default
.map(|expression| Expression::new(expression)),
}
}
}
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
pub struct Line {
pub fragments: Vec<Fragment>,
@ -184,11 +205,12 @@ impl Expression {
rhs: Box::new(Expression::new(*rhs)),
},
String { cooked_string } => Expression::String {
text: cooked_string.cooked,
text: cooked_string.cooked.to_string(),
},
Variable { name, .. } => Expression::Variable {
name: name.to_owned(),
},
Group { expression } => Expression::new(*expression),
}
}
}

View File

@ -1,8 +1,6 @@
use executable_path::executable_path;
use libc::{EXIT_FAILURE, EXIT_SUCCESS};
use std::env;
use std::process;
use std::str;
use std::{env, fs, process, str};
use tempdir::TempDir;
/// Instantiate integration tests for a given test case using
@ -93,6 +91,46 @@ fn integration_test(
if failure {
panic!("test failed");
}
if expected_status == EXIT_SUCCESS {
println!("Reparsing...");
let output = process::Command::new(&executable_path("just"))
.current_dir(tmp.path())
.arg("--dump")
.output()
.expect("just invocation failed");
if !output.status.success() {
panic!("dump failed: {}", output.status);
}
let dumped = String::from_utf8(output.stdout).unwrap();
let reparsed_path = tmp.path().join("reparsed.just");
fs::write(&reparsed_path, &dumped).unwrap();
let output = process::Command::new(&executable_path("just"))
.current_dir(tmp.path())
.arg("--justfile")
.arg(&reparsed_path)
.arg("--dump")
.output()
.expect("just invocation failed");
if !output.status.success() {
panic!("reparse failed: {}", output.status);
}
let reparsed = String::from_utf8(output.stdout).unwrap();
if reparsed != dumped {
print!("expected:\n{}", reparsed);
print!("got:\n{}", dumped);
assert_eq!(reparsed, dumped);
}
}
}
integration_test! {
@ -1115,10 +1153,10 @@ a Z="\t z":
_private-recipe:
"#,
args: ("--list"),
stdout: r"Available recipes:
a Z='\t z'
hello a b='B\t' c='C' # this does a thing
",
stdout: r#"Available recipes:
a Z="\t z"
hello a b='B ' c='C' # this does a thing
"#,
stderr: "",
status: EXIT_SUCCESS,
}
@ -1138,10 +1176,10 @@ a Z="\t z":
_private-recipe:
"#,
args: ("--list"),
stdout: r"Available recipes:
a Z='\t z' # something else
hello a b='B\t' c='C' # this does a thing
",
stdout: r#"Available recipes:
a Z="\t z" # something else
hello a b='B ' c='C' # this does a thing
"#,
stderr: "",
status: EXIT_SUCCESS,
}
@ -1165,11 +1203,11 @@ this-recipe-is-very-very-very-important Z="\t z":
_private-recipe:
"#,
args: ("--list"),
stdout: r"Available recipes:
hello a b='B\t' c='C' # this does a thing
this-recipe-is-very-very-very-important Z='\t z' # something else
x a b='B\t' c='C' # this does another thing
",
stdout: r#"Available recipes:
hello a b='B ' c='C' # this does a thing
this-recipe-is-very-very-very-important Z="\t z" # something else
x a b='B ' c='C' # this does another thing
"#,
stderr: "",
status: EXIT_SUCCESS,
}
@ -1386,8 +1424,6 @@ b
c
",
stderr: "",
status: EXIT_SUCCESS,
@ -1809,8 +1845,8 @@ a B C +D='hello':
args: ("--color", "always", "--list"),
stdout: "Available recipes:\n a \
\u{1b}[36mB\u{1b}[0m \u{1b}[36mC\u{1b}[0m \u{1b}[35m+\
\u{1b}[0m\u{1b}[36mD\u{1b}[0m=\'\u{1b}[32mhello\u{1b}[0m\
\' \u{1b}[34m#\u{1b}[0m \u{1b}[34mcomment\u{1b}[0m\n",
\u{1b}[0m\u{1b}[36mD\u{1b}[0m=\u{1b}[32m'hello'\u{1b}[0m \
\u{1b}[34m#\u{1b}[0m \u{1b}[34mcomment\u{1b}[0m\n",
stderr: "",
status: EXIT_SUCCESS,
}
@ -1924,3 +1960,94 @@ X = "\'"
"#,
status: EXIT_FAILURE,
}
integration_test! {
name: unknown_variable_in_default,
justfile: "
foo x=bar:
",
args: (),
stdout: "",
stderr: r#"error: Variable `bar` not defined
|
2 | foo x=bar:
| ^^^
"#,
status: EXIT_FAILURE,
}
integration_test! {
name: unknown_function_in_default,
justfile: "
foo x=bar():
",
args: (),
stdout: "",
stderr: r#"error: Call to unknown function `bar`
|
2 | foo x=bar():
| ^^^
"#,
status: EXIT_FAILURE,
}
integration_test! {
name: default_string,
justfile: "
foo x='bar':
echo {{x}}
",
args: (),
stdout: "bar\n",
stderr: "echo bar\n",
status: EXIT_SUCCESS,
}
integration_test! {
name: default_concatination,
justfile: "
foo x=(`echo foo` + 'bar'):
echo {{x}}
",
args: (),
stdout: "foobar\n",
stderr: "echo foobar\n",
status: EXIT_SUCCESS,
}
integration_test! {
name: default_backtick,
justfile: "
foo x=`echo foo`:
echo {{x}}
",
args: (),
stdout: "foo\n",
stderr: "echo foo\n",
status: EXIT_SUCCESS,
}
integration_test! {
name: default_variable,
justfile: "
y = 'foo'
foo x=y:
echo {{x}}
",
args: (),
stdout: "foo\n",
stderr: "echo foo\n",
status: EXIT_SUCCESS,
}
integration_test! {
name: test_os_arch_functions_in_default,
justfile: r#"
foo a=arch() o=os() f=os_family():
echo {{a}} {{o}} {{f}}
"#,
args: (),
stdout: format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
stderr: format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
status: EXIT_SUCCESS,
}

View File

@ -1,7 +1,6 @@
use executable_path::executable_path;
use std::{
process::Command,
thread,
time::{Duration, Instant},
};
use tempdir::TempDir;
@ -33,7 +32,7 @@ fn interrupt_test(justfile: &str) {
.spawn()
.expect("just invocation failed");
thread::sleep(Duration::new(1, 0));
while start.elapsed() < Duration::from_millis(500) {}
kill(child.id());
@ -41,11 +40,11 @@ fn interrupt_test(justfile: &str) {
let elapsed = start.elapsed();
if elapsed > Duration::new(4, 0) {
if elapsed > Duration::from_secs(2) {
panic!("process returned too late: {:?}", elapsed);
}
if elapsed < Duration::new(1, 0) {
if elapsed < Duration::from_millis(100) {
panic!("process returned too early : {:?}", elapsed);
}
@ -59,7 +58,7 @@ fn interrupt_shebang() {
"
default:
#!/usr/bin/env sh
sleep 2
sleep 1
",
);
}
@ -70,7 +69,7 @@ fn interrupt_line() {
interrupt_test(
"
default:
@sleep 2
@sleep 1
",
);
}
@ -80,7 +79,7 @@ default:
fn interrupt_backtick() {
interrupt_test(
"
foo = `sleep 2`
foo = `sleep 1`
default:
@echo hello