hecks 1.4.0 → 1.5.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/lib/hecks/adapters/driven/claude_code.rb +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +29 -5
- data/lib/hecks/projector.rb +74 -6
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +11 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
|
@@ -80,24 +80,68 @@ module Hecks
|
|
|
80
80
|
|
|
81
81
|
module_function
|
|
82
82
|
|
|
83
|
+
# Reads an aggregate IR hash's own declared entities.
|
|
84
|
+
#
|
|
85
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
86
|
+
# @return [Array<Hash>] the aggregate's own declared entities ("pieces"); `[]`
|
|
87
|
+
# if it declares none
|
|
83
88
|
def entities(aggregate) = aggregate["entities"] || []
|
|
89
|
+
|
|
90
|
+
# Reads an aggregate IR hash's own declared commands.
|
|
91
|
+
#
|
|
92
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
93
|
+
# @return [Array<Hash>] the aggregate's own declared commands; `[]` if it
|
|
94
|
+
# declares none
|
|
84
95
|
def commands(aggregate) = aggregate["commands"] || []
|
|
85
96
|
|
|
86
97
|
# One aggregate's commands, its pieces' included — a form carried by
|
|
87
98
|
# an entity command is carried by the aggregate that owns it, the
|
|
88
99
|
# same way `composite_piece`/`piece_lifecycle` already read pieces.
|
|
100
|
+
#
|
|
101
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
102
|
+
# @return [Array<Hash>] `aggregate`'s own commands, plus every entity's own
|
|
89
103
|
def every_command(aggregate) = commands(aggregate) + entities(aggregate).flat_map { |piece| commands(piece) }
|
|
90
104
|
|
|
105
|
+
# Answers whether a command IR hash declares a `corrects` mutation.
|
|
106
|
+
#
|
|
107
|
+
# @param verb [Hash] a string-keyed command IR hash
|
|
108
|
+
# @return [Boolean] true if `verb` declares a `corrects` mutation
|
|
91
109
|
def corrects?(verb) = (verb["mutations"] || []).any? { |change| change["op"].to_s == "corrects" }
|
|
110
|
+
|
|
111
|
+
# Reads an aggregate IR hash's own declared attributes.
|
|
112
|
+
#
|
|
113
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
114
|
+
# @return [Array<Hash>] the aggregate's own declared attributes; `[]` if it
|
|
115
|
+
# declares none
|
|
92
116
|
def attributes(aggregate) = aggregate["attributes"] || []
|
|
117
|
+
|
|
118
|
+
# Reads an aggregate IR hash's own declared queries.
|
|
119
|
+
#
|
|
120
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
121
|
+
# @return [Array<Hash>] the aggregate's own declared queries; `[]` if it
|
|
122
|
+
# declares none
|
|
93
123
|
def queries(aggregate) = aggregate["queries"] || []
|
|
124
|
+
|
|
125
|
+
# Answers whether an attribute IR hash's own declared type is a reference.
|
|
126
|
+
#
|
|
127
|
+
# @param attribute [Hash] a string-keyed attribute IR hash
|
|
128
|
+
# @return [Boolean] true if `attribute`'s own declared type is a reference
|
|
94
129
|
def reference?(attribute) = attribute["type"].to_s.start_with?("Reference<")
|
|
95
130
|
|
|
131
|
+
# Answers whether any of an aggregate's own commands crosses a two-hop given.
|
|
132
|
+
#
|
|
133
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
134
|
+
# @return [Boolean] true if any command's own given crosses at least two
|
|
135
|
+
# references (`TWO_HOP_GIVEN_PATH_LENGTH`)
|
|
96
136
|
def two_hop_given?(aggregate)
|
|
97
137
|
commands(aggregate).any? { |verb| (verb["givens"] || []).any? { |given| deep_lookup?(given["ast"]) } }
|
|
98
138
|
end
|
|
99
139
|
|
|
100
140
|
# A `where` whose field crosses two `/` — `member/sponsor/standing`.
|
|
141
|
+
#
|
|
142
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
143
|
+
# @return [Boolean] true if any query's own where clause crosses at least
|
|
144
|
+
# two `/` hops
|
|
101
145
|
def multi_hop_where?(aggregate)
|
|
102
146
|
queries(aggregate).any? { |query| (query["wheres"] || []).any? { |where| where["field"].to_s.count("/") >= 2 } }
|
|
103
147
|
end
|
|
@@ -106,6 +150,10 @@ module Hecks
|
|
|
106
150
|
# own reference-typed attributes while carrying a different, non-
|
|
107
151
|
# reference type — `attribute :member, Handle` against
|
|
108
152
|
# `reference_to Member`.
|
|
153
|
+
#
|
|
154
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
155
|
+
# @return [Boolean] true if any command redeclares one of the aggregate's
|
|
156
|
+
# own reference-typed attribute names under a non-reference type
|
|
109
157
|
def revalued_reference?(aggregate)
|
|
110
158
|
references = attributes(aggregate).select { |held| reference?(held) }.to_set { |held| held["name"].to_s }
|
|
111
159
|
commands(aggregate).any? do |verb|
|
|
@@ -115,6 +163,11 @@ module Hecks
|
|
|
115
163
|
|
|
116
164
|
# Walks a given's own exported AST for any `lookup` whose path is
|
|
117
165
|
# long enough to have crossed two references.
|
|
166
|
+
#
|
|
167
|
+
# @param node [Hash, Array, Object] an AST node (or subtree) from an
|
|
168
|
+
# exported given's `"ast"`
|
|
169
|
+
# @return [Boolean] true if any `lookup` node's own path is at least
|
|
170
|
+
# `TWO_HOP_GIVEN_PATH_LENGTH` long
|
|
118
171
|
def deep_lookup?(node)
|
|
119
172
|
case node
|
|
120
173
|
when Hash
|
|
@@ -129,21 +182,39 @@ module Hecks
|
|
|
129
182
|
end
|
|
130
183
|
|
|
131
184
|
# Every form, answered for one aggregate — the table above, applied.
|
|
185
|
+
#
|
|
186
|
+
# @param aggregate [Hash] a string-keyed aggregate IR hash
|
|
187
|
+
# @return [Hash{String => Boolean}] every `FORMS` name mapped to whether
|
|
188
|
+
# `aggregate` exhibits it
|
|
132
189
|
def properties(aggregate)
|
|
133
190
|
FORMS.transform_values { |form| form.call(aggregate) }
|
|
134
191
|
end
|
|
135
192
|
|
|
136
193
|
# Every unordered pair of forms, each rendered "left + right" in
|
|
137
194
|
# alphabetical order — the key both gates' excuse tables use.
|
|
195
|
+
#
|
|
196
|
+
# @return [Array<String>] every unordered pair of `FORMS` names, as
|
|
197
|
+
# `"left + right"`
|
|
138
198
|
def pairs
|
|
139
199
|
FORMS.keys.combination(2).map { |pair| pair_key(*pair) }
|
|
140
200
|
end
|
|
141
201
|
|
|
202
|
+
# Renders two form names as one sorted pair key.
|
|
203
|
+
#
|
|
204
|
+
# @param left [String] a form name
|
|
205
|
+
# @param right [String] a form name
|
|
206
|
+
# @return [String] `left`/`right`, alphabetically ordered, joined as
|
|
207
|
+
# `"left + right"`
|
|
142
208
|
def pair_key(left, right) = [left, right].sort.join(" + ")
|
|
143
209
|
|
|
144
210
|
# `held` is `[[aggregate_name, properties], ...]`. Answers which
|
|
145
211
|
# pairs are met on one aggregate, and by which — a Hash from pair
|
|
146
212
|
# key to the names carrying it, so a caller can say who.
|
|
213
|
+
#
|
|
214
|
+
# @param held [Array<Array(String, Hash)>] `[aggregate_name, properties]`
|
|
215
|
+
# pairs, `properties` as returned by `#properties`
|
|
216
|
+
# @return [Hash{String => Array<String>}] every met pair key mapped to the
|
|
217
|
+
# aggregate names that carry it
|
|
147
218
|
def covered_pairs(held)
|
|
148
219
|
held.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(name, shows), covered|
|
|
149
220
|
shows.select { |_, present| present }.keys.combination(2).each do |left, right|
|
|
@@ -155,6 +226,12 @@ module Hecks
|
|
|
155
226
|
# `[[\"Chapter::Aggregate\", properties], ...]` for every aggregate
|
|
156
227
|
# a string-keyed chapter IR declares — the same walk the golden
|
|
157
228
|
# spec makes over `spec/golden/ir/*.json`.
|
|
229
|
+
#
|
|
230
|
+
# @param chapter_ir [Hash] a string-keyed chapter IR hash, with `"name"` and
|
|
231
|
+
# `"aggregates"`
|
|
232
|
+
# @return [Array<Array(String, Hash)>] `["Chapter::Aggregate", properties]`
|
|
233
|
+
# pairs, one per declared aggregate, `properties` as returned by
|
|
234
|
+
# `#properties`
|
|
158
235
|
def aggregates_in(chapter_ir)
|
|
159
236
|
(chapter_ir["aggregates"] || []).map do |aggregate|
|
|
160
237
|
["#{chapter_ir['name']}::#{aggregate['name']}", properties(aggregate)]
|
|
@@ -164,6 +241,10 @@ module Hecks
|
|
|
164
241
|
# Where a domain path keeps its bluebooks — see
|
|
165
242
|
# `Hecks::Corpus.bluebook_files`, the one definition every corpus
|
|
166
243
|
# walk shares. `nil` when neither shape holds a bluebook.
|
|
244
|
+
#
|
|
245
|
+
# @param domain_path [String] path to a domain directory
|
|
246
|
+
# @return [Array<String>, nil] the domain's own `.bluebook` file paths, or
|
|
247
|
+
# `nil` if it holds none
|
|
167
248
|
def bluebook_files(domain_path)
|
|
168
249
|
Hecks::Corpus.bluebook_files(domain_path)
|
|
169
250
|
end
|
|
@@ -176,6 +257,11 @@ module Hecks
|
|
|
176
257
|
# domain's own). Only the domain's own chapter is measured — the
|
|
177
258
|
# first bluebook loaded, the same "target chapter is always first"
|
|
178
259
|
# fact `bin/project_rust` relies on.
|
|
260
|
+
# @param domain_path [String] path to a domain directory
|
|
261
|
+
# @return [Array<Array(String, Hash)>] `["Chapter::Aggregate", properties]`
|
|
262
|
+
# pairs, one per aggregate declared on `domain_path`'s own first-loaded
|
|
263
|
+
# chapter (see `Hecks::Projector::Exporter.call`)
|
|
264
|
+
# @raise [ArgumentError] if `domain_path` has no bluebook files to measure
|
|
179
265
|
def census(domain_path)
|
|
180
266
|
root = File.expand_path("../../..", __dir__)
|
|
181
267
|
files = bluebook_files(domain_path)
|
|
@@ -27,6 +27,27 @@ module Hecks
|
|
|
27
27
|
|
|
28
28
|
module_function
|
|
29
29
|
|
|
30
|
+
# Checks one generated domain over up to `seeds` generated sequences, stopping
|
|
31
|
+
# at the first divergence found.
|
|
32
|
+
#
|
|
33
|
+
# @param domain_path [String] path to the generated domain directory
|
|
34
|
+
# @param seeds [Integer] maximum number of seeds to try before declaring clean
|
|
35
|
+
# @param steps [Integer] steps to generate per seed
|
|
36
|
+
# @param adversarial [Float] fraction of command steps to mutate adversarially
|
|
37
|
+
# @param binary [String, nil] path to a compiled Rust conformance binary to
|
|
38
|
+
# diff against, or `nil` for a Ruby-only check
|
|
39
|
+
# @param differ [Object, nil] duck-typed comparison helper answering
|
|
40
|
+
# `RustConformanceHelpers`' interface (adapter-defined; constructed ad hoc
|
|
41
|
+
# by callers such as bin/qa_generated_domains), or `nil`
|
|
42
|
+
# @param match [Hash, nil] `{"mode" =>, "signature" =>}` from a parent
|
|
43
|
+
# domain's own finding — only a divergence matching it counts, for
|
|
44
|
+
# domain-level shrinking; `nil` accepts the first divergence found
|
|
45
|
+
# @param shrink_budget [Integer] step-level shrink attempts to spend on a
|
|
46
|
+
# found finding; `0` skips shrinking
|
|
47
|
+
# @return [Hash] `{"status" => "invalid", "error" =>}` if the domain does not
|
|
48
|
+
# boot; `{"status" => "clean", "seeds_run" =>}` if no seed diverged;
|
|
49
|
+
# otherwise a finding merged with `{"status" => "found", "seeds_run" =>}`
|
|
50
|
+
# (see `#finding`), plus `"shrunk_steps"` when `shrink_budget` is positive
|
|
30
51
|
def run(domain_path, seeds:, steps:, adversarial:, binary: nil, differ: nil, match: nil, shrink_budget: 0)
|
|
31
52
|
error = boot_error(domain_path)
|
|
32
53
|
return { "status" => "invalid", "error" => error } if error
|
|
@@ -41,6 +62,11 @@ module Hecks
|
|
|
41
62
|
{ "status" => "clean", "seeds_run" => seeds }
|
|
42
63
|
end
|
|
43
64
|
|
|
65
|
+
# Boots `domain_path` in an isolated copy, to find out whether it boots at all.
|
|
66
|
+
#
|
|
67
|
+
# @param domain_path [String] path to the domain directory to boot
|
|
68
|
+
# @return [String, nil] `nil` if the boot succeeds; otherwise the raising
|
|
69
|
+
# exception's class and first message line
|
|
44
70
|
def boot_error(domain_path)
|
|
45
71
|
IsolatedBoot.call(domain_path) { |copy| Hecks.boot(copy) }
|
|
46
72
|
nil
|
|
@@ -48,6 +74,20 @@ module Hecks
|
|
|
48
74
|
"#{e.class}: #{e.message.lines.first&.strip}"
|
|
49
75
|
end
|
|
50
76
|
|
|
77
|
+
# Generates one sequence for `seed` and checks it for a divergence.
|
|
78
|
+
#
|
|
79
|
+
# @param domain_path [String] path to the domain directory
|
|
80
|
+
# @param seed [Integer] RNG seed for `SequenceGenerator.generate`
|
|
81
|
+
# @param steps [Integer] steps to generate
|
|
82
|
+
# @param adversarial [Float] fraction of command steps to mutate adversarially
|
|
83
|
+
# @param binary [String, nil] path to a compiled Rust conformance binary, or
|
|
84
|
+
# `nil` for a Ruby-only check
|
|
85
|
+
# @param differ [Object, nil] duck-typed comparison helper answering
|
|
86
|
+
# `RustConformanceHelpers`' interface (adapter-defined), or `nil`
|
|
87
|
+
# @param match [Hash, nil] `{"mode" =>, "signature" =>}` a divergence must
|
|
88
|
+
# match to count, or `nil` to accept the first one found
|
|
89
|
+
# @return [Hash, nil] a finding (see `#finding`) if a matching divergence was
|
|
90
|
+
# found, or if generation itself crashed; `nil` if the sequence was clean
|
|
51
91
|
def check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
|
|
52
92
|
sequence = SequenceGenerator.generate(domain_path, seed: seed, steps: steps, adversarial: adversarial)
|
|
53
93
|
rescue StandardError => e
|
|
@@ -60,6 +100,19 @@ module Hecks
|
|
|
60
100
|
nil
|
|
61
101
|
end
|
|
62
102
|
|
|
103
|
+
# Runs the comparison appropriate to whether a compiled binary is available:
|
|
104
|
+
# `Differential.diff` against Rust when `binary` is given, otherwise a
|
|
105
|
+
# Ruby-only replay checked for property and self-consistency divergences.
|
|
106
|
+
#
|
|
107
|
+
# @param domain_path [String] path to the domain directory
|
|
108
|
+
# @param sequence [Array<Hash>] the generated step sequence to replay
|
|
109
|
+
# @param binary [String, nil] path to a compiled Rust conformance binary, or
|
|
110
|
+
# `nil` for a Ruby-only check
|
|
111
|
+
# @param differ [Object, nil] duck-typed comparison helper answering
|
|
112
|
+
# `RustConformanceHelpers`' interface (adapter-defined), or `nil`
|
|
113
|
+
# @return [Hash{Symbol => Array<Hash>}] one divergence list per mode checked;
|
|
114
|
+
# `{ruby_only: [{field: "crash", detail:}]}` if the Ruby-only replay itself
|
|
115
|
+
# raised
|
|
63
116
|
def outcomes(domain_path, sequence, binary, differ)
|
|
64
117
|
return Differential.diff(differ, domain_path, sequence, binary, modes: DIFFERENTIAL_MODES) if binary
|
|
65
118
|
|
|
@@ -70,6 +123,18 @@ module Hecks
|
|
|
70
123
|
{ ruby_only: [{ field: "crash", detail: "#{e.class}: #{e.message}" }] }
|
|
71
124
|
end
|
|
72
125
|
|
|
126
|
+
# Builds a reported finding from a mode's divergences, unless `divergences`
|
|
127
|
+
# is empty or `match` names a different signature.
|
|
128
|
+
#
|
|
129
|
+
# @param seed [Integer] the seed that produced `sequence`
|
|
130
|
+
# @param mode [Symbol] the comparison mode the divergences came from
|
|
131
|
+
# @param divergences [Array<Hash>] divergence entries for `mode`
|
|
132
|
+
# @param sequence [Array<Hash>] the generated step sequence
|
|
133
|
+
# @param match [Hash, nil] `{"mode" =>, "signature" =>}` the finding must
|
|
134
|
+
# match, or `nil` to accept any
|
|
135
|
+
# @return [Hash, nil] `{"seed" =>, "mode" =>, "signature" =>, "steps" =>,
|
|
136
|
+
# "divergences" =>}` if `divergences` is non-empty and matches `match`;
|
|
137
|
+
# `nil` otherwise
|
|
73
138
|
def finding(seed, mode, divergences, sequence, match)
|
|
74
139
|
return nil if divergences.empty?
|
|
75
140
|
|
|
@@ -82,6 +147,17 @@ module Hecks
|
|
|
82
147
|
|
|
83
148
|
# The step-level half of shrinking, in this same process: the domain
|
|
84
149
|
# is already as small as the parent could make it.
|
|
150
|
+
#
|
|
151
|
+
# @param domain_path [String] path to the (already domain-shrunk) domain
|
|
152
|
+
# directory
|
|
153
|
+
# @param found [Hash] the finding to shrink, as returned by `#finding`
|
|
154
|
+
# @param binary [String, nil] path to a compiled Rust conformance binary, or
|
|
155
|
+
# `nil` for a Ruby-only check
|
|
156
|
+
# @param differ [Object, nil] duck-typed comparison helper answering
|
|
157
|
+
# `RustConformanceHelpers`' interface (adapter-defined), or `nil`
|
|
158
|
+
# @param budget [Integer] step-level shrink attempts to spend
|
|
159
|
+
# @return [Array<Hash>] the smallest step list found that still reproduces
|
|
160
|
+
# `found`'s divergence signature
|
|
85
161
|
def shrink(domain_path, found, binary, differ, budget)
|
|
86
162
|
original = Set.new(found["signature"])
|
|
87
163
|
mode = found["mode"].to_sym
|
|
@@ -32,6 +32,16 @@ module Hecks
|
|
|
32
32
|
null
|
|
33
33
|
].freeze
|
|
34
34
|
|
|
35
|
+
# Builds one value of the wrong shape for `attribute`, chosen from the
|
|
36
|
+
# confusions that are actually meaningful for its declared type.
|
|
37
|
+
#
|
|
38
|
+
# @param attribute [Bluebook::Attribute] the attribute the corrupted value is
|
|
39
|
+
# handed to
|
|
40
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that declares
|
|
41
|
+
# `attribute` (and any value objects it references)
|
|
42
|
+
# @param random [Random] the seeded RNG to draw the kind and value from
|
|
43
|
+
# @return [Object] a value of the wrong shape for `attribute` — an Integer,
|
|
44
|
+
# Float, String, Array, Hash, or boolean, depending on the kind drawn
|
|
35
45
|
def corrupt(attribute, aggregate, random:)
|
|
36
46
|
kind = kinds_for(attribute, aggregate).sample(random: random)
|
|
37
47
|
build(kind, attribute, aggregate, random: random)
|
|
@@ -41,6 +51,13 @@ module Hecks
|
|
|
41
51
|
# `scalar_for_object` for a plain Integer would just be a second spelling of
|
|
42
52
|
# `numeral_string`, and a kind that cannot be wrong for the attribute it is
|
|
43
53
|
# handed teaches the corpus nothing.
|
|
54
|
+
#
|
|
55
|
+
# @param attribute [Bluebook::Attribute] the attribute to find applicable
|
|
56
|
+
# confusions for
|
|
57
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that declares
|
|
58
|
+
# `attribute` (and any value objects it references)
|
|
59
|
+
# @return [Array<Symbol>] the subset of `KINDS` that are a real confusion for
|
|
60
|
+
# `attribute`'s declared type
|
|
44
61
|
def kinds_for(attribute, aggregate)
|
|
45
62
|
value_object = aggregate.value_object(attribute.type.to_s)
|
|
46
63
|
return %i[scalar_for_object array_for_scalar null] if value_object
|
|
@@ -49,6 +66,16 @@ module Hecks
|
|
|
49
66
|
%i[object_for_scalar array_for_scalar boolean_for_string null]
|
|
50
67
|
end
|
|
51
68
|
|
|
69
|
+
# Builds the actual corrupted value for one already-chosen `kind`.
|
|
70
|
+
#
|
|
71
|
+
# @param kind [Symbol] one of `KINDS`
|
|
72
|
+
# @param attribute [Bluebook::Attribute] the attribute the value will be
|
|
73
|
+
# handed to
|
|
74
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that declares
|
|
75
|
+
# `attribute` (and any value objects it references)
|
|
76
|
+
# @param random [Random] the seeded RNG to draw the value from
|
|
77
|
+
# @return [Object, nil] the corrupted value for `kind`; `nil` if `kind` is not
|
|
78
|
+
# one `KINDS` declares
|
|
52
79
|
def build(kind, attribute, aggregate, random:)
|
|
53
80
|
case kind
|
|
54
81
|
when :object_for_scalar then { "cents" => random.rand(1..1000) }
|
|
@@ -64,6 +91,15 @@ module Hecks
|
|
|
64
91
|
# legitimately accepts one (that is the standing-in rule every domain relies
|
|
65
92
|
# on), so the interesting case is a value object with several fields, where a
|
|
66
93
|
# scalar cannot stand for anything and the refusal has to say so.
|
|
94
|
+
# @param attribute [Bluebook::Attribute] the value-object-typed attribute to
|
|
95
|
+
# build a bare scalar for
|
|
96
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that declares
|
|
97
|
+
# `attribute`'s value object
|
|
98
|
+
# @param random [Random] accepted for a uniform call signature with `#build`;
|
|
99
|
+
# this method draws nothing from it
|
|
100
|
+
# @return [String, Hash] `"a bare scalar"` when a scalar would legitimately be
|
|
101
|
+
# accepted (a single-field value object); otherwise a Hash corrupting the
|
|
102
|
+
# sole field's own type instead
|
|
67
103
|
def scalar_for(attribute, aggregate, random:)
|
|
68
104
|
value_object = aggregate.value_object(attribute.type.to_s)
|
|
69
105
|
sole = value_object&.sole_attribute
|
|
@@ -77,6 +113,9 @@ module Hecks
|
|
|
77
113
|
# An attribute the command never declared. `refuse_unknown_arguments` is a
|
|
78
114
|
# real dispatch step (Vocabulary::AggregateDispatchOrder), and nothing
|
|
79
115
|
# generated had ever exercised it.
|
|
116
|
+
# @param random [Random] the seeded RNG to draw the name and value from
|
|
117
|
+
# @return [Array(String, String)] a `[name, value]` pair naming an argument
|
|
118
|
+
# no command declares
|
|
80
119
|
def undeclared_argument(random:)
|
|
81
120
|
name = %w[colour flavour rank note].sample(random: random)
|
|
82
121
|
[name, %w[red loud third scribbled].sample(random: random)]
|
|
@@ -6,6 +6,8 @@ module Hecks
|
|
|
6
6
|
module Fuzzing
|
|
7
7
|
# A fresh, in-process adapter for every ephemeral boot.
|
|
8
8
|
#
|
|
9
|
+
# ## Why rebind at all
|
|
10
|
+
#
|
|
9
11
|
# Fuzzing/replay copies a domain to a tmpdir and boots from there
|
|
10
12
|
# specifically to get zero-history state — `rm_rf`ing the copy's own
|
|
11
13
|
# `data/` achieves that for a file-based adapter (Memory,
|
|
@@ -25,16 +27,18 @@ module Hecks
|
|
|
25
27
|
# one ephemeral copy answers through changes. What the domain is
|
|
26
28
|
# bound to for real deployment is never touched — only this tmp copy.
|
|
27
29
|
#
|
|
28
|
-
# `adapter:`
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
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
|
|
38
42
|
# `lib/hecks/adapters/driven/sqlite.adapter` actually registers under
|
|
39
43
|
# (`Sqlite` is the class; `SqlitePersistence` is a thin subclass
|
|
40
44
|
# that's the one real port binding names) — and needs no `.world`
|
|
@@ -44,18 +48,16 @@ module Hecks
|
|
|
44
48
|
# `root:`, which `Hecks.boot(copy)` passes as this ephemeral copy's
|
|
45
49
|
# own directory — a fresh, empty `data/` per run, exactly like
|
|
46
50
|
# Memory's own zero-history guarantee, just backed by a real SQLite
|
|
47
|
-
# file instead of a Hash.
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
# instead requires the caller to supply (and own the lifecycle of) its
|
|
58
|
-
# own throwaway `database:`/`schema:`, since its only caller
|
|
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
|
|
59
61
|
# (`bin/qa_sweep --persistence-parity`) already has to manage a
|
|
60
62
|
# disposable database of its own, never a shared one this module could
|
|
61
63
|
# safely default to.
|
|
@@ -83,6 +85,20 @@ module Hecks
|
|
|
83
85
|
# signature per adapter) keeps `SequenceGenerator`/`Replay`'s own
|
|
84
86
|
# single passthrough (`adapter:`, now joined by these two) uniform
|
|
85
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
|
+
# @yield [String] the freshly rebound copy's root directory, ready to boot
|
|
97
|
+
# @yieldreturn [Object] anything; becomes this method's own return value
|
|
98
|
+
# @return [Object] whatever the given block returns
|
|
99
|
+
# @raise [ArgumentError] if `adapter` is not `:memory`, `:sqlite`, `:postgres`, or
|
|
100
|
+
# `:postgres_era`, or if `adapter: :postgres_era` is given without both
|
|
101
|
+
# `database:` and `schema:`
|
|
86
102
|
def call(domain_path, adapter: :memory, database: nil, schema: nil)
|
|
87
103
|
Dir.mktmpdir("hecks-fuzz") do |tmp|
|
|
88
104
|
copy = File.join(tmp, File.basename(domain_path))
|
|
@@ -113,6 +129,10 @@ module Hecks
|
|
|
113
129
|
# what an isolated boot wants: the copy has to stand alone, since
|
|
114
130
|
# rebind! rewrites files in it and must not reach back through a
|
|
115
131
|
# link into the real tree.
|
|
132
|
+
#
|
|
133
|
+
# @param source [String] the real domain directory to copy
|
|
134
|
+
# @param destination [String] the tmpdir path to copy it into; created if missing
|
|
135
|
+
# @return [void]
|
|
116
136
|
def copy_dereferencing(source, destination)
|
|
117
137
|
FileUtils.mkdir_p(destination)
|
|
118
138
|
Dir.glob(File.join(source, "**", "*"), File::FNM_DOTMATCH).each do |path|
|
|
@@ -136,6 +156,11 @@ module Hecks
|
|
|
136
156
|
end
|
|
137
157
|
end
|
|
138
158
|
|
|
159
|
+
# Rewrites every `.hecksagon` in the copy to bind through Memory and drops its
|
|
160
|
+
# `.world` files, so the boot needs no settings at all.
|
|
161
|
+
#
|
|
162
|
+
# @param copy [String] the isolated copy's root directory
|
|
163
|
+
# @return [void]
|
|
139
164
|
def rebind_to_memory!(copy)
|
|
140
165
|
rewrite_bindings!(copy, "Memory")
|
|
141
166
|
strip_translations!(copy)
|
|
@@ -162,6 +187,9 @@ module Hecks
|
|
|
162
187
|
# needed" property Memory has. `data/` itself is left for Sqlite to
|
|
163
188
|
# recreate on first write, same as it always was for Memory/Heki —
|
|
164
189
|
# nothing here creates it up front.
|
|
190
|
+
#
|
|
191
|
+
# @param copy [String] the isolated copy's root directory
|
|
192
|
+
# @return [void]
|
|
165
193
|
def rebind_to_sqlite!(copy)
|
|
166
194
|
rewrite_bindings!(copy, "SqlitePersistence")
|
|
167
195
|
strip_translations!(copy)
|
|
@@ -193,6 +221,11 @@ module Hecks
|
|
|
193
221
|
FUZZ_POSTGRES_DATABASE = "hecks_fuzz".freeze
|
|
194
222
|
FUZZ_POSTGRES_SCHEMA = "hecks_fuzz".freeze
|
|
195
223
|
|
|
224
|
+
# Rewrites every `.hecksagon` in the copy to bind through Postgres, against the
|
|
225
|
+
# shared `FUZZ_POSTGRES_DATABASE`/`FUZZ_POSTGRES_SCHEMA` scratch schema.
|
|
226
|
+
#
|
|
227
|
+
# @param copy [String] the isolated copy's root directory
|
|
228
|
+
# @return [void]
|
|
196
229
|
def rebind_to_postgres!(copy)
|
|
197
230
|
require "pg"
|
|
198
231
|
rewrite_bindings!(copy, "Postgres")
|
|
@@ -244,6 +277,8 @@ module Hecks
|
|
|
244
277
|
# .available?` already uses) ; the schema inside it is dropped and
|
|
245
278
|
# recreated on every call, which is what actually isolates one
|
|
246
279
|
# ephemeral boot's data from the next.
|
|
280
|
+
#
|
|
281
|
+
# @return [void]
|
|
247
282
|
def ensure_fuzz_schema!
|
|
248
283
|
# `Adapters::Postgres#initialize` opens one real `PG::Connection`
|
|
249
284
|
# per aggregate and never explicitly closes it — fine for a
|
|
@@ -322,6 +357,12 @@ module Hecks
|
|
|
322
357
|
# keyword arguments (never a fallback constant) is what keeps that
|
|
323
358
|
# ownership from silently drifting back onto this module the way
|
|
324
359
|
# `:postgres`'s own `FUZZ_POSTGRES_DATABASE` already has.
|
|
360
|
+
#
|
|
361
|
+
# @param copy [String] the isolated copy's root directory
|
|
362
|
+
# @param database [String] the caller-owned throwaway database name
|
|
363
|
+
# @param schema [String] the caller-owned throwaway schema name
|
|
364
|
+
# @return [void]
|
|
365
|
+
# @raise [ArgumentError] if `database` or `schema` is empty
|
|
325
366
|
def rebind_to_postgres_era!(copy, database:, schema:)
|
|
326
367
|
require "pg"
|
|
327
368
|
if database.to_s.empty? || schema.to_s.empty?
|
|
@@ -400,6 +441,11 @@ module Hecks
|
|
|
400
441
|
# again is the caller's own job (its name and lifecycle belong to
|
|
401
442
|
# the caller — see `rebind_to_postgres_era!`'s own header), not
|
|
402
443
|
# something this per-ephemeral-boot helper should ever do mid-sweep.
|
|
444
|
+
#
|
|
445
|
+
# @param database [String] the caller-owned database name, created if it does not
|
|
446
|
+
# already exist
|
|
447
|
+
# @param schema [String] the caller-owned schema name, dropped and recreated
|
|
448
|
+
# @return [void]
|
|
403
449
|
def ensure_postgres_era_schema!(database:, schema:)
|
|
404
450
|
GC.start
|
|
405
451
|
|
|
@@ -467,10 +513,21 @@ module Hecks
|
|
|
467
513
|
# an ephemeral boot, deleted outright) — never called for
|
|
468
514
|
# `:postgres_era` itself, where the bound adapter genuinely is
|
|
469
515
|
# lineage-capable and the edge causes no refusal to begin with.
|
|
516
|
+
#
|
|
517
|
+
# @param copy [String] the isolated copy's root directory
|
|
518
|
+
# @return [void]
|
|
470
519
|
def strip_translations!(copy)
|
|
471
520
|
Dir.glob(File.join(copy, "**", "translations", "*.bluebook")).each { |path| File.delete(path) }
|
|
472
521
|
end
|
|
473
522
|
|
|
523
|
+
# Rewrites every `persisted_by` bind — aggregate-scoped or bare at the
|
|
524
|
+
# hecksagon's own root — in the copy's `.hecksagon` files to name `adapter_name`,
|
|
525
|
+
# and drops every `projected_by` bind outright.
|
|
526
|
+
#
|
|
527
|
+
# @param copy [String] the isolated copy's root directory
|
|
528
|
+
# @param adapter_name [String] the port binding name to rewrite every
|
|
529
|
+
# `persisted_by` to, such as `"Memory"` or `"SqlitePersistence"`
|
|
530
|
+
# @return [void]
|
|
474
531
|
def rewrite_bindings!(copy, adapter_name)
|
|
475
532
|
Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |path|
|
|
476
533
|
lines = File.readlines(path).grep_v(/\bprojected_by\s*\(?\s*"/)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Fuzzing
|
|
3
|
-
# The one declared set of FIELDS that leave a comparison — partition,
|
|
3
|
+
# The one declared set of `FIELDS` that leave a comparison — partition,
|
|
4
4
|
# not filter. Every comparison in lib/hecks/fuzzing that drops a field
|
|
5
5
|
# before comparing two histories names a group here instead of writing
|
|
6
6
|
# its own literal `except(...)` list, so the reason for dropping a field
|
|
@@ -48,17 +48,29 @@ module Hecks
|
|
|
48
48
|
|
|
49
49
|
module_function
|
|
50
50
|
|
|
51
|
+
# Lists the nondeterministic field names declared for one comparison group.
|
|
52
|
+
#
|
|
53
|
+
# @param group [Symbol] a key of `FIELDS`, such as `:query_row` or `:event`
|
|
54
|
+
# @return [Array<Symbol>] the field names declared for `group`
|
|
55
|
+
# @raise [KeyError] if `group` names no declared group
|
|
51
56
|
def names(group)
|
|
52
57
|
FIELDS.fetch(group).keys
|
|
53
58
|
end
|
|
54
59
|
|
|
55
60
|
# Every declared name, across every group.
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<Symbol>] the union of every group's field names, deduplicated
|
|
56
63
|
def all_names
|
|
57
64
|
FIELDS.values.flat_map(&:keys).uniq
|
|
58
65
|
end
|
|
59
66
|
|
|
60
67
|
# `hash` without `group`'s fields — symbol keys, the shape every
|
|
61
68
|
# Ruby-side history carries.
|
|
69
|
+
#
|
|
70
|
+
# @param hash [Hash] a symbol-keyed row, event, or history hash to compare
|
|
71
|
+
# @param group [Symbol] a key of `FIELDS` naming which fields to drop
|
|
72
|
+
# @return [Hash] `hash` with `group`'s declared fields removed
|
|
73
|
+
# @raise [KeyError] if `group` names no declared group
|
|
62
74
|
def strip(hash, group)
|
|
63
75
|
hash.except(*names(group))
|
|
64
76
|
end
|