Skip to content
Open
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
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
/FMDC_V03/
/Duet3_MB6HC_no_SD/
/Duet3_MB6HC_no_S_curve/
/INDX/
/tests/build/
/.clangd
/.clang-format
/src/Temp/
/.settings/
102 changes: 102 additions & 0 deletions tests/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# RepRapFirmware host-native test build

CXX ?= g++-15

ifeq ($(V),1)
Q :=
else
Q := @
endif

ROOT := ..
WORKSPACE := ../..
BUILD_DIR := build
TARGET := $(BUILD_DIR)/rrf_host_tests

CPPFLAGS := \
-I$(CURDIR)/support/shims \
-I$(ROOT) \
-I$(ROOT)/src \
-I$(WORKSPACE)/CANlib/src \
-I$(WORKSPACE)/RRFLibraries/src \
-I$(CURDIR)/support

# RRFLibraries/src/General must NOT be on the include path: it holds String.h, which on a
# case-insensitive filesystem (macOS) libc++'s <cstring> finds in place of the real <string.h>.
# Sources reach those headers through the General/ and Math/ prefixes instead.
CXXFLAGS ?= -std=gnu++20 -O2 -Wall -Wextra -Wpedantic

# unit/test_fopdt.cpp is excluded: it targets the pre-3.7.0-beta.3 FopDt API (CorrectPwmForVoltage,
# SetM301PidParameters, ArePidParametersOverridden, AppendM301Command, 3-argument EstimateRequiredPwm),
# all of which have since changed. It needs updating by whoever owns the heater model.
TEST_SOURCES := \
support/TestMain.cpp \
unit/test_bitmap.cpp \
unit/test_deviation.cpp \
unit/test_isqrt.cpp

LIB_SOURCES := \
$(WORKSPACE)/RRFLibraries/src/General/SafeVsnprintf.cpp \
$(WORKSPACE)/RRFLibraries/src/General/StringFunctions.cpp \
$(WORKSPACE)/RRFLibraries/src/General/StringRef.cpp \
$(WORKSPACE)/RRFLibraries/src/General/Strnlen.cpp \
$(WORKSPACE)/RRFLibraries/src/Math/Deviation.cpp \
$(WORKSPACE)/RRFLibraries/src/Math/Isqrt.cpp

OBJECTS := $(patsubst %.cpp,$(BUILD_DIR)/%.o,$(TEST_SOURCES)) \
$(BUILD_DIR)/rrflibraries/SafeVsnprintf.o \
$(BUILD_DIR)/rrflibraries/StringFunctions.o \
$(BUILD_DIR)/rrflibraries/StringRef.o \
$(BUILD_DIR)/rrflibraries/Strnlen.o \
$(BUILD_DIR)/rrflibraries/Deviation.o \
$(BUILD_DIR)/rrflibraries/Isqrt.o

.PHONY: all clean run

all: run

run: $(TARGET)
$(Q)./$(TARGET)

$(TARGET): $(OBJECTS)
$(Q)mkdir -p $(dir $@)
$(Q)$(CXX) $(CXXFLAGS) $^ -o $@

$(BUILD_DIR)/support/%.o: support/%.cpp support/TestRunner.h
$(Q)mkdir -p $(dir $@)
$(Q)$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

$(BUILD_DIR)/unit/%.o: unit/%.cpp support/TestRunner.h
$(Q)mkdir -p $(dir $@)
$(Q)$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

$(BUILD_DIR)/rrflibraries/Isqrt.o: $(WORKSPACE)/RRFLibraries/src/Math/Isqrt.cpp
$(Q)mkdir -p $(dir $@)
$(Q)$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

$(BUILD_DIR)/rrflibraries/SafeVsnprintf.o: $(WORKSPACE)/RRFLibraries/src/General/SafeVsnprintf.cpp
$(Q)mkdir -p $(dir $@)
$(Q)$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

$(BUILD_DIR)/rrflibraries/StringFunctions.o: $(WORKSPACE)/RRFLibraries/src/General/StringFunctions.cpp
$(Q)mkdir -p $(dir $@)
$(Q)$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

$(BUILD_DIR)/rrflibraries/StringRef.o: $(WORKSPACE)/RRFLibraries/src/General/StringRef.cpp
$(Q)mkdir -p $(dir $@)
$(Q)$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

$(BUILD_DIR)/rrflibraries/Strnlen.o: $(WORKSPACE)/RRFLibraries/src/General/Strnlen.cpp
$(Q)mkdir -p $(dir $@)
$(Q)$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

$(BUILD_DIR)/rrflibraries/Deviation.o: $(WORKSPACE)/RRFLibraries/src/Math/Deviation.cpp
$(Q)mkdir -p $(dir $@)
$(Q)$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

$(BUILD_DIR)/reprapfirmware/FOPDT.o: $(ROOT)/src/Heating/FOPDT.cpp
$(Q)mkdir -p $(dir $@)
$(Q)$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

clean:
$(Q)rm -rf $(BUILD_DIR)
51 changes: 51 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Host-Native Tests

This directory contains Linux-hosted tests for code that can be validated without running on a Duet board.

The initial scope is intentionally narrow:

- pure math and algorithm code that does not require MCU peripherals
- selected firmware-side model code compiled against narrow test-only shims
- deterministic tests that run with the system C++ compiler
- no dependency on the ARM cross-toolchain

Run the tests from the RepRapFirmware root:

```sh
make test-host
```

Current contents:

- `support/` - minimal in-repo test runner
- `support/shims/` - narrow host-only shims for firmware types needed by selected model code
- `unit/` - host-native unit tests
- `make test-host` from the repository root builds and runs the suite with `g++`

Current coverage:

- RRFLibraries helpers: bitmap, deviation, integer square root, fast square root
- RepRapFirmware heater model: `FOPDT`

Planned next steps:

- add deterministic time and captured-output test support
- add more RepRapFirmware-side tests for kinematics and control logic
## Why the motion system is not testable here

`Move.cpp` needs 98 RepRapFirmware headers across 18 subsystems just to parse,
and its body calls `reprap.GetGCodes()` 91 times, plus `GetPlatform()`,
`GetExpansion()` and `GetPortControl()`. `Move` and `DDA` are not separable from
the `reprap` singleton, so reaching the planner from here would mean either a
large fake Platform/GCodes layer or refactoring `Move` onto interfaces.

Until that changes, host tests are limited to leaf code. Validating motion
behaviour - junction blending, speed ceilings, deceleration on loss of input -
needs a peripheral-level emulator such as Renode.

Measure it again before believing it:

```sh
make Duet3_MB6HC ... # produces the .d files
tr ' ' '\n' < Duet3_MB6HC/src/Movement/Move.d | grep -E '^src/.*\.h$' | sort -u | wc -l
```
8 changes: 8 additions & 0 deletions tests/support/TestMain.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#include <cstdio>

#include "TestRunner.h"

int main()
{
return test::RunAllTests();
}
129 changes: 129 additions & 0 deletions tests/support/TestRunner.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#ifndef RRF_TEST_RUNNER_H
#define RRF_TEST_RUNNER_H

#include <cmath>
#include <cstdio>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

namespace test
{
using TestFunction = void (*)();

struct TestCase
{
const char* name;
TestFunction function;
};

inline std::vector<TestCase>& Registry()
{
static std::vector<TestCase> registry;
return registry;
}

class Registrar
{
public:
Registrar(const char* name, TestFunction function)
{
Registry().push_back({name, function});
}
};

class Failure : public std::runtime_error
{
public:
explicit Failure(const std::string& message)
: std::runtime_error(message)
{
}
};

[[noreturn]] inline void Fail(const char* file, int line, const std::string& message)
{
std::ostringstream stream;
stream << file << ':' << line << ": " << message;
throw Failure(stream.str());
}

template <typename T, typename U>
void ExpectEqual(const T& actual, const U& expected, const char* file, int line, const char* actualExpr, const char* expectedExpr)
{
if (!(actual == expected))
{
std::ostringstream stream;
stream << "expected " << actualExpr << " == " << expectedExpr << ", got " << actual << " and " << expected;
Fail(file, line, stream.str());
}
}

template <typename T, typename U, typename V>
void ExpectNear(const T& actual, const U& expected, const V& tolerance, const char* file, int line, const char* actualExpr, const char* expectedExpr, const char* toleranceExpr)
{
const auto delta = std::fabs(static_cast<double>(actual) - static_cast<double>(expected));
if (delta > static_cast<double>(tolerance))
{
std::ostringstream stream;
stream << "expected " << actualExpr << " ~= " << expectedExpr << " within " << toleranceExpr
<< ", got delta " << delta;
Fail(file, line, stream.str());
}
}

inline void ExpectTrue(bool condition, const char* file, int line, const char* expression)
{
if (!condition)
{
std::ostringstream stream;
stream << "expected true: " << expression;
Fail(file, line, stream.str());
}
}

inline int RunAllTests()
{
int failures = 0;
for (const TestCase& testCase : Registry())
{
try
{
testCase.function();
}
catch (const Failure& failure)
{
++failures;
std::fprintf(stderr, "[FAIL] %s\n %s\n", testCase.name, failure.what());
continue;
}
catch (const std::exception& exception)
{
++failures;
std::fprintf(stderr, "[FAIL] %s\n unexpected exception: %s\n", testCase.name, exception.what());
continue;
}

std::fprintf(stdout, "[PASS] %s\n", testCase.name);
}

std::fprintf(stdout, "Ran %zu test(s), %d failure(s)\n", Registry().size(), failures);
return failures;
}
}

#define TEST_CASE(name) \
static void name(); \
static test::Registrar name##_registrar(#name, &name); \
static void name()

#define EXPECT_TRUE(expr) test::ExpectTrue((expr), __FILE__, __LINE__, #expr)
#define EXPECT_EQ(actual, expected) test::ExpectEqual((actual), (expected), __FILE__, __LINE__, #actual, #expected)
#define EXPECT_NEAR(actual, expected, tolerance) test::ExpectNear((actual), (expected), (tolerance), __FILE__, __LINE__, #actual, #expected, #tolerance)

#endif
6 changes: 6 additions & 0 deletions tests/support/shims/ObjectModel/ObjectModel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef RRF_HOST_TEST_OBJECTMODEL_H
#define RRF_HOST_TEST_OBJECTMODEL_H

#include "../RepRapFirmware.h"

#endif
37 changes: 37 additions & 0 deletions tests/support/shims/RepRapFirmware.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#ifndef RRF_HOST_TEST_REPRAPFIRMWARE_H
#define RRF_HOST_TEST_REPRAPFIRMWARE_H

#include <cstdarg>
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>

#include "../../../../RRFLibraries/src/ecv_duet3d.h"
#include "../../../../RRFLibraries/src/General/SimpleMath.h"
#include "../../../../RRFLibraries/src/General/String.h"
#include "../../../../RRFLibraries/src/General/StringRef.h"

#define SUPPORT_OBJECT_MODEL 0
#define HAS_MASS_STORAGE 0
#define HAS_SBC_INTERFACE 0
#define SUPPORT_CAN_EXPANSION 0

#ifndef THROWS
#define THROWS(...) noexcept(false)
#endif

#ifndef INHERIT_OBJECT_MODEL
#define INHERIT_OBJECT_MODEL
#endif

#ifndef DECLARE_OBJECT_MODEL
#define DECLARE_OBJECT_MODEL
#endif

constexpr float NormalAmbientTemperature = 25.0;
constexpr unsigned int MaxFloatDigitsDisplayedAfterPoint = 3;

#define DEGREE_SYMBOL " "

#endif
Loading
Loading