use super::provider::LOG_TARGET;
use crate::Result;
use ohno::IntoAppError;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::Path;
#[derive(Debug, Default, Clone)]
pub struct GitHubWorkflowInfo {
pub workflows_detected: bool,
pub clippy_detected: bool,
pub miri_detected: bool,
}
pub fn sniff_github_workflows(repo_path: impl AsRef<Path>) -> Result<GitHubWorkflowInfo> {
const MAX_WORKFLOW_FILES: usize = 100;
let mut usage = GitHubWorkflowInfo::default();
let workflows_dir = repo_path.as_ref().join(".github").join("workflows");
if !workflows_dir.exists() {
return Ok(usage);
}
usage.workflows_detected = true;
let mut file_count = 0;
for entry_result in walkdir::WalkDir::new(&workflows_dir).follow_links(false) {
let entry = entry_result.into_app_err("walking workflows directory")?;
if entry.file_type().is_dir() {
continue;
}
let is_yaml = entry
.path()
.extension()
.and_then(|s| s.to_str())
.is_some_and(|ext| ext == "yml" || ext == "yaml");
if !is_yaml {
continue;
}
file_count += 1;
if file_count > MAX_WORKFLOW_FILES {
log::warn!(target: LOG_TARGET, "Workflow file count limit ({MAX_WORKFLOW_FILES}) exceeded in directory '{}', stopping scan", workflows_dir.display());
break;
}
let file =
fs::File::open(entry.path()).into_app_err_with(|| format!("opening workflow file '{}'", entry.path().display()))?;
let reader = BufReader::new(file);
for line in reader.lines().map_while(Result::ok) {
let lower = line.to_lowercase();
if !usage.miri_detected && lower.contains("miri") {
usage.miri_detected = true;
}
if !usage.clippy_detected && lower.contains("clippy") {
usage.clippy_detected = true;
}
if usage.miri_detected && usage.clippy_detected {
return Ok(usage);
}
}
}
Ok(usage)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetTempPathW")]
fn test_no_workflows_directory() {
let temp_dir = tempfile::tempdir().unwrap();
let result = sniff_github_workflows(temp_dir.path()).unwrap();
assert!(!result.workflows_detected);
assert!(!result.miri_detected);
assert!(!result.clippy_detected);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetTempPathW")]
fn test_empty_workflows_directory() {
let temp_dir = tempfile::tempdir().unwrap();
let workflows_dir = temp_dir.path().join(".github").join("workflows");
fs::create_dir_all(&workflows_dir).unwrap();
let result = sniff_github_workflows(temp_dir.path()).unwrap();
assert!(result.workflows_detected);
assert!(!result.miri_detected);
assert!(!result.clippy_detected);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetTempPathW")]
fn test_workflows_with_clippy() {
let temp_dir = tempfile::tempdir().unwrap();
let workflows_dir = temp_dir.path().join(".github").join("workflows");
fs::create_dir_all(&workflows_dir).unwrap();
let workflow_file = workflows_dir.join("ci.yml");
fs::write(
&workflow_file,
"
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run clippy
run: cargo clippy -- -D warnings
",
)
.unwrap();
let result = sniff_github_workflows(temp_dir.path()).unwrap();
assert!(result.workflows_detected);
assert!(result.clippy_detected);
assert!(!result.miri_detected);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetTempPathW")]
fn test_workflows_with_miri() {
let temp_dir = tempfile::tempdir().unwrap();
let workflows_dir = temp_dir.path().join(".github").join("workflows");
fs::create_dir_all(&workflows_dir).unwrap();
let workflow_file = workflows_dir.join("miri.yaml");
fs::write(
&workflow_file,
"
name: Miri
on: [push]
jobs:
miri:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run Miri
run: cargo +nightly miri test
",
)
.unwrap();
let result = sniff_github_workflows(temp_dir.path()).unwrap();
assert!(result.workflows_detected);
assert!(!result.clippy_detected);
assert!(result.miri_detected);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetTempPathW")]
fn test_workflows_with_both() {
let temp_dir = tempfile::tempdir().unwrap();
let workflows_dir = temp_dir.path().join(".github").join("workflows");
fs::create_dir_all(&workflows_dir).unwrap();
let workflow_file = workflows_dir.join("ci.yml");
fs::write(
&workflow_file,
"
name: CI
on: [push]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Run Clippy
run: cargo clippy
miri:
runs-on: ubuntu-latest
steps:
- name: Run Miri
run: cargo +nightly miri test
",
)
.unwrap();
let result = sniff_github_workflows(temp_dir.path()).unwrap();
assert!(result.workflows_detected);
assert!(result.clippy_detected);
assert!(result.miri_detected);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetTempPathW")]
fn test_case_insensitive_detection() {
let temp_dir = tempfile::tempdir().unwrap();
let workflows_dir = temp_dir.path().join(".github").join("workflows");
fs::create_dir_all(&workflows_dir).unwrap();
let workflow_file = workflows_dir.join("ci.yml");
fs::write(
&workflow_file,
"
name: CI
steps:
- name: Run CLIPPY in uppercase
run: cargo CLIPPY
- name: Run MiRi in mixed case
run: cargo MiRi test
",
)
.unwrap();
let result = sniff_github_workflows(temp_dir.path()).unwrap();
assert!(result.clippy_detected);
assert!(result.miri_detected);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetTempPathW")]
fn test_multiple_workflow_files() {
let temp_dir = tempfile::tempdir().unwrap();
let workflows_dir = temp_dir.path().join(".github").join("workflows");
fs::create_dir_all(&workflows_dir).unwrap();
fs::write(workflows_dir.join("clippy.yml"), "run: cargo clippy").unwrap();
fs::write(workflows_dir.join("miri.yaml"), "run: cargo miri test").unwrap();
fs::write(workflows_dir.join("test.yml"), "run: cargo test").unwrap();
let result = sniff_github_workflows(temp_dir.path()).unwrap();
assert!(result.workflows_detected);
assert!(result.clippy_detected);
assert!(result.miri_detected);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetTempPathW")]
fn test_non_yaml_files_ignored() {
let temp_dir = tempfile::tempdir().unwrap();
let workflows_dir = temp_dir.path().join(".github").join("workflows");
fs::create_dir_all(&workflows_dir).unwrap();
fs::write(workflows_dir.join("README.md"), "This mentions clippy and miri").unwrap();
fs::write(workflows_dir.join("ci.yml"), "run: cargo test").unwrap();
let result = sniff_github_workflows(temp_dir.path()).unwrap();
assert!(result.workflows_detected);
assert!(!result.clippy_detected);
assert!(!result.miri_detected);
}
#[test]
fn test_github_workflow_info_default() {
let info = GitHubWorkflowInfo::default();
assert!(!info.workflows_detected);
assert!(!info.clippy_detected);
assert!(!info.miri_detected);
}
#[test]
fn test_github_workflow_info_clone() {
let info1 = GitHubWorkflowInfo {
workflows_detected: true,
clippy_detected: true,
miri_detected: false,
};
let info2 = info1.clone();
assert_eq!(info1.workflows_detected, info2.workflows_detected);
assert_eq!(info1.clippy_detected, info2.clippy_detected);
assert_eq!(info1.miri_detected, info2.miri_detected);
}
}