diff --git a/util/benchmark.hh b/util/benchmark.hh
new file mode 100644
index 0000000000000000000000000000000000000000..050e6c640cf321f521ed033823e9c9c53007d579
--- /dev/null
+++ b/util/benchmark.hh
@@ -0,0 +1,115 @@
+#ifndef BENCHMARK_LOADED
+#define BENCHMARK_LOADED
+
+#include "exception.hh"
+#include <fmt/core.h>
+#include <fstream>
+#include <iostream>
+#include <nlohmann/json.hpp>
+#include <string>
+
+// Bookkeeping of test data to store data of one or more tests in a json file to
+// facilitate future accounting.
+//
+// Usage Example 1 (single test):
+// ==============================
+// 1. define our test
+// juggler_util::test_data test1{
+// {{"name", "example_test"},
+// {"title", "Example Test"},
+// {"description", "This is an example of a test definition"},
+// {"quantity", "efficiency"},
+// {"target", "1"}}};
+// 2. set pass/fail/error status and return value (in this case .99)
+// test1.pass(0.99)
+// 3. write our test data to a json file
+// juggler_util::write_test_data(test1, "test1.json");
+//
+// Usage Example 2 (multiple tests):
+// =================================
+// 1. define our tests
+// juggler_util::test_data test1{
+// {{"name", "example_test"},
+// {"title", "Example Test"},
+// {"description", "This is an example of a test definition"},
+// {"quantity", "efficiency"},
+// {"target", "1"}}};
+// juggler_util::test_data test2{
+// {{"name", "another_test"},
+// {"title", "Another example Test"},
+// {"description", "This is a second example of a test definition"},
+// {"quantity", "resolution"},
+// {"target", "3."}}};
+// 2. set pass/fail/error status and return value (in this case .99)
+// test1.fail(10)
+// 3. write our test data to a json file
+// juggler_util::write_test_data({test1, test2}, "test.json");
+
+// Namespace for utility scripts, FIXME this should be part of an independent
+// library
+namespace juggler_util {
+
+struct test_definition_error : exception {
+ test_definition_error(std::string_view msg)
+ : exception(msg, "test_definition_error") {}
+};
+
+// Wrapper for our test data json, with three methods to set the status
+// after test completion (to pass, fail or error). The default value
+// is error.
+// The following fields should be defined in the definitions json
+// for the test to make sense:
+// - name: unique identifier for this test
+// - title: Slightly more verbose identifier for this test
+// - description: Concise description of what is tested
+// - quantity: What quantity is tested? Unites of value/target
+// - target: Target value of <quantity> that we want to reach
+// - value: Actual value of <quantity>
+// - weight: Weight for this test (this is defaulted to 1.0 if not specified)
+// - result: pass/fail/error
+struct test_data {
+ test_data(nlohmann::json definition) : json{std::move(definition)} {
+ json["weight"] = 1.0;
+ // initialize with error (as we don't have a value yet)
+ error();
+ // Check that all required fields are present
+ for (const auto& field : {"name", "title", "description", "quantity",
+ "target", "value", "weight", "result"}) {
+ if (json.find(field) == json.end()) {
+ throw test_definition_error{
+ fmt::format("Error in test definition: field '{}' missing", field)};
+ }
+ }
+ }
+ // Set this test to pass/fail/error
+ void pass(double value) { update_result("pass", value); }
+ void fail(double value) { update_result("fail", value); }
+ void error(double value = 0) { update_result("error", value); }
+
+ nlohmann::json json;
+
+private:
+ void update_result(std::string_view status, double value) {
+ json["result"] = status;
+ json["value"] = value;
+ }
+};
+
+void write_test_data(const std::vector<test_data>& data,
+ const std::string& fname) {
+ nlohmann::json test_data;
+ for (auto& entry : data) {
+ test_data.push_back(entry.json);
+ }
+ std::cout << fmt::format("Writing test data to {}\n", fname);
+ std::ofstream output_file(fname);
+ output_file << std::setw(4) << test_data << "\n";
+}
+void write_test_data(const test_data& data, const std::string& fname) {
+ std::vector<test_data> vtd{data};
+ write_test_data(vtd, fname);
+}
+
+} // namespace juggler_util
+
+#endif
diff --git a/util/exception.hh b/util/exception.hh
new file mode 100644
index 0000000000000000000000000000000000000000..a443c79593d0225d0060e7a29451e8f2d914e020
--- /dev/null
+++ b/util/exception.hh
@@ -0,0 +1,23 @@
+#ifndef UTIL_EXCEPTION
+#define UTIL_EXCEPTION
+
+#include <exception>
+#include <string>
+
+namespace juggler_util {
+class exception : public std::exception {
+public:
+ exception(std::string_view msg, std::string_view type = "exception")
+ : msg_{msg}, type_{type} {}
+
+ virtual const char* what() const throw() { return msg_.c_str(); }
+ virtual const char* type() const throw() { return type_.c_str(); }
+ virtual ~exception() throw() {}
+
+private:
+ std::string msg_;
+ std::string type_;
+};
+} // namespace juggler_util
+
+#endif