diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4cc1b82 --- /dev/null +++ b/.gitmodules @@ -0,0 +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/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/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 new file mode 100644 index 0000000..a4b5189 --- /dev/null +++ b/rustlings_result.json @@ -0,0 +1,386 @@ +{ + "exercises": [ + { + "name": "strings4.rs", + "result": true + }, + { + "name": "strings1.rs", + "result": true + }, + { + "name": "strings3.rs", + "result": true + }, + { + "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": "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": "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": "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": 94, + "total_succeeds": 91, + "total_failures": 3, + "total_time": 25 + } +} \ 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..58430f8 100644 --- a/xtask/src/eval.rs +++ b/xtask/src/eval.rs @@ -1,10 +1,426 @@ -#[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 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_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 { + 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(()); + } + + 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("评测完成!"); + } + + let total_time = start_time.elapsed().as_secs(); + + // 打印统计信息 + println!("{}", "评测结果统计".green().bold()); + println!("{}: {}", "总练习数".blue(), total_exercations); + println!("{}: {}", "通过数量".green(), total_succeeds); + println!("{}: {}", "失败数量".red(), total_failures); + println!("{}: {}秒", "总耗时".blue(), total_time); + + 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()); + for exercise in exercise_results.iter() { + if !exercise.result { + println!(" {}", exercise.name.red()); + } + } + } + + let result = GradeResult { + exercises: exercise_results, + statistics: Statistics { + 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(()) + } +} + +/// 查找 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 { + 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 operators_path.exists() { + println!("{} {}", "找到练习文件:".blue(), operators_path.display()); + exercise_files.push(operators_path); + } else { + println!("{} {}", "警告:".yellow().bold(), "找不到operators.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()); + } + + 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()); + } + } + } + } else { + // 对于其他项目,遍历目录查找练习文件 + for entry in walkdir::WalkDir::new(&course_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(); + 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 + .file_name() + .context("无法获取文件名")? + .to_string_lossy() + .to_string(); + + println!("{} {}", "评测练习:".blue().bold(), exercise_name); + + let is_learning_lm = exercise_path.to_string_lossy().contains("learning-lm-rs"); + + 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("无法获取练习文件的绝对路径")?; + + // 从练习文件路径向上查找,直到找到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 manifest_path = project_root.join("Cargo.toml"); + if !manifest_path.exists() { + return Err(anyhow::anyhow!("找不到learning-lm-rs的Cargo.toml文件: {}", manifest_path.display())); + } + + Command::new("cargo") + .arg("test") + .arg("--manifest-path") + .arg(&manifest_path) + .arg("--package") + .arg("learning-lm-rust") + .current_dir(&project_root) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .context(format!("运行练习 {} 失败", exercise_name))? + } else { + // 对于rustlings练习,直接使用rustc编译和运行测试 + 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))? + }; + + 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))?; + + 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())); + } + + println!("{} {}", "✓".green().bold(), exercise_name); + Ok((exercise_name, true, 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(()) + } }