- 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).
151 lines
7.5 KiB
Plaintext
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();
|
|
}
|