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:
parent
52e8b61009
commit
b29a7585bd
|
@ -43,6 +43,7 @@ impl CommandExecute for NixInstallerCli {
|
|||
|
||||
match subcommand {
|
||||
NixInstallerSubcommand::Plan(plan) => plan.execute().await,
|
||||
NixInstallerSubcommand::SelfTest(self_test) => self_test.execute().await,
|
||||
NixInstallerSubcommand::Install(install) => install.execute().await,
|
||||
NixInstallerSubcommand::Uninstall(revert) => revert.execute().await,
|
||||
}
|
||||
|
|
|
@ -227,7 +227,7 @@ impl CommandExecute for Install {
|
|||
Err(err) => {
|
||||
if !no_confirm {
|
||||
// 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;
|
||||
if let Some(expected) = err.expected() {
|
||||
|
@ -301,7 +301,7 @@ impl CommandExecute for Install {
|
|||
}
|
||||
},
|
||||
Ok(_) => {
|
||||
copy_self_to_nix_store()
|
||||
copy_self_to_nix_dir()
|
||||
.await
|
||||
.wrap_err("Copying `nix-installer` to `/nix/nix-installer`")?;
|
||||
println!(
|
||||
|
@ -335,7 +335,7 @@ impl CommandExecute for Install {
|
|||
}
|
||||
|
||||
#[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()?;
|
||||
tokio::fs::copy(path, "/nix/nix-installer").await?;
|
||||
tokio::fs::set_permissions("/nix/nix-installer", PermissionsExt::from_mode(0o0755)).await?;
|
||||
|
|
|
@ -4,10 +4,13 @@ mod install;
|
|||
use install::Install;
|
||||
mod uninstall;
|
||||
use uninstall::Uninstall;
|
||||
mod self_test;
|
||||
use self_test::SelfTest;
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum NixInstallerSubcommand {
|
||||
Plan(Plan),
|
||||
Install(Install),
|
||||
Uninstall(Uninstall),
|
||||
SelfTest(SelfTest),
|
||||
}
|
||||
|
|
26
src/cli/subcommand/self_test.rs
Normal file
26
src/cli/subcommand/self_test.rs
Normal 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)
|
||||
}
|
||||
}
|
14
src/error.rs
14
src/error.rs
|
@ -2,7 +2,10 @@ use std::{error::Error, path::PathBuf};
|
|||
|
||||
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
|
||||
#[non_exhaustive]
|
||||
|
@ -11,6 +14,13 @@ pub enum NixInstallerError {
|
|||
/// An error originating from an [`Action`](crate::action::Action)
|
||||
#[error("Error executing action")]
|
||||
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
|
||||
#[error("Error reverting\n{}", .0.iter().map(|err| {
|
||||
if let Some(source) = err.source() {
|
||||
|
@ -90,6 +100,7 @@ impl HasExpectedErrors for NixInstallerError {
|
|||
match self {
|
||||
NixInstallerError::Action(action_error) => action_error.kind().expected(),
|
||||
NixInstallerError::ActionRevert(_) => None,
|
||||
NixInstallerError::SelfTest(_) => None,
|
||||
NixInstallerError::RecordingReceipt(_, _) => None,
|
||||
NixInstallerError::CopyingSelf(_) => None,
|
||||
NixInstallerError::SerializingReceipt(_) => None,
|
||||
|
@ -113,6 +124,7 @@ impl crate::diagnostics::ErrorDiagnostic for NixInstallerError {
|
|||
fn diagnostic(&self) -> String {
|
||||
let static_str: &'static str = (self).into();
|
||||
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::ActionRevert(action_errors) => action_errors
|
||||
.iter()
|
||||
|
|
|
@ -78,6 +78,7 @@ mod error;
|
|||
mod os;
|
||||
mod plan;
|
||||
pub mod planner;
|
||||
pub mod self_test;
|
||||
pub mod settings;
|
||||
|
||||
use std::{ffi::OsStr, path::Path, process::Output};
|
||||
|
|
20
src/plan.rs
20
src/plan.rs
|
@ -196,6 +196,26 @@ impl InstallPlan {
|
|||
}
|
||||
|
||||
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")]
|
||||
if let Some(diagnostic_data) = &self.diagnostic_data {
|
||||
diagnostic_data
|
||||
|
|
160
src/self_test.rs
Normal file
160
src/self_test.rs
Normal 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(())
|
||||
}
|
|
@ -25,11 +25,11 @@ fn plan_compat_steam_deck() -> eyre::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// // 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.
|
||||
// #[cfg(target_os = "macos")]
|
||||
// #[test]
|
||||
// fn plan_compat_macos() -> eyre::Result<()> {
|
||||
// let _: InstallPlan = serde_json::from_str(MACOS)?;
|
||||
// Ok(())
|
||||
// }
|
||||
// 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.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn plan_compat_macos() -> eyre::Result<()> {
|
||||
let _: InstallPlan = serde_json::from_str(MACOS)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue