#include #include #include #include #include #include #include #include #include #include #include #include #include namespace { auto check_command(std::optional 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); BOOST_TEST(!std::is_copy_assignable_v); BOOST_TEST(!std::is_move_constructible_v); BOOST_TEST(!std::is_move_assignable_v); } namespace { struct fake_network { std::array 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(data.size() / 2U); auto tail_size = static_cast(data.size() - half); auto tail = pbuf{nullptr, data.data() + half, tail_size, tail_size}; auto head = pbuf{&tail, data.data(), static_cast(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(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(destination); while (buffer != nullptr && copied < length) { if (offset < buffer->len) { auto size = std::min(length - copied, buffer->len - offset); std::memcpy(output + copied, static_cast(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); }