Files
khodowany 9fd5abd075
ci / checks (push) Successful in 1h16m10s
Rhythm Selection & Cleanup
2026-09-26 02:50:22 -05:00

906 lines
35 KiB
C++

#include <boost/test/unit_test.hpp>
#include <algorithm>
#include <array>
#include <cstring>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#include <lwip/tcp.h>
#include <pico/cyw43_arch.h>
#include <vamp_pico/http_client.hpp>
#include <vamp_pico/input.hpp>
#include <vamp_pico/artwork.hpp>
namespace {
auto check_command(std::optional<vamp_protocol::page_command> command, vamp_protocol::page_command expected) -> void {
BOOST_REQUIRE(command.has_value());
BOOST_CHECK(*command == expected);
}
auto take_command(vamp_pico::request_scheduler &scheduler, std::uint64_t now_ms, vamp_protocol::page_command expected) -> void {
auto request = scheduler.next(now_ms);
BOOST_REQUIRE(request.has_value());
BOOST_CHECK(request->kind == vamp_pico::request_kind::COMMAND);
BOOST_CHECK(request->command == expected);
}
}
BOOST_AUTO_TEST_CASE(pico_gesture_single_tap_waits_for_double_tap_deadline) {
auto gesture = vamp_protocol::gesture_state{};
BOOST_CHECK(!gesture.update(false, 0U));
BOOST_CHECK(!gesture.update(true, 10U));
BOOST_CHECK(!gesture.update(false, 50U));
BOOST_CHECK(!gesture.update(false, 270U));
check_command(gesture.update(false, 271U), vamp_protocol::page_command::NEXT);
BOOST_CHECK(!gesture.update(false, 900U));
}
BOOST_AUTO_TEST_CASE(pico_gesture_double_tap_includes_deadline_and_has_no_trailing_next) {
auto gesture = vamp_protocol::gesture_state{};
BOOST_CHECK(!gesture.update(true, 0U));
BOOST_CHECK(!gesture.update(false, 40U));
BOOST_CHECK(!gesture.update(true, 180U));
check_command(gesture.update(false, 260U), vamp_protocol::page_command::PREVIOUS);
BOOST_CHECK(!gesture.update(false, 1000U));
}
BOOST_AUTO_TEST_CASE(pico_gesture_late_second_release_preserves_both_single_taps) {
auto gesture = vamp_protocol::gesture_state{};
BOOST_CHECK(!gesture.update(true, 0U));
BOOST_CHECK(!gesture.update(false, 40U));
BOOST_CHECK(!gesture.update(true, 180U));
check_command(gesture.update(false, 261U), vamp_protocol::page_command::NEXT);
BOOST_CHECK(!gesture.update(false, 481U));
check_command(gesture.update(false, 482U), vamp_protocol::page_command::NEXT);
}
BOOST_AUTO_TEST_CASE(pico_gesture_expired_tap_is_emitted_when_next_press_starts) {
auto gesture = vamp_protocol::gesture_state{};
BOOST_CHECK(!gesture.update(true, 0U));
BOOST_CHECK(!gesture.update(false, 40U));
check_command(gesture.update(true, 261U), vamp_protocol::page_command::NEXT);
BOOST_CHECK(!gesture.update(false, 300U));
check_command(gesture.update(false, 521U), vamp_protocol::page_command::NEXT);
}
BOOST_AUTO_TEST_CASE(pico_gesture_hold_emits_once_and_suppresses_release) {
auto gesture = vamp_protocol::gesture_state{};
BOOST_CHECK(!gesture.update(true, 50U));
BOOST_CHECK(!gesture.update(true, 749U));
check_command(gesture.update(true, 750U), vamp_protocol::page_command::SELECT);
BOOST_CHECK(!gesture.update(true, 2000U));
BOOST_CHECK(!gesture.update(false, 2010U));
BOOST_CHECK(!gesture.update(false, 2300U));
BOOST_CHECK(!gesture.update(true, 2400U));
BOOST_CHECK(!gesture.update(false, 2450U));
check_command(gesture.update(false, 2671U), vamp_protocol::page_command::NEXT);
}
BOOST_AUTO_TEST_CASE(pico_gesture_hold_consumes_pending_tap_even_if_release_is_next_sample) {
auto gesture = vamp_protocol::gesture_state{};
BOOST_CHECK(!gesture.update(true, 0U));
BOOST_CHECK(!gesture.update(false, 40U));
BOOST_CHECK(!gesture.update(true, 180U));
BOOST_CHECK(!gesture.update(true, 879U));
check_command(gesture.update(false, 880U), vamp_protocol::page_command::SELECT);
BOOST_CHECK(!gesture.update(false, 1200U));
}
BOOST_AUTO_TEST_CASE(pico_scheduler_initial_poll_and_one_second_from_completion) {
auto scheduler = vamp_pico::request_scheduler{};
auto request = scheduler.next(500U);
BOOST_REQUIRE(request.has_value());
BOOST_CHECK(request->kind == vamp_pico::request_kind::DISPLAY);
BOOST_CHECK(!scheduler.next(100000U));
scheduler.complete(100000U);
BOOST_CHECK(!scheduler.next(100999U));
request = scheduler.next(101000U);
BOOST_REQUIRE(request.has_value());
BOOST_CHECK(request->kind == vamp_pico::request_kind::DISPLAY);
BOOST_CHECK(!scheduler.next(200000U));
}
BOOST_AUTO_TEST_CASE(pico_scheduler_commands_take_priority_and_never_retry) {
auto scheduler = vamp_pico::request_scheduler{};
BOOST_CHECK(scheduler.enqueue(vamp_protocol::page_command::SELECT) == vamp_pico::enqueue_result::QUEUED);
take_command(scheduler, 0U, vamp_protocol::page_command::SELECT);
BOOST_CHECK(!scheduler.next(700U));
scheduler.complete(700U);
BOOST_TEST(scheduler.queued() == 0U);
BOOST_CHECK(!scheduler.next(1699U));
BOOST_CHECK(scheduler.enqueue(vamp_protocol::page_command::NEXT) == vamp_pico::enqueue_result::QUEUED);
take_command(scheduler, 1700U, vamp_protocol::page_command::NEXT);
scheduler.complete(1900U);
BOOST_CHECK(!scheduler.next(2899U));
auto request = scheduler.next(2900U);
BOOST_REQUIRE(request.has_value());
BOOST_CHECK(request->kind == vamp_pico::request_kind::DISPLAY);
}
BOOST_AUTO_TEST_CASE(pico_scheduler_fifo_overflow_is_explicit_and_does_not_replace_commands) {
auto scheduler = vamp_pico::request_scheduler{};
BOOST_REQUIRE(scheduler.next(0U));
constexpr auto commands = std::array{
vamp_protocol::page_command::NEXT,
vamp_protocol::page_command::PREVIOUS,
vamp_protocol::page_command::SELECT,
};
for (auto index = std::size_t{}; index < 16U; ++index) {
BOOST_CHECK(scheduler.enqueue(commands[index % commands.size()]) == vamp_pico::enqueue_result::QUEUED);
}
BOOST_TEST(scheduler.queued() == 16U);
BOOST_CHECK(scheduler.enqueue(vamp_protocol::page_command::SELECT) == vamp_pico::enqueue_result::BUSY);
BOOST_TEST(scheduler.queued() == 16U);
BOOST_CHECK(!scheduler.next(1000U));
scheduler.complete(1000U);
for (auto index = std::size_t{}; index < 8U; ++index) {
take_command(scheduler, 1000U + index, commands[index % commands.size()]);
scheduler.complete(1000U + index);
}
for (auto index = std::size_t{16}; index < 24U; ++index) {
BOOST_CHECK(scheduler.enqueue(commands[index % commands.size()]) == vamp_pico::enqueue_result::QUEUED);
}
BOOST_TEST(scheduler.queued() == 16U);
BOOST_CHECK(scheduler.enqueue(vamp_protocol::page_command::NEXT) == vamp_pico::enqueue_result::BUSY);
for (auto index = std::size_t{8}; index < 24U; ++index) {
take_command(scheduler, 1000U + index, commands[index % commands.size()]);
scheduler.complete(1000U + index);
}
BOOST_TEST(scheduler.queued() == 0U);
BOOST_CHECK(!scheduler.next(2022U));
auto request = scheduler.next(2023U);
BOOST_REQUIRE(request.has_value());
BOOST_CHECK(request->kind == vamp_pico::request_kind::DISPLAY);
}
BOOST_AUTO_TEST_CASE(pico_scheduler_duplicate_commands_are_not_coalesced) {
auto scheduler = vamp_pico::request_scheduler{};
for (auto index = 0U; index < 16U; ++index) {
BOOST_CHECK(scheduler.enqueue(vamp_protocol::page_command::NEXT) == vamp_pico::enqueue_result::QUEUED);
}
for (auto index = 0U; index < 16U; ++index) {
take_command(scheduler, index, vamp_protocol::page_command::NEXT);
BOOST_CHECK(!scheduler.next(index));
scheduler.complete(index);
}
BOOST_TEST(scheduler.queued() == 0U);
BOOST_CHECK(!scheduler.next(16U));
}
BOOST_AUTO_TEST_CASE(pico_scheduler_extra_completion_does_not_delay_next_poll) {
auto scheduler = vamp_pico::request_scheduler{};
scheduler.complete(0U);
BOOST_REQUIRE(scheduler.next(0U));
scheduler.complete(500U);
scheduler.complete(1200U);
BOOST_CHECK(!scheduler.next(1499U));
BOOST_REQUIRE(scheduler.next(1500U));
}
BOOST_AUTO_TEST_CASE(pico_gestures_are_sampled_and_queued_during_slow_request) {
auto gesture = vamp_protocol::gesture_state{};
auto scheduler = vamp_pico::request_scheduler{};
BOOST_REQUIRE(scheduler.next(0U));
for (auto now_ms = std::uint64_t{}; now_ms < 1504U; now_ms += 8U) {
auto touching = (now_ms >= 16U && now_ms < 48U) || (now_ms >= 120U && now_ms < 152U) ||
(now_ms >= 240U && now_ms < 1008U) || (now_ms >= 1104U && now_ms < 1144U);
if (auto command = gesture.update(touching, now_ms); command) {
BOOST_CHECK(scheduler.enqueue(*command) == vamp_pico::enqueue_result::QUEUED);
}
BOOST_CHECK(!scheduler.next(now_ms));
}
BOOST_TEST(scheduler.queued() == 3U);
scheduler.complete(1504U);
take_command(scheduler, 1504U, vamp_protocol::page_command::PREVIOUS);
scheduler.complete(1505U);
take_command(scheduler, 1505U, vamp_protocol::page_command::SELECT);
scheduler.complete(1506U);
take_command(scheduler, 1506U, vamp_protocol::page_command::NEXT);
scheduler.complete(1507U);
BOOST_CHECK(!scheduler.next(2506U));
auto request = scheduler.next(2507U);
BOOST_REQUIRE(request.has_value());
BOOST_CHECK(request->kind == vamp_pico::request_kind::DISPLAY);
}
BOOST_AUTO_TEST_CASE(pico_timing_survives_32_bit_millisecond_boundary) {
constexpr auto start = std::uint64_t{0xffffffffU} - 100U;
auto gesture = vamp_protocol::gesture_state{};
BOOST_CHECK(!gesture.update(true, start));
BOOST_CHECK(!gesture.update(false, start + 40U));
check_command(gesture.update(false, start + 261U), vamp_protocol::page_command::NEXT);
auto scheduler = vamp_pico::request_scheduler{};
BOOST_REQUIRE(scheduler.next(start));
scheduler.complete(start + 500U);
BOOST_CHECK(!scheduler.next(start + 1499U));
BOOST_REQUIRE(scheduler.next(start + 1500U));
}
BOOST_AUTO_TEST_CASE(pico_http_client_has_stable_ownership) {
BOOST_TEST(!std::is_copy_constructible_v<vamp_pico::http_client>);
BOOST_TEST(!std::is_copy_assignable_v<vamp_pico::http_client>);
BOOST_TEST(!std::is_move_constructible_v<vamp_pico::http_client>);
BOOST_TEST(!std::is_move_assignable_v<vamp_pico::http_client>);
}
namespace {
auto title_bytes() -> std::string {
auto bytes = std::string(vamp_protocol::title_image_size, '\0');
bytes.replace(0U, 8U, "VPTI\x28\x01\x40\x00", 8U);
bytes[8] = static_cast<char>(0xa3);
return bytes;
}
auto title_request(char digit = 'a') -> vamp_pico::remote_request {
auto request = vamp_pico::remote_request{vamp_pico::request_kind::TITLE};
request.title_id.fill(digit);
request.title_id.back() = 0;
return request;
}
auto title_response() -> std::string {
return "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: " +
std::to_string(vamp_protocol::title_image_size) + "\r\nConnection: close\r\n\r\n" + title_bytes();
}
}
BOOST_AUTO_TEST_CASE(pico_title_identity_parser_is_strict_and_transactional) {
auto state = vamp_pico::remote_display_state{};
auto identifier = std::string(64U, 'a');
BOOST_REQUIRE(vamp_pico::parse_remote_display_body("active=1\ntitle_image=" + identifier + "\npage=2\npages=3\n", state));
BOOST_TEST(std::string{state.title_image.data()} == identifier);
for (auto invalid : {identifier + "a", std::string(64U, 'A'), std::string{"../../title"}, std::string(63U, 'a')}) {
BOOST_CHECK(!vamp_pico::parse_remote_display_body("title_image=" + invalid + "\n", state));
BOOST_TEST(std::string{state.title_image.data()} == identifier);
BOOST_TEST(state.page == 2U);
}
BOOST_REQUIRE(vamp_pico::parse_remote_display_body("active=0\ntitle_image=\n", state));
BOOST_TEST(state.title_image[0] == 0);
}
BOOST_AUTO_TEST_CASE(pico_title_cache_discards_old_selection_and_validates_before_copy) {
auto cache = vamp_pico::title_cache{};
auto first = std::string(64U, 'a');
auto second = std::string(64U, 'b');
auto image = title_bytes();
BOOST_CHECK(cache.select(first));
BOOST_CHECK(!cache.ready());
BOOST_REQUIRE(cache.accept(first, image));
image[8] = 0;
BOOST_TEST(cache.image().at(0U, 0U) == 10U);
BOOST_TEST(cache.image().at(1U, 0U) == 3U);
BOOST_TEST(cache.image().at(296U, 0U) == 0U);
BOOST_CHECK(!cache.select(first));
BOOST_CHECK(cache.ready());
BOOST_CHECK(cache.select(second));
BOOST_CHECK(!cache.ready());
BOOST_CHECK(!cache.accept(first, image));
BOOST_CHECK(!cache.accept(second, image.substr(1U)));
image[4] = 0;
BOOST_CHECK(!cache.accept(second, image));
BOOST_CHECK(cache.accept(second, title_bytes()));
BOOST_CHECK(cache.select(""));
BOOST_CHECK(!cache.ready());
BOOST_CHECK(!cache.accept(second, title_bytes()));
}
BOOST_AUTO_TEST_CASE(pico_title_failure_is_visible_until_success_or_selection_change) {
auto cache = vamp_pico::title_cache{};
auto first = std::string(64U, 'a');
auto second = std::string(64U, 'b');
cache.select(first);
BOOST_CHECK(cache.fail(first));
BOOST_CHECK(cache.failed());
BOOST_CHECK(!cache.ready());
BOOST_CHECK(!cache.select(first));
BOOST_CHECK(cache.failed());
BOOST_REQUIRE(cache.accept(first, title_bytes()));
BOOST_CHECK(!cache.failed());
BOOST_CHECK(cache.ready());
cache.select(second);
BOOST_CHECK(!cache.failed());
BOOST_CHECK(!cache.fail(first));
BOOST_CHECK(cache.fail(second));
cache.select("");
BOOST_CHECK(!cache.failed());
BOOST_CHECK(cache.fail(""));
BOOST_CHECK(cache.failed());
}
BOOST_AUTO_TEST_CASE(pico_page_counters_preserve_catalog_maximum_without_truncation) {
BOOST_TEST(std::string{vamp_pico::counter_text(10000U, 10000U).data()} == "10000/10000");
BOOST_TEST(std::string{vamp_pico::counter_text(1U, 10000U).data()} == "1/10000");
BOOST_TEST(std::string{vamp_pico::counter_text(0U, 10000U).data()}.empty());
BOOST_TEST(std::string{vamp_pico::counter_text(10001U, 10001U).data()}.empty());
}
BOOST_AUTO_TEST_CASE(pico_title_requests_follow_commands_and_do_not_postpone_status) {
auto scheduler = vamp_pico::request_scheduler{};
BOOST_REQUIRE(scheduler.next(0U));
scheduler.complete(10U);
scheduler.want_title(std::string(64U, 'a'));
auto title = scheduler.next(11U);
BOOST_REQUIRE(title);
BOOST_CHECK(title->kind == vamp_pico::request_kind::TITLE);
BOOST_CHECK(!scheduler.next(100U));
BOOST_CHECK(scheduler.enqueue(vamp_protocol::page_command::NEXT) == vamp_pico::enqueue_result::QUEUED);
scheduler.cancel_title();
take_command(scheduler, 100U, vamp_protocol::page_command::NEXT);
scheduler.complete(110U);
scheduler.want_title(std::string(64U, 'b'));
title = scheduler.next(111U);
BOOST_REQUIRE(title);
BOOST_CHECK(title->kind == vamp_pico::request_kind::TITLE);
BOOST_TEST(std::string{title->title_id.data()} == std::string(64U, 'b'));
scheduler.complete(1600U);
auto status = scheduler.next(1600U);
BOOST_REQUIRE(status);
BOOST_CHECK(status->kind == vamp_pico::request_kind::DISPLAY);
}
namespace {
struct fake_network {
std::array<tcp_pcb, 64> pcbs{};
std::size_t allocated{};
std::size_t writes{};
std::size_t closes{};
std::size_t aborts{};
std::size_t acknowledged{};
std::size_t freed{};
std::size_t lock_depth{};
std::bool_t address_valid{true};
std::bool_t allocate_failure{};
std::int8_t connect_error{ERR_OK};
std::int8_t write_error{ERR_OK};
std::int8_t output_error{ERR_OK};
std::int8_t close_error{ERR_OK};
std::string written{};
std::char_t const *written_data{};
std::uint16_t written_size{};
};
auto network = fake_network{};
auto check_locked(tcp_pcb const *pcb = nullptr) -> void {
BOOST_CHECK(network.lock_depth > 0U);
if (pcb != nullptr) {
BOOST_CHECK(pcb->alive);
}
}
auto check_detached(tcp_pcb const *pcb) -> void {
BOOST_CHECK(pcb->argument == nullptr);
BOOST_CHECK(pcb->receive == nullptr);
BOOST_CHECK(pcb->sent == nullptr);
BOOST_CHECK(pcb->poll == nullptr);
BOOST_CHECK(pcb->error == nullptr);
}
struct fake_lwip_fixture {
fake_lwip_fixture() {
network = fake_network{};
}
~fake_lwip_fixture() {
BOOST_CHECK(network.lock_depth == 0U);
for (auto index = std::size_t{}; index < network.allocated; ++index) {
BOOST_CHECK(!network.pcbs[index].alive);
}
}
};
struct callback_lock {
callback_lock() {
cyw43_arch_lwip_begin();
}
~callback_lock() {
cyw43_arch_lwip_end();
}
};
auto deliver_connected(tcp_pcb &pcb, std::int8_t error = ERR_OK) -> std::int8_t {
auto lock = callback_lock{};
if (!pcb.alive || pcb.connected == nullptr) {
return ERR_OK;
}
return pcb.connected(pcb.argument, &pcb, error);
}
auto deliver_data(tcp_pcb &pcb, std::string_view data, std::int8_t error = ERR_OK) -> std::int8_t {
auto lock = callback_lock{};
if (!pcb.alive || pcb.receive == nullptr) {
return ERR_OK;
}
auto half = static_cast<std::uint16_t>(data.size() / 2U);
auto tail_size = static_cast<std::uint16_t>(data.size() - half);
auto tail = pbuf{nullptr, data.data() + half, tail_size, tail_size};
auto head = pbuf{&tail, data.data(), static_cast<std::uint16_t>(data.size()), half};
auto result = pcb.receive(pcb.argument, &pcb, &head, error);
BOOST_CHECK(head.freed);
BOOST_CHECK(tail.freed);
return result;
}
auto deliver_eof(tcp_pcb &pcb) -> std::int8_t {
auto lock = callback_lock{};
if (!pcb.alive || pcb.receive == nullptr) {
return ERR_OK;
}
return pcb.receive(pcb.argument, &pcb, nullptr, ERR_OK);
}
auto deliver_error(tcp_pcb &pcb) -> void {
auto lock = callback_lock{};
if (!pcb.alive || pcb.error == nullptr) {
return;
}
auto callback = pcb.error;
auto argument = pcb.argument;
pcb.alive = false;
callback(argument, ERR_RST);
}
constexpr auto display_response = std::string_view{
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n"
"active=1\ntitle=Owned response\npart=Piano\nmode=score\npage=2\npages=3\n"};
auto require_failure(vamp_pico::http_client &client, std::uint64_t now_ms) -> void {
BOOST_CHECK(client.poll(now_ms) == vamp_pico::http_status::READY);
BOOST_CHECK(client.start({}, now_ms) == vamp_pico::http_start_result::BUSY);
auto result = client.take_result();
BOOST_REQUIRE(result.has_value());
BOOST_CHECK(!result->success);
BOOST_CHECK(!client.take_result());
BOOST_CHECK(client.poll(now_ms) == vamp_pico::http_status::IDLE);
}
}
auto cyw43_arch_lwip_begin() -> void {
++network.lock_depth;
}
auto cyw43_arch_lwip_end() -> void {
BOOST_CHECK(network.lock_depth > 0U);
--network.lock_depth;
}
auto ipaddr_aton(std::char_t const *, ip_addr_t *address) -> std::int32_t {
check_locked();
address->addr = 1U;
return network.address_valid ? 1 : 0;
}
auto tcp_new_ip_type(std::uint8_t type) -> tcp_pcb * {
check_locked();
BOOST_CHECK(type == IPADDR_TYPE_V4);
if (network.allocate_failure) {
return nullptr;
}
BOOST_REQUIRE(network.allocated < network.pcbs.size());
auto &pcb = network.pcbs[network.allocated++];
pcb.alive = true;
return &pcb;
}
auto tcp_arg(tcp_pcb *pcb, void *argument) -> void {
check_locked(pcb);
pcb->argument = argument;
}
auto tcp_recv(tcp_pcb *pcb, std::int8_t (*callback)(void *, tcp_pcb *, pbuf *, std::int8_t)) -> void {
check_locked(pcb);
pcb->receive = callback;
}
auto tcp_sent(tcp_pcb *pcb, std::int8_t (*callback)(void *, tcp_pcb *, std::uint16_t)) -> void {
check_locked(pcb);
pcb->sent = callback;
}
auto tcp_poll(tcp_pcb *pcb, std::int8_t (*callback)(void *, tcp_pcb *), std::uint8_t) -> void {
check_locked(pcb);
pcb->poll = callback;
}
auto tcp_err(tcp_pcb *pcb, void (*callback)(void *, std::int8_t)) -> void {
check_locked(pcb);
pcb->error = callback;
}
auto tcp_connect(tcp_pcb *pcb, ip_addr_t const *, std::uint16_t, std::int8_t (*callback)(void *, tcp_pcb *, std::int8_t)) -> std::int8_t {
check_locked(pcb);
pcb->connected = callback;
return network.connect_error;
}
auto tcp_write(tcp_pcb *pcb, void const *data, std::uint16_t size, std::uint8_t flags) -> std::int8_t {
check_locked(pcb);
BOOST_CHECK(flags == TCP_WRITE_FLAG_COPY);
network.written_data = static_cast<std::char_t const *>(data);
network.written_size = size;
network.written.assign(network.written_data, size);
++network.writes;
return network.write_error;
}
auto tcp_output(tcp_pcb *pcb) -> std::int8_t {
check_locked(pcb);
return network.output_error;
}
auto tcp_recved(tcp_pcb *pcb, std::uint16_t size) -> void {
check_locked(pcb);
network.acknowledged += size;
}
auto tcp_close(tcp_pcb *pcb) -> std::int8_t {
check_locked(pcb);
check_detached(pcb);
++network.closes;
if (network.close_error == ERR_OK) {
pcb->alive = false;
}
return network.close_error;
}
auto tcp_abort(tcp_pcb *pcb) -> void {
check_locked(pcb);
check_detached(pcb);
pcb->alive = false;
++network.aborts;
}
auto pbuf_copy_partial(pbuf const *buffer, void *destination, std::uint16_t length, std::uint16_t offset) -> std::uint16_t {
check_locked();
auto copied = std::uint16_t{};
auto *output = static_cast<std::char_t *>(destination);
while (buffer != nullptr && copied < length) {
if (offset < buffer->len) {
auto size = std::min<std::uint16_t>(length - copied, buffer->len - offset);
std::memcpy(output + copied, static_cast<std::char_t const *>(buffer->payload) + offset, size);
copied += size;
offset = 0U;
} else {
offset -= buffer->len;
}
buffer = buffer->next;
}
return copied;
}
auto pbuf_free(pbuf *buffer) -> std::uint8_t {
check_locked();
auto count = std::uint8_t{};
while (buffer != nullptr) {
BOOST_CHECK(!buffer->freed);
buffer->freed = true;
++count;
++network.freed;
buffer = buffer->next;
}
return count;
}
BOOST_FIXTURE_TEST_CASE(pico_http_owns_fragmented_buffers_and_unconsumed_result, fake_lwip_fixture) {
auto client = vamp_pico::http_client{};
BOOST_CHECK(client.poll(0U) == vamp_pico::http_status::IDLE);
BOOST_CHECK(!client.take_result());
auto request = vamp_pico::remote_request{vamp_pico::request_kind::COMMAND, vamp_protocol::page_command::SELECT};
BOOST_CHECK(client.start(request, 0U) == vamp_pico::http_start_result::STARTED);
request.command = vamp_protocol::page_command::PREVIOUS;
auto &pcb = network.pcbs[0];
BOOST_CHECK(client.poll(10U) == vamp_pico::http_status::IN_FLIGHT);
BOOST_TEST(network.writes == 0U);
BOOST_CHECK(deliver_connected(pcb) == ERR_OK);
BOOST_TEST(network.writes == 1U);
BOOST_CHECK(network.written.find("POST /api/remote/select HTTP/1.1\r\n") == 0U);
BOOST_CHECK(network.written.find("Content-Length: 2\r\n") != std::string::npos);
BOOST_CHECK(network.written.find("\r\n\r\n{}") != std::string::npos);
BOOST_CHECK(client.start(request, 20U) == vamp_pico::http_start_result::BUSY);
BOOST_CHECK(std::string_view(network.written_data, network.written_size) == network.written);
auto response = std::string{display_response};
BOOST_CHECK(deliver_data(pcb, std::string_view{response}.substr(0U, 9U)) == ERR_OK);
BOOST_CHECK(deliver_data(pcb, std::string_view{response}.substr(9U, 48U)) == ERR_OK);
BOOST_CHECK(!client.take_result());
BOOST_CHECK(deliver_data(pcb, std::string_view{response}.substr(57U)) == ERR_OK);
std::fill(response.begin(), response.end(), 'x');
BOOST_CHECK(deliver_eof(pcb) == ERR_OK);
check_detached(&pcb);
BOOST_CHECK(client.poll(30U) == vamp_pico::http_status::READY);
BOOST_CHECK(client.start(request, 30U) == vamp_pico::http_start_result::BUSY);
auto result = client.take_result();
BOOST_REQUIRE(result.has_value());
BOOST_CHECK(result->success);
BOOST_CHECK(result->request.command == vamp_protocol::page_command::SELECT);
BOOST_TEST(std::string{result->display.title} == "Owned response");
BOOST_TEST(result->display.page == 2U);
BOOST_TEST(result->display.pages == 3U);
BOOST_TEST(network.acknowledged == display_response.size());
BOOST_CHECK(!client.take_result());
BOOST_CHECK(client.start({}, 40U) == vamp_pico::http_start_result::STARTED);
BOOST_TEST(std::string{result->display.title} == "Owned response");
}
BOOST_FIXTURE_TEST_CASE(pico_http_cancel_detaches_callbacks_before_reuse, fake_lwip_fixture) {
auto client = vamp_pico::http_client{};
for (auto connected : {false, true}) {
BOOST_CHECK(client.start({}, 0U) == vamp_pico::http_start_result::STARTED);
auto &old = network.pcbs[network.allocated - 1U];
if (connected) {
BOOST_CHECK(deliver_connected(old) == ERR_OK);
BOOST_CHECK(deliver_data(old, "HTTP/1.1 200 OK\r\n") == ERR_OK);
}
client.cancel();
check_detached(&old);
BOOST_CHECK(!old.alive);
BOOST_CHECK(!client.take_result());
BOOST_CHECK(client.poll(1000U) == vamp_pico::http_status::IDLE);
BOOST_CHECK(client.start({}, 1000U) == vamp_pico::http_start_result::STARTED);
BOOST_CHECK(deliver_connected(old) == ERR_OK);
BOOST_CHECK(deliver_data(old, display_response) == ERR_OK);
BOOST_CHECK(deliver_eof(old) == ERR_OK);
deliver_error(old);
BOOST_CHECK(client.poll(1001U) == vamp_pico::http_status::IN_FLIGHT);
BOOST_CHECK(!client.take_result());
client.cancel();
client.cancel();
}
}
BOOST_FIXTURE_TEST_CASE(pico_http_timeout_detaches_callbacks_and_never_retries, fake_lwip_fixture) {
auto client = vamp_pico::http_client{};
BOOST_CHECK(client.start({vamp_pico::request_kind::COMMAND, vamp_protocol::page_command::NEXT}, 100U) == vamp_pico::http_start_result::STARTED);
auto &old = network.pcbs[0];
BOOST_CHECK(deliver_connected(old) == ERR_OK);
BOOST_CHECK(deliver_data(old, "HTTP/1.1 200 OK\r\n") == ERR_OK);
BOOST_CHECK(client.poll(799U) == vamp_pico::http_status::IN_FLIGHT);
require_failure(client, 800U);
check_detached(&old);
BOOST_TEST(network.writes == 1U);
BOOST_TEST(network.aborts == 1U);
BOOST_CHECK(client.start({}, 900U) == vamp_pico::http_start_result::STARTED);
BOOST_CHECK(deliver_eof(old) == ERR_OK);
deliver_error(old);
BOOST_CHECK(client.poll(1899U) == vamp_pico::http_status::IN_FLIGHT);
require_failure(client, 1900U);
BOOST_TEST(network.writes == 1U);
BOOST_TEST(network.allocated == 2U);
}
BOOST_FIXTURE_TEST_CASE(pico_http_destructor_aborts_and_detaches, fake_lwip_fixture) {
{
auto client = vamp_pico::http_client{};
BOOST_CHECK(client.start({}, 0U) == vamp_pico::http_start_result::STARTED);
BOOST_CHECK(deliver_connected(network.pcbs[0]) == ERR_OK);
}
check_detached(&network.pcbs[0]);
BOOST_TEST(network.aborts == 1U);
BOOST_CHECK(deliver_data(network.pcbs[0], display_response) == ERR_OK);
BOOST_CHECK(deliver_eof(network.pcbs[0]) == ERR_OK);
deliver_error(network.pcbs[0]);
}
BOOST_AUTO_TEST_CASE(pico_http_immediate_failures_are_consumed_once) {
for (auto failure = 0U; failure < 3U; ++failure) {
auto fixture = fake_lwip_fixture{};
auto client = vamp_pico::http_client{};
network.address_valid = failure != 0U;
network.allocate_failure = failure == 1U;
network.connect_error = failure == 2U ? ERR_MEM : ERR_OK;
BOOST_CHECK(client.start({}, 0U) == vamp_pico::http_start_result::STARTED);
require_failure(client, 0U);
BOOST_TEST(network.writes == 0U);
}
}
BOOST_AUTO_TEST_CASE(pico_http_connected_write_output_and_receive_failures_abort) {
for (auto failure = 0U; failure < 4U; ++failure) {
auto fixture = fake_lwip_fixture{};
auto client = vamp_pico::http_client{};
network.write_error = failure == 1U ? ERR_MEM : ERR_OK;
network.output_error = failure == 2U ? ERR_MEM : ERR_OK;
BOOST_CHECK(client.start({}, 0U) == vamp_pico::http_start_result::STARTED);
auto &pcb = network.pcbs[0];
auto connected = deliver_connected(pcb, failure == 0U ? ERR_RST : ERR_OK);
if (failure == 3U) {
BOOST_CHECK(connected == ERR_OK);
BOOST_CHECK(deliver_data(pcb, display_response, ERR_RST) == ERR_ABRT);
} else {
BOOST_CHECK(connected == ERR_ABRT);
}
require_failure(client, 10U);
check_detached(&pcb);
BOOST_TEST(network.aborts == 1U);
}
}
BOOST_FIXTURE_TEST_CASE(pico_http_error_callback_does_not_touch_freed_pcb, fake_lwip_fixture) {
auto client = vamp_pico::http_client{};
BOOST_CHECK(client.start({}, 0U) == vamp_pico::http_start_result::STARTED);
deliver_error(network.pcbs[0]);
require_failure(client, 1U);
BOOST_TEST(network.aborts == 0U);
BOOST_TEST(network.closes == 0U);
BOOST_CHECK(client.start({}, 2U) == vamp_pico::http_start_result::STARTED);
BOOST_CHECK(deliver_connected(network.pcbs[1]) == ERR_OK);
deliver_error(network.pcbs[1]);
require_failure(client, 3U);
BOOST_TEST(network.aborts == 0U);
}
BOOST_FIXTURE_TEST_CASE(pico_http_close_failure_aborts_with_correct_callback_result, fake_lwip_fixture) {
auto client = vamp_pico::http_client{};
network.close_error = ERR_MEM;
BOOST_CHECK(client.start({}, 0U) == vamp_pico::http_start_result::STARTED);
auto &pcb = network.pcbs[0];
BOOST_CHECK(deliver_connected(pcb) == ERR_OK);
BOOST_CHECK(deliver_data(pcb, display_response) == ERR_OK);
BOOST_CHECK(deliver_eof(pcb) == ERR_ABRT);
auto result = client.take_result();
BOOST_REQUIRE(result.has_value());
BOOST_CHECK(result->success);
BOOST_TEST(network.closes == 1U);
BOOST_TEST(network.aborts == 1U);
}
BOOST_FIXTURE_TEST_CASE(pico_http_malformed_headers_status_and_body_fail, fake_lwip_fixture) {
auto client = vamp_pico::http_client{};
for (auto response : {
"",
"HTTP/1.1 2",
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n",
"HTTP/1.1 500 Error\r\n\r\nactive=1\n",
"HTTP/1.1 2x0 OK\r\n\r\nactive=1\n",
"HTTP/1.1 20x OK\r\n\r\nactive=1\n",
"HTTP/1.1 2000 OK\r\n\r\nactive=1\n",
"HTTP/2.0 200 OK\r\n\r\nactive=1\n",
"HTTP/1.1 200 OK\r\n\r\nactive=1\npage=invalid\n",
}) {
BOOST_CHECK(client.start({}, 0U) == vamp_pico::http_start_result::STARTED);
auto &pcb = network.pcbs[network.allocated - 1U];
BOOST_CHECK(deliver_connected(pcb) == ERR_OK);
BOOST_CHECK(deliver_data(pcb, response) == ERR_OK);
deliver_eof(pcb);
require_failure(client, 1U);
}
}
BOOST_FIXTURE_TEST_CASE(pico_http_http10_and_heartbeat_are_supported, fake_lwip_fixture) {
auto client = vamp_pico::http_client{};
BOOST_CHECK(client.start({}, 0U) == vamp_pico::http_start_result::STARTED);
auto &display = network.pcbs[0];
BOOST_CHECK(deliver_connected(display) == ERR_OK);
BOOST_CHECK(network.written.find("GET /api/remote/display HTTP/1.1\r\n") == 0U);
BOOST_CHECK(deliver_data(display, "HTTP/1.0 200 OK\r\n\r\nactive=1\ntitle=HTTP10\n") == ERR_OK);
BOOST_CHECK(deliver_eof(display) == ERR_OK);
auto result = client.take_result();
BOOST_REQUIRE(result.has_value());
BOOST_CHECK(result->success);
BOOST_TEST(std::string{result->display.title} == "HTTP10");
BOOST_CHECK(client.start({vamp_pico::request_kind::HEARTBEAT}, 10U) == vamp_pico::http_start_result::STARTED);
auto &heartbeat = network.pcbs[1];
BOOST_CHECK(deliver_connected(heartbeat) == ERR_OK);
BOOST_CHECK(network.written.find("POST /api/remote/heartbeat HTTP/1.1\r\n") == 0U);
BOOST_CHECK(deliver_data(heartbeat, "HTTP/1.1 204 No Content\r\n\r\n") == ERR_OK);
BOOST_CHECK(deliver_eof(heartbeat) == ERR_OK);
result = client.take_result();
BOOST_REQUIRE(result.has_value());
BOOST_CHECK(result->success);
}
BOOST_FIXTURE_TEST_CASE(pico_http_oversized_response_is_rejected_without_truncation, fake_lwip_fixture) {
auto client = vamp_pico::http_client{};
BOOST_CHECK(client.start({}, 0U) == vamp_pico::http_start_result::STARTED);
auto &pcb = network.pcbs[0];
BOOST_CHECK(deliver_connected(pcb) == ERR_OK);
auto response = std::string{display_response};
response.resize(1024U, 'x');
BOOST_CHECK(deliver_data(pcb, response) == ERR_OK);
BOOST_CHECK(deliver_data(pcb, "x") == ERR_ABRT);
require_failure(client, 1U);
check_detached(&pcb);
BOOST_TEST(network.acknowledged == 1025U);
BOOST_TEST(network.aborts == 1U);
}
BOOST_FIXTURE_TEST_CASE(pico_http_title_binary_is_fragmented_bounded_and_owned, fake_lwip_fixture) {
auto client = vamp_pico::http_client{};
auto request = title_request();
BOOST_CHECK(client.start(request, 0U) == vamp_pico::http_start_result::STARTED);
auto &pcb = network.pcbs[0];
BOOST_CHECK(deliver_connected(pcb) == ERR_OK);
BOOST_CHECK(network.written.find("POST /api/remote/title HTTP/1.1\r\n") == 0U);
auto marker = network.written.find("\r\n\r\n");
BOOST_REQUIRE(marker != std::string::npos);
BOOST_TEST(network.written.substr(marker + 4U) == "{\"id\":\"" + std::string(64U, 'a') + "\"}");
BOOST_TEST(network.written.size() - marker - 4U == 73U);
auto response = title_response();
for (auto offset = std::size_t{}; offset < response.size(); offset += 137U) {
BOOST_CHECK(deliver_data(pcb, std::string_view{response}.substr(offset, 137U)) == ERR_OK);
}
BOOST_CHECK(deliver_eof(pcb) == ERR_OK);
auto result = client.take_result();
BOOST_REQUIRE(result);
BOOST_REQUIRE(result->success);
BOOST_CHECK(result->request.kind == vamp_pico::request_kind::TITLE);
BOOST_CHECK(client.title_image() == title_bytes());
BOOST_TEST(network.acknowledged == response.size());
client.start({}, 1U);
BOOST_CHECK(client.title_image().empty());
}
BOOST_FIXTURE_TEST_CASE(pico_http_title_rejects_incorrect_binary_framing, fake_lwip_fixture) {
auto client = vamp_pico::http_client{};
auto valid = title_response();
auto extra = valid;
extra.insert(extra.find("\r\n\r\n"), "\r\nContent-Length: 9480");
auto chunked = valid;
chunked.insert(chunked.find("\r\n\r\n"), "\r\nTransfer-Encoding: chunked");
auto compressed = valid;
compressed.insert(compressed.find("\r\n\r\n"), "\r\nContent-Encoding: gzip");
auto wrong_type = valid;
wrong_type.replace(wrong_type.find("application/octet-stream"), 24U, "text/plain");
auto wrong_length = valid;
wrong_length.replace(wrong_length.find("9480"), 4U, "9479");
auto wrong_image = valid;
wrong_image[wrong_image.find("VPTI") + 4U] = 0;
for (auto const &response : {valid.substr(0U, valid.size() - 1U), valid + "x", extra, chunked, compressed, wrong_type, wrong_length, wrong_image}) {
BOOST_CHECK(client.start(title_request(), 0U) == vamp_pico::http_start_result::STARTED);
auto &pcb = network.pcbs[network.allocated - 1U];
BOOST_CHECK(deliver_connected(pcb) == ERR_OK);
BOOST_CHECK(deliver_data(pcb, response) == ERR_OK);
deliver_eof(pcb);
require_failure(client, 1U);
BOOST_CHECK(client.title_image().empty());
}
}
BOOST_FIXTURE_TEST_CASE(pico_http_title_timeout_overflow_and_bad_identifier_are_failures, fake_lwip_fixture) {
auto client = vamp_pico::http_client{};
BOOST_CHECK(client.start(title_request('z'), 0U) == vamp_pico::http_start_result::STARTED);
require_failure(client, 0U);
BOOST_TEST(network.allocated == 0U);
client.start(title_request(), 100U);
BOOST_CHECK(client.poll(1599U) == vamp_pico::http_status::IN_FLIGHT);
require_failure(client, 1600U);
client.start(title_request(), 1601U);
auto &pcb = network.pcbs[1];
deliver_connected(pcb);
BOOST_CHECK(deliver_data(pcb, std::string(vamp_protocol::title_image_size + 1025U, 'x')) == ERR_ABRT);
require_failure(client, 1602U);
}
BOOST_FIXTURE_TEST_CASE(pico_http_title_preemption_detaches_late_callbacks, fake_lwip_fixture) {
auto client = vamp_pico::http_client{};
auto scheduler = vamp_pico::request_scheduler{};
scheduler.want_title(std::string(64U, 'a'));
auto request = scheduler.next(0U);
BOOST_REQUIRE(request);
client.start(*request, 0U);
auto &old = network.pcbs[0];
deliver_connected(old);
deliver_data(old, std::string_view{title_response()}.substr(0U, 200U));
scheduler.enqueue(vamp_protocol::page_command::SELECT);
client.cancel();
scheduler.cancel_title();
request = scheduler.next(1U);
BOOST_REQUIRE(request);
BOOST_CHECK(request->kind == vamp_pico::request_kind::COMMAND);
client.start(*request, 1U);
deliver_data(old, title_response());
deliver_eof(old);
BOOST_CHECK(client.poll(2U) == vamp_pico::http_status::IN_FLIGHT);
BOOST_CHECK(client.title_image().empty());
}