cumulusci-plus 5.0.21__py3-none-any.whl → 5.0.43__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cumulusci/__about__.py +1 -1
- cumulusci/cli/logger.py +2 -2
- cumulusci/cli/service.py +20 -0
- cumulusci/cli/task.py +19 -3
- cumulusci/cli/tests/test_error.py +3 -1
- cumulusci/cli/tests/test_flow.py +279 -2
- cumulusci/cli/tests/test_org.py +5 -0
- cumulusci/cli/tests/test_service.py +15 -12
- cumulusci/cli/tests/test_task.py +122 -2
- cumulusci/cli/tests/utils.py +1 -4
- cumulusci/core/config/__init__.py +1 -0
- cumulusci/core/config/base_task_flow_config.py +26 -1
- cumulusci/core/config/org_config.py +2 -1
- cumulusci/core/config/project_config.py +14 -20
- cumulusci/core/config/scratch_org_config.py +12 -0
- cumulusci/core/config/tests/test_config.py +1 -0
- cumulusci/core/config/tests/test_config_expensive.py +9 -3
- cumulusci/core/config/universal_config.py +3 -4
- cumulusci/core/dependencies/base.py +5 -1
- cumulusci/core/dependencies/dependencies.py +1 -1
- cumulusci/core/dependencies/github.py +1 -2
- cumulusci/core/dependencies/resolvers.py +1 -1
- cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
- cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
- cumulusci/core/flowrunner.py +90 -6
- cumulusci/core/github.py +1 -1
- cumulusci/core/sfdx.py +3 -1
- cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
- cumulusci/core/source_transforms/transforms.py +1 -1
- cumulusci/core/tasks.py +13 -2
- cumulusci/core/tests/test_flowrunner.py +100 -0
- cumulusci/core/tests/test_tasks.py +65 -0
- cumulusci/core/utils.py +3 -1
- cumulusci/core/versions.py +1 -1
- cumulusci/cumulusci.yml +73 -1
- cumulusci/oauth/client.py +1 -1
- cumulusci/plugins/plugin_base.py +5 -3
- cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
- cumulusci/salesforce_api/rest_deploy.py +1 -1
- cumulusci/schema/cumulusci.jsonschema.json +69 -0
- cumulusci/tasks/apex/anon.py +1 -1
- cumulusci/tasks/apex/testrunner.py +421 -144
- cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
- cumulusci/tasks/bulkdata/extract.py +0 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
- cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
- cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
- cumulusci/tasks/bulkdata/select_utils.py +1 -1
- cumulusci/tasks/bulkdata/snowfakery.py +100 -25
- cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
- cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
- cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
- cumulusci/tasks/bulkdata/tests/test_select_utils.py +46 -0
- cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
- cumulusci/tasks/create_package_version.py +190 -16
- cumulusci/tasks/datadictionary.py +1 -1
- cumulusci/tasks/metadata_etl/__init__.py +2 -0
- cumulusci/tasks/metadata_etl/applications.py +256 -0
- cumulusci/tasks/metadata_etl/base.py +7 -3
- cumulusci/tasks/metadata_etl/layouts.py +1 -1
- cumulusci/tasks/metadata_etl/permissions.py +1 -1
- cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
- cumulusci/tasks/metadata_etl/tests/test_applications.py +710 -0
- cumulusci/tasks/push/README.md +15 -17
- cumulusci/tasks/release_notes/README.md +13 -13
- cumulusci/tasks/release_notes/generator.py +13 -8
- cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
- cumulusci/tasks/salesforce/Deploy.py +53 -2
- cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
- cumulusci/tasks/salesforce/__init__.py +1 -0
- cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
- cumulusci/tasks/salesforce/composite.py +1 -1
- cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
- cumulusci/tasks/salesforce/enable_prediction.py +5 -1
- cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
- cumulusci/tasks/salesforce/insert_record.py +18 -19
- cumulusci/tasks/salesforce/sourcetracking.py +1 -1
- cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
- cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
- cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
- cumulusci/tasks/salesforce/tests/test_enable_prediction.py +4 -2
- cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
- cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
- cumulusci/tasks/salesforce/tests/test_update_external_auth_identity_provider.py +927 -0
- cumulusci/tasks/salesforce/tests/test_update_external_credential.py +1427 -0
- cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
- cumulusci/tasks/salesforce/tests/test_update_record.py +512 -0
- cumulusci/tasks/salesforce/update_dependencies.py +2 -2
- cumulusci/tasks/salesforce/update_external_auth_identity_provider.py +551 -0
- cumulusci/tasks/salesforce/update_external_credential.py +647 -0
- cumulusci/tasks/salesforce/update_named_credential.py +441 -0
- cumulusci/tasks/salesforce/update_profile.py +17 -13
- cumulusci/tasks/salesforce/update_record.py +217 -0
- cumulusci/tasks/salesforce/users/permsets.py +62 -5
- cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
- cumulusci/tasks/sfdmu/__init__.py +0 -0
- cumulusci/tasks/sfdmu/sfdmu.py +376 -0
- cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
- cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
- cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
- cumulusci/tasks/tests/test_create_package_version.py +716 -1
- cumulusci/tasks/tests/test_util.py +42 -0
- cumulusci/tasks/util.py +37 -1
- cumulusci/tasks/utility/copyContents.py +402 -0
- cumulusci/tasks/utility/credentialManager.py +302 -0
- cumulusci/tasks/utility/directoryRecreator.py +30 -0
- cumulusci/tasks/utility/env_management.py +1 -1
- cumulusci/tasks/utility/secretsToEnv.py +135 -0
- cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
- cumulusci/tasks/utility/tests/test_credentialManager.py +1150 -0
- cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
- cumulusci/tasks/utility/tests/test_secretsToEnv.py +1118 -0
- cumulusci/tests/test_integration_infrastructure.py +3 -1
- cumulusci/tests/test_utils.py +70 -6
- cumulusci/utils/__init__.py +54 -9
- cumulusci/utils/classutils.py +5 -2
- cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
- cumulusci/utils/options.py +23 -1
- cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
- cumulusci/utils/yaml/cumulusci_yml.py +8 -3
- cumulusci/utils/yaml/model_parser.py +2 -2
- cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
- cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
- cumulusci/vcs/base.py +23 -15
- cumulusci/vcs/bootstrap.py +5 -4
- cumulusci/vcs/utils/list_modified_files.py +189 -0
- cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/METADATA +11 -10
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/RECORD +135 -104
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/WHEEL +1 -1
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/LICENSE +0 -0
|
@@ -813,7 +813,9 @@ class TestRunApexTests(MockLoggerMixin):
|
|
|
813
813
|
task._init_options(task_config.config["options"])
|
|
814
814
|
|
|
815
815
|
assert (
|
|
816
|
-
task.
|
|
816
|
+
task.parsed_options.retry_failures[0].search(
|
|
817
|
+
"UNABLE_TO_LOCK_ROW: test failed"
|
|
818
|
+
)
|
|
817
819
|
is not None
|
|
818
820
|
)
|
|
819
821
|
|
|
@@ -971,6 +973,920 @@ class TestRunApexTests(MockLoggerMixin):
|
|
|
971
973
|
task()
|
|
972
974
|
assert task.result is None
|
|
973
975
|
|
|
976
|
+
def test_package_only_filter__filters_classes_not_in_package(self):
|
|
977
|
+
"""Test that dynamic_filter='package_only' filters out classes not in the package directory"""
|
|
978
|
+
from unittest.mock import PropertyMock
|
|
979
|
+
|
|
980
|
+
# Create temporary directory structure with some test class files
|
|
981
|
+
tmpdir = tempfile.mkdtemp()
|
|
982
|
+
try:
|
|
983
|
+
# Create a mock force-app directory structure
|
|
984
|
+
classes_dir = os.path.join(tmpdir, "main", "default", "classes")
|
|
985
|
+
os.makedirs(classes_dir)
|
|
986
|
+
|
|
987
|
+
# Create two test class files
|
|
988
|
+
with open(os.path.join(classes_dir, "TestClass1.cls"), "w") as f:
|
|
989
|
+
f.write("public class TestClass1 {}")
|
|
990
|
+
with open(os.path.join(classes_dir, "TestClass2.cls"), "w") as f:
|
|
991
|
+
f.write("public class TestClass2 {}")
|
|
992
|
+
|
|
993
|
+
# Setup task with dynamic_filter='package_only' option
|
|
994
|
+
task_config = TaskConfig(
|
|
995
|
+
{
|
|
996
|
+
"options": {
|
|
997
|
+
"test_name_match": "%_TEST",
|
|
998
|
+
"dynamic_filter": "package_only",
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
)
|
|
1002
|
+
task = RunApexTests(self.project_config, task_config, self.org_config)
|
|
1003
|
+
|
|
1004
|
+
# Mock the default_package_path property to point to our temp directory
|
|
1005
|
+
with patch.object(
|
|
1006
|
+
type(task.project_config),
|
|
1007
|
+
"default_package_path",
|
|
1008
|
+
new_callable=PropertyMock,
|
|
1009
|
+
) as mock_path:
|
|
1010
|
+
mock_path.return_value = tmpdir
|
|
1011
|
+
|
|
1012
|
+
# Create mock test classes result with 3 classes, but only 2 exist in package
|
|
1013
|
+
mock_classes = {
|
|
1014
|
+
"totalSize": 3,
|
|
1015
|
+
"done": True,
|
|
1016
|
+
"records": [
|
|
1017
|
+
{"Id": "01p000001", "Name": "TestClass1"},
|
|
1018
|
+
{"Id": "01p000002", "Name": "TestClass2"},
|
|
1019
|
+
{"Id": "01p000003", "Name": "TestClass3"}, # Not in package
|
|
1020
|
+
],
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
# Test the filter method
|
|
1024
|
+
filtered = task._filter_package_classes(mock_classes)
|
|
1025
|
+
|
|
1026
|
+
# Verify only 2 classes are returned
|
|
1027
|
+
assert filtered["totalSize"] == 2
|
|
1028
|
+
assert len(filtered["records"]) == 2
|
|
1029
|
+
|
|
1030
|
+
# Verify the correct classes were kept
|
|
1031
|
+
filtered_names = [record["Name"] for record in filtered["records"]]
|
|
1032
|
+
assert "TestClass1" in filtered_names
|
|
1033
|
+
assert "TestClass2" in filtered_names
|
|
1034
|
+
assert "TestClass3" not in filtered_names
|
|
1035
|
+
finally:
|
|
1036
|
+
shutil.rmtree(tmpdir)
|
|
1037
|
+
|
|
1038
|
+
def test_package_only_filter__disabled_returns_all_classes(self):
|
|
1039
|
+
"""Test that when dynamic_filter is not set to 'package_only', all classes are returned"""
|
|
1040
|
+
task_config = TaskConfig({"options": {"test_name_match": "%_TEST"}})
|
|
1041
|
+
task = RunApexTests(self.project_config, task_config, self.org_config)
|
|
1042
|
+
|
|
1043
|
+
mock_classes = {
|
|
1044
|
+
"totalSize": 3,
|
|
1045
|
+
"done": True,
|
|
1046
|
+
"records": [
|
|
1047
|
+
{"Id": "01p000001", "Name": "TestClass1"},
|
|
1048
|
+
{"Id": "01p000002", "Name": "TestClass2"},
|
|
1049
|
+
{"Id": "01p000003", "Name": "TestClass3"},
|
|
1050
|
+
],
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
# Test the filter method - should return all classes unchanged
|
|
1054
|
+
filtered = task._filter_package_classes(mock_classes)
|
|
1055
|
+
|
|
1056
|
+
assert filtered["totalSize"] == 3
|
|
1057
|
+
assert len(filtered["records"]) == 3
|
|
1058
|
+
|
|
1059
|
+
def test_class_exists_in_package__finds_class(self):
|
|
1060
|
+
"""Test that _class_exists_in_package correctly finds classes in subdirectories"""
|
|
1061
|
+
from unittest.mock import PropertyMock
|
|
1062
|
+
|
|
1063
|
+
tmpdir = tempfile.mkdtemp()
|
|
1064
|
+
try:
|
|
1065
|
+
# Create nested directory structure
|
|
1066
|
+
classes_dir = os.path.join(tmpdir, "main", "default", "classes")
|
|
1067
|
+
os.makedirs(classes_dir)
|
|
1068
|
+
|
|
1069
|
+
# Create a test class file
|
|
1070
|
+
with open(os.path.join(classes_dir, "MyTestClass.cls"), "w") as f:
|
|
1071
|
+
f.write("public class MyTestClass {}")
|
|
1072
|
+
|
|
1073
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1074
|
+
|
|
1075
|
+
with patch.object(
|
|
1076
|
+
type(task.project_config),
|
|
1077
|
+
"default_package_path",
|
|
1078
|
+
new_callable=PropertyMock,
|
|
1079
|
+
) as mock_path:
|
|
1080
|
+
mock_path.return_value = tmpdir
|
|
1081
|
+
|
|
1082
|
+
# Should find the class
|
|
1083
|
+
assert task._class_exists_in_package("MyTestClass") is True
|
|
1084
|
+
# Should not find non-existent class
|
|
1085
|
+
assert task._class_exists_in_package("NonExistentClass") is False
|
|
1086
|
+
finally:
|
|
1087
|
+
shutil.rmtree(tmpdir)
|
|
1088
|
+
|
|
1089
|
+
def test_individual_class_coverage__valid_dict(self):
|
|
1090
|
+
"""Test that individual class coverage requirements are parsed correctly"""
|
|
1091
|
+
task_config = TaskConfig(
|
|
1092
|
+
{
|
|
1093
|
+
"options": {
|
|
1094
|
+
"test_name_match": "%_TEST",
|
|
1095
|
+
"required_individual_class_code_coverage_percent": {
|
|
1096
|
+
"TestClass1": 75,
|
|
1097
|
+
"TestClass2": "80",
|
|
1098
|
+
},
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
)
|
|
1102
|
+
task = RunApexTests(self.project_config, task_config, self.org_config)
|
|
1103
|
+
|
|
1104
|
+
assert task.required_individual_class_code_coverage_percent == {
|
|
1105
|
+
"TestClass1": 75,
|
|
1106
|
+
"TestClass2": 80,
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
def test_individual_class_coverage__invalid_not_dict(self):
|
|
1110
|
+
"""Test that non-dictionary values raise TaskOptionsError"""
|
|
1111
|
+
task_config = TaskConfig(
|
|
1112
|
+
{
|
|
1113
|
+
"options": {
|
|
1114
|
+
"test_name_match": "%_TEST",
|
|
1115
|
+
"required_individual_class_code_coverage_percent": "not a dict",
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
)
|
|
1119
|
+
with pytest.raises(TaskOptionsError) as e:
|
|
1120
|
+
RunApexTests(self.project_config, task_config, self.org_config)
|
|
1121
|
+
assert "Var is not a name/value pair: not a dict" in str(e.value)
|
|
1122
|
+
|
|
1123
|
+
def test_individual_class_coverage__invalid_value(self):
|
|
1124
|
+
"""Test that invalid values in dictionary raise TaskOptionsError"""
|
|
1125
|
+
task_config = TaskConfig(
|
|
1126
|
+
{
|
|
1127
|
+
"options": {
|
|
1128
|
+
"test_name_match": "%_TEST",
|
|
1129
|
+
"required_individual_class_code_coverage_percent": {
|
|
1130
|
+
"TestClass1": "not a number",
|
|
1131
|
+
},
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
)
|
|
1135
|
+
with pytest.raises(TaskOptionsError) as e:
|
|
1136
|
+
RunApexTests(self.project_config, task_config, self.org_config)
|
|
1137
|
+
assert "value is not a valid integer" in str(e.value)
|
|
1138
|
+
|
|
1139
|
+
def test_check_code_coverage__individual_takes_priority(self):
|
|
1140
|
+
"""Test that individual class requirements override global per-class requirements"""
|
|
1141
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1142
|
+
task.required_per_class_code_coverage_percent = 50
|
|
1143
|
+
task.required_individual_class_code_coverage_percent = {
|
|
1144
|
+
"TestClass1": 75, # Higher than global
|
|
1145
|
+
"TestClass2": 30, # Lower than global
|
|
1146
|
+
}
|
|
1147
|
+
task.tooling = Mock()
|
|
1148
|
+
|
|
1149
|
+
# Mock class coverage: TestClass1 has 60% (fails individual req of 75%)
|
|
1150
|
+
# TestClass2 has 40% (passes individual req of 30%)
|
|
1151
|
+
# TestClass3 has 45% (fails global req of 50%)
|
|
1152
|
+
task.tooling.query.side_effect = [
|
|
1153
|
+
{
|
|
1154
|
+
"records": [
|
|
1155
|
+
{
|
|
1156
|
+
"ApexClassOrTrigger": {"Name": "TestClass1"},
|
|
1157
|
+
"NumLinesCovered": 60,
|
|
1158
|
+
"NumLinesUncovered": 40,
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
"ApexClassOrTrigger": {"Name": "TestClass2"},
|
|
1162
|
+
"NumLinesCovered": 40,
|
|
1163
|
+
"NumLinesUncovered": 60,
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
"ApexClassOrTrigger": {"Name": "TestClass3"},
|
|
1167
|
+
"NumLinesCovered": 45,
|
|
1168
|
+
"NumLinesUncovered": 55,
|
|
1169
|
+
},
|
|
1170
|
+
],
|
|
1171
|
+
},
|
|
1172
|
+
{"records": [{"PercentCovered": 90}]}, # Org-wide coverage
|
|
1173
|
+
]
|
|
1174
|
+
|
|
1175
|
+
with pytest.raises(ApexTestException) as e:
|
|
1176
|
+
task._check_code_coverage()
|
|
1177
|
+
|
|
1178
|
+
error_message = str(e.value)
|
|
1179
|
+
# TestClass1 should fail with 60% vs required 75% (individual)
|
|
1180
|
+
assert "TestClass1" in error_message
|
|
1181
|
+
assert "60.0%" in error_message
|
|
1182
|
+
assert "75%" in error_message
|
|
1183
|
+
|
|
1184
|
+
# TestClass2 should pass (40% >= 30% individual requirement)
|
|
1185
|
+
assert "TestClass2" not in error_message
|
|
1186
|
+
|
|
1187
|
+
# TestClass3 should fail with 45% vs required 50% (global)
|
|
1188
|
+
assert "TestClass3" in error_message
|
|
1189
|
+
assert "45.0%" in error_message
|
|
1190
|
+
assert "50%" in error_message
|
|
1191
|
+
|
|
1192
|
+
def test_check_code_coverage__fallback_to_global(self):
|
|
1193
|
+
"""Test that global per-class requirement is used when no individual requirement exists"""
|
|
1194
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1195
|
+
task.required_per_class_code_coverage_percent = 60
|
|
1196
|
+
task.required_individual_class_code_coverage_percent = {}
|
|
1197
|
+
task.tooling = Mock()
|
|
1198
|
+
|
|
1199
|
+
# TestClass1 has 55% coverage - should fail global requirement of 60%
|
|
1200
|
+
task.tooling.query.side_effect = [
|
|
1201
|
+
{
|
|
1202
|
+
"records": [
|
|
1203
|
+
{
|
|
1204
|
+
"ApexClassOrTrigger": {"Name": "TestClass1"},
|
|
1205
|
+
"NumLinesCovered": 55,
|
|
1206
|
+
"NumLinesUncovered": 45,
|
|
1207
|
+
},
|
|
1208
|
+
],
|
|
1209
|
+
},
|
|
1210
|
+
{"records": [{"PercentCovered": 90}]},
|
|
1211
|
+
]
|
|
1212
|
+
|
|
1213
|
+
with pytest.raises(ApexTestException) as e:
|
|
1214
|
+
task._check_code_coverage()
|
|
1215
|
+
|
|
1216
|
+
error_message = str(e.value)
|
|
1217
|
+
assert "TestClass1" in error_message
|
|
1218
|
+
assert "55.0%" in error_message
|
|
1219
|
+
assert "60%" in error_message
|
|
1220
|
+
|
|
1221
|
+
def test_check_code_coverage__no_requirement_no_check(self):
|
|
1222
|
+
"""Test that classes without requirements are not checked"""
|
|
1223
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1224
|
+
task.required_per_class_code_coverage_percent = 0 # No global requirement
|
|
1225
|
+
task.required_individual_class_code_coverage_percent = {
|
|
1226
|
+
"TestClass1": 75 # Only TestClass1 has requirement
|
|
1227
|
+
}
|
|
1228
|
+
task.tooling = Mock()
|
|
1229
|
+
|
|
1230
|
+
# TestClass1 has 80% (passes), TestClass2 has 10% (no requirement, should not fail)
|
|
1231
|
+
task.tooling.query.side_effect = [
|
|
1232
|
+
{
|
|
1233
|
+
"records": [
|
|
1234
|
+
{
|
|
1235
|
+
"ApexClassOrTrigger": {"Name": "TestClass1"},
|
|
1236
|
+
"NumLinesCovered": 80,
|
|
1237
|
+
"NumLinesUncovered": 20,
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
"ApexClassOrTrigger": {"Name": "TestClass2"},
|
|
1241
|
+
"NumLinesCovered": 10,
|
|
1242
|
+
"NumLinesUncovered": 90,
|
|
1243
|
+
},
|
|
1244
|
+
],
|
|
1245
|
+
},
|
|
1246
|
+
{"records": [{"PercentCovered": 90}]},
|
|
1247
|
+
]
|
|
1248
|
+
|
|
1249
|
+
# Should not raise an exception because TestClass2 has no requirement
|
|
1250
|
+
task._check_code_coverage()
|
|
1251
|
+
|
|
1252
|
+
def test_check_code_coverage__individual_only_success_message(self):
|
|
1253
|
+
"""Test success message when only individual requirements are set"""
|
|
1254
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1255
|
+
task.required_per_class_code_coverage_percent = 0
|
|
1256
|
+
task.required_individual_class_code_coverage_percent = {"TestClass1": 75}
|
|
1257
|
+
task.tooling = Mock()
|
|
1258
|
+
|
|
1259
|
+
task.tooling.query.side_effect = [
|
|
1260
|
+
{
|
|
1261
|
+
"records": [
|
|
1262
|
+
{
|
|
1263
|
+
"ApexClassOrTrigger": {"Name": "TestClass1"},
|
|
1264
|
+
"NumLinesCovered": 80,
|
|
1265
|
+
"NumLinesUncovered": 20,
|
|
1266
|
+
},
|
|
1267
|
+
],
|
|
1268
|
+
},
|
|
1269
|
+
{"records": [{"PercentCovered": 90}]},
|
|
1270
|
+
]
|
|
1271
|
+
|
|
1272
|
+
task._check_code_coverage()
|
|
1273
|
+
|
|
1274
|
+
# Check that appropriate success message was logged
|
|
1275
|
+
log = self._task_log_handler.messages
|
|
1276
|
+
assert any("individual coverage requirements" in msg for msg in log["info"])
|
|
1277
|
+
|
|
1278
|
+
def test_check_code_coverage__both_requirements_success_message(self):
|
|
1279
|
+
"""Test success message when both global and individual requirements are set"""
|
|
1280
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1281
|
+
task.required_per_class_code_coverage_percent = 50
|
|
1282
|
+
task.required_individual_class_code_coverage_percent = {"TestClass1": 75}
|
|
1283
|
+
task.tooling = Mock()
|
|
1284
|
+
|
|
1285
|
+
task.tooling.query.side_effect = [
|
|
1286
|
+
{
|
|
1287
|
+
"records": [
|
|
1288
|
+
{
|
|
1289
|
+
"ApexClassOrTrigger": {"Name": "TestClass1"},
|
|
1290
|
+
"NumLinesCovered": 80,
|
|
1291
|
+
"NumLinesUncovered": 20,
|
|
1292
|
+
},
|
|
1293
|
+
{
|
|
1294
|
+
"ApexClassOrTrigger": {"Name": "TestClass2"},
|
|
1295
|
+
"NumLinesCovered": 60,
|
|
1296
|
+
"NumLinesUncovered": 40,
|
|
1297
|
+
},
|
|
1298
|
+
],
|
|
1299
|
+
},
|
|
1300
|
+
{"records": [{"PercentCovered": 90}]},
|
|
1301
|
+
]
|
|
1302
|
+
|
|
1303
|
+
task._check_code_coverage()
|
|
1304
|
+
|
|
1305
|
+
# Check that appropriate success message was logged
|
|
1306
|
+
log = self._task_log_handler.messages
|
|
1307
|
+
assert any("global: 50%" in msg and "individual" in msg for msg in log["info"])
|
|
1308
|
+
|
|
1309
|
+
def test_check_code_coverage__nonexistent_class_in_individual_requirements(self):
|
|
1310
|
+
"""Test that specifying a non-existent class in individual requirements doesn't cause errors"""
|
|
1311
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1312
|
+
task.required_per_class_code_coverage_percent = 60
|
|
1313
|
+
task.required_individual_class_code_coverage_percent = {
|
|
1314
|
+
"ExistingClass_TEST": 80,
|
|
1315
|
+
"NonExistentClass_TEST": 90, # This class doesn't exist in the org
|
|
1316
|
+
"AnotherMissingClass_TEST": 95, # This also doesn't exist
|
|
1317
|
+
}
|
|
1318
|
+
task.tooling = Mock()
|
|
1319
|
+
|
|
1320
|
+
# Mock query returns only ExistingClass_TEST (the other two don't exist)
|
|
1321
|
+
task.tooling.query.side_effect = [
|
|
1322
|
+
{
|
|
1323
|
+
"records": [
|
|
1324
|
+
{
|
|
1325
|
+
"ApexClassOrTrigger": {"Name": "ExistingClass_TEST"},
|
|
1326
|
+
"NumLinesCovered": 85,
|
|
1327
|
+
"NumLinesUncovered": 15,
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
"ApexClassOrTrigger": {"Name": "OtherClass_TEST"},
|
|
1331
|
+
"NumLinesCovered": 70,
|
|
1332
|
+
"NumLinesUncovered": 30,
|
|
1333
|
+
},
|
|
1334
|
+
],
|
|
1335
|
+
},
|
|
1336
|
+
{"records": [{"PercentCovered": 90}]},
|
|
1337
|
+
]
|
|
1338
|
+
|
|
1339
|
+
# Should not raise any exception even though NonExistentClass_TEST and
|
|
1340
|
+
# AnotherMissingClass_TEST are in individual requirements but not in the org
|
|
1341
|
+
task._check_code_coverage()
|
|
1342
|
+
|
|
1343
|
+
# Verify the existing classes were checked correctly
|
|
1344
|
+
# ExistingClass_TEST: 85% >= 80% (individual req) - Pass
|
|
1345
|
+
# OtherClass_TEST: 70% >= 60% (global req) - Pass
|
|
1346
|
+
# NonExistentClass_TEST and AnotherMissingClass_TEST are simply ignored
|
|
1347
|
+
|
|
1348
|
+
@patch.object(BaseProjectConfig, "get_repo")
|
|
1349
|
+
def test_delta_changes_filter__no_git_repo(self, mock_get_repo):
|
|
1350
|
+
"""Test that delta_changes filter returns all classes when no git repo exists"""
|
|
1351
|
+
task_config = TaskConfig(
|
|
1352
|
+
{
|
|
1353
|
+
"options": {
|
|
1354
|
+
"test_name_match": "%_TEST",
|
|
1355
|
+
"dynamic_filter": "delta_changes",
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
)
|
|
1359
|
+
task = RunApexTests(self.project_config, task_config, self.org_config)
|
|
1360
|
+
mock_get_repo.return_value = None
|
|
1361
|
+
|
|
1362
|
+
mock_classes = {
|
|
1363
|
+
"totalSize": 2,
|
|
1364
|
+
"done": True,
|
|
1365
|
+
"records": [
|
|
1366
|
+
{"Id": "01p000001", "Name": "TestClass1"},
|
|
1367
|
+
{"Id": "01p000002", "Name": "TestClass2"},
|
|
1368
|
+
],
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
filtered = task._filter_test_classes_to_delta_changes(mock_classes)
|
|
1372
|
+
|
|
1373
|
+
# Should return all classes when no git repo
|
|
1374
|
+
assert len(filtered) == 2
|
|
1375
|
+
assert filtered == mock_classes["records"]
|
|
1376
|
+
log = self._task_log_handler.messages
|
|
1377
|
+
assert any("No git repository found" in msg for msg in log["info"])
|
|
1378
|
+
|
|
1379
|
+
@patch.object(BaseProjectConfig, "get_repo")
|
|
1380
|
+
@patch("cumulusci.tasks.apex.testrunner.ListModifiedFiles")
|
|
1381
|
+
def test_delta_changes_filter__list_modified_files_returns_none(
|
|
1382
|
+
self, mock_list_modified_files, mock_get_repo
|
|
1383
|
+
):
|
|
1384
|
+
"""Test that delta_changes filter handles None return from ListModifiedFiles"""
|
|
1385
|
+
task_config = TaskConfig(
|
|
1386
|
+
{
|
|
1387
|
+
"options": {
|
|
1388
|
+
"test_name_match": "%_TEST",
|
|
1389
|
+
"dynamic_filter": "delta_changes",
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
)
|
|
1393
|
+
task = RunApexTests(self.project_config, task_config, self.org_config)
|
|
1394
|
+
mock_get_repo.return_value = Mock()
|
|
1395
|
+
|
|
1396
|
+
# Mock ListModifiedFiles task - it's instantiated and then called
|
|
1397
|
+
# Create a simple object with return_values attribute
|
|
1398
|
+
class MockTaskInstance:
|
|
1399
|
+
def __init__(self):
|
|
1400
|
+
self.return_values = {"files": set(), "file_names": set()}
|
|
1401
|
+
self.parsed_options = Mock()
|
|
1402
|
+
self.parsed_options.base_ref = None
|
|
1403
|
+
|
|
1404
|
+
def __call__(self):
|
|
1405
|
+
pass
|
|
1406
|
+
|
|
1407
|
+
mock_list_modified_files.return_value = MockTaskInstance()
|
|
1408
|
+
|
|
1409
|
+
mock_classes = {
|
|
1410
|
+
"totalSize": 2,
|
|
1411
|
+
"done": True,
|
|
1412
|
+
"records": [
|
|
1413
|
+
{"Id": "01p000001", "Name": "TestClass1"},
|
|
1414
|
+
{"Id": "01p000002", "Name": "TestClass2"},
|
|
1415
|
+
],
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
filtered = task._filter_test_classes_to_delta_changes(mock_classes)
|
|
1419
|
+
|
|
1420
|
+
# Should return empty list when no files changed
|
|
1421
|
+
assert filtered == []
|
|
1422
|
+
log = self._task_log_handler.messages
|
|
1423
|
+
assert any("No changed files found" in msg for msg in log["info"])
|
|
1424
|
+
# Verify ListModifiedFiles was instantiated
|
|
1425
|
+
mock_list_modified_files.assert_called_once()
|
|
1426
|
+
|
|
1427
|
+
@patch.object(BaseProjectConfig, "get_repo")
|
|
1428
|
+
@patch("cumulusci.tasks.apex.testrunner.ListModifiedFiles")
|
|
1429
|
+
def test_delta_changes_filter__no_changed_files(
|
|
1430
|
+
self, mock_list_modified_files, mock_get_repo
|
|
1431
|
+
):
|
|
1432
|
+
"""Test that delta_changes filter handles empty changed files list"""
|
|
1433
|
+
task_config = TaskConfig(
|
|
1434
|
+
{
|
|
1435
|
+
"options": {
|
|
1436
|
+
"test_name_match": "%_TEST",
|
|
1437
|
+
"dynamic_filter": "delta_changes",
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
)
|
|
1441
|
+
task = RunApexTests(self.project_config, task_config, self.org_config)
|
|
1442
|
+
mock_get_repo.return_value = Mock()
|
|
1443
|
+
|
|
1444
|
+
# Mock ListModifiedFiles task
|
|
1445
|
+
# Create a simple object with return_values attribute
|
|
1446
|
+
class MockTaskInstance:
|
|
1447
|
+
def __init__(self):
|
|
1448
|
+
self.return_values = {"files": [], "file_names": None}
|
|
1449
|
+
self.parsed_options = Mock()
|
|
1450
|
+
self.parsed_options.base_ref = None
|
|
1451
|
+
|
|
1452
|
+
def __call__(self):
|
|
1453
|
+
pass
|
|
1454
|
+
|
|
1455
|
+
mock_list_modified_files.return_value = MockTaskInstance()
|
|
1456
|
+
|
|
1457
|
+
mock_classes = {
|
|
1458
|
+
"totalSize": 2,
|
|
1459
|
+
"done": True,
|
|
1460
|
+
"records": [
|
|
1461
|
+
{"Id": "01p000001", "Name": "TestClass1"},
|
|
1462
|
+
{"Id": "01p000002", "Name": "TestClass2"},
|
|
1463
|
+
],
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
filtered = task._filter_test_classes_to_delta_changes(mock_classes)
|
|
1467
|
+
|
|
1468
|
+
# Should return empty list when no files changed
|
|
1469
|
+
assert filtered == []
|
|
1470
|
+
log = self._task_log_handler.messages
|
|
1471
|
+
assert any("No changed files found" in msg for msg in log["info"])
|
|
1472
|
+
# Verify ListModifiedFiles was instantiated
|
|
1473
|
+
mock_list_modified_files.assert_called_once()
|
|
1474
|
+
|
|
1475
|
+
@patch.object(BaseProjectConfig, "get_repo")
|
|
1476
|
+
@patch("cumulusci.tasks.apex.testrunner.ListModifiedFiles")
|
|
1477
|
+
def test_delta_changes_filter__no_file_names(
|
|
1478
|
+
self, mock_list_modified_files, mock_get_repo
|
|
1479
|
+
):
|
|
1480
|
+
"""Test that delta_changes filter handles missing file_names in return_values"""
|
|
1481
|
+
task_config = TaskConfig(
|
|
1482
|
+
{
|
|
1483
|
+
"options": {
|
|
1484
|
+
"test_name_match": "%_TEST",
|
|
1485
|
+
"dynamic_filter": "delta_changes",
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
)
|
|
1489
|
+
task = RunApexTests(self.project_config, task_config, self.org_config)
|
|
1490
|
+
mock_get_repo.return_value = Mock()
|
|
1491
|
+
|
|
1492
|
+
# Mock ListModifiedFiles task - files exist but no file_names
|
|
1493
|
+
# Create a simple object with return_values attribute
|
|
1494
|
+
class MockTaskInstance:
|
|
1495
|
+
def __init__(self):
|
|
1496
|
+
self.return_values = {
|
|
1497
|
+
"files": ["force-app/main/default/classes/MyClass.cls"],
|
|
1498
|
+
"file_names": set(), # Empty set simulates no file names found
|
|
1499
|
+
}
|
|
1500
|
+
self.parsed_options = Mock()
|
|
1501
|
+
self.parsed_options.base_ref = None
|
|
1502
|
+
|
|
1503
|
+
def __call__(self):
|
|
1504
|
+
pass
|
|
1505
|
+
|
|
1506
|
+
mock_list_modified_files.return_value = MockTaskInstance()
|
|
1507
|
+
|
|
1508
|
+
mock_classes = {
|
|
1509
|
+
"totalSize": 2,
|
|
1510
|
+
"done": True,
|
|
1511
|
+
"records": [
|
|
1512
|
+
{"Id": "01p000001", "Name": "TestClass1"},
|
|
1513
|
+
{"Id": "01p000002", "Name": "TestClass2"},
|
|
1514
|
+
],
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
filtered = task._filter_test_classes_to_delta_changes(mock_classes)
|
|
1518
|
+
|
|
1519
|
+
# Should return empty list when no file names found
|
|
1520
|
+
assert filtered == []
|
|
1521
|
+
log = self._task_log_handler.messages
|
|
1522
|
+
assert any("No file names found" in msg for msg in log["info"])
|
|
1523
|
+
# Verify ListModifiedFiles was instantiated
|
|
1524
|
+
mock_list_modified_files.assert_called_once()
|
|
1525
|
+
|
|
1526
|
+
@patch.object(BaseProjectConfig, "get_repo")
|
|
1527
|
+
@patch("cumulusci.tasks.apex.testrunner.ListModifiedFiles")
|
|
1528
|
+
def test_delta_changes_filter__filters_test_classes(
|
|
1529
|
+
self, mock_list_modified_files, mock_get_repo
|
|
1530
|
+
):
|
|
1531
|
+
"""Test that delta_changes filter correctly filters test classes based on affected classes"""
|
|
1532
|
+
task_config = TaskConfig(
|
|
1533
|
+
{
|
|
1534
|
+
"options": {
|
|
1535
|
+
"test_name_match": "%_TEST",
|
|
1536
|
+
"dynamic_filter": "delta_changes",
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
)
|
|
1540
|
+
task = RunApexTests(self.project_config, task_config, self.org_config)
|
|
1541
|
+
mock_get_repo.return_value = Mock()
|
|
1542
|
+
|
|
1543
|
+
# Mock ListModifiedFiles task
|
|
1544
|
+
# Create a simple object with return_values attribute
|
|
1545
|
+
class MockTaskInstance:
|
|
1546
|
+
def __init__(self):
|
|
1547
|
+
self.return_values = {
|
|
1548
|
+
"files": ["force-app/main/default/classes/Account.cls"],
|
|
1549
|
+
"file_names": {"Account"},
|
|
1550
|
+
}
|
|
1551
|
+
self.parsed_options = Mock()
|
|
1552
|
+
self.parsed_options.base_ref = None
|
|
1553
|
+
|
|
1554
|
+
def __call__(self):
|
|
1555
|
+
pass
|
|
1556
|
+
|
|
1557
|
+
mock_list_modified_files.return_value = MockTaskInstance()
|
|
1558
|
+
|
|
1559
|
+
mock_classes = {
|
|
1560
|
+
"totalSize": 4,
|
|
1561
|
+
"done": True,
|
|
1562
|
+
"records": [
|
|
1563
|
+
{"Id": "01p000001", "Name": "Account"}, # Direct match
|
|
1564
|
+
{"Id": "01p000002", "Name": "AccountTest"}, # Affected class + Test
|
|
1565
|
+
{"Id": "01p000003", "Name": "TestAccount"}, # Test + Affected class
|
|
1566
|
+
{"Id": "01p000004", "Name": "ContactTest"}, # Not affected
|
|
1567
|
+
],
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
filtered = task._filter_test_classes_to_delta_changes(mock_classes)
|
|
1571
|
+
|
|
1572
|
+
# Should return only affected test classes
|
|
1573
|
+
assert len(filtered) == 3
|
|
1574
|
+
filtered_names = [record["Name"] for record in filtered]
|
|
1575
|
+
assert "Account" in filtered_names
|
|
1576
|
+
assert "AccountTest" in filtered_names
|
|
1577
|
+
assert "TestAccount" in filtered_names
|
|
1578
|
+
assert "ContactTest" not in filtered_names
|
|
1579
|
+
# Verify ListModifiedFiles was instantiated
|
|
1580
|
+
mock_list_modified_files.assert_called_once()
|
|
1581
|
+
# Verify logging for excluded classes
|
|
1582
|
+
log = self._task_log_handler.messages
|
|
1583
|
+
assert any(
|
|
1584
|
+
"Excluded" in msg and "not affected by delta changes" in msg
|
|
1585
|
+
for msg in log["info"]
|
|
1586
|
+
)
|
|
1587
|
+
|
|
1588
|
+
@patch.object(BaseProjectConfig, "get_repo")
|
|
1589
|
+
@patch("cumulusci.tasks.apex.testrunner.ListModifiedFiles")
|
|
1590
|
+
def test_delta_changes_filter__with_custom_base_ref(
|
|
1591
|
+
self, mock_list_modified_files, mock_get_repo
|
|
1592
|
+
):
|
|
1593
|
+
"""Test that delta_changes filter uses custom base_ref when provided"""
|
|
1594
|
+
task_config = TaskConfig(
|
|
1595
|
+
{
|
|
1596
|
+
"options": {
|
|
1597
|
+
"test_name_match": "%_TEST",
|
|
1598
|
+
"dynamic_filter": "delta_changes",
|
|
1599
|
+
"base_ref": "origin/develop",
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
)
|
|
1603
|
+
task = RunApexTests(self.project_config, task_config, self.org_config)
|
|
1604
|
+
mock_get_repo.return_value = Mock()
|
|
1605
|
+
|
|
1606
|
+
# Mock ListModifiedFiles task
|
|
1607
|
+
# Create a simple object with return_values attribute
|
|
1608
|
+
class MockTaskInstance:
|
|
1609
|
+
def __init__(self):
|
|
1610
|
+
self.return_values = {
|
|
1611
|
+
"files": ["force-app/main/default/classes/MyClass.cls"],
|
|
1612
|
+
"file_names": {"MyClass"},
|
|
1613
|
+
}
|
|
1614
|
+
self.parsed_options = Mock()
|
|
1615
|
+
self.parsed_options.base_ref = None
|
|
1616
|
+
|
|
1617
|
+
def __call__(self):
|
|
1618
|
+
pass
|
|
1619
|
+
|
|
1620
|
+
mock_list_modified_files.return_value = MockTaskInstance()
|
|
1621
|
+
|
|
1622
|
+
mock_classes = {
|
|
1623
|
+
"totalSize": 1,
|
|
1624
|
+
"done": True,
|
|
1625
|
+
"records": [{"Id": "01p000001", "Name": "MyClassTest"}],
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
filtered = task._filter_test_classes_to_delta_changes(mock_classes)
|
|
1629
|
+
|
|
1630
|
+
# Verify ListModifiedFiles was called with correct base_ref
|
|
1631
|
+
mock_list_modified_files.assert_called_once()
|
|
1632
|
+
call_args = mock_list_modified_files.call_args
|
|
1633
|
+
task_config_arg = call_args[0][1]
|
|
1634
|
+
assert task_config_arg.config["options"]["base_ref"] == "origin/develop"
|
|
1635
|
+
assert len(filtered) == 1
|
|
1636
|
+
# Verify logging for affected classes
|
|
1637
|
+
log = self._task_log_handler.messages
|
|
1638
|
+
assert any("Found" in msg and "affected class" in msg for msg in log["info"])
|
|
1639
|
+
|
|
1640
|
+
@patch.object(BaseProjectConfig, "get_repo")
|
|
1641
|
+
@patch("cumulusci.tasks.apex.testrunner.ListModifiedFiles")
|
|
1642
|
+
def test_delta_changes_filter__default_base_ref_none(
|
|
1643
|
+
self, mock_list_modified_files, mock_get_repo
|
|
1644
|
+
):
|
|
1645
|
+
"""Test that delta_changes filter uses None base_ref when not provided (default branch)"""
|
|
1646
|
+
task_config = TaskConfig(
|
|
1647
|
+
{
|
|
1648
|
+
"options": {
|
|
1649
|
+
"test_name_match": "%_TEST",
|
|
1650
|
+
"dynamic_filter": "delta_changes",
|
|
1651
|
+
# base_ref not provided, should be None
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
)
|
|
1655
|
+
task = RunApexTests(self.project_config, task_config, self.org_config)
|
|
1656
|
+
mock_get_repo.return_value = Mock()
|
|
1657
|
+
|
|
1658
|
+
# Mock ListModifiedFiles task to return None files (to test warning message)
|
|
1659
|
+
# Create a simple object with return_values attribute
|
|
1660
|
+
class MockTaskInstance:
|
|
1661
|
+
def __init__(self):
|
|
1662
|
+
self.return_values = {"files": set(), "file_names": set()}
|
|
1663
|
+
self.parsed_options = Mock()
|
|
1664
|
+
self.parsed_options.base_ref = None
|
|
1665
|
+
|
|
1666
|
+
def __call__(self):
|
|
1667
|
+
pass
|
|
1668
|
+
|
|
1669
|
+
mock_list_modified_files.return_value = MockTaskInstance()
|
|
1670
|
+
|
|
1671
|
+
mock_classes = {
|
|
1672
|
+
"totalSize": 1,
|
|
1673
|
+
"done": True,
|
|
1674
|
+
"records": [{"Id": "01p000001", "Name": "TestClass1"}],
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
task._filter_test_classes_to_delta_changes(mock_classes)
|
|
1678
|
+
|
|
1679
|
+
# Verify ListModifiedFiles was called with None base_ref
|
|
1680
|
+
mock_list_modified_files.assert_called_once()
|
|
1681
|
+
call_args = mock_list_modified_files.call_args
|
|
1682
|
+
task_config_arg = call_args[0][1]
|
|
1683
|
+
assert task_config_arg.config["options"]["base_ref"] is None
|
|
1684
|
+
# Note: Warning about "default branch" would come from ListModifiedFiles
|
|
1685
|
+
# implementation, but since we're mocking it, that warning won't appear here
|
|
1686
|
+
|
|
1687
|
+
def test_is_test_class_affected__direct_match(self):
|
|
1688
|
+
"""Test _is_test_class_affected with direct match"""
|
|
1689
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1690
|
+
affected_class_names = ["account", "contact"]
|
|
1691
|
+
|
|
1692
|
+
assert task._is_test_class_affected("account", affected_class_names) is True
|
|
1693
|
+
assert task._is_test_class_affected("contact", affected_class_names) is True
|
|
1694
|
+
assert (
|
|
1695
|
+
task._is_test_class_affected("opportunity", affected_class_names) is False
|
|
1696
|
+
)
|
|
1697
|
+
|
|
1698
|
+
def test_is_test_class_affected__suffix_test_pattern(self):
|
|
1699
|
+
"""Test _is_test_class_affected with ClassNameTest pattern"""
|
|
1700
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1701
|
+
affected_class_names = ["account", "myservice"]
|
|
1702
|
+
|
|
1703
|
+
assert task._is_test_class_affected("accounttest", affected_class_names) is True
|
|
1704
|
+
assert (
|
|
1705
|
+
task._is_test_class_affected("myservicetest", affected_class_names) is True
|
|
1706
|
+
)
|
|
1707
|
+
assert (
|
|
1708
|
+
task._is_test_class_affected("contacttest", affected_class_names) is False
|
|
1709
|
+
)
|
|
1710
|
+
|
|
1711
|
+
def test_is_test_class_affected__prefix_test_pattern(self):
|
|
1712
|
+
"""Test _is_test_class_affected with TestClassName pattern"""
|
|
1713
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1714
|
+
affected_class_names = ["account", "handler"]
|
|
1715
|
+
|
|
1716
|
+
assert task._is_test_class_affected("testaccount", affected_class_names) is True
|
|
1717
|
+
assert task._is_test_class_affected("testhandler", affected_class_names) is True
|
|
1718
|
+
assert (
|
|
1719
|
+
task._is_test_class_affected("testcontact", affected_class_names) is False
|
|
1720
|
+
)
|
|
1721
|
+
|
|
1722
|
+
def test_is_test_class_affected__underscore_suffix_pattern(self):
|
|
1723
|
+
"""Test _is_test_class_affected with ClassName_* pattern"""
|
|
1724
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1725
|
+
affected_class_names = ["account", "service"]
|
|
1726
|
+
|
|
1727
|
+
assert (
|
|
1728
|
+
task._is_test_class_affected("account_test", affected_class_names) is True
|
|
1729
|
+
)
|
|
1730
|
+
assert (
|
|
1731
|
+
task._is_test_class_affected("account_test_method", affected_class_names)
|
|
1732
|
+
is True
|
|
1733
|
+
)
|
|
1734
|
+
assert (
|
|
1735
|
+
task._is_test_class_affected("service_integration", affected_class_names)
|
|
1736
|
+
is True
|
|
1737
|
+
)
|
|
1738
|
+
assert (
|
|
1739
|
+
task._is_test_class_affected("contact_test", affected_class_names) is False
|
|
1740
|
+
)
|
|
1741
|
+
|
|
1742
|
+
def test_is_test_class_affected__test_underscore_prefix_pattern(self):
|
|
1743
|
+
"""Test _is_test_class_affected with TestClassName_* pattern"""
|
|
1744
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1745
|
+
affected_class_names = ["account", "handler"]
|
|
1746
|
+
|
|
1747
|
+
assert (
|
|
1748
|
+
task._is_test_class_affected("testaccount_unit", affected_class_names)
|
|
1749
|
+
is True
|
|
1750
|
+
)
|
|
1751
|
+
assert (
|
|
1752
|
+
task._is_test_class_affected(
|
|
1753
|
+
"testhandler_integration", affected_class_names
|
|
1754
|
+
)
|
|
1755
|
+
is True
|
|
1756
|
+
)
|
|
1757
|
+
assert (
|
|
1758
|
+
task._is_test_class_affected("testcontact_unit", affected_class_names)
|
|
1759
|
+
is False
|
|
1760
|
+
)
|
|
1761
|
+
|
|
1762
|
+
def test_is_test_class_affected__case_insensitive(self):
|
|
1763
|
+
"""Test _is_test_class_affected works with lowercase inputs (as called in practice)"""
|
|
1764
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1765
|
+
# Method is called with lowercase strings in practice
|
|
1766
|
+
affected_class_names = ["account", "myservice"]
|
|
1767
|
+
|
|
1768
|
+
assert task._is_test_class_affected("accounttest", affected_class_names) is True
|
|
1769
|
+
assert (
|
|
1770
|
+
task._is_test_class_affected("myservicetest", affected_class_names) is True
|
|
1771
|
+
)
|
|
1772
|
+
assert (
|
|
1773
|
+
task._is_test_class_affected("contacttest", affected_class_names) is False
|
|
1774
|
+
)
|
|
1775
|
+
|
|
1776
|
+
def test_is_test_class_affected__multiple_patterns(self):
|
|
1777
|
+
"""Test _is_test_class_affected with multiple affected classes and patterns"""
|
|
1778
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1779
|
+
affected_class_names = ["account", "contact", "handler"]
|
|
1780
|
+
|
|
1781
|
+
# Test various patterns with multiple affected classes
|
|
1782
|
+
assert task._is_test_class_affected("accounttest", affected_class_names) is True
|
|
1783
|
+
assert task._is_test_class_affected("testcontact", affected_class_names) is True
|
|
1784
|
+
assert (
|
|
1785
|
+
task._is_test_class_affected("handler_integration", affected_class_names)
|
|
1786
|
+
is True
|
|
1787
|
+
)
|
|
1788
|
+
assert (
|
|
1789
|
+
task._is_test_class_affected("testaccount_unit", affected_class_names)
|
|
1790
|
+
is True
|
|
1791
|
+
)
|
|
1792
|
+
assert (
|
|
1793
|
+
task._is_test_class_affected("opportunitytest", affected_class_names)
|
|
1794
|
+
is False
|
|
1795
|
+
)
|
|
1796
|
+
|
|
1797
|
+
def test_is_test_class_affected__underscore_removal_pattern(self):
|
|
1798
|
+
"""Test _is_test_class_affected with underscore removal (e.g., Account_Handler -> AccountHandlerTest)"""
|
|
1799
|
+
task = RunApexTests(self.project_config, self.task_config, self.org_config)
|
|
1800
|
+
affected_class_names = ["account_handler", "my_service_class"]
|
|
1801
|
+
|
|
1802
|
+
# Test that AccountHandlerTest matches account_handler
|
|
1803
|
+
assert (
|
|
1804
|
+
task._is_test_class_affected("accounthandlertest", affected_class_names)
|
|
1805
|
+
is True
|
|
1806
|
+
)
|
|
1807
|
+
# Test that MyServiceClassTest matches my_service_class
|
|
1808
|
+
assert (
|
|
1809
|
+
task._is_test_class_affected("myserviceclasstest", affected_class_names)
|
|
1810
|
+
is True
|
|
1811
|
+
)
|
|
1812
|
+
# Test no match
|
|
1813
|
+
assert (
|
|
1814
|
+
task._is_test_class_affected("contacttest", affected_class_names) is False
|
|
1815
|
+
)
|
|
1816
|
+
|
|
1817
|
+
def test_filter_package_classes__unsupported_dynamic_filter(self):
|
|
1818
|
+
"""Test that unsupported dynamic_filter values raise TaskOptionsError"""
|
|
1819
|
+
task_config = TaskConfig(
|
|
1820
|
+
{
|
|
1821
|
+
"options": {
|
|
1822
|
+
"test_name_match": "%_TEST",
|
|
1823
|
+
"dynamic_filter": "unsupported_filter",
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
)
|
|
1827
|
+
task = RunApexTests(self.project_config, task_config, self.org_config)
|
|
1828
|
+
|
|
1829
|
+
mock_classes = {
|
|
1830
|
+
"totalSize": 1,
|
|
1831
|
+
"done": True,
|
|
1832
|
+
"records": [{"Id": "01p000001", "Name": "TestClass1"}],
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
with pytest.raises(TaskOptionsError) as e:
|
|
1836
|
+
task._filter_package_classes(mock_classes)
|
|
1837
|
+
assert "Unsupported dynamic filter: unsupported_filter" in str(e.value)
|
|
1838
|
+
|
|
1839
|
+
@patch.object(BaseProjectConfig, "get_repo")
|
|
1840
|
+
@patch("cumulusci.tasks.apex.testrunner.ListModifiedFiles")
|
|
1841
|
+
@responses.activate
|
|
1842
|
+
def test_run_task__delta_changes_filter_integration(
|
|
1843
|
+
self, mock_list_modified_files, mock_get_repo
|
|
1844
|
+
):
|
|
1845
|
+
"""Integration test for delta_changes filter in full test run"""
|
|
1846
|
+
self._mock_api_version_discovery()
|
|
1847
|
+
self._mock_get_installpkg_results()
|
|
1848
|
+
|
|
1849
|
+
# Mock ListModifiedFiles task - it's instantiated and then called
|
|
1850
|
+
# Create a simple object with return_values attribute
|
|
1851
|
+
class MockTaskInstance:
|
|
1852
|
+
def __init__(self):
|
|
1853
|
+
self.return_values = {
|
|
1854
|
+
"files": ["force-app/main/default/classes/Account.cls"],
|
|
1855
|
+
"file_names": {"Account"},
|
|
1856
|
+
}
|
|
1857
|
+
self.parsed_options = Mock()
|
|
1858
|
+
self.parsed_options.base_ref = None
|
|
1859
|
+
|
|
1860
|
+
def __call__(self):
|
|
1861
|
+
pass
|
|
1862
|
+
|
|
1863
|
+
mock_list_modified_files.return_value = MockTaskInstance()
|
|
1864
|
+
mock_get_repo.return_value = Mock()
|
|
1865
|
+
|
|
1866
|
+
# Mock ApexClass query to return test classes
|
|
1867
|
+
self._mock_apex_class_query(name="AccountTest")
|
|
1868
|
+
self._mock_run_tests()
|
|
1869
|
+
self._mock_get_failed_test_classes()
|
|
1870
|
+
self._mock_tests_complete()
|
|
1871
|
+
self._mock_get_test_results()
|
|
1872
|
+
|
|
1873
|
+
task_config = TaskConfig(
|
|
1874
|
+
{
|
|
1875
|
+
"options": {
|
|
1876
|
+
"test_name_match": "%_TEST",
|
|
1877
|
+
"dynamic_filter": "delta_changes",
|
|
1878
|
+
"junit_output": "results_junit.xml",
|
|
1879
|
+
"poll_interval": 1,
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
)
|
|
1883
|
+
task = RunApexTests(self.project_config, task_config, self.org_config)
|
|
1884
|
+
|
|
1885
|
+
task()
|
|
1886
|
+
|
|
1887
|
+
# Verify ListModifiedFiles was instantiated
|
|
1888
|
+
mock_list_modified_files.assert_called_once()
|
|
1889
|
+
|
|
974
1890
|
|
|
975
1891
|
@patch(
|
|
976
1892
|
"cumulusci.core.tasks.BaseSalesforceTask._update_credentials",
|