Files
enviPy-bayer/tests/test_simpleambitrule.py
Tim Lorsbach 138846d84d ...
2025-10-29 19:46:20 +01:00

341 lines
14 KiB
Python

from unittest.mock import MagicMock, PropertyMock, patch
from django.conf import settings as s
from django.test import TestCase
from epdb.logic import PackageManager
from epdb.models import SimpleAmbitRule, User
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(f"{s.PACKAGE_MODULE_PATH}.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"
)