Repository: ResearchObject/ro-crate-py
Pull request #43 (2021-02-09 15:20:51)

Methods to add test-related entities
Adds specialized methods to add test-related entities to a crate.


Patch:
diff --git a/rocrate/model/softwareapplication.py b/rocrate/model/softwareapplication.py
index ddc6445..fa42b29 100644
--- a/rocrate/model/softwareapplication.py
+++ b/rocrate/model/softwareapplication.py
@@ -50,3 +50,29 @@ class SoftwareApplication(ContextEntity, CreativeWork):
     @version.setter
     def version(self, version):
         self["version"] = version
+
+
+PLANEMO_ID = "https://w3id.org/ro/terms/test#PlanemoEngine"
+PLANEMO_DEFAULT_VERSION = "0.74"
+
+
+def planemo(crate):
+    return SoftwareApplication(crate, identifier=PLANEMO_ID, properties={
+        "name": "Planemo",
+        "url": {
+            "@id": "https://github.com/galaxyproject/planemo"
+        }
+    })
+
+
+APP_MAP = {
+    "planemo": planemo,
+}
+
+
+def get_app(crate, name):
+    try:
+        func = APP_MAP[name.lower()]
+    except KeyError:
+        raise ValueError(f"Unknown application: {name}")
+    return func(crate)
diff --git a/rocrate/rocrate.py b/rocrate/rocrate.py
index 50412a3..9a5edc3 100644
--- a/rocrate/rocrate.py
+++ b/rocrate/rocrate.py
@@ -39,10 +39,10 @@ from .model.preview import Preview
 from .model.testdefinition import TestDefinition
 
 # Imports for the __subclasses__ hack below
-from .model.testinstance import TestInstance  # noqa
-from .model.testservice import TestService  # noqa
-from .model.softwareapplication import SoftwareApplication  # noqa
-from .model.testsuite import TestSuite  # noqa
+from .model.testinstance import TestInstance
+from .model.testservice import TestService, get_service
+from .model.softwareapplication import SoftwareApplication, get_app, PLANEMO_DEFAULT_VERSION
+from .model.testsuite import TestSuite
 
 from .utils import is_url
 
@@ -408,3 +408,65 @@ class ROCrate():
             writable_entity.write_zip(zf)
         zf.close()
         return zf.filename
+
+    def add_test_suite(self, identifier=None, name=None, main_entity=None):
+        if not main_entity:
+            main_entity = self.mainEntity
+            if not main_entity:
+                raise ValueError("crate does not have a main entity")
+        suite = self.add(TestSuite(self, identifier))
+        suite.name = name or suite.id.lstrip("#")
+        suite["mainEntity"] = main_entity
+        # note that a test dir is required (possibly empty) even if the suite
+        # is going to have only instances (no definitions)
+        test_dir = self.test_dir or self.add_directory(dest_path="test")
+        suite_set = set(test_dir["about"] or [])
+        suite_set.add(suite)
+        test_dir["about"] = list(suite_set)
+        return suite
+
+    def add_test_instance(self, suite, url, resource="", service="jenkins", identifier=None, name=None):
+        suite = self.__validate_suite(suite)
+        instance = self.add(TestInstance(self, identifier))
+        instance.url = url
+        instance.resource = resource
+        if isinstance(service, TestService):
+            assert service.crate is self
+        else:
+            service = get_service(self, service)
+            if not self.dereference(service.id):
+                self.add(service)
+        instance.service = service
+        instance.name = name or instance.id.lstrip("#")
+        instance_set = set(suite.instance or [])
+        instance_set.add(instance)
+        suite.instance = list(instance_set)
+        return instance
+
+    def add_test_definition(
+            self, suite, source=None, dest_path=None, fetch_remote=False, validate_url=True, properties=None,
+            engine="planemo", engine_version=PLANEMO_DEFAULT_VERSION
+    ):
+        suite = self.__validate_suite(suite)
+        definition = self.add(
+            TestDefinition(self, source=source, dest_path=dest_path, fetch_remote=fetch_remote, properties=properties)
+        )
+        if isinstance(engine, SoftwareApplication):
+            assert engine.crate is self
+        else:
+            engine = get_app(self, engine)
+            if not self.dereference(engine.id):
+                self.add(engine)
+        definition.engine = engine
+        definition.engineVersion = engine_version
+        suite.definition = definition
+        return definition
+
+    def __validate_suite(self, suite):
+        if isinstance(suite, TestSuite):
+            assert suite.crate is self
+        else:
+            suite = self.dereference(suite)
+            if suite is None:
+                raise ValueError("suite not found")
+        return suite


Test patch:
diff --git a/rocrate/model/testservice.py b/rocrate/model/testservice.py
index 4faaabf..ff031ad 100644
--- a/rocrate/model/testservice.py
+++ b/rocrate/model/testservice.py
@@ -45,3 +45,39 @@ class TestService(ContextEntity):
     @url.setter
     def url(self, url):
         self["url"] = url
+
+
+JENKINS_ID = "https://w3id.org/ro/terms/test#JenkinsService"
+TRAVIS_ID = "https://w3id.org/ro/terms/test#TravisService"
+
+
+def jenkins(crate):
+    return TestService(crate, identifier=JENKINS_ID, properties={
+        "name": "Jenkins",
+        "url": {
+            "@id": "https://www.jenkins.io"
+        },
+    })
+
+
+def travis(crate):
+    return TestService(crate, identifier=TRAVIS_ID, properties={
+        "name": "Travis CI",
+        "url": {
+            "@id": "https://www.travis-ci.com"
+        },
+    })
+
+
+SERVICE_MAP = {
+    "jenkins": jenkins,
+    "travis": travis,
+}
+
+
+def get_service(crate, name):
+    try:
+        func = SERVICE_MAP[name.lower()]
+    except KeyError:
+        raise ValueError(f"Unknown service: {name}")
+    return func(crate)
diff --git a/test/test_test_metadata.py b/test/test_test_metadata.py
index b468b5e..0a655a6 100644
--- a/test/test_test_metadata.py
+++ b/test/test_test_metadata.py
@@ -15,12 +15,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import pytest
+
 from rocrate.rocrate import ROCrate
 from rocrate.model.testservice import TestService
 from rocrate.model.testinstance import TestInstance
 from rocrate.model.testdefinition import TestDefinition
 from rocrate.model.testsuite import TestSuite
 from rocrate.model.softwareapplication import SoftwareApplication
+from rocrate.model.computationalworkflow import ComputationalWorkflow
 
 # Tell pytest these are not test classes (so it doesn't try to collect them)
 TestService.__test__ = False
@@ -30,6 +33,7 @@ TestSuite.__test__ = False
 
 
 JENKINS = "https://w3id.org/ro/terms/test#JenkinsService"
+TRAVIS = "https://w3id.org/ro/terms/test#TravisService"
 PLANEMO = "https://w3id.org/ro/terms/test#PlanemoEngine"
 
 
@@ -142,3 +146,103 @@ def test_create():
     assert len(test_suite.instance) == 1
     assert test_suite.instance[0] is test_instance
     assert test_suite.definition is test_definition
+
+
+def test_add_test_suite(test_data_dir, helpers):
+    top_dir = test_data_dir / "ro-crate-galaxy-sortchangecase"
+    wf_path = top_dir / "sort-and-change-case.ga"
+    crate = ROCrate()
+    with pytest.raises(ValueError):  # no main entity
+        crate.add_test_suite()
+    wf = crate.add(ComputationalWorkflow(crate, str(wf_path), wf_path.name))
+    crate.mainEntity = wf
+    suites = set()
+    assert crate.test_dir is None
+    s1 = crate.add_test_suite()
+    assert crate.test_dir is not None
+    assert s1["mainEntity"] is wf
+    suites.add(s1)
+    assert suites == set(crate.test_dir["about"])
+    s2 = crate.add_test_suite(identifier="test1")
+    assert s2["mainEntity"] is wf
+    assert s2.id == "#test1"
+    suites.add(s2)
+    assert suites == set(crate.test_dir["about"])
+    s3 = crate.add_test_suite(identifier="test2", name="Test 2")
+    assert s3["mainEntity"] is wf
+    assert s3.id == "#test2"
+    assert s3.name == "Test 2"
+    suites.add(s3)
+    assert suites == set(crate.test_dir["about"])
+    wf2_path = top_dir / "README.md"
+    wf2 = crate.add(ComputationalWorkflow(crate, wf2_path, wf2_path.name))
+    s4 = crate.add_test_suite(identifier="test3", name="Foo", main_entity=wf2)
+    assert s4["mainEntity"] is wf2
+    assert s4.id == "#test3"
+    assert s4.name == "Foo"
+    suites.add(s4)
+    assert suites == set(crate.test_dir["about"])
+
+
+def test_add_test_instance(test_data_dir, helpers):
+    top_dir = test_data_dir / "ro-crate-galaxy-sortchangecase"
+    wf_path = top_dir / "sort-and-change-case.ga"
+    crate = ROCrate()
+    wf = crate.add(ComputationalWorkflow(crate, str(wf_path), wf_path.name))
+    crate.mainEntity = wf
+    suite = crate.add_test_suite()
+    instances = set()
+    assert crate.dereference(JENKINS) is None
+    assert crate.dereference(TRAVIS) is None
+    i1 = crate.add_test_instance(suite, "http://example.com")
+    assert crate.dereference(JENKINS) is i1.service
+    assert crate.dereference(TRAVIS) is None
+    assert i1.url == "http://example.com"
+    instances.add(i1)
+    assert instances == set(suite.instance)
+    i2 = crate.add_test_instance(suite.id, "http://example.com")
+    assert i2.url == "http://example.com"
+    instances.add(i2)
+    assert instances == set(suite.instance)
+    i3 = crate.add_test_instance(suite, "http://example.com", resource="jobs")
+    assert i3.url == "http://example.com"
+    assert i3.resource == "jobs"
+    instances.add(i3)
+    assert instances == set(suite.instance)
+    i4 = crate.add_test_instance(suite, "http://example.com", service="travis")
+    assert i4.url == "http://example.com"
+    assert i4.service.id == TRAVIS
+    assert i4.service is crate.dereference(TRAVIS)
+    instances.add(i4)
+    assert instances == set(suite.instance)
+    i5 = crate.add_test_instance(suite, "http://example.com", identifier="test_1_1")
+    assert i5.url == "http://example.com"
+    assert i5.id == "#test_1_1"
+    instances.add(i5)
+    assert instances == set(suite.instance)
+    i6 = crate.add_test_instance(suite, "http://example.com", name="Test 1 Instance 1")
+    assert i6.url == "http://example.com"
+    assert i6.name == "Test 1 Instance 1"
+    instances.add(i6)
+    assert instances == set(suite.instance)
+
+
+@pytest.mark.parametrize("engine,engine_version", [(None, None), ("planemo", None), ("planemo", ">=0.70")])
+def test_add_test_definition(test_data_dir, helpers, engine, engine_version):
+    top_dir = test_data_dir / "ro-crate-galaxy-sortchangecase"
+    wf_path = top_dir / "sort-and-change-case.ga"
+    def_path = top_dir / "test" / "test1" / "sort-and-change-case-test.yml"
+    crate = ROCrate()
+    wf = crate.add(ComputationalWorkflow(crate, str(wf_path), wf_path.name))
+    crate.mainEntity = wf
+    suite = crate.add_test_suite()
+    assert crate.dereference(PLANEMO) is None
+    kwargs = {"source": def_path}
+    if engine:
+        kwargs["engine"] = engine
+    if engine_version:
+        kwargs["engine_version"] = engine_version
+    d = crate.add_test_definition(suite, **kwargs)
+    assert crate.dereference(PLANEMO) is d.engine
+    if engine_version:
+        assert d.engineVersion == engine_version