Skip to content

Commit

Permalink
Merge pull request #862 from axodotdev/ruin_ps1
Browse files Browse the repository at this point in the history
chore: add ruin_me powershell tests
  • Loading branch information
Gankra authored Mar 21, 2024
2 parents a10d300 + 5d8a763 commit 158f177
Show file tree
Hide file tree
Showing 28 changed files with 357 additions and 102 deletions.
6 changes: 4 additions & 2 deletions cargo-dist/templates/installer/installer.ps1.j2
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ function Invoke-Installer($bin_paths, $platforms) {
# Returns true if the registry was modified, otherwise returns false
# (indicating it was already on PATH)
function Add-Path($OrigPathToAdd) {
Write-Verbose "Adding $OrigPathToAdd to your PATH"
$RegistryPath = "HKCU:\Environment"
$PropertyName = "Path"
$PathToAdd = $OrigPathToAdd
Expand All @@ -305,21 +306,22 @@ function Add-Path($OrigPathToAdd) {
$PathToAdd = "$PathToAdd;"
} catch {
# We'll be creating the PATH from scratch
Write-Verbose "Adding $PropertyName Property to $RegistryPath"
Write-Verbose "No $PropertyName Property exists on $RegistryPath (we'll make one)"
}

# Check if the path is already there
#
# We don't want to incorrectly match "C:\blah\" to "C:\blah\blah\", so we include the semicolon
# delimiters when searching, ensuring exact matches. To avoid corner cases we add semicolons to
# both sides of the input, allowing us to pretend we're always in the middle of a list.
Write-Verbose "Old $PropertyName Property is $OldPath"
if (";$OldPath;" -like "*;$OrigPathToAdd;*") {
# Already on path, nothing to do
Write-Verbose "install dir already on PATH, all done!"
return $false
} else {
# Actually update PATH
Write-Verbose "Adding $OrigPathToAdd to your PATH"
Write-Verbose "Actually mutating $PropertyName Property"
$NewPath = $PathToAdd + $OldPath
# We use -Force here to make the value already existing not be an error
$Item | New-ItemProperty -Name $PropertyName -Value $NewPath -PropertyType String -Force | Out-Null
Expand Down
297 changes: 249 additions & 48 deletions cargo-dist/tests/gallery/dist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use super::repo::{Repo, TestContext, TestContextLock, ToolsImpl};
///
/// If everything's working right, then no problem.
/// Otherwise MEGA DANGER in messing up your computer.
#[cfg(target_family = "unix")]
#[cfg(any(target_family = "unix", target_family = "windows"))]
const ENV_RUIN_ME: &str = "RUIN_MY_COMPUTER_WITH_INSTALLERS";
/// Set this at runtime to override STATIC_CARGO_DIST_BIN
const ENV_RUNTIME_CARGO_DIST_BIN: &str = "OVERRIDE_CARGO_BIN_EXE_cargo-dist";
Expand Down Expand Up @@ -315,10 +315,13 @@ impl DistResult {
}

pub fn runtests(&self, ctx: &TestContext<Tools>, expected_bin_dir: &str) -> Result<()> {
// If we can, run the script in a temp HOME
// If we can, run the shell script in a temp HOME
self.runtest_shell_installer(ctx, expected_bin_dir)?;

// If we can, run the script in a temp HOME
// If we can, run the powershell script in a temp HOME
self.runtest_powershell_installer(ctx, expected_bin_dir)?;

// If we can, run the homebrew script in a temp HOME
self.runtest_homebrew_installer(ctx)?;

Ok(())
Expand Down Expand Up @@ -366,6 +369,247 @@ impl DistResult {
Ok(())
}

#[cfg(any(target_family = "unix", target_family = "windows"))]
fn check_install_receipt(
&self,
ctx: &TestContext<Tools>,
bin_dir: &Utf8Path,
receipt_file: &Utf8Path,
bin_ext: &str,
) {
// Check that the install receipt works
use serde::Deserialize;

#[derive(Deserialize)]
#[allow(dead_code)]
struct InstallReceipt {
binaries: Vec<String>,
install_prefix: String,
provider: InstallReceiptProvider,
source: InstallReceiptSource,
version: String,
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct InstallReceiptProvider {
source: String,
version: String,
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct InstallReceiptSource {
app_name: String,
name: String,
owner: String,
release_type: String,
}

assert!(receipt_file.exists());
let receipt_src = SourceFile::load_local(receipt_file).expect("couldn't load receipt file");
let receipt: InstallReceipt = receipt_src.deserialize_json().unwrap();
assert_eq!(receipt.source.app_name, ctx.repo.app_name);
assert_eq!(
receipt.binaries,
ctx.repo
.bins
.iter()
.map(|s| format!("{s}{bin_ext}"))
.collect::<Vec<_>>()
);
let receipt_bin_dir = receipt
.install_prefix
.trim_end_matches('/')
.trim_end_matches('\\')
.to_owned();
let expected_bin_dir = bin_dir
.to_string()
.trim_end_matches('/')
.trim_end_matches('\\')
.to_owned();
assert_eq!(receipt_bin_dir, expected_bin_dir);
}

// Runs the installer script in a temp dir, attempting to set env vars to contain it to that dir
#[allow(unused_variables)]
pub fn runtest_powershell_installer(
&self,
ctx: &TestContext<Tools>,
expected_bin_dir: &str,
) -> Result<()> {
// Only do this on windows, and only do it if RUIN_MY_COMPUTER_WITH_INSTALLERS is set
#[cfg(target_family = "windows")]
if std::env::var(ENV_RUIN_ME)
.map(|s| !s.is_empty())
.unwrap_or(false)
{
fn run_ps1_script(
powershell: &CommandInfo,
tempdir: &Utf8Path,
script_name: &str,
script_body: &str,
) -> Result<String> {
let script_path = tempdir.join("test.ps1");
LocalAsset::write_new(script_body, &script_path)?;
let output = powershell.output_checked(|cmd| {
cmd.arg("-c")
.arg(script_path)
.env("UserProfile", &tempdir)
.env_remove("PsModulePath")
})?;
eprintln!("{}", String::from_utf8(output.stderr).unwrap());
Ok(String::from_utf8(output.stdout).unwrap().trim().to_owned())
}

let app_name = ctx.repo.app_name;
let test_name = &self.test_name;

// only do this if the script exists
let Some(shell_path) = &self.powershell_installer_path else {
return Ok(());
};
eprintln!("running installer.ps1...");
let powershell = CommandInfo::new_unchecked("powershell", None);

// Create/clobber a temp dir in target
let repo_dir = &ctx.repo_dir;
let repo_id = &ctx.repo_id;
let parent = repo_dir.parent().unwrap();
let tempdir = parent.join(format!("{repo_id}__{test_name}"));
let appdata = tempdir.join("AppData/Local");
if appdata.exists() {
std::fs::remove_dir_all(&appdata).unwrap();
}
std::fs::create_dir_all(&appdata).unwrap();

// save the current PATH in the registry
let saved_path = run_ps1_script(
&powershell,
&tempdir,
"savepath.ps1",
r#"
$Item = Get-Item -Path "HKCU:\Environment"
$RegPath = $Item | Get-ItemPropertyValue -Name "Path"
return $RegPath
"#,
)?;
assert!(!saved_path.trim().is_empty(), "failed to load path");
eprintln!("backing up PATH: {saved_path}\n");

// on exit, retore the current PATH in the registry, even if we panic
struct RestorePath<'a> {
powershell: &'a CommandInfo,
tempdir: &'a Utf8Path,
saved_path: String,
}
impl Drop for RestorePath<'_> {
fn drop(&mut self) {
let saved_path = &self.saved_path;
eprintln!("restoring PATH: {saved_path}\n");
run_ps1_script(&self.powershell, &self.tempdir, "restorepath.ps1", &format!(r#"
$Item = Get-Item -Path "HKCU:\Environment"
$Item | New-ItemProperty -Name "Path" -Value "{saved_path}" -PropertyType String -Force | Out-Null
"#)).unwrap();
}
}
let _restore = RestorePath {
powershell: &powershell,
tempdir: &tempdir,
saved_path,
};

// Run the installer script with:
//
// UserProfile="{tempdir}" (for install-path=~/... and install-path=CARGO_HOME)
// LOCALAPPDATA="{tempdir}/AppData/Local" (for install receipts)
// MY_ENV_VAR=".{app_name}" (for install-path=$MY_ENV_VAR/...)
// CARGO_HOME=null (cargo test sets this so we have to clear it)
// PSModulePath=null (https://github.com/PowerShell/PowerShell/issues/18530)
let app_home = tempdir.join(format!(".{app_name}"));
let output = powershell.output_checked(|cmd| {
cmd.arg("-c")
.arg(shell_path)
.arg("-Verbose")
.env("UserProfile", &tempdir)
.env("LOCALAPPDATA", &appdata)
.env("MY_ENV_VAR", &app_home)
.env_remove("CARGO_HOME")
.env_remove("PSModulePath")
})?;
eprintln!(
"installer.ps1 stdout:\n{}",
String::from_utf8(output.stdout).unwrap()
);
eprintln!(
"installer.ps1 stderr:\n{}",
String::from_utf8(output.stderr).unwrap()
);
// log the current PATH in the registry
let new_path = run_ps1_script(
&powershell,
&tempdir,
"savepath.ps1",
r#"
$Item = Get-Item -Path "HKCU:\Environment"
$RegPath = $Item | Get-ItemPropertyValue -Name "Path"
return $RegPath
"#,
)?;
assert!(!new_path.trim().is_empty(), "failed to load path");
eprintln!("PATH updated to: {new_path}\n");

// Check that the script wrote files where we expected
let receipt_file = appdata.join(format!("{app_name}\\{app_name}-receipt.json"));
let expected_bin_dir = Utf8PathBuf::from(expected_bin_dir.replace('/', "\\"));
let bin_dir = tempdir.join(&expected_bin_dir);

assert!(bin_dir.exists(), "bin dir wasn't created");

// Check that all the binaries work
for bin_name in ctx.repo.bins {
let bin_path = bin_dir.join(format!("{bin_name}.exe"));
assert!(bin_path.exists(), "{bin_name} wasn't created");

let bin =
CommandInfo::new(bin_name, Some(bin_path.as_str())).expect("failed to run bin");
assert!(bin.version().is_some(), "failed to get app version");

// checking path...
// Make a test.ps1 script that runs `where.exe {bin_name}`
//
// (note that "where" and "where.exe" are completely different things...)
//
// also note that HKCU:\Environment\PATH is not actually the full PATH
// a shell will have, so preprend it to the current PATH (if we don't do
// this then where.exe won't be on PATH anymore!)
let empirical_path = run_ps1_script(
&powershell,
&tempdir,
"test.ps1",
&format!(
r#"
$Item = Get-Item -Path "HKCU:\Environment"
$RegPath = $Item | Get-ItemPropertyValue -Name "Path"
$env:PATH = "$RegPath;$env:PATH"
$Res = where.exe {bin_name}
return $Res
"#
),
)?;
// where.exe will return every matching result, but the one we
// want, the one selected by PATH, should appear first.
assert_eq!(
empirical_path.lines().next().unwrap_or_default(),
bin_path.as_str(),
"{bin_name} path wasn't right"
);
}
// check the install receipts
self.check_install_receipt(ctx, &bin_dir, &receipt_file, ".exe");
eprintln!("installer.ps1 worked!");
}
Ok(())
}

// Runs the installer script in a temp dir, attempting to set env vars to contain it to that dir
#[allow(unused_variables)]
pub fn runtest_shell_installer(
Expand Down Expand Up @@ -481,51 +725,8 @@ impl DistResult {
);
}

// Check that the install receipt works
{
use serde::Deserialize;

#[derive(Deserialize)]
#[allow(dead_code)]
struct InstallReceipt {
binaries: Vec<String>,
install_prefix: String,
provider: InstallReceiptProvider,
source: InstallReceiptSource,
version: String,
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct InstallReceiptProvider {
source: String,
version: String,
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct InstallReceiptSource {
app_name: String,
name: String,
owner: String,
release_type: String,
}

assert!(receipt_file.exists());
let receipt_src =
SourceFile::load_local(receipt_file).expect("couldn't load receipt file");
let receipt: InstallReceipt = receipt_src.deserialize_json().unwrap();
assert_eq!(receipt.source.app_name, app_name);
assert_eq!(
receipt.binaries,
ctx.repo
.bins
.iter()
.map(|s| s.to_owned())
.collect::<Vec<_>>()
);
let receipt_bin_dir = receipt.install_prefix.trim_end_matches('/').to_owned();
let expected_bin_dir = bin_dir.to_string().trim_end_matches('/').to_owned();
assert_eq!(receipt_bin_dir, expected_bin_dir);
}
// Check the install receipts
self.check_install_receipt(ctx, &bin_dir, &receipt_file, "");
}
Ok(())
}
Expand Down
6 changes: 4 additions & 2 deletions cargo-dist/tests/snapshots/akaikatana_basic.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,7 @@ function Invoke-Installer($bin_paths, $platforms) {
# Returns true if the registry was modified, otherwise returns false
# (indicating it was already on PATH)
function Add-Path($OrigPathToAdd) {
Write-Verbose "Adding $OrigPathToAdd to your PATH"
$RegistryPath = "HKCU:\Environment"
$PropertyName = "Path"
$PathToAdd = $OrigPathToAdd
Expand All @@ -1156,21 +1157,22 @@ function Add-Path($OrigPathToAdd) {
$PathToAdd = "$PathToAdd;"
} catch {
# We'll be creating the PATH from scratch
Write-Verbose "Adding $PropertyName Property to $RegistryPath"
Write-Verbose "No $PropertyName Property exists on $RegistryPath (we'll make one)"
}

# Check if the path is already there
#
# We don't want to incorrectly match "C:\blah\" to "C:\blah\blah\", so we include the semicolon
# delimiters when searching, ensuring exact matches. To avoid corner cases we add semicolons to
# both sides of the input, allowing us to pretend we're always in the middle of a list.
Write-Verbose "Old $PropertyName Property is $OldPath"
if (";$OldPath;" -like "*;$OrigPathToAdd;*") {
# Already on path, nothing to do
Write-Verbose "install dir already on PATH, all done!"
return $false
} else {
# Actually update PATH
Write-Verbose "Adding $OrigPathToAdd to your PATH"
Write-Verbose "Actually mutating $PropertyName Property"
$NewPath = $PathToAdd + $OldPath
# We use -Force here to make the value already existing not be an error
$Item | New-ItemProperty -Name $PropertyName -Value $NewPath -PropertyType String -Force | Out-Null
Expand Down
Loading

0 comments on commit 158f177

Please sign in to comment.