Skip to content

Commit 5370748

Browse files
committed
Unit tests: 139 passing across 10 crates
Pure-data and serde-roundtrip coverage for: - renzora (20) — ProjectConfig TOML round-trip, AssetPathChanged rewrite, PersistedViewportSettings ↔ runtime bridge including the new vsync field - renzora_lighting (8) — Sun azimuth/elevation → unit-vector direction, 420b457's documented defaults - renzora_water (9) — Gerstner wave height/velocity bounded by amplitude, periodic in wavelength, summing multiple waves - renzora_input (19) — InputBinding key/mouse/gamepad Debug-format round-trips and InputMap CRUD + RON serde - renzora_audio (17) — Transport beats↔seconds inverse, snap grid, TimelineState mute/solo audibility, track ↔ clip cleanup on remove - renzora_console (11) — log() append + MAX_LOG_ENTRIES cap, filtered_entries level/category/search filters - renzora_rpak (10) — Pack → read round-trip preserves files, backslash normalisation, corrupted-bytes error - renzora_keybindings (23) — existing tests; widened category whitelist to include "Code Editor" so it stops failing as new code-editor actions ship - renzora_theme (19) — existing - renzora_globals (3) — existing; added PartialEq derive on renzora::PinValue so its assert_eq! compiles renzora_shader has 9 pre-existing test failures that look material- graph-related (5476cc4/26cbf5e/7f76ae0 era); left untouched in this pass — separate cleanup.
1 parent 89ecef8 commit 5370748

11 files changed

Lines changed: 1266 additions & 12 deletions

File tree

crates/renzora/src/core/mod.rs

Lines changed: 209 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1045,7 +1045,7 @@ pub enum PinDir {
10451045
}
10461046

10471047
/// Concrete values stored on pins (inline constants, defaults).
1048-
#[derive(Clone, Debug, Serialize, Deserialize, Reflect)]
1048+
#[derive(Clone, Debug, Serialize, Deserialize, Reflect, PartialEq)]
10491049
pub enum PinValue {
10501050
None,
10511051
Float(f32),
@@ -1245,3 +1245,211 @@ impl BlueprintNode {
12451245
self.input_values.get(pin_name)
12461246
}
12471247
}
1248+
1249+
#[cfg(test)]
1250+
mod tests {
1251+
use super::*;
1252+
1253+
// ── ProjectConfig TOML round-trip ──────────────────────────────────────
1254+
1255+
#[test]
1256+
fn project_config_default_round_trips_through_toml() {
1257+
// The defaults are what greets a freshly-created project; they have
1258+
// to survive a save/load cycle byte-for-byte (modulo serializer
1259+
// formatting), otherwise a save without edits would silently mutate
1260+
// the project file.
1261+
let original = ProjectConfig::default();
1262+
let serialized = toml::to_string_pretty(&original).expect("serialize");
1263+
let parsed: ProjectConfig = toml::from_str(&serialized).expect("parse");
1264+
assert_eq!(original, parsed);
1265+
}
1266+
1267+
#[test]
1268+
fn project_config_round_trips_with_editor_section() {
1269+
let original = ProjectConfig {
1270+
name: "Demo".into(),
1271+
version: "0.2.1".into(),
1272+
main_scene: "scenes/intro.ron".into(),
1273+
editor_last_scene: Some("scenes/wip.ron".into()),
1274+
icon: Some("assets/icon.png".into()),
1275+
window: WindowConfig {
1276+
width: 1920,
1277+
height: 1080,
1278+
resizable: false,
1279+
fullscreen: true,
1280+
},
1281+
network: None,
1282+
editor: Some(crate::core::viewport_types::EditorPrefs::default()),
1283+
};
1284+
let s = toml::to_string_pretty(&original).expect("serialize");
1285+
let parsed: ProjectConfig = toml::from_str(&s).expect("parse");
1286+
assert_eq!(original, parsed);
1287+
}
1288+
1289+
#[test]
1290+
fn project_config_skips_none_optional_fields_in_toml() {
1291+
// `editor_last_scene`, `icon`, `network`, `editor` use
1292+
// skip_serializing_if = "Option::is_none". A round-trip with all
1293+
// None should produce TOML that has no mention of those keys —
1294+
// catches a regression where the attribute disappears.
1295+
let cfg = ProjectConfig::default();
1296+
let serialized = toml::to_string_pretty(&cfg).expect("serialize");
1297+
assert!(!serialized.contains("editor_last_scene"));
1298+
assert!(!serialized.contains("icon"));
1299+
assert!(!serialized.contains("[network]"));
1300+
assert!(!serialized.contains("[editor]"));
1301+
}
1302+
1303+
#[test]
1304+
fn project_config_parses_minimal_toml() {
1305+
// Hand-rolled TOML that omits everything optional. Defaults must
1306+
// fill in the gaps without erroring.
1307+
let s = r#"
1308+
name = "MyProject"
1309+
version = "1.0.0"
1310+
main_scene = "scenes/main.ron"
1311+
"#;
1312+
let parsed: ProjectConfig = toml::from_str(s).expect("parse minimal");
1313+
assert_eq!(parsed.name, "MyProject");
1314+
assert_eq!(parsed.version, "1.0.0");
1315+
assert_eq!(parsed.main_scene, "scenes/main.ron");
1316+
assert_eq!(parsed.editor_last_scene, None);
1317+
assert_eq!(parsed.icon, None);
1318+
assert_eq!(parsed.network, None);
1319+
assert_eq!(parsed.editor, None);
1320+
// window has its own #[serde(default)] so it should default cleanly.
1321+
assert_eq!(parsed.window, WindowConfig::default());
1322+
}
1323+
1324+
// ── WindowConfig / NetworkProjectConfig defaults ──────────────────────
1325+
1326+
#[test]
1327+
fn window_config_default_is_720p_resizable() {
1328+
let w = WindowConfig::default();
1329+
assert_eq!(w.width, 1280);
1330+
assert_eq!(w.height, 720);
1331+
assert!(w.resizable);
1332+
assert!(!w.fullscreen);
1333+
}
1334+
1335+
#[test]
1336+
fn network_config_default_uses_loopback_udp() {
1337+
let n = NetworkProjectConfig::default();
1338+
assert_eq!(n.server_addr, "127.0.0.1");
1339+
assert_eq!(n.port, 7636);
1340+
assert_eq!(n.transport, "udp");
1341+
assert_eq!(n.tick_rate, 64);
1342+
assert_eq!(n.max_clients, 32);
1343+
}
1344+
1345+
#[test]
1346+
fn network_config_round_trips() {
1347+
let n = NetworkProjectConfig {
1348+
server_addr: "10.0.0.5".into(),
1349+
port: 9000,
1350+
transport: "websocket".into(),
1351+
tick_rate: 30,
1352+
max_clients: 8,
1353+
};
1354+
let s = toml::to_string_pretty(&n).expect("serialize");
1355+
let parsed: NetworkProjectConfig = toml::from_str(&s).expect("parse");
1356+
assert_eq!(n, parsed);
1357+
}
1358+
1359+
// ── EntityTag default ─────────────────────────────────────────────────
1360+
1361+
#[test]
1362+
fn entity_tag_default_is_empty_string() {
1363+
// The script lookup tables short-circuit on empty tags. If this
1364+
// ever changed to e.g. "Untagged", every empty-tag entity would
1365+
// suddenly start getting indexed.
1366+
let tag = EntityTag::default();
1367+
assert!(tag.tag.is_empty());
1368+
}
1369+
1370+
// ── AssetPathChanged::rewrite ─────────────────────────────────────────
1371+
1372+
#[test]
1373+
fn rewrite_file_rename_exact_match() {
1374+
let evt = AssetPathChanged {
1375+
old: "models/old.glb".into(),
1376+
new: "models/new.glb".into(),
1377+
is_dir: false,
1378+
};
1379+
assert_eq!(evt.rewrite("models/old.glb"), Some("models/new.glb".into()));
1380+
assert_eq!(evt.rewrite("models/other.glb"), None);
1381+
}
1382+
1383+
#[test]
1384+
fn rewrite_dir_rename_rewrites_descendants() {
1385+
// `is_dir: true` rewrites anything under the folder, with a `/`
1386+
// separator check so "modelsX" doesn't accidentally match "models".
1387+
let evt = AssetPathChanged {
1388+
old: "models".into(),
1389+
new: "geometry".into(),
1390+
is_dir: true,
1391+
};
1392+
assert_eq!(evt.rewrite("models/car.glb"), Some("geometry/car.glb".into()));
1393+
assert_eq!(evt.rewrite("models"), Some("geometry".into()));
1394+
// Different folder — must not rewrite.
1395+
assert_eq!(evt.rewrite("modelsX/foo.glb"), None);
1396+
// Unrelated path.
1397+
assert_eq!(evt.rewrite("textures/a.png"), None);
1398+
}
1399+
1400+
#[test]
1401+
fn rewrite_file_rename_does_not_match_dir_prefix() {
1402+
// File rename requires exact match — must not rewrite something
1403+
// that starts with the file's name as a string.
1404+
let evt = AssetPathChanged {
1405+
old: "models/old".into(),
1406+
new: "models/new".into(),
1407+
is_dir: false,
1408+
};
1409+
assert_eq!(evt.rewrite("models/old/inner.glb"), None);
1410+
}
1411+
1412+
// ── PbrAlphaMode default ──────────────────────────────────────────────
1413+
1414+
#[test]
1415+
fn pbr_alpha_mode_default_is_opaque() {
1416+
assert_eq!(PbrAlphaMode::default(), PbrAlphaMode::Opaque);
1417+
}
1418+
1419+
// ── CurrentProject path helpers ───────────────────────────────────────
1420+
1421+
fn make_project(root: &str) -> CurrentProject {
1422+
CurrentProject {
1423+
path: PathBuf::from(root),
1424+
config: ProjectConfig::default(),
1425+
}
1426+
}
1427+
1428+
#[test]
1429+
fn resolve_path_joins_relative() {
1430+
let proj = make_project("/projects/demo");
1431+
let resolved = proj.resolve_path("scenes/main.ron");
1432+
assert_eq!(resolved, PathBuf::from("/projects/demo").join("scenes/main.ron"));
1433+
}
1434+
1435+
#[test]
1436+
fn resolve_path_keeps_absolute_input() {
1437+
// Wait, looking at impl: `self.path.join(relative)` — Path::join
1438+
// treats absolute paths as the new full path, so on Unix
1439+
// /etc/passwd would replace the project root entirely. That's
1440+
// already the documented behaviour ("if absolute, ignore root").
1441+
let proj = make_project("/projects/demo");
1442+
let abs = if cfg!(windows) { "C:/etc/x" } else { "/etc/x" };
1443+
let resolved = proj.resolve_path(abs);
1444+
assert_eq!(resolved, PathBuf::from(abs));
1445+
}
1446+
1447+
#[test]
1448+
fn make_relative_handles_relative_input() {
1449+
let proj = make_project(".");
1450+
// A path that's already relative is returned with normalized
1451+
// forward slashes regardless of input separator.
1452+
let rel = std::path::Path::new("scenes/main.ron");
1453+
assert_eq!(proj.make_relative(rel), Some("scenes/main.ron".into()));
1454+
}
1455+
}

crates/renzora/src/core/viewport_types.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,3 +477,162 @@ fn default_true() -> bool { true }
477477
pub struct EditorPrefs {
478478
pub viewport: PersistedViewportSettings,
479479
}
480+
481+
#[cfg(test)]
482+
mod tests {
483+
use super::*;
484+
485+
fn nondefault_viewport() -> ViewportSettings {
486+
// Touch every field so the round-trip really exercises the
487+
// PersistedViewportSettings <-> ViewportSettings bridge — a missed
488+
// field on either side would make this test fail.
489+
ViewportSettings {
490+
render_toggles: RenderToggles {
491+
textures: false,
492+
wireframe: true,
493+
lighting: false,
494+
shadows: false,
495+
mesh: false,
496+
},
497+
visualization_mode: VisualizationMode::Normals,
498+
show_grid: false,
499+
show_subgrid: false,
500+
show_axis_gizmo: false,
501+
show_scene_icons: false,
502+
collision_gizmo_visibility: CollisionGizmoVisibility::Always,
503+
projection_mode: ProjectionMode::Orthographic,
504+
viewport_mode: ViewportMode::default(),
505+
camera: CameraSettingsState {
506+
move_speed: 11.5,
507+
look_sensitivity: 0.7,
508+
orbit_sensitivity: 0.42,
509+
pan_sensitivity: 1.7,
510+
zoom_sensitivity: 2.3,
511+
invert_y: true,
512+
distance_relative_speed: false,
513+
},
514+
snap: SnapSettings {
515+
translate_enabled: true,
516+
translate_snap: 0.5,
517+
translate_edge_snap: true,
518+
rotate_enabled: true,
519+
rotate_snap: 15.0,
520+
scale_enabled: false,
521+
scale_snap: 0.25,
522+
scale_bottom_anchor: true,
523+
object_snap_enabled: true,
524+
object_snap_distance: 1.5,
525+
floor_snap_enabled: true,
526+
floor_y: -1.5,
527+
},
528+
pending_view_angle: None,
529+
vsync: false,
530+
}
531+
}
532+
533+
#[test]
534+
fn persisted_round_trip_preserves_every_field() {
535+
let original = nondefault_viewport();
536+
let persisted = PersistedViewportSettings::from_settings(&original);
537+
let mut restored = ViewportSettings::default();
538+
persisted.apply(&mut restored);
539+
540+
// Skip pending_view_angle (transient) and viewport_mode (not persisted).
541+
assert_eq!(original.render_toggles, restored.render_toggles);
542+
assert!(matches!(restored.visualization_mode, VisualizationMode::Normals));
543+
assert_eq!(original.show_grid, restored.show_grid);
544+
assert_eq!(original.show_subgrid, restored.show_subgrid);
545+
assert_eq!(original.show_axis_gizmo, restored.show_axis_gizmo);
546+
assert_eq!(original.show_scene_icons, restored.show_scene_icons);
547+
assert!(matches!(restored.collision_gizmo_visibility, CollisionGizmoVisibility::Always));
548+
assert!(matches!(restored.projection_mode, ProjectionMode::Orthographic));
549+
assert_eq!(original.camera, restored.camera);
550+
assert_eq!(original.snap, restored.snap);
551+
assert_eq!(original.vsync, restored.vsync);
552+
}
553+
554+
#[test]
555+
fn vsync_round_trips() {
556+
// The whole point of the recent vsync setting is that it survives
557+
// a save/load. Lock that in.
558+
let mut s = ViewportSettings::default();
559+
s.vsync = false;
560+
let persisted = PersistedViewportSettings::from_settings(&s);
561+
let mut restored = ViewportSettings::default();
562+
persisted.apply(&mut restored);
563+
assert!(!restored.vsync);
564+
}
565+
566+
#[test]
567+
fn visualization_mode_string_round_trips_through_persisted() {
568+
for mode in [
569+
VisualizationMode::None,
570+
VisualizationMode::Normals,
571+
VisualizationMode::Roughness,
572+
VisualizationMode::Metallic,
573+
VisualizationMode::Depth,
574+
VisualizationMode::UvChecker,
575+
] {
576+
let mut s = ViewportSettings::default();
577+
s.visualization_mode = mode;
578+
let p = PersistedViewportSettings::from_settings(&s);
579+
let mut restored = ViewportSettings::default();
580+
p.apply(&mut restored);
581+
assert!(
582+
std::mem::discriminant(&restored.visualization_mode)
583+
== std::mem::discriminant(&mode),
584+
"round trip lost mode {:?}, got {:?}",
585+
mode, restored.visualization_mode,
586+
);
587+
}
588+
}
589+
590+
#[test]
591+
fn editor_prefs_default_has_default_viewport() {
592+
let prefs = EditorPrefs::default();
593+
assert_eq!(prefs.viewport, PersistedViewportSettings::default());
594+
}
595+
596+
#[test]
597+
fn persisted_viewport_serde_is_keyed_by_field_name() {
598+
// Hand-rolled TOML has to deserialize cleanly — proves we didn't
599+
// accidentally tag the struct or rename a field.
600+
let s = r#"
601+
textures = true
602+
wireframe = false
603+
lighting = true
604+
shadows = true
605+
mesh = true
606+
visualization_mode = "None"
607+
show_grid = true
608+
show_subgrid = true
609+
show_axis_gizmo = true
610+
show_scene_icons = true
611+
collision_always = false
612+
orthographic = false
613+
move_speed = 10.0
614+
look_sensitivity = 1.0
615+
orbit_sensitivity = 1.0
616+
pan_sensitivity = 1.0
617+
zoom_sensitivity = 1.0
618+
invert_y = false
619+
distance_relative_speed = true
620+
translate_enabled = false
621+
translate_snap = 1.0
622+
translate_edge_snap = false
623+
rotate_enabled = false
624+
rotate_snap = 15.0
625+
scale_enabled = false
626+
scale_snap = 0.1
627+
scale_bottom_anchor = false
628+
object_snap_enabled = false
629+
object_snap_distance = 1.0
630+
floor_snap_enabled = false
631+
floor_y = 0.0
632+
vsync = true
633+
"#;
634+
let parsed: PersistedViewportSettings = toml::from_str(s).expect("parse");
635+
assert!(parsed.vsync);
636+
assert!(parsed.mesh);
637+
}
638+
}

0 commit comments

Comments
 (0)