"""Tests for the diagnostic expression library stored in a project.
The library is read and written through
:mod:`autowisp.diagnostics.expression_library`, against a throwaway project
database. What an expression *means* is tested where those rules live, in
the expression layer's own tests; here it is only what gets stored, and
that references survive a rename.
The management form is tested here too, for the one check it makes itself
rather than delegating: that a name is a slug and is not already taken.
"""
import os
import tempfile
import unittest
import django
from sqlalchemy.exc import IntegrityError
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE",
"autowisp.browser_interface.django_project.settings",
)
django.setup()
# pylint: disable=wrong-import-position
from autowisp.browser_interface.diagnostics.forms import (
DiagnosticExpressionForm,
)
from autowisp.database.interface import set_project_home, start_db_session
from autowisp.diagnostics.expression_library import (
delete_expressions,
get_expression_descriptions,
get_expression_entries,
get_expressions,
store_expression,
write_expressions,
)
# pylint: enable=wrong-import-position
[docs]
class ExpressionLibraryTestCase(unittest.TestCase):
"""Base creating one throwaway project, emptied before every test."""
[docs]
@classmethod
def setUpClass(cls):
# Closed in tearDownClass rather than by a context manager, which a
# fixture spanning every test of the class cannot use.
# pylint: disable=consider-using-with
cls._tmp = tempfile.TemporaryDirectory()
# pylint: enable=consider-using-with
set_project_home(cls._tmp.name)
[docs]
@classmethod
def tearDownClass(cls):
cls._tmp.cleanup()
[docs]
def setUp(self):
with start_db_session() as db_session:
delete_expressions(get_expressions(db_session), db_session)
[docs]
@staticmethod
def make(name, expression="astrom_residual[0] / diagonal_fov[0]", **kw):
"""Store one expression, returning what the store reported."""
with start_db_session() as db_session:
return store_expression(
db_session, name=name, expression=expression, **kw
)
[docs]
@staticmethod
def library():
"""The stored library, ``{name: expression}``."""
with start_db_session() as db_session:
return get_expressions(db_session)
[docs]
class TestNameSpace(ExpressionLibraryTestCase):
"""Names have to behave like the diagnostic names they sit beside."""
[docs]
def test_name_is_unique(self):
"""Two expressions cannot share a name.
The name is what a selector and a URL carry, and what other
expressions and exclusion rules reference, so a duplicate would be
ambiguous everywhere at once. The form refuses one first; this is
the database guaranteeing it regardless.
"""
self.make("rel_astrom_residual")
with self.assertRaises(IntegrityError):
self.make("rel_astrom_residual", "bg_center[0]")
[docs]
class TestStoredFields(ExpressionLibraryTestCase):
"""What the library keeps, and what it deliberately does not check."""
[docs]
def test_description_is_optional(self):
"""Most expressions are self-explanatory from their text."""
self.make("terse")
with start_db_session() as db_session:
self.assertEqual(
get_expression_descriptions(db_session), {"terse": ""}
)
[docs]
def test_unknown_names_are_not_rejected_here(self):
"""The library stores text; resolving names is not its job.
An expression may legitimately reference diagnostics the project has
never recorded -- it is then simply not offered -- so refusing it
here would be wrong.
"""
self.make("references_nothing_real", "no_such_diagnostic * 2")
self.assertEqual(
self.library(),
{"references_nothing_real": "no_such_diagnostic * 2"},
)
[docs]
class TestLibraryAccess(ExpressionLibraryTestCase):
"""What reading hands to the expression layer and to an export."""
[docs]
def test_empty_library_is_a_dictionary(self):
"""Not ``None``: the layers below iterate it without checking."""
self.assertEqual(self.library(), {})
[docs]
def test_names_map_to_their_text(self):
"""The shape the expression layer expects, and nothing more."""
self.make("rel_bg", "bg_center[0] - nanmedian(bg_center[0])")
self.make("twice_bg", "bg_center[0] * 2")
self.assertEqual(
self.library(),
{
"rel_bg": "bg_center[0] - nanmedian(bg_center[0])",
"twice_bg": "bg_center[0] * 2",
},
)
[docs]
def test_entries_for_export(self):
"""Every stored field, by name, skipping names not stored."""
self.make("zeta", "bg_center[0]", description="last")
self.make("alpha", "s_center[0]")
with start_db_session() as db_session:
entries = get_expression_entries(
["zeta", "alpha", "no_such_expression"], db_session
)
self.assertEqual(
entries,
[
{
"name": "alpha",
"expression": "s_center[0]",
"description": "",
},
{
"name": "zeta",
"expression": "bg_center[0]",
"description": "last",
},
],
)
[docs]
class TestStoreExpression(ExpressionLibraryTestCase):
"""Adding, replacing and renaming one expression."""
[docs]
def test_replacing_keeps_the_name(self):
"""An edit that keeps the name changes the text in place."""
self.make("rel_bg", "bg_center[0]")
self.assertEqual(
self.make("rel_bg", "bg_center[0] * 2", replacing="rel_bg"), []
)
self.assertEqual(self.library(), {"rel_bg": "bg_center[0] * 2"})
[docs]
def test_rename_carries_the_dependents(self):
"""What referenced the old name references the new one.
Leaving them naming something that no longer exists would break
them, and refusing the rename would not help: a dependent cannot be
pointed at the new name before it exists.
"""
self.make("rel_bg", "bg_center[0] - nanmedian(bg_center[0])")
self.make("twice_rel_bg", "rel_bg[0] * 2")
self.make("unrelated", "s_center[0]")
updated = self.make(
"bg_offset",
"bg_center[0] - nanmedian(bg_center[0])",
replacing="rel_bg",
)
self.assertEqual(updated, ["twice_rel_bg"])
self.assertEqual(
self.library(),
{
"bg_offset": "bg_center[0] - nanmedian(bg_center[0])",
"twice_rel_bg": "bg_offset[0] * 2",
"unrelated": "s_center[0]",
},
)
[docs]
def test_replacing_a_name_not_stored_adds(self):
"""A stale edit link adds the expression rather than losing it."""
self.assertEqual(self.make("rel_bg", replacing="gone"), [])
self.assertIn("rel_bg", self.library())
[docs]
class TestDeleteAndWrite(ExpressionLibraryTestCase):
"""Deleting several, and writing an import's worth at once."""
[docs]
def test_delete_only_the_named(self):
"""Other expressions, and names not stored, are left alone."""
for name in ("alpha", "mu", "zeta"):
self.make(name)
with start_db_session() as db_session:
delete_expressions(["alpha", "zeta", "not_stored"], db_session)
self.assertEqual(list(self.library()), ["mu"])
[docs]
def test_write_counts_added_and_replaced(self):
"""New names are added, existing ones overwritten, and counted."""
self.make("rel_bg", "bg_center[0]")
with start_db_session() as db_session:
counts = write_expressions(
{
"rel_bg": {
"expression": "bg_center[0] * 2",
"description": "replaced",
},
"rel_s": {
"expression": "s_center[0]",
"description": "",
},
},
db_session,
)
descriptions = get_expression_descriptions(db_session)
self.assertEqual(counts, (1, 1))
self.assertEqual(
self.library(),
{"rel_bg": "bg_center[0] * 2", "rel_s": "s_center[0]"},
)
self.assertEqual(descriptions, {"rel_bg": "replaced", "rel_s": ""})
if __name__ == "__main__":
unittest.main()