provenance-cli 0.2.3

Command-line interface for Provenance requirements traceability
use super::*;

#[test]
fn output_replacement_race_preserves_the_intervening_tree() {
    let temp = tempfile::tempdir().unwrap();
    let output = utf8(temp.path().join("wiki"));
    let original = utf8(temp.path().join("original-wiki"));
    publish(&empty_corpus(), PublicationOutput::custom(output.clone())).unwrap();

    let error = publish_with(
        &empty_corpus(),
        PublicationOutput::custom(output.clone()),
        |_stage| {
            std::fs::rename(&output, &original)?;
            std::fs::create_dir(&output)?;
            std::fs::copy(
                original.join(OWNERSHIP_MANIFEST),
                output.join(OWNERSHIP_MANIFEST),
            )?;
            std::fs::write(output.join("caller.txt"), "keep me")?;
            Ok(())
        },
    )
    .unwrap_err();

    assert!(matches!(error, PublishError::OutputChanged { .. }));
    assert_eq!(
        std::fs::read_to_string(output.join("caller.txt")).unwrap(),
        "keep me"
    );
    assert!(original.join("index.html").is_file());
    assert!(!artifact(&output, "stage").exists());
}

#[test]
fn a_returned_install_failure_restores_the_previous_output() {
    let temp = tempfile::tempdir().unwrap();
    let output = utf8(temp.path().join("wiki"));
    let paths = TransactionPaths::new(&output).unwrap();
    std::fs::create_dir(&output).unwrap();
    std::fs::write(output.join("generation"), "old").unwrap();
    std::fs::create_dir(&paths.stage).unwrap();
    std::fs::write(paths.stage.join("generation"), "new").unwrap();
    let mut rename_count = 0;

    let error = replace_output_with(
        &output,
        &paths,
        |_, _| {
            rename_count += 1;
            if rename_count == 2 {
                Err(std::io::Error::other("injected second rename failure"))
            } else {
                Ok(())
            }
        },
        |_| Ok(()),
    )
    .unwrap_err();

    assert!(matches!(error, PublishError::ReplacementRolledBack { .. }));
    assert_eq!(
        std::fs::read_to_string(output.join("generation")).unwrap(),
        "old"
    );
    assert_eq!(
        std::fs::read_to_string(paths.stage.join("generation")).unwrap(),
        "new"
    );
    assert!(!paths.backup.exists());
}

#[test]
fn output_appearance_between_renames_preserves_every_tree() {
    let temp = tempfile::tempdir().unwrap();
    let output = utf8(temp.path().join("wiki"));
    let paths = TransactionPaths::new(&output).unwrap();
    std::fs::create_dir(&output).unwrap();
    std::fs::write(output.join("generation"), "old").unwrap();
    std::fs::create_dir(&paths.stage).unwrap();
    std::fs::write(paths.stage.join("generation"), "new").unwrap();
    let mut rename_count = 0;

    let error = replace_output_with(
        &output,
        &paths,
        |_, _| {
            rename_count += 1;
            if rename_count == 2 {
                std::fs::create_dir(&output)?;
            }
            Ok(())
        },
        |_| Ok(()),
    )
    .unwrap_err();

    assert!(matches!(error, PublishError::RollbackFailed { .. }));
    assert!(output.is_dir());
    assert_eq!(
        std::fs::read_to_string(paths.backup.join("generation")).unwrap(),
        "old"
    );
    assert_eq!(
        std::fs::read_to_string(paths.stage.join("generation")).unwrap(),
        "new"
    );
}

#[test]
fn committed_cleanup_failure_is_a_warning_not_a_returned_failure() {
    let temp = tempfile::tempdir().unwrap();
    let output = utf8(temp.path().join("wiki"));
    let paths = TransactionPaths::new(&output).unwrap();
    std::fs::create_dir(&output).unwrap();
    std::fs::write(output.join("generation"), "old").unwrap();
    std::fs::create_dir(&paths.stage).unwrap();
    std::fs::write(paths.stage.join("generation"), "new").unwrap();

    let warnings = replace_output_with(
        &output,
        &paths,
        |_, _| Ok(()),
        |_path| Err(std::io::Error::other("injected cleanup failure")),
    )
    .unwrap();

    assert_eq!(warnings.len(), 1);
    assert_eq!(
        std::fs::read_to_string(output.join("generation")).unwrap(),
        "new"
    );
    assert_eq!(
        std::fs::read_to_string(paths.backup.join("generation")).unwrap(),
        "old"
    );
}

#[test]
fn backup_cleanup_race_preserves_the_replacement_tree() {
    let temp = tempfile::tempdir().unwrap();
    let output = utf8(temp.path().join("wiki"));
    let paths = TransactionPaths::new(&output).unwrap();
    let displaced_backup = utf8(temp.path().join("displaced-backup"));
    std::fs::create_dir(&output).unwrap();
    std::fs::write(output.join("generation"), "old").unwrap();
    std::fs::create_dir(&paths.stage).unwrap();
    std::fs::write(paths.stage.join("generation"), "new").unwrap();

    let warnings = replace_output_with(
        &output,
        &paths,
        |_, _| Ok(()),
        |path| {
            std::fs::rename(path, &displaced_backup)?;
            std::fs::create_dir(path)?;
            std::fs::write(path.join("caller"), "keep me")
        },
    )
    .unwrap();

    assert_eq!(warnings.len(), 1);
    assert_eq!(
        std::fs::read_to_string(paths.backup.join("caller")).unwrap(),
        "keep me"
    );
    assert_eq!(
        std::fs::read_to_string(displaced_backup.join("generation")).unwrap(),
        "old"
    );
}