kopi 0.2.3

Kopi is a JDK version management tool
Documentation
use std::ffi::OsString;
use std::sync::Arc;

use kopi::config::LockingConfig;
use kopi::error::KopiError;
use kopi::locking::{
    LockBackend, LockController, LockTimeoutValue, ScopedPackageLockGuard,
    installation_lock_scope_from_package,
};
use kopi::models::api::{Links, Package};
use kopi::platform::{AdvisorySupport, FilesystemInfo, FilesystemInspector, FilesystemKind};
use tempfile::TempDir;

fn sample_package() -> Package {
    Package {
        id: "pkg-id".to_string(),
        archive_type: "tar.gz".to_string(),
        distribution: "Temurin".to_string(),
        major_version: 21,
        java_version: "21.0.2".to_string(),
        distribution_version: "21.0.2".to_string(),
        jdk_version: 21,
        directly_downloadable: true,
        filename: "openjdk.tar.gz".to_string(),
        links: Links {
            pkg_download_redirect: "https://example.com".to_string(),
            pkg_info_uri: Some("https://example.com/info".to_string()),
        },
        free_use_in_production: true,
        tck_tested: "yes".to_string(),
        size: 1024,
        operating_system: "linux".to_string(),
        architecture: Some("x64".to_string()),
        lib_c_type: Some("gnu".to_string()),
        package_type: "JDK".to_string(),
        javafx_bundled: false,
        term_of_support: Some("lts".to_string()),
        release_status: Some("ga".to_string()),
        latest_build_available: Some(true),
    }
}

#[test]
fn installation_lock_serializes_writers() {
    let temp = TempDir::new().unwrap();
    let locking_home = temp.path().to_path_buf();

    let config = LockingConfig::default();
    let controller = LockController::with_default_inspector(locking_home.clone(), &config);

    let package = sample_package();
    let scope = installation_lock_scope_from_package(&package).unwrap();

    let primary = controller.acquire(scope.clone()).unwrap();
    let guard = ScopedPackageLockGuard::new(&controller, primary);

    let second = controller.try_acquire(scope.clone()).unwrap();
    assert!(
        second.is_none(),
        "secondary acquisition should block while lock held"
    );

    drop(guard);

    let reacquired = controller.try_acquire(scope.clone()).unwrap();
    assert!(
        reacquired.is_some(),
        "lock should be available after release"
    );

    if let Some(handle) = reacquired {
        ScopedPackageLockGuard::new(&controller, handle)
            .release()
            .unwrap();
    }
}

#[test]
fn installation_lock_honours_timeout() {
    let temp = TempDir::new().unwrap();
    let locking_home = temp.path().to_path_buf();

    let mut config = LockingConfig::default();
    config.set_timeout_value(LockTimeoutValue::from_secs(0));
    let controller = LockController::with_default_inspector(locking_home, &config);

    let package = sample_package();
    let scope = installation_lock_scope_from_package(&package).unwrap();

    let primary = controller.acquire(scope.clone()).unwrap();
    let guard = ScopedPackageLockGuard::new(&controller, primary);

    let err = controller.acquire(scope.clone()).unwrap_err();
    match err {
        KopiError::LockingTimeout { scope: label, .. } => {
            assert!(label.contains("installation"));
        }
        other => panic!("expected timeout error, got {other:?}"),
    }

    drop(guard);
}

#[derive(Debug)]
struct FallbackInspector;

impl FilesystemInspector for FallbackInspector {
    fn classify(&self, _path: &std::path::Path) -> kopi::error::Result<FilesystemInfo> {
        Ok(FilesystemInfo {
            kind: FilesystemKind::Nfs,
            advisory_support: AdvisorySupport::RequiresFallback,
            is_network_share: true,
        })
    }
}

#[test]
fn fallback_lock_creates_and_cleans_marker() {
    let temp = TempDir::new().unwrap();
    let locking_home = temp.path().to_path_buf();
    let config = LockingConfig::default();

    let inspector: Arc<dyn FilesystemInspector> = Arc::new(FallbackInspector);
    let controller = LockController::new(locking_home.clone(), &config, inspector);

    let package = sample_package();
    let scope = installation_lock_scope_from_package(&package).unwrap();
    let lock_path = scope.lock_path(&locking_home);
    let marker_path = {
        let mut name = OsString::from(lock_path.file_name().unwrap());
        name.push(".marker");
        lock_path.with_file_name(name)
    };

    let acquisition = controller.acquire(scope.clone()).unwrap();
    let guard = ScopedPackageLockGuard::new(&controller, acquisition);
    assert_eq!(guard.backend(), LockBackend::Fallback);
    assert!(lock_path.exists());
    assert!(marker_path.exists());

    guard.release().unwrap();

    assert!(
        !lock_path.exists() && !marker_path.exists(),
        "fallback artifacts for {lock_path:?} should be removed after release"
    );
}