Files
galaxy/test/unit/test_galaxy_mapping.py
T
John Chilton 67efd072ae Track workflow step input definitions in our model.
We don't track workflow step inputs in any formal way in our model currently. This has resulted in some current hacks and prevents future enhancements. This commit splits WorkflowStepConnection into two models WorkflowStepInput and WorkflowStepConnection - normalizing the previous table workflow_step_connection on input step and input name.

In terms of current hacks forced on it by restricting all of tool state to be confined to a big JSON blob in the database - we have problems distinguishing keys and values when walking tool state. As we store more and more JSON blobs inside of the giant tool state blob - the worse this problem gets. Take for instance checking for runtime parameters or the rules parameter values - these both use JSON blobs that aren't simple values, so it is hard to tell looking at the tool state blob in the database or the workflow export to tell what is a key or what is a value. Tracking state as normalized inputs with default values and explicit attributes runtime values should allow much more percise state definition and construction.

This variant of the models would also potentially allow defining runtime values with non-tool default values (so default values defined for the workflow but still explicitly settable at runtime). The combinations of overriding defaults and defining runtime values were not representable before.

In terms of future enhancements, there is a lot we cannot track with the current models - such as map/reduce options for collection operations (https://github.com/galaxyproject/galaxy/issues/4623#issuecomment-389544980). This should enable a lot of that. Obviously there are a lot of attributes defined here that are not yet utilized, but I'm using most (all?) of them downstream in the CWL branch. I'd rather populate this table fully realized and fill in the implementation around it as work continues to stream in from the CWL branch - to keep things simple and avoid extra database migrations. But I understand if this feels like speculative complexity we want to avoid despite the implementation being readily available for inspection downstream.
2018-11-15 21:34:02 +01:00

611 lines
26 KiB
Python

# -*- coding: utf-8 -*-
import time
import unittest
import uuid
from six import text_type
from sqlalchemy import inspect
import galaxy.datatypes.registry
import galaxy.model
import galaxy.model.mapping as mapping
datatypes_registry = galaxy.datatypes.registry.Registry()
datatypes_registry.load_datatypes()
galaxy.model.set_datatypes_registry(datatypes_registry)
class MappingTests(unittest.TestCase):
def test_annotations(self):
model = self.model
u = model.User(email="annotator@example.com", password="password")
self.persist(u)
def persist_and_check_annotation(annotation_class, **kwds):
annotated_association = annotation_class()
annotated_association.annotation = "Test Annotation"
annotated_association.user = u
for key, value in kwds.items():
setattr(annotated_association, key, value)
self.persist(annotated_association)
self.expunge()
stored_annotation = self.query(annotation_class).all()[0]
assert stored_annotation.annotation == "Test Annotation"
assert stored_annotation.user.email == "annotator@example.com"
sw = model.StoredWorkflow()
sw.user = u
self.persist(sw)
persist_and_check_annotation(model.StoredWorkflowAnnotationAssociation, stored_workflow=sw)
workflow = model.Workflow()
workflow.stored_workflow = sw
self.persist(workflow)
ws = model.WorkflowStep()
ws.workflow = workflow
self.persist(ws)
persist_and_check_annotation(model.WorkflowStepAnnotationAssociation, workflow_step=ws)
h = model.History(name="History for Annotation", user=u)
self.persist(h)
persist_and_check_annotation(model.HistoryAnnotationAssociation, history=h)
d1 = model.HistoryDatasetAssociation(extension="txt", history=h, create_dataset=True, sa_session=model.session)
self.persist(d1)
persist_and_check_annotation(model.HistoryDatasetAssociationAnnotationAssociation, hda=d1)
page = model.Page()
page.user = u
self.persist(page)
persist_and_check_annotation(model.PageAnnotationAssociation, page=page)
visualization = model.Visualization()
visualization.user = u
self.persist(visualization)
persist_and_check_annotation(model.VisualizationAnnotationAssociation, visualization=visualization)
dataset_collection = model.DatasetCollection(collection_type="paired")
history_dataset_collection = model.HistoryDatasetCollectionAssociation(collection=dataset_collection)
self.persist(history_dataset_collection)
persist_and_check_annotation(model.HistoryDatasetCollectionAssociationAnnotationAssociation, history_dataset_collection=history_dataset_collection)
library_dataset_collection = model.LibraryDatasetCollectionAssociation(collection=dataset_collection)
self.persist(library_dataset_collection)
persist_and_check_annotation(model.LibraryDatasetCollectionAnnotationAssociation, library_dataset_collection=library_dataset_collection)
def test_ratings(self):
model = self.model
u = model.User(email="rater@example.com", password="password")
self.persist(u)
def persist_and_check_rating(rating_class, **kwds):
rating_association = rating_class()
rating_association.rating = 5
rating_association.user = u
for key, value in kwds.items():
setattr(rating_association, key, value)
self.persist(rating_association)
self.expunge()
stored_annotation = self.query(rating_class).all()[0]
assert stored_annotation.rating == 5
assert stored_annotation.user.email == "rater@example.com"
sw = model.StoredWorkflow()
sw.user = u
self.persist(sw)
persist_and_check_rating(model.StoredWorkflowRatingAssociation, stored_workflow=sw)
h = model.History(name="History for Rating", user=u)
self.persist(h)
persist_and_check_rating(model.HistoryRatingAssociation, history=h)
d1 = model.HistoryDatasetAssociation(extension="txt", history=h, create_dataset=True, sa_session=model.session)
self.persist(d1)
persist_and_check_rating(model.HistoryDatasetAssociationRatingAssociation, hda=d1)
page = model.Page()
page.user = u
self.persist(page)
persist_and_check_rating(model.PageRatingAssociation, page=page)
visualization = model.Visualization()
visualization.user = u
self.persist(visualization)
persist_and_check_rating(model.VisualizationRatingAssociation, visualization=visualization)
dataset_collection = model.DatasetCollection(collection_type="paired")
history_dataset_collection = model.HistoryDatasetCollectionAssociation(collection=dataset_collection)
self.persist(history_dataset_collection)
persist_and_check_rating(model.HistoryDatasetCollectionRatingAssociation, history_dataset_collection=history_dataset_collection)
library_dataset_collection = model.LibraryDatasetCollectionAssociation(collection=dataset_collection)
self.persist(library_dataset_collection)
persist_and_check_rating(model.LibraryDatasetCollectionRatingAssociation, library_dataset_collection=library_dataset_collection)
def test_display_name(self):
def assert_display_name_converts_to_unicode(item, name):
assert isinstance(item.get_display_name(), text_type)
assert item.get_display_name() == name
ldda = self.model.LibraryDatasetDatasetAssociation(name='ldda_name')
assert_display_name_converts_to_unicode(ldda, 'ldda_name')
hda = self.model.HistoryDatasetAssociation(name='hda_name')
assert_display_name_converts_to_unicode(hda, 'hda_name')
history = self.model.History(name='history_name')
assert_display_name_converts_to_unicode(history, 'history_name')
library = self.model.Library(name='library_name')
assert_display_name_converts_to_unicode(library, 'library_name')
library_folder = self.model.LibraryFolder(name='library_folder')
assert_display_name_converts_to_unicode(library_folder, 'library_folder')
history = self.model.History(
name=u'Hello₩◎ґʟⅾ'
)
assert isinstance(history.name, text_type)
assert isinstance(history.get_display_name(), text_type)
assert history.get_display_name() == u'Hello₩◎ґʟⅾ'
def test_tags(self):
model = self.model
my_tag = model.Tag(name="Test Tag")
u = model.User(email="tagger@example.com", password="password")
self.persist(my_tag, u)
def tag_and_test(taggable_object, tag_association_class, backref_name):
assert len(getattr(self.query(model.Tag).filter(model.Tag.name == "Test Tag").all()[0], backref_name)) == 0
tag_association = tag_association_class()
tag_association.tag = my_tag
taggable_object.tags = [tag_association]
self.persist(tag_association, taggable_object)
assert len(getattr(self.query(model.Tag).filter(model.Tag.name == "Test Tag").all()[0], backref_name)) == 1
sw = model.StoredWorkflow()
sw.user = u
tag_and_test(sw, model.StoredWorkflowTagAssociation, "tagged_workflows")
h = model.History(name="History for Tagging", user=u)
tag_and_test(h, model.HistoryTagAssociation, "tagged_histories")
d1 = model.HistoryDatasetAssociation(extension="txt", history=h, create_dataset=True, sa_session=model.session)
tag_and_test(d1, model.HistoryDatasetAssociationTagAssociation, "tagged_history_dataset_associations")
page = model.Page()
page.user = u
tag_and_test(page, model.PageTagAssociation, "tagged_pages")
visualization = model.Visualization()
visualization.user = u
tag_and_test(visualization, model.VisualizationTagAssociation, "tagged_visualizations")
dataset_collection = model.DatasetCollection(collection_type="paired")
history_dataset_collection = model.HistoryDatasetCollectionAssociation(collection=dataset_collection)
tag_and_test(history_dataset_collection, model.HistoryDatasetCollectionTagAssociation, "tagged_history_dataset_collections")
library_dataset_collection = model.LibraryDatasetCollectionAssociation(collection=dataset_collection)
tag_and_test(library_dataset_collection, model.LibraryDatasetCollectionTagAssociation, "tagged_library_dataset_collections")
def test_collections_in_histories(self):
model = self.model
u = model.User(email="mary@example.com", password="password")
h1 = model.History(name="History 1", user=u)
d1 = model.HistoryDatasetAssociation(extension="txt", history=h1, create_dataset=True, sa_session=model.session)
d2 = model.HistoryDatasetAssociation(extension="txt", history=h1, create_dataset=True, sa_session=model.session)
c1 = model.DatasetCollection(collection_type="pair")
hc1 = model.HistoryDatasetCollectionAssociation(history=h1, collection=c1, name="HistoryCollectionTest1")
dce1 = model.DatasetCollectionElement(collection=c1, element=d1, element_identifier="left")
dce2 = model.DatasetCollectionElement(collection=c1, element=d2, element_identifier="right")
self.persist(u, h1, d1, d2, c1, hc1, dce1, dce2)
loaded_dataset_collection = self.query(model.HistoryDatasetCollectionAssociation).filter(model.HistoryDatasetCollectionAssociation.name == "HistoryCollectionTest1").first().collection
self.assertEqual(len(loaded_dataset_collection.elements), 2)
assert loaded_dataset_collection.collection_type == "pair"
assert loaded_dataset_collection["left"] == dce1
assert loaded_dataset_collection["right"] == dce2
def test_collections_in_library_folders(self):
model = self.model
u = model.User(email="mary2@example.com", password="password")
lf = model.LibraryFolder(name="RootFolder")
l = model.Library(name="Library1", root_folder=lf)
ld1 = model.LibraryDataset()
ld2 = model.LibraryDataset()
ldda1 = model.LibraryDatasetDatasetAssociation(extension="txt", library_dataset=ld1)
ldda2 = model.LibraryDatasetDatasetAssociation(extension="txt", library_dataset=ld1)
c1 = model.DatasetCollection(collection_type="pair")
dce1 = model.DatasetCollectionElement(collection=c1, element=ldda1)
dce2 = model.DatasetCollectionElement(collection=c1, element=ldda2)
self.persist(u, l, lf, ld1, ld2, c1, ldda1, ldda2, dce1, dce2)
# TODO:
# loaded_dataset_collection = self.query( model.DatasetCollection ).filter( model.DatasetCollection.name == "LibraryCollectionTest1" ).first()
# self.assertEqual(len(loaded_dataset_collection.datasets), 2)
# assert loaded_dataset_collection.collection_type == "pair"
def test_default_disk_usage(self):
model = self.model
u = model.User(email="disk_default@test.com", password="password")
self.persist(u)
u.adjust_total_disk_usage(1)
u_id = u.id
self.expunge()
user_reload = model.session.query(model.User).get(u_id)
assert user_reload.disk_usage == 1
def test_basic(self):
model = self.model
original_user_count = len(model.session.query(model.User).all())
# Make some changes and commit them
u = model.User(email="james@foo.bar.baz", password="password")
# gs = model.GalaxySession()
h1 = model.History(name="History 1", user=u)
# h1.queries.append( model.Query( "h1->q1" ) )
# h1.queries.append( model.Query( "h1->q2" ) )
h2 = model.History(name=("H" * 1024))
self.persist(u, h1, h2)
# q1 = model.Query( "h2->q1" )
metadata = dict(chromCol=1, startCol=2, endCol=3)
d1 = model.HistoryDatasetAssociation(extension="interval", metadata=metadata, history=h2, create_dataset=True, sa_session=model.session)
# h2.queries.append( q1 )
# h2.queries.append( model.Query( "h2->q2" ) )
self.persist(d1)
# Check
users = model.session.query(model.User).all()
assert len(users) == original_user_count + 1
user = [user for user in users if user.email == "james@foo.bar.baz"][0]
assert user.email == "james@foo.bar.baz"
assert user.password == "password"
assert len(user.histories) == 1
assert user.histories[0].name == "History 1"
hists = model.session.query(model.History).all()
hist0 = [history for history in hists if history.name == "History 1"][0]
hist1 = [history for history in hists if history.name == "H" * 255][0]
assert hist0.name == "History 1"
assert hist1.name == ("H" * 255)
assert hist0.user == user
assert hist1.user is None
assert hist1.datasets[0].metadata.chromCol == 1
# The filename test has moved to objectstore
# id = hist1.datasets[0].id
# assert hist1.datasets[0].file_name == os.path.join( "/tmp", *directory_hash_id( id ) ) + ( "/dataset_%d.dat" % id )
# Do an update and check
hist1.name = "History 2b"
self.expunge()
hists = model.session.query(model.History).all()
hist0 = [history for history in hists if history.name == "History 1"][0]
hist1 = [history for history in hists if history.name == "History 2b"][0]
assert hist0.name == "History 1"
assert hist1.name == "History 2b"
# gvk TODO need to ad test for GalaxySessions, but not yet sure what they should look like.
def test_jobs(self):
model = self.model
u = model.User(email="jobtest@foo.bar.baz", password="password")
job = model.Job()
job.user = u
job.tool_id = "cat1"
self.persist(u, job)
loaded_job = model.session.query(model.Job).filter(model.Job.user == u).first()
assert loaded_job.tool_id == "cat1"
def test_job_metrics(self):
model = self.model
u = model.User(email="jobtest@foo.bar.baz", password="password")
job = model.Job()
job.user = u
job.tool_id = "cat1"
job.add_metric("gx", "galaxy_slots", 5)
job.add_metric("system", "system_name", "localhost")
self.persist(u, job)
task = model.Task(job=job, working_directory="/tmp", prepare_files_cmd="split.sh")
task.add_metric("gx", "galaxy_slots", 5)
task.add_metric("system", "system_name", "localhost")
big_value = ":".join(["%d" % i for i in range(2000)])
task.add_metric("env", "BIG_PATH", big_value)
self.persist(task)
# Ensure big values truncated
assert len(task.text_metrics[1].metric_value) <= 1023
def test_tasks(self):
model = self.model
u = model.User(email="jobtest@foo.bar.baz", password="password")
job = model.Job()
task = model.Task(job=job, working_directory="/tmp", prepare_files_cmd="split.sh")
job.user = u
self.persist(u, job, task)
loaded_task = model.session.query(model.Task).filter(model.Task.job == job).first()
assert loaded_task.prepare_input_files_cmd == "split.sh"
def test_history_contents(self):
model = self.model
u = model.User(email="contents@foo.bar.baz", password="password")
# gs = model.GalaxySession()
h1 = model.History(name="HistoryContentsHistory1", user=u)
self.persist(u, h1, expunge=False)
d1 = self.new_hda(h1, name="1")
d2 = self.new_hda(h1, name="2", visible=False)
d3 = self.new_hda(h1, name="3", deleted=True)
d4 = self.new_hda(h1, name="4", visible=False, deleted=True)
self.session().flush()
def contents_iter_names(**kwds):
history = model.context.query(model.History).filter(
model.History.name == "HistoryContentsHistory1"
).first()
return list(map(lambda hda: hda.name, history.contents_iter(**kwds)))
self.assertEqual(contents_iter_names(), ["1", "2", "3", "4"])
assert contents_iter_names(deleted=False) == ["1", "2"]
assert contents_iter_names(visible=True) == ["1", "3"]
assert contents_iter_names(visible=False) == ["2", "4"]
assert contents_iter_names(deleted=True, visible=False) == ["4"]
assert contents_iter_names(ids=[d1.id, d2.id, d3.id, d4.id]) == ["1", "2", "3", "4"]
assert contents_iter_names(ids=[d1.id, d2.id, d3.id, d4.id], max_in_filter_length=1) == ["1", "2", "3", "4"]
assert contents_iter_names(ids=[d1.id, d3.id]) == ["1", "3"]
def _non_empty_flush(self):
model = self.model
lf = model.LibraryFolder(name="RootFolder")
session = self.session()
session.add(lf)
session.flush()
def test_flush_refreshes(self):
# Normally I don't believe in unit testing library code, but the behaviors around attribute
# states and flushing in SQL Alchemy is very subtle and it is good to have a executable
# reference for how it behaves in the context of Galaxy objects.
model = self.model
user = model.User(
email="testworkflows@bx.psu.edu",
password="password"
)
galaxy_session = model.GalaxySession()
galaxy_session_other = model.GalaxySession()
galaxy_session.user = user
galaxy_session_other.user = user
self.persist(user, galaxy_session_other, galaxy_session)
galaxy_session_id = galaxy_session.id
self.expunge()
session = self.session()
galaxy_model_object = self.query(model.GalaxySession).get(galaxy_session_id)
expected_id = galaxy_model_object.id
# id loaded as part of the object query, could be any non-deferred attribute.
assert 'id' not in inspect(galaxy_model_object).unloaded
# Perform an empty flush, verify empty flush doesn't reload all attributes.
session.flush()
assert 'id' not in inspect(galaxy_model_object).unloaded
# However, flushing anything non-empty - even unrelated object will invalidate
# the session ID.
self._non_empty_flush()
assert 'id' in inspect(galaxy_model_object).unloaded
# Fetch the ID loads the value from the database
assert expected_id == galaxy_model_object.id
assert 'id' not in inspect(galaxy_model_object).unloaded
# Using cached_id instead does not exhibit this behavior.
self._non_empty_flush()
assert expected_id == galaxy.model.cached_id(galaxy_model_object)
assert 'id' in inspect(galaxy_model_object).unloaded
# Keeping the following failed experiments here for future reference,
# I probed the internals of the attribute tracking and couldn't find an
# alternative, generalized way to get the previously loaded value for unloaded
# attributes.
# print(galaxy_model_object._sa_instance_state.attrs.id)
# print(dir(galaxy_model_object._sa_instance_state.attrs.id))
# print(galaxy_model_object._sa_instance_state.attrs.id.loaded_value)
# print(galaxy_model_object._sa_instance_state.attrs.id.state)
# print(galaxy_model_object._sa_instance_state.attrs.id.load_history())
# print(dir(galaxy_model_object._sa_instance_state.attrs.id.load_history()))
# print(galaxy_model_object._sa_instance_state.identity)
# print(dir(galaxy_model_object._sa_instance_state))
# print(galaxy_model_object._sa_instance_state.expired_attributes)
# print(galaxy_model_object._sa_instance_state.expired)
# print(galaxy_model_object._sa_instance_state._instance_dict().keys())
# print(dir(galaxy_model_object._sa_instance_state._instance_dict))
# assert False
# Verify cached_id works even immediately after an initial flush, prevents a second SELECT
# query that would be executed if object.id was used.
galaxy_model_object_new = model.GalaxySession()
session.add(galaxy_model_object_new)
session.flush()
assert galaxy.model.cached_id(galaxy_model_object_new)
assert 'id' in inspect(galaxy_model_object_new).unloaded
# Verify a targeted flush prevent expiring unrelated objects.
galaxy_model_object_new.id
assert 'id' not in inspect(galaxy_model_object_new).unloaded
session.flush(model.GalaxySession())
assert 'id' not in inspect(galaxy_model_object_new).unloaded
def test_workflows(self):
model = self.model
user = model.User(
email="testworkflows@bx.psu.edu",
password="password"
)
def workflow_from_steps(steps):
stored_workflow = model.StoredWorkflow()
stored_workflow.user = user
workflow = model.Workflow()
workflow.steps = steps
workflow.stored_workflow = stored_workflow
return workflow
child_workflow = workflow_from_steps([])
self.persist(child_workflow)
workflow_step_1 = model.WorkflowStep()
workflow_step_1.order_index = 0
workflow_step_1.type = "data_input"
workflow_step_2 = model.WorkflowStep()
workflow_step_2.order_index = 1
workflow_step_2.type = "subworkflow"
workflow_step_2.subworkflow = child_workflow
workflow_step_1.get_or_add_input("moo1")
workflow_step_1.get_or_add_input("moo2")
workflow_step_2.get_or_add_input("moo")
workflow_step_1.add_connection("foo", "cow", workflow_step_2)
workflow = workflow_from_steps([workflow_step_1, workflow_step_2])
self.persist(workflow)
assert workflow_step_1.id is not None
h1 = model.History(name="WorkflowHistory1", user=user)
invocation_uuid = uuid.uuid1()
workflow_invocation = model.WorkflowInvocation()
workflow_invocation.uuid = invocation_uuid
workflow_invocation.history = h1
workflow_invocation_step1 = model.WorkflowInvocationStep()
workflow_invocation_step1.workflow_invocation = workflow_invocation
workflow_invocation_step1.workflow_step = workflow_step_1
subworkflow_invocation = model.WorkflowInvocation()
workflow_invocation.attach_subworkflow_invocation_for_step(workflow_step_2, subworkflow_invocation)
workflow_invocation_step2 = model.WorkflowInvocationStep()
workflow_invocation_step2.workflow_invocation = workflow_invocation
workflow_invocation_step2.workflow_step = workflow_step_2
workflow_invocation.workflow = workflow
d1 = self.new_hda(h1, name="1")
workflow_request_dataset = model.WorkflowRequestToInputDatasetAssociation()
workflow_request_dataset.workflow_invocation = workflow_invocation
workflow_request_dataset.workflow_step = workflow_step_1
workflow_request_dataset.dataset = d1
self.persist(workflow_invocation)
assert workflow_request_dataset is not None
assert workflow_invocation.id is not None
history_id = h1.id
self.expunge()
loaded_invocation = self.query(model.WorkflowInvocation).get(workflow_invocation.id)
assert loaded_invocation.uuid == invocation_uuid, "%s != %s" % (loaded_invocation.uuid, invocation_uuid)
assert loaded_invocation
assert loaded_invocation.history.id == history_id
step_1, step_2 = loaded_invocation.workflow.steps
assert not step_1.subworkflow
assert step_2.subworkflow
assert len(loaded_invocation.steps) == 2
subworkflow_invocation_assoc = loaded_invocation.get_subworkflow_invocation_association_for_step(step_2)
assert subworkflow_invocation_assoc is not None
assert isinstance(subworkflow_invocation_assoc.subworkflow_invocation, model.WorkflowInvocation)
assert isinstance(subworkflow_invocation_assoc.parent_workflow_invocation, model.WorkflowInvocation)
assert subworkflow_invocation_assoc.subworkflow_invocation.history.id == history_id
u1 = loaded_invocation.update_time
time.sleep(1)
loaded_invocation.steps[0].update()
self.expunge()
loaded_invocation = self.query(model.WorkflowInvocation).get(workflow_invocation.id)
u2 = loaded_invocation.update_time
assert u1 != u2
def new_hda(self, history, **kwds):
return history.add_dataset(self.model.HistoryDatasetAssociation(create_dataset=True, sa_session=self.model.session, **kwds))
@classmethod
def setUpClass(cls):
# Start the database and connect the mapping
cls.model = mapping.init("/tmp", "sqlite:///:memory:", create_tables=True, object_store=MockObjectStore())
assert cls.model.engine is not None
@classmethod
def query(cls, type):
return cls.model.session.query(type)
@classmethod
def persist(cls, *args, **kwargs):
session = cls.session()
flush = kwargs.get('flush', True)
for arg in args:
session.add(arg)
if flush:
session.flush()
if kwargs.get('expunge', not flush):
cls.expunge()
return arg # Return last or only arg.
@classmethod
def session(cls):
return cls.model.session
@classmethod
def expunge(cls):
cls.model.session.flush()
cls.model.session.expunge_all()
class MockObjectStore(object):
def __init__(self):
pass
def size(self, dataset):
return 42
def exists(self, *args, **kwds):
return True
def get_filename(self, *args, **kwds):
return "dataest_14.dat"
def get_suite():
suite = unittest.TestSuite()
suite.addTest(MappingTests("test_basic"))
return suite