use std::fs;
use std::process::Command;
use tempfile::TempDir;
use crate::common::{setup_cc_project, run_rsconstruct, run_rsconstruct_with_env};
#[test]
fn cc_single_file_compile_single_c_file() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/main.c"),
"int main() { return 0; }\n"
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"rsconstruct build failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
assert!(project_path.join("out/cc_single_file/src/main.elf").exists(), "Executable should exist");
}
#[test]
fn cc_single_file_incremental_skip() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/main.c"),
"int main() { return 0; }\n"
).unwrap();
let output1 = run_rsconstruct_with_env(project_path, &["build", "-v"], &[("NO_COLOR", "1")]);
assert!(output1.status.success());
let stdout1 = String::from_utf8_lossy(&output1.stdout);
assert!(stdout1.contains("Processing:"), "First build should process: {}", stdout1);
let output2 = run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output2.status.success());
let stdout2 = String::from_utf8_lossy(&output2.stdout);
assert!(stdout2.contains("[cc_single_file] Skipping (unchanged):"), "Second build should skip: {}", stdout2);
}
#[test]
fn cc_single_file_header_dependency() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/utils.h"),
"#ifndef UTILS_H\n#define UTILS_H\n#define VALUE 42\n#endif\n"
).unwrap();
fs::write(
project_path.join("src/main.c"),
"#include \"utils.h\"\nint main() { return VALUE - 42; }\n"
).unwrap();
let output1 = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output1.status.success(),
"First build failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output1.stdout),
String::from_utf8_lossy(&output1.stderr));
std::thread::sleep(std::time::Duration::from_millis(100));
fs::write(
project_path.join("src/utils.h"),
"#ifndef UTILS_H\n#define UTILS_H\n#define VALUE 42\n#define OTHER 10\n#endif\n"
).unwrap();
let output2 = run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output2.status.success(),
"Rebuild failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output2.stdout),
String::from_utf8_lossy(&output2.stderr));
let stdout2 = String::from_utf8_lossy(&output2.stdout);
assert!(stdout2.contains("Processing:"),
"Should recompile after header change: {}", stdout2);
}
#[test]
fn cc_single_file_mixed_c_and_cpp() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/helper.c"),
"int main() { return 0; }\n"
).unwrap();
fs::write(
project_path.join("src/main.cc"),
"int main() { return 0; }\n"
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"Mixed C/C++ build failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
assert!(project_path.join("out/cc_single_file/src/helper.elf").exists(), "C executable should exist");
assert!(project_path.join("out/cc_single_file/src/main.elf").exists(), "C++ executable should exist");
}
#[test]
fn cc_single_file_clean() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/main.c"),
"int main() { return 0; }\n"
).unwrap();
let build_output = run_rsconstruct(project_path, &["build"]);
assert!(build_output.status.success());
assert!(project_path.join("out/cc_single_file/src/main.elf").exists());
let clean_output = run_rsconstruct(project_path, &["clean", "outputs"]);
assert!(clean_output.status.success());
assert!(!project_path.join("out/cc_single_file/src/main.elf").exists(), "output should be removed after clean");
assert!(project_path.join(".rsconstruct").exists(), ".rsconstruct cache should be preserved after clean");
}
#[test]
fn cc_single_file_dry_run() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/main.c"),
"int main() { return 0; }\n"
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build", "--dry-run"], &[("NO_COLOR", "1")]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("BUILD"), "Dry run should show BUILD for cc products: {}", stdout);
assert!(!project_path.join("out/cc_single_file/src/main.elf").exists(), "Dry run should not compile");
}
#[test]
fn cc_single_file_config_change_triggers_rebuild() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/main.c"),
"int main() { return 0; }\n"
).unwrap();
let output1 = run_rsconstruct_with_env(project_path, &["build", "-v"], &[("NO_COLOR", "1")]);
assert!(output1.status.success(),
"First build failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output1.stdout),
String::from_utf8_lossy(&output1.stderr));
let stdout1 = String::from_utf8_lossy(&output1.stdout);
assert!(stdout1.contains("Processing:"), "First build should process: {}", stdout1);
let output2 = run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output2.status.success());
let stdout2 = String::from_utf8_lossy(&output2.stdout);
assert!(stdout2.contains("[cc_single_file] Skipping (unchanged):"), "Second build should skip: {}", stdout2);
fs::write(
project_path.join("rsconstruct.toml"),
"[processor.cc_single_file]\nsrc_dirs = [\"src\"]\ncflags = [\"-O2\"]\n"
).unwrap();
let output3 = run_rsconstruct_with_env(project_path, &["build", "-v"], &[("NO_COLOR", "1")]);
assert!(output3.status.success(),
"Third build failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output3.stdout),
String::from_utf8_lossy(&output3.stderr));
let stdout3 = String::from_utf8_lossy(&output3.stdout);
assert!(stdout3.contains("Processing:"),
"Build after config change should reprocess, not skip: {}", stdout3);
}
#[test]
fn cc_single_file_per_file_compile_flags() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/flagtest.c"),
r#"// EXTRA_COMPILE_FLAGS_AFTER=-DTEST_VALUE=42
#include <stdio.h>
int main() {
printf("%d\n", TEST_VALUE);
return 0;
}
"#
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"Build with per-file compile flags failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
assert!(project_path.join("out/cc_single_file/src/flagtest.elf").exists(),
"Executable with per-file compile flags should exist");
let run_output = Command::new(project_path.join("out/cc_single_file/src/flagtest.elf"))
.output()
.expect("Failed to run flagtest");
assert!(run_output.status.success(), "flagtest exited with error");
let stdout = String::from_utf8_lossy(&run_output.stdout);
assert!(stdout.trim() == "42",
"Executable should output 42, got: {}", stdout.trim());
}
#[test]
fn cc_single_file_per_file_link_flags() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/mathtest.c"),
r#"// EXTRA_LINK_FLAGS_AFTER=-lm
#include <stdio.h>
#include <math.h>
int main() {
printf("%.0f\n", sqrt(144.0));
return 0;
}
"#
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"Build with per-file link flags failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
assert!(project_path.join("out/cc_single_file/src/mathtest.elf").exists(),
"Executable with per-file link flags should exist");
let run_output = Command::new(project_path.join("out/cc_single_file/src/mathtest.elf"))
.output()
.expect("Failed to run mathtest");
assert!(run_output.status.success(), "mathtest exited with error");
let stdout = String::from_utf8_lossy(&run_output.stdout);
assert!(stdout.trim() == "12",
"Executable should output 12, got: {}", stdout.trim());
}
#[test]
fn cc_single_file_per_file_backtick_substitution() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/backtick.c"),
r#"// EXTRA_COMPILE_FLAGS_AFTER=`echo -DBACKTICK_VAL=99`
#include <stdio.h>
int main() {
printf("%d\n", BACKTICK_VAL);
return 0;
}
"#
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"Build with backtick substitution failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
assert!(project_path.join("out/cc_single_file/src/backtick.elf").exists(),
"Executable with backtick substitution should exist");
let run_output = Command::new(project_path.join("out/cc_single_file/src/backtick.elf"))
.output()
.expect("Failed to run backtick");
assert!(run_output.status.success(), "backtick exited with error");
let stdout = String::from_utf8_lossy(&run_output.stdout);
assert!(stdout.trim() == "99",
"Executable should output 99, got: {}", stdout.trim());
}
#[test]
fn cc_single_file_per_file_no_flags() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/plain.c"),
r#"#include <stdio.h>
int main() {
printf("hello\n");
return 0;
}
"#
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"Build without per-file flags failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
assert!(project_path.join("out/cc_single_file/src/plain.elf").exists(),
"Executable without per-file flags should exist");
let run_output = Command::new(project_path.join("out/cc_single_file/src/plain.elf"))
.output()
.expect("Failed to run plain");
assert!(run_output.status.success(), "plain exited with error");
let stdout = String::from_utf8_lossy(&run_output.stdout);
assert!(stdout.trim() == "hello",
"Executable should output hello, got: {}", stdout.trim());
}
#[test]
fn cc_single_file_per_file_compile_cmd() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/compilecmd.c"),
r#"// EXTRA_COMPILE_CMD=echo -DCMD_VAL=77
#include <stdio.h>
int main() {
printf("%d\n", CMD_VAL);
return 0;
}
"#
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"Build with EXTRA_COMPILE_CMD failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
assert!(project_path.join("out/cc_single_file/src/compilecmd.elf").exists(),
"Executable with EXTRA_COMPILE_CMD should exist");
let run_output = Command::new(project_path.join("out/cc_single_file/src/compilecmd.elf"))
.output()
.expect("Failed to run compilecmd");
assert!(run_output.status.success(), "compilecmd exited with error");
let stdout = String::from_utf8_lossy(&run_output.stdout);
assert!(stdout.trim() == "77",
"Executable should output 77, got: {}", stdout.trim());
}
#[test]
fn cc_single_file_per_file_link_cmd() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/linkcmd.c"),
r#"// EXTRA_LINK_CMD=echo -lm
#include <stdio.h>
#include <math.h>
int main() {
printf("%.0f\n", sqrt(144.0));
return 0;
}
"#
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"Build with EXTRA_LINK_CMD failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
assert!(project_path.join("out/cc_single_file/src/linkcmd.elf").exists(),
"Executable with EXTRA_LINK_CMD should exist");
let run_output = Command::new(project_path.join("out/cc_single_file/src/linkcmd.elf"))
.output()
.expect("Failed to run linkcmd");
assert!(run_output.status.success(), "linkcmd exited with error");
let stdout = String::from_utf8_lossy(&run_output.stdout);
assert!(stdout.trim() == "12",
"Executable should output 12, got: {}", stdout.trim());
}
#[test]
fn cc_single_file_per_file_block_comment_star_prefix() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/blockstar.c"),
r#"/*
* EXTRA_LINK_FLAGS_AFTER=-lm
*/
#include <stdio.h>
#include <math.h>
int main() {
printf("%.0f\n", sqrt(144.0));
return 0;
}
"#
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"Build with block comment * prefix failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
assert!(project_path.join("out/cc_single_file/src/blockstar.elf").exists(),
"Executable with block comment * prefix should exist");
let run_output = Command::new(project_path.join("out/cc_single_file/src/blockstar.elf"))
.output()
.expect("Failed to run blockstar");
assert!(run_output.status.success(), "blockstar exited with error");
let stdout = String::from_utf8_lossy(&run_output.stdout);
assert!(stdout.trim() == "12",
"Executable should output 12, got: {}", stdout.trim());
}
#[test]
fn cc_single_file_per_file_compile_shell() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/compileshell.c"),
r#"// EXTRA_COMPILE_SHELL=echo -DSHELL_VALUE=$(echo 77)
#include <stdio.h>
int main() {
printf("%d\n", SHELL_VALUE);
return 0;
}
"#
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"Build with EXTRA_COMPILE_SHELL failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
assert!(project_path.join("out/cc_single_file/src/compileshell.elf").exists(),
"Executable with EXTRA_COMPILE_SHELL should exist");
let run_output = Command::new(project_path.join("out/cc_single_file/src/compileshell.elf"))
.output()
.expect("Failed to run compileshell");
assert!(run_output.status.success(), "compileshell exited with error");
let stdout = String::from_utf8_lossy(&run_output.stdout);
assert!(stdout.trim() == "77",
"Executable should output 77, got: {}", stdout.trim());
}
#[test]
fn cc_single_file_per_file_link_shell() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/linkshell.c"),
r#"// EXTRA_LINK_SHELL=echo -lm
#include <stdio.h>
#include <math.h>
int main() {
printf("%.0f\n", sqrt(49.0));
return 0;
}
"#
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"Build with EXTRA_LINK_SHELL failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
assert!(project_path.join("out/cc_single_file/src/linkshell.elf").exists(),
"Executable with EXTRA_LINK_SHELL should exist");
let run_output = Command::new(project_path.join("out/cc_single_file/src/linkshell.elf"))
.output()
.expect("Failed to run linkshell");
assert!(run_output.status.success(), "linkshell exited with error");
let stdout = String::from_utf8_lossy(&run_output.stdout);
assert!(stdout.trim() == "7",
"Executable should output 7, got: {}", stdout.trim());
}
#[test]
fn cc_single_file_direct_header_change_triggers_rebuild() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/direct.h"),
"#define DIRECT_VAL 10\n"
).unwrap();
fs::write(
project_path.join("src/main.c"),
"#include \"direct.h\"\nint main() { return DIRECT_VAL - 10; }\n"
).unwrap();
let output1 = run_rsconstruct_with_env(project_path, &["build", "-v"], &[("NO_COLOR", "1")]);
assert!(output1.status.success(),
"First build failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output1.stdout),
String::from_utf8_lossy(&output1.stderr));
let stdout1 = String::from_utf8_lossy(&output1.stdout);
assert!(stdout1.contains("Processing:"), "First build should process: {}", stdout1);
let output2 = run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output2.status.success());
let stdout2 = String::from_utf8_lossy(&output2.stdout);
assert!(stdout2.contains("[cc_single_file] Skipping (unchanged):"),
"Second build should skip: {}", stdout2);
fs::write(
project_path.join("src/direct.h"),
"#define DIRECT_VAL 20\n"
).unwrap();
let output3 = run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output3.status.success(),
"Rebuild after direct header change failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output3.stdout),
String::from_utf8_lossy(&output3.stderr));
let stdout3 = String::from_utf8_lossy(&output3.stdout);
assert!(stdout3.contains("Processing:"),
"Should recompile after direct header change: {}", stdout3);
let run_output = Command::new(project_path.join("out/cc_single_file/src/main.elf"))
.output()
.expect("Failed to run main");
assert!(!run_output.status.success(),
"Executable should exit nonzero after header change (DIRECT_VAL=20, returns 20-10=10)");
}
#[test]
fn cc_single_file_indirect_header_change_triggers_rebuild() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/indirect.h"),
"#define INDIRECT_VAL 5\n"
).unwrap();
fs::write(
project_path.join("src/middle.h"),
"#include \"indirect.h\"\n#define MIDDLE_VAL (INDIRECT_VAL + 1)\n"
).unwrap();
fs::write(
project_path.join("src/main.c"),
"#include \"middle.h\"\nint main() { return MIDDLE_VAL - 6; }\n"
).unwrap();
let output1 = run_rsconstruct_with_env(project_path, &["build", "-v"], &[("NO_COLOR", "1")]);
assert!(output1.status.success(),
"First build failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output1.stdout),
String::from_utf8_lossy(&output1.stderr));
let stdout1 = String::from_utf8_lossy(&output1.stdout);
assert!(stdout1.contains("Processing:"), "First build should process: {}", stdout1);
let run_output = Command::new(project_path.join("out/cc_single_file/src/main.elf"))
.output()
.expect("Failed to run main");
assert!(run_output.status.success(), "Executable should exit 0 initially");
let output2 = run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output2.status.success());
let stdout2 = String::from_utf8_lossy(&output2.stdout);
assert!(stdout2.contains("[cc_single_file] Skipping (unchanged):"),
"Second build should skip: {}", stdout2);
fs::write(
project_path.join("src/indirect.h"),
"#define INDIRECT_VAL 100\n"
).unwrap();
let output3 = run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output3.status.success(),
"Rebuild after indirect header change failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output3.stdout),
String::from_utf8_lossy(&output3.stderr));
let stdout3 = String::from_utf8_lossy(&output3.stdout);
assert!(stdout3.contains("Processing:"),
"Should recompile after indirect header change: {}", stdout3);
let run_output2 = Command::new(project_path.join("out/cc_single_file/src/main.elf"))
.output()
.expect("Failed to run main after indirect header change");
assert!(!run_output2.status.success(),
"Executable should exit nonzero after indirect header change (MIDDLE_VAL=101, returns 101-6=95)");
}
#[test]
fn cc_single_file_new_include_triggers_dependency_recomputation() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
setup_cc_project(project_path);
fs::write(
project_path.join("src/main.c"),
"int main() { return 0; }\n"
).unwrap();
let output1 = run_rsconstruct_with_env(project_path, &["build", "-v"], &[("NO_COLOR", "1")]);
assert!(output1.status.success(),
"First build failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output1.stdout),
String::from_utf8_lossy(&output1.stderr));
let stdout1 = String::from_utf8_lossy(&output1.stdout);
assert!(stdout1.contains("Processing:"), "First build should process: {}", stdout1);
let output2 = run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output2.status.success());
let stdout2 = String::from_utf8_lossy(&output2.stdout);
assert!(stdout2.contains("[cc_single_file] Skipping (unchanged):"),
"Second build should skip: {}", stdout2);
fs::write(
project_path.join("src/newheader.h"),
"#define NEW_VAL 55\n"
).unwrap();
std::thread::sleep(std::time::Duration::from_millis(100));
fs::write(
project_path.join("src/main.c"),
"#include \"newheader.h\"\nint main() { return NEW_VAL - 55; }\n"
).unwrap();
let output3 = run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output3.status.success(),
"Build after adding include failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output3.stdout),
String::from_utf8_lossy(&output3.stderr));
let stdout3 = String::from_utf8_lossy(&output3.stdout);
assert!(stdout3.contains("Processing:"),
"Should recompile after source changed to add include: {}", stdout3);
let output4 = run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output4.status.success());
let stdout4 = String::from_utf8_lossy(&output4.stdout);
assert!(stdout4.contains("[cc_single_file] Skipping (unchanged):"),
"Build should skip after no changes: {}", stdout4);
std::thread::sleep(std::time::Duration::from_millis(100));
fs::write(
project_path.join("src/newheader.h"),
"#define NEW_VAL 99\n"
).unwrap();
let output5 = run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output5.status.success(),
"Build after modifying new header failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output5.stdout),
String::from_utf8_lossy(&output5.stderr));
let stdout5 = String::from_utf8_lossy(&output5.stdout);
assert!(stdout5.contains("Processing:"),
"Should recompile after newly-tracked header changed: {}", stdout5);
let run_output = Command::new(project_path.join("out/cc_single_file/src/main.elf"))
.output()
.expect("Failed to run main");
assert!(!run_output.status.success(),
"Executable should exit nonzero after header change (NEW_VAL=99, returns 99-55=44)");
}
#[test]
fn cc_single_file_angle_bracket_include_dependency() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
fs::create_dir_all(project_path.join("src")).unwrap();
fs::create_dir_all(project_path.join("include")).unwrap();
fs::write(
project_path.join("rsconstruct.toml"),
r#"[processor.cc_single_file]
src_dirs = ["src"]
include_paths = ["include"]
[analyzer.cpp]
include_paths = ["include"]
"#
).unwrap();
fs::write(
project_path.join("include/mylib.h"),
"#ifndef MYLIB_H\n#define MYLIB_H\n#define MYLIB_VALUE 123\n#endif\n"
).unwrap();
fs::write(
project_path.join("src/main.c"),
"#include <mylib.h>\nint main() { return MYLIB_VALUE - 123; }\n"
).unwrap();
let output1 = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output1.status.success(),
"First build failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output1.stdout),
String::from_utf8_lossy(&output1.stderr));
let output2 = run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output2.status.success());
let stdout2 = String::from_utf8_lossy(&output2.stdout);
assert!(stdout2.contains("[cc_single_file] Skipping (unchanged):"),
"Second build should skip: {}", stdout2);
fs::write(
project_path.join("include/mylib.h"),
"#ifndef MYLIB_H\n#define MYLIB_H\n#define MYLIB_VALUE 456\n#endif\n"
).unwrap();
let output3 = run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output3.status.success(),
"Rebuild after angle-bracket header change failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output3.stdout),
String::from_utf8_lossy(&output3.stderr));
let stdout3 = String::from_utf8_lossy(&output3.stdout);
assert!(stdout3.contains("Processing:"),
"Should recompile after angle-bracket header change: {}", stdout3);
}
#[test]
fn cc_single_file_multiple_compiler_profiles() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
fs::create_dir_all(project_path.join("src")).unwrap();
fs::write(
project_path.join("rsconstruct.toml"),
r#"[processor.cc_single_file]
src_dirs = ["src"]
[[processor.cc_single_file.compilers]]
name = "gcc"
cc = "gcc"
cxx = "g++"
output_suffix = ".elf"
[[processor.cc_single_file.compilers]]
name = "clang"
cc = "clang"
cxx = "clang++"
output_suffix = ".elf"
"#
).unwrap();
fs::write(
project_path.join("src/main.c"),
"int main() { return 0; }\n"
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"rsconstruct build with multiple compilers failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
assert!(project_path.join("out/cc_single_file/gcc/src/main.elf").exists(),
"GCC executable should exist");
assert!(project_path.join("out/cc_single_file/clang/src/main.elf").exists(),
"Clang executable should exist");
let gcc_output = Command::new(project_path.join("out/cc_single_file/gcc/src/main.elf"))
.output()
.expect("Failed to run gcc executable");
assert!(gcc_output.status.success(), "GCC executable should run successfully");
let clang_output = Command::new(project_path.join("out/cc_single_file/clang/src/main.elf"))
.output()
.expect("Failed to run clang executable");
assert!(clang_output.status.success(), "Clang executable should run successfully");
}
#[test]
fn cc_single_file_missing_include_errors() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
fs::create_dir_all(project_path.join("src")).unwrap();
fs::write(
project_path.join("rsconstruct.toml"),
r#"[processor.cc_single_file]
src_dirs = ["src"]
[analyzer.icpp]
"#
).unwrap();
fs::write(
project_path.join("src/main.c"),
r#"#include "nonexistent.h"
int main() { return 0; }
"#
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(!output.status.success(),
"Build with missing include should fail");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("Include not found") && stderr.contains("nonexistent.h"),
"Error should mention missing include: {}", stderr);
}
#[test]
fn cc_single_file_profile_specific_flags() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
fs::create_dir_all(project_path.join("src")).unwrap();
fs::write(
project_path.join("rsconstruct.toml"),
r#"[processor.cc_single_file]
src_dirs = ["src"]
[[processor.cc_single_file.compilers]]
name = "gcc"
cc = "gcc"
cxx = "g++"
[[processor.cc_single_file.compilers]]
name = "clang"
cc = "clang"
cxx = "clang++"
"#
).unwrap();
fs::write(
project_path.join("src/profile_test.c"),
r#"// EXTRA_COMPILE_FLAGS_BEFORE=-DCOMMON
// EXTRA_COMPILE_FLAGS_BEFORE[gcc]=-DCOMPILER_GCC
// EXTRA_COMPILE_FLAGS_BEFORE[clang]=-DCOMPILER_CLANG
#include <stdio.h>
int main() {
#ifdef COMMON
printf("COMMON ");
#endif
#ifdef COMPILER_GCC
printf("GCC\n");
#endif
#ifdef COMPILER_CLANG
printf("CLANG\n");
#endif
return 0;
}
"#
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"Build with profile-specific flags failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
let gcc_exe = project_path.join("out/cc_single_file/gcc/src/profile_test.elf");
assert!(gcc_exe.exists(), "GCC executable should exist");
let gcc_output = Command::new(&gcc_exe)
.output()
.expect("Failed to run GCC executable");
let gcc_stdout = String::from_utf8_lossy(&gcc_output.stdout);
assert!(gcc_stdout.contains("COMMON") && gcc_stdout.contains("GCC") && !gcc_stdout.contains("CLANG"),
"GCC build should have COMMON and GCC defined, got: {}", gcc_stdout);
let clang_exe = project_path.join("out/cc_single_file/clang/src/profile_test.elf");
assert!(clang_exe.exists(), "Clang executable should exist");
let clang_output = Command::new(&clang_exe)
.output()
.expect("Failed to run Clang executable");
let clang_stdout = String::from_utf8_lossy(&clang_output.stdout);
assert!(clang_stdout.contains("COMMON") && clang_stdout.contains("CLANG") && !clang_stdout.contains("GCC"),
"Clang build should have COMMON and CLANG defined, got: {}", clang_stdout);
}
#[test]
fn cc_single_file_exclude_profile() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
fs::create_dir_all(project_path.join("src")).unwrap();
fs::write(
project_path.join("rsconstruct.toml"),
r#"[processor.cc_single_file]
src_dirs = ["src"]
[[processor.cc_single_file.compilers]]
name = "gcc"
cc = "gcc"
cxx = "g++"
[[processor.cc_single_file.compilers]]
name = "clang"
cc = "clang"
cxx = "clang++"
"#
).unwrap();
fs::write(
project_path.join("src/both.c"),
r#"int main() { return 0; }
"#
).unwrap();
fs::write(
project_path.join("src/gcc_only.c"),
r#"// EXCLUDE_PROFILE=clang
int main() { return 0; }
"#
).unwrap();
fs::write(
project_path.join("src/clang_only.c"),
r#"// EXCLUDE_PROFILE=gcc
int main() { return 0; }
"#
).unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output.status.success(),
"Build failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr));
assert!(project_path.join("out/cc_single_file/gcc/src/both.elf").exists(),
"both.elf should exist for gcc");
assert!(project_path.join("out/cc_single_file/clang/src/both.elf").exists(),
"both.elf should exist for clang");
assert!(project_path.join("out/cc_single_file/gcc/src/gcc_only.elf").exists(),
"gcc_only.elf should exist for gcc");
assert!(!project_path.join("out/cc_single_file/clang/src/gcc_only.elf").exists(),
"gcc_only.elf should NOT exist for clang");
assert!(!project_path.join("out/cc_single_file/gcc/src/clang_only.elf").exists(),
"clang_only.elf should NOT exist for gcc");
assert!(project_path.join("out/cc_single_file/clang/src/clang_only.elf").exists(),
"clang_only.elf should exist for clang");
}