Skip to content

Commit cac24a3

Browse files
authored
tests: improve tests structure (#53)
* tests: improve structure * refactor: move logic-heavy parts to own modules * refactor: extract configuration validation * refactor: centralize command parsing and Validation * test: fix state pollution * refactor: group git operations * refactor: reorganize github mod * refactor: add logging * tests: improve coverage * docs: update test inventory
1 parent d471f3e commit cac24a3

36 files changed

Lines changed: 4447 additions & 474 deletions

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ colored output and comprehensive logging.
2525
## Testing Standards
2626

2727
- **Maintain test-inventory.md with test cases for features, update test case status regularly**
28+
- **Avoid adding duplicate tests**
2829
- **Maintain high test coverage (aim for 80%)**
2930

3031
### Unit Tests

docs/test-inventory.md

Lines changed: 52 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -559,62 +559,64 @@ Classification criteria:
559559

560560
| Case | Type | Rationale | Status |
561561
|------|------|-----------|---------|
562-
|1.1 Load valid config| Integration | Parses real file from disk and produces model used by other commands| Automated |
563-
|1.2 Missing config file| Integration | Depends on filesystem error handling| Automated |
564-
|1.3 Malformed YAML| Unit | Focus on parse failure surfaced by loader logic| Gap |
565-
|1.4 Path forms (abs/rel)| Unit | Pure path resolution logic; can be isolated| Automated |
566-
|1.5 Empty repositories list| Unit | Behavior is early-return logic| Automated |
567-
|1.6 Empty recipes list| Unit | Lookup logic and conditional absence handling| Gap |
568-
|1.7 Resolve recipe names uniquely| Unit | Name lookup & matching only| Automated |
562+
|1.1 Load valid config| Integration | Parses real file from disk and produces model used by other commands| ✅ Automated |
563+
|1.2 Missing config file| Integration | Depends on filesystem error handling| ✅ Automated |
564+
|1.3 Malformed YAML| Unit | Focus on parse failure surfaced by loader logic| ⚠️ Partial - need structured tests |
565+
|1.4 Path forms (abs/rel)| Unit | Pure path resolution logic; can be isolated| ✅ Automated |
566+
|1.5 Empty repositories list| Unit | Behavior is early-return logic| ✅ Automated |
567+
|1.6 Empty recipes list| Unit | Lookup logic and conditional absence handling| ⚠️ Partial - needs dedicated tests |
568+
|1.7 Resolve recipe names uniquely| Unit | Name lookup & matching only| ✅ Automated |
569+
|Symlink repository path resolution| Integration | FS symlink target resolution & safety| ❌ Gap |
569570

570571
### 18.2 Repository Management
571572

572573
| Case | Type | Rationale | Status |
573574
|------|------|-----------|---------|
574-
|2.1 Clone single repository| Integration | Invokes `git clone` and filesystem| Automated |
575-
|2.2 Clone multiple sequential| Integration | Iterative multi repo interaction| Automated |
576-
|2.3 Clone multiple parallel| Integration | Concurrency + multiple git operations| Automated |
577-
|2.4 Skip cloning if directory exists| Integration | Relies on FS presence checks| Automated |
578-
|2.5 Invalid repo URL| Integration | Captures external command failure| Automated |
579-
|2.6 Branch override| Integration | Uses git branch checkout| Gap |
580-
|2.7 Tag filtering include-only| Unit | Pure filtering logic| Automated |
581-
|2.8 Tag exclusion| Unit | Pure filtering logic| Automated |
582-
|2.9 Explicit repos override| Unit | Selection precedence logic| Automated |
583-
|2.10 Mixed include/exclude| Unit | Logical combination test| Automated |
575+
|2.1 Clone single repository| Integration | Invokes `git clone` and filesystem| Automated |
576+
|2.2 Clone multiple sequential| Integration | Iterative multi repo interaction| Automated |
577+
|2.3 Clone multiple parallel| Integration | Concurrency + multiple git operations| Automated |
578+
|2.4 Skip cloning if directory exists| Integration | Relies on FS presence checks| Automated |
579+
|2.5 Invalid repo URL| Integration | Captures external command failure| Automated |
580+
|2.6 Branch override| Integration | Uses git branch checkout| Gap - needs implementation |
581+
|2.7 Tag filtering include-only| Unit | Pure filtering logic| Automated |
582+
|2.8 Tag exclusion| Unit | Pure filtering logic| Automated |
583+
|2.9 Explicit repos override| Unit | Selection precedence logic| Automated |
584+
|2.10 Mixed include/exclude| Unit | Logical combination test| Automated |
584585

585586
### 18.3 Run Command (Command Mode)
586587

587588
| Case | Type | Rationale | Status |
588589
|------|------|-----------|---------|
589-
|3.1 Single echo| Integration | Executes shell in repo context| Automated |
590-
|3.2 Multiple sequential| Integration | Iterative multi-repo execution| Automated |
591-
|3.3 Multiple parallel| Integration | Concurrency execution path| Automated |
592-
|3.4 Long command name sanitization| Unit | String transformation only| Automated |
593-
|3.5 Special characters command| Integration | Actual shell invocation & metadata capture| Automated |
594-
|3.6 Empty command string| Unit | Validation logic (candidate for stricter behavior) | Partial |
595-
|3.7 No-save mode behavior| Integration | Affects artifact creation side-effects| Automated |
596-
|3.8 Save mode directory creation| Integration | Filesystem structure| Automated |
597-
|3.9 Existing output directory reuse| Integration | FS existence + new path logic| Automated |
598-
|3.10 Exit code recording| Unit | Mapping + extraction (can isolate via fake status) | Automated |
599-
|3.11 Exit code description mapping| Unit | Pure match function| Partial |
600-
|3.12 Metadata.json structure (command)| Integration | Requires file writing & JSON content| Automated |
590+
|3.1 Single echo| Integration | Executes shell in repo context| Automated |
591+
|3.2 Multiple sequential| Integration | Iterative multi-repo execution| Automated |
592+
|3.3 Multiple parallel| Integration | Concurrency execution path| Automated |
593+
|3.4 Long command name sanitization| Unit | String transformation only| Automated |
594+
|3.5 Special characters command| Integration | Actual shell invocation & metadata capture| Automated |
595+
|3.6 Empty command string| Unit | Validation logic (candidate for stricter behavior) | ⚠️ Partial - behavior unclear |
596+
|3.7 No-save mode behavior| Integration | Affects artifact creation side-effects| Automated |
597+
|3.8 Save mode directory creation| Integration | Filesystem structure| Automated |
598+
|3.9 Existing output directory reuse| Integration | FS existence + new path logic| Automated |
599+
|3.10 Exit code recording| Unit | Mapping + extraction (can isolate via fake status) | Automated |
600+
|3.11 Exit code description mapping| Unit | Pure match function| ✅ Automated (added unit tests) |
601+
|3.12 Metadata.json structure (command)| Integration | Requires file writing & JSON content| Automated |
601602

602603
### 18.4 Run Command (Recipe Mode)
603604

604605
| Case | Type | Rationale | Status |
605606
|------|------|-----------|---------|
606-
|4.1 Single-step recipe| Integration | Script materialization + execution| Automated |
607-
|4.2 Multi-step sequential| Integration | Aggregate execution order| Automated |
608-
|4.3 Multi-repo parallel recipe| Integration | Concurrency + FS per repo| Automated |
609-
|4.4 Script created & removed| Integration | FS artifact lifecycle| Automated |
610-
|4.5 Metadata.json recipe fields| Integration | Generated file content| Automated |
611-
|4.6 Recipe name not found| Unit | Lookup error path| Automated |
612-
|4.7 Zero steps recipe| Unit | Validation / precondition logic| Automated |
613-
|4.8 Implicit shebang| Unit | Script content transformation| Automated |
614-
|4.9 Permissions set| Integration | Actual FS permissions required| Automated |
615-
|4.10 Cleanup on failure| Integration | Execution + post-failure cleanup| Automated |
616-
|4.11 Exit codes propagate| Integration | Real failing script status| Automated |
617-
|4.12 Mixed success/failure halts| Integration | Execution control flow| Partial |
607+
|4.1 Single-step recipe| Integration | Script materialization + execution| ✅ Automated |
608+
|4.2 Multi-step sequential| Integration | Aggregate execution order| ✅ Automated |
609+
|4.3 Multi-repo parallel recipe| Integration | Concurrency + FS per repo| ✅ Automated |
610+
|4.4 Script created & removed| Integration | FS artifact lifecycle| ✅ Automated |
611+
|4.5 Metadata.json recipe fields| Integration | Generated file content| ✅ Automated |
612+
|4.6 Recipe name not found| Unit | Lookup error path| ✅ Automated |
613+
|4.7 Zero steps recipe| Unit | Validation / precondition logic| ✅ Automated |
614+
|4.8 Implicit shebang| Unit | Script content transformation| ✅ Automated |
615+
|4.9 Permissions set| Integration | Actual FS permissions required| ✅ Automated |
616+
|4.10 Cleanup on failure| Integration | Execution + post-failure cleanup| ✅ Automated |
617+
|4.11 Exit codes propagate| Integration | Real failing script status| ✅ Automated |
618+
|4.12 Mixed success/failure halts| Integration | Execution control flow| ⚠️ Partial - needs verification |
619+
|Unicode script name sanitization| Unit | Ensures generated script filename handles Unicode safely| ❌ Gap |
618620

619621
### 18.5 Logging & Output
620622

@@ -626,6 +628,7 @@ Classification criteria:
626628
|5.4 Timestamp format| Unit | Formatting function| Gap |
627629
|5.5 Directory naming pattern| Unit | String assembly + sanitization| Partial |
628630
|5.6 Truncation behavior| Unit | String length logic| Automated |
631+
|Simultaneous runs distinct timestamps| Integration | Parallel invocations produce non-colliding directories| ❌ Gap |
629632

630633
### 18.6 Parallel vs Sequential Behavior
631634

@@ -645,13 +648,13 @@ All considered Integration (multi-repo orchestration). Stress/performance varian
645648

646649
| Case | Type | Rationale | Status |
647650
|------|------|-----------|---------|
648-
|8.1 Missing command & recipe| E2E | Full CLI argument validation path| Automated |
649-
|8.2 Nonexistent binary (plugin)| Integration | External process failure under plugin harness| Partial |
650-
|8.3 Mutual exclusivity metadata| Integration | Generated file schema| Automated |
651-
|8.4 Command not found 127| Integration | Real process exit| Automated |
652-
|8.5 Script cannot execute 126| Integration | Permission/exec failure| Gap |
653-
|8.6 Interrupted 130| Integration | Signal handling from process| Gap |
654-
|Signal >128 mapping (edge)| Unit | Mapping function correctness| Gap |
651+
|8.1 Missing command & recipe| E2E | Full CLI argument validation path| Automated (recently fixed) |
652+
|8.2 Nonexistent binary (plugin)| Integration | External process failure under plugin harness| ⚠️ Partial |
653+
|8.3 Mutual exclusivity metadata| Integration | Generated file schema| Automated |
654+
|8.4 Command not found 127| Integration | Real process exit| Automated |
655+
|8.5 Script cannot execute 126| Integration | Permission/exec failure| Gap |
656+
|8.6 Interrupted 130| Integration | Signal handling from process| Gap |
657+
|Signal >128 mapping (edge)| Unit | Mapping function correctness| Gap |
655658

656659
### 18.9 Plugins
657660

@@ -663,6 +666,7 @@ All considered Integration (multi-repo orchestration). Stress/performance varian
663666
|9.4 Fallback when no plugins present| Integration | Graceful empty state | Automated |
664667
|9.5 Help text still accessible with plugins| E2E | Full CLI parsing with dynamic plugin context | Gap |
665668
|9.6 Plugin does not interfere with core logging| Integration | Compare logs with/without plugins | Partial |
669+
|Multiple plugins simultaneously| Integration | Validates isolation & non-interference with more than one plugin | ❌ Gap |
666670

667671
### 18.10 Pull Requests
668672

plugins/repos-health/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ chrono = { version = "0.4", features = ["serde"] }
1515

1616
[dependencies.repos]
1717
path = "../.."
18+
19+
[dev-dependencies]
20+
tempfile = "3"

plugins/repos-health/src/main.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,109 @@ fn short_timestamp() -> String {
188188
let now = Utc::now();
189189
format!("{}", now.format("%Y%m%d"))
190190
}
191+
192+
#[cfg(test)]
193+
mod tests {
194+
use super::*;
195+
use tempfile::TempDir;
196+
197+
#[test]
198+
fn test_print_help() {
199+
// Test that print_help function executes without panicking
200+
print_help();
201+
// If we reach this point, the function executed successfully
202+
// Test passes if print_help() completes without panicking
203+
}
204+
205+
#[test]
206+
fn test_short_timestamp_format() {
207+
let timestamp = short_timestamp();
208+
// Should be 8 characters in YYYYMMDD format
209+
assert_eq!(timestamp.len(), 8);
210+
// Should be all digits
211+
assert!(timestamp.chars().all(|c| c.is_ascii_digit()));
212+
}
213+
214+
#[test]
215+
fn test_check_outdated_execution() {
216+
// Test execution path for check_outdated function
217+
let temp_dir = TempDir::new().unwrap();
218+
let repo_path = temp_dir.path();
219+
220+
// This will hit the npm command execution path
221+
// Expected to return empty vec since npm likely not available in test environment
222+
let result = check_outdated(repo_path);
223+
assert!(result.is_ok());
224+
}
225+
226+
#[test]
227+
fn test_update_dependencies_execution() {
228+
// Test execution path for update_dependencies function
229+
let temp_dir = TempDir::new().unwrap();
230+
let repo_path = temp_dir.path();
231+
232+
// This will execute the npm update command path
233+
let result = update_dependencies(repo_path);
234+
assert!(result.is_ok()); // Should always succeed (ignores npm failures)
235+
}
236+
237+
#[test]
238+
fn test_has_lockfile_changes_execution() {
239+
// Test execution path for has_lockfile_changes function
240+
let temp_dir = TempDir::new().unwrap();
241+
let repo_path = temp_dir.path();
242+
243+
// Initialize a git repo for the test
244+
let _ = Command::new("git")
245+
.arg("init")
246+
.current_dir(repo_path)
247+
.output();
248+
249+
// This will hit the git status execution path
250+
let result = has_lockfile_changes(repo_path);
251+
// May succeed or fail depending on git setup, but tests execution path
252+
let _ = result; // Don't assert result since git may not be available
253+
}
254+
255+
#[test]
256+
fn test_process_repo_no_package_json() {
257+
// Test process_repo execution path when no package.json exists
258+
let temp_dir = TempDir::new().unwrap();
259+
260+
let repo = Repository {
261+
name: "test-repo".to_string(),
262+
url: "https://github.com/test/repo.git".to_string(),
263+
path: Some(temp_dir.path().to_string_lossy().to_string()),
264+
branch: None,
265+
tags: vec![],
266+
config_dir: None,
267+
};
268+
269+
// This should hit the "no package.json" error path
270+
let result = process_repo(&repo);
271+
assert!(result.is_err());
272+
assert!(result.unwrap_err().to_string().contains("no package.json"));
273+
}
274+
275+
#[test]
276+
fn test_run_command_execution() {
277+
// Test the run function execution path
278+
let temp_dir = TempDir::new().unwrap();
279+
let repo_path = temp_dir.path();
280+
281+
// Test with a simple command that should succeed
282+
let result = run(repo_path, ["echo", "test"]);
283+
assert!(result.is_ok());
284+
}
285+
286+
#[test]
287+
fn test_run_command_failure() {
288+
// Test the run function error path
289+
let temp_dir = TempDir::new().unwrap();
290+
let repo_path = temp_dir.path();
291+
292+
// Test with a command that should fail
293+
let result = run(repo_path, ["nonexistent_command_12345"]);
294+
assert!(result.is_err());
295+
}
296+
}

src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod init;
66
pub mod pr;
77
pub mod remove;
88
pub mod run;
9+
pub mod validators;
910

1011
// Re-export the base types and all commands
1112
pub use base::{Command, CommandContext};

0 commit comments

Comments
 (0)