diff --git a/scripts/tests/test_font_checks.py b/scripts/tests/test_font_checks.py new file mode 100644 index 000000000..14ee03c55 --- /dev/null +++ b/scripts/tests/test_font_checks.py @@ -0,0 +1,32 @@ +import base64 +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "test/scripts")) +import check_fonts + + +class FontChecksTest(unittest.TestCase): + def test_each_embedding_keeps_its_family_without_duplicate_orphans(self): + payload = base64.b64encode(b"font bytes").decode() + url = f"data:font/ttf;base64,{payload}" + html = (f"" + f"

{url}

") + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "fonts.html" + path.write_text(html) + self.assertEqual(list(check_fonts.fonts_in(path)), [ + ("A", b"font bytes"), ("B", b"font bytes"), + ("(no @font-face)", b"font bytes"), + ]) + + def test_empty_output_is_not_a_successful_check(self): + with tempfile.TemporaryDirectory() as directory: + with patch.object(sys, "argv", ["check_fonts.py", directory]), \ + patch.object(check_fonts, "find_ots", return_value=[]): + with self.assertRaisesRegex(SystemExit, "No HTML files"): + check_fonts.main() diff --git a/test/scripts/check_fonts.py b/test/scripts/check_fonts.py index 85c1ebd01..3f74c5cb8 100755 --- a/test/scripts/check_fonts.py +++ b/test/scripts/check_fonts.py @@ -59,7 +59,7 @@ def html_files(roots): def fonts_in(path): - """(family, bytes) for every font the file embeds, in document order.""" + """Embedded fonts with their declared family, if any.""" with open(path, encoding="utf-8", errors="replace") as file: content = file.read() seen_at = set() @@ -67,7 +67,7 @@ def fonts_in(path): body = face.group(1) family = match.group(1) if (match := FONT_FAMILY.search(body)) else "?" for data in FONT_DATA_URL.finditer(body): - seen_at.add(face.start() + data.start()) + seen_at.add(face.start(1) + data.start()) yield family, decode(data.group(1), path, family) for data in FONT_DATA_URL.finditer(content): if data.start() not in seen_at: @@ -110,13 +110,18 @@ def main(): # distinct payload once and report every place it came from. verdicts = {} occurrences = {} + files = 0 for path in html_files(arguments.roots): + files += 1 for family, font in fonts_in(path): digest = hashlib.sha256(font).hexdigest() if digest not in verdicts: verdicts[digest] = sanitize(ots, font) occurrences.setdefault(digest, []).append((path, family)) + if files == 0: + sys.exit("No HTML files found to check") + failed = {d: v for d, v in verdicts.items() if v is not None} for digest, complaint in sorted(failed.items()): where = occurrences[digest] diff --git a/test/src/formula_corpus_test.cpp b/test/src/formula_corpus_test.cpp index 38cb273dd..cdd4b922a 100644 --- a/test/src/formula_corpus_test.cpp +++ b/test/src/formula_corpus_test.cpp @@ -125,9 +125,8 @@ void evaluate_file(const TestFile &file, Tally &tally) { } // namespace -/// Decision 29 of `docs/design/spreadsheet-editing.md`: an answer is never -/// wrong. Every formula of the spreadsheets in the corpus is evaluated, and -/// each answer is compared with the result the file caches. +/// Compare supported formulas with cached results (spreadsheet-editing decision +/// 29). TEST(FormulaCorpus, every_answer_is_the_result_the_file_caches) { Tally tally; for (const FileType type : {FileType::opendocument_spreadsheet, @@ -140,6 +139,11 @@ TEST(FormulaCorpus, every_answer_is_the_result_the_file_caches) { } } + if (tally.formulas == 0) { + GTEST_SKIP() << "No formula-bearing corpus files available"; + } + ASSERT_GT(tally.answered, 0u) << "No cached formula results were checked"; + std::cout << tally.answered << " of " << tally.formulas << " formulas answered, " << tally.differences.size() << " differ from the cached result" << std::endl; diff --git a/test/src/odr_test.cpp b/test/src/odr_test.cpp index d44e2cc3a..8f3a5732e 100644 --- a/test/src/odr_test.cpp +++ b/test/src/odr_test.cpp @@ -246,88 +246,95 @@ TEST(FileTypeTable, capabilities_build_on_each_other) { /// Holds the declared `color_scheme` against what the renderer emits. TEST(FileTypeCapabilities, color_scheme_matches_the_html) { - const auto &logger = Logger::null(); - + std::size_t candidates = 0; for (const FileType type : every_file_type()) { const FileTypeCapabilities declared = capabilities_by_file_type(type); if (!declared.translate_html) { continue; } - - const std::vector test_files = TestData::test_files(type); - if (test_files.empty() || test_files.front().password.has_value()) { - continue; + const std::size_t before = candidates; + bool checked = false; + std::string error; + for (const TestFile &test_file : TestData::test_files(type)) { + if (test_file.password.has_value()) { + continue; + } + ++candidates; + try { + const DecodedFile file = + open(test_file.absolute_path, DecodeOptions::as(type)); + const auto render = [&](const HtmlColorScheme scheme) { + HtmlConfig config; + config.color_scheme = scheme; + // one page of a pdf is enough, and the rest is slow + config.page_range_end = 1; + std::ostringstream out; + html::translate(file, config).list_views().at(0).write_html(out); + return out.str(); + }; + EXPECT_EQ(render(HtmlColorScheme::light) != + render(HtmlColorScheme::dark), + declared.color_scheme) + << test_file.short_path; + checked = true; + break; + } catch (const std::exception &failure) { + error = test_file.short_path + ": " + failure.what(); + } } - - std::optional file; - try { - file = open(test_files.front().absolute_path, DecodeOptions::as(type), - logger); - } catch (...) { - continue; + if (candidates != before) { + EXPECT_TRUE(checked) << file_type_to_string(type) << ": " << error; } - - const auto render = [&](const HtmlColorScheme scheme) { - HtmlConfig config; - config.color_scheme = scheme; - // one page of a pdf is enough, and the rest is slow - config.page_range_end = 1; - - std::ostringstream out; - html::translate(*file, config).list_views().at(0).write_html(out); - return out.str(); - }; - - std::string light; - std::string dark; - try { - light = render(HtmlColorScheme::light); - dark = render(HtmlColorScheme::dark); - } catch (...) { - continue; - } - - EXPECT_EQ(light != dark, declared.color_scheme) - << file_type_to_string(type) << " " << test_files.front().short_path; + } + if (candidates == 0) { + GTEST_SKIP() << "No unencrypted corpus files available for rendering"; } } -/// The anti-drift check behind `capabilities_by_file_type`: what the engines -/// actually do must never exceed what the table declares. Capabilities are -/// per-format constants today, so a couple of files per type is enough. +/// Engine capabilities must stay within the format's declarations. TEST(FileTypeCapabilities, declaration_matches_the_engines) { - static constexpr std::size_t files_per_file_type = 2; - - const auto &logger = Logger::null(); - + constexpr std::size_t files_per_file_type = 2; + std::size_t candidates = 0; for (const FileType type : every_file_type()) { const FileTypeCapabilities declared = capabilities_by_file_type(type); - const std::vector test_files = TestData::test_files(type); - - const std::size_t count = std::min(files_per_file_type, test_files.size()); - for (std::size_t i = 0; i < count; ++i) { - const TestFile &test_file = test_files[i]; - + const std::size_t before = candidates; + std::size_t checked = 0; + std::string error; + for (const TestFile &test_file : TestData::test_files(type)) { + if (checked == files_per_file_type) { + break; + } + if ((declared.edit || declared.save) && test_file.password.has_value()) { + continue; + } + ++candidates; if (!declared.open) { EXPECT_ANY_THROW(std::ignore = open(test_file.absolute_path, - DecodeOptions::as(type), logger)) + DecodeOptions::as(type))) << test_file.short_path; + ++checked; continue; } std::optional file; + std::optional document; try { - file = open(test_file.absolute_path, DecodeOptions::as(type), logger); - } catch (...) { - // declared support is an upper bound — a single file may still fail + file = open(test_file.absolute_path, DecodeOptions::as(type)); + if (file->is_document_file() && !file->password_encrypted()) { + document = file->as_document_file().document(); + } + } catch (const std::exception &failure) { + // declared support is an upper bound: one file may fail, as long as + // another of its type passes + error = test_file.short_path + ": " + failure.what(); continue; } - - // whatever detection sees, the table has to admit to — from the bytes, + ++checked; + // whatever detection sees, the table has to admit to - from the bytes, // or from the name for a type that has no signature to find const std::vector detected = - list_file_types(test_file.absolute_path, logger); - if (std::ranges::find(detected, type) != std::ranges::end(detected)) { + list_file_types(test_file.absolute_path); + if (std::ranges::find(detected, type) != detected.end()) { EXPECT_TRUE(declared.detect_by_content || file_type_by_file_extension( Path(test_file.absolute_path).extension()) == type) @@ -343,31 +350,26 @@ TEST(FileTypeCapabilities, declaration_matches_the_engines) { << test_file.short_path; EXPECT_TRUE(!actual.translate_html || declared_actual.translate_html) << test_file.short_path; - - // A plain file is not a document, so `Document::is_savable` never - // answers for it - and without this the table could claim anything. + // a plain file is not a document, so `Document::is_savable` never + // answers for it, and without this the table could claim anything if (file->is_text_file()) { EXPECT_EQ(file->as_text_file().is_savable(), declared_actual.save) << test_file.short_path; } - - if (!file->is_document_file() || file->password_encrypted()) { - continue; - } - - std::optional document; - try { - document = file->as_document_file().document(); - } catch (...) { - continue; + if (document.has_value()) { + EXPECT_EQ(document->is_editable(), declared_actual.edit) + << test_file.short_path; + EXPECT_EQ(document->is_savable(false), declared_actual.save) + << test_file.short_path; + EXPECT_EQ(document->is_savable(true), declared_actual.encrypt) + << test_file.short_path; } - - EXPECT_EQ(document->is_editable(), declared_actual.edit) - << test_file.short_path; - EXPECT_EQ(document->is_savable(false), declared_actual.save) - << test_file.short_path; - EXPECT_EQ(document->is_savable(true), declared_actual.encrypt) - << test_file.short_path; } + if (candidates != before) { + EXPECT_GT(checked, 0u) << file_type_to_string(type) << ": " << error; + } + } + if (candidates == 0) { + GTEST_SKIP() << "No corpus files available for capability checks"; } }