Skip to content

Commit 08a4d97

Browse files
authored
Merge pull request #3874 from SFDO-Tooling/feature/pre-flights-functionality
Added Preflight checks functionality
2 parents 2c164c8 + 632768d commit 08a4d97

3 files changed

Lines changed: 310 additions & 42 deletions

File tree

cumulusci/cli/checks.py

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,52 @@
1+
import logging
2+
13
import click
24

5+
from cumulusci.cli.ui import CliTable
6+
from cumulusci.core.config import FlowConfig
7+
from cumulusci.core.flowrunner import PreflightFlowCoordinator
8+
39
from .runtime import pass_runtime
410

511

6-
@click.group("checks", help="Commands for running preflight checks for a plan")
12+
@click.group(
13+
"checks",
14+
help="Commands for getting information about Preflight checks for a Radagast plan",
15+
)
716
def checks():
817
pass
918

1019

11-
@checks.command(name="info", help="Displays preflight checks for a plan")
20+
@checks.command(name="info", help="Displays checks for a plan")
1221
@click.argument("plan_name")
1322
@pass_runtime(require_project=True)
1423
def checks_info(runtime, plan_name):
15-
click.echo("This plan has the following preflight checks: ")
24+
25+
"""
26+
Displays checks for a Radagast plan.
27+
"""
28+
29+
# Check if the plan exists or not
30+
plans = runtime.project_config.plans or {}
31+
if plan_name not in plans:
32+
raise click.UsageError(
33+
f"Unknown plan '{plan_name}'. To view available plans run: `cci plan list`"
34+
)
35+
36+
# Get the checks under a plan
37+
preflight_checks = [
38+
[check.get("action", ""), check.get("message", ""), check.get("when", "")]
39+
for check in plans[plan_name].get("checks", [])
40+
]
41+
# Create Cli Table to display the checks
42+
plan_preflight_checks_table = CliTable(
43+
title="Plan Preflights",
44+
data=[
45+
["Action", "Message", "When"],
46+
*preflight_checks,
47+
],
48+
)
49+
plan_preflight_checks_table.echo()
1650

1751

1852
@checks.command(name="run", help="Runs checks under a plan")
@@ -23,8 +57,80 @@ def checks_info(runtime, plan_name):
2357
)
2458
@pass_runtime(require_keychain=True, require_project=True)
2559
def checks_run(runtime, plan_name, org):
60+
"""
61+
Runs checks for a Radagast plan.
62+
"""
63+
plans = runtime.project_config.plans or {}
64+
65+
# Check if the plan exists or not
66+
if plan_name not in plans:
67+
raise click.UsageError(
68+
f"Unknown plan '{plan_name}'. To view available plans run: `cci plan list`"
69+
)
70+
71+
logger = logging.getLogger("cumulusci.flows")
72+
org_logger = logging.getLogger("cumulusci.core.config.base_config")
73+
74+
def _rule(fill="=", length=60, new_line=False):
75+
logger.info(f"{fill * length}")
76+
if new_line:
77+
logger.info("")
2678

27-
# Get necessary configs
2879
org, org_config = runtime.get_org(org)
29-
m = "Running checks for the plan " + plan_name
30-
click.echo(m)
80+
81+
# Print the org details
82+
_rule(fill="-", new_line=False)
83+
logger.info("Organization:")
84+
logger.info(f" Username: {org_config.username}")
85+
logger.info(f" Org Id: {org_config.org_id}")
86+
_rule(fill="-", new_line=True)
87+
logger.info(f"Running preflight checks for the plan {plan_name} ...")
88+
_rule(new_line=True)
89+
checks = plans[plan_name]["checks"]
90+
91+
# Check if there are no checks available under the plan
92+
if len(checks) == 0:
93+
raise click.UsageError(
94+
f"No checks exists for the '{plan_name}'. To view available checks run: `cci checks info`"
95+
)
96+
97+
# Run the preflight checks under the plan
98+
flow_config = FlowConfig({"checks": checks, "steps": {}})
99+
flow_coordinator = PreflightFlowCoordinator(
100+
runtime.project_config,
101+
flow_config,
102+
name="preflight",
103+
)
104+
# Ignore the logs coming via the pre flight coordinator execution
105+
logger.setLevel(logging.WARNING)
106+
org_logger.setLevel(logging.WARNING)
107+
flow_coordinator.run(org_config)
108+
logger.setLevel(logging.INFO)
109+
org_logger.setLevel(logging.INFO)
110+
results = flow_coordinator.preflight_results
111+
112+
# Check if the there are any errors/warnings while running checks
113+
if results:
114+
raise_error = False
115+
print(results.items())
116+
for step_name, step_results in results.items():
117+
table_header_row = ["Status", "Message"]
118+
table_data = [table_header_row]
119+
120+
for result in step_results:
121+
table_data.append([result["status"], result["message"]])
122+
if result["status"] == "error":
123+
raise_error = True
124+
table = CliTable(
125+
table_data,
126+
)
127+
table.echo(plain=True)
128+
if raise_error:
129+
# Raise an exception if there are any failed pre flight checks
130+
raise Exception(
131+
"Some of the checks failed with errors. Please check the logs for details."
132+
)
133+
else:
134+
logger.info("The preflight checks ran succesfully with the warnings.")
135+
else:
136+
logger.info("The preflight checks ran succesfully.")

cumulusci/cli/tests/test_checks.py

Lines changed: 147 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,174 @@
11
from unittest import mock
22

3+
import click
4+
import pytest
5+
36
from cumulusci.cli.runtime import CliRuntime
47

58
from .. import checks
69
from .utils import run_click_command
710

811

9-
@mock.patch("click.echo")
10-
def test_flow_info(echo):
11-
12-
runtime = CliRuntime(
13-
config={
14-
"flows": {
15-
"test": {
16-
"steps": {
17-
1: {
18-
"task": "test_task",
19-
"options": {"option_name": "option_value"},
12+
@pytest.fixture
13+
def runtime():
14+
runtime = CliRuntime()
15+
runtime.project_config.config["plans"] = {
16+
"plan 1": {
17+
"title": "Test Plan #1",
18+
"slug": "plan1_slug",
19+
"tier": "primary",
20+
"preflight_message": "This is a preflight message",
21+
"error_message": "This is an error message",
22+
"steps": {
23+
1: {
24+
"task": "run_tests",
25+
"ui_options": {
26+
"name": "Run Tests",
27+
"is_recommended": False,
28+
"is_required": False,
29+
},
30+
"checks": [
31+
{
32+
"when": "soon",
33+
"action": "error",
34+
"message": "Danger Will Robinson!",
2035
}
21-
}
36+
],
2237
}
2338
},
24-
"tasks": {
25-
"test_task": {
26-
"class_path": "cumulusci.cli.tests.test_flow.DummyTask",
27-
"description": "Test Task",
39+
"checks": [
40+
{
41+
"when": "'test package' not in tasks.get_installed_packages()",
42+
"action": "error",
43+
"message": "Test Package must be installed in your org.",
2844
}
29-
},
45+
],
3046
},
31-
load_keychain=False,
47+
}
48+
49+
yield runtime
50+
51+
52+
@mock.patch("cumulusci.cli.checks.CliTable")
53+
def test_checks_info(cli_table, runtime):
54+
55+
run_click_command(checks.checks_info, runtime=runtime, plan_name="plan 1")
56+
cli_table.assert_called_once_with(
57+
title="Plan Preflights",
58+
data=[
59+
["Action", "Message", "When"],
60+
[
61+
"error",
62+
"Test Package must be installed in your org.",
63+
"'test package' not in tasks.get_installed_packages()",
64+
],
65+
],
3266
)
3367

34-
run_click_command(checks.checks_info, runtime=runtime, plan_name="test")
3568

36-
echo.assert_called_with("This plan has the following preflight checks: ")
69+
@mock.patch("cumulusci.cli.checks.PreflightFlowCoordinator")
70+
def test_checks_run(mock_flow_coordinator, runtime):
71+
org_config = mock.Mock(scratch=True, config={})
72+
org_config.username = "test_user"
73+
org_config.org_id = "test_org_id"
74+
runtime.get_org = mock.Mock(return_value=("test", org_config))
75+
76+
mock_flow_coordinator_return_value = mock.Mock()
77+
mock_flow_coordinator_return_value.preflight_results = {} # No errors or warnings
78+
mock_flow_coordinator.return_value = mock_flow_coordinator_return_value
79+
with mock.patch("logging.getLogger") as mock_logger:
80+
81+
run_click_command(
82+
checks.checks_run,
83+
runtime=runtime,
84+
plan_name="plan 1",
85+
org="test",
86+
)
87+
mock_logger.assert_called()
88+
mock_flow_coordinator.assert_called_once_with(
89+
runtime.project_config, mock.ANY, name="preflight"
90+
)
3791

3892

39-
@mock.patch("click.echo")
40-
def test_checks_run(echo):
93+
@mock.patch("cumulusci.cli.checks.PreflightFlowCoordinator")
94+
@mock.patch("logging.getLogger")
95+
def test_checks_run_unknown_plan(mock_flow_coordinator, mock_logger, runtime):
4196
org_config = mock.Mock(scratch=True, config={})
42-
runtime = CliRuntime(
43-
config={
44-
"flows": {"test": {"steps": {1: {"task": "test_task"}}}},
45-
"tasks": {
46-
"test_task": {
47-
"class_path": "cumulusci.cli.tests.test_flow.DummyTask",
48-
"description": "Test Task",
49-
}
50-
},
51-
},
52-
load_keychain=False,
53-
)
97+
org_config.username = "test_user"
98+
org_config.org_id = "test_org_id"
99+
runtime.get_org = mock.Mock(return_value=("test", org_config))
100+
101+
mock_flow_coordinator_return_value = mock.Mock()
102+
mock_flow_coordinator_return_value.preflight_results = {} # No errors or warnings
103+
mock_flow_coordinator.return_value = mock_flow_coordinator_return_value
104+
with pytest.raises(click.UsageError) as error:
105+
106+
run_click_command(
107+
checks.checks_run,
108+
runtime=runtime,
109+
plan_name="unknown plan",
110+
org="test",
111+
)
112+
mock_flow_coordinator.assert_called_once_with(
113+
runtime.project_config, mock.ANY, name="preflight"
114+
)
115+
assert "Unknown plan 'unknown_plan'" in error.value
116+
117+
118+
@mock.patch("cumulusci.cli.checks.PreflightFlowCoordinator")
119+
def test_checks_run_with_warnings(mock_flow_coordinator, runtime):
120+
org_config = mock.Mock(scratch=True, config={})
121+
org_config.username = "test_user"
122+
org_config.org_id = "test_org_id"
54123
runtime.get_org = mock.Mock(return_value=("test", org_config))
55124

125+
mock_flow_coordinator.return_value.preflight_results = {
126+
None: [
127+
{
128+
"status": "warning",
129+
"message": "You need Context Service AdminPsl in your Org assigned Admin User to use this feature. Contact your Administrator.",
130+
}
131+
]
132+
}
133+
56134
run_click_command(
57135
checks.checks_run,
58136
runtime=runtime,
59-
plan_name="test",
137+
plan_name="plan 1",
60138
org="test",
61139
)
140+
mock_flow_coordinator.assert_called_once_with(
141+
runtime.project_config, mock.ANY, name="preflight"
142+
)
143+
144+
145+
@mock.patch("cumulusci.cli.checks.PreflightFlowCoordinator")
146+
def test_checks_run_with_errors(mock_flow_coordinator, runtime):
147+
org_config = mock.Mock(scratch=True, config={})
148+
org_config.username = "test_user"
149+
org_config.org_id = "test_org_id"
150+
runtime.get_org = mock.Mock(return_value=("test", org_config))
151+
152+
mock_flow_coordinator.return_value.preflight_results = {
153+
None: [
154+
{
155+
"status": "error",
156+
"message": "You need Context Service AdminPsl in your Org assigned Admin User to use this feature. Contact your Administrator.",
157+
}
158+
]
159+
}
160+
with pytest.raises(Exception) as error:
62161

63-
echo.assert_called_with("Running checks for the plan test")
162+
run_click_command(
163+
checks.checks_run,
164+
runtime=runtime,
165+
plan_name="plan 1",
166+
org="test",
167+
)
168+
mock_flow_coordinator.assert_called_once_with(
169+
runtime.project_config, mock.ANY, name="preflight"
170+
)
171+
assert (
172+
"Some of the checks failed with errors. Please check the logs for details."
173+
in error.value
174+
)

0 commit comments

Comments
 (0)