From 88eb59117f4db63a7eec1299a74cc5e9c8eb0e8a Mon Sep 17 00:00:00 2001 From: wawahejun Date: Sun, 4 May 2025 19:43:07 +0800 Subject: [PATCH 1/2] refactoring eval and learn --- .gitmodules | 3 + Cargo.lock | 257 +++++++++++++++++++++++++++++ exercises/learning-lm-rs | 1 + rustlings_result.json | 38 +++++ xtask/Cargo.toml | 6 + xtask/src/eval.rs | 346 ++++++++++++++++++++++++++++++++++++++- xtask/src/learn.rs | 57 ++++++- 7 files changed, 704 insertions(+), 4 deletions(-) create mode 100644 .gitmodules create mode 160000 exercises/learning-lm-rs create mode 100644 rustlings_result.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3fb8692 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "exercises/learning-lm-rs"] + path = exercises/learning-lm-rs + url = https://github.com/wawahejun/learning-lm-rs diff --git a/Cargo.lock b/Cargo.lock index 01ddb7e..25757bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.5.37" @@ -98,10 +116,39 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys", +] + [[package]] name = "course" version = "0.0.0" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "environment" version = "0.0.0" @@ -112,18 +159,83 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + [[package]] name = "proc-macro2" version = "1.0.95" @@ -142,6 +254,53 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[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 = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "strsim" version = "0.11.1" @@ -165,12 +324,104 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[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 = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -248,7 +499,13 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" name = "xtask" version = "0.1.0" dependencies = [ + "anyhow", "clap", + "colored", "course", "environment", + "indicatif", + "serde", + "serde_json", + "walkdir", ] diff --git a/exercises/learning-lm-rs b/exercises/learning-lm-rs new file mode 160000 index 0000000..e960baf --- /dev/null +++ b/exercises/learning-lm-rs @@ -0,0 +1 @@ +Subproject commit e960baf455d97b41c8e6f3bc8c3f55190b21e460 diff --git a/rustlings_result.json b/rustlings_result.json new file mode 100644 index 0000000..cffed5e --- /dev/null +++ b/rustlings_result.json @@ -0,0 +1,38 @@ +{ + "exercises": [ + { + "name": "main.rs", + "result": false + }, + { + "name": "tensor.rs", + "result": false + }, + { + "name": "model.rs", + "result": false + }, + { + "name": "config.rs", + "result": false + }, + { + "name": "kvcache.rs", + "result": false + }, + { + "name": "params.rs", + "result": false + }, + { + "name": "operators.rs", + "result": false + } + ], + "statistics": { + "total_exercations": 7, + "total_succeeds": 0, + "total_failures": 7, + "total_time": 0 + } +} \ No newline at end of file diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 437b1d6..33754ee 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -7,3 +7,9 @@ edition.workspace = true environment.path = "../environment" course.path = "../course" clap = { version = "4.5", features = ["derive"] } +anyhow = "1.0" +colored = "2.0" +indicatif = "0.17" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +walkdir = "2.3" diff --git a/xtask/src/eval.rs b/xtask/src/eval.rs index 2ab5118..7056023 100644 --- a/xtask/src/eval.rs +++ b/xtask/src/eval.rs @@ -1,10 +1,352 @@ -#[derive(Args)] +use anyhow::{Context, Result}; +use clap::Args; +use colored::*; +use indicatif::{ProgressBar, ProgressStyle}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::time::Instant; + +#[derive(Args)] pub struct EvalArgs { /// 要评分的课程名称,不传则自动对所有已配置课程评分 #[clap(long)] course: Option, + + /// 练习目录路径,默认为当前目录 + #[clap(short, long, default_value = ".")] + path: PathBuf, + + /// 是否显示详细输出 + #[clap(short, long)] + verbose: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ExerciseResult { + pub name: String, + pub result: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Statistics { + pub total_exercations: usize, + pub total_succeeds: usize, + pub total_failures: usize, + pub total_time: u64, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GradeResult { + pub exercises: Vec, + pub statistics: Statistics, } impl EvalArgs { - pub fn eval(self) {} + pub fn eval(self) { + if let Err(e) = self.run_eval() { + eprintln!("{} {}", "评分失败:".red().bold(), e); + } + } + + fn run_eval(&self) -> Result<()> { + println!("{}", "开始评测练习...".blue().bold()); + + // 查找所有练习文件 + let exercise_files = find_exercise_files(&self.path)?; + + println!("{} {} {}", "找到".blue().bold(), exercise_files.len(), "个练习文件".blue().bold()); + + // 创建进度条 + let total_exercises = exercise_files.len() as u64; + let bar = ProgressBar::new(total_exercises); + bar.set_style( + ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})") + .unwrap() + .progress_chars("##-"), + ); + + let mut exercise_results = Vec::new(); + let mut total_succeeds = 0; + let mut total_failures = 0; + let mut total_time = 0; + + for exercise_path in exercise_files.iter() { + bar.inc(1); // 更新进度条 + + let (name, result, time) = grade_exercise(exercise_path, self.verbose)?; + + if result { + total_succeeds += 1; + } else { + total_failures += 1; + } + + total_time += time; + + exercise_results.push(ExerciseResult { + name, + result, + }); + } + + bar.finish_with_message("评测完成!"); + + // 打印统计信息 + println!("{}", "评测结果统计".green().bold()); + println!("{}: {}", "总练习数".blue(), exercise_files.len()); + println!("{}: {}", "通过数量".green(), total_succeeds); + println!("{}: {}", "失败数量".red(), total_failures); + println!("{}: {}秒", "总耗时".blue(), total_time); + + // 计算通过率 + let pass_rate = if exercise_files.len() > 0 { + (total_succeeds as f32 / exercise_files.len() as f32) * 100.0 + } else { + 0.0 + }; + println!("{}: {:.2}%", "通过率".green(), pass_rate); + + // 显示失败的练习 + if total_failures > 0 { + println!(""); + println!("{}", "失败的练习:".red().bold()); + for exercise in exercise_results.iter() { + if !exercise.result { + println!(" {}", exercise.name.red()); + } + } + } + + // 将结果保存到文件 + let result = GradeResult { + exercises: exercise_results, + statistics: Statistics { + total_exercations: exercise_files.len(), + total_succeeds, + total_failures, + total_time, + }, + }; + + let json_result = serde_json::to_string_pretty(&result)?; + fs::write("rustlings_result.json", json_result)?; + println!(""); + println!("{}", "评测结果已保存到 rustlings_result.json".blue()); + + Ok(()) + } +} + +/// 查找指定目录下的所有Rustlings练习文件 +fn find_exercise_files(exercises_path: &Path) -> Result> { + let mut exercise_files = Vec::new(); + let is_learning_lm = exercises_path.to_string_lossy().contains("learning-lm-rs"); + + // learning-lm-rs项目的测试文件列表 - 只测试这两个文件 + let learning_lm_files = [ + "model.rs", // test_mlp, test_load_safetensors + "operators.rs", // test_matmul_transb, test_silu, test_rms_norm + ]; + + if is_learning_lm { + // 对于learning-lm-rs项目,直接在src目录下查找文件 + let src_dir = if exercises_path.ends_with("learning-lm-rs") { + exercises_path.join("src") + } else if exercises_path.ends_with("src") { + exercises_path.to_path_buf() + } else { + // 尝试找到learning-lm-rs/src目录 + let mut lm_path = exercises_path.to_path_buf(); + while !lm_path.ends_with("learning-lm-rs") && lm_path.parent().is_some() { + lm_path = lm_path.parent().unwrap().to_path_buf(); + } + if lm_path.ends_with("learning-lm-rs") { + lm_path.join("src") + } else { + println!("{} {}", "警告:".yellow().bold(), "找不到learning-lm-rs/src目录"); + return Ok(Vec::new()); + } + }; + + // 检查src目录是否存在 + if !src_dir.exists() { + println!("{} {}", "警告:".yellow().bold(), "src目录不存在,请检查learning-lm-rs项目结构"); + return Ok(Vec::new()); + } + + println!("{}", "仅测试model.rs和operators.rs文件".blue().bold()); + + // 清空之前可能收集的所有文件 + exercise_files.clear(); + + // 在src目录下只添加model.rs和operators.rs文件 + for file_name in learning_lm_files.iter() { + let file_path = src_dir.join(file_name); + if file_path.exists() { + println!("{} {}", "添加测试文件:".green().bold(), file_path.display()); + exercise_files.push(file_path); + } + } + + // 确保只返回这两个文件,不进行其他文件的查找 + return Ok(exercise_files); + } else { + // Rustlings练习:在exercises目录下查找 + let exercises_dir = Path::new("exercises"); + let base_path = if exercises_path.ends_with(exercises_dir) { + exercises_path.to_path_buf() + } else { + exercises_path.join(exercises_dir) + }; + + // 检查exercises目录是否存在 + if !base_path.exists() { + println!("{} {}", "警告:".yellow().bold(), "exercises目录不存在,请先配置课程"); + return Ok(Vec::new()); + } + + for entry in walkdir::WalkDir::new(&base_path) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if path.components().any(|c| c.as_os_str() == "target") { + continue; + } + if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") { + let file_name = path.file_name().unwrap().to_string_lossy(); + + // Rustlings练习:排除测试文件和辅助文件 + if !file_name.starts_with("test_") && !file_name.starts_with("helper_") { + exercise_files.push(path.to_path_buf()); + } + } + } + } + + Ok(exercise_files) +} + +/// 评测单个练习文件 +fn grade_exercise(exercise_path: &Path, verbose: bool) -> Result<(String, bool, u64)> { + let start = Instant::now(); + let exercise_name = exercise_path + .file_name() + .context("无法获取文件名")? + .to_string_lossy() + .to_string(); + + println!("{} {}", "评测练习:".blue().bold(), exercise_name); + + // 确保target目录存在 + fs::create_dir_all("target/debug").context("创建target目录失败")?; + + // 检查是否为learning-lm-rs项目的文件 + let is_learning_lm = exercise_path.to_string_lossy().contains("learning-lm-rs"); + + let (compile_output, test_output) = if is_learning_lm { + // learning-lm-rs项目:使用cargo test + // 获取子模块项目的Cargo.toml路径 + let module_dir = exercise_path.parent().unwrap().parent().unwrap(); + let manifest_path = module_dir.join("Cargo.toml"); + + // 获取测试名称,根据文件名确定要运行的测试 + let test_name = if exercise_name == "model.rs" { + "test_mlp test_load_safetensors" + } else if exercise_name == "operators.rs" { + "test_matmul_transb test_silu test_rms_norm" + } else { + "" + }; + + println!("{} {}", "运行测试:".blue().bold(), test_name); + + // 编译项目 + let compile_output = Command::new("cargo") + .arg("test") + .arg("--manifest-path") + .arg(&manifest_path) + .arg("--no-run") // 仅编译不运行 + .arg("--release") // 使用release模式编译 + .current_dir(module_dir) // 移动到项目根目录 + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .context(format!("编译练习 {} 失败", exercise_name))?; + + if !compile_output.status.success() { + if verbose { + println!("{}", String::from_utf8_lossy(&compile_output.stderr)); + } + println!("{} {}", "编译失败:".red().bold(), exercise_name); + return Ok((exercise_name, false, start.elapsed().as_secs())); + } + + // 构建测试命令,只运行特定的测试 + let mut test_command = Command::new("cargo"); + test_command.arg("test") + .arg("--manifest-path") + .arg(&manifest_path) + .arg("--release"); // 使用release模式运行测试 + + // 添加特定的测试名称 + if !test_name.is_empty() { + for test in test_name.split_whitespace() { + test_command.arg(test); + } + } + + let test_output = test_command + .current_dir(module_dir) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .context(format!("运行练习 {} 失败", exercise_name))?; + + (compile_output, test_output) + } else { + // Rustlings练习:使用rustc + let compile_output = Command::new("rustc") + .arg(exercise_path) + .arg("--test") + .arg("-o") + .arg(format!("target/debug/{}", exercise_name)) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .context(format!("编译练习 {} 失败", exercise_name))?; + + if !compile_output.status.success() { + if verbose { + println!("{}", String::from_utf8_lossy(&compile_output.stderr)); + } + println!("{} {}", "编译失败:".red().bold(), exercise_name); + return Ok((exercise_name, false, start.elapsed().as_secs())); + } + + let test_output = Command::new(format!("target/debug/{}", exercise_name)) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .context(format!("运行练习 {} 失败", exercise_name))?; + + (compile_output, test_output) + }; + + let success = test_output.status.success(); + + if verbose || !success { + println!("{}", String::from_utf8_lossy(&test_output.stdout)); + println!("{}", String::from_utf8_lossy(&test_output.stderr)); + } + + if success { + println!("{} {}", "✓".green().bold(), exercise_name); + } else { + println!("{} {}", "✗".red().bold(), exercise_name); + } + + Ok((exercise_name, success, start.elapsed().as_secs())) } diff --git a/xtask/src/learn.rs b/xtask/src/learn.rs index 8592d91..1875cb4 100644 --- a/xtask/src/learn.rs +++ b/xtask/src/learn.rs @@ -1,4 +1,11 @@ -#[derive(Args)] +use anyhow::{Context, Result}; +use clap::Args; +use colored::*; +use std::fs; +use std::path::Path; +use std::process::Command; + +#[derive(Args)] pub struct LearnArgs { /// 课程名称 course: String, @@ -8,5 +15,51 @@ pub struct LearnArgs { } impl LearnArgs { - pub fn learn(self) {} + pub fn learn(self) { + if let Err(e) = self.run_learn() { + eprintln!("{} {}", "配置课程失败:".red().bold(), e); + } + } + + fn run_learn(&self) -> Result<()> { + println!("{} {}", "开始配置课程:".blue().bold(), self.course); + + // 确保exercises目录存在 + let exercises_dir = Path::new("exercises"); + if !exercises_dir.exists() { + fs::create_dir_all(exercises_dir) + .context("创建exercises目录失败")?; + } + + // 如果提供了子模块地址,则克隆仓库 + if let Some(repo_url) = &self.submodule { + println!("{} {}", "克隆仓库:".blue().bold(), repo_url); + + // 检查是否已存在同名子模块 + let course_dir = exercises_dir.join(&self.course); + if course_dir.exists() { + println!("{} {}", "警告:".yellow().bold(), format!("目录 {} 已存在,将被覆盖", course_dir.display())); + fs::remove_dir_all(&course_dir) + .context(format!("删除已存在的目录 {} 失败", course_dir.display()))?; + } + + // 使用git命令添加子模块 + let status = Command::new("git") + .args(["submodule", "add", "-f", repo_url, &format!("exercises/{}", self.course)]) + .status() + .context("执行git submodule add命令失败")?; + + if !status.success() { + return Err(anyhow::anyhow!("git submodule add命令执行失败")); + } + + println!("{} {}", "成功配置课程:".green().bold(), self.course); + println!("{} {}", "练习已克隆到:".green(), format!("exercises/{}", self.course)); + println!("{}", "你现在可以使用 'cargo xtask eval' 命令来评测练习".blue()); + } else { + println!("{}", "未提供仓库地址,请使用 --submodule 参数指定仓库地址".yellow()); + } + + Ok(()) + } } From 17d00b39132a8761e0b205dcc51083466a7f4884 Mon Sep 17 00:00:00 2001 From: wawahejun Date: Thu, 8 May 2025 15:07:25 +0800 Subject: [PATCH 2/2] fix: Complete some errors in xtask/eval and learn --- .gitmodules | 3 + exercises/rustlings | 1 + rustlings_result.json | 378 ++++++++++++++++++++++++++++++++-- xtask/src/eval.rs | 466 ++++++++++++++++++++++++------------------ 4 files changed, 637 insertions(+), 211 deletions(-) create mode 160000 exercises/rustlings diff --git a/.gitmodules b/.gitmodules index 3fb8692..4cc1b82 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "exercises/learning-lm-rs"] path = exercises/learning-lm-rs url = https://github.com/wawahejun/learning-lm-rs +[submodule "exercises/rustlings"] + path = exercises/rustlings + url = https://github.com/wawahejun/rustlings-completed-version diff --git a/exercises/rustlings b/exercises/rustlings new file mode 160000 index 0000000..9c5c664 --- /dev/null +++ b/exercises/rustlings @@ -0,0 +1 @@ +Subproject commit 9c5c66458b8826bf02c2f10cb951393d27e308d8 diff --git a/rustlings_result.json b/rustlings_result.json index cffed5e..a4b5189 100644 --- a/rustlings_result.json +++ b/rustlings_result.json @@ -1,38 +1,386 @@ { "exercises": [ { - "name": "main.rs", - "result": false + "name": "strings4.rs", + "result": true }, { - "name": "tensor.rs", - "result": false + "name": "strings1.rs", + "result": true }, { - "name": "model.rs", - "result": false + "name": "strings3.rs", + "result": true }, { - "name": "config.rs", - "result": false + "name": "strings2.rs", + "result": true + }, + { + "name": "primitive_types4.rs", + "result": true + }, + { + "name": "primitive_types5.rs", + "result": true + }, + { + "name": "primitive_types3.rs", + "result": true + }, + { + "name": "primitive_types6.rs", + "result": true }, { - "name": "kvcache.rs", + "name": "primitive_types2.rs", + "result": true + }, + { + "name": "primitive_types1.rs", + "result": true + }, + { + "name": "using_as.rs", + "result": true + }, + { + "name": "from_into.rs", + "result": true + }, + { + "name": "from_str.rs", + "result": true + }, + { + "name": "try_from_into.rs", + "result": true + }, + { + "name": "as_ref_mut.rs", + "result": true + }, + { + "name": "lifetimes2.rs", + "result": true + }, + { + "name": "lifetimes1.rs", + "result": true + }, + { + "name": "lifetimes3.rs", + "result": true + }, + { + "name": "modules2.rs", + "result": true + }, + { + "name": "modules1.rs", + "result": true + }, + { + "name": "modules3.rs", + "result": true + }, + { + "name": "iterators3.rs", "result": false }, { - "name": "params.rs", + "name": "iterators5.rs", + "result": true + }, + { + "name": "iterators1.rs", + "result": true + }, + { + "name": "iterators4.rs", + "result": true + }, + { + "name": "iterators2.rs", + "result": true + }, + { + "name": "enums1.rs", + "result": true + }, + { + "name": "enums3.rs", + "result": true + }, + { + "name": "enums2.rs", + "result": true + }, + { + "name": "traits2.rs", + "result": true + }, + { + "name": "traits5.rs", + "result": true + }, + { + "name": "traits4.rs", + "result": true + }, + { + "name": "traits1.rs", + "result": true + }, + { + "name": "traits3.rs", + "result": true + }, + { + "name": "if2.rs", + "result": true + }, + { + "name": "if1.rs", + "result": true + }, + { + "name": "if3.rs", + "result": true + }, + { + "name": "variables4.rs", + "result": true + }, + { + "name": "variables2.rs", + "result": true + }, + { + "name": "variables1.rs", + "result": true + }, + { + "name": "variables5.rs", + "result": true + }, + { + "name": "variables6.rs", + "result": true + }, + { + "name": "variables3.rs", + "result": true + }, + { + "name": "errors3.rs", + "result": true + }, + { + "name": "errors2.rs", + "result": true + }, + { + "name": "errors4.rs", + "result": true + }, + { + "name": "errors1.rs", + "result": true + }, + { + "name": "errors6.rs", + "result": true + }, + { + "name": "errors5.rs", + "result": true + }, + { + "name": "intro2.rs", + "result": true + }, + { + "name": "intro1.rs", + "result": true + }, + { + "name": "arc1.rs", + "result": true + }, + { + "name": "rc1.rs", + "result": true + }, + { + "name": "box1.rs", + "result": true + }, + { + "name": "cow1.rs", + "result": true + }, + { + "name": "functions4.rs", + "result": true + }, + { + "name": "functions5.rs", + "result": true + }, + { + "name": "functions3.rs", + "result": true + }, + { + "name": "functions1.rs", + "result": true + }, + { + "name": "functions2.rs", + "result": true + }, + { + "name": "move_semantics2.rs", + "result": true + }, + { + "name": "move_semantics5.rs", + "result": true + }, + { + "name": "move_semantics4.rs", + "result": true + }, + { + "name": "move_semantics1.rs", + "result": true + }, + { + "name": "move_semantics3.rs", + "result": true + }, + { + "name": "macros1.rs", + "result": true + }, + { + "name": "macros2.rs", + "result": true + }, + { + "name": "macros3.rs", + "result": true + }, + { + "name": "macros4.rs", + "result": true + }, + { + "name": "threads2.rs", + "result": true + }, + { + "name": "threads1.rs", + "result": true + }, + { + "name": "threads3.rs", "result": false }, { - "name": "operators.rs", + "name": "tests2.rs", + "result": true + }, + { + "name": "tests1.rs", + "result": true + }, + { + "name": "tests3.rs", + "result": true + }, + { + "name": "quiz2.rs", + "result": true + }, + { + "name": "quiz3.rs", + "result": true + }, + { + "name": "quiz1.rs", + "result": true + }, + { + "name": "clippy3.rs", + "result": true + }, + { + "name": "clippy2.rs", + "result": true + }, + { + "name": "clippy1.rs", + "result": true + }, + { + "name": "hashmaps1.rs", + "result": true + }, + { + "name": "hashmaps3.rs", + "result": true + }, + { + "name": "hashmaps2.rs", "result": false + }, + { + "name": "options3.rs", + "result": true + }, + { + "name": "options2.rs", + "result": true + }, + { + "name": "options1.rs", + "result": true + }, + { + "name": "structs1.rs", + "result": true + }, + { + "name": "structs3.rs", + "result": true + }, + { + "name": "structs2.rs", + "result": true + }, + { + "name": "vecs1.rs", + "result": true + }, + { + "name": "vecs2.rs", + "result": true + }, + { + "name": "generics2.rs", + "result": true + }, + { + "name": "generics1.rs", + "result": true } ], "statistics": { - "total_exercations": 7, - "total_succeeds": 0, - "total_failures": 7, - "total_time": 0 + "total_exercations": 94, + "total_succeeds": 91, + "total_failures": 3, + "total_time": 25 } } \ No newline at end of file diff --git a/xtask/src/eval.rs b/xtask/src/eval.rs index 7056023..58430f8 100644 --- a/xtask/src/eval.rs +++ b/xtask/src/eval.rs @@ -52,63 +52,135 @@ impl EvalArgs { fn run_eval(&self) -> Result<()> { println!("{}", "开始评测练习...".blue().bold()); - - // 查找所有练习文件 - let exercise_files = find_exercise_files(&self.path)?; - - println!("{} {} {}", "找到".blue().bold(), exercise_files.len(), "个练习文件".blue().bold()); - - // 创建进度条 - let total_exercises = exercise_files.len() as u64; - let bar = ProgressBar::new(total_exercises); - bar.set_style( - ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})") - .unwrap() - .progress_chars("##-"), - ); - + let start_time = Instant::now(); + + // 根据course参数确定评测目标 + let is_learning_lm = match &self.course { + Some(course) => course == "learning-lm-rs", + None => self.path.to_string_lossy().contains("learning-lm-rs") + }; + let mut exercise_results = Vec::new(); let mut total_succeeds = 0; let mut total_failures = 0; - let mut total_time = 0; - - for exercise_path in exercise_files.iter() { - bar.inc(1); // 更新进度条 - - let (name, result, time) = grade_exercise(exercise_path, self.verbose)?; - - if result { - total_succeeds += 1; + let mut total_exercations = 0; + + if is_learning_lm { + println!("{}", "评测 learning-lm-rs 项目...".blue().bold()); + // Resolve the path relative to the CWD where cargo xtask was run + let absolute_path = std::env::current_dir() + .context("无法获取当前工作目录")? + .join(&self.path); + + // 根据course参数确定exercises目录 + let exercises_dir = if absolute_path.ends_with("exercises") { + absolute_path.clone() } else { - total_failures += 1; + absolute_path.join("exercises") + }; + + // 如果指定了course参数,直接使用对应的目录 + let lm_path = if let Some(course) = &self.course { + exercises_dir.join(course) + } else { + exercises_dir.join("learning-lm-rs") + }; + if !lm_path.exists() { + println!("{} {}", "警告:".yellow().bold(), "找不到 exercises/learning-lm-rs 目录"); + return Ok(()); } - total_time += time; - - exercise_results.push(ExerciseResult { - name, - result, - }); + let manifest_path = lm_path.join("Cargo.toml"); + if !manifest_path.exists() { + println!("{} {} {}", "警告:".yellow().bold(), "找不到 learning-lm-rs/Cargo.toml 文件:", manifest_path.display()); + return Ok(()); + } + + println!("{} {}", "运行测试:".blue().bold(), "cargo test --release"); + let test_output = Command::new("cargo") + .arg("test") + .arg("--manifest-path") + .arg(&manifest_path) + .arg("--release") + .current_dir(&lm_path) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .context("运行 learning-lm-rs 测试失败")?; + + let success = test_output.status.success(); + + if self.verbose || !success { + println!("{}", String::from_utf8_lossy(&test_output.stdout)); + println!("{}", String::from_utf8_lossy(&test_output.stderr)); + } + + // learning-lm-rs 只包含 model.rs 和 operators.rs + let lm_exercises = ["model.rs", "operators.rs"]; + total_exercations = lm_exercises.len(); + + for &exercise_name in lm_exercises.iter() { + exercise_results.push(ExerciseResult { + name: exercise_name.to_string(), + result: success, + }); + if success { + total_succeeds += 1; + println!("{} {}", "✓".green().bold(), exercise_name); + } else { + total_failures += 1; + println!("{} {}", "✗".red().bold(), exercise_name); + } + } + println!("评测完成!"); + + } else { + // 处理 Rustlings 或其他非 learning-lm-rs 项目 + let exercise_files = find_exercise_files(&self.path, &self.course)?; + total_exercations = exercise_files.len(); + println!("{} {} {}", "找到".blue().bold(), total_exercations, "个练习文件".blue().bold()); + + if total_exercations == 0 { + println!("{}", "未找到练习文件,评测结束。".yellow()); + return Ok(()); + } + + let bar = ProgressBar::new(total_exercations as u64); + bar.set_style( + ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})") + .unwrap() + .progress_chars("##-"), + ); + + for exercise_path in exercise_files.iter() { + bar.inc(1); + let (name, result, _time) = grade_exercise(exercise_path, self.verbose)?; + if result { + total_succeeds += 1; + } else { + total_failures += 1; + } + exercise_results.push(ExerciseResult { name, result }); + } + bar.finish_with_message("评测完成!"); } - - bar.finish_with_message("评测完成!"); - + + let total_time = start_time.elapsed().as_secs(); + // 打印统计信息 println!("{}", "评测结果统计".green().bold()); - println!("{}: {}", "总练习数".blue(), exercise_files.len()); + println!("{}: {}", "总练习数".blue(), total_exercations); println!("{}: {}", "通过数量".green(), total_succeeds); println!("{}: {}", "失败数量".red(), total_failures); println!("{}: {}秒", "总耗时".blue(), total_time); - - // 计算通过率 - let pass_rate = if exercise_files.len() > 0 { - (total_succeeds as f32 / exercise_files.len() as f32) * 100.0 + + let pass_rate = if total_exercations > 0 { + (total_succeeds as f32 / total_exercations as f32) * 100.0 } else { 0.0 }; println!("{}: {:.2}%", "通过率".green(), pass_rate); - - // 显示失败的练习 + if total_failures > 0 { println!(""); println!("{}", "失败的练习:".red().bold()); @@ -118,96 +190,126 @@ impl EvalArgs { } } } - - // 将结果保存到文件 + let result = GradeResult { exercises: exercise_results, statistics: Statistics { - total_exercations: exercise_files.len(), + total_exercations, total_succeeds, total_failures, total_time, }, }; - + let json_result = serde_json::to_string_pretty(&result)?; fs::write("rustlings_result.json", json_result)?; println!(""); println!("{}", "评测结果已保存到 rustlings_result.json".blue()); - + Ok(()) } } -/// 查找指定目录下的所有Rustlings练习文件 -fn find_exercise_files(exercises_path: &Path) -> Result> { - let mut exercise_files = Vec::new(); - let is_learning_lm = exercises_path.to_string_lossy().contains("learning-lm-rs"); - - // learning-lm-rs项目的测试文件列表 - 只测试这两个文件 - let learning_lm_files = [ - "model.rs", // test_mlp, test_load_safetensors - "operators.rs", // test_matmul_transb, test_silu, test_rms_norm - ]; - - if is_learning_lm { - // 对于learning-lm-rs项目,直接在src目录下查找文件 - let src_dir = if exercises_path.ends_with("learning-lm-rs") { - exercises_path.join("src") - } else if exercises_path.ends_with("src") { - exercises_path.to_path_buf() +/// 查找 learning-lm-rs 项目的根目录 +fn find_learning_lm_root(start_path: &Path) -> Result { + // First, check if the provided path exists + if !start_path.exists() { + return Err(anyhow::anyhow!("提供的路径不存在: {}", start_path.display())); + } + // Canonicalize the starting path to resolve any relative components + let mut current_path = start_path + .canonicalize() + .with_context(|| format!("无法规范化路径: {}", start_path.display()))?; + loop { + if current_path.join("Cargo.toml").exists() && current_path.file_name().map_or(false, |name| name == "learning-lm-rs") { + return Ok(current_path); + } + if let Some(parent) = current_path.parent() { + current_path = parent.to_path_buf(); } else { - // 尝试找到learning-lm-rs/src目录 - let mut lm_path = exercises_path.to_path_buf(); - while !lm_path.ends_with("learning-lm-rs") && lm_path.parent().is_some() { - lm_path = lm_path.parent().unwrap().to_path_buf(); + break; + } + } + Err(anyhow::anyhow!("找不到 learning-lm-rs 项目根目录")) +} + +/// 查找指定目录下的所有练习文件 +fn find_exercise_files(exercises_path: &Path, course: &Option) -> Result> { + let mut exercise_files = Vec::new(); + let exercises_dir = Path::new("exercises"); + let base_path = if exercises_path.ends_with(exercises_dir) { + exercises_path.to_path_buf() + } else { + exercises_path.join(exercises_dir) + }; + + // 检查exercises目录是否存在 + if !base_path.exists() { + println!("{} {}", "警告:".yellow().bold(), "exercises目录不存在,请先配置课程"); + return Ok(Vec::new()); + } + + // 根据course参数确定评测目标目录 + let course_path = if let Some(course_name) = course { + base_path.join(course_name) + } else { + base_path.clone() + }; + + if !course_path.exists() { + println!("{} {}", "警告:".yellow().bold(), format!("找不到课程目录: {}", course_path.display())); + return Ok(Vec::new()); + } + + // 对于learning-lm-rs项目,只返回model.rs和operators.rs + if course_path.ends_with("learning-lm-rs") { + let src_path = course_path.join("src"); + if src_path.exists() { + println!("{} {}", "找到learning-lm-rs项目:".blue().bold(), src_path.display()); + let model_path = src_path.join("model.rs"); + let operators_path = src_path.join("operators.rs"); + + if model_path.exists() { + println!("{} {}", "找到练习文件:".blue(), model_path.display()); + exercise_files.push(model_path); + } else { + println!("{} {}", "警告:".yellow().bold(), "找不到model.rs文件"); } - if lm_path.ends_with("learning-lm-rs") { - lm_path.join("src") + + if operators_path.exists() { + println!("{} {}", "找到练习文件:".blue(), operators_path.display()); + exercise_files.push(operators_path); } else { - println!("{} {}", "警告:".yellow().bold(), "找不到learning-lm-rs/src目录"); - return Ok(Vec::new()); + println!("{} {}", "警告:".yellow().bold(), "找不到operators.rs文件"); } - }; - - // 检查src目录是否存在 - if !src_dir.exists() { - println!("{} {}", "警告:".yellow().bold(), "src目录不存在,请检查learning-lm-rs项目结构"); + + return Ok(exercise_files); + } + } + + // 对于rustlings项目,只查找exercises目录下的文件 + if course_path.ends_with("rustlings") { + let exercises_path = course_path.join("exercises"); + if !exercises_path.exists() { + println!("{} {}", "警告:".yellow().bold(), "找不到rustlings的exercises目录"); return Ok(Vec::new()); } - - println!("{}", "仅测试model.rs和operators.rs文件".blue().bold()); - - // 清空之前可能收集的所有文件 - exercise_files.clear(); - - // 在src目录下只添加model.rs和operators.rs文件 - for file_name in learning_lm_files.iter() { - let file_path = src_dir.join(file_name); - if file_path.exists() { - println!("{} {}", "添加测试文件:".green().bold(), file_path.display()); - exercise_files.push(file_path); + + for entry in walkdir::WalkDir::new(&exercises_path) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") { + let file_name = path.file_name().unwrap().to_string_lossy(); + if !file_name.starts_with("test_") && !file_name.starts_with("helper_") { + exercise_files.push(path.to_path_buf()); + } } } - - // 确保只返回这两个文件,不进行其他文件的查找 - return Ok(exercise_files); } else { - // Rustlings练习:在exercises目录下查找 - let exercises_dir = Path::new("exercises"); - let base_path = if exercises_path.ends_with(exercises_dir) { - exercises_path.to_path_buf() - } else { - exercises_path.join(exercises_dir) - }; - - // 检查exercises目录是否存在 - if !base_path.exists() { - println!("{} {}", "警告:".yellow().bold(), "exercises目录不存在,请先配置课程"); - return Ok(Vec::new()); - } - - for entry in walkdir::WalkDir::new(&base_path) + // 对于其他项目,遍历目录查找练习文件 + for entry in walkdir::WalkDir::new(&course_path) .into_iter() .filter_map(|e| e.ok()) { @@ -217,19 +319,17 @@ fn find_exercise_files(exercises_path: &Path) -> Result> { } if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") { let file_name = path.file_name().unwrap().to_string_lossy(); - - // Rustlings练习:排除测试文件和辅助文件 if !file_name.starts_with("test_") && !file_name.starts_with("helper_") { exercise_files.push(path.to_path_buf()); } } } } - + Ok(exercise_files) } -/// 评测单个练习文件 +/// 评测单个 Rustlings 练习文件 (不再处理 learning-lm-rs) fn grade_exercise(exercise_path: &Path, verbose: bool) -> Result<(String, bool, u64)> { let start = Instant::now(); let exercise_name = exercise_path @@ -237,78 +337,45 @@ fn grade_exercise(exercise_path: &Path, verbose: bool) -> Result<(String, bool, .context("无法获取文件名")? .to_string_lossy() .to_string(); - + println!("{} {}", "评测练习:".blue().bold(), exercise_name); - - // 确保target目录存在 - fs::create_dir_all("target/debug").context("创建target目录失败")?; - - // 检查是否为learning-lm-rs项目的文件 + let is_learning_lm = exercise_path.to_string_lossy().contains("learning-lm-rs"); - - let (compile_output, test_output) = if is_learning_lm { - // learning-lm-rs项目:使用cargo test - // 获取子模块项目的Cargo.toml路径 - let module_dir = exercise_path.parent().unwrap().parent().unwrap(); - let manifest_path = module_dir.join("Cargo.toml"); - - // 获取测试名称,根据文件名确定要运行的测试 - let test_name = if exercise_name == "model.rs" { - "test_mlp test_load_safetensors" - } else if exercise_name == "operators.rs" { - "test_matmul_transb test_silu test_rms_norm" - } else { - "" - }; + + let test_output = if is_learning_lm { + // 对于learning-lm-rs项目,需要找到项目根目录 + // 获取绝对路径,确保能找到正确的Cargo.toml + let absolute_path = std::env::current_dir() + .context("无法获取当前工作目录")? + .join(exercise_path) + .canonicalize() + .context("无法获取练习文件的绝对路径")?; - println!("{} {}", "运行测试:".blue().bold(), test_name); + // 从练习文件路径向上查找,直到找到learning-lm-rs目录 + let mut project_root = absolute_path.clone(); + while !project_root.file_name().map_or(false, |name| name == "learning-lm-rs") { + project_root = project_root.parent().context("找不到learning-lm-rs项目根目录")?.to_path_buf(); + } - // 编译项目 - let compile_output = Command::new("cargo") - .arg("test") - .arg("--manifest-path") - .arg(&manifest_path) - .arg("--no-run") // 仅编译不运行 - .arg("--release") // 使用release模式编译 - .current_dir(module_dir) // 移动到项目根目录 - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .context(format!("编译练习 {} 失败", exercise_name))?; - - if !compile_output.status.success() { - if verbose { - println!("{}", String::from_utf8_lossy(&compile_output.stderr)); - } - println!("{} {}", "编译失败:".red().bold(), exercise_name); - return Ok((exercise_name, false, start.elapsed().as_secs())); + let manifest_path = project_root.join("Cargo.toml"); + if !manifest_path.exists() { + return Err(anyhow::anyhow!("找不到learning-lm-rs的Cargo.toml文件: {}", manifest_path.display())); } - // 构建测试命令,只运行特定的测试 - let mut test_command = Command::new("cargo"); - test_command.arg("test") + Command::new("cargo") + .arg("test") .arg("--manifest-path") .arg(&manifest_path) - .arg("--release"); // 使用release模式运行测试 - - // 添加特定的测试名称 - if !test_name.is_empty() { - for test in test_name.split_whitespace() { - test_command.arg(test); - } - } - - let test_output = test_command - .current_dir(module_dir) + .arg("--package") + .arg("learning-lm-rust") + .current_dir(&project_root) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() - .context(format!("运行练习 {} 失败", exercise_name))?; - - (compile_output, test_output) + .context(format!("运行练习 {} 失败", exercise_name))? } else { - // Rustlings练习:使用rustc - let compile_output = Command::new("rustc") + // 对于rustlings练习,直接使用rustc编译和运行测试 + Command::new("rustc") .arg(exercise_path) .arg("--test") .arg("-o") @@ -316,37 +383,44 @@ fn grade_exercise(exercise_path: &Path, verbose: bool) -> Result<(String, bool, .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() - .context(format!("编译练习 {} 失败", exercise_name))?; - - if !compile_output.status.success() { - if verbose { - println!("{}", String::from_utf8_lossy(&compile_output.stderr)); - } - println!("{} {}", "编译失败:".red().bold(), exercise_name); - return Ok((exercise_name, false, start.elapsed().as_secs())); + .context(format!("编译练习 {} 失败", exercise_name))? + }; + + let success = test_output.status.success(); + + if !success { + if verbose { + println!("{}", String::from_utf8_lossy(&test_output.stdout)); + println!("{}", String::from_utf8_lossy(&test_output.stderr)); } - + println!("{} {}", "✗".red().bold(), exercise_name); + return Ok((exercise_name, false, start.elapsed().as_secs())); + } + + // 如果是rustlings练习且编译成功,运行测试 + if !is_learning_lm { let test_output = Command::new(format!("target/debug/{}", exercise_name)) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .context(format!("运行练习 {} 失败", exercise_name))?; - - (compile_output, test_output) - }; - - let success = test_output.status.success(); - - if verbose || !success { - println!("{}", String::from_utf8_lossy(&test_output.stdout)); - println!("{}", String::from_utf8_lossy(&test_output.stderr)); - } - - if success { - println!("{} {}", "✓".green().bold(), exercise_name); - } else { - println!("{} {}", "✗".red().bold(), exercise_name); + + let success = test_output.status.success(); + + if verbose || !success { + println!("{}", String::from_utf8_lossy(&test_output.stdout)); + println!("{}", String::from_utf8_lossy(&test_output.stderr)); + } + + if success { + println!("{} {}", "✓".green().bold(), exercise_name); + } else { + println!("{} {}", "✗".red().bold(), exercise_name); + } + + return Ok((exercise_name, success, start.elapsed().as_secs())); } - - Ok((exercise_name, success, start.elapsed().as_secs())) + + println!("{} {}", "✓".green().bold(), exercise_name); + Ok((exercise_name, true, start.elapsed().as_secs())) }