1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
|
//! Script-provided command E2E compatibility check: a `composer.json` script naming a
//! `Symfony\Component\Console\Command\Command` subclass is imported as an application command,
//! and `list`, `help`, executions and the mismatched-name warning are compared between upstream
//! Composer and Shirabe.
//!
//! The whole fixture is Shirabe-authored (`fixtures/e2e-script-command/`), so nothing has to be
//! fetched; the test skips only while the PHP runtime or the Composer checkout is missing.
use crate::e2e_extension_installer_test::{copy_dir, upstream_composer_bin};
use crate::plugin_installer_test::{lock_php_worker, php_runtime_available};
use std::path::{Path, PathBuf};
use tempfile::TempDir;
fn fixture_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/plugin/fixtures/e2e-script-command")
}
struct CommandRun {
exit_code: i32,
stdout: String,
stderr: String,
}
/// One composer-CLI invocation inside the prepared project.
fn run_command(work: &Path, program: &str, prefix_args: &[&str], args: &[&str]) -> CommandRun {
let output = std::process::Command::new(program)
.args(prefix_args)
.args(args)
.current_dir(work)
.env("COMPOSER_HOME", work.join("home"))
.env("COMPOSER_CACHE_DIR", work.join("cache"))
.env("COMPOSER_NO_INTERACTION", "1")
// Rendering width must not depend on the invoking terminal.
.env("COLUMNS", "120")
.env("LINES", "30")
.output()
.unwrap();
CommandRun {
exit_code: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
}
}
/// `list` opens with the application banner (logo and version line), which Shirabe owns and
/// upstream Composer cannot match. Everything from the `Usage:` section down still has to.
fn list_body(text: &str) -> &str {
let usage = text
.find("\nUsage:")
.expect("list output has a Usage section");
&text[usage + 1..]
}
fn lines_starting_with<'a>(text: &'a str, prefix: &str) -> Vec<&'a str> {
text.lines()
.map(str::trim_end)
.filter(|line| line.trim_start().starts_with(prefix))
.collect()
}
#[test]
fn test_script_command_class_import_matches_upstream_composer() {
if !php_runtime_available() {
return;
}
let Some(composer_bin) = upstream_composer_bin() else {
return;
};
let _worker = lock_php_worker();
let composer_bin = composer_bin.to_str().unwrap().to_string();
let implementations: [(&str, Vec<&str>); 2] = [
("php", vec![composer_bin.as_str()]),
(env!("CARGO_BIN_EXE_shirabe"), vec![]),
];
let mut results: Vec<[CommandRun; 4]> = Vec::new();
for (program, prefix) in &implementations {
let work = TempDir::new().unwrap();
copy_dir(&fixture_dir(), work.path());
let install = run_command(work.path(), program, prefix, &["install"]);
assert_eq!(0, install.exit_code, "{program}: install must succeed");
let greet = run_command(work.path(), program, prefix, &["greet", "World", "--shout"]);
let renamed = run_command(work.path(), program, prefix, &["renamed"]);
let help = run_command(work.path(), program, prefix, &["help", "greet"]);
let list = run_command(work.path(), program, prefix, &["list"]);
results.push([greet, renamed, help, list]);
}
let [upstream, shirabe] = <[_; 2]>::try_from(results).ok().unwrap();
let [u_greet, u_renamed, u_help, u_list] = upstream;
let [s_greet, s_renamed, s_help, s_list] = shirabe;
// The imported command runs with its own definition bound: the argument, the shorthand
// option, the name the constructor took from composer.json and the hosting application.
assert_eq!(0, u_greet.exit_code, "upstream greet must succeed");
assert_eq!(u_greet.exit_code, s_greet.exit_code);
assert_eq!(
lines_starting_with(&u_greet.stdout, "greet:"),
lines_starting_with(&s_greet.stdout, "greet:")
);
assert_eq!(
vec![
"greet: HELLO WORLD",
"greet: name=greet",
"greet: app=Composer\\Console\\Application",
],
lines_starting_with(&s_greet.stdout, "greet:")
);
// A class whose configure() sets a different name is renamed to the script name, and an
// empty description is filled in from scripts-descriptions.
assert_eq!(0, u_renamed.exit_code, "upstream renamed must succeed");
assert_eq!(u_renamed.exit_code, s_renamed.exit_code);
assert_eq!(
lines_starting_with(&u_renamed.stdout, "renamed:"),
lines_starting_with(&s_renamed.stdout, "renamed:")
);
assert_eq!(
vec![
"renamed: name=renamed",
"renamed: description=Description taken from composer.json",
],
lines_starting_with(&s_renamed.stdout, "renamed:")
);
let mismatch_warning =
"The script named renamed in composer.json has a mismatched name in its class definition.";
assert!(
u_renamed.stderr.contains(mismatch_warning),
"upstream must warn about the mismatched name: {}",
u_renamed.stderr
);
assert!(
s_renamed.stderr.contains(mismatch_warning),
"shirabe must warn about the mismatched name: {}",
s_renamed.stderr
);
// A class extending SingleCommandApplication is still imported, with a warning.
let single_warning = "The script named single extends SingleCommandApplication which is not compatible with Composer 2.9+";
assert!(
u_list.stderr.contains(single_warning),
"upstream must warn about SingleCommandApplication: {}",
u_list.stderr
);
assert!(
s_list.stderr.contains(single_warning),
"shirabe must warn about SingleCommandApplication: {}",
s_list.stderr
);
assert_eq!(0, u_help.exit_code, "upstream help greet must succeed");
assert_eq!(u_help.exit_code, s_help.exit_code);
assert_eq!(u_help.stdout, s_help.stdout, "help greet output differs");
// The imported commands are listed with the descriptions the class and composer.json give
// them, next to the plain shell script that stays a ScriptAliasCommand.
assert_eq!(0, u_list.exit_code);
assert_eq!(u_list.exit_code, s_list.exit_code);
assert_eq!(
list_body(&u_list.stdout),
list_body(&s_list.stdout),
"list output differs"
);
assert_eq!(
vec![" greet Greets someone from a script-provided command."],
lines_starting_with(&s_list.stdout, "greet")
);
}
|