forked from enviPath/enviPy
347 lines
13 KiB
Python
347 lines
13 KiB
Python
from django.conf import settings as s
|
||
from django.test import TestCase, override_settings
|
||
|
||
from epdb.exceptions import InvalidSMILESException, InvalidMolfileException
|
||
from epdb.logic import PackageManager
|
||
from epdb.models import Compound, User, CompoundStructure
|
||
|
||
|
||
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
|
||
class CompoundTest(TestCase):
|
||
fixtures = ["test_fixtures_incl_model.jsonl.gz"]
|
||
|
||
def setUp(self):
|
||
pass
|
||
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
super(CompoundTest, cls).setUpClass()
|
||
cls.user = User.objects.get(username="anonymous")
|
||
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
|
||
|
||
# A valid V2000 molfile for 4-Nitrobenzoic acid (O=C(O)C1=CC=C([N+](=O)[O-])C=C1)
|
||
cls.VALID_MOLFILE = """\n Mrv2211 01012500002D\n\n 12 12 0 0 0 0 999 V2000\n 1.4289 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.0625 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 1.2375 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 0.8250 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.8875 0.0000 N 0 3 0 0 0 0 0 0 0 0 0 0\n 0.0000 -3.3000 0.0000 O 0 5 0 0 0 0 0 0 0 0 0 0\n 1.4289 -3.3000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0 0\n 2 3 1 0 0 0 0\n 3 4 2 0 0 0 0\n 4 5 1 0 0 0 0\n 5 6 2 0 0 0 0\n 6 1 1 0 0 0 0\n 2 7 1 0 0 0 0\n 7 8 2 0 0 0 0\n 7 9 1 0 0 0 0\n 5 10 1 0 0 0 0\n 10 11 1 0 0 0 0\n 10 12 2 0 0 0 0\nM CHG 2 10 1 11 -1\nM END\n"""
|
||
cls.INVALID_MOLFILE = "this is not a valid molfile"
|
||
|
||
def test_smoke(self):
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
||
name="Afoxolaner",
|
||
description="No Desc",
|
||
)
|
||
|
||
self.assertEqual(
|
||
c.default_structure.smiles,
|
||
"C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
||
)
|
||
self.assertEqual(c.name, "Afoxolaner")
|
||
self.assertEqual(c.description, "No Desc")
|
||
|
||
def test_missing_smiles(self):
|
||
with self.assertRaises(InvalidSMILESException):
|
||
_ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
|
||
|
||
with self.assertRaises(InvalidSMILESException):
|
||
_ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc")
|
||
|
||
with self.assertRaises(InvalidSMILESException):
|
||
_ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
|
||
|
||
def test_smiles_are_trimmed(self):
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles=" C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F ",
|
||
name="Afoxolaner",
|
||
description="No Desc",
|
||
)
|
||
|
||
self.assertEqual(
|
||
c.default_structure.smiles,
|
||
"C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
||
)
|
||
|
||
def test_name_and_description_optional(self):
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
||
)
|
||
|
||
self.assertEqual(c.name, "Compound 1")
|
||
self.assertEqual(c.description, "no description")
|
||
|
||
def test_empty_name_and_description_are_ignored(self):
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
||
name="",
|
||
description="",
|
||
)
|
||
|
||
self.assertEqual(c.name, "Compound 1")
|
||
self.assertEqual(c.description, "no description")
|
||
|
||
def test_deduplication(self):
|
||
c1 = Compound.create(
|
||
self.package,
|
||
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
||
name="Afoxolaner",
|
||
description="No Desc",
|
||
)
|
||
|
||
c2 = Compound.create(
|
||
self.package,
|
||
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
||
name="Afoxolaner",
|
||
description="No Desc",
|
||
)
|
||
|
||
# Check if create detects that this Compound already exist
|
||
# In this case the existing object should be returned
|
||
self.assertEqual(c1.pk, c2.pk)
|
||
self.assertEqual(len(self.package.compounds), 1)
|
||
|
||
def test_wrong_smiles(self):
|
||
with self.assertRaises(InvalidSMILESException):
|
||
_ = Compound.create(
|
||
self.package,
|
||
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
||
name="Afoxolaner",
|
||
description="No Desc",
|
||
)
|
||
|
||
def test_create_with_standardized_smiles(self):
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
name="Standardized SMILES",
|
||
description="No Desc",
|
||
)
|
||
self.assertEqual(len(c.structures.all()), 1)
|
||
|
||
cs = c.structures.all()[0]
|
||
self.assertEqual(cs.normalized_structure, True)
|
||
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
|
||
|
||
def test_create_with_non_standardized_smiles(self):
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="[O-][N+](=O)c1ccc(C(=O)[O-])cc1",
|
||
name="Non Standardized SMILES",
|
||
description="No Desc",
|
||
)
|
||
|
||
self.assertEqual(len(c.structures.all()), 2)
|
||
for cs in c.structures.all():
|
||
if cs.normalized_structure:
|
||
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
|
||
break
|
||
else:
|
||
# Loop finished without break, lets fail...
|
||
self.assertEqual(1, 2)
|
||
|
||
def test_add_structure_smoke(self):
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
name="Standardized SMILES",
|
||
description="No Desc",
|
||
)
|
||
|
||
c.add_structure("[O-][N+](=O)c1ccc(C(=O)[O-])cc1", "Non Standardized SMILES")
|
||
|
||
self.assertEqual(len(c.structures.all()), 2)
|
||
|
||
def test_add_structure_with_different_normalized_smiles(self):
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
name="Standardized SMILES",
|
||
description="No Desc",
|
||
)
|
||
|
||
with self.assertRaises(ValueError):
|
||
c.add_structure(
|
||
"C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
||
"Different Standardized SMILES",
|
||
)
|
||
|
||
def test_delete(self):
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
name="Standardization Test",
|
||
description="No Desc",
|
||
)
|
||
|
||
c.delete()
|
||
|
||
self.assertEqual(Compound.objects.filter(package=self.package).count(), 0)
|
||
self.assertEqual(
|
||
CompoundStructure.objects.filter(compound__package=self.package).count(), 0
|
||
)
|
||
|
||
def test_set_as_default_structure(self):
|
||
c1 = Compound.create(
|
||
self.package,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
name="Standardized SMILES",
|
||
description="No Desc",
|
||
)
|
||
default_structure = c1.default_structure
|
||
|
||
c2 = c1.add_structure("[O-][N+](=O)c1ccc(C(=O)[O-])cc1", "Non Standardized SMILES")
|
||
|
||
c1.set_default_structure(c2)
|
||
self.assertNotEqual(default_structure, c2)
|
||
|
||
def test_create_structure_from_molfile(self):
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="PLACEHOLDER",
|
||
molfile=self.VALID_MOLFILE,
|
||
name="Molfile Compound",
|
||
description="Created from molfile",
|
||
)
|
||
|
||
cs = CompoundStructure.create(
|
||
compound=c,
|
||
smiles="PLACEHOLDER",
|
||
molfile=self.VALID_MOLFILE,
|
||
name="Molfile Structure",
|
||
description="Structure from molfile",
|
||
)
|
||
|
||
# The SMILES must have been derived from the molfile, not the placeholder
|
||
self.assertNotEqual(cs.smiles, "PLACEHOLDER")
|
||
self.assertIsNotNone(cs.smiles)
|
||
self.assertTrue(len(cs.smiles) > 0)
|
||
|
||
def test_create_structure_from_molfile_stores_molfile(self):
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="c1c(C(=O)O)ccc([N+]([O-])=O)c1",
|
||
name="Molfile Store Test",
|
||
description="No Desc",
|
||
)
|
||
|
||
cs = c.default_structure
|
||
|
||
# O=C(O)C1=CC=C([N+](=O)[O-])C=C1 will be overwritten with
|
||
# c1c(C(=O)O)ccc([N+]([O-])=O)c1 and molfile will be set
|
||
# on the existing structure
|
||
_ = CompoundStructure.create(
|
||
compound=c,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
molfile=self.VALID_MOLFILE,
|
||
name="Structure with Molfile",
|
||
description="No Desc",
|
||
)
|
||
|
||
# Fetch fresh from DB to confirm persistence
|
||
cs_db = CompoundStructure.objects.get(pk=cs.pk)
|
||
self.assertIsNotNone(cs_db.molfile)
|
||
self.assertNotEqual(cs_db.molfile.strip(), "")
|
||
|
||
def test_create_structure_from_invalid_molfile_raises(self):
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
name="Invalid Molfile Test",
|
||
description="No Desc",
|
||
)
|
||
|
||
with self.assertRaises(InvalidMolfileException):
|
||
CompoundStructure.create(
|
||
compound=c,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
molfile=self.INVALID_MOLFILE,
|
||
name="Bad Structure",
|
||
description="No Desc",
|
||
)
|
||
|
||
def test_molfile_takes_precedence_over_smiles(self):
|
||
"""When both molfile and smiles are supplied, molfile must win."""
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
name="Precedence Test",
|
||
description="No Desc",
|
||
)
|
||
|
||
cs = CompoundStructure.create(
|
||
compound=c,
|
||
smiles="C", # intentionally wrong / different SMILES
|
||
molfile=self.VALID_MOLFILE,
|
||
name="Molfile Wins",
|
||
description="No Desc",
|
||
)
|
||
|
||
# SMILES should be derived from molfile, not the supplied "C"
|
||
self.assertNotEqual(cs.smiles, "C")
|
||
|
||
def test_empty_molfile_falls_back_to_smiles(self):
|
||
"""An empty or whitespace-only molfile should be ignored and SMILES used instead."""
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
name="Empty Molfile Test",
|
||
description="No Desc",
|
||
)
|
||
|
||
cs = CompoundStructure.create(
|
||
compound=c,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
molfile=" ", # whitespace only – should be ignored
|
||
name="Fallback to SMILES",
|
||
description="No Desc",
|
||
)
|
||
|
||
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
|
||
|
||
def test_none_molfile_falls_back_to_smiles(self):
|
||
"""None as molfile should be ignored and SMILES used instead."""
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
name="None Molfile Test",
|
||
description="No Desc",
|
||
)
|
||
|
||
cs = CompoundStructure.create(
|
||
compound=c,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
molfile=None,
|
||
name="Fallback None Molfile",
|
||
description="No Desc",
|
||
)
|
||
|
||
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
|
||
|
||
def test_molfile_deduplication(self):
|
||
"""Creating a structure twice from the same molfile should return the existing object."""
|
||
c = Compound.create(
|
||
self.package,
|
||
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||
name="Molfile Dedup Test",
|
||
description="No Desc",
|
||
)
|
||
|
||
cs1 = CompoundStructure.create(
|
||
compound=c,
|
||
smiles="PLACEHOLDER",
|
||
molfile=self.VALID_MOLFILE,
|
||
name="Molfile Structure",
|
||
description="No Desc",
|
||
)
|
||
|
||
cs2 = CompoundStructure.create(
|
||
compound=c,
|
||
smiles="PLACEHOLDER",
|
||
molfile=self.VALID_MOLFILE,
|
||
name="Molfile Structure",
|
||
description="No Desc",
|
||
)
|
||
|
||
self.assertEqual(cs1.pk, cs2.pk)
|