spiegel-keyman/core/tests/unit/kmnkbd/action_api.cpp
Marc Durdin 7ec4832edc fix(core): memory management of options in action struct
Fixes #10067.

Management of memory for persisted options was wrong in the action
struct, as the members key and value would be freed immediately after
being added to the temporary vector (because the vector was of the
struct rather than of the class).

Given the struct is a C struct, we need the memory management to be
explicit, so we now release() each option into the vector as we create
it, which means that its member values will not be freed when the option
is then immediately deleted. (This allows us to use the initial copy of
the members of option that option() constructor does.)

Added the release() function as that was a relatively clear way of
indicating that the contents of the structure are now owned by the
caller, following the pattern from std::unique_ptr.

Finally, the unit test for persisted options was in the action_api.cpp
test module, but it was never called, so this was not being tested. Now
it is.
2023-11-24 07:52:31 +10:00

446 lines
15 KiB
C++

/*
Copyright: © 2018 SIL International.
Description: Tests for the context API family of functions.
Create Date: 23 Oct 2023
Authors: Marc Durdin
History: 23 Oct 2023 - MCD - Initial implementation.
*/
#include <string>
#include <keyman/keyman_core_api.h>
#include "path.hpp"
#include "action.hpp"
#include <test_assert.h>
#include "../emscripten_filesystem.h"
const km_core_action_item alert_action_item();
const km_core_action_item bksp_action_item(uint8_t type, uintptr_t value);
const km_core_action_item caps_action_item(uint8_t capsLock);
const km_core_action_item char_action_item(km_core_usv chr);
const km_core_action_item emit_keystroke_action_item();
const km_core_action_item persist_opt_action_item(km_core_option_item const *option);
const km_core_action_item end_action_item();
const km_core_action_item invalidate_context_action_item();
const km_core_action_item marker_action_item(uint32_t marker);
//-------------------------------------------------------------------------------------
void test_two_backspaces() {
const km_core_action_item action_items[] = {
char_action_item('D'),
bksp_action_item(KM_CORE_BT_CHAR, 'D'),
bksp_action_item(KM_CORE_BT_CHAR, 'E'),
end_action_item()
};
km_core_actions *actions = km::core::action_item_list_to_actions_object(action_items);
assert(actions->code_points_to_delete == 1);
assert(std::u32string(actions->output) == U"");
assert(actions->persist_options != nullptr);
assert(actions->persist_options[0].key == nullptr);
assert(actions->persist_options[0].value == nullptr);
assert(actions->persist_options[0].scope == KM_CORE_OPT_UNKNOWN);
assert(actions->do_alert == false);
assert(actions->emit_keystroke == false);
assert(actions->new_caps_lock_state == -1);
try_status(km_core_actions_dispose(actions));
}
//-------------------------------------------------------------------------------------
void test_marker_text_interleaved() {
const km_core_action_item action_items[] = {
char_action_item('A'),
marker_action_item(1),
char_action_item('B'),
marker_action_item(2),
char_action_item('C'),
bksp_action_item(KM_CORE_BT_CHAR, 'C'),
bksp_action_item(KM_CORE_BT_MARKER, 2),
char_action_item('D'),
end_action_item()
};
km_core_actions *actions = km::core::action_item_list_to_actions_object(action_items);
assert(actions->code_points_to_delete == 0);
assert(std::u32string(actions->output) == U"ABD");
assert(actions->persist_options != nullptr);
assert(actions->persist_options[0].key == nullptr);
assert(actions->persist_options[0].value == nullptr);
assert(actions->persist_options[0].scope == KM_CORE_OPT_UNKNOWN);
assert(actions->do_alert == false);
assert(actions->emit_keystroke == false);
assert(actions->new_caps_lock_state == -1);
try_status(km_core_actions_dispose(actions));
}
//-------------------------------------------------------------------------------------
void test_alert() {
const km_core_action_item action_items[] = {
alert_action_item(),
end_action_item()
};
km_core_actions *actions = km::core::action_item_list_to_actions_object(action_items);
assert(actions->code_points_to_delete == 0);
assert(std::u32string(actions->output) == U"");
assert(actions->persist_options != nullptr);
assert(actions->persist_options[0].key == nullptr);
assert(actions->persist_options[0].value == nullptr);
assert(actions->persist_options[0].scope == KM_CORE_OPT_UNKNOWN);
assert(actions->do_alert == KM_CORE_TRUE);
assert(actions->emit_keystroke == KM_CORE_FALSE);
assert(actions->new_caps_lock_state == KM_CORE_CAPS_UNCHANGED);
try_status(km_core_actions_dispose(actions));
}
//-------------------------------------------------------------------------------------
void test_emit_keystroke() {
const km_core_action_item action_items[] = {
emit_keystroke_action_item(),
end_action_item()
};
km_core_actions *actions = km::core::action_item_list_to_actions_object(action_items);
assert(actions->code_points_to_delete == 0);
assert(std::u32string(actions->output) == U"");
assert(actions->persist_options != nullptr);
assert(actions->persist_options[0].key == nullptr);
assert(actions->persist_options[0].value == nullptr);
assert(actions->persist_options[0].scope == KM_CORE_OPT_UNKNOWN);
assert(actions->do_alert == KM_CORE_FALSE);
assert(actions->emit_keystroke == KM_CORE_TRUE);
assert(actions->new_caps_lock_state == KM_CORE_CAPS_UNCHANGED);
try_status(km_core_actions_dispose(actions));
}
//-------------------------------------------------------------------------------------
void test_invalidate_context() {
// note, this generates a no-op
const km_core_action_item action_items[] = {
invalidate_context_action_item(),
end_action_item()
};
km_core_actions *actions = km::core::action_item_list_to_actions_object(action_items);
assert(actions->code_points_to_delete == 0);
assert(std::u32string(actions->output) == U"");
assert(actions->persist_options != nullptr);
assert(actions->persist_options[0].key == nullptr);
assert(actions->persist_options[0].value == nullptr);
assert(actions->persist_options[0].scope == KM_CORE_OPT_UNKNOWN);
assert(actions->do_alert == KM_CORE_FALSE);
assert(actions->emit_keystroke == KM_CORE_FALSE);
assert(actions->new_caps_lock_state == KM_CORE_CAPS_UNCHANGED);
try_status(km_core_actions_dispose(actions));
}
//-------------------------------------------------------------------------------------
void test_persist_opt() {
const km_core_option_item option = {
u"key",
u"value",
KM_CORE_OPT_KEYBOARD
};
const km_core_action_item action_items[] = {
persist_opt_action_item(&option),
end_action_item()
};
km_core_actions *actions = km::core::action_item_list_to_actions_object(action_items);
assert(actions->code_points_to_delete == 0);
assert(std::u32string(actions->output) == U"");
assert(actions->persist_options != nullptr);
assert(std::u16string(actions->persist_options[0].key) == u"key");
assert(std::u16string(actions->persist_options[0].value) == u"value");
assert(actions->persist_options[0].scope == KM_CORE_OPT_KEYBOARD);
// verify that data is copied
assert(actions->persist_options[0].key != option.key);
assert(actions->persist_options[0].value != option.value);
// verify that we have a KM_CORE_OPTIONS_END term
assert(actions->persist_options[1].key == nullptr);
assert(actions->persist_options[1].value == nullptr);
assert(actions->persist_options[1].scope == KM_CORE_OPT_UNKNOWN);
assert(actions->do_alert == KM_CORE_FALSE);
assert(actions->emit_keystroke == KM_CORE_FALSE);
assert(actions->new_caps_lock_state == KM_CORE_CAPS_UNCHANGED);
try_status(km_core_actions_dispose(actions));
}
//-------------------------------------------------------------------------------------
// Context tests
//-------------------------------------------------------------------------------------
km_core_option_item test_env_opts[] =
{
KM_CORE_OPTIONS_END
};
km_core_keyboard * test_kb = nullptr;
km_core_state * test_state = nullptr;
km_core_context_item * citems = nullptr;
std::string arg_path;
void teardown() {
if(citems) {
km_core_context_items_dispose(citems);
citems = nullptr;
}
if(test_state) {
km_core_state_dispose(test_state);
test_state = nullptr;
}
if(test_kb) {
km_core_keyboard_dispose(test_kb);
test_kb = nullptr;
}
}
void setup(const char *keyboard, const km_core_cp* context) {
teardown();
km::core::path path = km::core::path::join(arg_path, keyboard);
try_status(km_core_keyboard_load(path.native().c_str(), &test_kb));
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));
try_status(km_core_context_items_from_utf16(context, &citems));
try_status(km_core_context_set(km_core_state_context(test_state), citems));
}
bool is_identical_context(km_core_cp const *cached_context) {
size_t buf_size;
try_status(km_core_context_get(km_core_state_context(test_state), &citems));
try_status(km_core_context_items_to_utf16(citems, nullptr, &buf_size));
km_core_cp* new_cached_context = new km_core_cp[buf_size];
try_status(km_core_context_items_to_utf16(citems, new_cached_context, &buf_size));
bool result = std::u16string(cached_context) == new_cached_context;
delete[] new_cached_context;
return result;
}
void test_context_set_if_needed_identical_context() {
km_core_cp const *application_context = u"This is a test";
km_core_cp const *cached_context = u"This is a test";
setup("k_000___null_keyboard.kmx", cached_context);
assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UNCHANGED);
assert(is_identical_context(cached_context));
teardown();
}
void test_context_set_if_needed_different_context() {
km_core_cp const *application_context = u"This is a test";
km_core_cp const *cached_context = u"This isn't a test";
setup("k_000___null_keyboard.kmx", cached_context);
assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UPDATED);
assert(!is_identical_context(cached_context));
assert(is_identical_context(application_context));
teardown();
}
void test_context_set_if_needed_app_context_is_longer() {
km_core_cp const *application_context = u"Longer This is a test";
km_core_cp const *cached_context = u"This is a test";
setup("k_000___null_keyboard.kmx", cached_context);
assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UNCHANGED);
// Should be true -- longer, but what exists is identical to cached
assert(is_identical_context(cached_context));
teardown();
}
void test_context_set_if_needed_app_context_is_shorter() {
km_core_cp const *application_context = u"is a test";
km_core_cp const *cached_context = u"This is a test";
setup("k_000___null_keyboard.kmx", cached_context);
assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UPDATED);
// Should be false -- app ctxt is shorter, so doesn't matter that what we have
// matches
assert(!is_identical_context(cached_context));
assert(is_identical_context(application_context));
teardown();
}
void test_context_set_if_needed_cached_context_has_markers() {
km_core_cp const *application_context = u"123";
km_core_cp const *cached_context = u"123";
setup("k_000___null_keyboard.kmx", cached_context);
km_core_context_item const citems[] = {
{ KM_CORE_CT_MARKER, {0}, { 5 } },
{ KM_CORE_CT_CHAR, {0}, { '1' } },
{ KM_CORE_CT_MARKER, {0}, { 1 } },
{ KM_CORE_CT_CHAR, {0}, { '2' } },
{ KM_CORE_CT_MARKER, {0}, { 2 } },
{ KM_CORE_CT_CHAR, {0}, { '3' } },
{ KM_CORE_CT_MARKER, {0}, { 3 } },
{ KM_CORE_CT_MARKER, {0}, { 4 } },
KM_CORE_CONTEXT_ITEM_END
};
try_status(km_core_context_set(km_core_state_context(test_state), citems));
assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UNCHANGED);
km_core_context_item* citems_new;
try_status(km_core_context_get(km_core_state_context(test_state), &citems_new));
for(int i = 0; citems[i].type || citems_new[i].type; i++) {
assert(citems_new[i].type == citems[i].type);
if(citems[i].type == KM_CORE_CT_CHAR) {
assert(citems_new[i].character == citems[i].character);
} else {
assert(citems_new[i].marker == citems[i].marker);
}
}
teardown();
}
void test_context_set_if_needed() {
test_context_set_if_needed_identical_context();
test_context_set_if_needed_different_context();
test_context_set_if_needed_app_context_is_longer();
test_context_set_if_needed_app_context_is_shorter();
test_context_set_if_needed_cached_context_has_markers();
}
void test_context_clear() {
km_core_cp const *cached_context = u"This is a test";
setup("k_000___null_keyboard.kmx", cached_context);
try_status(km_core_state_context_clear(test_state));
assert(!is_identical_context(cached_context));
assert(is_identical_context(u""));
teardown();
}
//-------------------------------------------------------------------------------------
// Launcher
//-------------------------------------------------------------------------------------
constexpr const auto help_str = "\
action_api [--color] <SOURCE_PATH>\n\
\n\
--color Force color output\n\
SOURCE_PATH Path where debug_api.cpp is found; kmx files are\n\
located relative to this path.\n";
int error_args() {
std::cerr << "debug_api: Invalid arguments." << std::endl;
std::cout << help_str;
return 1;
}
int main(int argc, char *argv []) {
if(argc < 2) {
return error_args();
}
auto arg_color = std::string(argv[1]) == "--color";
if(arg_color && argc < 3) {
return error_args();
}
console_color::enabled = console_color::isaterminal() || arg_color;
#ifdef __EMSCRIPTEN__
arg_path = get_wasm_file_path(argv[arg_color ? 2 : 1]);
#else
arg_path = argv[arg_color ? 2 : 1];
#endif
// actions
test_two_backspaces();
test_marker_text_interleaved();
test_alert();
test_emit_keystroke();
test_invalidate_context();
test_persist_opt();
// context -- todo move to another file
test_context_set_if_needed();
test_context_clear();
}
//-------------------------------------------------------------------------------------
// Helper functions
//-------------------------------------------------------------------------------------
const km_core_action_item alert_action_item() {
km_core_action_item res = {0};
res.type = KM_CORE_IT_ALERT;
return res;
}
const km_core_action_item bksp_action_item(uint8_t type, uintptr_t value) {
km_core_action_item res = {0};
res.type = KM_CORE_IT_BACK;
res.backspace.expected_type = type;
res.backspace.expected_value = value;
return res;
}
const km_core_action_item caps_action_item(uint8_t capsLock) {
km_core_action_item res = {0};
res.type = KM_CORE_IT_CAPSLOCK;
res.capsLock = capsLock;
return res;
}
const km_core_action_item char_action_item(km_core_usv chr) {
km_core_action_item res = {0};
res.type = KM_CORE_IT_CHAR;
res.character = chr;
return res;
}
const km_core_action_item emit_keystroke_action_item() {
km_core_action_item res = {0};
res.type = KM_CORE_IT_EMIT_KEYSTROKE;
return res;
}
const km_core_action_item persist_opt_action_item(km_core_option_item const *option) {
km_core_action_item res = {0};
res.type = KM_CORE_IT_PERSIST_OPT;
res.option = option;
return res;
}
const km_core_action_item end_action_item() {
km_core_action_item res = {0};
res.type = KM_CORE_IT_END;
return res;
}
const km_core_action_item invalidate_context_action_item() {
km_core_action_item res = {0};
res.type = KM_CORE_IT_INVALIDATE_CONTEXT;
return res;
}
const km_core_action_item marker_action_item(uint32_t marker) {
km_core_action_item res = {0};
res.type = KM_CORE_IT_MARKER;
res.character = marker;
return res;
}