Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add ruin_me powershell tests #862

Merged
merged 2 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh man

$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...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh NO

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

//
// 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