use crate::result::CnResult;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum StartupError {
MissingData { blob: PathBuf },
UnreadableData { blob: PathBuf, cause: CnResult },
OverflowUnsupported { blob: PathBuf, needed: u32 },
NoStateRoot,
}
impl StartupError {
pub(crate) fn from_blob_failure(blob: PathBuf, cause: CnResult) -> Self {
if blob.exists() {
StartupError::UnreadableData { blob, cause }
} else {
StartupError::MissingData { blob }
}
}
pub(crate) fn io_kind(&self) -> std::io::ErrorKind {
match self {
StartupError::MissingData { .. } | StartupError::NoStateRoot => {
std::io::ErrorKind::NotFound
}
StartupError::UnreadableData { .. } | StartupError::OverflowUnsupported { .. } => {
std::io::ErrorKind::InvalidData
}
}
}
pub(crate) fn user_message(&self) -> String {
match self {
StartupError::MissingData { blob } => {
format!("Failed to find the data blob:\n{}", blob.display())
}
StartupError::UnreadableData { blob, .. } => {
format!("Failed to read the data blob:\n{}", blob.display())
}
StartupError::OverflowUnsupported { blob, .. } => {
format!("This app's data is incomplete:\n{}", blob.display())
}
StartupError::NoStateRoot => "Failed to find this app's data.".to_string(),
}
}
pub(crate) fn log_line(&self) -> String {
match self {
StartupError::MissingData { blob } => {
format!(
"no compiled world data at {} -- run `concinnity build` first",
blob.display()
)
}
StartupError::UnreadableData { blob, cause } => {
format!(
"compiled world data at {} failed to load: {cause}",
blob.display()
)
}
StartupError::OverflowUnsupported { blob, needed } => {
format!(
"{} is a single blob file, but this world spans {} more; \
re-export it so the player ships a `data/` directory",
blob.display(),
needed
)
}
StartupError::NoStateRoot => {
"no state directory was installed, so there is nowhere to read world data from"
.to_string()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_missing_blob_classifies_as_missing() {
let dir = tempfile::tempdir().expect("tempdir");
let blob = dir.path().join("data").join("0");
let err = StartupError::from_blob_failure(blob, CnResult::FileIo);
assert!(matches!(err, StartupError::MissingData { .. }));
assert!(err.user_message().contains("Failed to find"));
assert!(err.log_line().contains("concinnity build"));
}
#[test]
fn a_present_but_broken_blob_classifies_as_unreadable() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::create_dir_all(dir.path().join("data")).expect("data dir");
let blob = dir.path().join("data").join("0");
std::fs::write(&blob, b"garbage").expect("write blob");
let err = StartupError::from_blob_failure(blob, CnResult::FileIo);
assert!(matches!(err, StartupError::UnreadableData { .. }));
assert!(err.user_message().contains("Failed to read"));
assert!(err.log_line().contains(&CnResult::FileIo.to_string()));
}
#[test]
fn every_message_names_the_blob_path() {
for err in [
StartupError::MissingData {
blob: PathBuf::from("/somewhere/data/0"),
},
StartupError::UnreadableData {
blob: PathBuf::from("/somewhere/data/0"),
cause: CnResult::FileIo,
},
StartupError::OverflowUnsupported {
blob: PathBuf::from("/somewhere/data/0"),
needed: 2,
},
] {
assert!(err.user_message().contains("/somewhere/data/0"));
assert!(err.log_line().contains("/somewhere/data/0"));
}
}
#[test]
fn the_overflow_refusal_names_the_fix() {
let err = StartupError::OverflowUnsupported {
blob: PathBuf::from("/apps/MyGame/data"),
needed: 3,
};
assert!(err.log_line().contains("single blob file"), "{err:?}");
assert!(err.log_line().contains("`data/` directory"), "{err:?}");
assert!(err.log_line().contains('3'), "{err:?}");
assert_eq!(err.io_kind(), std::io::ErrorKind::InvalidData);
}
#[test]
fn no_state_root_reports_not_found_without_a_path() {
let err = StartupError::NoStateRoot;
assert_eq!(err.io_kind(), std::io::ErrorKind::NotFound);
assert!(err.log_line().contains("no state directory"));
}
}