Files
kernalix7--winpodx/tests/test_reverse_open_mime.py
wehub-resource-sync 3e779be6f3
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
CI / test (3.14, ubuntu-24.04-arm) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:37 +08:00

322 lines
12 KiB
Python

# SPDX-License-Identifier: MIT
"""Tests for ``winpodx.reverse_open.mime`` (#48 phase 1).
Covers the curated table, the defensive contract (never raises,
returns ``[]`` on bad input), case + whitespace normalisation, the
ambiguous-types decision (first-only for ``mime_to_extensions``,
all for ``mime_to_all_extensions``), and the graceful degradation
when pyxdg isn't installed.
"""
from __future__ import annotations
import sys
import textwrap
from pathlib import Path
import pytest
from winpodx.reverse_open import mime as mime_mod
from winpodx.reverse_open.mime import (
CURATED_MIME_EXT,
mime_to_all_extensions,
mime_to_extensions,
)
# ---------------------------------------------------------------------------
# Curated table — happy path
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"mime,expected",
[
("text/plain", [".txt"]),
("text/xml", [".xml"]),
("text/css", [".css"]),
("text/csv", [".csv"]),
("text/markdown", [".md"]),
("application/json", [".json"]),
("application/pdf", [".pdf"]),
("application/zip", [".zip"]),
("application/x-tar", [".tar"]),
("application/gzip", [".gz"]),
("application/x-7z-compressed", [".7z"]),
("application/rtf", [".rtf"]),
("application/msword", [".doc"]),
(
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
[".docx"],
),
("application/vnd.ms-excel", [".xls"]),
(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
[".xlsx"],
),
("application/vnd.ms-powerpoint", [".ppt"]),
(
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
[".pptx"],
),
("application/javascript", [".js"]),
("application/x-yaml", [".yaml"]),
("application/toml", [".toml"]),
("image/png", [".png"]),
("image/jpeg", [".jpg"]),
("image/gif", [".gif"]),
("image/bmp", [".bmp"]),
("image/webp", [".webp"]),
("image/svg+xml", [".svg"]),
("image/tiff", [".tiff"]),
("image/x-icon", [".ico"]),
("audio/mpeg", [".mp3"]),
("audio/wav", [".wav"]),
("audio/flac", [".flac"]),
("audio/x-m4a", [".m4a"]),
("video/mp4", [".mp4"]),
("video/x-matroska", [".mkv"]),
("video/webm", [".webm"]),
("video/quicktime", [".mov"]),
("video/x-msvideo", [".avi"]),
("font/ttf", [".ttf"]),
("font/otf", [".otf"]),
("font/woff", [".woff"]),
("font/woff2", [".woff2"]),
],
)
def test_curated_happy_path(mime, expected):
assert mime_to_extensions(mime) == expected
def test_curated_table_minimum_size():
"""The design budget is 'top ~80 unambiguous mappings'.
Don't tie this exactly to 80 (additions are fine), but guard
the floor so a future cleanup doesn't accidentally gut the table.
"""
assert len(CURATED_MIME_EXT) >= 60
def test_curated_extensions_all_lowercase_with_dot():
"""Canonical form is leading-dot lowercase: '.txt', not 'txt' or '.TXT'."""
for mime, exts in CURATED_MIME_EXT.items():
assert exts, f"{mime!r} has no extensions"
for ext in exts:
assert ext.startswith("."), f"{mime}: {ext!r} missing leading dot"
assert ext == ext.lower(), f"{mime}: {ext!r} not lowercase"
# ---------------------------------------------------------------------------
# Ambiguous-types decision (design #1)
# ---------------------------------------------------------------------------
def test_ambiguous_returns_first_extension_only():
"""text/html → ['.html'] (not ['.html', '.htm'])."""
assert mime_to_extensions("text/html") == [".html"]
# image/jpeg is similarly multi-extension in the curated table.
assert mime_to_extensions("image/jpeg") == [".jpg"]
def test_all_extensions_returns_full_set():
"""mime_to_all_extensions surfaces every known extension."""
assert mime_to_all_extensions("text/html") == [".html", ".htm"]
assert mime_to_all_extensions("image/jpeg") == [".jpg", ".jpeg"]
assert mime_to_all_extensions("image/tiff") == [".tiff", ".tif"]
def test_all_extensions_returns_copy_not_reference():
"""Mutating the returned list must not corrupt the curated table."""
result = mime_to_all_extensions("text/html")
result.append(".bogus")
# Re-fetch and confirm CURATED_MIME_EXT is intact.
assert mime_to_all_extensions("text/html") == [".html", ".htm"]
assert CURATED_MIME_EXT["text/html"] == [".html", ".htm"]
# ---------------------------------------------------------------------------
# Defensive contract — never raises, returns []
# ---------------------------------------------------------------------------
def test_unknown_mime_returns_empty_list():
assert mime_to_extensions("application/x-totally-made-up-by-the-test") == []
assert mime_to_all_extensions("application/x-totally-made-up-by-the-test") == []
def test_empty_string_returns_empty_list():
assert mime_to_extensions("") == []
assert mime_to_all_extensions("") == []
def test_whitespace_only_returns_empty_list():
assert mime_to_extensions(" ") == []
assert mime_to_all_extensions("\t\n ") == []
@pytest.mark.parametrize("bad_input", [None, 42, [], {}, object(), 3.14, b"text/plain"])
def test_non_string_input_returns_empty_list(bad_input):
"""Defensive: a malformed .desktop MimeType= field shouldn't crash refresh."""
assert mime_to_extensions(bad_input) == [] # type: ignore[arg-type]
assert mime_to_all_extensions(bad_input) == [] # type: ignore[arg-type]
# ---------------------------------------------------------------------------
# Case + whitespace normalisation
# ---------------------------------------------------------------------------
def test_case_insensitive_lookup():
assert mime_to_extensions("TEXT/PLAIN") == [".txt"]
assert mime_to_extensions("Text/Plain") == [".txt"]
assert mime_to_extensions("APPLICATION/PDF") == [".pdf"]
assert mime_to_all_extensions("TEXT/HTML") == [".html", ".htm"]
def test_leading_trailing_whitespace_stripped():
assert mime_to_extensions(" text/plain ") == [".txt"]
assert mime_to_extensions("\ttext/plain\n") == [".txt"]
assert mime_to_all_extensions(" text/html\n") == [".html", ".htm"]
def test_combined_whitespace_and_case():
assert mime_to_extensions(" TEXT/Plain ") == [".txt"]
# ---------------------------------------------------------------------------
# pyxdg soft-dep behaviour
# ---------------------------------------------------------------------------
def test_works_without_xdg_module(monkeypatch):
"""Simulate pyxdg not installed; non-curated lookups must still
not raise. Either the stdlib XML fallback finds it (on a
typical Linux dev box) or we return [].
"""
# Reset the import-tried sentinel and force the import to fail.
monkeypatch.setattr(mime_mod, "_XDG_IMPORT_TRIED", False)
monkeypatch.setattr(mime_mod, "_XDG_MIME_MODULE", None)
monkeypatch.setitem(sys.modules, "xdg", None) # forces ImportError
# Curated still works regardless of pyxdg state.
assert mime_to_extensions("text/plain") == [".txt"]
# Unknown still returns []; never raises.
assert mime_to_extensions("application/x-totally-fake") == []
def test_xdg_import_attempted_only_once(monkeypatch):
"""The 'pyxdg not installed' INFO log fires once per process."""
monkeypatch.setattr(mime_mod, "_XDG_IMPORT_TRIED", False)
monkeypatch.setattr(mime_mod, "_XDG_MIME_MODULE", None)
monkeypatch.setitem(sys.modules, "xdg", None)
# First call attempts import.
mime_mod._try_import_xdg()
assert mime_mod._XDG_IMPORT_TRIED is True
# Second call is a no-op; the sentinel short-circuits.
# We can't easily count log calls, but we can verify the flag
# stays True and the function still returns the cached None.
assert mime_mod._try_import_xdg() is None
assert mime_mod._XDG_IMPORT_TRIED is True
# ---------------------------------------------------------------------------
# stdlib XML fallback — synthetic database
# ---------------------------------------------------------------------------
def test_stdlib_xml_fallback_finds_unknown_type(monkeypatch, tmp_path):
"""Drop a synthetic freedesktop.org.xml in place and confirm
the stdlib parser path picks up a non-curated type.
"""
fake_db = tmp_path / "freedesktop.org.xml"
fake_db.write_text(
textwrap.dedent(
"""\
<?xml version="1.0" encoding="utf-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-winpodx-fake">
<glob pattern="*.wpx"/>
<glob pattern="*.winpodx"/>
</mime-type>
</mime-info>
"""
)
)
monkeypatch.setattr(mime_mod, "_FDO_MIME_XML", Path(fake_db))
# Ensure pyxdg path is bypassed.
monkeypatch.setattr(mime_mod, "_XDG_IMPORT_TRIED", True)
monkeypatch.setattr(mime_mod, "_XDG_MIME_MODULE", None)
assert mime_to_extensions("application/x-winpodx-fake") == [".wpx"]
assert mime_to_all_extensions("application/x-winpodx-fake") == [".wpx", ".winpodx"]
def test_stdlib_xml_fallback_skips_complex_globs(monkeypatch, tmp_path):
"""Multi-dot patterns (*.tar.gz) and literal filenames (Makefile)
don't map cleanly to a Windows extension — they must be skipped.
"""
fake_db = tmp_path / "freedesktop.org.xml"
fake_db.write_text(
textwrap.dedent(
"""\
<?xml version="1.0" encoding="utf-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-winpodx-multidot">
<glob pattern="*.tar.gz"/>
<glob pattern="Makefile"/>
<glob pattern="*.wpd2"/>
</mime-type>
</mime-info>
"""
)
)
monkeypatch.setattr(mime_mod, "_FDO_MIME_XML", Path(fake_db))
monkeypatch.setattr(mime_mod, "_XDG_IMPORT_TRIED", True)
monkeypatch.setattr(mime_mod, "_XDG_MIME_MODULE", None)
# Only .wpd2 survives the filter.
assert mime_to_all_extensions("application/x-winpodx-multidot") == [".wpd2"]
assert mime_to_extensions("application/x-winpodx-multidot") == [".wpd2"]
def test_stdlib_xml_fallback_handles_aliases(monkeypatch, tmp_path):
"""An alias should resolve to the canonical type's globs."""
fake_db = tmp_path / "freedesktop.org.xml"
fake_db.write_text(
textwrap.dedent(
"""\
<?xml version="1.0" encoding="utf-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-winpodx-canonical">
<alias type="application/x-winpodx-old"/>
<glob pattern="*.wpc"/>
</mime-type>
</mime-info>
"""
)
)
monkeypatch.setattr(mime_mod, "_FDO_MIME_XML", Path(fake_db))
monkeypatch.setattr(mime_mod, "_XDG_IMPORT_TRIED", True)
monkeypatch.setattr(mime_mod, "_XDG_MIME_MODULE", None)
# Canonical resolves directly.
assert mime_to_extensions("application/x-winpodx-canonical") == [".wpc"]
# Alias resolves via the alias-walk in the parser.
assert mime_to_extensions("application/x-winpodx-old") == [".wpc"]
def test_stdlib_xml_fallback_missing_db_returns_empty(monkeypatch, tmp_path):
"""If the freedesktop.org.xml file doesn't exist, lookups for
non-curated types return [] — they must not raise.
"""
nonexistent = tmp_path / "nope-not-here.xml"
monkeypatch.setattr(mime_mod, "_FDO_MIME_XML", Path(nonexistent))
monkeypatch.setattr(mime_mod, "_XDG_IMPORT_TRIED", True)
monkeypatch.setattr(mime_mod, "_XDG_MIME_MODULE", None)
assert mime_to_extensions("application/x-not-in-curated-or-anywhere") == []
assert mime_to_all_extensions("application/x-not-in-curated-or-anywhere") == []