#[allow(unused_imports)]
pub use super::*;
#[cfg(test)]
mod audit_tests {
use super::*;
#[test]
fn refstr_null_and_empty_is_empty_str() {
let null = Refstr {
ptr: core::ptr::null(),
len: 0,
};
assert_eq!(null.as_str(), "");
let null_lenful = Refstr {
ptr: core::ptr::null(),
len: 5,
};
assert_eq!(null_lenful.as_str(), "");
let s = "hello";
let good: Refstr = s.into();
assert_eq!(good.as_str(), "hello");
}
#[test]
fn refstr_vec_ref_null_is_empty_slice() {
let null = RefstrVecRef {
ptr: core::ptr::null(),
len: 3,
};
assert!(null.as_slice().is_empty());
}
#[test]
fn u8_vec_ref_null_is_empty_slice() {
let null = U8VecRef {
ptr: core::ptr::null(),
len: 8,
};
assert!(null.as_slice().is_empty());
let data = [1u8, 2, 3];
let good: U8VecRef = (&data[..]).into();
assert_eq!(good.as_slice(), &[1, 2, 3]);
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)] mod autotest_generated {
use super::*;
fn null_gl() -> Rc<GenericGlContext> {
Rc::new(unsafe { core::mem::zeroed::<GenericGlContext>() })
}
fn null_ctx() -> GlContextPtr {
GlContextPtr::new(RendererType::Software, null_gl())
}
fn test_texture(texture_id: GLuint, width: u32, height: u32) -> Texture {
Texture::create(
texture_id,
TextureFlags {
is_opaque: false,
is_video_texture: false,
},
PhysicalSizeU32 { width, height },
ColorU {
r: 1,
g: 2,
b: 3,
a: 4,
},
null_ctx(),
RawImageFormat::RGBA8,
)
}
const ALL_ATTRIB_TYPES: [VertexAttributeType; 5] = [
VertexAttributeType::Float,
VertexAttributeType::Double,
VertexAttributeType::UnsignedByte,
VertexAttributeType::UnsignedShort,
VertexAttributeType::UnsignedInt,
];
const ALL_INDEX_FORMATS: [IndexBufferFormat; 6] = [
IndexBufferFormat::Points,
IndexBufferFormat::Lines,
IndexBufferFormat::LineStrip,
IndexBufferFormat::Triangles,
IndexBufferFormat::TriangleStrip,
IndexBufferFormat::TriangleFan,
];
#[test]
fn refstr_roundtrips_unicode_multibyte_and_combining_marks() {
for s in [
"\u{1F600}",
"日本語のテキスト",
"e\u{0301}\u{0328}combining",
"\u{0}nul\u{0}embedded\u{0}",
"\u{FFFD}\u{200B}zero-width",
] {
let r: Refstr = s.into();
assert_eq!(r.as_str(), s);
assert_eq!(r.as_str().len(), s.len());
}
}
#[test]
fn refstr_len_zero_over_nonempty_buffer_is_empty_not_dangling() {
let backing = "not empty";
let r = Refstr {
ptr: backing.as_ptr(),
len: 0,
};
assert_eq!(r.as_str(), "");
}
#[test]
fn refstr_huge_input_does_not_hang() {
let huge = "x".repeat(1_000_000);
let r: Refstr = huge.as_str().into();
assert_eq!(r.as_str().len(), 1_000_000);
assert_eq!(r.as_str(), huge.as_str());
}
#[test]
fn refstr_debug_on_null_does_not_panic() {
let null = Refstr {
ptr: core::ptr::null(),
len: usize::MAX,
};
assert_eq!(alloc::format!("{null:?}"), "\"\"");
}
#[test]
fn refstr_clone_preserves_ptr_and_len() {
let s = "clone me";
let r: Refstr = s.into();
let c = r.clone();
assert_eq!(c.as_str(), s);
assert!(core::ptr::eq(c.ptr, r.ptr));
assert_eq!(c.len, r.len);
}
#[test]
fn every_vec_ref_with_null_ptr_and_nonzero_len_is_empty() {
const GARBAGE_LEN: usize = usize::MAX;
assert!(RefstrVecRef {
ptr: core::ptr::null(),
len: GARBAGE_LEN
}
.as_slice()
.is_empty());
assert!(GLuintVecRef {
ptr: core::ptr::null(),
len: GARBAGE_LEN
}
.as_slice()
.is_empty());
assert!(GLenumVecRef {
ptr: core::ptr::null(),
len: GARBAGE_LEN
}
.as_slice()
.is_empty());
assert!(U8VecRef {
ptr: core::ptr::null(),
len: GARBAGE_LEN
}
.as_slice()
.is_empty());
assert!(F32VecRef {
ptr: core::ptr::null(),
len: GARBAGE_LEN
}
.as_slice()
.is_empty());
assert!(I32VecRef {
ptr: core::ptr::null(),
len: GARBAGE_LEN
}
.as_slice()
.is_empty());
let mut m64 = GLint64VecRefMut {
ptr: core::ptr::null_mut(),
len: GARBAGE_LEN,
};
assert!(m64.as_slice().is_empty());
assert!(m64.as_mut_slice().is_empty());
let mut mf = GLfloatVecRefMut {
ptr: core::ptr::null_mut(),
len: GARBAGE_LEN,
};
assert!(mf.as_slice().is_empty());
assert!(mf.as_mut_slice().is_empty());
let mut mi = GLintVecRefMut {
ptr: core::ptr::null_mut(),
len: GARBAGE_LEN,
};
assert!(mi.as_slice().is_empty());
assert!(mi.as_mut_slice().is_empty());
let mut mb = GLbooleanVecRefMut {
ptr: core::ptr::null_mut(),
len: GARBAGE_LEN,
};
assert!(mb.as_slice().is_empty());
assert!(mb.as_mut_slice().is_empty());
let mut mu8 = U8VecRefMut {
ptr: core::ptr::null_mut(),
len: GARBAGE_LEN,
};
assert!(mu8.as_slice().is_empty());
assert!(mu8.as_mut_slice().is_empty());
}
#[test]
fn vec_refs_roundtrip_from_slice() {
let u: [GLuint; 3] = [0, 1, u32::MAX];
assert_eq!(GLuintVecRef::from(&u[..]).as_slice(), &u[..]);
let e: [GLenum; 2] = [gl::TRIANGLES, u32::MAX];
assert_eq!(GLenumVecRef::from(&e[..]).as_slice(), &e[..]);
let b: [u8; 4] = [0, 127, 128, 255];
assert_eq!(U8VecRef::from(&b[..]).as_slice(), &b[..]);
let i: [i32; 3] = [i32::MIN, 0, i32::MAX];
assert_eq!(I32VecRef::from(&i[..]).as_slice(), &i[..]);
let empty: [u8; 0] = [];
assert!(U8VecRef::from(&empty[..]).as_slice().is_empty());
}
#[test]
fn f32_vec_ref_preserves_nan_inf_and_subnormals_bit_exactly() {
let vals: [f32; 6] = [
f32::NAN,
f32::INFINITY,
f32::NEG_INFINITY,
-0.0,
f32::MIN_POSITIVE / 2.0, f32::MAX,
];
let r = F32VecRef::from(&vals[..]);
let got = r.as_slice();
assert_eq!(got.len(), vals.len());
for (g, v) in got.iter().zip(vals.iter()) {
assert_eq!(g.to_bits(), v.to_bits());
}
assert!(got[0].is_nan());
assert!(got[3].is_sign_negative());
}
#[test]
fn glint64_vec_ref_mut_handles_boundary_values() {
let mut vals: [GLint64; 4] = [i64::MIN, -1, 0, i64::MAX];
let mut r = GLint64VecRefMut::from(&mut vals[..]);
assert_eq!(r.as_slice(), &[i64::MIN, -1, 0, i64::MAX]);
r.as_mut_slice()[0] = i64::MAX;
r.as_mut_slice()[3] = i64::MIN;
assert_eq!(vals, [i64::MAX, -1, 0, i64::MIN]);
}
#[test]
fn mut_vec_refs_write_through_to_the_caller_buffer() {
let mut floats: [GLfloat; 2] = [0.0, 0.0];
GLfloatVecRefMut::from(&mut floats[..]).as_mut_slice()[1] = f32::NAN;
assert!(floats[1].is_nan());
let mut ints: [GLint; 2] = [0, 0];
GLintVecRefMut::from(&mut ints[..]).as_mut_slice()[0] = i32::MIN;
assert_eq!(ints[0], i32::MIN);
let mut bools: [GLboolean; 2] = [0, 0];
GLbooleanVecRefMut::from(&mut bools[..]).as_mut_slice()[1] = 255;
assert_eq!(bools[1], 255);
let mut bytes: [u8; 3] = [1, 2, 3];
U8VecRefMut::from(&mut bytes[..]).as_mut_slice()[2] = 0;
assert_eq!(bytes, [1, 2, 0]);
}
#[test]
fn u8_vec_ref_ord_eq_hash_agree_with_the_underlying_slice() {
use core::hash::{BuildHasher, Hasher};
use alloc::collections::BTreeSet;
let a = [1u8, 2, 3];
let b = [1u8, 2, 4];
let ra = U8VecRef::from(&a[..]);
let rb = U8VecRef::from(&b[..]);
assert_eq!(ra, U8VecRef::from(&a[..]));
assert!(ra < rb);
assert_eq!(ra.cmp(&rb), a[..].cmp(&b[..]));
let null = U8VecRef {
ptr: core::ptr::null(),
len: 99,
};
let empty: [u8; 0] = [];
assert_eq!(null, U8VecRef::from(&empty[..]));
fn hash_of(v: &U8VecRef) -> u64 {
core::hash::BuildHasherDefault::<TestHasher>::default().hash_one(v)
}
#[derive(Default)]
struct TestHasher(u64);
impl Hasher for TestHasher {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, bytes: &[u8]) {
for b in bytes {
self.0 = self.0.wrapping_mul(31).wrapping_add(u64::from(*b));
}
}
}
assert_eq!(hash_of(&ra), hash_of(&U8VecRef::from(&a[..])));
assert_eq!(hash_of(&null), hash_of(&U8VecRef::from(&empty[..])));
let mut set = BTreeSet::new();
set.insert(ra.clone());
set.insert(U8VecRef::from(&a[..]));
set.insert(rb);
assert_eq!(set.len(), 2);
}
#[test]
fn refstr_vec_ref_roundtrips_and_maps_back_to_strs() {
let strs = ["", "a", "\u{1F600}"];
let refstrs: Vec<Refstr> = strs.iter().map(|s| Refstr::from(*s)).collect();
let vec_ref = RefstrVecRef::from(&refstrs[..]);
let got: Vec<&str> = vec_ref.as_slice().iter().map(Refstr::as_str).collect();
assert_eq!(got, strs);
}
#[test]
fn glsync_ptr_roundtrips_null_and_nonnull() {
let null = GLsyncPtr::new(core::ptr::null());
assert!(null.clone().get().is_null());
assert_eq!(alloc::format!("{null:?}"), "0x0");
let sentinel = usize::MAX as *const c_void;
let p = GLsyncPtr::new(sentinel);
assert_eq!(p.clone().get() as usize, usize::MAX);
assert!(p.run_destructor);
}
#[cfg(feature = "std")]
#[test]
fn shader_with_glsl_version_replaces_only_the_first_line() {
let src = b"#version 150\nbody line 1\nbody line 2";
let out = shader_with_glsl_version(src, b"#version 300 es\n");
assert_eq!(out, b"#version 300 es\nbody line 1\nbody line 2".to_vec());
}
#[cfg(feature = "std")]
#[test]
fn shader_with_glsl_version_empty_src_yields_just_the_version_line() {
assert_eq!(
shader_with_glsl_version(b"", b"#version 150\n"),
b"#version 150\n".to_vec()
);
}
#[cfg(feature = "std")]
#[test]
fn shader_with_glsl_version_src_without_newline_is_kept_whole() {
let out = shader_with_glsl_version(b"void main(){}", b"#version 150\n");
assert_eq!(out, b"#version 150\nvoid main(){}".to_vec());
}
#[cfg(feature = "std")]
#[test]
fn shader_with_glsl_version_leading_newline_drops_only_that_newline() {
let out = shader_with_glsl_version(b"\nrest", b"V\n");
assert_eq!(out, b"V\nrest".to_vec());
}
#[cfg(feature = "std")]
#[test]
fn shader_with_glsl_version_empty_version_line_still_strips_first_line() {
let out = shader_with_glsl_version(b"#version 150\nbody", b"");
assert_eq!(out, b"body".to_vec());
assert!(shader_with_glsl_version(b"", b"").is_empty());
}
#[cfg(feature = "std")]
#[test]
fn shader_with_glsl_version_is_byte_exact_on_invalid_utf8_and_nul_bytes() {
let src = b"#version 150\n\xFF\xFE\x00\x80body";
let out = shader_with_glsl_version(src, b"#version 100\n");
assert_eq!(out, b"#version 100\n\xFF\xFE\x00\x80body".to_vec());
let out2 = shader_with_glsl_version(&[0xFF, 0xFE, 0x00], b"V\n");
assert_eq!(out2, vec![b'V', b'\n', 0xFF, 0xFE, 0x00]);
}
#[cfg(feature = "std")]
#[test]
fn shader_with_glsl_version_handles_a_1mb_source_without_hanging() {
let mut src = b"#version 150\n".to_vec();
src.resize(src.len() + 1_000_000, b'x');
let out = shader_with_glsl_version(&src, b"#version 300 es\n");
assert_eq!(out.len(), b"#version 300 es\n".len() + 1_000_000);
assert!(out.starts_with(b"#version 300 es\n"));
assert!(out.ends_with(b"xxxx"));
}
#[test]
fn glsl_version_candidates_are_wellformed_and_distinct() {
for gl_type in [GlType::Gl, GlType::Gles] {
let candidates = glsl_version_candidates(gl_type);
assert!(
!candidates.is_empty(),
"{gl_type:?} must have at least one candidate or the probe can never succeed"
);
for c in candidates {
let s = core::str::from_utf8(c).expect("candidate must be valid UTF-8");
assert!(s.starts_with("#version "), "{s:?}");
assert!(s.ends_with('\n'), "{s:?}");
let parsed = s.trim().trim_start_matches("#version ");
assert!(!parsed.is_empty(), "{s:?} must parse to a nonempty version");
}
for (i, a) in candidates.iter().enumerate() {
for b in candidates.iter().skip(i + 1) {
assert_ne!(a, b, "duplicate candidate in {gl_type:?}");
}
}
}
for g in glsl_version_candidates(GlType::Gl) {
assert!(!glsl_version_candidates(GlType::Gles).contains(g));
}
let first_gl = core::str::from_utf8(glsl_version_candidates(GlType::Gl)[0]).unwrap();
assert_eq!(first_gl.trim().trim_start_matches("#version "), "150");
let first_es = core::str::from_utf8(glsl_version_candidates(GlType::Gles)[0]).unwrap();
assert_eq!(first_es.trim().trim_start_matches("#version "), "300 es");
}
#[test]
fn gl_type_from_context_gl_type_is_total() {
assert_eq!(GlType::from(GlContextGlType::Gl), GlType::Gl);
assert_eq!(GlType::from(GlContextGlType::GlEs), GlType::Gles);
}
#[test]
fn gl_context_ptr_software_is_never_usable_and_has_no_shaders() {
let ctx = GlContextPtr::new(RendererType::Software, null_gl());
assert!(!ctx.is_gl_usable());
assert_eq!(ctx.get_svg_shader(), 0);
assert_eq!(ctx.get_brush_shader(), 0);
assert_eq!(ctx.get_fxaa_shader(), 0);
assert_eq!(ctx.get_usable_glsl_version().as_str(), "");
assert_eq!(ctx.renderer_type, RendererType::Software);
}
#[test]
fn gl_context_ptr_hardware_with_broken_driver_falls_back_instead_of_panicking() {
let ctx = GlContextPtr::new(RendererType::Hardware, null_gl());
assert!(
!ctx.is_gl_usable(),
"a driver that compiles nothing must report is_gl_usable() == false"
);
assert_eq!(ctx.get_svg_shader(), 0);
assert_eq!(ctx.get_brush_shader(), 0);
assert_eq!(ctx.get_fxaa_shader(), 0);
assert_eq!(
ctx.get_usable_glsl_version().as_str(),
"",
"glsl_version must be empty when the context is unusable"
);
}
#[test]
fn gl_context_ptr_get_type_defaults_to_desktop_gl_on_an_empty_version_string() {
assert_eq!(null_ctx().get_type(), GlType::Gl);
}
#[test]
fn gl_context_ptr_clone_is_eq_but_distinct_contexts_are_not() {
let a = null_ctx();
let clone = a.clone();
assert_eq!(a, clone);
assert_eq!(a.cmp(&clone), core::cmp::Ordering::Equal);
assert_eq!(a.partial_cmp(&clone), Some(core::cmp::Ordering::Equal));
let b = null_ctx();
assert_ne!(a, b);
assert_eq!(a.cmp(&b), a.partial_cmp(&b).unwrap());
assert_eq!(a.cmp(&b).reverse(), b.cmp(&a));
assert!(Rc::ptr_eq(a.get(), clone.get()));
assert!(!Rc::ptr_eq(a.get(), b.get()));
}
#[test]
fn gl_context_ptr_gen_family_returns_empty_for_every_n_including_negatives() {
let ctx = null_ctx();
for n in [0, 1, -1, i32::MIN, i32::MAX] {
assert!(ctx.gen_buffers(n).as_slice().is_empty(), "gen_buffers({n})");
assert!(
ctx.gen_textures(n).as_slice().is_empty(),
"gen_textures({n})"
);
assert!(
ctx.gen_framebuffers(n).as_slice().is_empty(),
"gen_framebuffers({n})"
);
assert!(
ctx.gen_renderbuffers(n).as_slice().is_empty(),
"gen_renderbuffers({n})"
);
assert!(
ctx.gen_vertex_arrays(n).as_slice().is_empty(),
"gen_vertex_arrays({n})"
);
assert!(ctx.gen_queries(n).as_slice().is_empty(), "gen_queries({n})");
}
}
#[test]
fn gl_context_ptr_integer_extremes_do_not_panic() {
let ctx = null_ctx();
let data = [0u8; 4];
let void_ptr = || GlVoidPtrConst {
ptr: data.as_ptr().cast(),
run_destructor: false,
};
for offset in [0isize, -1, isize::MIN, isize::MAX] {
for size in [0isize, -1, isize::MIN, isize::MAX] {
ctx.buffer_sub_data_untyped(gl::ARRAY_BUFFER, offset, size, void_ptr());
let _ = ctx.map_buffer_range(gl::ARRAY_BUFFER, offset, size, 0);
}
}
ctx.buffer_data_untyped(gl::ARRAY_BUFFER, isize::MIN, void_ptr(), gl::STATIC_DRAW);
for offset in [0usize, 1, usize::MAX] {
ctx.tex_sub_image_2d_pbo(
gl::TEXTURE_2D,
0,
0,
0,
1,
1,
gl::RGBA,
gl::UNSIGNED_BYTE,
offset,
);
}
ctx.pixel_store_i(gl::PACK_ALIGNMENT, i32::MIN);
ctx.pixel_store_i(gl::PACK_ALIGNMENT, i32::MAX);
let _ = ctx.unmap_buffer(gl::ARRAY_BUFFER);
}
#[test]
fn gl_context_ptr_float_extremes_do_not_panic() {
let ctx = null_ctx();
for v in [
0.0,
1.0,
-1.0,
f32::NAN,
f32::INFINITY,
f32::NEG_INFINITY,
f32::MIN,
f32::MAX,
] {
ctx.sample_coverage(v, false);
ctx.sample_coverage(v, true);
ctx.polygon_offset(v, v);
}
}
#[test]
fn gl_context_ptr_shader_source_handles_empty_unicode_and_embedded_nuls() {
let ctx = null_ctx();
let strings: StringVec = vec![
AzString::from(String::new()),
AzString::from("\u{1F600} emoji".to_string()),
AzString::from("has\u{0}nul".to_string()),
AzString::from("x".repeat(100_000)),
]
.into();
ctx.shader_source(0, strings);
ctx.shader_source(u32::MAX, Vec::<AzString>::new().into());
}
#[test]
fn gl_context_ptr_read_pixels_sizes_the_buffer_from_the_dimensions() {
let ctx = null_ctx();
let px = ctx.read_pixels(0, 0, 2, 3, gl::RGBA, gl::UNSIGNED_BYTE);
assert_eq!(
px.len(),
2 * 3 * 4,
"RGBA/UNSIGNED_BYTE = 4 bytes per pixel"
);
assert_eq!(
ctx.read_pixels(0, 0, 0, 0, gl::RGBA, gl::UNSIGNED_BYTE)
.len(),
0
);
}
#[test]
fn gl_context_ptr_read_pixels_into_buffer_accepts_null_and_undersized_targets() {
let ctx = null_ctx();
ctx.read_pixels_into_buffer(
0,
0,
4,
4,
gl::RGBA,
gl::UNSIGNED_BYTE,
U8VecRefMut {
ptr: core::ptr::null_mut(),
len: 64,
},
);
let mut small = [0u8; 1];
ctx.read_pixels_into_buffer(
0,
0,
4,
4,
gl::RGBA,
gl::UNSIGNED_BYTE,
(&mut small[..]).into(),
);
assert_eq!(small, [0u8; 1]);
}
#[test]
fn gl_context_ptr_uniform_getters_accept_null_result_buffers() {
let ctx = null_ctx();
ctx.get_uniform_iv(
0,
-1,
GLintVecRefMut {
ptr: core::ptr::null_mut(),
len: 16,
},
);
ctx.get_uniform_fv(
0,
i32::MIN,
GLfloatVecRefMut {
ptr: core::ptr::null_mut(),
len: 16,
},
);
let mut ints = [0i32; 2];
ctx.get_uniform_iv(u32::MAX, i32::MAX, (&mut ints[..]).into());
let mut floats = [0f32; 2];
ctx.get_uniform_fv(u32::MAX, i32::MAX, (&mut floats[..]).into());
}
#[test]
fn gl_context_ptr_get_uniform_indices_handles_empty_and_null_name_lists() {
let ctx = null_ctx();
let empty: &[Refstr] = &[];
assert!(ctx
.get_uniform_indices(0, empty.into())
.as_slice()
.is_empty());
assert!(ctx
.get_uniform_indices(
0,
RefstrVecRef {
ptr: core::ptr::null(),
len: 4,
},
)
.as_slice()
.is_empty());
}
#[test]
fn gl_context_ptr_delete_family_accepts_empty_and_null_id_lists() {
let ctx = null_ctx();
let empty: &[GLuint] = &[];
ctx.delete_buffers(empty.into());
ctx.delete_textures(empty.into());
ctx.delete_framebuffers(empty.into());
ctx.delete_renderbuffers(empty.into());
ctx.delete_vertex_arrays(empty.into());
ctx.delete_queries(empty.into());
let null = || GLuintVecRef {
ptr: core::ptr::null(),
len: 7,
};
ctx.delete_buffers(null());
ctx.delete_textures(null());
ctx.delete_framebuffers(null());
ctx.delete_renderbuffers(null());
ctx.delete_vertex_arrays(null());
ctx.delete_queries(null());
let ids: &[GLuint] = &[0, 1, u32::MAX];
ctx.delete_buffers(ids.into());
ctx.delete_textures(ids.into());
}
#[test]
fn gl_context_ptr_draw_buffers_accepts_an_empty_list() {
let ctx = null_ctx();
let empty: &[GLenum] = &[];
ctx.draw_buffers(empty.into());
ctx.draw_buffers(GLenumVecRef {
ptr: core::ptr::null(),
len: 3,
});
}
#[test]
fn vertex_attribute_type_mem_size_matches_the_rust_type_it_names() {
assert_eq!(
VertexAttributeType::Float.get_mem_size(),
core::mem::size_of::<f32>()
);
assert_eq!(
VertexAttributeType::Double.get_mem_size(),
core::mem::size_of::<f64>()
);
assert_eq!(
VertexAttributeType::UnsignedByte.get_mem_size(),
core::mem::size_of::<u8>()
);
assert_eq!(
VertexAttributeType::UnsignedShort.get_mem_size(),
core::mem::size_of::<u16>()
);
assert_eq!(
VertexAttributeType::UnsignedInt.get_mem_size(),
core::mem::size_of::<u32>()
);
for t in ALL_ATTRIB_TYPES {
assert!(t.get_mem_size() > 0, "{t:?}");
}
}
#[test]
fn vertex_attribute_type_gl_ids_are_distinct_and_nonzero() {
for (i, a) in ALL_ATTRIB_TYPES.iter().enumerate() {
assert_ne!(a.get_gl_id(), 0, "{a:?} maps to the GL 'no type' id 0");
for b in ALL_ATTRIB_TYPES.iter().skip(i + 1) {
assert_ne!(
a.get_gl_id(),
b.get_gl_id(),
"{a:?} and {b:?} share a GL id -- one of them would upload as the wrong type"
);
}
}
assert_eq!(VertexAttributeType::Float.get_gl_id(), gl::FLOAT);
assert_eq!(
VertexAttributeType::UnsignedByte.get_gl_id(),
gl::UNSIGNED_BYTE
);
}
#[test]
fn vertex_attribute_get_stride_at_zero_and_at_the_overflow_boundary() {
let attr = |ty, item_count| VertexAttribute {
va_name: AzString::from_const_str("vAttrXY"),
layout_location: OptionUsize::None,
attribute_type: ty,
item_count,
};
for t in ALL_ATTRIB_TYPES {
assert_eq!(attr(t, 0).get_stride(), 0, "{t:?}");
}
assert_eq!(attr(VertexAttributeType::Float, 2).get_stride(), 8);
assert_eq!(attr(VertexAttributeType::Double, 4).get_stride(), 32);
assert_eq!(attr(VertexAttributeType::UnsignedByte, 3).get_stride(), 3);
for t in ALL_ATTRIB_TYPES {
let max_items = usize::MAX / t.get_mem_size();
assert_eq!(
attr(t, max_items).get_stride(),
max_items * t.get_mem_size(),
"{t:?} at the overflow boundary"
);
}
}
#[test]
fn vertex_layout_stride_is_the_sum_of_its_fields() {
let attr = |name: &str, ty, item_count| VertexAttribute {
va_name: AzString::from(name.to_string()),
layout_location: OptionUsize::None,
attribute_type: ty,
item_count,
};
let empty = VertexLayout {
fields: VertexAttributeVec::from_const_slice(&[]),
};
assert_eq!(
empty
.fields
.iter()
.map(VertexAttribute::get_stride)
.sum::<usize>(),
0
);
let layout = VertexLayout {
fields: vec![
attr("vAttrXY", VertexAttributeType::Float, 2),
attr("vColor", VertexAttributeType::UnsignedByte, 4),
]
.into(),
};
let total: usize = layout.fields.iter().map(VertexAttribute::get_stride).sum();
assert_eq!(total, 12);
assert_eq!(layout, layout.clone());
}
#[test]
fn index_buffer_format_gl_ids_are_distinct() {
for (i, a) in ALL_INDEX_FORMATS.iter().enumerate() {
for b in ALL_INDEX_FORMATS.iter().skip(i + 1) {
assert_ne!(a.get_gl_id(), b.get_gl_id(), "{a:?} vs {b:?}");
}
}
assert_eq!(IndexBufferFormat::Points.get_gl_id(), gl::POINTS);
assert_eq!(
IndexBufferFormat::TriangleStrip.get_gl_id(),
gl::TRIANGLE_STRIP
);
}
#[test]
fn uniform_type_nan_never_equals_itself_so_draw_always_reuploads_it() {
let nan = UniformType::Float(f32::NAN);
assert_ne!(nan, UniformType::Float(f32::NAN));
assert_ne!(Some(nan), Some(nan));
let nan_vec = UniformType::FloatVec4([f32::NAN, 0.0, 0.0, 0.0]);
assert_ne!(nan_vec, nan_vec);
assert_eq!(UniformType::Float(0.0), UniformType::Float(-0.0));
assert_eq!(UniformType::Int(i32::MIN), UniformType::Int(i32::MIN));
assert_ne!(UniformType::Int(0), UniformType::UnsignedInt(0));
}
#[test]
fn uniform_type_matrix_transpose_flag_is_part_of_its_identity() {
let m = [0.0f32; 4];
assert_ne!(
UniformType::Matrix2 {
transpose: false,
matrix: m
},
UniformType::Matrix2 {
transpose: true,
matrix: m
},
);
assert_ne!(
UniformType::FloatVec2([1.0, 2.0]),
UniformType::IntVec2([1, 2])
);
}
#[test]
fn uniform_create_preserves_empty_unicode_and_huge_names() {
let huge = "n".repeat(100_000);
for name in ["", "u_color", "\u{1F600}", "e\u{0301}", huge.as_str()] {
let u = Uniform::create(name.to_string(), UniformType::Int(0));
assert_eq!(u.uniform_name.as_str(), name);
}
}
#[test]
fn gl_shader_new_reports_no_shader_compiler_instead_of_panicking() {
let ctx = null_ctx();
let huge = "x".repeat(200_000);
for (vert, frag) in [
("", ""),
(" \t\n ", "\n\n"),
("not glsl at all ;;;{{{", "\u{1F600}"),
("void main(){}", "void main(){}"),
(huge.as_str(), huge.as_str()),
] {
let err = GlShader::new(&ctx, vert, frag).unwrap_err();
assert_eq!(err, GlShaderCreateError::NoShaderCompiler);
}
}
#[test]
fn shader_error_types_format_without_panicking_on_unicode_and_extremes() {
let vert = VertexShaderCompileError {
error_id: i32::MIN,
info_log: AzString::from("\u{1F600} log".to_string()),
};
let frag = FragmentShaderCompileError {
error_id: i32::MAX,
info_log: AzString::from(String::new()),
};
let link = GlShaderLinkError {
error_id: -1,
info_log: AzString::from("multi\nline\0log".to_string()),
};
assert!(alloc::format!("{vert}").contains("-2147483648"));
assert!(alloc::format!("{vert}").contains("\u{1F600}"));
assert!(alloc::format!("{frag}").contains("2147483647"));
let compile = GlShaderCompileError::Vertex(vert);
assert!(alloc::format!("{compile}").contains("Failed to compile vertex shader"));
assert_eq!(alloc::format!("{compile:?}"), alloc::format!("{compile}"));
let frag_err = GlShaderCompileError::Fragment(frag);
assert!(alloc::format!("{frag_err}").contains("Failed to compile fragment shader"));
let create = GlShaderCreateError::Link(link);
assert!(alloc::format!("{create}").contains("Shader linking error"));
assert_eq!(alloc::format!("{create:?}"), alloc::format!("{create}"));
assert!(alloc::format!("{}", GlShaderCreateError::NoShaderCompiler)
.contains("doesn't include a shader compiler"));
}
#[test]
fn texture_descriptor_mirrors_the_texture_including_extreme_sizes() {
let tex = test_texture(42, 640, 480);
let d = tex.get_descriptor();
assert_eq!(d.width, 640);
assert_eq!(d.height, 480);
assert_eq!(d.format, RawImageFormat::RGBA8);
assert_eq!(d.offset, 0);
assert!(!d.flags.is_opaque);
assert!(!d.flags.allow_mipmaps, "textures map 1:1, never mipmapped");
let zero = test_texture(1, 0, 0);
assert_eq!(zero.get_descriptor().width, 0);
assert_eq!(zero.get_descriptor().height, 0);
let huge = test_texture(1, u32::MAX, u32::MAX);
assert_eq!(huge.get_descriptor().width, u32::MAX as usize);
assert_eq!(huge.get_descriptor().height, u32::MAX as usize);
let opaque = Texture::create(
7,
TextureFlags {
is_opaque: true,
is_video_texture: true,
},
PhysicalSizeU32 {
width: 2,
height: 2,
},
ColorU {
r: 0,
g: 0,
b: 0,
a: 0,
},
null_ctx(),
RawImageFormat::BGRA8,
);
assert!(opaque.get_descriptor().flags.is_opaque);
assert_eq!(opaque.get_descriptor().format, RawImageFormat::BGRA8);
}
#[test]
fn texture_clone_and_drop_share_one_refcount_without_double_freeing() {
let tex = test_texture(9, 4, 4);
let clones: Vec<Texture> = (0..16).map(|_| tex.clone()).collect();
for c in &clones {
assert_eq!(c.texture_id, 9);
assert_eq!(c.size, tex.size);
assert_eq!(c.format, tex.format);
assert!(c.run_destructor);
}
assert_eq!(clones[0], tex);
assert_eq!(clones[0], clones[1]);
assert_ne!(tex, test_texture(10, 4, 4));
drop(clones); drop(tex); }
#[test]
fn texture_display_and_debug_render_id_and_size() {
let tex = test_texture(3, 16, 32);
assert_eq!(alloc::format!("{tex}"), "Texture { id: 3, 16x32 }");
assert_eq!(alloc::format!("{tex:?}"), alloc::format!("{tex}"));
}
#[test]
fn texture_paint_stroke_is_a_noop_when_gl_is_unusable() {
let mut tex = test_texture(5, 8, 8);
assert_eq!(tex.gl_context.get_brush_shader(), 0);
for radius in [1.0, 0.0, -1.0, f32::NAN, f32::INFINITY, f32::MAX] {
let mut brush = Brush::new(
ColorU {
r: 255,
g: 0,
b: 0,
a: 255,
},
radius,
);
brush.hardness = f32::NAN;
brush.flow = f32::INFINITY;
brush.spacing = 0.0; tex.paint_stroke(f32::NAN, f32::NEG_INFINITY, f32::MAX, -0.0, brush);
tex.paint_dot(f32::NAN, f32::NAN, brush);
}
let mut zero = test_texture(6, 0, 0);
zero.paint_dot(
0.0,
0.0,
Brush::new(
ColorU {
r: 1,
g: 1,
b: 1,
a: 1,
},
4.0,
),
);
}
#[test]
fn texture_copy_to_raw_image_returns_a_null_image_on_every_degenerate_input() {
let is_null_image = |img: &RawImage| img.width == 0 && img.height == 0;
assert!(is_null_image(&test_texture(0, 16, 16).copy_to_raw_image()));
assert!(is_null_image(&test_texture(1, 0, 0).copy_to_raw_image()));
assert!(is_null_image(&test_texture(1, 16, 0).copy_to_raw_image()));
assert!(is_null_image(&test_texture(1, 0, 16).copy_to_raw_image()));
assert!(is_null_image(
&test_texture(1, u32::MAX, u32::MAX).copy_to_raw_image()
));
assert!(is_null_image(
&test_texture(1, 1 << 31, 1 << 31).copy_to_raw_image()
));
assert!(is_null_image(&test_texture(1, 4, 4).copy_to_raw_image()));
}
#[test]
#[should_panic(expected = "called `Option::unwrap()` on a `None` value")]
fn texture_clear_panics_when_the_driver_allocates_no_framebuffer() {
test_texture(1, 4, 4).clear();
}
#[test]
fn vertex_array_object_clone_and_drop_share_one_refcount() {
let layout = VertexLayout {
fields: VertexAttributeVec::from_const_slice(&[]),
};
let vao = VertexArrayObject::new(layout, 77, null_ctx());
assert_eq!(vao.vao_id, 77);
assert!(vao.run_destructor);
let clones: Vec<VertexArrayObject> = (0..8).map(|_| vao.clone()).collect();
assert!(clones.iter().all(|c| c.vao_id == 77));
assert_eq!(clones[0], vao);
drop(clones);
drop(vao);
}
#[test]
fn vertex_buffer_new_raw_keeps_its_fields_and_refcounts_its_clones() {
let vao = VertexArrayObject::new(
VertexLayout {
fields: VertexAttributeVec::from_const_slice(&[]),
},
1,
null_ctx(),
);
let vb = VertexBuffer::new_raw(11, 300, vao, 22, 40, IndexBufferFormat::TriangleStrip);
assert_eq!(vb.vertex_buffer_id, 11);
assert_eq!(vb.vertex_buffer_len, 300);
assert_eq!(vb.index_buffer_id, 22);
assert_eq!(vb.index_buffer_len, 40);
assert_eq!(vb.index_buffer_format, IndexBufferFormat::TriangleStrip);
assert_eq!(
alloc::format!("{vb}"),
"VertexBuffer { buffer: 11 (length: 300) }"
);
let clones: Vec<VertexBuffer> = (0..8).map(|_| vb.clone()).collect();
assert_eq!(clones[0], vb);
drop(clones);
drop(vb);
let empty_vao = VertexArrayObject::new(
VertexLayout {
fields: VertexAttributeVec::from_const_slice(&[]),
},
0,
null_ctx(),
);
let empty = VertexBuffer::new_raw(0, 0, empty_vao, 0, 0, IndexBufferFormat::Points);
assert_eq!(empty.vertex_buffer_len, 0);
}
#[allow(dead_code)] struct TestVertex {
_xy: [f32; 2],
}
impl VertexLayoutDescription for TestVertex {
fn get_description() -> VertexLayout {
VertexLayout {
fields: vec![VertexAttribute {
va_name: AzString::from_const_str("vAttrXY"),
layout_location: OptionUsize::None,
attribute_type: VertexAttributeType::Float,
item_count: 2,
}]
.into(),
}
}
}
#[test]
#[should_panic(expected = "called `Option::unwrap()` on a `None` value")]
fn vertex_buffer_new_panics_when_the_driver_allocates_no_vao() {
let verts = [TestVertex { _xy: [0.0, 0.0] }];
let _ = VertexBuffer::new(
null_ctx(),
0,
&verts[..],
&[0u32],
IndexBufferFormat::Triangles,
);
}
#[test]
fn gl_texture_cache_insert_lookup_and_eviction_lifecycle() {
let doc = DocumentId {
namespace_id: crate::resources::IdNamespace(7),
id: 1,
};
let other_doc = DocumentId {
namespace_id: crate::resources::IdNamespace(7),
id: 2,
};
gl_textures_clear_opengl_cache();
let stale = ExternalImageId { inner: u64::MAX };
assert!(get_opengl_texture(&stale).is_none());
assert!(
remove_single_texture_from_active_gl_textures(&doc, &Epoch::from(0), &stale).is_none()
);
gl_textures_remove_epochs_from_pipeline(&doc, Epoch::from(0));
gl_textures_remove_active_pipeline(&doc);
gl_textures_clear_opengl_cache();
let id5 = insert_into_active_gl_textures(doc, Epoch::from(5), test_texture(11, 4, 8));
let id7 = insert_into_active_gl_textures(doc, Epoch::from(7), test_texture(22, 16, 32));
assert_ne!(id5, id7, "each insert must mint a unique ExternalImageId");
assert_eq!(get_opengl_texture(&id5), Some((11, (4.0, 8.0))));
assert_eq!(get_opengl_texture(&id7), Some((22, (16.0, 32.0))));
assert!(get_opengl_texture(&stale).is_none());
let id_huge =
insert_into_active_gl_textures(doc, Epoch::from(5), test_texture(33, u32::MAX, 1));
assert_eq!(
get_opengl_texture(&id_huge),
Some((33, (4_294_967_296.0, 1.0)))
);
gl_textures_remove_epochs_from_pipeline(&doc, Epoch::from(7));
assert!(
get_opengl_texture(&id5).is_none(),
"epoch 5 < 7 must be evicted"
);
assert!(
get_opengl_texture(&id_huge).is_none(),
"epoch 5 < 7 must be evicted"
);
assert_eq!(
get_opengl_texture(&id7),
Some((22, (16.0, 32.0))),
"epoch 7 is NOT < 7, so it must survive"
);
gl_textures_remove_epochs_from_pipeline(&other_doc, Epoch::from(u32::MAX));
assert_eq!(get_opengl_texture(&id7), Some((22, (16.0, 32.0))));
assert_eq!(
remove_single_texture_from_active_gl_textures(&doc, &Epoch::from(7), &id7),
Some(())
);
assert!(get_opengl_texture(&id7).is_none());
assert_eq!(
remove_single_texture_from_active_gl_textures(&doc, &Epoch::from(7), &id7),
Some(())
);
assert!(
remove_single_texture_from_active_gl_textures(&other_doc, &Epoch::from(7), &id7)
.is_none()
);
assert!(
remove_single_texture_from_active_gl_textures(&doc, &Epoch::from(u32::MAX), &id7)
.is_none()
);
let id_a = insert_into_active_gl_textures(doc, Epoch::from(1), test_texture(44, 2, 2));
let id_b =
insert_into_active_gl_textures(other_doc, Epoch::from(1), test_texture(55, 2, 2));
assert!(get_opengl_texture(&id_a).is_some());
gl_textures_remove_active_pipeline(&doc);
assert!(get_opengl_texture(&id_a).is_none(), "doc was removed");
assert!(
get_opengl_texture(&id_b).is_some(),
"other_doc must be untouched"
);
gl_textures_clear_opengl_cache();
assert!(get_opengl_texture(&id_b).is_none());
gl_textures_clear_opengl_cache();
}
}