test: add tests for review fixes and clean up dead code

The previous commit fixed a bunch of bugs but left a few loose
ends. The next_job() function still had a redundant bounds check
that previous_job() already had cleaned up — the .filter() call
makes the inner `if workflow_idx >= self.workflows.len()` dead
code. Let's not leave half-finished refactors lying around.

While at it, add tests for the three behavioral changes that
*really* should have had tests from the start: emulation runtime
returning Ok on non-zero exit codes, log processor not panicking
on multi-byte UTF-8 near bracket boundaries, and step validator
correctly rejecting steps with only a name field.

Also fix formatting (cargo fmt) and a clippy warning about items
defined after the test module.
This commit is contained in:
bahdotsh
2026-04-01 19:08:44 +05:30
parent aa3366a797
commit 422a035c40
5 changed files with 116 additions and 24 deletions

View File

@@ -827,3 +827,25 @@ pub fn get_tracked_processes() -> Vec<u32> {
vec![]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_nonzero_exit_code_returns_ok() {
let runtime = EmulationRuntime::new();
let result = runtime
.run_container(
"alpine:latest",
&["exit", "42"],
&[],
Path::new("."),
&[(Path::new("."), Path::new("/github/workspace"))],
)
.await;
let output = result.expect("non-zero exit should return Ok, not Err");
assert_eq!(output.exit_code, 42);
}
}

View File

@@ -308,7 +308,6 @@ impl App {
.filter(|&idx| idx < self.workflows.len());
if let Some(workflow_idx) = current_workflow_idx {
if let Some(execution) = &self.workflows[workflow_idx].execution_details {
if execution.jobs.is_empty() {
return;
@@ -340,10 +339,6 @@ impl App {
.filter(|&idx| idx < self.workflows.len());
if let Some(workflow_idx) = current_workflow_idx {
if workflow_idx >= self.workflows.len() {
return;
}
if let Some(execution) = &self.workflows[workflow_idx].execution_details {
if execution.jobs.is_empty() {
return;

View File

@@ -307,3 +307,24 @@ impl Default for LogProcessor {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multibyte_log_line_does_not_panic() {
// Emoji and multi-byte characters near bracket boundaries
let entry = LogProcessor::process_log_entry("[🚀] deployed service", "");
assert_eq!(entry.log_type, "INFO");
let entry2 = LogProcessor::process_log_entry("[ñ] latin char", "");
assert!(!entry2.timestamp.is_empty());
}
#[test]
fn test_normal_timestamp_extraction() {
let entry = LogProcessor::process_log_entry("[12:34:56] some log", "");
assert_eq!(entry.timestamp, "12:34:56");
}
}

View File

@@ -55,3 +55,53 @@ pub fn validate_steps(steps: &[Value], job_name: &str, result: &mut ValidationRe
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use wrkflw_models::ValidationResult;
#[test]
fn test_step_with_only_name_is_invalid() {
let yaml = r#"
- name: "just a name"
"#;
let steps: Vec<Value> = serde_yaml::from_str(yaml).unwrap();
let mut result = ValidationResult::new();
validate_steps(&steps, "test-job", &mut result);
assert!(!result.is_valid);
assert!(result
.issues
.iter()
.any(|i| i.contains("Missing required 'uses' or 'run' field")));
}
#[test]
fn test_step_with_run_is_valid() {
let yaml = r#"
- name: "build"
run: "cargo build"
"#;
let steps: Vec<Value> = serde_yaml::from_str(yaml).unwrap();
let mut result = ValidationResult::new();
validate_steps(&steps, "test-job", &mut result);
assert!(result.is_valid);
assert!(result.issues.is_empty());
}
#[test]
fn test_step_with_uses_is_valid() {
let yaml = r#"
- name: "checkout"
uses: "actions/checkout@v4"
"#;
let steps: Vec<Value> = serde_yaml::from_str(yaml).unwrap();
let mut result = ValidationResult::new();
validate_steps(&steps, "test-job", &mut result);
assert!(result.is_valid);
assert!(result.issues.is_empty());
}
}

View File

@@ -341,19 +341,23 @@ async fn main() {
let entries = match std::fs::read_dir(&validate_path) {
Ok(rd) => rd,
Err(e) => {
eprintln!("Failed to read directory {}: {}", validate_path.display(), e);
eprintln!(
"Failed to read directory {}: {}",
validate_path.display(),
e
);
std::process::exit(1);
}
}
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry.path().is_file()
&& entry
.path()
.extension()
.is_some_and(|ext| ext == "yml" || ext == "yaml")
})
.collect::<Vec<_>>();
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry.path().is_file()
&& entry
.path()
.extension()
.is_some_and(|ext| ext == "yml" || ext == "yaml")
})
.collect::<Vec<_>>();
println!(
"Validating {} workflow file(s) in {}...",
@@ -660,15 +664,15 @@ fn list_workflows_and_pipelines(verbose: bool) {
return;
}
}
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry.path().is_file()
&& entry
.path()
.extension()
.is_some_and(|ext| ext == "yml" || ext == "yaml")
})
.collect::<Vec<_>>();
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry.path().is_file()
&& entry
.path()
.extension()
.is_some_and(|ext| ext == "yml" || ext == "yaml")
})
.collect::<Vec<_>>();
if entries.is_empty() {
println!(" No workflow files found in .github/workflows");