Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/Assimp/BoneProcessor_test.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <cmath>
#include "BoneProcessor.h"

class BoneProcessorTest : public ::testing::Test {
Expand Down Expand Up @@ -155,3 +156,90 @@ TEST_F(BoneProcessorTest, BoneTransformation) {
EXPECT_EQ(testBone->getOrientation(), expectedOrientation);
EXPECT_EQ(testBone->getScale(), expectedScale);
}

TEST_F(BoneProcessorTest, BakeZupToYupRotatesOnlyRootBones)
{
Ogre::SkeletonPtr skeleton = Ogre::SkeletonManager::getSingleton().create(
"BakeZupToYupSkeleton",
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
true);
ASSERT_TRUE(skeleton);

Ogre::Bone* root = skeleton->createBone("Root");
Ogre::Bone* child = skeleton->createBone("Child");
root->addChild(child);

root->setPosition(Ogre::Vector3(0.0f, 0.0f, 2.0f));
root->setOrientation(Ogre::Quaternion::IDENTITY);
child->setPosition(Ogre::Vector3(1.0f, 2.0f, 3.0f));
child->setOrientation(Ogre::Quaternion::IDENTITY);

BoneProcessor::bakeZupToYup(skeleton);

const Ogre::Quaternion rx90(Ogre::Degree(90), Ogre::Vector3::UNIT_X);
const Ogre::Vector3 expectedRootPos = rx90 * Ogre::Vector3(0.0f, 0.0f, 2.0f);
EXPECT_NEAR(root->getPosition().x, expectedRootPos.x, 1e-5);
EXPECT_NEAR(root->getPosition().y, expectedRootPos.y, 1e-5);
EXPECT_NEAR(root->getPosition().z, expectedRootPos.z, 1e-5);
EXPECT_NEAR(std::abs(root->getOrientation().Dot(rx90)), 1.0f, 1e-5);

// Child local transforms are preserved.
EXPECT_EQ(child->getPosition(), Ogre::Vector3(1.0f, 2.0f, 3.0f));
EXPECT_EQ(child->getOrientation(), Ogre::Quaternion::IDENTITY);
}

TEST_F(BoneProcessorTest, AnimationOnlyHierarchyCreatesAnimatedBonesAndLeafChildren)
{
mockScene.mNumMeshes = 0;
mockScene.mMeshes = nullptr;

aiNode* rootNode = new aiNode();
rootNode->mName = aiString("Root");
rootNode->mTransformation = aiMatrix4x4();

aiNode* armatureNode = new aiNode();
armatureNode->mName = aiString("Armature");
armatureNode->mTransformation = aiMatrix4x4();
armatureNode->mParent = rootNode;

aiNode* hipsNode = new aiNode();
hipsNode->mName = aiString("Hips");
hipsNode->mTransformation = aiMatrix4x4();
hipsNode->mParent = armatureNode;

aiNode* tipNode = new aiNode();
tipNode->mName = aiString("HandTip");
tipNode->mTransformation = aiMatrix4x4();
tipNode->mParent = hipsNode;

rootNode->mNumChildren = 1;
rootNode->mChildren = new aiNode*[1]{armatureNode};
armatureNode->mNumChildren = 1;
armatureNode->mChildren = new aiNode*[1]{hipsNode};
hipsNode->mNumChildren = 1;
hipsNode->mChildren = new aiNode*[1]{tipNode};
tipNode->mNumChildren = 0;
tipNode->mChildren = nullptr;

mockScene.mRootNode = rootNode;

aiAnimation* anim = new aiAnimation();
anim->mName = aiString("Take001");
anim->mNumChannels = 1;
anim->mChannels = new aiNodeAnim*[1];
anim->mChannels[0] = new aiNodeAnim();
anim->mChannels[0]->mNodeName = aiString("Hips");
anim->mChannels[0]->mNumPositionKeys = 0;
anim->mChannels[0]->mNumRotationKeys = 0;
anim->mChannels[0]->mNumScalingKeys = 0;

mockScene.mNumAnimations = 1;
mockScene.mAnimations = new aiAnimation*[1]{anim};

processor.processBones(mockSkeleton, &mockScene);

EXPECT_TRUE(mockSkeleton->hasBone("Hips"));
EXPECT_TRUE(mockSkeleton->hasBone("HandTip"));
EXPECT_FALSE(mockSkeleton->hasBone("Armature"));
EXPECT_EQ(mockSkeleton->getBone("HandTip")->getParent(), mockSkeleton->getBone("Hips"));
}
55 changes: 55 additions & 0 deletions src/Assimp/MaterialProcessor_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,58 @@ TEST(MaterialProcessorTest, ProcessMaterialReturnsExistingMaterialIfAlreadyCreat

Ogre::MaterialManager::getSingleton().remove(name);
}

TEST(MaterialProcessorTest, ExistingMaterialWithoutNormalMapIsReturnedUnchanged) {
auto ogreRoot = std::make_unique<Ogre::Root>();
ensureMaterialManagerInitialised();

const std::string name = "MaterialProcessorExistingNoNormal";
Ogre::MaterialPtr existing = Ogre::MaterialManager::getSingleton().create(
name, Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
ASSERT_TRUE(existing);
ASSERT_NE(existing->getTechnique(0), nullptr);
ASSERT_NE(existing->getTechnique(0)->getPass(0), nullptr);
EXPECT_EQ(existing->getTechnique(0)->getPass(0)->getNumTextureUnitStates(), 0u);

MaterialProcessor processor;
aiScene scene{};
aiMaterial material;
aiString matName(name);
material.AddProperty(&matName, AI_MATKEY_NAME);

Ogre::MaterialPtr out = processor.processMaterial(&material, &scene);
ASSERT_TRUE(out);
EXPECT_EQ(out.get(), existing.get());
EXPECT_EQ(out->getTechnique(0)->getPass(0)->getNumTextureUnitStates(), 0u);

Ogre::MaterialManager::getSingleton().remove(name);
}

TEST(MaterialProcessorTest, LoadSceneUnnamedMaterialsGetSequentialImportedNames) {
auto ogreRoot = std::make_unique<Ogre::Root>();
ensureMaterialManagerInitialised();

// Ensure predictable names even if previous tests left materials around.
if (Ogre::MaterialManager::getSingleton().getByName("importedMaterial0"))
Ogre::MaterialManager::getSingleton().remove("importedMaterial0");
if (Ogre::MaterialManager::getSingleton().getByName("importedMaterial1"))
Ogre::MaterialManager::getSingleton().remove("importedMaterial1");

MaterialProcessor processor;
aiScene scene{};
scene.mNumMaterials = 2;
scene.mMaterials = new aiMaterial*[2];
scene.mMaterials[0] = new aiMaterial();
scene.mMaterials[1] = new aiMaterial();

processor.loadScene(&scene);

ASSERT_EQ(processor.size(), 2UL);
EXPECT_EQ(processor[0]->getName(), "importedMaterial0");
EXPECT_EQ(processor[1]->getName(), "importedMaterial1");

if (Ogre::MaterialManager::getSingleton().getByName("importedMaterial0"))
Ogre::MaterialManager::getSingleton().remove("importedMaterial0");
if (Ogre::MaterialManager::getSingleton().getByName("importedMaterial1"))
Ogre::MaterialManager::getSingleton().remove("importedMaterial1");
}
187 changes: 186 additions & 1 deletion src/CLIPipeline_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ QString testDataDir()
return dir.absoluteFilePath("media/models");
}

QString writeMinimalObj(const QString& dirPath, const QString& fileName)
{
const QString path = QDir(dirPath).filePath(fileName);
QFile f(path);
if (!f.open(QIODevice::WriteOnly | QIODevice::Text))
return QString();

f.write(
"o Tri\n"
"v 0 0 0\n"
"v 1 0 0\n"
"v 0 1 0\n"
"f 1 2 3\n");
f.close();
return path;
}

Ogre::MeshPtr createTwoSubmeshSharedMesh(const std::string& name)
{
Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().createManual(
Expand Down Expand Up @@ -1769,6 +1786,44 @@ TEST_F(CLIPipelineCmdLodTest, CmdLod_InfoAndRemoveFromGeneratedMesh)
QFile::remove(QDir::tempPath() + "/cli_lod_removed.material");
}

// ==========================================================================
// cmdPose error paths
// ==========================================================================

TEST(CLIPipelineCmdPoseError, NoFile)
{
TestArgv args({"qtmesh", "pose"});
EXPECT_EQ(CLIPipeline::cmdPose(args.argc(), args.argv()), 2);
}

TEST(CLIPipelineCmdPoseError, MissingAnimation)
{
TestArgv args({"qtmesh", "pose", "some_file.fbx", "--time", "0.0", "-o", "pose.obj"});
EXPECT_EQ(CLIPipeline::cmdPose(args.argc(), args.argv()), 2);
}

TEST(CLIPipelineCmdPoseError, MissingOutput)
{
TestArgv args({"qtmesh", "pose", "some_file.fbx", "--animation", "Idle", "--time", "0.0"});
EXPECT_EQ(CLIPipeline::cmdPose(args.argc(), args.argv()), 2);
}

TEST(CLIPipelineCmdPoseError, MissingTimeAndCount)
{
TestArgv args({"qtmesh", "pose", "some_file.fbx", "--animation", "Idle", "-o", "pose.obj"});
EXPECT_EQ(CLIPipeline::cmdPose(args.argc(), args.argv()), 2);
}

TEST(CLIPipelineCmdPoseError, NonexistentFile)
{
QTemporaryDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());
const QString missingFile = QDir(tmpDir.path()).filePath("qtmesh_pose_missing.fbx");
QByteArray missingFileBa = missingFile.toUtf8();
TestArgv args({"qtmesh", "pose", missingFileBa.constData(), "--animation", "Idle", "--time", "0.0", "-o", "pose.obj"});
EXPECT_EQ(CLIPipeline::cmdPose(args.argc(), args.argv()), 1);
}

// ==========================================================================
// cmdScan tests
// ==========================================================================
Expand All @@ -1780,7 +1835,7 @@ TEST(CLIPipelineCmdScanError, MissingConfigFileReturns2)

const QString missingConfig = tmpDir.filePath("qtmesh_scan_missing_config.yml");
QFile::remove(missingConfig); // Ensure this path does not exist.
ASSERT_FALSE(QFileInfo::exists(missingConfig));
ASSERT_FALSE(QFile::exists(missingConfig));

QByteArray configBa = missingConfig.toUtf8();
TestArgv args({"qtmesh", "scan", "--config", configBa.constData()});
Expand Down Expand Up @@ -1808,6 +1863,82 @@ TEST(CLIPipelineCmdScanError, NonDirectoryScanRootReturns2)
EXPECT_EQ(CLIPipeline::cmdScan(args.argc(), args.argv()), 2);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

TEST(CLIPipelineCmdScanError, ScanRootMustBeDirectoryWithConfigReturns2)
{
QTemporaryDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());

const QString filePath = QDir(tmpDir.path()).filePath("not_a_dir.txt");
QFile file(filePath);
ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text));
file.write("x");
file.close();

const QString configPath = QDir(tmpDir.path()).filePath("scan.yml");
QFile cfg(configPath);
ASSERT_TRUE(cfg.open(QIODevice::WriteOnly | QIODevice::Text));
cfg.write(
"scan:\n"
" include:\n"
" - \"**/*.obj\"\n");
cfg.close();

QByteArray filePathBa = filePath.toUtf8();
QByteArray configBa = configPath.toUtf8();
TestArgv args({"qtmesh", "scan", filePathBa.constData(), "--config", configBa.constData()});
EXPECT_EQ(CLIPipeline::cmdScan(args.argc(), args.argv()), 2);
}

TEST(CLIPipelineCmdScan, WritesJsonAndSarifReports)
{
QTemporaryDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());

const QString rootPath = QDir(tmpDir.path()).filePath("assets");
ASSERT_TRUE(QDir().mkpath(rootPath));
ASSERT_FALSE(writeMinimalObj(rootPath, "scan_mesh.obj").isEmpty());

const QString configPath = QDir(tmpDir.path()).filePath("scan.yml");
QFile cfg(configPath);
ASSERT_TRUE(cfg.open(QIODevice::WriteOnly | QIODevice::Text));
cfg.write(
"scan:\n"
" include:\n"
" - \"**/*.obj\"\n"
"rules:\n"
" allow_missing_materials: true\n");
cfg.close();

const QString reportPath = QDir(tmpDir.path()).filePath("reports/out/scan.json");
const QString sarifPath = QDir(tmpDir.path()).filePath("reports/out/scan.sarif");
QFile::remove(reportPath);
QFile::remove(sarifPath);

QByteArray rootBa = rootPath.toUtf8();
QByteArray configBa = configPath.toUtf8();
QByteArray reportBa = reportPath.toUtf8();
QByteArray sarifBa = sarifPath.toUtf8();
TestArgv args({"qtmesh", "scan", rootBa.constData(), "--config", configBa.constData(),
"--json", "--report", reportBa.constData(), "--sarif", sarifBa.constData(),
"--fail-on", "never"});

EXPECT_EQ(CLIPipeline::cmdScan(args.argc(), args.argv()), 0);
ASSERT_TRUE(QFile::exists(reportPath));
ASSERT_TRUE(QFile::exists(sarifPath));

QFile reportFile(reportPath);
ASSERT_TRUE(reportFile.open(QIODevice::ReadOnly | QIODevice::Text));
const QString reportContent = QString::fromUtf8(reportFile.readAll());
EXPECT_TRUE(reportContent.contains("\"summary\""));
EXPECT_TRUE(reportContent.contains("\"assets\""));

QFile sarifFile(sarifPath);
ASSERT_TRUE(sarifFile.open(QIODevice::ReadOnly | QIODevice::Text));
const QString sarifContent = QString::fromUtf8(sarifFile.readAll());
EXPECT_TRUE(sarifContent.contains("\"runs\""));
EXPECT_TRUE(sarifContent.contains("qtmesh scan"));
}

TEST(CLIPipelineCmdScan, ReportAndSarifAreWrittenWithFailOnNever)
{
QTemporaryDir tmpDir;
Expand Down Expand Up @@ -1851,6 +1982,60 @@ TEST(CLIPipelineCmdScan, ReportAndSarifAreWrittenWithFailOnNever)
EXPECT_TRUE(sarif.contains("\"tool\""));
}

TEST(CLIPipelineCmdScan, FailOnWarningReturnsFailure)
{
QTemporaryDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());

const QString rootPath = QDir(tmpDir.path()).filePath("assets");
ASSERT_TRUE(QDir().mkpath(rootPath));
ASSERT_FALSE(writeMinimalObj(rootPath, "PlayerModel.obj").isEmpty());

const QString configPath = QDir(tmpDir.path()).filePath("scan.yml");
QFile cfg(configPath);
ASSERT_TRUE(cfg.open(QIODevice::WriteOnly | QIODevice::Text));
cfg.write(
"scan:\n"
" include:\n"
" - \"**/*.obj\"\n"
"rules:\n"
" file_name_case: snake_case\n");
cfg.close();

QByteArray rootBa = rootPath.toUtf8();
QByteArray configBa = configPath.toUtf8();
TestArgv args({"qtmesh", "scan", rootBa.constData(), "--config", configBa.constData(),
"--fail-on", "warning"});
EXPECT_EQ(CLIPipeline::cmdScan(args.argc(), args.argv()), 1);
}

TEST(CLIPipelineCmdScan, FailOnNeverAllowsWarnings)
{
QTemporaryDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());

const QString rootPath = QDir(tmpDir.path()).filePath("assets");
ASSERT_TRUE(QDir().mkpath(rootPath));
ASSERT_FALSE(writeMinimalObj(rootPath, "PlayerModel.obj").isEmpty());

const QString configPath = QDir(tmpDir.path()).filePath("scan.yml");
QFile cfg(configPath);
ASSERT_TRUE(cfg.open(QIODevice::WriteOnly | QIODevice::Text));
cfg.write(
"scan:\n"
" include:\n"
" - \"**/*.obj\"\n"
"rules:\n"
" file_name_case: snake_case\n");
cfg.close();

QByteArray rootBa = rootPath.toUtf8();
QByteArray configBa = configPath.toUtf8();
TestArgv args({"qtmesh", "scan", rootBa.constData(), "--config", configBa.constData(),
"--fail-on", "never"});
EXPECT_EQ(CLIPipeline::cmdScan(args.argc(), args.argv()), 0);
}

TEST(CLIPipelineCmdScan, IncludePatternNormalizesBareExtension)
{
QTemporaryDir tmpDir;
Expand Down
12 changes: 12 additions & 0 deletions src/MeshImporterExporter_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1281,3 +1281,15 @@ TEST(MeshImporterExporterStandaloneTest, FormatFileURI_GlbFormat_CorrectExtensio
QString result = MeshImporterExporter::formatFileURI("/tmp/model", "glTF 2.0 Binary (*.glb)");
EXPECT_EQ(result, "/tmp/model.glb");
}

TEST(MeshImporterExporterStandaloneTest, FormatFileURI_HumanReadableUnknownFormatDoesNotAppend)
{
QString result = MeshImporterExporter::formatFileURI("/tmp/model", "Custom Export Format");
EXPECT_EQ(result, "/tmp/model");
}

TEST(MeshImporterExporterStandaloneTest, FormatFileURI_ShortAliasUppercaseIsAppendedAsGiven)
{
QString result = MeshImporterExporter::formatFileURI("/tmp/model", "FBX");
EXPECT_EQ(result, "/tmp/model.FBX");
}
Loading
Loading