Files
pip/tests/thread/pigrabberbase_test.cpp
andrey 92283e29b1 thread: PIGrabberBase and PIPipelineThread on PIConditionVariable instead of delays
- PIGrabberBase: replace piMSleep(200)/piMinSleep() polling with interruptible
  PIConditionVariable::waitFor waits; add wakeUp() handler for event-driven
  polling and setPollDelay()/pollDelay() for the fallback poll interval
- PIPipelineThread: single mutex + cv_not_empty/cv_not_full (bounded queue
  pattern), terminating in wait predicates so stop/clear wake blocked
  producers/consumers; drop bogus mutex unlocks in stopCalc()
- fix potential bug: calculated() was emitted with 2 of 3 arguments
- fix potential bug: connectTo() ABI mismatch for POD Tout (event dispatch
  passes first arg by value, handler took it by reference) - route through
  a by-value fromCalculated() adapter
- PIOBJECT_SUBCLASS/PIOBJECT_PARENT made variadic so template parents with
  commas (PIPipelineThread<int, int>) can be passed directly
- add gtest suites for both classes (tests/thread/)
2026-08-24 08:20:28 +03:00

219 lines
4.8 KiB
C++

#include "pibase_macros.h"
#include "pigrabberbase.h"
#include "pisystemtime.h"
#include "pithread.h"
#include "pitime.h"
#include "gtest/gtest.h"
#include <atomic>
#include <thread>
namespace {
class FakeGrabber: public PIGrabberBase<int> {
PIOBJECT_SUBCLASS(FakeGrabber, PIGrabberBase<int>)
public:
std::atomic_int open_count{0};
std::atomic_bool open_ok{true};
int open_fail_times = 0; // fail first N open attempts
std::atomic_bool data_available{false};
int ready_value = 7;
std::atomic_int poll_count{0};
std::atomic_int record_count{0};
protected:
bool openInternal() override {
open_count++;
if (open_fail_times > 0) {
open_fail_times--;
return false;
}
return open_ok;
}
void closeInternal() override {}
int get(int & val) override {
poll_count++;
if (!data_available) return 1; // no item ready yet
val = ready_value;
return 0;
}
void record(const int & val) override {
(void)val;
record_count++;
}
};
bool waitForQueSize(PIGrabberBase<int> & g, int size, double timeout_s = 5.) {
PITimeMeasurer tm;
while (g.queSize() < size && tm.elapsed_s() < timeout_s)
piMSleep(1);
return g.queSize() >= size;
}
} // namespace
TEST(PIGrabberBase_Basic, CaptureAndLast) {
FakeGrabber g;
g.data_available = true;
g.ready_value = 7;
std::atomic_int ready_events(0);
CONNECTL(&g, dataReady, [&ready_events]() { ready_events++; });
g.start();
ASSERT_TRUE(waitForQueSize(g, 5));
EXPECT_EQ(g.last(), 7);
EXPECT_GE(ready_events.load(), 5);
g.stopGrabber();
EXPECT_FALSE(g.isRunning());
EXPECT_FALSE(g.isOpened());
// queue is frozen now, counters must match exactly
EXPECT_EQ(g.diag().state().received_packets, (ullong)g.queSize());
EXPECT_EQ(g.dequeue(), 7);
}
TEST(PIGrabberBase_Wake, WakeUpBeatsPollDelay) {
FakeGrabber g;
g.data_available = false;
g.setPollDelay(PISystemTime::fromMilliseconds(1000));
g.start();
piMSleep(100);
ASSERT_EQ(g.queSize(), 0); // loop is in its 1 second wait
g.ready_value = 5;
g.data_available = true;
g.wakeUp();
PITimeMeasurer tm;
while (g.queSize() < 1 && tm.elapsed_s() < 2.)
piMSleep(1);
EXPECT_GE(g.queSize(), 1);
EXPECT_EQ(g.last(), 5);
EXPECT_LT(tm.elapsed_m(), 500.); // woke up long before the 1 second poll interval
g.stopGrabber();
EXPECT_FALSE(g.isRunning());
}
TEST(PIGrabberBase_Stop, FastStopDuringLongPollWait) {
FakeGrabber g;
g.data_available = false;
g.setPollDelay(PISystemTime::fromMilliseconds(2000));
g.start();
piMSleep(100); // loop is now waiting up to 2 seconds
PITimeMeasurer tm;
g.stopGrabber();
EXPECT_LT(tm.elapsed_m(), 500.); // stop interrupts the wait, does not wait for it
EXPECT_FALSE(g.isRunning());
}
TEST(PIGrabberBase_Open, RetryUntilSuccess) {
FakeGrabber g;
g.open_fail_times = 2; // first two opens fail, the third succeeds
g.data_available = true;
g.start();
PITimeMeasurer tm;
while (!g.isOpened() && tm.elapsed_s() < 3.)
piMSleep(1);
EXPECT_TRUE(g.isOpened());
EXPECT_LE(tm.elapsed_s(), 1.); // two 200 ms retry waits
EXPECT_EQ(g.open_count.load(), 3);
ASSERT_TRUE(waitForQueSize(g, 1));
EXPECT_EQ(g.last(), g.ready_value);
g.stopGrabber();
EXPECT_FALSE(g.isRunning());
}
TEST(PIGrabberBase_Open, FastStopDuringOpenRetry) {
FakeGrabber g;
g.open_ok = false; // opens always fail, loop retries every 200 ms
g.start();
piMSleep(100); // loop is in the 200 ms open-retry wait
PITimeMeasurer tm;
g.stopGrabber();
EXPECT_LT(tm.elapsed_m(), 150.); // stop interrupts the retry wait
EXPECT_FALSE(g.isRunning());
EXPECT_FALSE(g.isOpened());
}
TEST(PIGrabberBase_Recording, StartStopRecord) {
FakeGrabber g;
g.data_available = true;
g.start();
ASSERT_TRUE(waitForQueSize(g, 2));
EXPECT_FALSE(g.isRecording());
g.startRecord("test");
ASSERT_TRUE(g.isRecording());
PITimeMeasurer tm;
while (g.record_count.load() < 1 && tm.elapsed_s() < 5.)
piMSleep(1);
EXPECT_GE(g.record_count.load(), 1);
g.stopRecord();
EXPECT_FALSE(g.isRecording());
g.stopGrabber();
EXPECT_FALSE(g.isRunning());
}
TEST(PIGrabberBase_Close, RestartReopens) {
FakeGrabber g;
g.data_available = true;
g.start();
ASSERT_TRUE(waitForQueSize(g, 1));
EXPECT_EQ(g.last(), g.ready_value);
// stop the data flow, then let the loop settle before clearing
g.data_available = false;
piMSleep(20);
g.restart(); // clear queue and close
EXPECT_FALSE(g.isOpened());
EXPECT_TRUE(g.isEmpty());
// the running loop must reopen the grabber on its own
PITimeMeasurer tm;
while (!g.isOpened() && tm.elapsed_s() < 3.)
piMSleep(1);
EXPECT_TRUE(g.isOpened());
g.stopGrabber();
EXPECT_FALSE(g.isRunning());
}
TEST(PIGrabberBase_PollDelay, GetterSetter) {
FakeGrabber g;
EXPECT_DOUBLE_EQ(g.pollDelay().toMilliseconds(), (double)PIP_MIN_MSLEEP);
g.setPollDelay(PISystemTime::fromMilliseconds(55));
EXPECT_DOUBLE_EQ(g.pollDelay().toMilliseconds(), 55.);
}