Add tests for Image functionality and refactor image handling logic

- Implement unit tests to cover `Image` creation, metadata management, saving/loading, and error handling.
- Add separate constructors for `Full` and `Partial` image types (`new_full`, `new_partial`).
- Introduce robust path handling methods (`metadata_dir`, `image_json_path`) for consistent metadata structure.
- Enhance error diagnostics with `InvalidPath` variant in `ImageError`.
- Update `Cargo.toml` for test module inclusion.
This commit is contained in:
Till Wegmueller 2025-08-02 22:12:37 +02:00
parent f60abfdbd1
commit f7f017f7b9
No known key found for this signature in database
5 changed files with 235 additions and 19 deletions

View file

@ -8,7 +8,7 @@ members = [
"userland",
"specfile",
"ports",
"crates/*",
"pkg6",
"xtask",
]
resolver = "2"

View file

@ -1,10 +1,12 @@
mod properties;
#[cfg(test)]
mod tests;
use miette::Diagnostic;
use properties::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use thiserror::Error;
@ -23,23 +25,49 @@ pub enum ImageError {
help("Check the JSON format and try again")
)]
Json(#[from] serde_json::Error),
#[error("Invalid image path: {0}")]
#[diagnostic(
code(ips::image_error::invalid_path),
help("Provide a valid path for the image")
)]
InvalidPath(String),
}
pub type Result<T> = std::result::Result<T, ImageError>;
/// Type of image, either Full (base path of "/") or Partial (attached to a full image)
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum ImageType {
/// Full image with base path of "/"
Full,
/// Partial image attached to a full image
Partial,
}
/// Represents an IPS image, which can be either a Full image or a Partial image
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Image {
/// Path to the image
path: PathBuf,
/// Type of image (Full or Partial)
image_type: ImageType,
/// Image properties
props: Vec<ImageProperty>,
/// Image version
version: i32,
/// Variants
variants: HashMap<String, String>,
/// Mediators
mediators: HashMap<String, String>,
}
impl Image {
pub fn new<P: Into<PathBuf>>(path: P) -> Image {
/// Creates a new Full image at the specified path
pub fn new_full<P: Into<PathBuf>>(path: P) -> Image {
Image {
path: path.into(),
image_type: ImageType::Full,
version: 5,
variants: HashMap::new(),
mediators: HashMap::new(),
@ -47,21 +75,95 @@ impl Image {
}
}
pub fn open<P: AsRef<Path>>(path: P) -> Result<Image> {
/// Creates a new Partial image at the specified path
pub fn new_partial<P: Into<PathBuf>>(path: P) -> Image {
Image {
path: path.into(),
image_type: ImageType::Partial,
version: 5,
variants: HashMap::new(),
mediators: HashMap::new(),
props: vec![],
}
}
/// Returns the path to the image
pub fn path(&self) -> &Path {
&self.path
}
/// Returns the type of the image
pub fn image_type(&self) -> &ImageType {
&self.image_type
}
/// Returns the path to the metadata directory for this image
pub fn metadata_dir(&self) -> PathBuf {
match self.image_type {
ImageType::Full => self.path.join("var/pkg"),
ImageType::Partial => self.path.join(".pkg"),
}
}
/// Returns the path to the image JSON file
pub fn image_json_path(&self) -> PathBuf {
self.metadata_dir().join("pkg6.image.json")
}
/// Creates the metadata directory if it doesn't exist
pub fn create_metadata_dir(&self) -> Result<()> {
let metadata_dir = self.metadata_dir();
fs::create_dir_all(&metadata_dir).map_err(|e| {
ImageError::IO(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to create metadata directory at {:?}: {}", metadata_dir, e),
))
})
}
/// Saves the image data to the metadata directory
pub fn save(&self) -> Result<()> {
self.create_metadata_dir()?;
let json_path = self.image_json_path();
let file = File::create(&json_path).map_err(|e| {
ImageError::IO(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to create image JSON file at {:?}: {}", json_path, e),
))
})?;
serde_json::to_writer_pretty(file, self).map_err(ImageError::Json)
}
/// Loads an image from the specified path
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
//TODO: Parse the old INI format of pkg5
//TODO once root images are implemented, look for metadata under sub directory var/pkg
let props_path = path.join("pkg6.image.json");
let mut f = File::open(props_path)?;
Ok(serde_json::from_reader(&mut f)?)
}
// Check for both full and partial image JSON files
let full_image = Image::new_full(path);
let partial_image = Image::new_partial(path);
pub fn open_default<P: AsRef<Path>>(path: P) -> Image {
if let Ok(img) = Image::open(path.as_ref()) {
img
let full_json_path = full_image.image_json_path();
let partial_json_path = partial_image.image_json_path();
// Determine which JSON file exists
let json_path = if full_json_path.exists() {
full_json_path
} else if partial_json_path.exists() {
partial_json_path
} else {
Image::new(path.as_ref())
}
return Err(ImageError::InvalidPath(format!(
"Image JSON file not found at either {:?} or {:?}",
full_json_path, partial_json_path
)));
};
let file = File::open(&json_path).map_err(|e| {
ImageError::IO(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to open image JSON file at {:?}: {}", json_path, e),
))
})?;
serde_json::from_reader(file).map_err(ImageError::Json)
}
}

114
libips/src/image/tests.rs Normal file
View file

@ -0,0 +1,114 @@
use super::*;
use std::path::Path;
use tempfile::tempdir;
#[test]
fn test_image_types() {
let full_image = Image::new_full("/");
let partial_image = Image::new_partial("/tmp/partial");
assert_eq!(*full_image.image_type(), ImageType::Full);
assert_eq!(*partial_image.image_type(), ImageType::Partial);
}
#[test]
fn test_metadata_paths() {
let full_image = Image::new_full("/");
let partial_image = Image::new_partial("/tmp/partial");
assert_eq!(full_image.metadata_dir(), Path::new("/var/pkg"));
assert_eq!(partial_image.metadata_dir(), Path::new("/tmp/partial/.pkg"));
assert_eq!(
full_image.image_json_path(),
Path::new("/var/pkg/pkg6.image.json")
);
assert_eq!(
partial_image.image_json_path(),
Path::new("/tmp/partial/.pkg/pkg6.image.json")
);
}
#[test]
fn test_save_and_load() -> Result<()> {
// Create a temporary directory for testing
let temp_dir = tempdir().expect("Failed to create temp directory");
let temp_path = temp_dir.path();
// Create a full image
let mut full_image = Image::new_full(temp_path);
// Add some test data
full_image.props.push(ImageProperty::String("test_prop".to_string()));
// Save the image
full_image.save()?;
// Check that the metadata directory and JSON file were created
let metadata_dir = temp_path.join("var/pkg");
let json_path = metadata_dir.join("pkg6.image.json");
assert!(metadata_dir.exists());
assert!(json_path.exists());
// Load the image
let loaded_image = Image::load(temp_path)?;
// Check that the loaded image matches the original
assert_eq!(*loaded_image.image_type(), ImageType::Full);
assert_eq!(loaded_image.path, full_image.path);
assert_eq!(loaded_image.props.len(), 1);
// Clean up
temp_dir.close().expect("Failed to clean up temp directory");
Ok(())
}
#[test]
fn test_partial_image() -> Result<()> {
// Create a temporary directory for testing
let temp_dir = tempdir().expect("Failed to create temp directory");
let temp_path = temp_dir.path();
// Create a partial image
let mut partial_image = Image::new_partial(temp_path);
// Add some test data
partial_image.props.push(ImageProperty::Boolean(true));
// Save the image
partial_image.save()?;
// Check that the metadata directory and JSON file were created
let metadata_dir = temp_path.join(".pkg");
let json_path = metadata_dir.join("pkg6.image.json");
assert!(metadata_dir.exists());
assert!(json_path.exists());
// Load the image
let loaded_image = Image::load(temp_path)?;
// Check that the loaded image matches the original
assert_eq!(*loaded_image.image_type(), ImageType::Partial);
assert_eq!(loaded_image.path, partial_image.path);
assert_eq!(loaded_image.props.len(), 1);
// Clean up
temp_dir.close().expect("Failed to clean up temp directory");
Ok(())
}
#[test]
fn test_invalid_path() {
let result = Image::load("/nonexistent/path");
assert!(result.is_err());
if let Err(ImageError::InvalidPath(_)) = result {
// Expected error
} else {
panic!("Expected InvalidPath error, got {:?}", result);
}
}

View file

@ -9,6 +9,6 @@ readme.workspace = true
keywords.workspace = true
[dependencies]
libips = { version = "*", path = "../../libips" }
libips = { version = "*", path = "../libips" }
diff-struct = "0.5.3"
serde = { version = "1.0.207", features = ["derive"] }