summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_deck.c443
-rw-r--r--tests/test_image.c62
2 files changed, 505 insertions, 0 deletions
diff --git a/tests/test_deck.c b/tests/test_deck.c
new file mode 100644
index 0000000..3103a80
--- /dev/null
+++ b/tests/test_deck.c
@@ -0,0 +1,443 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "kd_deck.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+static int write_file(const char *path, const char *contents)
+{
+ FILE *file;
+
+ file = fopen(path, "w");
+ if (file == NULL) {
+ return 0;
+ }
+ if (fputs(contents, file) == EOF) {
+ (void)fclose(file);
+ return 0;
+ }
+ return fclose(file) == 0;
+}
+
+static int remove_tree(const char *root)
+{
+ char path[1024];
+ int failed;
+
+ failed = 0;
+ (void)snprintf(path, sizeof(path), "%s/cards/1/jukugo/1.ppm", root);
+ if (unlink(path) != 0) failed = 1;
+ (void)snprintf(path, sizeof(path), "%s/cards/1/jukugo/1_ans.ppm", root);
+ if (unlink(path) != 0) failed = 1;
+ (void)snprintf(path, sizeof(path), "%s/cards/1/jukugo/2.ppm", root);
+ if (unlink(path) != 0) failed = 1;
+ (void)snprintf(path, sizeof(path), "%s/cards/1/jukugo/2_ans.ppm", root);
+ if (unlink(path) != 0) failed = 1;
+ (void)snprintf(path, sizeof(path), "%s/cards/1/jukugo", root);
+ if (rmdir(path) != 0) failed = 1;
+ (void)snprintf(path, sizeof(path), "%s/cards/1/kanji.ppm", root);
+ if (unlink(path) != 0) failed = 1;
+ (void)snprintf(path, sizeof(path), "%s/cards/1/kanji_ans.ppm", root);
+ if (unlink(path) != 0) failed = 1;
+ (void)snprintf(path, sizeof(path), "%s/cards/1/URL.txt", root);
+ if (unlink(path) != 0) failed = 1;
+ (void)snprintf(path, sizeof(path), "%s/cards/1", root);
+ if (rmdir(path) != 0) failed = 1;
+ (void)snprintf(path, sizeof(path), "%s/cards", root);
+ if (rmdir(path) != 0) failed = 1;
+ (void)snprintf(path, sizeof(path), "%s/progress.txt", root);
+ (void)unlink(path);
+ (void)snprintf(path, sizeof(path), "%s/progress.txt.tmp", root);
+ (void)unlink(path);
+ if (rmdir(root) != 0) failed = 1;
+ return failed == 0;
+}
+
+static size_t find_key(const KDDeck *deck, const char *key)
+{
+ size_t i;
+
+ for (i = 0U; i < deck->item_count; i++) {
+ if (strcmp(deck->items[i].key, key) == 0) {
+ return i;
+ }
+ }
+ return (size_t)-1;
+}
+
+static int create_order_card(const char *cards_path, const char *id,
+ int jukugo_count)
+{
+ char card_path[1024];
+ char jukugo_path[1024];
+ char path[1024];
+ char filename[64];
+ int i;
+
+ (void)snprintf(card_path, sizeof(card_path), "%s/%s", cards_path, id);
+ (void)snprintf(jukugo_path, sizeof(jukugo_path), "%s/jukugo", card_path);
+ if (mkdir(card_path, 0700) != 0 || mkdir(jukugo_path, 0700) != 0) {
+ return 0;
+ }
+ (void)snprintf(path, sizeof(path), "%s/kanji.ppm", card_path);
+ if (!write_file(path, "P3\n1 1\n255\n255 255 255\n")) return 0;
+ (void)snprintf(path, sizeof(path), "%s/kanji_ans.ppm", card_path);
+ if (!write_file(path, "P3\n1 1\n255\n255 255 255\n")) return 0;
+ (void)snprintf(path, sizeof(path), "%s/URL.txt", card_path);
+ if (!write_file(path, "https://www.example.com/kanji/example\n")) return 0;
+ for (i = 1; i <= jukugo_count; i++) {
+ (void)snprintf(filename, sizeof(filename), "%d.ppm", i);
+ (void)snprintf(path, sizeof(path), "%s/%s", jukugo_path, filename);
+ if (!write_file(path, "P3\n1 1\n255\n0 0 0\n")) return 0;
+ (void)snprintf(filename, sizeof(filename), "%d_ans.ppm", i);
+ (void)snprintf(path, sizeof(path), "%s/%s", jukugo_path, filename);
+ if (!write_file(path, "P3\n1 1\n255\n255 255 255\n")) return 0;
+ }
+ return 1;
+}
+
+static void remove_order_fixture(const char *root)
+{
+ char path[1024];
+ int card;
+ int jukugo;
+
+ for (card = 1; card <= 2; card++) {
+ for (jukugo = 1; jukugo <= 2; jukugo++) {
+ (void)snprintf(path, sizeof(path),
+ "%s/cards/%d/jukugo/%d.ppm", root, card, jukugo);
+ (void)unlink(path);
+ (void)snprintf(path, sizeof(path),
+ "%s/cards/%d/jukugo/%d_ans.ppm", root, card, jukugo);
+ (void)unlink(path);
+ }
+ (void)snprintf(path, sizeof(path), "%s/cards/%d/jukugo", root, card);
+ (void)rmdir(path);
+ (void)snprintf(path, sizeof(path), "%s/cards/%d/kanji.ppm", root, card);
+ (void)unlink(path);
+ (void)snprintf(path, sizeof(path), "%s/cards/%d/kanji_ans.ppm", root, card);
+ (void)unlink(path);
+ (void)snprintf(path, sizeof(path), "%s/cards/%d/URL.txt", root, card);
+ (void)unlink(path);
+ (void)snprintf(path, sizeof(path), "%s/cards/%d", root, card);
+ (void)rmdir(path);
+ }
+ (void)snprintf(path, sizeof(path), "%s/cards", root);
+ (void)rmdir(path);
+ (void)snprintf(path, sizeof(path), "%s/progress.txt", root);
+ (void)unlink(path);
+ (void)snprintf(path, sizeof(path), "%s/progress.txt.tmp", root);
+ (void)unlink(path);
+ (void)rmdir(root);
+}
+
+static int test_new_prompt_order(void)
+{
+ char template_path[] = "/tmp/kd-deck-order-test-XXXXXX";
+ char cards_path[1024];
+ char progress_path[1024];
+ char *root;
+ KDDeck deck;
+ size_t selected;
+ size_t step;
+ time_t now;
+ static const char *expected[] = {
+ "K:1", "J:1:1", "J:1:2", "K:2", "J:2:1"
+ };
+ int passed;
+
+ root = mkdtemp(template_path);
+ if (root == NULL) {
+ (void)fprintf(stderr, "FAIL: creating prompt-order fixture\n");
+ return 0;
+ }
+ (void)snprintf(cards_path, sizeof(cards_path), "%s/cards", root);
+ (void)snprintf(progress_path, sizeof(progress_path), "%s/progress.txt", root);
+ if (mkdir(cards_path, 0700) != 0 ||
+ !create_order_card(cards_path, "1", 2) ||
+ !create_order_card(cards_path, "2", 1)) {
+ (void)fprintf(stderr, "FAIL: creating multi-card prompt-order fixture\n");
+ remove_order_fixture(root);
+ return 0;
+ }
+
+ kd_deck_init(&deck);
+ passed = kd_deck_scan(&deck, cards_path, progress_path);
+ now = time(NULL);
+ for (step = 0U; passed && step < sizeof(expected) / sizeof(expected[0]); step++) {
+ if (!kd_select_next(&deck, now, &selected) ||
+ strcmp(deck.items[selected].key, expected[step]) != 0) {
+ (void)fprintf(stderr,
+ "FAIL: new prompt order at step %lu: expected %s\n",
+ (unsigned long)step, expected[step]);
+ passed = 0;
+ break;
+ }
+ kd_grade(&deck, selected, 1, now);
+ }
+ if (passed && kd_select_next(&deck, now, &selected)) {
+ (void)fprintf(stderr,
+ "FAIL: a just-reviewed prompt was selected again before due\n");
+ passed = 0;
+ }
+ kd_deck_free(&deck);
+ remove_order_fixture(root);
+ if (passed) {
+ (void)printf("PASS: new prompts are introduced by numeric kanji group: K:1, its jukugo, K:2, its jukugo\n");
+ }
+ return passed;
+}
+
+int main(void)
+{
+ char template_path[] = "/tmp/kd-deck-test-XXXXXX";
+ char cards_path[1024];
+ char card_path[1024];
+ char jukugo_path[1024];
+ char path[1024];
+ char progress_path[1024];
+ char *root;
+ KDDeck deck;
+ size_t selected;
+ size_t numbered_item;
+ size_t kanji_item;
+ size_t jukugo_one;
+ size_t jukugo_two;
+ long kanji_due;
+ long jukugo_one_due;
+ long jukugo_two_due;
+ long saved_lapses;
+ time_t now;
+ const char *url;
+ int ok;
+
+ if (!test_new_prompt_order()) {
+ return EXIT_FAILURE;
+ }
+ root = mkdtemp(template_path);
+ if (root == NULL) {
+ (void)fprintf(stderr, "FAIL: mkdtemp\n");
+ return EXIT_FAILURE;
+ }
+ (void)snprintf(cards_path, sizeof(cards_path), "%s/cards", root);
+ (void)snprintf(card_path, sizeof(card_path), "%s/cards/1", root);
+ (void)snprintf(jukugo_path, sizeof(jukugo_path), "%s/cards/1/jukugo", root);
+ (void)snprintf(progress_path, sizeof(progress_path), "%s/progress.txt", root);
+ if (mkdir(cards_path, 0700) != 0 || mkdir(card_path, 0700) != 0 ||
+ mkdir(jukugo_path, 0700) != 0) {
+ (void)fprintf(stderr, "FAIL: creating fixture directories\n");
+ return EXIT_FAILURE;
+ }
+ (void)snprintf(path, sizeof(path), "%s/kanji.ppm", card_path);
+ ok = write_file(path, "P3\n1 1\n255\n255 255 255\n");
+ (void)snprintf(path, sizeof(path), "%s/kanji_ans.ppm", card_path);
+ ok = ok && write_file(path, "P3\n1 1\n255\n255 255 255\n");
+ (void)snprintf(path, sizeof(path), "%s/URL.txt", card_path);
+ ok = ok && write_file(path, "https://www.example.com/kanji/example\n");
+ (void)snprintf(path, sizeof(path), "%s/1.ppm", jukugo_path);
+ ok = ok && write_file(path, "P3\n1 1\n255\n0 0 0\n");
+ (void)snprintf(path, sizeof(path), "%s/1_ans.ppm", jukugo_path);
+ ok = ok && write_file(path, "P3\n1 1\n255\n255 255 255\n");
+ (void)snprintf(path, sizeof(path), "%s/2.ppm", jukugo_path);
+ ok = ok && write_file(path, "P3\n1 1\n255\n0 0 0\n");
+ (void)snprintf(path, sizeof(path), "%s/2_ans.ppm", jukugo_path);
+ ok = ok && write_file(path, "P3\n1 1\n255\n255 255 255\n");
+ if (!ok) {
+ (void)fprintf(stderr, "FAIL: creating fixture files\n");
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+
+ kd_deck_init(&deck);
+ if (!kd_deck_scan(&deck, cards_path, progress_path)) {
+ (void)fprintf(stderr, "FAIL: scanning fixture deck\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ if (deck.card_count != 1U || deck.item_count != 3U) {
+ (void)fprintf(stderr, "FAIL: expected one kanji and two jukugo prompts\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ url = kd_card_url(&deck.cards[0]);
+ if (url == NULL || strstr(url, "example.com") == NULL) {
+ (void)fprintf(stderr, "FAIL: URL.txt was not read\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ if (!deck.cards[0].has_answer_image) {
+ (void)fprintf(stderr, "FAIL: kanji_ans.ppm was not detected\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ if (kd_count_unintroduced(&deck) != 1U) {
+ (void)fprintf(stderr, "FAIL: new kanji count was incorrect (jukugo must not count)\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ if (!kd_find_kanji_item_by_number(&deck, "1", &numbered_item) ||
+ deck.items[numbered_item].jukugo_index != KD_NO_JUKUGO) {
+ (void)fprintf(stderr, "FAIL: typed number did not resolve to the main kanji\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ if (kd_find_kanji_item_by_number(&deck, "2", &numbered_item) ||
+ kd_find_kanji_item_by_number(&deck, "0", &numbered_item) ||
+ kd_find_kanji_item_by_number(&deck, "1x", &numbered_item)) {
+ (void)fprintf(stderr, "FAIL: invalid/nonexistent kanji number was accepted\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ if (kd_count_unintroduced(&deck) != 1U) {
+ (void)fprintf(stderr, "FAIL: choosing a kanji by number changed its introduction state\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ now = time(NULL);
+ if (!kd_select_next(&deck, now, &selected) ||
+ deck.items[selected].jukugo_index != KD_NO_JUKUGO) {
+ (void)fprintf(stderr, "FAIL: scheduler did not prioritize the main kanji\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ kanji_item = selected;
+ kd_grade(&deck, kanji_item, 1, now);
+ if (deck.reviews[deck.items[kanji_item].review_index].interval_days != 1L) {
+ (void)fprintf(stderr, "FAIL: first successful interval is not one day\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ kanji_due = deck.reviews[deck.items[kanji_item].review_index].due_epoch;
+
+ if (!kd_select_next(&deck, now, &selected) ||
+ strcmp(deck.items[selected].key, "J:1:1") != 0) {
+ (void)fprintf(stderr, "FAIL: first jukugo was not independently selected\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ jukugo_one = selected;
+ kd_grade(&deck, jukugo_one, 1, now);
+ jukugo_one_due = deck.reviews[deck.items[jukugo_one].review_index].due_epoch;
+
+ if (!kd_select_next(&deck, now, &selected) ||
+ strcmp(deck.items[selected].key, "J:1:2") != 0) {
+ (void)fprintf(stderr, "FAIL: second jukugo was not independently selected\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ jukugo_two = selected;
+ now += (time_t)100;
+ kd_grade(&deck, jukugo_two, 0, now);
+ if (deck.reviews[deck.items[kanji_item].review_index].due_epoch != kanji_due ||
+ deck.reviews[deck.items[jukugo_one].review_index].due_epoch != jukugo_one_due ||
+ deck.reviews[deck.items[jukugo_two].review_index].due_epoch != (long)now + 600L) {
+ (void)fprintf(stderr, "FAIL: forgetting one jukugo changed a sibling or kanji schedule\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+
+ now += (time_t)1;
+ saved_lapses = deck.reviews[deck.items[jukugo_two].review_index].lapses;
+ if (!kd_relearn_prompt(&deck, jukugo_two, now) ||
+ deck.reviews[deck.items[jukugo_two].review_index].interval_days != 1L ||
+ !deck.reviews[deck.items[jukugo_two].review_index].relearning ||
+ deck.reviews[deck.items[jukugo_two].review_index].due_epoch != (long)now + 600L ||
+ deck.reviews[deck.items[jukugo_two].review_index].lapses != saved_lapses + 1L ||
+ deck.reviews[deck.items[kanji_item].review_index].due_epoch != kanji_due ||
+ deck.reviews[deck.items[jukugo_one].review_index].due_epoch != jukugo_one_due) {
+ (void)fprintf(stderr, "FAIL: prompt-only relearning changed other schedules\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ kd_grade(&deck, jukugo_two, 1, now + (time_t)600);
+ if (deck.reviews[deck.items[jukugo_two].review_index].interval_days != 1L ||
+ deck.reviews[deck.items[jukugo_two].review_index].relearning ||
+ deck.reviews[deck.items[jukugo_two].review_index].due_epoch !=
+ (long)now + 600L + 86400L) {
+ (void)fprintf(stderr, "FAIL: successful relearning did not graduate at the 1-day interval\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+
+ jukugo_one_due = deck.reviews[deck.items[jukugo_one].review_index].due_epoch;
+ jukugo_two_due = deck.reviews[deck.items[jukugo_two].review_index].due_epoch;
+ now += (time_t)10;
+ kd_grade(&deck, kanji_item, 0, now);
+ if (deck.reviews[deck.items[kanji_item].review_index].due_epoch != (long)now + 600L ||
+ deck.reviews[deck.items[jukugo_one].review_index].due_epoch != jukugo_one_due ||
+ deck.reviews[deck.items[jukugo_two].review_index].due_epoch != jukugo_two_due) {
+ (void)fprintf(stderr, "FAIL: forgetting the kanji changed its jukugo schedules\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+
+ saved_lapses = deck.reviews[deck.items[jukugo_two].review_index].lapses;
+ now += (time_t)100;
+ if (!kd_relearn_card(&deck, 0U, now)) {
+ (void)fprintf(stderr, "FAIL: whole-kanji relearning could not save\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ if (deck.reviews[deck.items[kanji_item].review_index].due_epoch != (long)now + 600L ||
+ deck.reviews[deck.items[kanji_item].review_index].interval_days != 1L ||
+ !deck.reviews[deck.items[kanji_item].review_index].relearning ||
+ deck.reviews[deck.items[jukugo_one].review_index].due_epoch != (long)now + 600L ||
+ deck.reviews[deck.items[jukugo_two].review_index].due_epoch != (long)now + 600L ||
+ deck.reviews[deck.items[jukugo_two].review_index].lapses != saved_lapses + 1L) {
+ (void)fprintf(stderr, "FAIL: whole-kanji relearning did not reset the full set correctly\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ kd_deck_free(&deck);
+
+ kd_deck_init(&deck);
+ if (!kd_deck_scan(&deck, cards_path, progress_path) ||
+ deck.card_count != 1U || deck.item_count != 3U) {
+ (void)fprintf(stderr, "FAIL: rescanning after progress save\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ kanji_item = find_key(&deck, "K:1");
+ jukugo_one = find_key(&deck, "J:1:1");
+ jukugo_two = find_key(&deck, "J:1:2");
+ if (kanji_item == (size_t)-1 || jukugo_one == (size_t)-1 ||
+ jukugo_two == (size_t)-1 ||
+ deck.reviews[deck.items[kanji_item].review_index].interval_days != 1L ||
+ deck.reviews[deck.items[jukugo_one].review_index].interval_days != 1L ||
+ deck.reviews[deck.items[jukugo_two].review_index].interval_days != 1L) {
+ (void)fprintf(stderr, "FAIL: relearning schedule reset did not persist\n");
+ kd_deck_free(&deck);
+ (void)remove_tree(root);
+ return EXIT_FAILURE;
+ }
+ kd_deck_free(&deck);
+ if (!remove_tree(root)) {
+ (void)fprintf(stderr, "WARNING: could not remove every temporary test file\n");
+ }
+ (void)printf("PASS: independent kanji/jukugo schedules, Anki-default lapse interval and relearning steps, whole-kanji group relearn, typed-number lookup, answer images, URL.txt, and persistence\n");
+ return EXIT_SUCCESS;
+}
diff --git a/tests/test_image.c b/tests/test_image.c
new file mode 100644
index 0000000..c637770
--- /dev/null
+++ b/tests/test_image.c
@@ -0,0 +1,62 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "kd_image.h"
+
+#include <stdio.h>
+#include <X11/Xutil.h>
+
+typedef struct KDTestImageHandle {
+ XImage *ximage;
+ GC clip_mask_gc;
+ Pixmap clip_mask;
+} KDTestImageHandle;
+
+int main(int argc, char **argv)
+{
+ Display *display;
+ struct nk_image image;
+ KDTestImageHandle *handle;
+ Visual *visual;
+ unsigned long first_pixel;
+ unsigned long second_pixel;
+
+ if (argc != 3) {
+ (void)fprintf(stderr, "Usage: %s red-green-p3.ppm red-green-p6.ppm\n", argv[0]);
+ return 2;
+ }
+ display = XOpenDisplay(NULL);
+ if (display == NULL) {
+ (void)fprintf(stderr, "Could not open DISPLAY.\n");
+ return 2;
+ }
+ image = kd_ppm_load(display, DefaultScreen(display), argv[1], 20U, 20U);
+ if (image.handle.ptr == NULL || image.w != 2U || image.h != 1U) {
+ (void)fprintf(stderr, "P3 image failed to load.\n");
+ XCloseDisplay(display);
+ return 1;
+ }
+ handle = (KDTestImageHandle *)image.handle.ptr;
+ visual = DefaultVisual(display, DefaultScreen(display));
+ first_pixel = XGetPixel(handle->ximage, 0, 0);
+ second_pixel = XGetPixel(handle->ximage, 1, 0);
+ if ((first_pixel & visual->red_mask) == 0UL ||
+ (first_pixel & visual->green_mask) != 0UL ||
+ (second_pixel & visual->green_mask) == 0UL ||
+ (second_pixel & visual->red_mask) != 0UL) {
+ (void)fprintf(stderr, "P3 pixel colors were incorrect.\n");
+ kd_ppm_free(&image);
+ XCloseDisplay(display);
+ return 1;
+ }
+ kd_ppm_free(&image);
+ image = kd_ppm_load(display, DefaultScreen(display), argv[2], 20U, 20U);
+ if (image.handle.ptr == NULL || image.w != 2U || image.h != 1U) {
+ (void)fprintf(stderr, "P6 image failed to load.\n");
+ XCloseDisplay(display);
+ return 1;
+ }
+ kd_ppm_free(&image);
+ XCloseDisplay(display);
+ (void)puts("PASS: P3/P6 PPM decoding and XImage pixels");
+ return 0;
+}