Repository: ResearchObject/ro-crate-py
Pull request #49 (2021-02-17 16:39:27)

CLI commands to add test metadata
Adds commands to add test metadata to the CLI.

Docker container example (create a workflow testing ro-crate starting from a directory tree):

```
root@b5ef27b0dbc7:/ro-crate-py# cp -rf test/test-data/ro-crate-galaxy-sortchangecase /tmp/test_crate
root@b5ef27b0dbc7:/ro-crate-py# cd /tmp/test_crate
root@b5ef27b0dbc7:/tmp/test_crate# rm -fv ro-crate-metadata.json
removed 'ro-crate-metadata.json'
root@b5ef27b0dbc7:/tmp/test_crate# rocrate init
root@b5ef27b0dbc7:/tmp/test_crate# rocrate add workflow -l galaxy sort-and-change-case.ga
root@b5ef27b0dbc7:/tmp/test_crate# suite_id=$(rocrate add test-suite)
root@b5ef27b0dbc7:/tmp/test_crate# instance_id=$(rocrate add test-instance "${suite_id}" "http://example.com" -r "jobs")
root@b5ef27b0dbc7:/tmp/test_crate# rocrate add test-definition -e planemo -v '>=0.70' "${suite_id}" test/test1/sort-and-change-case-test.yml
root@b5ef27b0dbc7:/tmp/test_crate# cat ro-crate-metadata.json
{
    "@context": "https://w3id.org/ro/crate/1.1/context",
    "@graph": [
        {
            "@id": "ro-crate-metadata.json",
            "@type": "CreativeWork",
            "about": {
                "@id": "./"
            },
            "conformsTo": {
                "@id": "https://w3id.org/ro/crate/1.1"
            }
        },
        {
            "@id": "./",
            "@type": "Dataset",
            "datePublished": "2021-02-17T16:37:05.493186",
            "hasPart": [
                {
                    "@id": "test/"
                },
                {
                    "@id": "LICENSE"
                },
                {
                    "@id": "sort-and-change-case.ga"
                },
                {
                    "@id": "README.md"
                },
                {
                    "@id": "test/test1/"
                },
                {
                    "@id": "test/test1/sort-and-change-case-test.yml"
                },
                {
                    "@id": "test/test1/input.bed"
                },
                {
                    "@id": "test/test1/output_exp.bed"
                }
            ],
            "mainEntity": {
                "@id": "sort-and-change-case.ga"
            }
        },
        {
            "@id": "ro-crate-preview.html",
            "@type": "CreativeWork",
            "about": {
                "@id": "./"
            }
        },
        {
            "@id": "test/",
            "@type": "Dataset",
            "about": [
                {
                    "@id": "#74d5fd2d-ba70-474f-b1c1-1170c3e5d075"
                }
            ]
        },
        {
            "@id": "LICENSE",
            "@type": "File"
        },
        {
            "@id": "sort-and-change-case.ga",
            "@type": [
                "File",
                "SoftwareSourceCode",
                "ComputationalWorkflow"
            ],
            "name": "sort-and-change-case",
            "programmingLanguage": {
                "@id": "#galaxy"
            }
        },
        {
            "@id": "README.md",
            "@type": "File"
        },
        {
            "@id": "test/test1/",
            "@type": "Dataset"
        },
        {
            "@id": "test/test1/sort-and-change-case-test.yml",
            "@type": [
                "File",
                "TestDefinition"
            ],
            "conformsTo": {
                "@id": "https://w3id.org/ro/terms/test#PlanemoEngine"
            },
            "engineVersion": ">=0.70"
        },
        {
            "@id": "test/test1/input.bed",
            "@type": "File"
        },
        {
            "@id": "test/test1/output_exp.bed",
            "@type": "File"
        },
        {
            "@id": "#galaxy",
            "@type": "ComputerLanguage",
            "identifier": {
                "@id": "https://galaxyproject.org/"
            },
            "name": "Galaxy",
            "url": {
                "@id": "https://galaxyproject.org/"
            },
            "version": "v19_09"
        },
        {
            "@id": "#74d5fd2d-ba70-474f-b1c1-1170c3e5d075",
            "@type": "TestSuite",
            "definition": {
                "@id": "test/test1/sort-and-change-case-test.yml"
            },
            "instance": [
                {
                    "@id": "#9caa85a4-41e7-496c-8e57-24f1f434f820"
                }
            ],
            "mainEntity": {
                "@id": "sort-and-change-case.ga"
            },
            "name": "74d5fd2d-ba70-474f-b1c1-1170c3e5d075"
        },
        {
            "@id": "#9caa85a4-41e7-496c-8e57-24f1f434f820",
            "@type": "TestInstance",
            "name": "9caa85a4-41e7-496c-8e57-24f1f434f820",
            "resource": "jobs",
            "runsOn": {
                "@id": "https://w3id.org/ro/terms/test#JenkinsService"
            },
            "url": "http://example.com"
        },
        {
            "@id": "https://w3id.org/ro/terms/test#JenkinsService",
            "@type": "TestService",
            "name": "Jenkins",
            "url": {
                "@id": "https://www.jenkins.io"
            }
        },
        {
            "@id": "https://w3id.org/ro/terms/test#PlanemoEngine",
            "@type": "SoftwareApplication",
            "name": "Planemo",
            "url": {
                "@id": "https://github.com/galaxyproject/planemo"
            }
        }
    ]
}
```


Patch:
diff --git a/rocrate/cli.py b/rocrate/cli.py
index 0986665..057aefe 100644
--- a/rocrate/cli.py
+++ b/rocrate/cli.py
@@ -5,9 +5,13 @@ import click
 from .rocrate import ROCrate
 from .model.preview import Preview
 from .model.computerlanguage import LANG_MAP
+from .model.testservice import SERVICE_MAP
+from .model.softwareapplication import APP_MAP
 
 
 LANG_CHOICES = list(LANG_MAP)
+SERVICE_CHOICES = list(SERVICE_MAP)
+ENGINE_CHOICES = list(APP_MAP)
 
 
 class State:
@@ -39,7 +43,7 @@ def add(state):
 
 @add.command()
 @click.argument('path', type=click.Path(exists=True))
-@click.option('-l', '--language', type=click.Choice(LANG_CHOICES))
+@click.option('-l', '--language', type=click.Choice(LANG_CHOICES), default="cwl")
 @click.pass_obj
 def workflow(state, path, language):
     crate_dir = state.crate_dir or os.getcwd()
@@ -57,5 +61,50 @@ def workflow(state, path, language):
         state.crate.preview.write(crate_dir)
 
 
+@add.command(name="test-suite")
+@click.option('-i', '--identifier')
+@click.option('-n', '--name')
+@click.option('-m', '--main-entity')
+@click.pass_obj
+def suite(state, identifier, name, main_entity):
+    crate_dir = state.crate_dir or os.getcwd()
+    suite_ = state.crate.add_test_suite(identifier=identifier, name=name, main_entity=main_entity)
+    state.crate.metadata.write(crate_dir)
+    print(suite_.id)
+
+
+@add.command(name="test-instance")
+@click.argument('suite')
+@click.argument('url')
+@click.option('-r', '--resource', default="")
+@click.option('-s', '--service', type=click.Choice(SERVICE_CHOICES), default="jenkins")
+@click.option('-i', '--identifier')
+@click.option('-n', '--name')
+@click.pass_obj
+def instance(state, suite, url, resource, service, identifier, name):
+    crate_dir = state.crate_dir or os.getcwd()
+    instance_ = state.crate.add_test_instance(suite, url, resource=resource, service=service, identifier=identifier, name=name)
+    state.crate.metadata.write(crate_dir)
+    print(instance_.id)
+
+
+@add.command(name="test-definition")
+@click.argument('suite')
+@click.argument('path', type=click.Path(exists=True))
+@click.option('-e', '--engine', type=click.Choice(ENGINE_CHOICES), default="planemo")
+@click.option('-v', '--engine-version')
+@click.pass_obj
+def definition(state, suite, path, engine, engine_version):
+    crate_dir = state.crate_dir or os.getcwd()
+    source = Path(path).resolve(strict=True)
+    try:
+        dest_path = source.relative_to(crate_dir)
+    except ValueError:
+        # For now, only support marking an existing file as a test definition
+        raise ValueError(f"{source} is not in the crate dir")
+    state.crate.add_test_definition(suite, source=source, dest_path=dest_path, engine=engine, engine_version=engine_version)
+    state.crate.metadata.write(crate_dir)
+
+
 if __name__ == '__main__':
     cli()
diff --git a/rocrate/rocrate.py b/rocrate/rocrate.py
index b201278..8b170a1 100644
--- a/rocrate/rocrate.py
+++ b/rocrate/rocrate.py
@@ -510,8 +510,11 @@ class ROCrate():
 
     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
+            engine="planemo", engine_version=None
     ):
+        if engine_version is None:
+            # FIXME: this should be engine-specific
+            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,


Test patch:
diff --git a/test/test_cli.py b/test/test_cli.py
index 8062d82..0339b04 100644
--- a/test/test_cli.py
+++ b/test/test_cli.py
@@ -70,3 +70,44 @@ def test_cli_add_workflow(test_data_dir, helpers, monkeypatch, cwd):
         lang_id = f"#{lang}"
         assert lang_id in json_entities
         assert json_entities["sort-and-change-case.ga"]["programmingLanguage"]["@id"] == lang_id
+
+
+@pytest.mark.parametrize("cwd", [False, True])
+def test_cli_add_test_metadata(test_data_dir, helpers, monkeypatch, cwd):
+    # init
+    crate_dir = test_data_dir / "ro-crate-galaxy-sortchangecase"
+    runner = CliRunner()
+    assert runner.invoke(cli, ["-c", str(crate_dir), "init"]).exit_code == 0
+    json_entities = helpers.read_json_entities(crate_dir)
+    def_id = "test/test1/sort-and-change-case-test.yml"
+    assert def_id in json_entities
+    assert json_entities[def_id]["@type"] == "File"
+    # add workflow
+    wf_path = crate_dir / "sort-and-change-case.ga"
+    runner.invoke(cli, ["-c", str(crate_dir), "add", "workflow", "-l", "galaxy", str(wf_path)]).exit_code == 0
+    # add test suite
+    result = runner.invoke(cli, ["-c", str(crate_dir), "add", "test-suite"])
+    assert result.exit_code == 0
+    suite_id = result.output.strip()
+    json_entities = helpers.read_json_entities(crate_dir)
+    assert suite_id in json_entities
+    # add test instance
+    result = runner.invoke(cli, ["-c", str(crate_dir), "add", "test-instance", suite_id, "http://example.com", "-r", "jobs"])
+    assert result.exit_code == 0
+    instance_id = result.output.strip()
+    json_entities = helpers.read_json_entities(crate_dir)
+    assert instance_id in json_entities
+    # add test definition
+    def_path = crate_dir / def_id
+    args = []
+    if cwd:
+        monkeypatch.chdir(str(crate_dir))
+        def_path = def_path.relative_to(crate_dir)
+    else:
+        args.extend(["-c", str(crate_dir)])
+    extra_args = ["add", "test-definition", "-e", "planemo", "-v", ">=0.70", suite_id, str(def_path)]
+    result = runner.invoke(cli, args + extra_args)
+    assert result.exit_code == 0
+    json_entities = helpers.read_json_entities(crate_dir)
+    assert def_id in json_entities
+    assert set(json_entities[def_id]["@type"]) == {"File", "TestDefinition"}