Using C 17. I am trying to setup a gtest fixture that will create a fresh io_context to run timers on for each test case. My test segfault about 90% of the time. If I debug and step very slowly, I can get it to run all the way through.
I am not sure what's going on here. I've went and created a new thread and new ioservice every run, just to make sure there was no carry over from previous tests. Then I just deleted the test contents entirely to narrow it down. It throws on the stop call.
#include "gtest/gtest.h"
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <iostream>
class TrafficLightTestSuite : public testing::Test
{
public:
protected:
boost::asio::io_context * m_ioContext;
boost::thread * m_ioThread;
void SetUp() override
{
std::cout << "Setup" << std::endl;
m_ioThread = new boost::thread([&]()
{
m_ioContext = new boost::asio::io_context();
std::cout << "IO Service created" << std::endl;
// Keep the io service alive until we are done
boost::asio::io_service::work work(*m_ioContext);
m_ioContext->run();
std::cout << "IO Context run exited" << std::endl;
delete m_ioContext;
m_ioContext = nullptr;
std::cout << "IO Context deleted" << std::endl;
});
}
void TearDown() override
{
std::cout << "Tear down stopping IO Context" << std::endl;
m_ioContext->stop();
m_ioThread->join();
std::cout << "Thread exit" << std::endl;
delete m_ioThread;
}
};
TEST_F(TrafficLightTestSuite, testTimeLapse)
{
std::cout << "Performing test" << std::endl;
}
Output when running:
Testing started at 1:49 PM ...
Setup
Performing test
Tear down stopping IO Context
Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)
Output when stepping through with debugger:
Testing started at 1:55 PM ...
Setup
IO Service created
Performing test
Tear down stopping IO Context
IO Context run exited
IO Context deleted
Thread exit
Process finished with exit code 0
io_context has to be thread safe, how else would you tell it to stop? Anyone spot a problem?
CodePudding user response:
m_ioContext->stop();
happens before m_ioContext = new boost::asio::io_context();
. Just move its creation to SetUp
and deletion to the TearDown
. You even do not need pointers.
#include "gtest/gtest.h"
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <iostream>
class TrafficLightTestSuite : public testing::Test
{
public:
protected:
boost::asio::io_context m_ioContext{};
boost::thread m_ioThread{};
void SetUp() override
{
std::cout << "Setup" << std::endl;
std::cout << "IO Service created" << std::endl;
m_ioThread = boost::thread([&]()
{
// Keep the io service alive until we are done
boost::asio::io_service::work work(m_ioContext);
m_ioContext.run();
std::cout << "IO Context run exited" << std::endl;
});
}
void TearDown() override
{
std::cout << "Tear down stopping IO Context" << std::endl;
m_ioContext.stop();
m_ioThread.join();
std::cout << "Thread exit" << std::endl;
std::cout << "IO Context deleted" << std::endl;
}
};
TEST_F(TrafficLightTestSuite, testTimeLapse)
{
std::cout << "Performing test" << std::endl;
}
CodePudding user response:
The problem is that there is no guarantee that anything on a thread is going to execute before the main thread resumes execution. This caused problems where the ioservice was not created and running before the test case executed, where it was assumed that the ioservice would be running.
This can be fixed using a semaphore that can be waiting on, and queuing up a post on that semaphore before calling ioservice::run. That way, as soon as the ioservice runs, the semaphore posts and the test case cannot execute until that occurs.
#include <boost/asio.hpp>
#include <boost/interprocess/sync/interprocess_semaphore.hpp>
#include <boost/thread.hpp>
#include "gtest/gtest.h"
class TrafficLightTestSuite : public testing::Test
{
public:
protected:
boost::asio::io_context m_ioContext;
boost::thread m_ioThread;
boost::interprocess::interprocess_semaphore m_semaphore{0};
void SetUp() override
{
// Keep the ioservice running until it is explicitly stopped, even if all posted work has completed
boost::asio::io_service::work work(m_ioContext);
// When the ioservice starts up, the semaphore will notify anyone that was waiting on it to run
m_ioContext.post([&]() { m_semaphore.post(); });
// Run the io service on its own thread, where completion handlers will be called
m_ioThread = boost::thread(boost::bind(&boost::asio::io_service::run, &m_ioContext));
// Test cases should wait on the semaphore to ensure the ioservice is running before they execute.
// In production environment, you'd probably init the ioservice in some manner of application setup and do a
// similar notification when everything was initialized.
m_semaphore.wait();
}
void TearDown() override
{
m_ioContext.stop();
m_ioThread.join();
}
};
TEST_F(TrafficLightTestSuite, testInitialColor)
{
// This is guaranteed not to execute until the ioservice is running
}
You could also make your own semaphore using a condition variable. If using C 20, there are standard semaphores available,