Skip to content

Commit 6de101e

Browse files
committed
add initial tests
1 parent 499cf84 commit 6de101e

File tree

3 files changed

+389
-3
lines changed

3 files changed

+389
-3
lines changed

res/fs_test.ltx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
; File system configuration for tests
2+
3+
[path]
4+
$fs_root$ = false | false | $fs_root$
5+
$game_data$ = false | true | $fs_root$ | gamedata\
6+
$game_meshes$ = false | true | $game_data$ | meshes\
7+
$game_textures$ = false | true | $game_data$ | textures\
8+
$logs$ = false | false | $fs_root$ | logs\
9+
$screenshots$ = false | false | $fs_root$ | screenshots\
10+
$downloads$ = false | false | $fs_root$ | downloads\
11+
$temp$ = false | false | $fs_root$ | temp\
12+
13+
[file_systems]
14+
; Add paths that should be mounted
15+
$fs_root$ = ./
16+
$game_data$ = ./gamedata/
17+
$logs$ = ./logs/
18+
$temp$ = ./temp/
Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,23 @@
1-
add_library(xrAnimation_tests INTERFACE)
1+
cmake_minimum_required(VERSION 3.8)
22

3-
# Placeholder until we wire up real gtest suites.
4-
target_link_libraries(xrAnimation_tests INTERFACE xrAnimation)
3+
add_executable(xrAnimation_converter_tests
4+
test_converter.cpp)
5+
6+
# Provide include paths for dependency headers.
7+
target_include_directories(xrAnimation_converter_tests PRIVATE
8+
${CMAKE_CURRENT_SOURCE_DIR}/..
9+
${CMAKE_SOURCE_DIR}/Externals/ozz-animation/include)
10+
11+
target_link_libraries(xrAnimation_converter_tests PRIVATE
12+
ozz_animation
13+
ozz_base)
14+
15+
target_compile_features(xrAnimation_converter_tests PRIVATE cxx_std_17)
16+
17+
get_filename_component(XRAY_WORKSPACE_ROOT ${CMAKE_SOURCE_DIR} DIRECTORY)
18+
19+
target_compile_definitions(xrAnimation_converter_tests PRIVATE
20+
WORKSPACE_ROOT="${XRAY_WORKSPACE_ROOT}")
21+
22+
set_target_properties(xrAnimation_converter_tests PROPERTIES
23+
FOLDER "xrAnimation/Tests")
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
#include <array>
2+
#include <cerrno>
3+
#include <cmath>
4+
#include <cstdlib>
5+
#include <filesystem>
6+
#include <iostream>
7+
#include <optional>
8+
#include <sstream>
9+
#include <string>
10+
#include <string_view>
11+
#include <system_error>
12+
#include <vector>
13+
14+
#ifndef WORKSPACE_ROOT
15+
#error "WORKSPACE_ROOT compile definition must be provided"
16+
#endif
17+
18+
#ifdef _WIN32
19+
#include <windows.h>
20+
#else
21+
#include <sys/wait.h>
22+
#endif
23+
24+
#include "ozz/animation/runtime/skeleton.h"
25+
#include "ozz/animation/runtime/skeleton_utils.h"
26+
#include "ozz/base/io/archive.h"
27+
#include "ozz/base/io/stream.h"
28+
29+
namespace fs = std::filesystem;
30+
31+
namespace
32+
{
33+
34+
fs::path WorkspaceRoot()
35+
{
36+
static const fs::path root = fs::path(WORKSPACE_ROOT);
37+
return root;
38+
}
39+
40+
fs::path TestArtifactsDir()
41+
{
42+
return WorkspaceRoot() / "asset_tests" / "test_outputs";
43+
}
44+
45+
std::string QuoteForShell(const std::string& value)
46+
{
47+
#ifdef _WIN32
48+
std::string quoted = "\"";
49+
for (char ch : value)
50+
{
51+
if (ch == '\\' || ch == '"')
52+
quoted.push_back('\\');
53+
quoted.push_back(ch);
54+
}
55+
quoted.push_back('"');
56+
return quoted;
57+
#else
58+
std::string quoted;
59+
quoted.reserve(value.size() + 2);
60+
quoted.push_back('\'');
61+
for (char ch : value)
62+
{
63+
if (ch == '\'')
64+
quoted.append("'\\''");
65+
else
66+
quoted.push_back(ch);
67+
}
68+
quoted.push_back('\'');
69+
return quoted;
70+
#endif
71+
}
72+
73+
fs::path ResolveConverterBinary()
74+
{
75+
#ifdef _WIN32
76+
const std::string executable_name = "xray_to_ozz_converter.exe";
77+
#else
78+
const std::string executable_name = "xray_to_ozz_converter";
79+
#endif
80+
81+
const fs::path build_bin = WorkspaceRoot() / "xray-16" / "ozz_utils" / "bin";
82+
83+
const std::array<fs::path, 2> candidates = {
84+
build_bin / "Debug" / executable_name,
85+
build_bin / executable_name};
86+
87+
for (const auto& path : candidates)
88+
{
89+
if (fs::exists(path))
90+
return path;
91+
}
92+
93+
std::ostringstream oss;
94+
oss << "Unable to locate xray_to_ozz_converter binary. Checked:";
95+
for (const auto& candidate : candidates)
96+
oss << '\n' << " " << candidate.string();
97+
throw std::runtime_error(oss.str());
98+
}
99+
100+
std::string BuildCommand(const std::vector<std::string>& args)
101+
{
102+
const fs::path converter = ResolveConverterBinary();
103+
const fs::path converter_dir = converter.parent_path();
104+
105+
#ifdef _WIN32
106+
std::string command = QuoteForShell(converter.string());
107+
for (const std::string& arg : args)
108+
{
109+
command.push_back(' ');
110+
command.append(QuoteForShell(arg));
111+
}
112+
return command;
113+
#else
114+
// Prepend LD_LIBRARY_PATH so the converter can locate ozz shared objects.
115+
std::string command = "LD_LIBRARY_PATH=";
116+
command.append(QuoteForShell(converter_dir.string()));
117+
command.push_back(' ');
118+
command.append(QuoteForShell(converter.string()));
119+
for (const std::string& arg : args)
120+
{
121+
command.push_back(' ');
122+
command.append(QuoteForShell(arg));
123+
}
124+
return command;
125+
#endif
126+
}
127+
128+
int ExecuteCommand(const std::vector<std::string>& args)
129+
{
130+
const std::string command = BuildCommand(args);
131+
const int result = std::system(command.c_str());
132+
if (result == -1)
133+
return -1;
134+
135+
#ifdef _WIN32
136+
return result;
137+
#else
138+
if (WIFEXITED(result))
139+
return WEXITSTATUS(result);
140+
if (WIFSIGNALED(result))
141+
return 128 + WTERMSIG(result);
142+
return result;
143+
#endif
144+
}
145+
146+
bool GenerateSkeleton(bool force)
147+
{
148+
const fs::path output_dir = TestArtifactsDir();
149+
const fs::path output_file = output_dir / "stalker_hero_bind_pose.ozz";
150+
const fs::path input_file = WorkspaceRoot() / "gamedata" / "stalker_hero" / "stalker_hero_1.ogf";
151+
152+
std::error_code ec;
153+
fs::create_directories(output_dir, ec);
154+
(void)ec;
155+
156+
if (force && fs::exists(output_file))
157+
fs::remove(output_file);
158+
159+
if (!force && fs::exists(output_file))
160+
return true;
161+
162+
std::vector<std::string> args = {
163+
"skeleton",
164+
input_file.string(),
165+
output_file.string()};
166+
167+
const int exit_code = ExecuteCommand(args);
168+
if (exit_code != 0)
169+
{
170+
std::cerr << "xray_to_ozz_converter returned exit code " << exit_code << std::endl;
171+
return false;
172+
}
173+
174+
if (!fs::exists(output_file))
175+
{
176+
std::cerr << "converter reported success but output file is missing: " << output_file << std::endl;
177+
return false;
178+
}
179+
180+
return true;
181+
}
182+
183+
bool EnsureSkeletonGenerated()
184+
{
185+
static bool cached = false;
186+
static bool status = false;
187+
if (!cached)
188+
{
189+
status = GenerateSkeleton(false);
190+
cached = true;
191+
}
192+
return status;
193+
}
194+
195+
struct ExpectedBindPose
196+
{
197+
const char* joint;
198+
float tx;
199+
float ty;
200+
float tz;
201+
};
202+
203+
const std::array<ExpectedBindPose, 5> kExpectedBindPose = {{
204+
{"root_stalker", 0.0f, 0.0f, 0.0f},
205+
{"bip01", 6.96513e-06f, 0.987438f, 4.5056e-06f},
206+
{"bip01_pelvis", 0.0f, 0.0f, 0.0f},
207+
{"bip01_spine", 0.102435f, 1.76455e-07f, 0.0213843f},
208+
{"bip01_head", 0.0559939f, 2.85225e-09f, 1.90456e-08f},
209+
}};
210+
211+
constexpr float kTranslationTolerance = 1e-4f;
212+
213+
fs::path SkeletonOutputPath()
214+
{
215+
return TestArtifactsDir() / "stalker_hero_bind_pose.ozz";
216+
}
217+
218+
bool TestGenerateSkeleton()
219+
{
220+
std::cout << "Generating skeleton via converter..." << std::endl;
221+
return GenerateSkeleton(true);
222+
}
223+
224+
bool TestBindPoseMatchesBlender()
225+
{
226+
if (!EnsureSkeletonGenerated())
227+
return false;
228+
229+
const fs::path skeleton_path = SkeletonOutputPath();
230+
ozz::io::File file(skeleton_path.string().c_str(), "rb");
231+
if (!file.opened())
232+
{
233+
std::cerr << "failed to open skeleton: " << skeleton_path << std::endl;
234+
return false;
235+
}
236+
237+
ozz::io::IArchive archive(&file);
238+
ozz::animation::Skeleton skeleton;
239+
archive >> skeleton;
240+
241+
bool ok = true;
242+
for (const auto& expected : kExpectedBindPose)
243+
{
244+
const int joint_index = ozz::animation::FindJoint(skeleton, expected.joint);
245+
if (joint_index < 0)
246+
{
247+
std::cerr << "joint not found in skeleton: " << expected.joint << std::endl;
248+
ok = false;
249+
continue;
250+
}
251+
252+
const ozz::math::Transform rest = ozz::animation::GetJointLocalRestPose(skeleton, joint_index);
253+
const float dx = std::fabs(rest.translation.x - expected.tx);
254+
const float dy = std::fabs(rest.translation.y - expected.ty);
255+
const float dz = std::fabs(rest.translation.z - expected.tz);
256+
if (dx > kTranslationTolerance || dy > kTranslationTolerance || dz > kTranslationTolerance)
257+
{
258+
std::cerr << "bind pose mismatch for joint '" << expected.joint << "'\n"
259+
<< " expected: [" << expected.tx << ", " << expected.ty << ", " << expected.tz << "]\n"
260+
<< " actual: [" << rest.translation.x << ", " << rest.translation.y << ", " << rest.translation.z << "]\n";
261+
ok = false;
262+
}
263+
}
264+
265+
return ok;
266+
}
267+
268+
bool TestGenerateAnimation()
269+
{
270+
const fs::path input_skeleton = WorkspaceRoot() / "gamedata" / "stalker_hero" / "stalker_hero_1.ogf";
271+
const fs::path input_animation = WorkspaceRoot() / "gamedata" / "critical_hit_grup_1.omf";
272+
const fs::path output_dir = TestArtifactsDir();
273+
const fs::path output_animation = output_dir / "critical_hit_grup_1.ozz";
274+
275+
std::vector<std::string> args = {
276+
"animation",
277+
input_animation.string(),
278+
output_animation.string(),
279+
input_skeleton.string()};
280+
281+
const int exit_code = ExecuteCommand(args);
282+
if (exit_code != 0)
283+
{
284+
std::cerr << "[TODO] animation conversion is not yet implemented (converter exit code " << exit_code << ")" << std::endl;
285+
return false;
286+
}
287+
288+
return true;
289+
}
290+
291+
struct TestCase
292+
{
293+
const char* name;
294+
bool (*func)();
295+
bool expected_to_fail;
296+
};
297+
298+
} // namespace
299+
300+
int main()
301+
{
302+
const std::array<TestCase, 3> tests = {{
303+
{"GenerateSkeleton", &TestGenerateSkeleton, false},
304+
{"BindPoseMatchesBlender", &TestBindPoseMatchesBlender, false},
305+
{"GenerateAnimation", &TestGenerateAnimation, true},
306+
}};
307+
308+
int failures = 0;
309+
for (const auto& test : tests)
310+
{
311+
std::cout << "[ RUN ] " << test.name << std::endl;
312+
bool result = false;
313+
try
314+
{
315+
result = test.func();
316+
}
317+
catch (const std::exception& ex)
318+
{
319+
std::cerr << "Test threw exception: " << ex.what() << std::endl;
320+
result = false;
321+
}
322+
catch (...)
323+
{
324+
std::cerr << "Test threw unknown exception" << std::endl;
325+
result = false;
326+
}
327+
328+
if (result)
329+
{
330+
std::cout << "[ OK ] " << test.name << std::endl;
331+
}
332+
else
333+
{
334+
++failures;
335+
std::cout << "[ FAILED ] " << test.name;
336+
if (test.expected_to_fail)
337+
std::cout << " (expected TODO failure)";
338+
std::cout << std::endl;
339+
}
340+
}
341+
342+
const int passed = static_cast<int>(tests.size()) - failures;
343+
std::cout << "[==========] " << tests.size() << " tests run.\n";
344+
std::cout << "[ PASSED ] " << passed << " tests." << std::endl;
345+
if (failures > 0)
346+
std::cout << "[ FAILED ] " << failures << " tests." << std::endl;
347+
348+
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
349+
}

0 commit comments

Comments
 (0)