Skip to content

Commit 76d8a48

Browse files
committed
Merge branch 'main' into feature/add_checks_command
2 parents 08a4d97 + a0494b8 commit 76d8a48

10 files changed

Lines changed: 465 additions & 84 deletions

File tree

cumulusci/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "4.2.0"
1+
__version__ = "4.3.0.dev0"

cumulusci/tasks/bulkdata/step.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,18 @@ def select_records(self, records):
463463
records, records_copy = tee(records)
464464
# Count total number of records to fetch using the copy
465465
total_num_records = sum(1 for _ in records_copy)
466+
467+
# In the case that records are zero, return success
468+
if total_num_records == 0:
469+
self.logger.info(f"No records present for {self.sobject}")
470+
self.job_result = DataOperationJobResult(
471+
status=DataOperationStatus.SUCCESS,
472+
job_errors=[],
473+
records_processed=0,
474+
total_row_errors=0,
475+
)
476+
return
477+
466478
limit_clause = self._determine_limit_clause(total_num_records=total_num_records)
467479

468480
# Generate and execute SOQL query
@@ -882,6 +894,17 @@ def select_records(self, records):
882894
# Count total number of records to fetch using the copy
883895
total_num_records = sum(1 for _ in records_copy)
884896

897+
# In the case that records are zero, return success
898+
self.logger.info(f"No records present for {self.sobject}")
899+
if total_num_records == 0:
900+
self.job_result = DataOperationJobResult(
901+
status=DataOperationStatus.SUCCESS,
902+
job_errors=[],
903+
records_processed=0,
904+
total_row_errors=0,
905+
)
906+
return
907+
885908
# Set LIMIT condition
886909
limit_clause = self._determine_limit_clause(total_num_records)
887910

cumulusci/tasks/bulkdata/tests/test_step.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,47 @@ def test_select_records_standard_strategy_success(self, download_mock):
591591
== 3
592592
)
593593

594+
@mock.patch("cumulusci.tasks.bulkdata.step.download_file")
595+
def test_select_records_zero_load_records(self, download_mock):
596+
# Set up mock context and BulkApiDmlOperation
597+
context = mock.Mock()
598+
step = BulkApiDmlOperation(
599+
sobject="Contact",
600+
operation=DataOperationType.QUERY,
601+
api_options={"batch_size": 10, "update_key": "LastName"},
602+
context=context,
603+
fields=["LastName"],
604+
selection_strategy=SelectStrategy.STANDARD,
605+
content_type="JSON",
606+
)
607+
608+
# Mock Bulk API responses
609+
step.bulk.endpoint = "https://test"
610+
step.bulk.create_query_job.return_value = "JOB"
611+
step.bulk.query.return_value = "BATCH"
612+
step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
613+
614+
# Mock the downloaded CSV content with a single record
615+
download_mock.return_value = io.StringIO('[{"Id":"003000000000001"}]')
616+
617+
# Mock the _wait_for_job method to simulate a successful job
618+
step._wait_for_job = mock.Mock()
619+
step._wait_for_job.return_value = DataOperationJobResult(
620+
DataOperationStatus.SUCCESS, [], 0, 0
621+
)
622+
623+
# Prepare input records
624+
records = iter([])
625+
626+
# Execute the select_records operation
627+
step.start()
628+
step.select_records(records)
629+
step.end()
630+
631+
# Get the results and assert their properties
632+
results = list(step.get_results())
633+
assert len(results) == 0 # Expect 0 results (no records to process)
634+
594635
@mock.patch("cumulusci.tasks.bulkdata.step.download_file")
595636
def test_select_records_standard_strategy_failure__no_records(self, download_mock):
596637
# Set up mock context and BulkApiDmlOperation
@@ -1927,6 +1968,45 @@ def test_select_records_standard_strategy_success(self):
19271968
== 3
19281969
)
19291970

1971+
@responses.activate
1972+
def test_select_records_zero_load_records(self):
1973+
mock_describe_calls()
1974+
task = _make_task(
1975+
LoadData,
1976+
{
1977+
"options": {
1978+
"database_url": "sqlite:///test.db",
1979+
"mapping": "mapping.yml",
1980+
}
1981+
},
1982+
)
1983+
task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
1984+
task._init_task()
1985+
1986+
step = RestApiDmlOperation(
1987+
sobject="Contact",
1988+
operation=DataOperationType.UPSERT,
1989+
api_options={"batch_size": 10, "update_key": "LastName"},
1990+
context=task,
1991+
fields=["LastName"],
1992+
selection_strategy=SelectStrategy.STANDARD,
1993+
)
1994+
1995+
results = {
1996+
"records": [],
1997+
"done": True,
1998+
}
1999+
step.sf.restful = mock.Mock()
2000+
step.sf.restful.return_value = results
2001+
records = iter([])
2002+
step.start()
2003+
step.select_records(records)
2004+
step.end()
2005+
2006+
# Get the results and assert their properties
2007+
results = list(step.get_results())
2008+
assert len(results) == 0 # Expect 0 results (matching the input records count)
2009+
19302010
@responses.activate
19312011
def test_select_records_standard_strategy_success_pagination(self):
19322012
mock_describe_calls()

cumulusci/tasks/metadata/package.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,19 @@ def process_common_components(response_messages: List, components: Dict):
4545
"""Compare compoents in the api responce object with list of components and return common common components"""
4646
if not response_messages or not components:
4747
return components
48-
4948
for message in response_messages:
49+
5050
message_list = message.firstChild.nextSibling.firstChild.nodeValue.split("'")
5151
if len(message_list) > 1:
5252
component_type = message_list[1]
5353
message_txt = message_list[2]
54-
5554
if "is not available in this organization" in message_txt:
5655
del components[component_type]
56+
elif "is unknown" in message_txt:
57+
component_type = message_list[0].split(" ")
58+
components[component_type[0]].remove(message_list[1])
59+
if len(components[component_type]) == 0:
60+
del components[component_type]
5761
else:
5862
component_name = message_list[3]
5963
if component_name in components[component_type]:

cumulusci/tasks/preflight/licenses.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@ def _run_task(self):
1515

1616
class GetAvailablePermissionSetLicenses(BaseSalesforceApiTask):
1717
def _run_task(self):
18+
query = "SELECT PermissionSetLicenseKey FROM PermissionSetLicense WHERE Status = 'Active'"
1819
self.return_values = [
1920
result["PermissionSetLicenseKey"]
20-
for result in self.sf.query(
21-
"SELECT PermissionSetLicenseKey FROM PermissionSetLicense"
22-
)["records"]
21+
for result in self.sf.query(query)["records"]
2322
]
2423
licenses = "\n".join(self.return_values)
2524
self.logger.info(f"Found permission set licenses:\n{licenses}")

cumulusci/tasks/preflight/sobjects.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ class CheckSObjectsAvailable(BaseSalesforceApiTask):
1717
action: error
1818
message: "Enhanced Notes are not turned on."
1919
"""
20-
api_version = "48.0"
2120

2221
def _run_task(self):
22+
2323
self.return_values = {entry["name"] for entry in self.sf.describe()["sobjects"]}
2424

2525
self.logger.info(

cumulusci/tasks/preflight/tests/test_licenses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def test_psl_preflight(self):
4141
task()
4242

4343
task._init_api.return_value.query.assert_called_once_with(
44-
"SELECT PermissionSetLicenseKey FROM PermissionSetLicense"
44+
"SELECT PermissionSetLicenseKey FROM PermissionSetLicense WHERE Status = 'Active'"
4545
)
4646
assert task.return_values == ["TEST1", "TEST2"]
4747

cumulusci/tasks/salesforce/check_components.py

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import shutil
44
import tempfile
55
from collections import defaultdict
6+
from itertools import chain
67
from xml.etree.ElementTree import ParseError
78

89
from defusedxml.minidom import parseString
@@ -47,6 +48,35 @@ def _run_task(self):
4748
paths = self.options.get("paths")
4849
plan_or_flow_name = self.options.get("name")
4950

51+
(
52+
components,
53+
api_retrieve_unpackaged_response,
54+
) = self.get_repo_existing_components(plan_or_flow_name, paths)
55+
56+
if not components:
57+
self.logger.info("No components found in deploy path")
58+
raise TaskOptionsError("No plan or paths options provided")
59+
60+
self.logger.debug("Components detected at source")
61+
for component_type, component_names in components.items():
62+
self.logger.debug(f"{component_type}: {', '.join(component_names)}")
63+
# check common components
64+
components.pop("Settings", None)
65+
existing_components = process_common_components(
66+
api_retrieve_unpackaged_response, components
67+
)
68+
69+
if existing_components:
70+
self.logger.info("Components exists in the target org:")
71+
for component_type, component_names in existing_components.items():
72+
self.logger.info(f"{component_type}: {', '.join(component_names)}")
73+
self.return_values["existing_components"] = existing_components
74+
else:
75+
self.logger.info(
76+
"No components from the deploy paths exist in the target org."
77+
)
78+
79+
def get_repo_existing_components(self, plan_or_flow_name, paths=""):
5080
if paths:
5181
paths = process_list_arg(paths)
5282
self.logger.info(f"Using provided paths: {paths}")
@@ -71,16 +101,37 @@ def _run_task(self):
71101
self.logger.debug(
72102
f"deploy paths found in the plan or flow.{self.deploy_paths}"
73103
)
74-
75104
# Temp dir to copy all deploy paths from task options
76105
temp_dir = tempfile.mkdtemp()
77106
self.logger.info(f"Temporary deploy directory created: {temp_dir}")
78-
107+
mdapi_components = {}
108+
mdapi_response_messages = []
79109
for path in self.deploy_paths:
80110
full_path = os.path.join(self.project_config.repo_root, path)
81111
if not os.path.exists(full_path):
82112
self.logger.info(f"Skipping path: '{path}' - path doesn't exist")
83113
continue
114+
elif "package.xml" in os.listdir(full_path):
115+
package_xml_path = os.path.join(full_path, "package.xml")
116+
source_xml_tree = metadata_tree.parse(package_xml_path)
117+
components = metadata_tree.parse_package_xml_types(
118+
"name", source_xml_tree
119+
)
120+
response_messages = self._get_api_object_responce(
121+
package_xml_path, source_xml_tree.version.text
122+
)
123+
merged = {}
124+
for key in set(components).union(mdapi_components):
125+
merged[key] = list(
126+
set(
127+
chain(
128+
components.get(key, []), mdapi_components.get(key, [])
129+
)
130+
)
131+
)
132+
mdapi_components = merged
133+
mdapi_response_messages.extend(response_messages)
134+
continue
84135
self._copy_to_tempdir(path, temp_dir)
85136

86137
(
@@ -90,28 +141,22 @@ def _run_task(self):
90141

91142
# remove temp dir
92143
shutil.rmtree(temp_dir)
144+
merged = {}
145+
if components:
146+
for key in set(components).union(mdapi_components):
147+
merged[key] = list(
148+
set(chain(components.get(key, []), mdapi_components.get(key, [])))
149+
)
150+
components = merged
151+
else:
152+
components = mdapi_components
93153

94-
if not components:
95-
self.logger.info(f"No components found in deploy path{path}")
96-
raise TaskOptionsError("No plan or paths options provided")
97-
98-
self.logger.debug("Components detected at source")
99-
for component_type, component_names in components.items():
100-
self.logger.debug(f"{component_type}: {', '.join(component_names)}")
101-
# check common components
102-
existing_components = process_common_components(
103-
api_retrieve_unpackaged_response, components
104-
)
105-
106-
if existing_components:
107-
self.logger.info("Components exists in the target org:")
108-
for component_type, component_names in existing_components.items():
109-
self.logger.info(f"{component_type}: {', '.join(component_names)}")
110-
self.return_values["existing_components"] = existing_components
154+
if api_retrieve_unpackaged_response:
155+
api_retrieve_unpackaged_response.extend(mdapi_response_messages)
111156
else:
112-
self.logger.info(
113-
"No components from the deploy paths exist in the target org."
114-
)
157+
api_retrieve_unpackaged_response = mdapi_response_messages
158+
159+
return [components, api_retrieve_unpackaged_response]
115160

116161
def _copy_to_tempdir(self, src_dir, temp_dir):
117162
for item in os.listdir(src_dir):

0 commit comments

Comments
 (0)