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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
//! Cargo.toml generation for Rust e2e test crates.
use crate::core::hash::{self, CommentStyle};
use crate::core::template_versions as tv;
/// Render a `Cargo.toml` for the Rust e2e test crate.
///
/// Generates all dependency lines based on which test features are needed
/// (mock server, HTTP tests, tokio, trait-bridge stubs, etc.).
///
/// The emitted file is idempotent under `cargo sort`: dependencies are in
/// alphabetical order and `[package.metadata.cargo-machete]` appears
/// immediately after `[package]`, which is the canonical position that
/// `cargo sort` would choose if it ran on the file.
#[allow(clippy::too_many_arguments)]
pub fn render_cargo_toml(
crate_name: &str,
dep_name: &str,
crate_path: &str,
needs_serde_json: bool,
needs_mock_server: bool,
needs_http_tests: bool,
needs_tokio: bool,
needs_tower_http: bool,
needs_anyhow: bool,
dep_mode: crate::e2e::config::DependencyMode,
version: Option<&str>,
features: &[String],
) -> String {
let e2e_name = format!("{dep_name}-e2e-rust");
// Use only the features explicitly configured in alef.toml.
// Do NOT auto-add "serde" — the target crate may not have that feature.
// serde_json is added as a separate dependency when needed.
let effective_features: Vec<&str> = features.iter().map(|s| s.as_str()).collect();
let features_str = if effective_features.is_empty() {
String::new()
} else {
format!(", default-features = false, features = {:?}", effective_features)
};
let dep_spec = match dep_mode {
crate::e2e::config::DependencyMode::Registry => {
let ver = version.unwrap_or("0.1.0");
if crate_name != dep_name {
format!("{dep_name} = {{ package = \"{crate_name}\", version = \"{ver}\"{features_str} }}")
} else if effective_features.is_empty() {
format!("{dep_name} = \"{ver}\"")
} else {
format!("{dep_name} = {{ version = \"{ver}\"{features_str} }}")
}
}
crate::e2e::config::DependencyMode::Local => {
if crate_name != dep_name {
format!("{dep_name} = {{ package = \"{crate_name}\", path = \"{crate_path}\"{features_str} }}")
} else if effective_features.is_empty() {
format!("{dep_name} = {{ path = \"{crate_path}\" }}")
} else {
format!("{dep_name} = {{ path = \"{crate_path}\"{features_str} }}")
}
}
};
// serde_json is needed either when args use json_object/handle, or when the
// mock server binary is present (it uses serde_json::Value for fixture bodies),
// or when http integration tests are generated (they serialize fixture bodies).
let effective_needs_serde_json = needs_serde_json || needs_mock_server || needs_http_tests;
// An empty `[workspace]` table makes the e2e crate its own workspace root, so
// it never gets pulled into a parent crate's workspace. This means consumers
// don't have to remember to add `e2e/rust` to `workspace.exclude`, and
// `cargo fmt`/`cargo build` work the same whether the parent has a
// workspace or not.
// Mock server requires axum (HTTP router) and tokio-stream (SSE streaming).
// The standalone binary additionally needs serde (derive) and walkdir.
// Http integration tests require axum-test for the test server.
let needs_axum = needs_mock_server || needs_http_tests;
// Build the cargo-machete ignore list.
let mut machete_ignored: Vec<&str> = Vec::new();
if effective_needs_serde_json {
machete_ignored.push("\"serde_json\"");
}
if needs_axum {
machete_ignored.push("\"axum\"");
machete_ignored.push("\"serde\"");
machete_ignored.push("\"walkdir\"");
}
if needs_mock_server {
machete_ignored.push("\"tokio-stream\"");
}
if needs_http_tests {
machete_ignored.push("\"axum-test\"");
machete_ignored.push("\"bytes\"");
}
if needs_tower_http {
machete_ignored.push("\"tower-http\"");
machete_ignored.push("\"tempfile\"");
}
// anyhow and async-trait are deps used by trait-bridge stubs; machete would
// flag them as unused since they're only referenced in generated impl code.
if needs_anyhow {
machete_ignored.push("\"anyhow\"");
machete_ignored.push("\"async-trait\"");
}
// Build metadata and bin sections as self-contained block strings.
// [package.metadata.cargo-machete] must appear immediately after [package]
// and before [[bin]] — that is the canonical position cargo-sort would
// choose, so emitting them in this order keeps the output idempotent.
let machete_section = if machete_ignored.is_empty() {
String::new()
} else {
format!(
"[package.metadata.cargo-machete]\nignored = [{}]",
machete_ignored.join(", ")
)
};
let bin_section = if needs_mock_server || needs_http_tests {
String::from("[[bin]]\nname = \"mock-server\"\npath = \"src/main.rs\"")
} else {
String::new()
};
// E2e package version tracks the consumer crate version so `alef sync-versions`
// doesn't rewrite the generated file on every prek run. Fall back to "0.1.0" only
// when no consumer version is known (test fixtures, etc.).
let pkg_version = version.unwrap_or("0.1.0");
let header = hash::header(CommentStyle::Hash);
// Collect all dependency entries into a sortable Vec so the emitted
// [dependencies] block is alphabetically ordered. cargo-sort rewrites any
// non-alphabetical block, causing prek to oscillate between alef's order
// and cargo-sort's canonical order on every run.
let mut dep_entries: Vec<(String, String)> = Vec::new();
dep_entries.push((dep_name.to_string(), dep_spec.clone()));
if effective_needs_serde_json {
dep_entries.push(("serde_json".to_string(), "serde_json = \"1\"".to_string()));
}
if needs_anyhow {
dep_entries.push(("anyhow".to_string(), "anyhow = \"1\"".to_string()));
dep_entries.push((
"async-trait".to_string(),
format!("async-trait = \"{async_trait}\"", async_trait = tv::cargo::ASYNC_TRAIT),
));
}
if needs_axum {
dep_entries.push(("axum".to_string(), format!("axum = \"{axum}\"", axum = tv::cargo::AXUM)));
dep_entries.push((
"serde".to_string(),
"serde = { version = \"1\", features = [\"derive\"] }".to_string(),
));
dep_entries.push((
"walkdir".to_string(),
format!("walkdir = \"{walkdir}\"", walkdir = tv::cargo::WALKDIR),
));
if needs_mock_server {
dep_entries.push((
"tokio-stream".to_string(),
format!(
"tokio-stream = \"{tokio_stream}\"",
tokio_stream = tv::cargo::TOKIO_STREAM
),
));
}
if needs_http_tests {
dep_entries.push(("axum-test".to_string(), "axum-test = \"20\"".to_string()));
dep_entries.push(("bytes".to_string(), "bytes = \"1\"".to_string()));
}
if needs_tower_http {
dep_entries.push((
"tower-http".to_string(),
format!(
"tower-http = {{ version = \"{tower_http}\", features = [\"cors\", \"fs\"] }}",
tower_http = tv::cargo::TOWER_HTTP
),
));
dep_entries.push((
"tempfile".to_string(),
format!("tempfile = \"{tempfile}\"", tempfile = tv::cargo::TEMPFILE),
));
}
}
if needs_tokio {
dep_entries.push((
"tokio".to_string(),
"tokio = { version = \"1\", features = [\"full\"] }".to_string(),
));
}
dep_entries.sort_by(|a, b| a.0.cmp(&b.0));
let dep_block = dep_entries
.iter()
.map(|(_, line)| line.as_str())
.collect::<Vec<_>>()
.join("\n");
// Assemble the middle block (between [package] and [dependencies]).
// Each present section is separated by exactly one blank line.
let mut middle_sections: Vec<&str> = Vec::new();
if !machete_section.is_empty() {
middle_sections.push(&machete_section);
}
if !bin_section.is_empty() {
middle_sections.push(&bin_section);
}
let middle_block = middle_sections.join("\n\n");
let middle_separator = if middle_block.is_empty() { "" } else { "\n\n" };
format!(
"{header}\n[workspace]\n\n[package]\nname = \"{e2e_name}\"\nversion = \"{pkg_version}\"\nedition = \"2021\"\nlicense = \"MIT\"\npublish = false{middle_separator}{middle_block}\n\n[dependencies]\n{dep_block}\n"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::e2e::config::DependencyMode;
#[test]
fn render_cargo_toml_local_no_features_produces_path_dep() {
// When crate_name ("my-crate") differs from dep_name ("my_crate") a
// `package = …` key is required to tell Cargo the actual crate name.
let out = render_cargo_toml(
"my-crate",
"my_crate",
"../../crates/my-crate",
false,
false,
false,
false,
false,
false,
DependencyMode::Local,
None,
&[],
);
assert!(
out.contains("my_crate = { package = \"my-crate\", path = \"../../crates/my-crate\" }"),
"got:\n{out}"
);
assert!(out.contains("edition = \"2021\""));
}
#[test]
fn render_cargo_toml_local_same_name_produces_simple_path_dep() {
// When crate_name and dep_name are identical no `package` key is needed.
let out = render_cargo_toml(
"my_crate",
"my_crate",
"../../crates/my_crate",
false,
false,
false,
false,
false,
false,
DependencyMode::Local,
None,
&[],
);
assert!(
out.contains("my_crate = { path = \"../../crates/my_crate\" }"),
"got:\n{out}"
);
}
#[test]
fn render_cargo_toml_has_no_issues_docs_header_line() {
// Regression: older alef injected a `# Issues & docs: …` header line via
// header_for_config. cargo-sort always strips it, so every prek run
// oscillated between cargo-sort removing it and alef re-adding it.
// The e2e rust Cargo.toml must use the plain hash::header (no issues_url).
let out = render_cargo_toml(
"my-crate",
"my_crate",
"../../crates/my-crate",
false,
false,
false,
false,
false,
false,
DependencyMode::Local,
None,
&[],
);
assert!(
!out.contains("Issues & docs:"),
"e2e Cargo.toml must not contain 'Issues & docs:' — cargo-sort strips it, \
causing prek to loop forever:\n{out}"
);
}
#[test]
fn render_cargo_toml_deps_are_alphabetically_sorted() {
// Regression: alef emitted deps in code order (consumer crate first,
// then serde_json, axum, serde, walkdir, tokio-stream, tokio).
// cargo-sort rewrites unsorted blocks causing prek to oscillate.
let out = render_cargo_toml(
"my-crate",
"my_crate",
"../../crates/my-crate",
true, // needs_serde_json
true, // needs_mock_server
false, // needs_http_tests
true, // needs_tokio
false, // needs_tower_http
false, // needs_anyhow
DependencyMode::Local,
Some("1.2.3"),
&[],
);
// Extract the [dependencies] block and verify key order.
let deps_start = out.find("[dependencies]\n").expect("missing [dependencies]");
let deps_block = &out[deps_start + "[dependencies]\n".len()..];
let dep_keys: Vec<&str> = deps_block
.lines()
.take_while(|l| !l.starts_with('['))
.filter(|l| !l.is_empty())
.map(|l| l.split_once(" = ").map(|(k, _)| k).unwrap_or(l))
.collect();
let mut sorted = dep_keys.clone();
sorted.sort();
assert_eq!(
dep_keys, sorted,
"dependencies must be in alphabetical order (cargo-sort canonical), got:\n{out}"
);
}
#[test]
fn render_cargo_toml_machete_section_precedes_bin_section() {
// Regression: older alef put [package.metadata.cargo-machete] at the
// end of the file. cargo-sort moves it to immediately after [package],
// before [[bin]], causing prek to oscillate.
let out = render_cargo_toml(
"my-crate",
"my_crate",
"../../crates/my-crate",
true, // needs_serde_json → triggers machete section
true, // needs_mock_server → triggers [[bin]] section
false, // needs_http_tests
true, // needs_tokio
false, // needs_tower_http
false, // needs_anyhow
DependencyMode::Local,
Some("1.2.3"),
&[],
);
let machete_pos = out
.find("[package.metadata.cargo-machete]")
.expect("[package.metadata.cargo-machete] missing");
let bin_pos = out.find("[[bin]]").expect("[[bin]] missing");
let deps_pos = out.find("[dependencies]").expect("[dependencies] missing");
assert!(
machete_pos < bin_pos,
"[package.metadata.cargo-machete] must precede [[bin]] — cargo-sort canonical order:\n{out}"
);
assert!(bin_pos < deps_pos, "[[bin]] must precede [dependencies]:\n{out}");
// Must not appear at the end (after [dependencies]).
assert!(
machete_pos < deps_pos,
"[package.metadata.cargo-machete] must not appear after [dependencies]:\n{out}"
);
}
}