From 03f72adadeb273e5f36ae805e0c088ba0416cf11 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Fri, 10 Apr 2026 14:31:58 +0800 Subject: [PATCH] feat: Update version to 0.4.0, add test command, and improve documentation for testing functionality --- Cargo.lock | 2 +- Cargo.toml | 2 +- docs/commands/dcr-build.md | 1 + docs/commands/dcr-gen.md | 2 + docs/configuration/build-profiles.md | 4 +- .../compilers-and-linking.md | 2 +- src/cli/gen.rs | 22 +- src/cli/help.rs | 1 + src/cli/init.rs | 4 +- src/cli/mod.rs | 1 + src/cli/new.rs | 4 +- src/cli/test.rs | 260 ++++++++++++++++++ src/config.rs | 74 ++++- src/core/builder/unix_cc.rs | 5 +- src/main.rs | 1 + tests/cli_basic.rs | 30 +- 16 files changed, 387 insertions(+), 28 deletions(-) create mode 100644 src/cli/test.rs diff --git a/Cargo.lock b/Cargo.lock index a846250..4ceeb9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,7 +90,7 @@ dependencies = [ [[package]] name = "dcr" -version = "0.3.0" +version = "0.4.0" dependencies = [ "glob", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index d982357..f02502e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dcr" -version = "0.3.0" +version = "0.4.0" edition = "2024" build = "build.rs" diff --git a/docs/commands/dcr-build.md b/docs/commands/dcr-build.md index 7e805f5..e8029f2 100644 --- a/docs/commands/dcr-build.md +++ b/docs/commands/dcr-build.md @@ -57,6 +57,7 @@ dcr build --debug --clean - Incremental rebuild checks source/object mtime and tracked header dependencies. - `--force` skips build cache checks and recompiles. - `--clean` removes `target/` and `build.clean` paths before building. +- Default GCC/Clang profile flags: `debug` -> `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`, `release` -> `-O3 -DNDEBUG`. - For `language = "asm"` with `compiler = "as"`/`"gas"`, use `.s` files (no preprocessing). For `.S`, use `gcc` or `clang`. - In workspace root, `dcr build` builds all members in dependency order. - `build.exclude` removes paths from source/header collection; `build.include` re-allows matching paths and has priority over `exclude`. diff --git a/docs/commands/dcr-gen.md b/docs/commands/dcr-gen.md index f9311dc..2215551 100644 --- a/docs/commands/dcr-gen.md +++ b/docs/commands/dcr-gen.md @@ -89,6 +89,7 @@ Usage in VSCode: - Open the Command Palette (`Ctrl+Shift+P`) and select **"Debug: Start Debugging"** to run debugger - Press `Ctrl+Shift+B` to run the build task - Clangd will provide IntelliSense and diagnostics based on `compile_commands.json` +- Generated `launch.json` uses CodeLLDB (`type: "lldb"`), so install `vadimcn.vscode-lldb` ### `dcr gen clion` @@ -152,6 +153,7 @@ When running `dcr gen` in a workspace root, all commands process both the root p - Verify binary paths in `launch.json` match actual build output (`target//binary-name`) - For workspace projects, each member's debug config uses its own `target/` directory - Ensure GDB/LLDB is installed: `gdb --version` or `lldb --version` +- If VSCode reports unsupported debugger type, install the extension that matches `launch.json` type (`vadimcn.vscode-lldb` for `lldb`) ### CLion not recognizing includes diff --git a/docs/configuration/build-profiles.md b/docs/configuration/build-profiles.md index 0e22ec2..b7e9a08 100644 --- a/docs/configuration/build-profiles.md +++ b/docs/configuration/build-profiles.md @@ -21,7 +21,7 @@ You can also add `--force` or `--clean` to `build`/`run`. ### GCC/Clang-like toolchains - `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG` -- `release`: `-O3 -DNDEBUG -march=native` +- `release`: `-O3 -DNDEBUG` ### MSVC toolchain @@ -40,7 +40,7 @@ Default compiler flags: GCC/Clang: - `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG` -- `release`: `-O3 -DNDEBUG -march=native` +- `release`: `-O3 -DNDEBUG` MSVC: diff --git a/docs/dependencies-and-build/compilers-and-linking.md b/docs/dependencies-and-build/compilers-and-linking.md index dd46751..d24214e 100644 --- a/docs/dependencies-and-build/compilers-and-linking.md +++ b/docs/dependencies-and-build/compilers-and-linking.md @@ -28,7 +28,7 @@ These flags are applied automatically based on profile and compiler backend. GCC/Clang: - `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG` -- `release`: `-O3 -DNDEBUG -march=native` +- `release`: `-O3 -DNDEBUG` MSVC: diff --git a/src/cli/gen.rs b/src/cli/gen.rs index 1e87f67..9dffc4f 100644 --- a/src/cli/gen.rs +++ b/src/cli/gen.rs @@ -801,7 +801,6 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec "release" => { cmd.push("-O3".to_string()); cmd.push("-DNDEBUG".to_string()); - cmd.push("-march=native".to_string()); } "debug" => { cmd.push("-O0".to_string()); @@ -922,7 +921,8 @@ fn gen_vscode(args: &[String]) -> i32 { fn gen_extensions_json() -> String { r#"{ "recommendations": [ - "llvm-vs-code-extensions.vscode-clangd" + "llvm-vs-code-extensions.vscode-clangd", + "vadimcn.vscode-lldb" ], "unwantedRecommendations": [ "ms-vscode.cpptools", @@ -1025,15 +1025,13 @@ fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String { let debug_entry = format!( r#" {{ "name": {name}, - "type": "cppdbg", + "type": "lldb", "request": "launch", "program": {prog}, "args": [], - "stopAtEntry": false, + "stopOnEntry": false, "cwd": {cwd}, - "environment": [], - "externalConsole": false, - "MIMode": "gdb", + "terminal": "integrated", "preLaunchTask": "dcr: build (debug)" }}"#, name = json_str(&format!("{} (debug)", info.name)), @@ -1044,15 +1042,13 @@ fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String { let release_entry = format!( r#" {{ "name": {name}, - "type": "cppdbg", + "type": "lldb", "request": "launch", "program": {prog}, "args": [], - "stopAtEntry": false, + "stopOnEntry": false, "cwd": {cwd}, - "environment": [], - "externalConsole": false, - "MIMode": "gdb", + "terminal": "integrated", "preLaunchTask": "dcr: build (release)" }}"#, name = json_str(&format!("{} (release)", info.name)), @@ -1069,7 +1065,7 @@ fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String { configs.push( r#" { "name": "(placeholder — no binary targets found)", - "type": "cppdbg", + "type": "lldb", "request": "launch", "program": "", "cwd": "${workspaceFolder}" diff --git a/src/cli/help.rs b/src/cli/help.rs index c4a104e..3601dbe 100644 --- a/src/cli/help.rs +++ b/src/cli/help.rs @@ -12,6 +12,7 @@ pub fn help() -> i32 { println!(" init Initialize the current directory as a project"); println!(" build [--profile] Build the project (default: --debug)"); println!(" run [--profile] Build and run the project (default: --debug)"); + println!(" test Run project tests (alias: tests)"); println!(" clean Remove the target directory"); println!(" gen Generate IDE integration files"); printc("FLAGS:", BOLD_GREEN); diff --git a/src/cli/init.rs b/src/cli/init.rs index 60e36f2..80ccbcb 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -2,7 +2,7 @@ use crate::config::FILE_MAIN_C; use crate::core::config::Config; use crate::utils::fs::check_dir; use crate::utils::log::{error, warn}; -use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; +use crate::utils::text::{colored, printc, BOLD_CYAN, BOLD_GREEN}; use std::fs; use std::io::Write; use toml::Value; @@ -65,7 +65,7 @@ pub fn init(args: &[String]) -> i32 { return 1; } println!( - " {} Created file {}\n", + " {} Created file {}", colored("✔", BOLD_GREEN), colored("src/main.c", BOLD_CYAN) ); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 814950c..f0fa7bb 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -7,3 +7,4 @@ pub mod help; pub mod init; pub mod new; pub mod run; +pub mod test; diff --git a/src/cli/new.rs b/src/cli/new.rs index 1d14148..a779c43 100644 --- a/src/cli/new.rs +++ b/src/cli/new.rs @@ -2,7 +2,7 @@ use crate::config::FILE_MAIN_C; use crate::core::config::Config; use crate::utils::fs::check_dir; use crate::utils::log::{error, warn}; -use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; +use crate::utils::text::{colored, printc, BOLD_CYAN, BOLD_GREEN}; use std::fs; use std::io::Write; use toml::Value; @@ -86,7 +86,7 @@ pub fn new(args: &[String]) -> i32 { return 1; } println!( - " {} Created file {}\n", + " {} Created file {}", colored("✔", BOLD_GREEN), colored("src/main.c", BOLD_CYAN) ); diff --git a/src/cli/test.rs b/src/cli/test.rs new file mode 100644 index 0000000..a610cc8 --- /dev/null +++ b/src/cli/test.rs @@ -0,0 +1,260 @@ +use crate::cli::build; +use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags}; +use crate::core::config::Config; +use crate::utils::fs::find_project_root; +use crate::utils::log::error; +use crate::utils::text::{BOLD_GREEN, BOLD_RED, RESET, colored}; +use std::env; +use std::fs; +use std::path::Path; +use std::process::Command; + +const BOLD_BLUE: &str = "\x1b[1m\x1b[94m"; + +pub fn test(args: &[String]) -> i32 { + let mut init_header = false; + for arg in args { + if arg == "--help" { + println!("USAGE:"); + println!(" dcr test [--init]"); + println!(); + println!("ALIASES:"); + println!(" dcr tests"); + println!(); + println!("DESCRIPTION:"); + println!(" Runs project tests and prints a unified testsuite report."); + println!(); + println!("OPTIONS:"); + println!(" --init Create tests/dcr_test.h in current project"); + return 0; + } + if arg == "--init" { + init_header = true; + continue; + } + error("Unknown argument"); + return 1; + } + + let start_dir = match std::env::current_dir() { + Ok(dir) => dir, + Err(_) => { + error("Failed to determine current directory"); + return 1; + } + }; + let root = match find_project_root(&start_dir) { + Ok(Some(dir)) => dir, + Ok(None) => { + error("dcr.toml file not found"); + return 1; + } + Err(_) => { + error("Failed to find project root"); + return 1; + } + }; + + if init_header { + match with_dir(&root, ensure_test_header) { + Ok(()) => {} + Err(msg) => { + error(&msg); + return 1; + } + } + } + + match with_dir(&root, run_testsuite) { + Ok(code) => code, + Err(msg) => { + error(&msg); + 1 + } + } +} + +fn run_testsuite() -> Result { + if build::build(&[String::from("--release")]) != 0 { + return Ok(1); + } + + let config = Config::open("./dcr.toml").map_err(|_| "Failed to read dcr.toml".to_string())?; + let name = config.get("package.name").and_then(|v| v.as_str()).ok_or("package.name not found in dcr.toml")?; + let kind = config.get("package.kind").and_then(|v| v.as_str()).unwrap_or("bin"); + + let compiler = env::var("DCR_CC").unwrap_or_else(|_| "cc".to_string()); + let profile = "release"; + let flags_vec = flags(profile).unwrap_or(&[]); + let flags_str = flags_vec.join(" "); + + let test_c = "./tests/test.c"; + if !Path::new(test_c).exists() { + return Err("tests/test.c not found; run 'dcr test --init' first".to_string()); + } + + // Compile test.c to object + let obj_path = "./tests/test.o"; + let compile_cmd = format!("{} -c {} -o {} {}", compiler, test_c, obj_path, flags_str); + let compile_status = Command::new("sh") + .arg("-c") + .arg(&compile_cmd) + .status() + .map_err(|_| format!("Failed to compile {}", test_c))?; + if !compile_status.success() { + return Err(format!("Compilation of {} failed", test_c)); + } + + // Link + let bin_path = format!("./tests/test{}", std::env::consts::EXE_SUFFIX); + let link_cmd = if kind == "lib" { + format!("{} {} -o {} -Ltarget/release -l{}", compiler, obj_path, bin_path, name) + } else { + format!("{} {} -o {}", compiler, obj_path, bin_path) + }; + let link_status = Command::new("sh") + .arg("-c") + .arg(&link_cmd) + .status() + .map_err(|_| format!("Failed to link {}", bin_path))?; + if !link_status.success() { + return Err(format!("Linking of {} failed", bin_path)); + } + + let out = Command::new(&bin_path) + .output() + .map_err(|_| format!("Failed to run `{bin_path}`"))?; + + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + if !stderr.trim().is_empty() { + eprint!("{}", stderr); + } + + let declared = extract_test_names("./tests/test.c"); + let mut pass = 0; + let mut skip = 0; + let mut fail = 0; + let mut parsed_any = false; + + for line in stdout.lines() { + let line = line.trim(); + if let Some(name) = line.strip_prefix("[PASS] ") { + println!("{} {}", colored("[PASS]", BOLD_GREEN), name); + pass += 1; + parsed_any = true; + continue; + } + if let Some(name) = line.strip_prefix("[SKIP] ") { + println!("{} {}", colored("[SKIP]", BOLD_BLUE), name); + skip += 1; + parsed_any = true; + continue; + } + if let Some(name) = line.strip_prefix("[FAIL] ") { + println!("{} {}", colored("[FAIL]", BOLD_RED), name); + fail += 1; + parsed_any = true; + continue; + } + if let Some((status, name)) = line.split_once('\t') { + match status { + "PASS" => { + println!("{} {}", colored("[PASS]", BOLD_GREEN), name); + pass += 1; + parsed_any = true; + } + "SKIP" => { + println!("{} {}", colored("[SKIP]", BOLD_BLUE), name); + skip += 1; + parsed_any = true; + } + "FAIL" => { + println!("{} {}", colored("[FAIL]", BOLD_RED), name); + fail += 1; + parsed_any = true; + } + _ => {} + } + } + } + + if !parsed_any { + if out.status.success() { + for name in &declared { + println!("{} {}", colored("[PASS]", BOLD_GREEN), name); + } + pass = declared.len() as i32; + } else { + println!("{} testsuite", colored("[FAIL]", BOLD_RED)); + fail = 1; + } + } + + let total = if parsed_any || declared.is_empty() { + pass + skip + fail + } else { + declared.len() as i32 + }; + + println!(); + println!("{}", colored("=====================", BOLD_GREEN)); + println!("{}", colored(" Testsuite summary ", BOLD_GREEN)); + println!("{}", colored("=====================", BOLD_GREEN)); + println!("TOTAL: {}", total); + print_field("PASS", pass, BOLD_GREEN); + print_field("SKIP", skip, BOLD_BLUE); + print_field("FAIL", fail, BOLD_RED); + println!("{}", colored("=====================", BOLD_GREEN)); + + if fail > 0 || !out.status.success() { + return Ok(1); + } + Ok(0) +} + +fn ensure_test_header() -> Result<(), String> { + fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?; + fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H) + .map_err(|_| "Failed to write tests/dcr_test.h".to_string())?; + println!("Created tests/dcr_test.h"); + fs::write("./tests/test.c", FILE_TEST_C) + .map_err(|_| "Failed to write tests/test.c".to_string())?; + println!("Created tests/test.c"); + Ok(()) +} + +fn extract_test_names(path: &str) -> Vec { + let Ok(content) = fs::read_to_string(path) else { + return Vec::new(); + }; + let mut names = Vec::new(); + for line in content.lines() { + let trimmed = line.trim(); + if let Some(rest) = trimmed.strip_prefix("TEST_CASE(") + && let Some(name) = rest.strip_suffix("),") + { + names.push(name.to_string()); + } + } + names +} + +fn print_field(label: &str, value: i32, color: &str) { + if value == 0 { + println!("{}: {}", label, value); + } else { + println!("{}{}: {}{}", color, label, value, RESET); + } +} + +fn with_dir(dir: &Path, f: F) -> Result +where + F: FnOnce() -> Result, +{ + let prev = std::env::current_dir().map_err(|_| "Failed to get current dir".to_string())?; + std::env::set_current_dir(dir).map_err(|_| "Failed to change directory".to_string())?; + let result = f(); + let _ = std::env::set_current_dir(prev); + result +} diff --git a/src/config.rs b/src/config.rs index 74579d5..0dde759 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,9 +7,81 @@ int main(void) { } "#; +pub const FILE_DCR_TEST_H: &str = r#"#pragma once +#include +#include + +#define DCR_TEST_PASS 0 +#define DCR_TEST_FAIL 1 +#define DCR_TEST_SKIP 2 + +#define EXPECT(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "assert failed: %s (%s:%d)\n", #expr, __FILE__, __LINE__); \ + return DCR_TEST_FAIL; \ + } \ + } while (0) + +#define SKIP(reason) \ + do { \ + (void)(reason); \ + return DCR_TEST_SKIP; \ + } while (0) + +#define TEST(name) static int name(void) +#define TEST_CASE(name) {#name, name} + +typedef int (*test_fn_t)(void); + +typedef struct { + const char *name; + test_fn_t fn; +} test_case_t; + +static inline int dcr_test_raw_mode(int argc, char **argv) { + return argc > 1 && strcmp(argv[1], "--dcr-raw") == 0; +} + +static inline void dcr_test_print_result(const char *name, int rc, int raw_mode) { + const char *tag = (rc == DCR_TEST_PASS) ? "PASS" : (rc == DCR_TEST_SKIP) ? "SKIP" : "FAIL"; + if (raw_mode) { + printf("%s\t%s\n", tag, name); + } else { + printf("[%s] %s\n", tag, name); + } +} +"#; + +pub const FILE_TEST_C: &str = r#"#include "dcr_test.h" + +TEST(test_example) { + EXPECT(1 + 1 == 2); + return DCR_TEST_PASS; +} + +static test_case_t CASES[] = { + TEST_CASE(test_example), +}; + +int main(int argc, char **argv) { + int raw = dcr_test_raw_mode(argc, argv); + int rc = 0; + int count = (int)(sizeof(CASES) / sizeof(CASES[0])); + for (int i = 0; i < count; ++i) { + int status = CASES[i].fn(); + dcr_test_print_result(CASES[i].name, status, raw); + if (status == DCR_TEST_FAIL) { + rc = 1; + } + } + return rc; +} +"#; + pub fn flags(profile: &str) -> Option<&'static [&'static str]> { match profile { - "release" => Some(&["-O3", "-DNDEBUG", "-march=native"]), + "release" => Some(&["-O3", "-DNDEBUG"]), "debug" => Some(&[ "-O0", "-g", diff --git a/src/core/builder/unix_cc.rs b/src/core/builder/unix_cc.rs index 8f017c5..635f66c 100644 --- a/src/core/builder/unix_cc.rs +++ b/src/core/builder/unix_cc.rs @@ -112,7 +112,7 @@ fn source_extensions(language: &str) -> Vec<&str> { fn default_flags(profile: &str) -> &'static [&'static str] { match profile { - "release" => &["-O3", "-DNDEBUG", "-march=native"], + "release" => &["-O3", "-DNDEBUG"], "debug" => &[ "-O0", "-g", @@ -156,9 +156,6 @@ fn build_objects( cmd.arg(format!("-std={}", ctx.standard)); } for flag in default_flags(ctx.profile) { - if ctx.platform.is_some() && *flag == "-march=native" { - continue; - } cmd.arg(flag); } for flag in ctx.cflags { diff --git a/src/main.rs b/src/main.rs index a6edff3..c4f04af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ fn main() { "init" => cli::init::init(rest), "build" => cli::build::build(rest), "run" => cli::run::run(rest), + "test" | "tests" => cli::test::test(rest), "clean" => cli::clean::clean(rest), "gen" => cli::r#gen::r#gen(rest), "--version" => { diff --git a/tests/cli_basic.rs b/tests/cli_basic.rs index a1213d5..5c3b6d7 100644 --- a/tests/cli_basic.rs +++ b/tests/cli_basic.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; -use std::sync::Once; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Once; use std::time::{SystemTime, UNIX_EPOCH}; static COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -218,3 +218,31 @@ fn workspace_build_and_clean_all() { "root target/debug should remain" ); } + +#[test] +fn dcr_test_runs_without_sandbox_dependency() { + let Some(compiler) = available_compiler() else { + eprintln!("no compiler found; skipping dcr test integration"); + return; + }; + + let dir = unique_sandbox_dir("dcr_test_independent"); + let out = run_dcr(&["init"], &dir); + assert!(out.status.success(), "dcr init should succeed"); + + let envs = [("DCR_CC", compiler)]; + let out_init = run_dcr_env(&["test", "--init"], &dir, &envs); + assert!(out_init.status.success(), "dcr test --init should succeed"); + + let out = run_dcr_env(&["test"], &dir, &envs); + assert!(out.status.success(), "dcr test should succeed"); + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stdout.contains("TOTAL: 1"), "TOTAL summary line missing"); + assert!( + stdout.contains("PASS: 1"), + "PASS summary line missing\nstdout:\n{}\nstderr:\n{}", + stdout, + stderr + ); +}