#[cfg(test)]
#[allow(clippy::module_inception)]
mod tests {
use super::super::*;
#[test]
fn test_join_continuation_simple() {
let input = "RUN apt-get update && \\\n apt-get install -y curl";
let lines = join_continuation_lines(input);
assert_eq!(lines.len(), 1);
assert!(lines[0].contains("apt-get update"));
assert!(lines[0].contains("apt-get install"));
}
#[test]
fn test_join_continuation_no_continuation() {
let input = "FROM alpine:3.19\nRUN echo hello";
let lines = join_continuation_lines(input);
assert_eq!(lines.len(), 2);
}
#[test]
fn test_join_continuation_multiple() {
let input = "RUN a \\\n b \\\n c";
let lines = join_continuation_lines(input);
assert_eq!(lines.len(), 1);
assert!(lines[0].contains('a'));
assert!(lines[0].contains('b'));
assert!(lines[0].contains('c'));
}
#[test]
fn test_parse_from_simple() {
let result = parsers::parse_from("alpine:3.19", 1).unwrap();
assert_eq!(
result,
Instruction::From {
image: "alpine:3.19".to_string(),
alias: None,
}
);
}
#[test]
fn test_parse_from_with_alias() {
let result = parsers::parse_from("golang:1.21 AS builder", 1).unwrap();
assert_eq!(
result,
Instruction::From {
image: "golang:1.21".to_string(),
alias: Some("builder".to_string()),
}
);
}
#[test]
fn test_parse_from_empty() {
assert!(parsers::parse_from("", 1).is_err());
}
#[test]
fn test_parse_run_shell() {
let result = parsers::parse_run("apt-get update && apt-get install -y curl", 1).unwrap();
assert_eq!(
result,
Instruction::Run {
command: "apt-get update && apt-get install -y curl".to_string(),
}
);
}
#[test]
fn test_parse_run_json() {
let result = parsers::parse_run(r#"["echo", "hello"]"#, 1).unwrap();
assert_eq!(
result,
Instruction::Run {
command: "echo hello".to_string(),
}
);
}
#[test]
fn test_parse_run_empty() {
assert!(parsers::parse_run("", 1).is_err());
}
#[test]
fn test_parse_copy_simple() {
let result = parsers::parse_copy("app.py /workspace/", 1).unwrap();
assert_eq!(
result,
Instruction::Copy {
src: vec!["app.py".to_string()],
dst: "/workspace/".to_string(),
from: None,
}
);
}
#[test]
fn test_parse_copy_multiple_sources() {
let result = parsers::parse_copy("file1.txt file2.txt /dest/", 1).unwrap();
assert_eq!(
result,
Instruction::Copy {
src: vec!["file1.txt".to_string(), "file2.txt".to_string()],
dst: "/dest/".to_string(),
from: None,
}
);
}
#[test]
fn test_parse_copy_from_stage() {
let result = parsers::parse_copy("--from=builder /app/bin /usr/local/bin/", 1).unwrap();
assert_eq!(
result,
Instruction::Copy {
src: vec!["/app/bin".to_string()],
dst: "/usr/local/bin/".to_string(),
from: Some("builder".to_string()),
}
);
}
#[test]
fn test_parse_copy_empty() {
assert!(parsers::parse_copy("", 1).is_err());
}
#[test]
fn test_parse_copy_single_arg() {
assert!(parsers::parse_copy("onlysource", 1).is_err());
}
#[test]
fn test_parse_env_equals() {
let result = parsers::parse_env("PATH=/usr/local/bin:/usr/bin", 1).unwrap();
assert_eq!(
result,
Instruction::Env {
key: "PATH".to_string(),
value: "/usr/local/bin:/usr/bin".to_string(),
}
);
}
#[test]
fn test_parse_env_quoted() {
let result = parsers::parse_env(r#"MSG="hello world""#, 1).unwrap();
assert_eq!(
result,
Instruction::Env {
key: "MSG".to_string(),
value: "hello world".to_string(),
}
);
}
#[test]
fn test_parse_env_legacy() {
let result = parsers::parse_env("MY_VAR my_value", 1).unwrap();
assert_eq!(
result,
Instruction::Env {
key: "MY_VAR".to_string(),
value: "my_value".to_string(),
}
);
}
#[test]
fn test_parse_env_empty() {
assert!(parsers::parse_env("", 1).is_err());
}
#[test]
fn test_parse_entrypoint_exec() {
let result = parsers::parse_entrypoint(r#"["/bin/agent", "--listen"]"#, 1).unwrap();
assert_eq!(
result,
Instruction::Entrypoint {
exec: vec!["/bin/agent".to_string(), "--listen".to_string()],
}
);
}
#[test]
fn test_parse_entrypoint_shell() {
let result = parsers::parse_entrypoint("/bin/agent --listen", 1).unwrap();
assert_eq!(
result,
Instruction::Entrypoint {
exec: vec![
"/bin/sh".to_string(),
"-c".to_string(),
"/bin/agent --listen".to_string(),
],
}
);
}
#[test]
fn test_parse_cmd_exec() {
let result = parsers::parse_cmd(r#"["--port", "8080"]"#, 1).unwrap();
assert_eq!(
result,
Instruction::Cmd {
exec: vec!["--port".to_string(), "8080".to_string()],
}
);
}
#[test]
fn test_parse_cmd_shell() {
let result = parsers::parse_cmd("echo hello", 1).unwrap();
assert_eq!(
result,
Instruction::Cmd {
exec: vec![
"/bin/sh".to_string(),
"-c".to_string(),
"echo hello".to_string(),
],
}
);
}
#[test]
fn test_parse_expose() {
let result = parsers::parse_expose("8080", 1).unwrap();
assert_eq!(
result,
Instruction::Expose {
port: "8080".to_string(),
}
);
}
#[test]
fn test_parse_expose_with_proto() {
let result = parsers::parse_expose("8080/tcp", 1).unwrap();
assert_eq!(
result,
Instruction::Expose {
port: "8080/tcp".to_string(),
}
);
}
#[test]
fn test_parse_label_equals() {
let result = parsers::parse_label("version=1.0.0", 1).unwrap();
assert_eq!(
result,
Instruction::Label {
key: "version".to_string(),
value: "1.0.0".to_string(),
}
);
}
#[test]
fn test_parse_label_quoted() {
let result = parsers::parse_label(r#"description="My App""#, 1).unwrap();
assert_eq!(
result,
Instruction::Label {
key: "description".to_string(),
value: "My App".to_string(),
}
);
}
#[test]
fn test_parse_user() {
let result = parsers::parse_user("nobody", 1).unwrap();
assert_eq!(
result,
Instruction::User {
user: "nobody".to_string(),
}
);
}
#[test]
fn test_parse_user_with_group() {
let result = parsers::parse_user("1000:1000", 1).unwrap();
assert_eq!(
result,
Instruction::User {
user: "1000:1000".to_string(),
}
);
}
#[test]
fn test_parse_arg_no_default() {
let result = parsers::parse_arg("VERSION", 1).unwrap();
assert_eq!(
result,
Instruction::Arg {
name: "VERSION".to_string(),
default: None,
}
);
}
#[test]
fn test_parse_arg_with_default() {
let result = parsers::parse_arg("VERSION=1.0.0", 1).unwrap();
assert_eq!(
result,
Instruction::Arg {
name: "VERSION".to_string(),
default: Some("1.0.0".to_string()),
}
);
}
#[test]
fn test_parse_minimal_dockerfile() {
let content = "FROM alpine:3.19\nCMD [\"echo\", \"hello\"]";
let df = Dockerfile::parse(content).unwrap();
assert_eq!(df.instructions.len(), 2);
assert!(
matches!(&df.instructions[0], Instruction::From { image, .. } if image == "alpine:3.19")
);
}
#[test]
fn test_parse_complex_dockerfile() {
let content = r#"
# Build stage
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
LABEL version="1.0.0"
LABEL maintainer="team@example.com"
USER nobody
ENTRYPOINT ["python"]
CMD ["app.py"]
"#;
let df = Dockerfile::parse(content).unwrap();
assert_eq!(df.instructions.len(), 13);
}
#[test]
fn test_parse_with_continuations() {
let content = "FROM alpine:3.19\nRUN apk add --no-cache \\\n curl \\\n wget";
let df = Dockerfile::parse(content).unwrap();
assert_eq!(df.instructions.len(), 2);
if let Instruction::Run { command } = &df.instructions[1] {
assert!(command.contains("curl"));
assert!(command.contains("wget"));
} else {
panic!("Expected RUN instruction");
}
}
#[test]
fn test_parse_empty_dockerfile() {
let content = "# just a comment\n\n";
assert!(Dockerfile::parse(content).is_err());
}
#[test]
fn test_parse_no_from() {
let content = "RUN echo hello";
assert!(Dockerfile::parse(content).is_err());
}
#[test]
fn test_parse_arg_before_from() {
let content = "ARG VERSION=3.19\nFROM alpine:${VERSION}";
let df = Dockerfile::parse(content).unwrap();
assert_eq!(df.instructions.len(), 2);
assert!(matches!(&df.instructions[0], Instruction::Arg { .. }));
assert!(matches!(&df.instructions[1], Instruction::From { .. }));
}
#[test]
fn test_parse_comments_and_blanks() {
let content = "\n# comment\n\nFROM alpine\n\n# another comment\nRUN echo hi\n\n";
let df = Dockerfile::parse(content).unwrap();
assert_eq!(df.instructions.len(), 2);
}
#[test]
fn test_unquote_double() {
assert_eq!(utils::unquote(r#""hello world""#), "hello world");
}
#[test]
fn test_unquote_single() {
assert_eq!(utils::unquote("'hello world'"), "hello world");
}
#[test]
fn test_unquote_none() {
assert_eq!(utils::unquote("hello"), "hello");
}
#[test]
fn test_unquote_mismatched() {
assert_eq!(utils::unquote(r#""hello'"#), r#""hello'"#);
}
#[test]
fn test_parse_add_simple() {
let result = parsers::parse_add("app.tar.gz /app/", 1).unwrap();
assert_eq!(
result,
Instruction::Add {
src: vec!["app.tar.gz".to_string()],
dst: "/app/".to_string(),
chown: None,
}
);
}
#[test]
fn test_parse_add_with_chown() {
let result = parsers::parse_add("--chown=1000:1000 files/ /data/", 1).unwrap();
assert_eq!(
result,
Instruction::Add {
src: vec!["files/".to_string()],
dst: "/data/".to_string(),
chown: Some("1000:1000".to_string()),
}
);
}
#[test]
fn test_parse_add_url() {
let result = parsers::parse_add("https://example.com/file.tar.gz /tmp/", 1).unwrap();
assert_eq!(
result,
Instruction::Add {
src: vec!["https://example.com/file.tar.gz".to_string()],
dst: "/tmp/".to_string(),
chown: None,
}
);
}
#[test]
fn test_parse_add_empty() {
assert!(parsers::parse_add("", 1).is_err());
}
#[test]
fn test_parse_add_single_arg() {
assert!(parsers::parse_add("onlysource", 1).is_err());
}
#[test]
fn test_parse_shell_bash() {
let result = parsers::parse_shell(r#"["/bin/bash", "-c"]"#, 1).unwrap();
assert_eq!(
result,
Instruction::Shell {
exec: vec!["/bin/bash".to_string(), "-c".to_string()],
}
);
}
#[test]
fn test_parse_shell_powershell() {
let result = parsers::parse_shell(r#"["powershell", "-command"]"#, 1).unwrap();
assert_eq!(
result,
Instruction::Shell {
exec: vec!["powershell".to_string(), "-command".to_string()],
}
);
}
#[test]
fn test_parse_shell_empty() {
assert!(parsers::parse_shell("", 1).is_err());
}
#[test]
fn test_parse_shell_not_json() {
assert!(parsers::parse_shell("/bin/bash -c", 1).is_err());
}
#[test]
fn test_parse_shell_empty_array() {
assert!(parsers::parse_shell("[]", 1).is_err());
}
#[test]
fn test_parse_stopsignal_name() {
let result = parsers::parse_stopsignal("SIGTERM", 1).unwrap();
assert_eq!(
result,
Instruction::StopSignal {
signal: "SIGTERM".to_string(),
}
);
}
#[test]
fn test_parse_stopsignal_number() {
let result = parsers::parse_stopsignal("9", 1).unwrap();
assert_eq!(
result,
Instruction::StopSignal {
signal: "9".to_string(),
}
);
}
#[test]
fn test_parse_stopsignal_empty() {
assert!(parsers::parse_stopsignal("", 1).is_err());
}
#[test]
fn test_parse_healthcheck_none() {
let result = parsers::parse_healthcheck("NONE", 1).unwrap();
assert_eq!(
result,
Instruction::HealthCheck {
cmd: None,
interval: None,
timeout: None,
retries: None,
start_period: None,
}
);
}
#[test]
fn test_parse_healthcheck_simple_cmd() {
let result = parsers::parse_healthcheck("CMD curl -f http://localhost/", 1).unwrap();
assert_eq!(
result,
Instruction::HealthCheck {
cmd: Some(vec![
"/bin/sh".to_string(),
"-c".to_string(),
"curl -f http://localhost/".to_string(),
]),
interval: None,
timeout: None,
retries: None,
start_period: None,
}
);
}
#[test]
fn test_parse_healthcheck_with_options() {
let result = parsers::parse_healthcheck(
"--interval=10s --timeout=5s --retries=5 --start-period=30s CMD curl -f http://localhost/",
1,
)
.unwrap();
assert_eq!(
result,
Instruction::HealthCheck {
cmd: Some(vec![
"/bin/sh".to_string(),
"-c".to_string(),
"curl -f http://localhost/".to_string(),
]),
interval: Some(10),
timeout: Some(5),
retries: Some(5),
start_period: Some(30),
}
);
}
#[test]
fn test_parse_healthcheck_json_cmd() {
let result =
parsers::parse_healthcheck(r#"CMD ["curl", "-f", "http://localhost/"]"#, 1).unwrap();
if let Instruction::HealthCheck { cmd, .. } = &result {
assert_eq!(
cmd.as_ref().unwrap(),
&vec![
"curl".to_string(),
"-f".to_string(),
"http://localhost/".to_string()
]
);
} else {
panic!("Expected HealthCheck");
}
}
#[test]
fn test_parse_healthcheck_empty() {
assert!(parsers::parse_healthcheck("", 1).is_err());
}
#[test]
fn test_parse_healthcheck_no_cmd() {
assert!(parsers::parse_healthcheck("--interval=10s", 1).is_err());
}
#[test]
fn test_parse_onbuild_run() {
let result = parsers::parse_onbuild("RUN echo hello", 1).unwrap();
assert_eq!(
result,
Instruction::OnBuild {
instruction: Box::new(Instruction::Run {
command: "echo hello".to_string(),
}),
}
);
}
#[test]
fn test_parse_onbuild_copy() {
let result = parsers::parse_onbuild("COPY . /app", 1).unwrap();
assert_eq!(
result,
Instruction::OnBuild {
instruction: Box::new(Instruction::Copy {
src: vec![".".to_string()],
dst: "/app".to_string(),
from: None,
}),
}
);
}
#[test]
fn test_parse_onbuild_empty() {
assert!(parsers::parse_onbuild("", 1).is_err());
}
#[test]
fn test_parse_onbuild_onbuild() {
assert!(parsers::parse_onbuild("ONBUILD RUN echo", 1).is_err());
}
#[test]
fn test_parse_onbuild_from() {
assert!(parsers::parse_onbuild("FROM alpine", 1).is_err());
}
#[test]
fn test_parse_volume_single() {
let result = parsers::parse_volume("/data", 1).unwrap();
assert_eq!(
result,
Instruction::Volume {
paths: vec!["/data".to_string()],
}
);
}
#[test]
fn test_parse_volume_multiple() {
let result = parsers::parse_volume("/data /var/log", 1).unwrap();
assert_eq!(
result,
Instruction::Volume {
paths: vec!["/data".to_string(), "/var/log".to_string()],
}
);
}
#[test]
fn test_parse_volume_json() {
let result = parsers::parse_volume(r#"["/data", "/var/log"]"#, 1).unwrap();
assert_eq!(
result,
Instruction::Volume {
paths: vec!["/data".to_string(), "/var/log".to_string()],
}
);
}
#[test]
fn test_parse_volume_empty() {
assert!(parsers::parse_volume("", 1).is_err());
}
#[test]
fn test_parse_duration_seconds() {
assert_eq!(utils::parse_duration_secs("30s", 1).unwrap(), 30);
}
#[test]
fn test_parse_duration_minutes() {
assert_eq!(utils::parse_duration_secs("5m", 1).unwrap(), 300);
}
#[test]
fn test_parse_duration_hours() {
assert_eq!(utils::parse_duration_secs("1h", 1).unwrap(), 3600);
}
#[test]
fn test_parse_duration_plain_number() {
assert_eq!(utils::parse_duration_secs("30", 1).unwrap(), 30);
}
#[test]
fn test_parse_duration_empty() {
assert_eq!(utils::parse_duration_secs("", 1).unwrap(), 0);
}
#[test]
fn test_parse_dockerfile_with_shell() {
let content = "FROM alpine\nSHELL [\"/bin/bash\", \"-c\"]\nRUN echo hello";
let df = Dockerfile::parse(content).unwrap();
assert_eq!(df.instructions.len(), 3);
assert!(matches!(&df.instructions[1], Instruction::Shell { exec } if exec.len() == 2));
}
#[test]
fn test_parse_dockerfile_with_healthcheck() {
let content = "FROM alpine\nHEALTHCHECK CMD curl -f http://localhost/";
let df = Dockerfile::parse(content).unwrap();
assert_eq!(df.instructions.len(), 2);
assert!(matches!(
&df.instructions[1],
Instruction::HealthCheck { cmd: Some(_), .. }
));
}
#[test]
fn test_parse_dockerfile_with_volume() {
let content = "FROM alpine\nVOLUME /data";
let df = Dockerfile::parse(content).unwrap();
assert_eq!(df.instructions.len(), 2);
assert!(
matches!(&df.instructions[1], Instruction::Volume { paths } if paths == &["/data".to_string()])
);
}
#[test]
fn test_parse_dockerfile_with_add() {
let content = "FROM alpine\nADD app.tar.gz /app/";
let df = Dockerfile::parse(content).unwrap();
assert_eq!(df.instructions.len(), 2);
assert!(matches!(&df.instructions[1], Instruction::Add { .. }));
}
#[test]
fn test_parse_dockerfile_with_stopsignal() {
let content = "FROM alpine\nSTOPSIGNAL SIGKILL";
let df = Dockerfile::parse(content).unwrap();
assert_eq!(df.instructions.len(), 2);
assert!(
matches!(&df.instructions[1], Instruction::StopSignal { signal } if signal == "SIGKILL")
);
}
#[test]
fn test_parse_dockerfile_with_onbuild() {
let content = "FROM alpine\nONBUILD RUN echo triggered";
let df = Dockerfile::parse(content).unwrap();
assert_eq!(df.instructions.len(), 2);
assert!(matches!(&df.instructions[1], Instruction::OnBuild { .. }));
}
}