From f5cacd83e9c9dc2e9ae033cc4d7a95bd2d72af1f Mon Sep 17 00:00:00 2001 From: Pierre Le Marre Date: Tue, 30 Jul 2024 14:19:18 +0200 Subject: [PATCH] compose: Add quickcheck test for traversal Test against the `foreach` implementation: - Suffle compose file lines randomly; - Compare traversal entry by entry. --- meson.build | 11 +++++- test/compose-iter.c | 88 ++++++++++++++++++++++++++++++++++++++++++++ test/compose-iter.h | 36 ++++++++++++++++++ test/compose.c | 83 +++++++++++++++++++++++++++++++++++++++-- test/shuffle-lines.c | 59 +++++++++++++++++++++++++++++ test/shuffle-lines.h | 13 +++++++ 6 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 test/compose-iter.c create mode 100644 test/compose-iter.h create mode 100644 test/shuffle-lines.c create mode 100644 test/shuffle-lines.h diff --git a/meson.build b/meson.build index 01a7c87f..310a84d9 100644 --- a/meson.build +++ b/meson.build @@ -765,7 +765,15 @@ test( ) test( 'compose', - executable('test-compose', 'test/compose.c', dependencies: test_dep), + executable( + 'test-compose', + 'test/compose.c', + 'test/shuffle-lines.c', + 'test/shuffle-lines.h', + 'test/compose-iter.c', + 'test/compose-iter.h', + dependencies: test_dep + ), env: test_env, ) test( @@ -853,6 +861,7 @@ if valgrind.found() '--track-origins=yes', '--gen-suppressions=all', '--error-exitcode=99'], + env: {'RUNNING_VALGRIND': '1'}, timeout_multiplier : 10) else message('valgrind not found, disabling valgrind test setup') diff --git a/test/compose-iter.c b/test/compose-iter.c new file mode 100644 index 00000000..90044efc --- /dev/null +++ b/test/compose-iter.c @@ -0,0 +1,88 @@ +#include "config.h" + +#include "src/darray.h" +#include +#include +#include + +#include "xkbcommon/xkbcommon-compose.h" +#include "src/compose/dump.h" +#include "src/compose/parser.h" +#include "src/keysym.h" +#include "src/utils.h" +#include "test/compose-iter.h" + +static void +for_each_helper(struct xkb_compose_table *table, + xkb_compose_table_iter_t iter, + void *data, + xkb_keysym_t *syms, + size_t nsyms, + uint16_t p) +{ + if (!p) { + return; + } + const struct compose_node *node = &darray_item(table->nodes, p); + for_each_helper(table, iter, data, syms, nsyms, node->lokid); + syms[nsyms++] = node->keysym; + if (node->is_leaf) { + struct xkb_compose_table_entry entry = { + .sequence = syms, + .sequence_length = nsyms, + .keysym = node->leaf.keysym, + .utf8 = &darray_item(table->utf8, node->leaf.utf8), + }; + iter(&entry, data); + } else { + for_each_helper(table, iter, data, syms, nsyms, node->internal.eqkid); + } + nsyms--; + for_each_helper(table, iter, data, syms, nsyms, node->hikid); +} + +XKB_EXPORT void +xkb_compose_table_for_each(struct xkb_compose_table *table, + xkb_compose_table_iter_t iter, + void *data) +{ + if (darray_size(table->nodes) <= 1) { + return; + } + xkb_keysym_t syms[MAX_LHS_LEN]; + for_each_helper(table, iter, data, syms, 0, 1); +} + +bool +print_compose_table_entry(FILE *file, struct xkb_compose_table_entry *entry) +{ + size_t nsyms; + const xkb_keysym_t *syms = xkb_compose_table_entry_sequence(entry, &nsyms); + char buf[XKB_KEYSYM_NAME_MAX_SIZE]; + for (size_t i = 0; i < nsyms; i++) { + xkb_keysym_get_name(syms[i], buf, sizeof(buf)); + fprintf(file, "<%s>", buf); + if (i + 1 < nsyms) { + fprintf(file, " "); + } + } + fprintf(file, " : "); + const char *utf8 = xkb_compose_table_entry_utf8(entry); + if (*utf8 != '\0') { + char *escaped = escape_utf8_string_literal(utf8); + if (!escaped) { + fprintf(file, "ERROR: Cannot escape the string: allocation error\n"); + return false; + } else { + fprintf(file, " \"%s\"", escaped); + free(escaped); + } + } + const xkb_keysym_t keysym = xkb_compose_table_entry_keysym(entry); + if (keysym != XKB_KEY_NoSymbol) { + xkb_keysym_get_name(keysym, buf, sizeof(buf)); + fprintf(file, " %s", buf); + } + fprintf(file, "\n"); + return true; +} diff --git a/test/compose-iter.h b/test/compose-iter.h new file mode 100644 index 00000000..aa52f937 --- /dev/null +++ b/test/compose-iter.h @@ -0,0 +1,36 @@ +#ifndef COMPOSE_LEGACY_ITER_H +#define COMPOSE_LEGACY_ITER_H + +#include "config.h" +#include "src/compose/table.h" + +/** + * The iterator function type used by xkb_compose_table_for_each(). + * + * @sa xkb_compose_table_for_each + * @memberof xkb_compose + * @since 1.6.0 + */ +typedef void +(*xkb_compose_table_iter_t)(struct xkb_compose_table_entry *entry, + void *data); + +/** + * Run a specified function for every valid entry in the table. + * + * The entries are returned in lexicographic order of the left-hand + * side of entries. This does not correspond to the order in which + * the entries appear in the Compose file. + * + * @memberof xkb_compose_table + * @since 1.6.0 + */ +void +xkb_compose_table_for_each(struct xkb_compose_table *table, + xkb_compose_table_iter_t iter, + void *data); + +bool +print_compose_table_entry(FILE *file, struct xkb_compose_table_entry *entry); + +#endif diff --git a/test/compose.c b/test/compose.c index 905b1af1..fa3f2ebd 100644 --- a/test/compose.c +++ b/test/compose.c @@ -32,6 +32,8 @@ #include "src/keysym.h" #include "src/compose/parser.h" #include "src/compose/dump.h" +#include "test/shuffle-lines.h" +#include "test/compose-iter.h" static const char * compose_status_string(enum xkb_compose_status status) @@ -717,8 +719,45 @@ test_eq_entry(struct xkb_compose_table_entry *entry, xkb_keysym_t keysym, const return ok; } +static bool +test_eq_entries(struct xkb_compose_table_entry *entry1, struct xkb_compose_table_entry *entry2) +{ + if (!entry1 || !entry2) + goto error; + bool ok = true; + if (entry1->keysym != entry2->keysym || + !streq_null(entry1->utf8, entry2->utf8) || + entry1->sequence_length != entry2->sequence_length) + ok = false; + for (size_t k = 0; k < entry1->sequence_length; k++) { + if (entry1->sequence[k] != entry2->sequence[k]) + ok = false; + } + if (ok) + return true; +error: +#define print_entry(msg, entry) \ + fprintf(stderr, msg); \ + if (entry) \ + print_compose_table_entry(stderr, entry); \ + else \ + fprintf(stderr, "\n"); + print_entry("Expected: ", entry1); + print_entry("Got: ", entry2); +#undef print_entry + return false; +} + +static void +compose_traverse_fn(struct xkb_compose_table_entry *entry_ref, void *data) +{ + struct xkb_compose_table_iterator *iter = (struct xkb_compose_table_iterator *)data; + struct xkb_compose_table_entry *entry = xkb_compose_table_iterator_next(iter); + assert(test_eq_entries(entry_ref, entry)); +} + static void -test_traverse(struct xkb_context *ctx) +test_traverse(struct xkb_context *ctx, size_t quickcheck_loops) { struct xkb_compose_table *table; struct xkb_compose_table_iterator *iter; @@ -796,6 +835,34 @@ test_traverse(struct xkb_context *ctx) xkb_compose_table_iterator_free(iter); xkb_compose_table_unref(table); + + /* QuickCheck: shuffle compose file lines and compare against + * reference implementation */ + char *input = test_read_file("locale/en_US.UTF-8/Compose"); + assert(input); + struct text_line lines[6000]; + size_t input_length = strlen(input); + size_t lines_count = split_lines(input, input_length, lines, ARRAY_SIZE(lines)); + /* Note: we may add additional new line char */ + char *shuffled = calloc(input_length + 1, sizeof(char)); + assert(shuffled); + for (size_t k = 0; k < quickcheck_loops; k++) { + size_t shuffled_length = shuffle_lines(lines, lines_count, shuffled); + table = xkb_compose_table_new_from_buffer(ctx, shuffled, shuffled_length, "", + XKB_COMPOSE_FORMAT_TEXT_V1, + XKB_COMPOSE_COMPILE_NO_FLAGS); + assert(table); + + iter = xkb_compose_table_iterator_new(table); + assert(iter); + xkb_compose_table_for_each(table, compose_traverse_fn, iter); + assert(xkb_compose_table_iterator_next(iter) == NULL); + xkb_compose_table_iterator_free(iter); + + xkb_compose_table_unref(table); + } + free(shuffled); + free(input); } static void @@ -950,7 +1017,7 @@ main(int argc, char *argv[]) /* Initialize pseudo-random generator with program arg or current time */ int seed; - if (argc == 2) { + if (argc >= 2 && !streq(argv[1], "-")) { seed = atoi(argv[1]); } else { seed = (int)time(NULL); @@ -958,6 +1025,16 @@ main(int argc, char *argv[]) fprintf(stderr, "Seed for the pseudo-random generator: %d\n", seed); srand(seed); + /* Determine number of loops for quickchecks */ + size_t quickcheck_loops = 100; /* Default */ + if (argc > 2) { + /* From command-line */ + quickcheck_loops = (size_t)atoi(argv[2]); + } else if (getenv("RUNNING_VALGRIND") != NULL) { + /* Reduce if running Valgrind */ + quickcheck_loops = quickcheck_loops / 20; + } + /* * Ensure no environment variables but “top_srcdir” is set. This ensures * that user Compose file paths are unset before the tests and set @@ -985,7 +1062,7 @@ main(int argc, char *argv[]) test_modifier_syntax(ctx); test_include(ctx); test_override(ctx); - test_traverse(ctx); + test_traverse(ctx, quickcheck_loops); test_string_length(ctx); test_decode_escape_sequences(ctx); test_encode_escape_sequences(ctx); diff --git a/test/shuffle-lines.c b/test/shuffle-lines.c new file mode 100644 index 00000000..3d29129f --- /dev/null +++ b/test/shuffle-lines.c @@ -0,0 +1,59 @@ +#include "config.h" + +#include +#include +#include "src/utils.h" +#include "test/shuffle-lines.h" + +/* Split string into lines */ +size_t +split_lines(const char *input, size_t input_length, + struct text_line *output, size_t output_length) +{ + const char *start = input; + char *next; + size_t l; + size_t i = 0; + + for (l = 0; i < input_length && l < output_length && *start != '\0'; l++) { + /* Look for newline character */ + next = strchr(start, 0x0a); + output[l].start = start; + if (next == NULL) { + /* Not found: add the rest of the string */ + output[l++].length = strlen(start); + break; + } + output[l].length = (size_t)(next - start) + 1; + start = next + 1; + i += output[l].length; + } + return l; +} + +size_t +shuffle_lines(struct text_line *lines, size_t length, char *output) +{ + /* Shuffle in-place using Fisher–Yates algorithm, then append lines. + * See: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle */ + + assert(length < RAND_MAX); + char *out = output; + if (length > 1) { + for (size_t i = length - 1; i > 0; i--) { + size_t j = (size_t)(rand() % (i+1)); + struct text_line tmp = lines[j]; + lines[j] = lines[i]; + lines[i] = tmp; + /* Append current line */ + memcpy(out, lines[i].start, lines[i].length); + out += lines[i].length; + /* Ensure line ends with newline */ + if (out[-1] != '\n') { + out[0] = '\n'; + out++; + } + } + } + return (size_t)(out - output); +} diff --git a/test/shuffle-lines.h b/test/shuffle-lines.h new file mode 100644 index 00000000..d0c596bf --- /dev/null +++ b/test/shuffle-lines.h @@ -0,0 +1,13 @@ +#include + +struct text_line { + const char *start; + size_t length; +}; + +size_t +split_lines(const char *input, size_t input_length, + struct text_line *output, size_t output_length); + +size_t +shuffle_lines(struct text_line *lines, size_t length, char *output);