Skip to content

Commit 06ff2fd

Browse files
authored
Run commands for phony targets (#71)
* Run commands for phony targets * Refactor Ninja integration tests with rstest
1 parent 91c2e60 commit 06ff2fd

File tree

4 files changed

+126
-113
lines changed

4 files changed

+126
-113
lines changed

src/ir.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ pub struct BuildEdge {
6262
pub implicit_outputs: Vec<PathBuf>,
6363
/// Order-only dependencies that do not trigger rebuilds (Ninja `||`).
6464
pub order_only_deps: Vec<PathBuf>,
65-
/// Always run the command even if the output exists.
65+
/// Output does not correspond to a real file.
6666
pub phony: bool,
6767
/// Run the command on every invocation regardless of timestamps.
6868
pub always: bool,

src/ninja_gen.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,7 @@ impl Display for DisplayEdge<'_> {
145145
if !self.edge.implicit_outputs.is_empty() {
146146
write!(f, " | {}", join(&self.edge.implicit_outputs))?;
147147
}
148-
let rule = if self.edge.phony {
149-
"phony"
150-
} else {
151-
&self.edge.action_id
152-
};
153-
write!(f, ": {rule}")?;
148+
write!(f, ": {}", self.edge.action_id)?;
154149
if !self.edge.inputs.is_empty() {
155150
write!(f, " {}", join(&self.edge.inputs))?;
156151
}

tests/features/ninja.feature

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ Feature: Ninja file generation
66
Then the ninja file contains "rule"
77
And the ninja file contains "build hello.o:"
88

9-
Scenario: Phony target rule
9+
Scenario: Phony target runs its command
1010
When the manifest file "tests/data/phony.yml" is compiled to IR
1111
And the ninja file is generated
12-
Then the ninja file contains "build clean: phony"
12+
Then the ninja file contains "build clean:"
13+
And the ninja file contains "rm -rf build"

tests/ninja_gen_tests.rs

Lines changed: 121 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ use insta::{Settings, assert_snapshot};
99
use netsuke::ast::Recipe;
1010
use netsuke::ir::{Action, BuildEdge, BuildGraph};
1111
use netsuke::ninja_gen::generate;
12-
use rstest::rstest;
12+
use rstest::{fixture, rstest};
1313
use std::{fs, path::PathBuf, process::Command};
14-
use tempfile::tempdir;
14+
use tempfile::{TempDir, tempdir};
1515

1616
fn skip_if_ninja_unavailable() -> bool {
1717
match Command::new("ninja").arg("--version").output() {
@@ -27,8 +27,26 @@ fn skip_if_ninja_unavailable() -> bool {
2727
}
2828
}
2929

30+
/// Define how the integration test should assert Ninja's behaviour.
31+
#[derive(Debug)]
32+
enum AssertionType {
33+
FileContent(String),
34+
FileExists,
35+
StatusSuccess,
36+
}
37+
38+
/// Provide a temporary directory when Ninja is available, skipping otherwise.
39+
#[fixture]
40+
fn ninja_integration_setup() -> Option<TempDir> {
41+
if skip_if_ninja_unavailable() {
42+
None
43+
} else {
44+
Some(tempdir().expect("temp dir"))
45+
}
46+
}
47+
3048
#[rstest]
31-
#[case::phony(
49+
#[case::phony_target_runs_command(
3250
Action {
3351
recipe: Recipe::Command { command: "true".into() },
3452
description: None,
@@ -50,7 +68,7 @@ fn skip_if_ninja_unavailable() -> bool {
5068
concat!(
5169
"rule a\n",
5270
" command = true\n\n",
53-
"build out: phony in\n\n",
71+
"build out: a in\n\n",
5472
),
5573
)]
5674
#[case::standard_build(
@@ -169,138 +187,137 @@ fn generate_multiline_script_snapshot() {
169187
);
170188
}
171189

172-
/// Ensure a multi-line script produces a Ninja manifest that Ninja accepts.
173-
#[rstest]
174-
#[ignore = "requires Ninja"]
175-
fn integration_multiline_script_valid() {
176-
if skip_if_ninja_unavailable() {
177-
return;
178-
}
179-
180-
let mut graph = BuildGraph::default();
181-
graph.actions.insert(
182-
"script".into(),
183-
Action {
184-
recipe: Recipe::Script {
185-
script: "echo one\necho two".into(),
186-
},
187-
description: None,
188-
depfile: None,
189-
deps_format: None,
190-
pool: None,
191-
restat: false,
192-
},
193-
);
194-
graph.targets.insert(
195-
PathBuf::from("out"),
196-
BuildEdge {
197-
action_id: "script".into(),
198-
inputs: Vec::new(),
199-
explicit_outputs: vec![PathBuf::from("out")],
200-
implicit_outputs: Vec::new(),
201-
order_only_deps: Vec::new(),
202-
phony: false,
203-
always: false,
204-
},
205-
);
206-
graph.default_targets.push(PathBuf::from("out"));
207-
208-
let ninja = generate(&graph);
209-
let dir = tempdir().expect("temp dir");
210-
fs::write(dir.path().join("build.ninja"), &ninja).expect("write ninja");
211-
let status = Command::new("ninja")
212-
.arg("-n")
213-
.current_dir(dir.path())
214-
.status()
215-
.expect("run ninja");
216-
assert!(status.success());
217-
}
218-
219-
/// Test that scripts containing percent signs execute correctly.
190+
/// Integration scenarios to confirm Ninja executes commands correctly.
220191
#[rstest]
221-
#[ignore = "requires Ninja"]
222-
fn generate_script_with_percent() {
223-
if skip_if_ninja_unavailable() {
224-
return;
225-
}
226-
227-
let action = Action {
228-
recipe: Recipe::Script {
229-
script: "echo 100% > out".into(),
230-
},
192+
#[case::multiline_script_valid(
193+
Action {
194+
recipe: Recipe::Script { script: "echo one\necho two".into() },
231195
description: None,
232196
depfile: None,
233197
deps_format: None,
234198
pool: None,
235199
restat: false,
236-
};
237-
let edge = BuildEdge {
200+
},
201+
BuildEdge {
202+
action_id: "script".into(),
203+
inputs: Vec::new(),
204+
explicit_outputs: vec![PathBuf::from("out")],
205+
implicit_outputs: Vec::new(),
206+
order_only_deps: Vec::new(),
207+
phony: false,
208+
always: false,
209+
},
210+
PathBuf::from("out"),
211+
vec!["-n"],
212+
AssertionType::StatusSuccess,
213+
)]
214+
#[case::script_with_percent(
215+
Action {
216+
recipe: Recipe::Script { script: "echo 100% > out".into() },
217+
description: None,
218+
depfile: None,
219+
deps_format: None,
220+
pool: None,
221+
restat: false,
222+
},
223+
BuildEdge {
238224
action_id: "percent".into(),
239225
inputs: Vec::new(),
240226
explicit_outputs: vec![PathBuf::from("out")],
241227
implicit_outputs: Vec::new(),
242228
order_only_deps: Vec::new(),
243229
phony: false,
244230
always: false,
245-
};
246-
let mut graph = BuildGraph::default();
247-
graph.actions.insert("percent".into(), action);
248-
graph.targets.insert(PathBuf::from("out"), edge);
249-
graph.default_targets.push(PathBuf::from("out"));
250-
251-
let ninja = generate(&graph);
252-
let dir = tempdir().expect("temp dir");
253-
fs::write(dir.path().join("build.ninja"), &ninja).expect("write ninja");
254-
let status = Command::new("ninja")
255-
.arg("out")
256-
.current_dir(dir.path())
257-
.status()
258-
.expect("run ninja");
259-
assert!(status.success());
260-
let content = fs::read_to_string(dir.path().join("out")).expect("read out");
261-
assert_eq!(content.trim(), "100%");
262-
}
263-
264-
#[rstest]
265-
#[ignore = "requires Ninja"]
266-
fn generate_script_with_backtick() {
267-
if skip_if_ninja_unavailable() {
268-
return;
269-
}
270-
271-
let action = Action {
272-
recipe: Recipe::Script {
273-
script: "echo `echo hi` > out".into(),
274-
},
231+
},
232+
PathBuf::from("out"),
233+
vec!["out"],
234+
AssertionType::FileContent("100%".into()),
235+
)]
236+
#[case::script_with_backtick(
237+
Action {
238+
recipe: Recipe::Script { script: "echo `echo hi` > out".into() },
275239
description: None,
276240
depfile: None,
277241
deps_format: None,
278242
pool: None,
279243
restat: false,
280-
};
281-
let edge = BuildEdge {
244+
},
245+
BuildEdge {
282246
action_id: "tick".into(),
283247
inputs: Vec::new(),
284248
explicit_outputs: vec![PathBuf::from("out")],
285249
implicit_outputs: Vec::new(),
286250
order_only_deps: Vec::new(),
287251
phony: false,
288252
always: false,
253+
},
254+
PathBuf::from("out"),
255+
vec!["out"],
256+
AssertionType::FileContent("hi".into()),
257+
)]
258+
#[case::phony_action_executes_command(
259+
Action {
260+
recipe: Recipe::Command { command: "touch action-called.txt".into() },
261+
description: None,
262+
depfile: None,
263+
deps_format: None,
264+
pool: None,
265+
restat: false,
266+
},
267+
BuildEdge {
268+
action_id: "hello".into(),
269+
inputs: Vec::new(),
270+
explicit_outputs: vec![PathBuf::from("say-hello")],
271+
implicit_outputs: Vec::new(),
272+
order_only_deps: Vec::new(),
273+
phony: true,
274+
always: false,
275+
},
276+
PathBuf::from("action-called.txt"),
277+
vec!["say-hello"],
278+
AssertionType::FileExists,
279+
)]
280+
fn ninja_integration_tests(
281+
ninja_integration_setup: Option<TempDir>,
282+
#[case] action: Action,
283+
#[case] edge: BuildEdge,
284+
#[case] target_name: PathBuf,
285+
#[case] ninja_args: Vec<&str>,
286+
#[case] assertion: AssertionType,
287+
) {
288+
let Some(dir) = ninja_integration_setup else {
289+
return;
289290
};
291+
292+
let output = edge
293+
.explicit_outputs
294+
.first()
295+
.expect("explicit output")
296+
.clone();
290297
let mut graph = BuildGraph::default();
291-
graph.actions.insert("tick".into(), action);
292-
graph.targets.insert(PathBuf::from("out"), edge);
293-
graph.default_targets.push(PathBuf::from("out"));
298+
graph.actions.insert(edge.action_id.clone(), action);
299+
graph.targets.insert(output.clone(), edge);
300+
graph.default_targets.push(output);
294301

295302
let ninja = generate(&graph);
296-
let dir = tempdir().expect("temp dir");
297303
fs::write(dir.path().join("build.ninja"), &ninja).expect("write ninja");
298304
let status = Command::new("ninja")
299-
.arg("out")
305+
.args(&ninja_args)
300306
.current_dir(dir.path())
301307
.status()
302308
.expect("run ninja");
303-
assert!(status.success());
304-
let content = fs::read_to_string(dir.path().join("out")).expect("read out");
305-
assert_eq!(content.trim(), "hi");
309+
310+
match assertion {
311+
AssertionType::StatusSuccess => assert!(status.success()),
312+
AssertionType::FileExists => {
313+
assert!(status.success());
314+
assert!(dir.path().join(target_name).exists());
315+
}
316+
AssertionType::FileContent(expected) => {
317+
assert!(status.success());
318+
let content =
319+
fs::read_to_string(dir.path().join(target_name)).expect("read target file");
320+
assert_eq!(content.trim(), expected);
321+
}
322+
}
306323
}

0 commit comments

Comments
 (0)