use anyhow::{Context, Result};
use async_trait::async_trait;
use changepacks_core::publish::{
PublishOutput, resolve_dry_run_publish_command, run_publish_command,
};
use changepacks_core::{Config, Language, UpdateType, Workspace};
use changepacks_utils::next_version;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use tokio::fs::{read_to_string, write};
use crate::dry_run::run_managed_dry_run;
use crate::xml_utils::update_version_in_xml;
#[derive(Debug)]
pub struct CSharpWorkspace {
path: PathBuf,
relative_path: PathBuf,
version: Option<String>,
name: Option<String>,
is_changed: bool,
dependencies: HashSet<String>,
}
impl CSharpWorkspace {
#[must_use]
pub fn new(
name: Option<String>,
version: Option<String>,
path: PathBuf,
relative_path: PathBuf,
) -> Self {
Self {
path,
relative_path,
name,
version,
is_changed: false,
dependencies: HashSet::new(),
}
}
}
#[async_trait]
impl Workspace for CSharpWorkspace {
fn name(&self) -> Option<&str> {
self.name.as_deref()
}
fn path(&self) -> &Path {
&self.path
}
fn version(&self) -> Option<&str> {
self.version.as_deref()
}
async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
let next_version = next_version(
self.version.as_ref().unwrap_or(&String::from("0.0.0")),
update_type,
)?;
let csproj_raw = read_to_string(&self.path).await?;
let has_version = self.version.is_some();
let updated_content = update_version_in_xml(&csproj_raw, &next_version, has_version)?;
write(&self.path, updated_content).await?;
self.version = Some(next_version);
Ok(())
}
fn language(&self) -> Language {
Language::CSharp
}
fn is_changed(&self) -> bool {
self.is_changed
}
fn set_changed(&mut self, changed: bool) {
self.is_changed = changed;
}
fn relative_path(&self) -> &Path {
&self.relative_path
}
fn set_name(&mut self, name: String) {
self.name = Some(name);
}
fn default_publish_command(&self) -> String {
"dotnet pack -c Release && dotnet nuget push".to_string()
}
fn default_dry_run_publish_command(&self) -> Option<String> {
None
}
#[cfg(not(tarpaulin_include))]
async fn dry_run_publish(&self, config: &Config) -> Result<Option<PublishOutput>> {
let dir = self
.path()
.parent()
.context("Workspace directory not found")?;
if let Some(user_cmd) =
resolve_dry_run_publish_command(self.relative_path(), self.language(), None, config)
{
return Ok(Some(run_publish_command(&user_cmd, dir).await?));
}
Ok(Some(run_managed_dry_run(dir).await?))
}
fn dependencies(&self) -> &HashSet<String> {
&self.dependencies
}
fn add_dependency(&mut self, dependency: &str) {
self.dependencies.insert(dependency.to_string());
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[tokio::test]
async fn test_new_with_name_and_version() {
let temp_dir = TempDir::new().unwrap();
let csproj_path = temp_dir.path().join("Test.csproj");
fs::write(
&csproj_path,
r#"<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.0</Version>
</PropertyGroup>
</Project>
"#,
)
.unwrap();
let workspace = CSharpWorkspace::new(
Some("Test".to_string()),
Some("1.0.0".to_string()),
csproj_path.clone(),
PathBuf::from("Test.csproj"),
);
assert_eq!(workspace.name(), Some("Test"));
assert_eq!(workspace.version(), Some("1.0.0"));
assert_eq!(workspace.path(), csproj_path);
assert_eq!(workspace.relative_path(), PathBuf::from("Test.csproj"));
assert!(!workspace.is_changed());
assert_eq!(workspace.language(), Language::CSharp);
assert_eq!(
workspace.default_publish_command(),
"dotnet pack -c Release && dotnet nuget push"
);
assert!(workspace.default_dry_run_publish_command().is_none());
temp_dir.close().unwrap();
}
#[tokio::test]
async fn test_new_without_name_and_version() {
let temp_dir = TempDir::new().unwrap();
let csproj_path = temp_dir.path().join("Test.csproj");
fs::write(
&csproj_path,
r#"<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>
"#,
)
.unwrap();
let workspace = CSharpWorkspace::new(
None,
None,
csproj_path.clone(),
PathBuf::from("Test.csproj"),
);
assert_eq!(workspace.name(), None);
assert_eq!(workspace.version(), None);
assert_eq!(workspace.path(), csproj_path);
assert!(!workspace.is_changed());
temp_dir.close().unwrap();
}
#[tokio::test]
async fn test_set_changed() {
let temp_dir = TempDir::new().unwrap();
let csproj_path = temp_dir.path().join("Test.csproj");
fs::write(
&csproj_path,
r#"<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.0</Version>
</PropertyGroup>
</Project>
"#,
)
.unwrap();
let mut workspace = CSharpWorkspace::new(
Some("Test".to_string()),
Some("1.0.0".to_string()),
csproj_path.clone(),
PathBuf::from("Test.csproj"),
);
assert!(!workspace.is_changed());
workspace.set_changed(true);
assert!(workspace.is_changed());
workspace.set_changed(false);
assert!(!workspace.is_changed());
temp_dir.close().unwrap();
}
#[tokio::test]
async fn test_update_version_with_existing_version() {
let temp_dir = TempDir::new().unwrap();
let csproj_path = temp_dir.path().join("Test.csproj");
fs::write(
&csproj_path,
r#"<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.0</Version>
</PropertyGroup>
</Project>
"#,
)
.unwrap();
let mut workspace = CSharpWorkspace::new(
Some("Test".to_string()),
Some("1.0.0".to_string()),
csproj_path.clone(),
PathBuf::from("Test.csproj"),
);
workspace.update_version(UpdateType::Patch).await.unwrap();
let content = fs::read_to_string(&csproj_path).unwrap();
assert!(content.contains("<Version>1.0.1</Version>"));
temp_dir.close().unwrap();
}
#[tokio::test]
async fn test_update_version_without_version() {
let temp_dir = TempDir::new().unwrap();
let csproj_path = temp_dir.path().join("Test.csproj");
fs::write(
&csproj_path,
r#"<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>
"#,
)
.unwrap();
let mut workspace = CSharpWorkspace::new(
Some("Test".to_string()),
None,
csproj_path.clone(),
PathBuf::from("Test.csproj"),
);
workspace.update_version(UpdateType::Patch).await.unwrap();
let content = fs::read_to_string(&csproj_path).unwrap();
assert!(content.contains("<Version>0.0.1</Version>"));
temp_dir.close().unwrap();
}
#[tokio::test]
async fn test_update_version_minor() {
let temp_dir = TempDir::new().unwrap();
let csproj_path = temp_dir.path().join("Test.csproj");
fs::write(
&csproj_path,
r#"<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.0</Version>
</PropertyGroup>
</Project>
"#,
)
.unwrap();
let mut workspace = CSharpWorkspace::new(
Some("Test".to_string()),
Some("1.0.0".to_string()),
csproj_path.clone(),
PathBuf::from("Test.csproj"),
);
workspace.update_version(UpdateType::Minor).await.unwrap();
let content = fs::read_to_string(&csproj_path).unwrap();
assert!(content.contains("<Version>1.1.0</Version>"));
temp_dir.close().unwrap();
}
#[tokio::test]
async fn test_update_version_major() {
let temp_dir = TempDir::new().unwrap();
let csproj_path = temp_dir.path().join("Test.csproj");
fs::write(
&csproj_path,
r#"<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.0</Version>
</PropertyGroup>
</Project>
"#,
)
.unwrap();
let mut workspace = CSharpWorkspace::new(
Some("Test".to_string()),
Some("1.0.0".to_string()),
csproj_path.clone(),
PathBuf::from("Test.csproj"),
);
workspace.update_version(UpdateType::Major).await.unwrap();
let content = fs::read_to_string(&csproj_path).unwrap();
assert!(content.contains("<Version>2.0.0</Version>"));
temp_dir.close().unwrap();
}
#[test]
fn test_dependencies() {
let mut workspace = CSharpWorkspace::new(
Some("Test".to_string()),
Some("1.0.0".to_string()),
PathBuf::from("/test/Test.csproj"),
PathBuf::from("test/Test.csproj"),
);
assert!(workspace.dependencies().is_empty());
workspace.add_dependency("Newtonsoft.Json");
workspace.add_dependency("CoreLib");
let deps = workspace.dependencies();
assert_eq!(deps.len(), 2);
assert!(deps.contains("Newtonsoft.Json"));
assert!(deps.contains("CoreLib"));
workspace.add_dependency("Newtonsoft.Json");
assert_eq!(workspace.dependencies().len(), 2);
}
#[test]
fn test_set_name() {
let mut workspace = CSharpWorkspace::new(
None,
Some("1.0.0".to_string()),
PathBuf::from("/test/Test.csproj"),
PathBuf::from("Test.csproj"),
);
assert_eq!(workspace.name(), None);
workspace.set_name("my-project".to_string());
assert_eq!(workspace.name(), Some("my-project"));
}
}