Add self test functionality (#506)

* Add self test functionality

* Fix mac ci

* Improve erorr messaging

* i32 support

* Fixup self-test comment

* Fix review nits
This commit is contained in:
Ana Hobden 2023-06-08 08:09:04 -07:00 committed by GitHub
parent 52e8b61009
commit b29a7585bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 235 additions and 12 deletions

View file

@ -43,6 +43,7 @@ impl CommandExecute for NixInstallerCli {
match subcommand { match subcommand {
NixInstallerSubcommand::Plan(plan) => plan.execute().await, NixInstallerSubcommand::Plan(plan) => plan.execute().await,
NixInstallerSubcommand::SelfTest(self_test) => self_test.execute().await,
NixInstallerSubcommand::Install(install) => install.execute().await, NixInstallerSubcommand::Install(install) => install.execute().await,
NixInstallerSubcommand::Uninstall(revert) => revert.execute().await, NixInstallerSubcommand::Uninstall(revert) => revert.execute().await,
} }

View file

@ -227,7 +227,7 @@ impl CommandExecute for Install {
Err(err) => { Err(err) => {
if !no_confirm { if !no_confirm {
// Attempt to copy self to the store if possible, but since the install failed, this might not work, that's ok. // Attempt to copy self to the store if possible, but since the install failed, this might not work, that's ok.
copy_self_to_nix_store().await.ok(); copy_self_to_nix_dir().await.ok();
let mut was_expected = false; let mut was_expected = false;
if let Some(expected) = err.expected() { if let Some(expected) = err.expected() {
@ -301,7 +301,7 @@ impl CommandExecute for Install {
} }
}, },
Ok(_) => { Ok(_) => {
copy_self_to_nix_store() copy_self_to_nix_dir()
.await .await
.wrap_err("Copying `nix-installer` to `/nix/nix-installer`")?; .wrap_err("Copying `nix-installer` to `/nix/nix-installer`")?;
println!( println!(
@ -335,7 +335,7 @@ impl CommandExecute for Install {
} }
#[tracing::instrument(level = "debug")] #[tracing::instrument(level = "debug")]
async fn copy_self_to_nix_store() -> Result<(), std::io::Error> { async fn copy_self_to_nix_dir() -> Result<(), std::io::Error> {
let path = std::env::current_exe()?; let path = std::env::current_exe()?;
tokio::fs::copy(path, "/nix/nix-installer").await?; tokio::fs::copy(path, "/nix/nix-installer").await?;
tokio::fs::set_permissions("/nix/nix-installer", PermissionsExt::from_mode(0o0755)).await?; tokio::fs::set_permissions("/nix/nix-installer", PermissionsExt::from_mode(0o0755)).await?;

View file

@ -4,10 +4,13 @@ mod install;
use install::Install; use install::Install;
mod uninstall; mod uninstall;
use uninstall::Uninstall; use uninstall::Uninstall;
mod self_test;
use self_test::SelfTest;
#[derive(Debug, clap::Subcommand)] #[derive(Debug, clap::Subcommand)]
pub enum NixInstallerSubcommand { pub enum NixInstallerSubcommand {
Plan(Plan), Plan(Plan),
Install(Install), Install(Install),
Uninstall(Uninstall), Uninstall(Uninstall),
SelfTest(SelfTest),
} }

View file

@ -0,0 +1,26 @@
use std::process::ExitCode;
use clap::Parser;
use crate::cli::CommandExecute;
/// Run a self test of Nix to ensure that the install worked.
#[derive(Debug, Parser)]
pub struct SelfTest {}
#[async_trait::async_trait]
impl CommandExecute for SelfTest {
#[tracing::instrument(level = "debug", skip_all, fields())]
async fn execute(self) -> eyre::Result<ExitCode> {
crate::self_test::self_test().await?;
tracing::info!(
shells = ?crate::self_test::Shell::discover()
.iter()
.map(|v| v.executable())
.collect::<Vec<_>>(),
"Successfully tested Nix install in all discovered shells."
);
Ok(ExitCode::SUCCESS)
}
}

View file

@ -2,7 +2,10 @@ use std::{error::Error, path::PathBuf};
use semver::Version; use semver::Version;
use crate::{action::ActionError, planner::PlannerError, settings::InstallSettingsError}; use crate::{
action::ActionError, planner::PlannerError, self_test::SelfTestError,
settings::InstallSettingsError,
};
/// An error occurring during a call defined in this crate /// An error occurring during a call defined in this crate
#[non_exhaustive] #[non_exhaustive]
@ -11,6 +14,13 @@ pub enum NixInstallerError {
/// An error originating from an [`Action`](crate::action::Action) /// An error originating from an [`Action`](crate::action::Action)
#[error("Error executing action")] #[error("Error executing action")]
Action(#[source] ActionError), Action(#[source] ActionError),
/// An error originating from a [`self_test`](crate::self_test)
#[error("Self test")]
SelfTest(
#[source]
#[from]
SelfTestError,
),
/// An error originating from an [`Action`](crate::action::Action) while reverting /// An error originating from an [`Action`](crate::action::Action) while reverting
#[error("Error reverting\n{}", .0.iter().map(|err| { #[error("Error reverting\n{}", .0.iter().map(|err| {
if let Some(source) = err.source() { if let Some(source) = err.source() {
@ -90,6 +100,7 @@ impl HasExpectedErrors for NixInstallerError {
match self { match self {
NixInstallerError::Action(action_error) => action_error.kind().expected(), NixInstallerError::Action(action_error) => action_error.kind().expected(),
NixInstallerError::ActionRevert(_) => None, NixInstallerError::ActionRevert(_) => None,
NixInstallerError::SelfTest(_) => None,
NixInstallerError::RecordingReceipt(_, _) => None, NixInstallerError::RecordingReceipt(_, _) => None,
NixInstallerError::CopyingSelf(_) => None, NixInstallerError::CopyingSelf(_) => None,
NixInstallerError::SerializingReceipt(_) => None, NixInstallerError::SerializingReceipt(_) => None,
@ -113,6 +124,7 @@ impl crate::diagnostics::ErrorDiagnostic for NixInstallerError {
fn diagnostic(&self) -> String { fn diagnostic(&self) -> String {
let static_str: &'static str = (self).into(); let static_str: &'static str = (self).into();
let context = match self { let context = match self {
Self::SelfTest(self_test) => vec![self_test.diagnostic().to_string()],
Self::Action(action_error) => vec![action_error.diagnostic().to_string()], Self::Action(action_error) => vec![action_error.diagnostic().to_string()],
Self::ActionRevert(action_errors) => action_errors Self::ActionRevert(action_errors) => action_errors
.iter() .iter()

View file

@ -78,6 +78,7 @@ mod error;
mod os; mod os;
mod plan; mod plan;
pub mod planner; pub mod planner;
pub mod self_test;
pub mod settings; pub mod settings;
use std::{ffi::OsStr, path::Path, process::Output}; use std::{ffi::OsStr, path::Path, process::Output};

View file

@ -196,6 +196,26 @@ impl InstallPlan {
} }
write_receipt(self.clone()).await?; write_receipt(self.clone()).await?;
if let Err(err) = crate::self_test::self_test()
.await
.map_err(NixInstallerError::SelfTest)
{
#[cfg(feature = "diagnostics")]
if let Some(diagnostic_data) = &self.diagnostic_data {
diagnostic_data
.clone()
.failure(&err)
.send(
crate::diagnostics::DiagnosticAction::Install,
crate::diagnostics::DiagnosticStatus::Failure,
)
.await?;
}
return Err(err);
}
#[cfg(feature = "diagnostics")] #[cfg(feature = "diagnostics")]
if let Some(diagnostic_data) = &self.diagnostic_data { if let Some(diagnostic_data) = &self.diagnostic_data {
diagnostic_data diagnostic_data

160
src/self_test.rs Normal file
View file

@ -0,0 +1,160 @@
use std::{process::Output, time::SystemTime};
use tokio::process::Command;
use which::which;
#[non_exhaustive]
#[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
pub enum SelfTestError {
#[error("Shell `{shell}` failed self-test with command `{command}`, stderr:\n{}", String::from_utf8_lossy(&output.stderr))]
ShellFailed {
shell: Shell,
command: String,
output: Output,
},
/// Failed to execute command
#[error("Failed to execute command `{command}`",
command = .command,
)]
Command {
shell: Shell,
command: String,
#[source]
error: std::io::Error,
},
#[error(transparent)]
SystemTime(#[from] std::time::SystemTimeError),
}
#[cfg(feature = "diagnostics")]
impl crate::diagnostics::ErrorDiagnostic for SelfTestError {
fn diagnostic(&self) -> String {
let static_str: &'static str = (self).into();
let context = match self {
Self::ShellFailed { shell, .. } => vec![shell.to_string()],
Self::Command { shell, .. } => vec![shell.to_string()],
Self::SystemTime(_) => vec![],
};
return format!(
"{}({})",
static_str,
context
.iter()
.map(|v| format!("\"{v}\""))
.collect::<Vec<_>>()
.join(", ")
);
}
}
#[derive(Clone, Copy, Debug)]
pub enum Shell {
Sh,
Bash,
Fish,
Zsh,
}
impl std::fmt::Display for Shell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.executable())
}
}
impl Shell {
pub fn all() -> &'static [Shell] {
&[Shell::Sh, Shell::Bash, Shell::Fish, Shell::Zsh]
}
pub fn executable(&self) -> &'static str {
match &self {
Shell::Sh => "sh",
Shell::Bash => "bash",
Shell::Fish => "fish",
Shell::Zsh => "zsh",
}
}
#[tracing::instrument(skip_all)]
pub async fn self_test(&self) -> Result<(), SelfTestError> {
let executable = self.executable();
let mut command = match &self {
// On Mac, `bash -ic nix` won't work, but `bash -lc nix` will.
Shell::Sh | Shell::Bash => {
let mut command = Command::new(executable);
command.arg("-lc");
command
},
Shell::Zsh | Shell::Fish => {
let mut command = Command::new(executable);
command.arg("-ic");
command
},
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SYSTEM: &str = "x86_64-linux";
#[cfg(all(target_os = "linux", target_arch = "x86"))]
const SYSTEM: &str = "x86-linux";
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
const SYSTEM: &str = "aarch64-linux";
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
const SYSTEM: &str = "x86_64-darwin";
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
const SYSTEM: &str = "aarch64-darwin";
let timestamp_millis = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)?
.as_millis();
command.arg(format!(
r#"nix build --no-link --expr 'derivation {{ name = "self-test-{executable}-{timestamp_millis}"; system = "{SYSTEM}"; builder = "/bin/sh"; args = ["-c" "echo hello > \$out"]; }}'"#
));
let command_str = format!("{:?}", command.as_std());
tracing::debug!(
command = command_str,
"Testing Nix install via `{executable}`"
);
let output = command
.output()
.await
.map_err(|error| SelfTestError::Command {
shell: *self,
command: command_str.clone(),
error,
})?;
if output.status.success() {
Ok(())
} else {
Err(SelfTestError::ShellFailed {
shell: *self,
command: command_str,
output,
})
}
}
#[tracing::instrument(skip_all)]
pub fn discover() -> Vec<Shell> {
let mut found_shells = vec![];
for shell in Self::all() {
if which(shell.executable()).is_ok() {
tracing::debug!("Discovered `{shell}`");
found_shells.push(*shell)
}
}
found_shells
}
}
#[tracing::instrument(skip_all)]
pub async fn self_test() -> Result<(), SelfTestError> {
let shells = Shell::discover();
for shell in shells {
shell.self_test().await?;
}
Ok(())
}

View file

@ -25,11 +25,11 @@ fn plan_compat_steam_deck() -> eyre::Result<()> {
Ok(()) Ok(())
} }
// // Ensure existing plans still parse // Ensure existing plans still parse
// // If this breaks and you need to update the fixture, disable these tests, bump `nix_installer` to a new version, and update the plans. // If this breaks and you need to update the fixture, disable these tests, bump `nix_installer` to a new version, and update the plans.
// #[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
// #[test] #[test]
// fn plan_compat_macos() -> eyre::Result<()> { fn plan_compat_macos() -> eyre::Result<()> {
// let _: InstallPlan = serde_json::from_str(MACOS)?; let _: InstallPlan = serde_json::from_str(MACOS)?;
// Ok(()) Ok(())
// } }