Files
enviPy-bayer/tests/test_simpleambitrule.py
jebus 50db2fb372 [Feature] MultiGen Eval (Backend) (#117)
Fixes #16

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#117
2025-09-18 18:40:45 +12:00

363 lines
14 KiB
Python

from unittest.mock import patch, MagicMock, PropertyMock
from django.test import TestCase
from epdb.logic import PackageManager
from epdb.models import User, SimpleAmbitRule
class SimpleAmbitRuleTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"]
@classmethod
def setUpClass(cls):
super(SimpleAmbitRuleTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous')
cls.package = PackageManager.create_package(cls.user, 'Simple Ambit Rule Test Package',
'Test Package for SimpleAmbitRule')
def test_create_basic_rule(self):
"""Test creating a basic SimpleAmbitRule with minimal parameters."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
rule = SimpleAmbitRule.create(
package=self.package,
smirks=smirks
)
self.assertIsInstance(rule, SimpleAmbitRule)
self.assertEqual(rule.smirks, smirks)
self.assertEqual(rule.package, self.package)
self.assertRegex(rule.name, r'Rule \d+')
self.assertEqual(rule.description, 'no description')
self.assertIsNone(rule.reactant_filter_smarts)
self.assertIsNone(rule.product_filter_smarts)
def test_create_with_all_parameters(self):
"""Test creating SimpleAmbitRule with all parameters."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
name = 'Test Rule'
description = 'A test biotransformation rule'
reactant_filter = '[CH2X4]'
product_filter = '[OH]'
rule = SimpleAmbitRule.create(
package=self.package,
name=name,
description=description,
smirks=smirks,
reactant_filter_smarts=reactant_filter,
product_filter_smarts=product_filter
)
self.assertEqual(rule.name, name)
self.assertEqual(rule.description, description)
self.assertEqual(rule.smirks, smirks)
self.assertEqual(rule.reactant_filter_smarts, reactant_filter)
self.assertEqual(rule.product_filter_smarts, product_filter)
def test_smirks_required(self):
"""Test that SMIRKS is required for rule creation."""
with self.assertRaises(ValueError) as cm:
SimpleAmbitRule.create(package=self.package, smirks=None)
self.assertIn('SMIRKS is required', str(cm.exception))
with self.assertRaises(ValueError) as cm:
SimpleAmbitRule.create(package=self.package, smirks='')
self.assertIn('SMIRKS is required', str(cm.exception))
with self.assertRaises(ValueError) as cm:
SimpleAmbitRule.create(package=self.package, smirks=' ')
self.assertIn('SMIRKS is required', str(cm.exception))
@patch('epdb.models.FormatConverter.is_valid_smirks')
def test_invalid_smirks_validation(self, mock_is_valid):
"""Test validation of SMIRKS format."""
mock_is_valid.return_value = False
invalid_smirks = 'invalid_smirks_string'
with self.assertRaises(ValueError) as cm:
SimpleAmbitRule.create(
package=self.package,
smirks=invalid_smirks
)
self.assertIn(f'SMIRKS "{invalid_smirks}" is invalid', str(cm.exception))
mock_is_valid.assert_called_once_with(invalid_smirks)
def test_smirks_trimming(self):
"""Test that SMIRKS strings are trimmed during creation."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
smirks_with_whitespace = f' {smirks} '
rule = SimpleAmbitRule.create(
package=self.package,
smirks=smirks_with_whitespace
)
self.assertEqual(rule.smirks, smirks)
def test_empty_name_and_description_handling(self):
"""Test that empty name and description are handled appropriately."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
rule = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
name='',
description=' '
)
self.assertRegex(rule.name, r'Rule \d+')
self.assertEqual(rule.description, 'no description')
def test_deduplication_basic(self):
"""Test that identical rules are deduplicated."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
rule1 = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
name='Rule 1'
)
rule2 = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
name='Rule 2' # Different name, but same SMIRKS
)
self.assertEqual(rule1.pk, rule2.pk)
self.assertEqual(SimpleAmbitRule.objects.filter(package=self.package, smirks=smirks).count(), 1)
def test_deduplication_with_filters(self):
"""Test deduplication with filter SMARTS."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
reactant_filter = '[CH2X4]'
product_filter = '[OH]'
rule1 = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
reactant_filter_smarts=reactant_filter,
product_filter_smarts=product_filter
)
rule2 = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
reactant_filter_smarts=reactant_filter,
product_filter_smarts=product_filter
)
self.assertEqual(rule1.pk, rule2.pk)
def test_no_deduplication_different_filters(self):
"""Test that rules with different filters are not deduplicated."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
rule1 = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
reactant_filter_smarts='[CH2X4]'
)
rule2 = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
reactant_filter_smarts='[CH3X4]'
)
self.assertNotEqual(rule1.pk, rule2.pk)
self.assertEqual(SimpleAmbitRule.objects.filter(package=self.package, smirks=smirks).count(), 2)
def test_filter_smarts_trimming(self):
"""Test that filter SMARTS are trimmed and handled correctly."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
# Test with whitespace-only filters (should be treated as None)
rule = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
reactant_filter_smarts=' ',
product_filter_smarts=' '
)
self.assertIsNone(rule.reactant_filter_smarts)
self.assertIsNone(rule.product_filter_smarts)
def test_url_property(self):
"""Test the URL property generation."""
rule = SimpleAmbitRule.create(
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
expected_url = f'{self.package.url}/simple-ambit-rule/{rule.uuid}'
self.assertEqual(rule.url, expected_url)
@patch('epdb.models.FormatConverter.apply')
def test_apply_method(self, mock_apply):
"""Test the apply method delegates to FormatConverter."""
mock_apply.return_value = ['product1', 'product2']
rule = SimpleAmbitRule.create(
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
test_smiles = 'CCO'
result = rule.apply(test_smiles)
mock_apply.assert_called_once_with(test_smiles, rule.smirks)
self.assertEqual(result, ['product1', 'product2'])
def test_reactants_smarts_property(self):
"""Test reactants_smarts property extracts correct part of SMIRKS."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
expected_reactants = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]'
rule = SimpleAmbitRule.create(
package=self.package,
smirks=smirks
)
self.assertEqual(rule.reactants_smarts, expected_reactants)
def test_products_smarts_property(self):
"""Test products_smarts property extracts correct part of SMIRKS."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
expected_products = '[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
rule = SimpleAmbitRule.create(
package=self.package,
smirks=smirks
)
self.assertEqual(rule.products_smarts, expected_products)
@patch('epdb.models.Package.objects')
def test_related_reactions_property(self, mock_package_objects):
"""Test related_reactions property returns correct queryset."""
mock_qs = MagicMock()
mock_package_objects.filter.return_value = mock_qs
rule = SimpleAmbitRule.create(
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
# Instead of directly assigning, patch the property or use with patch.object
with patch.object(type(rule), 'reaction_rule', new_callable=PropertyMock) as mock_reaction_rule:
mock_reaction_rule.return_value.filter.return_value.order_by.return_value = ['reaction1', 'reaction2']
result = rule.related_reactions
mock_package_objects.filter.assert_called_once_with(reviewed=True)
mock_reaction_rule.return_value.filter.assert_called_once_with(package__in=mock_qs)
mock_reaction_rule.return_value.filter.return_value.order_by.assert_called_once_with('name')
self.assertEqual(result, ['reaction1', 'reaction2'])
@patch('epdb.models.Pathway.objects')
@patch('epdb.models.Edge.objects')
def test_related_pathways_property(self, mock_edge_objects, mock_pathway_objects):
"""Test related_pathways property returns correct queryset."""
mock_related_reactions = ['reaction1', 'reaction2']
with patch.object(SimpleAmbitRule, "related_reactions", new_callable=PropertyMock) as mock_prop:
mock_prop.return_value = mock_related_reactions
rule = SimpleAmbitRule.create(
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
# Mock Edge objects query
mock_edge_values = MagicMock()
mock_edge_values.values.return_value = ['pathway_id1', 'pathway_id2']
mock_edge_objects.filter.return_value = mock_edge_values
# Mock Pathway objects query
mock_pathway_qs = MagicMock()
mock_pathway_objects.filter.return_value.order_by.return_value = mock_pathway_qs
result = rule.related_pathways
mock_edge_objects.filter.assert_called_once_with(edge_label__in=mock_related_reactions)
mock_edge_values.values.assert_called_once_with('pathway_id')
mock_pathway_objects.filter.assert_called_once()
self.assertEqual(result, mock_pathway_qs)
@patch('epdb.models.IndigoUtils.smirks_to_svg')
def test_as_svg_property(self, mock_smirks_to_svg):
"""Test as_svg property calls IndigoUtils correctly."""
mock_smirks_to_svg.return_value = '<svg>test_svg</svg>'
rule = SimpleAmbitRule.create(
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
result = rule.as_svg
mock_smirks_to_svg.assert_called_once_with(rule.smirks, True, width=800, height=400)
self.assertEqual(result, '<svg>test_svg</svg>')
def test_atomic_transaction(self):
"""Test that rule creation is atomic."""
smirks = '[H:1][C:2]>>[H:1][O:2]'
# This should work normally
rule = SimpleAmbitRule.create(package=self.package, smirks=smirks)
self.assertIsInstance(rule, SimpleAmbitRule)
# Test transaction rollback on error
with patch('epdb.models.SimpleAmbitRule.save', side_effect=Exception('Database error')):
with self.assertRaises(Exception):
SimpleAmbitRule.create(package=self.package, smirks='[H:3][C:4]>>[H:3][O:4]')
# Verify no partial data was saved
self.assertEqual(SimpleAmbitRule.objects.filter(package=self.package).count(), 1)
def test_multiple_duplicate_warning(self):
"""Test logging when multiple duplicates are found."""
smirks = '[H:1][C:2]>>[H:1][O:2]'
# Create first rule
rule1 = SimpleAmbitRule.create(package=self.package, smirks=smirks)
# Manually create a duplicate to simulate the error condition
rule2 = SimpleAmbitRule(package=self.package, smirks=smirks, name='Manual Rule')
rule2.save()
with patch('epdb.models.logger') as mock_logger:
# This should find the existing rule and log an error about multiple matches
result = SimpleAmbitRule.create(package=self.package, smirks=smirks)
# Should return the first matching rule
self.assertEqual(result.pk, rule1.pk)
# Should log an error about multiple matches
mock_logger.error.assert_called()
self.assertIn('More than one rule matched', mock_logger.error.call_args[0][0])
def test_model_fields(self):
"""Test model field properties."""
rule = SimpleAmbitRule.create(
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]',
reactant_filter_smarts='[CH3]',
product_filter_smarts='[OH]'
)
# Test field properties
self.assertFalse(rule._meta.get_field('smirks').blank)
self.assertFalse(rule._meta.get_field('smirks').null)
self.assertTrue(rule._meta.get_field('reactant_filter_smarts').null)
self.assertTrue(rule._meta.get_field('product_filter_smarts').null)
# Test verbose names
self.assertEqual(rule._meta.get_field('smirks').verbose_name, 'SMIRKS')
self.assertEqual(rule._meta.get_field('reactant_filter_smarts').verbose_name, 'Reactant Filter SMARTS')
self.assertEqual(rule._meta.get_field('product_filter_smarts').verbose_name, 'Product Filter SMARTS')