acceptance: schema-driven invariant fuzzing for bundle configs#5686
Draft
radakam wants to merge 35 commits into
Draft
acceptance: schema-driven invariant fuzzing for bundle configs#5686radakam wants to merge 35 commits into
radakam wants to merge 35 commits into
Conversation
Collaborator
Integration test reportCommit: 9a4e181
8 interesting tests: 4 RECOVERED, 4 SKIP
Top 10 slowest tests (at least 2 minutes):
|
Contributor
Approval status: pending
|
denik
reviewed
Jun 26, 2026
The nightly test-fuzz job is intentionally excluded from test-result, so a failure was only visible in the Actions tab. Add a failure step that opens (or comments on) a single deduped GitHub issue with a one-command repro. Also correct the jobsCreatePath comment: a different API version shows up as a capture failure (the testserver registers only this route, so a mismatched version 404s and the deploy fails), not as a payload diff.
…ion test Rename the capture/deploy/recorder helpers to *_test.go so the parity harness compiles only under `go test` instead of into the package's regular build, and add a committed regression test (cluster_fixups_test.go) covering the single-node task-cluster num_workers force-send fix so the divergence is guarded at PR time, not just in the nightly suite.
…ting Move the remaining generator/diff/rand implementation into _test.go files (keeping only a doc.go for the package comment) so nothing in the harness compiles into the regular build, since no product code imports it. Distinguish deploy/capture failures from create-payload divergences in checkJobParity: skip when neither engine deploys the generated config, fail distinctly when exactly one engine accepts it (an acceptance divergence, not a payload diff), and only diff payloads when both deploys succeed. This keeps nightly triage from misdirecting a deploy failure into regressionSeeds. Also document the unique-identity-key assumption in diffKeyedSlice.
Use strings.SplitSeq instead of ranging over strings.Split (modernize stringsseq) and require.Positivef instead of require.Greaterf(t, n, 0) (testifylint negative-positive).
The failure-reporting step used `gh issue list --jq '.[0].number'`, which prints the literal "null" when no open issue exists, so it always took the comment branch and tried to comment on issue "null" instead of creating one. Use `// empty` so the create branch runs on the first divergence.
Revert the num_workers single-node task-cluster fix along with its unit test and acceptance updates so this PR adds only the parity harness. Both terraform/direct divergences the harness found are now documented and suppressed via DefaultIgnorePaths rather than fixed (fixes follow separately): num_workers on single-node task clusters (seed 29) and the spark.databricks.delta.preview.enabled spark conf key.
Address review feedback on the create-payload parity harness: - Replace the path-only ignore list with value-conditional ignore rules so the documented num_workers divergence (direct omits, terraform force-sends 0) is suppressed only for that exact shape; a real value mismatch at the same path now fails again. - Unexport package-internal identifiers (generateJob, diffPayloads, difference, defaultIgnoreRules) that are only used within the package. - Document why TestCaptureJobCreateDirect is intentionally not opt-in. - Reword the one-sided-deploy failures as deploy/capture differences rather than asserting one engine "rejected" the config. - Make TestParitySeeds hermetic against ambient FUZZ_* env vars. - Correct the seed 29 comment to reflect that the divergence is suppressed.
The terraform provider force-sends num_workers:0 for a single-node new_cluster on task-level clusters too, not just shared job_clusters, but prepareJobSettingsForUpdate only applied initializeNumWorkers to job_clusters. The direct engine therefore omitted num_workers on task clusters and the two engines produced divergent create payloads (found by the bundle/fuzz parity harness, seed 29). Apply initializeNumWorkers to task new_cluster too so the direct engine matches terraform, drop the now-obsolete tasks[*].new_cluster.num_workers ignore entry, and simplify the fuzz ignore list to a plain []string now that value-conditional matching is no longer needed.
Switch the fuzz suite from comparing terraform and direct create payloads to asserting invariants on the direct engine's payload. Terraform and direct can disagree for legitimate reasons, so a payload diff is noisy; an invariant has no legitimate reason to fail, so a failure is a real bug. This drops the payload diff and its ignore-list of documented divergences, and removes terraform from the harness (each seed is now one in-process direct deploy). Gate on `bundle validate` so the suite distinguishes the two fuzzing outcomes: an invalid config skips (it can't violate an invariant), while a validated config that fails to deploy or breaks an invariant fails. This is the distinction a looser, schema-driven generator will rely on. Revert the num_workers:0 force-send for single-node task clusters (and its acceptance goldens): it only matched terraform's payload, with no demonstrated behavior benefit, and direct has shipped without it. If a real backend requirement is confirmed, it can return as a standalone change.
…uzzing Drop the terraform/direct create-payload parity package in favor of fuzzing the existing acceptance/bundle/invariant framework, which already checks invariants across all resource types and is prepped for fuzzing via its INPUT_CONFIG_OK contract. - add acceptance/bin/gen_fuzz_config.py: a seeded generator that walks the bundle schema and emits a random databricks.yml for any resource type - add acceptance/bundle/invariant/fuzz: generates configs over a seed window and asserts the CLI never panics; the no-drift invariant is opt-in (FUZZ_CHECK_DRIFT) for the nightly wide-window run - point task test-fuzz and the nightly job at the new variant - remove bundle/fuzz and its parity harness
- Correct misleading comments: the nightly test-fuzz job runs the same local harness against the fake server (wider seed window + drift on), not a real workspace. - Run config generation inside the per-seed subshell so a generator crash also prints the "reproduce with" hint. - Document the schema-driven fuzz subdir in the invariant README, including that a failure is a real CLI bug and how to reproduce it. - Drop the unused name hint in gen_config (objects ignore it).
Make the comments across the schema fuzz harness more concise while keeping the non-obvious "why" context.
Mirror the integration-test flow: comment on the PR that introduced the failing commit rather than opening/deduping a tracking issue.
…ating it Extract the no_drift deploy/drift/destroy body into a shared no_drift.sh sourced by both the no_drift test and the fuzzer, so the invariant lives in one place and other invariant tests can be fuzzed the same way.
A rejected config never deploys, so emitting the marker made the fuzzer read the re-plan's "needs create" as drift.
The fuzzer runs the shared no_drift.sh body with errexit off and classifies each seed from the captured exit code. The drift block ended with a no-panic check that reset $? to 0, so a config that deployed cleanly but drifted was silently treated as a pass. Accumulate the drift assertions into drift_rc and return it instead. The curated no_drift test (errexit on) is unaffected. Also make verify_no_drift.py fail cleanly on empty/unparseable plan output (when bundle plan itself failed) instead of crashing with a traceback, and tighten the fuzz harness comments.
Extract the migrate invariant body into a shared migrate.sh (mirroring no_drift.sh) and have the fuzzer source ../$FUZZ_INVARIANT.sh so it can exercise any invariant. Wire up FUZZ_INVARIANT=[no_drift, migrate] so the schema fuzzer now also stress-tests the Terraform->direct migration on random configs. The fuzzer's panic scan now globs LOG.* rather than naming LOG.validate/LOG.deploy, since different bodies write different logs.
CatalogsCreate only echoed a subset of the create request, so a re-read returned null for connection_name, managed_encryption_settings, and custom_max_retention_hours. Because connection_name is recreate_on_changes (immutable), the schema fuzzer's no_drift invariant saw a perpetual recreate; the others showed as update drift. Persist these fields on create so the re-read matches the deployed config. Also clamp the fuzzer's custom_max_retention_hours to UC-valid values (0 or 168-720 hours) so generated catalog configs deploy.
…variants Broaden the fuzz invariant matrix beyond no_drift/migrate with four more schema-driven invariant bodies, each selectable via FUZZ_INVARIANT and following the existing INPUT_CONFIG_OK / SKIP_DRIFT_CHECK contract: - redeploy.sh: deploy twice; the second deploy must be a clean no-op, which exercises the write path twice and catches create handlers that don't round-trip their inputs. - canonical.sh: `bundle validate -o json` must be byte-identical across two runs; guards against nondeterministic serialization. Cloud-independent, so it always runs (not gated behind SKIP_DRIFT_CHECK). - update.sh: edit a comment/description and assert the redeploy is an in-place update (not a recreate) that converges with no drift. Configs without an editable field are skipped before the marker (treated as a rejection). - destroy_recreate.sh: deploy then destroy; a re-plan must want to create everything again, proving destroy left no orphaned state. Add two stdlib-only helpers: edit_fuzz_config.py (flips one comment/description scalar via a line match, no YAML dependency) and verify_plan_action.py (asserts a plan shows the expected action, mirroring bundle/deployplan/action.go).
Add common.sh with invariant_deploy and _invariant_cleanup so the deploy-based bodies (no_drift, redeploy, update, destroy_recreate) and migrate no longer duplicate the validate/deploy/cleanup prologue. Unify the panic check on '!panic:' across all invariant bodies and fuzz/script so a random generated token containing "panic" can't be a false positive. Also fold in the supporting helpers: check_schema_types.py (fail loud on a schema type the generator can't produce), gen_fuzz_config_check.py plus its selftest (to_yaml contract), util.load_plan shared by the verify_* scripts, and a pass to shorten comments across the harness.
The comment describes connection_name, managed_encryption_settings and custom_max_retention_hours, which are the fields the fix added. It sat above StorageRoot/ProviderName/ShareName, which were already round-tripped. Move it directly above the three added fields so it annotates them.
Replace the shared `<name>.sh` invariant bodies (sourced via FUZZ_INVARIANT and common.sh) with self-contained invariant test directories that double as fuzz targets, selected by FUZZ_TARGET. Each target runs over the curated INPUT_CONFIG matrix and, when FUZZ_SEED is set, against a schema-generated random config. - Inline the deploy/drift logic into no_drift/script and migrate/script - Re-add redeploy, canonical, update, and destroy_recreate as invariant dirs - Drop common.sh and the standalone *.sh bodies - Point fuzz/script at ../$FUZZ_TARGET/script and refresh the README
The origin/main merge added the volume_path_job_ref.yml.tmpl fuzz template to the INPUT_CONFIG matrix, but the canonical/destroy_recreate/redeploy/update out.test.toml goldens were not regenerated, failing the "changed or new files" CI guard.
The generator produced random catalog_name/schema_name values and random or
empty grants. The fake test server accepts them, but real UC rejects them
(CATALOG_DOES_NOT_EXIST, invalid principal/privilege), so such configs deploy
locally yet drift or fail on cloud, masking real invariant coverage.
Pin catalog_name to "main" and schema_name to "default" (the seeded objects
used by the curated invariant configs), and emit one known-good grant per
grant-bearing securable type ("account users" plus a privilege valid for that
type). This removes the spurious grants drift and the reference-rejection class.
Extend the invariant fuzzer to emit more than one resource per config
(--resource-count, matrixed as FUZZ_RESOURCE_COUNT) and link two of them
with a ${resources.*} reference so the interpolation and deploy-ordering
paths are exercised. The reference targets an input identity field
(name/display_name) so it resolves for every resource type and converges
without drift.
Also improve generated-config validity: skip output-only/computed fields
(x-databricks-field-behaviors OUTPUT_ONLY, readOnly, and a name list) to
avoid false drift after migrate, emit valid permissions per resource type,
force prevent_destroy=false so destroy_recreate can run, and add required
fields the schema omits for registered_models.
Gate the resource key/name suffix on the resource index, not the total count, so a given seed produces the same first resource whether --resource-count is 1 or greater. Later resources stay index-suffixed to remain unique.
inject_cross_ref stopped after a single ${resources.*} edge, so a config
with N resources still exercised only one reference. Link each resource to
an earlier one instead, keeping the graph acyclic so deploy can order it,
and add resource-count 3 to the matrix so the multi-link path runs in CI.
test.toml already lists resource count 3; regenerate the recorded matrix to match so the acceptance framework does not flag a mismatch.
Point file_path/source_code_path fields at the staged data/. fixtures and pin typed-string fields so generated configs deploy instead of getting rejected. Also ignore the local .fuzztmp/ driver scratch directory.
Add a mutate engine (mutate_fuzz_config.py) that applies seeded delete/set/dangerous-value mutations to a curated base config, and a fuzz_gen_config.py dispatcher selected by FUZZ_MODE (generate|mutate). Wire the dispatcher into the six invariant target scripts, matrix FUZZ_MODE in fuzz/test.toml (excluding redundant mutate runs at resource-count 2/3), and add a selftest covering the YAML loader round-trip and mutation determinism.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
A schema-driven config fuzzer for
acceptance/bundle/invariant/.gen_fuzz_config.pywalks the livedatabricks bundle schemaand emits a random, schema-validdatabricks.yml. Each invariant is a self-contained test directory whosescriptdoubles as a curated test over theINPUT_CONFIGmatrix and a fuzz target selected byFUZZ_TARGET:no_drift— deploy, then no driftmigrate— Terraform deploy, migrate to direct, then no driftredeploy— deploy twice; the second must be a no-opcanonical—validate -o jsonbyte-identical across two runsupdate— edit a description; the redeploy must update in place, not recreatedestroy_recreate— deploy then destroy; a re-plan must recreate everythingFUZZ_RESOURCE_COUNTsets how many resources a config gets; with more than one, two are linked via a${resources.*}reference to exercise interpolation and deploy ordering. To stay deployable and drift-free, the generator pins UC references to seeded objects, emits valid grants and permissions per resource type, skips output-only fields, and forcesprevent_destroy=false. Seeds are deterministic, so any failure reproduces from the printed seed.Why
Random schema-valid configs exercise the full deploy path across every resource type and surface panics and invariant violations real-world configs rarely hit. A rejected config isn't a bug — scripts print
INPUT_CONFIG_OKonce a config deploys, so only a panic or a post-marker failure counts. Every PR runs a small no-panic window; drift checking is opt-in (FUZZ_CHECK_DRIFT) since the fake server accepts configs the real backend rejects.task test-fuzzand the nightly job sweep a wider window and report failures on the PR.Testing
go test ./acceptance -run TestAccept/bundle/invariant— curated invariants across every config.go test ./acceptance -run TestAccept/bundle/invariant/fuzz— generate, deploy, run invariant, destroy; assert no panic.task test-fuzz— wide-window run with drift checking (nightly); reproduce a failure withFUZZ_SEED_START=<seed> FUZZ_SEED_COUNT=1 FUZZ_TARGET=<target> task test-fuzz.