use std::fs;
use std::path::Path;
use projd_core::{
BuildSystemKind, CiProvider, HealthSignalKind, LanguageKind, LicenseKind, ProjectHealth,
scan_path,
};
use tempfile::TempDir;
#[test]
fn detects_common_project_markers_from_local_directory() {
let fixture = ProjectFixture::new();
fixture.write("Cargo.toml", "[workspace]\nmembers = [\"app\"]\n");
fixture.write("Cargo.lock", "# lock\n");
fixture.write(
"package.json",
"{\"scripts\":{\"test\":\"node test.js\"}}\n",
);
fixture.write("pyproject.toml", "[project]\nname = \"demo\"\n");
fixture.write("requirements.txt", "pytest\n");
fixture.write("CMakeLists.txt", "cmake_minimum_required(VERSION 3.20)\n");
fixture.write("README.md", "# Demo\n");
fixture.write("LICENSE", "MIT\n");
fixture.write("docs/guide.md", "# Guide\n");
fixture.write(".github/workflows/ci.yml", "name: ci\n");
fixture.write(".workflow/rust-ci.yml", "version: '1.0'\n");
fixture.write(".gitlab-ci.yml", "stages: []\n");
fixture.write(".circleci/config.yml", "version: 2.1\n");
fixture.write("Jenkinsfile", "pipeline {}\n");
fixture.write("Dockerfile", "FROM scratch\n");
fixture.write("docker-compose.yml", "services: {}\n");
fixture.write("src/main.rs", "fn main() {}\n");
fixture.write("web/app.ts", "export const value = 1;\n");
fixture.write("scripts/build.py", "print('build')\n");
fixture.write("native/main.cpp", "int main() { return 0; }\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
assert_eq!(scan.project_name, fixture.name());
assert!(scan.has_language(LanguageKind::Rust));
assert!(scan.has_language(LanguageKind::TypeScript));
assert!(scan.has_language(LanguageKind::Python));
assert!(scan.has_language(LanguageKind::Cpp));
assert!(scan.has_build_system(BuildSystemKind::Cargo));
assert!(scan.has_build_system(BuildSystemKind::NodePackage));
assert!(scan.has_build_system(BuildSystemKind::PythonProject));
assert!(scan.has_build_system(BuildSystemKind::CMake));
assert!(scan.documentation.has_readme);
assert!(scan.documentation.has_license);
assert_eq!(scan.license.kind, LicenseKind::Mit);
assert!(scan.documentation.has_docs_dir);
assert!(scan.ci.has_github_actions);
assert!(scan.ci.has_gitee_go);
assert!(scan.ci.has_gitlab_ci);
assert!(scan.ci.has_circle_ci);
assert!(scan.ci.has_jenkins);
assert!(scan.ci.has_provider(CiProvider::GithubActions));
assert!(scan.ci.has_provider(CiProvider::GiteeGo));
assert!(scan.ci.has_provider(CiProvider::GitlabCi));
assert!(scan.ci.has_provider(CiProvider::CircleCi));
assert!(scan.ci.has_provider(CiProvider::Jenkins));
assert_eq!(scan.ci.providers.len(), 5);
assert!(scan.ci.providers.iter().any(|provider| {
provider.provider == CiProvider::GithubActions && provider.path.ends_with("ci.yml")
}));
assert!(scan.ci.providers.iter().any(|provider| {
provider.provider == CiProvider::GiteeGo && provider.path.ends_with("rust-ci.yml")
}));
assert!(scan.ci.providers.iter().any(|provider| {
provider.provider == CiProvider::GitlabCi && provider.path.ends_with(".gitlab-ci.yml")
}));
assert!(scan.ci.providers.iter().any(|provider| {
provider.provider == CiProvider::CircleCi && provider.path.ends_with("config.yml")
}));
assert!(scan.ci.providers.iter().any(|provider| {
provider.provider == CiProvider::Jenkins && provider.path.ends_with("Jenkinsfile")
}));
assert!(scan.containers.has_dockerfile);
assert!(scan.containers.has_compose_file);
assert!(scan.files_scanned >= 16);
assert_eq!(scan.health.grade, ProjectHealth::NeedsAttention);
assert!(scan.health.score >= 70);
assert!(
scan.health
.signals
.iter()
.any(|signal| signal.kind == HealthSignalKind::Ci)
);
}
#[test]
fn detects_git_summary_from_local_repository() {
let fixture = ProjectFixture::new();
if !git_available() {
return;
}
run_git(fixture.path(), &["init"]);
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
);
fixture.write("README.md", "# Demo\n");
fixture.write("LICENSE", "MIT\n");
fixture.write("src/main.rs", "fn main() {}\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
assert!(scan.vcs.is_repository);
assert!(
scan.vcs
.root
.as_ref()
.is_some_and(|path| path == fixture.path())
);
assert!(scan.vcs.branch.is_some());
assert!(scan.vcs.is_dirty);
assert!(scan.vcs.untracked_files > 0);
}
#[test]
fn skips_heavy_and_internal_directories() {
let fixture = ProjectFixture::new();
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
);
fixture.write("src/main.rs", "fn main() {}\n");
fixture.write("target/generated.py", "print('ignore me')\n");
fixture.write(
"node_modules/pkg/index.ts",
"export const ignored = true;\n",
);
fixture.write(".git/hooks/pre-commit.cpp", "int ignored() { return 0; }\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
assert!(scan.has_language(LanguageKind::Rust));
assert!(!scan.has_language(LanguageKind::Python));
assert!(!scan.has_language(LanguageKind::TypeScript));
assert!(!scan.has_language(LanguageKind::Cpp));
assert!(
scan.skipped_dirs
.iter()
.any(|path| path.ends_with("target"))
);
assert!(
scan.skipped_dirs
.iter()
.any(|path| path.ends_with("node_modules"))
);
assert!(scan.skipped_dirs.iter().any(|path| path.ends_with(".git")));
}
fn git_available() -> bool {
std::process::Command::new("git")
.arg("--version")
.output()
.is_ok_and(|output| output.status.success())
}
fn run_git(cwd: &Path, args: &[&str]) {
let output = std::process::Command::new("git")
.args(args)
.current_dir(cwd)
.output()
.expect("run git");
assert!(
output.status.success(),
"git {:?} failed: {}{}",
args,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn respects_gitignore_rules() {
let fixture = ProjectFixture::new();
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
);
fixture.write(".gitignore", "generated/\nignored.py\n");
fixture.write("src/main.rs", "fn main() {}\n");
fixture.write("generated/app.ts", "export const ignored = true;\n");
fixture.write("ignored.py", "print('ignore me')\n");
fixture.write("native/main.cpp", "int main() { return 0; }\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
assert!(scan.has_language(LanguageKind::Rust));
assert!(scan.has_language(LanguageKind::Cpp));
assert!(!scan.has_language(LanguageKind::TypeScript));
assert!(!scan.has_language(LanguageKind::Python));
assert!(
scan.skipped_dirs
.iter()
.any(|path| path.ends_with("generated"))
);
}
#[test]
fn respects_ignore_file_rules() {
let fixture = ProjectFixture::new();
fixture.write(
"Cargo.toml",
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
);
fixture.write(".ignore", "scratch/\nignored.cpp\n");
fixture.write("src/main.rs", "fn main() {}\n");
fixture.write("scratch/script.py", "print('ignore me')\n");
fixture.write("ignored.cpp", "int ignored() { return 0; }\n");
let scan = scan_path(fixture.path()).expect("scan succeeds");
assert!(scan.has_language(LanguageKind::Rust));
assert!(!scan.has_language(LanguageKind::Python));
assert!(!scan.has_language(LanguageKind::Cpp));
assert!(
scan.skipped_dirs
.iter()
.any(|path| path.ends_with("scratch"))
);
}
struct ProjectFixture {
temp_dir: TempDir,
}
impl ProjectFixture {
fn new() -> Self {
Self {
temp_dir: tempfile::tempdir().expect("create temp dir"),
}
}
fn path(&self) -> &Path {
self.temp_dir.path()
}
fn name(&self) -> String {
self.path()
.file_name()
.and_then(|value| value.to_str())
.expect("temp dir has name")
.to_owned()
}
fn write(&self, relative: &str, content: &str) {
let path = self.path().join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create fixture parent");
}
fs::write(path, content).expect("write fixture file");
}
}