Refactor app loading and window management (#609)
This commit is contained in:
committed by
GitHub
parent
dc3f6104b8
commit
37c507544b
@@ -8,6 +8,7 @@ add_subdirectory(TactilityFreeRtos)
|
||||
add_subdirectory(TactilityKernel)
|
||||
add_subdirectory(Tactility)
|
||||
add_subdirectory(crypt-module)
|
||||
add_subdirectory(app-module)
|
||||
|
||||
add_custom_target(build-tests)
|
||||
add_dependencies(build-tests ServiceModuleTests)
|
||||
@@ -15,3 +16,4 @@ add_dependencies(build-tests TactilityFreeRtosTests)
|
||||
add_dependencies(build-tests TactilityTests)
|
||||
add_dependencies(build-tests TactilityKernelTests)
|
||||
add_dependencies(build-tests CryptModuleTests)
|
||||
add_dependencies(build-tests AppModuleTests)
|
||||
|
||||
@@ -10,7 +10,7 @@ else()
|
||||
endif()
|
||||
|
||||
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH} ${TACTILITY_SDK_PATH}/Modules ${TACTILITY_SDK_PATH}/Drivers)
|
||||
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH} ${TACTILITY_SDK_PATH}/Modules)
|
||||
|
||||
project(SdkTest)
|
||||
tactility_project(SdkTest)
|
||||
|
||||
@@ -3,11 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||
idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
REQUIRES TactilitySDK
|
||||
lvgl-module
|
||||
bm8563-module
|
||||
bmi270-module
|
||||
mpu6886-module
|
||||
pi4ioe5v6408-module
|
||||
qmi8658-module
|
||||
rx8130ce-module
|
||||
app-module crypt-module gps-module lvgl-module lvgl-window-manager-module service-module
|
||||
)
|
||||
|
||||
@@ -1,37 +1,15 @@
|
||||
#include <tt_app.h>
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <tactility/concurrent/dispatcher.h>
|
||||
#include <tactility/concurrent/event_group.h>
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/concurrent/recursive_mutex.h>
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/concurrent/timer.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
#include <tactility/drivers/gpio_controller.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/i2s_controller.h>
|
||||
#include <tactility/drivers/root.h>
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/defines.h>
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/module.h>
|
||||
#include <tactility/time.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <drivers/bm8563.h>
|
||||
#include <drivers/bmi270.h>
|
||||
#include <drivers/mpu6886.h>
|
||||
#include <drivers/pi4ioe5v6408.h>
|
||||
#include <drivers/qmi8658.h>
|
||||
#include <drivers/rx8130ce.h>
|
||||
|
||||
static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
||||
static void create_widgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Title");
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
@@ -41,8 +19,27 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
tt_app_register((AppRegistration) {
|
||||
.onShow = onShowApp
|
||||
});
|
||||
AppInstanceId app_instance_id = app_scheduler_current_app_id();
|
||||
|
||||
struct AppEventSubscription sub = { .app_instance_id = app_instance_id };
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
WindowId window = window_manager_create(app_instance_id, create_widgets, NULL);
|
||||
|
||||
bool should_close = false;
|
||||
while (!should_close) {
|
||||
struct AppEvent event;
|
||||
if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) {
|
||||
break;
|
||||
}
|
||||
if (event.type == APP_EVENT_CLOSE) {
|
||||
app_manager_finish(app_instance_id);
|
||||
should_close = true;
|
||||
}
|
||||
}
|
||||
|
||||
window_manager_remove(window);
|
||||
app_event_unsubscribe(&sub);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ target_link_libraries(TactilityTests PRIVATE
|
||||
TactilityKernel
|
||||
platform-posix
|
||||
lvgl-module
|
||||
lvgl-window-manager-module
|
||||
app-module
|
||||
crypt-module
|
||||
gps-module
|
||||
gps-generic-module
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
#include "TestFile.h"
|
||||
#include "../../Tactility/Private/Tactility/app/AppManifestParsing.h"
|
||||
#include "../../Tactility/Private/Tactility/app/AppManifestParsingInternal.h"
|
||||
|
||||
#include "doctest.h"
|
||||
|
||||
using namespace tt;
|
||||
using namespace tt::app;
|
||||
|
||||
TEST_CASE("parseManifest() should parse a V1 (sectioned) manifest.properties file") {
|
||||
TestFile file("test-manifest-v1.properties");
|
||||
file.writeData(
|
||||
"[manifest]\n"
|
||||
"version=0.1\n"
|
||||
"[target]\n"
|
||||
"sdk=0.0.0\n"
|
||||
"platforms=esp32,esp32s3,esp32c6,esp32p4\n"
|
||||
"[app]\n"
|
||||
"id=one.tactility.sdktest\n"
|
||||
"versionName=0.1.0\n"
|
||||
"versionCode=1\n"
|
||||
"name=SDK Test\n"
|
||||
);
|
||||
|
||||
AppManifest manifest;
|
||||
CHECK_EQ(parseManifest(file.getPath(), manifest), true);
|
||||
CHECK_EQ(manifest.targetSdk, "0.0.0");
|
||||
CHECK_EQ(manifest.targetPlatforms, "esp32,esp32s3,esp32c6,esp32p4");
|
||||
CHECK_EQ(manifest.appId, "one.tactility.sdktest");
|
||||
CHECK_EQ(manifest.appName, "SDK Test");
|
||||
CHECK_EQ(manifest.appVersionName, "0.1.0");
|
||||
CHECK_EQ(manifest.appVersionCode, 1);
|
||||
}
|
||||
|
||||
TEST_CASE("parseManifest() should parse a V2 (flat) manifest.properties file") {
|
||||
TestFile file("test-manifest-v2.properties");
|
||||
file.writeData(
|
||||
"manifest.version=0.1\n"
|
||||
"target.sdk=0.0.0\n"
|
||||
"target.platforms=esp32,esp32s3,esp32c6,esp32p4\n"
|
||||
"app.id=one.tactility.sdktest\n"
|
||||
"app.version.name=0.1.0\n"
|
||||
"app.version.code=1\n"
|
||||
"app.name=SDK Test\n"
|
||||
);
|
||||
|
||||
AppManifest manifest;
|
||||
CHECK_EQ(parseManifest(file.getPath(), manifest), true);
|
||||
CHECK_EQ(manifest.targetSdk, "0.0.0");
|
||||
CHECK_EQ(manifest.targetPlatforms, "esp32,esp32s3,esp32c6,esp32p4");
|
||||
CHECK_EQ(manifest.appId, "one.tactility.sdktest");
|
||||
CHECK_EQ(manifest.appName, "SDK Test");
|
||||
CHECK_EQ(manifest.appVersionName, "0.1.0");
|
||||
CHECK_EQ(manifest.appVersionCode, 1);
|
||||
}
|
||||
|
||||
TEST_CASE("parseManifestV1() should fail when a required key is missing") {
|
||||
std::map<std::string, std::string> properties = {
|
||||
{"[manifest]version", "0.1"},
|
||||
{"[app]id", "one.tactility.sdktest"},
|
||||
{"[app]name", "SDK Test"},
|
||||
{"[app]versionName", "0.1.0"},
|
||||
{"[app]versionCode", "1"},
|
||||
// Missing [target]sdk
|
||||
{"[target]platforms", "esp32"},
|
||||
};
|
||||
AppManifest manifest;
|
||||
CHECK_EQ(parseManifestV1(properties, manifest), false);
|
||||
}
|
||||
|
||||
TEST_CASE("parseManifestV2() should fail when the app id is invalid") {
|
||||
std::map<std::string, std::string> properties = {
|
||||
{"manifest.version", "0.1"},
|
||||
{"app.id", "abc"}, // too short (isValidId requires >= 5 chars)
|
||||
{"app.name", "SDK Test"},
|
||||
{"app.version.name", "0.1.0"},
|
||||
{"app.version.code", "1"},
|
||||
{"target.sdk", "0.0.0"},
|
||||
{"target.platforms", "esp32"},
|
||||
};
|
||||
AppManifest manifest;
|
||||
CHECK_EQ(parseManifestV2(properties, manifest), false);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
#include "doctest.h"
|
||||
#include <Tactility/Bundle.h>
|
||||
|
||||
using namespace tt;
|
||||
|
||||
TEST_CASE("boolean can be stored and retrieved") {
|
||||
Bundle bundle;
|
||||
bundle.putBool("key", true);
|
||||
CHECK(bundle.hasBool("key"));
|
||||
CHECK(bundle.getBool("key"));
|
||||
bool opt_result = false;
|
||||
CHECK(bundle.optBool("key", opt_result));
|
||||
CHECK_EQ(opt_result, true);
|
||||
}
|
||||
|
||||
TEST_CASE("int32 can be stored and retrieved") {
|
||||
Bundle bundle;
|
||||
bundle.putInt32("key", true);
|
||||
CHECK(bundle.hasInt32("key"));
|
||||
CHECK(bundle.getInt32("key"));
|
||||
int32_t opt_result = false;
|
||||
CHECK(bundle.optInt32("key", opt_result));
|
||||
CHECK_EQ(opt_result, true);
|
||||
}
|
||||
|
||||
TEST_CASE("string can be stored and retrieved") {
|
||||
Bundle bundle;
|
||||
bundle.putString("key", "test");
|
||||
CHECK(bundle.hasString("key"));
|
||||
CHECK_EQ(bundle.getString("key"), "test");
|
||||
std::string opt_result;
|
||||
CHECK(bundle.optString("key", opt_result));
|
||||
CHECK_EQ(opt_result, "test");
|
||||
}
|
||||
|
||||
TEST_CASE("bundle copy makes an actual copy") {
|
||||
auto* original_ptr = new Bundle();
|
||||
Bundle& original = *original_ptr;
|
||||
original.putBool("bool", true);
|
||||
original.putInt32("int32", 123);
|
||||
original.putString("string", "text");
|
||||
|
||||
Bundle copy = original;
|
||||
delete original_ptr;
|
||||
|
||||
CHECK_EQ(copy.getBool("bool"), true);
|
||||
CHECK_EQ(copy.getInt32("int32"), 123);
|
||||
CHECK_EQ(copy.getString("string"), "text");
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
#include "TestFile.h"
|
||||
#include "../../Tactility/Include/Tactility/file/PropertiesFile.h"
|
||||
|
||||
#include "doctest.h"
|
||||
|
||||
using namespace tt;
|
||||
|
||||
TEST_CASE("loadPropertiesFile() should return false when the file does not exist") {
|
||||
std::map<std::string, std::string> properties;
|
||||
CHECK_EQ(file::loadPropertiesFile("does_not_exist.properties", properties), false);
|
||||
}
|
||||
|
||||
TEST_CASE("PropertiesFile should parse a valid file properly") {
|
||||
TestFile file("test.properties");
|
||||
file.writeData(
|
||||
"# Comment\n" // Regular comment
|
||||
" \t# Comment\n" // Prefixed comment
|
||||
"key1=value1\n" // Regular property
|
||||
" \tkey 2\t = \tvalue 2\t " // Property with empty space
|
||||
);
|
||||
|
||||
std::map<std::string, std::string> properties;
|
||||
|
||||
// Load data
|
||||
CHECK_EQ(file::loadPropertiesFile(file.getPath(), properties), true);
|
||||
|
||||
CHECK_EQ(properties.size(), 2);
|
||||
CHECK_EQ(properties["key1"], "value1");
|
||||
CHECK_EQ(properties["key 2"], "value 2");
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
#include "doctest.h"
|
||||
#include <tactility/bundle.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
TEST_CASE("bundle_alloc/bundle_free round-trip") {
|
||||
Bundle* bundle = bundle_alloc();
|
||||
CHECK_NE(bundle, nullptr);
|
||||
bundle_free(bundle);
|
||||
}
|
||||
|
||||
TEST_CASE("bool can be stored and retrieved") {
|
||||
Bundle* bundle = bundle_alloc();
|
||||
bundle_put_bool(bundle, "key", true);
|
||||
|
||||
CHECK(bundle_has_bool(bundle, "key"));
|
||||
CHECK_EQ(bundle_get_bool(bundle, "key"), true);
|
||||
|
||||
bool out = false;
|
||||
CHECK(bundle_opt_bool(bundle, "key", &out));
|
||||
CHECK_EQ(out, true);
|
||||
|
||||
bundle_free(bundle);
|
||||
}
|
||||
|
||||
TEST_CASE("int32 can be stored and retrieved") {
|
||||
Bundle* bundle = bundle_alloc();
|
||||
bundle_put_int32(bundle, "key", -42);
|
||||
|
||||
CHECK(bundle_has_int32(bundle, "key"));
|
||||
CHECK_EQ(bundle_get_int32(bundle, "key"), -42);
|
||||
|
||||
int32_t out = 0;
|
||||
CHECK(bundle_opt_int32(bundle, "key", &out));
|
||||
CHECK_EQ(out, -42);
|
||||
|
||||
bundle_free(bundle);
|
||||
}
|
||||
|
||||
TEST_CASE("int64 can be stored and retrieved") {
|
||||
Bundle* bundle = bundle_alloc();
|
||||
bundle_put_int64(bundle, "key", 123456789012345LL);
|
||||
|
||||
CHECK(bundle_has_int64(bundle, "key"));
|
||||
CHECK_EQ(bundle_get_int64(bundle, "key"), 123456789012345LL);
|
||||
|
||||
int64_t out = 0;
|
||||
CHECK(bundle_opt_int64(bundle, "key", &out));
|
||||
CHECK_EQ(out, 123456789012345LL);
|
||||
|
||||
bundle_free(bundle);
|
||||
}
|
||||
|
||||
TEST_CASE("string can be stored and retrieved") {
|
||||
Bundle* bundle = bundle_alloc();
|
||||
bundle_put_string(bundle, "key", "hello world");
|
||||
|
||||
CHECK(bundle_has_string(bundle, "key"));
|
||||
|
||||
char buffer[32];
|
||||
CHECK_EQ(bundle_get_string(bundle, "key", buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "hello world"), 0);
|
||||
|
||||
char tiny[4];
|
||||
CHECK_EQ(bundle_get_string(bundle, "key", tiny, sizeof(tiny)), ERROR_BUFFER_OVERFLOW);
|
||||
|
||||
char out[32];
|
||||
CHECK_EQ(bundle_opt_string(bundle, "key", out, sizeof(out)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(out, "hello world"), 0);
|
||||
|
||||
bundle_free(bundle);
|
||||
}
|
||||
|
||||
TEST_CASE("has_*/opt_* reject a key stored with a different type") {
|
||||
Bundle* bundle = bundle_alloc();
|
||||
bundle_put_bool(bundle, "key", true);
|
||||
|
||||
CHECK_FALSE(bundle_has_int32(bundle, "key"));
|
||||
CHECK_FALSE(bundle_has_int64(bundle, "key"));
|
||||
CHECK_FALSE(bundle_has_string(bundle, "key"));
|
||||
|
||||
int32_t out_int32 = 0;
|
||||
CHECK_FALSE(bundle_opt_int32(bundle, "key", &out_int32));
|
||||
|
||||
char out_string[8];
|
||||
CHECK_EQ(bundle_opt_string(bundle, "key", out_string, sizeof(out_string)), ERROR_NOT_FOUND);
|
||||
|
||||
bundle_free(bundle);
|
||||
}
|
||||
|
||||
TEST_CASE("opt_string reports ERROR_NOT_FOUND for a missing key") {
|
||||
Bundle* bundle = bundle_alloc();
|
||||
char out[8];
|
||||
CHECK_EQ(bundle_opt_string(bundle, "missing", out, sizeof(out)), ERROR_NOT_FOUND);
|
||||
bundle_free(bundle);
|
||||
}
|
||||
|
||||
TEST_CASE("bundle_clone makes an independent deep copy") {
|
||||
Bundle* original = bundle_alloc();
|
||||
bundle_put_bool(original, "bool", true);
|
||||
bundle_put_int32(original, "int32", 123);
|
||||
bundle_put_string(original, "string", "text");
|
||||
|
||||
Bundle* clone = bundle_clone(original);
|
||||
bundle_free(original); // clone must not be affected
|
||||
|
||||
CHECK_EQ(bundle_get_bool(clone, "bool"), true);
|
||||
CHECK_EQ(bundle_get_int32(clone, "int32"), 123);
|
||||
|
||||
char buffer[16];
|
||||
CHECK_EQ(bundle_get_string(clone, "string", buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "text"), 0);
|
||||
|
||||
// Mutating the clone must not affect a re-clone of the (already-freed) original's data.
|
||||
bundle_put_int32(clone, "int32", 456);
|
||||
CHECK_EQ(bundle_get_int32(clone, "int32"), 456);
|
||||
|
||||
bundle_free(clone);
|
||||
}
|
||||
|
||||
TEST_CASE("put overwrites a previously stored value, including across types") {
|
||||
Bundle* bundle = bundle_alloc();
|
||||
bundle_put_int32(bundle, "key", 1);
|
||||
bundle_put_string(bundle, "key", "now a string");
|
||||
|
||||
CHECK_FALSE(bundle_has_int32(bundle, "key"));
|
||||
CHECK(bundle_has_string(bundle, "key"));
|
||||
|
||||
char buffer[32];
|
||||
CHECK_EQ(bundle_get_string(bundle, "key", buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "now a string"), 0);
|
||||
|
||||
bundle_free(bundle);
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
#include "doctest.h"
|
||||
#include <tactility/preferences.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
|
||||
const char* TEST_PATH = "/tmp/tactility_kernel_preferences_test.properties";
|
||||
|
||||
struct ScratchFile {
|
||||
ScratchFile() { std::remove(TEST_PATH); }
|
||||
~ScratchFile() { std::remove(TEST_PATH); }
|
||||
};
|
||||
|
||||
bool file_exists(const char* path) {
|
||||
FILE* file = std::fopen(path, "r");
|
||||
if (file == nullptr) {
|
||||
return false;
|
||||
}
|
||||
std::fclose(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_directory(const char* path) {
|
||||
struct stat info {};
|
||||
return stat(path, &info) == 0 && (info.st_mode & S_IFMT) == S_IFDIR;
|
||||
}
|
||||
|
||||
// Writes a raw properties file directly (bypassing preferences_put_*()) so a test can exercise
|
||||
// a hand-crafted/corrupted payload that preferences_put_*() itself would never produce.
|
||||
void write_raw(const char* path, const char* content) {
|
||||
FILE* file = std::fopen(path, "w");
|
||||
std::fputs(content, file);
|
||||
std::fclose(file);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("preferences_open_path on a missing file starts out empty, without creating it") {
|
||||
ScratchFile scratch;
|
||||
|
||||
Preferences* preferences = preferences_open(TEST_PATH);
|
||||
CHECK_NE(preferences, nullptr);
|
||||
CHECK_FALSE(preferences_has_bool(preferences, "key"));
|
||||
CHECK_FALSE(file_exists(TEST_PATH));
|
||||
|
||||
preferences_close(preferences);
|
||||
}
|
||||
|
||||
TEST_CASE("put_*/has_*/opt_* round-trip all four types") {
|
||||
ScratchFile scratch;
|
||||
|
||||
Preferences* preferences = preferences_open(TEST_PATH);
|
||||
preferences_put_bool(preferences, "flag", true);
|
||||
preferences_put_int32(preferences, "count", -42);
|
||||
preferences_put_int64(preferences, "big", 123456789012345LL);
|
||||
preferences_put_string(preferences, "text", "hello world");
|
||||
|
||||
CHECK(preferences_has_bool(preferences, "flag"));
|
||||
bool bool_out = false;
|
||||
CHECK(preferences_opt_bool(preferences, "flag", &bool_out));
|
||||
CHECK_EQ(bool_out, true);
|
||||
|
||||
CHECK(preferences_has_int32(preferences, "count"));
|
||||
int32_t int32_out = 0;
|
||||
CHECK(preferences_opt_int32(preferences, "count", &int32_out));
|
||||
CHECK_EQ(int32_out, -42);
|
||||
|
||||
CHECK(preferences_has_int64(preferences, "big"));
|
||||
int64_t int64_out = 0;
|
||||
CHECK(preferences_opt_int64(preferences, "big", &int64_out));
|
||||
CHECK_EQ(int64_out, 123456789012345LL);
|
||||
|
||||
CHECK(preferences_has_string(preferences, "text"));
|
||||
char buffer[32];
|
||||
CHECK_EQ(preferences_opt_string(preferences, "text", buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "hello world"), 0);
|
||||
|
||||
preferences_close(preferences);
|
||||
}
|
||||
|
||||
TEST_CASE("opt_string reports ERROR_BUFFER_OVERFLOW and ERROR_NOT_FOUND") {
|
||||
ScratchFile scratch;
|
||||
|
||||
Preferences* preferences = preferences_open(TEST_PATH);
|
||||
preferences_put_string(preferences, "text", "hello world");
|
||||
|
||||
char tiny[4];
|
||||
CHECK_EQ(preferences_opt_string(preferences, "text", tiny, sizeof(tiny)), ERROR_BUFFER_OVERFLOW);
|
||||
|
||||
char buffer[32];
|
||||
CHECK_EQ(preferences_opt_string(preferences, "missing", buffer, sizeof(buffer)), ERROR_NOT_FOUND);
|
||||
|
||||
preferences_close(preferences);
|
||||
}
|
||||
|
||||
TEST_CASE("has_*/opt_* reject a key stored with a different type") {
|
||||
ScratchFile scratch;
|
||||
|
||||
Preferences* preferences = preferences_open(TEST_PATH);
|
||||
preferences_put_bool(preferences, "key", true);
|
||||
|
||||
CHECK_FALSE(preferences_has_int32(preferences, "key"));
|
||||
CHECK_FALSE(preferences_has_int64(preferences, "key"));
|
||||
CHECK_FALSE(preferences_has_string(preferences, "key"));
|
||||
|
||||
int32_t out = 0;
|
||||
CHECK_FALSE(preferences_opt_int32(preferences, "key", &out));
|
||||
|
||||
preferences_close(preferences);
|
||||
}
|
||||
|
||||
TEST_CASE("has_*/opt_* reject malformed scalar payloads instead of misparsing them") {
|
||||
ScratchFile scratch;
|
||||
write_raw(TEST_PATH,
|
||||
"bad_bool=b:garbage\n"
|
||||
"bad_bool_2=b:2\n"
|
||||
"trailing_junk=i32:42abc\n"
|
||||
"int32_overflow=i32:5000000000\n"
|
||||
"int64_overflow=i64:99999999999999999999\n"
|
||||
"empty_int=i32:\n");
|
||||
|
||||
Preferences* preferences = preferences_open(TEST_PATH);
|
||||
|
||||
// "b:garbage" must not silently read back as false - has_bool()/opt_bool() must agree it's
|
||||
// not a valid bool at all.
|
||||
CHECK_FALSE(preferences_has_bool(preferences, "bad_bool"));
|
||||
bool bool_out = true;
|
||||
CHECK_FALSE(preferences_opt_bool(preferences, "bad_bool", &bool_out));
|
||||
CHECK_FALSE(preferences_has_bool(preferences, "bad_bool_2"));
|
||||
CHECK_FALSE(preferences_opt_bool(preferences, "bad_bool_2", &bool_out));
|
||||
|
||||
// "42abc" must not silently parse as 42 - the full payload must be consumed.
|
||||
CHECK_FALSE(preferences_has_int32(preferences, "trailing_junk"));
|
||||
int32_t int32_out = 0;
|
||||
CHECK_FALSE(preferences_opt_int32(preferences, "trailing_junk", &int32_out));
|
||||
|
||||
// Fits in a (64-bit, on this platform) `long` but overflows int32_t - must not silently
|
||||
// truncate on the narrowing cast.
|
||||
CHECK_FALSE(preferences_has_int32(preferences, "int32_overflow"));
|
||||
CHECK_FALSE(preferences_opt_int32(preferences, "int32_overflow", &int32_out));
|
||||
|
||||
// Overflows even a 64-bit integer - strtoll() itself reports ERANGE.
|
||||
CHECK_FALSE(preferences_has_int64(preferences, "int64_overflow"));
|
||||
int64_t int64_out = 0;
|
||||
CHECK_FALSE(preferences_opt_int64(preferences, "int64_overflow", &int64_out));
|
||||
|
||||
CHECK_FALSE(preferences_has_int32(preferences, "empty_int"));
|
||||
CHECK_FALSE(preferences_opt_int32(preferences, "empty_int", &int32_out));
|
||||
|
||||
preferences_close(preferences);
|
||||
}
|
||||
|
||||
TEST_CASE("a string value with embedded newlines and backslashes survives a reopen") {
|
||||
ScratchFile scratch;
|
||||
|
||||
{
|
||||
Preferences* preferences = preferences_open(TEST_PATH);
|
||||
preferences_put_string(preferences, "text", "line1\nline2 with \\ backslash");
|
||||
preferences_close(preferences);
|
||||
}
|
||||
{
|
||||
Preferences* preferences = preferences_open(TEST_PATH);
|
||||
char buffer[64];
|
||||
CHECK_EQ(preferences_opt_string(preferences, "text", buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "line1\nline2 with \\ backslash"), 0);
|
||||
preferences_close(preferences);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("preferences_close persists changes, and only close persists them") {
|
||||
ScratchFile scratch;
|
||||
|
||||
Preferences* preferences = preferences_open(TEST_PATH);
|
||||
preferences_put_bool(preferences, "flag", true);
|
||||
|
||||
// Not persisted yet - only preferences_close() writes to disk.
|
||||
CHECK_FALSE(file_exists(TEST_PATH));
|
||||
|
||||
preferences_close(preferences);
|
||||
CHECK(file_exists(TEST_PATH));
|
||||
|
||||
Preferences* reopened = preferences_open(TEST_PATH);
|
||||
CHECK(preferences_has_bool(reopened, "flag"));
|
||||
preferences_close(reopened);
|
||||
}
|
||||
|
||||
TEST_CASE("put_* on an already-closed value is visible without reopening") {
|
||||
ScratchFile scratch;
|
||||
|
||||
Preferences* preferences = preferences_open(TEST_PATH);
|
||||
preferences_put_int32(preferences, "count", 1);
|
||||
preferences_close(preferences);
|
||||
|
||||
Preferences* reopened = preferences_open(TEST_PATH);
|
||||
preferences_put_int32(reopened, "count", 2);
|
||||
int32_t out = 0;
|
||||
CHECK(preferences_opt_int32(reopened, "count", &out));
|
||||
CHECK_EQ(out, 2);
|
||||
preferences_close(reopened);
|
||||
|
||||
Preferences* final_instance = preferences_open(TEST_PATH);
|
||||
CHECK(preferences_opt_int32(final_instance, "count", &out));
|
||||
CHECK_EQ(out, 2);
|
||||
preferences_close(final_instance);
|
||||
}
|
||||
|
||||
TEST_CASE("preferences_open creates missing parent directories (recursively) and persists into them") {
|
||||
const char* nested_dir_a = "/tmp/tactility_kernel_preferences_test_nested";
|
||||
const char* nested_dir_b = "/tmp/tactility_kernel_preferences_test_nested/a";
|
||||
const char* nested_dir_c = "/tmp/tactility_kernel_preferences_test_nested/a/b";
|
||||
const char* nested_path = "/tmp/tactility_kernel_preferences_test_nested/a/b/settings.properties";
|
||||
|
||||
std::remove(nested_path);
|
||||
rmdir(nested_dir_c);
|
||||
rmdir(nested_dir_b);
|
||||
rmdir(nested_dir_a);
|
||||
REQUIRE_FALSE(is_directory(nested_dir_a));
|
||||
|
||||
Preferences* preferences = preferences_open(nested_path);
|
||||
REQUIRE_NE(preferences, nullptr);
|
||||
CHECK(is_directory(nested_dir_a));
|
||||
CHECK(is_directory(nested_dir_b));
|
||||
CHECK(is_directory(nested_dir_c));
|
||||
|
||||
preferences_put_int32(preferences, "count", 7);
|
||||
preferences_close(preferences);
|
||||
CHECK(file_exists(nested_path));
|
||||
|
||||
Preferences* reopened = preferences_open(nested_path);
|
||||
REQUIRE_NE(reopened, nullptr);
|
||||
int32_t out = 0;
|
||||
CHECK(preferences_opt_int32(reopened, "count", &out));
|
||||
CHECK_EQ(out, 7);
|
||||
preferences_close(reopened);
|
||||
|
||||
std::remove(nested_path);
|
||||
rmdir(nested_dir_c);
|
||||
rmdir(nested_dir_b);
|
||||
rmdir(nested_dir_a);
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
#include "doctest.h"
|
||||
#include <tactility/properties_file.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
const char* TEST_PATH = "/tmp/tactility_kernel_properties_file_test.properties";
|
||||
|
||||
struct ScratchFile {
|
||||
ScratchFile() { std::remove(TEST_PATH); }
|
||||
~ScratchFile() { std::remove(TEST_PATH); }
|
||||
};
|
||||
|
||||
bool file_exists(const char* path) {
|
||||
FILE* file = std::fopen(path, "r");
|
||||
if (file == nullptr) {
|
||||
return false;
|
||||
}
|
||||
std::fclose(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
void write_raw(const char* path, const char* content) {
|
||||
FILE* file = std::fopen(path, "w");
|
||||
std::fputs(content, file);
|
||||
std::fclose(file);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("properties_file_open on a missing file starts out empty, without creating it") {
|
||||
ScratchFile scratch;
|
||||
|
||||
PropertiesFile* file = properties_file_open(TEST_PATH);
|
||||
CHECK_NE(file, nullptr);
|
||||
CHECK_FALSE(properties_file_has(file, "key"));
|
||||
CHECK_FALSE(file_exists(TEST_PATH));
|
||||
|
||||
properties_file_close(file);
|
||||
}
|
||||
|
||||
TEST_CASE("properties_file_open returns NULL when a genuine I/O error interrupts reading") {
|
||||
// fopen() on a directory succeeds on Linux, but the first read fails with EISDIR and sets
|
||||
// the stream's error indicator - a deterministic way to exercise load_from_file()'s
|
||||
// ferror() check without needing real storage-hardware fault injection.
|
||||
const char* dir_path = "/tmp/tactility_kernel_properties_file_test_is_a_directory";
|
||||
rmdir(dir_path);
|
||||
REQUIRE_EQ(mkdir(dir_path, 0777), 0);
|
||||
|
||||
CHECK_EQ(properties_file_open(dir_path), nullptr);
|
||||
|
||||
rmdir(dir_path);
|
||||
}
|
||||
|
||||
TEST_CASE("set/has/get round-trip, and close persists while unclosed changes don't") {
|
||||
ScratchFile scratch;
|
||||
|
||||
PropertiesFile* file = properties_file_open(TEST_PATH);
|
||||
properties_file_set(file, "key", "value");
|
||||
|
||||
CHECK(properties_file_has(file, "key"));
|
||||
char buffer[32];
|
||||
CHECK_EQ(properties_file_get(file, "key", buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "value"), 0);
|
||||
|
||||
// Not persisted yet - only properties_file_close() writes to disk.
|
||||
CHECK_FALSE(file_exists(TEST_PATH));
|
||||
|
||||
properties_file_close(file);
|
||||
CHECK(file_exists(TEST_PATH));
|
||||
|
||||
PropertiesFile* reopened = properties_file_open(TEST_PATH);
|
||||
CHECK(properties_file_has(reopened, "key"));
|
||||
properties_file_close(reopened);
|
||||
}
|
||||
|
||||
TEST_CASE("properties_file_get reports ERROR_BUFFER_OVERFLOW and ERROR_NOT_FOUND") {
|
||||
ScratchFile scratch;
|
||||
|
||||
PropertiesFile* file = properties_file_open(TEST_PATH);
|
||||
properties_file_set(file, "key", "value");
|
||||
|
||||
char tiny[3];
|
||||
CHECK_EQ(properties_file_get(file, "key", tiny, sizeof(tiny)), ERROR_BUFFER_OVERFLOW);
|
||||
|
||||
char buffer[32];
|
||||
CHECK_EQ(properties_file_get(file, "missing", buffer, sizeof(buffer)), ERROR_NOT_FOUND);
|
||||
|
||||
properties_file_close(file);
|
||||
}
|
||||
|
||||
TEST_CASE("set overwrites a previously stored value") {
|
||||
ScratchFile scratch;
|
||||
|
||||
PropertiesFile* file = properties_file_open(TEST_PATH);
|
||||
properties_file_set(file, "key", "first");
|
||||
properties_file_set(file, "key", "second");
|
||||
|
||||
char buffer[32];
|
||||
CHECK_EQ(properties_file_get(file, "key", buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "second"), 0);
|
||||
|
||||
properties_file_close(file);
|
||||
}
|
||||
|
||||
TEST_CASE("comments and blank lines are skipped, keys and values are trimmed") {
|
||||
ScratchFile scratch;
|
||||
write_raw(TEST_PATH,
|
||||
"# Comment\n"
|
||||
" \t# Indented comment\n"
|
||||
"\n"
|
||||
"key1=value1\n"
|
||||
" \tkey 2\t = \tvalue 2\t \n");
|
||||
|
||||
PropertiesFile* file = properties_file_open(TEST_PATH);
|
||||
|
||||
char buffer[32];
|
||||
CHECK_EQ(properties_file_get(file, "key1", buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "value1"), 0);
|
||||
|
||||
// Only leading/trailing whitespace is trimmed - the internal space in "key 2"/"value 2"
|
||||
// survives.
|
||||
CHECK_EQ(properties_file_get(file, "key 2", buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "value 2"), 0);
|
||||
|
||||
properties_file_close(file);
|
||||
}
|
||||
|
||||
TEST_CASE("a malformed line (no '=') is skipped without aborting the rest of the file") {
|
||||
ScratchFile scratch;
|
||||
write_raw(TEST_PATH, "not_a_key_value_pair\nkey=value\n");
|
||||
|
||||
PropertiesFile* file = properties_file_open(TEST_PATH);
|
||||
CHECK(properties_file_has(file, "key"));
|
||||
CHECK_FALSE(properties_file_has(file, "not_a_key_value_pair"));
|
||||
properties_file_close(file);
|
||||
}
|
||||
|
||||
TEST_CASE("a [section] line prefixes every following key until the next section") {
|
||||
ScratchFile scratch;
|
||||
write_raw(TEST_PATH,
|
||||
"[app]\n"
|
||||
"id=one.tactility.helloworld\n"
|
||||
"name=Hello\n"
|
||||
"[other]\n"
|
||||
"id=x\n");
|
||||
|
||||
PropertiesFile* file = properties_file_open(TEST_PATH);
|
||||
|
||||
char buffer[64];
|
||||
CHECK_EQ(properties_file_get(file, "[app]id", buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "one.tactility.helloworld"), 0);
|
||||
|
||||
CHECK_EQ(properties_file_get(file, "[app]name", buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "Hello"), 0);
|
||||
|
||||
CHECK_EQ(properties_file_get(file, "[other]id", buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "x"), 0);
|
||||
|
||||
CHECK_FALSE(properties_file_has(file, "id"));
|
||||
|
||||
properties_file_close(file);
|
||||
}
|
||||
|
||||
TEST_CASE("properties_file_close reports ERROR_NONE on success") {
|
||||
ScratchFile scratch;
|
||||
|
||||
PropertiesFile* file = properties_file_open(TEST_PATH);
|
||||
properties_file_set(file, "key", "value");
|
||||
|
||||
CHECK_EQ(properties_file_close(file), ERROR_NONE);
|
||||
}
|
||||
|
||||
TEST_CASE("properties_file_close reports ERROR_RESOURCE when the parent directory doesn't exist") {
|
||||
const char* path = "/tmp/tactility_kernel_properties_file_test_missing_dir/settings.properties";
|
||||
std::remove(path); // no-op if the directory doesn't exist, which is the point of this test
|
||||
|
||||
// Missing directory is not an error for open() - it starts out empty, same as a missing
|
||||
// file (see the "starts out empty" test above).
|
||||
PropertiesFile* file = properties_file_open(path);
|
||||
REQUIRE_NE(file, nullptr);
|
||||
properties_file_set(file, "key", "value");
|
||||
|
||||
// close()'s save can't create its temp file in a directory that doesn't exist.
|
||||
CHECK_EQ(properties_file_close(file), ERROR_RESOURCE);
|
||||
}
|
||||
|
||||
TEST_CASE("a failed close leaves previously-saved content on disk untouched") {
|
||||
if (geteuid() == 0) {
|
||||
// Root bypasses directory write permissions, so the read-only directory below would
|
||||
// not make save_to_file() fail.
|
||||
return;
|
||||
}
|
||||
|
||||
const char* dir = "/tmp/tactility_kernel_properties_file_readonly_test";
|
||||
const char* path = "/tmp/tactility_kernel_properties_file_readonly_test/settings.properties";
|
||||
|
||||
mkdir(dir, 0700);
|
||||
chmod(dir, 0700);
|
||||
std::remove(path);
|
||||
|
||||
{
|
||||
PropertiesFile* file = properties_file_open(path);
|
||||
properties_file_set(file, "key", "original");
|
||||
REQUIRE_EQ(properties_file_close(file), ERROR_NONE);
|
||||
}
|
||||
|
||||
// Read-only directory - save_to_file()'s temp file can't be created there, so the close
|
||||
// below must fail without disturbing the "original" content already on disk.
|
||||
REQUIRE_EQ(chmod(dir, 0555), 0);
|
||||
|
||||
PropertiesFile* file = properties_file_open(path);
|
||||
properties_file_set(file, "key", "corrupted");
|
||||
CHECK_EQ(properties_file_close(file), ERROR_RESOURCE);
|
||||
|
||||
chmod(dir, 0700); // restore write access for the check below and for cleanup
|
||||
|
||||
PropertiesFile* reloaded = properties_file_open(path);
|
||||
char buffer[32];
|
||||
CHECK_EQ(properties_file_get(reloaded, "key", buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "original"), 0);
|
||||
properties_file_close(reloaded);
|
||||
|
||||
std::remove(path);
|
||||
rmdir(dir);
|
||||
}
|
||||
|
||||
TEST_CASE("properties_file_for_each visits every key exactly once") {
|
||||
ScratchFile scratch;
|
||||
|
||||
PropertiesFile* file = properties_file_open(TEST_PATH);
|
||||
properties_file_set(file, "a", "1");
|
||||
properties_file_set(file, "b", "2");
|
||||
properties_file_set(file, "c", "3");
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> seen;
|
||||
properties_file_for_each(file, [](const char* key, const char* value, void* context) {
|
||||
auto* out = static_cast<std::vector<std::pair<std::string, std::string>>*>(context);
|
||||
out->emplace_back(key, value);
|
||||
}, &seen);
|
||||
|
||||
CHECK_EQ(seen.size(), 3);
|
||||
for (const auto& [key, value] : seen) {
|
||||
if (key == "a") CHECK_EQ(value, "1");
|
||||
else if (key == "b") CHECK_EQ(value, "2");
|
||||
else if (key == "c") CHECK_EQ(value, "3");
|
||||
else FAIL("unexpected key: " << key);
|
||||
}
|
||||
|
||||
properties_file_close(file);
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/system_event.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
// system_event_emit() snapshots matching subscriptions under the lock, then invokes them
|
||||
// after unlocking (see the @warning on system_event_subscribe() in system_event.h), so a
|
||||
// callback calling system_event_subscribe()/_unsubscribe()/_emit() must not deadlock -
|
||||
// after unlocking (see the @warning on system_event_callback_add() in system_event.h), so a
|
||||
// callback calling system_event_callback_add()/_unsubscribe()/_emit() must not deadlock -
|
||||
// covered below, mirroring DeviceListenerTest.cpp's reentrancy test.
|
||||
|
||||
struct RecordedCall {
|
||||
void* context;
|
||||
SystemEventType type;
|
||||
const void* data;
|
||||
size_t data_len;
|
||||
// Copied out of event->data during the callback - event->data is only valid for the
|
||||
// duration of the callback (it lives in system_event_emit()'s own stack frame), so a bare
|
||||
// pointer/length pair recorded here would dangle by the time a TEST_CASE inspects it.
|
||||
std::vector<uint8_t> data;
|
||||
uint64_t timestamp;
|
||||
};
|
||||
|
||||
@@ -22,11 +26,11 @@ static std::vector<RecordedCall> calls_a;
|
||||
static std::vector<RecordedCall> calls_b;
|
||||
|
||||
static void listener_a(SystemEvent* event, void* context) {
|
||||
calls_a.push_back({ context, event->type, event->data, event->data_len, event->timestamp });
|
||||
calls_a.push_back({ context, event->type, std::vector<uint8_t>(event->data, event->data + event->data_len), event->timestamp });
|
||||
}
|
||||
|
||||
static void listener_b(SystemEvent* event, void* context) {
|
||||
calls_b.push_back({ context, event->type, event->data, event->data_len, event->timestamp });
|
||||
calls_b.push_back({ context, event->type, std::vector<uint8_t>(event->data, event->data + event->data_len), event->timestamp });
|
||||
}
|
||||
|
||||
static void reset_calls() {
|
||||
@@ -39,8 +43,8 @@ TEST_CASE("system_event_emit invokes every subscriber registered for that type")
|
||||
int context_a = 1;
|
||||
int context_b = 2;
|
||||
|
||||
CHECK_EQ(system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a), ERROR_NONE);
|
||||
CHECK_EQ(system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b, &context_b), ERROR_NONE);
|
||||
CHECK_EQ(system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a), ERROR_NONE);
|
||||
CHECK_EQ(system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_b, &context_b), ERROR_NONE);
|
||||
|
||||
CHECK_EQ(system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0), ERROR_NONE);
|
||||
|
||||
@@ -51,14 +55,14 @@ TEST_CASE("system_event_emit invokes every subscriber registered for that type")
|
||||
REQUIRE_EQ(calls_b.size(), 1);
|
||||
CHECK_EQ(calls_b[0].context, &context_b);
|
||||
|
||||
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
|
||||
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
|
||||
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
|
||||
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_emit only invokes subscribers registered for the emitted type") {
|
||||
reset_calls();
|
||||
int context_a = 1;
|
||||
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
|
||||
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
|
||||
|
||||
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
|
||||
CHECK_EQ(calls_a.size(), 0);
|
||||
@@ -66,80 +70,78 @@ TEST_CASE("system_event_emit only invokes subscribers registered for the emitted
|
||||
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
|
||||
CHECK_EQ(calls_a.size(), 1);
|
||||
|
||||
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
|
||||
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_emit passes the data pointer and length through unchanged") {
|
||||
TEST_CASE("system_event_emit copies the data into the delivered event") {
|
||||
reset_calls();
|
||||
int context_a = 1;
|
||||
struct Payload { int value; } payload { 42 };
|
||||
|
||||
system_event_subscribe(KERNEL_EVENT_TIME_CHANGED, listener_a, &context_a);
|
||||
system_event_callback_add(KERNEL_EVENT_TIME_CHANGED, listener_a, &context_a);
|
||||
system_event_emit(KERNEL_EVENT_TIME_CHANGED, &payload, sizeof(payload));
|
||||
|
||||
REQUIRE_EQ(calls_a.size(), 1);
|
||||
CHECK_EQ(calls_a[0].data, &payload);
|
||||
CHECK_EQ(calls_a[0].data_len, sizeof(payload));
|
||||
CHECK_EQ(static_cast<const Payload*>(calls_a[0].data)->value, 42);
|
||||
REQUIRE_EQ(calls_a[0].data.size(), sizeof(payload));
|
||||
CHECK_EQ(reinterpret_cast<const Payload*>(calls_a[0].data.data())->value, 42);
|
||||
|
||||
system_event_unsubscribe(KERNEL_EVENT_TIME_CHANGED, listener_a);
|
||||
system_event_callback_remove(KERNEL_EVENT_TIME_CHANGED, listener_a);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_emit with no data passes a null pointer and zero length") {
|
||||
TEST_CASE("system_event_emit with no data delivers an empty payload") {
|
||||
reset_calls();
|
||||
int context_a = 1;
|
||||
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
|
||||
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
|
||||
|
||||
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
|
||||
|
||||
REQUIRE_EQ(calls_a.size(), 1);
|
||||
CHECK_EQ(calls_a[0].data, nullptr);
|
||||
CHECK_EQ(calls_a[0].data_len, 0);
|
||||
CHECK(calls_a[0].data.empty());
|
||||
|
||||
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
|
||||
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_unsubscribe stops further notifications for that callback only") {
|
||||
TEST_CASE("system_event_callback_remove stops further notifications for that callback only") {
|
||||
reset_calls();
|
||||
int context_a = 1;
|
||||
int context_b = 2;
|
||||
|
||||
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
|
||||
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b, &context_b);
|
||||
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
|
||||
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_b, &context_b);
|
||||
|
||||
CHECK_EQ(system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a), ERROR_NONE);
|
||||
CHECK_EQ(system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a), ERROR_NONE);
|
||||
|
||||
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
|
||||
|
||||
CHECK_EQ(calls_a.size(), 0);
|
||||
CHECK_EQ(calls_b.size(), 1);
|
||||
|
||||
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
|
||||
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_unsubscribe on an unregistered callback returns ERROR_NOT_FOUND and is a no-op") {
|
||||
TEST_CASE("system_event_callback_remove on an unregistered callback returns ERROR_NOT_FOUND and is a no-op") {
|
||||
reset_calls();
|
||||
int context_b = 2;
|
||||
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b, &context_b);
|
||||
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_b, &context_b);
|
||||
|
||||
// listener_a was never added for this type, so removing it must not disturb listener_b.
|
||||
CHECK_EQ(system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a), ERROR_NOT_FOUND);
|
||||
CHECK_EQ(system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a), ERROR_NOT_FOUND);
|
||||
|
||||
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
|
||||
CHECK_EQ(calls_b.size(), 1);
|
||||
|
||||
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
|
||||
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_unsubscribe matches on (type, callback), not the callback alone") {
|
||||
TEST_CASE("system_event_callback_remove matches on (type, callback), not the callback alone") {
|
||||
reset_calls();
|
||||
int context_a = 1;
|
||||
|
||||
// Same callback subscribed for two different event types.
|
||||
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
|
||||
system_event_subscribe(KERNEL_EVENT_TIME_CHANGED, listener_a, &context_a);
|
||||
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
|
||||
system_event_callback_add(KERNEL_EVENT_TIME_CHANGED, listener_a, &context_a);
|
||||
|
||||
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
|
||||
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
|
||||
|
||||
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
|
||||
CHECK_EQ(calls_a.size(), 0);
|
||||
@@ -147,7 +149,7 @@ TEST_CASE("system_event_unsubscribe matches on (type, callback), not the callbac
|
||||
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
|
||||
CHECK_EQ(calls_a.size(), 1);
|
||||
|
||||
system_event_unsubscribe(KERNEL_EVENT_TIME_CHANGED, listener_a);
|
||||
system_event_callback_remove(KERNEL_EVENT_TIME_CHANGED, listener_a);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_emit with no subscribers for that type returns ERROR_NONE") {
|
||||
@@ -157,7 +159,7 @@ TEST_CASE("system_event_emit with no subscribers for that type returns ERROR_NON
|
||||
TEST_CASE("system_event_emit stamps the event with the current boot-relative time") {
|
||||
reset_calls();
|
||||
int context_a = 1;
|
||||
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
|
||||
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a);
|
||||
|
||||
auto before = static_cast<uint64_t>(get_micros_since_boot());
|
||||
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
|
||||
@@ -167,22 +169,22 @@ TEST_CASE("system_event_emit stamps the event with the current boot-relative tim
|
||||
CHECK_GE(calls_a[0].timestamp, before);
|
||||
CHECK_LE(calls_a[0].timestamp, after);
|
||||
|
||||
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
|
||||
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a);
|
||||
}
|
||||
|
||||
static bool reentrant_add_triggered = false;
|
||||
|
||||
static void reentrant_listener(SystemEvent* event, void* context) {
|
||||
calls_a.push_back({ context, event->type, event->data, event->data_len, event->timestamp });
|
||||
calls_a.push_back({ context, event->type, std::vector<uint8_t>(event->data, event->data + event->data_len), event->timestamp });
|
||||
if (!reentrant_add_triggered) {
|
||||
reentrant_add_triggered = true;
|
||||
// Subscribing from within a notification must not deadlock: emit() releases the
|
||||
// lock before invoking callbacks, so this only blocks briefly on the (already
|
||||
// unlocked) mutex.
|
||||
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b, context);
|
||||
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_b, context);
|
||||
// Also exercise unsubscribe() and a nested emit() of a different type from within
|
||||
// a callback - all must complete without deadlocking.
|
||||
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, reentrant_listener);
|
||||
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, reentrant_listener);
|
||||
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
|
||||
}
|
||||
}
|
||||
@@ -192,8 +194,8 @@ TEST_CASE("system_event_emit is safe when a callback subscribes, unsubscribes an
|
||||
reentrant_add_triggered = false;
|
||||
int context_a = 1;
|
||||
|
||||
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, reentrant_listener, &context_a);
|
||||
system_event_subscribe(KERNEL_EVENT_TIME_CHANGED, listener_b, &context_a);
|
||||
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, reentrant_listener, &context_a);
|
||||
system_event_callback_add(KERNEL_EVENT_TIME_CHANGED, listener_b, &context_a);
|
||||
|
||||
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
|
||||
|
||||
@@ -210,6 +212,228 @@ TEST_CASE("system_event_emit is safe when a callback subscribes, unsubscribes an
|
||||
CHECK_EQ(calls_a.size(), 1);
|
||||
CHECK_EQ(calls_b.size(), 2);
|
||||
|
||||
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
|
||||
system_event_unsubscribe(KERNEL_EVENT_TIME_CHANGED, listener_b);
|
||||
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_b);
|
||||
system_event_callback_remove(KERNEL_EVENT_TIME_CHANGED, listener_b);
|
||||
}
|
||||
|
||||
// gps.h-style poll subscription: system_event_subscribe()/_await()/_unsubscribe().
|
||||
//
|
||||
// system_event_await() only detects sequence increments that happen *after* it starts
|
||||
// waiting (same as gps_api_event_await()), so the emit must be started from another task
|
||||
// while this one is already blocked in await() - emitting first and awaiting after would
|
||||
// race the notification the same way it would with any FreeRTOS task-notify consumer.
|
||||
|
||||
TEST_CASE("system_event_subscribe/_await deliver the event payload by value") {
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_NETWORK_CONNECTED;
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
|
||||
NetworkConnectedEvent connected { .device = nullptr, .ipv4_addr = 0x0A000001, .gateway = 0x0A0000FE };
|
||||
auto* thread = thread_alloc_full(
|
||||
"system-event-emitter",
|
||||
4096,
|
||||
[](void* context) {
|
||||
delay_millis(20);
|
||||
auto* connected_ptr = static_cast<NetworkConnectedEvent*>(context);
|
||||
system_event_emit(KERNEL_EVENT_NETWORK_CONNECTED, connected_ptr, sizeof(*connected_ptr));
|
||||
return 0;
|
||||
},
|
||||
&connected,
|
||||
-1
|
||||
);
|
||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||
|
||||
CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
|
||||
NetworkConnectedEvent received {};
|
||||
CHECK_EQ(system_event_get_data(&sub, reinterpret_cast<uint8_t*>(&received), sizeof(received)), ERROR_NONE);
|
||||
CHECK_EQ(received.ipv4_addr, connected.ipv4_addr);
|
||||
CHECK_EQ(received.gateway, connected.gateway);
|
||||
|
||||
CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE);
|
||||
thread_free(thread);
|
||||
|
||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
|
||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_await returns a matching event that arrived before it started waiting") {
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_NETWORK_CONNECTED;
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
|
||||
// Same-thread emit, no background thread needed: unlike the "detects a change after it
|
||||
// starts waiting" tests above, this is exactly the case system_event_await() must handle -
|
||||
// sequence already moved ahead of consumed_sequence before await() is even called.
|
||||
NetworkConnectedEvent connected { .device = nullptr, .ipv4_addr = 0x0A000001, .gateway = 0x0A0000FE };
|
||||
CHECK_EQ(system_event_emit(KERNEL_EVENT_NETWORK_CONNECTED, &connected, sizeof(connected)), ERROR_NONE);
|
||||
|
||||
CHECK_EQ(system_event_await(&sub, 0), ERROR_NONE);
|
||||
|
||||
NetworkConnectedEvent received {};
|
||||
CHECK_EQ(system_event_get_data(&sub, reinterpret_cast<uint8_t*>(&received), sizeof(received)), ERROR_NONE);
|
||||
CHECK_EQ(received.ipv4_addr, connected.ipv4_addr);
|
||||
CHECK_EQ(received.gateway, connected.gateway);
|
||||
|
||||
// The pending event was consumed by the call above - a second await() with no further
|
||||
// emit must time out rather than returning the same event again.
|
||||
CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT);
|
||||
|
||||
system_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_await times out when no matching event has arrived") {
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_TIME_CHANGED;
|
||||
system_event_subscribe(&sub);
|
||||
|
||||
CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT);
|
||||
|
||||
system_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_emit does not notify a poll subscriber of a different type") {
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_BOOT_COMPLETED;
|
||||
system_event_subscribe(&sub);
|
||||
|
||||
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
|
||||
CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT);
|
||||
|
||||
system_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_get_data reports ERROR_BUFFER_OVERFLOW and leaves the buffer untouched") {
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_NETWORK_DISCONNECTED;
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
|
||||
// system_event_await() only detects sequence increments that happen *after* it starts
|
||||
// waiting (see the comment above), so the emit must come from another task while this one
|
||||
// is already blocked in await() - same pattern as the payload-delivery test above.
|
||||
NetworkDisconnectedEvent disconnected { .device = nullptr };
|
||||
auto* thread = thread_alloc_full(
|
||||
"system-event-emitter",
|
||||
4096,
|
||||
[](void* context) {
|
||||
delay_millis(20);
|
||||
auto* disconnected_ptr = static_cast<NetworkDisconnectedEvent*>(context);
|
||||
system_event_emit(KERNEL_EVENT_NETWORK_DISCONNECTED, disconnected_ptr, sizeof(*disconnected_ptr));
|
||||
return 0;
|
||||
},
|
||||
&disconnected,
|
||||
-1
|
||||
);
|
||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||
CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE);
|
||||
thread_free(thread);
|
||||
|
||||
uint8_t tiny[1] = { 0xAA };
|
||||
CHECK_EQ(system_event_get_data(&sub, tiny, sizeof(tiny)), ERROR_BUFFER_OVERFLOW);
|
||||
CHECK_EQ(tiny[0], 0xAA);
|
||||
|
||||
uint8_t exact[sizeof(NetworkDisconnectedEvent)];
|
||||
CHECK_EQ(system_event_get_data(&sub, exact, sizeof(exact)), ERROR_NONE);
|
||||
|
||||
system_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("system_event_get_data on a subscription with no payload copies nothing and succeeds") {
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_BOOT_COMPLETED;
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
|
||||
auto* thread = thread_alloc_full(
|
||||
"system-event-emitter",
|
||||
4096,
|
||||
[](void*) {
|
||||
delay_millis(20);
|
||||
system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0);
|
||||
return 0;
|
||||
},
|
||||
nullptr,
|
||||
-1
|
||||
);
|
||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||
CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE);
|
||||
thread_free(thread);
|
||||
|
||||
uint8_t buffer[1] = { 0x42 };
|
||||
CHECK_EQ(system_event_get_data(&sub, buffer, 0), ERROR_NONE);
|
||||
CHECK_EQ(buffer[0], 0x42); // untouched - nothing to copy
|
||||
|
||||
system_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
// Regression coverage for system_event_unsubscribe() racing a task blocked in
|
||||
// system_event_await() on the same subscription, and for reusing a subscription node after
|
||||
// unsubscribing it - see the @warning on system_event_unsubscribe() in system_event.h.
|
||||
|
||||
TEST_CASE("system_event_unsubscribe wakes a task blocked in system_event_await with ERROR_INVALID_STATE") {
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_SERVICE_STARTED;
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
|
||||
auto* thread = thread_alloc_full(
|
||||
"system-event-awaiter",
|
||||
4096,
|
||||
[](void* context) {
|
||||
auto* awaited_sub = static_cast<SystemEventSubscription*>(context);
|
||||
// Long timeout - the point is that unsubscribe() wakes this early, not that it
|
||||
// eventually times out on its own.
|
||||
return static_cast<int32_t>(system_event_await(awaited_sub, pdMS_TO_TICKS(5000)));
|
||||
},
|
||||
&sub,
|
||||
-1
|
||||
);
|
||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||
|
||||
// Give the awaiter task a moment to actually reach xSemaphoreTake() before unsubscribing -
|
||||
// otherwise this test wouldn't exercise the "already blocked" race at all.
|
||||
delay_millis(20);
|
||||
|
||||
// Must return promptly (nudging the blocked awaiter awake), not by waiting out its timeout.
|
||||
TickType_t before = get_ticks();
|
||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
|
||||
CHECK_LT(get_ticks() - before, pdMS_TO_TICKS(1000));
|
||||
|
||||
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE);
|
||||
CHECK_EQ(thread_get_return_code(thread), ERROR_INVALID_STATE);
|
||||
thread_free(thread);
|
||||
|
||||
// A second unsubscribe() has nothing left to do.
|
||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
TEST_CASE("a subscription node can be re-subscribed after system_event_unsubscribe") {
|
||||
SystemEventSubscription sub {};
|
||||
sub.event.type = KERNEL_EVENT_SERVICE_STOPPED;
|
||||
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
|
||||
|
||||
// Re-registering the same node (same storage, not a fresh SystemEventSubscription) must
|
||||
// work as if it were new - a fresh semaphore, and no leftover `cancelled` state from the
|
||||
// unsubscribe() above causing an immediate spurious ERROR_INVALID_STATE below.
|
||||
CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE);
|
||||
|
||||
auto* thread = thread_alloc_full(
|
||||
"system-event-emitter",
|
||||
4096,
|
||||
[](void*) {
|
||||
delay_millis(20);
|
||||
system_event_emit(KERNEL_EVENT_SERVICE_STOPPED, nullptr, 0);
|
||||
return 0;
|
||||
},
|
||||
nullptr,
|
||||
-1
|
||||
);
|
||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||
CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE);
|
||||
thread_free(thread);
|
||||
|
||||
CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
project(AppModuleTests)
|
||||
|
||||
enable_language(C CXX ASM)
|
||||
|
||||
|
||||
file(GLOB_RECURSE TEST_SOURCES ${PROJECT_SOURCE_DIR}/Source/*.cpp)
|
||||
add_executable(AppModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
|
||||
|
||||
target_include_directories(AppModuleTests PRIVATE ${DOCTESTINC})
|
||||
|
||||
add_test(NAME AppModuleTests COMMAND AppModuleTests)
|
||||
|
||||
target_link_libraries(AppModuleTests PUBLIC
|
||||
TactilityKernel
|
||||
app-module
|
||||
service-module
|
||||
platform-posix
|
||||
freertos_kernel
|
||||
)
|
||||
@@ -0,0 +1,137 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <app/event.h>
|
||||
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
TEST_CASE("app_event_subscribe/_await deliver events in FIFO order") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 1;
|
||||
CHECK_EQ(app_event_subscribe(&sub), ERROR_NONE);
|
||||
|
||||
for (uint32_t i = 0; i < 3; i++) {
|
||||
AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = { .launch_id = i, .result = 0 } };
|
||||
CHECK_EQ(app_event_emit(1, &event), ERROR_NONE);
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < 3; i++) {
|
||||
AppEvent out {};
|
||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_NONE);
|
||||
CHECK_EQ(out.type, APP_EVENT_RESULT);
|
||||
CHECK_EQ(out.result.launch_id, i);
|
||||
}
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("app_event_emit only delivers to subscriptions for that app_instance_id") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 10;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
CHECK_EQ(app_event_emit(11, &event), ERROR_NOT_FOUND);
|
||||
|
||||
AppEvent out {};
|
||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_TIMEOUT);
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("app_event_emit returns ERROR_RESOURCE and drops the newest event once a subscription's queue is full") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 20;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
for (uint32_t i = 0; i < APP_EVENT_QUEUE_CAPACITY; i++) {
|
||||
AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = { .launch_id = i, .result = 0 } };
|
||||
CHECK_EQ(app_event_emit(20, &event), ERROR_NONE);
|
||||
}
|
||||
|
||||
// Queue is now full; this one should be dropped.
|
||||
AppEvent overflow_event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = { .launch_id = 999, .result = 0 } };
|
||||
CHECK_EQ(app_event_emit(20, &overflow_event), ERROR_RESOURCE);
|
||||
|
||||
// The already-queued events survive, in order, and the dropped one never arrives.
|
||||
for (uint32_t i = 0; i < APP_EVENT_QUEUE_CAPACITY; i++) {
|
||||
AppEvent out {};
|
||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_NONE);
|
||||
CHECK_EQ(out.result.launch_id, i);
|
||||
}
|
||||
AppEvent out {};
|
||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_TIMEOUT);
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("app_event_unsubscribe stops further delivery") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 30;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
CHECK_EQ(app_event_unsubscribe(&sub), ERROR_NONE);
|
||||
CHECK_EQ(app_event_unsubscribe(&sub), ERROR_NOT_FOUND);
|
||||
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
CHECK_EQ(app_event_emit(30, &event), ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
TEST_CASE("app_event_await times out when no event has arrived") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 40;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
AppEvent out {};
|
||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_TIMEOUT);
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("app_event_emit stamps the event with the current boot-relative time") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 50;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
auto before = static_cast<uint64_t>(get_micros_since_boot());
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(50, &event);
|
||||
auto after = static_cast<uint64_t>(get_micros_since_boot());
|
||||
|
||||
AppEvent out {};
|
||||
REQUIRE_EQ(app_event_await(&sub, &out, 0), ERROR_NONE);
|
||||
CHECK_GE(out.timestamp, before);
|
||||
CHECK_LE(out.timestamp, after);
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("app_event_await wakes when the event is emitted from another task") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 60;
|
||||
CHECK_EQ(app_event_subscribe(&sub), ERROR_NONE);
|
||||
|
||||
auto* thread = thread_alloc_full(
|
||||
"app-event-emitter",
|
||||
4096,
|
||||
[](void*) -> int32_t {
|
||||
delay_millis(20);
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(60, &event);
|
||||
return 0;
|
||||
},
|
||||
nullptr,
|
||||
-1
|
||||
);
|
||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||
|
||||
AppEvent out {};
|
||||
CHECK_EQ(app_event_await(&sub, &out, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(out.type, APP_EVENT_CLOSE);
|
||||
|
||||
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), 1), ERROR_NONE);
|
||||
thread_free(thread);
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/loader.h>
|
||||
#include <app/manager.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
extern ServiceManifest app_internal_loader_service_manifest;
|
||||
|
||||
namespace {
|
||||
|
||||
error_t fake_load(AppLocation, void** out_runtime) {
|
||||
*out_runtime = nullptr;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// Stashed by fake_run() on every call, for tests that need to verify exactly what argc/argv it
|
||||
// received (e.g. that app-module deep-copied the caller's argv) without a getter to query it
|
||||
// through.
|
||||
int last_received_argc = -1;
|
||||
std::vector<std::string> last_received_argv;
|
||||
|
||||
// fake_run() runs on the app's own task; stash_received_arguments() writes
|
||||
// last_received_argc/last_received_argv there while a test thread reads them - wait_for_state()
|
||||
// only establishes that the instance reached APP_INSTANCE_STATE_ACTIVE (set before
|
||||
// AppLoaderApi::run() is even called, i.e. before fake_run() runs at all), not that
|
||||
// stash_received_arguments() has finished writing. This flag is the actual ordering: reset
|
||||
// before starting the app, set (release) as the last step of stash_received_arguments(), waited
|
||||
// on (acquire) before a test reads the stashed values.
|
||||
std::atomic<bool> arguments_stashed { false };
|
||||
|
||||
void stash_received_arguments(int argc, char* argv[]) {
|
||||
last_received_argc = argc;
|
||||
last_received_argv.clear();
|
||||
for (int i = 0; i < argc; i++) {
|
||||
last_received_argv.emplace_back(argv[i]);
|
||||
}
|
||||
arguments_stashed.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// A minimal stand-in for a real app's main(): subscribes to its own app_event stream and exits
|
||||
// as soon as it's asked to close - exactly the contract every app instance (with its own
|
||||
// dedicated task for its whole lifetime) is expected to follow. If launched with a single
|
||||
// parameter (app_manager_start_for_result()), acts as a modal dialog instead: returns the
|
||||
// requested result (argv[0], parsed as an int) immediately (the app's own return value IS the
|
||||
// delivered APP_EVENT_RESULT.result - see app_scheduler.cpp's thread_main()).
|
||||
int32_t fake_run(void*, uint32_t app_instance_id, int argc, char* argv[]) {
|
||||
stash_received_arguments(argc, argv);
|
||||
|
||||
if (argc == 1) {
|
||||
// Single-arg shortcut used by the start_for_result() result-delivery tests: returns
|
||||
// immediately with argv[0] parsed as the result code, instead of running the normal
|
||||
// event loop below. Tests that pass other argc (0, or >1 to check deep-copy) fall
|
||||
// through and run the loop as usual.
|
||||
return static_cast<int32_t>(strtol(argv[0], nullptr, 10));
|
||||
}
|
||||
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = app_instance_id;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
while (true) {
|
||||
AppEvent event {};
|
||||
if (app_event_await(&sub, &event, pdMS_TO_TICKS(5000)) != ERROR_NONE) {
|
||||
break; // safety net so a bug here can't hang the test suite
|
||||
}
|
||||
if (event.type == APP_EVENT_CLOSE) {
|
||||
app_manager_finish(app_instance_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void fake_unload(void*) {
|
||||
}
|
||||
|
||||
AppLoaderApi fake_loader_api = {
|
||||
.load = fake_load,
|
||||
.run = fake_run,
|
||||
.unload = fake_unload,
|
||||
};
|
||||
|
||||
void* create_loader_service(const ServiceManifest*) {
|
||||
return &fake_loader_api;
|
||||
}
|
||||
|
||||
void destroy_loader_service(const ServiceManifest*, void*) {
|
||||
}
|
||||
|
||||
ServiceManifest fake_loader_manifest = {
|
||||
.id = APP_LOADER_PATH_SERVICE_ID,
|
||||
.create_service = create_loader_service,
|
||||
.destroy_service = destroy_loader_service,
|
||||
.on_start = nullptr,
|
||||
.on_stop = nullptr,
|
||||
};
|
||||
|
||||
void ensure_fake_loader_registered() {
|
||||
static bool registered = false;
|
||||
if (!registered) {
|
||||
CHECK_EQ(service_manager_add(&fake_loader_manifest, /*auto_start=*/true), ERROR_NONE);
|
||||
registered = true;
|
||||
}
|
||||
}
|
||||
|
||||
// app-module's real APP_LOCATION_MEMORY loader (source/app_internal_loader.cpp) - not a fake,
|
||||
// since it has no platform dependency and is exactly what a statically-linked app would go
|
||||
// through.
|
||||
void ensure_memory_loader_registered() {
|
||||
static bool registered = false;
|
||||
if (!registered) {
|
||||
CHECK_EQ(service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true), ERROR_NONE);
|
||||
registered = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Same subscribe-until-close contract as fake_run() above, but called directly as an AppMainFn -
|
||||
// this is what a real internal app's entry point looks like.
|
||||
int32_t fake_app_main(uint32_t app_instance_id, int argc, char* argv[]) {
|
||||
return fake_run(nullptr, app_instance_id, argc, argv);
|
||||
}
|
||||
|
||||
// Wraps app_manager_get_topmost_instance_id() for terse assertions: 0 if no app is Active.
|
||||
AppInstanceId topmost_instance_id() {
|
||||
AppInstanceId id = 0;
|
||||
return app_manager_get_topmost_instance_id(&id) == ERROR_NONE ? id : 0;
|
||||
}
|
||||
|
||||
bool wait_for_state(uint32_t instance_id, AppInstanceState target, uint32_t timeout_ms) {
|
||||
uint32_t waited = 0;
|
||||
while (waited < timeout_ms) {
|
||||
if (app_manager_get_state(instance_id) == target) {
|
||||
return true;
|
||||
}
|
||||
delay_millis(10);
|
||||
waited += 10;
|
||||
}
|
||||
return app_manager_get_state(instance_id) == target;
|
||||
}
|
||||
|
||||
bool wait_for_arguments_stashed(uint32_t timeout_ms) {
|
||||
uint32_t waited = 0;
|
||||
while (waited < timeout_ms) {
|
||||
if (arguments_stashed.load(std::memory_order_acquire)) {
|
||||
return true;
|
||||
}
|
||||
delay_millis(10);
|
||||
waited += 10;
|
||||
}
|
||||
return arguments_stashed.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("app_manager_start activates an app instance, app_manager_stop terminates it") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
AppManifest manifest { "test.app.a", "Test App A", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
|
||||
uint32_t instance_id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.a", &instance_id), ERROR_NONE);
|
||||
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE);
|
||||
CHECK_EQ(app_manager_get_state(instance_id), APP_INSTANCE_STATE_STOPPED);
|
||||
|
||||
app_manager_remove("test.app.a");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start never touches another already-running app - every instance gets its own task") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
AppManifest manifest_b { "test.app.b", "Test App B", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
AppManifest manifest_c { "test.app.c", "Test App C", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest_b), ERROR_NONE);
|
||||
REQUIRE_EQ(app_manager_add(&manifest_c), ERROR_NONE);
|
||||
|
||||
uint32_t id_b = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.b", &id_b), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_b, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
uint32_t id_c = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.c", &id_c), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_c, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
// b is untouched by c starting - both stay Active at once, each with its own task.
|
||||
CHECK_EQ(app_manager_get_state(id_b), APP_INSTANCE_STATE_ACTIVE);
|
||||
|
||||
app_manager_stop(id_b);
|
||||
app_manager_stop(id_c);
|
||||
app_manager_remove("test.app.b");
|
||||
app_manager_remove("test.app.c");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start always creates a fresh instance, even for the same manifest id twice") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
AppManifest manifest { "test.app.twice", "Test App Twice", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
|
||||
uint32_t id_first = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.twice", &id_first), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_first, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
uint32_t id_second = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.twice", &id_second), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_second, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
CHECK_NE(id_first, id_second);
|
||||
CHECK_EQ(app_manager_get_state(id_first), APP_INSTANCE_STATE_ACTIVE);
|
||||
|
||||
app_manager_stop(id_first);
|
||||
app_manager_stop(id_second);
|
||||
app_manager_remove("test.app.twice");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_get_state returns STOPPED for an unknown instance id") {
|
||||
CHECK_EQ(app_manager_get_state(999999), APP_INSTANCE_STATE_STOPPED);
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start_with_parameters deep-copies argv before the app instance receives it") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
AppManifest manifest { "test.app.args", "Test App Args", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
|
||||
uint32_t instance_id = 0;
|
||||
arguments_stashed.store(false, std::memory_order_relaxed);
|
||||
{
|
||||
// Caller's argv is stack-local and goes out of scope immediately after this block -
|
||||
// proves app-module made its own copy rather than aliasing the caller's strings.
|
||||
std::string ssid = "MyNetwork";
|
||||
std::string password = "hunter2";
|
||||
const char* argv[] = { ssid.c_str(), password.c_str() };
|
||||
REQUIRE_EQ(app_manager_start_with_parameters("test.app.args", 2, argv, &instance_id), ERROR_NONE);
|
||||
}
|
||||
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
REQUIRE(wait_for_arguments_stashed(1000));
|
||||
|
||||
REQUIRE_EQ(last_received_argc, 2);
|
||||
REQUIRE_EQ(last_received_argv.size(), 2u);
|
||||
CHECK_EQ(last_received_argv[0], "MyNetwork");
|
||||
CHECK_EQ(last_received_argv[1], "hunter2");
|
||||
|
||||
app_manager_stop(instance_id);
|
||||
app_manager_remove("test.app.args");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_add rejects a duplicate id") {
|
||||
AppManifest manifest { "test.app.dup", "Test App Dup", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
CHECK_EQ(app_manager_add(&manifest), ERROR_INVALID_ARGUMENT);
|
||||
app_manager_remove("test.app.dup");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_for_each_manifest visits every registered manifest, including newly added ones") {
|
||||
AppManifest manifest_x { "test.app.foreach.x", "X", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
AppManifest manifest_y { "test.app.foreach.y", "Y", APP_CATEGORY_SETTINGS, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest_x), ERROR_NONE);
|
||||
REQUIRE_EQ(app_manager_add(&manifest_y), ERROR_NONE);
|
||||
|
||||
std::vector<std::string> seen_ids;
|
||||
app_manager_for_each_manifest([](const AppManifest* manifest, void* context) {
|
||||
static_cast<std::vector<std::string>*>(context)->emplace_back(manifest->id);
|
||||
}, &seen_ids);
|
||||
|
||||
CHECK(std::ranges::find(seen_ids, "test.app.foreach.x") != seen_ids.end());
|
||||
CHECK(std::ranges::find(seen_ids, "test.app.foreach.y") != seen_ids.end());
|
||||
|
||||
app_manager_remove("test.app.foreach.x");
|
||||
app_manager_remove("test.app.foreach.y");
|
||||
|
||||
seen_ids.clear();
|
||||
app_manager_for_each_manifest([](const AppManifest* manifest, void* context) {
|
||||
static_cast<std::vector<std::string>*>(context)->emplace_back(manifest->id);
|
||||
}, &seen_ids);
|
||||
CHECK(std::ranges::find(seen_ids, "test.app.foreach.x") == seen_ids.end());
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start fails for an unregistered manifest id") {
|
||||
uint32_t instance_id = 0;
|
||||
CHECK_EQ(app_manager_start("test.app.nonexistent", &instance_id), ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start runs an APP_LOCATION_MEMORY app via its function pointer, through the real internal loader") {
|
||||
ensure_memory_loader_registered();
|
||||
|
||||
AppManifest manifest {
|
||||
"test.app.memory",
|
||||
"Test App Memory",
|
||||
APP_CATEGORY_USER,
|
||||
{ APP_LOCATION_MEMORY, reinterpret_cast<void*>(fake_app_main) }
|
||||
};
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
|
||||
uint32_t instance_id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.memory", &instance_id), ERROR_NONE);
|
||||
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE);
|
||||
CHECK_EQ(app_manager_get_state(instance_id), APP_INSTANCE_STATE_STOPPED);
|
||||
|
||||
app_manager_remove("test.app.memory");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start_for_result delivers APP_EVENT_RESULT to the parent, which stays Active throughout") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
AppManifest parent_manifest { "test.app.parent", "Parent", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
AppManifest child_manifest { "test.app.child", "Child", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
|
||||
REQUIRE_EQ(app_manager_add(&child_manifest), ERROR_NONE);
|
||||
|
||||
uint32_t parent_id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.parent", &parent_id), ERROR_NONE);
|
||||
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
AppEventSubscription parent_sub {};
|
||||
parent_sub.app_instance_id = parent_id;
|
||||
REQUIRE_EQ(app_event_subscribe(&parent_sub), ERROR_NONE);
|
||||
|
||||
const char* argv[] = { "42" };
|
||||
uint32_t child_id = 0;
|
||||
REQUIRE_EQ(app_manager_start_for_result("test.app.child", parent_id, 1, argv, &child_id), ERROR_NONE);
|
||||
|
||||
// Launching a modal child never touches the parent's own task/state.
|
||||
CHECK_EQ(app_manager_get_state(parent_id), APP_INSTANCE_STATE_ACTIVE);
|
||||
|
||||
AppEvent event {};
|
||||
REQUIRE_EQ(app_event_await(&parent_sub, &event, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(event.type, APP_EVENT_RESULT);
|
||||
CHECK_EQ(event.result.launch_id, child_id);
|
||||
CHECK_EQ(event.result.result, 42);
|
||||
|
||||
app_event_unsubscribe(&parent_sub);
|
||||
app_manager_stop(child_id);
|
||||
app_manager_stop(parent_id);
|
||||
app_manager_remove("test.app.parent");
|
||||
app_manager_remove("test.app.child");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start_for_result delivers the child's own return value as the result") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
AppManifest parent_manifest { "test.app.parent2", "Parent2", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
AppManifest child_manifest { "test.app.child2", "Child2", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
|
||||
REQUIRE_EQ(app_manager_add(&child_manifest), ERROR_NONE);
|
||||
|
||||
uint32_t parent_id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.parent2", &parent_id), ERROR_NONE);
|
||||
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
AppEventSubscription parent_sub {};
|
||||
parent_sub.app_instance_id = parent_id;
|
||||
REQUIRE_EQ(app_event_subscribe(&parent_sub), ERROR_NONE);
|
||||
|
||||
uint32_t child_id = 0;
|
||||
// No parameters - fake_run falls through to its normal CLOSE loop instead of acting as a
|
||||
// dialog.
|
||||
REQUIRE_EQ(app_manager_start_for_result("test.app.child2", parent_id, 0, nullptr, &child_id), ERROR_NONE);
|
||||
CHECK(wait_for_state(child_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
app_manager_stop(child_id); // force-close
|
||||
|
||||
AppEvent event {};
|
||||
REQUIRE_EQ(app_event_await(&parent_sub, &event, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(event.type, APP_EVENT_RESULT);
|
||||
CHECK_EQ(event.result.launch_id, child_id);
|
||||
CHECK_EQ(event.result.result, 0); // fake_run's CLOSE loop always returns 0
|
||||
|
||||
app_event_unsubscribe(&parent_sub);
|
||||
app_manager_stop(parent_id);
|
||||
app_manager_remove("test.app.parent2");
|
||||
app_manager_remove("test.app.child2");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_get_topmost_instance_id returns NOT_FOUND when nothing is active, then tracks who's on top") {
|
||||
ensure_fake_loader_registered();
|
||||
AppInstanceId id = 999999;
|
||||
CHECK_EQ(app_manager_get_topmost_instance_id(&id), ERROR_NOT_FOUND);
|
||||
|
||||
AppManifest manifest_a { "test.app.top_a", "A", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
AppManifest manifest_b { "test.app.top_b", "B", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest_a), ERROR_NONE);
|
||||
REQUIRE_EQ(app_manager_add(&manifest_b), ERROR_NONE);
|
||||
|
||||
uint32_t id_a = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.top_a", &id_a), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_a, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
CHECK_EQ(topmost_instance_id(), id_a);
|
||||
|
||||
// a stays Active - b just has a higher (more recently allocated) instance id, so it becomes
|
||||
// topmost without a superseding/saving.
|
||||
uint32_t id_b = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.top_b", &id_b), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_b, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
CHECK_EQ(topmost_instance_id(), id_b);
|
||||
|
||||
char app_id_buffer[64];
|
||||
REQUIRE_EQ(app_manager_get_topmost_app_id(app_id_buffer, sizeof(app_id_buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::string(app_id_buffer), "test.app.top_b");
|
||||
|
||||
// A modal child stays Active alongside its parent while shown - the child (started more
|
||||
// recently) must be reported as topmost, not the parent. No parameters, so fake_run() takes
|
||||
// its persistent CLOSE loop branch instead of instantly resolving like a real dialog would -
|
||||
// needed here so there's a reliable window to observe it as topmost.
|
||||
uint32_t id_c = 0;
|
||||
REQUIRE_EQ(app_manager_start_for_result("test.app.top_a", id_b, 0, nullptr, &id_c), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_c, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
CHECK_EQ(topmost_instance_id(), id_c);
|
||||
|
||||
app_manager_stop(id_c);
|
||||
CHECK_EQ(topmost_instance_id(), id_b);
|
||||
|
||||
app_manager_stop(id_a);
|
||||
app_manager_stop(id_b);
|
||||
app_manager_remove("test.app.top_a");
|
||||
app_manager_remove("test.app.top_b");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_get_topmost_app_id returns BUFFER_OVERFLOW for a too-small buffer, NOT_FOUND when nothing is active") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
char buffer[4];
|
||||
CHECK_EQ(app_manager_get_topmost_app_id(buffer, sizeof(buffer)), ERROR_NOT_FOUND);
|
||||
CHECK_EQ(app_manager_get_topmost_app_id(buffer, 0), ERROR_BUFFER_OVERFLOW);
|
||||
|
||||
AppManifest manifest { "test.app.top_overflow", "Overflow", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
|
||||
uint32_t id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.top_overflow", &id), ERROR_NONE);
|
||||
CHECK(wait_for_state(id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
// "test.app.top_overflow" doesn't fit in a 4-byte buffer.
|
||||
CHECK_EQ(app_manager_get_topmost_app_id(buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
|
||||
|
||||
app_manager_stop(id);
|
||||
app_manager_remove("test.app.top_overflow");
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#define DOCTEST_CONFIG_IMPLEMENT
|
||||
#include "doctest.h"
|
||||
#include <cassert>
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
|
||||
typedef struct {
|
||||
int argc;
|
||||
char** argv;
|
||||
int result;
|
||||
} TestTaskData;
|
||||
|
||||
void test_task(void* parameter) {
|
||||
auto* data = (TestTaskData*)parameter;
|
||||
|
||||
doctest::Context context;
|
||||
|
||||
context.applyCommandLine(data->argc, data->argv);
|
||||
|
||||
// overrides
|
||||
context.setOption("no-breaks", true); // don't break in the debugger when assertions fail
|
||||
|
||||
data->result = context.run();
|
||||
|
||||
vTaskEndScheduler();
|
||||
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
TestTaskData data = {
|
||||
.argc = argc,
|
||||
.argv = argv,
|
||||
.result = 0
|
||||
};
|
||||
|
||||
BaseType_t task_result = xTaskCreate(
|
||||
test_task,
|
||||
"test_task",
|
||||
8192,
|
||||
&data,
|
||||
1,
|
||||
nullptr
|
||||
);
|
||||
|
||||
if (task_result != pdPASS) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
vTaskStartScheduler();
|
||||
|
||||
return data.result;
|
||||
}
|
||||
Reference in New Issue
Block a user