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
25 changes: 14 additions & 11 deletions src/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,11 @@ fn find_cycle(targets: &HashMap<PathBuf, BuildEdge>) -> Option<Vec<PathBuf>> {

stack.push(node.clone());

if let Some(edge) = targets.get(node)
&& let Some(cycle) = visit_dependencies(targets, &edge.inputs, stack, states)
{
return Some(cycle);
if let Some(edge) = targets.get(node) {
let deps_result = visit_dependencies(targets, &edge.inputs, stack, states);
if let Some(cycle) = deps_result {
return Some(cycle);
}
}

stack.pop();
Expand All @@ -340,10 +341,11 @@ fn find_cycle(targets: &HashMap<PathBuf, BuildEdge>) -> Option<Vec<PathBuf>> {
states: &mut HashMap<PathBuf, VisitState>,
) -> Option<Vec<PathBuf>> {
for dep in deps {
if targets.contains_key(dep)
&& let Some(cycle) = visit(targets, dep, stack, states)
{
return Some(cycle);
if targets.contains_key(dep) {
let visit_result = visit(targets, dep, stack, states);
if let Some(cycle) = visit_result {
return Some(cycle);
}
}
}
None
Expand All @@ -353,9 +355,10 @@ fn find_cycle(targets: &HashMap<PathBuf, BuildEdge>) -> Option<Vec<PathBuf>> {
let mut stack = Vec::new();

for node in targets.keys() {
if !states.contains_key(node)
&& let Some(cycle) = visit(targets, node, &mut stack, &mut states)
{
if states.contains_key(node) {
continue;
}
if let Some(cycle) = visit(targets, node, &mut stack, &mut states) {
return Some(cycle);
}
}
Expand Down
43 changes: 43 additions & 0 deletions tests/features/ir_generation.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Feature: Intermediate Representation (IR) Generation
As a developer,
I want to compile a manifest into a valid build graph,
So that I can detect configuration errors before execution.

Scenario: A new, empty BuildGraph has no content
Given a new BuildGraph is created
When its contents are checked
Then the graph has 0 actions
And the graph has 0 targets
And the graph has 0 default targets

Scenario: Compiling a valid manifest with one rule and one target
Given the manifest file "tests/data/rules.yml" is compiled to IR
When the graph contents are checked
Then the graph has 1 actions
And the graph has 1 targets

Scenario: Identical rules are deduplicated during IR generation
Given the manifest file "tests/data/duplicate_rules.yml" is compiled to IR
When the graph contents are checked
Then the graph has 1 actions
And the graph has 2 targets

Scenario: IR generation fails if a target references a rule that does not exist
Given the manifest file "tests/data/missing_rule.yml" is compiled to IR
When the generation result is checked
Then IR generation fails

Scenario: IR generation fails if a target specifies multiple rules
Given the manifest file "tests/data/multiple_rules_per_target.yml" is compiled to IR
When the generation result is checked
Then IR generation fails

Scenario: IR generation fails if multiple targets produce the same output file
Given the manifest file "tests/data/duplicate_outputs.yml" is compiled to IR
When the generation result is checked
Then IR generation fails

Scenario: IR generation fails if there is a circular dependency between targets
Given the manifest file "tests/data/circular.yml" is compiled to IR
When the generation result is checked
Then IR generation fails
35 changes: 33 additions & 2 deletions tests/steps/ir_steps.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
//! Step definitions for `BuildGraph` scenarios.

use crate::CliWorld;
use cucumber::{then, when};
use cucumber::{given, then, when};
use netsuke::ir::BuildGraph;

#[when("a new BuildGraph is created")]
fn assert_graph(world: &CliWorld) {
assert!(
world.build_graph.is_some(),
"build graph should have been generated",
);
}

fn assert_generation_attempted(world: &CliWorld) {
match (world.build_graph.is_some(), world.manifest_error.is_some()) {
(true, false) | (false, true) => (),
(true, true) => panic!("unexpected: graph and error present"),
(false, false) => panic!("IR generation not attempted"),
}
}

#[given("a new BuildGraph is created")]
fn create_graph(world: &mut CliWorld) {
world.build_graph = Some(BuildGraph::default());
}
Expand All @@ -31,6 +46,7 @@ fn graph_defaults(world: &mut CliWorld, count: usize) {
clippy::needless_pass_by_value,
reason = "Cucumber requires owned String arguments"
)]
#[given(expr = "the manifest file {string} is compiled to IR")]
#[when(expr = "the manifest file {string} is compiled to IR")]
fn compile_manifest(world: &mut CliWorld, path: String) {
match netsuke::manifest::from_path(&path)
Expand All @@ -47,6 +63,21 @@ fn compile_manifest(world: &mut CliWorld, path: String) {
}
}

#[when("its contents are checked")]
fn graph_checked(world: &mut CliWorld) {
assert_graph(world);
}

#[when("the graph contents are checked")]
fn graph_contents_checked(world: &mut CliWorld) {
assert_graph(world);
}

#[when("the generation result is checked")]
fn generation_result_checked(world: &mut CliWorld) {
assert_generation_attempted(world);
}

#[then("IR generation fails")]
fn ir_generation_fails(world: &mut CliWorld) {
assert!(
Expand Down
3 changes: 2 additions & 1 deletion tests/steps/manifest_steps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ fn assert_parsed(world: &CliWorld) {
"manifest should have been parsed"
);
}

#[given(expr = "the manifest file {string} is parsed")]
fn given_parse_manifest(world: &mut CliWorld, path: String) {
parse_manifest_inner(world, &path);
Expand All @@ -44,7 +45,7 @@ fn parse_manifest(world: &mut CliWorld, path: String) {
parse_manifest_inner(world, &path);
}

#[when(regex = r"^the (?P<item>[a-z ]+) (?:is|are) checked$")]
#[when(regex = r"^the (?P<item>parsing result|manifest|version|flags|rules) (?:is|are) checked$")]
fn when_item_checked(world: &mut CliWorld, item: String) {
match item.as_str() {
"parsing result" => assert_parsed(world),
Expand Down