Fix Docker state drift: add 5s background state poller and make mutating operations idempotent (404/304 as success)
Some checks failed
Build & Upload tux-dock / build (push) Has been cancelled
Some checks failed
Build & Upload tux-dock / build (push) Has been cancelled
This commit is contained in:
parent
c3419524e5
commit
23a461c584
10 changed files with 465 additions and 32 deletions
136
tests/test_docker_manager_idempotent.cpp
Normal file
136
tests/test_docker_manager_idempotent.cpp
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
#include "docker_manager.hpp"
|
||||
#include "docker_engine_client.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class MockEngineClient : public DockerEngineClient {
|
||||
public:
|
||||
mutable std::string last_method;
|
||||
mutable std::string last_path;
|
||||
EngineResponse response;
|
||||
|
||||
EngineResponse request(const std::string& method,
|
||||
const std::string& path,
|
||||
const std::string& /*body*/ = {},
|
||||
int /*timeout_seconds*/ = 10) const override {
|
||||
last_method = method;
|
||||
last_path = path;
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
static std::unique_ptr<MockEngineClient> make_engine(const EngineResponse& response) {
|
||||
auto engine = std::make_unique<MockEngineClient>();
|
||||
engine->response = response;
|
||||
return engine;
|
||||
}
|
||||
|
||||
void test_start_detached() {
|
||||
{
|
||||
auto engine = make_engine(EngineResponse{204, {}, {}});
|
||||
auto* raw = engine.get();
|
||||
DockerManager manager(std::move(engine));
|
||||
std::string message;
|
||||
assert(manager.startDetached("abc123", message));
|
||||
assert(message == "Container started in detached mode.");
|
||||
assert(raw->last_method == "POST");
|
||||
assert(raw->last_path == "/containers/abc123/start");
|
||||
}
|
||||
{
|
||||
auto engine = make_engine(EngineResponse{304, {}, {}});
|
||||
DockerManager manager(std::move(engine));
|
||||
std::string message;
|
||||
assert(manager.startDetached("abc123", message));
|
||||
assert(message == "Container already started.");
|
||||
}
|
||||
{
|
||||
auto engine = make_engine(EngineResponse{500, {}, "engine error"});
|
||||
DockerManager manager(std::move(engine));
|
||||
std::string message;
|
||||
assert(!manager.startDetached("abc123", message));
|
||||
assert(message == "engine error");
|
||||
}
|
||||
}
|
||||
|
||||
void test_remove_container() {
|
||||
{
|
||||
auto engine = make_engine(EngineResponse{204, {}, {}});
|
||||
DockerManager manager(std::move(engine));
|
||||
std::string message;
|
||||
assert(manager.removeContainer("abc123", message));
|
||||
assert(message == "Container removed.");
|
||||
}
|
||||
{
|
||||
auto engine = make_engine(EngineResponse{404, {}, "No such container"});
|
||||
DockerManager manager(std::move(engine));
|
||||
std::string message;
|
||||
assert(manager.removeContainer("abc123", message));
|
||||
assert(message == "Container already removed.");
|
||||
}
|
||||
{
|
||||
auto engine = make_engine(EngineResponse{409, {}, "conflict"});
|
||||
DockerManager manager(std::move(engine));
|
||||
std::string message;
|
||||
assert(!manager.removeContainer("abc123", message));
|
||||
assert(message == "conflict");
|
||||
}
|
||||
}
|
||||
|
||||
void test_delete_image() {
|
||||
{
|
||||
auto engine = make_engine(EngineResponse{200, {}, {}});
|
||||
DockerManager manager(std::move(engine));
|
||||
std::string message;
|
||||
assert(manager.deleteImage("sha256:abc", message));
|
||||
assert(message == "Image deleted.");
|
||||
}
|
||||
{
|
||||
auto engine = make_engine(EngineResponse{404, {}, "No such image"});
|
||||
DockerManager manager(std::move(engine));
|
||||
std::string message;
|
||||
assert(manager.deleteImage("sha256:abc", message));
|
||||
assert(message == "Image already deleted.");
|
||||
}
|
||||
{
|
||||
auto engine = make_engine(EngineResponse{409, {}, "image in use"});
|
||||
DockerManager manager(std::move(engine));
|
||||
std::string message;
|
||||
assert(!manager.deleteImage("sha256:abc", message));
|
||||
assert(message == "image in use");
|
||||
}
|
||||
}
|
||||
|
||||
void test_stop_container() {
|
||||
{
|
||||
auto engine = make_engine(EngineResponse{204, {}, {}});
|
||||
DockerManager manager(std::move(engine));
|
||||
std::string message;
|
||||
assert(manager.stopContainer("abc123", message, 10));
|
||||
assert(message == "Container stopped.");
|
||||
}
|
||||
{
|
||||
auto engine = make_engine(EngineResponse{304, {}, {}});
|
||||
DockerManager manager(std::move(engine));
|
||||
std::string message;
|
||||
assert(manager.stopContainer("abc123", message, 10));
|
||||
assert(message == "Container stopped.");
|
||||
}
|
||||
{
|
||||
auto engine = make_engine(EngineResponse{404, {}, "No such container"});
|
||||
DockerManager manager(std::move(engine));
|
||||
std::string message;
|
||||
assert(manager.stopContainer("abc123", message, 10));
|
||||
assert(message == "Container stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_start_detached();
|
||||
test_remove_container();
|
||||
test_delete_image();
|
||||
test_stop_container();
|
||||
std::cout << "Docker manager idempotent operation tests passed\n";
|
||||
}
|
||||
121
tests/test_docker_state_poller.cpp
Normal file
121
tests/test_docker_state_poller.cpp
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
#include "docker_state_poller.hpp"
|
||||
#include "docker_manager.hpp"
|
||||
#include "docker_engine_client.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
class FakeEngineClient : public DockerEngineClient {
|
||||
public:
|
||||
EngineResponse container_response;
|
||||
EngineResponse image_response;
|
||||
|
||||
EngineResponse request(const std::string& method,
|
||||
const std::string& path,
|
||||
const std::string& /*body*/ = {},
|
||||
int /*timeout_seconds*/ = 10) const override {
|
||||
(void)method;
|
||||
if (path == "/containers/json?all=true") return container_response;
|
||||
if (path == "/images/json") return image_response;
|
||||
return EngineResponse{};
|
||||
}
|
||||
};
|
||||
|
||||
struct CallbackState {
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
int count = 0;
|
||||
std::size_t container_count = 0;
|
||||
std::size_t image_count = 0;
|
||||
bool last_containers_ok = false;
|
||||
bool last_images_ok = false;
|
||||
|
||||
bool wait_for_count(int n, std::chrono::milliseconds timeout) {
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
return cv.wait_for(lock, timeout, [this, n] { return count >= n; });
|
||||
}
|
||||
};
|
||||
|
||||
static std::unique_ptr<DockerManager> make_manager(const std::string& containers_json,
|
||||
const std::string& images_json) {
|
||||
auto engine = std::make_unique<FakeEngineClient>();
|
||||
engine->container_response = EngineResponse{200, containers_json, {}};
|
||||
engine->image_response = EngineResponse{200, images_json, {}};
|
||||
return std::make_unique<DockerManager>(std::move(engine));
|
||||
}
|
||||
|
||||
void test_trigger_now() {
|
||||
auto manager = make_manager(
|
||||
R"([{"Id":"c1","Names":["/web"],"State":"running","Ports":[]}])",
|
||||
"[]");
|
||||
|
||||
CallbackState state;
|
||||
DockerStatePoller poller;
|
||||
poller.start(
|
||||
*manager,
|
||||
[](std::function<void()> fn) { fn(); },
|
||||
[&state](auto containers, auto images) {
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
++state.count;
|
||||
state.container_count = containers.items.size();
|
||||
state.image_count = images.items.size();
|
||||
state.last_containers_ok = containers.ok();
|
||||
state.last_images_ok = images.ok();
|
||||
state.cv.notify_all();
|
||||
});
|
||||
|
||||
poller.trigger_now();
|
||||
assert(state.wait_for_count(1, std::chrono::seconds(2)));
|
||||
assert(state.container_count == 1);
|
||||
assert(state.image_count == 0);
|
||||
assert(state.last_containers_ok);
|
||||
assert(state.last_images_ok);
|
||||
|
||||
poller.stop();
|
||||
}
|
||||
|
||||
void test_periodic_polling() {
|
||||
auto manager = make_manager(
|
||||
R"([{"Id":"c1","Names":["/web"],"State":"running","Ports":[]}])",
|
||||
"[]");
|
||||
|
||||
CallbackState state;
|
||||
DockerStatePoller poller;
|
||||
poller.start(
|
||||
*manager,
|
||||
[](std::function<void()> fn) { fn(); },
|
||||
[&state](auto containers, auto images) {
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
++state.count;
|
||||
state.container_count = containers.items.size();
|
||||
state.image_count = images.items.size();
|
||||
state.cv.notify_all();
|
||||
});
|
||||
|
||||
assert(state.wait_for_count(2, std::chrono::seconds(12)));
|
||||
assert(state.container_count == 1);
|
||||
assert(state.image_count == 0);
|
||||
|
||||
poller.stop();
|
||||
}
|
||||
|
||||
void test_stop_does_not_hang() {
|
||||
auto manager = make_manager("[]", "[]");
|
||||
DockerStatePoller poller;
|
||||
poller.start(*manager, [](std::function<void()>) {}, [](auto, auto) {});
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
poller.stop();
|
||||
auto elapsed = std::chrono::steady_clock::now() - start;
|
||||
assert(elapsed < std::chrono::seconds(1));
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_trigger_now();
|
||||
test_periodic_polling();
|
||||
test_stop_does_not_hang();
|
||||
std::cout << "Docker state poller tests passed\n";
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue