use std::path::Path;
#[test]
fn test_contract_load_from_file() {
let d = tempfile::tempdir().unwrap();
let dir = d.path().join(".quanttide/devops");
std::fs::create_dir_all(&dir).unwrap();
let yaml = "\
scopes:
cli:
dir: src/cli
language: rust
";
std::fs::write(dir.join("contract.yaml"), yaml).unwrap();
let c = quanttide_devops::contract::load(d.path()).unwrap();
assert_eq!(c.scopes.len(), 1);
assert_eq!(c.scopes[0].name, "cli");
}
#[test]
fn test_contract_load_file_not_found() {
let d = tempfile::tempdir().unwrap();
let err = quanttide_devops::contract::load(d.path()).unwrap_err();
assert!(
err.to_string().contains("读取契约文件失败") || err.to_string().contains("契约文件不存在")
);
}
#[test]
fn test_contract_load_invalid_yaml() {
let d = tempfile::tempdir().unwrap();
let dir = d.path().join(".quanttide/devops");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("contract.yaml"), "invalid: [").unwrap();
let err = quanttide_devops::contract::load(d.path()).unwrap_err();
assert!(err.to_string().contains("解析失败") || err.to_string().contains("YAML"));
}
#[test]
fn test_contract_load_empty_yaml() {
let d = tempfile::tempdir().unwrap();
let dir = d.path().join(".quanttide/devops");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("contract.yaml"), "").unwrap();
let c = quanttide_devops::contract::load(d.path()).unwrap();
assert!(c.scopes.is_empty());
assert_eq!(c.stages.test.threshold, 70.0);
}
#[test]
fn test_contract_load_full_config() {
let d = tempfile::tempdir().unwrap();
let dir = d.path().join(".quanttide/devops");
std::fs::create_dir_all(&dir).unwrap();
let yaml = "\
stages:
build:
command: cargo build
test:
command: cargo test
threshold: 80
release:
changelog: CHANGELOG.md
pre_publish:
- scripts/preflight.sh
platform:
source_control: github
pipeline: github_actions
artifact_registry: crates
sources:
version:
type: cargo
path: Cargo.toml
scopes:
cli:
dir: src/cli
language: rust
build_tool: cargo
framework: clap
registry: crates
test_threshold: 90
studio:
dir: src/studio
language: dart
build_tool: flutter
registry: pubdev
release:
changelog: src/studio/CHANGELOG.md
";
std::fs::write(dir.join("contract.yaml"), yaml).unwrap();
let c = quanttide_devops::contract::load(d.path()).unwrap();
assert_eq!(c.stages.build.command.as_deref(), Some("cargo build"));
assert_eq!(c.stages.test.command.as_deref(), Some("cargo test"));
assert_eq!(c.stages.test.threshold, 80.0);
assert_eq!(c.stages.release.changelog, "CHANGELOG.md");
assert_eq!(c.stages.release.pre_publish, vec!["scripts/preflight.sh"]);
assert_eq!(
c.platform.source_control,
quanttide_devops::contract::SourceControl::Github
);
assert_eq!(
c.platform.pipeline,
quanttide_devops::contract::Pipeline::GithubActions
);
assert_eq!(c.platform.artifact_registry.to_string(), "crates.io");
assert_eq!(
c.sources.version.source_type,
quanttide_devops::contract::SourceType::Cargo
);
assert_eq!(c.sources.version.path.as_deref(), Some("Cargo.toml"));
assert_eq!(c.scopes.len(), 2);
let cli = &c.scopes[0];
assert_eq!(cli.name, "cli");
assert_eq!(cli.dir, "src/cli");
assert_eq!(cli.language, quanttide_devops::contract::Language::Rust);
assert_eq!(cli.build_tool, quanttide_devops::contract::BuildTool::Cargo);
assert_eq!(cli.framework, "clap");
assert_eq!(cli.test_threshold, Some(90.0));
let studio = &c.scopes[1];
assert_eq!(studio.name, "studio");
assert_eq!(studio.dir, "src/studio");
assert_eq!(studio.language, quanttide_devops::contract::Language::Dart);
}
#[test]
fn test_source_type_detect_cargo() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("Cargo.toml"), "").unwrap();
assert_eq!(
quanttide_devops::contract::SourceType::detect(d.path()),
quanttide_devops::contract::SourceType::Cargo
);
}
#[test]
fn test_source_type_detect_pyproject() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("pyproject.toml"), "").unwrap();
assert_eq!(
quanttide_devops::contract::SourceType::detect(d.path()),
quanttide_devops::contract::SourceType::Pyproject
);
}
#[test]
fn test_source_type_detect_pubspec() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("pubspec.yaml"), "").unwrap();
assert_eq!(
quanttide_devops::contract::SourceType::detect(d.path()),
quanttide_devops::contract::SourceType::Pubspec
);
}
#[test]
fn test_source_type_detect_package_json() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("package.json"), "").unwrap();
assert_eq!(
quanttide_devops::contract::SourceType::detect(d.path()),
quanttide_devops::contract::SourceType::PackageJson
);
}
#[test]
fn test_source_type_detect_tag_only() {
let d = tempfile::tempdir().unwrap();
assert_eq!(
quanttide_devops::contract::SourceType::detect(d.path()),
quanttide_devops::contract::SourceType::TagOnly
);
}
#[test]
fn test_source_type_detect_priority() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("Cargo.toml"), "").unwrap();
std::fs::write(d.path().join("pyproject.toml"), "").unwrap();
std::fs::write(d.path().join("package.json"), "").unwrap();
assert_eq!(
quanttide_devops::contract::SourceType::detect(d.path()),
quanttide_devops::contract::SourceType::Cargo
);
}
#[test]
fn test_registry_serde_roundtrip() {
use quanttide_devops::contract::Registry;
let cases = vec![
(Registry::Crates, "crates"),
(Registry::PyPI, "pypi"),
(Registry::PubDev, "pubdev"),
(Registry::Npm, "npm"),
(Registry::GitHubReleases, "github_releases"),
(Registry::Docker, "docker"),
(Registry::None, "none"),
];
for (reg, yaml) in cases {
let serialized = serde_yaml::to_string(®).unwrap();
let trimmed = serialized.trim();
assert_eq!(trimmed, yaml, "Registry::{:?} serializes to {}", reg, yaml);
let deserialized: Registry = serde_yaml::from_str(trimmed).unwrap();
assert_eq!(deserialized, reg);
}
}
#[test]
fn test_detect_language_by_files_rust() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("Cargo.toml"), "").unwrap();
assert_eq!(
quanttide_devops::contract::detect_language_by_files(d.path()),
quanttide_devops::contract::Language::Rust
);
}
#[test]
fn test_detect_language_by_files_unknown() {
let d = tempfile::tempdir().unwrap();
let lang = quanttide_devops::contract::detect_language_by_files(d.path());
assert!(matches!(
lang,
quanttide_devops::contract::Language::Unknown(_)
));
}
#[test]
fn test_read_all_config_versions_integration() {
let d = tempfile::tempdir().unwrap();
std::fs::write(
d.path().join("Cargo.toml"),
r#"[package]
name = "test"
version = "0.1.0"
"#,
)
.unwrap();
std::fs::write(d.path().join("pyproject.toml"), "version = \"0.2.0\"\n").unwrap();
let versions = quanttide_devops::contract::read_all_config_versions(d.path());
assert!(!versions.is_empty());
assert!(versions.iter().any(|(_, v)| v.as_deref() == Some("0.1.0")));
}
#[test]
fn test_contract_validate_missing_dir() {
use quanttide_devops::contract::Contract;
let c = Contract::default();
assert!(c.validate(Path::new("/tmp")).is_empty());
let d = tempfile::tempdir().unwrap();
let dir = d.path().join(".quanttide/devops");
std::fs::create_dir_all(&dir).unwrap();
let yaml = "\
scopes:
nonexistent:
dir: does/not/exist
";
std::fs::write(dir.join("contract.yaml"), yaml).unwrap();
let c = quanttide_devops::contract::load(d.path()).unwrap();
let errors = c.validate(d.path());
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("does/not/exist"));
}
#[test]
fn test_detect_language_by_files_python() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("pyproject.toml"), "").unwrap();
assert_eq!(
quanttide_devops::contract::detect_language_by_files(d.path()),
quanttide_devops::contract::Language::Python
);
}
#[test]
fn test_detect_language_by_files_go() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("go.mod"), "").unwrap();
assert_eq!(
quanttide_devops::contract::detect_language_by_files(d.path()),
quanttide_devops::contract::Language::Go
);
}
#[test]
fn test_detect_language_by_files_dart() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("pubspec.yaml"), "").unwrap();
assert_eq!(
quanttide_devops::contract::detect_language_by_files(d.path()),
quanttide_devops::contract::Language::Dart
);
}
#[test]
fn test_detect_language_by_files_typescript() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("package.json"), "").unwrap();
assert_eq!(
quanttide_devops::contract::detect_language_by_files(d.path()),
quanttide_devops::contract::Language::TypeScript
);
}
#[test]
fn test_detect_language_by_files_python_requirements() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("requirements.txt"), "").unwrap();
assert_eq!(
quanttide_devops::contract::detect_language_by_files(d.path()),
quanttide_devops::contract::Language::Python
);
}
#[test]
fn test_git_error_display() {
use quanttide_devops::source::git::GitSourceError;
let err = GitSourceError::RepoOpen("/nonexistent".into());
assert!(err.to_string().contains("无法打开仓库"));
if let Err(git_err) = git2::Repository::open("/nonexistent") {
let from_git: GitSourceError = git_err.into();
assert!(from_git.to_string().contains("git2 错误"));
}
}
#[test]
fn test_git_from_impl() {
use quanttide_devops::source::git::GitSourceError;
if let Err(git_err) = git2::Repository::open("/nonexistent") {
let err: GitSourceError = git_err.into();
assert!(matches!(err, GitSourceError::Git2(_)));
}
}
#[test]
fn test_git_version_status() {
let d = tempfile::tempdir().unwrap();
let scope = quanttide_devops::contract::Scope {
name: "test".into(),
dir: ".".into(),
language: quanttide_devops::contract::Language::Rust,
build_tool: quanttide_devops::contract::BuildTool::Unknown("auto".into()),
registry: quanttide_devops::contract::Registry::None,
framework: String::new(),
release: quanttide_devops::contract::StageRelease::default(),
test_threshold: None,
ci_workflow: None,
};
let result = quanttide_devops::source::git::version_status(d.path(), &scope);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("无法打开仓库"));
let repo = git2::Repository::init(d.path()).unwrap();
let sig = git2::Signature::now("test", "test@test.com").unwrap();
let tree = {
let mut index = repo.index().unwrap();
let oid = index.write_tree().unwrap();
repo.find_tree(oid).unwrap()
};
repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[])
.unwrap();
std::fs::write(
d.path().join("Cargo.toml"),
r#"[package]
name = "test"
version = "0.1.0"
"#,
)
.unwrap();
let vs = quanttide_devops::source::git::version_status(d.path(), &scope).unwrap();
assert!(vs.tag_version.is_none());
assert!(vs.config_version.is_some());
repo.tag_lightweight(
"test/v0.1.0",
&repo
.find_object(repo.head().unwrap().target().unwrap(), None)
.unwrap(),
false,
)
.unwrap();
let vs = quanttide_devops::source::git::version_status(d.path(), &scope).unwrap();
assert_eq!(vs.tag_version.as_deref(), Some("0.1.0"));
assert_eq!(vs.config_version.as_deref(), Some("0.1.0"));
assert!(vs.consistent);
}
#[test]
fn test_validate_version_invalid_scope_chars() {
assert!(!quanttide_devops::contract::validate_version("/v0.1.0"));
assert!(!quanttide_devops::contract::validate_version(
"bad space/v0.1.0"
));
}
#[test]
fn test_validate_version_empty_prerelease() {
assert!(!quanttide_devops::contract::validate_version("v0.1.0-"));
assert!(!quanttide_devops::contract::validate_version("v0.1.0-."));
}
#[test]
fn test_read_config_versions_package_json_with_version() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("package.json"), r#"{"version":"1.2.3"}"#).unwrap();
let versions = quanttide_devops::contract::read_all_config_versions(d.path());
assert!(
versions
.iter()
.any(|(n, v)| n == "package.json" && v.as_deref() == Some("1.2.3"))
);
}
#[test]
fn test_read_config_versions_package_json_empty() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("package.json"), r#"{"version":""}"#).unwrap();
let versions = quanttide_devops::contract::read_all_config_versions(d.path());
assert!(
versions
.iter()
.any(|(n, v)| n == "package.json" && v.is_none())
);
}
#[test]
fn test_read_config_versions_pubspec_commented() {
let d = tempfile::tempdir().unwrap();
std::fs::write(
d.path().join("pubspec.yaml"),
"# version: 0.1.0\nname: test\n",
)
.unwrap();
let versions = quanttide_devops::contract::read_all_config_versions(d.path());
assert!(
versions
.iter()
.any(|(n, v)| n == "pubspec.yaml" && v.is_none())
);
}
#[test]
fn test_changelog_error_display() {
use quanttide_devops::source::changelog::ChangelogError;
let err = ChangelogError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
assert!(err.to_string().contains("读取 CHANGELOG 失败"));
let err = ChangelogError::Parse("syntax error".into());
assert!(err.to_string().contains("解析 CHANGELOG 失败"));
}
#[test]
fn test_git_version_status_config_no_version() {
let d = tempfile::tempdir().unwrap();
let repo = git2::Repository::init(d.path()).unwrap();
let sig = git2::Signature::now("test", "test@test.com").unwrap();
let tree = {
let mut index = repo.index().unwrap();
let oid = index.write_tree().unwrap();
repo.find_tree(oid).unwrap()
};
repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[])
.unwrap();
repo.tag_lightweight(
"test/v0.1.0",
&repo
.find_object(repo.head().unwrap().target().unwrap(), None)
.unwrap(),
false,
)
.unwrap();
std::fs::write(
d.path().join("Cargo.toml"),
r#"[package]
name = "test"
"#,
)
.unwrap();
let scope = quanttide_devops::contract::Scope {
name: "test".into(),
dir: ".".into(),
language: quanttide_devops::contract::Language::Rust,
build_tool: quanttide_devops::contract::BuildTool::Unknown("auto".into()),
registry: quanttide_devops::contract::Registry::None,
framework: String::new(),
release: quanttide_devops::contract::StageRelease::default(),
test_threshold: None,
ci_workflow: None,
};
let vs = quanttide_devops::source::git::version_status(d.path(), &scope).unwrap();
assert_eq!(vs.tag_version.as_deref(), Some("0.1.0"));
assert!(
vs.config_files
.iter()
.any(|(n, v)| n == "Cargo.toml" && v.is_none())
);
assert!(vs.consistent);
}
#[test]
fn test_scope_deserialize_wrong_type() {
let yaml = "scopes: not_a_map\n";
let result = quanttide_devops::contract::load_from_str(yaml);
assert!(result.is_err());
}
#[test]
fn test_scope_deserialize_type_error() {
let d = tempfile::tempdir().unwrap();
let dir = d.path().join(".quanttide/devops");
std::fs::create_dir_all(&dir).unwrap();
let yaml = r#"
scopes:
cli:
dir:
nested: value
"#;
std::fs::write(dir.join("contract.yaml"), yaml).unwrap();
let err = quanttide_devops::contract::load(d.path()).unwrap_err();
assert!(err.to_string().contains("解析失败") || err.to_string().contains("作用域"));
}
#[test]
fn test_scope_deserialize_missing_dir() {
let d = tempfile::tempdir().unwrap();
let dir = d.path().join(".quanttide/devops");
std::fs::create_dir_all(&dir).unwrap();
let yaml = r#"
scopes:
cli:
language: rust
"#;
std::fs::write(dir.join("contract.yaml"), yaml).unwrap();
let err = quanttide_devops::contract::load(d.path()).unwrap_err();
assert!(err.to_string().contains("dir"));
}
#[test]
fn test_read_config_versions_cargo_empty_version() {
let d = tempfile::tempdir().unwrap();
std::fs::write(
d.path().join("Cargo.toml"),
r#"[package]
name = "test"
version = ""
"#,
)
.unwrap();
let versions = quanttide_devops::contract::read_all_config_versions(d.path());
assert!(
versions
.iter()
.any(|(n, v)| n == "Cargo.toml" && v.is_none())
);
}
#[test]
fn test_read_config_versions_yaml_empty_value() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("pubspec.yaml"), "version: \nname: test\n").unwrap();
let versions = quanttide_devops::contract::read_all_config_versions(d.path());
assert!(
versions
.iter()
.any(|(n, v)| n == "pubspec.yaml" && v.is_none())
);
}