Skip to content

Commit 3e63f82

Browse files
committed
[add] unit tests
1 parent 60d30f1 commit 3e63f82

File tree

3 files changed

+309
-0
lines changed

3 files changed

+309
-0
lines changed

crates/lambda-rs-platform/src/wgpu/bind.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,30 @@ impl Visibility {
7070
}
7171
}
7272

73+
#[cfg(test)]
74+
mod tests {
75+
use super::*;
76+
77+
/// This test verifies that each public binding visibility option is
78+
/// converted into the correct set of shader stage flags expected by the
79+
/// underlying graphics layer. It checks single stage selections, a
80+
/// combination of vertex and fragment stages, and the catch‑all option that
81+
/// enables all stages. The goal is to demonstrate that the mapping logic is
82+
/// precise and predictable so higher level code can rely on it when building
83+
/// layouts and groups.
84+
#[test]
85+
fn visibility_maps_to_expected_shader_stages() {
86+
assert_eq!(Visibility::Vertex.to_wgpu(), wgpu::ShaderStages::VERTEX);
87+
assert_eq!(Visibility::Fragment.to_wgpu(), wgpu::ShaderStages::FRAGMENT);
88+
assert_eq!(Visibility::Compute.to_wgpu(), wgpu::ShaderStages::COMPUTE);
89+
assert_eq!(
90+
Visibility::VertexAndFragment.to_wgpu(),
91+
wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT
92+
);
93+
assert_eq!(Visibility::All.to_wgpu(), wgpu::ShaderStages::all());
94+
}
95+
}
96+
7397
#[derive(Default)]
7498
/// Builder for creating a `wgpu::BindGroupLayout`.
7599
pub struct BindGroupLayoutBuilder {

crates/lambda-rs/src/render/bind.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,36 @@ impl BindingVisibility {
3636
}
3737
}
3838

39+
#[cfg(test)]
40+
mod tests {
41+
use super::*;
42+
43+
/// This test confirms that every high‑level binding visibility option maps
44+
/// directly to the corresponding visibility option in the platform layer.
45+
/// Matching these values ensures that builder code in this module forwards
46+
/// intent without alteration, which is important for readability and for
47+
/// maintenance when constructing layouts and groups.
48+
#[test]
49+
fn binding_visibility_maps_to_platform_enum() {
50+
use lambda_platform::wgpu::bind::Visibility as P;
51+
52+
assert!(matches!(BindingVisibility::Vertex.to_platform(), P::Vertex));
53+
assert!(matches!(
54+
BindingVisibility::Fragment.to_platform(),
55+
P::Fragment
56+
));
57+
assert!(matches!(
58+
BindingVisibility::Compute.to_platform(),
59+
P::Compute
60+
));
61+
assert!(matches!(
62+
BindingVisibility::VertexAndFragment.to_platform(),
63+
P::VertexAndFragment
64+
));
65+
assert!(matches!(BindingVisibility::All.to_platform(), P::All));
66+
}
67+
}
68+
3969
#[derive(Debug, Clone)]
4070
/// Bind group layout used when creating pipelines and bind groups.
4171
pub struct BindGroupLayout {

crates/lambda-rs/src/render/scene_math.rs

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,258 @@ pub fn compute_model_view_projection_matrix_about_pivot(
198198
);
199199
projection.multiply(&view).multiply(&model)
200200
}
201+
202+
#[cfg(test)]
203+
mod tests {
204+
use super::*;
205+
use crate::math::matrix as m;
206+
207+
/// This test demonstrates the complete order of operations used to produce a
208+
/// model matrix. It rotates a base identity matrix by a chosen axis and
209+
/// angle, applies a uniform scale using a diagonal scaling matrix, and then
210+
/// applies a world translation. The expected matrix is built step by step in
211+
/// the same manner so that individual differences are easy to reason about
212+
/// when reading a failure. Every element is compared using a small tolerance
213+
/// to account for floating point rounding.
214+
#[test]
215+
fn model_matrix_composes_rotation_scale_and_translation() {
216+
let translation = [3.0, -2.0, 5.0];
217+
let axis = [0.0, 1.0, 0.0];
218+
let angle_in_turns = 0.25; // quarter turn
219+
let scale = 2.0;
220+
221+
// Compute via the public function under test.
222+
let actual = compute_model_matrix(translation, axis, angle_in_turns, scale);
223+
224+
// Build the expected matrix explicitly: R, then S, then T.
225+
let mut expected: [[f32; 4]; 4] = m::identity_matrix(4, 4);
226+
expected = m::rotate_matrix(expected, axis, angle_in_turns);
227+
228+
let mut s: [[f32; 4]; 4] = [[0.0; 4]; 4];
229+
for i in 0..4 {
230+
for j in 0..4 {
231+
s[i][j] = if i == j {
232+
if i == 3 {
233+
1.0
234+
} else {
235+
scale
236+
}
237+
} else {
238+
0.0
239+
};
240+
}
241+
}
242+
expected = expected.multiply(&s);
243+
244+
let t: [[f32; 4]; 4] = m::translation_matrix(translation);
245+
let expected = t.multiply(&expected);
246+
247+
for i in 0..4 {
248+
for j in 0..4 {
249+
crate::assert_approximately_equal!(
250+
actual.at(i, j),
251+
expected.at(i, j),
252+
1e-5
253+
);
254+
}
255+
}
256+
}
257+
258+
/// This test verifies that rotating and scaling around a local pivot point
259+
/// produces the same result as translating into the pivot, applying the base
260+
/// transform, and translating back out, followed by the world translation.
261+
/// It constructs both forms and checks that all elements match within a
262+
/// small tolerance.
263+
#[test]
264+
fn model_matrix_respects_local_pivot() {
265+
let translation = [1.0, 2.0, 3.0];
266+
let axis = [1.0, 0.0, 0.0];
267+
let angle_in_turns = 0.125; // one eighth of a full turn
268+
let scale = 0.5;
269+
let pivot = [10.0, -4.0, 2.0];
270+
271+
let actual = compute_model_matrix_about_pivot(
272+
translation,
273+
axis,
274+
angle_in_turns,
275+
scale,
276+
pivot,
277+
);
278+
279+
let base =
280+
compute_model_matrix([0.0, 0.0, 0.0], axis, angle_in_turns, scale);
281+
let to_pivot: [[f32; 4]; 4] = m::translation_matrix(pivot);
282+
let from_pivot: [[f32; 4]; 4] =
283+
m::translation_matrix([-pivot[0], -pivot[1], -pivot[2]]);
284+
let world: [[f32; 4]; 4] = m::translation_matrix(translation);
285+
let expected = world
286+
.multiply(&to_pivot)
287+
.multiply(&base)
288+
.multiply(&from_pivot);
289+
290+
for i in 0..4 {
291+
for j in 0..4 {
292+
crate::assert_approximately_equal!(
293+
actual.at(i, j),
294+
expected.at(i, j),
295+
1e-5
296+
);
297+
}
298+
}
299+
}
300+
301+
/// This test confirms that the view computation is the inverse of a camera
302+
/// translation. For a camera expressed only as a world space translation, the
303+
/// inverse is a translation by the negated vector. The test constructs that
304+
/// expected matrix directly and compares it to the function result.
305+
#[test]
306+
fn view_matrix_is_inverse_translation() {
307+
let camera_position = [7.0, -3.0, 2.5];
308+
let expected: [[f32; 4]; 4] = m::translation_matrix([
309+
-camera_position[0],
310+
-camera_position[1],
311+
-camera_position[2],
312+
]);
313+
let actual = compute_view_matrix(camera_position);
314+
assert_eq!(actual, expected);
315+
}
316+
317+
/// This test validates that the perspective projection matches an
318+
/// OpenGL‑style projection that is converted into the normalized device
319+
/// coordinate range used by the target platforms. The expected conversion is
320+
/// performed by multiplying a fixed conversion matrix with the projection
321+
/// produced by the existing matrix helper. The result is compared element by
322+
/// element within a small tolerance.
323+
#[test]
324+
fn perspective_projection_matches_converted_reference() {
325+
let fov_turns = 0.25;
326+
let width = 1280;
327+
let height = 720;
328+
let near = 0.1;
329+
let far = 100.0;
330+
331+
let actual =
332+
compute_perspective_projection(fov_turns, width, height, near, far);
333+
334+
let aspect = width as f32 / height as f32;
335+
let projection_gl: [[f32; 4]; 4] =
336+
m::perspective_matrix(fov_turns, aspect, near, far);
337+
let conversion = [
338+
[1.0, 0.0, 0.0, 0.0],
339+
[0.0, 1.0, 0.0, 0.0],
340+
[0.0, 0.0, 0.5, 0.0],
341+
[0.0, 0.0, 0.5, 1.0],
342+
];
343+
let expected = conversion.multiply(&projection_gl);
344+
345+
for i in 0..4 {
346+
for j in 0..4 {
347+
crate::assert_approximately_equal!(
348+
actual.at(i, j),
349+
expected.at(i, j),
350+
1e-5
351+
);
352+
}
353+
}
354+
}
355+
356+
/// This test builds a full model, view, and projection composition using both
357+
/// the public helper and a reference expression that multiplies the same
358+
/// parts in the same order. It uses a simple camera and a non‑trivial model
359+
/// transform to provide coverage for the code paths. Results are compared
360+
/// with a small tolerance to account for floating point rounding.
361+
#[test]
362+
fn model_view_projection_composition_matches_reference() {
363+
let camera = SimpleCamera {
364+
position: [0.5, -1.0, 2.0],
365+
field_of_view_in_turns: 0.3,
366+
near_clipping_plane: 0.01,
367+
far_clipping_plane: 500.0,
368+
};
369+
let (w, h) = (1024, 600);
370+
let model_t = [2.0, 0.5, -3.0];
371+
let axis = [0.0, 0.0, 1.0];
372+
let angle = 0.2;
373+
let scale = 1.25;
374+
375+
let actual = compute_model_view_projection_matrix(
376+
&camera, w, h, model_t, axis, angle, scale,
377+
);
378+
379+
let model = compute_model_matrix(model_t, axis, angle, scale);
380+
let view = compute_view_matrix(camera.position);
381+
let proj = compute_perspective_projection(
382+
camera.field_of_view_in_turns,
383+
w,
384+
h,
385+
camera.near_clipping_plane,
386+
camera.far_clipping_plane,
387+
);
388+
let expected = proj.multiply(&view).multiply(&model);
389+
390+
for i in 0..4 {
391+
for j in 0..4 {
392+
crate::assert_approximately_equal!(
393+
actual.at(i, j),
394+
expected.at(i, j),
395+
1e-5
396+
);
397+
}
398+
}
399+
}
400+
401+
/// This test builds a full model, view, and projection composition for a
402+
/// model that rotates and scales around a local pivot point. It compares the
403+
/// public helper result to a reference expression that expands the pivot
404+
/// operations into individual translations and the base transform. Elements
405+
/// are compared with a small tolerance to make the test robust to floating
406+
/// point differences.
407+
#[test]
408+
fn model_view_projection_about_pivot_matches_reference() {
409+
let camera = SimpleCamera {
410+
position: [-3.0, 0.0, 1.0],
411+
field_of_view_in_turns: 0.15,
412+
near_clipping_plane: 0.1,
413+
far_clipping_plane: 50.0,
414+
};
415+
let (w, h) = (800, 480);
416+
let model_t = [0.0, -1.0, 2.0];
417+
let axis = [0.0, 1.0, 0.0];
418+
let angle = 0.4;
419+
let scale = 0.75;
420+
let pivot = [5.0, 0.0, -2.0];
421+
422+
let actual = compute_model_view_projection_matrix_about_pivot(
423+
&camera, w, h, model_t, axis, angle, scale, pivot,
424+
);
425+
426+
let base = compute_model_matrix([0.0, 0.0, 0.0], axis, angle, scale);
427+
let to_pivot: [[f32; 4]; 4] = m::translation_matrix(pivot);
428+
let from_pivot: [[f32; 4]; 4] =
429+
m::translation_matrix([-pivot[0], -pivot[1], -pivot[2]]);
430+
let world: [[f32; 4]; 4] = m::translation_matrix(model_t);
431+
let model = world
432+
.multiply(&to_pivot)
433+
.multiply(&base)
434+
.multiply(&from_pivot);
435+
let view = compute_view_matrix(camera.position);
436+
let proj = compute_perspective_projection(
437+
camera.field_of_view_in_turns,
438+
w,
439+
h,
440+
camera.near_clipping_plane,
441+
camera.far_clipping_plane,
442+
);
443+
let expected = proj.multiply(&view).multiply(&model);
444+
445+
for i in 0..4 {
446+
for j in 0..4 {
447+
crate::assert_approximately_equal!(
448+
actual.at(i, j),
449+
expected.at(i, j),
450+
1e-5
451+
);
452+
}
453+
}
454+
}
455+
}

0 commit comments

Comments
 (0)