698 lines
26 KiB
C++
698 lines
26 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>
|
|
|
|
|
|
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 {
|
|
|
|
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);
|
|
}
|