Implement native archive and model workflows (#74)
Some checks failed
Native code generation / deterministic (push) Failing after 1m48s
Imaging and meshing gate / native (push) Successful in 5m26s
JPEG 2000 feature / linux (push) Successful in 2m51s
Native Rust workspace compile / compile (push) Successful in 5m28s
Skia feature / linux (push) Successful in 31m53s
Some checks failed
Native code generation / deterministic (push) Failing after 1m48s
Imaging and meshing gate / native (push) Successful in 5m26s
JPEG 2000 feature / linux (push) Successful in 2m51s
Native Rust workspace compile / compile (push) Successful in 5m28s
Skia feature / linux (push) Successful in 31m53s
This commit is contained in:
288
tests/compat/tests/archive_model_semantics.rs
Normal file
288
tests/compat/tests/archive_model_semantics.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use libremetaverse::GridClient;
|
||||
use libremetaverse::assets::{
|
||||
OarFile, OarFileAssetLoadedCallback, OarFileSceneObjectLoadedCallback,
|
||||
OarFileSettingsLoadedCallback, OarFileTerrainLoadedCallback, RegionSettings,
|
||||
};
|
||||
use libremetaverse::import_export::{ColladaLoader, ModelFace, ModelPrim, ModelUploader};
|
||||
use libremetaverse::rendering::Vertex;
|
||||
use libremetaverse_structured_data::OSD;
|
||||
use libremetaverse_types::{Quaternion, UUID, Vector2, Vector3};
|
||||
|
||||
fn temporary_directory(label: &str) -> PathBuf {
|
||||
let id = UUID::random().expect("temporary UUID").to_string();
|
||||
let path = std::env::temp_dir().join(format!("metacrate-{label}-{id}"));
|
||||
fs::create_dir_all(&path).expect("create temporary directory");
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oar_packaging_is_deterministic_and_sorted() {
|
||||
let root = temporary_directory("oar");
|
||||
for directory in ["assets", "objects", "terrains", "landdata", "settings"] {
|
||||
fs::create_dir_all(root.join(directory)).expect("create OAR directory");
|
||||
}
|
||||
fs::write(root.join("assets/z.texture"), b"z").expect("write z asset");
|
||||
fs::write(root.join("assets/a.texture"), b"a").expect("write a asset");
|
||||
fs::write(root.join("terrains/region.r32"), 1.0_f32.to_le_bytes()).expect("write terrain");
|
||||
let first = root.join("first.oar");
|
||||
let second = root.join("second.oar");
|
||||
OarFile::package_archive(
|
||||
root.to_string_lossy().into_owned(),
|
||||
first.to_string_lossy().into_owned(),
|
||||
)
|
||||
.expect("first package");
|
||||
OarFile::package_archive(
|
||||
root.to_string_lossy().into_owned(),
|
||||
second.to_string_lossy().into_owned(),
|
||||
)
|
||||
.expect("second package");
|
||||
assert_eq!(
|
||||
fs::read(first).expect("first bytes"),
|
||||
fs::read(second).expect("second bytes")
|
||||
);
|
||||
fs::remove_dir_all(root).expect("remove temporary OAR directory");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oar_canonical_asset_and_settings_names_round_trip() {
|
||||
let root = temporary_directory("oar-roundtrip");
|
||||
for directory in ["assets", "objects", "terrains", "landdata", "settings"] {
|
||||
fs::create_dir_all(root.join(directory)).expect("create OAR directory");
|
||||
}
|
||||
let asset_id = UUID::random().expect("asset UUID");
|
||||
let asset_bytes = b"native-j2c-payload".to_vec();
|
||||
fs::write(
|
||||
root.join(format!("assets/{asset_id}_texture.jp2")),
|
||||
&asset_bytes,
|
||||
)
|
||||
.expect("write canonical asset");
|
||||
let mut settings = RegionSettings::new().expect("region settings");
|
||||
settings.allow_damage = true;
|
||||
settings.water_height = 23.5;
|
||||
settings
|
||||
.to_xml(
|
||||
root.join("settings/Native Region.xml")
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
)
|
||||
.expect("write settings");
|
||||
let archive = root.join("roundtrip.oar");
|
||||
OarFile::package_archive(
|
||||
root.to_string_lossy().into_owned(),
|
||||
archive.to_string_lossy().into_owned(),
|
||||
)
|
||||
.expect("package round trip");
|
||||
|
||||
let loaded_asset = Arc::new(Mutex::new(None));
|
||||
let loaded_asset_out = Arc::clone(&loaded_asset);
|
||||
let loaded_settings = Arc::new(Mutex::new(None));
|
||||
let loaded_settings_out = Arc::clone(&loaded_settings);
|
||||
OarFile::unpackage_archive(
|
||||
archive.to_string_lossy().into_owned(),
|
||||
OarFileAssetLoadedCallback::from_callback(move |asset, _, _| {
|
||||
*loaded_asset_out.lock().expect("asset lock") =
|
||||
Some((asset.asset_id(), asset.asset_type(), asset.asset_data));
|
||||
}),
|
||||
OarFileTerrainLoadedCallback::from_callback(|_, _, _| {}),
|
||||
OarFileSceneObjectLoadedCallback::from_callback(|_, _, _| {}),
|
||||
OarFileSettingsLoadedCallback::from_callback(move |name, settings| {
|
||||
*loaded_settings_out.lock().expect("settings lock") =
|
||||
Some((name, settings.allow_damage, settings.water_height));
|
||||
}),
|
||||
)
|
||||
.expect("unpackage round trip");
|
||||
|
||||
let asset = loaded_asset.lock().expect("loaded asset lock").clone();
|
||||
assert_eq!(
|
||||
asset,
|
||||
Some((
|
||||
asset_id,
|
||||
libremetaverse_types::AssetType::Texture,
|
||||
asset_bytes
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
loaded_settings
|
||||
.lock()
|
||||
.expect("loaded settings lock")
|
||||
.clone(),
|
||||
Some(("Native Region".to_owned(), true, 23.5))
|
||||
);
|
||||
fs::remove_dir_all(root).expect("remove round-trip directory");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_face_deduplicates_vertices_and_asset_is_deterministic() {
|
||||
let vertex = || Vertex {
|
||||
position: Vector3 {
|
||||
x: -0.5,
|
||||
y: 0.25,
|
||||
z: 0.5,
|
||||
},
|
||||
normal: Vector3 {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
z: 1.0,
|
||||
},
|
||||
tex_coord: Vector2 { x: 0.0, y: 1.0 },
|
||||
};
|
||||
let mut face = ModelFace::new().expect("model face");
|
||||
face.add_vertex(vertex()).expect("first vertex");
|
||||
face.add_vertex(vertex()).expect("duplicate vertex");
|
||||
assert_eq!(face.vertices.len(), 1);
|
||||
assert_eq!(face.indices, [0, 0]);
|
||||
|
||||
let mut prim = ModelPrim::new().expect("model prim");
|
||||
prim.faces.push(face);
|
||||
prim.create_asset(UUID::zero()).expect("first model asset");
|
||||
let first = prim.asset.clone();
|
||||
prim.create_asset(UUID::zero()).expect("second model asset");
|
||||
assert!(!first.is_empty());
|
||||
assert_eq!(prim.asset, first);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_upload_resources_are_deterministic_and_offline() {
|
||||
let mut face = ModelFace::new().expect("model face");
|
||||
face.material.texture = "surface.jp2".into();
|
||||
face.material.texture_data = vec![1, 2, 3, 4];
|
||||
for position in [
|
||||
Vector3::zero(),
|
||||
Vector3 {
|
||||
x: 1.0,
|
||||
y: 0.0,
|
||||
z: 0.0,
|
||||
},
|
||||
Vector3 {
|
||||
x: 0.0,
|
||||
y: 1.0,
|
||||
z: 0.0,
|
||||
},
|
||||
] {
|
||||
face.add_vertex(Vertex {
|
||||
position,
|
||||
normal: Vector3 {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
z: 1.0,
|
||||
},
|
||||
tex_coord: Vector2::zero(),
|
||||
})
|
||||
.expect("upload vertex");
|
||||
}
|
||||
let mut prim = ModelPrim::new().expect("model prim");
|
||||
prim.faces.push(face);
|
||||
prim.create_asset(UUID::zero()).expect("mesh asset");
|
||||
let uploader = ModelUploader::new(
|
||||
GridClient::new().expect("offline client"),
|
||||
vec![prim],
|
||||
"Offline mesh".into(),
|
||||
"No network required".into(),
|
||||
)
|
||||
.expect("model uploader");
|
||||
let first = uploader.asset_resources(false).expect("offline resources");
|
||||
let second = uploader.asset_resources(false).expect("repeat resources");
|
||||
assert_eq!(first, second);
|
||||
let OSD::Map(map) = first else {
|
||||
panic!("resource map");
|
||||
};
|
||||
let OSD::Array(textures) = map.get("texture_list").expect("texture list") else {
|
||||
panic!("texture array");
|
||||
};
|
||||
assert_eq!(textures, &[OSD::Binary(Vec::new())]);
|
||||
let OSD::Map(upload) = uploader.asset_resources(true).expect("upload resources") else {
|
||||
panic!("upload map");
|
||||
};
|
||||
assert_eq!(
|
||||
upload.get("texture_list"),
|
||||
Some(&OSD::Array(vec![OSD::Binary(vec![1, 2, 3, 4])]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collada_triangle_converts_to_native_model_asset() {
|
||||
let root = temporary_directory("collada");
|
||||
let file = root.join("triangle.dae");
|
||||
fs::write(
|
||||
&file,
|
||||
r##"<?xml version="1.0" encoding="utf-8"?>
|
||||
<COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1">
|
||||
<library_geometries><geometry id="triangle"><mesh>
|
||||
<source id="positions"><float_array id="positions-array" count="9">0 0 0 1 0 0 0 1 0</float_array>
|
||||
<technique_common><accessor source="#positions-array" count="3" stride="3"/></technique_common></source>
|
||||
<vertices id="vertices"><input semantic="POSITION" source="#positions"/></vertices>
|
||||
<triangles count="1"><input semantic="VERTEX" source="#vertices" offset="0"/><p>0 1 2</p></triangles>
|
||||
</mesh></geometry></library_geometries>
|
||||
</COLLADA>"##,
|
||||
)
|
||||
.expect("write Collada fixture");
|
||||
let prims = ColladaLoader::new(None)
|
||||
.expect("Collada loader")
|
||||
.load(file.to_string_lossy().into_owned(), false)
|
||||
.expect("load Collada");
|
||||
assert_eq!(prims.len(), 1);
|
||||
assert_eq!(prims[0].faces.len(), 1);
|
||||
assert_eq!(prims[0].faces[0].indices, [0, 1, 2]);
|
||||
assert!(!prims[0].asset.is_empty());
|
||||
assert_eq!(prims[0].rotation, Quaternion::identity());
|
||||
fs::remove_dir_all(root).expect("remove temporary Collada directory");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collada_applies_units_effects_and_instance_material_bindings() {
|
||||
let root = temporary_directory("collada-material");
|
||||
let file = root.join("material.dae");
|
||||
fs::write(
|
||||
&file,
|
||||
r##"<COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema">
|
||||
<asset><unit meter="2"/><up_axis>Z_UP</up_axis></asset>
|
||||
<library_effects><effect id="green"><profile_COMMON><technique sid="common"><lambert>
|
||||
<diffuse><color>0.1 0.8 0.2 1</color></diffuse>
|
||||
</lambert></technique></profile_COMMON></effect></library_effects>
|
||||
<library_materials><material id="surface"><instance_effect url="#green"/></material></library_materials>
|
||||
<library_geometries><geometry id="triangle"><mesh>
|
||||
<source id="positions"><float_array>0 0 0 1 0 0 0 1 0</float_array>
|
||||
<technique_common><accessor stride="3"/></technique_common></source>
|
||||
<vertices id="vertices"><input semantic="POSITION" source="#positions"/></vertices>
|
||||
<triangles count="1" material="slot"><input semantic="VERTEX" source="#vertices" offset="0"/><p>0 1 2</p></triangles>
|
||||
</mesh></geometry></library_geometries>
|
||||
<library_visual_scenes><visual_scene id="scene"><node id="instance"><instance_geometry url="#triangle">
|
||||
<bind_material><technique_common><instance_material symbol="slot" target="#surface"/></technique_common></bind_material>
|
||||
</instance_geometry></node></visual_scene></library_visual_scenes>
|
||||
</COLLADA>"##,
|
||||
)
|
||||
.expect("write material Collada fixture");
|
||||
let prims = ColladaLoader::new(None)
|
||||
.expect("Collada loader")
|
||||
.load(file.to_string_lossy().into_owned(), false)
|
||||
.expect("load material Collada");
|
||||
assert_eq!(prims.len(), 1);
|
||||
assert_eq!(prims[0].scale.x, 2.0);
|
||||
assert_eq!(prims[0].scale.y, 2.0);
|
||||
assert_eq!(prims[0].faces[0].material.id, "surface");
|
||||
assert_eq!(prims[0].faces[0].material.diffuse_color.g, 0.8);
|
||||
fs::remove_dir_all(root).expect("remove material Collada directory");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collada_texture_path_cannot_escape_document_root() {
|
||||
let root = temporary_directory("collada-traversal");
|
||||
let file = root.join("traversal.dae");
|
||||
fs::write(
|
||||
&file,
|
||||
r##"<COLLADA><library_images><image id="escape"><init_from>../secret.jp2</init_from></image></library_images>
|
||||
<library_materials><material id="mat"><diffuse><texture texture="escape"/></diffuse></material></library_materials></COLLADA>"##,
|
||||
)
|
||||
.expect("write traversal fixture");
|
||||
assert!(
|
||||
ColladaLoader::new(None)
|
||||
.expect("Collada loader")
|
||||
.load(file.to_string_lossy().into_owned(), true)
|
||||
.is_err()
|
||||
);
|
||||
fs::remove_dir_all(root).expect("remove traversal directory");
|
||||
}
|
||||
@@ -515,3 +515,67 @@ fn gltf_node_explicit_matrix() {
|
||||
assert_close(matrix.m24, 6.0, 1e-5);
|
||||
assert_close(matrix.m34, 7.0, 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gltf_rejects_buffer_view_outside_declared_buffer() {
|
||||
let json = r#"{
|
||||
"asset":{"version":"2.0"},
|
||||
"buffers":[{"byteLength":4,"uri":"data:application/octet-stream;base64,AAAAAA=="}],
|
||||
"bufferViews":[{"buffer":0,"byteOffset":3,"byteLength":4}]
|
||||
}"#;
|
||||
assert!(GltfDocument::load_gltf(json.into(), None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gltf_rejects_cyclic_node_graphs() {
|
||||
let json = r#"{
|
||||
"asset":{"version":"2.0"},
|
||||
"nodes":[{"children":[1]},{"children":[0]}],
|
||||
"scenes":[{"nodes":[0]}],
|
||||
"scene":0
|
||||
}"#;
|
||||
assert!(GltfDocument::load_gltf(json.into(), None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gltf_surface_skin_and_animation_round_trip() {
|
||||
let json = r#"{
|
||||
"asset":{"version":"2.0"},
|
||||
"accessors":[
|
||||
{"componentType":5126,"count":0,"type":"SCALAR"},
|
||||
{"componentType":5126,"count":0,"type":"VEC3"}
|
||||
],
|
||||
"images":[{"uri":"data:image/png;base64,iVBORw0KGgo=","mimeType":"image/png","name":"image"}],
|
||||
"samplers":[{"magFilter":9729,"minFilter":9987,"wrapS":10497,"wrapT":33071,"name":"sampler"}],
|
||||
"textures":[{"sampler":0,"source":0,"name":"texture"}],
|
||||
"materials":[{
|
||||
"name":"surface",
|
||||
"pbrMetallicRoughness":{"baseColorTexture":{"index":0},"metallicRoughnessTexture":{"index":0}},
|
||||
"normalTexture":{"index":0,"scale":0.5},
|
||||
"occlusionTexture":{"index":0,"strength":0.75},
|
||||
"emissiveTexture":{"index":0},
|
||||
"emissiveFactor":[0.1,0.2,0.3]
|
||||
}],
|
||||
"nodes":[{"skin":0}],
|
||||
"skins":[{"joints":[0],"skeleton":0,"name":"skin"}],
|
||||
"animations":[{
|
||||
"name":"move",
|
||||
"samplers":[{"input":0,"output":1,"interpolation":"LINEAR"}],
|
||||
"channels":[{"sampler":0,"target":{"node":0,"path":"translation"}}]
|
||||
}]
|
||||
}"#;
|
||||
let document = GltfDocument::load_gltf(json.into(), None).expect("load complete surface");
|
||||
let serialized = document.to_json(None).expect("serialize complete surface");
|
||||
let round_trip = GltfDocument::load_gltf(serialized, None).expect("reload complete surface");
|
||||
assert_eq!(round_trip.images().len(), 1);
|
||||
assert_eq!(round_trip.samplers().len(), 1);
|
||||
assert_eq!(round_trip.textures().len(), 1);
|
||||
assert_eq!(round_trip.skins()[0].joints(), [0]);
|
||||
assert_eq!(round_trip.animations()[0].channels().len(), 1);
|
||||
let material = &round_trip.materials()[0];
|
||||
assert!(material.base_color_texture().is_some());
|
||||
assert!(material.metallic_roughness_texture().is_some());
|
||||
assert!(material.normal_texture().is_some());
|
||||
assert!(material.occlusion_texture().is_some());
|
||||
assert!(material.emissive_texture().is_some());
|
||||
}
|
||||
|
||||
@@ -6,16 +6,8 @@ use libremetaverse::messages::linden::AgentDropGroupMessage;
|
||||
use libremetaverse::packets::UseCircuitCodePacket;
|
||||
use libremetaverse::{BitPack, PacketFrequency, PermissionMask, Permissions, Primitive};
|
||||
|
||||
fn member_id<T>(result: Result<T, libremetaverse::Error>) -> &'static str {
|
||||
result
|
||||
.err()
|
||||
.expect("failure-only shim unexpectedly returned a value")
|
||||
.csharp_member()
|
||||
.expect("failure-only shim error")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_wire_data_domain_is_callable_and_failure_only() {
|
||||
fn each_wire_data_domain_is_callable_at_its_native_boundary() {
|
||||
let packet = UseCircuitCodePacket::new_with_constructor()
|
||||
.expect("generated packet constructor is native");
|
||||
assert_eq!(packet.circuit_code.code, 0);
|
||||
@@ -26,20 +18,26 @@ fn each_wire_data_domain_is_callable_and_failure_only() {
|
||||
.is_empty()
|
||||
);
|
||||
assert_eq!(
|
||||
member_id(AssetTexture::new_with_constructor()),
|
||||
"M:LibreMetaverse.Assets.AssetTexture.#ctor"
|
||||
AssetTexture::new_with_constructor()
|
||||
.expect("native asset texture constructor")
|
||||
.components,
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
member_id(GltfDocument::new()),
|
||||
"M:LibreMetaverse.Assets.Gltf.GltfDocument.#ctor"
|
||||
GltfDocument::new()
|
||||
.expect("native glTF constructor")
|
||||
.version(),
|
||||
"2.0"
|
||||
);
|
||||
assert!(
|
||||
ModelPrim::new()
|
||||
.expect("native model constructor")
|
||||
.asset
|
||||
.is_empty()
|
||||
);
|
||||
assert_eq!(
|
||||
member_id(ModelPrim::new()),
|
||||
"M:LibreMetaverse.ImportExport.ModelPrim.#ctor"
|
||||
);
|
||||
assert_eq!(
|
||||
member_id(OarFile::package_archive(String::new(), String::new())),
|
||||
"M:LibreMetaverse.Assets.OarFile.PackageArchive(System.String,System.String)"
|
||||
OarFile::package_archive(String::new(), String::new()),
|
||||
Err(libremetaverse::Error::InvalidOperation)
|
||||
);
|
||||
let bit_pack = BitPack::new(Vec::new(), 0).expect("native bit pack constructor");
|
||||
assert_eq!(bit_pack.byte_pos(), 0);
|
||||
|
||||
Reference in New Issue
Block a user