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 {
|
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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?;
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
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 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()
|
||||||
|
|
|
@ -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};
|
||||||
|
|
20
src/plan.rs
20
src/plan.rs
|
@ -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
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(())
|
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(())
|
||||||
// }
|
}
|
||||||
|
|
Loading…
Reference in a new issue