Files

150 lines
7.7 KiB
C++

// Exercise the production collector with a test-only SDK and a real PTY.
#define main collector_program_main
#include "../rph_radar_collector/src/main.cpp"
#undef main
#include <sys/wait.h>
#include <fcntl.h>
#include <fstream>
#include <cstring>
#include <filesystem>
void test_source_session_restart() {
char directory[] = "/tmp/rph-collector-XXXXXX";
if (!::mkdtemp(directory)) { fail("mkdtemp failed"); }
const std::string tty = std::string(directory) + "/tty", log = std::string(directory) + "/sdk.log";
const auto child = ::fork();
if (child == 0) {
::setenv("RPH_SDK_LOG", log.c_str(), 1);
Config cfg;
cfg.tty_path = tty;
cfg.switching = Json::parse(R"({"input":{"name":"input","driver_mode":"mock","positions":2,"default_position":0,"gpio_chip":"/dev/unused","pin_a":1,"pin_b":2},"output":{"name":"output","driver_mode":"mock","positions":2,"default_position":0,"gpio_chip":"/dev/unused","pin_a":3,"pin_b":4},"combos":[[0,0],[0,1]],"settling_ms":0})");
try { ::_exit(run(cfg)); }
catch (const std::exception& error) { std::cerr << error.what() << '\n'; ::_exit(2); }
}
if (child < 0) { std::filesystem::remove_all(directory); fail("collector fork failed"); }
int fd = -1, status = 0;
std::vector<unsigned char> packets;
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
bool finished = false;
const auto drain = [&]() {
unsigned char bytes[4096];
while (fd >= 0) {
const auto count = ::read(fd, bytes, sizeof(bytes));
if (count <= 0) { break; }
packets.insert(packets.end(), bytes, bytes + count);
}
};
while (!finished && std::chrono::steady_clock::now() < deadline) {
if (fd < 0) { fd = ::open(tty.c_str(), O_RDONLY | O_NONBLOCK); }
drain();
finished = ::waitpid(child, &status, WNOHANG) == child;
if (!finished) { ::usleep(1000); }
}
drain();
if (fd >= 0) { ::close(fd); }
if (!finished) { ::kill(child, SIGKILL); (void)::waitpid(child, &status, 0); }
std::ifstream file(log);
const std::string calls{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
std::filesystem::remove_all(directory);
if (!finished || !WIFEXITED(status) || WEXITSTATUS(status)) { fail("SDK session regression did not finish successfully"); }
const std::string session = "Create\nOpen\nStreamsStop\nStreamsDisable\nConfigure\nOutCycleLoadStart\nPrepareData\nSend\nOutCycleSetup WAIT_DONE\nStart\nOutCycleStop WAIT_DONE\nStreamsStop\nStreamsDisable\nClose\nFree\n";
if (calls != session + session + session + session) { fail("SDK session/DOUT restart order changed: " + calls); }
const auto read_le = [&packets](std::size_t offset, unsigned size) {
uint64_t value = 0;
for (unsigned i = 0; i < size; ++i) { value |= uint64_t{packets.at(offset + i)} << (8 * i); }
return value;
};
unsigned clean = 0, total = 0;
for (std::size_t offset = 0; offset < packets.size();) {
if (packets.size() - offset < 48 || std::memcmp(packets.data() + offset, "RPH1", 4)) { fail("Invalid SDK fixture packet"); }
const auto payload = read_le(offset + 4, 4), dirty = read_le(offset + 44, 4);
if (payload != 16 || offset + 48 + payload > packets.size()) { fail("Incomplete phase sweep from SDK fixture"); }
++total;
if (!dirty) {
++clean;
if (read_le(offset + 8, 8) != 2 * clean || read_le(offset + 16, 8) != (clean + 1) / 2 ||
read_le(offset + 40, 2) != 0 || read_le(offset + 42, 2) != (clean - 1) % 2 ||
static_cast<int16_t>(read_le(offset + 52, 2)) != pack_raw_code_to_int16(10000 * clean)) {
fail("Old ADC block crossed GPIO change, or source/cycle identity reset");
}
}
offset += 48 + payload;
}
if (clean != 3 || total != 6) { fail("Expected only new-session full sweeps and dirty initial partials"); }
}
int main() {
try {
test_source_session_restart();
for (const auto* period : {"do8_cycle_period:262145", "do8_cycle_period:4294967295"}) {
std::vector<std::string> args{"collector", "profile:phase", "clock:internal", "tty:/tmp/unused",
"do1_toggle_per_frame", "do1_pair_subtract_avg", "do8_freq_ref", period};
std::vector<char*> argv;
for (auto& arg : args) { argv.push_back(arg.data()); }
bool rejected = false;
try { (void)parse_args(static_cast<int>(argv.size()), argv.data()); }
catch (const std::runtime_error& error) {
rejected = std::string(error.what()).find("cyclic pattern exceeds") != std::string::npos;
}
if (!rejected) { throw std::runtime_error("DO8 overflow must be rejected before allocation or SDK access"); }
}
// Preserve the application's low-minus-high IQ averaging and quantization.
Do1PairSubtractAverageState state;
state.start_run(false);
state.add_pair_sample(false, 1000, -3000);
state.add_pair_sample(false, 5000, 1000);
state.add_pair_sample(true, 200, -1000);
state.add_pair_sample(true, 1000, -1000);
if (state.diff_count != 2 || state.avg_diff_ch1() != 2400 || state.avg_diff_ch2() != 0 ||
pack_raw_code_to_int16(X502_ADC_SCALE_CODE_MAX) != 32767 ||
pack_raw_code_to_int16(-2 * X502_ADC_SCALE_CODE_MAX) != -32768) {
throw std::runtime_error("phase IQ subtraction/averaging changed");
}
// An SDK teardown can hang or ignore SIGTERM; parent death must still
// release the ADC. Reap the orphan here so the regression leaks nothing.
if (::prctl(PR_SET_CHILD_SUBREAPER, 1) != 0) { throw std::runtime_error("subreaper failed"); }
int ready[2], identity[2];
if (::pipe(ready) != 0 || ::pipe(identity) != 0) { throw std::runtime_error("pipe failed"); }
const auto owner = ::fork();
if (owner == 0) {
const auto collector = ::fork();
if (collector < 0) { ::_exit(2); }
if (collector == 0) {
::close(ready[0]);
::signal(SIGTERM, SIG_IGN);
bind_to_parent();
const char started = 1;
if (::write(ready[1], &started, 1) != 1) { ::_exit(2); }
for (;;) { ::pause(); }
}
::close(ready[1]);
if (::write(identity[1], &collector, sizeof(collector)) != sizeof(collector)) { ::_exit(2); }
char started = 0;
::_exit(::read(ready[0], &started, 1) == 1 ? 0 : 2);
}
if (owner < 0) { throw std::runtime_error("fork failed"); }
::close(ready[0]);
::close(ready[1]);
::close(identity[1]);
int status = 0;
(void)::waitpid(owner, &status, 0);
pid_t child = 0;
if (::read(identity[0], &child, sizeof(child)) != sizeof(child)) { throw std::runtime_error("missing child PID"); }
::close(identity[0]);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1);
pid_t orphan = 0;
while ((orphan = ::waitpid(-1, &status, WNOHANG)) == 0 && std::chrono::steady_clock::now() < deadline) {
::usleep(1000);
}
if (orphan <= 0 || !WIFSIGNALED(status) || WTERMSIG(status) != SIGKILL) {
if (child > 0) { ::kill(child, SIGKILL); (void)::waitpid(child, &status, 0); }
throw std::runtime_error("collector survived parent death or entered SIGTERM teardown");
}
std::cout << "collector regressions passed\n";
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}