Skip to content

Commit 02cf29d

Browse files
Feat/test agent (#3)
* feat: add integration test * feat: add main * feat: add .gitignore and cargo.toml * tests: remove QEMU boot test, add builder validation * fix: pipeline
1 parent be228b2 commit 02cf29d

File tree

1 file changed

+322
-0
lines changed

1 file changed

+322
-0
lines changed

tests/integration_test.rs

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
use initramfs_builder::{Compression, InitramfsBuilder};
2+
use std::io::Read;
3+
use std::path::PathBuf;
4+
use tokio::fs;
5+
6+
// Create a basic init script for testing
7+
async fn create_test_init_script(dir: &std::path::Path) -> PathBuf {
8+
let init_path = dir.join("init.sh");
9+
let content = r#"#!/bin/sh
10+
mount -t proc proc /proc
11+
mount -t sysfs sysfs /sys
12+
exec /bin/sh
13+
"#;
14+
fs::write(&init_path, content).await.unwrap();
15+
init_path
16+
}
17+
18+
// Create a dummy binary to inject
19+
async fn create_test_binary(dir: &std::path::Path, name: &str) -> PathBuf {
20+
let path = dir.join(name);
21+
fs::write(&path, b"#!/bin/sh\necho hello\n").await.unwrap();
22+
#[cfg(unix)]
23+
{
24+
use std::os::unix::fs::PermissionsExt;
25+
let perms = std::fs::Permissions::from_mode(0o755);
26+
std::fs::set_permissions(&path, perms).unwrap();
27+
}
28+
path
29+
}
30+
31+
// Parse CPIO newc format and extract entries
32+
fn parse_cpio_entries(data: &[u8]) -> Vec<(String, u32, usize)> {
33+
let mut entries = Vec::new();
34+
let mut offset = 0;
35+
36+
while offset + 110 <= data.len() {
37+
let header = &data[offset..offset + 110];
38+
let magic = std::str::from_utf8(&header[0..6]).unwrap_or("");
39+
if magic != "070701" {
40+
break;
41+
}
42+
43+
let mode = u32::from_str_radix(std::str::from_utf8(&header[14..22]).unwrap_or("0"), 16)
44+
.unwrap_or(0);
45+
let filesize =
46+
usize::from_str_radix(std::str::from_utf8(&header[54..62]).unwrap_or("0"), 16)
47+
.unwrap_or(0);
48+
let namesize =
49+
usize::from_str_radix(std::str::from_utf8(&header[94..102]).unwrap_or("0"), 16)
50+
.unwrap_or(0);
51+
52+
let name_start = offset + 110;
53+
if name_start + namesize > data.len() {
54+
break;
55+
}
56+
57+
let name = std::str::from_utf8(&data[name_start..name_start + namesize - 1])
58+
.unwrap_or("")
59+
.to_string();
60+
61+
if name == "TRAILER!!!" {
62+
break;
63+
}
64+
65+
let header_plus_name = 110 + namesize;
66+
let name_padding = (4 - (header_plus_name % 4)) % 4;
67+
let data_start = name_start + namesize + name_padding;
68+
69+
let data_padding = (4 - (filesize % 4)) % 4;
70+
offset = data_start + filesize + data_padding;
71+
72+
entries.push((name, mode, filesize));
73+
}
74+
75+
entries
76+
}
77+
78+
fn decompress_gzip(data: &[u8]) -> Vec<u8> {
79+
let mut decoder = flate2::read::GzDecoder::new(data);
80+
let mut out = Vec::new();
81+
decoder.read_to_end(&mut out).unwrap();
82+
out
83+
}
84+
85+
// Test 1: CPIO content validation
86+
#[tokio::test]
87+
async fn test_build_produces_valid_cpio() -> anyhow::Result<()> {
88+
let tmp = tempfile::tempdir()?;
89+
let output = tmp.path().join("output.cpio.gz");
90+
91+
let result = InitramfsBuilder::new()
92+
.image("debian:stable-slim")
93+
.compression(Compression::Gzip)
94+
.build(&output)
95+
.await?;
96+
97+
let metadata = std::fs::metadata(&output)?;
98+
assert!(metadata.len() > 0);
99+
assert!(result.entries > 0);
100+
101+
let compressed = std::fs::read(&output)?;
102+
let raw_cpio = decompress_gzip(&compressed);
103+
let entries = parse_cpio_entries(&raw_cpio);
104+
105+
assert!(!entries.is_empty());
106+
107+
let paths: Vec<&str> = entries.iter().map(|(p, _, _)| p.as_str()).collect();
108+
assert!(paths.iter().any(|p| *p == "bin"
109+
|| p.starts_with("bin/")
110+
|| *p == "usr/bin"
111+
|| p.starts_with("usr/bin/")));
112+
assert!(paths.iter().any(|p| *p == "etc" || p.starts_with("etc/")));
113+
114+
println!("CPIO contains {} entries", entries.len());
115+
Ok(())
116+
}
117+
118+
// Test 2: BuildResult metadata
119+
#[tokio::test]
120+
async fn test_build_result_metadata() -> anyhow::Result<()> {
121+
let tmp = tempfile::tempdir()?;
122+
let output = tmp.path().join("output.cpio.gz");
123+
let init_script = create_test_init_script(tmp.path()).await;
124+
let inject_file = create_test_binary(tmp.path(), "my-tool").await;
125+
126+
let result = InitramfsBuilder::new()
127+
.image("debian:stable-slim")
128+
.compression(Compression::Gzip)
129+
.inject(&inject_file, "/usr/bin/my-tool")
130+
.init_script(&init_script)
131+
.build(&output)
132+
.await?;
133+
134+
assert!(result.entries > 0);
135+
assert!(result.uncompressed_size > 0);
136+
assert!(result.compressed_size > 0);
137+
assert!(result.compressed_size < result.uncompressed_size);
138+
assert_eq!(result.compression, Compression::Gzip);
139+
assert_eq!(result.injected_files, 1);
140+
assert!(result.has_custom_init);
141+
142+
println!(
143+
"BuildResult: {} entries, {}B compressed, {}B uncompressed",
144+
result.entries, result.compressed_size, result.uncompressed_size
145+
);
146+
Ok(())
147+
}
148+
149+
// Test 3: Init script injection
150+
#[tokio::test]
151+
async fn test_init_script_injection() -> anyhow::Result<()> {
152+
let tmp = tempfile::tempdir()?;
153+
let output = tmp.path().join("output.cpio.gz");
154+
let init_script = create_test_init_script(tmp.path()).await;
155+
156+
let result = InitramfsBuilder::new()
157+
.image("debian:stable-slim")
158+
.compression(Compression::Gzip)
159+
.init_script(&init_script)
160+
.build(&output)
161+
.await?;
162+
163+
assert!(result.has_custom_init);
164+
165+
let compressed = std::fs::read(&output)?;
166+
let raw_cpio = decompress_gzip(&compressed);
167+
let entries = parse_cpio_entries(&raw_cpio);
168+
169+
let init_entry = entries.iter().find(|(path, _, _)| path == "init");
170+
assert!(init_entry.is_some());
171+
172+
let (_, mode, size) = init_entry.unwrap();
173+
assert!(
174+
mode & 0o100 != 0,
175+
"init should be executable, got mode {:o}",
176+
mode
177+
);
178+
assert!(*size > 0);
179+
180+
println!("init entry: mode={:o}, size={}", mode, size);
181+
Ok(())
182+
}
183+
184+
// Test 4: File injection
185+
#[tokio::test]
186+
async fn test_file_injection() -> anyhow::Result<()> {
187+
let tmp = tempfile::tempdir()?;
188+
let output = tmp.path().join("output.cpio.gz");
189+
let inject_file = create_test_binary(tmp.path(), "custom-tool").await;
190+
191+
let result = InitramfsBuilder::new()
192+
.image("debian:stable-slim")
193+
.compression(Compression::Gzip)
194+
.inject(&inject_file, "/usr/bin/custom-tool")
195+
.build(&output)
196+
.await?;
197+
198+
assert_eq!(result.injected_files, 1);
199+
200+
let compressed = std::fs::read(&output)?;
201+
let raw_cpio = decompress_gzip(&compressed);
202+
let entries = parse_cpio_entries(&raw_cpio);
203+
204+
let injected = entries
205+
.iter()
206+
.find(|(path, _, _)| path == "usr/bin/custom-tool");
207+
assert!(
208+
injected.is_some(),
209+
"CPIO should contain 'usr/bin/custom-tool'"
210+
);
211+
212+
let (_, mode, size) = injected.unwrap();
213+
assert!(mode & 0o100 != 0);
214+
assert!(*size > 0);
215+
216+
println!("Injected file: mode={:o}, size={}", mode, size);
217+
Ok(())
218+
}
219+
220+
// Test 5: Compression modes
221+
#[tokio::test]
222+
async fn test_compression_modes() -> anyhow::Result<()> {
223+
let tmp = tempfile::tempdir()?;
224+
225+
let modes = vec![
226+
("gzip", Compression::Gzip, "output.cpio.gz"),
227+
("zstd", Compression::Zstd, "output.cpio.zst"),
228+
("none", Compression::None, "output.cpio"),
229+
];
230+
231+
let mut sizes: Vec<(String, u64)> = Vec::new();
232+
233+
for (label, compression, filename) in &modes {
234+
let output = tmp.path().join(filename);
235+
236+
let result = InitramfsBuilder::new()
237+
.image("debian:stable-slim")
238+
.compression(*compression)
239+
.build(&output)
240+
.await?;
241+
242+
let file_size = std::fs::metadata(&output)?.len();
243+
assert!(file_size > 0);
244+
assert_eq!(result.compression, *compression);
245+
246+
sizes.push((label.to_string(), file_size));
247+
println!("{}: {} bytes", label, file_size);
248+
}
249+
250+
let none_size = sizes.iter().find(|(l, _)| l == "none").unwrap().1;
251+
let gzip_size = sizes.iter().find(|(l, _)| l == "gzip").unwrap().1;
252+
assert!(none_size > gzip_size);
253+
254+
Ok(())
255+
}
256+
257+
// Test 6: Exclude patterns
258+
#[tokio::test]
259+
async fn test_exclude_patterns() -> anyhow::Result<()> {
260+
let tmp = tempfile::tempdir()?;
261+
262+
let output_full = tmp.path().join("full.cpio.gz");
263+
let result_full = InitramfsBuilder::new()
264+
.image("debian:stable-slim")
265+
.compression(Compression::Gzip)
266+
.build(&output_full)
267+
.await?;
268+
269+
let output_excluded = tmp.path().join("excluded.cpio.gz");
270+
let result_excluded = InitramfsBuilder::new()
271+
.image("debian:stable-slim")
272+
.compression(Compression::Gzip)
273+
.exclude(&["usr/share/doc/*", "usr/share/man/*", "var/cache/*"])
274+
.build(&output_excluded)
275+
.await?;
276+
277+
assert!(result_excluded.entries < result_full.entries);
278+
assert!(result_excluded.compressed_size < result_full.compressed_size);
279+
280+
println!(
281+
"Full: {} entries, Excluded: {} entries",
282+
result_full.entries, result_excluded.entries
283+
);
284+
Ok(())
285+
}
286+
287+
// Test 7: Reproducibility
288+
#[tokio::test]
289+
async fn test_reproducibility() -> anyhow::Result<()> {
290+
let tmp = tempfile::tempdir()?;
291+
292+
let output1 = tmp.path().join("build1.cpio.gz");
293+
let result1 = InitramfsBuilder::new()
294+
.image("debian:stable-slim")
295+
.compression(Compression::Gzip)
296+
.build(&output1)
297+
.await?;
298+
299+
let output2 = tmp.path().join("build2.cpio.gz");
300+
let result2 = InitramfsBuilder::new()
301+
.image("debian:stable-slim")
302+
.compression(Compression::Gzip)
303+
.build(&output2)
304+
.await?;
305+
306+
assert_eq!(result1.entries, result2.entries);
307+
308+
let cpio1 = decompress_gzip(&std::fs::read(&output1)?);
309+
let cpio2 = decompress_gzip(&std::fs::read(&output2)?);
310+
let entries1 = parse_cpio_entries(&cpio1);
311+
let entries2 = parse_cpio_entries(&cpio2);
312+
313+
let paths1: Vec<&str> = entries1.iter().map(|(p, _, _)| p.as_str()).collect();
314+
let paths2: Vec<&str> = entries2.iter().map(|(p, _, _)| p.as_str()).collect();
315+
assert_eq!(paths1, paths2);
316+
317+
println!(
318+
"Reproducibility: both builds have {} entries with identical paths",
319+
result1.entries
320+
);
321+
Ok(())
322+
}

0 commit comments

Comments
 (0)