Skip to main content

kotoba_tester/
lib.rs

1//! Kotoba Test Runner
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use std::time::Duration;
6use uuid::Uuid;
7
8// モジュール宣言(後で実装)
9pub mod config;
10pub mod runner;
11pub mod assertions;
12pub mod reporter;
13pub mod coverage;
14
15/// テスト結果の状態
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub enum TestResult {
18    Passed,
19    Failed,
20    Skipped,
21    Pending,
22    Timeout,
23}
24
25impl std::fmt::Display for TestResult {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            TestResult::Passed => write!(f, "PASS"),
29            TestResult::Failed => write!(f, "FAIL"),
30            TestResult::Skipped => write!(f, "SKIP"),
31            TestResult::Pending => write!(f, "PEND"),
32            TestResult::Timeout => write!(f, "TIMEOUT"),
33        }
34    }
35}
36
37/// テストケース
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct TestCase {
40    pub id: String,
41    pub name: String,
42    pub description: Option<String>,
43    pub file_path: PathBuf,
44    pub line: usize,
45    pub result: TestResult,
46    pub duration: Duration,
47    pub error_message: Option<String>,
48}
49
50impl TestCase {
51    pub fn new(name: String, file_path: PathBuf, line: usize) -> Self {
52        Self {
53            id: Uuid::new_v4().to_string(),
54            name,
55            description: None,
56            file_path,
57            line,
58            result: TestResult::Pending,
59            duration: Duration::default(),
60            error_message: None,
61        }
62    }
63
64    pub fn pass(&mut self, duration: Duration) {
65        self.result = TestResult::Passed;
66        self.duration = duration;
67    }
68
69    pub fn fail(&mut self, error_message: String, duration: Duration) {
70        self.result = TestResult::Failed;
71        self.error_message = Some(error_message);
72        self.duration = duration;
73    }
74}
75
76/// テストスイート
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct TestSuite {
79    pub id: String,
80    pub name: String,
81    pub description: Option<String>,
82    pub file_path: PathBuf,
83    pub test_cases: Vec<TestCase>,
84    pub duration: Duration,
85}
86
87impl TestSuite {
88    pub fn new(name: String, file_path: PathBuf) -> Self {
89        Self {
90            id: Uuid::new_v4().to_string(),
91            name,
92            description: None,
93            file_path,
94            test_cases: Vec::new(),
95            duration: Duration::default(),
96        }
97    }
98
99    pub fn add_test_case(&mut self, test_case: TestCase) {
100        self.test_cases.push(test_case);
101    }
102
103    pub fn passed_count(&self) -> usize {
104        self.test_cases.iter().filter(|tc| tc.result == TestResult::Passed).count()
105    }
106
107    pub fn failed_count(&self) -> usize {
108        self.test_cases.iter().filter(|tc| tc.result == TestResult::Failed).count()
109    }
110
111    pub fn skipped_count(&self) -> usize {
112        self.test_cases.iter().filter(|tc| tc.result == TestResult::Skipped).count()
113    }
114
115    pub fn total_count(&self) -> usize {
116        self.test_cases.len()
117    }
118}
119
120/// テスト実行結果
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct TestRunResult {
123    pub id: String,
124    pub test_suites: Vec<TestSuite>,
125    pub start_time: chrono::DateTime<chrono::Utc>,
126    pub end_time: Option<chrono::DateTime<chrono::Utc>>,
127    pub total_duration: Duration,
128}
129
130impl TestRunResult {
131    pub fn new() -> Self {
132        Self {
133            id: Uuid::new_v4().to_string(),
134            test_suites: Vec::new(),
135            start_time: chrono::Utc::now(),
136            end_time: None,
137            total_duration: Duration::default(),
138        }
139    }
140
141    pub fn add_test_suite(&mut self, test_suite: TestSuite) {
142        self.test_suites.push(test_suite);
143    }
144
145    pub fn complete(&mut self) {
146        self.end_time = Some(chrono::Utc::now());
147        if let Some(end_time) = self.end_time {
148            self.total_duration = end_time.signed_duration_since(self.start_time).to_std().unwrap_or_default();
149        }
150    }
151
152    pub fn total_tests(&self) -> usize {
153        self.test_suites.iter().map(|suite| suite.total_count()).sum()
154    }
155
156    pub fn passed_tests(&self) -> usize {
157        self.test_suites.iter().map(|suite| suite.passed_count()).sum()
158    }
159
160    pub fn failed_tests(&self) -> usize {
161        self.test_suites.iter().map(|suite| suite.failed_count()).sum()
162    }
163}
164
165/// テスト設定
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct TestConfig {
168    pub timeout: u64,
169    pub concurrency: usize,
170    pub filter: Option<String>,
171    pub verbose: bool,
172    pub coverage: bool,
173    pub exclude_patterns: Vec<String>,
174}
175
176impl Default for TestConfig {
177    fn default() -> Self {
178        Self {
179            timeout: 30,
180            concurrency: num_cpus::get(),
181            filter: None,
182            verbose: false,
183            coverage: false,
184            exclude_patterns: vec![
185                "node_modules".to_string(),
186                ".git".to_string(),
187                "target".to_string(),
188                "build".to_string(),
189            ],
190        }
191    }
192}
193
194/// メインのテストランナー
195#[derive(Debug)]
196pub struct TestRunner {
197    config: TestConfig,
198}
199
200impl TestRunner {
201    pub fn new(config: TestConfig) -> Self {
202        Self { config }
203    }
204
205    pub fn default() -> Self {
206        Self::new(TestConfig::default())
207    }
208
209    pub async fn run_tests(&self, patterns: Vec<String>) -> Result<TestRunResult, Box<dyn std::error::Error>> {
210        let mut result = TestRunResult::new();
211        let start_time = std::time::Instant::now();
212
213        // テストファイルの発見
214        let test_files = self.discover_test_files(&patterns).await?;
215
216        if test_files.is_empty() {
217            println!("No test files found.");
218            result.complete();
219            return Ok(result);
220        }
221
222        println!("Found {} test files", test_files.len());
223
224        // テストファイルの実行
225        for test_file in test_files {
226            let suite_result = self.run_test_file(&test_file).await?;
227            result.add_test_suite(suite_result);
228        }
229
230        result.total_duration = start_time.elapsed();
231        result.complete();
232
233        Ok(result)
234    }
235
236    async fn discover_test_files(&self, patterns: &[String]) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
237        let mut test_files = Vec::new();
238
239        if patterns.is_empty() {
240            // デフォルトのパターンで検索
241            test_files.extend(self.find_test_files(".", &["test_*.kotoba", "*_test.kotoba", "tests/**/*.kotoba"]).await?);
242        } else {
243            for pattern in patterns {
244                if std::path::Path::new(pattern).is_dir() {
245                    test_files.extend(self.find_test_files(pattern, &["test_*.kotoba", "*_test.kotoba", "**/*.kotoba"]).await?);
246                } else {
247                    test_files.push(PathBuf::from(pattern));
248                }
249            }
250        }
251
252        // フィルターの適用
253        if let Some(filter) = &self.config.filter {
254            test_files.retain(|path| {
255                path.to_string_lossy().contains(filter)
256            });
257        }
258
259        Ok(test_files)
260    }
261
262    async fn find_test_files(&self, dir: &str, patterns: &[&str]) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
263        let mut test_files = Vec::new();
264
265        for pattern in patterns {
266            let glob_pattern = if dir == "." {
267                pattern.to_string()
268            } else {
269                format!("{}/{}", dir, pattern)
270            };
271
272            // glob crate を使用したファイル検索
273            for entry in glob::glob(&glob_pattern).unwrap() {
274                if let Ok(path) = entry {
275                    if path.is_file() && self.is_test_file(&path) {
276                        test_files.push(path);
277                    }
278                }
279            }
280        }
281
282        // 重複を除去
283        test_files.sort();
284        test_files.dedup();
285
286        Ok(test_files)
287    }
288
289    fn is_test_file(&self, path: &PathBuf) -> bool {
290        if let Some(file_name) = path.file_name() {
291            let file_name_str = file_name.to_string_lossy();
292
293            // 除外パターンをチェック
294            for pattern in &self.config.exclude_patterns {
295                if file_name_str.contains(pattern) {
296                    return false;
297                }
298            }
299
300            // 拡張子が .kotoba かチェック
301            path.extension().map_or(false, |ext| ext == "kotoba")
302        } else {
303            false
304        }
305    }
306
307    async fn run_test_file(&self, test_file: &PathBuf) -> Result<TestSuite, Box<dyn std::error::Error>> {
308        let suite_name = test_file.file_stem()
309            .unwrap_or_default()
310            .to_string_lossy()
311            .to_string();
312
313        let mut suite = TestSuite::new(suite_name, test_file.clone());
314
315        // テストファイルの内容を読み込んで解析
316        let content = tokio::fs::read_to_string(test_file).await?;
317
318        // テストケースの抽出と実行
319        let test_cases = self.extract_and_run_tests(&content, test_file).await?;
320        suite.test_cases = test_cases;
321
322        Ok(suite)
323    }
324
325    async fn extract_and_run_tests(&self, content: &str, file_path: &PathBuf) -> Result<Vec<TestCase>, Box<dyn std::error::Error>> {
326        let mut test_cases = Vec::new();
327
328        // シンプルなテスト関数検出
329        let test_pattern = regex::Regex::new("(test_\\w+|describe|it)\\s*\\(\\s*[\"']([^\"']+)[\"']\\)?;")?; 
330
331        for (line_num, line) in content.lines().enumerate() {
332            for cap in test_pattern.captures_iter(line) {
333                if let Some(test_name) = cap.get(2) {
334                    let mut test_case = TestCase::new(
335                        test_name.as_str().to_string(),
336                        file_path.clone(),
337                        line_num + 1
338                    );
339
340                    // テストの実行
341                    let start_time = std::time::Instant::now();
342                    match self.run_single_test(&test_case).await {
343                        Ok(_) => {
344                            test_case.pass(start_time.elapsed());
345                        }
346                        Err(e) => {
347                            test_case.fail(e.to_string(), start_time.elapsed());
348                        }
349                    }
350
351                    test_cases.push(test_case);
352                }
353            }
354        }
355
356        // デフォルトで1つのテストケースを作成
357        if test_cases.is_empty() {
358            let mut test_case = TestCase::new(
359                "default_test".to_string(),
360                file_path.clone(),
361                1
362            );
363
364            let start_time = std::time::Instant::now();
365            test_case.pass(start_time.elapsed());
366            test_cases.push(test_case);
367        }
368
369        Ok(test_cases)
370    }
371
372    async fn run_single_test(&self, test_case: &TestCase) -> Result<(), Box<dyn std::error::Error>> {
373        // タイムアウト付きでテスト実行
374        tokio::time::timeout(
375            Duration::from_secs(self.config.timeout),
376            self.execute_test_logic(test_case)
377        ).await??;
378
379        Ok(())
380    }
381
382    async fn execute_test_logic(&self, test_case: &TestCase) -> Result<(), Box<dyn std::error::Error>> {
383        // ランダムに成功/失敗をシミュレート
384        use std::collections::hash_map::DefaultHasher;
385        use std::hash::{Hash, Hasher};
386
387        let mut hasher = DefaultHasher::new();
388        test_case.name.hash(&mut hasher);
389        let hash = hasher.finish();
390
391        if hash % 10 == 0 {
392            // 10%の確率で失敗
393            return Err("Simulated test failure".into());
394        }
395
396        Ok(())
397    }
398}
399
400/// 便利関数
401pub async fn run_tests(patterns: Vec<String>) -> Result<TestRunResult, Box<dyn std::error::Error>> {
402    let runner = TestRunner::default();
403    runner.run_tests(patterns).await
404}
405
406// 各モジュールの再エクスポート(後で実装)
407// pub use config::*;
408// pub use runner::*;
409// pub use assertions::*;
410// pub use reporter::*;
411// pub use coverage::*;