use anyhow::Result;
use ignore::WalkBuilder;
use rayon::prelude::*;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use super::workspace_id::{
MetaFileInfo as WorkspaceIdMetaFile, WorkspaceIdSource, compute_workspace_id, get_git_remote,
};
use super::workspace_settings::{LoadedSettings, WorkspaceSettings, load_settings};
use unfault_core::types::context::RepoLayout as ProjectLayout;
use unfault_core::types::workspace::{AdvertisedProfile, MetaFile, WorkspaceDescriptor};
#[derive(Debug, Clone)]
pub struct ScanProgress {
pub file_count: usize,
pub languages: Vec<String>,
pub frameworks: Vec<String>,
}
pub type ProgressCallback = Arc<dyn Fn(ScanProgress) + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Language {
Python,
Rust,
Go,
Java,
TypeScript,
JavaScript,
}
impl Language {
pub fn as_str(&self) -> &'static str {
match self {
Language::Python => "python",
Language::Rust => "rust",
Language::Go => "go",
Language::Java => "java",
Language::TypeScript => "typescript",
Language::JavaScript => "javascript",
}
}
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.to_lowercase().as_str() {
"py" => Some(Language::Python),
"rs" => Some(Language::Rust),
"go" => Some(Language::Go),
"java" => Some(Language::Java),
"ts" | "tsx" => Some(Language::TypeScript),
"js" | "jsx" | "mjs" | "cjs" => Some(Language::JavaScript),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct DetectedFramework {
pub name: String,
pub confidence: f64,
pub signals: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct WorkspaceInfo {
pub root: PathBuf,
pub label: String,
pub workspace_id: Option<String>,
pub workspace_id_source: Option<WorkspaceIdSource>,
pub git_remote: Option<String>,
pub languages: HashMap<Language, usize>,
pub frameworks: Vec<DetectedFramework>,
pub source_files: Vec<(PathBuf, Language)>,
pub meta_files: Vec<LocalMetaFileInfo>,
pub layout: ProjectLayout,
pub settings: Option<LoadedSettings>,
}
#[derive(Debug, Clone)]
pub struct LocalMetaFileInfo {
pub path: PathBuf,
pub kind: MetaFileKind,
pub contents: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetaFileKind {
Pyproject,
RequirementsTxt,
SetupPy,
PackageJson,
GoMod,
CargoToml,
PomXml,
BuildGradle,
Other,
}
impl MetaFileKind {
pub fn as_str(&self) -> &'static str {
match self {
MetaFileKind::Pyproject => "pyproject",
MetaFileKind::RequirementsTxt => "requirements_txt",
MetaFileKind::SetupPy => "setup_py",
MetaFileKind::PackageJson => "package_json",
MetaFileKind::GoMod => "go_mod",
MetaFileKind::CargoToml => "cargo_toml",
MetaFileKind::PomXml => "pom_xml",
MetaFileKind::BuildGradle => "build_gradle",
MetaFileKind::Other => "other",
}
}
pub fn language(&self) -> &'static str {
match self {
MetaFileKind::Pyproject | MetaFileKind::CargoToml => "toml",
MetaFileKind::RequirementsTxt => "text",
MetaFileKind::SetupPy => "python",
MetaFileKind::PackageJson => "json",
MetaFileKind::GoMod => "go",
MetaFileKind::PomXml => "xml",
MetaFileKind::BuildGradle => "groovy",
MetaFileKind::Other => "text",
}
}
pub fn from_filename(filename: &str) -> Option<Self> {
match filename {
"pyproject.toml" => Some(MetaFileKind::Pyproject),
"requirements.txt" => Some(MetaFileKind::RequirementsTxt),
"setup.py" => Some(MetaFileKind::SetupPy),
"package.json" => Some(MetaFileKind::PackageJson),
"go.mod" => Some(MetaFileKind::GoMod),
"Cargo.toml" => Some(MetaFileKind::CargoToml),
"pom.xml" => Some(MetaFileKind::PomXml),
"build.gradle" | "build.gradle.kts" => Some(MetaFileKind::BuildGradle),
_ => None,
}
}
}
impl WorkspaceInfo {
pub fn to_workspace_descriptor(&self) -> WorkspaceDescriptor {
let mut ws = WorkspaceDescriptor::new(self.label.clone());
ws.profiles = self.build_profiles();
ws.meta_files = self
.meta_files
.iter()
.map(|mf| {
MetaFile::new(
mf.path.to_string_lossy().to_string(),
mf.kind.language(),
unfault_core::types::workspace::MetaFileKind::Pyproject, mf.contents.clone(),
)
})
.collect();
ws
}
pub fn workspace_settings(&self) -> Option<&WorkspaceSettings> {
self.settings.as_ref().map(|s| &s.settings)
}
fn build_profiles(&self) -> Vec<AdvertisedProfile> {
let mut profiles = Vec::new();
if let Some(ref loaded_settings) = self.settings {
if let Some(ref profile_override) = loaded_settings.settings.profile {
profiles.push(AdvertisedProfile {
id: profile_override.clone(),
confidence: 1.0, });
}
}
let framework_confidence_factor = if profiles.is_empty() { 1.0 } else { 0.8 };
for framework in &self.frameworks {
let profile_id = match framework.name.as_str() {
"fastapi" => "python_fastapi_backend",
"flask" => "python_flask_backend",
"django" => "python_django_backend",
"express" => "typescript_express_backend",
"gin" => "go_gin_service",
"echo" | "fiber" => "go_generic_service",
"spring" | "springboot" => "java_spring_backend",
"axum" => "rust_axum_service",
"actix" => "rust_actix_service",
"rocket" | "warp" => "rust_generic",
_ => continue,
};
if profiles.iter().any(|p| p.id == profile_id) {
continue;
}
profiles.push(AdvertisedProfile {
id: profile_id.to_string(),
confidence: (framework.confidence * framework_confidence_factor) as f32,
});
}
for (language, _count) in &self.languages {
let has_framework_profile = profiles.iter().any(|p| {
p.id.starts_with(match language {
Language::Python => "python_",
Language::Rust => "rust_",
Language::Go => "go_",
Language::Java => "java_",
Language::TypeScript | Language::JavaScript => "typescript_",
})
});
if !has_framework_profile {
let profile_id = match language {
Language::Python => "python_generic_backend",
Language::Rust => "rust_axum_service", Language::Go => "go_generic_service",
Language::Java => "java_generic",
Language::TypeScript | Language::JavaScript => "typescript_express_backend",
};
profiles.push(AdvertisedProfile {
id: profile_id.to_string(),
confidence: (0.7 * framework_confidence_factor) as f32,
});
}
}
if profiles.is_empty() {
profiles.push(AdvertisedProfile {
id: "generic".to_string(),
confidence: 0.5,
});
}
profiles
}
pub fn language_strings(&self) -> Vec<String> {
self.languages
.keys()
.map(|l| l.as_str().to_string())
.collect()
}
pub fn framework_strings(&self) -> Vec<String> {
self.frameworks.iter().map(|f| f.name.clone()).collect()
}
}
#[derive(Debug, Clone)]
struct ScannedFile {
path: PathBuf,
language: Language,
}
#[derive(Debug, Clone)]
struct ScannedDir {
relative_path: String,
is_src: bool,
is_test: bool,
}
pub struct WorkspaceScanner {
root: PathBuf,
progress_callback: Option<ProgressCallback>,
}
impl WorkspaceScanner {
pub fn new(root: impl AsRef<Path>) -> Self {
Self {
root: root.as_ref().to_path_buf(),
progress_callback: None,
}
}
pub fn with_progress<F>(mut self, callback: F) -> Self
where
F: Fn(ScanProgress) + Send + Sync + 'static,
{
self.progress_callback = Some(Arc::new(callback));
self
}
pub fn scan(&mut self) -> Result<WorkspaceInfo> {
let label = self
.root
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("workspace")
.to_string();
let walker = WalkBuilder::new(&self.root)
.hidden(true) .git_ignore(true) .git_global(true) .git_exclude(true) .ignore(true) .require_git(false) .add_custom_ignore_filename(".gitignore") .add_custom_ignore_filename(".dockerignore") .filter_entry(|entry| {
if let Some(name) = entry.file_name().to_str() {
!Self::should_skip_directory(name)
} else {
true
}
})
.build();
let entries: Vec<_> = walker.filter_map(|e| e.ok()).collect();
let (files, dirs): (Vec<_>, Vec<_>) = entries
.iter()
.filter(|e| e.path() != self.root)
.partition(|e| e.file_type().is_some_and(|ft| ft.is_file()));
let file_count = AtomicUsize::new(0);
let has_fastapi = AtomicBool::new(false);
let has_flask = AtomicBool::new(false);
let has_django = AtomicBool::new(false);
let has_express = AtomicBool::new(false);
let has_gin = AtomicBool::new(false);
let progress_callback = &self.progress_callback;
let last_progress_count = AtomicUsize::new(0);
let file_results: Vec<_> = files
.par_iter()
.filter_map(|entry| {
let path = entry.path();
let filename = path.file_name()?.to_str()?;
let meta_file = if let Some(kind) = MetaFileKind::from_filename(filename) {
fs::read_to_string(path).ok().map(|contents| {
let relative_path =
path.strip_prefix(&self.root).unwrap_or(path).to_path_buf();
LocalMetaFileInfo {
path: relative_path,
kind,
contents,
}
})
} else {
None
};
let source_file = path
.extension()
.and_then(|e| e.to_str())
.and_then(Language::from_extension)
.map(|language| {
match language {
Language::Python => {
if !has_fastapi.load(Ordering::Relaxed)
|| !has_flask.load(Ordering::Relaxed)
|| !has_django.load(Ordering::Relaxed)
{
if let Ok(contents) = fs::read_to_string(path) {
if contents.contains("from fastapi")
|| contents.contains("import fastapi")
{
has_fastapi.store(true, Ordering::Relaxed);
}
if contents.contains("from flask")
|| contents.contains("import flask")
|| contents.contains("from flask_smorest")
|| contents.contains("import flask_smorest")
|| contents.contains("from flask_restful")
|| contents.contains("import flask_restful")
|| contents.contains(".action_route(")
{
has_flask.store(true, Ordering::Relaxed);
}
if contents.contains("from django")
|| contents.contains("import django")
{
has_django.store(true, Ordering::Relaxed);
}
}
}
}
Language::JavaScript | Language::TypeScript => {
if !has_express.load(Ordering::Relaxed) {
if let Ok(contents) = fs::read_to_string(path) {
if contents.contains("require('express')")
|| contents.contains("require(\"express\")")
|| contents.contains("from 'express'")
|| contents.contains("from \"express\"")
{
has_express.store(true, Ordering::Relaxed);
}
}
}
}
Language::Go => {
if !has_gin.load(Ordering::Relaxed) {
if let Ok(contents) = fs::read_to_string(path) {
if contents.contains("github.com/gin-gonic/gin")
|| contents.contains("gin.Context")
|| contents.contains("gin.Engine")
{
has_gin.store(true, Ordering::Relaxed);
}
}
}
}
_ => {}
}
let count = file_count.fetch_add(1, Ordering::Relaxed) + 1;
if let Some(callback) = progress_callback {
let last = last_progress_count.load(Ordering::Relaxed);
if count >= last + 50 || count == 1 {
last_progress_count.store(count, Ordering::Relaxed);
callback(ScanProgress {
file_count: count,
languages: vec![], frameworks: Self::build_frameworks_list(
has_fastapi.load(Ordering::Relaxed),
has_flask.load(Ordering::Relaxed),
has_django.load(Ordering::Relaxed),
has_express.load(Ordering::Relaxed),
has_gin.load(Ordering::Relaxed),
),
});
}
}
ScannedFile {
path: path.to_path_buf(),
language,
}
});
Some((source_file, meta_file))
})
.collect();
let dir_results: Vec<ScannedDir> = dirs
.iter()
.filter_map(|entry| {
let path = entry.path();
let dir_name = path.file_name()?.to_str()?;
let relative = path
.strip_prefix(&self.root)
.unwrap_or(path)
.to_string_lossy()
.to_string();
Some(ScannedDir {
relative_path: relative,
is_src: dir_name == "src" || dir_name == "lib" || dir_name == "app",
is_test: dir_name == "tests" || dir_name == "test" || dir_name == "spec",
})
})
.collect();
let mut source_files = Vec::new();
let mut meta_files = Vec::new();
let mut language_counts: HashMap<Language, usize> = HashMap::new();
for (source_file, meta_file) in file_results {
if let Some(sf) = source_file {
*language_counts.entry(sf.language).or_insert(0) += 1;
source_files.push((sf.path, sf.language));
}
if let Some(mf) = meta_file {
meta_files.push(mf);
}
}
let mut src_dirs = Vec::new();
let mut test_dirs = Vec::new();
for dir in &dir_results {
if dir.is_src {
src_dirs.push(dir.relative_path.clone());
}
if dir.is_test {
test_dirs.push(dir.relative_path.clone());
}
}
let mut frameworks = Vec::new();
if has_fastapi.load(Ordering::Relaxed) {
frameworks.push(DetectedFramework {
name: "fastapi".to_string(),
confidence: 0.9,
signals: vec!["import fastapi".to_string()],
});
}
if has_flask.load(Ordering::Relaxed) {
frameworks.push(DetectedFramework {
name: "flask".to_string(),
confidence: 0.9,
signals: vec!["import flask".to_string()],
});
}
if has_django.load(Ordering::Relaxed) {
frameworks.push(DetectedFramework {
name: "django".to_string(),
confidence: 0.9,
signals: vec!["import django".to_string()],
});
}
if has_express.load(Ordering::Relaxed) {
frameworks.push(DetectedFramework {
name: "express".to_string(),
confidence: 0.9,
signals: vec!["require('express')".to_string()],
});
}
if has_gin.load(Ordering::Relaxed) {
frameworks.push(DetectedFramework {
name: "gin".to_string(),
confidence: 0.9,
signals: vec!["github.com/gin-gonic/gin".to_string()],
});
}
let layout = ProjectLayout {
src_dirs,
test_dirs,
other_dirs: vec![],
directories: vec![],
};
let settings = load_settings(&self.root);
let git_remote = get_git_remote(&self.root);
let workspace_id_meta: Vec<WorkspaceIdMetaFile> = meta_files
.iter()
.map(|mf| WorkspaceIdMetaFile {
kind: mf.kind.as_str(),
contents: mf.contents.clone(),
})
.collect();
let (workspace_id, workspace_id_source) = if let Some(result) = compute_workspace_id(
git_remote.as_deref(),
Some(&workspace_id_meta),
Some(&label),
) {
(Some(result.id), Some(result.source))
} else {
(None, None)
};
if let Some(ref callback) = self.progress_callback {
callback(ScanProgress {
file_count: source_files.len(),
languages: language_counts
.keys()
.map(|l| l.as_str().to_string())
.collect(),
frameworks: Self::build_frameworks_list(
has_fastapi.load(Ordering::Relaxed),
has_flask.load(Ordering::Relaxed),
has_django.load(Ordering::Relaxed),
has_express.load(Ordering::Relaxed),
has_gin.load(Ordering::Relaxed),
),
});
}
Ok(WorkspaceInfo {
root: self.root.clone(),
label,
workspace_id,
workspace_id_source,
git_remote,
languages: language_counts,
frameworks,
source_files,
meta_files,
layout,
settings,
})
}
fn build_frameworks_list(
has_fastapi: bool,
has_flask: bool,
has_django: bool,
has_express: bool,
has_gin: bool,
) -> Vec<String> {
let mut frameworks = Vec::new();
if has_fastapi {
frameworks.push("fastapi".to_string());
}
if has_flask {
frameworks.push("flask".to_string());
}
if has_django {
frameworks.push("django".to_string());
}
if has_express {
frameworks.push("express".to_string());
}
if has_gin {
frameworks.push("gin".to_string());
}
frameworks
}
fn should_skip_directory(name: &str) -> bool {
name == "node_modules"
|| name == "__pycache__"
|| name == "target"
|| name == "venv"
|| name == "env"
|| name == ".venv"
|| name == ".env"
|| name == "dist"
|| name == "build"
|| name == "vendor"
|| name == "site-packages"
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_language_from_extension() {
assert_eq!(Language::from_extension("py"), Some(Language::Python));
assert_eq!(Language::from_extension("rs"), Some(Language::Rust));
assert_eq!(Language::from_extension("go"), Some(Language::Go));
assert_eq!(Language::from_extension("java"), Some(Language::Java));
assert_eq!(Language::from_extension("ts"), Some(Language::TypeScript));
assert_eq!(Language::from_extension("tsx"), Some(Language::TypeScript));
assert_eq!(Language::from_extension("js"), Some(Language::JavaScript));
assert_eq!(Language::from_extension("jsx"), Some(Language::JavaScript));
assert_eq!(Language::from_extension("txt"), None);
}
#[test]
fn test_language_as_str() {
assert_eq!(Language::Python.as_str(), "python");
assert_eq!(Language::Rust.as_str(), "rust");
assert_eq!(Language::Go.as_str(), "go");
assert_eq!(Language::Java.as_str(), "java");
assert_eq!(Language::TypeScript.as_str(), "typescript");
assert_eq!(Language::JavaScript.as_str(), "javascript");
}
#[test]
fn test_meta_file_kind_from_filename() {
assert_eq!(
MetaFileKind::from_filename("pyproject.toml"),
Some(MetaFileKind::Pyproject)
);
assert_eq!(
MetaFileKind::from_filename("requirements.txt"),
Some(MetaFileKind::RequirementsTxt)
);
assert_eq!(
MetaFileKind::from_filename("package.json"),
Some(MetaFileKind::PackageJson)
);
assert_eq!(
MetaFileKind::from_filename("Cargo.toml"),
Some(MetaFileKind::CargoToml)
);
assert_eq!(MetaFileKind::from_filename("random.txt"), None);
}
#[test]
fn test_scan_empty_workspace() {
let temp_dir = TempDir::new().unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
assert!(info.source_files.is_empty());
assert!(info.languages.is_empty());
assert!(info.frameworks.is_empty());
}
#[test]
fn test_scan_python_workspace() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("main.py");
fs::write(&file_path, "print('hello')").unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
assert_eq!(info.source_files.len(), 1);
assert_eq!(info.languages.get(&Language::Python), Some(&1));
}
#[test]
fn test_scan_fastapi_detection() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("app.py");
fs::write(&file_path, "from fastapi import FastAPI\napp = FastAPI()").unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
assert!(info.frameworks.iter().any(|f| f.name == "fastapi"));
}
#[test]
fn test_scan_flask_detection() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("app.py");
fs::write(&file_path, "from flask import Flask\napp = Flask(__name__)").unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
assert!(info.frameworks.iter().any(|f| f.name == "flask"));
}
#[test]
fn test_scan_django_detection() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("settings.py");
fs::write(&file_path, "from django.conf import settings").unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
assert!(info.frameworks.iter().any(|f| f.name == "django"));
}
#[test]
fn test_scan_skips_hidden_dirs() {
let temp_dir = TempDir::new().unwrap();
let hidden_dir = temp_dir.path().join(".hidden");
fs::create_dir(&hidden_dir).unwrap();
let file_path = hidden_dir.join("secret.py");
fs::write(&file_path, "secret = 'password'").unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
assert!(info.source_files.is_empty());
}
#[test]
fn test_scan_skips_node_modules() {
let temp_dir = TempDir::new().unwrap();
let node_modules = temp_dir.path().join("node_modules");
fs::create_dir(&node_modules).unwrap();
let file_path = node_modules.join("package.js");
fs::write(&file_path, "module.exports = {}").unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
assert!(info.source_files.is_empty());
}
#[test]
fn test_scan_meta_files() {
let temp_dir = TempDir::new().unwrap();
let pyproject = temp_dir.path().join("pyproject.toml");
fs::write(&pyproject, "[tool.poetry]\nname = \"test\"").unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
assert_eq!(info.meta_files.len(), 1);
assert_eq!(info.meta_files[0].kind, MetaFileKind::Pyproject);
}
#[test]
fn test_scan_project_layout() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let tests_dir = temp_dir.path().join("tests");
fs::create_dir(&src_dir).unwrap();
fs::create_dir(&tests_dir).unwrap();
fs::write(src_dir.join("main.py"), "pass").unwrap();
fs::write(tests_dir.join("test_main.py"), "pass").unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
assert!(info.layout.src_dirs.contains(&"src".to_string()));
assert!(info.layout.test_dirs.contains(&"tests".to_string()));
}
#[test]
fn test_workspace_info_to_descriptor() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("app.py");
fs::write(&file_path, "from fastapi import FastAPI").unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
let descriptor = info.to_workspace_descriptor();
assert!(!descriptor.label.is_empty());
assert!(!descriptor.profiles.is_empty());
assert!(
descriptor
.profiles
.iter()
.any(|p| p.id == "python_fastapi_backend")
);
}
#[test]
fn test_build_profiles_generic_fallback() {
let temp_dir = TempDir::new().unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
let descriptor = info.to_workspace_descriptor();
assert_eq!(descriptor.profiles.len(), 1);
assert_eq!(descriptor.profiles[0].id, "generic");
}
#[test]
fn test_build_profiles_python_generic() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("main.py");
fs::write(&file_path, "print('hello')").unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
let descriptor = info.to_workspace_descriptor();
assert!(
descriptor
.profiles
.iter()
.any(|p| p.id == "python_generic_backend")
);
}
#[test]
fn test_language_strings() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("main.py"), "pass").unwrap();
fs::write(temp_dir.path().join("main.rs"), "fn main() {}").unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
let languages = info.language_strings();
assert!(languages.contains(&"python".to_string()));
assert!(languages.contains(&"rust".to_string()));
}
#[test]
fn test_framework_strings() {
let temp_dir = TempDir::new().unwrap();
fs::write(
temp_dir.path().join("app.py"),
"from fastapi import FastAPI",
)
.unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
let frameworks = info.framework_strings();
assert!(frameworks.contains(&"fastapi".to_string()));
}
#[test]
fn test_gitignore_respected() {
let temp_dir = TempDir::new().unwrap();
let gitignore_path = temp_dir.path().join(".gitignore");
fs::write(&gitignore_path, "*.log\nsecret.py").unwrap();
fs::write(temp_dir.path().join("app.log"), "log content").unwrap();
fs::write(temp_dir.path().join("secret.py"), "secret = 'password'").unwrap();
fs::write(temp_dir.path().join("main.py"), "print('hello')").unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
assert_eq!(info.source_files.len(), 1);
assert!(
info.source_files
.iter()
.any(|(p, _)| p.ends_with("main.py"))
);
assert!(
info.source_files
.iter()
.all(|(p, _)| !p.ends_with("app.log"))
);
assert!(
info.source_files
.iter()
.all(|(p, _)| !p.ends_with("secret.py"))
);
}
#[test]
fn test_dockerignore_respected() {
let temp_dir = TempDir::new().unwrap();
let dockerignore_path = temp_dir.path().join(".dockerignore");
fs::write(&dockerignore_path, "node_modules/\n*.tmp").unwrap();
let node_modules = temp_dir.path().join("node_modules");
fs::create_dir(&node_modules).unwrap();
fs::write(node_modules.join("package.json"), "{}").unwrap();
fs::write(temp_dir.path().join("temp.tmp"), "temp content").unwrap();
fs::write(temp_dir.path().join("main.py"), "print('hello')").unwrap();
let mut scanner = WorkspaceScanner::new(temp_dir.path());
let info = scanner.scan().unwrap();
assert_eq!(info.source_files.len(), 1);
assert!(
info.source_files
.iter()
.any(|(p, _)| p.ends_with("main.py"))
);
assert!(
info.source_files
.iter()
.all(|(p, _)| !p.ends_with("temp.tmp"))
);
}
}