use projm::classify::{classify as classify_core, prefix_key, split_suffix, Category};
use std::fs;
use tempfile::TempDir;
fn classify(path: &std::path::Path) -> Category {
classify_core(path, &[])
}
fn project(files: &[&str]) -> TempDir {
let dir = tempfile::tempdir().unwrap();
for f in files {
let p = dir.path().join(f);
if let Some(parent) = p.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&p, "").unwrap();
}
dir
}
fn named_project(name: &str, files: &[&str]) -> (TempDir, std::path::PathBuf) {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join(name);
fs::create_dir_all(&dir).unwrap();
for f in files {
let p = dir.join(f);
if let Some(parent) = p.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&p, "").unwrap();
}
(root, dir)
}
#[test]
fn split_suffix_dash() {
assert_eq!(split_suffix("drivetrack-api"), Some(("drivetrack", "api")));
}
#[test]
fn split_suffix_underscore() {
assert_eq!(split_suffix("drivetrack_web"), Some(("drivetrack", "web")));
}
#[test]
fn split_suffix_case_insensitive() {
assert_eq!(split_suffix("DriveTrack-API"), Some(("DriveTrack", "API")));
}
#[test]
fn split_suffix_unknown_suffix() {
assert!(split_suffix("drivetrack-xyz").is_none());
}
#[test]
fn split_suffix_no_separator() {
assert!(split_suffix("trashnet").is_none());
}
#[test]
fn split_suffix_fw() {
assert_eq!(
split_suffix("rocket-telemetry-fw"),
Some(("rocket-telemetry", "fw"))
);
}
#[test]
fn split_suffix_uses_last_separator() {
let (prefix, suffix) = split_suffix("my-app-api").unwrap();
assert_eq!(prefix, "my-app");
assert_eq!(suffix.to_lowercase(), "api");
}
#[test]
fn prefix_key_lowercases() {
assert_eq!(prefix_key("DriveTrack-Api"), Some("drivetrack".into()));
assert_eq!(prefix_key("DriveTrack-Web"), Some("drivetrack".into()));
}
#[test]
fn prefix_key_none_for_no_suffix() {
assert!(prefix_key("trashnet").is_none());
}
#[test]
fn classify_doc_lab_wins_over_everything() {
let dir = project(&["doc-lab.md", "Cargo.toml", "package.json", "src-tauri"]);
assert_eq!(classify(dir.path()), Category::Labs);
}
#[test]
fn classify_memory_x() {
let dir = project(&["Cargo.toml", "memory.x"]);
assert_eq!(classify(dir.path()), Category::Embedded);
}
#[test]
fn classify_openocd() {
let dir = project(&["Cargo.toml", "openocd.cfg"]);
assert_eq!(classify(dir.path()), Category::Embedded);
}
#[test]
fn classify_embedded_via_cargo_target() {
let dir = project(&["Cargo.toml", ".cargo/config.toml"]);
fs::write(
dir.path().join(".cargo/config.toml"),
"[build]\ntarget = \"thumbv7em-none-eabihf\"\n",
)
.unwrap();
assert_eq!(classify(dir.path()), Category::Embedded);
}
#[test]
fn classify_fw_suffix_wins() {
let (_root, dir) = named_project("rocket-telemetry-fw", &["Cargo.toml"]);
assert_eq!(classify(&dir), Category::Embedded);
}
#[test]
fn classify_tauri() {
let dir = project(&["Cargo.toml", "package.json", "src-tauri"]);
assert_eq!(classify(dir.path()), Category::Apps);
}
#[test]
fn classify_cargo_plus_package_json_is_apps() {
let dir = project(&["Cargo.toml", "package.json"]);
assert_eq!(classify(dir.path()), Category::Apps);
}
#[test]
fn classify_desk_suffix() {
let (_root, dir) = named_project("drivetrack-desk", &["Cargo.toml", "package.json"]);
assert_eq!(classify(&dir), Category::Apps);
}
#[test]
fn classify_api_suffix_rust() {
let (_root, dir) = named_project("drivetrack-api", &["Cargo.toml"]);
assert_eq!(classify(&dir), Category::Services);
}
#[test]
fn classify_pure_rust_is_services() {
let (_root, dir) = named_project("medlink-core", &["Cargo.toml"]);
assert_eq!(classify(&dir), Category::Services);
}
#[test]
fn classify_hono_backend() {
let (_root, dir) = named_project("medlink-api", &["package.json"]);
fs::write(
dir.join("package.json"),
r#"{"dependencies":{"hono":"^4.0.0"}}"#,
)
.unwrap();
assert_eq!(classify(&dir), Category::Services);
}
#[test]
fn classify_web_suffix() {
let (_root, dir) = named_project("drivetrack-web", &["package.json"]);
assert_eq!(classify(&dir), Category::Ui);
}
#[test]
fn classify_react_frontend() {
let dir = project(&["package.json"]);
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"react":"^18.0.0","vite":"^5.0.0"}}"#,
)
.unwrap();
assert_eq!(classify(dir.path()), Category::Ui);
}
#[test]
fn classify_mob_suffix() {
let (_root, dir) = named_project("pioneers-mob", &["package.json"]);
assert_eq!(classify(&dir), Category::Ui);
}
#[test]
fn classify_fullstack_package_json() {
let dir = project(&["package.json"]);
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"react":"^18.0.0","hono":"^4.0.0"}}"#,
)
.unwrap();
assert_eq!(classify(dir.path()), Category::Apps);
}
#[test]
fn classify_ml_with_train_py() {
let dir = project(&["requirements.txt", "train.py"]);
assert_eq!(classify(dir.path()), Category::Ml);
}
#[test]
fn classify_ml_with_uv_and_model() {
let dir = project(&["uv.lock", "model.py"]);
assert_eq!(classify(dir.path()), Category::Ml);
}
#[test]
fn classify_python_no_ml_markers_is_tools() {
let dir = project(&["requirements.txt"]);
assert_eq!(classify(dir.path()), Category::Tools);
}
#[test]
fn classify_rust_cli_by_name() {
let (_root, dir) = named_project("projm-cli", &["Cargo.toml"]);
assert_eq!(classify(&dir), Category::Tools);
}
#[test]
fn classify_empty_dir_is_labs() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(classify(dir.path()), Category::Labs);
}
#[test]
fn classify_readme_only_is_labs() {
let dir = project(&["README.md"]);
assert_eq!(classify(dir.path()), Category::Labs);
}
#[test]
fn classify_flutter_app() {
let dir = project(&["pubspec.yaml", "android/build.gradle"]);
assert_eq!(classify(dir.path()), Category::Apps);
let dir_ios = project(&["pubspec.yaml", "ios/Runner.xcworkspace"]);
assert_eq!(classify(dir_ios.path()), Category::Apps);
}
#[test]
fn classify_dart_package() {
let dir = project(&["pubspec.yaml"]);
assert_eq!(classify(dir.path()), Category::Ui);
}
#[test]
fn classify_kotlin_android() {
let dir = project(&["build.gradle", "src/main/AndroidManifest.xml"]);
assert_eq!(classify(dir.path()), Category::Apps);
let dir_kts = project(&["build.gradle.kts", "app/src/main/AndroidManifest.xml"]);
assert_eq!(classify(dir_kts.path()), Category::Apps);
}
#[test]
fn classify_spring_boot_gradle() {
let dir = project(&["build.gradle"]);
assert_eq!(classify(dir.path()), Category::Services);
let dir_kts = project(&["build.gradle.kts"]);
assert_eq!(classify(dir_kts.path()), Category::Services);
}
#[test]
fn classify_java_maven() {
let dir = project(&["pom.xml"]);
assert_eq!(classify(dir.path()), Category::Services);
}
#[test]
fn classify_swift_project() {
let dir = project(&["Package.swift"]);
assert_eq!(classify(dir.path()), Category::Apps);
let dir_xcodeproj = project(&["App.xcodeproj/project.pbxproj"]);
assert_eq!(classify(dir_xcodeproj.path()), Category::Apps);
}
#[test]
fn classify_go_projects() {
let (_root, dir_service) = named_project("my-go-service", &["go.mod"]);
assert_eq!(classify(&dir_service), Category::Services);
let (_root, dir_cli) = named_project("my-go-cli", &["go.mod"]);
assert_eq!(classify(&dir_cli), Category::Tools);
}
#[test]
fn classify_ruby_on_rails() {
let dir = project(&["Gemfile", "config/routes.rb"]);
assert_eq!(classify(dir.path()), Category::Services);
}
#[test]
fn classify_laravel_php() {
let dir = project(&["composer.json", "artisan"]);
assert_eq!(classify(dir.path()), Category::Services);
}
#[test]
fn classify_elixir_phoenix() {
let dir = project(&["mix.exs"]);
assert_eq!(classify(dir.path()), Category::Services);
}
#[test]
fn classify_csharp_dotnet() {
let (_root, dir_app) = named_project("my-dotnet-app", &["MyProj.csproj"]);
assert_eq!(classify(&dir_app), Category::Apps);
let (_root, dir_service) = named_project("my-dotnet-service", &["MyProj.csproj"]);
assert_eq!(classify(&dir_service), Category::Services);
}
#[test]
fn classify_c_cpp_embedded() {
let dir_ld = project(&["CMakeLists.txt", "linker.ld"]);
assert_eq!(classify(dir_ld.path()), Category::Embedded);
let dir_openocd = project(&["CMakeLists.txt", "openocd.cfg"]);
assert_eq!(classify(dir_openocd.path()), Category::Embedded);
}
#[test]
fn classify_c_cpp_native() {
let dir = project(&["CMakeLists.txt"]);
assert_eq!(classify(dir.path()), Category::Tools);
}
#[test]
fn category_dir_names_are_stable() {
assert_eq!(Category::Apps.dir_name(), "apps");
assert_eq!(Category::Services.dir_name(), "services");
assert_eq!(Category::Ui.dir_name(), "ui");
assert_eq!(Category::Embedded.dir_name(), "embedded");
assert_eq!(Category::Ml.dir_name(), "ml");
assert_eq!(Category::Tools.dir_name(), "tools");
assert_eq!(Category::Labs.dir_name(), "labs");
assert_eq!(Category::Content.dir_name(), "content");
}
#[test]
fn category_all_has_eight_variants() {
assert_eq!(Category::all().len(), 8);
}