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
110 changes: 110 additions & 0 deletions src/BatchExporter_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#include <gtest/gtest.h>
#include "BatchExporter.h"
#include <QApplication>
#include <QCoreApplication>
#include <QSignalSpy>
#include <QTemporaryDir>

class BatchExporterTests : public ::testing::Test {
protected:
QApplication* app = nullptr;

void SetUp() override {
app = qobject_cast<QApplication*>(QCoreApplication::instance());
ASSERT_NE(app, nullptr);
}

void TearDown() override {
if (app) app->processEvents();
}
};

TEST_F(BatchExporterTests, ConstructDestruct) {
BatchExporter exporter;
// Should construct and destruct without crash
}

TEST_F(BatchExporterTests, SetInputFiles) {
BatchExporter exporter;
QStringList files = {"file1.fbx", "file2.obj", "file3.dae"};
EXPECT_NO_THROW(exporter.setInputFiles(files));
}

TEST_F(BatchExporterTests, SetOutputFormat) {
BatchExporter exporter;
EXPECT_NO_THROW(exporter.setOutputFormat("gltf2"));
}

TEST_F(BatchExporterTests, SetOutputDirectory) {
BatchExporter exporter;
EXPECT_NO_THROW(exporter.setOutputDirectory("/tmp/batch_export"));
}

TEST_F(BatchExporterTests, ExecuteWithEmptyFileList) {
BatchExporter exporter;
exporter.setInputFiles(QStringList());
exporter.setOutputFormat("gltf2");
exporter.setOutputDirectory("/tmp/batch_export_test");

QSignalSpy finishedSpy(&exporter, &BatchExporter::finished);

exporter.execute();

ASSERT_EQ(finishedSpy.count(), 1);
// With empty file list, success=0, fail=0
EXPECT_EQ(finishedSpy.at(0).at(0).toInt(), 0); // successCount
EXPECT_EQ(finishedSpy.at(0).at(1).toInt(), 0); // failCount
}

TEST_F(BatchExporterTests, SetOutputDirectoryEmpty) {
// Verify that setting empty output dir doesn't crash
BatchExporter exporter;
EXPECT_NO_THROW(exporter.setOutputDirectory(""));
}

TEST_F(BatchExporterTests, ParentOwnership) {
QObject parent;
auto* exporter = new BatchExporter(&parent);
EXPECT_EQ(exporter->parent(), &parent);
// parent will delete exporter on destruction
}

TEST_F(BatchExporterTests, SignalConnections) {
BatchExporter exporter;

// Verify all signals can be connected to without issue
QSignalSpy progressSpy(&exporter, &BatchExporter::progressChanged);
QSignalSpy finishedSpy(&exporter, &BatchExporter::finished);
QSignalSpy errorSpy(&exporter, &BatchExporter::error);

EXPECT_TRUE(progressSpy.isValid());
EXPECT_TRUE(finishedSpy.isValid());
EXPECT_TRUE(errorSpy.isValid());
}

TEST_F(BatchExporterTests, SetMultipleFormats) {
BatchExporter exporter;
EXPECT_NO_THROW(exporter.setOutputFormat("gltf2"));
EXPECT_NO_THROW(exporter.setOutputFormat("obj"));
EXPECT_NO_THROW(exporter.setOutputFormat("fbx"));
EXPECT_NO_THROW(exporter.setOutputFormat("dae"));
}

TEST_F(BatchExporterTests, ExecuteEmptyListWithEmptyDir) {
BatchExporter exporter;
exporter.setInputFiles(QStringList());
exporter.setOutputFormat("obj");
exporter.setOutputDirectory(""); // empty dir

QSignalSpy finishedSpy(&exporter, &BatchExporter::finished);
exporter.execute();

ASSERT_EQ(finishedSpy.count(), 1);
EXPECT_EQ(finishedSpy.at(0).at(0).toInt(), 0); // successCount
EXPECT_EQ(finishedSpy.at(0).at(1).toInt(), 0); // failCount
}

// Note: Tests that call execute() with actual files are NOT included here
// because BatchExporter::execute() calls CLIPipeline::run(), which creates
// a new QApplication and calls _exit(), terminating the test process.
// Integration tests with real files should be done separately.
154 changes: 154 additions & 0 deletions src/MaterialPresetLibrary_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#include <gtest/gtest.h>
#include "MaterialPresetLibrary.h"
#include "Manager.h"
#include "SelectionSet.h"
#include "TestHelpers.h"
#include <QApplication>
#include <QCoreApplication>
#include <QSignalSpy>
#include <QThread>

class MaterialPresetLibraryTests : public ::testing::Test {
protected:
QApplication* app = nullptr;

void SetUp() override {
app = qobject_cast<QApplication*>(QCoreApplication::instance());
ASSERT_NE(app, nullptr);
}

void TearDown() override {
if (app) app->processEvents();
}
};

TEST_F(MaterialPresetLibraryTests, SingletonInstance) {
auto* inst = MaterialPresetLibrary::instance();
ASSERT_NE(inst, nullptr);
EXPECT_EQ(inst, MaterialPresetLibrary::instance());
}

TEST_F(MaterialPresetLibraryTests, KillAndRecreate) {
auto* inst1 = MaterialPresetLibrary::instance();
ASSERT_NE(inst1, nullptr);

MaterialPresetLibrary::kill();

auto* inst2 = MaterialPresetLibrary::instance();
ASSERT_NE(inst2, nullptr);
// After kill+recreate, it should be a new instance
// (pointer may or may not differ due to memory reuse, but it should work)
EXPECT_NE(inst2, nullptr);
}

TEST_F(MaterialPresetLibraryTests, PresetNamesNotEmpty) {
auto* inst = MaterialPresetLibrary::instance();
QStringList names = inst->presetNames();
EXPECT_FALSE(names.isEmpty());
EXPECT_GE(names.size(), 10); // We know there are 12 presets
}

TEST_F(MaterialPresetLibraryTests, PresetNamesContainsExpected) {
auto* inst = MaterialPresetLibrary::instance();
QStringList names = inst->presetNames();

EXPECT_TRUE(names.contains("Plastic (Red)"));
EXPECT_TRUE(names.contains("Plastic (Blue)"));
EXPECT_TRUE(names.contains("Plastic (White)"));
EXPECT_TRUE(names.contains("Metal (Silver)"));
EXPECT_TRUE(names.contains("Metal (Gold)"));
EXPECT_TRUE(names.contains("Metal (Copper)"));
EXPECT_TRUE(names.contains("Wood (Oak)"));
EXPECT_TRUE(names.contains("Wood (Birch)"));
EXPECT_TRUE(names.contains("Glass (Clear)"));
EXPECT_TRUE(names.contains("Glass (Tinted)"));
EXPECT_TRUE(names.contains("Unlit (White)"));
EXPECT_TRUE(names.contains("Wireframe"));
}

TEST_F(MaterialPresetLibraryTests, ApplyPresetWithoutSelection) {
Manager::kill();
QThread::msleep(50);

if (!tryInitOgre()) {
GTEST_SKIP() << "Skipping: Ogre initialization failed";
}
createStandardOgreMaterials();

auto* inst = MaterialPresetLibrary::instance();

// With no selection, applyPreset should return early without crashing
SelectionSet::getSingleton()->clear();
EXPECT_NO_THROW(inst->applyPreset("Plastic (Red)"));
}

TEST_F(MaterialPresetLibraryTests, ApplyPresetEmitsSignalWithEntity) {
Manager::kill();
QThread::msleep(50);

if (!tryInitOgre()) {
GTEST_SKIP() << "Skipping: Ogre initialization failed";
}
if (!canLoadMeshFiles()) {
GTEST_SKIP() << "Skipping: entity creation not supported without render window";
}
createStandardOgreMaterials();

auto mesh = createInMemoryTriangleMesh("PresetTestMesh");
auto* sceneMgr = Manager::getSingleton()->getSceneMgr();
auto* node = Manager::getSingleton()->addSceneNode("PresetTestNode");
auto* entity = sceneMgr->createEntity("PresetTestEnt", mesh);
node->attachObject(entity);

SelectionSet::getSingleton()->selectOne(entity);

auto* inst = MaterialPresetLibrary::instance();
QSignalSpy spy(inst, &MaterialPresetLibrary::presetApplied);

inst->applyPreset("Plastic (Blue)");

EXPECT_EQ(spy.count(), 1);
EXPECT_EQ(spy.at(0).at(0).toString(), "Plastic (Blue)");

// Verify the material was applied
EXPECT_EQ(std::string(entity->getSubEntity(0)->getMaterialName()), "Preset/Plastic (Blue)");

SelectionSet::getSingleton()->clear();
}

TEST_F(MaterialPresetLibraryTests, ApplyAllPresets) {
Manager::kill();
QThread::msleep(50);

if (!tryInitOgre()) {
GTEST_SKIP() << "Skipping: Ogre initialization failed";
}
if (!canLoadMeshFiles()) {
GTEST_SKIP() << "Skipping: entity creation not supported without render window";
}
createStandardOgreMaterials();

auto mesh = createInMemoryTriangleMesh("PresetAllMesh");
auto* sceneMgr = Manager::getSingleton()->getSceneMgr();
auto* node = Manager::getSingleton()->addSceneNode("PresetAllNode");
auto* entity = sceneMgr->createEntity("PresetAllEnt", mesh);
node->attachObject(entity);

SelectionSet::getSingleton()->selectOne(entity);

auto* inst = MaterialPresetLibrary::instance();
QStringList names = inst->presetNames();

// Apply every preset to ensure none crash
for (const QString& name : names) {
EXPECT_NO_THROW(inst->applyPreset(name));
}

SelectionSet::getSingleton()->clear();
}

TEST_F(MaterialPresetLibraryTests, QmlInstanceReturnsSameAsInstance) {
auto* inst1 = MaterialPresetLibrary::instance();
auto* inst2 = MaterialPresetLibrary::qmlInstance(nullptr, nullptr);
EXPECT_EQ(inst1, inst2);
}
101 changes: 101 additions & 0 deletions src/ScaleGizmo_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,104 @@ TEST_F(ScaleGizmoTests, CreateAxis) {
ASSERT_NE(&mScaleGizmo->getYAxis(), nullptr);
ASSERT_NE(&mScaleGizmo->getZAxis(), nullptr);
}

TEST_F(ScaleGizmoTests, SetFading) {
mScaleGizmo->setFading(0.8f);
EXPECT_FLOAT_EQ(mScaleGizmo->getFading(), 0.8f);

mScaleGizmo->setFading(0.0f);
EXPECT_FLOAT_EQ(mScaleGizmo->getFading(), 0.0f);

mScaleGizmo->setFading(1.0f);
EXPECT_FLOAT_EQ(mScaleGizmo->getFading(), 1.0f);
}

TEST_F(ScaleGizmoTests, DefaultColorsAreRGB) {
// X axis should default to red
EXPECT_FLOAT_EQ(mScaleGizmo->getXaxisColour().r, 1.0f);
EXPECT_FLOAT_EQ(mScaleGizmo->getXaxisColour().g, 0.0f);
EXPECT_FLOAT_EQ(mScaleGizmo->getXaxisColour().b, 0.0f);

// Y axis should default to green
EXPECT_FLOAT_EQ(mScaleGizmo->getYaxisColour().r, 0.0f);
EXPECT_FLOAT_EQ(mScaleGizmo->getYaxisColour().g, 1.0f);
EXPECT_FLOAT_EQ(mScaleGizmo->getYaxisColour().b, 0.0f);

// Z axis should default to blue
EXPECT_FLOAT_EQ(mScaleGizmo->getZaxisColour().r, 0.0f);
EXPECT_FLOAT_EQ(mScaleGizmo->getZaxisColour().g, 0.0f);
EXPECT_FLOAT_EQ(mScaleGizmo->getZaxisColour().b, 1.0f);
}

TEST_F(ScaleGizmoTests, UnnamedGizmoCreation) {
// Create a gizmo with empty name
auto emptyNameGizmo = std::make_unique<ScaleGizmo>(mLinkNode, "");
EXPECT_FALSE(emptyNameGizmo->isHighlighted());
EXPECT_FLOAT_EQ(emptyNameGizmo->getScale(), 1.0f);
}

TEST_F(ScaleGizmoTests, HighlightNullReturnsZero) {
auto result = mScaleGizmo->highlightAxis(nullptr);
EXPECT_EQ(result, Ogre::Vector3::ZERO);
EXPECT_FALSE(mScaleGizmo->isHighlighted());
}

TEST_F(ScaleGizmoTests, SetScaleMultipleTimes) {
mScaleGizmo->setScale(0.5f);
EXPECT_FLOAT_EQ(mScaleGizmo->getScale(), 0.5f);

mScaleGizmo->setScale(3.0f);
EXPECT_FLOAT_EQ(mScaleGizmo->getScale(), 3.0f);

mScaleGizmo->setScale(0.1f);
EXPECT_FLOAT_EQ(mScaleGizmo->getScale(), 0.1f);
}

TEST_F(ScaleGizmoTests, SetCustomColours) {
Ogre::ColourValue cyan(0.0f, 1.0f, 1.0f);
Ogre::ColourValue magenta(1.0f, 0.0f, 1.0f);
Ogre::ColourValue yellow(1.0f, 1.0f, 0.0f);

mScaleGizmo->setXaxisColour(cyan);
mScaleGizmo->setYaxisColour(magenta);
mScaleGizmo->setZaxisColour(yellow);

EXPECT_EQ(mScaleGizmo->getXaxisColour(), cyan);
EXPECT_EQ(mScaleGizmo->getYaxisColour(), magenta);
EXPECT_EQ(mScaleGizmo->getZaxisColour(), yellow);
}

TEST_F(ScaleGizmoTests, VisibilityToggle) {
mScaleGizmo->setVisible(true);
EXPECT_TRUE(mScaleGizmo->getXAxis().isVisible());

mScaleGizmo->setVisible(false);
EXPECT_FALSE(mScaleGizmo->getXAxis().isVisible());

mScaleGizmo->setVisible(true);
EXPECT_TRUE(mScaleGizmo->getXAxis().isVisible());
}

TEST_F(ScaleGizmoTests, HighlightEachAxisSequentially) {
// Highlight X then Y then Z then clear
auto r1 = mScaleGizmo->highlightAxis(&mScaleGizmo->getXAxis());
EXPECT_EQ(r1, Ogre::Vector3::UNIT_X);
EXPECT_TRUE(mScaleGizmo->isHighlighted());

auto r2 = mScaleGizmo->highlightAxis(&mScaleGizmo->getYAxis());
EXPECT_EQ(r2, Ogre::Vector3::UNIT_Y);
EXPECT_TRUE(mScaleGizmo->isHighlighted());

auto r3 = mScaleGizmo->highlightAxis(&mScaleGizmo->getZAxis());
EXPECT_EQ(r3, Ogre::Vector3::UNIT_Z);
EXPECT_TRUE(mScaleGizmo->isHighlighted());

auto r4 = mScaleGizmo->highlightAxis(nullptr);
EXPECT_EQ(r4, Ogre::Vector3::ZERO);
EXPECT_FALSE(mScaleGizmo->isHighlighted());
}

TEST_F(ScaleGizmoTests, ConstructWithCustomScale) {
auto customGizmo = std::make_unique<ScaleGizmo>(mLinkNode, "CustomScaleGizmo", 2.5f);
EXPECT_FLOAT_EQ(customGizmo->getScale(), 2.5f);
}
Loading
Loading