New test support harness primitives for testing stimbuffapis

This commit is contained in:
2026-06-13 18:47:44 -04:00
parent 2f31e9a034
commit 3d81ee92aa
5 changed files with 320 additions and 0 deletions
+2
View File
@@ -1,9 +1,11 @@
add_library(spinscale_test_support STATIC add_library(spinscale_test_support STATIC
support/threadHarness.cpp support/threadHarness.cpp
support/probeComponentThread.cpp
) )
target_include_directories(spinscale_test_support PUBLIC target_include_directories(spinscale_test_support PUBLIC
${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/tests/fixtures
) )
target_link_libraries(spinscale_test_support PUBLIC target_link_libraries(spinscale_test_support PUBLIC
+71
View File
@@ -0,0 +1,71 @@
#ifndef SPINSCALE_TEST_SUPPORT_BAKED_DEVICE_CATALOG_H
#define SPINSCALE_TEST_SUPPORT_BAKED_DEVICE_CATALOG_H
#include <cstddef>
#include <optional>
#include <string>
#include <vector>
#include <bakedCameraProfiles.h>
namespace sscl::tests {
inline std::vector<const test_fixtures::BakedCameraProfile *>
profilesForMachine(const char *machineTag)
{
std::vector<const test_fixtures::BakedCameraProfile *> matches;
for (std::size_t i = 0; i < test_fixtures::bakedCameraProfileCount; ++i)
{
const test_fixtures::BakedCameraProfile& profile =
test_fixtures::bakedCameraProfiles[i];
if (std::string(profile.machineTag) == machineTag) {
matches.push_back(&profile);
}
}
return matches;
}
inline std::optional<const test_fixtures::BakedCameraProfile *>
findProfileByTag(const char *machineTag, const char *profileTag)
{
for (std::size_t i = 0; i < test_fixtures::bakedCameraProfileCount; ++i)
{
const test_fixtures::BakedCameraProfile& profile =
test_fixtures::bakedCameraProfiles[i];
if (std::string(profile.machineTag) == machineTag
&& std::string(profile.profileTag) == profileTag)
{
return &profile;
}
}
return std::nullopt;
}
inline std::vector<const test_fixtures::BakedCameraProfile *>
requiredProfilesForMachine(const char *machineTag)
{
std::vector<const test_fixtures::BakedCameraProfile *> matches;
for (std::size_t i = 0; i < test_fixtures::bakedCameraProfileCount; ++i)
{
const test_fixtures::BakedCameraProfile& profile =
test_fixtures::bakedCameraProfiles[i];
if (std::string(profile.machineTag) == machineTag
&& profile.requiredOnMachine)
{
matches.push_back(&profile);
}
}
return matches;
}
} // namespace sscl::tests
#endif // SPINSCALE_TEST_SUPPORT_BAKED_DEVICE_CATALOG_H
+63
View File
@@ -0,0 +1,63 @@
#ifndef SPINSCALE_TEST_SUPPORT_EXCEPTION_ASSERTIONS_H
#define SPINSCALE_TEST_SUPPORT_EXCEPTION_ASSERTIONS_H
#include <exception>
#include <stdexcept>
#include <string>
#include <gtest/gtest.h>
namespace sscl::tests {
inline void requireExceptionMessageContains(
const std::exception &exception,
const std::string &expectedSubstring)
{
const std::string message = exception.what();
if (message.find(expectedSubstring) == std::string::npos) {
throw std::runtime_error(
"Expected exception message to contain \""
+ expectedSubstring
+ "\", got \""
+ message
+ "\"");
}
}
inline void expectExceptionMessageContains(
const std::exception &exception,
const std::string &expectedSubstring)
{
EXPECT_NO_THROW(
requireExceptionMessageContains(exception, expectedSubstring));
}
inline void requireExceptionPtrMessageContains(
const std::exception_ptr &exceptionPtr,
const std::string &expectedSubstring)
{
try {
std::rethrow_exception(exceptionPtr);
}
catch (const std::exception &exception) {
requireExceptionMessageContains(exception, expectedSubstring);
return;
}
catch (...) {
throw std::runtime_error("Expected std::exception in exception_ptr");
}
}
inline void expectExceptionPtrMessageContains(
const std::exception_ptr &exceptionPtr,
const std::string &expectedSubstring)
{
EXPECT_NO_THROW(
requireExceptionPtrMessageContains(
exceptionPtr,
expectedSubstring));
}
} // namespace sscl::tests
#endif // SPINSCALE_TEST_SUPPORT_EXCEPTION_ASSERTIONS_H
+118
View File
@@ -0,0 +1,118 @@
#include <support/probeComponentThread.h>
#include <iostream>
#include <spinscale/component.h>
namespace sscl::tests {
namespace {
constexpr sscl::ThreadId PROBE_PUPPETEER_THREAD_ID = 2;
class ProbeDummyPuppeteerComponent
: public sscl::pptr::PuppeteerComponent
{
public:
explicit ProbeDummyPuppeteerComponent(
const std::shared_ptr<sscl::PuppeteerThread>& componentThreadIn)
: sscl::pptr::PuppeteerComponent(componentThreadIn)
{}
void handleLoopExceptionHook() override
{
std::cerr << "ProbeComponentThreadHarness: puppeteer loop exception\n";
}
};
void probePuppeteerMain(
const sscl::PuppeteerThread::EntryFnArguments& args,
const std::function<void(
const std::shared_ptr<sscl::ComponentThread>&)>& work,
std::promise<std::exception_ptr>& donePromise)
{
sscl::PuppeteerThread& thr = args.usableBeforeJolt;
thr.initializeTls();
sscl::ComponentThread::setPuppeteerThreadId(PROBE_PUPPETEER_THREAD_ID);
std::shared_ptr<sscl::PuppeteerThread> thrPtr =
std::static_pointer_cast<sscl::PuppeteerThread>(thr.shared_from_this());
sscl::ComponentThread::setPuppeteerThread(thrPtr);
try {
work(thrPtr);
donePromise.set_value(nullptr);
}
catch (...) {
donePromise.set_value(std::current_exception());
}
thr.getIoContext().stop();
}
} // namespace
ProbeComponentThreadHarness::ProbeComponentThreadHarness(
const char *threadName)
: threadName(threadName),
dummyComponent(std::make_shared<ProbeDummyPuppeteerComponent>(
std::shared_ptr<sscl::PuppeteerThread>()))
{}
ProbeComponentThreadHarness::~ProbeComponentThreadHarness() = default;
std::shared_ptr<sscl::ComponentThread>
ProbeComponentThreadHarness::componentThread() const
{
return lastComponentThread;
}
void ProbeComponentThreadHarness::runSync(
const std::function<void(
const std::shared_ptr<sscl::ComponentThread>&)>& work)
{
std::promise<std::exception_ptr> donePromise;
std::future<std::exception_ptr> doneFuture = donePromise.get_future();
std::shared_ptr<sscl::PuppeteerThread> runThread =
std::make_shared<sscl::PuppeteerThread>(
PROBE_PUPPETEER_THREAD_ID,
threadName,
[&work, &donePromise](
const sscl::PuppeteerThread::EntryFnArguments& args)
{
probePuppeteerMain(args, work, donePromise);
},
*dummyComponent,
nullptr);
dummyComponent->thread = runThread;
lastComponentThread = runThread;
runThread->thread.join();
std::exception_ptr probeException = doneFuture.get();
if (probeException) {
std::rethrow_exception(probeException);
}
}
void runNonViralNurseryOnComponentThread(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
std::function<sscl::co::NonViralNonPostingInvoker(
sscl::co::NonViralTaskNursery::Slot::Lease&)> invokerFactory,
std::chrono::milliseconds timeout)
{
(void)timeout;
sscl::co::NonViralTaskNursery nursery;
nursery.openAdmission();
nursery.launch(
[&invokerFactory](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return invokerFactory(lease);
});
nursery.closeAdmission();
nursery.syncAwaitAllSettlements(componentThread->getIoContext());
}
} // namespace sscl::tests
+66
View File
@@ -0,0 +1,66 @@
#ifndef SPINSCALE_TEST_SUPPORT_PROBE_COMPONENT_THREAD_H
#define SPINSCALE_TEST_SUPPORT_PROBE_COMPONENT_THREAD_H
#include <chrono>
#include <exception>
#include <functional>
#include <future>
#include <memory>
#include <stdexcept>
#include <spinscale/componentThread.h>
#include <spinscale/co/invokers.h>
#include <spinscale/co/nonViralTaskNursery.h>
namespace sscl::tests {
constexpr std::chrono::milliseconds defaultProbeTaskTimeout{10000};
void runNonViralNurseryOnComponentThread(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
std::function<sscl::co::NonViralNonPostingInvoker(
sscl::co::NonViralTaskNursery::Slot::Lease&)> invokerFactory,
std::chrono::milliseconds timeout = defaultProbeTaskTimeout);
class ProbeComponentThreadHarness
{
public:
explicit ProbeComponentThreadHarness(
const char *threadName = "spinscale-probe");
~ProbeComponentThreadHarness();
ProbeComponentThreadHarness(const ProbeComponentThreadHarness &) = delete;
ProbeComponentThreadHarness &operator=(
const ProbeComponentThreadHarness &) = delete;
std::shared_ptr<sscl::ComponentThread> componentThread() const;
void runSync(
const std::function<void(
const std::shared_ptr<sscl::ComponentThread>&)>& work);
template <typename InvokerFactory>
void runNonViralNurseryTask(
InvokerFactory &&invokerFactory,
std::chrono::milliseconds timeout = defaultProbeTaskTimeout)
{
runSync(
[this, &invokerFactory, timeout](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
std::forward<InvokerFactory>(invokerFactory),
timeout);
});
}
private:
std::string threadName;
std::shared_ptr<sscl::pptr::PuppeteerComponent> dummyComponent;
std::shared_ptr<sscl::ComponentThread> lastComponentThread;
};
} // namespace sscl::tests
#endif // SPINSCALE_TEST_SUPPORT_PROBE_COMPONENT_THREAD_H