hecks 3.6.0 → 3.9.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/bluebook/meta_validator/modes.rb +14 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot/disk_cache.rb +28 -3
- data/lib/hecks/bluebook/meta_validator/verdict_cache/prebuilt.rb +44 -0
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +3 -17
- data/lib/hecks/bluebook/meta_validator.rb +4 -2
- data/lib/hecks/cache_dir.rb +10 -0
- data/lib/hecks/corpus/routes.rb +1 -1
- data/lib/hecks/deploy/bluebook/deploy.bluebook +109 -0
- data/lib/hecks/deploy/oidc.json +10 -0
- data/lib/hecks/doors/usage_cache.rb +19 -9
- data/lib/hecks/fuzzing/coverage_campaign.rb +42 -17
- data/lib/hecks/fuzzing/failure_triage.rb +80 -0
- data/lib/hecks/fuzzing/form_census/chapter_forms.rb +38 -0
- data/lib/hecks/fuzzing/form_census/reference_forms.rb +49 -0
- data/lib/hecks/fuzzing/form_census.rb +28 -52
- data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +22 -7
- data/lib/hecks/fuzzing/isolated_boot/scratch_schema.rb +27 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +6 -2
- data/lib/hecks/fuzzing/mutation/checks.rb +76 -0
- data/lib/hecks/fuzzing/mutation/finders.rb +136 -0
- data/lib/hecks/fuzzing/mutation/judge.rb +94 -0
- data/lib/hecks/fuzzing/mutation/observation.rb +38 -0
- data/lib/hecks/fuzzing/mutation/operators.rb +52 -0
- data/lib/hecks/fuzzing/mutation/report.rb +78 -0
- data/lib/hecks/fuzzing/mutation/run.rb +99 -0
- data/lib/hecks/fuzzing/mutation/selection.rb +32 -0
- data/lib/hecks/fuzzing/mutation/site.rb +21 -0
- data/lib/hecks/fuzzing/mutation/source_map.rb +59 -0
- data/lib/hecks/fuzzing/mutation.rb +12 -0
- data/lib/hecks/fuzzing/properties/catalog.rb +10 -59
- data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +63 -0
- data/lib/hecks/fuzzing/properties/read_model_names.rb +50 -0
- data/lib/hecks/fuzzing/properties/read_models.rb +138 -0
- data/lib/hecks/fuzzing/properties/role_checks.rb +50 -0
- data/lib/hecks/fuzzing/properties.rb +7 -0
- data/lib/hecks/fuzzing/replay/observation.rb +1 -1
- data/lib/hecks/fuzzing/replay/role_check.rb +49 -0
- data/lib/hecks/fuzzing/replay/session.rb +22 -6
- data/lib/hecks/fuzzing/replay.rb +1 -0
- data/lib/hecks/fuzzing/runtime_coverage.rb +70 -0
- data/lib/hecks/fuzzing/shrinker/value_simplification.rb +64 -0
- data/lib/hecks/fuzzing/shrinker.rb +3 -0
- data/lib/hecks/fuzzing.rb +3 -0
- data/lib/hecks/gate/stages.yml +12 -2
- data/lib/hecks/hecks/adapters/deploy_toolchain/answers.rb +2 -1
- data/lib/hecks/hecks/adapters/github_rulesets.rb +7 -5
- data/lib/hecks/hecks/adapters/process_pool.rb +26 -4
- data/lib/hecks/hecks/adapters/site_toolchain/prober.rb +64 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +41 -5
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +37 -0
- data/lib/hecks/hecks/hecks.hecksagon +6 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -1
- data/lib/hecks/projections/deploy/box/roll_scripts.rb +6 -4
- data/lib/hecks/projections/deploy/box/settings/patterns.rb +4 -0
- data/lib/hecks/projections/deploy/box/settings/types.rb +7 -3
- data/lib/hecks/projections/deploy/box/settings.rb +18 -0
- data/lib/hecks/projections/deploy/box/shared_database.rb +69 -0
- data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +6 -7
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/box/templates/provision-database.sh.tmpl +97 -0
- data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +4 -2
- data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +2 -1
- data/lib/hecks/projections/deploy/box.rb +14 -2
- data/lib/hecks/projections/deploy/shared_database_instance/templates/Makefile.tmpl +11 -0
- data/lib/hecks/projections/deploy/shared_database_instance/templates/README.md.tmpl +25 -0
- data/lib/hecks/projections/deploy/shared_database_instance/templates/rds.yaml.tmpl +180 -0
- data/lib/hecks/projections/deploy/shared_database_instance.rb +106 -0
- data/lib/hecks/projections/deploy/vercel/settings.rb +83 -0
- data/lib/hecks/projections/deploy/vercel.rb +134 -0
- data/lib/hecks/projections/site/probe.rb +131 -0
- data/lib/hecks/projections/site/site_host/dockerfile.rb +1 -1
- data/lib/hecks/projections/site/site_host/templates.rb +12 -4
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +9 -2
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +1 -0
- data/lib/hecks/quality_control/cli/qa_sweep/seed_loop.rb +10 -2
- data/lib/hecks/runtime/value/validation.rb +34 -3
- data/lib/hecks/rust_build/native_build.rb +29 -1
- data/lib/hecks/rust_build/wasm.rb +16 -3
- data/lib/hecks/site/bluebook/site.bluebook +31 -0
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/tools/deploy_recipe/generation.rb +59 -0
- data/lib/hecks/tools/deploy_recipe/targets.rb +56 -2
- data/lib/hecks/tools/deploy_recipe/worlds.rb +2 -1
- data/lib/hecks/tools/deploy_recipe.rb +20 -10
- data/lib/hecks/tools/fuzz_sweep/options.rb +21 -10
- data/lib/hecks/tools/fuzz_sweep/reporting.rb +25 -3
- data/lib/hecks/tools/fuzz_sweep.rb +26 -9
- data/lib/hecks/tools/lanes/files.rb +2 -4
- data/lib/hecks/tools/lanes/live.rb +3 -4
- data/lib/hecks/tools/lanes/ruleset.rb +23 -1
- data/lib/hecks/tools/lanes.rb +12 -1
- data/lib/hecks/tools/mutation_run.rb +73 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +3 -1
- data/prebuilt/4e6c3cd1f578a38732ea5454dae52562d03d8e9886216077a5c34cbf589f3463.marshal +0 -0
- data/prebuilt/911cdcc56b5a094ed26583a4c76401dba6455b1f434722ed22b850bc1b1bdd28.marshal +0 -0
- data/prebuilt/verdicts-3230a251570aa03666708d16f92e646feab6a3f8cc2a3540c017d748e41d41a1.json +1 -0
- data/qa/settings.yml +1 -0
- data/rust/codegen/src/fielded.rs +3 -3
- data/rust/codegen/src/json_codec.rs +2 -2
- data/rust/codegen/src/naming.rs +20 -0
- data/rust/host/Cargo.toml +6 -2
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +28 -5
- data/rust/host/src/auth.rs +11 -1
- data/rust/host/src/boundary_fuzz/api.rs +260 -0
- data/rust/host/src/boundary_fuzz/auth.rs +119 -0
- data/rust/host/src/boundary_fuzz/expr_json.rs +158 -0
- data/rust/host/src/boundary_fuzz/ir.rs +79 -0
- data/rust/host/src/boundary_fuzz/server.rs +227 -0
- data/rust/host/src/boundary_fuzz/web.rs +125 -0
- data/rust/host/src/commerce_ir.rs +459 -0
- data/rust/host/src/extension.rs +100 -0
- data/rust/host/src/fuzz_support.rs +165 -0
- data/rust/host/src/ir.rs +4 -448
- data/rust/host/src/journal.rs +1 -1
- data/rust/host/src/lib.rs +39 -0
- data/rust/host/src/main.rs +10 -44
- data/rust/host/src/mint.rs +1 -1
- data/rust/host/src/payments/tests.rs +8 -8
- data/rust/host/src/payments.rs +10 -10
- data/rust/host/src/rate_limit.rs +27 -41
- data/rust/host/src/secrets.rs +44 -0
- data/rust/host/src/server.rs +6 -1
- data/rust/host/src/web/commerce.rs +113 -0
- data/rust/host/src/web/newsletter.rs +2 -1
- data/rust/host/src/web/newsletter_send.rs +1 -1
- data/rust/host/src/web/registration_receipt.rs +2 -2
- data/rust/host/src/web/registrations.rs +49 -49
- data/rust/host/src/web/resend_webhook.rs +299 -0
- data/rust/host/src/web.rs +55 -49
- data/rust/parser/fuzz/.gitignore +4 -0
- data/rust/parser/fuzz/Cargo.lock +89 -0
- data/rust/parser/fuzz/Cargo.toml +27 -0
- data/rust/parser/fuzz/README.md +19 -0
- data/rust/parser/fuzz/fuzz_targets/chapter.rs +50 -0
- data/rust/parser/fuzz/fuzz_targets/resolve.rs +35 -0
- data/rust/parser/src/parse/command.rs +14 -2
- data/rust/parser/src/parse/mod.rs +25 -0
- data/rust/parser/src/ruby_value.rs +95 -6
- data/rust/src/kernel/json.rs +34 -3
- metadata +58 -2
|
@@ -8,11 +8,17 @@ module Hecks
|
|
|
8
8
|
module FuzzSweep
|
|
9
9
|
# The command line of a sweep: its options, and what the chosen adapter needs.
|
|
10
10
|
module Options
|
|
11
|
+
# The options that take a whole number, and the key each fills.
|
|
12
|
+
INTEGER_OPTIONS = { "--seeds" => :seeds, "--steps" => :steps, "--workers" => :workers }.freeze
|
|
13
|
+
|
|
14
|
+
# The most children a Postgres sweep runs at once unless `--workers` says otherwise.
|
|
15
|
+
POSTGRES_WORKERS = 4
|
|
16
|
+
|
|
11
17
|
# @param args [Array<String>] the command line, consumed
|
|
12
|
-
# @return [Hash, nil] `seeds`, `steps`, `workers`, `adapter` and `domain`; nil
|
|
13
|
-
# refusal
|
|
18
|
+
# @return [Hash, nil] `seeds`, `steps`, `workers`, `adapter`, `persist` and `domain`; nil
|
|
19
|
+
# after a refusal
|
|
14
20
|
def parse(args)
|
|
15
|
-
options = { seeds: 20, steps: 30, workers: nil, adapter: :memory, domain: nil }
|
|
21
|
+
options = { seeds: 20, steps: 30, workers: nil, adapter: :memory, domain: nil, persist: false }
|
|
16
22
|
until args.empty?
|
|
17
23
|
apply_option(options, args.shift, args)
|
|
18
24
|
return nil unless adapter_known?(options[:adapter])
|
|
@@ -22,11 +28,9 @@ module Hecks
|
|
|
22
28
|
|
|
23
29
|
# Records one command-line word in `options`, taking its value from `rest` when it has one.
|
|
24
30
|
def apply_option(options, arg, rest)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
when "--workers" then options[:workers] = Integer(rest.shift)
|
|
29
|
-
when "--adapter" then options[:adapter] = rest.shift.to_s.downcase.to_sym
|
|
31
|
+
if (key = INTEGER_OPTIONS[arg]) then options[key] = Integer(rest.shift)
|
|
32
|
+
elsif arg == "--persist-regressions" then options[:persist] = true
|
|
33
|
+
elsif arg == "--adapter" then options[:adapter] = rest.shift.to_s.downcase.to_sym
|
|
30
34
|
else options[:domain] = arg
|
|
31
35
|
end
|
|
32
36
|
end
|
|
@@ -44,11 +48,18 @@ module Hecks
|
|
|
44
48
|
options[:adapter] != :postgres || postgres_reachable?
|
|
45
49
|
end
|
|
46
50
|
|
|
47
|
-
# One child per core
|
|
51
|
+
# One child per core. A real Postgres is capped, because each child holds its own
|
|
52
|
+
# connections and its own scratch schema, and runs one at a time on macOS, where libpq
|
|
53
|
+
# segfaults when a forked child connects.
|
|
48
54
|
#
|
|
49
55
|
# @return [Integer] how many children may run at once
|
|
50
56
|
def worker_count(options)
|
|
51
|
-
options[:workers] || (options[:adapter] == :postgres ?
|
|
57
|
+
options[:workers] || (options[:adapter] == :postgres ? postgres_workers : Etc.nprocessors)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Integer] the children a Postgres sweep runs at once by default
|
|
61
|
+
def postgres_workers
|
|
62
|
+
RUBY_PLATFORM.include?("darwin") ? 1 : [Etc.nprocessors, POSTGRES_WORKERS].min
|
|
52
63
|
end
|
|
53
64
|
|
|
54
65
|
# `postgres` needs a reachable local server and is slower per dispatch, so pass smaller
|
|
@@ -51,9 +51,17 @@ module Hecks
|
|
|
51
51
|
def report_findings(result)
|
|
52
52
|
grouped = result.failures.group_by { |failure| failure[:signature] }
|
|
53
53
|
puts " #{grouped.size} distinct finding(s):"
|
|
54
|
+
shrunk = shrink_groups(result, grouped.values)
|
|
55
|
+
persist_regressions(result, shrunk) if result.persist
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Prints each finding and saves its shrunk repro.
|
|
59
|
+
#
|
|
60
|
+
# @return [Array<Hash>] the findings, with their shrunk `:steps`
|
|
61
|
+
def shrink_groups(result, groups)
|
|
54
62
|
save_dir = save_directory(result)
|
|
55
|
-
|
|
56
|
-
print_finding(group,
|
|
63
|
+
groups.each_with_index.map do |group, index|
|
|
64
|
+
print_finding(group, index + 1, result.seeds)
|
|
57
65
|
save_finding(result, group.min_by { |failure| failure[:steps].length }, save_dir)
|
|
58
66
|
end
|
|
59
67
|
end
|
|
@@ -71,11 +79,25 @@ module Hecks
|
|
|
71
79
|
end
|
|
72
80
|
|
|
73
81
|
# Shrinks the smallest failing sequence of a finding and saves it as a repro script.
|
|
82
|
+
#
|
|
83
|
+
# @return [Hash] the finding, with its shrunk `:steps`
|
|
84
|
+
def repro_path(result, finding, save_dir) = File.join(save_dir, "#{result.name}-seed#{finding[:seed]}.json")
|
|
85
|
+
|
|
74
86
|
def save_finding(result, smallest, save_dir)
|
|
75
87
|
shrunk = shrink(result.domain, smallest[:steps], smallest[:signature], result.adapter)
|
|
76
|
-
save_path =
|
|
88
|
+
save_path = repro_path(result, smallest, save_dir)
|
|
77
89
|
write_script(shrunk, save_path, smallest[:seed], result.name)
|
|
78
90
|
print_saved(smallest[:steps].length, shrunk.length, "hecks run #{result.domain} #{save_path}")
|
|
91
|
+
smallest.merge(steps: shrunk)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Keeps one minimized repro per distinct finding. Findings that shrank to the same thing
|
|
95
|
+
# collapse here, and one already kept from an earlier run is left alone.
|
|
96
|
+
def persist_regressions(result, shrunk)
|
|
97
|
+
Hecks::Fuzzing::FailureTriage.dedupe(shrunk).each do |finding|
|
|
98
|
+
path = Hecks::Fuzzing::FailureTriage.persist(result.root, result.name, finding)
|
|
99
|
+
puts(path ? " kept regression: #{path}" : " regression #{finding[:triage]} already kept")
|
|
100
|
+
end
|
|
79
101
|
end
|
|
80
102
|
|
|
81
103
|
def print_saved(from, to, command)
|
|
@@ -15,6 +15,7 @@ module Hecks
|
|
|
15
15
|
# properties, then shrinks each distinct finding and saves it under `tmp/fuzz-failures/`.
|
|
16
16
|
#
|
|
17
17
|
# hecks fuzz [domain] [--seeds N] [--steps N] [--workers N] [--adapter memory|sqlite|postgres]
|
|
18
|
+
# [--persist-regressions]
|
|
18
19
|
#
|
|
19
20
|
# The sweep is single-runtime on purpose; the cross-runtime differential is
|
|
20
21
|
# `spec/rust_conformance_fuzz_spec.rb`. Without a domain it sweeps every domain
|
|
@@ -40,7 +41,7 @@ module Hecks
|
|
|
40
41
|
Draw = Struct.new(:clean, :failures, :drawn, :skipped)
|
|
41
42
|
|
|
42
43
|
# What the sweep of one domain drew and found, for the final report.
|
|
43
|
-
Result = Struct.new(:domain, :name, :seeds, :drawn, :clean, :failures, :adapter, :root)
|
|
44
|
+
Result = Struct.new(:domain, :name, :seeds, :drawn, :clean, :failures, :adapter, :root, :persist)
|
|
44
45
|
|
|
45
46
|
extend Options
|
|
46
47
|
extend Findings
|
|
@@ -83,7 +84,16 @@ module Hecks
|
|
|
83
84
|
require "hecks/bluebook/meta_validator"
|
|
84
85
|
Hecks::Bluebook::MetaValidator.grammar_registry
|
|
85
86
|
$stdout.flush
|
|
86
|
-
fuzz_in_pool(domains, workers) { |domain|
|
|
87
|
+
fuzz_in_pool(domains, workers) { |domain| fuzz_in_child(options, domain, root) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Runs in a forked child. A Postgres child gets its own scratch schema, so children can fuzz
|
|
91
|
+
# one database at once.
|
|
92
|
+
#
|
|
93
|
+
# @return [Boolean] whether the domain was clean
|
|
94
|
+
def fuzz_in_child(options, domain, root)
|
|
95
|
+
ENV[Hecks::Fuzzing::IsolatedBoot::FUZZ_SCHEMA_ENV] = "hecks_fuzz_#{Process.pid}" if options[:adapter] == :postgres
|
|
96
|
+
fuzz_with(options, domain, root)
|
|
87
97
|
end
|
|
88
98
|
|
|
89
99
|
# @return [Boolean] whether every domain was clean
|
|
@@ -92,29 +102,36 @@ module Hecks
|
|
|
92
102
|
end
|
|
93
103
|
|
|
94
104
|
def fuzz_with(options, domain, root)
|
|
95
|
-
fuzz_domain(domain, options
|
|
105
|
+
fuzz_domain(domain, options, root)
|
|
96
106
|
end
|
|
97
107
|
|
|
98
108
|
# Draws seeds until `seeds` results execute or the draw cap is hit.
|
|
99
109
|
#
|
|
100
110
|
# @param domain [String] the domain directory
|
|
101
|
-
# @param
|
|
102
|
-
#
|
|
103
|
-
#
|
|
111
|
+
# @param options [Hash] the parsed command line: `seeds` (how many executing seeds to ask
|
|
112
|
+
# for), `steps` (how many steps each sequence asks for), `adapter` (the persistence to boot
|
|
113
|
+
# on) and `persist` (whether each distinct finding's minimized repro is also kept under
|
|
114
|
+
# `spec/corpus/regressions/`)
|
|
104
115
|
# @param root [String] the checkout
|
|
105
116
|
# @return [Boolean] true if clean or skipped
|
|
106
|
-
def fuzz_domain(domain,
|
|
117
|
+
def fuzz_domain(domain, options, root = Tools::ROOT)
|
|
107
118
|
domain = domain.chomp("/")
|
|
108
119
|
name = File.basename(domain)
|
|
120
|
+
adapter = options[:adapter]
|
|
109
121
|
puts "── #{name}#{" (#{adapter})" unless adapter == :memory}"
|
|
110
122
|
|
|
111
|
-
draw = draw_seeds(domain, seeds, steps, adapter)
|
|
123
|
+
draw = draw_seeds(domain, options[:seeds], options[:steps], adapter)
|
|
112
124
|
if draw.skipped
|
|
113
125
|
puts " skipped — #{draw.skipped} (not fuzzable under :#{adapter})"
|
|
114
126
|
return true
|
|
115
127
|
end
|
|
116
128
|
|
|
117
|
-
report(settle_known(
|
|
129
|
+
report(settle_known(sweep_result(domain, name, draw, options, root)))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def sweep_result(domain, name, draw, options, root)
|
|
133
|
+
Result.new(domain, name, options[:seeds], draw.drawn, draw.clean, draw.failures,
|
|
134
|
+
options[:adapter], root, options[:persist] || false)
|
|
118
135
|
end
|
|
119
136
|
|
|
120
137
|
# Known findings print but count as clean, so the sweep does not read as short.
|
|
@@ -41,7 +41,7 @@ module Hecks
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
43
|
def rulesets
|
|
44
|
-
Lanes.
|
|
44
|
+
Lanes.rulesets.to_h { |name, ruleset| [ruleset_path(name), "#{JSON.pretty_generate(ruleset)}\n"] }
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# The workflow at `path` for the lanes whose `column` is set, or no file if none is.
|
|
@@ -50,9 +50,7 @@ module Hecks
|
|
|
50
50
|
lanes.empty? ? {} : { File.join(@root, path) => yield(lanes) }
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
def ruleset_path(
|
|
54
|
-
|
|
55
|
-
def ruleset(lane) = "#{JSON.pretty_generate(Lanes.ruleset(lane))}\n"
|
|
53
|
+
def ruleset_path(name) = File.join(@root, Lanes::RULESETS, "#{name}.json")
|
|
56
54
|
|
|
57
55
|
def write(files, stale, extra)
|
|
58
56
|
stale.each do |path|
|
|
@@ -14,7 +14,7 @@ module Hecks
|
|
|
14
14
|
# @param confirm [Boolean] whether to create or update the rulesets
|
|
15
15
|
# @return [Integer] 0 when GitHub agrees (or was made to), else 1
|
|
16
16
|
def run(confirm)
|
|
17
|
-
found = Lanes.
|
|
17
|
+
found = Lanes.rulesets.values.flat_map { |projected| sync(projected, confirm) }
|
|
18
18
|
puts "lanes: GitHub holds every guarded lane's ruleset as projected" if found.empty? && !confirm
|
|
19
19
|
return 0 if found.empty?
|
|
20
20
|
|
|
@@ -24,9 +24,8 @@ module Hecks
|
|
|
24
24
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
|
-
# @return [Array<String>] the differences left after the
|
|
28
|
-
def sync(
|
|
29
|
-
projected = Lanes.ruleset(lane)
|
|
27
|
+
# @return [Array<String>] the differences left after the ruleset was looked at
|
|
28
|
+
def sync(projected, confirm)
|
|
30
29
|
differences = @github.differences(projected, @github.named(projected["name"]))
|
|
31
30
|
differences.each { |line| puts line }
|
|
32
31
|
return [] if differences.empty?
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "hecks/vocabulary"
|
|
4
|
+
require_relative "../../quality_control/adapters/github_checks"
|
|
4
5
|
|
|
5
6
|
module Hecks
|
|
6
7
|
module Tools
|
|
@@ -12,6 +13,10 @@ module Hecks
|
|
|
12
13
|
# past it: a push of a commit that passed is how a promotion moves the lane, and the GitHub
|
|
13
14
|
# Actions app cannot be named as a bypass actor.
|
|
14
15
|
module Ruleset
|
|
16
|
+
# The GitHub Actions app. A required check counts only when this app reported it, so no
|
|
17
|
+
# other app, token or person can post a passing check of the same name against a commit.
|
|
18
|
+
GITHUB_ACTIONS_APP_ID = Hecks::Adapters::GithubChecks::GITHUB_ACTIONS_APP_ID
|
|
19
|
+
|
|
15
20
|
# What every guarded lane forbids: deleting it and rewinding it.
|
|
16
21
|
FIXED = [{ "type" => "deletion" }, { "type" => "non_fast_forward" }].freeze
|
|
17
22
|
|
|
@@ -25,6 +30,21 @@ module Hecks
|
|
|
25
30
|
"bypass_actors" => [], "rules" => rules(lane) }
|
|
26
31
|
end
|
|
27
32
|
|
|
33
|
+
# The ruleset for the tag a guarded lane feeds. The tag follows the lane, so it may move
|
|
34
|
+
# forward, which GitHub allows under `non_fast_forward` (a descendant is not a force-push);
|
|
35
|
+
# it cannot be deleted or pointed at an older commit, and no actor is let past it.
|
|
36
|
+
#
|
|
37
|
+
# @param lane [Hash{String => String}] a guarded `Lane` row
|
|
38
|
+
# @return [Hash, nil] the ruleset GitHub is given for the tag, nil when the lane feeds none
|
|
39
|
+
def tag(lane)
|
|
40
|
+
feed = lane["feeds"].to_s
|
|
41
|
+
return if feed.empty?
|
|
42
|
+
|
|
43
|
+
{ "name" => "tag-#{feed}", "target" => "tag", "enforcement" => "active",
|
|
44
|
+
"conditions" => { "ref_name" => { "include" => ["refs/tags/#{feed}"], "exclude" => [] } },
|
|
45
|
+
"bypass_actors" => [], "rules" => FIXED }
|
|
46
|
+
end
|
|
47
|
+
|
|
28
48
|
# @param lane [Hash{String => String}] a `Lane` row
|
|
29
49
|
# @return [Array<Hash>] the rules: the fixed ones, and the required checks of a `green` lane
|
|
30
50
|
def rules(lane)
|
|
@@ -33,7 +53,9 @@ module Hecks
|
|
|
33
53
|
|
|
34
54
|
# @return [Hash] the rule that a pushed commit has already passed every `RequiredCheck`
|
|
35
55
|
def required_checks
|
|
36
|
-
contexts = Hecks::Vocabulary.rows("RequiredCheck").map
|
|
56
|
+
contexts = Hecks::Vocabulary.rows("RequiredCheck").map do |check|
|
|
57
|
+
{ "context" => check["name"], "integration_id" => GITHUB_ACTIONS_APP_ID }
|
|
58
|
+
end
|
|
37
59
|
{ "type" => "required_status_checks",
|
|
38
60
|
"parameters" => { "strict_required_status_checks_policy" => false, "do_not_enforce_on_create" => false,
|
|
39
61
|
"required_status_checks" => contexts } }
|
data/lib/hecks/tools/lanes.rb
CHANGED
|
@@ -7,7 +7,8 @@ require_relative "../hecks/adapters/github_rulesets"
|
|
|
7
7
|
module Hecks
|
|
8
8
|
module Tools
|
|
9
9
|
# Projects the `Lane` rows of the Vocabulary chapter into what GitHub is told about them: a
|
|
10
|
-
# ruleset for each guarded lane (`.github/rulesets/<lane>.json`)
|
|
10
|
+
# ruleset for each guarded lane (`.github/rulesets/<lane>.json`), one for the tag such a lane
|
|
11
|
+
# feeds (`.github/rulesets/tag-<tag>.json`), and the workflow that promotes
|
|
11
12
|
# each lane that follows another (`.github/workflows/promote.yml`). The workflow only runs
|
|
12
13
|
# `hecks promotion_run.promote`; the rule that decides a promotion is `PromotionRun.Accept`.
|
|
13
14
|
#
|
|
@@ -56,6 +57,16 @@ module Hecks
|
|
|
56
57
|
# @return [Hash] the ruleset GitHub is given for it
|
|
57
58
|
def ruleset(lane) = Ruleset.build(lane)
|
|
58
59
|
|
|
60
|
+
# @return [Hash{String => Hash}] every ruleset the rows project, by the name of its file
|
|
61
|
+
# under `.github/rulesets`: for each guarded lane its own, and one for the tag it feeds
|
|
62
|
+
def rulesets
|
|
63
|
+
lanes.select { |lane| lane["guarded"] == "yes" }.each_with_object({}) do |lane, found|
|
|
64
|
+
found[lane["name"]] = Ruleset.build(lane)
|
|
65
|
+
tag = Ruleset.tag(lane)
|
|
66
|
+
found[tag["name"]] = tag if tag
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
59
70
|
# @param promoted [Array<Hash{String => String}>] the lanes that follow another
|
|
60
71
|
# @return [String] the promotion workflow
|
|
61
72
|
def workflow(promoted) = Workflow.text(promoted)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../tools"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Tools
|
|
7
|
+
# Mutation-tests a domain's own checks: changes its bluebooks in small ways and reports every
|
|
8
|
+
# change that the fuzz properties, the corpus script and the behaviors tests let through.
|
|
9
|
+
#
|
|
10
|
+
# hecks mutate <domain> [--seed N] [--budget N] [--seeds N] [--steps N] [--corpus path]
|
|
11
|
+
# [--operators a,b] [--min-score fraction]
|
|
12
|
+
#
|
|
13
|
+
# Exit 0 unless `--min-score` is given and the score is below it. See
|
|
14
|
+
# `Hecks::Fuzzing::Mutation`.
|
|
15
|
+
module MutationRun
|
|
16
|
+
USAGE = "usage: hecks mutate <domain> [--seed N] [--budget N] [--seeds N] [--steps N] [--corpus PATH] " \
|
|
17
|
+
"[--operators a,b] [--min-score FRACTION]"
|
|
18
|
+
|
|
19
|
+
# Each flag: the option it sets and how its value is read.
|
|
20
|
+
FLAGS = {
|
|
21
|
+
"--seed" => [:seed, :integer],
|
|
22
|
+
"--budget" => [:budget, :integer],
|
|
23
|
+
"--seeds" => [:seeds, :integer],
|
|
24
|
+
"--steps" => [:steps, :integer],
|
|
25
|
+
"--corpus" => [:corpus, :text],
|
|
26
|
+
"--operators" => [:operators, :names],
|
|
27
|
+
"--min-score" => [:min_score, :fraction]
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
READERS = {
|
|
31
|
+
integer: ->(text) { Integer(text) },
|
|
32
|
+
fraction: ->(text) { Float(text) },
|
|
33
|
+
text: ->(text) { text },
|
|
34
|
+
names: ->(text) { text.to_s.split(",").map(&:to_sym) }
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
# @param argv [Array<String>] the domain directory, then the flags
|
|
40
|
+
# @param root [String] the checkout (unused: the domain is named by path)
|
|
41
|
+
# @return [Integer] 0 when the score is high enough or no minimum was asked, 1 when it is not
|
|
42
|
+
# @raise [SystemExit] with the usage line when an argument is missing or unknown
|
|
43
|
+
def main(argv, root: Tools::ROOT)
|
|
44
|
+
_ = root
|
|
45
|
+
require "hecks"
|
|
46
|
+
require "hecks/fuzzing/mutation"
|
|
47
|
+
args = argv.dup
|
|
48
|
+
domain = args.shift or abort USAGE
|
|
49
|
+
options = parse(args)
|
|
50
|
+
|
|
51
|
+
report = Hecks::Fuzzing::Mutation::Run.call(domain, **options.except(:min_score))
|
|
52
|
+
puts report
|
|
53
|
+
report.passes?(options.fetch(:min_score, 0.0)) ? 0 : 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param args [Array<String>] the flags, consumed
|
|
57
|
+
# @return [Hash] the options the flags set
|
|
58
|
+
def parse(args)
|
|
59
|
+
options = {}
|
|
60
|
+
apply_flag(options, args.shift, args) until args.empty?
|
|
61
|
+
options
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Records one flag in `options`, taking its value from `rest`.
|
|
65
|
+
def apply_flag(options, flag, rest)
|
|
66
|
+
key, kind = FLAGS.fetch(flag) { abort "unknown argument #{flag.inspect}\n#{USAGE}" }
|
|
67
|
+
options[key] = READERS.fetch(kind).call(rest.shift)
|
|
68
|
+
rescue ArgumentError, TypeError
|
|
69
|
+
abort "#{flag} needs a #{kind == :fraction ? "number" : "value"}\n#{USAGE}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/hecks/tools.rb
CHANGED
|
@@ -39,6 +39,7 @@ module Hecks
|
|
|
39
39
|
"heki_compact" => ["tools/heki_compaction", "HekiCompaction"],
|
|
40
40
|
"fuzz" => ["tools/fuzz_sweep", "FuzzSweep"],
|
|
41
41
|
"generate" => ["tools/sequence_script", "SequenceScript"],
|
|
42
|
+
"mutate" => ["tools/mutation_run", "MutationRun"],
|
|
42
43
|
"corpus" => ["tools/corpus_report", "CorpusReport"],
|
|
43
44
|
"evolve" => ["tools/evolve_run", "EvolveRun"],
|
|
44
45
|
"query_ir" => ["tools/query_ir_run", "QueryIrRun"],
|
data/lib/hecks/version.rb
CHANGED
data/lib/hecks/vocabulary.rb
CHANGED
|
@@ -51,7 +51,7 @@ module Hecks
|
|
|
51
51
|
].freeze,
|
|
52
52
|
"CiGate" => [
|
|
53
53
|
{"name"=>"runtime_changed", "workflow"=>"ci.yml", "mode"=>"touches", "pattern"=>"^lib/hecks/runtime/", "push"=>"before_sha", "label"=>"does this change touch lib/hecks/runtime/**?"}.freeze,
|
|
54
|
-
{"name"=>"postgres_io_relevant_changed", "workflow"=>"ci-postgres-io-parallel.yml", "mode"=>"skips_unless", "pattern"=>"^(docs/|editors/|release/|deploy/|\\.claude/|\\.githooks/|rust/(parser|codegen|host|build|lsp|web|tests)/|\\.rubocop\\.yml$|\\.rubocop_todo\\.yml$|\\.mcp\\.json$|\\.rspec-local\\.example$|README\\.md$|CHANGELOG\\.md$|CONTRIBUTING\\.md$|SECURITY\\.md$|LICENSE$|\\.gitignore$)", "push"=>"
|
|
54
|
+
{"name"=>"postgres_io_relevant_changed", "workflow"=>"ci-postgres-io-parallel.yml", "mode"=>"skips_unless", "pattern"=>"^(docs/|editors/|release/|deploy/|\\.claude/|\\.githooks/|rust/(parser|codegen|host|build|lsp|web|tests)/|\\.rubocop\\.yml$|\\.rubocop_todo\\.yml$|\\.mcp\\.json$|\\.rspec-local\\.example$|README\\.md$|CHANGELOG\\.md$|CONTRIBUTING\\.md$|SECURITY\\.md$|LICENSE$|\\.gitignore$)", "push"=>"skip", "label"=>"does this change touch anything rspec_postgres_io_parallel covers?"}.freeze
|
|
55
55
|
].freeze,
|
|
56
56
|
"Comparison" => [
|
|
57
57
|
{"symbol"=>">=", "compares_less_than"=>"true", "compares_equal"=>"false", "negated"=>"true"}.freeze,
|
|
@@ -153,6 +153,7 @@ module Hecks
|
|
|
153
153
|
{"name"=>"guided_generation", "type"=>"Boolean", "meaning"=>"Coverage-guided generation across one sweep's seeds (`Hecks::Fuzzing::CoverageCampaign`)."}.freeze,
|
|
154
154
|
{"name"=>"corpus_splice_probability", "type"=>"Numeric", "meaning"=>"Fraction (0.0..1.0) of seeds that start from a prefix of an earlier seed's new coverage. Off by default: measured, it trades breadth for depth (banking reached 1.41x the deep-state tuples, chess lost 0.12x of all tuples), while favoring rare verbs alone never lost."}.freeze,
|
|
155
155
|
{"name"=>"favor_rare_verbs", "type"=>"Integer", "meaning"=>"How many of the least-hit verbs the next seed weights up; 0 turns the lever off."}.freeze,
|
|
156
|
+
{"name"=>"runtime_coverage_feedback", "type"=>"Boolean", "meaning"=>"Whether guided generation also admits a seed to the corpus for reaching a new line or branch of the Ruby runtime (`Hecks::Fuzzing::RuntimeCoverage`), not only a new (verb, kind, state, mutation, outcome) tuple. Off by default: tracing every line slows each seed."}.freeze,
|
|
156
157
|
{"name"=>"self_consistency_checks", "type"=>"Boolean", "meaning"=>"Whether `hecks quality_control query sweep.run` also runs the self-consistency pass (cold rehydration, replay idempotency, value-object JSON round trip) on the same generated sequence."}.freeze,
|
|
157
158
|
{"name"=>"shrink_budget", "type"=>"Integer", "meaning"=>"Candidate replays `hecks quality_control query sweep.run` may spend shrinking one surprised check; 0 turns shrinking off. `concurrency` is never shrunk: a race does not reproduce on demand."}.freeze,
|
|
158
159
|
{"name"=>"yield_weight_seconds", "type"=>"Integer", "meaning"=>"Seconds of staleness that one point of yield score is worth in `RotationPriority.pick`. Zero would make yield invisible, so pick a number that lets it matter."}.freeze,
|
|
@@ -414,6 +415,7 @@ module Hecks
|
|
|
414
415
|
{"script"=>"fuzz", "section"=>"Custodian", "aggregate"=>"FuzzRun", "verb"=>"fuzz"}.freeze,
|
|
415
416
|
{"script"=>"generate", "section"=>"Custodian", "aggregate"=>"FuzzRun", "verb"=>"generate_sequence"}.freeze,
|
|
416
417
|
{"script"=>"bench", "section"=>"Custodian", "aggregate"=>"FuzzRun", "verb"=>"bench"}.freeze,
|
|
418
|
+
{"script"=>"(new)", "section"=>"Custodian", "aggregate"=>"FuzzRun", "verb"=>"mutate"}.freeze,
|
|
417
419
|
{"script"=>"(new)", "section"=>"Custodian", "aggregate"=>"Era", "verb"=>"hold_first"}.freeze,
|
|
418
420
|
{"script"=>"project_deploy", "section"=>"Deploy", "aggregate"=>"Recipe", "verb"=>"project"}.freeze,
|
|
419
421
|
{"script"=>"lint_deploy_recipes", "section"=>"Deploy", "aggregate"=>"MakefileCheck", "verb"=>"lint"}.freeze,
|
|
Binary file
|