#[allow(unused_imports)]
pub use super::*;
#[cfg(test)]
#[allow(
clippy::items_after_statements,
clippy::redundant_clone,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
trivial_casts,
clippy::borrow_as_ptr,
clippy::cast_ptr_alignment,
clippy::unused_self,
unused_qualifications,
unreachable_pub,
private_interfaces
)] mod tests {
use super::*;
#[test]
fn normalize_u16_maps_full_range() {
assert_eq!(normalize_u16(0), 0);
assert_eq!(normalize_u16(u16::MAX), 255);
let mid = normalize_u16(u16::MAX / 2);
assert!((126..=128).contains(&mid), "midpoint normalized to {mid}");
assert_eq!(normalize_u16(256), 0);
assert_eq!(normalize_u16(257), 1);
}
#[test]
fn load_bgra8_rejects_wrong_length() {
let short = RawImageData::U8(vec![0u8; 4 * 3].into()); assert!(RawImage::load_bgra8(short, 4, true).is_none());
let ok = RawImageData::U8(vec![255u8; 4 * 4].into()); assert!(RawImage::load_bgra8(ok, 4, true).is_some());
let short2 = RawImageData::U8(vec![0u8; 4 * 2].into());
assert!(RawImage::load_bgra8(short2, 4, false).is_none());
}
#[test]
fn imageref_get_data_reads_backing_box() {
let img = ImageRef::null_image(2, 3, RawImageFormat::RGBA8, vec![7, 8]);
match img.get_data() {
DecodedImage::NullImage {
width, height, tag, ..
} => {
assert_eq!((*width, *height), (2, 3));
assert_eq!(tag.as_slice(), &[7, 8]);
}
_ => panic!("expected NullImage"),
}
}
#[test]
fn imageref_clone_shares_refcount_and_identity() {
let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
let c = img.clone();
assert_eq!(img, c); assert!(c.into_inner().is_none());
assert!(img.into_inner().is_some());
}
#[test]
fn imageref_deep_copy_has_distinct_identity() {
let img = ImageRef::null_image(4, 4, RawImageFormat::RGBA8, vec![1]);
let d = img.deep_copy();
assert_ne!(img, d);
drop(img);
assert_eq!(d.get_size().width as usize, 4);
}
#[test]
fn imageref_last_drop_frees_once() {
let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
let c = img.clone();
drop(img);
drop(c);
}
#[test]
fn imageref_get_callback_none_for_non_callback_and_when_shared() {
let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
assert!(img.get_image_callback().is_none());
let c = img.clone();
assert!(img.get_image_callback().is_none()); drop(c);
}
#[test]
fn shared_raw_image_data_read_paths() {
let s = SharedRawImageData::new(vec![10u8, 20, 30].into());
assert_eq!(s.as_ref(), &[10, 20, 30]);
assert_eq!(s.len(), 3);
assert!(!s.is_empty());
assert_eq!(unsafe { *s.as_ptr() }, 10);
assert!(SharedRawImageData::new(Vec::<u8>::new().into()).is_empty());
}
#[test]
fn shared_raw_image_data_clone_shares_alloc() {
let s = SharedRawImageData::new(vec![1u8, 2, 3, 4].into());
let c = s.clone();
assert_eq!(s, c); assert_eq!(s.as_ptr(), c.as_ptr());
assert!(c.into_inner().is_none()); let inner = s.into_inner().expect("sole owner extraction");
assert_eq!(inner.as_ref(), &[1, 2, 3, 4]);
}
#[test]
fn shared_raw_image_data_last_drop_frees_once() {
let s = SharedRawImageData::new(vec![0u8; 8].into());
let c = s.clone();
drop(s);
drop(c);
}
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::items_after_statements,
clippy::redundant_clone,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::cast_lossless,
clippy::unreadable_literal,
clippy::too_many_lines,
clippy::many_single_char_names,
clippy::similar_names,
unused_qualifications,
unreachable_pub,
private_interfaces
)] mod autotest_generated {
use alloc::string::String;
use super::*;
fn dummy_font_ref() -> FontRef {
static DUMMY_FONT_DATA: u8 = 0;
extern "C" fn dummy_destructor(_: *mut core::ffi::c_void) {}
FontRef::new(
core::ptr::addr_of!(DUMMY_FONT_DATA).cast::<core::ffi::c_void>(),
dummy_destructor,
)
}
fn load_font_none(_: &StyleFontFamily, _: &FcFontCache) -> Option<LoadedFontSource> {
None
}
fn parse_font_none(_: LoadedFontSource) -> Option<FontRef> {
None
}
fn store_gl_texture_noop(_: DocumentId, _: Epoch, _: Texture, _: ExternalImageId) {}
fn test_document_id() -> DocumentId {
DocumentId {
namespace_id: IdNamespace(7),
id: 0,
}
}
fn rgba8_image(w: usize, h: usize) -> RawImage {
RawImage {
pixels: RawImageData::U8(vec![0u8; w * h * 4].into()),
width: w,
height: h,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
}
}
fn opaque_red() -> ColorU {
ColorU {
r: 255,
g: 0,
b: 0,
a: 255,
}
}
#[test]
fn match_route_valid_minimal_positive_control() {
let m = match_route("/user/:id", "/user/42").expect("documented example must match");
assert_eq!(m.pattern.as_str(), "/user/:id");
assert_eq!(m.get_param("id").map(AzString::as_str), Some("42"));
let root = match_route("/", "/").expect("root must match root");
assert!(root.params.as_ref().is_empty());
assert!(match_route("/about", "/settings").is_none());
}
#[test]
fn match_route_empty_input_does_not_panic() {
let m = match_route("", "").expect("empty vs empty is a zero-segment match");
assert!(m.params.as_ref().is_empty());
assert!(match_route("", "/").is_some()); assert!(match_route("/", "").is_some());
assert!(match_route("", "/a").is_none()); assert!(match_route("/a", "").is_none());
}
#[test]
fn match_route_whitespace_only_is_not_trimmed() {
assert!(match_route(" ", " ").is_some());
assert!(match_route(" ", "\t\n").is_none());
assert!(match_route("/ ", "/").is_none()); assert!(match_route("/\t\n", "/\t\n").is_some());
}
#[test]
fn match_route_garbage_never_panics() {
for pat in [
"\0\0\0",
"///////",
"::::",
":",
"%%%$#@!",
"\u{feff}",
"/a/../../etc/passwd",
] {
for path in ["", "/", "\0", "/a/b/c", "%%%$#@!", "\u{feff}"] {
let _ = match_route(pat, path);
}
}
assert!(match_route("///////", "/").is_some());
let m = match_route("/:", "/hello").expect("empty param name still matches");
assert_eq!(m.get_param("").map(AzString::as_str), Some("hello"));
}
#[test]
fn match_route_leading_trailing_junk_is_rejected_or_ignored() {
assert!(match_route("/user/:id/", "/user/42").is_some());
assert!(match_route("/user/:id", "/user/42/").is_some());
assert!(match_route(" /about ", "/about").is_none());
assert!(match_route("/about", "/about;garbage").is_none());
}
#[test]
fn match_route_boundary_number_strings_are_opaque_segments() {
for v in [
"0",
"-0",
"9223372036854775807",
"-9223372036854775808",
"1e400",
"NaN",
"inf",
"-inf",
"0.0000000000000000001",
] {
let path = String::from("/user/") + v;
let m = match_route("/user/:id", &path).expect("any segment matches a :param");
assert_eq!(m.get_param("id").map(AzString::as_str), Some(v));
}
}
#[test]
fn match_route_unicode_multibyte_does_not_panic() {
let m = match_route("/user/:id", "/user/\u{1F600}").expect("emoji segment matches");
assert_eq!(m.get_param("id").map(AzString::as_str), Some("\u{1F600}"));
let m = match_route("/:é\u{0301}", "/e\u{0301}\u{202E}x").expect("unicode param name");
assert_eq!(
m.get_param("é\u{0301}").map(AzString::as_str),
Some("e\u{0301}\u{202E}x")
);
assert!(match_route("/é", "/e\u{0301}").is_none());
}
#[test]
fn match_route_extremely_long_input_does_not_hang() {
let huge = String::from("/") + &"a".repeat(1_000_000);
assert!(match_route("/x", &huge).is_none());
let m = match_route("/:id", &huge).expect("one long segment is still one segment");
assert_eq!(m.get_param("id").map(|s| s.as_str().len()), Some(1_000_000));
}
#[test]
fn match_route_deeply_nested_input_does_not_stack_overflow() {
let deep = "/a".repeat(10_000);
let m = match_route(&deep, &deep).expect("identical deep paths match");
assert!(m.params.as_ref().is_empty());
let all_params = "/:p".repeat(10_000);
let m = match_route(&all_params, &deep).expect("10k params extract");
assert_eq!(m.params.as_ref().len(), 10_000);
assert_eq!(m.get_param("p").map(AzString::as_str), Some("a"));
let brackets = String::from("/") + &"[".repeat(10_000);
assert!(match_route("/:x", &brackets).is_some());
}
#[test]
fn match_route_segment_count_mismatch_is_none() {
assert!(match_route("/a/:b", "/a").is_none());
assert!(match_route("/a", "/a/b").is_none());
assert!(match_route("/:a/:b/:c", "/1/2").is_none());
}
#[test]
fn route_match_get_param_missing_keys_return_none() {
let empty = RouteMatch {
pattern: AzString::from_const_str("/"),
params: StringPairVec::from_vec(Vec::new()),
};
assert!(empty.get_param("").is_none());
assert!(empty.get_param(" ").is_none());
assert!(empty.get_param("\t\n").is_none());
assert!(empty.get_param("\u{1F600}").is_none());
assert!(empty.get_param("\0").is_none());
assert!(empty.get_param(&"k".repeat(100_000)).is_none());
let m = match_route("/u/:id", "/u/7").expect("valid");
assert_eq!(m.get_param("id").map(AzString::as_str), Some("7"));
assert!(m.get_param("ID").is_none()); assert!(m.get_param("i").is_none()); assert!(m.get_param(":id").is_none()); }
#[test]
fn app_config_match_route_for_path_adversarial_inputs() {
let mut config = AppConfig::create();
let cb: crate::callbacks::LayoutCallbackType = autotest_layout;
extern "C" fn autotest_layout(
_: RefAny,
_: crate::callbacks::LayoutCallbackInfo,
) -> crate::dom::Dom {
crate::dom::Dom::create_body()
}
config.add_route(AzString::from_const_str("/"), cb);
config.add_route(AzString::from_const_str("/user/:id"), cb);
let (route, m) = config
.match_route_for_path("/user/42")
.expect("registered route must match");
assert_eq!(route.pattern.as_str(), "/user/:id");
assert_eq!(m.get_param("id").map(AzString::as_str), Some("42"));
assert!(config.match_route_for_path("").is_some());
assert!(config.match_route_for_path("/").is_some());
assert!(config.match_route_for_path("/nope/nope/nope").is_none());
assert!(config.match_route_for_path("\0\0").is_none());
assert!(config.match_route_for_path(" ").is_none());
let long = String::from("/user/") + &"9".repeat(500_000);
assert!(config.match_route_for_path(&long).is_some());
let m = config
.match_route_for_path("/user/\u{1F600}")
.expect("unicode param");
assert_eq!(m.1.get_param("id").map(AzString::as_str), Some("\u{1F600}"));
}
#[test]
fn app_config_add_route_replaces_same_pattern_and_orders_by_insertion() {
extern "C" fn layout_a(
_: RefAny,
_: crate::callbacks::LayoutCallbackInfo,
) -> crate::dom::Dom {
crate::dom::Dom::create_body()
}
let cb: crate::callbacks::LayoutCallbackType = layout_a;
let mut config = AppConfig::create();
assert!(config.routes.as_ref().is_empty());
config.add_route(AzString::from_const_str("/dup"), cb);
config.add_route(AzString::from_const_str("/dup"), cb);
assert_eq!(config.routes.as_ref().len(), 1, "same pattern must replace");
let mut config = AppConfig::create();
config.add_route(AzString::from_const_str("/:anything"), cb);
config.add_route(AzString::from_const_str("/about"), cb);
let (route, _) = config.match_route_for_path("/about").expect("matches");
assert_eq!(route.pattern.as_str(), "/:anything");
}
#[test]
fn dpi_scale_factor_new_handles_nan_and_infinities() {
assert_eq!(DpiScaleFactor::new(0.0).inner.get(), 0.0);
assert_eq!(DpiScaleFactor::new(1.0).inner.get(), 1.0);
assert_eq!(DpiScaleFactor::new(f32::NAN).inner.get(), 0.0);
assert!(DpiScaleFactor::new(f32::INFINITY).inner.get().is_finite());
assert!(DpiScaleFactor::new(f32::NEG_INFINITY)
.inner
.get()
.is_finite());
assert!(DpiScaleFactor::new(f32::MAX).inner.get().is_finite());
assert!(DpiScaleFactor::new(f32::MIN).inner.get().is_finite());
assert_eq!(DpiScaleFactor::new(f32::MIN_POSITIVE).inner.get(), 0.0);
assert_eq!(DpiScaleFactor::new(1.5), DpiScaleFactor::new(1.5));
assert_ne!(DpiScaleFactor::new(1.5), DpiScaleFactor::new(2.0));
}
#[test]
fn named_font_new_keeps_fields_verbatim() {
let f = NamedFont::new(AzString::from_const_str(""), U8Vec::from_vec(Vec::new()));
assert_eq!(f.name.as_str(), "");
assert!(f.bytes.as_ref().is_empty());
let bytes = vec![0u8, 255, 128];
let f = NamedFont::new(
AzString::from(String::from("\u{1F600}")),
bytes.clone().into(),
);
assert_eq!(f.name.as_str(), "\u{1F600}");
assert_eq!(f.bytes.as_ref(), bytes.as_slice());
}
#[test]
fn loaded_font_new_keeps_fields_verbatim_at_limits() {
let f = LoadedFont::new(0, AzString::from_const_str(""), 0, false);
assert_eq!(f.font_hash, 0);
assert_eq!(f.num_glyphs, 0);
assert!(!f.has_bytes);
let f = LoadedFont::new(u64::MAX, AzString::from_const_str("x"), u32::MAX, true);
assert_eq!(f.font_hash, u64::MAX);
assert_eq!(f.num_glyphs, u32::MAX);
assert!(f.has_bytes);
}
#[test]
fn brush_new_defaults_and_extreme_radius() {
let b = Brush::new(opaque_red(), 4.0);
assert_eq!(b.radius, 4.0);
assert_eq!(b.hardness, 0.5);
assert_eq!(b.flow, 1.0);
assert_eq!(b.spacing, 0.25);
assert_eq!(b.color, opaque_red());
assert!(Brush::new(opaque_red(), f32::NAN).radius.is_nan());
assert_eq!(Brush::new(opaque_red(), -0.0).radius, -0.0);
assert_eq!(
Brush::new(opaque_red(), f32::INFINITY).radius,
f32::INFINITY
);
}
#[test]
fn image_cache_new_is_empty_and_default_is_neutral() {
let cache = ImageCache::new();
assert!(cache.image_id_map.is_empty());
assert!(ImageCache::default().image_id_map.is_empty());
}
#[test]
fn gl_texture_cache_empty_is_neutral() {
let cache = GlTextureCache::empty();
assert!(cache.solved_textures.is_empty());
assert!(cache.hashes.is_empty());
let d = GlTextureCache::default();
assert!(d.solved_textures.is_empty());
assert!(d.hashes.is_empty());
}
#[test]
fn external_image_id_new_is_monotonic() {
let a = ExternalImageId::new();
let b = ExternalImageId::new();
assert!(b.inner > a.inner, "the counter must strictly increase");
assert!(ExternalImageId::default().inner > b.inner);
}
#[test]
fn shared_raw_image_data_new_invariants() {
let empty = SharedRawImageData::new(U8Vec::from_vec(Vec::new()));
assert_eq!(empty.len(), 0);
assert!(empty.is_empty());
assert!(empty.as_ref().is_empty());
assert!(empty.get_bytes().is_empty());
assert!(!empty.as_ptr().is_null());
let big = SharedRawImageData::new(vec![7u8; 100_000].into());
assert_eq!(big.len(), 100_000);
assert!(!big.is_empty());
assert_eq!(big.as_ref().len(), big.len());
assert_eq!(big.get_bytes(), big.as_ref());
assert_eq!(
big.into_inner().expect("sole owner").as_ref().len(),
100_000
);
}
#[test]
fn app_config_create_registers_builtins_and_defaults() {
let config = AppConfig::create();
assert_eq!(config.log_level, AppLogLevel::Error);
assert!(
config.enable_visual_panic_hook,
"a release build is panic=abort: the crash dialog is the only report surface, on by default"
);
assert!(config.enable_logging_on_panic);
assert_eq!(
config.termination_behavior,
AppTerminationBehavior::EndProcess
);
assert!(config.routes.as_ref().is_empty());
assert!(matches!(
config.mock_css_environment,
OptionCssMockEnvironment::None
));
let libs = config.component_libraries.as_ref();
assert_eq!(libs.len(), 1);
assert_eq!(libs[0].name.as_str(), "builtin");
assert!(!libs[0].components.as_ref().is_empty());
}
#[test]
fn app_config_add_component_library_replaces_same_name() {
let register: crate::xml::RegisterComponentLibraryFnType =
crate::xml::register_builtin_components;
let mut config = AppConfig::create();
let n_builtin = config.component_libraries.as_ref()[0]
.components
.as_ref()
.len();
config.add_component_library(AzString::from_const_str("builtin"), register);
assert_eq!(config.component_libraries.as_ref().len(), 1);
assert_eq!(
config.component_libraries.as_ref()[0]
.components
.as_ref()
.len(),
n_builtin
);
config.add_component_library(AzString::from_const_str(""), register);
config.add_component_library(AzString::from_const_str("\u{1F600}"), register);
assert_eq!(config.component_libraries.as_ref().len(), 3);
assert_eq!(
config.component_libraries.as_ref()[2].name.as_str(),
"\u{1F600}"
);
}
#[test]
fn app_config_with_mock_environment_sets_the_option() {
let config = AppConfig::create().with_mock_environment(CssMockEnvironment::dark_theme());
match config.mock_css_environment {
OptionCssMockEnvironment::Some(env) => {
assert!(matches!(
env.theme,
azul_css::dynamic_selector::OptionThemeCondition::Some(
azul_css::dynamic_selector::ThemeCondition::Dark
)
));
}
OptionCssMockEnvironment::None => panic!("mock env must be Some"),
}
let config = AppConfig::create()
.with_mock_environment(CssMockEnvironment::linux())
.with_mock_environment(CssMockEnvironment::windows());
match config.mock_css_environment {
OptionCssMockEnvironment::Some(env) => assert!(matches!(
env.os,
azul_css::dynamic_selector::OptionOsCondition::Some(
azul_css::dynamic_selector::OsCondition::Windows
)
)),
OptionCssMockEnvironment::None => panic!("mock env must be Some"),
}
}
#[test]
fn css_mock_environment_presets_only_set_their_own_field() {
use azul_css::dynamic_selector::{
OptionOsCondition, OptionThemeCondition, OsCondition, ThemeCondition,
};
for (mock, os) in [
(CssMockEnvironment::linux(), OsCondition::Linux),
(CssMockEnvironment::windows(), OsCondition::Windows),
(CssMockEnvironment::macos(), OsCondition::MacOS),
] {
assert!(matches!(mock.os, OptionOsCondition::Some(o) if o == os));
assert!(matches!(mock.theme, OptionThemeCondition::None));
assert!(matches!(mock.viewport_width, azul_css::OptionF32::None));
}
assert!(matches!(
CssMockEnvironment::dark_theme().theme,
OptionThemeCondition::Some(ThemeCondition::Dark)
));
assert!(matches!(
CssMockEnvironment::light_theme().theme,
OptionThemeCondition::Some(ThemeCondition::Light)
));
assert!(matches!(
CssMockEnvironment::dark_theme().os,
OptionOsCondition::None
));
}
#[test]
fn css_mock_environment_apply_to_overrides_only_set_fields() {
use azul_css::dynamic_selector::{
BoolCondition, DynamicSelectorContext, OptionOsCondition, OptionThemeCondition,
OsCondition, ThemeCondition,
};
let mut ctx = DynamicSelectorContext::default();
let before_os = ctx.os;
let before_lang = ctx.language.clone();
let before_w = ctx.viewport_width;
CssMockEnvironment::default().apply_to(&mut ctx);
assert_eq!(ctx.os, before_os);
assert_eq!(ctx.language.as_str(), before_lang.as_str());
assert_eq!(ctx.viewport_width, before_w);
let mock = CssMockEnvironment {
os: OptionOsCondition::Some(OsCondition::Windows),
theme: OptionThemeCondition::Some(ThemeCondition::Dark),
language: azul_css::OptionString::Some(AzString::from_const_str("de-DE")),
viewport_width: azul_css::OptionF32::Some(f32::NAN),
viewport_height: azul_css::OptionF32::Some(f32::INFINITY),
prefers_reduced_motion: azul_css::OptionBool::Some(true),
prefers_high_contrast: azul_css::OptionBool::Some(false),
..Default::default()
};
let mut ctx = DynamicSelectorContext::default();
mock.apply_to(&mut ctx);
assert_eq!(ctx.os, OsCondition::Windows);
assert_eq!(ctx.theme, ThemeCondition::Dark);
assert_eq!(ctx.language.as_str(), "de-DE");
assert!(ctx.viewport_width.is_nan());
assert_eq!(ctx.viewport_height, f32::INFINITY);
assert_eq!(ctx.prefers_reduced_motion, BoolCondition::True);
assert_eq!(ctx.prefers_high_contrast, BoolCondition::False);
let mut ctx2 = ctx.clone();
mock.apply_to(&mut ctx2);
assert_eq!(ctx2.os, ctx.os);
assert_eq!(ctx2.theme, ctx.theme);
}
#[test]
fn brush_dab_coverage_boundaries_and_monotonicity() {
assert_eq!(brush_dab_coverage(0.0, 0.5), 1.0);
assert_eq!(brush_dab_coverage(1.0, 0.5), 0.0);
assert_eq!(brush_dab_coverage(-5.0, 0.5), 1.0);
assert_eq!(brush_dab_coverage(2.0, 0.5), 0.0);
assert_eq!(brush_dab_coverage(f32::INFINITY, 0.5), 0.0);
assert_eq!(brush_dab_coverage(f32::NEG_INFINITY, 0.5), 1.0);
let mut prev = f32::INFINITY;
for i in 0..=100 {
let t = i as f32 / 100.0;
let c = brush_dab_coverage(t, 0.5);
assert!(
(0.0..=1.0).contains(&c),
"coverage {c} out of range at t={t}"
);
assert!(c <= prev + 1.0e-6, "not monotonic at t={t}");
prev = c;
}
}
#[test]
fn brush_dab_coverage_hardness_limits_never_divide_by_zero() {
assert_eq!(brush_dab_coverage(0.5, 1.0), 1.0);
assert!(brush_dab_coverage(1.0, 1.0).is_finite());
assert_eq!(brush_dab_coverage(1.0, 1.0), 1.0); assert_eq!(brush_dab_coverage(2.0, 1.0), 0.0);
assert_eq!(brush_dab_coverage(0.5, -10.0), brush_dab_coverage(0.5, 0.0));
assert_eq!(brush_dab_coverage(0.5, 10.0), brush_dab_coverage(0.5, 1.0));
assert_eq!(
brush_dab_coverage(0.5, f32::NEG_INFINITY),
brush_dab_coverage(0.5, 0.0)
);
assert!(brush_dab_coverage(0.5, f32::INFINITY).is_finite());
}
#[test]
fn brush_dab_coverage_nan_propagates_without_panicking() {
assert!(brush_dab_coverage(f32::NAN, 0.5).is_nan());
assert!(brush_dab_coverage(0.5, f32::NAN).is_nan());
assert!(brush_dab_coverage(f32::NAN, f32::NAN).is_nan());
}
#[test]
fn normalize_u16_is_monotonic_and_saturating() {
assert_eq!(normalize_u16(u16::MIN), 0);
assert_eq!(normalize_u16(u16::MAX), u8::MAX);
let mut prev = 0u8;
for i in (0..=u16::MAX).step_by(97) {
let v = normalize_u16(i);
assert!(v >= prev, "normalize_u16 must be monotonic ({i} -> {v})");
prev = v;
}
}
#[test]
fn premultiply_alpha_ignores_non_4_byte_slices() {
for len in [0usize, 1, 2, 3, 5, 8] {
let mut buf = vec![200u8; len];
let before = buf.clone();
premultiply_alpha(&mut buf);
assert_eq!(buf, before, "len {len} must be left untouched");
}
}
#[test]
fn premultiply_alpha_boundary_values_never_overflow() {
let mut opaque = [255u8, 128, 0, 255];
premultiply_alpha(&mut opaque);
assert_eq!(opaque, [255, 128, 0, 255]);
let mut transparent = [255u8, 255, 255, 0];
premultiply_alpha(&mut transparent);
assert_eq!(transparent, [0, 0, 0, 0]);
let mut half = [255u8, 128, 0, 128];
premultiply_alpha(&mut half);
assert_eq!(half, [128, 64, 0, 128]);
let mut max = [255u8, 255, 255, 255];
premultiply_alpha(&mut max);
assert_eq!(max, [255, 255, 255, 255]);
}
#[test]
fn au_from_px_saturates_at_limits_and_nan() {
assert_eq!(Au::from_px(0.0).0, 0);
assert_eq!(Au::from_px(-0.0).0, 0);
assert_eq!(Au::from_px(1.0).0, AU_PER_PX);
assert_eq!(Au::from_px(-1.0).0, -AU_PER_PX);
assert_eq!(Au::from_px(f32::NAN).0, 0);
assert_eq!(Au::from_px(f32::INFINITY).0, MAX_AU);
assert_eq!(Au::from_px(f32::NEG_INFINITY).0, MIN_AU);
assert_eq!(Au::from_px(f32::MAX).0, MAX_AU);
assert_eq!(Au::from_px(f32::MIN).0, MIN_AU);
for px in [-1.0e9_f32, -1.0, 0.5, 16.0, 1.0e9] {
let au = Au::from_px(px).0;
assert!(
(MIN_AU..=MAX_AU).contains(&au),
"{px} -> {au} escaped the clamp"
);
}
}
#[test]
fn au_px_round_trip_is_stable() {
for px in [0.0_f32, 0.5, 1.0, 12.0, 16.0, 72.5, -3.25, 1000.0] {
let back = Au::from_px(px).into_px();
assert!(
(back - px).abs() <= 1.0 / AU_PER_PX as f32,
"{px} round-tripped to {back}"
);
}
assert_eq!(Au::from_px(16.0).into_px(), 16.0);
assert!(Au(MAX_AU).into_px().is_finite());
assert!(Au(MIN_AU).into_px().is_finite());
assert!(Au(i32::MIN).into_px().is_finite());
assert!(Au(i32::MAX).into_px().is_finite());
}
#[test]
fn font_size_to_au_zero_negative_and_typical() {
use azul_css::props::basic::PixelValue;
let au = |px: isize| {
font_size_to_au(StyleFontSize {
inner: PixelValue::const_px(px),
})
.0
};
assert_eq!(au(0), 0);
assert_eq!(au(16), 16 * AU_PER_PX);
assert_eq!(au(-10), -10 * AU_PER_PX);
assert!((MIN_AU..=MAX_AU).contains(&au(1_000_000)));
assert!((MIN_AU..=MAX_AU).contains(&au(-1_000_000)));
}
#[test]
fn epoch_new_from_and_into_u32() {
assert_eq!(Epoch::new().into_u32(), 0);
assert_eq!(Epoch::default().into_u32(), 0);
assert_eq!(Epoch::from(0).into_u32(), 0);
assert_eq!(Epoch::from(1).into_u32(), 1);
assert_eq!(Epoch::from(u32::MAX).into_u32(), u32::MAX);
assert_eq!(Epoch::from(u32::MAX - 1).into_u32(), u32::MAX - 1);
}
#[test]
fn epoch_increment_wraps_at_max_minus_one_and_never_reaches_max() {
let mut e = Epoch::new();
e.increment();
assert_eq!(e.into_u32(), 1);
let mut e = Epoch::from(u32::MAX - 1);
e.increment();
assert_eq!(e.into_u32(), 0, "MAX-1 must wrap to 0, never to u32::MAX");
let mut e = Epoch::from(u32::MAX);
e.increment();
assert_eq!(e.into_u32(), u32::MAX);
let mut e = Epoch::from(u32::MAX - 3);
for _ in 0..8 {
e.increment();
assert_ne!(e.into_u32(), u32::MAX);
}
}
#[test]
fn epoch_display_is_non_empty_for_edge_values() {
assert_eq!(alloc::format!("{}", Epoch::new()), "0");
assert_eq!(alloc::format!("{}", Epoch::from(42)), "42");
assert_eq!(
alloc::format!("{}", Epoch::from(u32::MAX)),
alloc::format!("{}", u32::MAX)
);
assert!(!alloc::format!("{:?}", Epoch::default()).is_empty());
}
#[test]
fn id_namespace_display_and_debug_are_well_formed() {
assert_eq!(alloc::format!("{}", IdNamespace(0)), "IdNamespace(0)");
assert_eq!(
alloc::format!("{}", IdNamespace(u32::MAX)),
alloc::format!("IdNamespace({})", u32::MAX)
);
assert_eq!(
alloc::format!("{:?}", IdNamespace(7)),
alloc::format!("{}", IdNamespace(7))
);
}
#[test]
fn unique_keys_are_strictly_increasing_and_keep_their_namespace() {
let ns = IdNamespace(u32::MAX);
let a = ImageKey::unique(ns);
let b = ImageKey::unique(ns);
assert_eq!(a.namespace, ns);
assert!(b.key > a.key, "ImageKey counter must strictly increase");
assert_eq!(ImageKey::DUMMY.key, 0);
assert_ne!(a, ImageKey::DUMMY);
let a = FontKey::unique(ns);
let b = FontKey::unique(ns);
assert_eq!(a.namespace, ns);
assert!(b.key > a.key);
let a = FontInstanceKey::unique(IdNamespace(0));
let b = FontInstanceKey::unique(IdNamespace(0));
assert_eq!(a.namespace, IdNamespace(0));
assert!(b.key > a.key);
}
#[test]
fn image_ref_id_counter_is_monotonic_and_never_zero() {
let a = next_image_ref_id();
let b = next_image_ref_id();
assert!(a > 0 && b > a);
}
#[test]
fn image_ref_hash_conversions_are_lossless_and_agree() {
let img = ImageRef::null_image(1, 1, RawImageFormat::RGBA8, Vec::new());
let hash = img.get_hash();
assert_eq!(hash, image_ref_get_hash(&img));
let key = image_ref_hash_to_image_key(hash, IdNamespace(9));
assert_eq!(key.namespace, IdNamespace(9));
assert_eq!(key.key, hash.inner, "the u64 id must survive verbatim");
let ext = image_ref_hash_to_external_image_id(hash);
assert_eq!(ext.inner, hash.inner);
for inner in [0u64, 1, u64::MAX, u64::MAX - 1] {
let h = ImageRefHash { inner };
assert_eq!(image_ref_hash_to_image_key(h, IdNamespace(0)).key, inner);
assert_eq!(image_ref_hash_to_external_image_id(h).inner, inner);
}
}
#[test]
fn texture_external_image_id_is_deterministic_and_collision_free() {
let id = |d: usize, n: usize| texture_external_image_id(DomId { inner: d }, NodeId::new(n));
assert_eq!(id(3, 7), id(3, 7));
assert_eq!(id(0, 0).inner, 0);
assert_eq!(id(1, 2).inner, (1u64 << 32) | 2);
assert_ne!(id(0, 1), id(1, 0));
assert_eq!(id(0, u32::MAX as usize).inner, u64::from(u32::MAX));
assert_eq!(id(u32::MAX as usize, 0).inner, u64::from(u32::MAX) << 32);
}
#[test]
fn raw_image_data_typed_getters_only_match_their_own_variant() {
let u8v = RawImageData::U8(vec![1u8, 2].into());
let u16v = RawImageData::U16(vec![1u16, 2].into());
let f32v = RawImageData::F32(vec![1.0f32, 2.0].into());
assert_eq!(u8v.get_u8_vec_ref().map(|v| v.len()), Some(2));
assert!(u8v.get_u16_vec_ref().is_none());
assert!(u8v.get_f32_vec_ref().is_none());
assert!(u16v.get_u8_vec_ref().is_none());
assert_eq!(u16v.get_u16_vec_ref().map(|v| v.len()), Some(2));
assert!(u16v.get_f32_vec_ref().is_none());
assert!(f32v.get_u8_vec_ref().is_none());
assert!(f32v.get_u16_vec_ref().is_none());
assert_eq!(f32v.get_f32_vec_ref().map(|v| v.len()), Some(2));
let empty = RawImageData::U8(U8Vec::from_vec(Vec::new()));
assert_eq!(empty.get_u8_vec_ref().map(|v| v.len()), Some(0));
assert!(RawImageData::U8(vec![9u8].into()).get_u8_vec().is_some());
assert!(RawImageData::U16(vec![9u16].into()).get_u8_vec().is_none());
assert!(RawImageData::U16(vec![9u16].into()).get_u16_vec().is_some());
assert!(RawImageData::F32(vec![9.0f32].into())
.get_u16_vec()
.is_none());
}
#[test]
fn load_fns_reject_wrong_payload_type() {
let u16_1px = || RawImageData::U16(vec![0u16; 4].into());
let f32_1px = || RawImageData::F32(vec![0.0f32; 4].into());
let u8_1px = || RawImageData::U8(vec![0u8; 4].into());
assert!(RawImage::load_r8(u16_1px(), 4).is_none());
assert!(RawImage::load_rg8(f32_1px(), 2, true).is_none());
assert!(RawImage::load_rgb8(u16_1px(), 1).is_none());
assert!(RawImage::load_rgba8(f32_1px(), 1, true).is_none());
assert!(RawImage::load_r16(u8_1px(), 4).is_none());
assert!(RawImage::load_rg16(f32_1px(), 2).is_none());
assert!(RawImage::load_rgb16(u8_1px(), 1).is_none());
assert!(RawImage::load_rgba16(u8_1px(), 1, true).is_none());
assert!(RawImage::load_bgr8(u16_1px(), 1).is_none());
assert!(RawImage::load_bgra8(u16_1px(), 1, true).is_none());
assert!(RawImage::load_rgbf32(u8_1px(), 1).is_none());
assert!(RawImage::load_rgbaf32(u16_1px(), 1, true).is_none());
}
#[test]
fn load_fns_reject_every_wrong_length() {
assert!(RawImage::load_r8(RawImageData::U8(vec![0u8; 3].into()), 4).is_none());
assert!(RawImage::load_r8(RawImageData::U8(vec![0u8; 5].into()), 4).is_none());
assert!(RawImage::load_rg8(RawImageData::U8(vec![0u8; 3].into()), 2, true).is_none());
assert!(RawImage::load_rg8(RawImageData::U8(vec![0u8; 5].into()), 2, true).is_none());
assert!(RawImage::load_rgb8(RawImageData::U8(vec![0u8; 5].into()), 2).is_none());
assert!(RawImage::load_rgb8(RawImageData::U8(vec![0u8; 7].into()), 2).is_none());
assert!(RawImage::load_rgba8(RawImageData::U8(vec![0u8; 7].into()), 2, true).is_none());
assert!(RawImage::load_rgba8(RawImageData::U8(vec![0u8; 9].into()), 2, false).is_none());
assert!(RawImage::load_r16(RawImageData::U16(vec![0u16; 3].into()), 4).is_none());
assert!(RawImage::load_rg16(RawImageData::U16(vec![0u16; 3].into()), 2).is_none());
assert!(RawImage::load_rgb16(RawImageData::U16(vec![0u16; 5].into()), 2).is_none());
assert!(RawImage::load_rgba16(RawImageData::U16(vec![0u16; 7].into()), 2, true).is_none());
assert!(RawImage::load_bgr8(RawImageData::U8(vec![0u8; 5].into()), 2).is_none());
assert!(RawImage::load_bgra8(RawImageData::U8(vec![0u8; 7].into()), 2, false).is_none());
assert!(RawImage::load_rgbf32(RawImageData::F32(vec![0.0f32; 5].into()), 2).is_none());
assert!(
RawImage::load_rgbaf32(RawImageData::F32(vec![0.0f32; 7].into()), 2, true).is_none()
);
}
#[test]
fn load_fns_accept_zero_pixels() {
let empty_u8 = || RawImageData::U8(U8Vec::from_vec(Vec::new()));
let empty_u16 = || RawImageData::U16(U16Vec::from_vec(Vec::new()));
let empty_f32 = || RawImageData::F32(F32Vec::from_vec(Vec::new()));
assert_eq!(
RawImage::load_r8(empty_u8(), 0).map(|(b, o)| (b.len(), o)),
Some((0, false))
);
assert_eq!(
RawImage::load_rg8(empty_u8(), 0, true).map(|(b, _)| b.len()),
Some(0)
);
assert_eq!(
RawImage::load_rgb8(empty_u8(), 0).map(|(b, _)| b.len()),
Some(0)
);
assert_eq!(
RawImage::load_rgba8(empty_u8(), 0, true).map(|(b, _)| b.len()),
Some(0)
);
assert_eq!(
RawImage::load_r16(empty_u16(), 0).map(|(b, _)| b.len()),
Some(0)
);
assert_eq!(
RawImage::load_rg16(empty_u16(), 0).map(|(b, _)| b.len()),
Some(0)
);
assert_eq!(
RawImage::load_rgb16(empty_u16(), 0).map(|(b, _)| b.len()),
Some(0)
);
assert_eq!(
RawImage::load_rgba16(empty_u16(), 0, true).map(|(b, _)| b.len()),
Some(0)
);
assert_eq!(
RawImage::load_bgr8(empty_u8(), 0).map(|(b, _)| b.len()),
Some(0)
);
assert_eq!(
RawImage::load_bgra8(empty_u8(), 0, true).map(|(b, _)| b.len()),
Some(0)
);
assert_eq!(
RawImage::load_rgbf32(empty_f32(), 0).map(|(b, _)| b.len()),
Some(0)
);
assert_eq!(
RawImage::load_rgbaf32(empty_f32(), 0, true).map(|(b, _)| b.len()),
Some(0)
);
}
#[test]
fn load_r8_passes_data_through_and_is_never_opaque() {
let (bytes, is_opaque) =
RawImage::load_r8(RawImageData::U8(vec![0u8, 128, 255, 1].into()), 4)
.expect("exact length");
assert_eq!(bytes.as_ref(), &[0, 128, 255, 1]);
assert!(!is_opaque, "R8 is documented as never opaque");
}
#[test]
fn load_rgb8_and_bgr8_swizzle_to_bgra_opaque() {
let (bytes, is_opaque) =
RawImage::load_rgb8(RawImageData::U8(vec![1u8, 2, 3].into()), 1).expect("1 px");
assert_eq!(bytes.as_ref(), &[3, 2, 1, 255]);
assert!(is_opaque);
let (bytes, is_opaque) =
RawImage::load_bgr8(RawImageData::U8(vec![1u8, 2, 3].into()), 1).expect("1 px");
assert_eq!(bytes.as_ref(), &[1, 2, 3, 255]);
assert!(is_opaque);
}
#[test]
fn load_rgba8_swizzles_and_detects_transparency() {
let (bytes, is_opaque) =
RawImage::load_rgba8(RawImageData::U8(vec![10u8, 20, 30, 255].into()), 1, true)
.expect("1 px");
assert_eq!(bytes.as_ref(), &[30, 20, 10, 255]);
assert!(is_opaque);
let (_, is_opaque) =
RawImage::load_rgba8(RawImageData::U8(vec![0u8, 0, 0, 254].into()), 1, true)
.expect("1 px");
assert!(!is_opaque);
let (bytes, is_opaque) =
RawImage::load_rgba8(RawImageData::U8(vec![10u8, 20, 30, 128].into()), 1, false)
.expect("1 px");
assert_eq!(bytes.as_ref(), &[15, 10, 5, 128]);
assert!(!is_opaque);
let (bytes, _) =
RawImage::load_rgba8(RawImageData::U8(vec![255u8, 255, 255, 0].into()), 1, false)
.expect("1 px");
assert_eq!(bytes.as_ref(), &[0, 0, 0, 0]);
}
#[test]
fn load_rg8_expands_grey_to_bgra() {
let (bytes, is_opaque) =
RawImage::load_rg8(RawImageData::U8(vec![100u8, 255].into()), 1, true).expect("1 px");
assert_eq!(bytes.as_ref(), &[100, 100, 100, 255]);
assert!(is_opaque);
let (bytes, is_opaque) =
RawImage::load_rg8(RawImageData::U8(vec![100u8, 128].into()), 1, false).expect("1 px");
assert_eq!(bytes.as_ref(), &[50, 50, 50, 128]);
assert!(!is_opaque);
}
#[test]
fn load_16_bit_formats_normalize_to_8_bit() {
let (bytes, is_opaque) =
RawImage::load_r16(RawImageData::U16(vec![u16::MAX].into()), 1).expect("1 px");
assert_eq!(bytes.as_ref(), &[255, 255, 255, 255]);
assert!(is_opaque);
let (bytes, is_opaque) =
RawImage::load_rg16(RawImageData::U16(vec![0u16, u16::MAX].into()), 1).expect("1 px");
assert_eq!(bytes.as_ref(), &[0, 0, 0, 255]);
assert!(is_opaque);
let (bytes, _) =
RawImage::load_rgb16(RawImageData::U16(vec![u16::MAX, 0, 0].into()), 1).expect("1 px");
assert_eq!(bytes.as_ref(), &[0, 0, 255, 255]);
let (bytes, is_opaque) = RawImage::load_rgba16(
RawImageData::U16(vec![u16::MAX, u16::MAX, u16::MAX, 0].into()),
1,
false,
)
.expect("1 px");
assert_eq!(bytes.as_ref(), &[0, 0, 0, 0]);
assert!(!is_opaque);
}
#[test]
fn load_f32_formats_saturate_on_out_of_range_nan_and_inf() {
let (bytes, is_opaque) =
RawImage::load_rgbf32(RawImageData::F32(vec![2.0f32, -1.0, f32::NAN].into()), 1)
.expect("1 px");
assert_eq!(
bytes.as_ref(),
&[0, 0, 255, 255],
"b=NaN->0, g=-1->0, r=2.0->255"
);
assert!(is_opaque);
let (bytes, is_opaque) = RawImage::load_rgbaf32(
RawImageData::F32(vec![f32::INFINITY, f32::NEG_INFINITY, 0.5, 1.0].into()),
1,
true,
)
.expect("1 px");
assert_eq!(bytes.as_ref(), &[127, 0, 255, 255]);
assert!(is_opaque);
let (_, is_opaque) = RawImage::load_rgbaf32(
RawImageData::F32(vec![1.0f32, 1.0, 1.0, f32::NAN].into()),
1,
true,
)
.expect("1 px");
assert!(!is_opaque);
}
#[test]
fn raw_image_null_image_encodes_to_an_empty_bgra8_descriptor() {
let null = RawImage::null_image();
assert_eq!(null.width, 0);
assert_eq!(null.height, 0);
assert_eq!(null.data_format, RawImageFormat::BGRA8);
assert!(null.premultiplied_alpha);
let (data, descriptor) = null
.into_loaded_image_source()
.expect("a 0x0 image is still a valid (empty) source");
assert_eq!(descriptor.width, 0);
assert_eq!(descriptor.height, 0);
assert_eq!(descriptor.format, RawImageFormat::BGRA8);
assert_eq!(descriptor.offset, 0);
match data {
ImageData::Raw(bytes) => assert!(bytes.is_empty()),
ImageData::External(_) => panic!("a RawImage must never encode to External"),
}
}
#[test]
fn raw_image_allocate_mask_zero_and_negative_sizes() {
let mask = RawImage::allocate_mask(LayoutSize::zero());
assert_eq!(mask.data_format, RawImageFormat::R8);
assert_eq!(mask.width, 0);
assert_eq!(mask.height, 0);
assert_eq!(mask.pixels.get_u8_vec_ref().map(|v| v.len()), Some(0));
let mask = RawImage::allocate_mask(LayoutSize::new(4, 4));
assert_eq!(mask.pixels.get_u8_vec_ref().map(|v| v.len()), Some(16));
assert!(mask
.pixels
.get_u8_vec_ref()
.expect("u8")
.as_ref()
.iter()
.all(|b| *b == 0));
let mask = RawImage::allocate_mask(LayoutSize::new(-4, 4));
assert_eq!(
mask.pixels.get_u8_vec_ref().map(|v| v.len()),
Some(0),
"a negative extent must never allocate"
);
assert!(
mask.width > 1_000_000,
"negative width wraps via `as usize`"
);
}
#[test]
fn raw_image_mask_round_trips_as_r8() {
let mask = RawImage::allocate_mask(LayoutSize::new(2, 2));
let (data, descriptor) = mask.into_loaded_image_source().expect("consistent mask");
assert_eq!(descriptor.format, RawImageFormat::R8);
assert_eq!((descriptor.width, descriptor.height), (2, 2));
assert!(!descriptor.flags.is_opaque, "R8 is never opaque");
match data {
ImageData::Raw(bytes) => assert_eq!(bytes.len(), 4),
ImageData::External(_) => panic!("expected raw bytes"),
}
}
#[test]
fn raw_image_rgba8_encode_decode_round_trip() {
let raw = RawImage {
pixels: RawImageData::U8(vec![10u8, 20, 30, 255].into()),
width: 1,
height: 1,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
};
let img = ImageRef::new_rawimage(raw).expect("1x1 RGBA8 with 4 bytes is valid");
assert!(img.is_raw_image());
assert!(!img.is_null_image());
assert!(!img.is_gl_texture());
assert!(!img.is_callback());
assert_eq!(img.get_size(), LogicalSize::new(1.0, 1.0));
assert_eq!(img.get_bytes(), Some(&[30u8, 20, 10, 255][..]));
assert!(!img.get_bytes_ptr().is_null());
let decoded = img.get_rawimage().expect("raw image round-trips");
assert_eq!(decoded.width, 1);
assert_eq!(decoded.height, 1);
assert_eq!(decoded.data_format, RawImageFormat::BGRA8);
assert!(decoded.premultiplied_alpha);
assert_eq!(
decoded.pixels.get_u8_vec_ref().map(|v| v.as_ref().to_vec()),
Some(vec![30, 20, 10, 255])
);
}
#[test]
fn image_ref_new_rawimage_rejects_dimension_mismatch() {
let too_small = RawImage {
pixels: RawImageData::U8(vec![0u8; 4].into()),
width: 2,
height: 2,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
};
assert!(ImageRef::new_rawimage(too_small).is_none());
let too_big = RawImage {
pixels: RawImageData::U8(vec![0u8; 64].into()),
width: 2,
height: 2,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
};
assert!(ImageRef::new_rawimage(too_big).is_none());
let wrong_type = RawImage {
pixels: RawImageData::U16(vec![0u16; 16].into()),
width: 2,
height: 2,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
};
assert!(ImageRef::new_rawimage(wrong_type).is_none());
}
#[test]
fn image_ref_null_image_predicates_and_accessors() {
let img = ImageRef::null_image(0, 0, RawImageFormat::BGRA8, Vec::new());
assert!(img.is_null_image());
assert!(!img.is_raw_image());
assert!(!img.is_gl_texture());
assert!(!img.is_callback());
assert_eq!(img.get_size(), LogicalSize::new(0.0, 0.0));
assert!(img.get_bytes().is_none());
assert!(img.get_rawimage().is_none());
assert!(img.get_bytes_ptr().is_null());
assert!(img.get_image_callback().is_none());
assert!(matches!(img.get_data(), DecodedImage::NullImage { .. }));
}
#[test]
fn image_ref_null_image_at_usize_max_reports_a_finite_size() {
let img = ImageRef::null_image(usize::MAX, usize::MAX, RawImageFormat::R8, Vec::new());
let size = img.get_size();
assert!(size.width.is_finite() && size.height.is_finite());
assert!(size.width > 0.0 && size.height > 0.0);
assert!(img.is_null_image());
let img = ImageRef::null_image(1, 1, RawImageFormat::R8, vec![9u8; 10_000]);
match img.get_data() {
DecodedImage::NullImage { tag, .. } => assert_eq!(tag.len(), 10_000),
_ => panic!("expected NullImage"),
}
}
#[test]
fn image_ref_hash_identity_rules() {
let a = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
let b = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
assert_ne!(a.get_hash(), b.get_hash());
assert_ne!(a, b);
let a2 = a.clone();
assert_eq!(a.get_hash(), a2.get_hash());
assert_eq!(a, a2);
let deep = a.deep_copy();
assert_ne!(a.get_hash(), deep.get_hash());
assert!(deep.is_null_image());
assert_eq!(deep.get_size(), a.get_size());
}
#[test]
fn image_ref_callback_accessors() {
let mut img = ImageRef::callback(0usize, RefAny::new(123u32));
assert!(img.is_callback());
assert!(!img.is_null_image());
assert!(!img.is_raw_image());
assert_eq!(img.get_size(), LogicalSize::new(0.0, 0.0));
assert!(img.get_bytes().is_none());
assert!(img.get_bytes_ptr().is_null());
assert!(img.get_rawimage().is_none());
assert!(img.get_image_callback().is_some());
assert!(img.get_image_callback_mut().is_some());
let clone = img.clone();
assert!(img.get_image_callback().is_none());
assert!(img.get_image_callback_mut().is_none());
drop(clone);
assert!(img.get_image_callback().is_some());
}
#[test]
fn image_ref_deep_copy_of_a_callback_keeps_it_a_callback() {
let img = ImageRef::callback(0usize, RefAny::new(1u8));
let deep = img.deep_copy();
assert!(deep.is_callback());
assert_ne!(img.get_hash(), deep.get_hash());
}
#[test]
fn image_ref_into_inner_only_when_sole_owner() {
let img = ImageRef::null_image(2, 2, RawImageFormat::RGBA8, vec![1, 2, 3]);
let clone = img.clone();
assert!(clone.into_inner().is_none(), "shared -> must refuse");
let inner = img.into_inner().expect("sole owner -> takes ownership");
match inner {
DecodedImage::NullImage {
width,
height,
format,
tag,
} => {
assert_eq!((width, height), (2, 2));
assert_eq!(format, RawImageFormat::RGBA8);
assert_eq!(tag, vec![1, 2, 3]);
}
_ => panic!("expected NullImage"),
}
}
#[test]
fn image_cache_add_get_delete_round_trip() {
let mut cache = ImageCache::new();
let key = AzString::from_const_str("my_image");
let img = ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new());
let hash = img.get_hash();
assert!(cache.get_css_image_id(&key).is_none());
cache.add_css_image_id(key.clone(), img);
assert_eq!(
cache.get_css_image_id(&key).map(ImageRef::get_hash),
Some(hash)
);
let img2 = ImageRef::null_image(2, 2, RawImageFormat::R8, Vec::new());
let hash2 = img2.get_hash();
cache.add_css_image_id(key.clone(), img2);
assert_eq!(cache.image_id_map.len(), 1);
assert_eq!(
cache.get_css_image_id(&key).map(ImageRef::get_hash),
Some(hash2)
);
cache.delete_css_image_id(&key);
assert!(cache.get_css_image_id(&key).is_none());
assert!(cache.image_id_map.is_empty());
cache.delete_css_image_id(&key);
cache.delete_css_image_id(&AzString::from_const_str("never-existed"));
}
#[test]
fn image_cache_handles_empty_and_unicode_keys() {
let mut cache = ImageCache::new();
let empty = AzString::from_const_str("");
let unicode = AzString::from(String::from("\u{1F600}\u{0301}"));
let long = AzString::from("k".repeat(100_000));
cache.add_css_image_id(
empty.clone(),
ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new()),
);
cache.add_css_image_id(
unicode.clone(),
ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new()),
);
cache.add_css_image_id(
long.clone(),
ImageRef::null_image(1, 1, RawImageFormat::R8, Vec::new()),
);
assert_eq!(cache.image_id_map.len(), 3);
assert!(cache.get_css_image_id(&empty).is_some());
assert!(cache.get_css_image_id(&unicode).is_some());
assert!(cache.get_css_image_id(&long).is_some());
assert!(cache
.get_css_image_id(&AzString::from_const_str("\u{1F600}"))
.is_none());
}
#[test]
fn renderer_resources_lookups_on_an_empty_registry_are_none() {
let rr = RendererResources::default();
let ns = IdNamespace(1);
assert!(rr
.get_renderable_font_data(&FontInstanceKey::unique(ns))
.is_none());
let families = StyleFontFamiliesHash::new(&[]);
assert!(rr
.get_font_instance_key(&families, Au(0), DpiScaleFactor::new(1.0))
.is_none());
assert!(rr
.get_font_instance_key(&families, Au(MAX_AU), DpiScaleFactor::new(f32::NAN))
.is_none());
assert!(rr.get_image(&ImageRefHash { inner: 0 }).is_none());
assert!(rr
.get_font_key(&StyleFontFamilyHash::new(&StyleFontFamily::System(
AzString::from_const_str("Arial")
)))
.is_none());
}
#[test]
fn renderer_resources_gc_helper_is_a_noop_on_empty_maps() {
let mut rr = RendererResources::default();
rr.remove_font_families_with_zero_references();
assert!(rr.font_id_map.is_empty());
assert!(rr.font_families_map.is_empty());
let family = StyleFontFamily::System(AzString::from_const_str("Arial"));
let family_hash = StyleFontFamilyHash::new(&family);
let families_hash = StyleFontFamiliesHash::new(core::slice::from_ref(&family));
rr.font_id_map
.insert(family_hash, FontKey::unique(IdNamespace(1)));
rr.font_families_map.insert(families_hash, family_hash);
rr.remove_font_families_with_zero_references();
assert!(
rr.font_id_map.is_empty(),
"dangling font key must be pruned"
);
assert!(rr.font_families_map.is_empty());
}
#[test]
fn get_font_instance_key_for_text_is_none_on_empty_resources_for_all_sane_sizes() {
let rr = RendererResources::default();
let cache = CssPropertyCache::default();
let node = NodeData::default();
let node_id = NodeId::new(0);
let state = StyledNodeState::default();
for size in [0.0_f32, -0.0, 1.0, -12.0, f32::NAN, 1.0e6, -1.0e6] {
for dpi in [1.0_f32, 0.0, -1.0, f32::NAN, f32::INFINITY] {
assert!(
rr.get_font_instance_key_for_text(size, &cache, &node, &node_id, &state, dpi)
.is_none(),
"size={size} dpi={dpi} must miss cleanly"
);
}
}
}
#[test]
fn bug_get_font_instance_key_for_text_overflow_panics_on_infinite_font_size() {
let rr = RendererResources::default();
let cache = CssPropertyCache::default();
let node = NodeData::default();
let node_id = NodeId::new(0);
let state = StyledNodeState::default();
assert!(rr
.get_font_instance_key_for_text(f32::INFINITY, &cache, &node, &node_id, &state, 1.0)
.is_none());
}
#[test]
fn font_ref_get_hash_is_stable_per_font_and_distinct_across_fonts() {
let a = dummy_font_ref();
let b = dummy_font_ref();
assert_eq!(font_ref_get_hash(&a), font_ref_get_hash(&a));
assert_eq!(font_ref_get_hash(&a), font_ref_get_hash(&a.clone()));
assert_ne!(
font_ref_get_hash(&a),
font_ref_get_hash(&b),
"two distinct FontRefs must not share a hash"
);
}
#[test]
fn build_add_font_resource_updates_on_empty_input_is_empty() {
let mut rr = RendererResources::default();
let fonts = OrderedMap::new();
let updates = build_add_font_resource_updates(
&mut rr,
DpiScaleFactor::new(1.0),
&FcFontCache::default(),
IdNamespace(1),
&fonts,
load_font_none,
parse_font_none,
);
assert!(updates.is_empty());
assert!(rr.font_id_map.is_empty());
}
#[test]
fn build_add_font_resource_updates_skips_unloadable_fonts() {
let mut rr = RendererResources::default();
let mut fonts = OrderedMap::new();
let mut sizes = FastBTreeSet::new();
sizes.insert(Au::from_px(16.0));
fonts.insert(
ImmediateFontId::Unresolved(StyleFontFamilyVec::from_vec(vec![
StyleFontFamily::System(AzString::from_const_str("DoesNotExist")),
])),
sizes,
);
let updates = build_add_font_resource_updates(
&mut rr,
DpiScaleFactor::new(1.0),
&FcFontCache::default(),
IdNamespace(1),
&fonts,
load_font_none,
parse_font_none,
);
assert!(
updates.is_empty(),
"an unloadable font must add no resources"
);
assert!(rr.font_id_map.is_empty());
assert!(rr.font_families_map.is_empty());
}
#[test]
fn build_add_font_resource_updates_registers_a_font_and_deduplicates_sizes() {
let mut rr = RendererResources::default();
let font = dummy_font_ref();
let family = StyleFontFamily::Ref(font.clone());
let dpi = DpiScaleFactor::new(1.0);
let mut sizes = FastBTreeSet::new();
sizes.insert(Au::from_px(16.0));
sizes.insert(Au::from_px(24.0));
sizes.insert(Au::from_px(16.0)); assert_eq!(sizes.len(), 2);
let mut fonts = OrderedMap::new();
fonts.insert(
ImmediateFontId::Unresolved(StyleFontFamilyVec::from_vec(vec![family.clone()])),
sizes,
);
let updates = build_add_font_resource_updates(
&mut rr,
dpi,
&FcFontCache::default(),
IdNamespace(1),
&fonts,
load_font_none,
parse_font_none,
);
assert_eq!(updates.len(), 3);
assert_eq!(
updates
.iter()
.filter(|(_, m)| matches!(m, AddFontMsg::Font(..)))
.count(),
1
);
assert_eq!(
updates
.iter()
.filter(|(_, m)| matches!(m, AddFontMsg::Instance(..)))
.count(),
2
);
assert_eq!(rr.font_id_map.len(), 1);
assert_eq!(rr.font_families_map.len(), 1);
let mut all_updates = Vec::new();
add_resources(&mut rr, &mut all_updates, updates, Vec::new());
assert_eq!(all_updates.len(), 3);
let families_hash = StyleFontFamiliesHash::new(core::slice::from_ref(&family));
assert!(rr
.get_font_instance_key(&families_hash, Au::from_px(16.0), dpi)
.is_some());
assert!(rr
.get_font_instance_key(&families_hash, Au::from_px(24.0), dpi)
.is_some());
assert!(rr
.get_font_instance_key(&families_hash, Au::from_px(99.0), dpi)
.is_none());
assert!(rr
.get_font_instance_key(&families_hash, Au::from_px(16.0), DpiScaleFactor::new(2.0))
.is_none());
let key = rr
.get_font_instance_key(&families_hash, Au::from_px(16.0), dpi)
.expect("registered");
let (font_ref, au, got_dpi) = rr
.get_renderable_font_data(&key)
.expect("registered instance must be renderable");
assert_eq!(font_ref.get_hash(), font.get_hash());
assert_eq!(au, Au::from_px(16.0));
assert_eq!(got_dpi, dpi);
let again = build_add_font_resource_updates(
&mut rr,
dpi,
&FcFontCache::default(),
IdNamespace(1),
&fonts,
load_font_none,
parse_font_none,
);
assert!(
again.is_empty(),
"already-registered fonts must not be re-added"
);
}
#[test]
fn add_font_msg_into_resource_update_preserves_keys() {
let font = dummy_font_ref();
let key = FontKey::unique(IdNamespace(3));
let family_hash = StyleFontFamilyHash::new(&StyleFontFamily::Ref(font.clone()));
let msg = AddFontMsg::Font(key, family_hash, font.clone());
match msg.into_resource_update() {
ResourceUpdate::AddFont(add) => {
assert_eq!(add.key, key);
assert_eq!(add.font.get_hash(), font.get_hash());
}
other => panic!("expected AddFont, got {other:?}"),
}
}
#[test]
fn delete_font_msg_into_resource_update_preserves_keys() {
let fk = FontKey::unique(IdNamespace(1));
match DeleteFontMsg::Font(fk).into_resource_update() {
ResourceUpdate::DeleteFont(k) => assert_eq!(k, fk),
other => panic!("expected DeleteFont, got {other:?}"),
}
let fik = FontInstanceKey::unique(IdNamespace(1));
let size = (Au::from_px(16.0), DpiScaleFactor::new(1.0));
match DeleteFontMsg::Instance(fik, size).into_resource_update() {
ResourceUpdate::DeleteFontInstance(k) => assert_eq!(k, fik),
other => panic!("expected DeleteFontInstance, got {other:?}"),
}
}
#[test]
fn add_image_msg_into_resource_update_preserves_the_key_and_descriptor() {
let key = ImageKey::unique(IdNamespace(2));
let descriptor = ImageDescriptor {
format: RawImageFormat::BGRA8,
width: 3,
height: 5,
stride: None.into(),
offset: 0,
flags: ImageDescriptorFlags {
is_opaque: false,
allow_mipmaps: true,
},
};
let msg = AddImageMsg(AddImage {
key,
descriptor,
data: ImageData::Raw(SharedRawImageData::new(vec![0u8; 60].into())),
tiling: None,
});
match msg.into_resource_update() {
ResourceUpdate::AddImage(add) => {
assert_eq!(add.key, key);
assert_eq!(add.descriptor, descriptor);
assert!(add.tiling.is_none());
}
other => panic!("expected AddImage, got {other:?}"),
}
}
#[test]
fn build_add_image_resource_updates_skips_null_and_callback_images() {
let rr = RendererResources::default();
let mut images = FastBTreeSet::new();
images.insert(ImageRef::null_image(
4,
4,
RawImageFormat::RGBA8,
Vec::new(),
));
images.insert(ImageRef::callback(0usize, RefAny::new(0u8)));
let updates = build_add_image_resource_updates(
&rr,
IdNamespace(1),
Epoch::new(),
&test_document_id(),
&images,
store_gl_texture_noop,
);
assert!(updates.is_empty());
let empty = FastBTreeSet::new();
assert!(build_add_image_resource_updates(
&rr,
IdNamespace(1),
Epoch::new(),
&test_document_id(),
&empty,
store_gl_texture_noop,
)
.is_empty());
}
#[test]
fn build_add_image_resource_updates_then_add_resources_round_trip() {
let mut rr = RendererResources::default();
let img = ImageRef::new_rawimage(rgba8_image(2, 2)).expect("valid 2x2");
let hash = img.get_hash();
let ns = IdNamespace(11);
let mut images = FastBTreeSet::new();
images.insert(img.clone());
let updates = build_add_image_resource_updates(
&rr,
ns,
Epoch::new(),
&test_document_id(),
&images,
store_gl_texture_noop,
);
assert_eq!(updates.len(), 1);
assert_eq!(updates[0].0, hash);
assert_eq!(updates[0].1 .0.key, image_ref_hash_to_image_key(hash, ns));
assert_eq!(updates[0].1 .0.descriptor.width, 2);
assert_eq!(updates[0].1 .0.descriptor.height, 2);
let key = updates[0].1 .0.key;
let mut all_updates = Vec::new();
add_resources(&mut rr, &mut all_updates, Vec::new(), updates);
assert_eq!(all_updates.len(), 1);
assert!(matches!(all_updates[0], ResourceUpdate::AddImage(_)));
assert_eq!(rr.get_image(&hash).map(|r| r.key), Some(key));
assert_eq!(rr.image_key_map.get(&key), Some(&hash));
let again = build_add_image_resource_updates(
&rr,
ns,
Epoch::new(),
&test_document_id(),
&images,
store_gl_texture_noop,
);
assert!(again.is_empty());
let new_descriptor = ImageDescriptor {
format: RawImageFormat::BGRA8,
width: 8,
height: 8,
stride: None.into(),
offset: 0,
flags: ImageDescriptorFlags {
is_opaque: true,
allow_mipmaps: true,
},
};
rr.update_image(&hash, new_descriptor);
assert_eq!(rr.get_image(&hash).map(|r| r.descriptor.width), Some(8));
assert_eq!(rr.get_image(&hash).map(|r| r.key), Some(key));
rr.update_image(&ImageRefHash { inner: u64::MAX }, new_descriptor);
}
#[test]
fn add_resources_with_empty_input_changes_nothing() {
let mut rr = RendererResources::default();
let mut updates = Vec::new();
add_resources(&mut rr, &mut updates, Vec::new(), Vec::new());
assert!(updates.is_empty());
assert!(rr.currently_registered_images.is_empty());
assert!(rr.currently_registered_fonts.is_empty());
assert!(rr.image_key_map.is_empty());
}
#[test]
fn paint_dot_composites_at_the_center_and_leaves_far_pixels_alone() {
let mut img = rgba8_image(4, 4);
img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), 2.0));
let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
let idx = (4 + 1) * 4;
assert_eq!(
&px[idx..idx + 4],
&[255, 0, 0, 255],
"center pixel must be opaque red"
);
assert_eq!(
&px[0..4],
&[0, 0, 0, 0],
"pixels beyond the radius stay untouched"
);
}
#[test]
fn paint_dot_honours_bgra_channel_order() {
let mut img = rgba8_image(4, 4);
img.data_format = RawImageFormat::BGRA8;
img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), 2.0));
let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
let idx = (4 + 1) * 4;
assert_eq!(&px[idx..idx + 4], &[0, 0, 255, 255]);
}
#[test]
fn paint_dot_rejects_degenerate_radii_and_sizes() {
let untouched = |img: &RawImage| {
img.pixels
.get_u8_vec_ref()
.expect("u8")
.as_ref()
.iter()
.all(|b| *b == 0)
};
for r in [0.0_f32, -1.0, -0.0, f32::NAN, f32::NEG_INFINITY] {
let mut img = rgba8_image(4, 4);
img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), r));
assert!(untouched(&img), "radius {r} must not paint");
}
let mut img = rgba8_image(0, 0);
img.paint_dot(0.0, 0.0, Brush::new(opaque_red(), 4.0));
assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(0));
for format in [
RawImageFormat::R8,
RawImageFormat::RGB8,
RawImageFormat::RGBA16,
RawImageFormat::RGBAF32,
] {
let mut img = rgba8_image(4, 4);
img.data_format = format;
img.paint_dot(2.0, 2.0, Brush::new(opaque_red(), 2.0));
assert!(untouched(&img), "format {format:?} must not be painted");
}
}
#[test]
fn paint_dot_with_nan_and_infinite_coordinates_is_a_safe_noop() {
for (cx, cy) in [
(f32::NAN, 2.0_f32),
(2.0, f32::NAN),
(f32::NAN, f32::NAN),
(f32::INFINITY, 2.0),
(f32::NEG_INFINITY, 2.0),
(2.0, f32::INFINITY),
(1.0e30, 1.0e30),
(-1.0e30, -1.0e30),
] {
let mut img = rgba8_image(4, 4);
img.paint_dot(cx, cy, Brush::new(opaque_red(), 2.0));
let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
assert!(
px.iter().all(|b| *b == 0),
"({cx}, {cy}) must not paint anything"
);
}
}
#[test]
fn paint_dot_alpha_saturates_and_never_overflows() {
let mut img = rgba8_image(4, 4);
let brush = Brush::new(opaque_red(), 2.0);
for _ in 0..50 {
img.paint_dot(2.0, 2.0, brush);
}
let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
let idx = (4 + 1) * 4;
assert_eq!(&px[idx..idx + 4], &[255, 0, 0, 255]);
let mut img = rgba8_image(4, 4);
let mut nan_brush = Brush::new(opaque_red(), 2.0);
nan_brush.hardness = f32::NAN;
img.paint_dot(2.0, 2.0, nan_brush);
assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(64));
}
#[test]
fn paint_dot_zero_flow_and_transparent_color_do_not_paint() {
let mut img = rgba8_image(4, 4);
let mut brush = Brush::new(opaque_red(), 2.0);
brush.flow = 0.0;
img.paint_dot(2.0, 2.0, brush);
assert!(img
.pixels
.get_u8_vec_ref()
.expect("u8")
.as_ref()
.iter()
.all(|b| *b == 0));
let mut img = rgba8_image(4, 4);
let transparent = ColorU {
r: 255,
g: 0,
b: 0,
a: 0,
};
img.paint_dot(2.0, 2.0, Brush::new(transparent, 2.0));
assert!(img
.pixels
.get_u8_vec_ref()
.expect("u8")
.as_ref()
.iter()
.all(|b| *b == 0));
let mut img = rgba8_image(4, 4);
let mut brush = Brush::new(opaque_red(), 2.0);
brush.flow = -5.0;
img.paint_dot(2.0, 2.0, brush);
assert!(img
.pixels
.get_u8_vec_ref()
.expect("u8")
.as_ref()
.iter()
.all(|b| *b == 0));
}
#[test]
fn paint_stroke_paints_both_endpoints() {
let mut img = rgba8_image(8, 8);
img.paint_stroke(1.5, 1.5, 6.5, 6.5, Brush::new(opaque_red(), 1.5));
let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
let alpha_at = |x: usize, y: usize| px[(y * 8 + x) * 4 + 3];
assert!(alpha_at(1, 1) > 0, "start of the stroke must be painted");
assert!(alpha_at(6, 6) > 0, "end of the stroke must be painted");
assert_eq!(alpha_at(7, 0), 0, "off-line pixels stay untouched");
}
#[test]
fn paint_stroke_zero_length_stamps_a_single_dab() {
let mut img = rgba8_image(4, 4);
img.paint_stroke(2.0, 2.0, 2.0, 2.0, Brush::new(opaque_red(), 2.0));
let px = img.pixels.get_u8_vec_ref().expect("u8").as_ref().to_vec();
let idx = (4 + 1) * 4;
assert_eq!(&px[idx..idx + 4], &[255, 0, 0, 255]);
}
#[test]
fn paint_stroke_degenerate_brush_params_do_not_divide_by_zero_or_hang() {
for spacing in [0.0_f32, -1.0, f32::NAN] {
let mut img = rgba8_image(8, 8);
let mut brush = Brush::new(opaque_red(), 2.0);
brush.spacing = spacing;
img.paint_stroke(0.0, 0.0, 7.0, 7.0, brush);
assert_eq!(
img.pixels.get_u8_vec_ref().map(|v| v.len()),
Some(8 * 8 * 4)
);
}
let mut img = rgba8_image(4, 4);
img.paint_stroke(f32::NAN, 0.0, 1.0, 1.0, Brush::new(opaque_red(), 1.0));
assert_eq!(img.pixels.get_u8_vec_ref().map(|v| v.len()), Some(64));
let mut img = rgba8_image(4, 4);
img.paint_stroke(0.0, 0.0, 3.0, 3.0, Brush::new(opaque_red(), 0.0));
assert!(img
.pixels
.get_u8_vec_ref()
.expect("u8")
.as_ref()
.iter()
.all(|b| *b == 0));
}
#[test]
fn bug_paint_stroke_with_infinite_endpoint_loops_2_billion_times() {
let mut img = rgba8_image(4, 4);
img.paint_stroke(0.0, 0.0, f32::INFINITY, 0.0, Brush::new(opaque_red(), 2.0));
}
#[test]
fn bug_paint_dot_indexes_out_of_bounds_when_dims_exceed_the_buffer() {
let mut img = RawImage {
pixels: RawImageData::U8(vec![0u8; 4].into()),
width: 100,
height: 100,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
};
img.paint_dot(50.0, 50.0, Brush::new(opaque_red(), 4.0));
}
#[test]
fn bug_into_loaded_image_source_overflows_on_huge_dimensions() {
let img = RawImage {
pixels: RawImageData::U8(vec![0u8; 4].into()),
width: usize::MAX,
height: 2,
premultiplied_alpha: true,
data_format: RawImageFormat::RGBA8,
tag: Vec::new().into(),
};
assert!(img.into_loaded_image_source().is_none());
}
}