use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use kcode_rust_libs::{Error, KcodeRustLibs, RustLibFile, RustLibPath};
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
struct TestDirectory(PathBuf);
impl TestDirectory {
fn new(label: &str) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"kcode-rust-libs-test-{label}-{}-{timestamp}-{counter}",
std::process::id()
));
fs::create_dir(&path).unwrap();
Self(path)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TestDirectory {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn rust_libs(test: &TestDirectory) -> KcodeRustLibs {
KcodeRustLibs::new(test.path().join("rust_libs"), "test-registry-token").unwrap()
}
const SELF_HOSTED_FILES: &[(&str, &str)] = &[
("Cargo.lock", include_str!("../Cargo.lock")),
("Cargo.toml", include_str!("../Cargo.toml")),
("Containerfile", include_str!("../Containerfile")),
("Documentation.md", include_str!("../Documentation.md")),
("Specification.md", include_str!("../Specification.md")),
("src/error.rs", include_str!("../src/error.rs")),
("src/files.rs", include_str!("../src/files.rs")),
("src/lib.rs", include_str!("../src/lib.rs")),
("src/model.rs", include_str!("../src/model.rs")),
("src/path.rs", include_str!("../src/path.rs")),
("src/repository.rs", include_str!("../src/repository.rs")),
("src/sandbox.rs", include_str!("../src/sandbox.rs")),
("tests/library.rs", include_str!("library.rs")),
];
#[test]
fn this_crate_is_a_self_hosted_managed_rust_library() {
let test = TestDirectory::new("self-hosting");
let rust_libs = rust_libs(&test);
let root = test.path().join("rust_libs/kcode-rust-libs");
for &(path, contents) in SELF_HOSTED_FILES {
let destination = root.join(path);
fs::create_dir_all(destination.parent().unwrap()).unwrap();
fs::write(destination, contents).unwrap();
}
let mut rust_lib = rust_libs.open_rust_lib("kcode-rust-libs").unwrap();
assert_eq!(rust_lib.docs().version, env!("CARGO_PKG_VERSION"));
assert_eq!(rust_lib.files().len(), SELF_HOSTED_FILES.len());
assert!(rust_lib.files().iter().any(|file| {
file.path.as_str() == "Cargo.toml" && file.contents.contains("name = \"kcode-rust-libs\"")
}));
rust_lib
.write(&[RustLibFile::new(
RustLibPath::new("tests/agent-maintenance.txt").unwrap(),
"written through kcode-rust-libs\n",
)])
.unwrap();
assert!(
rust_lib
.files()
.iter()
.any(|file| file.path.as_str() == "tests/agent-maintenance.txt")
);
}
#[test]
fn creates_opens_documents_and_overwrites_complete_files() {
let test = TestDirectory::new("files");
let rust_libs = rust_libs(&test);
let mut rust_lib = rust_libs.create_rust_lib("calculator").unwrap();
assert_eq!(rust_lib.name(), "calculator");
assert_eq!(rust_lib.docs().version, "0.1.0");
assert_eq!(rust_lib.docs().documentation, "");
assert_eq!(
rust_lib
.files()
.iter()
.map(|file| file.path.as_str())
.collect::<Vec<_>>(),
["Cargo.toml", "Documentation.md", "src/lib.rs"]
);
rust_lib
.write(&[
RustLibFile::new(
RustLibPath::new("Documentation.md").unwrap(),
"Call `value()` to obtain the answer.\n",
),
RustLibFile::new(
RustLibPath::new("src/lib.rs").unwrap(),
"pub fn value() -> u8 { 42 }\n",
),
RustLibFile::new(
RustLibPath::new("tests/value.rs").unwrap(),
"#[test]\nfn value_is_42() {}\n",
),
])
.unwrap();
assert_eq!(
rust_lib.docs().documentation,
"Call `value()` to obtain the answer.\n"
);
let source = rust_lib
.files()
.iter()
.find(|file| file.path.as_str() == "src/lib.rs")
.unwrap();
assert_eq!(source.contents, "pub fn value() -> u8 { 42 }\n");
drop(rust_lib);
let reopened = rust_libs.open_rust_lib("calculator").unwrap();
assert_eq!(reopened.files().len(), 4);
assert_eq!(
reopened
.files()
.iter()
.map(|file| file.path.as_str())
.collect::<Vec<_>>(),
[
"Cargo.toml",
"Documentation.md",
"src/lib.rs",
"tests/value.rs"
]
);
}
#[test]
fn rust_libs_root_contains_libraries_but_not_registry_credentials() {
let test = TestDirectory::new("backup");
let rust_libs = rust_libs(&test);
rust_libs.create_rust_lib("one").unwrap();
rust_libs.create_rust_lib("two").unwrap();
let mut entries = fs::read_dir(test.path().join("rust_libs"))
.unwrap()
.map(|entry| entry.unwrap().file_name().into_string().unwrap())
.collect::<Vec<_>>();
entries.sort();
assert_eq!(entries, ["one", "two"]);
assert!(
!test
.path()
.join("rust_libs/cargo_registry_token.txt")
.exists()
);
assert!(!test.path().join("rust_libs/target").exists());
}
#[test]
fn rejects_empty_registry_tokens_before_creating_storage() {
for token in ["", " \n\t"] {
let test = TestDirectory::new("empty-token");
let root = test.path().join("rust_libs");
let result = KcodeRustLibs::new(&root, token);
assert!(matches!(result, Err(Error::InvalidRegistryToken)));
assert!(!root.exists());
}
}
#[test]
fn debug_output_redacts_the_registry_token() {
let test = TestDirectory::new("debug-token");
let rust_libs = KcodeRustLibs::new(test.path().join("rust_libs"), "debug-secret").unwrap();
let debug = format!("{rust_libs:?}");
assert!(!debug.contains("debug-secret"));
assert!(debug.contains("[REDACTED]"));
}
#[test]
fn rejects_bad_names() {
let test = TestDirectory::new("validation");
let rust_libs = rust_libs(&test);
for name in ["", "../outside", "with/slash", "two words", "_leading"] {
assert!(matches!(
rust_libs.create_rust_lib(name),
Err(Error::InvalidRustLibName(_))
));
}
}
#[test]
fn requires_documentation_and_a_canonical_manifest_version() {
let test = TestDirectory::new("metadata");
let rust_libs = rust_libs(&test);
let mut rust_lib = rust_libs.create_rust_lib("demo").unwrap();
let result = rust_lib.write(&[
RustLibFile::new(RustLibPath::new("src/lib.rs").unwrap(), "changed"),
RustLibFile::new(
RustLibPath::new("Cargo.toml").unwrap(),
"[package]\nname = \"demo\"\nversion = \"1.0.0-beta.1\"\nedition = \"2024\"\n",
),
]);
assert!(matches!(result, Err(Error::InvalidVersion(_))));
assert_eq!(
fs::read_to_string(test.path().join("rust_libs/demo/src/lib.rs")).unwrap(),
""
);
fs::remove_file(test.path().join("rust_libs/demo/Documentation.md")).unwrap();
assert!(matches!(
rust_lib.write(&[RustLibFile::new(
RustLibPath::new("src/lib.rs").unwrap(),
"changed"
)]),
Err(Error::MissingRequiredFile("Documentation.md"))
));
assert_eq!(
fs::read_to_string(test.path().join("rust_libs/demo/src/lib.rs")).unwrap(),
""
);
drop(rust_lib);
assert!(matches!(
rust_libs.open_rust_lib("demo"),
Err(Error::MissingRequiredFile("Documentation.md"))
));
}
#[test]
fn updating_the_manifest_updates_the_public_version() {
let test = TestDirectory::new("manifest-version");
let rust_libs = rust_libs(&test);
let mut rust_lib = rust_libs.create_rust_lib("demo").unwrap();
rust_lib
.write(&[RustLibFile::new(
RustLibPath::new("Cargo.toml").unwrap(),
"[package]\nname = \"demo\"\nversion = \"1.2.3\"\nedition = \"2024\"\n",
)])
.unwrap();
assert_eq!(rust_lib.docs().version, "1.2.3");
}
#[test]
fn rejects_duplicate_write_paths_before_writing() {
let test = TestDirectory::new("duplicates");
let rust_libs = rust_libs(&test);
let mut rust_lib = rust_libs.create_rust_lib("demo").unwrap();
let path = RustLibPath::new("src/lib.rs").unwrap();
let result = rust_lib.write(&[
RustLibFile::new(path.clone(), "first"),
RustLibFile::new(path, "second"),
]);
assert!(matches!(result, Err(Error::DuplicateWritePath(_))));
assert_eq!(
fs::read_to_string(test.path().join("rust_libs/demo/src/lib.rs")).unwrap(),
""
);
}
#[cfg(unix)]
#[test]
fn rejects_symlinks_while_opening_and_writing() {
use std::os::unix::fs::symlink;
let test = TestDirectory::new("symlinks");
let rust_libs = rust_libs(&test);
let mut rust_lib = rust_libs.create_rust_lib("demo").unwrap();
let outside = test.path().join("outside");
fs::create_dir(&outside).unwrap();
symlink(&outside, test.path().join("rust_libs/demo/link")).unwrap();
let result = rust_lib.write(&[RustLibFile::new(
RustLibPath::new("link/file.rs").unwrap(),
"outside",
)]);
assert!(matches!(result, Err(Error::SymlinkNotAllowed(_))));
assert!(!outside.join("file.rs").exists());
drop(rust_lib);
assert!(matches!(
rust_libs.open_rust_lib("demo"),
Err(Error::SymlinkNotAllowed(_))
));
}