cumulusci-plus 5.0.21__py3-none-any.whl → 5.0.35__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.
Files changed (121) hide show
  1. cumulusci/__about__.py +1 -1
  2. cumulusci/cli/logger.py +2 -2
  3. cumulusci/cli/service.py +20 -0
  4. cumulusci/cli/task.py +17 -0
  5. cumulusci/cli/tests/test_error.py +3 -1
  6. cumulusci/cli/tests/test_flow.py +279 -2
  7. cumulusci/cli/tests/test_service.py +15 -12
  8. cumulusci/cli/tests/test_task.py +88 -2
  9. cumulusci/cli/tests/utils.py +1 -4
  10. cumulusci/core/config/base_task_flow_config.py +26 -1
  11. cumulusci/core/config/project_config.py +2 -20
  12. cumulusci/core/config/tests/test_config_expensive.py +9 -3
  13. cumulusci/core/config/universal_config.py +3 -4
  14. cumulusci/core/dependencies/base.py +1 -1
  15. cumulusci/core/dependencies/dependencies.py +1 -1
  16. cumulusci/core/dependencies/github.py +1 -2
  17. cumulusci/core/dependencies/resolvers.py +1 -1
  18. cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
  19. cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
  20. cumulusci/core/flowrunner.py +90 -6
  21. cumulusci/core/github.py +1 -1
  22. cumulusci/core/sfdx.py +3 -1
  23. cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
  24. cumulusci/core/source_transforms/transforms.py +1 -1
  25. cumulusci/core/tasks.py +13 -2
  26. cumulusci/core/tests/test_flowrunner.py +100 -0
  27. cumulusci/core/tests/test_tasks.py +65 -0
  28. cumulusci/core/utils.py +3 -1
  29. cumulusci/core/versions.py +1 -1
  30. cumulusci/cumulusci.yml +55 -0
  31. cumulusci/oauth/client.py +1 -1
  32. cumulusci/plugins/plugin_base.py +5 -3
  33. cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
  34. cumulusci/salesforce_api/rest_deploy.py +1 -1
  35. cumulusci/schema/cumulusci.jsonschema.json +64 -0
  36. cumulusci/tasks/apex/anon.py +1 -1
  37. cumulusci/tasks/apex/testrunner.py +416 -142
  38. cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
  39. cumulusci/tasks/bulkdata/extract.py +0 -1
  40. cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
  41. cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
  42. cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
  43. cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
  44. cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
  45. cumulusci/tasks/bulkdata/select_utils.py +1 -1
  46. cumulusci/tasks/bulkdata/snowfakery.py +100 -25
  47. cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
  48. cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
  49. cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
  50. cumulusci/tasks/bulkdata/tests/test_select_utils.py +26 -0
  51. cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
  52. cumulusci/tasks/create_package_version.py +190 -16
  53. cumulusci/tasks/datadictionary.py +1 -1
  54. cumulusci/tasks/metadata_etl/base.py +7 -3
  55. cumulusci/tasks/metadata_etl/layouts.py +1 -1
  56. cumulusci/tasks/metadata_etl/permissions.py +1 -1
  57. cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
  58. cumulusci/tasks/push/README.md +15 -17
  59. cumulusci/tasks/release_notes/README.md +13 -13
  60. cumulusci/tasks/release_notes/generator.py +13 -8
  61. cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
  62. cumulusci/tasks/salesforce/Deploy.py +53 -2
  63. cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
  64. cumulusci/tasks/salesforce/__init__.py +1 -0
  65. cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
  66. cumulusci/tasks/salesforce/composite.py +1 -1
  67. cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
  68. cumulusci/tasks/salesforce/enable_prediction.py +5 -1
  69. cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
  70. cumulusci/tasks/salesforce/sourcetracking.py +1 -1
  71. cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
  72. cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
  73. cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
  74. cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
  75. cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
  76. cumulusci/tasks/salesforce/tests/test_update_external_credential.py +912 -0
  77. cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
  78. cumulusci/tasks/salesforce/update_dependencies.py +2 -2
  79. cumulusci/tasks/salesforce/update_external_credential.py +562 -0
  80. cumulusci/tasks/salesforce/update_named_credential.py +441 -0
  81. cumulusci/tasks/salesforce/update_profile.py +17 -13
  82. cumulusci/tasks/salesforce/users/permsets.py +62 -5
  83. cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
  84. cumulusci/tasks/sfdmu/__init__.py +0 -0
  85. cumulusci/tasks/sfdmu/sfdmu.py +363 -0
  86. cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
  87. cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
  88. cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
  89. cumulusci/tasks/tests/test_create_package_version.py +716 -1
  90. cumulusci/tasks/tests/test_util.py +42 -0
  91. cumulusci/tasks/util.py +37 -1
  92. cumulusci/tasks/utility/copyContents.py +402 -0
  93. cumulusci/tasks/utility/credentialManager.py +256 -0
  94. cumulusci/tasks/utility/directoryRecreator.py +30 -0
  95. cumulusci/tasks/utility/env_management.py +1 -1
  96. cumulusci/tasks/utility/secretsToEnv.py +135 -0
  97. cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
  98. cumulusci/tasks/utility/tests/test_credentialManager.py +564 -0
  99. cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
  100. cumulusci/tasks/utility/tests/test_secretsToEnv.py +1091 -0
  101. cumulusci/tests/test_integration_infrastructure.py +3 -1
  102. cumulusci/tests/test_utils.py +70 -6
  103. cumulusci/utils/__init__.py +54 -9
  104. cumulusci/utils/classutils.py +5 -2
  105. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
  106. cumulusci/utils/options.py +23 -1
  107. cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
  108. cumulusci/utils/yaml/cumulusci_yml.py +7 -3
  109. cumulusci/utils/yaml/model_parser.py +2 -2
  110. cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
  111. cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
  112. cumulusci/vcs/base.py +23 -15
  113. cumulusci/vcs/bootstrap.py +5 -4
  114. cumulusci/vcs/utils/list_modified_files.py +189 -0
  115. cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
  116. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/METADATA +12 -10
  117. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/RECORD +121 -96
  118. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/WHEEL +0 -0
  119. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/entry_points.txt +0 -0
  120. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/AUTHORS.rst +0 -0
  121. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,588 @@
1
+ """Tests for list_modified_files task."""
2
+ from pathlib import Path
3
+ from unittest import mock
4
+ from unittest.mock import Mock, PropertyMock, patch
5
+
6
+ import pytest
7
+
8
+ from cumulusci.core.config import BaseProjectConfig, TaskConfig
9
+ from cumulusci.vcs.utils.list_modified_files import ListModifiedFiles
10
+
11
+
12
+ @pytest.fixture
13
+ def project_config_with_git():
14
+ """Create a project config with git and package settings for testing.
15
+
16
+ This follows the pattern from cumulusci/vcs/tests/conftest.py but adds
17
+ the specific git and package configuration needed for these tests.
18
+ """
19
+ from cumulusci.core.config import UniversalConfig
20
+
21
+ universal_config = UniversalConfig()
22
+ project_config = BaseProjectConfig(universal_config, config={"no_yaml": True})
23
+ project_config.config["project"] = {
24
+ "git": {"default_branch": "main"},
25
+ "package": {"path": "force-app"},
26
+ }
27
+ return project_config
28
+
29
+
30
+ class TestListModifiedFiles:
31
+ """Test cases for ListModifiedFiles task."""
32
+
33
+ @pytest.fixture(autouse=True)
34
+ def setup_project_config(self, project_config_with_git):
35
+ """Auto-use fixture to set up project_config for all tests."""
36
+ self.project_config = project_config_with_git
37
+
38
+ def _create_task(self, options=None):
39
+ """Helper to create a task instance."""
40
+ if options is None:
41
+ options = {}
42
+ task_config = TaskConfig({"options": options})
43
+ return ListModifiedFiles(self.project_config, task_config, org_config=None)
44
+
45
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
46
+ def test_no_git_repo(self, mock_subprocess):
47
+ """Test behavior when no git repository is found."""
48
+ task = self._create_task()
49
+ task.project_config.get_repo = Mock(return_value=None)
50
+
51
+ task()
52
+
53
+ assert task.return_values == {"files": set(), "file_names": set()}
54
+ mock_subprocess.assert_not_called()
55
+
56
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
57
+ def test_git_diff_fails(self, mock_subprocess):
58
+ """Test behavior when git diff command fails."""
59
+ task = self._create_task({"base_ref": "origin/main"})
60
+ task.project_config.get_repo = Mock(return_value=Mock())
61
+
62
+ # Mock subprocess.run to return non-zero exit code
63
+ mock_result = Mock()
64
+ mock_result.returncode = 1
65
+ mock_result.stderr = "fatal: ambiguous argument"
66
+ mock_subprocess.return_value = mock_result
67
+
68
+ task()
69
+
70
+ assert task.return_values == {"files": set(), "file_names": set()}
71
+ mock_subprocess.assert_called_once_with(
72
+ ["git", "diff", "--name-only", "origin/main"],
73
+ capture_output=True,
74
+ text=True,
75
+ check=False,
76
+ )
77
+
78
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
79
+ def test_git_command_not_found(self, mock_subprocess):
80
+ """Test behavior when git command is not found."""
81
+ task = self._create_task({"base_ref": "origin/main"})
82
+ task.project_config.get_repo = Mock(return_value=Mock())
83
+
84
+ # Mock FileNotFoundError
85
+ mock_subprocess.side_effect = FileNotFoundError("git: command not found")
86
+
87
+ task()
88
+
89
+ assert task.return_values == {"files": set(), "file_names": set()}
90
+
91
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
92
+ def test_git_diff_exception(self, mock_subprocess):
93
+ """Test behavior when git diff raises an exception."""
94
+ task = self._create_task({"base_ref": "origin/main"})
95
+ task.project_config.get_repo = Mock(return_value=Mock())
96
+
97
+ # Mock generic exception
98
+ mock_subprocess.side_effect = Exception("Unexpected error")
99
+
100
+ task()
101
+
102
+ assert task.return_values == {"files": set(), "file_names": set()}
103
+
104
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
105
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
106
+ def test_no_files_changed(self, mock_package_path, mock_subprocess):
107
+ """Test behavior when no files are changed."""
108
+ mock_package_path.return_value = Path("force-app").absolute()
109
+ task = self._create_task({"base_ref": "origin/main"})
110
+ task.project_config.get_repo = Mock(return_value=Mock())
111
+
112
+ # Mock subprocess.run to return empty output
113
+ mock_result = Mock()
114
+ mock_result.returncode = 0
115
+ mock_result.stdout = ""
116
+ mock_subprocess.return_value = mock_result
117
+
118
+ task()
119
+
120
+ assert task.return_values == {"files": set(), "file_names": set()}
121
+
122
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
123
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
124
+ def test_files_changed_not_in_package_dirs(
125
+ self, mock_package_path, mock_subprocess
126
+ ):
127
+ """Test behavior when files are changed but not in package directories."""
128
+ mock_package_path.return_value = Path("force-app").absolute()
129
+ task = self._create_task({"base_ref": "origin/main"})
130
+ task.project_config.get_repo = Mock(return_value=Mock())
131
+
132
+ # Mock subprocess.run to return files outside package directories
133
+ mock_result = Mock()
134
+ mock_result.returncode = 0
135
+ mock_result.stdout = "README.md\n.gitignore\n"
136
+ mock_subprocess.return_value = mock_result
137
+
138
+ task()
139
+
140
+ assert task.return_values == {"files": set(), "file_names": set()}
141
+
142
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
143
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
144
+ def test_files_changed_in_package_dirs(self, mock_package_path, mock_subprocess):
145
+ """Test behavior when files are changed in package directories."""
146
+ mock_package_path.return_value = Path("force-app").absolute()
147
+ task = self._create_task({"base_ref": "origin/main"})
148
+ task.project_config.get_repo = Mock(return_value=Mock())
149
+
150
+ # Mock subprocess.run to return files in package directories
151
+ mock_result = Mock()
152
+ mock_result.returncode = 0
153
+ mock_result.stdout = "force-app/main/default/classes/MyClass.cls\n"
154
+ mock_subprocess.return_value = mock_result
155
+
156
+ task()
157
+
158
+ assert task.return_values["files"] == [
159
+ "force-app/main/default/classes/MyClass.cls"
160
+ ]
161
+ assert task.return_values["file_names"] == set()
162
+
163
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
164
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
165
+ def test_files_changed_in_src_directory(self, mock_package_path, mock_subprocess):
166
+ """Test behavior when files are changed in src directory."""
167
+ mock_package_path.return_value = Path("src").absolute()
168
+ task = self._create_task({"base_ref": "origin/main"})
169
+ task.project_config.get_repo = Mock(return_value=Mock())
170
+
171
+ # Mock subprocess.run to return files in src directory
172
+ mock_result = Mock()
173
+ mock_result.returncode = 0
174
+ mock_result.stdout = "src/classes/MyClass.cls\n"
175
+ mock_subprocess.return_value = mock_result
176
+
177
+ task()
178
+
179
+ assert task.return_values["files"] == ["src/classes/MyClass.cls"]
180
+ assert task.return_values["file_names"] == set()
181
+
182
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
183
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
184
+ def test_extract_file_names_with_cls_extension(
185
+ self, mock_package_path, mock_subprocess
186
+ ):
187
+ """Test extracting file names with .cls extension."""
188
+ mock_package_path.return_value = Path("force-app").absolute()
189
+ task = self._create_task(
190
+ {
191
+ "base_ref": "origin/main",
192
+ "file_extensions": ["cls"],
193
+ }
194
+ )
195
+ task.project_config.get_repo = Mock(return_value=Mock())
196
+
197
+ # Mock subprocess.run to return .cls files
198
+ mock_result = Mock()
199
+ mock_result.returncode = 0
200
+ mock_result.stdout = "force-app/main/default/classes/MyClass.cls\n"
201
+ mock_subprocess.return_value = mock_result
202
+
203
+ task()
204
+
205
+ assert task.return_values["files"] == [
206
+ "force-app/main/default/classes/MyClass.cls"
207
+ ]
208
+ assert task.return_values["file_names"] == {"MyClass"}
209
+
210
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
211
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
212
+ def test_extract_file_names_with_dot_cls_extension(
213
+ self, mock_package_path, mock_subprocess
214
+ ):
215
+ """Test extracting file names with .cls extension (with dot prefix)."""
216
+ mock_package_path.return_value = Path("force-app").absolute()
217
+ task = self._create_task(
218
+ {
219
+ "base_ref": "origin/main",
220
+ "file_extensions": [".cls"],
221
+ }
222
+ )
223
+ task.project_config.get_repo = Mock(return_value=Mock())
224
+
225
+ # Mock subprocess.run to return .cls files
226
+ mock_result = Mock()
227
+ mock_result.returncode = 0
228
+ mock_result.stdout = "force-app/main/default/classes/MyClass.cls\n"
229
+ mock_subprocess.return_value = mock_result
230
+
231
+ task()
232
+
233
+ assert task.return_values["files"] == [
234
+ "force-app/main/default/classes/MyClass.cls"
235
+ ]
236
+ assert task.return_values["file_names"] == {"MyClass"}
237
+
238
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
239
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
240
+ def test_extract_file_names_multiple_extensions(
241
+ self, mock_package_path, mock_subprocess
242
+ ):
243
+ """Test extracting file names with multiple extensions."""
244
+ mock_package_path.return_value = Path("force-app").absolute()
245
+ task = self._create_task(
246
+ {
247
+ "base_ref": "origin/main",
248
+ "file_extensions": ["cls", "trigger"],
249
+ }
250
+ )
251
+ task.project_config.get_repo = Mock(return_value=Mock())
252
+
253
+ # Mock subprocess.run to return multiple file types
254
+ mock_result = Mock()
255
+ mock_result.returncode = 0
256
+ mock_result.stdout = (
257
+ "force-app/main/default/classes/MyClass.cls\n"
258
+ "force-app/main/default/triggers/MyTrigger.trigger\n"
259
+ )
260
+ mock_subprocess.return_value = mock_result
261
+
262
+ task()
263
+
264
+ assert len(task.return_values["files"]) == 2
265
+ assert task.return_values["file_names"] == {"MyClass", "MyTrigger"}
266
+
267
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
268
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
269
+ def test_extract_file_names_no_matching_extensions(
270
+ self, mock_package_path, mock_subprocess
271
+ ):
272
+ """Test extracting file names when no files match the extensions."""
273
+ mock_package_path.return_value = Path("force-app").absolute()
274
+ task = self._create_task(
275
+ {
276
+ "base_ref": "origin/main",
277
+ "file_extensions": ["cls"],
278
+ }
279
+ )
280
+ task.project_config.get_repo = Mock(return_value=Mock())
281
+
282
+ # Mock subprocess.run to return files without matching extensions
283
+ mock_result = Mock()
284
+ mock_result.returncode = 0
285
+ mock_result.stdout = "force-app/main/default/flows/MyFlow.flow-meta.xml\n"
286
+ mock_subprocess.return_value = mock_result
287
+
288
+ task()
289
+
290
+ assert len(task.return_values["files"]) == 1
291
+ assert task.return_values["file_names"] == set()
292
+
293
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
294
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
295
+ def test_default_base_ref_from_config(self, mock_package_path, mock_subprocess):
296
+ """Test that default base_ref is taken from project config."""
297
+ mock_package_path.return_value = Path("force-app").absolute()
298
+ task = self._create_task() # No base_ref specified
299
+ task.project_config.get_repo = Mock(return_value=Mock())
300
+
301
+ # Mock subprocess.run
302
+ mock_result = Mock()
303
+ mock_result.returncode = 0
304
+ mock_result.stdout = ""
305
+ mock_subprocess.return_value = mock_result
306
+
307
+ task()
308
+
309
+ # Should use default branch from config
310
+ mock_subprocess.assert_called_once_with(
311
+ ["git", "diff", "--name-only", "main"],
312
+ capture_output=True,
313
+ text=True,
314
+ check=False,
315
+ )
316
+
317
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
318
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
319
+ def test_default_base_ref_fallback_to_main(
320
+ self, mock_package_path, mock_subprocess
321
+ ):
322
+ """Test that default base_ref falls back to 'main' if not in config."""
323
+ mock_package_path.return_value = Path("force-app").absolute()
324
+ # Remove default_branch from config
325
+ self.project_config.config["project"]["git"] = {}
326
+ task = self._create_task() # No base_ref specified
327
+ task.project_config.get_repo = Mock(return_value=Mock())
328
+
329
+ # Mock subprocess.run
330
+ mock_result = Mock()
331
+ mock_result.returncode = 0
332
+ mock_result.stdout = ""
333
+ mock_subprocess.return_value = mock_result
334
+
335
+ task()
336
+
337
+ # Should fall back to "main"
338
+ mock_subprocess.assert_called_once_with(
339
+ ["git", "diff", "--name-only", "main"],
340
+ capture_output=True,
341
+ text=True,
342
+ check=False,
343
+ )
344
+
345
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
346
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
347
+ def test_custom_directories_option(self, mock_package_path, mock_subprocess):
348
+ """Test behavior with custom directories option."""
349
+ mock_package_path.return_value = Path("force-app").absolute()
350
+ task = self._create_task(
351
+ {
352
+ "base_ref": "origin/main",
353
+ "directories": ["custom-dir"],
354
+ }
355
+ )
356
+ task.project_config.get_repo = Mock(return_value=Mock())
357
+
358
+ # Mock subprocess.run to return files in custom directory
359
+ mock_result = Mock()
360
+ mock_result.returncode = 0
361
+ mock_result.stdout = "custom-dir/classes/MyClass.cls\n"
362
+ mock_subprocess.return_value = mock_result
363
+
364
+ task()
365
+
366
+ assert task.return_values["files"] == ["custom-dir/classes/MyClass.cls"]
367
+
368
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
369
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
370
+ def test_filter_package_changed_files_adds_default_package_dir(
371
+ self, mock_package_path, mock_subprocess
372
+ ):
373
+ """Test that default package directory is added to directories list."""
374
+ mock_package_path.return_value = Path("custom-package").absolute()
375
+ task = self._create_task(
376
+ {
377
+ "base_ref": "origin/main",
378
+ "directories": ["force-app"],
379
+ }
380
+ )
381
+ task.project_config.get_repo = Mock(return_value=Mock())
382
+
383
+ # Mock subprocess.run
384
+ mock_result = Mock()
385
+ mock_result.returncode = 0
386
+ mock_result.stdout = "custom-package/classes/MyClass.cls\n"
387
+ mock_subprocess.return_value = mock_result
388
+
389
+ task()
390
+
391
+ # Should include custom-package directory
392
+ assert task.return_values["files"] == ["custom-package/classes/MyClass.cls"]
393
+
394
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
395
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
396
+ def test_filter_package_changed_files_windows_paths(
397
+ self, mock_package_path, mock_subprocess
398
+ ):
399
+ """Test filtering with Windows-style paths."""
400
+ mock_package_path.return_value = Path("force-app").absolute()
401
+ task = self._create_task({"base_ref": "origin/main"})
402
+ task.project_config.get_repo = Mock(return_value=Mock())
403
+
404
+ # Mock subprocess.run to return Windows-style paths
405
+ mock_result = Mock()
406
+ mock_result.returncode = 0
407
+ mock_result.stdout = "force-app\\main\\default\\classes\\MyClass.cls\n"
408
+ mock_subprocess.return_value = mock_result
409
+
410
+ task()
411
+
412
+ assert len(task.return_values["files"]) == 1
413
+
414
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
415
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
416
+ def test_logging_with_few_files(self, mock_package_path, mock_subprocess):
417
+ """Test logging when there are few files (<= 20)."""
418
+ mock_package_path.return_value = Path("force-app").absolute()
419
+ task = self._create_task({"base_ref": "origin/main"})
420
+ task.project_config.get_repo = Mock(return_value=Mock())
421
+
422
+ # Mock subprocess.run to return 5 files
423
+ mock_result = Mock()
424
+ mock_result.returncode = 0
425
+ mock_result.stdout = "\n".join(
426
+ [f"force-app/main/default/classes/Class{i}.cls" for i in range(5)]
427
+ )
428
+ mock_subprocess.return_value = mock_result
429
+
430
+ task()
431
+
432
+ assert len(task.return_values["files"]) == 5
433
+
434
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
435
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
436
+ def test_logging_with_many_files(self, mock_package_path, mock_subprocess):
437
+ """Test logging when there are many files (> 20)."""
438
+ mock_package_path.return_value = Path("force-app").absolute()
439
+ task = self._create_task({"base_ref": "origin/main"})
440
+ task.project_config.get_repo = Mock(return_value=Mock())
441
+
442
+ # Mock subprocess.run to return 25 files
443
+ mock_result = Mock()
444
+ mock_result.returncode = 0
445
+ mock_result.stdout = "\n".join(
446
+ [f"force-app/main/default/classes/Class{i}.cls" for i in range(25)]
447
+ )
448
+ mock_subprocess.return_value = mock_result
449
+
450
+ task()
451
+
452
+ assert len(task.return_values["files"]) == 25
453
+
454
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
455
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
456
+ def test_logging_with_few_file_names(self, mock_package_path, mock_subprocess):
457
+ """Test logging when there are few file names (<= 20)."""
458
+ mock_package_path.return_value = Path("force-app").absolute()
459
+ task = self._create_task(
460
+ {
461
+ "base_ref": "origin/main",
462
+ "file_extensions": ["cls"],
463
+ }
464
+ )
465
+ task.project_config.get_repo = Mock(return_value=Mock())
466
+
467
+ # Mock subprocess.run to return 5 files
468
+ mock_result = Mock()
469
+ mock_result.returncode = 0
470
+ mock_result.stdout = "\n".join(
471
+ [f"force-app/main/default/classes/Class{i}.cls" for i in range(5)]
472
+ )
473
+ mock_subprocess.return_value = mock_result
474
+
475
+ task()
476
+
477
+ assert len(task.return_values["file_names"]) == 5
478
+
479
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
480
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
481
+ def test_logging_with_many_file_names(self, mock_package_path, mock_subprocess):
482
+ """Test logging when there are many file names (> 20)."""
483
+ mock_package_path.return_value = Path("force-app").absolute()
484
+ task = self._create_task(
485
+ {
486
+ "base_ref": "origin/main",
487
+ "file_extensions": ["cls"],
488
+ }
489
+ )
490
+ task.project_config.get_repo = Mock(return_value=Mock())
491
+
492
+ # Mock subprocess.run to return 25 files
493
+ mock_result = Mock()
494
+ mock_result.returncode = 0
495
+ mock_result.stdout = "\n".join(
496
+ [f"force-app/main/default/classes/Class{i}.cls" for i in range(25)]
497
+ )
498
+ mock_subprocess.return_value = mock_result
499
+
500
+ task()
501
+
502
+ assert len(task.return_values["file_names"]) == 25
503
+
504
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
505
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
506
+ def test_multiple_files_mixed_directories(self, mock_package_path, mock_subprocess):
507
+ """Test filtering with multiple files in different directories."""
508
+ mock_package_path.return_value = Path("force-app").absolute()
509
+ task = self._create_task({"base_ref": "origin/main"})
510
+ task.project_config.get_repo = Mock(return_value=Mock())
511
+
512
+ # Mock subprocess.run to return files in different locations
513
+ mock_result = Mock()
514
+ mock_result.returncode = 0
515
+ mock_result.stdout = (
516
+ "force-app/main/default/classes/MyClass.cls\n"
517
+ "src/classes/OtherClass.cls\n"
518
+ "README.md\n"
519
+ "force-app/main/default/flows/MyFlow.flow\n"
520
+ )
521
+ mock_subprocess.return_value = mock_result
522
+
523
+ task()
524
+
525
+ # Should only include files from force-app and src
526
+ assert len(task.return_values["files"]) == 3
527
+ assert "README.md" not in task.return_values["files"]
528
+
529
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
530
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
531
+ def test_extract_file_names_with_meta_xml(self, mock_package_path, mock_subprocess):
532
+ """Test extracting file names from .cls-meta.xml files."""
533
+ mock_package_path.return_value = Path("force-app").absolute()
534
+ task = self._create_task(
535
+ {
536
+ "base_ref": "origin/main",
537
+ "file_extensions": ["cls-meta.xml"],
538
+ }
539
+ )
540
+ task.project_config.get_repo = Mock(return_value=Mock())
541
+
542
+ # Mock subprocess.run to return .cls-meta.xml files
543
+ mock_result = Mock()
544
+ mock_result.returncode = 0
545
+ mock_result.stdout = "force-app/main/default/classes/MyClass.cls-meta.xml\n"
546
+ mock_subprocess.return_value = mock_result
547
+
548
+ task()
549
+
550
+ assert task.return_values["file_names"] == {"MyClass"}
551
+
552
+ @mock.patch("cumulusci.vcs.utils.list_modified_files.subprocess.run")
553
+ @patch.object(BaseProjectConfig, "default_package_path", new_callable=PropertyMock)
554
+ def test_git_diff_with_whitespace(self, mock_package_path, mock_subprocess):
555
+ """Test handling of git diff output with extra whitespace."""
556
+ mock_package_path.return_value = Path("force-app").absolute()
557
+ task = self._create_task({"base_ref": "origin/main"})
558
+ task.project_config.get_repo = Mock(return_value=Mock())
559
+
560
+ # Mock subprocess.run to return files with whitespace
561
+ mock_result = Mock()
562
+ mock_result.returncode = 0
563
+ mock_result.stdout = (
564
+ " force-app/main/default/classes/MyClass.cls \n"
565
+ "\n"
566
+ " force-app/main/default/classes/OtherClass.cls \n"
567
+ )
568
+ mock_subprocess.return_value = mock_result
569
+
570
+ task()
571
+
572
+ # Whitespace should be stripped
573
+ assert len(task.return_values["files"]) == 2
574
+ assert all(" " not in f for f in task.return_values["files"])
575
+
576
+ def test_init_options_sets_default_base_ref(self):
577
+ """Test that _init_options sets default base_ref from config."""
578
+ task = self._create_task()
579
+
580
+ # base_ref should be set from config
581
+ assert task.parsed_options.base_ref == "main"
582
+
583
+ def test_init_options_preserves_provided_base_ref(self):
584
+ """Test that _init_options preserves provided base_ref."""
585
+ task = self._create_task({"base_ref": "origin/develop"})
586
+
587
+ # base_ref should be preserved
588
+ assert task.parsed_options.base_ref == "origin/develop"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cumulusci-plus
3
- Version: 5.0.21
3
+ Version: 5.0.35
4
4
  Summary: Build and release tools for Salesforce developers
5
5
  Project-URL: Homepage, https://github.com/jorgesolebur/CumulusCI
6
6
  Project-URL: Changelog, https://cumulusci.readthedocs.io/en/stable/history.html
@@ -22,7 +22,7 @@ Requires-Python: >=3.11
22
22
  Requires-Dist: click>=8.1
23
23
  Requires-Dist: cryptography
24
24
  Requires-Dist: defusedxml
25
- Requires-Dist: docutils==0.21.2
25
+ Requires-Dist: docutils<=0.21.2
26
26
  Requires-Dist: faker
27
27
  Requires-Dist: fs
28
28
  Requires-Dist: github3-py
@@ -32,9 +32,10 @@ Requires-Dist: lxml
32
32
  Requires-Dist: markupsafe
33
33
  Requires-Dist: packaging>=23.0
34
34
  Requires-Dist: psutil
35
- Requires-Dist: pydantic<2
35
+ Requires-Dist: pydantic<3,>=2.0
36
36
  Requires-Dist: pyjwt
37
37
  Requires-Dist: python-dateutil
38
+ Requires-Dist: python-dotenv
38
39
  Requires-Dist: pytz
39
40
  Requires-Dist: pyyaml
40
41
  Requires-Dist: requests
@@ -49,17 +50,18 @@ Requires-Dist: salesforce-bulk
49
50
  Requires-Dist: sarge
50
51
  Requires-Dist: selenium<4
51
52
  Requires-Dist: simple-salesforce==1.11.4
52
- Requires-Dist: snowfakery>=4.0.0
53
+ Requires-Dist: snowfakery>=4.1.0
53
54
  Requires-Dist: sqlalchemy<2
54
55
  Requires-Dist: xmltodict
55
56
  Provides-Extra: select
56
57
  Requires-Dist: annoy; extra == 'select'
58
+ Requires-Dist: boto3; extra == 'select'
57
59
  Requires-Dist: numpy; extra == 'select'
58
60
  Requires-Dist: pandas; extra == 'select'
59
61
  Requires-Dist: scikit-learn; extra == 'select'
60
62
  Description-Content-Type: text/markdown
61
63
 
62
- # CumulusCI
64
+ # CumulusCI Plus
63
65
 
64
66
  [![Code Coverage](https://coveralls.io/repos/github/SFDO-Tooling/CumulusCI/badge.svg?branch=main)](https://coveralls.io/github/SFDO-Tooling/CumulusCI?branch=main)
65
67
  [![PyPI](https://img.shields.io/pypi/v/cumulusci)](https://pypi.org/project/cumulusci-plus/)
@@ -127,14 +129,14 @@ license](https://github.com/SFDO-Tooling/CumulusCI/blob/main/LICENSE)
127
129
  and is not covered by the Salesforce Master Subscription Agreement.
128
130
 
129
131
  <!-- Changelog -->
130
- ## v5.0.21 (2025-09-08)
131
-
132
+ ## v5.0.35 (2025-11-20)
132
133
  <!-- Release notes generated using configuration in .github/release.yml at main -->
133
134
 
134
135
  ## What's Changed
135
-
136
136
  ### Changes
137
+ * Feature/pm 2198 by [@rupeshjSFDC](https://github.com/rupeshjSFDC) in [#129](https://github.com/jorgesolebur/CumulusCI/pull/129)
138
+ * Improvements with SFDMU when exporting data from org with namespace injection by [@jorgesolebur](https://github.com/jorgesolebur) in [#131](https://github.com/jorgesolebur/CumulusCI/pull/131)
139
+ * Rename project from CumulusCI to CumulusCI Plus by [@rupeshjSFDC](https://github.com/rupeshjSFDC) in [#133](https://github.com/jorgesolebur/CumulusCI/pull/133)
137
140
 
138
- - Fix profile query from tooling api field to simple salesforce. by [@rupeshjSFDC](https://github.com/rupeshjSFDC) in [#67](https://github.com/jorgesolebur/CumulusCI/pull/67)
139
141
 
140
- **Full Changelog**: https://github.com/jorgesolebur/CumulusCI/compare/v5.0.20...v5.0.21
142
+ **Full Changelog**: https://github.com/jorgesolebur/CumulusCI/compare/v5.0.34...v5.0.35