Skip to content

Commit 9e44029

Browse files
Add strict-mode to snowfakery task
1 parent 7fdcc3f commit 9e44029

6 files changed

Lines changed: 162 additions & 8 deletions

File tree

cumulusci/tasks/bulkdata/generate_from_yaml.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ def _init_options(self, kwargs):
9595
self.working_directory = self.options.get("working_directory")
9696
loading_rules = process_list_arg(self.options.get("loading_rules")) or []
9797
self.loading_rules = [Path(path) for path in loading_rules if path]
98+
# These flags may be provided internally by callers (e.g., Snowfakery task)
99+
self.strict_mode = bool(self.options.get("strict_mode", False))
100+
self.validate_only = bool(self.options.get("validate_only", False))
98101

99102
def _generate_data(self, db_url, mapping_file_path, num_records, current_batch_num):
100103
"""Generate all of the data"""
@@ -171,6 +174,7 @@ def generate_data(self, dburl, num_records, current_batch_num):
171174
"project_config": self.project_config,
172175
**self.plugin_options,
173176
},
177+
strict_mode=self.strict_mode or self.validate_only,
174178
)
175179

176180
if (

cumulusci/tasks/bulkdata/snowfakery.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,12 @@ class Snowfakery(BaseSalesforceApiTask):
141141
"Defaults to False."
142142
},
143143
"validate_only": {
144-
"description": "Boolean: if True, only validate the generated mapping against the org schema without loading data. "
144+
"description": "Boolean: if True, validates snowfakery recipe and generated mapping against the org schema without loading data. "
145145
"Defaults to False."
146146
},
147+
"strict_mode": {
148+
"description": "Boolean: If True, validates the Snowfakery recipe and generated mapping against the org schema (strict mode) and then proceeds with the run",
149+
},
147150
}
148151

149152
def _validate_options(self):
@@ -165,6 +168,7 @@ def _validate_options(self):
165168
self.options.get("drop_missing_schema", False)
166169
)
167170
self.validate_only = process_bool_arg(self.options.get("validate_only", False))
171+
self.strict_mode = process_bool_arg(self.options.get("strict_mode", False))
168172

169173
loading_rules = process_list_arg(self.options.get("loading_rules")) or []
170174
self.loading_rules = [Path(path) for path in loading_rules if path]
@@ -614,6 +618,7 @@ def _run_generate_and_load_subtask(
614618
"ignore_row_errors": self.ignore_row_errors,
615619
"drop_missing_schema": self.drop_missing_schema,
616620
"validate_only": validate_only,
621+
"strict_mode": self.strict_mode,
617622
}
618623
subtask_config = TaskConfig({"options": options})
619624
subtask = GenerateAndLoadDataFromYaml(

cumulusci/tasks/bulkdata/tests/test_generate_from_snowfakery_task.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,102 @@ def test_exception_handled_cleanly(self, generate_data):
206206
assert "Foo" in str(e.value)
207207
assert len(generate_data.mock_calls) == 1
208208

209+
@mock.patch("cumulusci.tasks.bulkdata.generate_from_yaml.generate_data")
210+
def test_validate_only_forces_strict_mode(self, generate_data):
211+
with temp_sqlite_database_url() as database_url:
212+
task = _make_task(
213+
GenerateDataFromYaml,
214+
{
215+
"options": {
216+
"generator_yaml": simple_yaml,
217+
"database_url": database_url,
218+
"validate_only": True,
219+
}
220+
},
221+
)
222+
task()
223+
224+
assert len(generate_data.mock_calls) == 1
225+
_, kwargs = generate_data.call_args
226+
assert kwargs["strict_mode"] is True
227+
228+
@mock.patch("cumulusci.tasks.bulkdata.generate_from_yaml.generate_data")
229+
def test_strict_mode_only(self, generate_data):
230+
with temp_sqlite_database_url() as database_url:
231+
task = _make_task(
232+
GenerateDataFromYaml,
233+
{
234+
"options": {
235+
"generator_yaml": simple_yaml,
236+
"database_url": database_url,
237+
"strict_mode": True,
238+
}
239+
},
240+
)
241+
task()
242+
243+
assert len(generate_data.mock_calls) == 1
244+
_, kwargs = generate_data.call_args
245+
assert kwargs["strict_mode"] is True
246+
247+
@mock.patch("cumulusci.tasks.bulkdata.generate_from_yaml.generate_data")
248+
def test_defaults_no_strict_no_validate(self, generate_data):
249+
with temp_sqlite_database_url() as database_url:
250+
task = _make_task(
251+
GenerateDataFromYaml,
252+
{
253+
"options": {
254+
"generator_yaml": simple_yaml,
255+
"database_url": database_url,
256+
}
257+
},
258+
)
259+
task()
260+
261+
assert len(generate_data.mock_calls) == 1
262+
_, kwargs = generate_data.call_args
263+
assert kwargs["strict_mode"] is False
264+
265+
def test_validate_only_runs_and_creates_mapping(self):
266+
with temporary_file_path("mapping.yml") as mapping_path:
267+
with temp_sqlite_database_url() as database_url:
268+
task = _make_task(
269+
GenerateDataFromYaml,
270+
{
271+
"options": {
272+
"generator_yaml": simple_yaml,
273+
"database_url": database_url,
274+
"generate_mapping_file": mapping_path,
275+
"validate_only": True,
276+
}
277+
},
278+
)
279+
task()
280+
281+
assert mapping_path.exists()
282+
mapping = yaml.safe_load(open(mapping_path))
283+
assert mapping # ensure something was written
284+
285+
def test_strict_mode_runs_and_creates_mapping(self):
286+
with temporary_file_path("mapping.yml") as mapping_path:
287+
with temp_sqlite_database_url() as database_url:
288+
task = _make_task(
289+
GenerateDataFromYaml,
290+
{
291+
"options": {
292+
"generator_yaml": simple_yaml,
293+
"database_url": database_url,
294+
"generate_mapping_file": mapping_path,
295+
"strict_mode": True,
296+
}
297+
},
298+
)
299+
task()
300+
301+
assert mapping_path.exists()
302+
mapping = yaml.safe_load(open(mapping_path))
303+
assert mapping # ensure something was written
304+
209305
@mock.patch(
210306
"cumulusci.tasks.bulkdata.generate_and_load_data_from_yaml.GenerateAndLoadDataFromYaml._dataload"
211307
)

cumulusci/tasks/bulkdata/tests/test_snowfakery.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,59 @@ def _run_snowfakery_and_inspect_mapping(**options):
237237
return _run_snowfakery_and_inspect_mapping
238238

239239

240+
@mock.patch("cumulusci.tasks.bulkdata.snowfakery.GenerateAndLoadDataFromYaml")
241+
def test_snowfakery_validate_only_passes_flags(mock_subtask_cls, snowfakery):
242+
mock_subtask = mock.Mock()
243+
mock_subtask.__call__ = mock.Mock(return_value=None)
244+
mock_subtask.return_values = {"validation_result": "ok"}
245+
mock_subtask_cls.return_value = mock_subtask
246+
247+
task = snowfakery(
248+
recipe=str(simple_salesforce_yaml),
249+
validate_only=True,
250+
)
251+
252+
with TemporaryDirectory() as tmpdir:
253+
task._run_generate_and_load_subtask(
254+
Path(tmpdir),
255+
DummyOrgConfig({}, "test"),
256+
options={},
257+
validate_only=True,
258+
)
259+
260+
# task_config passed into subtask should carry validate_only=True and strict_mode flag
261+
call_kwargs = mock_subtask_cls.call_args.kwargs
262+
task_config = call_kwargs["task_config"]
263+
assert task_config.options["validate_only"] is True
264+
assert task_config.options["strict_mode"] is False
265+
266+
267+
@mock.patch("cumulusci.tasks.bulkdata.snowfakery.GenerateAndLoadDataFromYaml")
268+
def test_snowfakery_strict_mode_passes_flags(mock_subtask_cls, snowfakery):
269+
mock_subtask = mock.Mock()
270+
mock_subtask.__call__ = mock.Mock(return_value=None)
271+
mock_subtask.return_values = {"validation_result": "ok"}
272+
mock_subtask_cls.return_value = mock_subtask
273+
274+
task = snowfakery(
275+
recipe=str(simple_salesforce_yaml),
276+
strict_mode=True,
277+
)
278+
279+
with TemporaryDirectory() as tmpdir:
280+
task._run_generate_and_load_subtask(
281+
Path(tmpdir),
282+
DummyOrgConfig({}, "test"),
283+
options={},
284+
validate_only=False,
285+
)
286+
287+
call_kwargs = mock_subtask_cls.call_args.kwargs
288+
task_config = call_kwargs["task_config"]
289+
assert task_config.options["validate_only"] is False
290+
assert task_config.options["strict_mode"] is True
291+
292+
240293
def get_mapping_from_snowfakery_task_results(results: SnowfakeryTaskResults):
241294
"""Find the shared mapping file and return it."""
242295
template_dir = SnowfakeryWorkingDirectory(results.working_dir / "template_1/")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ dependencies = [
5353
"sarge",
5454
"selenium<4",
5555
"simple-salesforce==1.11.4",
56-
"snowfakery>=4.1.0",
56+
"snowfakery @ git+https://github.com/SFDO-Tooling/Snowfakery.git@W-19976090/standard-function-validation",
5757
"xmltodict",
5858
"docutils<=0.21.2",
5959
]

uv.lock

Lines changed: 2 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)