use std::fs;
use std::path::PathBuf;
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn read_repo_file(rel: &str) -> String {
let path = repo_root().join(rel);
fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()))
}
fn assert_repo_path_exists(rel: &str, context: &str) {
let path = repo_root().join(rel);
assert!(
path.exists(),
"expected path `{rel}` to exist (resolved: {}) — referenced by {context}",
path.display()
);
}
fn indent_of(line: &str) -> usize {
line.len() - line.trim_start_matches(' ').len()
}
fn unquote(s: &str) -> &str {
let s = s.trim();
let bytes = s.as_bytes();
if bytes.len() >= 2 {
let first = bytes[0];
let last = bytes[bytes.len() - 1];
if (first == b'"' && last == b'"') || (first == b'\'' && last == b'\'') {
return &s[1..s.len() - 1];
}
}
s
}
fn collect_service_volume_mounts(content: &str) -> Vec<String> {
let mut mounts = Vec::new();
let mut block_indent: Option<usize> = None;
for raw in content.lines() {
let trimmed = raw.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let indent = indent_of(raw);
if let Some(open_indent) = block_indent {
if indent > open_indent {
if let Some(rest) = trimmed.strip_prefix("- ") {
mounts.push(unquote(rest).to_string());
}
continue;
}
block_indent = None;
}
if indent > 0 && trimmed == "volumes:" {
block_indent = Some(indent);
}
}
mounts
}
fn collect_declared_volumes(content: &str) -> Vec<String> {
let mut names = Vec::new();
let mut in_block = false;
let mut base_indent: Option<usize> = None;
for raw in content.lines() {
let trimmed = raw.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let indent = indent_of(raw);
if in_block {
if indent == 0 {
in_block = false;
base_indent = None;
} else {
let base = *base_indent.get_or_insert(indent);
if indent == base {
if let Some(name) = trimmed.strip_suffix(':') {
if !name.is_empty() && !name.contains(char::is_whitespace) {
names.push(name.to_string());
}
}
}
continue;
}
}
if indent == 0 && trimmed == "volumes:" {
in_block = true;
base_indent = None;
}
}
names
}
#[test]
fn test_dockerfile_copy_paths_exist() {
let content = read_repo_file("Dockerfile");
for (idx, raw) in content.lines().enumerate() {
let lineno = idx + 1;
let line = raw.trim();
let upper = line.to_ascii_uppercase();
if !(upper.starts_with("COPY ") || upper.starts_with("ADD ")) {
continue;
}
if line.contains("--from=") {
continue;
}
let tokens: Vec<&str> = line
.split_whitespace()
.skip(1)
.filter(|t| !t.starts_with("--"))
.collect();
if tokens.len() < 2 {
continue;
}
let sources = &tokens[..tokens.len() - 1];
for &src in sources {
let src = unquote(src);
if src.starts_with("http://") || src.starts_with("https://") {
continue;
}
if src.contains('*') || src.contains('?') {
let parent = match src.rsplit_once('/') {
Some((dir, _)) => dir,
None => ".",
};
assert_repo_path_exists(
parent,
&format!("Dockerfile:{lineno} glob source `{src}`"),
);
} else {
assert_repo_path_exists(src, &format!("Dockerfile:{lineno} `{line}`"));
}
}
}
}
#[test]
fn test_dockerfile_exposes_valid_ports_and_targets_binary() {
let content = read_repo_file("Dockerfile");
let mut expose_count = 0usize;
for raw in content.lines() {
let line = raw.trim();
if !line.to_ascii_uppercase().starts_with("EXPOSE ") {
continue;
}
expose_count += 1;
for tok in line.split_whitespace().skip(1) {
let port = tok.split('/').next().unwrap_or(tok);
assert!(
port.parse::<u16>().is_ok(),
"EXPOSE port `{tok}` is not a valid u16 (line: `{line}`)"
);
}
}
assert!(
expose_count >= 1,
"Dockerfile must contain at least one EXPOSE instruction"
);
assert!(
content.contains("/usr/local/bin/rs3gw"),
"Dockerfile must reference the runtime binary path /usr/local/bin/rs3gw"
);
let has_healthcheck = content
.lines()
.any(|l| l.trim().to_ascii_uppercase().starts_with("HEALTHCHECK"))
|| (content.contains("curl") && content.contains("/health"));
assert!(
has_healthcheck,
"Dockerfile must define a HEALTHCHECK (or a curl .../health check)"
);
}
#[test]
fn test_compose_files_reference_existing_assets() {
for file in ["docker-compose.yml", "docker-compose.dev.yml"] {
let content = read_repo_file(file);
assert!(
content.lines().any(|l| l.starts_with("services:")),
"{file} must declare a top-level `services:` key"
);
for mount in collect_service_volume_mounts(&content) {
let host = mount.split(':').next().unwrap_or_default();
if let Some(rel) = host.strip_prefix("./") {
assert_repo_path_exists(rel, &format!("{file} bind mount `{mount}`"));
}
}
}
}
#[test]
fn test_compose_named_volumes_declared() {
for file in ["docker-compose.yml", "docker-compose.dev.yml"] {
let content = read_repo_file(file);
let declared = collect_declared_volumes(&content);
for mount in collect_service_volume_mounts(&content) {
let host = mount.split(':').next().unwrap_or_default();
if host.is_empty() || host.starts_with('.') || host.starts_with('/') {
continue;
}
assert!(
declared.iter().any(|d| d == host),
"{file}: service references named volume `{host}` (mount `{mount}`) \
that is not declared under the top-level `volumes:` block \
(declared: {declared:?})"
);
}
}
}
#[test]
fn test_compose_files_have_no_obsolete_version_key() {
for file in ["docker-compose.yml", "docker-compose.dev.yml"] {
let content = read_repo_file(file);
for (idx, raw) in content.lines().enumerate() {
let line = raw.trim_end();
assert!(
!line.starts_with("version:"),
"{file}:{} declares an obsolete top-level `version:` key (`{line}`) — \
Compose v2 ignores it and `docker compose config` warns; remove it",
idx + 1
);
}
}
}
fn collect_quoted(s: &str) -> Vec<String> {
let mut out = Vec::new();
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c == b'"' || c == b'\'' {
let start = i + 1;
let mut j = start;
while j < bytes.len() && bytes[j] != c {
j += 1;
}
if j >= bytes.len() {
break; }
out.push(s[start..j].to_string());
i = j + 1;
} else {
i += 1;
}
}
out
}
fn parse_workspace_members(content: &str) -> Vec<String> {
let mut members = Vec::new();
let mut in_workspace = false;
let mut in_members = false;
for raw in content.lines() {
let line = match raw.split_once('#') {
Some((before, _)) => before,
None => raw,
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if in_members {
members.extend(collect_quoted(trimmed));
if trimmed.contains(']') {
in_members = false;
}
continue;
}
if trimmed.starts_with('[') && trimmed.ends_with(']') {
in_workspace = trimmed == "[workspace]";
continue;
}
if !in_workspace {
continue;
}
if let Some(after_key) = trimmed.strip_prefix("members") {
if let Some(array) = after_key.trim_start().strip_prefix('=') {
members.extend(collect_quoted(array));
if !array.contains(']') {
in_members = true;
}
}
}
}
members
}
fn dockerfile_copy_sources(content: &str) -> Vec<String> {
let mut sources = Vec::new();
for raw in content.lines() {
let line = raw.trim();
let upper = line.to_ascii_uppercase();
if !(upper.starts_with("COPY ") || upper.starts_with("ADD ")) {
continue;
}
if line.contains("--from=") {
continue;
}
let tokens: Vec<&str> = line
.split_whitespace()
.skip(1)
.filter(|t| !t.starts_with("--"))
.collect();
if tokens.len() < 2 {
continue;
}
for &src in &tokens[..tokens.len() - 1] {
sources.push(unquote(src).to_string());
}
}
sources
}
#[test]
fn test_dockerfile_copies_all_workspace_members() {
let members = parse_workspace_members(&read_repo_file("Cargo.toml"));
assert!(
!members.is_empty(),
"failed to parse any `[workspace]` members from root Cargo.toml"
);
let sources = dockerfile_copy_sources(&read_repo_file("Dockerfile"));
for member in &members {
if member == "." {
continue;
}
let covered = sources
.iter()
.any(|s| s == member || s == "." || member.starts_with(&format!("{s}/")));
assert!(
covered,
"workspace member `{member}` is not COPYed into the Docker builder \
stage — add a `COPY` covering it (builder COPY sources: {sources:?})"
);
}
}
struct DeclaredTarget {
kind: String,
name: Option<String>,
path: Option<String>,
}
fn parse_declared_targets(content: &str) -> Vec<DeclaredTarget> {
const KINDS: [&str; 4] = ["bench", "bin", "example", "test"];
let mut targets: Vec<DeclaredTarget> = Vec::new();
let mut active: Option<usize> = None;
for raw in content.lines() {
let line = match raw.split_once('#') {
Some((before, _)) => before,
None => raw,
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with("[[") && trimmed.ends_with("]]") {
let inner = trimmed[2..trimmed.len() - 2].trim();
if KINDS.contains(&inner) {
targets.push(DeclaredTarget {
kind: inner.to_string(),
name: None,
path: None,
});
active = Some(targets.len() - 1);
} else {
active = None;
}
continue;
}
if trimmed.starts_with('[') && trimmed.ends_with(']') {
active = None;
continue;
}
if let Some(idx) = active {
if let Some(val) = trimmed
.strip_prefix("name")
.and_then(|rest| rest.trim_start().strip_prefix('='))
{
if let Some(s) = collect_quoted(val).into_iter().next() {
targets[idx].name = Some(s);
}
} else if let Some(val) = trimmed
.strip_prefix("path")
.and_then(|rest| rest.trim_start().strip_prefix('='))
{
if let Some(s) = collect_quoted(val).into_iter().next() {
targets[idx].path = Some(s);
}
}
}
}
targets
}
fn resolve_target_path(target: &DeclaredTarget) -> Option<String> {
if let Some(path) = &target.path {
return Some(path.clone());
}
let name = target.name.as_ref()?;
let dir = match target.kind.as_str() {
"bench" => "benches",
"example" => "examples",
"test" => "tests",
"bin" => "src/bin",
_ => return None,
};
Some(format!("{dir}/{name}.rs"))
}
#[test]
fn test_dockerfile_copies_all_declared_target_dirs() {
let targets = parse_declared_targets(&read_repo_file("Cargo.toml"));
assert!(
!targets.is_empty(),
"failed to parse any `[[bench]]`/`[[bin]]`/`[[example]]`/`[[test]]` \
targets from root Cargo.toml"
);
let sources = dockerfile_copy_sources(&read_repo_file("Dockerfile"));
for target in &targets {
let resolved = resolve_target_path(target).unwrap_or_else(|| {
panic!(
"declared [[{}]] target sets neither `name` nor `path` in Cargo.toml",
target.kind
)
});
let top = resolved.split('/').next().unwrap_or(resolved.as_str());
let covered = sources
.iter()
.any(|s| s == top || s == "." || top.starts_with(&format!("{s}/")));
assert!(
covered,
"declared [[{}]] target `{}` (source `{resolved}`, top dir `{top}`) is \
not COPYed into the Docker builder stage — add a `COPY` covering \
`{top}` (builder COPY sources: {sources:?})",
target.kind,
target.name.as_deref().unwrap_or("<unnamed>"),
);
}
}
#[test]
fn test_dockerfile_copies_build_script_inputs() {
let sources = dockerfile_copy_sources(&read_repo_file("Dockerfile"));
if repo_root().join("build.rs").exists() {
let covered = sources.iter().any(|s| s == "build.rs" || s == ".");
assert!(
covered,
"repo has a root `build.rs` but the Docker builder stage never \
COPYs it — the proto build script will not run and \
`tonic::include_proto!` will have no `$OUT_DIR` output \
(builder COPY sources: {sources:?})"
);
}
if repo_root().join("proto").exists() {
let covered = sources
.iter()
.any(|s| s == "proto" || s == "." || "proto".starts_with(&format!("{s}/")));
assert!(
covered,
"repo has a root `proto/` directory (the build script's \
`compile_protos` input) but the Docker builder stage never COPYs \
it — the proto codegen will fail (builder COPY sources: {sources:?})"
);
}
}