mcap/src/test/catalog_tests.jai
Musa Mahmood 3c2c07bff6 [0.0.14] Segmentation phase 5: has: search, and what is in the images in Stats
- has: searches what segmentation found, by the whole class name: has:dog, has:"dining table"
  (has:table is a table, not a dining table). Plain words do not match segments.
- The catalog keeps each capture's segments (class, score, area) and its classes between newlines,
  so a has: term is one contains per capture.
- Stats: "What is in the images", how many of the segmented images hold each class, each one a
  has: search; shown even when nothing has been read by OCR. -stats logs it.
- Tests: has: (whole names, quoted names, case, combinations, plain words not matching).
2026-10-10 04:25:24 +01:00

151 lines
7.5 KiB
Plaintext

// The catalog's pieces on their own (app/catalog.jai, app/viewer.jai): the search query's parsing and
// matching, the RTF stripper on Snagit's annotations, and the grid's order by date.
#scope_file
checks_run := 0;
checks_failed := 0;
MAX_GROUPS :: 8;
group_names : [MAX_GROUPS] string;
group_checks : [MAX_GROUPS] int;
group_fails : [MAX_GROUPS] int;
group_count := 0;
output_builder: *String_Builder;
emit :: (format_string: string, args: ..Any) {
if output_builder print_to_builder(output_builder, format_string, ..args);
else print(format_string, ..args);
} @PrintLike
group :: (name: string) {
assert(group_count < MAX_GROUPS, "raise MAX_GROUPS");
group_names[group_count] = name;
group_count += 1;
emit("\n-- % --\n", name);
}
expect :: (condition: bool, format: string, args: ..Any, loc := #caller_location) {
checks_run += 1;
if group_count group_checks[group_count-1] += 1;
if condition return;
checks_failed += 1;
if group_count group_fails[group_count-1] += 1;
emit(" FAIL [%:%] %\n", group_names[max(group_count-1, 0)], loc.line_number, tprint(format, ..args));
}
report :: () -> bool {
emit("\n");
for 0..group_count-1 {
if group_fails[it] emit(" % -- % checks, % FAILED\n", group_names[it], group_checks[it], group_fails[it]);
else emit(" % -- % checks\n", group_names[it], group_checks[it]);
}
if checks_failed emit("\n% OF % CATALOG CHECKS FAILED\n", checks_failed, checks_run);
else emit("\nALL CATALOG TESTS PASSED (% checks over % groups)\n", checks_run, group_count);
return checks_failed == 0;
}
query_tests :: () {
group("query");
terms := search_parse(" KiCad \"Footprint Assignments\" url:digikey note:late http://x.y ");
expect(terms.count == 5, "five terms (got %)", terms.count);
if terms.count != 5 return;
expect(terms[0].field == .ANY && terms[0].text == "kicad", "a word, lowercased: %", terms[0]);
expect(terms[1].field == .ANY && terms[1].text == "footprint assignments", "a phrase: %", terms[1]);
expect(terms[2].field == .URL && terms[2].text == "digikey", "a field prefix: %", terms[2]);
expect(terms[3].field == .NOTE && terms[3].text == "late", "note: %", terms[3]);
expect(terms[4].field == .ANY && terms[4].text == "http://x.y", "an unknown prefix stays part of the word: %", terms[4]);
labels := search_parse("label:schematic like:\"a circuit board\"");
expect(labels.count == 2 && labels[0].field == .LABEL && labels[0].text == "schematic" && labels[1].field == .LIKE && labels[1].text == "a circuit board",
"label: and like: prefixes: %", labels);
phrase := search_parse("title:\"two words\"");
expect(phrase.count == 1 && phrase[0].field == .TITLE && phrase[0].text == "two words", "a prefixed phrase: %", phrase);
expect(search_parse(" ").count == 0, "blank is no terms");
}
match_tests :: () {
group("matching");
entry: Catalog_Entry;
entry.find_title = "assign footprints - kicad";
entry.find_url = "https://www.digikey.com/x";
entry.find_app = "kicad eda 7.0.2";
entry.find_notes = "late";
entry.find_ocr = "0.1uf : capacitor_smd:c_0201_0603metric\nc1 -\n";
expect(search_matches(*entry, search_parse("capacitor kicad")), "words from different fields all match");
expect(!search_matches(*entry, search_parse("capacitor resistor")), "every word must match");
expect(search_matches(*entry, search_parse("url:digikey")), "a URL word");
expect(!search_matches(*entry, search_parse("title:digikey")), "a prefix limits the word to its field");
expect(search_matches(*entry, search_parse("ocr:\"c1 -\"")), "an OCR phrase");
expect(search_matches(*entry, search_parse("note:LATE")), "matching ignores case");
expect(search_matches(*entry, search_parse("")), "no terms match everything");
expect(!search_matches(null, search_parse("x")), "an entry not loaded matches no search");
entry.find_labels = "schematic\npcb layout\n";
expect(search_matches(*entry, search_parse("label:schematic")), "a label word");
expect(search_matches(*entry, search_parse("schematic")), "a plain word matches labels too");
expect(!search_matches(*entry, search_parse("ocr:schematic")), "an OCR word is not a label's");
expect(!search_matches(*entry, search_parse("like:anything")), "like: needs an image vector");
half := u16.[0x3C00];
entry.embedding = half;
expect(search_matches(*entry, search_parse("like:anything")), "like: takes a labelled capture (the ranking is the background's)");
entry.embedding = .[];
// has: a segment's class, the whole name (find_segments, as catalog_load makes it).
entry.find_segments = "\nperson\ndog\ndining table\n";
expect(search_matches(*entry, search_parse("has:dog")), "has: a class");
expect(search_matches(*entry, search_parse("has:DOG has:person")), "has: twice, ignoring case");
expect(search_matches(*entry, search_parse("has:\"dining table\"")), "has: a quoted name with a space");
expect(!search_matches(*entry, search_parse("has:table")), "has: is the whole name: table is not dining table");
expect(!search_matches(*entry, search_parse("has:do")), "nor part of a name");
expect(!search_matches(*entry, search_parse("has:cat")), "a class it does not hold");
expect(!search_matches(*entry, search_parse("dog")), "a plain word does not match segments");
expect(search_matches(*entry, search_parse("has:dog kicad")), "has: with other words");
terms := search_parse("has:dog");
expect(terms.count == 1 && terms[0].field == .HAS, "the has: prefix is read");
entry.find_segments = "";
expect(!search_matches(*entry, search_parse("has:dog")), "nothing segmented: no has: matches");
expect(search_line_matches("0.1uF : Capacitor_SMD", search_parse("capacitor")), "a line holding a word");
expect(!search_line_matches("0.1uF : Capacitor_SMD", search_parse("title:capacitor")), "a title word is not an OCR line's");
}
rtf_tests :: () {
group("rtf");
// A Snagit text annotation, as stored (base64 RTF), whose text is "late".
encoded :: "e1xydGYxXGFuc2lcYW5zaWNwZzEyNTJcZGVmZjBcZGVmbGFuZzEwMzN7XGZvbnR0Ymx7XGYwXGZzd2lzc1xmcHJxMlxmY2hhcnNldDAgQXJpYWw7fX0NCntcY29sb3J0YmwgO1xyZWQyNTJcZ3JlZW42NlxibHVlNjY7fQ0KXHZpZXdraW5kNFx1YzFccGFyZFxxY1x0eDcyMFxjZjFcZjBcZnMyNCBsYXRlXHBhcg0KfQ0K";
rtf, decoded := base64_decode(encoded,, temp);
expect(decoded, "the sample decodes");
text := rtf_to_text(rtf);
expect(text == "late", "a Snagit annotation reads as its text: \"%\"", text);
text = rtf_to_text("{\\rtf1{\\fonttbl{\\f0 Arial;}}{\\*\\generator Riched20;}one\\par two\\tab three caf\\'e9 \\u8364? \\{x\\}}");
expect(text == "one\ntwo\tthree caf\xC3\xA9 \xE2\x82\xAC {x}", "breaks, tabs, \\'hh, \\uN and escaped braces: \"%\"", text);
expect(rtf_to_text("{\\rtf1\\ansi\\'93quoted\\'94}") == "\xE2\x80\x9Cquoted\xE2\x80\x9D", "Windows-1252 quotes");
}
order_tests :: () {
group("order");
expect(viewer_display_order("2026-10-07 08:00:00", "a", "2025-01-01 00:00:00", "b") < 0, "newer first");
expect(viewer_display_order("", "a", "2025-01-01 00:00:00", "b") > 0, "undated after dated");
expect(viewer_display_order("2025-01-01 00:00:00", "a", "", "b") < 0, "dated before undated");
expect(viewer_display_order("2025-01-01 00:00:00", "b", "2025-01-01 00:00:00", "a") < 0, "the same date: by name, newest first");
}
#scope_export
run_catalog_tests :: (builder: *String_Builder = null) -> bool {
output_builder = builder;
emit("=== catalog test suite ===\n");
push_allocator(context.default_allocator);
query_tests();
match_tests();
rtf_tests();
order_tests();
return report();
}