rialoman 0.2.0

Rialo native toolchain manager
Documentation
// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Integration tests for directory layout migration.
//! Old structure:
//! ```text
//! toolchains/
//! ├── stable/0.1.9/
//! ├── commit/abc123/
//! └── rialo-rust-0.0.1/
//! ```
//!
//! New structure:
//! ```text
//! releases/
//! ├── stable/0.1.9/
//! └── commit/abc123/
//! toolchains/
//! └── rialo-rust-0.0.1/
//! ```

use std::fs;

use rialoman::RialoDirs;

/// Fresh install - no migration needed, just creates the layout.
#[test]
fn fresh_install_creates_layout() {
    let tmp = tempfile::tempdir().unwrap();
    let dirs = RialoDirs::new(Some(&tmp.path().to_path_buf())).unwrap();

    dirs.ensure_layout().unwrap();

    assert!(dirs.releases().exists());
    assert!(dirs.toolchains().exists());
    assert!(dirs.bin().exists());
    assert!(dirs.downloads().exists());
    assert!(dirs.tmp().exists());
}

/// Migration from old layout with toolchains/stable/.
#[test]
fn migrate_stable_channel() {
    let tmp = tempfile::tempdir().unwrap();
    let home = tmp.path();

    // Set up old layout: toolchains/stable/0.1.9/
    let old_stable = home.join("toolchains/stable/0.1.9");
    fs::create_dir_all(&old_stable).unwrap();
    fs::write(old_stable.join("manifest.json"), "{}").unwrap();

    let dirs = RialoDirs::new(Some(&home.to_path_buf())).unwrap();
    dirs.ensure_layout().unwrap();

    // Old location should be gone
    assert!(!home.join("toolchains/stable").exists());

    // New location should have the content
    let new_stable = home.join("releases/stable/0.1.9");
    assert!(new_stable.exists());
    assert!(new_stable.join("manifest.json").exists());
}

/// Migration from old layout with toolchains/commit/.
#[test]
fn migrate_commit_channel() {
    let tmp = tempfile::tempdir().unwrap();
    let home = tmp.path();

    // Set up old layout: toolchains/commit/abc123/
    let old_commit = home.join("toolchains/commit/abc123");
    fs::create_dir_all(&old_commit).unwrap();
    fs::write(old_commit.join("manifest.json"), "{}").unwrap();

    let dirs = RialoDirs::new(Some(&home.to_path_buf())).unwrap();
    dirs.ensure_layout().unwrap();

    // Old location should be gone
    assert!(!home.join("toolchains/commit").exists());

    // New location should have the content
    let new_commit = home.join("releases/commit/abc123");
    assert!(new_commit.exists());
    assert!(new_commit.join("manifest.json").exists());
}

/// Migration moves multiple channels.
#[test]
fn migrate_multiple_channels() {
    let tmp = tempfile::tempdir().unwrap();
    let home = tmp.path();

    // Set up old layout with both stable and commit
    fs::create_dir_all(home.join("toolchains/stable/0.1.9")).unwrap();
    fs::create_dir_all(home.join("toolchains/commit/abc123")).unwrap();
    fs::write(home.join("toolchains/stable/0.1.9/marker"), "stable").unwrap();
    fs::write(home.join("toolchains/commit/abc123/marker"), "commit").unwrap();

    let dirs = RialoDirs::new(Some(&home.to_path_buf())).unwrap();
    dirs.ensure_layout().unwrap();

    // Both should be migrated
    assert!(home.join("releases/stable/0.1.9/marker").exists());
    assert!(home.join("releases/commit/abc123/marker").exists());

    // Old locations should be gone
    assert!(!home.join("toolchains/stable").exists());
    assert!(!home.join("toolchains/commit").exists());
}

/// Migration preserves actual toolchains (e.g., rialo-rust/).
#[test]
fn migrate_preserves_actual_toolchains() {
    let tmp = tempfile::tempdir().unwrap();
    let home = tmp.path();

    // Set up old layout with releases AND actual toolchain
    fs::create_dir_all(home.join("toolchains/stable/0.1.9")).unwrap();
    fs::create_dir_all(home.join("toolchains/rialo-rust-0.3.0/bin")).unwrap();
    fs::write(home.join("toolchains/rialo-rust-0.3.0/bin/rustc"), "fake").unwrap();

    let dirs = RialoDirs::new(Some(&home.to_path_buf())).unwrap();
    dirs.ensure_layout().unwrap();

    // Releases should be migrated
    assert!(home.join("releases/stable/0.1.9").exists());
    assert!(!home.join("toolchains/stable").exists());

    // Actual toolchain should be preserved in toolchains/
    assert!(home.join("toolchains/rialo-rust-0.3.0/bin/rustc").exists());
}

/// No migration when releases/ already has the channel.
#[test]
fn no_migration_when_already_migrated() {
    let tmp = tempfile::tempdir().unwrap();
    let home = tmp.path();

    // Set up already-migrated layout
    fs::create_dir_all(home.join("releases/stable/0.1.9")).unwrap();
    fs::write(home.join("releases/stable/0.1.9/marker"), "new").unwrap();

    // Also have old toolchains/ but empty (no channels)
    fs::create_dir_all(home.join("toolchains")).unwrap();

    let dirs = RialoDirs::new(Some(&home.to_path_buf())).unwrap();
    dirs.ensure_layout().unwrap();

    // Content should be unchanged
    let content = fs::read_to_string(home.join("releases/stable/0.1.9/marker")).unwrap();
    assert_eq!(content, "new");
}

/// Migration is idempotent - running twice doesn't break anything.
#[test]
fn migration_is_idempotent() {
    let tmp = tempfile::tempdir().unwrap();
    let home = tmp.path();

    // Set up old layout
    fs::create_dir_all(home.join("toolchains/stable/0.1.9")).unwrap();
    fs::write(home.join("toolchains/stable/0.1.9/marker"), "test").unwrap();

    let dirs = RialoDirs::new(Some(&home.to_path_buf())).unwrap();

    // Run twice
    dirs.ensure_layout().unwrap();
    dirs.ensure_layout().unwrap();

    // Should still work
    assert!(home.join("releases/stable/0.1.9/marker").exists());
    let content = fs::read_to_string(home.join("releases/stable/0.1.9/marker")).unwrap();
    assert_eq!(content, "test");
}

/// Conflict: both old toolchains/stable/ and new releases/stable/ exist.
/// This can happen if user manually created releases/ or partial migration occurred.
/// Expected behavior: fail with error, preserve both directories (no data loss).
#[test]
fn migrate_conflict_both_exist() {
    let tmp = tempfile::tempdir().unwrap();
    let home = tmp.path();

    // Set up conflicting state: both old and new locations have stable/
    fs::create_dir_all(home.join("toolchains/stable/0.1.8")).unwrap();
    fs::write(home.join("toolchains/stable/0.1.8/marker"), "old").unwrap();

    fs::create_dir_all(home.join("releases/stable/0.1.9")).unwrap();
    fs::write(home.join("releases/stable/0.1.9/marker"), "new").unwrap();

    let dirs = RialoDirs::new(Some(&home.to_path_buf())).unwrap();
    let result = dirs.ensure_layout();

    // Should fail with "Directory not empty" io::Error
    let err = result.expect_err("Expected error when both dirs exist");
    let io_err = err
        .chain()
        .find_map(|e| e.downcast_ref::<std::io::Error>())
        .expect("Expected io::Error in chain");
    assert_eq!(io_err.kind(), std::io::ErrorKind::DirectoryNotEmpty);

    // Both directories should be preserved (no data loss)
    assert!(home.join("toolchains/stable/0.1.8/marker").exists());
    assert!(home.join("releases/stable/0.1.9/marker").exists());
}