From a5b725086211f3518386a772c78159f831d43b50 Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Sat, 23 May 2026 04:23:23 +0000 Subject: [PATCH 1/2] Add real unit tests for core math, util, UID, and shape logic Replace the placeholder TestMaths (which only exercised Qt's QString::toUpper) with unit tests that cover production code: - TestMaths: degree/radian conversion, wrapAround, distance helpers, xOr - TestUtil: map_float/map_int range mapping and clipping, isNumeric, fileExists/eraseFile - TestUidAllocator: sequential allocation, lowest-id reuse, reserve, free - TestShape: vertex accessors, getCenter, includesPoint, translate, applyTransform, polygon round-trip The test project compiles the real MapMap sources under test and runs all suites from a single binary via a custom QtTest runner. Util.cpp uses glTexCoord2f and glVertex2f, which require -lopengl32 on Windows. The main project already links this via src.pri, but the test project manages its own linkage independently. --- .gitignore | 2 + tests/TestMaths.cpp | 63 +++++++++++++++++++++++-- tests/TestMaths.h | 26 ++++++++-- tests/TestShape.cpp | 97 ++++++++++++++++++++++++++++++++++++++ tests/TestShape.h | 30 ++++++++++++ tests/TestUidAllocator.cpp | 69 +++++++++++++++++++++++++++ tests/TestUidAllocator.h | 28 +++++++++++ tests/TestUtil.cpp | 78 ++++++++++++++++++++++++++++++ tests/TestUtil.h | 29 ++++++++++++ tests/main.cpp | 44 +++++++++++++++++ tests/tests.pro | 56 ++++++++++++++++++---- 11 files changed, 507 insertions(+), 15 deletions(-) create mode 100644 tests/TestShape.cpp create mode 100644 tests/TestShape.h create mode 100644 tests/TestUidAllocator.cpp create mode 100644 tests/TestUidAllocator.h create mode 100644 tests/TestUtil.cpp create mode 100644 tests/TestUtil.h create mode 100644 tests/main.cpp diff --git a/.gitignore b/.gitignore index 7df723e3..6b297958 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,8 @@ Makefile* # Generated by Qt mapmap +tests/mapmap_tests +moc_predefs.h moc_*.cpp qrc_*.cpp *.moc diff --git a/tests/TestMaths.cpp b/tests/TestMaths.cpp index a214eb09..862179bd 100644 --- a/tests/TestMaths.cpp +++ b/tests/TestMaths.cpp @@ -1,10 +1,65 @@ +/* + * TestMaths.cpp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + #include "TestMaths.h" -void TestMaths::toUpper() +#include "Maths.h" + +using namespace mmp; + +void TestMaths::degreesRadiansRoundTrip() { - QString str = "Hello"; - QCOMPARE(str.toUpper(), QString("HELLO")); + QVERIFY(qFuzzyCompare(degreesToRadians(180.0), M_PI)); + QVERIFY(qFuzzyCompare(radiansToDegrees(M_PI), 180.0)); + QVERIFY(qFuzzyCompare(radiansToDegrees(degreesToRadians(57.3)), 57.3)); } -QTEST_MAIN(TestMaths) +void TestMaths::wrapAroundInt() +{ + // Example taken from the documentation in Maths.h. + QCOMPARE(wrapAround(-1, 3), 2); + QCOMPARE(wrapAround(0, 3), 0); + QCOMPARE(wrapAround(3, 3), 0); + QCOMPARE(wrapAround(5, 3), 2); + QCOMPARE(wrapAround(-4, 3), 2); +} +void TestMaths::wrapAroundReal() +{ + QVERIFY(qFuzzyCompare(wrapAround(qreal(-0.5), qreal(1.0)), qreal(0.5))); + QVERIFY(qFuzzyCompare(wrapAround(qreal(1.5), qreal(1.0)), qreal(0.5))); + QVERIFY(qFuzzyCompare(wrapAround(qreal(0.25), qreal(1.0)), qreal(0.25))); +} + +void TestMaths::squareAndDistance() +{ + QVERIFY(qFuzzyCompare(sq(3.0), 9.0)); + + QPointF a(0, 0); + QPointF b(3, 4); + QVERIFY(qFuzzyCompare(distSq(a, b), 25.0)); + QVERIFY(qFuzzyCompare(dist(a, b), 5.0)); +} + +void TestMaths::distanceInside() +{ + QPointF a(0, 0); + QPointF b(3, 4); // distance 5 + QVERIFY(distIsInside(a, b, 6.0)); + QVERIFY(!distIsInside(a, b, 5.0)); // strictly inside: equal is outside + QVERIFY(!distIsInside(a, b, 4.0)); +} + +void TestMaths::booleanXor() +{ + QCOMPARE(xOr(true, false), true); + QCOMPARE(xOr(false, true), true); + QCOMPARE(xOr(true, true), false); + QCOMPARE(xOr(false, false), false); +} diff --git a/tests/TestMaths.h b/tests/TestMaths.h index a04d5972..70440553 100644 --- a/tests/TestMaths.h +++ b/tests/TestMaths.h @@ -1,10 +1,30 @@ +/* + * TestMaths.h + * + * Unit tests for the math helpers in src/core/Maths.h. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#ifndef TEST_MATHS_H_ +#define TEST_MATHS_H_ + #include class TestMaths: public QObject { - Q_OBJECT + Q_OBJECT - private slots: - void toUpper(); +private slots: + void degreesRadiansRoundTrip(); + void wrapAroundInt(); + void wrapAroundReal(); + void squareAndDistance(); + void distanceInside(); + void booleanXor(); }; +#endif /* TEST_MATHS_H_ */ diff --git a/tests/TestShape.cpp b/tests/TestShape.cpp new file mode 100644 index 00000000..76a07830 --- /dev/null +++ b/tests/TestShape.cpp @@ -0,0 +1,97 @@ +/* + * TestShape.cpp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#include "TestShape.h" + +#include "Quad.h" +#include "Triangle.h" + +using namespace mmp; + +namespace { +// A unit-ish axis-aligned square from (0,0) to (2,2). +Quad makeSquare() +{ + return Quad(QPointF(0, 0), QPointF(2, 0), QPointF(2, 2), QPointF(0, 2)); +} + +bool fuzzyPoint(const QPointF& a, const QPointF& b) +{ + return qFuzzyCompare(a.x(), b.x()) && qFuzzyCompare(a.y(), b.y()); +} +} + +void TestShape::verticesAccessors() +{ + // Triangle has 3 vertices and (size <= 3) so setVertex does not constrain. + Triangle tri(QPointF(0, 0), QPointF(4, 0), QPointF(0, 3)); + QCOMPARE(tri.nVertices(), 3); + QVERIFY(fuzzyPoint(tri.getVertex(1), QPointF(4, 0))); + + tri.setVertex(0, QPointF(1, 1)); + QVERIFY(fuzzyPoint(tri.getVertex(0), QPointF(1, 1))); + + // setVertices replaces the whole set (deep copy). + QVector newVerts{ QPointF(5, 5), QPointF(6, 6), QPointF(7, 7) }; + tri.setVertices(newVerts); + QCOMPARE(tri.nVertices(), 3); + QVERIFY(fuzzyPoint(tri.getVertex(2), QPointF(7, 7))); +} + +void TestShape::getCenter() +{ + Quad square = makeSquare(); + QVERIFY(fuzzyPoint(square.getCenter(), QPointF(1, 1))); +} + +void TestShape::includesPoint() +{ + Quad square = makeSquare(); + QVERIFY(square.includesPoint(QPointF(1, 1))); + QVERIFY(square.includesPoint(QPointF(0.5, 1.5))); + QVERIFY(!square.includesPoint(QPointF(3, 3))); + QVERIFY(!square.includesPoint(QPointF(-1, -1))); +} + +void TestShape::translate() +{ + Quad square = makeSquare(); + square.translate(QPointF(1, 1)); + + QVERIFY(fuzzyPoint(square.getVertex(0), QPointF(1, 1))); + QVERIFY(fuzzyPoint(square.getVertex(2), QPointF(3, 3))); + QVERIFY(fuzzyPoint(square.getCenter(), QPointF(2, 2))); +} + +void TestShape::applyTransform() +{ + Quad square = makeSquare(); + + QTransform t; + t.translate(5, 0); + square.applyTransform(t); + + QVERIFY(fuzzyPoint(square.getVertex(0), QPointF(5, 0))); + QVERIFY(fuzzyPoint(square.getVertex(1), QPointF(7, 0))); +} + +void TestShape::polygonRoundTrip() +{ + Quad square = makeSquare(); + + QPolygonF poly = square.toPolygon(); + QCOMPARE(poly.size(), 4); + QVERIFY(fuzzyPoint(poly.at(2), QPointF(2, 2))); + + // Shift every point and feed it back; the shape must reflect the change. + QPolygonF shifted = poly.translated(10, 10); + square.fromPolygon(shifted); + QVERIFY(fuzzyPoint(square.getVertex(0), QPointF(10, 10))); + QVERIFY(fuzzyPoint(square.getCenter(), QPointF(11, 11))); +} diff --git a/tests/TestShape.h b/tests/TestShape.h new file mode 100644 index 00000000..c26a8b0b --- /dev/null +++ b/tests/TestShape.h @@ -0,0 +1,30 @@ +/* + * TestShape.h + * + * Unit tests for the shape geometry in src/shape (MShape / Polygon). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#ifndef TEST_SHAPE_H_ +#define TEST_SHAPE_H_ + +#include + +class TestShape: public QObject +{ + Q_OBJECT + +private slots: + void verticesAccessors(); + void getCenter(); + void includesPoint(); + void translate(); + void applyTransform(); + void polygonRoundTrip(); +}; + +#endif /* TEST_SHAPE_H_ */ diff --git a/tests/TestUidAllocator.cpp b/tests/TestUidAllocator.cpp new file mode 100644 index 00000000..d19b7b60 --- /dev/null +++ b/tests/TestUidAllocator.cpp @@ -0,0 +1,69 @@ +/* + * TestUidAllocator.cpp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#include "TestUidAllocator.h" + +#include "UidAllocator.h" + +using namespace mmp; + +void TestUidAllocator::allocateIsSequential() +{ + UidAllocator allocator; + QCOMPARE(allocator.allocate(), uid(1)); + QCOMPARE(allocator.allocate(), uid(2)); + QCOMPARE(allocator.allocate(), uid(3)); + + QVERIFY(allocator.exists(1)); + QVERIFY(allocator.exists(2)); + QVERIFY(allocator.exists(3)); + QVERIFY(!allocator.exists(4)); + QCOMPARE(int(allocator.list().size()), 3); +} + +void TestUidAllocator::freeReusesLowestId() +{ + UidAllocator allocator; + allocator.allocate(); // 1 + allocator.allocate(); // 2 + allocator.allocate(); // 3 + + QVERIFY(allocator.free(2)); + QVERIFY(!allocator.exists(2)); + + // The next allocation should reuse the lowest free id (2). + QCOMPARE(allocator.allocate(), uid(2)); + QVERIFY(allocator.exists(2)); +} + +void TestUidAllocator::reserve() +{ + UidAllocator allocator; + + // Reserving an unused id succeeds and marks it as existing. + QVERIFY(allocator.reserve(42)); + QVERIFY(allocator.exists(42)); + + // Reserving an already-reserved id fails. + QVERIFY(!allocator.reserve(42)); + + // A subsequent allocate must skip the reserved id. + QCOMPARE(allocator.allocate(), uid(1)); +} + +void TestUidAllocator::freeUnknownReturnsFalse() +{ + UidAllocator allocator; + QVERIFY(!allocator.free(99)); + + allocator.allocate(); // 1 + QVERIFY(!allocator.free(99)); + QVERIFY(allocator.free(1)); + QVERIFY(!allocator.free(1)); // already freed +} diff --git a/tests/TestUidAllocator.h b/tests/TestUidAllocator.h new file mode 100644 index 00000000..f8c695f8 --- /dev/null +++ b/tests/TestUidAllocator.h @@ -0,0 +1,28 @@ +/* + * TestUidAllocator.h + * + * Unit tests for src/core/UidAllocator. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#ifndef TEST_UID_ALLOCATOR_H_ +#define TEST_UID_ALLOCATOR_H_ + +#include + +class TestUidAllocator: public QObject +{ + Q_OBJECT + +private slots: + void allocateIsSequential(); + void freeReusesLowestId(); + void reserve(); + void freeUnknownReturnsFalse(); +}; + +#endif /* TEST_UID_ALLOCATOR_H_ */ diff --git a/tests/TestUtil.cpp b/tests/TestUtil.cpp new file mode 100644 index 00000000..13214e4e --- /dev/null +++ b/tests/TestUtil.cpp @@ -0,0 +1,78 @@ +/* + * TestUtil.cpp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#include "TestUtil.h" + +#include "Util.h" + +#include +#include + +using namespace mmp; + +void TestUtil::mapFloat() +{ + QVERIFY(qFuzzyCompare(Util::map_float(0.0f, 0.0f, 127.0f, 0.0f, 1.0f), 0.0f)); + QVERIFY(qFuzzyCompare(Util::map_float(127.0f, 0.0f, 127.0f, 0.0f, 1.0f), 1.0f)); + QVERIFY(qFuzzyCompare(Util::map_float(64.0f, 0.0f, 127.0f, 0.0f, 1.0f), 64.0f / 127.0f)); +} + +void TestUtil::mapFloatClips() +{ + // Values outside the input range are clipped to [ostart, ostop]. + QVERIFY(qFuzzyCompare(Util::map_float(200.0f, 0.0f, 127.0f, 0.0f, 1.0f), 1.0f)); + QVERIFY(qFuzzyCompare(Util::map_float(-10.0f, 0.0f, 127.0f, 0.0f, 1.0f) + 1.0f, 1.0f)); // == 0 +} + +void TestUtil::mapInt() +{ + QCOMPARE(Util::map_int(0, 0, 127, 0, 127), 0); + QCOMPARE(Util::map_int(127, 0, 127, 0, 127), 127); + QCOMPARE(Util::map_int(64, 0, 127, 0, 127), 64); + // Clipping. + QCOMPARE(Util::map_int(500, 0, 127, 0, 127), 127); + QCOMPARE(Util::map_int(-20, 0, 127, 0, 127), 0); +} + +void TestUtil::isNumeric() +{ + QVERIFY(Util::isNumeric("123")); + QVERIFY(Util::isNumeric("007")); + QVERIFY(!Util::isNumeric("12a")); + QVERIFY(!Util::isNumeric("1.5")); + QVERIFY(!Util::isNumeric("-5")); + + // Documents current behaviour: the regex "^\\d*$" also matches the empty + // string. If this is ever tightened, this expectation should change. + QVERIFY(Util::isNumeric("")); +} + +void TestUtil::fileExistsAndErase() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + + const QString path = dir.filePath("sample.txt"); + + // Erasing / probing a non-existent file. + QVERIFY(!Util::fileExists(path)); + QVERIFY(!Util::eraseFile(path)); + + // Create the file then verify detection and removal. + { + QFile file(path); + QVERIFY(file.open(QIODevice::WriteOnly)); + file.write("hello"); + file.close(); + } + + QVERIFY(Util::fileExists(path)); + QVERIFY(Util::eraseFile(path)); + QVERIFY(!Util::fileExists(path)); +} diff --git a/tests/TestUtil.h b/tests/TestUtil.h new file mode 100644 index 00000000..708d3dd6 --- /dev/null +++ b/tests/TestUtil.h @@ -0,0 +1,29 @@ +/* + * TestUtil.h + * + * Unit tests for the helpers in src/core/Util. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#ifndef TEST_UTIL_H_ +#define TEST_UTIL_H_ + +#include + +class TestUtil: public QObject +{ + Q_OBJECT + +private slots: + void mapFloat(); + void mapFloatClips(); + void mapInt(); + void isNumeric(); + void fileExistsAndErase(); +}; + +#endif /* TEST_UTIL_H_ */ diff --git a/tests/main.cpp b/tests/main.cpp new file mode 100644 index 00000000..d3bdaeb1 --- /dev/null +++ b/tests/main.cpp @@ -0,0 +1,44 @@ +/* + * main.cpp + * + * Test runner for the MapMap unit tests. Runs every test suite in a single + * executable and returns a non-zero status if any suite fails. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#include +#include + +#include "TestMaths.h" +#include "TestUtil.h" +#include "TestUidAllocator.h" +#include "TestShape.h" + +int main(int argc, char** argv) +{ + QCoreApplication app(argc, argv); + + int status = 0; + { + TestMaths test; + status |= QTest::qExec(&test, argc, argv); + } + { + TestUtil test; + status |= QTest::qExec(&test, argc, argv); + } + { + TestUidAllocator test; + status |= QTest::qExec(&test, argc, argv); + } + { + TestShape test; + status |= QTest::qExec(&test, argc, argv); + } + + return status; +} diff --git a/tests/tests.pro b/tests/tests.pro index 6919f104..1716b722 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -1,16 +1,56 @@ QT += testlib -QT += core +QT += core gui widgets opengl openglwidgets multimedia multimediawidgets network -CONFIG += c++11 +CONFIG += c++11 console testcase +CONFIG -= app_bundle -TARGET = TestMaths +TEMPLATE = app +TARGET = mapmap_tests -CONFIG += console -CONFIG += app_bundle +DEFINES += UNICODE QT_THREAD_SUPPORT QT_CORE_LIB QT_GUI_LIB QT_MESSAGELOGCONTEXT +unix:!macx: DEFINES += UNIX +win32: LIBS += -lopengl32 -SOURCES = TestMaths.cpp +CORE = $$PWD/../src/core +SHAPE = $$PWD/../src/shape -HEADERS = TestMaths.h +INCLUDEPATH += $$CORE $$SHAPE -INCLUDEPATH += $$PWD/../src/ +# Production sources under test plus the minimal set of dependencies they +# need to link. We deliberately avoid pulling in the whole core/shape .pri +# files because some core sources (Commands, Paint, ...) depend on the GUI +# and multimedia back-ends, which are out of scope for these unit tests. +HEADERS += \ + $$CORE/MM.h \ + $$CORE/Maths.h \ + $$CORE/Util.h \ + $$CORE/Serializable.h \ + $$CORE/ProjectLabels.h \ + $$CORE/UidAllocator.h \ + $$SHAPE/Shape.h \ + $$SHAPE/Polygon.h \ + $$SHAPE/Triangle.h \ + $$SHAPE/Quad.h \ + $$SHAPE/Mesh.h \ + $$SHAPE/Ellipse.h \ + $$SHAPE/Shapes.h \ + TestMaths.h \ + TestUtil.h \ + TestUidAllocator.h \ + TestShape.h +SOURCES += \ + $$CORE/MM.cpp \ + $$CORE/Util.cpp \ + $$CORE/Serializable.cpp \ + $$CORE/ProjectLabels.cpp \ + $$CORE/UidAllocator.cpp \ + $$SHAPE/Shape.cpp \ + $$SHAPE/Polygon.cpp \ + $$SHAPE/Mesh.cpp \ + $$SHAPE/Ellipse.cpp \ + main.cpp \ + TestMaths.cpp \ + TestUtil.cpp \ + TestUidAllocator.cpp \ + TestShape.cpp From 46d4d73a64b13ea0cecc4c8c589e959fb4a049aa Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Sat, 23 May 2026 15:53:52 -0400 Subject: [PATCH 2/2] Add a check step to the GitHub actions --- .github/workflows/macos-build.yml | 8 ++++++++ .github/workflows/ubuntu-build.yml | 8 ++++++-- .github/workflows/windows-build.yml | 10 ++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml index 006c33a6..a15c74ad 100644 --- a/.github/workflows/macos-build.yml +++ b/.github/workflows/macos-build.yml @@ -28,3 +28,11 @@ jobs: run: | export PATH="$(brew --prefix qt)/bin:$PATH" make -j$(sysctl -n hw.logicalcpu) + + - name: Build and run tests + run: | + export PATH="($brew --prefix=qt)/bin:$PATH" + cd tests/ + qmake6 tests.pro + make -j$(sysctl -n hw.logicalcpu) + QT_QPA_PLATFORM=offscreen make check diff --git a/.github/workflows/ubuntu-build.yml b/.github/workflows/ubuntu-build.yml index 1200f50a..676602c3 100644 --- a/.github/workflows/ubuntu-build.yml +++ b/.github/workflows/ubuntu-build.yml @@ -34,5 +34,9 @@ jobs: - name: Build run: make -j$(nproc) - - name: Run tests - run: make check || true + - name: Build and run tests + run: | + cd tests/ + qmake6 tests.pro + make -j$(nproc) + QT_QPA_PLATFORM=offscreen make check diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 492d7708..50e82586 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -30,3 +30,13 @@ jobs: - name: Build run: mingw32-make -j4 + + - name: Build and run tests + run: | + cd tests/ + qmake6 tests.pro + mingw32-make -j4 + mingw32-make check + env: + QT_QPA_PLATFORM: offscreen +