| import os |
| import sys |
| import tempfile |
| import unittest |
| from unittest.mock import MagicMock, patch |
|
|
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) |
|
|
|
|
| class FakeModel: |
| "Minimal model stub for testing without GPU" |
| def __init__(self): |
| self.alphabet = {c: i + 3 for i, c in enumerate("ACDEFGHIKLMNPQRSTVWY")} |
| self.alphabet.update({'<pad>': 1, '<eos>': 2, '<cls>': 0}) |
|
|
|
|
| class TestAppValidation(unittest.TestCase): |
| "app() input guards raise gradio.Error on bad input" |
|
|
| @patch('data.ModelFactory', return_value=FakeModel()) |
| def test_empty_sequence_raises(self, _mock_factory): |
| from app import app |
| from gradio import Error |
| with self.assertRaises(Error) as ctx: |
| app("", "scan", "test/model", True) |
| self.assertIn("empty", str(ctx.exception).lower()) |
|
|
| @patch('data.ModelFactory', return_value=FakeModel()) |
| def test_empty_substitutions_raises(self, _mock_factory): |
| from app import app |
| from gradio import Error |
| with self.assertRaises(Error) as ctx: |
| app("MVEQYLLEAI", "", "test/model", True) |
| self.assertIn("substitution", str(ctx.exception).lower()) |
|
|
|
|
| class TestDmsLayout(unittest.TestCase): |
| "ncols/nrows layout calculation logic from _render_dms" |
|
|
| def _calc_layout(self, num_cols): |
| """Replicate the ncols selection logic from data.py""" |
| from math import ceil |
| ncols = min([d for d in range(1, num_cols + 1) if num_cols % d == 0 and 30 <= d <= 60] or [60], |
| key=lambda x: abs(x - 60)) |
| nrows = ceil(num_cols / ncols) |
| while num_cols / ncols < nrows and ncols > 45 and ncols * nrows >= num_cols: |
| ncols -= 1 |
| ncols += 1 |
| return ncols, nrows |
|
|
| def test_short_sequence(self): |
| |
| ncols, nrows = self._calc_layout(7) |
| self.assertEqual(nrows, 1) |
| self.assertGreater(ncols, 0) |
|
|
| def test_medium_sequence(self): |
| ncols, nrows = self._calc_layout(100) |
| self.assertGreaterEqual(ncols, 30) |
| self.assertTrue(ncols * nrows >= 100) |
|
|
| def test_long_sequence_fits_grid(self): |
| ncols, nrows = self._calc_layout(300) |
| self.assertTrue(ncols * nrows >= 300) |
|
|
| def test_very_long_sequence(self): |
| ncols, nrows = self._calc_layout(600) |
| self.assertTrue(ncols * nrows >= 600) |
|
|
|
|
| class TestDmsRender(unittest.TestCase): |
| "Full _render_dms pipeline produces PNG file" |
|
|
| @patch('data.ModelFactory', return_value=FakeModel()) |
| def test_render_creates_png(self, _mock_factory): |
| from data import Data |
| import pandas as pd |
| with tempfile.TemporaryDirectory() as tmpdir: |
| out_path = os.path.join(tmpdir, 'test_heatmap.png') |
| csv_path = os.path.join(tmpdir, 'test_out.csv') |
| d = object.__new__(Data) |
| d.model_name = 'test' |
| d.seq = "MVEQYLL" |
| d.mode = 'DMS' |
| d.resi = list(range(1, len(d.seq) + 1)) |
| |
| AA = "ACDEFGHIKLMNPQRSTVWY" |
| rows = [] |
| for i, src_c in enumerate(d.seq, 1): |
| for trg_c in AA.replace(src_c, ''): |
| rows.append(f"{src_c}{i}{trg_c}") |
| scores = [float(i % 5 - 2) for i in range(len(rows))] |
| d.out = pd.DataFrame({'0': rows, 'test': scores}) |
| d.out_img_path = out_path |
| d.out_csv = csv_path |
| d._render_dms() |
| self.assertTrue(os.path.exists(out_path), f"PNG not created at {out_path}") |
| self.assertTrue(os.path.getsize(out_path) > 0, "PNG is empty") |
|
|
|
|
| class TestParseOutputDispatch(unittest.TestCase): |
| "parse_output routes to correct handler per mode" |
|
|
| @patch('data.ModelFactory', return_value=FakeModel()) |
| def test_dms_dispatch_calls_render(self, _mock_factory): |
| from data import Data |
| import pandas as pd |
| with tempfile.TemporaryDirectory() as tmpdir: |
| d = object.__new__(Data) |
| d.model_name = 'test' |
| d.mode = 'DMS' |
| d.seq = "MV" |
| d.resi = [1, 2] |
| AA = "ACDEFGHIKLMNPQRSTVWY" |
| rows = [] |
| for src_c in "MV": |
| pos = {"M": 1, "V": 2}[src_c] |
| for trg_c in AA.replace(src_c, ''): |
| rows.append(f"{src_c}{pos}{trg_c}") |
| d.out = pd.DataFrame({'0': rows, 'test': list(range(len(rows)))}) |
| d.out_img_path = os.path.join(tmpdir, 't.png') |
| d.out_csv = os.path.join(tmpdir, 't.csv') |
| d.parse_output() |
| self.assertTrue(os.path.exists(d.out_img_path)) |
|
|
| @patch('data.ModelFactory', return_value=FakeModel()) |
| def test_mut_dispatch_sorts_and_styles(self, _mock_factory): |
| from data import Data |
| import pandas as pd |
| with tempfile.TemporaryDirectory() as tmpdir: |
| d = object.__new__(Data) |
| d.model_name = 'test' |
| d.mode = 'MUT' |
| d.out = pd.DataFrame({'0': ['V2A', 'E5K'], 'test': [3.0, -1.0]}) |
| d.out_csv = os.path.join(tmpdir, 'm.csv') |
| d.out_img_path = os.path.join(tmpdir, 'm.png') |
| d.parse_output() |
| |
| self.assertEqual(d.out.iloc[0]['0'], 'V2A') |
| self.assertIsNotNone(d.out_table) |
|
|
|
|
| if __name__ == '__main__': |
| unittest.main() |
|
|