Skip to content
Merged

latest #3890

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
877a277
Instead of taking the hard coded api version, take the api version fr…
lakshmi2506 Feb 7, 2025
1ec8347
Merge pull request #3875 from SFDO-Tooling/fix_check_settings_task
jkasturi-sf Feb 7, 2025
e16a784
Update changelog (automated)
github-actions[bot] Feb 7, 2025
0cefe4f
Lint fix
lakshmi2506 Feb 7, 2025
31e7afb
Merge pull request #3876 from SFDO-Tooling/release-4.3.0
lakshmi2506 Feb 10, 2025
3081417
Fix for issue where 0 records in sql causes error during select
aditya-balachander Feb 10, 2025
4bab655
@W-17717398: Fix for Error When Local Records Are Empty During SELECT…
lakshmi2506 Feb 11, 2025
915c36a
Changed Check Components to add a get repo components function
jain-naman-sf Feb 18, 2025
df58663
Merge pull request #3881 from SFDO-Tooling/Check_Components_Enhancements
jain-naman-sf Feb 18, 2025
a863843
Fixed Bug
jain-naman-sf Feb 18, 2025
6ecc206
Removed Debug Statements
jain-naman-sf Feb 18, 2025
15ff51e
Fixed Merging Issue
jain-naman-sf Feb 18, 2025
9918156
Added Test for Get Repo Function
jain-naman-sf Feb 18, 2025
227086e
Fixed Corner Scenario
jain-naman-sf Feb 19, 2025
5a8e936
Fixed Corner Scenario
jain-naman-sf Feb 19, 2025
dd39d53
Merge pull request #3882 from SFDO-Tooling/Bugs_Check_Components
jain-naman-sf Feb 19, 2025
0d06985
Update changelog (automated)
github-actions[bot] Feb 19, 2025
b2ea546
Fixed Lint Issues
jain-naman-sf Feb 19, 2025
b45b2d7
Merge pull request #3883 from SFDO-Tooling/release-4.3.0.dev0
jain-naman-sf Feb 19, 2025
a0494b8
fix: Make get permset licenses return active only (#3888)
jstvz Mar 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cumulusci/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "4.2.0"
__version__ = "4.3.0.dev0"
23 changes: 23 additions & 0 deletions cumulusci/tasks/bulkdata/step.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,18 @@ def select_records(self, records):
records, records_copy = tee(records)
# Count total number of records to fetch using the copy
total_num_records = sum(1 for _ in records_copy)

# In the case that records are zero, return success
if total_num_records == 0:
self.logger.info(f"No records present for {self.sobject}")
self.job_result = DataOperationJobResult(
status=DataOperationStatus.SUCCESS,
job_errors=[],
records_processed=0,
total_row_errors=0,
)
return

limit_clause = self._determine_limit_clause(total_num_records=total_num_records)

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

# In the case that records are zero, return success
self.logger.info(f"No records present for {self.sobject}")
if total_num_records == 0:
self.job_result = DataOperationJobResult(
status=DataOperationStatus.SUCCESS,
job_errors=[],
records_processed=0,
total_row_errors=0,
)
return

# Set LIMIT condition
limit_clause = self._determine_limit_clause(total_num_records)

Expand Down
80 changes: 80 additions & 0 deletions cumulusci/tasks/bulkdata/tests/test_step.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,47 @@ def test_select_records_standard_strategy_success(self, download_mock):
== 3
)

@mock.patch("cumulusci.tasks.bulkdata.step.download_file")
def test_select_records_zero_load_records(self, download_mock):
# Set up mock context and BulkApiDmlOperation
context = mock.Mock()
step = BulkApiDmlOperation(
sobject="Contact",
operation=DataOperationType.QUERY,
api_options={"batch_size": 10, "update_key": "LastName"},
context=context,
fields=["LastName"],
selection_strategy=SelectStrategy.STANDARD,
content_type="JSON",
)

# Mock Bulk API responses
step.bulk.endpoint = "https://test"
step.bulk.create_query_job.return_value = "JOB"
step.bulk.query.return_value = "BATCH"
step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]

# Mock the downloaded CSV content with a single record
download_mock.return_value = io.StringIO('[{"Id":"003000000000001"}]')

# Mock the _wait_for_job method to simulate a successful job
step._wait_for_job = mock.Mock()
step._wait_for_job.return_value = DataOperationJobResult(
DataOperationStatus.SUCCESS, [], 0, 0
)

# Prepare input records
records = iter([])

# Execute the select_records operation
step.start()
step.select_records(records)
step.end()

# Get the results and assert their properties
results = list(step.get_results())
assert len(results) == 0 # Expect 0 results (no records to process)

@mock.patch("cumulusci.tasks.bulkdata.step.download_file")
def test_select_records_standard_strategy_failure__no_records(self, download_mock):
# Set up mock context and BulkApiDmlOperation
Expand Down Expand Up @@ -1927,6 +1968,45 @@ def test_select_records_standard_strategy_success(self):
== 3
)

@responses.activate
def test_select_records_zero_load_records(self):
mock_describe_calls()
task = _make_task(
LoadData,
{
"options": {
"database_url": "sqlite:///test.db",
"mapping": "mapping.yml",
}
},
)
task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
task._init_task()

step = RestApiDmlOperation(
sobject="Contact",
operation=DataOperationType.UPSERT,
api_options={"batch_size": 10, "update_key": "LastName"},
context=task,
fields=["LastName"],
selection_strategy=SelectStrategy.STANDARD,
)

results = {
"records": [],
"done": True,
}
step.sf.restful = mock.Mock()
step.sf.restful.return_value = results
records = iter([])
step.start()
step.select_records(records)
step.end()

# Get the results and assert their properties
results = list(step.get_results())
assert len(results) == 0 # Expect 0 results (matching the input records count)

@responses.activate
def test_select_records_standard_strategy_success_pagination(self):
mock_describe_calls()
Expand Down
8 changes: 6 additions & 2 deletions cumulusci/tasks/metadata/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,19 @@ def process_common_components(response_messages: List, components: Dict):
"""Compare compoents in the api responce object with list of components and return common common components"""
if not response_messages or not components:
return components

for message in response_messages:

message_list = message.firstChild.nextSibling.firstChild.nodeValue.split("'")
if len(message_list) > 1:
component_type = message_list[1]
message_txt = message_list[2]

if "is not available in this organization" in message_txt:
del components[component_type]
elif "is unknown" in message_txt:
component_type = message_list[0].split(" ")
components[component_type[0]].remove(message_list[1])
if len(components[component_type]) == 0:
del components[component_type]
else:
component_name = message_list[3]
if component_name in components[component_type]:
Expand Down
5 changes: 2 additions & 3 deletions cumulusci/tasks/preflight/licenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ def _run_task(self):

class GetAvailablePermissionSetLicenses(BaseSalesforceApiTask):
def _run_task(self):
query = "SELECT PermissionSetLicenseKey FROM PermissionSetLicense WHERE Status = 'Active'"
self.return_values = [
result["PermissionSetLicenseKey"]
for result in self.sf.query(
"SELECT PermissionSetLicenseKey FROM PermissionSetLicense"
)["records"]
for result in self.sf.query(query)["records"]
]
licenses = "\n".join(self.return_values)
self.logger.info(f"Found permission set licenses:\n{licenses}")
Expand Down
2 changes: 1 addition & 1 deletion cumulusci/tasks/preflight/sobjects.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ class CheckSObjectsAvailable(BaseSalesforceApiTask):
action: error
message: "Enhanced Notes are not turned on."
"""
api_version = "48.0"

def _run_task(self):

self.return_values = {entry["name"] for entry in self.sf.describe()["sobjects"]}

self.logger.info(
Expand Down
2 changes: 1 addition & 1 deletion cumulusci/tasks/preflight/tests/test_licenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def test_psl_preflight(self):
task()

task._init_api.return_value.query.assert_called_once_with(
"SELECT PermissionSetLicenseKey FROM PermissionSetLicense"
"SELECT PermissionSetLicenseKey FROM PermissionSetLicense WHERE Status = 'Active'"
)
assert task.return_values == ["TEST1", "TEST2"]

Expand Down
89 changes: 67 additions & 22 deletions cumulusci/tasks/salesforce/check_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import shutil
import tempfile
from collections import defaultdict
from itertools import chain
from xml.etree.ElementTree import ParseError

from defusedxml.minidom import parseString
Expand Down Expand Up @@ -47,6 +48,35 @@ def _run_task(self):
paths = self.options.get("paths")
plan_or_flow_name = self.options.get("name")

(
components,
api_retrieve_unpackaged_response,
) = self.get_repo_existing_components(plan_or_flow_name, paths)

if not components:
self.logger.info("No components found in deploy path")
raise TaskOptionsError("No plan or paths options provided")

self.logger.debug("Components detected at source")
for component_type, component_names in components.items():
self.logger.debug(f"{component_type}: {', '.join(component_names)}")
# check common components
components.pop("Settings", None)
existing_components = process_common_components(
api_retrieve_unpackaged_response, components
)

if existing_components:
self.logger.info("Components exists in the target org:")
for component_type, component_names in existing_components.items():
self.logger.info(f"{component_type}: {', '.join(component_names)}")
self.return_values["existing_components"] = existing_components
else:
self.logger.info(
"No components from the deploy paths exist in the target org."
)

def get_repo_existing_components(self, plan_or_flow_name, paths=""):
if paths:
paths = process_list_arg(paths)
self.logger.info(f"Using provided paths: {paths}")
Expand All @@ -71,16 +101,37 @@ def _run_task(self):
self.logger.debug(
f"deploy paths found in the plan or flow.{self.deploy_paths}"
)

# Temp dir to copy all deploy paths from task options
temp_dir = tempfile.mkdtemp()
self.logger.info(f"Temporary deploy directory created: {temp_dir}")

mdapi_components = {}
mdapi_response_messages = []
for path in self.deploy_paths:
full_path = os.path.join(self.project_config.repo_root, path)
if not os.path.exists(full_path):
self.logger.info(f"Skipping path: '{path}' - path doesn't exist")
continue
elif "package.xml" in os.listdir(full_path):
package_xml_path = os.path.join(full_path, "package.xml")
source_xml_tree = metadata_tree.parse(package_xml_path)
components = metadata_tree.parse_package_xml_types(
"name", source_xml_tree
)
response_messages = self._get_api_object_responce(
package_xml_path, source_xml_tree.version.text
)
merged = {}
for key in set(components).union(mdapi_components):
merged[key] = list(
set(
chain(
components.get(key, []), mdapi_components.get(key, [])
)
)
)
mdapi_components = merged
mdapi_response_messages.extend(response_messages)
continue
self._copy_to_tempdir(path, temp_dir)

(
Expand All @@ -90,28 +141,22 @@ def _run_task(self):

# remove temp dir
shutil.rmtree(temp_dir)
merged = {}
if components:
for key in set(components).union(mdapi_components):
merged[key] = list(
set(chain(components.get(key, []), mdapi_components.get(key, [])))
)
components = merged
else:
components = mdapi_components

if not components:
self.logger.info(f"No components found in deploy path{path}")
raise TaskOptionsError("No plan or paths options provided")

self.logger.debug("Components detected at source")
for component_type, component_names in components.items():
self.logger.debug(f"{component_type}: {', '.join(component_names)}")
# check common components
existing_components = process_common_components(
api_retrieve_unpackaged_response, components
)

if existing_components:
self.logger.info("Components exists in the target org:")
for component_type, component_names in existing_components.items():
self.logger.info(f"{component_type}: {', '.join(component_names)}")
self.return_values["existing_components"] = existing_components
if api_retrieve_unpackaged_response:
api_retrieve_unpackaged_response.extend(mdapi_response_messages)
else:
self.logger.info(
"No components from the deploy paths exist in the target org."
)
api_retrieve_unpackaged_response = mdapi_response_messages

return [components, api_retrieve_unpackaged_response]

def _copy_to_tempdir(self, src_dir, temp_dir):
for item in os.listdir(src_dir):
Expand Down
Loading
Loading