Repository: SFDO-Tooling/CumulusCI
Pull request #3357 (2022-09-09 17:34:51)

Improve logging and add error handling for Vlocity tasks
1. Remove`--json` because it suppresses logging and error handling
2. Turn on `interactive` because it improves the colors of the stdout
3. Add some improvements to testing infrastructure discovered while working on integration tests
4. Incomplete attempt of making a formal integration test. At least the documentation describes how to do an integration test by hand.


Patch:
diff --git a/cumulusci.yml b/cumulusci.yml
index 5eff80776..0d8a653b9 100644
--- a/cumulusci.yml
+++ b/cumulusci.yml
@@ -9,6 +9,10 @@ sources:
         github: https://github.com/SFDO-Tooling/CumulusCI-Test
         branch: main
 
+    omni:
+        github: https://github.com/SalesforceFoundation/omni-cci
+        branch: main
+
 tasks:
     robot:
         options:
@@ -216,3 +220,6 @@ orgs:
 
         person_accounts:
             config_file: orgs/person_accounts.json
+
+        omni:
+            config_file: orgs/omni.json
diff --git a/cumulusci/tasks/vlocity/vlocity.py b/cumulusci/tasks/vlocity/vlocity.py
index 196a639d3..92d8b1847 100644
--- a/cumulusci/tasks/vlocity/vlocity.py
+++ b/cumulusci/tasks/vlocity/vlocity.py
@@ -1,3 +1,4 @@
+import sys
 from abc import ABC
 from typing import Final
 
@@ -34,6 +35,10 @@ class VlocityBaseTask(Command, BaseSalesforceTask):
         "extra": {"description": "Any extra arguments to pass to the vlocity CLI"},
     }
 
+    def _init_options(self, kwargs):
+        kwargs.setdefault("interactive", sys.stdout.isatty())
+        return super()._init_options(kwargs)
+
     def _init_task(self):
         tool_exists = self._vlocity_build_tool_exists()
         if not tool_exists:
@@ -69,7 +74,7 @@ class VlocitySimpleJobTask(VlocityBaseTask, ABC):
         username: str = self.org_config.username
         job_file: str = self.options.get("job_file")
 
-        command: str = f"{self.command_keyword} -job {job_file} --json"
+        command: str = f"{self.command_keyword} -job {job_file}"
 
         if isinstance(self.org_config, ScratchOrgConfig):
             command = f"{command} -sfdx.username '{username}'"
@@ -101,7 +106,7 @@ class OmniStudioDeployRemoteSiteSettings(AddRemoteSiteSettings):
 
     task_options: dict = {
         "namespace": {
-            "description": "The namespace to inject into RemoteSiteSettings.url values. Defaults to '{OMNI_NAMESPACE}'."
+            "description": f"The namespace to inject into RemoteSiteSettings.url values. Defaults to '{OMNI_NAMESPACE}'."
         }
     }
 
diff --git a/cumulusci/version.txt b/cumulusci/version.txt
index 9b6c27198..b57a127a6 100644
--- a/cumulusci/version.txt
+++ b/cumulusci/version.txt
@@ -1,1 +1,1 @@
-3.64.0
+3.65.0.dev1
diff --git a/orgs/omni.json b/orgs/omni.json
new file mode 100644
index 000000000..df5db129b
--- /dev/null
+++ b/orgs/omni.json
@@ -0,0 +1,25 @@
+{
+    "orgName": "omni-cci - Dev Org",
+    "edition": "Developer",
+    "features": ["OmniStudioDesigner", "OmniStudioRuntime"],
+    "settings": {
+        "lightningExperienceSettings": {
+            "enableS1DesktopEnabled": true
+        },
+        "chatterSettings": {
+            "enableChatter": true
+        },
+        "userManagementSettings": {
+            "enableNewProfileUI": true
+        },
+        "securitySettings": {
+            "enableAdminLoginAsAnyUser": true,
+            "sessionSettings": {
+                "forceRelogin": false
+            }
+        },
+        "languageSettings": {
+            "enableTranslationWorkbench": true
+        }
+    }
+}


Test patch:
diff --git a/cumulusci/tasks/vlocity/tests/test_vlocity.py b/cumulusci/tasks/vlocity/tests/test_vlocity.py
index 4a8e0a9fa..9a77a9728 100644
--- a/cumulusci/tasks/vlocity/tests/test_vlocity.py
+++ b/cumulusci/tasks/vlocity/tests/test_vlocity.py
@@ -5,6 +5,8 @@ import pytest
 from cumulusci.core.config import TaskConfig
 from cumulusci.core.config.org_config import OrgConfig
 from cumulusci.core.config.scratch_org_config import ScratchOrgConfig
+from cumulusci.core.exceptions import CommandException
+from cumulusci.core.utils import import_global
 from cumulusci.tasks.vlocity.exceptions import BuildToolMissingError
 from cumulusci.tasks.vlocity.vlocity import (
     BUILD_TOOL_MISSING_ERROR,
@@ -48,31 +50,31 @@ vlocity_test_cases = [
         scratch_org_config,
         VlocityRetrieveTask,
         None,
-        f"vlocity packExport -job vlocity.yaml --json -sfdx.username '{username}'",
+        f"vlocity packExport -job vlocity.yaml -sfdx.username '{username}'",
     ),
     (
         persistent_org_config,
         VlocityRetrieveTask,
         None,
-        f"vlocity packExport -job vlocity.yaml --json -sf.accessToken '{access_token}' -sf.instanceUrl '{instance_url}'",
+        f"vlocity packExport -job vlocity.yaml -sf.accessToken '{access_token}' -sf.instanceUrl '{instance_url}'",
     ),
     (
         scratch_org_config,
         VlocityDeployTask,
         None,
-        f"vlocity packDeploy -job vlocity.yaml --json -sfdx.username '{username}'",
+        f"vlocity packDeploy -job vlocity.yaml -sfdx.username '{username}'",
     ),
     (
         persistent_org_config,
         VlocityDeployTask,
         None,
-        f"vlocity packDeploy -job vlocity.yaml --json -sf.accessToken '{access_token}' -sf.instanceUrl '{instance_url}'",
+        f"vlocity packDeploy -job vlocity.yaml -sf.accessToken '{access_token}' -sf.instanceUrl '{instance_url}'",
     ),
     (
         persistent_org_config,
         VlocityDeployTask,
         "foo=bar",
-        f"vlocity packDeploy -job vlocity.yaml --json -sf.accessToken '{access_token}' -sf.instanceUrl '{instance_url}' foo=bar",
+        f"vlocity packDeploy -job vlocity.yaml -sf.accessToken '{access_token}' -sf.instanceUrl '{instance_url}' foo=bar",
     ),
 ]
 
@@ -143,3 +145,52 @@ def test_deploy_omni_studio_site_settings(
     )
     actual_urls = set([r.url for r in records])
     assert expected_urls == actual_urls
+
+
+# Run these tests with a command like:
+# $ cci flow run omni:install_omnistudio --org omni
+# $ pytest --org omni --run-slow-tests -k VlocityIntegration cumulusci/tasks/vlocity/tests/test_vlocity.py --opt-in
+
+# Note that these tests are not totally reliable but I
+# ran out of time. Worse comes to worse, run the same code at
+# the CLI:
+#
+# $ cci task run omni:test_success --org omni
+# $ cci task run omni:test_failure --org omni
+#
+# Finishing this code requires investigation into why SFDX cannot
+# always find its orgs. Perhaps relating to cwd/env that the program.
+#
+# The frequent error is:
+#
+#     "name": "NoOrgFound",
+#     "action": "Run the \"sfdx force:auth\" commands with --setdefaultusername to connect to an org and set it as your default org.\nRun \"force:org:create\" with --setdefaultusername to create a scratch org and set it as your default org.\nRun \"sfdx force:config:set defaultusername=<username>\" to set your default username."
+# }
+
+
+@pytest.mark.needs_org()
+@pytest.mark.slow()
+@pytest.mark.org_shape("omni", "omni:install_omnistudio")  # SUPER-SLOW!!!!
+@pytest.mark.opt_in()  # this test probably does not work in CI yet due to dependence on omni-cci
+class TestVlocityIntegration:
+    def test_vlocity_integration(self, project_config, create_task, caplog):
+        task_config = project_config.get_task("omni:test_success")
+        task_class = import_global(task_config.class_path)
+        task = create_task(
+            task_class, task_config.options, project_config=task_config.project_config
+        )
+        try:
+            task()
+            assert task.return_values["returncode"] == 0
+        except Exception:
+            print(caplog.records)
+            raise
+
+    def test_vlocity_integration__error_handling(self, project_config, create_task):
+        task_config = project_config.get_task("omni:test_failure")
+        task_class = import_global(task_config.class_path)
+        task = create_task(
+            task_class, task_config.options, project_config=task_config.project_config
+        )
+        with pytest.raises(CommandException):
+            task()
diff --git a/cumulusci/tests/pytest_plugins/pytest_sf_orgconnect.py b/cumulusci/tests/pytest_plugins/pytest_sf_orgconnect.py
index 2d5695ab4..d22654911 100644
--- a/cumulusci/tests/pytest_plugins/pytest_sf_orgconnect.py
+++ b/cumulusci/tests/pytest_plugins/pytest_sf_orgconnect.py
@@ -29,6 +29,12 @@ def pytest_addoption(parser, pluginmanager):
         default=False,
         help="Replace VCR files.",
     )
+    parser.addoption(
+        "--opt-in",
+        action="store_true",
+        default=False,
+        help="disable custom_skip marks",
+    )
 
 
 def pytest_configure(config):
@@ -38,6 +44,7 @@ def pytest_configure(config):
         "large_vcr(): a network-based test that generates VCR cassettes too large for version control. Use --org to generate them locally.",
         "needs_org(): a test that needs an org (or at least access to the network) but should not attempt to store VCR cassettes",
         "org_shape(org_name, init_flow): a test that needs a particular org shape",
+        "opt_in(): a test that is 'off' by default (perhaps because it depends on some setup)",
     ]
 
     for marker in markers:
@@ -157,6 +164,9 @@ def vcr_cassette_name_for_item(item):
 
 
 def classify_and_modify_test(item, marker_names):
+    if "opt_in" in marker_names and not item.config.getoption("--opt-in"):
+        pytest.skip("Skip by default turned on")
+
     if "slow" in marker_names:
         if not item.config.getoption("--run-slow-tests"):
             pytest.skip("slow: test requires --run-slow-tests")
@@ -275,18 +285,23 @@ def _create_org(org_name: str, config_name: str, flow_name: str = None):
         org = None
     if org:
         cleanup_org_shapes([org_name])
-    org_scratch.callback.__wrapped__(
-        runtime,
-        config_name,
-        org_name,
-        default=False,
-        devhub=None,
-        days=1,
-        no_password=False,
-    )
-    org, org_config = runtime.get_org(org_name)
+    try:
+        org_scratch.callback.__wrapped__(
+            runtime,
+            config_name,
+            org_name,
+            default=False,
+            devhub=None,
+            days=1,
+            no_password=False,
+        )
+        org, org_config = runtime.get_org(org_name)
 
-    if flow_name:
-        coordinator = runtime.get_flow(flow_name)
-        coordinator.run(org_config)
+        if flow_name:
+            coordinator = runtime.get_flow(flow_name)
+            coordinator.run(org_config)
+    except Exception:
+        if runtime.get_org(org_name, fail_if_missing=False):
+            org_scratch_delete.callback.__wrapped__(runtime, org_name)
+        raise
     return org_config