Files
vdm/core/tests/net/testserver_fixture.hpp
T
samiandClaude Sonnet 5 79c29b47e8 core: finish the hostile-mode matrix -- 8 remaining end-to-end cases
Was 7/16 of tools/testserver/README.md's mode table covered by
engine_test.cpp. Adds the rest:

- engine_expiring_signed_url_recovers_via_refresh_url: an expired signed
  URL 403s, the engine asks (paused, decision_calls >= 1) rather than
  failing terminally, and DownloadHandle::refresh_url() with a freshly
  signed URL completes it -- exercises both do_refresh_url() fixes and
  the probe-level referrer retry's second-403 path from the previous
  commit.
- engine_403_without_referer_retries_with_origin: no spec.referrer set,
  the automatic single retry (previous commit) recovers with zero
  decisions asked.
- engine_redirect_chain_follows_to_completion: 5 hops of a plain 302.
  No core-side change needed -- documents that CURLOPT_FOLLOWLOCATION/
  MAXREDIRS (already on, RequestOptions::follow_redirects) cover both
  the probe's and every worker's own request, not just one of the two.
- engine_slow_loris_stall_timeout_fires: proves curl's stall detector
  (CURLOPT_LOW_SPEED_LIMIT/_TIME, download_task.cpp's hardcoded 1024 B/s
  for 30s) actually fires rather than hanging. Needed a real fix, not
  just a test: every other test in this file relies on TestServer's
  short 1s loris dribble to keep runtime down, but 1s of trickle
  followed by full-speed streaming never accumulates curl's required 30
  CONSECUTIVE seconds under the floor, so it would never actually abort
  -- a test built on the default dribble would pass by the download
  merely finishing a bit late, not by observing the stall timeout fire.
  testserver_fixture.hpp's TestServer gained an explicit-loris-seconds
  constructor (default ctor unchanged, still 1s) so this one test can
  ask for a dribble (40s) that genuinely outlasts the threshold.
- engine_401_digest_then_provide_auth_completes: same shape as the
  existing 401-basic test: http_client.cpp already asks libcurl for
  CURLAUTH_ANY regardless of net::AuthScheme, so this needed no core
  change -- it passed on the first run and is here to prove that's true
  end-to-end, not just at the http_client unit level.
- engine_chunked_no_length_completes_single_segment: Transfer-Encoding:
  chunked, no Content-Length anywhere (including HEAD). No core change
  needed -- takes the same size-agnostic "unknown size, one plain-GET
  segment" path as the existing no-range test.
- utf8/legacy-content-disposition: already covered end-to-end by
  probe_reads_utf8_content_disposition and
  probe_reads_legacy_content_disposition in probe_test.cpp (probe-level,
  as these modes only affect the initial request) -- verified passing,
  no new test needed.

All 20 engine_test.cpp cases and all 10 probe_test.cpp cases pass. Every
testserver.py spawned while writing and running this was reaped by
TestServer's destructor; verified no stragglers with `ps aux` after each
run.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q3QrF7rCt21bkAjt9BCDFQ
2026-09-12 21:34:24 +04:00

122 lines
3.7 KiB
C++

// testserver_fixture.hpp — spawn tools/testserver for a test, tear it down after.
//
// Linux-only (fork/exec/pipe/kill). The path to testserver.py is injected by CMake as
// VDM_TESTSERVER_PY; if it's empty or missing the fixture reports unavailable() and the
// test should skip.
#ifndef VDM_TESTS_NET_TESTSERVER_FIXTURE_HPP
#define VDM_TESTS_NET_TESTSERVER_FIXTURE_HPP
#include <fcntl.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <thread>
#ifndef VDM_TESTSERVER_PY
#define VDM_TESTSERVER_PY ""
#endif
namespace vdm::testing {
class TestServer {
public:
TestServer() : TestServer(1.0) {}
// loris_seconds overrides the dribble duration slow-loris mode uses (default matches the
// no-arg ctor's long-standing 1s). A test that needs curl's stall detector
// (CURLOPT_LOW_SPEED_TIME, hardcoded to 30s in download_task.cpp) to actually fire needs a
// dribble that outlasts that threshold, not the short one every other test relies on to
// keep runtime down.
explicit TestServer(double loris_seconds) {
const char *script = VDM_TESTSERVER_PY;
if (!script || !*script || ::access(script, R_OK) != 0)
return;
int pipefd[2];
if (::pipe(pipefd) != 0)
return;
pid_ = ::fork();
if (pid_ < 0) {
::close(pipefd[0]);
::close(pipefd[1]);
return;
}
if (pid_ == 0) {
::dup2(pipefd[1], STDOUT_FILENO);
::close(pipefd[0]);
::close(pipefd[1]);
int devnull = ::open("/dev/null", O_WRONLY);
if (devnull >= 0)
::dup2(devnull, STDERR_FILENO);
std::string loris_str = std::to_string(loris_seconds);
::execlp("python3", "python3", script, "--port", "0", "--seed", "9", "--loris-seconds",
loris_str.c_str(), "--throttle-bps", "131072", static_cast<char *>(nullptr));
::_exit(127);
}
::close(pipefd[1]);
// Read the port line the server prints to stdout.
std::string line;
char c = 0;
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
while (std::chrono::steady_clock::now() < deadline) {
ssize_t r = ::read(pipefd[0], &c, 1);
if (r == 1) {
if (c == '\n')
break;
line += c;
} else if (r == 0) {
break;
} else if (errno != EINTR) {
break;
}
}
::close(pipefd[0]);
if (!line.empty())
port_ = std::atoi(line.c_str());
// Give the listener a moment to accept.
std::this_thread::sleep_for(std::chrono::milliseconds(150));
}
~TestServer() {
if (pid_ > 0) {
::kill(pid_, SIGTERM);
int status = 0;
for (int i = 0; i < 50; ++i) {
if (::waitpid(pid_, &status, WNOHANG) == pid_)
return;
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
::kill(pid_, SIGKILL);
::waitpid(pid_, &status, 0);
}
}
TestServer(const TestServer &) = delete;
TestServer &operator=(const TestServer &) = delete;
[[nodiscard]] bool available() const { return port_ > 0; }
[[nodiscard]] int port() const { return port_; }
[[nodiscard]] std::string url(const std::string &path) const {
return "http://127.0.0.1:" + std::to_string(port_) + path;
}
private:
pid_t pid_ = -1;
int port_ = 0;
};
} // namespace vdm::testing
#endif // VDM_TESTS_NET_TESTSERVER_FIXTURE_HPP