//! Choosing where icons go, and bundling them up.
use std::fs;
use std::path::{Path, PathBuf};
use icon_studio::spec::IconSpec;
use icon_studio::target::{self, Kind};
/// A scratch directory that cleans itself up.
struct Tmp(PathBuf);
impl Tmp {
fn new(tag: &str) -> Tmp {
let p = std::env::temp_dir().join(format!(
"icon-studio-{tag}-{}-{:?}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::create_dir_all(&p).unwrap();
Tmp(p)
}
fn path(&self) -> &Path {
&self.0
}
fn mkdir(&self, rel: &str) -> PathBuf {
let p = self.0.join(rel);
fs::create_dir_all(&p).unwrap();
p
}
}
impl Drop for Tmp {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
#[test]
fn an_android_project_root_resolves_to_its_res_directory() {
// The mistake this exists to prevent: picking the project root and
// getting mipmap directories dropped beside build.gradle.kts.
let t = Tmp::new("proj");
t.mkdir("app/src/main/res/values");
fs::write(t.path().join("build.gradle.kts"), "").unwrap();
let r = target::resolve(t.path());
assert_eq!(r.kind, Kind::AndroidProject);
assert_eq!(r.root, t.path().join("app/src/main/res"));
assert!(r.note.contains("app/src/main/res"), "{}", r.note);
}
#[test]
fn a_module_root_resolves_too() {
let t = Tmp::new("module");
t.mkdir("src/main/res/values");
let r = target::resolve(t.path());
assert_eq!(r.kind, Kind::AndroidProject);
assert_eq!(r.root, t.path().join("src/main/res"));
}
#[test]
fn a_res_directory_is_used_as_is() {
let t = Tmp::new("res");
let res = t.mkdir("res");
t.mkdir("res/values");
let r = target::resolve(&res);
assert_eq!(r.kind, Kind::AndroidRes);
assert_eq!(r.root, res);
}
#[test]
fn a_res_directory_is_recognised_by_content_not_only_by_name() {
// Resource directories are not always called `res`.
let t = Tmp::new("byname");
let odd = t.mkdir("resources");
t.mkdir("resources/mipmap-xxxhdpi");
let r = target::resolve(&odd);
assert_eq!(r.kind, Kind::AndroidRes, "{}", r.note);
}
#[test]
fn picking_a_density_bucket_steps_up_to_the_res_directory() {
// Browsing in to look at the current icons and stopping there would
// otherwise nest mipmap-xxxhdpi/mipmap-xxxhdpi.
let t = Tmp::new("bucket");
t.mkdir("res/values");
let bucket = t.mkdir("res/mipmap-xxxhdpi");
let r = target::resolve(&bucket);
assert_eq!(r.kind, Kind::AndroidRes);
assert_eq!(r.root, t.path().join("res"));
assert!(r.note.contains("parent res"), "{}", r.note);
}
#[test]
fn a_lone_bucket_outside_a_res_tree_is_left_alone() {
// Only step up when the parent really is a resource directory.
let t = Tmp::new("lonebucket");
let bucket = t.mkdir("mipmap-xxxhdpi");
let r = target::resolve(&bucket);
assert_eq!(r.kind, Kind::Plain);
assert_eq!(r.root, bucket);
}
#[test]
fn an_ordinary_folder_is_written_to_literally() {
let t = Tmp::new("plain");
let r = target::resolve(t.path());
assert_eq!(r.kind, Kind::Plain);
assert_eq!(r.root, t.path());
assert!(
r.note.to_lowercase().contains("not an android"),
"{}",
r.note
);
}
#[test]
fn the_plan_lists_every_file_and_flags_replacements() {
let t = Tmp::new("plan");
let fresh = target::plan(t.path(), "ic_icon");
assert_eq!(fresh.len(), icon_studio::export::DENSITIES.len() * 3);
assert!(fresh.iter().all(|f| !f.overwrites));
assert!(target::summarise(&fresh).contains("none existing"));
// Put one of them in place; it should come back flagged.
let victim = &fresh[0].path;
fs::create_dir_all(victim.parent().unwrap()).unwrap();
fs::write(victim, b"x").unwrap();
let again = target::plan(t.path(), "ic_icon");
assert_eq!(again.iter().filter(|f| f.overwrites).count(), 1);
assert!(target::summarise(&again).contains("1 replaced"));
}
#[test]
fn writability_is_checked_before_writing() {
assert!(target::writable(Path::new("")).is_err(), "empty path");
let t = Tmp::new("writable");
assert!(target::writable(t.path()).is_ok());
// A destination that no longer exists must be caught here, not on the
// fifteenth file. This is the case that actually happens: a remembered
// folder that has since been deleted or unmounted.
assert!(
target::writable(&t.path().join("since-deleted")).is_err(),
"missing directory"
);
assert!(
target::writable(Path::new("/nonexistent-root-xyz/sub")).is_err(),
"unreachable path"
);
// A file is not a destination.
let f = t.path().join("a-file");
fs::write(&f, b"x").unwrap();
assert!(target::writable(&f).is_err(), "regular file");
}
#[test]
fn the_zip_bundle_carries_the_images_and_the_document() {
let t = Tmp::new("zip");
let out = t.path().join("bundle.zip");
let names = icon_studio::export::export_zip(
&IconSpec::default(),
&out,
"ic_icon",
Some(("ic_icon.toml", "version = 1\n")),
)
.expect("bundle written");
assert_eq!(names.len(), icon_studio::export::DENSITIES.len() * 3 + 1);
let f = fs::File::open(&out).unwrap();
let mut zip = zip::ZipArchive::new(f).expect("valid zip");
let entries: Vec<String> = (0..zip.len())
.map(|i| zip.by_index(i).unwrap().name().to_string())
.collect();
assert!(entries.contains(&"mipmap-xxxhdpi/ic_icon.png".to_string()));
assert!(entries.contains(&"mipmap-mdpi/ic_icon_foreground.png".to_string()));
assert!(
entries.contains(&"ic_icon.toml".to_string()),
"the bundle should be reproducible, not just a pile of PNGs"
);
// And the PNGs are real PNGs at the right size.
let mut img = zip.by_name("mipmap-xxxhdpi/ic_icon.png").unwrap();
let mut bytes = Vec::new();
std::io::Read::read_to_end(&mut img, &mut bytes).unwrap();
let decoded = image::load_from_memory(&bytes).expect("decodable png");
assert_eq!(decoded.width(), 432);
}
neiam
/icon-studio
public · Issues · Pulls · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
6.1 KiB · text
History
bf97ff0