Implement posix app loading (#643)
- Added POSIX desktop support for SDK builds, application packaging, and integration testing. - Added POSIX filesystem partitions and improved application path handling. - Added simulator support for loading and running applications dynamically. - Improved simulator task stack handling and display startup reliability. - Improved simulator display scaling, resizing, high-DPI support, and pointer accuracy. - Fixed LVGL timers and input polling on POSIX. (fixes simulator with Linux on some Intel graphics platforms) - Standardized data paths across platforms. - Logging now works via separate task: this allows apps to write to log without it affecting their stdout (for apps that output text as relevant date for other apps, like the File Selection app) - Fixes for app stdio
This commit is contained in:
committed by
GitHub
parent
d3656bcd3d
commit
643cbc3806
@@ -0,0 +1,37 @@
|
||||
project(AppPosixModuleTests)
|
||||
|
||||
enable_language(C CXX ASM)
|
||||
|
||||
# Fixture: a tiny shared object with a main() the test dlopen()s through app-posix-module's real
|
||||
# loader service. Deliberately not linked against app-module, so its call into
|
||||
# app_scheduler_current_app_id() stays undefined until dlopen() resolves it against
|
||||
# AppPosixModuleTests's own copy, exactly like a real Tactility-embedded app would resolve
|
||||
# against the running Tactility binary.
|
||||
add_library(app_posix_module_test_fixture SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_LIST_DIR}/fixtures/fixture_app.cpp)
|
||||
target_include_directories(app_posix_module_test_fixture PRIVATE ${CMAKE_SOURCE_DIR}/Modules/app-module/include)
|
||||
set_target_properties(app_posix_module_test_fixture PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
|
||||
add_executable(AppPosixModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
|
||||
add_dependencies(AppPosixModuleTests app_posix_module_test_fixture)
|
||||
|
||||
target_include_directories(AppPosixModuleTests PRIVATE ${DOCTESTINC})
|
||||
target_compile_definitions(AppPosixModuleTests PRIVATE
|
||||
FIXTURE_APP_PATH="$<TARGET_FILE:app_posix_module_test_fixture>"
|
||||
)
|
||||
|
||||
add_test(NAME AppPosixModuleTests COMMAND AppPosixModuleTests)
|
||||
|
||||
target_link_libraries(AppPosixModuleTests PUBLIC
|
||||
TactilityKernel
|
||||
app-module
|
||||
app-posix-module
|
||||
service-module
|
||||
platform-posix
|
||||
freertos_kernel
|
||||
${CMAKE_DL_LIBS}
|
||||
)
|
||||
# So the fixture's undefined app_scheduler_current_app_id() reference can resolve against this
|
||||
# test binary's own copy at dlopen() time. See app-posix-module's own ENABLE_EXPORTS comment on
|
||||
# the real Tactility executable for why this is needed.
|
||||
set_target_properties(AppPosixModuleTests PROPERTIES ENABLE_EXPORTS ON)
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Deliberately not linked against app-module: app_scheduler_current_app_id() stays an undefined
|
||||
// symbol in this .so, resolved at dlopen() time against the loading process's own copy. Proves
|
||||
// app-posix-module's loader lets a loaded app call straight back into Tactility without linking
|
||||
// its own copy of it.
|
||||
extern "C" int32_t main(int, char*[]) {
|
||||
return static_cast<int32_t>(app_scheduler_current_app_id());
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "doctest.h"
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/loader.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/delay.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
extern ServiceManifest loader_service_manifest; // app-posix-module's own
|
||||
extern ServiceManifest app_internal_loader_service_manifest; // app-module's real memory loader
|
||||
|
||||
namespace {
|
||||
|
||||
void ensure_path_loader_registered() {
|
||||
if (service_manager_find_instance(APP_LOADER_PATH_SERVICE_ID) == nullptr) {
|
||||
service_manager_add(&loader_service_manifest, /*auto_start=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
void ensure_memory_loader_registered() {
|
||||
if (service_manager_find_instance(APP_LOADER_MEMORY_SERVICE_ID) == nullptr) {
|
||||
service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
bool wait_for_state(AppInstanceId id, AppInstanceState target, uint32_t timeout_ms) {
|
||||
uint32_t waited = 0;
|
||||
while (waited < timeout_ms) {
|
||||
if (app_manager_get_state(id) == target) {
|
||||
return true;
|
||||
}
|
||||
delay_millis(10);
|
||||
waited += 10;
|
||||
}
|
||||
return app_manager_get_state(id) == target;
|
||||
}
|
||||
|
||||
std::atomic<int32_t> g_fixture_result { -1 };
|
||||
std::atomic<bool> g_fixture_result_received { false };
|
||||
|
||||
// Starts the dlopen()ed fixture as its own modal child and stashes its returned result, so the
|
||||
// test can inspect that result from the (in-process, directly readable) parent's own task.
|
||||
int32_t parent_app_main(int, char*[]) {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
AppEventSubscription sub {};
|
||||
app_event_subscribe(&sub, &event_group);
|
||||
|
||||
AppInstanceId self_id = app_scheduler_current_app_id();
|
||||
|
||||
AppManifest fixture_manifest {
|
||||
"test.posix.fixture", "Fixture", APP_CATEGORY_USER,
|
||||
{ APP_LOCATION_PATH, const_cast<char*>(FIXTURE_APP_PATH) }
|
||||
};
|
||||
app_manager_add(&fixture_manifest);
|
||||
|
||||
AppInstanceId fixture_id = 0;
|
||||
app_manager_start_for_result("test.posix.fixture", self_id, 0, nullptr, &fixture_id);
|
||||
|
||||
while (true) {
|
||||
if (task_event_group_wait_any(&event_group, nullptr, pdMS_TO_TICKS(5000)) != ERROR_NONE) {
|
||||
break; // safety net so a bug here can't hang the test suite
|
||||
}
|
||||
AppEvent event {};
|
||||
bool got_result = false;
|
||||
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||
if (event.type == APP_EVENT_RESULT && event.result.launch_id == fixture_id) {
|
||||
g_fixture_result.store(event.result.result, std::memory_order_release);
|
||||
got_result = true;
|
||||
}
|
||||
}
|
||||
if (got_result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
g_fixture_result_received.store(true, std::memory_order_release);
|
||||
|
||||
app_manager_remove("test.posix.fixture");
|
||||
app_event_unsubscribe(&sub);
|
||||
task_event_group_destruct(&event_group);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("app-posix-module's loader-path service dlopen()s a .so and calls its main(), which resolves a real Tactility symbol against the host") {
|
||||
ensure_path_loader_registered();
|
||||
ensure_memory_loader_registered();
|
||||
g_fixture_result.store(-1, std::memory_order_relaxed);
|
||||
g_fixture_result_received.store(false, std::memory_order_relaxed);
|
||||
|
||||
AppManifest parent_manifest { "test.posix.parent", "Parent", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(parent_app_main) } };
|
||||
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
|
||||
|
||||
AppInstanceId parent_id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.posix.parent", &parent_id), ERROR_NONE);
|
||||
REQUIRE(wait_for_state(parent_id, APP_INSTANCE_STATE_STOPPED, 3000));
|
||||
|
||||
CHECK(g_fixture_result_received.load(std::memory_order_acquire));
|
||||
// A positive AppInstanceId proves the fixture's dlopen()ed main() actually resolved and
|
||||
// called app_scheduler_current_app_id() against the host process, not just "ran and returned
|
||||
// a hardcoded value" - 0 would mean it thought it wasn't running as an app instance at all.
|
||||
CHECK_GT(g_fixture_result.load(std::memory_order_acquire), 0);
|
||||
|
||||
app_manager_remove("test.posix.parent");
|
||||
}
|
||||
@@ -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