Skip to content

Commit

Permalink
chore: break up ExtensionError into method-specific error types (#3740)
Browse files Browse the repository at this point in the history
  • Loading branch information
ericswanson-dfinity authored May 6, 2024
1 parent 41378d5 commit 36c7352
Show file tree
Hide file tree
Showing 12 changed files with 263 additions and 145 deletions.
30 changes: 20 additions & 10 deletions e2e/tests-dfx/extension.bash
Original file line number Diff line number Diff line change
Expand Up @@ -216,36 +216,46 @@ echo testoutput' > "$CACHE_DIR"/extensions/test_extension/test_extension
chmod +x "$CACHE_DIR"/extensions/test_extension/test_extension

assert_command_fail dfx extension list
assert_match "Error.*Cannot load extension manifest.*Failed to read JSON file.*Failed to read .*extensions/test_extension/extension.json.*No such file or directory"
assert_match "Error.*Failed to load extension manifest.*Failed to read JSON file.*Failed to read .*extensions/test_extension/extension.json.*No such file or directory"

assert_command_fail dfx extension run test_extension
assert_match "Error.*Cannot load extension manifest.*Failed to read JSON file.*Failed to read .*extensions/test_extension/extension.json.*No such file or directory"
assert_match "Error.*Failed to load extension manifest.*Failed to read JSON file.*Failed to read .*extensions/test_extension/extension.json.*No such file or directory"

assert_command_fail dfx test_extension
assert_match "Error.*Cannot load extension manifest.*Failed to read JSON file.*Failed to read .*extensions/test_extension/extension.json.*No such file or directory"
assert_match "Error.*Failed to load extension manifest.*Failed to read JSON file.*Failed to read .*extensions/test_extension/extension.json.*No such file or directory"

assert_command_fail dfx --help
assert_match "Error.*Cannot load extension manifest.*Failed to read JSON file.*Failed to read .*extensions/test_extension/extension.json.*No such file or directory"
assert_match "Error.*Failed to load extension manifest.*Failed to read JSON file.*Failed to read .*extensions/test_extension/extension.json.*No such file or directory"

assert_command_fail dfx test_extension --help
assert_match "Error.*Cannot load extension manifest.*Failed to read JSON file.*Failed to read .*extensions/test_extension/extension.json.*No such file or directory"
assert_match "Error.*Failed to load extension manifest.*Failed to read JSON file.*Failed to read .*extensions/test_extension/extension.json.*No such file or directory"

echo "{}" > "$CACHE_DIR"/extensions/test_extension/extension.json

assert_command_fail dfx extension list
assert_match "Error.*Cannot load extension manifest.*Failed to parse contents of .*extensions/test_extension/extension.json as json.* missing field .* at line .* column .*"
assert_contains "Failed to load extension manifest"
assert_match "Failed to parse contents of .*extensions/test_extension/extension.json as json"
assert_match "missing field .* at line .* column .*"

assert_command_fail dfx extension run test_extension
assert_match "Error.*Cannot load extension manifest.*Failed to parse contents of .*extensions/test_extension/extension.json as json.* missing field .* at line .* column .*"
assert_contains "Failed to load extension manifest"
assert_match "Failed to parse contents of .*extensions/test_extension/extension.json as json.*"
assert_match "missing field .* at line .* column .*"

assert_command_fail dfx test_extension
assert_match "Error.*Cannot load extension manifest.*Failed to parse contents of .*extensions/test_extension/extension.json as json.* missing field .* at line .* column .*"
assert_contains "Failed to load extension manifest"
assert_match "Failed to parse contents of .*extensions/test_extension/extension.json as json.*"
assert_match "missing field .* at line .* column .*"

assert_command_fail dfx --help
assert_match "Error.*Cannot load extension manifest.*Failed to parse contents of .*extensions/test_extension/extension.json as json.* missing field .* at line .* column .*"
assert_contains "Failed to load extension manifest"
assert_match "Failed to parse contents of .*extensions/test_extension/extension.json as json.*"
assert_match "missing field .* at line .* column .*"

assert_command_fail dfx test_extension --help
assert_match "Error.*Cannot load extension manifest.*Failed to parse contents of .*extensions/test_extension/extension.json as json.* missing field .* at line .* column .*"
assert_contains "Failed to load extension manifest"
assert_match "Failed to parse contents of .*extensions/test_extension/extension.json as json.*"
assert_match "missing field .* at line .* column .*"

echo '{
"name": "test_extension",
Expand Down
4 changes: 2 additions & 2 deletions src/dfx-core/src/error/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::error::extension::ExtensionError;
use crate::error::extension::LoadExtensionManifestError;
use crate::error::fs::FsError;
use crate::error::get_user_home::GetUserHomeError;
use handlebars::RenderError;
Expand Down Expand Up @@ -79,7 +79,7 @@ pub enum ApplyExtensionCanisterTypeError {
NoExtensionForUnknownCanisterType { canister: String, extension: String },

#[error(transparent)]
LoadExtensionManifest(ExtensionError),
LoadExtensionManifest(LoadExtensionManifestError),

#[error("canister '{canister}' has type '{extension}', but that extension does not define a canister type")]
ExtensionDoesNotDefineCanisterType { canister: String, extension: String },
Expand Down
186 changes: 127 additions & 59 deletions src/dfx-core/src/error/extension.rs
Original file line number Diff line number Diff line change
@@ -1,97 +1,165 @@
#![allow(dead_code)]
use crate::error::structured_file::StructuredFileError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ExtensionError {
// errors related to extension directory management
#[error("Cannot find cache directory")]
FindCacheDirectoryFailed(#[source] crate::error::cache::CacheError),
#[error("Failed to load extension manifest")]
pub struct LoadExtensionManifestError(#[from] StructuredFileError);

#[error("Cannot get extensions directory")]
EnsureExtensionDirExistsFailed(#[source] crate::error::fs::FsError),
#[derive(Error, Debug)]
pub enum ConvertExtensionIntoClapCommandError {
#[error(transparent)]
LoadExtensionManifest(#[from] LoadExtensionManifestError),

#[error("Extension directory '{0}' does not exist.")]
ExtensionDirDoesNotExist(std::path::PathBuf),
#[error(transparent)]
ListInstalledExtensionsError(#[from] ListInstalledExtensionsError),

#[error("Extension '{0}' not installed.")]
ExtensionNotInstalled(String),
#[error(transparent)]
ConvertExtensionSubcommandIntoClapCommandError(
#[from] ConvertExtensionSubcommandIntoClapCommandError,
),
}

// errors related to installing extensions
#[error("Extension '{0}' is already installed.")]
ExtensionAlreadyInstalled(String),
#[derive(Error, Debug)]
pub enum ConvertExtensionSubcommandIntoClapCommandError {
#[error(transparent)]
ConvertExtensionSubcommandIntoClapArgError(#[from] ConvertExtensionSubcommandIntoClapArgError),
}

#[error("Extension '{0}' cannot be installed because it conflicts with an existing command. Consider using '--install-as' flag to install this extension under different name.")]
CommandAlreadyExists(String),
#[derive(Error, Debug)]
pub enum ListInstalledExtensionsError {
#[error(transparent)]
ExtensionsDirectoryIsNotReadable(#[from] crate::error::fs::FsError),
}

#[error("Cannot fetch compatibility.json from '{0}'")]
CompatibilityMatrixFetchError(String, #[source] reqwest::Error),
#[derive(Error, Debug)]
pub enum ConvertExtensionSubcommandIntoClapArgError {
#[error("Extension's subcommand argument '{0}' is missing description.")]
ExtensionSubcommandArgMissingDescription(String),
}

#[error("Cannot parse compatibility.json")]
MalformedCompatibilityMatrix(#[source] reqwest::Error),
#[derive(Error, Debug)]
pub enum RunExtensionError {
#[error("Invalid extension name '{0:?}'.")]
InvalidExtensionName(std::ffi::OsString),

#[error("Cannot parse compatibility.json due to malformed semver '{0}'")]
MalformedVersionsEntryForExtensionInCompatibilityMatrix(String, #[source] semver::Error),
#[error("Cannot find cache directory")]
FindCacheDirectoryFailed(#[source] crate::error::cache::CacheError),

#[error("Cannot find compatible extension for dfx version '{1}': compatibility.json (downloaded from '{0}') has empty list of extension versions.")]
ListOfVersionsForExtensionIsEmpty(String, semver::Version),
#[error("Failed to run extension '{0}'")]
FailedToLaunchExtension(String, #[source] std::io::Error),

#[error("Cannot parse extension manifest URL '{0}'")]
MalformedExtensionDownloadUrl(String, #[source] url::ParseError),
#[error("Extension '{0}' never finished")]
ExtensionNeverFinishedExecuting(String, #[source] std::io::Error),

#[error("DFX version '{0}' is not supported.")]
DfxVersionNotFoundInCompatibilityJson(semver::Version),
#[error("Extension terminated by signal.")]
ExtensionExecutionTerminatedViaSignal,

#[error("Extension '{0}' (version '{1}') not found for DFX version {2}.")]
ExtensionVersionNotFoundInRepository(String, semver::Version, String),
#[error("Extension exited with non-zero status code '{0}'.")]
ExtensionExitedWithNonZeroStatus(i32),

#[error(transparent)]
GetExtensionBinaryError(#[from] GetExtensionBinaryError),
}

#[derive(Error, Debug)]
pub enum GetExtensionBinaryError {
#[error("Extension '{0}' not installed.")]
ExtensionNotInstalled(String),

#[error("Cannot find extension binary at '{0}'.")]
ExtensionBinaryDoesNotExist(std::path::PathBuf),

#[error("Extension binary at {0} is not an executable file.")]
ExtensionBinaryIsNotAFile(std::path::PathBuf),
}

#[derive(Error, Debug)]
pub enum NewExtensionManagerError {
#[error("Cannot find cache directory")]
FindCacheDirectoryFailed(#[source] crate::error::cache::CacheError),
}

#[derive(Error, Debug)]
pub enum DownloadAndInstallExtensionToTempdirError {
#[error("Downloading extension from '{0}' failed")]
ExtensionDownloadFailed(url::Url, #[source] reqwest::Error),

#[error("Cannot decompress extension archive (downloaded from: '{0}')")]
DecompressFailed(url::Url, #[source] std::io::Error),
#[error("Cannot get extensions directory")]
EnsureExtensionDirExistsFailed(#[source] crate::error::fs::FsError),

#[error("Cannot create temporary directory at '{0}'")]
CreateTemporaryDirectoryFailed(std::path::PathBuf, #[source] std::io::Error),

#[error("Cannot decompress extension archive (downloaded from: '{0}')")]
DecompressFailed(url::Url, #[source] std::io::Error),
}

#[derive(Error, Debug)]
pub enum InstallExtensionError {
#[error("Extension '{0}' is already installed.")]
ExtensionAlreadyInstalled(String),

#[error(transparent)]
Io(#[from] crate::error::fs::FsError),
GetExtensionArchiveName(#[from] GetExtensionArchiveNameError),

#[error("Platform '{0}' is not supported.")]
PlatformNotSupported(String),
#[error(transparent)]
FindLatestExtensionCompatibleVersion(#[from] FindLatestExtensionCompatibleVersionError),

// errors related to uninstalling extensions
#[error("Cannot uninstall extension")]
InsufficientPermissionsToDeleteExtensionDirectory(#[source] crate::error::fs::FsError),
#[error(transparent)]
GetExtensionDownloadUrl(#[from] GetExtensionDownloadUrlError),

// errors related to listing extensions
#[error("Cannot list extensions")]
ExtensionsDirectoryIsNotReadable(#[source] crate::error::fs::FsError),
#[error(transparent)]
DownloadAndInstallExtensionToTempdir(#[from] DownloadAndInstallExtensionToTempdirError),

#[error("Cannot load extension manifest")]
LoadExtensionManifestFailed(#[source] crate::error::structured_file::StructuredFileError),
#[error(transparent)]
FinalizeInstallation(#[from] FinalizeInstallationError),
}

// errors related to executing extensions
#[error("Invalid extension name '{0:?}'.")]
InvalidExtensionName(std::ffi::OsString),
#[derive(Error, Debug)]
pub enum GetExtensionArchiveNameError {
#[error("Platform '{0}' is not supported.")]
PlatformNotSupported(String),
}

#[error("Extension's subcommand argument '{0}' is missing description.")]
ExtensionSubcommandArgMissingDescription(String),
#[derive(Error, Debug)]
pub enum FindLatestExtensionCompatibleVersionError {
#[error("DFX version '{0}' is not supported.")]
DfxVersionNotFoundInCompatibilityJson(semver::Version),

#[error("Cannot find extension binary at '{0}'.")]
ExtensionBinaryDoesNotExist(std::path::PathBuf),
#[error("Extension '{0}' (version '{1}') not found for DFX version {2}.")]
ExtensionVersionNotFoundInRepository(String, semver::Version, String),

#[error("Extension binary at {0} is not an executable file.")]
ExtensionBinaryIsNotAFile(std::path::PathBuf),
#[error("Cannot parse compatibility.json due to malformed semver '{0}'")]
MalformedVersionsEntryForExtensionInCompatibilityMatrix(String, #[source] semver::Error),

#[error("Failed to run extension '{0}'")]
FailedToLaunchExtension(String, #[source] std::io::Error),
#[error("Cannot find compatible extension for dfx version '{1}': compatibility.json (downloaded from '{0}') has empty list of extension versions.")]
ListOfVersionsForExtensionIsEmpty(String, semver::Version),

#[error("Extension '{0}' never finished")]
ExtensionNeverFinishedExecuting(String, #[source] std::io::Error),
#[error(transparent)]
FetchExtensionCompatibilityMatrix(#[from] FetchExtensionCompatibilityMatrixError),
}

#[error("Extension terminated by signal.")]
ExtensionExecutionTerminatedViaSignal,
#[derive(Error, Debug)]
#[error("Failed to parse extension manifest URL '{url}'")]
pub struct GetExtensionDownloadUrlError {
pub url: String,
pub source: url::ParseError,
}

#[error("Extension exited with non-zero status code '{0}'.")]
ExtensionExitedWithNonZeroStatus(i32),
#[derive(Error, Debug)]
#[error(transparent)]
pub struct FinalizeInstallationError(#[from] crate::error::fs::FsError);

#[derive(Error, Debug)]
pub enum FetchExtensionCompatibilityMatrixError {
#[error("Cannot fetch compatibility.json from '{0}'")]
CompatibilityMatrixFetchError(String, #[source] reqwest::Error),

#[error("Cannot parse compatibility.json")]
MalformedCompatibilityMatrix(#[source] reqwest::Error),
}

#[derive(Error, Debug)]
#[error(transparent)]
pub struct UninstallExtensionError(#[from] crate::error::fs::FsError);
16 changes: 8 additions & 8 deletions src/dfx-core/src/extension/manager/execute.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
use super::ExtensionManager;
use crate::config::cache::get_bin_cache;
use crate::error::extension::ExtensionError;
use crate::error::extension::RunExtensionError;
use std::ffi::OsString;

impl ExtensionManager {
pub fn run_extension(
&self,
extension_name: OsString,
mut params: Vec<OsString>,
) -> Result<(), ExtensionError> {
) -> Result<(), RunExtensionError> {
let extension_name = extension_name
.into_string()
.map_err(ExtensionError::InvalidExtensionName)?;
.map_err(RunExtensionError::InvalidExtensionName)?;

let mut extension_binary = self.get_extension_binary(&extension_name)?;
let dfx_cache = get_bin_cache(self.dfx_version.to_string().as_str())
.map_err(ExtensionError::FindCacheDirectoryFailed)?;
.map_err(RunExtensionError::FindCacheDirectoryFailed)?;

params.extend(["--dfx-cache-path".into(), dfx_cache.into_os_string()]);

let mut child = extension_binary
.args(&params)
.spawn()
.map_err(|e| ExtensionError::FailedToLaunchExtension(extension_name.clone(), e))?;
.map_err(|e| RunExtensionError::FailedToLaunchExtension(extension_name.clone(), e))?;

let exit_status = child.wait().map_err(|e| {
ExtensionError::ExtensionNeverFinishedExecuting(extension_name.clone(), e)
RunExtensionError::ExtensionNeverFinishedExecuting(extension_name.clone(), e)
})?;

let code = exit_status
.code()
.ok_or(ExtensionError::ExtensionExecutionTerminatedViaSignal)?;
.ok_or(RunExtensionError::ExtensionExecutionTerminatedViaSignal)?;

if code != 0 {
Err(ExtensionError::ExtensionExitedWithNonZeroStatus(code))
Err(RunExtensionError::ExtensionExitedWithNonZeroStatus(code))
} else {
Ok(())
}
Expand Down
Loading

0 comments on commit 36c7352

Please sign in to comment.