Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ pub struct BuildIns {
pub ids: Vec<FileId>,
pub explicit: usize,
pub implicit: usize,
// order_only count implied by other counts.
// pub order_only: usize,
pub order_only: usize,
// validation count implied by other counts.
// pub validation: usize,
}

/// Output files from a Build.
Expand Down Expand Up @@ -204,7 +205,15 @@ impl Build {
/// Note that we don't order on discovered_ins, because they're not allowed to
/// affect build order.
pub fn ordering_ins(&self) -> &[FileId] {
&self.ins.ids
&self.ins.ids[0..(self.ins.order_only + self.ins.explicit + self.ins.implicit)]
}

/// Inputs that are needed before validating information.
/// Validation inputs will be built whenever this Build is built, but this Build will not
/// wait for them to complete before running. The validation inputs can fail to build, which
/// will cause the overall build to fail.
pub fn validation_ins(&self) -> &[FileId] {
&self.ins.ids[(self.ins.order_only + self.ins.explicit + self.ins.implicit)..]
}

/// Potentially update discovered_ins with a new set of deps, returning true if they changed.
Expand Down
3 changes: 2 additions & 1 deletion src/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ impl Loader {
ids: b.ins,
explicit: b.explicit_ins,
implicit: b.implicit_ins,
// order_only is unused
order_only: b.order_only_ins,
// validation is implied by the other counts
};
let outs = graph::BuildOuts {
ids: b.outs,
Expand Down
20 changes: 17 additions & 3 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub struct Build<'text, Path> {
pub explicit_ins: usize,
pub implicit_ins: usize,
pub order_only_ins: usize,
pub validation_ins: usize,
pub vars: VarList<'text>,
}

Expand Down Expand Up @@ -215,7 +216,8 @@ impl<'text> Parser<'text> {

if self.scanner.peek() == '|' {
self.scanner.next();
if self.scanner.peek() == '|' {
let peek = self.scanner.peek();
if peek == '|' || peek == '@' {
self.scanner.back();
} else {
self.read_paths_to(loader, &mut ins)?;
Expand All @@ -225,11 +227,22 @@ impl<'text> Parser<'text> {

if self.scanner.peek() == '|' {
self.scanner.next();
self.scanner.expect('|')?;
self.read_paths_to(loader, &mut ins)?;
if self.scanner.peek() == '@' {
self.scanner.back();
} else {
self.scanner.expect('|')?;
self.read_paths_to(loader, &mut ins)?;
}
}
let order_only_ins = ins.len() - implicit_ins - explicit_ins;

if self.scanner.peek() == '|' {
self.scanner.next();
self.scanner.expect('@')?;
self.read_paths_to(loader, &mut ins)?;
}
let validation_ins = ins.len() - order_only_ins - implicit_ins - explicit_ins;

self.scanner.skip('\r');
self.scanner.expect('\n')?;
let vars = self.read_scoped_vars()?;
Expand All @@ -242,6 +255,7 @@ impl<'text> Parser<'text> {
explicit_ins,
implicit_ins,
order_only_ins,
validation_ins,
vars,
})
}
Expand Down
7 changes: 7 additions & 0 deletions src/work.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,13 @@ impl BuildStates {
self.want_file(graph, stack, id)?;
ready = ready && graph.file(id).input.is_none();
}
for &id in build.validation_ins() {
// This build doesn't technically depend on the validation inputs, so
// allocate a new stack. Validation inputs could in theory depend on this build's
// outputs.
let mut stack = Vec::new();
self.want_file(graph, &mut stack, id)?;
}

if ready {
self.set(id, build, BuildState::Ready);
Expand Down
5 changes: 5 additions & 0 deletions tests/e2e/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod basic;
mod discovered;
mod missing;
mod regen;
mod validations;

pub fn n2_binary() -> std::path::PathBuf {
std::env::current_exe()
Expand Down Expand Up @@ -72,6 +73,10 @@ impl TestSpace {
std::fs::metadata(self.dir.path().join(path))
}

pub fn path(&self) -> &std::path::Path {
self.dir.path()
}

/// Invoke n2, returning process output.
pub fn run(&self, cmd: &mut std::process::Command) -> std::io::Result<std::process::Output> {
cmd.current_dir(self.dir.path()).output()
Expand Down
103 changes: 103 additions & 0 deletions tests/e2e/validations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use crate::e2e::*;

#[test]
fn basic_validation() -> anyhow::Result<()> {
let space = TestSpace::new()?;
space.write(
"build.ninja",
&[
TOUCH_RULE,
"build my_validation: touch",
"build out: touch |@ my_validation",
"",
]
.join("\n"),
)?;
space.run_expect(&mut n2_command(vec!["out"]))?;
assert!(space.read("out").is_ok());
assert!(space.read("my_validation").is_ok());
Ok(())
}

#[cfg(unix)]
#[test]
fn build_starts_before_validation_finishes() -> anyhow::Result<()> {
let space = TestSpace::new()?;
space.write(
"build.ninja",
"
rule build_slow
command = sleep 0.3 && touch $out

rule build_fast
command = sleep 0.1 && touch $out

build out: build_fast regular_input |@ validation_input
build regular_input: build_fast
build validation_input: build_slow
",
)?;
let command = n2_command(vec!["out"])
.current_dir(space.path())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.stdin(std::process::Stdio::null())
.spawn()?;
std::thread::sleep(std::time::Duration::from_millis(50));
assert!(space.read("out").is_err());
assert!(space.read("regular_input").is_err());
assert!(space.read("validation_input").is_err());
std::thread::sleep(std::time::Duration::from_millis(100));
assert!(space.read("out").is_err());
assert!(space.read("regular_input").is_ok());
assert!(space.read("validation_input").is_err());
std::thread::sleep(std::time::Duration::from_millis(100));
assert!(space.read("out").is_ok());
assert!(space.read("regular_input").is_ok());
assert!(space.read("validation_input").is_err());
std::thread::sleep(std::time::Duration::from_millis(100));
assert!(space.read("out").is_ok());
assert!(space.read("regular_input").is_ok());
assert!(space.read("validation_input").is_ok());
assert!(command.wait_with_output()?.status.success());
Ok(())
}

#[cfg(unix)]
#[test]
fn build_fails_when_validation_fails() -> anyhow::Result<()> {
let space = TestSpace::new()?;
space.write(
"build.ninja",
"
rule touch
command = touch $out

rule fail
command = exit 1

build out: touch |@ validation_input
build validation_input: fail
",
)?;
let output = space.run(&mut n2_command(vec!["out"]))?;
assert!(!output.status.success());
Ok(())
}

#[test]
fn validation_inputs_break_cycles() -> anyhow::Result<()> {
let space = TestSpace::new()?;
space.write(
"build.ninja",
&[
TOUCH_RULE,
"build out: touch |@ validation_input",
"build validation_input: touch out",
"",
]
.join("\n"),
)?;
space.run_expect(&mut n2_command(vec!["out"]))?;
Ok(())
}