@@ -7,6 +7,7 @@ add_executable(product-vamp-test
|
||||
test_health.cpp
|
||||
test_pico.cpp
|
||||
test_score_loader.cpp
|
||||
test_touch_input.cpp
|
||||
)
|
||||
target_include_directories(product-vamp-test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
@@ -14,6 +15,7 @@ target_include_directories(product-vamp-test PRIVATE
|
||||
)
|
||||
target_link_libraries(product-vamp-test PRIVATE
|
||||
vamp-zero-core
|
||||
modulus::raylib
|
||||
)
|
||||
target_compile_features(product-vamp-test PRIVATE cxx_std_23)
|
||||
target_compile_definitions(product-vamp-test PRIVATE
|
||||
|
||||
@@ -62,3 +62,20 @@ class CatalogueTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, 'output path collides'):
|
||||
generate_practice_scores(source, output)
|
||||
self.assertFalse(output.exists())
|
||||
def test_runtime_id_collisions_preserve_existing_output(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
source, output = Path(temporary) / 'source', Path(temporary) / 'output'
|
||||
names = ('chord/one-two/three.xml', 'chord/one/two-three.xml')
|
||||
for name in names:
|
||||
path = source / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(xml_score(xml_note()), encoding='utf-8')
|
||||
output.mkdir()
|
||||
sentinel = output / 'existing.json'
|
||||
sentinel.write_text('unchanged', encoding='utf-8')
|
||||
with self.assertRaisesRegex(ValueError, 'practice exercise ID collision') as error:
|
||||
generate_practice_scores(source, output)
|
||||
for name in names:
|
||||
self.assertIn(name, str(error.exception))
|
||||
self.assertEqual(list(output.iterdir()), [sentinel])
|
||||
self.assertEqual(sentinel.read_text(), 'unchanged')
|
||||
|
||||
@@ -562,3 +562,74 @@ BOOST_AUTO_TEST_CASE(catalogue_changed_score_json_invalidates_metadata_and_requi
|
||||
BOOST_TEST(db.query("SELECT id FROM " + std::string{table} + " WHERE measure_count IS NULL AND total_pages IS NULL AND authored_key_signature IS NULL").rows.size() == 1U);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(catalogue_rejects_ambiguous_paths_before_import_or_direct_load) {
|
||||
for (auto key : {"c", "d"}) {
|
||||
auto db = catalogue_database{};
|
||||
BOOST_REQUIRE(db.ensure());
|
||||
db.write("songs/tune.json", catalogue_bundle("Original"));
|
||||
db.write("practice/chord/one-two/three/c.json", catalogue_bundle("Original"));
|
||||
BOOST_REQUIRE(db.import_assets());
|
||||
auto songs = db.rows("songs");
|
||||
auto practice = db.rows("practice_exercises");
|
||||
db.write("songs/tune.json", catalogue_bundle("Changed"));
|
||||
auto second = std::string{"practice/chord/one/two-three/"} + key + ".json";
|
||||
db.write(second, catalogue_bundle("Conflicting", key));
|
||||
auto result = db.import_assets();
|
||||
BOOST_REQUIRE(!result);
|
||||
auto message = result.error().describe();
|
||||
BOOST_TEST(message.find("chord-one-two-three") != std::string::npos);
|
||||
BOOST_TEST(message.find("practice/chord/one-two/three/c.json") != std::string::npos);
|
||||
BOOST_TEST(message.find(second) != std::string::npos);
|
||||
BOOST_TEST(!vamp_zero::load_practice_library(db.assets.string()));
|
||||
BOOST_TEST(db.rows("songs") == songs, boost::test_tools::per_element());
|
||||
BOOST_TEST(db.rows("practice_exercises") == practice, boost::test_tools::per_element());
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(catalogue_changed_import_keeps_complete_cached_metadata) {
|
||||
auto db = catalogue_database{};
|
||||
BOOST_REQUIRE(db.ensure());
|
||||
for (auto title : {"Original", "Changed"}) {
|
||||
db.write("songs/tune.json", catalogue_bundle(title, "D", "Eb"));
|
||||
db.write("practice/chord/lesson/exercise/d.json", catalogue_bundle(title, "D", "Eb"));
|
||||
BOOST_REQUIRE(db.import_assets());
|
||||
for (auto table : {"songs", "practice_exercises"}) {
|
||||
auto result = db.query("SELECT measure_count, total_pages, authored_key_signature FROM " + std::string{table});
|
||||
BOOST_REQUIRE_EQUAL(result.rows.size(), 1U);
|
||||
BOOST_TEST(std::get<std::int64_t>(result.rows[0][0].value) == 3);
|
||||
BOOST_TEST(std::get<std::int64_t>(result.rows[0][1].value) == 2);
|
||||
BOOST_TEST(std::get<std::string>(result.rows[0][2].value) == "Eb");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(catalogue_rejects_ambiguous_variant_ids_with_distinct_exercise_ids) {
|
||||
auto db = catalogue_database{};
|
||||
BOOST_REQUIRE(db.ensure());
|
||||
db.write("practice/chord/lesson/exercise/c-d.json", catalogue_bundle("First"));
|
||||
db.write("practice/chord/lesson/exercise-c/d.json", catalogue_bundle("Second"));
|
||||
auto result = db.import_assets();
|
||||
BOOST_REQUIRE(!result);
|
||||
BOOST_TEST(result.error().describe().find("practice variant ID collision 'chord-lesson-exercise-c-d'") != std::string::npos);
|
||||
BOOST_TEST(db.rows("practice_exercises").empty());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(catalogue_backfill_uses_pending_indexes_and_preserves_large_mixed_catalogues) {
|
||||
auto db = catalogue_database{};
|
||||
BOOST_REQUIRE(db.ensure());
|
||||
BOOST_REQUIRE(db.ensure());
|
||||
for (auto table : {"songs", "practice_exercises"}) {
|
||||
auto name = std::string{table};
|
||||
db.query("WITH RECURSIVE sequence(n) AS (VALUES(1) UNION ALL SELECT n + 1 FROM sequence WHERE n < 4000) INSERT INTO " + name + "(id, title, source_json, created_at_ms, updated_at_ms, catalog_managed, measure_count, total_pages, authored_key_signature) SELECT 'score-' || n, 'Fixture', ?1, 1, 2, 0, CASE WHEN n % 2 = 0 THEN 3 END, 2, 'Eb' FROM sequence", {noto::sqlite_value{catalogue_bundle("Fixture", "D", "Eb")}});
|
||||
auto plan = db.query("EXPLAIN QUERY PLAN SELECT id, source_path, source_json FROM " + name + " WHERE measure_count IS NULL OR total_pages IS NULL OR authored_key_signature IS NULL LIMIT 1");
|
||||
BOOST_REQUIRE_EQUAL(plan.rows.size(), 1U);
|
||||
BOOST_TEST(std::get<std::string>(plan.rows[0][3].value).find("idx_" + name + "_pending_metadata") != std::string::npos);
|
||||
}
|
||||
BOOST_REQUIRE(db.performance());
|
||||
BOOST_REQUIRE(db.practice());
|
||||
for (auto table : {"songs", "practice_exercises"}) {
|
||||
auto rows = db.query("SELECT count(*) FROM " + std::string{table} + " WHERE measure_count = 3 AND total_pages = 2 AND authored_key_signature = 'Eb' AND created_at_ms = 1 AND updated_at_ms = 2 AND catalog_managed = 0 AND source_json = ?1", {noto::sqlite_value{catalogue_bundle("Fixture", "D", "Eb")}});
|
||||
BOOST_TEST(std::get<std::int64_t>(rows.rows[0][0].value) == 4000);
|
||||
}
|
||||
}
|
||||
|
||||
+80
-5
@@ -18,6 +18,9 @@
|
||||
#include <boost/asio/use_future.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <openssl/sha.h>
|
||||
#include <raylib.h>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <vamp_zero/app.hpp>
|
||||
#include <vamp_zero/database.hpp>
|
||||
@@ -118,10 +121,15 @@ auto config_file(std::filesystem::path const &root, vamp_zero::config const &con
|
||||
return path.string();
|
||||
}
|
||||
|
||||
auto fixture_page() -> std::string {
|
||||
auto bytes = std::array<std::uchar_t, 24>{137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13,
|
||||
73, 72, 68, 82, 0, 0, 3, 208, 0, 0, 1, 228};
|
||||
return {reinterpret_cast<std::char_t const *>(bytes.data()), bytes.size()};
|
||||
auto fixture_page(std::int32_t width = 976, std::int32_t height = 484) -> std::string {
|
||||
auto image = GenImageColor(width, height, WHITE);
|
||||
auto size = std::int32_t{};
|
||||
auto bytes = ExportImageToMemory(image, ".png", &size);
|
||||
UnloadImage(image);
|
||||
BOOST_REQUIRE(bytes != nullptr);
|
||||
auto result = std::string{reinterpret_cast<std::char_t const *>(bytes), static_cast<std::size_t>(size)};
|
||||
MemFree(bytes);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto checksum(std::string const &bytes) -> std::string {
|
||||
@@ -354,7 +362,7 @@ BOOST_AUTO_TEST_CASE(readiness_rejects_corrupt_checksums_and_escaping_pages) {
|
||||
std::filesystem::create_symlink(root.path / "outside.png", page);
|
||||
} else {
|
||||
auto bytes = fixture_page();
|
||||
bytes.back() = 0;
|
||||
bytes.back() ^= 1;
|
||||
write_file(page, bytes);
|
||||
}
|
||||
auto ready = run_once(root.path, config);
|
||||
@@ -387,6 +395,73 @@ BOOST_AUTO_TEST_CASE(readiness_rejects_unusable_database_and_runtime_paths) {
|
||||
BOOST_TEST(read_file(root.path / "blocked") == "regular file");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(readiness_decodes_pages_even_when_checksums_match) {
|
||||
for (auto kind : {"truncated", "missing pixels", "wrong dimensions", "oversized"}) {
|
||||
auto root = temporary_directory{};
|
||||
auto config = fixture_config(root.path);
|
||||
make_assets(config);
|
||||
auto bytes = fixture_page(std::string_view{kind} == "wrong dimensions" ? 10 : 976);
|
||||
if (std::string_view{kind} == "truncated") {
|
||||
bytes.resize(24U);
|
||||
} else if (std::string_view{kind} == "missing pixels") {
|
||||
bytes.resize(33U);
|
||||
} else if (std::string_view{kind} == "oversized") {
|
||||
bytes[16] = 127;
|
||||
}
|
||||
auto score_path = std::filesystem::path{config.runtime.assets_path} / "songs" / "first.json";
|
||||
auto score = boost::json::parse(read_file(score_path));
|
||||
auto &page = score.at("presentation").at("pages").as_array()[0].as_object();
|
||||
write_file(score_path.parent_path() / std::string{page.at("path").as_string()}, bytes);
|
||||
page["sha256"] = checksum(bytes);
|
||||
write_file(score_path, boost::json::serialize(score));
|
||||
auto ready = run_once(root.path, config);
|
||||
BOOST_REQUIRE_MESSAGE(!ready, kind);
|
||||
BOOST_TEST(ready.error().describe().find("first.pages/1.png") != std::string::npos);
|
||||
BOOST_TEST(!std::filesystem::exists(config.runtime.heartbeat_path));
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(readiness_rejects_actual_heartbeat_directories_and_symlinks) {
|
||||
for (auto kind : {"directory", "symlink", "dangling symlink"}) {
|
||||
auto root = temporary_directory{};
|
||||
auto config = fixture_config(root.path);
|
||||
make_assets(config);
|
||||
std::filesystem::create_directories(std::filesystem::path{config.runtime.heartbeat_path}.parent_path());
|
||||
if (std::string_view{kind} == "directory") {
|
||||
std::filesystem::create_directory(config.runtime.heartbeat_path);
|
||||
} else {
|
||||
if (std::string_view{kind} == "symlink") {
|
||||
write_file(root.path / "target", "untouched");
|
||||
}
|
||||
std::filesystem::create_symlink(root.path / "target", config.runtime.heartbeat_path);
|
||||
}
|
||||
auto ready = run_once(root.path, config);
|
||||
BOOST_REQUIRE(!ready);
|
||||
BOOST_TEST(ready.error().describe().find(config.runtime.heartbeat_path) != std::string::npos);
|
||||
require_no_probe(config, true);
|
||||
if (std::string_view{kind} == "symlink") {
|
||||
BOOST_TEST(read_file(root.path / "target") == "untouched");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(readiness_rejects_unwritable_parent_without_leaving_probes) {
|
||||
if (::geteuid() == 0) {
|
||||
BOOST_TEST_MESSAGE("Permission check requires an unprivileged test process");
|
||||
return;
|
||||
}
|
||||
auto root = temporary_directory{};
|
||||
auto config = fixture_config(root.path);
|
||||
make_assets(config);
|
||||
auto parent = std::filesystem::path{config.runtime.heartbeat_path}.parent_path();
|
||||
std::filesystem::create_directory(parent);
|
||||
std::filesystem::permissions(parent, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec);
|
||||
auto ready = run_once(root.path, config);
|
||||
std::filesystem::permissions(parent, std::filesystem::perms::owner_all);
|
||||
BOOST_TEST(!ready);
|
||||
require_no_probe(config);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(readiness_rejects_occupied_bind_and_releases_successful_probe) {
|
||||
auto root = temporary_directory{};
|
||||
auto config = fixture_config(root.path);
|
||||
|
||||
+8
-8
@@ -33,7 +33,7 @@ auto take_command(vamp_pico::request_scheduler &scheduler, std::uint64_t now_ms,
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(pico_gesture_single_tap_waits_for_double_tap_deadline) {
|
||||
auto gesture = vamp_pico::gesture_state{};
|
||||
auto gesture = vamp_protocol::gesture_state{};
|
||||
BOOST_CHECK(!gesture.update(false, 0U));
|
||||
BOOST_CHECK(!gesture.update(true, 10U));
|
||||
BOOST_CHECK(!gesture.update(false, 50U));
|
||||
@@ -43,7 +43,7 @@ BOOST_AUTO_TEST_CASE(pico_gesture_single_tap_waits_for_double_tap_deadline) {
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(pico_gesture_double_tap_includes_deadline_and_has_no_trailing_next) {
|
||||
auto gesture = vamp_pico::gesture_state{};
|
||||
auto gesture = vamp_protocol::gesture_state{};
|
||||
BOOST_CHECK(!gesture.update(true, 0U));
|
||||
BOOST_CHECK(!gesture.update(false, 40U));
|
||||
BOOST_CHECK(!gesture.update(true, 180U));
|
||||
@@ -52,7 +52,7 @@ BOOST_AUTO_TEST_CASE(pico_gesture_double_tap_includes_deadline_and_has_no_traili
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(pico_gesture_late_second_release_preserves_both_single_taps) {
|
||||
auto gesture = vamp_pico::gesture_state{};
|
||||
auto gesture = vamp_protocol::gesture_state{};
|
||||
BOOST_CHECK(!gesture.update(true, 0U));
|
||||
BOOST_CHECK(!gesture.update(false, 40U));
|
||||
BOOST_CHECK(!gesture.update(true, 180U));
|
||||
@@ -62,7 +62,7 @@ BOOST_AUTO_TEST_CASE(pico_gesture_late_second_release_preserves_both_single_taps
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(pico_gesture_expired_tap_is_emitted_when_next_press_starts) {
|
||||
auto gesture = vamp_pico::gesture_state{};
|
||||
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);
|
||||
@@ -71,7 +71,7 @@ BOOST_AUTO_TEST_CASE(pico_gesture_expired_tap_is_emitted_when_next_press_starts)
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(pico_gesture_hold_emits_once_and_suppresses_release) {
|
||||
auto gesture = vamp_pico::gesture_state{};
|
||||
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);
|
||||
@@ -84,7 +84,7 @@ BOOST_AUTO_TEST_CASE(pico_gesture_hold_emits_once_and_suppresses_release) {
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(pico_gesture_hold_consumes_pending_tap_even_if_release_is_next_sample) {
|
||||
auto gesture = vamp_pico::gesture_state{};
|
||||
auto gesture = vamp_protocol::gesture_state{};
|
||||
BOOST_CHECK(!gesture.update(true, 0U));
|
||||
BOOST_CHECK(!gesture.update(false, 40U));
|
||||
BOOST_CHECK(!gesture.update(true, 180U));
|
||||
@@ -185,7 +185,7 @@ BOOST_AUTO_TEST_CASE(pico_scheduler_extra_completion_does_not_delay_next_poll) {
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(pico_gestures_are_sampled_and_queued_during_slow_request) {
|
||||
auto gesture = vamp_pico::gesture_state{};
|
||||
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) {
|
||||
@@ -212,7 +212,7 @@ BOOST_AUTO_TEST_CASE(pico_gestures_are_sampled_and_queued_during_slow_request) {
|
||||
|
||||
BOOST_AUTO_TEST_CASE(pico_timing_survives_32_bit_millisecond_boundary) {
|
||||
constexpr auto start = std::uint64_t{0xffffffffU} - 100U;
|
||||
auto gesture = vamp_pico::gesture_state{};
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
|
||||
#include <vamp_protocol/gesture.hpp>
|
||||
#include <vamp_zero/touch_input.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto touch_event(luma::touch_phase phase, std::chrono::steady_clock::time_point time, std::int32_t id = 1) -> luma::touch_event {
|
||||
return {.points = {{.id = id, .phase = phase}}, .time = time};
|
||||
}
|
||||
|
||||
struct touch_sample {
|
||||
std::bool_t pressed;
|
||||
std::uint64_t milliseconds;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(zero_and_pico_share_timestamped_gesture_transitions) {
|
||||
auto sequences = std::vector<std::vector<touch_sample>>{
|
||||
{{true, 0}, {false, 30}},
|
||||
{{true, 0}, {false, 30}, {true, 200}, {false, 300}},
|
||||
{{true, 0}, {false, 40}, {true, 180}, {false, 260}},
|
||||
{{true, 0}, {false, 40}, {true, 180}, {false, 261}},
|
||||
{{true, 0}, {false, 40}, {true, 261}, {false, 300}},
|
||||
{{true, 50}, {true, 749}, {true, 750}, {true, 900}, {false, 910}},
|
||||
{{true, 0}, {false, 40}, {true, 180}, {false, 880}},
|
||||
{{true, 0}, {false, 699}},
|
||||
{{true, 0}, {false, 700}},
|
||||
};
|
||||
auto expected = std::vector<std::vector<std::string>>{
|
||||
{"next"},
|
||||
{"next", "next"},
|
||||
{"previous"},
|
||||
{"next", "next"},
|
||||
{"next", "next"},
|
||||
{"select"},
|
||||
{"select"},
|
||||
{"next"},
|
||||
{"select"},
|
||||
};
|
||||
for (auto index = std::size_t{}; index < sequences.size(); ++index) {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto zero_commands = std::vector<std::string>{};
|
||||
auto pico_commands = std::vector<std::string>{};
|
||||
auto zero = vamp_zero::touch_input{io.get_executor(), [&](auto command) { zero_commands.emplace_back(vamp_protocol::page_command_name(command)); }};
|
||||
auto pico = vamp_protocol::gesture_state{};
|
||||
auto base = std::chrono::steady_clock::now() - std::chrono::seconds{10};
|
||||
auto down = std::bool_t{false};
|
||||
for (auto sample : sequences[index]) {
|
||||
auto phase = sample.pressed ? (down ? luma::touch_phase::MOVE : luma::touch_phase::DOWN) : luma::touch_phase::UP;
|
||||
BOOST_TEST(zero.handle(touch_event(phase, base + std::chrono::milliseconds{sample.milliseconds})));
|
||||
if (auto command = pico.update(sample.pressed, sample.milliseconds)) {
|
||||
pico_commands.emplace_back(vamp_protocol::page_command_name(*command));
|
||||
}
|
||||
down = sample.pressed;
|
||||
}
|
||||
if (auto command = pico.update(false, 2000U)) {
|
||||
pico_commands.emplace_back(vamp_protocol::page_command_name(*command));
|
||||
}
|
||||
io.run();
|
||||
BOOST_TEST(zero_commands == expected[index], boost::test_tools::per_element());
|
||||
BOOST_TEST(pico_commands == expected[index], boost::test_tools::per_element());
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(shared_gesture_deadline_and_reset_discard_pending_actions) {
|
||||
auto gesture = vamp_protocol::gesture_state{};
|
||||
BOOST_TEST(!gesture.pending_deadline());
|
||||
BOOST_TEST(!gesture.update(true, 0U));
|
||||
BOOST_TEST(!gesture.update(false, 30U));
|
||||
BOOST_REQUIRE(gesture.pending_deadline());
|
||||
BOOST_TEST(*gesture.pending_deadline() == 251U);
|
||||
BOOST_TEST(!gesture.update(false, 250U));
|
||||
BOOST_TEST(!gesture.update(true, 250U));
|
||||
BOOST_TEST(!gesture.pending_deadline());
|
||||
gesture.reset();
|
||||
BOOST_TEST(!gesture.pending_deadline());
|
||||
BOOST_TEST(!gesture.update(false, 2000U));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(zero_touch_timer_emits_a_single_tap_without_further_input) {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto commands = std::vector<std::string>{};
|
||||
auto touch = vamp_zero::touch_input{io.get_executor(), [&](auto command) { commands.emplace_back(vamp_protocol::page_command_name(command)); }};
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
touch.handle(touch_event(luma::touch_phase::DOWN, now));
|
||||
touch.handle(touch_event(luma::touch_phase::UP, now));
|
||||
io.poll();
|
||||
BOOST_TEST(commands.empty());
|
||||
io.run();
|
||||
BOOST_REQUIRE_EQUAL(commands.size(), 1U);
|
||||
BOOST_TEST(commands.front() == "next");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(zero_touch_filters_other_fingers_and_cancels_pending_actions) {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto commands = std::vector<std::string>{};
|
||||
auto touch = vamp_zero::touch_input{io.get_executor(), [&](auto command) { commands.emplace_back(vamp_protocol::page_command_name(command)); }};
|
||||
auto base = std::chrono::steady_clock::now() - std::chrono::seconds{10};
|
||||
BOOST_TEST(touch.handle(touch_event(luma::touch_phase::DOWN, base)));
|
||||
BOOST_TEST(!touch.handle(touch_event(luma::touch_phase::DOWN, base, 2)));
|
||||
BOOST_TEST(!touch.handle(touch_event(luma::touch_phase::UP, base, 2)));
|
||||
BOOST_TEST(touch.handle(touch_event(luma::touch_phase::UP, base + std::chrono::milliseconds{30})));
|
||||
BOOST_TEST(touch.handle(touch_event(luma::touch_phase::DOWN, base + std::chrono::milliseconds{200})));
|
||||
BOOST_TEST(touch.handle(touch_event(luma::touch_phase::CANCEL, base + std::chrono::milliseconds{210})));
|
||||
BOOST_TEST(!touch.handle(touch_event(luma::touch_phase::UP, base + std::chrono::milliseconds{220})));
|
||||
io.run();
|
||||
BOOST_TEST(commands.empty());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(zero_touch_stale_timer_cannot_consume_newer_gesture) {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto commands = std::vector<std::string>{};
|
||||
auto touch = vamp_zero::touch_input{io.get_executor(), [&](auto command) { commands.emplace_back(vamp_protocol::page_command_name(command)); }};
|
||||
auto base = std::chrono::steady_clock::now() - std::chrono::seconds{10};
|
||||
touch.handle(touch_event(luma::touch_phase::DOWN, base));
|
||||
touch.handle(touch_event(luma::touch_phase::UP, base + std::chrono::milliseconds{30}));
|
||||
boost::asio::post(io, [&] {
|
||||
touch.handle(touch_event(luma::touch_phase::DOWN, base + std::chrono::milliseconds{200}));
|
||||
touch.handle(touch_event(luma::touch_phase::UP, base + std::chrono::milliseconds{220}));
|
||||
});
|
||||
io.run();
|
||||
BOOST_REQUIRE_EQUAL(commands.size(), 1U);
|
||||
BOOST_TEST(commands.front() == "previous");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(zero_touch_stop_and_destruction_suppress_queued_commands) {
|
||||
for (auto hold : {false, true}) {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto commands = std::vector<std::string>{};
|
||||
{
|
||||
auto touch = vamp_zero::touch_input{io.get_executor(), [&](auto command) { commands.emplace_back(vamp_protocol::page_command_name(command)); }};
|
||||
auto base = std::chrono::steady_clock::now() - std::chrono::seconds{10};
|
||||
touch.handle(touch_event(luma::touch_phase::DOWN, base));
|
||||
touch.handle(touch_event(luma::touch_phase::UP, base + std::chrono::milliseconds{hold ? 700 : 30}));
|
||||
touch.stop();
|
||||
BOOST_TEST(!touch.handle(touch_event(luma::touch_phase::DOWN, base)));
|
||||
}
|
||||
io.run();
|
||||
BOOST_TEST(commands.empty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user