diff options
Diffstat (limited to 'tests/test_deck.c')
| -rw-r--r-- | tests/test_deck.c | 443 |
1 files changed, 443 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; +} |
