feat: use isproj-only project files and local settings

Closes #6
This commit is contained in:
jychoi4 committed 2026-11-06 14:46:00 +09:00
1 parent 3071e0d1ef
commit 916b79e5d6
12 files changed
+230 -40

No files matched your search

@@ -0,0 +1,145 @@
use package_infra as infra;
use std::fs;
#[test]
fn new_projects_normalize_extension_and_roundtrip_local_settings_and_backups() {
let temp = tempfile::tempdir().unwrap();
let mut doc = infra::create_project(&temp.path().join("한글 프로젝트")).unwrap();
assert!(doc.path.ends_with("한글 프로젝트.isproj"));
let original = fs::read(&doc.path).unwrap();
assert!(serde_json::from_slice::<serde_json::Value>(&original)
.unwrap()
.get("projectId")
.is_some());
doc.project.product.display_name = "변경된 제품".into();
doc.local
.sources
.insert("appPayload".into(), "payload".into());
doc.local.developer_certificate_thumbprint = Some("A".repeat(40));
let saved = infra::save_project(&doc).unwrap();
assert_eq!(saved.local.sources["appPayload"], "payload");
assert_eq!(
saved.local.developer_certificate_thumbprint,
doc.local.developer_certificate_thumbprint
);
assert_eq!(
saved.project.formats.msix.identity.name,
doc.project.formats.msix.identity.name
);
assert_eq!(
saved.project.formats.msix.identity.publisher,
doc.project.formats.msix.identity.publisher
);
assert_eq!(
fs::read(infra::appended_path(&doc.path, ".bak")).unwrap(),
original
);
let local_path = infra::local_config_path(&doc.path);
assert!(local_path.ends_with("한글 프로젝트.isproj.local.json"));
let local_bytes = fs::read(&local_path).unwrap();
infra::save_project(&saved).unwrap();
assert_eq!(
fs::read(infra::appended_path(&local_path, ".bak")).unwrap(),
local_bytes
);
assert!(!temp.path().join("한글 프로젝트.json.bak").exists());
}
#[test]
fn json_and_duplicate_extensions_are_rejected_even_with_valid_contents() {
let temp = tempfile::tempdir().unwrap();
let doc = infra::create_project(&temp.path().join("Sample.ISPROJ")).unwrap();
assert!(infra::open_project(&doc.path).is_ok());
for name in [
"legacy.json",
"bad.txt",
"duplicate.isproj.isproj",
"no-extension",
] {
let path = temp.path().join(name);
fs::copy(&doc.path, &path).unwrap();
assert!(infra::open_project(&path)
.unwrap_err()
.to_string()
.contains("UnsupportedProjectExtension"));
if name != "no-extension" {
assert!(infra::create_project(&path)
.unwrap_err()
.to_string()
.contains("UnsupportedProjectExtension"));
}
}
assert!(infra::create_project(&doc.path).is_err());
fs::write(temp.path().join("broken.isproj"), "not json").unwrap();
assert!(infra::open_project(&temp.path().join("broken.isproj")).is_err());
}
#[test]
fn no_legacy_local_fallback_and_no_overwrite_of_orphan_local_settings() {
let temp = tempfile::tempdir().unwrap();
fs::write(
temp.path().join("package-project.local.json"),
r#"{"sources":{"legacy":"do-not-load"},"makeAppx":null}"#,
)
.unwrap();
let doc = infra::create_project(&temp.path().join("fresh.isproj")).unwrap();
assert!(doc.local.sources.is_empty());
let path = temp.path().join("orphan.isproj");
fs::write(infra::local_config_path(&path), "keep").unwrap();
assert!(infra::create_project(&path)
.unwrap_err()
.to_string()
.contains("LocalConfigAlreadyExists"));
assert!(!path.exists());
}
#[test]
fn local_revision_conflicts_and_failed_backup_preserve_both_files() {
let temp = tempfile::tempdir().unwrap();
let doc = infra::create_project(&temp.path().join("sample.isproj")).unwrap();
let mut doc = infra::save_project(&doc).unwrap();
let local_path = infra::local_config_path(&doc.path);
let original = fs::read(&doc.path).unwrap();
let local = fs::read(&local_path).unwrap();
fs::write(&local_path, b"{\"sources\":{},\"makeAppx\":null}\n").unwrap();
assert!(infra::save_project(&doc)
.unwrap_err()
.to_string()
.contains("RevisionConflict"));
fs::write(&local_path, &local).unwrap();
fs::create_dir(infra::appended_path(&local_path, ".bak")).unwrap();
doc.project.product.display_name = "must not commit".into();
doc.local
.sources
.insert("appPayload".into(), "changed".into());
assert!(infra::save_project(&doc).is_err());
assert_eq!(fs::read(&doc.path).unwrap(), original);
assert_eq!(fs::read(&local_path).unwrap(), local);
}
#[cfg(windows)]
#[test]
fn failed_project_replacement_restores_previous_local_settings() {
use std::os::windows::fs::OpenOptionsExt;
let temp = tempfile::tempdir().unwrap();
let doc = infra::create_project(&temp.path().join("locked.isproj")).unwrap();
let mut doc = infra::save_project(&doc).unwrap();
let local_path = infra::local_config_path(&doc.path);
let project_before = fs::read(&doc.path).unwrap();
let local_before = fs::read(&local_path).unwrap();
// Permit reading and writing, but hold back FILE_SHARE_DELETE so replacement fails.
let _held = fs::OpenOptions::new()
.read(true)
.share_mode(3)
.open(&doc.path)
.unwrap();
doc.project.product.display_name = "must roll back".into();
doc.local
.sources
.insert("appPayload".into(), "changed".into());
assert!(infra::save_project(&doc).is_err());
assert_eq!(fs::read(&doc.path).unwrap(), project_before);
assert_eq!(fs::read(local_path).unwrap(), local_before);
}
+1 -1
View File
@@ -18,7 +18,7 @@ fn png(path: &Path, size: u32) {
}
fn fixture() -> (tempfile::TempDir, PathBuf) {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("package-project.json");
let path = temp.path().join("InstallerProject.isproj");
let mut doc = infra::create_project(&path).unwrap();
let payload = temp.path().join("payload");
fs::create_dir_all(payload.join("Assets")).unwrap();