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
2 changes: 1 addition & 1 deletion .github/workflows/colcon_build_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
git submodule update --init --recursive

- name: Cache Build Artifacts
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: build/
key: ${{ runner.os }}-build-${{ hashFiles('**/CMakeLists.txt') }}
Expand Down
71 changes: 71 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Coverage

on:
push:
branches:
- main
pull_request:
branches:
- '**'
types: [opened, synchronize, reopened]
merge_group:
branches:
- main

jobs:
coverage:
runs-on: ubuntu-latest
timeout-minutes: 180
container: linuxmintd/mint22-amd64:latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Cache Bazel Disk Cache
uses: actions/cache@v4
with:
path: ~/.cache/bazel
key: ${{ runner.os }}-bazel-coverage-${{ github.sha }}
restore-keys: |
${{ runner.os }}-bazel-coverage-
${{ runner.os }}-bazel-

- name: Cache APT packages
uses: actions/cache@v4
with:
path: /var/cache/apt
key: ${{ runner.os }}-apt-${{ hashFiles('dependencies.txt') }}
restore-keys: |
${{ runner.os }}-apt-

- name: Install Base Dependencies
run: |
sudo apt-get update && sudo apt-get upgrade -y
sudo apt-get update && apt-get install -y --no-install-recommends \
curl gnupg2 lsb-release software-properties-common \
locales \
git \
wget \
lcov

- name: Install Project Dependencies
run: |
sudo apt-get install -y --no-install-recommends $(cat dependencies.txt)

- name: Install Bazel (Bazelisk)
run: |
wget https://github.com/bazelbuild/bazelisk/releases/download/v1.27.0/bazelisk-linux-amd64
chmod +x bazelisk-linux-amd64
sudo mv bazelisk-linux-amd64 /usr/local/bin/bazel

- name: Run Coverage
run: |
bazel coverage //motorium/... --combined_report=lcov

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: bazel-out/_coverage/_coverage_report.dat
fail_ci_if_error: false
8 changes: 8 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ bazel_dep(name = "glew", version = "2.2.0")

non_module_deps = use_extension("//:extensions.bzl", "non_module_deps")
use_repo(non_module_deps, "mujoco")

bazel_dep(name = "rules_python", version = "1.7.0")

python = use_extension("@rules_python//python/extensions:python.bzl", "python")
python.toolchain(
ignore_root_user_error = True,
python_version = "3.10",
)
2,538 changes: 164 additions & 2,374 deletions MODULE.bazel.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
![Bazel Build](https://github.com/manumerous/motorium/actions/workflows/bazel_build_test.yml/badge.svg)
![Colcon Build](https://github.com/manumerous/motorium/actions/workflows/colcon_build_test.yml/badge.svg)
![Style Check](https://github.com/manumerous/motorium/actions/workflows/format_test.yml/badge.svg)
[![Coverage Status](https://codecov.io/gh/manumerous/motorium/branch/main/graph/badge.svg)](https://codecov.io/gh/manumerous/motorium)


Motorium is a high-performance, realtime-safe framework for robot control, designed to allow rapid prototyping of robot controllers across hardware and simulators. It prioritizes deterministic execution, cache locality, and type safety.

Expand Down
9 changes: 9 additions & 0 deletions motorium/motorium_model/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,12 @@ cc_test(
"@com_google_googletest//:gtest_main",
],
)

cc_test(
name = "test_fixed_id_array",
srcs = ["test/testFixedIDArray.cpp"],
deps = [
":motorium_model",
"@com_google_googletest//:gtest_main",
],
)
7 changes: 7 additions & 0 deletions motorium/motorium_model/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ ament_target_dependencies(${PROJECT_NAME}
target_link_libraries(${PROJECT_NAME}
${URDFDOM_LIBRARIES}
absl::flat_hash_map
absl::log
absl::check
)
target_include_directories(${PROJECT_NAME}
PUBLIC
Expand All @@ -72,6 +74,11 @@ if (BUILD_TESTING)
${dependencies}
)

ament_add_gtest(test_fixed_id_array test/testFixedIDArray.cpp)
target_link_libraries(test_fixed_id_array
${PROJECT_NAME}
)

endif ()

#############
Expand Down
8 changes: 5 additions & 3 deletions motorium/motorium_model/include/motorium_model/FixedIDArray.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ namespace motorium::model {
// Define an extractor that gets a scalar value out of T for eigen vectorization
// when T is itself a composite type (e.g. struct).
template <typename E, typename T, typename ScalarType>
concept IDMapExtractor = requires(E e, const T& val) {
{ e(val) } -> std::same_as<ScalarType>;
};
concept IDMapExtractor =
(requires(const T& val) { val.has_value(); *val; } &&
requires(E e, const T& val) { { e(*val) } -> std::convertible_to<ScalarType>; }) ||
(!requires(const T& val) { val.has_value(); } &&
requires(E e, const T& val) { { e(val) } -> std::convertible_to<ScalarType>; });

template <typename T>
class FixedIDArray {
Expand Down
148 changes: 148 additions & 0 deletions motorium/motorium_model/test/testFixedIDArray.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#include <gtest/gtest.h>
#include <numeric>

#include <optional>
#include "motorium_model/FixedIDArray.h"

using namespace motorium::model;

namespace {

// Helper struct for testing non-trivial types
struct TestStruct {
double value = 0.0;
bool operator==(const TestStruct& other) const { return value == other.value; }
};

} // namespace

class FixedIDArrayTest : public ::testing::Test {};

TEST_F(FixedIDArrayTest, InitializationCalculatesSizeCorrectly) {
FixedIDArray<int> array(10);
EXPECT_EQ(array.size(), 10);
}

TEST_F(FixedIDArrayTest, InitializationFailsForZeroSize) {
EXPECT_DEATH(FixedIDArray<int> array(0), "Cannot initialize empty map");
}

TEST_F(FixedIDArrayTest, InitializationFailsForTooLargeSize) {
EXPECT_DEATH(FixedIDArray<int> array(300), "Too many elements");
}

TEST_F(FixedIDArrayTest, AccessWorksForValidIndices) {
FixedIDArray<int> array(5);
array[0] = 42;
array.at(4) = 100;

EXPECT_EQ(array[0], 42);
EXPECT_EQ(array.at(4), 100);
}

TEST_F(FixedIDArrayTest, AccessFailsForOutOfBounds) {
FixedIDArray<int> array(5);
EXPECT_DEATH(array.at(5), "Element with ID 5 not found");
EXPECT_DEATH(array[10], "Element with ID 10 not found");
}

TEST_F(FixedIDArrayTest, ConstAccessWorks) {
FixedIDArray<int> array(5);
array[2] = 7;

const FixedIDArray<int>& const_array = array;
EXPECT_EQ(const_array[2], 7);
EXPECT_EQ(const_array.at(2), 7);
}

TEST_F(FixedIDArrayTest, AssignmentWorksIdeally) {
FixedIDArray<int> arr1(3);
arr1[0] = 1;
arr1[1] = 2;
arr1[2] = 3;

FixedIDArray<int> arr2(3);
arr2 = arr1;

EXPECT_EQ(arr2[0], 1);
EXPECT_EQ(arr2[1], 2);
EXPECT_EQ(arr2[2], 3);
}

TEST_F(FixedIDArrayTest, AssignmentFailsForSizeMismatch) {
FixedIDArray<int> arr1(3);
FixedIDArray<int> arr2(4);
EXPECT_DEATH(arr2 = arr1, "size mismatch");
}

TEST_F(FixedIDArrayTest, IterationWorks) {
FixedIDArray<int> array(3);
array[0] = 10;
array[1] = 20;
array[2] = 30;

int sum = std::accumulate(array.begin(), array.end(), 0);
EXPECT_EQ(sum, 60);
}

TEST_F(FixedIDArrayTest, ToEigenVectorWorksForSimpleType) {
FixedIDArray<double> array(3);
array[0] = 1.1;
array[1] = 2.2;
array[2] = 3.3;

std::vector<size_t> indices = {0, 2};
auto extractor = [](double v) { return v; };

auto vec = array.toEigenVector<std::vector<size_t>, double>(indices, extractor);

ASSERT_EQ(vec.size(), 2);
EXPECT_DOUBLE_EQ(vec(0), 1.1);
EXPECT_DOUBLE_EQ(vec(1), 3.3);
}

TEST_F(FixedIDArrayTest, ToEigenVectorWorksWithOptional) {
FixedIDArray<std::optional<double>> array(3);
array[0] = 1.0;
array[1] = std::nullopt;
array[2] = 3.0;

std::vector<size_t> indices = {0, 1, 2};
auto extractor = [](double v) { return v; };

auto vec = array.toEigenVector<std::vector<size_t>, double>(indices, extractor, -1.0); // Default -1.0 for nullopt

ASSERT_EQ(vec.size(), 3);
EXPECT_DOUBLE_EQ(vec(0), 1.0);
EXPECT_DOUBLE_EQ(vec(1), -1.0);
EXPECT_DOUBLE_EQ(vec(2), 3.0);
}

TEST_F(FixedIDArrayTest, ToEigenVectorWorksWithStruct) {
FixedIDArray<TestStruct> array(2);
array[0].value = 5.5;
array[1].value = 6.6;

std::vector<size_t> indices = {1};
auto extractor = [](const TestStruct& s) { return s.value; };

auto vec = array.toEigenVector<std::vector<size_t>, double>(indices, extractor);

ASSERT_EQ(vec.size(), 1);
EXPECT_DOUBLE_EQ(vec(0), 6.6);
}

TEST_F(FixedIDArrayTest, ToEigenVectorWorksWithStructAndOptional) {
FixedIDArray<std::optional<TestStruct>> array(2);
array[0] = TestStruct{5.5};
array[1] = std::nullopt;

std::vector<size_t> indices = {0, 1};
auto extractor = [](const TestStruct& s) { return s.value; };

auto vec = array.toEigenVector<std::vector<size_t>, double>(indices, extractor, -1.0); // Default -1.0 for nullopt

ASSERT_EQ(vec.size(), 2);
EXPECT_DOUBLE_EQ(vec(0), 5.5);
EXPECT_DOUBLE_EQ(vec(1), -1.0);
}