"""Error-display tests. Tool pages catch core exceptions (via ``format_for_user``) and surface them through ``st.error``. We verify that the message structure makes it through the GUI layer, not just that it gets raised by core (the core tests already cover that). These tests deliberately feed garbage bytes / malformed content and check the rendered error, not just that the page didn't crash. """ from __future__ import annotations import pytest from .conftest import collected_text, stash_upload # --------------------------------------------------------------------------- # Malformed upload # --------------------------------------------------------------------------- class TestMalformedUploadErrors: """Bytes that look like a CSV but aren't parseable. The Deduplicator page wraps ``read_file`` failures in an ``st.error`` with the file name and the structured ``format_for_user`` output.""" @pytest.fixture def garbage_bytes(self) -> bytes: """Binary garbage with embedded NULs and non-UTF-8 sequences — triggers the gate's repair pipeline failures, ultimately produces a parse error on the dedup page if it makes it that far. We bypass the gate so the dedup page sees it raw.""" return b"\xff\xfe\x00\x01\x02garbage,without,structure\n\x00\xff" * 50 def test_garbage_bytes_do_not_crash_dedup( self, app_factory, garbage_bytes, ): app = app_factory("1_Deduplicator") stash_upload(app, name="garbage.csv", data=garbage_bytes) app.run() # The page should either render an error OR successfully parse # the bytes as text (the gate has been pre-passed, so the # pre-parse repair didn't run on this fixture). We just need # no uncaught Python exception. assert not app.exception # --------------------------------------------------------------------------- # Empty upload # --------------------------------------------------------------------------- class TestEmptyUpload: """Zero-byte upload — must be handled gracefully.""" def test_empty_bytes_renders(self, app_factory): app = app_factory("1_Deduplicator") stash_upload(app, name="empty.csv", data=b"") app.run() # Either: (a) we render an error, or (b) we render the page # with no preview. Either is acceptable — what's NOT is an # uncaught Python exception bubbling up. assert not app.exception # --------------------------------------------------------------------------- # Single-column file # --------------------------------------------------------------------------- class TestSingleColumnFile: """A 1-column CSV is technically valid but produces no auto-detect strategies. The page must explain this to the user rather than silently producing zero match groups.""" def test_single_column_does_not_crash(self, app_factory): app = app_factory("1_Deduplicator") data = b"only_col\nvalue1\nvalue2\nvalue3\n" stash_upload(app, name="single.csv", data=data) app.run() assert not app.exception # --------------------------------------------------------------------------- # Header collision in column_mapper # --------------------------------------------------------------------------- class TestColumnMapperDuplicateTarget: """The column mapper rejects mappings where two source columns point at the same target. This is surfaced as an error. Test approach: ``map_columns`` validates upfront via core, and raises ``InputValidationError`` — the GUI wraps it. We invoke the core function directly to pin the validation contract.""" def test_duplicate_target_raises(self): import pandas as pd from src.core.column_mapper import map_columns, MapOptions from src.core.errors import InputValidationError df = pd.DataFrame({"a": [1, 2], "b": [3, 4]}) opts = MapOptions(mapping={"a": "name", "b": "name"}) with pytest.raises(InputValidationError): map_columns(df, opts)