Files

186 lines
9.1 KiB
Rust

//! Integration tests for the `unlock` command.
use assert_cmd::Command;
use predicates::prelude::*;
fn cargo_command() -> Command {
Command::cargo_bin("unlock").expect("binary")
}
#[test]
#[ignore = "slow Windows file-lock force-delete integration; covered by just stable-test"]
fn reports_file_lockers_and_can_force_delete() {
let binary = assert_cmd::cargo::cargo_bin("unlock");
let script = format!(
"$temp = Join-Path $env:TEMP ('mercury-unlock-test-' + [guid]::NewGuid()); \
New-Item -ItemType Directory -Path $temp | Out-Null; \
$path = Join-Path $temp 'locked.txt'; \
$locker = Join-Path $temp 'locker.ps1'; \
Set-Content -Path $path -Value 'locked'; \
Set-Content -Path $locker -Value \"param([string]`$Path)`n`$stream = [System.IO.File]::Open(`$Path, 'Open', 'ReadWrite', 'None')`nStart-Sleep -Seconds 10\"; \
$child = Start-Process pwsh -ArgumentList '-NoProfile','-File',$locker,$path -PassThru; \
Start-Sleep -Milliseconds 500; \
$delete = & '{}' delete --json --force --wait 0ms $path | ConvertFrom-Json; \
$exists = Test-Path $path; \
Stop-Process -Id $child.Id -Force -ErrorAction SilentlyContinue; \
Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue; \
[pscustomobject]@{{ delete = $delete; exists = $exists }} | ConvertTo-Json -Depth 8 -Compress",
binary.display()
);
let mut command = Command::new("pwsh");
command
.arg("-NoProfile")
.arg("-Command")
.arg(script)
.assert()
.success()
.stdout(predicate::str::contains("\"results\":[{"))
.stdout(predicate::str::contains("\"summary\":{\"results\":1"))
.stdout(predicate::str::contains("\"initial_blockers\""))
.stdout(predicate::str::contains("\"command_line\""))
.stdout(predicate::str::contains("\"action\":\"delete\""))
.stdout(predicate::str::contains("\"ok\":true"))
.stdout(predicate::str::contains("\"exists\":false"));
}
#[test]
fn help_includes_unlock_examples() {
let mut command = cargo_command();
command
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("who"))
.stdout(predicate::str::contains("--deep"))
.stdout(predicate::str::contains("delete"))
.stdout(predicate::str::contains("ConvertFrom-Json"));
}
#[test]
fn who_returns_success_for_unlocked_files() {
let temp = std::env::temp_dir().join(format!(
"unlock-who-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock")
.as_nanos()
));
std::fs::create_dir_all(&temp).expect("tempdir");
let path = temp.join("plain.txt");
std::fs::write(&path, "ready").expect("fixture");
let mut command = cargo_command();
command
.arg("who")
.arg("--json")
.arg(&path)
.assert()
.success()
.stdout(predicate::str::contains("\"summary\":{\"results\":1"))
.stdout(predicate::str::contains("\"ok\":true"))
.stdout(predicate::str::contains("\"final_blockers\":[]"));
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_dir_all(temp);
}
#[cfg(windows)]
#[test]
#[ignore = "slow Windows junction integration; covered by just stable-test"]
fn delete_refuses_junction_directory_without_touching_target() {
let binary = assert_cmd::cargo::cargo_bin("unlock");
let script = format!(
"$temp = Join-Path $env:TEMP ('mercury-unlock-junction-' + [guid]::NewGuid()); \
$target = Join-Path $temp 'target'; \
$junction = Join-Path $temp 'junction'; \
$sentinel = Join-Path $target 'sentinel.txt'; \
try {{ \
New-Item -ItemType Directory -Path $target | Out-Null; \
Set-Content -LiteralPath $sentinel -Value 'keep'; \
New-Item -ItemType Junction -Path $junction -Target $target | Out-Null; \
$json = & '{}' delete --json $junction; \
$exit = $LASTEXITCODE; \
$report = $json | ConvertFrom-Json; \
if ($exit -eq 0) {{ throw 'unlock delete unexpectedly succeeded for a junction directory' }}; \
if (-not (Test-Path -LiteralPath $sentinel)) {{ throw 'junction target sentinel was deleted' }}; \
if (-not (Test-Path -LiteralPath $junction)) {{ throw 'junction itself was removed instead of refused' }}; \
if ($report.results[0].ok -ne $false) {{ throw 'unlock report should mark the junction delete as not ok' }}; \
if ($report.results[0].error -notmatch 'reparse') {{ throw ('expected reparse error, got: ' + $report.results[0].error) }}; \
[pscustomobject]@{{ exit = $exit; sentinel = (Test-Path -LiteralPath $sentinel); junction = (Test-Path -LiteralPath $junction); error = $report.results[0].error }} | ConvertTo-Json -Compress \
}} finally {{ \
if (Test-Path -LiteralPath $junction) {{ Remove-Item -LiteralPath $junction -Force -ErrorAction SilentlyContinue }}; \
if (Test-Path -LiteralPath $temp) {{ Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue }} \
}}",
binary.display()
);
let mut command = Command::new("pwsh");
command
.arg("-NoProfile")
.arg("-Command")
.arg(script)
.assert()
.success()
.stdout(predicate::str::contains("\"sentinel\":true"))
.stdout(predicate::str::contains("\"junction\":true"))
.stdout(predicate::str::contains("reparse"));
}
#[cfg(windows)]
#[test]
#[ignore = "slow Windows junction integration; covered by just stable-test"]
fn copy_move_and_rename_refuse_junction_sources_with_reparse_errors() {
let binary = assert_cmd::cargo::cargo_bin("unlock");
let script = format!(
"$temp = Join-Path $env:TEMP ('mercury-unlock-junction-actions-' + [guid]::NewGuid()); \
$target = Join-Path $temp 'target'; \
$copyJunction = Join-Path $temp 'copy-junction'; \
$moveJunction = Join-Path $temp 'move-junction'; \
$renameJunction = Join-Path $temp 'rename-junction'; \
$copyDest = Join-Path $temp 'copy-dest'; \
$moveDest = Join-Path $temp 'move-dest'; \
$sentinel = Join-Path $target 'sentinel.txt'; \
function Invoke-Refused([string[]]$ArgumentList) {{ \
$json = & '{}' @ArgumentList; \
$exit = $LASTEXITCODE; \
$report = $json | ConvertFrom-Json; \
if ($exit -eq 0) {{ throw ('expected nonzero exit for ' + ($ArgumentList -join ' ')) }}; \
if ($report.results[0].ok -ne $false) {{ throw ('expected ok=false for ' + ($ArgumentList -join ' ')) }}; \
if ($report.results[0].error -notmatch 'reparse') {{ throw ('expected reparse error for ' + ($ArgumentList -join ' ') + ', got: ' + $report.results[0].error) }}; \
return $report.results[0].error \
}}; \
try {{ \
New-Item -ItemType Directory -Path $target | Out-Null; \
Set-Content -LiteralPath $sentinel -Value 'keep'; \
New-Item -ItemType Junction -Path $copyJunction -Target $target | Out-Null; \
New-Item -ItemType Junction -Path $moveJunction -Target $target | Out-Null; \
New-Item -ItemType Junction -Path $renameJunction -Target $target | Out-Null; \
$copyError = Invoke-Refused @('copy','--json',$copyJunction,$copyDest); \
$moveError = Invoke-Refused @('move','--json',$moveJunction,$moveDest); \
$renameError = Invoke-Refused @('rename','--json',$renameJunction,'renamed-junction'); \
if (-not (Test-Path -LiteralPath $sentinel)) {{ throw 'junction target sentinel was deleted' }}; \
if (-not (Test-Path -LiteralPath $copyJunction)) {{ throw 'copy junction was removed' }}; \
if (-not (Test-Path -LiteralPath $moveJunction)) {{ throw 'move junction was removed' }}; \
if (-not (Test-Path -LiteralPath $renameJunction)) {{ throw 'rename junction was removed' }}; \
if (Test-Path -LiteralPath $copyDest) {{ throw 'copy destination was created' }}; \
if (Test-Path -LiteralPath $moveDest) {{ throw 'move destination was created' }}; \
[pscustomobject]@{{ sentinel = (Test-Path -LiteralPath $sentinel); copy = $copyError; move = $moveError; rename = $renameError }} | ConvertTo-Json -Compress \
}} finally {{ \
foreach ($path in @($copyJunction,$moveJunction,$renameJunction)) {{ if (Test-Path -LiteralPath $path) {{ Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue }} }}; \
if (Test-Path -LiteralPath $temp) {{ Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue }} \
}}",
binary.display()
);
let mut command = Command::new("pwsh");
command
.arg("-NoProfile")
.arg("-Command")
.arg(script)
.assert()
.success()
.stdout(predicate::str::contains("\"sentinel\":true"))
.stdout(predicate::str::contains("reparse"));
}