hecks 2.7.0 → 2.8.0
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.
- checksums.yaml +4 -4
- data/exe/hecks +13 -0
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +1 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +5 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +12 -13
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +12 -6
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +1 -1
- data/lib/hecks/projections/deploy/lambda.rb +3 -3
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +1 -1
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/verification.rb +11 -11
- data/lib/hecks/runtime/registry.rb +8 -4
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +6 -1
- metadata +28 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
|
@@ -1,658 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
require "tmpdir"
|
|
3
|
-
require "securerandom"
|
|
4
|
-
|
|
5
|
-
module Hecks
|
|
6
|
-
module Fuzzing
|
|
7
|
-
# A fresh, in-process adapter for every ephemeral boot.
|
|
8
|
-
#
|
|
9
|
-
# ## Why rebind at all
|
|
10
|
-
#
|
|
11
|
-
# Fuzzing/replay copies a domain to a tmpdir and boots from there
|
|
12
|
-
# specifically to get zero-history state — `rm_rf`ing the copy's own
|
|
13
|
-
# `data/` achieves that for a file-based adapter (Memory,
|
|
14
|
-
# SqlitePersistence, Heki), because copying the directory copies the
|
|
15
|
-
# store. It achieves nothing for an adapter that lives outside the
|
|
16
|
-
# copied directory entirely — Postgres, named by a fixed connection
|
|
17
|
-
# string in `.world` (examples/pizzas/bluebook/pizzas.world, for
|
|
18
|
-
# instance). Copying the directory does not copy or isolate the
|
|
19
|
-
# database, so a "fresh" boot against a Postgres-bound domain would
|
|
20
|
-
# still see every record any other run, ever, wrote to it.
|
|
21
|
-
#
|
|
22
|
-
# So every `.hecksagon` in the copy gets its persistence binding
|
|
23
|
-
# rewritten to Memory before booting, and any `projected_by` bind
|
|
24
|
-
# dropped outright (optional — `Registry#read_repository` already
|
|
25
|
-
# falls back to the authoritative repository when none exists). The
|
|
26
|
-
# domain's own rules and shape are untouched ; only which adapter this
|
|
27
|
-
# one ephemeral copy answers through changes. What the domain is
|
|
28
|
-
# bound to for real deployment is never touched — only this tmp copy.
|
|
29
|
-
#
|
|
30
|
-
# ## `adapter:` modes
|
|
31
|
-
#
|
|
32
|
-
# `adapter:` (PRD 02) picks which real persistence one ephemeral boot
|
|
33
|
-
# answers through. `:memory` is the default and the only mode every
|
|
34
|
-
# caller that passes no `adapter:` still gets. `:sqlite` rebinds to
|
|
35
|
-
# the real SQLite adapter instead of the in-memory one: 15 declared
|
|
36
|
-
# properties (properties.rb) and every fuzz/replay run only ever
|
|
37
|
-
# exercised Memory's own hand-written repository until this mode
|
|
38
|
-
# existed, and `spec/adapters/query_agreement_spec.rb` already found 4
|
|
39
|
-
# shipped query bugs from exactly that comparison, on a fixed corpus
|
|
40
|
-
# far smaller than what the fuzzer generates. `:sqlite` rewrites to
|
|
41
|
-
# `"SqlitePersistence"` — the name
|
|
42
|
-
# `lib/hecks/adapters/driven/sqlite.adapter` actually registers under
|
|
43
|
-
# (`Sqlite` is the class; `SqlitePersistence` is a thin subclass
|
|
44
|
-
# that's the one real port binding names) — and needs no `.world`
|
|
45
|
-
# settings at all, the same reason Memory needs none:
|
|
46
|
-
# `Sqlite#resolve_path` already defaults an
|
|
47
|
-
# unbound `database` setting to `"data/<table>.db"` relative to
|
|
48
|
-
# `root:`, which `Hecks.boot(copy)` passes as this ephemeral copy's
|
|
49
|
-
# own directory — a fresh, empty `data/` per run, exactly like
|
|
50
|
-
# Memory's own zero-history guarantee, just backed by a real SQLite
|
|
51
|
-
# file instead of a Hash. `:postgres` (PRD 02, docs/prds/02-fuzzer-
|
|
52
|
-
# real-adapters.md) and `:postgres_era` (`rebind_to_postgres_era!`'s
|
|
53
|
-
# own header, below) are the two real-Postgres modes. Each writes its
|
|
54
|
-
# own fresh `.world` per `.hecksagon` rather than relying on the
|
|
55
|
-
# zero-config default Sqlite/Memory get, and each needs a real,
|
|
56
|
-
# reachable Postgres server: `:postgres` sources one shared,
|
|
57
|
-
# permanent scratch database/schema this module itself owns (see
|
|
58
|
-
# `FUZZ_POSTGRES_DATABASE`'s own header); `:postgres_era` instead
|
|
59
|
-
# requires the caller to supply (and own the lifecycle of) its own
|
|
60
|
-
# throwaway `database:`/`schema:`, since its only caller
|
|
61
|
-
# (`bin/qa_sweep --persistence-parity`) already has to manage a
|
|
62
|
-
# disposable database of its own, never a shared one this module could
|
|
63
|
-
# safely default to.
|
|
64
|
-
module IsolatedBoot
|
|
65
|
-
module_function
|
|
66
|
-
|
|
67
|
-
# PRD 02 (docs/future-features.md) — `adapter:` picks which real
|
|
68
|
-
# persistence this one ephemeral boot answers through, not just
|
|
69
|
-
# Memory. `:memory` is the original, zero-config behavior (every
|
|
70
|
-
# other caller in this codebase that doesn't pass `adapter:` gets
|
|
71
|
-
# exactly what it always got). `:sqlite` is nearly as cheap —
|
|
72
|
-
# `Adapters::Sqlite#resolve_path` already defaults to
|
|
73
|
-
# `data/<table>.db` under the copy's own root with no settings at
|
|
74
|
-
# all, the same "needs nothing declared" property Memory has, so a
|
|
75
|
-
# real on-disk B-tree gets exercised for free. `:postgres` is the
|
|
76
|
-
# expensive one — a real server, a real round trip per dispatch —
|
|
77
|
-
# so `bin/fuzz --adapter postgres` is meant to run with smaller
|
|
78
|
-
# seed/step counts than the Memory default, not as a like-for-like
|
|
79
|
-
# swap; see that flag's own comment.
|
|
80
|
-
# `database:`/`schema:` are only meaningful for `adapter: :postgres_era`
|
|
81
|
-
# — see `rebind_to_postgres_era!`'s own header for why that mode takes
|
|
82
|
-
# caller-supplied connection identity instead of a hardcoded shared
|
|
83
|
-
# constant the way `:postgres` does. Every other adapter ignores both;
|
|
84
|
-
# accepting them unconditionally here (rather than a separate method
|
|
85
|
-
# signature per adapter) keeps `SequenceGenerator`/`Replay`'s own
|
|
86
|
-
# single passthrough (`adapter:`, now joined by these two) uniform
|
|
87
|
-
# across all four modes.
|
|
88
|
-
#
|
|
89
|
-
# @param domain_path [String] path to the real domain directory to copy and isolate
|
|
90
|
-
# @param adapter [Symbol] which persistence the copy answers through: `:memory`
|
|
91
|
-
# (default), `:sqlite`, `:postgres`, or `:postgres_era`
|
|
92
|
-
# @param database [String, nil] the database name, required only for
|
|
93
|
-
# `adapter: :postgres_era`; ignored by every other adapter
|
|
94
|
-
# @param schema [String, nil] the schema name, required only for
|
|
95
|
-
# `adapter: :postgres_era`; ignored by every other adapter
|
|
96
|
-
# @param scratch [Hash{Symbol => String}] `:database` and/or `:schema` for
|
|
97
|
-
# `adapter: :postgres` to use instead of the shared fuzz scratch schema, so a caller
|
|
98
|
-
# such as `bin/bench` never drops the schema a concurrent `bin/fuzz` is using;
|
|
99
|
-
# ignored by every other adapter
|
|
100
|
-
# @yield [String] the freshly rebound copy's root directory, ready to boot
|
|
101
|
-
# @yieldreturn [Object] anything; becomes this method's own return value
|
|
102
|
-
# @return [Object] whatever the given block returns
|
|
103
|
-
# @raise [ArgumentError] if `adapter` is not `:memory`, `:sqlite`, `:postgres`, or
|
|
104
|
-
# `:postgres_era`, or if `adapter: :postgres_era` is given without both
|
|
105
|
-
# `database:` and `schema:`
|
|
106
|
-
def call(domain_path, adapter: :memory, database: nil, schema: nil, scratch: {})
|
|
107
|
-
Dir.mktmpdir("hecks-fuzz") do |tmp|
|
|
108
|
-
copy = File.join(tmp, File.basename(domain_path))
|
|
109
|
-
copy_dereferencing(domain_path, copy)
|
|
110
|
-
FileUtils.rm_rf(File.join(copy, "data"))
|
|
111
|
-
case adapter
|
|
112
|
-
when :memory then rebind_to_memory!(copy)
|
|
113
|
-
when :sqlite then rebind_to_sqlite!(copy)
|
|
114
|
-
when :postgres then rebind_to_postgres!(copy, **scratch)
|
|
115
|
-
when :postgres_era then rebind_to_postgres_era!(copy, database: database, schema: schema)
|
|
116
|
-
else raise ArgumentError,
|
|
117
|
-
"unknown fuzz adapter #{adapter.inspect} — :memory, :sqlite, :postgres, or :postgres_era"
|
|
118
|
-
end
|
|
119
|
-
yield copy
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Symlinks are followed, not copied. `FileUtils.cp_r` reproduces a
|
|
124
|
-
# symlink as a symlink, and a relative one then points at nothing
|
|
125
|
-
# from a tmpdir — `lib/hecks/framework/bluebook/compliance
|
|
126
|
-
# .bluebook` is exactly that, a link to
|
|
127
|
-
# `examples/compliance/bluebook/compliance.bluebook`, so the whole
|
|
128
|
-
# framework domain failed to boot here with a LoadError naming a
|
|
129
|
-
# path under /var/folders that had never existed. Nothing about the
|
|
130
|
-
# domain was wrong; the copy was.
|
|
131
|
-
#
|
|
132
|
-
# `FileUtils.cp` follows a symlink and copies its content, which is
|
|
133
|
-
# what an isolated boot wants: the copy has to stand alone, since
|
|
134
|
-
# rebind! rewrites files in it and must not reach back through a
|
|
135
|
-
# link into the real tree.
|
|
136
|
-
#
|
|
137
|
-
# The vendored bluebook packages the domain's hecksagons name with
|
|
138
|
-
# `uses_embryonaut_bluebook` are carried too — see `carry_vendored_bluebooks!`.
|
|
139
|
-
#
|
|
140
|
-
# @param source [String] the real domain directory to copy
|
|
141
|
-
# @param destination [String] the tmpdir path to copy it into; created if missing
|
|
142
|
-
# @return [void]
|
|
143
|
-
def copy_dereferencing(source, destination)
|
|
144
|
-
copy_files(source, destination)
|
|
145
|
-
carry_vendored_bluebooks!(source, destination)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# Copies every file under `source` into `destination`, following symlinks.
|
|
149
|
-
#
|
|
150
|
-
# @param source [String] the directory to copy
|
|
151
|
-
# @param destination [String] the path to copy it into; created if missing
|
|
152
|
-
# @return [void]
|
|
153
|
-
def copy_files(source, destination)
|
|
154
|
-
FileUtils.mkdir_p(destination)
|
|
155
|
-
Dir.glob(File.join(source, "**", "*"), File::FNM_DOTMATCH).each do |path|
|
|
156
|
-
next if [".", ".."].include?(File.basename(path))
|
|
157
|
-
|
|
158
|
-
target = File.join(destination, path.delete_prefix("#{source}/"))
|
|
159
|
-
if File.directory?(path)
|
|
160
|
-
FileUtils.mkdir_p(target)
|
|
161
|
-
else
|
|
162
|
-
FileUtils.mkdir_p(File.dirname(target))
|
|
163
|
-
begin
|
|
164
|
-
FileUtils.cp(path, target)
|
|
165
|
-
rescue Errno::ENOENT
|
|
166
|
-
# A transient sibling (an adapter's atomic-write `.tmp.<pid>`
|
|
167
|
-
# file, caught mid-rename by the glob above) can vanish
|
|
168
|
-
# between listing and copy — under parallel_r-spec two
|
|
169
|
-
# workers share the real example tree. A file that no longer
|
|
170
|
-
# exists was never part of the state this copy needs.
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Copies the vendored bluebook packages the copy's hecksagons name into
|
|
177
|
-
# the place a boot of the copy looks for them.
|
|
178
|
-
#
|
|
179
|
-
# `Hecks.boot` roots a registry at the parent of the directory it boots,
|
|
180
|
-
# and `uses_embryonaut_bluebook "<name>"` loads from
|
|
181
|
-
# `<root>/vendor/embryonaut_bluebooks/<name>/bluebook` (see
|
|
182
|
-
# `EmbryonautBluebook.load!`). The copy holds only the domain directory,
|
|
183
|
-
# so without this a target that vendors a package crashed the boot with
|
|
184
|
-
# "no vendored embryonaut bluebook named ...". Only the packages a
|
|
185
|
-
# hecksagon names are copied, never the rest of `vendor/`. A package the
|
|
186
|
-
# source root does not have is skipped, so the boot's own error stands.
|
|
187
|
-
#
|
|
188
|
-
# @param source [String] the real domain directory
|
|
189
|
-
# @param destination [String] the copy's directory; its parent is the copy's root
|
|
190
|
-
# @return [void]
|
|
191
|
-
def carry_vendored_bluebooks!(source, destination)
|
|
192
|
-
names = Dir.glob(File.join(destination, "**", "*.hecksagon")).flat_map do |path|
|
|
193
|
-
File.read(path).scan(/uses_embryonaut_bluebook\s*\(?\s*"([\w-]+)"/).flatten
|
|
194
|
-
end
|
|
195
|
-
names.uniq.each do |name|
|
|
196
|
-
package = File.join(File.dirname(source), "vendor", "embryonaut_bluebooks", name)
|
|
197
|
-
next unless File.directory?(package)
|
|
198
|
-
|
|
199
|
-
copy_files(package, File.join(File.dirname(destination), "vendor", "embryonaut_bluebooks", name))
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# Rewrites every `.hecksagon` in the copy to bind through Memory and replaces its
|
|
204
|
-
# `.world` files with a `default_adapter "Memory"` one, so the boot needs no settings.
|
|
205
|
-
#
|
|
206
|
-
# @param copy [String] the isolated copy's root directory
|
|
207
|
-
# @return [void]
|
|
208
|
-
def rebind_to_memory!(copy)
|
|
209
|
-
rewrite_bindings!(copy, "Memory")
|
|
210
|
-
strip_translations!(copy)
|
|
211
|
-
|
|
212
|
-
# **The settings, not just the bind** — `WorldBuilder#method_missing`
|
|
213
|
-
# stores a settings block under both "verb:adapter" and the bare
|
|
214
|
-
# "verb" (world_builder.rb:32-33), so a bind rewritten to Memory
|
|
215
|
-
# still falls back to whatever adapter's settings were declared
|
|
216
|
-
# bare — Postgres's `database:`, which Memory does not take and
|
|
217
|
-
# WiringError refuses on sight. Both Memory and Sqlite need no
|
|
218
|
-
# settings at all (`Sqlite#resolve_path` defaults `database` to
|
|
219
|
-
# `data/<table>.db` under `root:` when unset — the same
|
|
220
|
-
# zero-config default Memory gets from having no `.world` at
|
|
221
|
-
# all), so the simplest correct fix for either target is
|
|
222
|
-
# dropping the shipped `.world` from the copy. What replaces it is
|
|
223
|
-
# only a `default_adapter`, so an aggregate the hecksagon leaves
|
|
224
|
-
# unbound (the real world's `default_adapter` bound it) still lands on
|
|
225
|
-
# this mode's adapter — see `write_default_worlds!`.
|
|
226
|
-
write_default_worlds!(copy, "Memory")
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
# Same dance as memory, one adapter over — `Adapters::Sqlite#
|
|
230
|
-
# resolve_path` (adapters/driven/sqlite.rb) defaults to
|
|
231
|
-
# `data/<table>.db` under the boot's own root when no `database`
|
|
232
|
-
# setting is declared, which `data/` already being cleared makes a
|
|
233
|
-
# fresh on-disk store with zero configuration, the same "no .world
|
|
234
|
-
# needed" property Memory has. `data/` itself is left for Sqlite to
|
|
235
|
-
# recreate on first write, same as it always was for Memory/Heki —
|
|
236
|
-
# nothing here creates it up front.
|
|
237
|
-
#
|
|
238
|
-
# @param copy [String] the isolated copy's root directory
|
|
239
|
-
# @return [void]
|
|
240
|
-
def rebind_to_sqlite!(copy)
|
|
241
|
-
rewrite_bindings!(copy, "SqlitePersistence")
|
|
242
|
-
strip_translations!(copy)
|
|
243
|
-
write_default_worlds!(copy, "SqlitePersistence")
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# **The expensive one** — Postgres has no zero-config default the way
|
|
247
|
-
# Sqlite/Memory do (`Adapters::Postgres.connect_for` refuses outright
|
|
248
|
-
# with no `database` setting), so dropping `.world` the way the
|
|
249
|
-
# other two do would just move the WiringError from "wrong adapter"
|
|
250
|
-
# to "no adapter." Every domain name this copy declares gets a
|
|
251
|
-
# fresh `.world` written for it instead — not a rewrite of whatever
|
|
252
|
-
# was there, a replacement, same reasoning `rewrite_bindings!`
|
|
253
|
-
# already applies to `.hecksagon`: this ephemeral boot owns every
|
|
254
|
-
# binding decision, nothing about a real deployment's own settings
|
|
255
|
-
# is relevant or safe to half-preserve here.
|
|
256
|
-
#
|
|
257
|
-
# One shared schema, dropped and recreated before every boot — not a
|
|
258
|
-
# fresh randomly-named one per call. `bin/fuzz` drives every
|
|
259
|
-
# ephemeral boot sequentially (one `IsolatedBoot.call` fully exits
|
|
260
|
-
# before the next begins — see that file's own single-threaded
|
|
261
|
-
# `while` loop), so nothing is ever concurrent here ; a fresh name
|
|
262
|
-
# every time would just leak schemas in `FUZZ_POSTGRES_DATABASE`
|
|
263
|
-
# forever with nothing to ever drop them. If a caller ever does
|
|
264
|
-
# start running fuzz adapters concurrently, this needs to move to a
|
|
265
|
-
# process-unique schema name (`SecureRandom.hex` is already
|
|
266
|
-
# `require`d here for exactly that day) — flagged, not solved,
|
|
267
|
-
# since nothing today calls this from more than one thread.
|
|
268
|
-
FUZZ_POSTGRES_DATABASE = "hecks_fuzz".freeze
|
|
269
|
-
FUZZ_POSTGRES_SCHEMA = "hecks_fuzz".freeze
|
|
270
|
-
|
|
271
|
-
# Rewrites every `.hecksagon` in the copy to bind through Postgres, against the
|
|
272
|
-
# shared `FUZZ_POSTGRES_DATABASE`/`FUZZ_POSTGRES_SCHEMA` scratch schema unless the
|
|
273
|
-
# caller names its own, which lets `bin/bench` run without touching the fuzzer's.
|
|
274
|
-
#
|
|
275
|
-
# @param copy [String] the isolated copy's root directory
|
|
276
|
-
# @param database [String, nil] the scratch database, `FUZZ_POSTGRES_DATABASE` when nil
|
|
277
|
-
# @param schema [String, nil] the scratch schema, `FUZZ_POSTGRES_SCHEMA` when nil
|
|
278
|
-
# @return [void]
|
|
279
|
-
def rebind_to_postgres!(copy, database: nil, schema: nil)
|
|
280
|
-
require "pg"
|
|
281
|
-
database ||= FUZZ_POSTGRES_DATABASE
|
|
282
|
-
schema ||= FUZZ_POSTGRES_SCHEMA
|
|
283
|
-
rewrite_bindings!(copy, "Postgres")
|
|
284
|
-
strip_translations!(copy)
|
|
285
|
-
ensure_fuzz_schema!(database, schema)
|
|
286
|
-
|
|
287
|
-
# One `.world` per directory a `.hecksagon` actually lives in, not
|
|
288
|
-
# one at `copy`'s own root — `Folder#load_domain` resolves a
|
|
289
|
-
# single `bluebook_directory` and globs `*.world` there, non-
|
|
290
|
-
# recursively (`Folder#load_each`); a domain can hold several
|
|
291
|
-
# `Hecks.hecksagon "<Name>" do ... end` sibling blocks in that one
|
|
292
|
-
# file (banking.hecksagon declares "Banking", "Governance", and
|
|
293
|
-
# "Identity" together), so every name found in one directory
|
|
294
|
-
# is bundled into one `.world` written there, not scattered by name.
|
|
295
|
-
write_worlds!(copy, "hecks_fuzz_postgres.world") do |name|
|
|
296
|
-
<<~WORLD
|
|
297
|
-
Hecks.world "#{name}" do
|
|
298
|
-
default_adapter "Postgres"
|
|
299
|
-
persisted_by("Postgres") do
|
|
300
|
-
database "#{database}"
|
|
301
|
-
schema "#{schema}"
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
WORLD
|
|
305
|
-
end
|
|
306
|
-
write_unbound_chapter_worlds!(copy)
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
# Admin connection lives outside the tmp copy entirely — same as
|
|
310
|
-
# every other real-Postgres spec in this repo (`support/
|
|
311
|
-
# postgres_probe.rb`'s own header). Database created once per
|
|
312
|
-
# process and remembered (`@fuzz_database_ready` on this module's
|
|
313
|
-
# own singleton, the same memoization shape `PostgresProbe
|
|
314
|
-
# .available?` already uses) ; the schema inside it is dropped and
|
|
315
|
-
# recreated on every call, which is what actually isolates one
|
|
316
|
-
# ephemeral boot's data from the next.
|
|
317
|
-
#
|
|
318
|
-
# @param database [String] the scratch database to create if missing
|
|
319
|
-
# @param schema [String] the schema to drop and recreate inside it
|
|
320
|
-
# @return [void]
|
|
321
|
-
def ensure_fuzz_schema!(database = FUZZ_POSTGRES_DATABASE, schema = FUZZ_POSTGRES_SCHEMA)
|
|
322
|
-
# `Adapters::Postgres#initialize` opens one real `PG::Connection`
|
|
323
|
-
# per aggregate and never explicitly closes it — fine for a
|
|
324
|
-
# process that boots once and runs, exactly what every other
|
|
325
|
-
# caller of this adapter is. A fuzz run boots dozens to hundreds
|
|
326
|
-
# of ephemeral times in one process (every seed does at least a
|
|
327
|
-
# generate + a replay, `replay_is_deterministic` doubles that,
|
|
328
|
-
# shrinking multiplies it further), and `PG::Connection` only
|
|
329
|
-
# actually closes its socket when Ruby's GC finalizes the
|
|
330
|
-
# (by-then-unreferenced) object — which, under a tight loop like
|
|
331
|
-
# this, does not reliably keep pace with how fast new
|
|
332
|
-
# connections open. Discovered live: `bin/fuzz --adapter
|
|
333
|
-
# postgres` against `examples/banking` hit a real local
|
|
334
|
-
# Postgres's own `max_connections` after roughly a dozen
|
|
335
|
-
# ephemeral boots, `PG::ConnectionBad: ... "too many clients
|
|
336
|
-
# already"`. A `GC.start` here — right before the next ephemeral
|
|
337
|
-
# boot's connections open, not on some timer — reclaims every
|
|
338
|
-
# connection the previous boot's now-unreferenced adapters held,
|
|
339
|
-
# keeping the live count bounded regardless of run length. This
|
|
340
|
-
# is a real constraint on running Postgres in a loop, not
|
|
341
|
-
# something to route around by connecting less carefully.
|
|
342
|
-
GC.start
|
|
343
|
-
|
|
344
|
-
@fuzz_databases_ready ||= {}
|
|
345
|
-
unless @fuzz_databases_ready[database]
|
|
346
|
-
admin = PG.connect(dbname: "postgres")
|
|
347
|
-
exists = admin.exec_params(
|
|
348
|
-
"SELECT 1 FROM pg_database WHERE datname = $1", [database]
|
|
349
|
-
).ntuples.positive?
|
|
350
|
-
admin.exec(%(CREATE DATABASE "#{database}")) unless exists
|
|
351
|
-
admin.close
|
|
352
|
-
@fuzz_databases_ready[database] = true
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
db = PG.connect(dbname: database)
|
|
356
|
-
# **Quiet on purpose** — same as `Adapters::Postgres.connect_for`'s
|
|
357
|
-
# own `SET client_min_messages`: a `DROP SCHEMA ... CASCADE` that
|
|
358
|
-
# actually has something to drop (every boot after the first)
|
|
359
|
-
# NOTICEs once per dropped object, which is the ordinary case
|
|
360
|
-
# here, not news, and would otherwise bury `bin/fuzz`'s own
|
|
361
|
-
# output under a wall of "drop cascades to table ..." on every
|
|
362
|
-
# single ephemeral boot.
|
|
363
|
-
db.exec("SET client_min_messages = warning")
|
|
364
|
-
quoted = db.quote_ident(schema)
|
|
365
|
-
db.exec("DROP SCHEMA IF EXISTS #{quoted} CASCADE")
|
|
366
|
-
db.exec("CREATE SCHEMA #{quoted}")
|
|
367
|
-
db.close
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
# The adapter `:postgres` never touches — `Postgres` and `PostgresEra`
|
|
371
|
-
# are sibling, not interchangeable, adapters (see postgres_era.rb's
|
|
372
|
-
# own header: "the only one that declares the LINEAGE capability").
|
|
373
|
-
# PRD 02 (docs/prds/02-fuzzer-real-adapters.md) shipped `:postgres`
|
|
374
|
-
# and explicitly scoped `PostgresEra` out: "nothing here touches
|
|
375
|
-
# era/lineage machinery." That gap is real, not cosmetic —
|
|
376
|
-
# `examples/directory` (a `compute`/`rekey` translation edge, the
|
|
377
|
-
# one domain in this corpus that actually exercises PostgresEra-
|
|
378
|
-
# bound SQL compilation) had to be shelved out of `hecks_qa`'s own
|
|
379
|
-
# rotation for exactly this reason: every existing fuzz/replay path,
|
|
380
|
-
# `:memory` included, structurally cannot reach it. This mode closes
|
|
381
|
-
# that — `bin/qa_sweep --persistence-parity` is its first caller.
|
|
382
|
-
#
|
|
383
|
-
# No shared constant database, unlike `:postgres` above — deliberate.
|
|
384
|
-
# `rebind_to_postgres!`'s own `FUZZ_POSTGRES_DATABASE`/`_SCHEMA` are
|
|
385
|
-
# module-level constants because `bin/fuzz --adapter postgres` is a
|
|
386
|
-
# general-purpose, run-it-anytime tool with no caller-tracked
|
|
387
|
-
# lifecycle of its own. This mode's only caller
|
|
388
|
-
# (`bin/qa_sweep --persistence-parity`) is different: it dispatches
|
|
389
|
-
# through `QualityControl::Target.claim!`'s own cross-process lock
|
|
390
|
-
# first (see that script's own header), so at most one sweep is ever
|
|
391
|
-
# touching a given target's own disposable database at a time — but
|
|
392
|
-
# the caller, not this module, is what owns that database's name and
|
|
393
|
-
# lifecycle (created, and genuinely dropped, by the caller itself),
|
|
394
|
-
# exactly the discipline `spec/support/qa_sweep_all_fixture.rb`'s own header
|
|
395
|
-
# describes and this repository's persistence-parity work is
|
|
396
|
-
# required to follow. Accepting `database:`/`schema:` as required
|
|
397
|
-
# keyword arguments (never a fallback constant) is what keeps that
|
|
398
|
-
# ownership from silently drifting back onto this module the way
|
|
399
|
-
# `:postgres`'s own `FUZZ_POSTGRES_DATABASE` already has.
|
|
400
|
-
#
|
|
401
|
-
# @param copy [String] the isolated copy's root directory
|
|
402
|
-
# @param database [String] the caller-owned throwaway database name
|
|
403
|
-
# @param schema [String] the caller-owned throwaway schema name
|
|
404
|
-
# @return [void]
|
|
405
|
-
# @raise [ArgumentError] if `database` or `schema` is empty
|
|
406
|
-
def rebind_to_postgres_era!(copy, database:, schema:)
|
|
407
|
-
require "pg"
|
|
408
|
-
if database.to_s.empty? || schema.to_s.empty?
|
|
409
|
-
raise ArgumentError,
|
|
410
|
-
"adapter: :postgres_era requires both database: and schema: — a throwaway database/schema " \
|
|
411
|
-
"THIS CALLER creates and drops itself (see rebind_to_postgres_era!'s own header). " \
|
|
412
|
-
"There is no shared default, unlike :postgres, so the caller cannot forget to own the lifecycle."
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
rewrite_bindings!(copy, "PostgresEra")
|
|
416
|
-
ensure_postgres_era_schema!(database: database, schema: schema)
|
|
417
|
-
|
|
418
|
-
# Same one-`.world`-per-`.hecksagon`-directory shape `rebind_to_
|
|
419
|
-
# postgres!` uses, for the identical reason (`Folder#
|
|
420
|
-
# load_domain` globs `*.world` non-recursively) — see
|
|
421
|
-
# `write_worlds!`. `PostgresEra`
|
|
422
|
-
# additionally takes `schema:` (postgres_era.rb's own "shared-
|
|
423
|
-
# instance isolation" comment): the caller-supplied throwaway
|
|
424
|
-
# schema is what actually isolates this one ephemeral boot from
|
|
425
|
-
# the next, the same job `FUZZ_POSTGRES_SCHEMA` does for `:postgres`
|
|
426
|
-
# — `connect_for` itself idempotently `CREATE SCHEMA IF NOT
|
|
427
|
-
# EXISTS`s it, so this method only ever needs to drop it first
|
|
428
|
-
# (in `ensure_postgres_era_schema!`, below) for the zero-history
|
|
429
|
-
# guarantee every other adapter mode already gives.
|
|
430
|
-
#
|
|
431
|
-
# `allow_superuser true` — on the record, on purpose. A bare
|
|
432
|
-
# `database` connects as the ambient Postgres user, and
|
|
433
|
-
# PostgresEra refuses to boot at all when that user is a
|
|
434
|
-
# superuser (its era write-fence is row-level security, which a
|
|
435
|
-
# superuser walks through — `Lineage#check_fence_applies!`,
|
|
436
|
-
# BUG#24). That refusal protects a real ledger from an old
|
|
437
|
-
# checkout's stale writes; nothing here is one. This is an
|
|
438
|
-
# ephemeral boot into a throwaway schema the caller itself
|
|
439
|
-
# creates and drops, whose data no second checkout ever shares,
|
|
440
|
-
# and what it compares is Memory's answers against PostgresEra's
|
|
441
|
-
# own SQL — the era fence is not under test and cannot be
|
|
442
|
-
# crossed. So opt in explicitly rather than make every
|
|
443
|
-
# persistence-parity run first provision a fenced role for a
|
|
444
|
-
# database it is about to throw away; the one-line warning
|
|
445
|
-
# PostgresEra prints per boot under the opt-in is the honest
|
|
446
|
-
# price. Inert on a machine whose ambient user is ordinary.
|
|
447
|
-
write_worlds!(copy, "hecks_fuzz_postgres_era.world") do |name|
|
|
448
|
-
<<~WORLD
|
|
449
|
-
Hecks.world "#{name}" do
|
|
450
|
-
default_adapter "PostgresEra"
|
|
451
|
-
persisted_by("PostgresEra") do
|
|
452
|
-
database "#{database}"
|
|
453
|
-
schema "#{schema}"
|
|
454
|
-
allow_superuser true
|
|
455
|
-
end
|
|
456
|
-
end
|
|
457
|
-
WORLD
|
|
458
|
-
end
|
|
459
|
-
write_unbound_chapter_worlds!(copy)
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
# Replaces every `.world` in the copy with one that declares only `default_adapter`,
|
|
463
|
-
# so the copy needs no settings and an aggregate its hecksagon leaves unbound
|
|
464
|
-
# (a chapter whose real world bound it through `default_adapter`) persists through
|
|
465
|
-
# `adapter_name` like every bound one.
|
|
466
|
-
#
|
|
467
|
-
# @param copy [String] the isolated copy's root directory
|
|
468
|
-
# @param adapter_name [String] the persistence adapter every unbound aggregate falls
|
|
469
|
-
# back to, such as `"Memory"` or `"SqlitePersistence"`
|
|
470
|
-
# @return [void]
|
|
471
|
-
def write_default_worlds!(copy, adapter_name)
|
|
472
|
-
write_worlds!(copy, "hecks_fuzz_default.world") do |name|
|
|
473
|
-
<<~WORLD
|
|
474
|
-
Hecks.world "#{name}" do
|
|
475
|
-
default_adapter "#{adapter_name}"
|
|
476
|
-
end
|
|
477
|
-
WORLD
|
|
478
|
-
end
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
# Gives every chapter no hecksagon names a world of its own that falls back to Memory.
|
|
482
|
-
#
|
|
483
|
-
# A chapter with a bluebook and no hecksagon block (the framework's Privacy) has no
|
|
484
|
-
# bind, and the worlds `write_worlds!` wrote declare a `default_adapter` that needs a
|
|
485
|
-
# `database`: left alone, the chapter resolves that adapter through the project's
|
|
486
|
-
# world with no connection and the boot refuses (`WiringError ... needs a database
|
|
487
|
-
# connection`). It persisted through Memory before those worlds carried a
|
|
488
|
-
# `default_adapter`, and a chapter's own world's default wins over the project's, so
|
|
489
|
-
# this keeps it there.
|
|
490
|
-
#
|
|
491
|
-
# @param copy [String] the isolated copy's root directory
|
|
492
|
-
# @return [void]
|
|
493
|
-
def write_unbound_chapter_worlds!(copy)
|
|
494
|
-
named = Dir.glob(File.join(copy, "**", "*.hecksagon")).flat_map do |path|
|
|
495
|
-
File.read(path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
|
|
496
|
-
end
|
|
497
|
-
bluebooks = Dir.glob(File.join(copy, "**", "*.bluebook")).group_by { |path| File.dirname(path) }
|
|
498
|
-
bluebooks.each do |dir, files|
|
|
499
|
-
unbound = files.flat_map { |path| File.read(path).scan(/Hecks\.bluebook[\s(]+"([^"]+)"/).flatten }
|
|
500
|
-
.uniq - named
|
|
501
|
-
named.concat(unbound)
|
|
502
|
-
next if unbound.empty?
|
|
503
|
-
|
|
504
|
-
worlds = unbound.map { |name| %(Hecks.world "#{name}" do\n default_adapter "Memory"\nend\n) }
|
|
505
|
-
File.write(File.join(dir, "hecks_fuzz_unbound.world"), worlds.join("\n"))
|
|
506
|
-
end
|
|
507
|
-
end
|
|
508
|
-
|
|
509
|
-
# Writes one `.world` file per directory that holds a `.hecksagon`, naming every
|
|
510
|
-
# `Hecks.hecksagon` block found in that directory, and deletes every other `.world`
|
|
511
|
-
# in the copy.
|
|
512
|
-
#
|
|
513
|
-
# One world file per directory, every hecksagon name in that directory — never one
|
|
514
|
-
# write per `*.hecksagon` file. A domain's `context_map.hecksagon` sits beside its
|
|
515
|
-
# main hecksagon (the `qa` domain declares Governance in one and QualityControl in
|
|
516
|
-
# the other), and a second `File.write` to the same world path would drop the first
|
|
517
|
-
# file's names, leaving that domain bound to Postgres with no `database` to open.
|
|
518
|
-
#
|
|
519
|
-
# Any `.world` the copy shipped with (a real deployment's own connection string) is
|
|
520
|
-
# redundant with, and would conflict with (`Registry#add_world`'s own header, on
|
|
521
|
-
# loading the same domain name twice), the fresh one written here, so it goes the same
|
|
522
|
-
# way `rebind_to_memory!` sends every `.world` in a Memory-mode boot.
|
|
523
|
-
#
|
|
524
|
-
# @param copy [String] the isolated copy's root directory
|
|
525
|
-
# @param world_file [String] the basename of the world file written in each directory
|
|
526
|
-
# @yield [name] builds the world text for one hecksagon name
|
|
527
|
-
# @yieldparam name [String] a domain name a `Hecks.hecksagon` block in the directory declares
|
|
528
|
-
# @yieldreturn [String] the `Hecks.world` block for that name
|
|
529
|
-
# @return [void]
|
|
530
|
-
def write_worlds!(copy, world_file, &world_for)
|
|
531
|
-
names_by_dir = Hash.new { |hash, dir| hash[dir] = [] }
|
|
532
|
-
Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |hecksagon_path|
|
|
533
|
-
names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
|
|
534
|
-
names_by_dir[File.dirname(hecksagon_path)].concat(names)
|
|
535
|
-
end
|
|
536
|
-
names_by_dir.each do |dir, names|
|
|
537
|
-
next if names.empty?
|
|
538
|
-
|
|
539
|
-
File.write(File.join(dir, world_file), names.uniq.map(&world_for).join("\n"))
|
|
540
|
-
end
|
|
541
|
-
|
|
542
|
-
Dir.glob(File.join(copy, "**", "*.world")).each do |path|
|
|
543
|
-
File.delete(path) unless File.basename(path) == world_file
|
|
544
|
-
end
|
|
545
|
-
end
|
|
546
|
-
|
|
547
|
-
# The zero-history guarantee for this mode — `DROP SCHEMA ... CASCADE`
|
|
548
|
-
# before every ephemeral boot, mirroring `ensure_fuzz_schema!` above
|
|
549
|
-
# (same `GC.start`-before-connecting fix for the identical
|
|
550
|
-
# `max_connections` exhaustion that method's own comment documents —
|
|
551
|
-
# `PostgresEra` opens real `PG::Connection`s exactly like `Postgres`
|
|
552
|
-
# does, same unclosed-until-GC'd lifetime). The database itself is
|
|
553
|
-
# created here too, idempotently (`CREATE DATABASE IF NOT EXISTS`
|
|
554
|
-
# has no Postgres spelling, hence the existence check) — but never
|
|
555
|
-
# dropped here: this module creates it once per process because
|
|
556
|
-
# `Hecks.boot` needs it to exist before `PostgresEra.connect_for`'s
|
|
557
|
-
# own `PG.connect(dbname: ...)` can succeed at all, but dropping it
|
|
558
|
-
# again is the caller's own job (its name and lifecycle belong to
|
|
559
|
-
# the caller — see `rebind_to_postgres_era!`'s own header), not
|
|
560
|
-
# something this per-ephemeral-boot helper should ever do mid-sweep.
|
|
561
|
-
#
|
|
562
|
-
# @param database [String] the caller-owned database name, created if it does not
|
|
563
|
-
# already exist
|
|
564
|
-
# @param schema [String] the caller-owned schema name, dropped and recreated
|
|
565
|
-
# @return [void]
|
|
566
|
-
def ensure_postgres_era_schema!(database:, schema:)
|
|
567
|
-
GC.start
|
|
568
|
-
|
|
569
|
-
admin = PG.connect(dbname: "postgres")
|
|
570
|
-
exists = admin.exec_params(
|
|
571
|
-
"SELECT 1 FROM pg_database WHERE datname = $1", [database]
|
|
572
|
-
).ntuples.positive?
|
|
573
|
-
admin.exec(%(CREATE DATABASE "#{database}")) unless exists
|
|
574
|
-
admin.close
|
|
575
|
-
|
|
576
|
-
db = PG.connect(dbname: database)
|
|
577
|
-
db.exec("SET client_min_messages = warning")
|
|
578
|
-
quoted = db.quote_ident(schema)
|
|
579
|
-
db.exec("DROP SCHEMA IF EXISTS #{quoted} CASCADE")
|
|
580
|
-
db.close
|
|
581
|
-
end
|
|
582
|
-
|
|
583
|
-
# The shared rewrite — factored out of `rebind_to_memory!` when
|
|
584
|
-
# Sqlite/Postgres modes needed the identical `.hecksagon` surgery
|
|
585
|
-
# with only the target adapter name differing. `persisted_by`/
|
|
586
|
-
# `projected_by` can be spelled two ways: aggregate-scoped
|
|
587
|
-
# (`Banking::Customer.persisted_by("Heki")`, always parenthesised)
|
|
588
|
-
# and, since §0's domain-level default binds, a bare call at the
|
|
589
|
-
# hecksagon's own root (`persisted_by "Heki"`, no parens, no
|
|
590
|
-
# receiver). Both must be caught here or a domain that only
|
|
591
|
-
# declares the bare form keeps its real adapter under an
|
|
592
|
-
# "isolated" fuzz boot. `projected_by` is dropped outright rather
|
|
593
|
-
# than rebound, for every target adapter — `Registry
|
|
594
|
-
# #read_repository` already falls back to the authoritative
|
|
595
|
-
# repository when none exists, so a read model this ephemeral copy
|
|
596
|
-
# never wires is simply unread, not broken.
|
|
597
|
-
# A `compute`/`rekey` translation edge refuses to boot at all under
|
|
598
|
-
# any non-lineage-capable adapter — found live, wiring this very
|
|
599
|
-
# mode up against `examples/directory`: `Runtime::EraCheck
|
|
600
|
-
# .check_compute_rules!` (era_check.rb) runs unconditionally for
|
|
601
|
-
# every loaded bluebook once the era plugin is loaded at all
|
|
602
|
-
# (`bin/qa_sweep`'s own top-of-file `require "hecks/ports/
|
|
603
|
-
# persistence/plugins/era"`, needed for the ledger's own
|
|
604
|
-
# PostgresEra-bound aggregates), and refuses outright — "compute
|
|
605
|
-
# rules require the Postgres adapter" — for any aggregate whose
|
|
606
|
-
# lineage carries a `compute` rule and whose bound adapter is not
|
|
607
|
-
# lineage-capable. `PostgresEra` is the only adapter that answers
|
|
608
|
-
# `lineage_capable? == true` (postgres_era.rb's own `self.
|
|
609
|
-
# lineage_capable? = true`) — plain `Postgres` does not, so this
|
|
610
|
-
# refusal was already real for `:postgres`/`:sqlite`/`:memory`
|
|
611
|
-
# alike, for any domain with a translation edge, before this
|
|
612
|
-
# mode's own `:postgres_era` ever existed. This is very likely the
|
|
613
|
-
# mechanical reason `examples/directory` had to be shelved out of
|
|
614
|
-
# `hecks_qa`'s own rotation in the first place — not merely "less
|
|
615
|
-
# interesting to fuzz on Memory," but "cannot boot on Memory at
|
|
616
|
-
# all" once the era plugin is loaded, which every real
|
|
617
|
-
# `bin/qa_sweep` invocation already does.
|
|
618
|
-
#
|
|
619
|
-
# The fix is to drop the edge, not to chase the refusal — an
|
|
620
|
-
# ephemeral, zero-history replay boot (every mode `IsolatedBoot`
|
|
621
|
-
# offers) never has a pre-existing era-1 row to translate in the
|
|
622
|
-
# first place, so the translation edge is irrelevant to anything a
|
|
623
|
-
# fuzz/replay run actually exercises (ordinary command dispatch
|
|
624
|
-
# against a fresh boot) — it only ever matters at mint time,
|
|
625
|
-
# against a real, pre-existing database
|
|
626
|
-
# (`PostgresEra::LineageManager.check!`, a wholly separate,
|
|
627
|
-
# human-approved path this harness was never meant to reach).
|
|
628
|
-
# Dropping it here is exactly the same move `rebind_to_memory!`
|
|
629
|
-
# already makes for `.world` (irrelevant/conflicting settings for
|
|
630
|
-
# an ephemeral boot, deleted outright) — never called for
|
|
631
|
-
# `:postgres_era` itself, where the bound adapter genuinely is
|
|
632
|
-
# lineage-capable and the edge causes no refusal to begin with.
|
|
633
|
-
#
|
|
634
|
-
# @param copy [String] the isolated copy's root directory
|
|
635
|
-
# @return [void]
|
|
636
|
-
def strip_translations!(copy)
|
|
637
|
-
Dir.glob(File.join(copy, "**", "translations", "*.bluebook")).each { |path| File.delete(path) }
|
|
638
|
-
end
|
|
639
|
-
|
|
640
|
-
# Rewrites every `persisted_by` bind — aggregate-scoped or bare at the
|
|
641
|
-
# hecksagon's own root, naming its adapter as a literal or through a local
|
|
642
|
-
# variable — in the copy's `.hecksagon` files to name `adapter_name`,
|
|
643
|
-
# and drops every `projected_by` bind outright.
|
|
644
|
-
#
|
|
645
|
-
# @param copy [String] the isolated copy's root directory
|
|
646
|
-
# @param adapter_name [String] the port binding name to rewrite every
|
|
647
|
-
# `persisted_by` to, such as `"Memory"` or `"SqlitePersistence"`
|
|
648
|
-
# @return [void]
|
|
649
|
-
def rewrite_bindings!(copy, adapter_name)
|
|
650
|
-
Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |path|
|
|
651
|
-
lines = File.readlines(path).grep_v(/\bprojected_by\s*\(?\s*"/)
|
|
652
|
-
bind = /persisted_by\s*\(?\s*(?:"[^"]+"|[a-z_]\w*)\s*\)?/
|
|
653
|
-
File.write(path, lines.join.gsub(bind, "persisted_by(\"#{adapter_name}\")"))
|
|
654
|
-
end
|
|
655
|
-
end
|
|
656
|
-
end
|
|
657
|
-
end
|
|
658
|
-
end
|