hecks 2.6.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/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- 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 +2 -2
- 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/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
- 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/port_operation_builder.rb +1 -1
- 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 +47 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- 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/world_judge.rb +3 -1
- 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 +13 -14
- 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/embryonaut_bluebook/lock.rb +72 -0
- data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
- data/lib/hecks/embryonaut_bluebook.rb +29 -3
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
- 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/expected_era.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
- 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 +13 -7
- data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
- data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/ports/persistence.rb +4 -0
- data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
- data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
- data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
- data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
- data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
- data/lib/hecks/projections/deploy/fargate.rb +188 -139
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
- data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
- data/lib/hecks/projections/deploy/preview/script.rb +40 -0
- data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
- data/lib/hecks/projections/deploy/preview/template.rb +247 -0
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
- data/lib/hecks/projections/deploy/preview.rb +103 -0
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
- data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
- data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
- data/lib/hecks/projections/deploy/scripts.rb +171 -0
- data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
- data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
- data/lib/hecks/projections/deploy/smoke.rb +159 -0
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
- data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
- data/lib/hecks/projections/deploy/template_diff.rb +173 -0
- data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- 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/command_rules/arithmetic.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 +27 -3
- 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/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +15 -13
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +22 -13
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/tenant_check.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/vendoring/git_environment.rb +35 -0
- data/lib/hecks/vendoring/git_source.rb +102 -0
- data/lib/hecks/vendoring.rb +102 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +8 -2
- metadata +72 -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 -604
- 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,344 +0,0 @@
|
|
|
1
|
-
require_relative "../../bluebook/expression/resolver"
|
|
2
|
-
require_relative "../invalid_value_generator"
|
|
3
|
-
require_relative "../value_generator"
|
|
4
|
-
require_relative "../../runtime/errors"
|
|
5
|
-
require_relative "../../runtime/value"
|
|
6
|
-
require_relative "../../naming"
|
|
7
|
-
|
|
8
|
-
module Hecks
|
|
9
|
-
module Fuzzing
|
|
10
|
-
class SequenceGenerator
|
|
11
|
-
# Turn a picked entry into a corpus step: generate its arguments,
|
|
12
|
-
# occasionally malform exactly one of them, shape its identity, and
|
|
13
|
-
# dispatch it for real.
|
|
14
|
-
module StepBuilder
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def build_query_step(runtime, entry)
|
|
18
|
-
args = args_for(entry[:query].attributes, entry[:aggregate])
|
|
19
|
-
bind_to_written_row!(args, entry)
|
|
20
|
-
safe_call { runtime.query(entry[:verb], **symbolize(args)) }
|
|
21
|
-
{ "query" => entry[:verb], "args" => args }
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# A report ask — the bare domain form `entry[:verb]` already carries
|
|
25
|
-
# ("Domain.report_name", no "::"), so `Dispatcher#query` routes it
|
|
26
|
-
# to the read model rather than an aggregate query. A `ReadModel`
|
|
27
|
-
# has no declared `.attributes` the way a `Query` does — its own
|
|
28
|
-
# argument surface is exactly one key, `reference_name`, and only
|
|
29
|
-
# for a rooted model (`read_model_actionable?` already gated a
|
|
30
|
-
# rootless one straight into eligibility with nothing to supply).
|
|
31
|
-
# A bare scalar, not `identity_shaped` — `ReadModelInterpreter#
|
|
32
|
-
# refuse_object_reference` explicitly rejects a Hash/Value offered
|
|
33
|
-
# here (this is the one place in the whole generator where the
|
|
34
|
-
# subject's own identity must not be wrapped the way a command
|
|
35
|
-
# argument's would be).
|
|
36
|
-
def build_read_model_step(runtime, entry)
|
|
37
|
-
model = entry[:model]
|
|
38
|
-
args = model.reference_target.nil? ? {} : { model.reference_name.to_s => pick_known(model.reference_target) }
|
|
39
|
-
|
|
40
|
-
safe_call { runtime.query(entry[:verb], **symbolize(args)) }
|
|
41
|
-
{ "query" => entry[:verb], "args" => args }
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# The adversarial layer sits here, and only here — after the step's
|
|
45
|
-
# arguments and identity are fully built, before the one real
|
|
46
|
-
# dispatch this generator makes to learn what the step did, and
|
|
47
|
-
# before the step is returned as corpus data. That ordering is
|
|
48
|
-
# the whole contract: the mutated `args` are what this generator's
|
|
49
|
-
# own inline dispatch sees (so `known_ids` tracking reflects what
|
|
50
|
-
# actually happened), and they are the bytes `Fuzzing::Replay`
|
|
51
|
-
# later hands Ruby's runtime and the bytes `JSON.generate({steps:
|
|
52
|
-
# ...})` hands the compiled Rust binary — one step, one payload,
|
|
53
|
-
# both engines. Nothing downstream of this method can tell a
|
|
54
|
-
# mutated step from an ordinary one except by reading the
|
|
55
|
-
# `"adversarial"` metadata it carries (see adversary.rb).
|
|
56
|
-
#
|
|
57
|
-
# Two more draws sit right after the mutation, in a fixed order —
|
|
58
|
-
# a caller (`caller_draw!`, adversary.rb: `role:`/`actor_id:` on
|
|
59
|
-
# the step, bound around this one dispatch exactly the way
|
|
60
|
-
# `Fuzzing::Replay` and `kernel/cli.rs` will later bind it) and
|
|
61
|
-
# then the dry-run coin (`{"dry_run": verb}` instead of
|
|
62
|
-
# `{"verb": verb}` — `Dispatcher#dry_run?` here, the same door on
|
|
63
|
-
# both replay sides). Order matters for the seed contract: every
|
|
64
|
-
# draw comes from the one `Random.new(seed)`, so the sequence of
|
|
65
|
-
# draws per step is what makes a seed reproducible. Both are
|
|
66
|
-
# off by default and draw nothing when off.
|
|
67
|
-
def build_command_step(runtime, catalog, entry)
|
|
68
|
-
args = args_for(entry[:command].attributes, entry[:aggregate])
|
|
69
|
-
add_identity!(args, entry)
|
|
70
|
-
steer_grant!(args, entry, catalog)
|
|
71
|
-
mutations = adversarial_mutations!(args, entry, catalog)
|
|
72
|
-
caller, caller_note = caller_draw!(entry, catalog)
|
|
73
|
-
mutations << caller_note if caller_note
|
|
74
|
-
@state_before = state_before(runtime, entry, args)
|
|
75
|
-
|
|
76
|
-
step =
|
|
77
|
-
if dry_run_draw?
|
|
78
|
-
safe_call { as_caller(caller) { runtime.dry_run?(entry[:verb], **symbolize(args)) } }
|
|
79
|
-
{ "dry_run" => entry[:verb], "args" => args }
|
|
80
|
-
else
|
|
81
|
-
outcome = safe_call { as_caller(caller) { runtime.dispatch_flat(entry[:verb], symbolize(args)) } }
|
|
82
|
-
if outcome
|
|
83
|
-
record_outcome(catalog, entry, args)
|
|
84
|
-
harvest_written_rows(runtime, catalog)
|
|
85
|
-
@event_count += outcome.events.length
|
|
86
|
-
end
|
|
87
|
-
{ "verb" => entry[:verb], "args" => args }
|
|
88
|
-
end
|
|
89
|
-
step.merge!(caller) if caller
|
|
90
|
-
step["adversarial"] = mutations unless mutations.empty?
|
|
91
|
-
step
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# No RNG draw at all when the fraction is zero — the same "off is
|
|
95
|
-
# byte-identical" contract `adversarial?` keeps.
|
|
96
|
-
def dry_run_draw? = @dry_run.positive? && @random.rand < @dry_run
|
|
97
|
-
|
|
98
|
-
# `Hecks.as_caller` for exactly this block, or a bare yield — the
|
|
99
|
-
# same binding `Fuzzing::Replay` makes from the step's own keys
|
|
100
|
-
# later, so the generator's inline dispatch and both replays see
|
|
101
|
-
# one caller.
|
|
102
|
-
def as_caller(caller, &)
|
|
103
|
-
return yield unless caller
|
|
104
|
-
|
|
105
|
-
Hecks.as_caller(role: caller["role"], actor_id: caller["actor_id"], &)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def args_for(attributes, aggregate)
|
|
109
|
-
args = attributes.each_with_object({}) do |attribute, built|
|
|
110
|
-
# An optional argument is sometimes not given, and that is an
|
|
111
|
-
# ordinary payload rather than a damaged one — see
|
|
112
|
-
# OPTIONAL_OMITTED_PROBABILITY for why this cannot live in
|
|
113
|
-
# `malform` below and what it was costing while it did not
|
|
114
|
-
# exist at all.
|
|
115
|
-
next if attribute.optional? && @random.rand < SequenceGenerator::OPTIONAL_OMITTED_PROBABILITY
|
|
116
|
-
|
|
117
|
-
if attribute.list?
|
|
118
|
-
value = list_value_for(attribute, aggregate)
|
|
119
|
-
# A list-of-entity command attribute has no real example
|
|
120
|
-
# anywhere in this repo's domains — every entity-owned list is
|
|
121
|
-
# populated via a per-element append command instead, never a
|
|
122
|
-
# whole-list command argument — so `list_value_for` (below)
|
|
123
|
-
# answers `nil` for one rather than guessing at an entity's own
|
|
124
|
-
# shape, and this step still skips it exactly as it always
|
|
125
|
-
# has. A list-of-value-object attribute (`ConsoleSettings::
|
|
126
|
-
# Collection.ReplaceColumns`' own `columns`, `list_of(Column)`)
|
|
127
|
-
# is the real case this covers —
|
|
128
|
-
# `sets :columns` imports the owner aggregate's own declared
|
|
129
|
-
# `list_of` attribute onto the command verbatim (Command
|
|
130
|
-
# Builder#resolve_bare_set!), so it is a required, ordinary
|
|
131
|
-
# top-level argument like any other, not an entity mutation.
|
|
132
|
-
next if value.nil?
|
|
133
|
-
|
|
134
|
-
built[attribute.name.to_s] = value
|
|
135
|
-
else
|
|
136
|
-
built[attribute.name.to_s] = ValueGenerator.value_for(attribute, aggregate, random: @random, known_ids: @known_ids)
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
malform(args, attributes, aggregate)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# A `list_of` attribute's own value — an array of independently
|
|
144
|
-
# generated elements, each shaped exactly the way a bare (non-list)
|
|
145
|
-
# attribute of the same declared element type already is
|
|
146
|
-
# (`ValueGenerator.value_for`), since `list_of(X)`'s own element
|
|
147
|
-
# coercion is `X`'s ordinary shape repeated, not a different one
|
|
148
|
-
# (`Attribute#type` is already unwrapped from `list_of(...)` at
|
|
149
|
-
# declare time — the same fact `command_builder.rb`'s own comment
|
|
150
|
-
# on `resolve_bare_set!` names). 0-3 elements — enough to exercise a
|
|
151
|
-
# genuinely non-trivial replace without generating pathologically
|
|
152
|
-
# large payloads every time. `nil`, not `[]`, when the element type
|
|
153
|
-
# isn't a declared value object at all (an entity-typed list) — see
|
|
154
|
-
# `args_for`'s own comment on why that case still skips rather than
|
|
155
|
-
# guesses.
|
|
156
|
-
def list_value_for(attribute, aggregate)
|
|
157
|
-
value_object = Runtime::Value.value_object_for(aggregate, attribute.type.to_s)
|
|
158
|
-
return nil unless value_object
|
|
159
|
-
|
|
160
|
-
Array.new(@random.rand(0..3)) { ValueGenerator.value_for(attribute, aggregate, random: @random, known_ids: @known_ids) }
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
# One malformation at a time, and usually none. A step whose payload is
|
|
164
|
-
# wrong in three ways only ever proves which check runs first ; wrong in
|
|
165
|
-
# exactly one way names the check that fired. And the rate stays low on
|
|
166
|
-
# purpose — a corrupted step is almost always refused, a sequence of
|
|
167
|
-
# refusals reaches no state at all, and bin/fuzz already counts those as
|
|
168
|
-
# silent rather than scoring them.
|
|
169
|
-
def malform(args, attributes, aggregate)
|
|
170
|
-
return args if args.empty? || @random.rand >= MALFORMED_ARGUMENT_PROBABILITY
|
|
171
|
-
|
|
172
|
-
case @random.rand(3)
|
|
173
|
-
when 0 then corrupt_one(args, attributes, aggregate)
|
|
174
|
-
when 1 then drop_one(args, aggregate)
|
|
175
|
-
else args.merge([InvalidValueGenerator.undeclared_argument(random: @random)].to_h)
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Never the identity. A creating command with no id auto-mints one, and
|
|
180
|
-
# a minted id is deliberately unreproducible — a random hex, never a
|
|
181
|
-
# guessable counter — so dropping it manufactures a step whose outcome
|
|
182
|
-
# cannot be replayed and says nothing about the runtime's behaviour.
|
|
183
|
-
# Every step in the hand-written corpus
|
|
184
|
-
# supplies an id for the same reason. Whether an auto-minted id ought to
|
|
185
|
-
# be reproducible is a real question, but it is not one a payload fuzzer
|
|
186
|
-
# can ask.
|
|
187
|
-
def drop_one(args, aggregate)
|
|
188
|
-
identity = (aggregate.identified_by || :id).to_s
|
|
189
|
-
droppable = args.keys - [identity, "id"]
|
|
190
|
-
return args if droppable.empty?
|
|
191
|
-
|
|
192
|
-
args.reject { |name, _| name == droppable.sample(random: @random) }
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def corrupt_one(args, attributes, aggregate)
|
|
196
|
-
named = attributes.reject(&:list?).select { |attribute| args.key?(attribute.name.to_s) }
|
|
197
|
-
return args if named.empty?
|
|
198
|
-
|
|
199
|
-
attribute = named.sample(random: @random)
|
|
200
|
-
args.merge(attribute.name.to_s => InvalidValueGenerator.corrupt(attribute, aggregate, random: @random))
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def add_identity!(args, entry)
|
|
204
|
-
aggregate = entry[:aggregate]
|
|
205
|
-
parent_key = (aggregate.identified_by || :id).to_s
|
|
206
|
-
|
|
207
|
-
if entry[:entity]
|
|
208
|
-
parent_scalar = pick_known(aggregate.hecks_name)
|
|
209
|
-
args[parent_key] = identity_shaped(aggregate, aggregate.identified_by, parent_scalar, aggregate)
|
|
210
|
-
# One identity per hop, each drawn from the pool its own
|
|
211
|
-
# parent-plus-hops landed elements in (`entity_pool_key`) —
|
|
212
|
-
# a depth-1 chain draws exactly what it always did; a
|
|
213
|
-
# `Board.Card` chain draws a Board under this Workspace, then
|
|
214
|
-
# a Card under that Board. Flat args, one head per hop, is
|
|
215
|
-
# the legacy addressing `EntityElement#locate_chain` reads
|
|
216
|
-
# (`args[head]` per identity path); the routed `to: {
|
|
217
|
-
# aggregate:, entities: [...] }` spelling is an adversarial
|
|
218
|
-
# shape layered on top (adversary.rb), never the default.
|
|
219
|
-
scalars = [parent_scalar]
|
|
220
|
-
names = []
|
|
221
|
-
entry[:chain].each do |piece|
|
|
222
|
-
names << piece.hecks_name
|
|
223
|
-
scalar = pick_entity_known(entity_pool_key(aggregate.hecks_name, names, scalars))
|
|
224
|
-
args[(piece.identified_by || :id).to_s] = identity_shaped(piece, piece.identified_by, scalar, aggregate)
|
|
225
|
-
scalars << scalar
|
|
226
|
-
end
|
|
227
|
-
elsif entry[:command].creates?
|
|
228
|
-
# A composite identity (`identified_by` answering nil with more
|
|
229
|
-
# than one declared path — Behaviour::Identified's own "a
|
|
230
|
-
# composite has no single head" comment) supplies every one of
|
|
231
|
-
# its parts as its own ordinary, individually-declared command
|
|
232
|
-
# attribute already — `RoleAssignment::Assign` takes actor_id/
|
|
233
|
-
# role_name/starts_at directly, `args_for` (above) already
|
|
234
|
-
# generated all three. Forcing a synthetic top-level `id` here
|
|
235
|
-
# too — this codebase's own fallback for the single-key and the
|
|
236
|
-
# genuinely untyped (`identity_paths.empty?`, no `identified_by`
|
|
237
|
-
# declared at all) cases — hands a composite creating command
|
|
238
|
-
# an argument it never declared at all, refused every time as
|
|
239
|
-
# unknown before this check existed (a creating `Assign`/`Grant`
|
|
240
|
-
# step was never anything but refused). `identity_paths.empty?`
|
|
241
|
-
# is the untyped default (falls all the way back to a minted
|
|
242
|
-
# `:id` the runtime itself never declared as an attribute
|
|
243
|
-
# either), which still needs exactly the old minting behavior.
|
|
244
|
-
unless composite_identity?(aggregate)
|
|
245
|
-
args[parent_key] ||= identity_shaped(aggregate, aggregate.identified_by, ValueGenerator.random_id(@random),
|
|
246
|
-
aggregate)
|
|
247
|
-
end
|
|
248
|
-
else
|
|
249
|
-
scalar = pick_known(aggregate.hecks_name)
|
|
250
|
-
args[parent_key] = identity_shaped(aggregate, aggregate.identified_by, scalar, aggregate)
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
# True only for a genuine multi-field identity — `identified_by`
|
|
255
|
-
# returns nil both for a real composite (`identity_paths.size > 1`)
|
|
256
|
-
# and for the untyped default with no identity declared at all
|
|
257
|
-
# (`identity_paths.size == 0`, Behaviour::Identified's own
|
|
258
|
-
# `Array(@identified_by)` fallback) ; only the first of those two
|
|
259
|
-
# has its own parts already sitting in `args` as real, individually-
|
|
260
|
-
# generated command attributes.
|
|
261
|
-
def composite_identity?(aggregate) = aggregate.identified_by.nil? && aggregate.identity_paths.size > 1
|
|
262
|
-
|
|
263
|
-
# A bare scalar id, shaped to match whatever `construct` itself
|
|
264
|
-
# declares that identity field as. `Account::LedgerEntry` is addressed
|
|
265
|
-
# by `sequence`, and `sequence` is declared as a value-object-typed
|
|
266
|
-
# attribute (`LedgerSequence`), not a plain identifier — a fuzz run
|
|
267
|
-
# that skipped this wrapping and dispatched a bare `"1"` had every
|
|
268
|
-
# such step refused at the type gate (a bare scalar against a
|
|
269
|
-
# value-object-typed identity is a TypeMismatch, not a NotFound), so
|
|
270
|
-
# the wrapping is what lets a fuzz step address the record at
|
|
271
|
-
# all. Left as a bare scalar when the construct declares no such
|
|
272
|
-
# attribute at all — the default `:id` case, which really is untyped.
|
|
273
|
-
def identity_shaped(construct, key, scalar, aggregate)
|
|
274
|
-
return scalar unless key
|
|
275
|
-
|
|
276
|
-
attribute = construct.attribute(key)
|
|
277
|
-
return scalar unless attribute
|
|
278
|
-
|
|
279
|
-
value_object = aggregate.value_object(attribute.type.to_s)
|
|
280
|
-
return scalar unless value_object
|
|
281
|
-
|
|
282
|
-
field = value_object.attributes.first
|
|
283
|
-
return scalar unless field
|
|
284
|
-
|
|
285
|
-
{ field.name.to_s => coerce_scalar(field.type.to_s, scalar) }
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
def coerce_scalar(type_name, scalar)
|
|
289
|
-
case type_name
|
|
290
|
-
when "Integer" then scalar.to_i
|
|
291
|
-
when "Float" then scalar.to_f
|
|
292
|
-
else scalar.to_s
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def symbolize(args) = args.transform_keys(&:to_sym)
|
|
297
|
-
|
|
298
|
-
# A step the runtime declines is not a generator failure — it simply did
|
|
299
|
-
# not take effect, so nothing is recorded and the sequence carries on. The
|
|
300
|
-
# step still goes into the corpus, because a refusal is an answer, and
|
|
301
|
-
# its wording is pinned by the corpus.
|
|
302
|
-
#
|
|
303
|
-
# EvaluationError sits alongside the declared refusals deliberately : a
|
|
304
|
-
# payload the interpreter cannot read is the domain declining it, and
|
|
305
|
-
# bin/run already records exactly those in a corpus's refusals —
|
|
306
|
-
# `positive? expects a number, got "lots"` is one of banking's. Anything
|
|
307
|
-
# else still propagates and fails spec/fuzzing, which is what says the
|
|
308
|
-
# generator built a step that breaks the interpreter for reasons that have
|
|
309
|
-
# nothing to do with the domain declining a payload.
|
|
310
|
-
def safe_call
|
|
311
|
-
result = yield
|
|
312
|
-
@last_outcome = "ok"
|
|
313
|
-
result
|
|
314
|
-
rescue *Hecks::Runtime::DOMAIN_REFUSALS, Hecks::Bluebook::Expression::EvaluationError => e
|
|
315
|
-
@last_outcome = e.class.name.split("::").last
|
|
316
|
-
nil
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
# Where the addressed aggregate stood just before this dispatch — its
|
|
320
|
-
# lifecycle value, or `exists`/`absent` when it declares none. Read
|
|
321
|
-
# straight off the repository, never through dispatch, and draws
|
|
322
|
-
# nothing from the RNG, so it changes no generated byte. An identity
|
|
323
|
-
# an adversarial mutation mangled past resolving is `?`.
|
|
324
|
-
def state_before(runtime, entry, args)
|
|
325
|
-
aggregate = entry[:aggregate]
|
|
326
|
-
symbolic = symbolize(args)
|
|
327
|
-
id = Runtime::Identity.of(aggregate, symbolic) || Runtime::Identity.from(aggregate, symbolic, :id)
|
|
328
|
-
return "absent" unless id
|
|
329
|
-
|
|
330
|
-
record = runtime.registry.repository(entry[:verb].split("::").first, aggregate).find(id)
|
|
331
|
-
return "absent" unless record
|
|
332
|
-
|
|
333
|
-
lifecycle = aggregate.lifecycle
|
|
334
|
-
return "exists" unless lifecycle
|
|
335
|
-
|
|
336
|
-
key = record.state.key?(lifecycle.field) ? lifecycle.field : lifecycle.field.to_s
|
|
337
|
-
record.state[key].to_s
|
|
338
|
-
rescue StandardError
|
|
339
|
-
"?"
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
end
|
|
343
|
-
end
|
|
344
|
-
end
|
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
require "tmpdir"
|
|
3
|
-
require_relative "isolated_boot"
|
|
4
|
-
require_relative "invalid_value_generator"
|
|
5
|
-
require_relative "value_generator"
|
|
6
|
-
require_relative "sequence_generator/catalog"
|
|
7
|
-
require_relative "sequence_generator/picker"
|
|
8
|
-
require_relative "sequence_generator/step_builder"
|
|
9
|
-
require_relative "sequence_generator/outcome_tracker"
|
|
10
|
-
require_relative "sequence_generator/query_binding"
|
|
11
|
-
require_relative "sequence_generator/adversary"
|
|
12
|
-
|
|
13
|
-
module Hecks
|
|
14
|
-
module Fuzzing
|
|
15
|
-
# A random-but-valid sequence of dispatches and queries, in the exact
|
|
16
|
-
# `{name, note, steps}` shape `spec/corpus/*.json` already uses — so a
|
|
17
|
-
# generated sequence replays as a corpus member, completely unchanged.
|
|
18
|
-
# Boots a throwaway copy of
|
|
19
|
-
# the domain and dispatches each candidate step for real as it builds the
|
|
20
|
-
# sequence (not just synthesizing plausible-looking JSON) : the only way to
|
|
21
|
-
# know whether a step actually reached a new state, or which id an
|
|
22
|
-
# auto-minted entity landed on, is to run it and watch what happened.
|
|
23
|
-
#
|
|
24
|
-
# Single-call fuzzing mostly misses the bugs this project has actually
|
|
25
|
-
# found — they needed state first (a saga leg acting on a transfer that
|
|
26
|
-
# already exists, a reference pointing at a customer already registered).
|
|
27
|
-
# So this tracks what it has created as it goes, the same way
|
|
28
|
-
# spec/banking_state_machine_spec.rb's hand-written generator does, and
|
|
29
|
-
# weights later steps toward acting on what already exists.
|
|
30
|
-
#
|
|
31
|
-
# One concern per file beside this one: what the domain offers
|
|
32
|
-
# (sequence_generator/catalog.rb), which step to try next (picker.rb),
|
|
33
|
-
# how a step is built and dispatched (step_builder.rb), what a
|
|
34
|
-
# success taught us (outcome_tracker.rb), which stored values a query is
|
|
35
|
-
# asked about (query_binding.rb), and — opt-in, `adversarial:` — the
|
|
36
|
-
# argument shapes real Ruby/Rust divergences were found through
|
|
37
|
-
# (adversary.rb).
|
|
38
|
-
class SequenceGenerator
|
|
39
|
-
include Catalog
|
|
40
|
-
include Picker
|
|
41
|
-
include StepBuilder
|
|
42
|
-
include OutcomeTracker
|
|
43
|
-
include QueryBinding
|
|
44
|
-
include Adversary
|
|
45
|
-
|
|
46
|
-
# Creating commands are always eligible ; weighting them heavier (not
|
|
47
|
-
# exclusively — a domain with only one or two aggregates would starve
|
|
48
|
-
# everything else) makes a sequence reach an actionable state sooner
|
|
49
|
-
# instead of spending its early budget refusing "acts on nothing yet."
|
|
50
|
-
CREATING_WEIGHT = 2
|
|
51
|
-
|
|
52
|
-
# How often a payload is deliberately the wrong shape. Low, because a
|
|
53
|
-
# refused step reaches no new state and a sequence of them is silent.
|
|
54
|
-
MALFORMED_ARGUMENT_PROBABILITY = 0.12
|
|
55
|
-
|
|
56
|
-
# How often an optional argument is simply not given — a fair coin,
|
|
57
|
-
# because that is exactly what `optional:` means: present or absent,
|
|
58
|
-
# both legal, neither the interesting case.
|
|
59
|
-
#
|
|
60
|
-
# This is not a malformation and must not be filed as one. `malform`
|
|
61
|
-
# drops an argument too, but a dropped required argument is refused,
|
|
62
|
-
# a refusal writes no record, and bin/fuzz counts it silent — so the
|
|
63
|
-
# one outcome worth reaching (a stored record carrying a null, then
|
|
64
|
-
# queried) was unreachable from that path by construction. Until
|
|
65
|
-
# this existed no generated history contained a null at all, which
|
|
66
|
-
# meant `query_answers_match_reference` — the differential that
|
|
67
|
-
# diffs every native adapter against the reference interpreter — had
|
|
68
|
-
# never once compared a nullable field. Four query bugs shipped in
|
|
69
|
-
# exactly that blind spot (`ne:` with an empty string, array `in:`,
|
|
70
|
-
# `ne:` against a null, `in` on a numeric field); the adapter
|
|
71
|
-
# agreement gate had the identical hole and found a fourth bug the
|
|
72
|
-
# hour it was closed.
|
|
73
|
-
OPTIONAL_OMITTED_PROBABILITY = 0.5
|
|
74
|
-
|
|
75
|
-
# How strongly an unexercised verb is preferred over one this sequence has
|
|
76
|
-
# already dispatched. Random picking revisits the same handful of verbs and
|
|
77
|
-
# leaves whole commands untouched for a whole run — which is the same
|
|
78
|
-
# "reached no interesting state" problem the silent count reports, seen from
|
|
79
|
-
# the generating end rather than the scoring end.
|
|
80
|
-
UNEXERCISED_WEIGHT = 4
|
|
81
|
-
|
|
82
|
-
# `adversarial:` — the fraction of generated command steps (0.0..1.0)
|
|
83
|
-
# that get one deliberately adversarial argument mutation
|
|
84
|
-
# (adversary.rb — the shapes BUG#7–#16 were found through). `0.0`,
|
|
85
|
-
# the default, draws nothing extra from the seeded RNG, so a seed's
|
|
86
|
-
# output is byte-for-byte what it was before the option existed;
|
|
87
|
-
# any positive value is just as deterministic per seed, since every
|
|
88
|
-
# choice the mutation makes comes from the same `Random.new(seed)`.
|
|
89
|
-
#
|
|
90
|
-
# `role_draw:` / `dry_run:` — two more opt-in fractions with the
|
|
91
|
-
# identical contract (adversary.rb's `caller_draw!`, step_builder.rb's
|
|
92
|
-
# `dry_run_draw?`): `0.0` draws nothing, so every pinned seed is
|
|
93
|
-
# byte-for-byte what it was; `bin/qa_sweep` reads them from
|
|
94
|
-
# `QualityControlDials::ROLE_DRAW_PROBABILITY`/`DRY_RUN_FRACTION`.
|
|
95
|
-
#
|
|
96
|
-
# `prefix:` / `favor:` — the two levers `CoverageCampaign` pulls
|
|
97
|
-
# (coverage_campaign.rb has the why). `prefix:` is `{ "seed", "steps",
|
|
98
|
-
# "favor", "prefix" }`: re-generate that seed's first `steps` attempts
|
|
99
|
-
# (itself recursively prefixed) before this seed's own randomness
|
|
100
|
-
# starts, so a seed can begin from state an earlier seed reached.
|
|
101
|
-
# `favor:` names verbs the picker weights up. `nil`/`[]`, the
|
|
102
|
-
# defaults, draw nothing extra and change nothing: every pinned seed
|
|
103
|
-
# is byte-for-byte what it was.
|
|
104
|
-
#
|
|
105
|
-
# Generates one random-but-valid step sequence for `domain_path`, dispatching
|
|
106
|
-
# each step for real against a throwaway boot as it builds it.
|
|
107
|
-
#
|
|
108
|
-
# @param domain_path [String] path to the domain directory to boot
|
|
109
|
-
# @param seed [Integer] RNG seed; every draw this run makes is reproducible
|
|
110
|
-
# from it
|
|
111
|
-
# @param steps [Integer] number of generation attempts to make
|
|
112
|
-
# @param adapter [Symbol] persistence adapter to boot with (default `:memory`)
|
|
113
|
-
# @param adversarial [Float] fraction of command steps to mutate adversarially
|
|
114
|
-
# (default `0.0`, drawing nothing extra)
|
|
115
|
-
# @param role_draw [Float] fraction of gated commands to draw a caller for
|
|
116
|
-
# (default `0.0`, drawing nothing extra)
|
|
117
|
-
# @param dry_run [Float] fraction of command steps dispatched as dry runs
|
|
118
|
-
# (default `0.0`, drawing nothing extra)
|
|
119
|
-
# @param prefix [Hash, nil] another seed's own generation spec to replay
|
|
120
|
-
# first (`{"seed" =>, "steps" =>, "favor" =>, "prefix" =>}`), or `nil`
|
|
121
|
-
# @param favor [Array<String>, Array<Symbol>] verbs the picker weights up
|
|
122
|
-
# @return [Array<Hash>] the generated step list, each a command, query, or
|
|
123
|
-
# read-model step
|
|
124
|
-
def self.generate(domain_path, seed:, steps:, **)
|
|
125
|
-
new(domain_path, seed: seed, steps: steps, **).call
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# The same generation, with what it reached — `coverage` is
|
|
129
|
-
# `[[attempt_index, tuple], ...]` (`coverage_tuple`), `verbs` every
|
|
130
|
-
# verb the booted catalog offered, so a campaign can tell a verb it
|
|
131
|
-
# never hit from one that does not exist.
|
|
132
|
-
Trace = Struct.new(:steps, :coverage, :verbs, keyword_init: true)
|
|
133
|
-
|
|
134
|
-
# Generates one sequence exactly like `.generate`, but also returns the
|
|
135
|
-
# coverage and verb data a campaign needs, which `.generate` discards.
|
|
136
|
-
#
|
|
137
|
-
# @param domain_path [String] path to the domain directory to boot
|
|
138
|
-
# @param seed [Integer] RNG seed; every draw this run makes is reproducible
|
|
139
|
-
# from it
|
|
140
|
-
# @param steps [Integer] number of generation attempts to make
|
|
141
|
-
# @return [Hecks::Fuzzing::SequenceGenerator::Trace] the generated steps,
|
|
142
|
-
# the coverage tuples reached, and every verb the booted catalog offered
|
|
143
|
-
def self.trace(domain_path, seed:, steps:, **)
|
|
144
|
-
generator = new(domain_path, seed: seed, steps: steps, **)
|
|
145
|
-
Trace.new(steps: generator.call, coverage: generator.coverage, verbs: generator.verbs)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# How strongly a `favor:` verb is preferred when it is eligible —
|
|
149
|
-
# the same order of magnitude as an unexercised verb, so favor
|
|
150
|
-
# steers without drowning out what this sequence has not touched.
|
|
151
|
-
FAVOR_WEIGHT = 4
|
|
152
|
-
|
|
153
|
-
attr_reader :coverage, :verbs
|
|
154
|
-
|
|
155
|
-
# How many events the generated sequence actually produced — not
|
|
156
|
-
# steps, not successful dispatches, but the sum of every Result#events
|
|
157
|
-
# length across the run. This is the count bin/fuzz declares as the
|
|
158
|
-
# script's own `expectations.events` claim: whatever was achieved
|
|
159
|
-
# during generation becomes the claim a fresh replay of the same
|
|
160
|
-
# script is held to. Zero means the sequence never
|
|
161
|
-
# reached an interesting state — a fuzzer-effectiveness fact, not a
|
|
162
|
-
# replay one.
|
|
163
|
-
attr_reader :event_count
|
|
164
|
-
|
|
165
|
-
# @param domain_path [String] path to the domain directory to boot
|
|
166
|
-
# @param seed [Integer] RNG seed; every draw this run makes is reproducible
|
|
167
|
-
# from it
|
|
168
|
-
# @param steps [Integer] number of generation attempts `#call` will make
|
|
169
|
-
# @param adapter [Symbol] persistence adapter to boot with
|
|
170
|
-
# @param adversarial [Float] fraction of command steps to mutate adversarially
|
|
171
|
-
# @param role_draw [Float] fraction of gated commands to draw a caller for
|
|
172
|
-
# @param dry_run [Float] fraction of command steps dispatched as dry runs
|
|
173
|
-
# @param prefix [Hash, nil] another seed's own generation spec to replay
|
|
174
|
-
# first, or `nil`
|
|
175
|
-
# @param favor [Array<String>, Array<Symbol>] verbs the picker weights up
|
|
176
|
-
# @raise [ArgumentError] if `adversarial`, `role_draw`, or `dry_run` is not a
|
|
177
|
-
# Numeric between 0.0 and 1.0
|
|
178
|
-
def initialize(domain_path, seed:, steps:, adapter: :memory, adversarial: 0.0, role_draw: 0.0, dry_run: 0.0,
|
|
179
|
-
prefix: nil, favor: [])
|
|
180
|
-
{ adversarial: adversarial, role_draw: role_draw, dry_run: dry_run }.each do |name, fraction|
|
|
181
|
-
next if fraction.is_a?(Numeric) && fraction.between?(0, 1)
|
|
182
|
-
|
|
183
|
-
raise ArgumentError, "#{name}: must be a fraction between 0.0 and 1.0, got #{fraction.inspect}"
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
@domain_path = domain_path
|
|
187
|
-
@seed = seed
|
|
188
|
-
@step_count = steps
|
|
189
|
-
@adapter = adapter
|
|
190
|
-
@adversarial = adversarial.to_f
|
|
191
|
-
@role_draw = role_draw.to_f
|
|
192
|
-
@dry_run = dry_run.to_f
|
|
193
|
-
restart_random(seed)
|
|
194
|
-
@known_ids = Hash.new { |h, k| h[k] = [] }
|
|
195
|
-
@entity_known_ids = Hash.new { |h, k| h[k] = [] }
|
|
196
|
-
@appended_identities = Hash.new { |h, k| h[k] = [] }
|
|
197
|
-
# "Domain::Aggregate" => the rows this sequence has stored that a
|
|
198
|
-
# query filters on (query_binding.rb).
|
|
199
|
-
@written_rows = Hash.new { |h, k| h[k] = [] }
|
|
200
|
-
# Role => [actor ids] this sequence's own successful
|
|
201
|
-
# `Governance::RoleAssignment.Assign` steps granted — what the
|
|
202
|
-
# `actor_known` caller shape draws from (adversary.rb).
|
|
203
|
-
@granted = Hash.new { |h, k| h[k] = [] }
|
|
204
|
-
@precedence_caller = nil
|
|
205
|
-
@exercised = Set.new
|
|
206
|
-
@event_count = 0
|
|
207
|
-
@prefix = prefix
|
|
208
|
-
@favor = Array(favor)
|
|
209
|
-
@own_favor = @favor
|
|
210
|
-
@coverage = []
|
|
211
|
-
@verbs = []
|
|
212
|
-
@attempt = 0
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
# Runs the generation this instance was configured for, against a fresh,
|
|
216
|
-
# isolated boot of `@domain_path`.
|
|
217
|
-
#
|
|
218
|
-
# @return [Array<Hash>] the generated step list, each a command, query, or
|
|
219
|
-
# read-model step; a picker miss that produced no step is dropped
|
|
220
|
-
def call
|
|
221
|
-
# Real leftover data from ordinary use (bin/console, whatever) lives
|
|
222
|
-
# under the example's data/ — a generator that boots against it
|
|
223
|
-
# starts from state its own known_ids tracking doesn't know about.
|
|
224
|
-
# IsolatedBoot resets that and rebinds persistence to Memory, since
|
|
225
|
-
# a Postgres-bound domain's real store lives outside the copied
|
|
226
|
-
# directory entirely and `rm_rf`ing data/ alone cannot reach it —
|
|
227
|
-
# see isolated_boot.rb's own header.
|
|
228
|
-
IsolatedBoot.call(@domain_path, adapter: @adapter) do |copy|
|
|
229
|
-
runtime = Hecks.boot(copy)
|
|
230
|
-
catalog = build_catalog(runtime)
|
|
231
|
-
@verbs = catalog.values_at(:creating, :instance, :entity_commands, :queries, :entity_queries, :read_models)
|
|
232
|
-
.flatten.map { |entry| entry[:verb] }.uniq
|
|
233
|
-
|
|
234
|
-
steps = []
|
|
235
|
-
if @prefix
|
|
236
|
-
realize_prefix(runtime, catalog, @prefix, prefix_limit(@prefix), steps)
|
|
237
|
-
restart_random(@seed)
|
|
238
|
-
@favor = @own_favor
|
|
239
|
-
end
|
|
240
|
-
@step_count.times { steps << attempt_step(runtime, catalog) }
|
|
241
|
-
steps.compact
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
private
|
|
246
|
-
|
|
247
|
-
# A prefix is the first `limit` attempts of another seed's generation,
|
|
248
|
-
# re-run for real: its own nested prefix first (capped the same way it
|
|
249
|
-
# was capped when that seed was generated), then that seed's own
|
|
250
|
-
# `Random.new(seed)` and favor for the rest. Same inputs, same
|
|
251
|
-
# catalog, same draws — the same steps, and the same known ids and
|
|
252
|
-
# exercised verbs carried forward into this seed.
|
|
253
|
-
#
|
|
254
|
-
# The prefix is on top of this seed's own budget, not out of it. A
|
|
255
|
-
# spliced seed still makes all `steps` attempts of its own after the
|
|
256
|
-
# prefix; a prefix is capped at `steps` attempts, so a spliced
|
|
257
|
-
# sequence is at most twice as long as an unspliced one. Taking the
|
|
258
|
-
# prefix out of the budget (the first version of this) left a spliced
|
|
259
|
-
# seed replaying state already seen with almost nothing left to
|
|
260
|
-
# explore from it — measured: fewer distinct tuples than unguided.
|
|
261
|
-
def realize_prefix(runtime, catalog, spec, limit, steps)
|
|
262
|
-
return 0 unless limit.positive?
|
|
263
|
-
|
|
264
|
-
inner = spec["prefix"]
|
|
265
|
-
used = inner ? realize_prefix(runtime, catalog, inner, [prefix_limit(inner), limit].min, steps) : 0
|
|
266
|
-
restart_random(Integer(spec.fetch("seed")))
|
|
267
|
-
@favor = Array(spec["favor"])
|
|
268
|
-
(limit - used).times { steps << attempt_step(runtime, catalog) }
|
|
269
|
-
limit
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
# Both random streams start over together, so a prefix replay draws the
|
|
273
|
-
# same query bindings the prefix seed's own generation did.
|
|
274
|
-
def restart_random(seed)
|
|
275
|
-
@random = Random.new(seed)
|
|
276
|
-
@binding_random = Random.new(seed + QueryBinding::BINDING_SEED_OFFSET)
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def prefix_limit(spec) = Integer(spec.fetch("steps")).clamp(0, @step_count)
|
|
280
|
-
|
|
281
|
-
def attempt_step(runtime, catalog)
|
|
282
|
-
index = @attempt
|
|
283
|
-
@attempt += 1
|
|
284
|
-
entry = pick(catalog)
|
|
285
|
-
return nil unless entry
|
|
286
|
-
|
|
287
|
-
@exercised << entry[:verb]
|
|
288
|
-
@state_before = "-"
|
|
289
|
-
step =
|
|
290
|
-
if entry[:query] then build_query_step(runtime, entry)
|
|
291
|
-
elsif entry[:model] then build_read_model_step(runtime, entry)
|
|
292
|
-
else build_command_step(runtime, catalog, entry)
|
|
293
|
-
end
|
|
294
|
-
@coverage << [index, coverage_tuple(entry, step)]
|
|
295
|
-
step
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
# `verb | kind | state before | mutation | outcome` — see
|
|
299
|
-
# CoverageCampaign's header for why the unit is this and not the
|
|
300
|
-
# verb. `state` is the addressed aggregate's lifecycle value (or
|
|
301
|
-
# `exists`/`absent` for one without a lifecycle) read just before
|
|
302
|
-
# dispatch; `mutation` names every adversarial mutation and its shape;
|
|
303
|
-
# `outcome` is `ok` or the refusal class `safe_call` rescued.
|
|
304
|
-
def coverage_tuple(entry, step)
|
|
305
|
-
kind = %w[verb query dry_run].find { |key| step.key?(key) }
|
|
306
|
-
mutation = Array(step["adversarial"]).map { |m| [m["mutation"], m["shape"]].compact.join(":") }.join("+")
|
|
307
|
-
[entry[:verb], kind, @state_before, mutation.empty? ? "-" : mutation, @last_outcome].join(" | ")
|
|
308
|
-
end
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
end
|