use std::path::Path;
use crate::project::Project;
use anyhow::Result;
use async_trait::async_trait;
#[async_trait]
pub trait ProjectFinder: std::fmt::Debug + Send + Sync {
fn projects(&self) -> Vec<&Project>;
fn projects_mut(&mut self) -> Vec<&mut Project>;
fn project_files(&self) -> &[&str];
async fn visit(&mut self, path: &Path, relative_path: &Path) -> Result<()>;
fn check_changed(&mut self, path: &Path) -> Result<()> {
for project in self.projects_mut() {
project.check_changed(path)?;
}
Ok(())
}
#[cfg(not(tarpaulin_include))]
async fn finalize(&mut self) -> Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Language, Package, UpdateType, Workspace};
use async_trait::async_trait;
use std::collections::HashSet;
use std::path::PathBuf;
#[derive(Debug)]
struct MockPackage {
name: Option<String>,
path: PathBuf,
relative_path: PathBuf,
changed: bool,
dependencies: HashSet<String>,
}
impl MockPackage {
fn new(name: &str, path: &str) -> Self {
Self {
name: Some(name.to_string()),
path: PathBuf::from(path),
relative_path: PathBuf::from(path),
changed: false,
dependencies: HashSet::new(),
}
}
}
#[async_trait]
impl Package for MockPackage {
fn name(&self) -> Option<&str> {
self.name.as_deref()
}
fn version(&self) -> Option<&str> {
Some("1.0.0")
}
fn path(&self) -> &Path {
&self.path
}
fn relative_path(&self) -> &Path {
&self.relative_path
}
async fn update_version(&mut self, _update_type: UpdateType) -> Result<()> {
Ok(())
}
fn is_changed(&self) -> bool {
self.changed
}
fn language(&self) -> Language {
Language::Node
}
fn dependencies(&self) -> &HashSet<String> {
&self.dependencies
}
fn add_dependency(&mut self, dep: &str) {
self.dependencies.insert(dep.to_string());
}
fn set_changed(&mut self, changed: bool) {
self.changed = changed;
}
fn default_publish_command(&self) -> String {
"echo test".to_string()
}
fn default_dry_run_publish_command(&self) -> Option<String> {
Some("echo test --dry-run".to_string())
}
fn inherits_workspace_version(&self) -> bool {
false
}
fn workspace_root_path(&self) -> Option<&Path> {
None
}
}
#[derive(Debug)]
struct MockWorkspace {
name: Option<String>,
path: PathBuf,
relative_path: PathBuf,
changed: bool,
dependencies: HashSet<String>,
}
impl MockWorkspace {
fn new(name: &str, path: &str) -> Self {
Self {
name: Some(name.to_string()),
path: PathBuf::from(path),
relative_path: PathBuf::from(path),
changed: false,
dependencies: HashSet::new(),
}
}
}
#[async_trait]
impl Workspace for MockWorkspace {
fn name(&self) -> Option<&str> {
self.name.as_deref()
}
fn path(&self) -> &Path {
&self.path
}
fn relative_path(&self) -> &Path {
&self.relative_path
}
fn version(&self) -> Option<&str> {
Some("1.0.0")
}
async fn update_version(&mut self, _update_type: UpdateType) -> Result<()> {
Ok(())
}
fn language(&self) -> Language {
Language::Node
}
fn dependencies(&self) -> &HashSet<String> {
&self.dependencies
}
fn add_dependency(&mut self, dep: &str) {
self.dependencies.insert(dep.to_string());
}
fn is_changed(&self) -> bool {
self.changed
}
fn set_changed(&mut self, changed: bool) {
self.changed = changed;
}
fn default_publish_command(&self) -> String {
"echo test".to_string()
}
fn default_dry_run_publish_command(&self) -> Option<String> {
Some("echo test --dry-run".to_string())
}
}
#[derive(Debug)]
struct MockProjectFinder {
projects: Vec<Project>,
}
impl MockProjectFinder {
fn new() -> Self {
Self { projects: vec![] }
}
fn with_package(mut self, package: MockPackage) -> Self {
self.projects.push(Project::Package(Box::new(package)));
self
}
fn with_workspace(mut self, workspace: MockWorkspace) -> Self {
self.projects.push(Project::Workspace(Box::new(workspace)));
self
}
}
#[async_trait]
impl ProjectFinder for MockProjectFinder {
fn projects(&self) -> Vec<&Project> {
self.projects.iter().collect()
}
fn projects_mut(&mut self) -> Vec<&mut Project> {
self.projects.iter_mut().collect()
}
fn project_files(&self) -> &[&str] {
&["package.json"]
}
async fn visit(&mut self, _path: &Path, _relative_path: &Path) -> Result<()> {
Ok(())
}
}
#[test]
fn test_project_finder_check_changed() {
let package = MockPackage::new("test", "/project/package.json");
let mut finder = MockProjectFinder::new().with_package(package);
finder
.check_changed(Path::new("/project/src/index.js"))
.unwrap();
assert!(finder.projects()[0].is_changed());
}
#[test]
fn test_project_finder_check_changed_multiple_projects() {
let package1 = MockPackage::new("pkg1", "/project1/package.json");
let package2 = MockPackage::new("pkg2", "/project2/package.json");
let mut finder = MockProjectFinder::new()
.with_package(package1)
.with_package(package2);
finder
.check_changed(Path::new("/project1/src/index.js"))
.unwrap();
assert!(finder.projects()[0].is_changed());
assert!(!finder.projects()[1].is_changed());
}
#[test]
fn test_project_finder_with_workspace() {
let workspace = MockWorkspace::new("root", "/project/package.json");
let mut finder = MockProjectFinder::new().with_workspace(workspace);
finder
.check_changed(Path::new("/project/src/index.js"))
.unwrap();
assert!(finder.projects()[0].is_changed());
}
#[tokio::test]
async fn test_project_finder_finalize() {
let mut finder = MockProjectFinder::new();
let result = finder.finalize().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_project_finder_finalize_with_projects() {
let package = MockPackage::new("pkg1", "/project/package.json");
let mut finder = MockProjectFinder::new().with_package(package);
let result = finder.finalize().await;
assert!(result.is_ok());
assert_eq!(finder.projects().len(), 1);
}
}