feat(bake): implement full bake functionality with template-based script
generation and pipeline scheduling - Add benchmark infrastructure (Criterion) for parser performance testing - Enhance decorator parsing with new types: Pipe, Parallel, Daemon, Health, and Unknown - Improve parser to track line numbers and handle nested braces correctly - Add implicit `@after` dependencies for pipeline functions in topological sort - Introduce `use_template` flag in builder to support trivial script execution - Update bake function signature to accept Cli parameters and privilege detection - Refactor error handling with structured `ScriptGenerationFailed` variant - Extend integration tests for privilege dropping, engine execution, and bake phase - Comment out daemon and apt modules temporarily for refactoring
This commit is contained in:
Generated
+229
-1
@@ -20,6 +20,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
@@ -437,6 +443,12 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.51"
|
||||
@@ -487,6 +499,33 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.53"
|
||||
@@ -616,6 +655,61 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools 0.10.5",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -974,6 +1068,17 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -1334,12 +1439,32 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
@@ -1773,6 +1898,12 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "openssh"
|
||||
version = "0.11.6"
|
||||
@@ -1962,6 +2093,34 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters-backend"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
|
||||
|
||||
[[package]]
|
||||
name = "plotters-svg"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
|
||||
dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
@@ -2060,7 +2219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -2119,6 +2278,26 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -2296,6 +2475,15 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.28"
|
||||
@@ -2703,6 +2891,16 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.48.0"
|
||||
@@ -3051,6 +3249,16 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
@@ -3120,6 +3328,16 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.4"
|
||||
@@ -3154,6 +3372,15 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
@@ -3401,6 +3628,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"config",
|
||||
"criterion",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"lazy_static",
|
||||
|
||||
@@ -48,3 +48,10 @@ libc = "0.2"
|
||||
os_info = "3.14.0"
|
||||
privdrop = "0.5.6"
|
||||
which = "8.0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5"
|
||||
|
||||
[[bench]]
|
||||
name = "parser_bench"
|
||||
harness = false
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
|
||||
OUTPUT_DIR = "/tmp"
|
||||
DECORATORS = [
|
||||
"# @pipeline",
|
||||
"# @fallible",
|
||||
"# @parallel",
|
||||
"# @export",
|
||||
"# @timeout(60)",
|
||||
"# @retry(3, 5)",
|
||||
"# @if(condition)",
|
||||
"# @after(dep)",
|
||||
"# @pipe(step1, step2)",
|
||||
"# @health(/health)",
|
||||
"# @loop(10)",
|
||||
]
|
||||
FUNCTION_BODIES = [
|
||||
"echo 'Processing...';",
|
||||
"local var=$(date +%s);",
|
||||
"if [ -f /tmp/test ]; then echo 'exists'; fi;",
|
||||
"for i in $(seq 1 10); do echo $i; done;",
|
||||
"case $1 in start) echo 'starting';; stop) echo 'stopping';; esac;",
|
||||
"read -r line < /dev/stdin;",
|
||||
"export PATH=$PATH:/usr/local/bin;",
|
||||
"set -e; set -u;",
|
||||
"trap 'echo error' ERR;",
|
||||
"cd /tmp || exit 1;",
|
||||
"local result=$(grep -r 'pattern' /var/log/ 2>/dev/null);",
|
||||
'for f in /tmp/*.tmp; do rm -f "$f"; done',
|
||||
"if [[ $var -gt 100 ]]; then echo 'large'; fi;",
|
||||
'while read -r line; do echo "$line"; done < /etc/passwd;',
|
||||
'select opt in a b c; do echo "Selected $opt"; break; done;',
|
||||
]
|
||||
|
||||
|
||||
def random_string(length=8):
|
||||
return "".join(random.choices(string.ascii_letters + string.digits, k=length))
|
||||
|
||||
|
||||
def generate_function(body_lines):
|
||||
lines = []
|
||||
func_name = f"func_{random_string(6)}"
|
||||
for _ in range(random.randint(0, 3)):
|
||||
lines.append(random.choice(DECORATORS))
|
||||
lines.append(f"{func_name}() {{")
|
||||
for _ in range(body_lines):
|
||||
lines.append(" " + random.choice(FUNCTION_BODIES))
|
||||
lines.append("}")
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
|
||||
def generate_script(target_lines, num_functions, target_bytes):
|
||||
lines = []
|
||||
total_lines = 0
|
||||
body_per_func = max(1, (target_lines - num_functions * 3) // num_functions)
|
||||
|
||||
for _ in range(num_functions):
|
||||
func_lines = generate_function(body_per_func)
|
||||
lines.extend(func_lines)
|
||||
total_lines += len(func_lines)
|
||||
|
||||
while (
|
||||
total_lines < target_lines
|
||||
or len("\n".join(lines).encode("utf-8")) < target_bytes
|
||||
):
|
||||
lines.append(random.choice(FUNCTION_BODIES))
|
||||
total_lines += 1
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
sizes = [
|
||||
("small", 1000, 10, 50_000),
|
||||
("medium", 5000, 50, 500_000),
|
||||
("large", 10000, 100, 950_000),
|
||||
]
|
||||
for name, lines, funcs, target_bytes in sizes:
|
||||
script = generate_script(lines, funcs, target_bytes)
|
||||
filepath = os.path.join(OUTPUT_DIR, f"bench_{name}.sh")
|
||||
with open(filepath, "w") as f:
|
||||
f.write(script)
|
||||
actual_bytes = len(script.encode("utf-8"))
|
||||
actual_lines = len(script.splitlines())
|
||||
print(f"Generated {filepath}: lines={actual_lines}, bytes={actual_bytes}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
fn parse_large_script(c: &mut Criterion) {
|
||||
let script_path = Path::new("/tmp/bench_large.sh");
|
||||
|
||||
let script_content = fs::read_to_string(script_path)
|
||||
.expect("Failed to read benchmark script. Run `python3 benches/generate.py` first.");
|
||||
|
||||
let byte_size = script_content.len();
|
||||
let line_count = script_content.lines().count();
|
||||
|
||||
println!("Benchmark file: {} lines, {} bytes", line_count, byte_size);
|
||||
|
||||
c.bench_function("parse_script_large", |b| {
|
||||
b.iter(|| {
|
||||
let result =
|
||||
workshop_baker::bake::parser::parse_script(black_box(script_content.clone()));
|
||||
black_box(result)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_medium_script(c: &mut Criterion) {
|
||||
let script_path = Path::new("/tmp/bench_medium.sh");
|
||||
|
||||
let script_content = fs::read_to_string(script_path)
|
||||
.expect("Failed to read benchmark script. Run `python3 benches/generate.py` first.");
|
||||
|
||||
c.bench_function("parse_script_medium", |b| {
|
||||
b.iter(|| {
|
||||
let result =
|
||||
workshop_baker::bake::parser::parse_script(black_box(script_content.clone()));
|
||||
black_box(result)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_small_script(c: &mut Criterion) {
|
||||
let script_path = Path::new("/tmp/bench_small.sh");
|
||||
|
||||
let script_content = fs::read_to_string(script_path)
|
||||
.expect("Failed to read benchmark script. Run `python3 benches/generate.py` first.");
|
||||
|
||||
c.bench_function("parse_script_small", |b| {
|
||||
b.iter(|| {
|
||||
let result =
|
||||
workshop_baker::bake::parser::parse_script(black_box(script_content.clone()));
|
||||
black_box(result)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
parse_small_script,
|
||||
parse_medium_script,
|
||||
parse_large_script
|
||||
);
|
||||
criterion_main!(benches);
|
||||
+56
-22
@@ -1,20 +1,22 @@
|
||||
use crate::prebake::security::get_drop_after;
|
||||
use crate::prebake::PrebakeConfig;
|
||||
use crate::prebake::stage::PrebakeStage;
|
||||
use std::path::PathBuf;
|
||||
use crate::error::BakeError;
|
||||
use crate::{Engine, prebake};
|
||||
use crate::cli::Cli;
|
||||
use crate::constant::DEFAULT_WORKSPACE;
|
||||
use crate::error::BakeError;
|
||||
use crate::prebake::PrebakeConfig;
|
||||
use crate::prebake::security::get_drop_after;
|
||||
use crate::prebake::stage::PrebakeStage;
|
||||
use crate::{Engine, prebake};
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod builder;
|
||||
mod decorator;
|
||||
mod parser;
|
||||
pub mod parser;
|
||||
mod schedule;
|
||||
|
||||
pub async fn bake(
|
||||
script_path: PathBuf,
|
||||
bake_base_path: PathBuf,
|
||||
prebake_path: PathBuf,
|
||||
script_path: &PathBuf,
|
||||
bake_base_path: &PathBuf,
|
||||
prebake_path: &PathBuf,
|
||||
cli: &Cli,
|
||||
) -> Result<(), BakeError> {
|
||||
let script_content = std::fs::read_to_string(script_path)?;
|
||||
let prebake_content = std::fs::read_to_string(prebake_path)?;
|
||||
@@ -25,14 +27,22 @@ pub async fn bake(
|
||||
})?;
|
||||
let workspace = get_workspace(&prebake);
|
||||
let functions = parser::parse_script(script_content)?;
|
||||
let sorted_functions = schedule::sort_function(functions)?;
|
||||
|
||||
let has_pipeline = functions.iter().any(|f| {
|
||||
f.decorators
|
||||
.iter()
|
||||
.any(|d| matches!(d, decorator::Decorator::Pipeline))
|
||||
});
|
||||
let trivial = false;
|
||||
let use_template = true;
|
||||
|
||||
let engine = Engine::new();
|
||||
let mut ctx = crate::ExecutionContext {
|
||||
task_id: format!("{}-{}-bake", cli.pipeline, cli.build_id),
|
||||
pipeline_name: cli.pipeline.clone(),
|
||||
username: cli.username.clone(),
|
||||
dry_run: cli.dry_run,
|
||||
privileged: what,
|
||||
privileged: privileged(&prebake),
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(env) = &prebake.envvars {
|
||||
@@ -40,14 +50,41 @@ pub async fn bake(
|
||||
ctx.env_vars.extend(prebake.clone());
|
||||
}
|
||||
}
|
||||
for func in sorted_functions {
|
||||
builder::build_script(func.name, func.body, &bake_base_path, &workspace, None);
|
||||
|
||||
if !has_pipeline || trivial {
|
||||
let script_body = functions
|
||||
.iter()
|
||||
.map(|f| f.body.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let script = builder::build_script(
|
||||
"bake".to_string(),
|
||||
script_body,
|
||||
&bake_base_path,
|
||||
&workspace,
|
||||
None,
|
||||
use_template,
|
||||
)?;
|
||||
engine.execute_script(&script, &ctx, &None).await?;
|
||||
} else {
|
||||
let sorted_functions = schedule::sort_function(functions)?;
|
||||
for func in sorted_functions {
|
||||
let script = builder::build_script(
|
||||
func.name,
|
||||
func.body,
|
||||
&bake_base_path,
|
||||
&workspace,
|
||||
None,
|
||||
use_template,
|
||||
)?;
|
||||
engine.execute_script(&script, &ctx, &None).await?;
|
||||
}
|
||||
}
|
||||
todo!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_workspace(config: &PrebakeConfig) -> PathBuf {
|
||||
config
|
||||
fn get_workspace(prebake_config: &PrebakeConfig) -> PathBuf {
|
||||
prebake_config
|
||||
.bootstrap
|
||||
.as_ref()
|
||||
.and_then(|b| b.workspace.as_ref())
|
||||
@@ -67,9 +104,6 @@ fn privileged(prebake_config: &PrebakeConfig) -> bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let drop_after = get_drop_after(&prebake_config.security)
|
||||
.unwrap_or(PrebakeStage::default());
|
||||
|
||||
PrebakeStage::Ready == drop_after
|
||||
let drop_after = get_drop_after(&prebake_config.security).unwrap_or(PrebakeStage::default());
|
||||
PrebakeStage::Never == drop_after
|
||||
}
|
||||
|
||||
@@ -1,168 +1,112 @@
|
||||
use std::path::Path;
|
||||
use minijinja::{Environment, context};
|
||||
use minijinja::{context, Environment};
|
||||
use std::fs::Permissions;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::error::BakeError;
|
||||
|
||||
/// Template for use_template=false, which just executes the main body without any wrapping.
|
||||
const TRIVIAL_TEMPLATE: &'static str = "{{ main }}";
|
||||
|
||||
pub fn build_script(
|
||||
name: String,
|
||||
body: String,
|
||||
bake_base: &Path,
|
||||
target_dir: &Path,
|
||||
temp_path: Option<PathBuf>,
|
||||
use_template: bool,
|
||||
) -> Result<PathBuf, BakeError> {
|
||||
let temp_path = temp_path.unwrap_or_else(|| PathBuf::from("/tmp"));
|
||||
let filename = format!("bakefn_{}.sh", name);
|
||||
let target = target_dir.join(filename);
|
||||
|
||||
let bake_base_content = std::fs::read_to_string(&bake_base).map_err(|e| {
|
||||
BakeError::ScriptGenerationFailed(bake_base.display().to_string(), e.to_string())
|
||||
})?;
|
||||
let bake_base_content = if use_template {
|
||||
std::fs::read_to_string(&bake_base).map_err(|e| BakeError::ScriptGenerationFailed {
|
||||
script: bake_base.display().to_string(),
|
||||
reason: e.to_string(),
|
||||
})?
|
||||
} else {
|
||||
TRIVIAL_TEMPLATE.to_string()
|
||||
};
|
||||
|
||||
let mut env = Environment::new();
|
||||
env.add_template("script", bake_base_content.as_str())?;
|
||||
let script = env.get_template("script")?;
|
||||
let rendered = script.render(context!(main => body))?;
|
||||
|
||||
let mut temp_file = NamedTempFile::new_in(&temp_path).map_err(|e| {
|
||||
BakeError::ScriptGenerationFailed(temp_path.display().to_string(), e.to_string())
|
||||
})?;
|
||||
let mut temp_file =
|
||||
NamedTempFile::new_in(&temp_path).map_err(|e| BakeError::ScriptGenerationFailed {
|
||||
script: temp_path.display().to_string(),
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
temp_file
|
||||
.as_file_mut()
|
||||
.set_permissions(Permissions::from_mode(0o700))
|
||||
.map_err(|e| {
|
||||
BakeError::ScriptGenerationFailed(temp_path.display().to_string(), e.to_string())
|
||||
.map_err(|e| BakeError::ScriptGenerationFailed {
|
||||
script: temp_path.display().to_string(),
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
std::fs::write(&temp_file, rendered).map_err(|e| {
|
||||
BakeError::ScriptGenerationFailed(temp_path.display().to_string(), e.to_string())
|
||||
})?;
|
||||
temp_file.persist(&target).map_err(|e| {
|
||||
BakeError::ScriptGenerationFailed(target.display().to_string(), e.error.to_string())
|
||||
std::fs::write(&temp_file, rendered).map_err(|e| BakeError::ScriptGenerationFailed {
|
||||
script: temp_path.display().to_string(),
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
temp_file
|
||||
.persist(&target)
|
||||
.map_err(|e| BakeError::ScriptGenerationFailed {
|
||||
script: target.display().to_string(),
|
||||
reason: e.error.to_string(),
|
||||
})?;
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
#[cfg(test_disabled)]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn create_test_bake_base(content: &str) -> (PathBuf, tempfile::TempDir) {
|
||||
#[test]
|
||||
fn test_build_script_with_template() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("bake_base.sh");
|
||||
fs::write(&file_path, content).unwrap();
|
||||
(file_path, temp_dir)
|
||||
}
|
||||
let bake_base = temp_dir.path().join("bake_base.sh");
|
||||
|
||||
#[test]
|
||||
fn test_build_script_success() {
|
||||
let test_name = "test_success";
|
||||
let test_body = "echo 'hello world'";
|
||||
let bake_base_content = "#!/bin/bash\nmain() { {{ main }} }\nmain";
|
||||
let (bake_base_path, _temp_dir) = create_test_bake_base(bake_base_content);
|
||||
std::fs::write(&bake_base, "{{ main }}\n").unwrap();
|
||||
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let temp_path = temp_dir.path().to_str().unwrap().to_string();
|
||||
|
||||
let target_path = build_script(
|
||||
test_name.to_string(),
|
||||
test_body.to_string(),
|
||||
bake_base_path,
|
||||
Some(temp_path.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(target_path.exists());
|
||||
assert_eq!(
|
||||
target_path.file_name().unwrap().to_str().unwrap(),
|
||||
format!("bakefn_{}.sh", test_name)
|
||||
);
|
||||
let script_content = fs::read_to_string(&target_path).unwrap();
|
||||
assert!(script_content.contains("#!/bin/bash"));
|
||||
assert!(script_content.contains(&format!("main() {{ {} }}", test_body)));
|
||||
assert!(script_content.contains("main"));
|
||||
let metadata = fs::metadata(&target_path).unwrap();
|
||||
assert_eq!(metadata.permissions().mode() & 0o777, 0o700);
|
||||
|
||||
fs::remove_file(&target_path).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_script_default_temp_path() {
|
||||
let test_name = "test_default_path";
|
||||
let test_body = "echo 'default path'";
|
||||
let bake_base_content = "#!/bin/bash\necho {{ main }}\n";
|
||||
let (bake_base_path, _temp_dir) = create_test_bake_base(bake_base_content);
|
||||
|
||||
let temp_path = std::env::temp_dir().to_str().unwrap().to_string();
|
||||
let target_path = build_script(
|
||||
test_name.to_string(),
|
||||
test_body.to_string(),
|
||||
bake_base_path,
|
||||
Some(temp_path.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(target_path.exists());
|
||||
assert_eq!(
|
||||
target_path.file_name().unwrap().to_str().unwrap(),
|
||||
format!("bakefn_{}.sh", test_name)
|
||||
);
|
||||
|
||||
fs::remove_file(&target_path).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_script_bake_base_not_found() {
|
||||
let non_exist_path = PathBuf::from("/non/exist/bake_base.sh");
|
||||
let body = "echo 'hello world'".to_string();
|
||||
let result = build_script(
|
||||
"test_not_found".to_string(),
|
||||
"echo 'test'".to_string(),
|
||||
non_exist_path,
|
||||
Some("/tmp".to_string()),
|
||||
"test".to_string(),
|
||||
body,
|
||||
&bake_base,
|
||||
temp_dir.path(),
|
||||
None,
|
||||
true,
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result.is_ok());
|
||||
let script_path = result.unwrap();
|
||||
let content = std::fs::read_to_string(&script_path).unwrap();
|
||||
assert!(content.contains("echo 'hello world'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_script_invalid_template() {
|
||||
let invalid_template = "{{";
|
||||
let (bake_base_path, _temp_dir) = create_test_bake_base(invalid_template);
|
||||
fn test_build_script_trivial_mode() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let body = "echo 'direct output'".to_string();
|
||||
|
||||
let result = build_script(
|
||||
"test_invalid_template".to_string(),
|
||||
"echo 'test'".to_string(),
|
||||
bake_base_path,
|
||||
Some("/tmp".to_string()),
|
||||
"trivial".to_string(),
|
||||
body,
|
||||
Path::new("/nonexistent"),
|
||||
temp_dir.path(),
|
||||
None,
|
||||
false,
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_script_permissions() {
|
||||
let bake_base_content = "#!/bin/bash\necho {{ main }}\n";
|
||||
let (bake_base_path, _temp_dir) = create_test_bake_base(bake_base_content);
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let temp_path = temp_dir.path().to_str().unwrap().to_string();
|
||||
|
||||
let target_path = build_script(
|
||||
"test_permissions".to_string(),
|
||||
"echo 'permissions'".to_string(),
|
||||
bake_base_path,
|
||||
Some(temp_path),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let metadata = fs::metadata(&target_path).unwrap();
|
||||
let mode = metadata.permissions().mode() & 0o777;
|
||||
assert_eq!(mode, 0o700);
|
||||
|
||||
fs::remove_file(&target_path).ok();
|
||||
assert!(result.is_ok());
|
||||
let script_path = result.unwrap();
|
||||
let content = std::fs::read_to_string(&script_path).unwrap();
|
||||
assert_eq!(content.trim(), "echo 'direct output'");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use crate::error::BakeError;
|
||||
|
||||
@@ -7,10 +9,15 @@ lazy_static! {
|
||||
// "# @decorator" or "# @decorator(parameters)"
|
||||
static ref DECORATOR_REGEX: Regex = Regex::new(r"^\s*#\s+@(\w+)\s?(\((.*)\))?").unwrap()
|
||||
;
|
||||
static ref UNEXPECTED_ARGUMENT: &'static str = "decorator does not accept arguments";
|
||||
static ref MISSING_ARGUMENT: &'static str = "decorator requires an argument";
|
||||
static ref UNKNOWN_DECORATOR: &'static str = "unknown decorator";
|
||||
static ref INVALID_ARGUMENT: &'static str = "invalid argument";
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Decorator {
|
||||
Unknown, // for general error reporting when the decorator name is not recognized
|
||||
Pipeline,
|
||||
If(String),
|
||||
Fallible,
|
||||
@@ -19,6 +26,30 @@ pub enum Decorator {
|
||||
Timeout(u32),
|
||||
Retry(usize, u32),
|
||||
Export,
|
||||
Pipe(Vec<String>),
|
||||
Parallel,
|
||||
Daemon,
|
||||
Health(String),
|
||||
}
|
||||
|
||||
impl Display for Decorator {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Decorator::Unknown => write!(f, "@<unknown>"),
|
||||
Decorator::Pipeline => write!(f, "@pipeline"),
|
||||
Decorator::If(cond) => write!(f, "@if({})", cond),
|
||||
Decorator::Fallible => write!(f, "@fallible"),
|
||||
Decorator::Loop(count) => write!(f, "@loop({})", count),
|
||||
Decorator::After(func) => write!(f, "@after({})", func),
|
||||
Decorator::Timeout(timeout) => write!(f, "@timeout({})", timeout),
|
||||
Decorator::Retry(count, delay) => write!(f, "@retry({}, {})", count, delay),
|
||||
Decorator::Export => write!(f, "@export"),
|
||||
Decorator::Pipe(funcs) => write!(f, "@pipe({})", funcs.join(", ")),
|
||||
Decorator::Parallel => write!(f, "@parallel"),
|
||||
Decorator::Daemon => write!(f, "@daemon"),
|
||||
Decorator::Health(check) => write!(f, "@health({})", check),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_decorator(line: &str, line_num: usize) -> Result<Option<Decorator>, BakeError> {
|
||||
@@ -28,66 +59,782 @@ pub fn parse_decorator(line: &str, line_num: usize) -> Result<Option<Decorator>,
|
||||
};
|
||||
|
||||
let name = caps.get(1).unwrap().as_str();
|
||||
let has_arg = caps.get(2).is_some();
|
||||
let args = caps.get(3).map(|m| m.as_str()).unwrap_or("");
|
||||
|
||||
let decorator_noarg = |d: Decorator| -> Result<Option<Decorator>, BakeError> {
|
||||
if has_arg {
|
||||
Err(BakeError::DecoratorParseError {
|
||||
decorator: name.to_string(),
|
||||
line_num,
|
||||
reason: UNEXPECTED_ARGUMENT.to_string(),
|
||||
})
|
||||
} else {
|
||||
Ok(Some(d))
|
||||
}
|
||||
};
|
||||
|
||||
let required_arg = || -> Result<&str, BakeError> {
|
||||
if !has_arg || args.is_empty() {
|
||||
Err(BakeError::DecoratorParseError {
|
||||
decorator: name.to_string(),
|
||||
line_num,
|
||||
reason: MISSING_ARGUMENT.to_string(),
|
||||
})
|
||||
} else {
|
||||
Ok(args)
|
||||
}
|
||||
};
|
||||
|
||||
match name {
|
||||
"pipeline" => Ok(Some(Decorator::Pipeline)),
|
||||
"if" => Ok(Some(Decorator::If(args.to_string()))),
|
||||
"fallible" => Ok(Some(Decorator::Fallible)),
|
||||
"loop" => {
|
||||
let count = args
|
||||
.parse()
|
||||
.map_err(|e| BakeError::DecoratorParseError {})?;
|
||||
"pipeline" => decorator_noarg(Decorator::Pipeline),
|
||||
"if" => required_arg().map(|a| Some(Decorator::If(a.to_string()))),
|
||||
"fallible" => decorator_noarg(Decorator::Fallible),
|
||||
"loop" => required_arg().and_then(|a| {
|
||||
let count = a.parse().map_err(|_e| BakeError::DecoratorParseError {
|
||||
decorator: "loop".to_string(),
|
||||
line_num,
|
||||
reason: format!("invalid loop count: {}", a),
|
||||
})?;
|
||||
Ok(Some(Decorator::Loop(count)))
|
||||
}
|
||||
"after" => Ok(Some(Decorator::After(args.to_string()))),
|
||||
"timeout" => {
|
||||
let timeout = args
|
||||
.parse()
|
||||
.map_err(|e| BakeError::DecoratorParseError {})?;
|
||||
}),
|
||||
"after" => required_arg().map(|a| Some(Decorator::After(a.to_string()))),
|
||||
"timeout" => required_arg().and_then(|a| {
|
||||
let timeout = a.parse().map_err(|_e| BakeError::DecoratorParseError {
|
||||
decorator: "timeout".to_string(),
|
||||
line_num,
|
||||
reason: format!("invalid timeout: {}", a),
|
||||
})?;
|
||||
Ok(Some(Decorator::Timeout(timeout)))
|
||||
}
|
||||
"retry" => {
|
||||
let parts: Vec<&str> = args.split(',').collect();
|
||||
Decorator::Retry(
|
||||
parts[0].trim().parse().unwrap(),
|
||||
parts[1].trim().parse().unwrap(),
|
||||
)
|
||||
}
|
||||
"export" => Decorator::Export,
|
||||
d => Err(BakeError::UnknownDecorator {}),
|
||||
}),
|
||||
"retry" => required_arg().and_then(|a| {
|
||||
let parts: Vec<&str> = a.split(',').collect();
|
||||
if parts.len() != 2 {
|
||||
Err(BakeError::DecoratorParseError {
|
||||
decorator: "retry".to_string(),
|
||||
line_num,
|
||||
reason: format!("invalid retry args: {}", a),
|
||||
})
|
||||
} else {
|
||||
let count =
|
||||
parts[0]
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_e| BakeError::DecoratorParseError {
|
||||
decorator: "retry".to_string(),
|
||||
line_num,
|
||||
reason: format!("invalid retry count: {}", parts[0]),
|
||||
})?;
|
||||
let delay_str = parts[1].trim();
|
||||
if delay_str.is_empty() {
|
||||
Err(BakeError::DecoratorParseError {
|
||||
decorator: "retry".to_string(),
|
||||
line_num,
|
||||
reason: "no delay specified".to_string(),
|
||||
})
|
||||
} else {
|
||||
let delay = delay_str
|
||||
.parse()
|
||||
.map_err(|_e| BakeError::DecoratorParseError {
|
||||
decorator: "retry".to_string(),
|
||||
line_num,
|
||||
reason: format!("invalid retry delay: {}", parts[1]),
|
||||
})?;
|
||||
Ok(Some(Decorator::Retry(count, delay)))
|
||||
}
|
||||
}
|
||||
}),
|
||||
"export" => decorator_noarg(Decorator::Export),
|
||||
"parallel" => decorator_noarg(Decorator::Parallel),
|
||||
"daemon" => decorator_noarg(Decorator::Daemon),
|
||||
"health" => required_arg().map(|a| Some(Decorator::Health(a.to_string()))),
|
||||
"pipe" => required_arg().and_then(|a| {
|
||||
let functions: Vec<String> = a
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
if functions.is_empty() {
|
||||
Err(BakeError::DecoratorParseError {
|
||||
decorator: "pipe".to_string(),
|
||||
line_num,
|
||||
reason: "no functions specified".to_string(),
|
||||
})
|
||||
} else {
|
||||
Ok(Some(Decorator::Pipe(functions)))
|
||||
}
|
||||
}),
|
||||
d => Err(BakeError::DecoratorParseError {
|
||||
decorator: d.to_string(),
|
||||
line_num,
|
||||
reason: UNKNOWN_DECORATOR.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test_disabled)]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// =====================================================================
|
||||
// Tests for Decorator Display implementation
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator() {
|
||||
fn test_decorator_display_pipeline() {
|
||||
assert_eq!(format!("{}", Decorator::Pipeline), "@pipeline");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_display_if() {
|
||||
assert_eq!(
|
||||
parse_decorator("# @if(true)"),
|
||||
Some(Decorator::If("true".to_string()))
|
||||
);
|
||||
assert_eq!(parse_decorator("# @pipeline"), Some(Decorator::Pipeline));
|
||||
assert_eq!(parse_decorator("# @fallible"), Some(Decorator::Fallible));
|
||||
assert_eq!(parse_decorator("# @loop (10)"), Some(Decorator::Loop(10)));
|
||||
assert_eq!(
|
||||
parse_decorator("# @after (build)"),
|
||||
Some(Decorator::After("build".to_string()))
|
||||
format!("{}", Decorator::If("cond".to_string())),
|
||||
"@if(cond)"
|
||||
);
|
||||
assert_eq!(
|
||||
parse_decorator("# @timeout(5)"),
|
||||
Some(Decorator::Timeout(5))
|
||||
format!("{}", Decorator::If("x > 0".to_string())),
|
||||
"@if(x > 0)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_display_fallible() {
|
||||
assert_eq!(format!("{}", Decorator::Fallible), "@fallible");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_display_loop() {
|
||||
assert_eq!(format!("{}", Decorator::Loop(5)), "@loop(5)");
|
||||
assert_eq!(format!("{}", Decorator::Loop(0)), "@loop(0)");
|
||||
assert_eq!(
|
||||
format!("{}", Decorator::Loop(usize::MAX)),
|
||||
format!("@loop({})", usize::MAX)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_display_after() {
|
||||
assert_eq!(
|
||||
format!("{}", Decorator::After("func1".to_string())),
|
||||
"@after(func1)"
|
||||
);
|
||||
assert_eq!(
|
||||
parse_decorator("# @retry (3, 2)"),
|
||||
Some(Decorator::Retry(3, 2))
|
||||
format!("{}", Decorator::After("build_step_1".to_string())),
|
||||
"@after(build_step_1)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_display_timeout() {
|
||||
assert_eq!(format!("{}", Decorator::Timeout(30)), "@timeout(30)");
|
||||
assert_eq!(format!("{}", Decorator::Timeout(0)), "@timeout(0)");
|
||||
assert_eq!(
|
||||
format!("{}", Decorator::Timeout(u32::MAX)),
|
||||
format!("@timeout({})", u32::MAX)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_display_retry() {
|
||||
assert_eq!(format!("{}", Decorator::Retry(3, 100)), "@retry(3, 100)");
|
||||
assert_eq!(format!("{}", Decorator::Retry(0, 0)), "@retry(0, 0)");
|
||||
assert_eq!(
|
||||
format!("{}", Decorator::Retry(10, 5000)),
|
||||
"@retry(10, 5000)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_display_export() {
|
||||
assert_eq!(format!("{}", Decorator::Export), "@export");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_display_pipe() {
|
||||
assert_eq!(format!("{}", Decorator::Pipe(vec![])), "@pipe()");
|
||||
assert_eq!(
|
||||
format!("{}", Decorator::Pipe(vec!["f1".to_string()])),
|
||||
"@pipe(f1)"
|
||||
);
|
||||
assert_eq!(
|
||||
parse_decorator("# @export # comment"),
|
||||
Some(Decorator::Export)
|
||||
format!(
|
||||
"{}",
|
||||
Decorator::Pipe(vec!["f1".to_string(), "f2".to_string()])
|
||||
),
|
||||
"@pipe(f1, f2)"
|
||||
);
|
||||
assert_eq!(parse_decorator("# fallible"), None);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Decorator::Pipe(vec!["a".to_string(), "b".to_string(), "c".to_string()])
|
||||
),
|
||||
"@pipe(a, b, c)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_display_parallel() {
|
||||
assert_eq!(format!("{}", Decorator::Parallel), "@parallel");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_display_daemon() {
|
||||
assert_eq!(format!("{}", Decorator::Daemon), "@daemon");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_display_health() {
|
||||
assert_eq!(
|
||||
format!("{}", Decorator::Health("http://localhost:8080".to_string())),
|
||||
"@health(http://localhost:8080)"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Decorator::Health("/health".to_string())),
|
||||
"@health(/health)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_display_unknown() {
|
||||
assert_eq!(format!("{}", Decorator::Unknown), "@<unknown>");
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Tests for parse_decorator - Valid no-argument decorators
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_pipeline() {
|
||||
let result = parse_decorator("# @pipeline", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Pipeline));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_fallible() {
|
||||
let result = parse_decorator("# @fallible", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Fallible));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_export() {
|
||||
let result = parse_decorator("# @export", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Export));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_parallel() {
|
||||
let result = parse_decorator("# @parallel", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Parallel));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_daemon() {
|
||||
let result = parse_decorator("# @daemon", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Daemon));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Tests for parse_decorator - Valid single-argument decorators
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_if() {
|
||||
let result = parse_decorator("# @if(condition)", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::If("condition".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_if_with_complex_condition() {
|
||||
let result = parse_decorator("# @if(env.STATUS == \"ready\")", 1).unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(Decorator::If("env.STATUS == \"ready\"".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_after() {
|
||||
let result = parse_decorator("# @after(build_step)", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::After("build_step".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_after_with_underscore() {
|
||||
let result = parse_decorator("# @after(build_step_1)", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::After("build_step_1".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_loop() {
|
||||
let result = parse_decorator("# @loop(5)", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Loop(5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_loop_zero() {
|
||||
let result = parse_decorator("# @loop(0)", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Loop(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_health() {
|
||||
let result = parse_decorator("# @health(/health)", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Health("/health".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_health_url() {
|
||||
let result = parse_decorator("# @health(http://localhost:8080/health)", 1).unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(Decorator::Health(
|
||||
"http://localhost:8080/health".to_string()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_timeout() {
|
||||
let result = parse_decorator("# @timeout(30)", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Timeout(30)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_timeout_zero() {
|
||||
let result = parse_decorator("# @timeout(0)", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Timeout(0)));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Tests for parse_decorator - Valid multi-argument decorators
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_retry() {
|
||||
let result = parse_decorator("# @retry(3, 100)", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Retry(3, 100)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_retry_with_spaces() {
|
||||
let result = parse_decorator("# @retry(3,100)", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Retry(3, 100)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_retry_with_many_spaces() {
|
||||
let result = parse_decorator("# @retry( 3 , 100 )", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Retry(3, 100)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_retry_zero_count() {
|
||||
let result = parse_decorator("# @retry(0, 0)", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Retry(0, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_pipe() {
|
||||
let result = parse_decorator("# @pipe(func1)", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Pipe(vec!["func1".to_string()])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_pipe_multiple() {
|
||||
let result = parse_decorator("# @pipe(func1, func2, func3)", 1).unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(Decorator::Pipe(vec![
|
||||
"func1".to_string(),
|
||||
"func2".to_string(),
|
||||
"func3".to_string()
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_pipe_with_spaces() {
|
||||
let result = parse_decorator("# @pipe( func1 , func2 )", 1).unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(Decorator::Pipe(vec![
|
||||
"func1".to_string(),
|
||||
"func2".to_string()
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Tests for parse_decorator - Whitespace handling
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_with_leading_whitespace() {
|
||||
let result = parse_decorator(" # @pipeline", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Pipeline));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_with_extra_spaces() {
|
||||
let result = parse_decorator("# @pipeline", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Pipeline));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_with_tab() {
|
||||
let result = parse_decorator("#\t@pipeline", 1).unwrap();
|
||||
assert_eq!(result, Some(Decorator::Pipeline));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_no_space_between_hash_and_at() {
|
||||
// Regex requires at least one whitespace between # and @
|
||||
// So "#@pipeline" is NOT a valid decorator
|
||||
let result = parse_decorator("#@pipeline", 1).unwrap();
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Tests for parse_decorator - Non-decorator lines
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_no_hash() {
|
||||
let result = parse_decorator("@pipeline", 1).unwrap();
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_empty_line() {
|
||||
let result = parse_decorator("", 1).unwrap();
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_only_whitespace() {
|
||||
let result = parse_decorator(" ", 1).unwrap();
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_regular_comment() {
|
||||
let result = parse_decorator("# This is a regular comment", 1).unwrap();
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_code_line() {
|
||||
let result = parse_decorator("echo 'Hello World'", 1).unwrap();
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Tests for parse_decorator - Invalid: decorators with wrong arguments
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_pipeline_with_arg() {
|
||||
let result = parse_decorator("# @pipeline(something)", 1);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(matches!(err, BakeError::DecoratorParseError { .. }));
|
||||
if let BakeError::DecoratorParseError {
|
||||
decorator, reason, ..
|
||||
} = err
|
||||
{
|
||||
assert_eq!(decorator, "pipeline");
|
||||
assert!(reason.contains("does not accept arguments"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_fallible_with_arg() {
|
||||
let result = parse_decorator("# @fallible(true)", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_export_with_arg() {
|
||||
let result = parse_decorator("# @export(mode)", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_parallel_with_arg() {
|
||||
let result = parse_decorator("# @parallel(true)", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_daemon_with_arg() {
|
||||
let result = parse_decorator("# @daemon(stop)", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Tests for parse_decorator - Invalid: decorators missing required arguments
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_if_no_arg() {
|
||||
let result = parse_decorator("# @if", 1);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(matches!(err, BakeError::DecoratorParseError { .. }));
|
||||
if let BakeError::DecoratorParseError {
|
||||
decorator, reason, ..
|
||||
} = err
|
||||
{
|
||||
assert_eq!(decorator, "if");
|
||||
assert!(reason.contains("requires an argument"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_if_empty_parens() {
|
||||
let result = parse_decorator("# @if()", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_after_no_arg() {
|
||||
let result = parse_decorator("# @after", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_loop_no_arg() {
|
||||
let result = parse_decorator("# @loop", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_loop_empty_parens() {
|
||||
let result = parse_decorator("# @loop()", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_timeout_no_arg() {
|
||||
let result = parse_decorator("# @timeout", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_retry_no_arg() {
|
||||
let result = parse_decorator("# @retry", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_retry_only_count() {
|
||||
let result = parse_decorator("# @retry(3)", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_retry_only_comma() {
|
||||
let result = parse_decorator("# @retry(,)", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_health_no_arg() {
|
||||
let result = parse_decorator("# @health", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_pipe_no_arg() {
|
||||
let result = parse_decorator("# @pipe", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_pipe_empty_parens() {
|
||||
let result = parse_decorator("# @pipe()", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_pipe_only_commas() {
|
||||
let result = parse_decorator("# @pipe(,,)", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Tests for parse_decorator - Invalid: wrong argument types
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_loop_non_numeric() {
|
||||
let result = parse_decorator("# @loop(abc)", 1);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
if let BakeError::DecoratorParseError { reason, .. } = err {
|
||||
assert!(reason.contains("invalid loop count"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_loop_float() {
|
||||
let result = parse_decorator("# @loop(3.14)", 1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_loop_negative() {
|
||||
let result = parse_decorator("# @loop(-5)", 1);
|
||||
// usize parsing will fail for negative numbers
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_timeout_non_numeric() {
|
||||
let result = parse_decorator("# @timeout(abc)", 1);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
if let BakeError::DecoratorParseError { reason, .. } = err {
|
||||
assert!(reason.contains("invalid timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_retry_non_numeric_count() {
|
||||
let result = parse_decorator("# @retry(abc, 100)", 1);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
if let BakeError::DecoratorParseError { reason, .. } = err {
|
||||
assert!(reason.contains("invalid retry count"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_retry_non_numeric_delay() {
|
||||
let result = parse_decorator("# @retry(3, abc)", 1);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
if let BakeError::DecoratorParseError { reason, .. } = err {
|
||||
assert!(reason.contains("invalid retry delay"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_retry_empty_delay() {
|
||||
let result = parse_decorator("# @retry(3, )", 1);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
if let BakeError::DecoratorParseError { reason, .. } = err {
|
||||
assert!(reason.contains("no delay specified"));
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Tests for parse_decorator - Invalid: unknown decorators
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_unknown() {
|
||||
let result = parse_decorator("# @unknown", 1);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(matches!(err, BakeError::DecoratorParseError { .. }));
|
||||
if let BakeError::DecoratorParseError {
|
||||
decorator,
|
||||
reason,
|
||||
line_num,
|
||||
} = err
|
||||
{
|
||||
assert_eq!(decorator, "unknown");
|
||||
assert_eq!(line_num, 1);
|
||||
assert!(reason.contains("unknown decorator"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_typo_name() {
|
||||
let result = parse_decorator("# @pipelne", 1);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
if let BakeError::DecoratorParseError { decorator, .. } = err {
|
||||
assert_eq!(decorator, "pipelne");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_uppercase_name() {
|
||||
let result = parse_decorator("# @PIPELINE", 1);
|
||||
assert!(result.is_err());
|
||||
// Regex is case-sensitive, so it won't match @PIPELINE
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Tests for parse_decorator - Line number tracking
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_line_number_preserved() {
|
||||
let result = parse_decorator("# @pipeline", 42);
|
||||
assert!(result.is_ok());
|
||||
// Line number is preserved in error cases
|
||||
let result = parse_decorator("# @pipeline(invalid)", 42);
|
||||
assert!(result.is_err());
|
||||
if let BakeError::DecoratorParseError { line_num, .. } = result.unwrap_err() {
|
||||
assert_eq!(line_num, 42);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Tests for parse_decorator - Edge cases
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_very_large_loop_count() {
|
||||
let result = parse_decorator(&format!("# @loop({})", usize::MAX), 1);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), Some(Decorator::Loop(usize::MAX)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_very_large_timeout() {
|
||||
let result = parse_decorator(&format!("# @timeout({})", u32::MAX), 1);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), Some(Decorator::Timeout(u32::MAX)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_if_with_special_chars() {
|
||||
// Test condition with special characters that might break parsing
|
||||
let result = parse_decorator(r#"# @if(x != "" && y > 0)"#, 1).unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(Decorator::If(r#"x != "" && y > 0"#.to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_pipe_with_underscores() {
|
||||
let result = parse_decorator("# @pipe(step_1, step_2, step_3)", 1).unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(Decorator::Pipe(vec![
|
||||
"step_1".to_string(),
|
||||
"step_2".to_string(),
|
||||
"step_3".to_string()
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_multiple_in_sequence() {
|
||||
// Simulate multiple decorator lines
|
||||
let line1 = parse_decorator("# @pipeline", 1).unwrap();
|
||||
let line2 = parse_decorator("# @if(condition)", 2).unwrap();
|
||||
let line3 = parse_decorator("# @timeout(60)", 3).unwrap();
|
||||
|
||||
assert_eq!(line1, Some(Decorator::Pipeline));
|
||||
assert_eq!(line2, Some(Decorator::If("condition".to_string())));
|
||||
assert_eq!(line3, Some(Decorator::Timeout(60)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::decorator::{parse_decorator, Decorator};
|
||||
use crate::bake::BakeError;
|
||||
use super::decorator::{Decorator, parse_decorator};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use shlex::Shlex;
|
||||
@@ -22,7 +22,6 @@ pub struct Function {
|
||||
// NOTE: To be used
|
||||
// impl Function {
|
||||
// pub fn find_decorator<T>(&self, matcher: impl Fn(&Decorator) -> Option<T>) -> Option<T> {
|
||||
// // find_map:遍历迭代器,对每个元素执行matcher,返回第一个非None的结果
|
||||
// self.decorators.iter().find_map(matcher)
|
||||
// }
|
||||
// }
|
||||
@@ -34,7 +33,8 @@ pub fn parse_script(text: String) -> Result<Vec<Function>, BakeError> {
|
||||
let mut current_body: String = String::new();
|
||||
let mut in_function = false;
|
||||
let mut brace_count: i8 = 0;
|
||||
for line in text.lines() {
|
||||
for (line_number, line) in text.lines().enumerate() {
|
||||
let line_number = line_number + 1;
|
||||
if in_function {
|
||||
current_body.push_str(line);
|
||||
current_body.push('\n');
|
||||
@@ -56,7 +56,7 @@ pub fn parse_script(text: String) -> Result<Vec<Function>, BakeError> {
|
||||
continue;
|
||||
}
|
||||
if DECORATOR_HEADER_REGEX.find(line).is_some() {
|
||||
if let Some(decorator) = parse_decorator(line) {
|
||||
if let Some(decorator) = parse_decorator(line, line_number)? {
|
||||
current_decorators.push(decorator);
|
||||
}
|
||||
continue;
|
||||
@@ -108,111 +108,307 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_script() {
|
||||
let script = r#"
|
||||
# @pipeline
|
||||
# @fallible
|
||||
function my_function() {
|
||||
echo "Hello"
|
||||
{
|
||||
echo "2"
|
||||
}
|
||||
}
|
||||
"#;
|
||||
fn test_empty_script() {
|
||||
let script = "";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(functions[0].name, "my_function");
|
||||
assert_eq!(functions[0].decorators.len(), 2);
|
||||
assert_eq!(functions[0].decorators[0], Decorator::Pipeline);
|
||||
assert_eq!(functions[0].decorators[1], Decorator::Fallible);
|
||||
// assert_eq!(functions[0].body, " echo \"Hello\"\n {\n echo \"2\"\n }\n");
|
||||
assert!(functions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weird_script() {
|
||||
let weird_script = r#"
|
||||
# 注释行
|
||||
# @fallible
|
||||
fn test_only_comments() {
|
||||
let script = "# comment\n# another";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert!(functions.is_empty());
|
||||
}
|
||||
|
||||
# 空行
|
||||
#[test]
|
||||
fn test_function_with_parens() {
|
||||
let script = "func() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(functions[0].name, "func");
|
||||
}
|
||||
|
||||
# @pipeline
|
||||
function func1 {
|
||||
echo "函数1"
|
||||
# 字符串中的花括号 { 不应该影响计数
|
||||
echo "This is a { in string"
|
||||
if [ true ]; then
|
||||
echo "嵌套代码块"
|
||||
fi
|
||||
}
|
||||
#[test]
|
||||
fn test_function_keyword_with_parens() {
|
||||
let script = "function func() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(functions[0].name, "func");
|
||||
}
|
||||
|
||||
# 单行函数
|
||||
func2() { echo "单行函数"; }
|
||||
#[test]
|
||||
fn test_function_keyword_without_parens() {
|
||||
let script = "function func { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(functions[0].name, "func");
|
||||
}
|
||||
|
||||
# 装饰器后面有注释
|
||||
# @if(some)
|
||||
# 这是注释
|
||||
func3() {
|
||||
# 函数中的注释
|
||||
echo "函数3"
|
||||
# 复杂的括号嵌套
|
||||
{
|
||||
{
|
||||
echo "深度嵌套"
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_single_decorator() {
|
||||
let script = "# @pipeline\nfunc() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(functions[0].decorators.len(), 1);
|
||||
assert_eq!(functions[0].decorators[0], Decorator::Pipeline);
|
||||
}
|
||||
|
||||
valid-function() {
|
||||
echo "这个也应该被解析"
|
||||
}
|
||||
#[test]
|
||||
fn test_multiple_decorators() {
|
||||
let script = "# @pipeline\n# @fallible\nfunc() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(functions[0].decorators.len(), 2);
|
||||
}
|
||||
|
||||
# 无效的函数定义
|
||||
another_invalid-function {
|
||||
echo "你的括号呢"
|
||||
}
|
||||
#[test]
|
||||
fn test_decorator_invalidated_by_empty_line() {
|
||||
let script = "# @pipeline\n\nfunc() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert!(functions[0].decorators.is_empty());
|
||||
}
|
||||
|
||||
# 另一个有效函数
|
||||
# @after(what)
|
||||
function final_func()
|
||||
{
|
||||
# 多行定义
|
||||
echo "最后函数"
|
||||
}
|
||||
"#;
|
||||
#[test]
|
||||
fn test_decorator_invalidated_by_code() {
|
||||
let script = "# @pipeline\necho before\nfunc() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert!(functions[0].decorators.is_empty());
|
||||
}
|
||||
|
||||
let functions = parse_script(weird_script.to_string()).unwrap();
|
||||
#[test]
|
||||
fn test_decorator_after_function() {
|
||||
let script = "# @pipeline\nfunc1() { echo 1; }\nfunc2() { echo 2; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 2);
|
||||
assert_eq!(functions[0].decorators.len(), 1);
|
||||
assert!(functions[1].decorators.is_empty());
|
||||
}
|
||||
|
||||
// 验证解析的函数数量
|
||||
assert_eq!(functions.len(), 5, "应该解析出5个有效函数");
|
||||
#[test]
|
||||
fn test_nested_braces() {
|
||||
let script = "func() { if true; then { echo hi; } fi }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
}
|
||||
|
||||
// 验证函数1
|
||||
let func1 = functions.iter().find(|f| f.name == "func1").unwrap();
|
||||
assert_eq!(func1.decorators.len(), 1);
|
||||
assert!(func1.body.contains("echo \"函数1\""));
|
||||
assert!(func1.body.contains("echo \"This is a { in string\""));
|
||||
#[test]
|
||||
fn test_multiline_function() {
|
||||
let script = "func() {\necho line1\necho line2\n}";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert!(functions[0].body.contains("echo line1"));
|
||||
assert!(functions[0].body.contains("echo line2"));
|
||||
}
|
||||
|
||||
// 验证函数2(单行函数)
|
||||
let func2 = functions.iter().find(|f| f.name == "func2").unwrap();
|
||||
assert_eq!(func2.decorators.len(), 0);
|
||||
assert!(func2.body.trim().ends_with("{ echo \"单行函数\"; }"));
|
||||
#[test]
|
||||
fn test_multiple_functions() {
|
||||
let script = "func1() { echo 1; }\nfunc2() { echo 2; }\nfunc3() { echo 3; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 3);
|
||||
}
|
||||
|
||||
// 验证函数3
|
||||
let func3 = functions.iter().find(|f| f.name == "func3").unwrap();
|
||||
assert_eq!(func3.decorators.len(), 0);
|
||||
assert!(func3.body.contains("深度嵌套"));
|
||||
#[test]
|
||||
fn test_all_decorator_types() {
|
||||
let script = "# @pipeline\n# @fallible\n# @loop(3)\n# @timeout(60)\n# @export\n# @parallel\nfunc() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(functions[0].decorators.len(), 6);
|
||||
}
|
||||
|
||||
// 验证函数4
|
||||
let final_func = functions.iter().find(|f| f.name == "final_func").unwrap();
|
||||
assert_eq!(final_func.decorators.len(), 1);
|
||||
assert!(final_func.body.contains("最后函数"));
|
||||
|
||||
// 确保无效函数没有被解析
|
||||
assert!(
|
||||
functions
|
||||
.iter()
|
||||
.find(|f| f.name == "another_invalid-function")
|
||||
.is_none()
|
||||
#[test]
|
||||
fn test_decorator_if() {
|
||||
let script = "# @if(condition)\nfunc() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(
|
||||
functions[0].decorators[0],
|
||||
Decorator::If("condition".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_retry() {
|
||||
let script = "# @retry(3, 5)\nfunc() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(functions[0].decorators[0], Decorator::Retry(3, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_pipe() {
|
||||
let script = "# @pipe(step1, step2)\nfunc() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(
|
||||
functions[0].decorators[0],
|
||||
Decorator::Pipe(vec!["step1".to_string(), "step2".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_after_dep() {
|
||||
let script = "# @after(other)\nfunc() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(
|
||||
functions[0].decorators[0],
|
||||
Decorator::After("other".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_health() {
|
||||
let script = "# @health(/health)\nfunc() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(
|
||||
functions[0].decorators[0],
|
||||
Decorator::Health("/health".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_style_parallel_retry() {
|
||||
let script = r#"
|
||||
# @pipeline
|
||||
# @if(WS_PIPELINE_ARCH == amd64)
|
||||
# @timeout(720)
|
||||
# @retry(3, 5)
|
||||
# @parallel
|
||||
fetch_library() {
|
||||
get_custom_library ffmpeg amd64
|
||||
}
|
||||
"#;
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(functions[0].decorators.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_name_with_underscores_and_numbers() {
|
||||
let script = "func_123_test() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(functions[0].name, "func_123_test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_empty_body() {
|
||||
let script = "empty_func() { }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(functions[0].name, "empty_func");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_with_semicolons() {
|
||||
let script = "multi() { echo a; echo b; echo c; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert!(functions[0].body.contains("echo a;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comment_between_decorator_and_function() {
|
||||
let script = "# @pipeline\n# this is a comment\nfunc() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert!(functions[0].decorators.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whitespace_only_script() {
|
||||
let script = " \n\n\t\n ";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert!(functions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_without_keyword_no_parens() {
|
||||
let script = "simple { echo hi; }";
|
||||
let result = parse_script(script.to_string());
|
||||
assert!(result.is_err() || result.unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_malformed_no_closing_brace() {
|
||||
let script = "incomplete() { echo hi;";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert!(functions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_with_whitespace_variations() {
|
||||
let script = "#\t@pipeline \nfunc() { echo hi; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(functions[0].decorators.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_functions_with_mixed_decorators() {
|
||||
let script = "# @pipeline\nfunc1() { echo 1; }\n# @parallel\nfunc2() { echo 2; }\nfunc3() { echo 3; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 3);
|
||||
assert_eq!(functions[0].decorators.len(), 1);
|
||||
assert_eq!(functions[1].decorators.len(), 1);
|
||||
assert!(functions[2].decorators.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_with_command_substitution() {
|
||||
let script = "cmd_sub() { local x=$(date +%s); echo $x; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert!(functions[0].body.contains("$(date"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_with_case_statement() {
|
||||
let script = "case_func() { case $x in a) echo a;; b) echo b;; esac; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert!(functions[0].body.contains("case $x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_very_short_function_names() {
|
||||
let script = "a() { echo; }\nf() { echo; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_with_redirect() {
|
||||
let script = "redirect_func() { echo hi > /tmp/out.txt; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert!(functions[0].body.contains("> /tmp/out.txt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_with_pipe_in_body() {
|
||||
let script = "pipe_func() { cat file.txt | grep pattern | wc -l; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert!(functions[0].body.contains("| grep"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decorator_at_end_of_script() {
|
||||
let script = "func() { echo hi; }\n# @pipeline";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert!(functions[0].decorators.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_functions_with_only_comments_between() {
|
||||
let script = "func1() { echo 1; }\n# comment\n# another\nfunc2() { echo 2; }";
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,31 @@ pub fn sort_function(functions: Vec<Function>) -> Result<Vec<Function>, BakeErro
|
||||
|
||||
let mut ts = TopologicalSort::<String>::new();
|
||||
|
||||
// Track last pipeline function for implicit @after
|
||||
let mut last_pipeline: Option<String> = None;
|
||||
|
||||
for function in &functions {
|
||||
ts.insert(function.name.clone());
|
||||
|
||||
let is_pipeline = function
|
||||
.decorators
|
||||
.iter()
|
||||
.any(|d| matches!(d, Decorator::Pipeline));
|
||||
let has_after = function
|
||||
.decorators
|
||||
.iter()
|
||||
.any(|d| matches!(d, Decorator::After(_)));
|
||||
|
||||
// Add implicit @after for pipeline functions that don't have explicit @after
|
||||
if is_pipeline {
|
||||
if !has_after {
|
||||
if let Some(ref last) = last_pipeline {
|
||||
ts.add_dependency(last, function.name.clone());
|
||||
}
|
||||
}
|
||||
last_pipeline = Some(function.name.clone());
|
||||
}
|
||||
|
||||
for decorator in &function.decorators {
|
||||
if let Decorator::After(dependency) = decorator {
|
||||
if !function_mapped.contains_key(dependency.as_str()) {
|
||||
@@ -159,15 +182,15 @@ mod tests {
|
||||
|
||||
assert_eq!(result.len(), 4);
|
||||
|
||||
let positions: std::collections::HashMap<_, _> = result
|
||||
let positions: std::collections::HashMap<String, usize> = result
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| (&f.name, i))
|
||||
.map(|(i, f)| (f.name.clone(), i))
|
||||
.collect();
|
||||
|
||||
assert!(positions[&"func1".to_string()] < positions[&"func2".to_string()]);
|
||||
assert!(positions[&"func1".to_string()] < positions[&"func3".to_string()]);
|
||||
assert!(positions[&"func3".to_string()] < positions[&"func2".to_string()]);
|
||||
assert!(positions.get("func1").unwrap() < positions.get("func2").unwrap());
|
||||
assert!(positions.get("func1").unwrap() < positions.get("func3").unwrap());
|
||||
assert!(positions.get("func3").unwrap() < positions.get("func2").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -213,14 +236,14 @@ mod tests {
|
||||
|
||||
assert_eq!(result.len(), 3);
|
||||
|
||||
let positions: std::collections::HashMap<_, _> = result
|
||||
let positions: std::collections::HashMap<String, usize> = result
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| (&f.name, i))
|
||||
.map(|(i, f)| (f.name.clone(), i))
|
||||
.collect();
|
||||
|
||||
assert!(positions[&"func1".to_string()] < positions[&"func3".to_string()]);
|
||||
assert!(positions[&"func2".to_string()] < positions[&"func3".to_string()]);
|
||||
assert!(positions.get("func1").unwrap() < positions.get("func3").unwrap());
|
||||
assert!(positions.get("func2").unwrap() < positions.get("func3").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -253,4 +276,238 @@ mod tests {
|
||||
assert!(names.contains(&"func1".to_string()));
|
||||
assert!(names.contains(&"func2".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_implicit_after_pipeline() {
|
||||
let func1 = create_function("func1", vec![Decorator::Pipeline], "body1");
|
||||
let func2 = create_function("func2", vec![Decorator::Pipeline], "body2");
|
||||
let func3 = create_function("func3", vec![Decorator::Pipeline], "body3");
|
||||
let functions = vec![func1.clone(), func2.clone(), func3.clone()];
|
||||
|
||||
let result = sort_function(functions).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 3);
|
||||
assert_eq!(result[0].name, "func1");
|
||||
assert_eq!(result[1].name, "func2");
|
||||
assert_eq!(result[2].name, "func3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_pipeline_no_implicit_after() {
|
||||
let func1 = create_function("func1", vec![Decorator::Pipeline], "body1");
|
||||
let functions = vec![func1.clone()];
|
||||
|
||||
let result = sort_function(functions).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].name, "func1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_explicit_after_overrides_implicit() {
|
||||
let func1 = create_function("func1", vec![Decorator::Pipeline], "body1");
|
||||
let func2 = create_function(
|
||||
"func2",
|
||||
vec![Decorator::Pipeline, Decorator::After("func1".to_string())],
|
||||
"body2",
|
||||
);
|
||||
let func3 = create_function("func3", vec![Decorator::Pipeline], "body3");
|
||||
let functions = vec![func1.clone(), func2.clone(), func3.clone()];
|
||||
|
||||
let result = sort_function(functions).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 3);
|
||||
let positions: std::collections::HashMap<String, usize> = result
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| (f.name.clone(), i))
|
||||
.collect();
|
||||
|
||||
assert!(positions.get("func1").unwrap() < positions.get("func2").unwrap());
|
||||
assert!(positions.get("func2").unwrap() < positions.get("func3").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_pipeline_and_non_pipeline() {
|
||||
let func1 = create_function("func1", vec![], "body1");
|
||||
let func2 = create_function("func2", vec![Decorator::Pipeline], "body2");
|
||||
let func3 = create_function("func3", vec![], "body3");
|
||||
let func4 = create_function("func4", vec![Decorator::Pipeline], "body4");
|
||||
let functions = vec![func1.clone(), func2.clone(), func3.clone(), func4.clone()];
|
||||
|
||||
let result = sort_function(functions).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 4);
|
||||
let positions: std::collections::HashMap<String, usize> = result
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| (f.name.clone(), i))
|
||||
.collect();
|
||||
|
||||
assert!(positions.get("func2").unwrap() < positions.get("func4").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_pipeline_works() {
|
||||
let func1 = create_function("func1", vec![Decorator::Pipeline], "body1");
|
||||
let functions = vec![func1.clone()];
|
||||
|
||||
let result = sort_function(functions).unwrap();
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].name, "func1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_pipelines_implicit_chain() {
|
||||
let func1 = create_function("func1", vec![Decorator::Pipeline], "body1");
|
||||
let func2 = create_function("func2", vec![Decorator::Pipeline], "body2");
|
||||
let functions = vec![func1.clone(), func2.clone()];
|
||||
|
||||
let result = sort_function(functions).unwrap();
|
||||
assert_eq!(result.len(), 2);
|
||||
let positions: std::collections::HashMap<String, usize> = result
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| (f.name.clone(), i))
|
||||
.collect();
|
||||
assert!(positions.get("func1").unwrap() < positions.get("func2").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_explicit_after_skips_implicit() {
|
||||
let func1 = create_function("func1", vec![Decorator::Pipeline], "body1");
|
||||
let func2 = create_function("func2", vec![Decorator::Pipeline], "body2");
|
||||
let func3 = create_function(
|
||||
"func3",
|
||||
vec![Decorator::Pipeline, Decorator::After("func1".to_string())],
|
||||
"body3",
|
||||
);
|
||||
let functions = vec![func1.clone(), func2.clone(), func3.clone()];
|
||||
|
||||
let result = sort_function(functions).unwrap();
|
||||
assert_eq!(result.len(), 3);
|
||||
let positions: std::collections::HashMap<String, usize> = result
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| (f.name.clone(), i))
|
||||
.collect();
|
||||
assert!(positions.get("func1").unwrap() < positions.get("func3").unwrap());
|
||||
assert!(positions.get("func2").unwrap() < positions.get("func3").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circular_dependency_detected() {
|
||||
let func1 = create_function(
|
||||
"func1",
|
||||
vec![Decorator::After("func2".to_string())],
|
||||
"body1",
|
||||
);
|
||||
let func2 = create_function(
|
||||
"func2",
|
||||
vec![Decorator::After("func1".to_string())],
|
||||
"body2",
|
||||
);
|
||||
let functions = vec![func1.clone(), func2.clone()];
|
||||
|
||||
let result = sort_function(functions);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
BakeError::CircularDependency(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_dependency_error() {
|
||||
let func1 = create_function(
|
||||
"func1",
|
||||
vec![Decorator::After("nonexistent".to_string())],
|
||||
"body1",
|
||||
);
|
||||
let functions = vec![func1.clone()];
|
||||
|
||||
let result = sort_function(functions);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
BakeError::UnknownDependency(_, _)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_with_multiple_decorators() {
|
||||
let func1 = create_function(
|
||||
"func1",
|
||||
vec![
|
||||
Decorator::Pipeline,
|
||||
Decorator::Timeout(60),
|
||||
Decorator::Fallible,
|
||||
],
|
||||
"body1",
|
||||
);
|
||||
let func2 = create_function(
|
||||
"func2",
|
||||
vec![
|
||||
Decorator::Pipeline,
|
||||
Decorator::Retry(3, 5),
|
||||
Decorator::After("func1".to_string()),
|
||||
],
|
||||
"body2",
|
||||
);
|
||||
let functions = vec![func1.clone(), func2.clone()];
|
||||
|
||||
let result = sort_function(functions).unwrap();
|
||||
assert_eq!(result.len(), 2);
|
||||
let positions: std::collections::HashMap<String, usize> = result
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| (f.name.clone(), i))
|
||||
.collect();
|
||||
assert!(positions.get("func1").unwrap() < positions.get("func2").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_long_pipeline_chain() {
|
||||
let func1 = create_function("func1", vec![Decorator::Pipeline], "body1");
|
||||
let func2 = create_function("func2", vec![Decorator::Pipeline], "body2");
|
||||
let func3 = create_function("func3", vec![Decorator::Pipeline], "body3");
|
||||
let func4 = create_function("func4", vec![Decorator::Pipeline], "body4");
|
||||
let func5 = create_function("func5", vec![Decorator::Pipeline], "body5");
|
||||
let functions = vec![
|
||||
func1.clone(),
|
||||
func2.clone(),
|
||||
func3.clone(),
|
||||
func4.clone(),
|
||||
func5.clone(),
|
||||
];
|
||||
|
||||
let result = sort_function(functions).unwrap();
|
||||
assert_eq!(result.len(), 5);
|
||||
let positions: std::collections::HashMap<String, usize> = result
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| (f.name.clone(), i))
|
||||
.collect();
|
||||
assert!(positions.get("func1").unwrap() < positions.get("func2").unwrap());
|
||||
assert!(positions.get("func2").unwrap() < positions.get("func3").unwrap());
|
||||
assert!(positions.get("func3").unwrap() < positions.get("func4").unwrap());
|
||||
assert!(positions.get("func4").unwrap() < positions.get("func5").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_pipeline_between_pipelines() {
|
||||
let func1 = create_function("func1", vec![Decorator::Pipeline], "body1");
|
||||
let func2 = create_function("func2", vec![], "body2");
|
||||
let func3 = create_function("func3", vec![Decorator::Pipeline], "body3");
|
||||
let functions = vec![func1.clone(), func2.clone(), func3.clone()];
|
||||
|
||||
let result = sort_function(functions).unwrap();
|
||||
assert_eq!(result.len(), 3);
|
||||
let positions: std::collections::HashMap<String, usize> = result
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| (f.name.clone(), i))
|
||||
.collect();
|
||||
assert!(positions.get("func1").unwrap() < positions.get("func3").unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub const DEFAULT_WORKSPACE: &str = "/workspace";
|
||||
pub const BOOTSTRAP_SCRIPT_PATH: &str = "/bootstrap.sh";
|
||||
pub const BAKE_SCRIPT_PATH: &str = "bake.sh";
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
use crate::cli::{Cli, TaskSpec};
|
||||
use config::Config;
|
||||
use serde::Serialize;
|
||||
use crate::cli::{Cli};
|
||||
// use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
// use std::path::PathBuf;
|
||||
// use std::sync::Arc;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::signal;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::time::{Duration, interval};
|
||||
use tokio::net::{UnixStream};
|
||||
// use tokio::process::{Child, Command};
|
||||
// use tokio::signal;
|
||||
// use tokio::sync::RwLock;
|
||||
// use tokio::time::{Duration, interval};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct Heartbeat {
|
||||
pipeline_id: String,
|
||||
build_id: String,
|
||||
stage: String,
|
||||
timestamp: u64,
|
||||
status: String,
|
||||
subtasks: Vec<String>,
|
||||
}
|
||||
// #[derive(Debug, Clone, Serialize)]
|
||||
// struct Heartbeat {
|
||||
// pipeline_id: String,
|
||||
// build_id: String,
|
||||
// stage: String,
|
||||
// timestamp: u64,
|
||||
// status: String,
|
||||
// subtasks: Vec<String>,
|
||||
// }
|
||||
|
||||
pub async fn daemon(cli: &Cli) {
|
||||
pub async fn daemon(_cli: &Cli) {
|
||||
todo!("Daemon should be refactored");
|
||||
// let socket = PathBuf::from(settings.get_string("socket").unwrap());
|
||||
// let agentsocket = settings.get_string("agentsocket").unwrap();
|
||||
@@ -132,7 +131,7 @@ pub async fn daemon(cli: &Cli) {
|
||||
// let _ = std::fs::remove_file(&socket);
|
||||
}
|
||||
|
||||
async fn handle_connection(stream: UnixStream) {
|
||||
async fn _handle_connection(stream: UnixStream) {
|
||||
let peer_addr = match stream.peer_addr() {
|
||||
Ok(addr) => format!("{:?}", addr),
|
||||
Err(_) => "unknown".to_string(),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use os_info::Info;
|
||||
use serde_yaml::Value;
|
||||
use tokio::process::Command;
|
||||
mod apt;
|
||||
// TODO: apt
|
||||
// mod apt;
|
||||
mod pacman;
|
||||
|
||||
pub trait PackageManager {
|
||||
@@ -36,7 +37,7 @@ pub trait PackageManager {
|
||||
|
||||
pub fn all_managers() -> Vec<Box<dyn PackageManager>> {
|
||||
vec![
|
||||
Box::new(apt::Apt),
|
||||
// Box::new(apt::Apt),
|
||||
Box::new(pacman::Pacman),
|
||||
// Box::new(dnf::Dnf),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::PackageManager;
|
||||
use os_info::Info;
|
||||
use os_info::{Info, Type};
|
||||
use serde::Deserialize;
|
||||
use serde_yaml::Value;
|
||||
use tokio::process::Command;
|
||||
@@ -75,19 +75,17 @@ impl PackageManager for Pacman {
|
||||
}
|
||||
|
||||
fn adopt(&self, distro: &Info) -> bool {
|
||||
let d = distro.os_type().to_string().to_lowercase();
|
||||
matches!(
|
||||
d.as_str(),
|
||||
"arch linux"
|
||||
| "manjaro"
|
||||
| "endeavouros"
|
||||
| "garuda"
|
||||
| "cachyos"
|
||||
| "artix"
|
||||
| "mabox"
|
||||
| "nobara"
|
||||
| "instantos"
|
||||
| "archlinux"
|
||||
distro.os_type(),
|
||||
Type::Arch
|
||||
| Type::Manjaro
|
||||
| Type::EndeavourOS
|
||||
| Type::Garuda
|
||||
| Type::CachyOS
|
||||
| Type::Artix
|
||||
| Type::Mabox
|
||||
| Type::Nobara
|
||||
| Type::InstantOS
|
||||
)
|
||||
}
|
||||
|
||||
@@ -210,19 +208,52 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_adopt_true_for_arch_based_distros() {
|
||||
let pacman = Pacman;
|
||||
// TODO: reimplement adopt_test
|
||||
todo!()
|
||||
let arch_distros = [
|
||||
Type::Arch,
|
||||
Type::Manjaro,
|
||||
Type::EndeavourOS,
|
||||
Type::Garuda,
|
||||
Type::CachyOS,
|
||||
Type::Artix,
|
||||
Type::Mabox,
|
||||
Type::Nobara,
|
||||
Type::InstantOS,
|
||||
];
|
||||
for distro in arch_distros {
|
||||
let info = Info::with_type(distro);
|
||||
assert!(
|
||||
pacman.adopt(&info),
|
||||
"Pacman should adopt distro: {:?}",
|
||||
distro
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_adopt_false_for_non_arch_distros() {
|
||||
let pacman = Pacman;
|
||||
// TODO: reimplement adopt_test
|
||||
todo!()
|
||||
let non_arch_distros = [
|
||||
Type::Ubuntu,
|
||||
Type::Debian,
|
||||
Type::Fedora,
|
||||
Type::CentOS,
|
||||
Type::RedHatEnterprise,
|
||||
Type::openSUSE,
|
||||
Type::Gentoo,
|
||||
Type::FreeBSD,
|
||||
Type::Macos,
|
||||
Type::Windows,
|
||||
];
|
||||
for distro in non_arch_distros {
|
||||
let info = Info::with_type(distro);
|
||||
assert!(
|
||||
!pacman.adopt(&info),
|
||||
"Pacman should NOT adopt distro: {:?}",
|
||||
distro
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
use os_info::Info;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
pub const EXITCODE_OK: i32 = 0;
|
||||
@@ -13,8 +13,8 @@ pub const EXITCODE_PRIV_DROP_FAILED: i32 = 201;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BakeError {
|
||||
#[error("Failed to generate staged script {0}: {1}")]
|
||||
ScriptGenerationFailed(String, String),
|
||||
#[error("Failed to generate staged script {script}: {reason}")]
|
||||
ScriptGenerationFailed{script: String, reason: String},
|
||||
|
||||
#[error("Template error: {0}")]
|
||||
TemplateError(#[from] minijinja::Error),
|
||||
@@ -31,12 +31,15 @@ pub enum BakeError {
|
||||
#[error("YAML parse error: {0}")]
|
||||
YamlParseError(#[from] serde_yaml::Error),
|
||||
|
||||
#[error("Failed to parse decorator @{decorator} (line {line_num}): {reason}")]
|
||||
DecoratorParseError{
|
||||
#[error("Failed to parse decorator @{decorator} (line #{line_num}): {reason}")]
|
||||
DecoratorParseError {
|
||||
decorator: String,
|
||||
line_num: usize,
|
||||
reason: String,
|
||||
}
|
||||
},
|
||||
|
||||
#[error("Script execution error: {0}")]
|
||||
ScriptExecutionError(#[from] ExecutionError),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -94,7 +97,6 @@ pub enum BootstrapError {
|
||||
|
||||
#[error("Failed to create user {0}: {1}")]
|
||||
UserCreationFailed(String, String),
|
||||
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -109,10 +111,10 @@ pub enum DependencyError {
|
||||
ParseError(),
|
||||
|
||||
#[error("Stage {stage} failed: {source}")]
|
||||
StageFailed{
|
||||
StageFailed {
|
||||
stage: &'static str,
|
||||
#[source]
|
||||
source: ExecutionError
|
||||
source: ExecutionError,
|
||||
},
|
||||
|
||||
#[error("Custom command execution failed: {0}")]
|
||||
|
||||
@@ -66,6 +66,8 @@ pub mod cli {
|
||||
/// Path to bake_base.sh
|
||||
#[arg(short, long, default_value = "./bake_base.sh")]
|
||||
bake_base: PathBuf,
|
||||
#[arg(short, long, default_value = "./prebake.yml")]
|
||||
prebake: PathBuf,
|
||||
},
|
||||
/// Finish a task
|
||||
Finalize { config: String },
|
||||
|
||||
+21
-13
@@ -1,9 +1,10 @@
|
||||
use env_logger;
|
||||
use workshop_baker::{
|
||||
cli::{Cli, Commands, Parser},
|
||||
config, daemon,
|
||||
monitor::{self, monitor},
|
||||
prebake::{self, prebake},
|
||||
daemon,
|
||||
error::PrebakeError,
|
||||
prebake::prebake,
|
||||
bake::bake,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
@@ -17,22 +18,29 @@ async fn main() {
|
||||
Some(Commands::Monitor { group: _ }) => {
|
||||
unimplemented!("executor-monitor should be moved to daemon instead.")
|
||||
}
|
||||
Some(Commands::Client { config }) => {}
|
||||
Some(Commands::Client { config:_ }) => {}
|
||||
Some(Commands::Prebake { config }) => {
|
||||
log::info!("Running in standalone mode.");
|
||||
let result = prebake(config, &cli, None).await;
|
||||
dbg!(&result);
|
||||
if result.is_err() {
|
||||
log::error!("Prebake stage failed! Exiting...");
|
||||
std::process::exit(1);
|
||||
if let Err(ref e) = result {
|
||||
log::error!("Prebake stage failed: {}", e);
|
||||
std::process::exit(
|
||||
e.downcast_ref::<PrebakeError>()
|
||||
.map(|pe| pe.exit_code())
|
||||
.unwrap_or(1)
|
||||
);
|
||||
}
|
||||
// result.unwrap();
|
||||
}
|
||||
Some(Commands::Bake {
|
||||
script: _,
|
||||
bake_base: _,
|
||||
script, bake_base, prebake
|
||||
}) => {
|
||||
unimplemented!("bake not implemented.")
|
||||
log::info!("Running in standalone mode.");
|
||||
let result = bake(script, bake_base, prebake, &cli).await;
|
||||
dbg!(&result);
|
||||
if result.is_err() {
|
||||
log::error!("Bake stage failed! Exiting...");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Some(Commands::Finalize { config: _ }) => {
|
||||
unimplemented!("finalize not implemented.")
|
||||
@@ -41,7 +49,7 @@ async fn main() {
|
||||
// Running in daemon mode
|
||||
log::info!("Executor will be running in daemon mode.");
|
||||
|
||||
let result = daemon::daemon(&cli).await;
|
||||
let _result = daemon::daemon(&cli).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::str::FromStr;
|
||||
use crate::ExecutionContext;
|
||||
use crate::error::PrebakeError;
|
||||
use crate::prebake::config::Security;
|
||||
@@ -6,6 +5,7 @@ use crate::prebake::stage::PrebakeStage;
|
||||
use privdrop::PrivDrop;
|
||||
use std::ffi::CString;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::str::FromStr;
|
||||
|
||||
const ROOT_UID: u32 = 0;
|
||||
|
||||
@@ -177,15 +177,23 @@ pub fn prebake_drop_privilege(
|
||||
ctx: &mut ExecutionContext,
|
||||
) -> Result<(), PrebakeError> {
|
||||
if ctx.dry_run {
|
||||
return Ok(())
|
||||
return Ok(());
|
||||
}
|
||||
if stage_current > stage_expected {
|
||||
let current_uid = unsafe { libc::geteuid() };
|
||||
|
||||
let target_uid = lookup_uid_by_name(user)?;
|
||||
let target_uid = lookup_uid_by_name(user).map_err(|e| {
|
||||
log::error!(
|
||||
"Cannot drop privileges: target user '{}' does not exist at stage {}",
|
||||
user,
|
||||
stage_current
|
||||
);
|
||||
log::error!("Aborting pipeline.");
|
||||
e
|
||||
})?;
|
||||
|
||||
if current_uid == target_uid {
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"Already running as target user (UID {}), skipping privilege drop",
|
||||
target_uid
|
||||
);
|
||||
@@ -229,7 +237,8 @@ pub fn prebake_drop_privilege(
|
||||
}
|
||||
|
||||
pub fn get_drop_after(security: &Option<Security>) -> Result<PrebakeStage, String> {
|
||||
security.clone()
|
||||
security
|
||||
.clone()
|
||||
.and_then(|sec| sec.drop_after)
|
||||
.map(|stage_str| PrebakeStage::from_str(&stage_str))
|
||||
.unwrap_or(Ok(PrebakeStage::default()))
|
||||
|
||||
@@ -18,21 +18,6 @@ pub enum PrebakeStage {
|
||||
Never,
|
||||
}
|
||||
|
||||
impl PrebakeStage {
|
||||
pub const fn order(&self) -> u32 {
|
||||
match self {
|
||||
PrebakeStage::Init => 0,
|
||||
PrebakeStage::Bootstrap => 100,
|
||||
PrebakeStage::EarlyHook => 200,
|
||||
PrebakeStage::DepsSystem => 300,
|
||||
PrebakeStage::DepsUser => 400,
|
||||
PrebakeStage::LateHook => 500,
|
||||
PrebakeStage::Ready => 1000,
|
||||
PrebakeStage::Never => u32::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PrebakeStage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
@@ -43,6 +28,7 @@ impl std::fmt::Display for PrebakeStage {
|
||||
PrebakeStage::DepsUser => write!(f, "DepsUser"),
|
||||
PrebakeStage::LateHook => write!(f, "LateHook"),
|
||||
PrebakeStage::Ready => write!(f, "Ready"),
|
||||
PrebakeStage::Never => write!(f, "Never"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +45,7 @@ impl FromStr for PrebakeStage {
|
||||
"depsuser" => Ok(PrebakeStage::DepsUser),
|
||||
"latehook" => Ok(PrebakeStage::LateHook),
|
||||
"ready" => Ok(PrebakeStage::Ready),
|
||||
"never" => Ok(PrebakeStage::Never),
|
||||
_ => Err(format!("Unknown stage: {}", s)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,12 +128,117 @@ class DockerContainer:
|
||||
self.container.put_archive(os.path.dirname(container_path), tar_stream)
|
||||
|
||||
|
||||
def check_user_exists(container: DockerContainer, username: str) -> bool:
|
||||
"""Check if user <USER> exists in the container."""
|
||||
exit_code, stdout, stderr = container.exec_run(f"id {username}")
|
||||
return exit_code == 0
|
||||
|
||||
|
||||
def check_user_home(container: DockerContainer, username: str) -> str | None:
|
||||
"""Check user <USER>'s home directory. Returns path or None if not found."""
|
||||
exit_code, stdout, stderr = container.exec_run(f"getent passwd {username}")
|
||||
if exit_code != 0:
|
||||
return None
|
||||
parts = stdout.strip().split(":")
|
||||
if len(parts) >= 6:
|
||||
return parts[5]
|
||||
return None
|
||||
|
||||
|
||||
def check_directory_exists(container: DockerContainer, path: str) -> bool:
|
||||
"""Check if directory <DIR> exists."""
|
||||
exit_code, stdout, stderr = container.exec_run(f"test -d {path}")
|
||||
return exit_code == 0
|
||||
|
||||
|
||||
def check_file_exists(container: DockerContainer, path: str) -> bool:
|
||||
"""Check if file <FILE> exists."""
|
||||
exit_code, stdout, stderr = container.exec_run(f"test -f {path}")
|
||||
return exit_code == 0
|
||||
|
||||
|
||||
def check_file_content(
|
||||
container: DockerContainer, path: str, pattern: str, fuzzy: bool = True
|
||||
) -> bool:
|
||||
"""Check if file <FILE> contains content matching pattern.
|
||||
|
||||
Args:
|
||||
container: DockerContainer instance
|
||||
path: file path in container
|
||||
pattern: pattern to match
|
||||
fuzzy: if True, use grep (partial match); if False, use exact match
|
||||
"""
|
||||
if not check_file_exists(container, path):
|
||||
return False
|
||||
|
||||
if fuzzy:
|
||||
exit_code, stdout, stderr = container.exec_run(f"grep -q '{pattern}' {path}")
|
||||
return exit_code == 0
|
||||
else:
|
||||
exit_code, stdout, stderr = container.exec_run(f"grep -F '{pattern}' {path}")
|
||||
return exit_code == 0
|
||||
|
||||
|
||||
def check_sudo_permission(
|
||||
container: DockerContainer, username: str, command: str
|
||||
) -> tuple[bool, str]:
|
||||
"""Check if user <USER> can/cannot sudo/doas to complete an operation.
|
||||
|
||||
Returns:
|
||||
(can_execute, error_message)
|
||||
"""
|
||||
exit_code, stdout, stderr = container.exec_run(
|
||||
f"sudo -u {username} -- sh -c '{command}'"
|
||||
)
|
||||
if exit_code == 0:
|
||||
return True, ""
|
||||
return False, stderr
|
||||
|
||||
|
||||
def check_package_installed(container: DockerContainer, package: str) -> bool:
|
||||
"""Check if software package <PACKAGE> is installed."""
|
||||
exit_code, stdout, stderr = container.exec_run(
|
||||
"sh -c 'command -v pacman >/dev/null 2>&1 && pacman -Q "
|
||||
+ package
|
||||
+ " >/dev/null 2>&1 || command -v apt >/dev/null 2>&1 && dpkg -l "
|
||||
+ package
|
||||
+ " >/dev/null 2>&1 || command -v rpm >/dev/null 2>&1 && rpm -q "
|
||||
+ package
|
||||
+ " >/dev/null 2>&1'"
|
||||
)
|
||||
return exit_code == 0
|
||||
|
||||
|
||||
def check_env_var(
|
||||
container: DockerContainer, var_name: str, expected_value: str = ""
|
||||
) -> tuple[bool, str]:
|
||||
"""Check environment variable <ENV> value.
|
||||
|
||||
Args:
|
||||
container: DockerContainer instance
|
||||
var_name: environment variable name
|
||||
expected_value: if provided, check for exact match; if None, just check existence
|
||||
|
||||
Returns:
|
||||
(matches, actual_value)
|
||||
"""
|
||||
exit_code, stdout, stderr = container.exec_run(f"echo ${var_name}")
|
||||
if exit_code != 0:
|
||||
return False, ""
|
||||
|
||||
actual_value = stdout.strip()
|
||||
if expected_value is None:
|
||||
return True, actual_value
|
||||
|
||||
return actual_value == expected_value, actual_value
|
||||
|
||||
|
||||
class TestBootstrapUnit:
|
||||
"""Unit tests for bootstrap stage - tests bootstrap.sh generation."""
|
||||
|
||||
IMAGE = "archlinux:latest"
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
BINARY_NAME = "workshop-executor"
|
||||
BINARY_NAME = "workshop-baker"
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def container_with_binary(self):
|
||||
@@ -227,7 +332,7 @@ class TestPrebakeStages:
|
||||
|
||||
IMAGE = "archlinux:latest"
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
BINARY_NAME = "workshop-executor"
|
||||
BINARY_NAME = "workshop-baker"
|
||||
EXAMPLES_DIR = PROJECT_ROOT / "examples"
|
||||
PREBAKE_YML = EXAMPLES_DIR / "prebake.yml"
|
||||
|
||||
@@ -401,13 +506,19 @@ class TestPrebakeStages:
|
||||
f"Full prebake pipeline failed with exit code {exit_code}: {stderr}"
|
||||
)
|
||||
|
||||
def test_bootstrap_creates_user(self, container_with_binary: DockerContainer):
|
||||
assert check_user_exists(container_with_binary, "vulcan")
|
||||
|
||||
def test_bootstrap_creates_workspace(self, container_with_binary: DockerContainer):
|
||||
assert check_directory_exists(container_with_binary, "/workspace")
|
||||
|
||||
|
||||
class TestPrebakeIntegration:
|
||||
"""End-to-end integration tests for prebake workflow."""
|
||||
|
||||
IMAGE = "archlinux:latest"
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
BINARY_NAME = "workshop-executor"
|
||||
BINARY_NAME = "workshop-baker"
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def container_with_binary(self):
|
||||
@@ -631,6 +742,506 @@ class TestPrebakeExamples:
|
||||
pytest.fail(f"Invalid YAML in prebake_build.yml: {e}")
|
||||
|
||||
|
||||
class TestEngineInDocker:
|
||||
"""Engine integration tests running in Docker container.
|
||||
|
||||
These tests verify the engine can execute commands correctly inside a container.
|
||||
If Docker is not available, tests are skipped with appropriate logging.
|
||||
"""
|
||||
|
||||
IMAGE = "archlinux:latest"
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
BINARY_NAME = "workshop-baker"
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def container_with_binary(self):
|
||||
if not docker_available():
|
||||
pytest.skip("Docker未启动")
|
||||
|
||||
release_binary = self.PROJECT_ROOT / "target" / "release" / self.BINARY_NAME
|
||||
debug_binary = self.PROJECT_ROOT / "target" / "debug" / self.BINARY_NAME
|
||||
|
||||
if release_binary.exists():
|
||||
binary_path = release_binary
|
||||
elif debug_binary.exists():
|
||||
binary_path = debug_binary
|
||||
else:
|
||||
pytest.skip(
|
||||
f"Binary {self.BINARY_NAME} not found. Run 'cargo build --release' first."
|
||||
)
|
||||
|
||||
client = docker.from_env()
|
||||
try:
|
||||
client.images.get(self.IMAGE)
|
||||
except docker.errors.ImageNotFound:
|
||||
try:
|
||||
print(f"Pulling image {self.IMAGE}...")
|
||||
client.images.pull(self.IMAGE)
|
||||
except docker.errors.DockerException as e:
|
||||
pytest.skip(f"Could not pull image {self.IMAGE}: {e}")
|
||||
except Exception as e:
|
||||
pytest.skip(f"Could not access Docker: {e}")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
container_kwargs = {
|
||||
"volumes": {
|
||||
str(self.PROJECT_ROOT): {"bind": "/workshop-baker", "mode": "rw"},
|
||||
},
|
||||
}
|
||||
|
||||
container = DockerContainer(
|
||||
self.IMAGE,
|
||||
f"workshop-baker-engine-{int(time.time())}",
|
||||
**container_kwargs,
|
||||
)
|
||||
|
||||
with container:
|
||||
container.copy_file(binary_path, f"/usr/local/bin/{self.BINARY_NAME}")
|
||||
|
||||
exit_code, stdout, stderr = container.exec_run(
|
||||
f"chmod +x /usr/local/bin/{self.BINARY_NAME}"
|
||||
)
|
||||
if exit_code != 0:
|
||||
pytest.fail(f"Failed to make binary executable: {stderr}")
|
||||
|
||||
yield container
|
||||
|
||||
def test_execute_echo_outputs_correct_message(
|
||||
self, container_with_binary: DockerContainer
|
||||
):
|
||||
cmd = "echo 'hello world'"
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(cmd)
|
||||
assert exit_code == 0, f"echo failed: {stderr}"
|
||||
assert "hello world" in stdout
|
||||
|
||||
def test_execute_true_returns_zero_exit_code(
|
||||
self, container_with_binary: DockerContainer
|
||||
):
|
||||
cmd = "true"
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(cmd)
|
||||
assert exit_code == 0, f"true command failed"
|
||||
|
||||
def test_execute_false_returns_nonzero_exit_code(
|
||||
self, container_with_binary: DockerContainer
|
||||
):
|
||||
cmd = "false"
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(cmd)
|
||||
assert exit_code != 0, f"false should return non-zero exit code"
|
||||
|
||||
def test_execute_ls_lists_directory(self, container_with_binary: DockerContainer):
|
||||
cmd = "ls -la /tmp"
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(cmd)
|
||||
assert exit_code == 0, f"ls failed: {stderr}"
|
||||
|
||||
def test_execute_date_command(self, container_with_binary: DockerContainer):
|
||||
cmd = "date"
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(cmd)
|
||||
assert exit_code == 0, f"date failed: {stderr}"
|
||||
assert len(stdout.strip()) > 0
|
||||
|
||||
def test_execute_invalid_command_returns_error(
|
||||
self, container_with_binary: DockerContainer
|
||||
):
|
||||
cmd = "this_command_definitely_does_not_exist_12345"
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(cmd)
|
||||
assert exit_code != 0, f"invalid command should fail"
|
||||
|
||||
def test_execute_custom_env_var(self, container_with_binary: DockerContainer):
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run("env")
|
||||
assert exit_code == 0, f"env failed: {stderr}"
|
||||
|
||||
def test_execute_working_directory(self, container_with_binary: DockerContainer):
|
||||
cmd = "pwd"
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(cmd, workdir="/tmp")
|
||||
assert exit_code == 0, f"pwd failed: {stderr}"
|
||||
|
||||
def test_execute_captures_stderr(self, container_with_binary: DockerContainer):
|
||||
cmd = "bash -c 'echo error >&2'"
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(cmd)
|
||||
assert exit_code == 0, f"stderr capture failed"
|
||||
assert "error" in stderr
|
||||
|
||||
|
||||
class TestBake:
|
||||
"""Bake integration tests.
|
||||
|
||||
Tests the bake phase with trivial=true, use_template=true.
|
||||
Uses bake_base.sh from workshop-pipeline/examples/.workshop/
|
||||
"""
|
||||
|
||||
IMAGE = "archlinux:latest"
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
BINARY_NAME = "workshop-baker"
|
||||
BAKE_BASE = (
|
||||
PROJECT_ROOT.parent
|
||||
/ "workshop-pipeline"
|
||||
/ "examples"
|
||||
/ ".workshop"
|
||||
/ "bake_base.sh"
|
||||
)
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def container_with_binary(self):
|
||||
if not docker_available():
|
||||
pytest.skip("Docker未启动")
|
||||
|
||||
release_binary = self.PROJECT_ROOT / "target" / "release" / self.BINARY_NAME
|
||||
debug_binary = self.PROJECT_ROOT / "target" / "debug" / self.BINARY_NAME
|
||||
|
||||
if release_binary.exists():
|
||||
binary_path = release_binary
|
||||
elif debug_binary.exists():
|
||||
binary_path = debug_binary
|
||||
else:
|
||||
pytest.skip(
|
||||
f"Binary {self.BINARY_NAME} not found. Run 'cargo build --release' first."
|
||||
)
|
||||
|
||||
client = docker.from_env()
|
||||
try:
|
||||
client.images.get(self.IMAGE)
|
||||
except docker.errors.ImageNotFound:
|
||||
try:
|
||||
print(f"Pulling image {self.IMAGE}...")
|
||||
client.images.pull(self.IMAGE)
|
||||
except docker.errors.DockerException as e:
|
||||
pytest.skip(f"Could not pull image {self.IMAGE}: {e}")
|
||||
except Exception as e:
|
||||
pytest.skip(f"Could not access Docker: {e}")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
container_kwargs = {
|
||||
"volumes": {
|
||||
str(self.PROJECT_ROOT): {"bind": "/workshop-baker", "mode": "rw"},
|
||||
},
|
||||
}
|
||||
|
||||
container = DockerContainer(
|
||||
self.IMAGE,
|
||||
f"workshop-baker-bake-{int(time.time())}",
|
||||
**container_kwargs,
|
||||
)
|
||||
|
||||
with container:
|
||||
container.copy_file(binary_path, f"/usr/local/bin/{self.BINARY_NAME}")
|
||||
|
||||
exit_code, stdout, stderr = container.exec_run(
|
||||
f"chmod +x /usr/local/bin/{self.BINARY_NAME}"
|
||||
)
|
||||
if exit_code != 0:
|
||||
pytest.fail(f"Failed to make binary executable: {stderr}")
|
||||
|
||||
if not self.BAKE_BASE.exists():
|
||||
pytest.skip(f"bake_base.sh not found at {self.BAKE_BASE}")
|
||||
container.copy_file(self.BAKE_BASE, "/bake_base.sh")
|
||||
|
||||
exit_code, stdout, stderr = container.exec_run("chmod +x /bake_base.sh")
|
||||
if exit_code != 0:
|
||||
pytest.fail(f"Failed to make bake_base.sh executable: {stderr}")
|
||||
|
||||
exit_code, stdout, stderr = container.exec_run("mkdir -p /workspace")
|
||||
if exit_code != 0:
|
||||
pytest.fail(f"Failed to create /workspace: {stderr}")
|
||||
|
||||
yield container
|
||||
|
||||
def test_bake_base_exists(self, container_with_binary: DockerContainer):
|
||||
assert check_file_exists(container_with_binary, "/bake_base.sh")
|
||||
|
||||
def test_bake_binary_exists(self, container_with_binary: DockerContainer):
|
||||
cmd = f"/usr/local/bin/{self.BINARY_NAME} --help"
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(cmd)
|
||||
assert exit_code == 0 or "bake" in (stdout + stderr).lower()
|
||||
|
||||
def test_bake_simple_echo(self, container_with_binary: DockerContainer):
|
||||
script_sh = container_with_binary.host_dir / "script.sh"
|
||||
script_sh.write_text("echo 'Hello from bake'\n")
|
||||
container_with_binary.copy_file(script_sh, "/script.sh")
|
||||
|
||||
cmd = (
|
||||
f"/usr/local/bin/{self.BINARY_NAME} "
|
||||
"--username test-user "
|
||||
"--pipeline test-pipeline "
|
||||
"--build-id test-build-001 "
|
||||
"--dry-run "
|
||||
f"bake /script.sh --bake-base /bake_base.sh --prebake /workshop-baker/examples/prebake.yml"
|
||||
)
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(
|
||||
cmd, workdir="/workshop-baker"
|
||||
)
|
||||
print(f"Bake simple echo:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
|
||||
assert "error" not in (stdout + stderr).lower() or exit_code == 0
|
||||
|
||||
def test_bake_executes_script_content(self, container_with_binary: DockerContainer):
|
||||
script_sh = container_with_binary.host_dir / "script.sh"
|
||||
script_sh.write_text("echo 'Bake test output'\n")
|
||||
container_with_binary.copy_file(script_sh, "/script.sh")
|
||||
|
||||
cmd = (
|
||||
f"/usr/local/bin/{self.BINARY_NAME} "
|
||||
"--username test-user "
|
||||
"--pipeline test-pipeline "
|
||||
"--build-id test-build-001 "
|
||||
f"bake /script.sh --bake-base /bake_base.sh --prebake /workshop-baker/examples/prebake.yml"
|
||||
)
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(
|
||||
cmd, workdir="/workshop-baker"
|
||||
)
|
||||
print(f"Bake executes:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
|
||||
assert exit_code == 0, f"Bake failed: {stderr}"
|
||||
|
||||
|
||||
class TestPrivilegeDropping:
|
||||
"""Tests for drop-after privilege dropping mechanism.
|
||||
|
||||
Verifies that privileges are correctly dropped at the specified stage boundary.
|
||||
The drop-after setting determines when the process transitions from root to non-root.
|
||||
|
||||
Stage order: Init < Bootstrap < EarlyHook < DepsSystem < DepsUser < LateHook < Ready
|
||||
If drop-after: LateHook, privileges are dropped AFTER LateHook completes.
|
||||
If drop-after: DepsSystem (default), privileges are dropped AFTER DepsSystem completes.
|
||||
"""
|
||||
|
||||
IMAGE = "archlinux:latest"
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
BINARY_NAME = "workshop-baker"
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def container_with_binary(self):
|
||||
if not docker_available():
|
||||
pytest.skip("Docker未启动")
|
||||
|
||||
release_binary = self.PROJECT_ROOT / "target" / "release" / self.BINARY_NAME
|
||||
debug_binary = self.PROJECT_ROOT / "target" / "debug" / self.BINARY_NAME
|
||||
|
||||
if release_binary.exists():
|
||||
binary_path = release_binary
|
||||
elif debug_binary.exists():
|
||||
binary_path = debug_binary
|
||||
else:
|
||||
pytest.skip(
|
||||
f"Binary {self.BINARY_NAME} not found. Run 'cargo build --release' first."
|
||||
)
|
||||
|
||||
client = docker.from_env()
|
||||
try:
|
||||
client.images.get(self.IMAGE)
|
||||
except docker.errors.ImageNotFound:
|
||||
try:
|
||||
print(f"Pulling image {self.IMAGE}...")
|
||||
client.images.pull(self.IMAGE)
|
||||
except docker.errors.DockerException as e:
|
||||
pytest.skip(f"Could not pull image {self.IMAGE}: {e}")
|
||||
except Exception as e:
|
||||
pytest.skip(f"Could not access Docker: {e}")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
container_kwargs = {
|
||||
"volumes": {
|
||||
str(self.PROJECT_ROOT): {"bind": "/workshop-baker", "mode": "rw"},
|
||||
},
|
||||
"user": "0:0",
|
||||
}
|
||||
|
||||
container = DockerContainer(
|
||||
self.IMAGE,
|
||||
f"workshop-baker-dropafter-{int(time.time())}",
|
||||
**container_kwargs,
|
||||
)
|
||||
|
||||
with container:
|
||||
container.copy_file(binary_path, f"/usr/local/bin/{self.BINARY_NAME}")
|
||||
|
||||
exit_code, stdout, stderr = container.exec_run(
|
||||
f"chmod +x /usr/local/bin/{self.BINARY_NAME}"
|
||||
)
|
||||
if exit_code != 0:
|
||||
pytest.fail(f"Failed to make binary executable: {stderr}")
|
||||
|
||||
bootstrap_sh = container.host_dir / "bootstrap.sh"
|
||||
bootstrap_sh.write_text(
|
||||
"#!/bin/bash\n"
|
||||
"set -e\n"
|
||||
"if ! id -u testuser >/dev/null 2>&1; then\n"
|
||||
" useradd -m -s /bin/bash testuser\n"
|
||||
"fi\n"
|
||||
"if ! id -u vulcan >/dev/null 2>&1; then\n"
|
||||
" useradd -m -s /bin/nologin vulcan\n"
|
||||
"fi\n"
|
||||
"mkdir -p /workspace\n"
|
||||
"chmod 777 /workspace\n"
|
||||
"exit 0\n"
|
||||
)
|
||||
container.copy_file(bootstrap_sh, "/bootstrap.sh")
|
||||
|
||||
exit_code, stdout, stderr = container.exec_run("chmod +x /bootstrap.sh")
|
||||
if exit_code != 0:
|
||||
pytest.fail(f"Failed to set bootstrap.sh executable: {stderr}")
|
||||
|
||||
yield container
|
||||
|
||||
def _create_prebake_yml(
|
||||
self, container: DockerContainer, drop_after: str, hook_cmd: str, hook_name: str
|
||||
) -> Path:
|
||||
prebake_content = f"""version: "1.0"
|
||||
environment:
|
||||
builder: "docker"
|
||||
docker:
|
||||
image: "archlinux:latest"
|
||||
security:
|
||||
drop-after: "{drop_after}"
|
||||
hooks:
|
||||
{hook_name}:
|
||||
- command: "{hook_cmd}"
|
||||
"""
|
||||
prebake_path = container.host_dir / "test_prebake.yml"
|
||||
prebake_path.write_text(prebake_content)
|
||||
return prebake_path
|
||||
|
||||
def test_drop_after_late_hook_early_runs_privileged(
|
||||
self, container_with_binary: DockerContainer
|
||||
):
|
||||
"""With drop-after: LateHook, EarlyHook runs as root (UID 0)."""
|
||||
prebake_path = self._create_prebake_yml(
|
||||
container_with_binary,
|
||||
drop_after="LateHook",
|
||||
hook_cmd="echo $(id -u) > /workspace/hook_uid.txt",
|
||||
hook_name="early",
|
||||
)
|
||||
container_with_binary.copy_file(prebake_path, "/test_prebake.yml")
|
||||
|
||||
cmd = (
|
||||
f"/usr/local/bin/{self.BINARY_NAME} "
|
||||
"--username testuser "
|
||||
"--pipeline test-pipeline "
|
||||
"--build-id test-build-001 "
|
||||
"prebake /test_prebake.yml"
|
||||
)
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(
|
||||
cmd, workdir="/workshop-baker"
|
||||
)
|
||||
|
||||
print(f"drop-after: LateHook, early hook exit: {exit_code}")
|
||||
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(
|
||||
"cat /workspace/hook_uid.txt"
|
||||
)
|
||||
uid_str = stdout.strip() if stdout else ""
|
||||
print(f"Early hook UID from file: {uid_str}")
|
||||
assert uid_str == "0", (
|
||||
f"EarlyHook should run as root (UID 0), but got {uid_str}"
|
||||
)
|
||||
|
||||
def test_drop_after_late_hook_late_runs_unprivileged(
|
||||
self, container_with_binary: DockerContainer
|
||||
):
|
||||
"""With drop-after: LateHook, LateHook runs as non-root (non-UID 0)."""
|
||||
container_with_binary.exec_run("rm -f /workspace/hook_uid.txt")
|
||||
|
||||
prebake_path = self._create_prebake_yml(
|
||||
container_with_binary,
|
||||
drop_after="LateHook",
|
||||
hook_cmd="echo $(id -u) > /workspace/hook_uid.txt",
|
||||
hook_name="late",
|
||||
)
|
||||
container_with_binary.copy_file(prebake_path, "/test_prebake.yml")
|
||||
|
||||
cmd = (
|
||||
f"/usr/local/bin/{self.BINARY_NAME} "
|
||||
"--username testuser "
|
||||
"--pipeline test-pipeline "
|
||||
"--build-id test-build-001 "
|
||||
"prebake /test_prebake.yml"
|
||||
)
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(
|
||||
cmd, workdir="/workshop-baker"
|
||||
)
|
||||
|
||||
print(f"drop-after: LateHook, late hook exit: {exit_code}")
|
||||
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(
|
||||
"cat /workspace/hook_uid.txt"
|
||||
)
|
||||
uid_str = stdout.strip() if stdout else ""
|
||||
print(f"Late hook UID from file: {uid_str}")
|
||||
uid = int(uid_str)
|
||||
assert uid != 0, f"LateHook should run as non-root, but got UID {uid}"
|
||||
|
||||
def test_drop_after_depssystem_early_runs_privileged(
|
||||
self, container_with_binary: DockerContainer
|
||||
):
|
||||
"""With drop-after: DepsSystem (default), EarlyHook runs as root (UID 0)."""
|
||||
container_with_binary.exec_run("rm -f /workspace/hook_uid.txt")
|
||||
|
||||
prebake_path = self._create_prebake_yml(
|
||||
container_with_binary,
|
||||
drop_after="DepsSystem",
|
||||
hook_cmd="echo $(id -u) > /workspace/hook_uid.txt",
|
||||
hook_name="early",
|
||||
)
|
||||
container_with_binary.copy_file(prebake_path, "/test_prebake.yml")
|
||||
|
||||
cmd = (
|
||||
f"/usr/local/bin/{self.BINARY_NAME} "
|
||||
"--username testuser "
|
||||
"--pipeline test-pipeline "
|
||||
"--build-id test-build-001 "
|
||||
"prebake /test_prebake.yml"
|
||||
)
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(
|
||||
cmd, workdir="/workshop-baker"
|
||||
)
|
||||
|
||||
print(f"drop-after: DepsSystem, early hook exit: {exit_code}")
|
||||
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(
|
||||
"cat /workspace/hook_uid.txt"
|
||||
)
|
||||
uid_str = stdout.strip() if stdout else ""
|
||||
print(f"Early hook UID from file: {uid_str}")
|
||||
assert uid_str == "0", (
|
||||
f"EarlyHook should run as root (UID 0), but got {uid_str}"
|
||||
)
|
||||
|
||||
def test_drop_after_depssystem_late_runs_unprivileged(
|
||||
self, container_with_binary: DockerContainer
|
||||
):
|
||||
"""With drop-after: DepsSystem (default), LateHook runs as non-root (non-UID 0)."""
|
||||
container_with_binary.exec_run("rm -f /workspace/hook_uid.txt")
|
||||
|
||||
prebake_path = self._create_prebake_yml(
|
||||
container_with_binary,
|
||||
drop_after="DepsSystem",
|
||||
hook_cmd="echo $(id -u) > /workspace/hook_uid.txt",
|
||||
hook_name="late",
|
||||
)
|
||||
container_with_binary.copy_file(prebake_path, "/test_prebake.yml")
|
||||
|
||||
cmd = (
|
||||
f"/usr/local/bin/{self.BINARY_NAME} "
|
||||
"--username testuser "
|
||||
"--pipeline test-pipeline "
|
||||
"--build-id test-build-001 "
|
||||
"prebake /test_prebake.yml"
|
||||
)
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(
|
||||
cmd, workdir="/workshop-baker"
|
||||
)
|
||||
|
||||
print(f"drop-after: DepsSystem, late hook exit: {exit_code}")
|
||||
|
||||
exit_code, stdout, stderr = container_with_binary.exec_run(
|
||||
"cat /workspace/hook_uid.txt"
|
||||
)
|
||||
uid_str = stdout.strip() if stdout else ""
|
||||
print(f"Late hook UID from file: {uid_str}")
|
||||
uid = int(uid_str)
|
||||
assert uid != 0, f"LateHook should run as non-root, but got UID {uid}"
|
||||
|
||||
|
||||
def run_integration_tests():
|
||||
"""Run integration tests with Docker."""
|
||||
import sys
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Color for output
|
||||
# 颜色定义用于输出
|
||||
readonly RED='\033[0;31m'
|
||||
readonly GREEN='\033[0;32m'
|
||||
readonly YELLOW='\033[1;33m'
|
||||
readonly BLUE='\033[0;34m'
|
||||
readonly NC='\033[0m' # 无颜色
|
||||
readonly NC='\033[0m' # No color, 无颜色
|
||||
|
||||
# 日志函数
|
||||
log() {
|
||||
# logging function
|
||||
# 日志辅助函数
|
||||
bake_log() {
|
||||
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
bake_log_warn() {
|
||||
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
bake_log_error() {
|
||||
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*${NC}"
|
||||
}
|
||||
|
||||
log_info() {
|
||||
bake_log_info() {
|
||||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*${NC}"
|
||||
}
|
||||
|
||||
# 读取环境变量
|
||||
source .env
|
||||
# 执行bake步骤
|
||||
source bake.sh
|
||||
{{ main }}
|
||||
|
||||
Reference in New Issue
Block a user