forked from enviPath/enviPy
341 lines
14 KiB
Python
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"
|
|
)
|