Spaces:
Sleeping
Sleeping
| """Unit tests for mosaic UI utility functions.""" | |
| import tempfile | |
| from pathlib import Path | |
| import pandas as pd | |
| import pytest | |
| from mosaic.ui.utils import ( | |
| IHC_SUBTYPES, | |
| SETTINGS_COLUMNS, | |
| load_settings, | |
| validate_settings, | |
| export_to_csv, | |
| get_oncotree_code_name, | |
| oncotree_code_map, | |
| ) | |
| class TestConstants: | |
| """Test constants in gradio_app.""" | |
| def test_ihc_subtypes_list(self): | |
| """Test that IHC_SUBTYPES is a list.""" | |
| assert isinstance(IHC_SUBTYPES, list) | |
| def test_ihc_subtypes_has_entries(self): | |
| """Test that IHC_SUBTYPES has entries.""" | |
| assert len(IHC_SUBTYPES) > 0 | |
| def test_ihc_subtypes_contains_expected_values(self): | |
| """Test that IHC_SUBTYPES contains expected breast cancer subtypes.""" | |
| expected_subtypes = ["HR+/HER2+", "HR+/HER2-", "HR-/HER2+", "HR-/HER2-"] | |
| for subtype in expected_subtypes: | |
| assert subtype in IHC_SUBTYPES | |
| def test_ihc_subtypes_includes_empty_string(self): | |
| """Test that IHC_SUBTYPES includes empty string for non-breast cancers.""" | |
| assert "" in IHC_SUBTYPES | |
| def test_settings_columns_list(self): | |
| """Test that SETTINGS_COLUMNS is a list.""" | |
| assert isinstance(SETTINGS_COLUMNS, list) | |
| def test_settings_columns_required_fields(self): | |
| """Test that SETTINGS_COLUMNS contains required fields.""" | |
| required_fields = [ | |
| "Slide", | |
| "Site Type", | |
| "Cancer Subtype", | |
| "IHC Subtype", | |
| "Segmentation Config", | |
| ] | |
| for field in required_fields: | |
| assert field in SETTINGS_COLUMNS | |
| class TestLoadSettings: | |
| """Test load_settings function.""" | |
| def sample_cancer_subtype_maps(self): | |
| """Create sample cancer subtype maps for testing.""" | |
| cancer_subtypes = ["LUAD", "BRCA", "COAD"] | |
| cancer_subtype_name_map = { | |
| "Lung Adenocarcinoma (LUAD)": "LUAD", | |
| "Breast Invasive Carcinoma (BRCA)": "BRCA", | |
| "Colon Adenocarcinoma (COAD)": "COAD", | |
| "Unknown": "UNK", | |
| } | |
| reversed_cancer_subtype_name_map = { | |
| value: key for key, value in cancer_subtype_name_map.items() | |
| } | |
| return ( | |
| cancer_subtype_name_map, | |
| cancer_subtypes, | |
| reversed_cancer_subtype_name_map, | |
| ) | |
| def temp_settings_csv(self): | |
| """Create a temporary settings CSV file with all columns.""" | |
| with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".csv") as f: | |
| f.write("Slide,Site Type,Sex,Cancer Subtype,IHC Subtype,Segmentation Config\n") | |
| f.write("slide1.svs,Primary,Male,Unknown,,Biopsy\n") | |
| f.write("slide2.svs,Metastatic,Female,Unknown,,Resection\n") | |
| temp_path = f.name | |
| yield temp_path | |
| Path(temp_path).unlink() | |
| def temp_minimal_settings_csv(self): | |
| """Create a temporary settings CSV file with minimal columns.""" | |
| with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".csv") as f: | |
| f.write("Slide,Site Type,Sex\n") | |
| f.write("slide1.svs,Primary,Male\n") | |
| f.write("slide2.svs,Metastatic,Female\n") | |
| temp_path = f.name | |
| yield temp_path | |
| Path(temp_path).unlink() | |
| def test_load_settings_returns_dataframe(self, temp_settings_csv): | |
| """Test that load_settings returns a DataFrame.""" | |
| df = load_settings(temp_settings_csv) | |
| assert isinstance(df, pd.DataFrame) | |
| def test_load_settings_has_all_columns(self, temp_settings_csv): | |
| """Test that all required columns are present.""" | |
| df = load_settings(temp_settings_csv) | |
| for col in SETTINGS_COLUMNS: | |
| assert col in df.columns | |
| def test_load_settings_adds_missing_columns(self, temp_minimal_settings_csv): | |
| """Test that missing columns are added with defaults.""" | |
| df = load_settings(temp_minimal_settings_csv) | |
| assert "Segmentation Config" in df.columns | |
| assert "Cancer Subtype" in df.columns | |
| assert "IHC Subtype" in df.columns | |
| assert df["Segmentation Config"].iloc[0] == "Biopsy" | |
| assert df["Cancer Subtype"].iloc[0] == "Unknown" | |
| assert df["IHC Subtype"].iloc[0] == "" | |
| def test_load_settings_preserves_data(self, temp_settings_csv): | |
| """Test that data is preserved correctly.""" | |
| df = load_settings(temp_settings_csv) | |
| assert len(df) == 2 | |
| assert df["Slide"].iloc[0] == "slide1.svs" | |
| assert df["Site Type"].iloc[0] == "Primary" | |
| def test_load_settings_missing_required_column_raises_error(self): | |
| """Test that missing required column raises ValueError.""" | |
| with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".csv") as f: | |
| f.write("RandomColumn\n") | |
| f.write("value\n") | |
| temp_path = f.name | |
| try: | |
| with pytest.raises(ValueError, match="Missing required column"): | |
| load_settings(temp_path) | |
| finally: | |
| Path(temp_path).unlink() | |
| def test_load_settings_filters_to_settings_columns(self, temp_settings_csv): | |
| """Test that only SETTINGS_COLUMNS are returned.""" | |
| df = load_settings(temp_settings_csv) | |
| assert list(df.columns) == SETTINGS_COLUMNS | |
| class TestGetOncotreeCodeName: | |
| """Test get_oncotree_code_name function.""" | |
| def test_oncotree_code_name_caching(self, mocker): | |
| """Test that oncotree code names are cached.""" | |
| # Mock the requests.get call | |
| mock_response = mocker.Mock() | |
| mock_response.status_code = 200 | |
| mock_response.json.return_value = [{"name": "Lung Adenocarcinoma"}] | |
| mocker.patch("requests.get", return_value=mock_response) | |
| # Clear the cache | |
| oncotree_code_map.clear() | |
| # First call should populate cache | |
| code = "LUAD" | |
| result1 = get_oncotree_code_name(code) | |
| # Cache should now contain the code | |
| assert code in oncotree_code_map | |
| # Second call should use cache | |
| result2 = get_oncotree_code_name(code) | |
| assert result1 == result2 | |
| def test_oncotree_code_name_returns_string(self, mocker): | |
| """Test that function returns a string.""" | |
| # Mock the requests.get call | |
| mock_response = mocker.Mock() | |
| mock_response.status_code = 200 | |
| mock_response.json.return_value = [{"name": "Lung Adenocarcinoma"}] | |
| mocker.patch("requests.get", return_value=mock_response) | |
| # Clear cache first | |
| oncotree_code_map.clear() | |
| result = get_oncotree_code_name("LUAD") | |
| assert isinstance(result, str) | |
| def test_oncotree_invalid_code_returns_unknown(self, mocker): | |
| """Test that invalid code returns 'Unknown'.""" | |
| # Mock the requests.get call to return empty response (no matching codes) | |
| mock_response = mocker.Mock() | |
| mock_response.status_code = 200 | |
| mock_response.json.return_value = [] # Empty list means no matching codes found | |
| mocker.patch("requests.get", return_value=mock_response) | |
| # Clear cache and use an invalid code | |
| oncotree_code_map.clear() | |
| result = get_oncotree_code_name("INVALID_CODE_XYZ123") | |
| assert result == "Unknown" | |
| class TestExportToCsv: | |
| """Test export_to_csv function.""" | |
| def test_export_to_csv_returns_path(self): | |
| """Test that export_to_csv returns a file path.""" | |
| df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) | |
| result = export_to_csv(df) | |
| assert isinstance(result, str) | |
| assert result.endswith(".csv") | |
| # Clean up | |
| Path(result).unlink(missing_ok=True) | |
| def test_export_to_csv_creates_file(self): | |
| """Test that export_to_csv creates a CSV file.""" | |
| df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) | |
| result = export_to_csv(df) | |
| assert Path(result).exists() | |
| # Clean up | |
| Path(result).unlink() | |
| def test_export_to_csv_with_empty_dataframe_raises_error(self): | |
| """Test that exporting empty DataFrame raises error.""" | |
| import gradio as gr | |
| df = pd.DataFrame() | |
| with pytest.raises(gr.Error): | |
| export_to_csv(df) | |
| def test_export_to_csv_with_none_raises_error(self): | |
| """Test that exporting None raises error.""" | |
| import gradio as gr | |
| with pytest.raises(gr.Error): | |
| export_to_csv(None) | |