Skip to main content
Glama
puran-water

Corrosion Engineering MCP Server

by puran-water
test_material_database.py9.71 kB
""" Unit tests for AuthoritativeMaterialDatabase Tests lazy loading, multi-source data merging, PREN calculation, composition provenance, and fallback logic. """ import pytest from unittest.mock import Mock, patch, MagicMock import pandas as pd from pathlib import Path import sys # Add parent directory to path sys.path.insert(0, str(Path(__file__).parent.parent)) from utils.material_database import AuthoritativeMaterialDatabase class TestAuthoritativeMaterialDatabase: """Test suite for AuthoritativeMaterialDatabase""" def test_lazy_loading_not_triggered_on_init(self): """Test that lazy loading doesn't occur during initialization""" db = AuthoritativeMaterialDatabase(use_cache=False) assert db._loaded is False assert db._galvanic_data is None assert db._kittycad_stainless is None @patch('utils.material_database.pd.read_xml') def test_lazy_loading_triggered_on_first_access(self, mock_read_xml): """Test that lazy loading triggers on first property access""" # Mock XML data mock_galvanic_df = pd.DataFrame({ 'Material': ['Carbon Steel', '316L'], 'Potential': [-0.6, -0.1] }) mock_read_xml.return_value = mock_galvanic_df with patch('utils.material_database.requests.get') as mock_get: # Mock KittyCAD JSON responses mock_response = Mock() mock_response.json.return_value = {} mock_response.raise_for_status = Mock() mock_get.return_value = mock_response db = AuthoritativeMaterialDatabase(use_cache=False) properties = db.get_material_properties("316L") # Verify lazy loading occurred assert db._loaded is True assert db._galvanic_data is not None def test_pren_calculation_316l(self): """Test PREN calculation for 316L (from ASTM A240 CSV data)""" db = AuthoritativeMaterialDatabase(use_cache=False) # 316L per ASTM A240: Cr=16.5, Mo=2.0, N=0.1 # PREN = 16.5 + 3.3*2.0 + 16*0.1 = 16.5 + 6.6 + 1.6 = 24.7 pren = db.calculate_pren("316L") assert pren is not None assert 24.0 <= pren <= 25.0 # ASTM A240 composition def test_pren_calculation_duplex_2205(self): """Test PREN calculation for Duplex 2205 (now using CSV key: 2205)""" db = AuthoritativeMaterialDatabase(use_cache=False) # Duplex 2205 per ASTM A240: Cr=22, Mo=3.0, N=0.17 # PREN = 22 + 3.3*3.0 + 16*0.17 = 22 + 9.9 + 2.72 = 34.62 pren = db.calculate_pren("2205") # CSV uses "2205" not "duplex_2205" assert pren is not None assert 34.0 <= pren <= 35.5 def test_cpt_estimation_from_pren(self): """Test CPT estimation from PREN""" db = AuthoritativeMaterialDatabase(use_cache=False) # CPT ≈ PREN - 10 (simplified correlation) # 316L PREN ~25.7 → CPT ~15-16°C cpt = db.estimate_cpt("316L") assert cpt is not None assert 14.0 <= cpt <= 17.0 def test_composition_provenance_metadata(self): """Test that composition provenance is tagged as fallback""" db = AuthoritativeMaterialDatabase(use_cache=False) properties = db.get_material_properties("316L") # Verify provenance metadata exists (now using CSV-backed data) assert "composition_provenance" in properties assert properties["composition_provenance"]["method"] == "CSV_loader" assert properties["composition_provenance"]["authoritative"] is True assert "authoritative" in properties["composition_provenance"]["quality"] def test_fallback_galvanic_potential(self): """Test fallback galvanic potential values""" db = AuthoritativeMaterialDatabase(use_cache=False) # Test fallback values assert db._fallback_galvanic_potential("CS") == -0.6 assert db._fallback_galvanic_potential("316L") == -0.1 assert db._fallback_galvanic_potential("duplex_2205") == -0.05 def test_cost_factor_retrieval(self): """Test cost factor relative to carbon steel""" db = AuthoritativeMaterialDatabase(use_cache=False) # Carbon steel = 1.0 (reference) assert db._get_cost_factor("CS") == 1.0 # 316L more expensive cost_316l = db._get_cost_factor("316L") assert cost_316l > 1.0 # Super duplex very expensive cost_super = db._get_cost_factor("super_duplex") assert cost_super > cost_316l def test_material_name_mapping(self): """Test full material name retrieval""" db = AuthoritativeMaterialDatabase(use_cache=False) assert db._get_full_name("CS") == "Carbon Steel" assert db._get_full_name("316L") == "316L Stainless Steel" assert db._get_full_name("duplex_2205") == "Duplex 2205" assert db._get_full_name("C276") == "Hastelloy C-276" @patch('utils.material_database.pd.read_xml') def test_network_failure_fallback(self, mock_read_xml): """Test that network failures trigger fallback to hard-coded values""" # Simulate network failure mock_read_xml.side_effect = Exception("Network error") with patch('utils.material_database.requests.get') as mock_get: mock_get.side_effect = Exception("Network error") db = AuthoritativeMaterialDatabase(use_cache=False) properties = db.get_material_properties("316L") # Should still return data via fallback assert properties is not None assert "material_id" in properties assert properties["material_id"] == "316L" # Galvanic potential should use fallback galvanic = db._get_galvanic_potential("316L") assert galvanic == -0.1 # Fallback value def test_cache_behavior(self): """Test that caching prevents redundant lookups""" db = AuthoritativeMaterialDatabase(use_cache=True) # First call props1 = db.get_material_properties("316L") # Second call should hit cache props2 = db.get_material_properties("316L") # Should be same object reference (from cache) assert props1 is props2 def test_no_cache_behavior(self): """Test that cache can be disabled""" db = AuthoritativeMaterialDatabase(use_cache=False) # First call props1 = db.get_material_properties("316L") # Second call creates new object props2 = db.get_material_properties("316L") # Should be different object references assert props1 is not props2 def test_unknown_material_handling(self): """Test handling of unknown material IDs""" db = AuthoritativeMaterialDatabase(use_cache=False) properties = db.get_material_properties("UNKNOWN_ALLOY") # Should return basic structure with minimal data assert properties["material_id"] == "UNKNOWN_ALLOY" assert properties["name"] == "UNKNOWN_ALLOY" # Falls back to ID # Composition should be None composition = db._get_composition("UNKNOWN_ALLOY") assert composition is None def test_pren_with_missing_composition(self): """Test PREN calculation with missing composition data""" db = AuthoritativeMaterialDatabase(use_cache=False) # Unknown material has no composition pren = db.calculate_pren("UNKNOWN_ALLOY") assert pren is None def test_multi_source_data_merging(self): """Test that properties from multiple sources are merged correctly""" with patch('utils.material_database.pd.read_xml') as mock_xml: with patch('utils.material_database.requests.get') as mock_get: # Mock USNRL galvanic data mock_galvanic_df = pd.DataFrame({ 'Material': ['Stainless Steel 316'], 'Potential': [-0.08] }) mock_xml.return_value = mock_galvanic_df # Mock KittyCAD data mock_response = Mock() mock_response.json.return_value = { "316L": { "density": 8000, "yield_strength": 290, } } mock_response.raise_for_status = Mock() mock_get.return_value = mock_response db = AuthoritativeMaterialDatabase(use_cache=False) properties = db.get_material_properties("316L") # Should have data from multiple sources assert "material_id" in properties assert "composition" in properties # From fallback assert "PREN" in properties # Calculated assert "cost_factor" in properties # From internal DB class TestPRENCalculations: """Focused tests for PREN calculation accuracy""" @pytest.mark.parametrize("material_id,expected_range", [ ("304", (19.0, 20.0)), # Cr=19, Mo=0, N=0.08 ("316L", (24.0, 25.0)), # Cr=16.5, Mo=2.0, N=0.1 (ASTM A240) ("2205", (34.0, 36.0)), # Cr=22, Mo=3, N=0.17 (CSV key is "2205") ("2507", (41.0, 43.0)), # Cr=25, Mo=4, N=0.27 (CSV key is "2507") ]) def test_pren_ranges(self, material_id, expected_range): """Test PREN values for various stainless steels""" db = AuthoritativeMaterialDatabase(use_cache=False) pren = db.calculate_pren(material_id) assert pren is not None assert expected_range[0] <= pren <= expected_range[1] if __name__ == "__main__": pytest.main([__file__, "-v"])

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/puran-water/corrosion-engineering-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server