hegeltest 0.1.1 → 0.2.1
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/CHANGELOG.md +91 -0
- data/README.md +32 -9
- data/Rakefile +81 -0
- data/docs/README.md +10 -0
- data/docs/adr/0015-follow-the-hegeldev-interface-and-take-changes-upstream-first.md +101 -0
- data/docs/adr/0016-run-mutation-testing-with-mutineer.md +87 -0
- data/docs/adr/0017-raise-the-ruby-floor-to-3-4.md +43 -0
- data/docs/adr/0018-gate-mutation-testing-on-a-committed-baseline.md +74 -0
- data/docs/adr/0019-report-failures-from-the-cases-the-engine-stamps.md +70 -0
- data/docs/adr/0020-derive-span-labels-from-generator-names.md +64 -0
- data/docs/adr/0021-run-a-state-machine-in-rounds-with-its-own-step-count.md +59 -0
- data/docs/adr/0022-keep-microsecond-times-over-a-nanosecond-engine.md +45 -0
- data/docs/adr/0023-leave-unset-settings-to-the-engines-profile.md +50 -0
- data/docs/adr/0024-release-when-a-merge-changes-the-gem-version.md +49 -0
- data/docs/architecture.md +11 -6
- data/lib/hegel/draw_name.rb +3 -1
- data/lib/hegel/generator.rb +50 -9
- data/lib/hegel/generators.rb +134 -102
- data/lib/hegel/lib_hegel/real.rb +193 -94
- data/lib/hegel/lib_hegel.rb +30 -56
- data/lib/hegel/libhegel_version.rb +1 -1
- data/lib/hegel/report.rb +17 -8
- data/lib/hegel/runner.rb +153 -201
- data/lib/hegel/settings.rb +9 -14
- data/lib/hegel/state_machine.rb +15 -8
- data/lib/hegel/stateful/pool.rb +0 -2
- data/lib/hegel/stateful.rb +64 -36
- data/lib/hegel/syntax/methods.rb +3 -2
- data/lib/hegel/test_case.rb +22 -8
- data/lib/hegel/version.rb +1 -1
- data/lib/hegel.rb +11 -17
- data/lib/tasks/libhegel.rake +9 -2
- data/sig/hegel.rbs +58 -60
- data/skills/hegel-ruby/references/ruby/reference.md +96 -52
- metadata +12 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# 0019: Report failures from the cases the engine stamps
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Up to libhegel 0.32.5, a run ended with its failures shrunk, and the
|
|
10
|
+
binding built each report itself. After the run, it read each failure's
|
|
11
|
+
reproduction blob, rebuilt a test case from it with
|
|
12
|
+
`hegel_test_case_from_blob`, and ran the body against that case once more
|
|
13
|
+
while it recorded the drawn values.
|
|
14
|
+
|
|
15
|
+
libhegel 0.44.0 changed both halves. The engine now runs every failure it
|
|
16
|
+
reports once more before the run ends, inside the loop the binding drives.
|
|
17
|
+
It stamps that case, and a few others, through
|
|
18
|
+
`hegel_test_case_should_capture`. The header asks a binding to keep the
|
|
19
|
+
output of a stamped failing case under its origin, and to report each
|
|
20
|
+
failure from its freshest capture instead of replaying its blob after the
|
|
21
|
+
run. A failure can also arrive with no blob: under nondeterministic
|
|
22
|
+
handling, an unconfirmed failure has none, and it carries a caveat
|
|
23
|
+
(`hegel_failure_caveat`) that quotes the engine's replay evidence.
|
|
24
|
+
`hegel_run_start_blob` replays a blob as a run, and the header names it as
|
|
25
|
+
the call a reproduce-failure feature should use.
|
|
26
|
+
|
|
27
|
+
hegel-rust and hegel-java follow this design. hegel-go, hegel-cpp, and
|
|
28
|
+
hegel-typescript still replay the blob after the run; each pins an engine
|
|
29
|
+
from before 0.44.0, or has not moved its report yet.
|
|
30
|
+
|
|
31
|
+
## Decision
|
|
32
|
+
|
|
33
|
+
The runner reads the stamp when each case starts. Only a stamped case
|
|
34
|
+
records its draws. A failing case keeps its exception and its entries
|
|
35
|
+
under its origin. A newer capture replaces an older one, except that an
|
|
36
|
+
unstamped case never replaces a stamped one. hegel-rust ranks its captures
|
|
37
|
+
the same way: a newer capture replaces an older one of the same rank or
|
|
38
|
+
lower.
|
|
39
|
+
|
|
40
|
+
After the run, each failure is reported from the capture under its
|
|
41
|
+
origin. No blob is replayed.
|
|
42
|
+
|
|
43
|
+
A failure with no blob is reported without the reproduction line. A
|
|
44
|
+
failure with a caveat prints it as a `note:` line after the drawn values,
|
|
45
|
+
as hegel-rust does.
|
|
46
|
+
|
|
47
|
+
`Hegel.test(reproduce_failure:)` starts its run with
|
|
48
|
+
`hegel_run_start_blob` and drives it through the same loop. A replay that
|
|
49
|
+
never fails raises `Hegel::Error` that names both causes: a fixed bug, or
|
|
50
|
+
a nondeterministic body whose failure did not recur.
|
|
51
|
+
|
|
52
|
+
Keeping the replay after the run was refused. It ran the body once more
|
|
53
|
+
per failure on top of the engine's own final replay. It also had no blob
|
|
54
|
+
to replay for an unconfirmed failure, so a nondeterministic body would have
|
|
55
|
+
raised an internal error in place of a report.
|
|
56
|
+
|
|
57
|
+
## Consequences
|
|
58
|
+
|
|
59
|
+
A failing run calls the body once fewer per failure. The cost of naming a
|
|
60
|
+
drawn value, a read of the caller's source, falls on the stamped cases
|
|
61
|
+
only. Measured against libhegel 0.45.0, a run that calls its body 60 to
|
|
62
|
+
80 times stamps 5 of them.
|
|
63
|
+
|
|
64
|
+
A `tc.note` block runs on every stamped case, which can be more than one
|
|
65
|
+
per failure: the engine also stamps the first replays that check a newly
|
|
66
|
+
found failure.
|
|
67
|
+
|
|
68
|
+
`hegel_test_case_from_blob` has no caller left, and its binding is gone.
|
|
69
|
+
The sixth layer of verification in ADR 0006 lists "blob replay", which now
|
|
70
|
+
means the replay that `hegel_run_start_blob` drives.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# 0020: Derive span labels from generator names
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`hegel_start_span` takes a label. Up to libhegel 0.38, `hegel.h` defined a
|
|
10
|
+
table of `HEGEL_LABEL_*` constants, and this binding passed the matching
|
|
11
|
+
constant for each kind of compound draw: a list, a list element, a map
|
|
12
|
+
entry, and so on.
|
|
13
|
+
|
|
14
|
+
libhegel 0.39.0 removed the table. A label is now an opaque u64 that names
|
|
15
|
+
the generator which opened the span. The engine treats two spans with one
|
|
16
|
+
label as draws of one generator, and swaps, duplicates, or reorders them
|
|
17
|
+
when it shrinks and mutates a test case. The header asks a binding to
|
|
18
|
+
derive labels from names of its own, prefixed with the binding's name to
|
|
19
|
+
keep clear of the engine's own `hegel.<kind>` names. It asks a generator
|
|
20
|
+
built from other generators to combine its own label with its components'
|
|
21
|
+
labels, so that `lists(integers())` and `lists(text())` differ.
|
|
22
|
+
`hegel_label_from_name` is 64-bit FNV-1a over the name's bytes, and the
|
|
23
|
+
header says a binding may compute it ahead of time. hegel-rust computes
|
|
24
|
+
both functions in the language and pins its hash against the engine's.
|
|
25
|
+
|
|
26
|
+
The other implementations differ on names and on combining. hegel-java
|
|
27
|
+
hashes `dev.hegel.*` names in Java, hegel-cpp calls
|
|
28
|
+
`hegel_label_from_name` with `hegel-cpp.*` names, hegel-go hashes the Go
|
|
29
|
+
type, and hegel-typescript passes small integers. Labels are not part of
|
|
30
|
+
any implementation's public interface.
|
|
31
|
+
|
|
32
|
+
## Decision
|
|
33
|
+
|
|
34
|
+
A generator's label comes from its class name with a `hegel-ruby.`
|
|
35
|
+
prefix. A generator built from others combines its own class label with
|
|
36
|
+
the labels of the generators it draws from, in order. Both functions are
|
|
37
|
+
computed in Ruby, with the same hash the header defines, and a test pins
|
|
38
|
+
them against vectors measured from the engine's own functions.
|
|
39
|
+
|
|
40
|
+
A compound generator computes its label once, when it is built, and keeps
|
|
41
|
+
it, as hegel-rust's generators do. Two alternatives were refused. Computing
|
|
42
|
+
the label on every draw slowed a run of nested generators by more than
|
|
43
|
+
twenty times. Computing it on the first draw and keeping it writes to the
|
|
44
|
+
generator at draw time, so a generator the caller froze raised
|
|
45
|
+
`FrozenError` on every draw. A component that is not a generator
|
|
46
|
+
contributes 0 to the label, so building around it does not raise; the
|
|
47
|
+
draw reports the mistake, where a generator validates its arguments.
|
|
48
|
+
|
|
49
|
+
A deferred generator answers with its installed generator's label, or its
|
|
50
|
+
own class label before `set`. A generator built around it before `set`
|
|
51
|
+
keeps that class label, so a self-referential definition has no cycle to
|
|
52
|
+
follow. hegel-rust's deferred generator answers the same way.
|
|
53
|
+
|
|
54
|
+
The span sites stay where they were: around each compound draw, and around
|
|
55
|
+
each element of a collection, labelled with the element generator's own
|
|
56
|
+
label.
|
|
57
|
+
|
|
58
|
+
## Consequences
|
|
59
|
+
|
|
60
|
+
`Hegel::LibHegel` loses the `HEGEL_LABEL_*` constants and gains
|
|
61
|
+
`label_from_name` and `label_combine`. A generator class written outside
|
|
62
|
+
this library gets a label from its own class name with no extra code.
|
|
63
|
+
Renaming a generator class changes its label, which affects nothing past
|
|
64
|
+
the run that uses it.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# 0021: Run a state machine in rounds, with its own step count
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted. Supersedes the `stateful_step_count:` keyword of `Hegel.test`.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Up to libhegel 0.32.5, a state machine had one call to pick the next rule,
|
|
10
|
+
and the step budget was a run setting,
|
|
11
|
+
`hegel_settings_set_stateful_step_count`, with an engine default of 50.
|
|
12
|
+
This binding exposed it as `Hegel.test(stateful_step_count:)`, and ran
|
|
13
|
+
every invariant after every rule that completed.
|
|
14
|
+
|
|
15
|
+
libhegel 0.33 to 0.43 changed the protocol. A machine runs in rounds:
|
|
16
|
+
`hegel_state_machine_next_group` starts each round, including the first,
|
|
17
|
+
and `hegel_state_machine_next_rule` takes a worker index. The step count
|
|
18
|
+
became an argument of `hegel_new_state_machine`, and the engine has no
|
|
19
|
+
default for it. Between rounds, `hegel_state_machine_should_check_invariant`
|
|
20
|
+
samples each invariant with probability 1 / step_count, so it runs about
|
|
21
|
+
once in a test case of full length; a flag at creation makes an invariant
|
|
22
|
+
run at every round instead. The caller runs every invariant on the initial
|
|
23
|
+
state and on the final state. The machine can also run concurrently, with
|
|
24
|
+
rule groups and weights.
|
|
25
|
+
|
|
26
|
+
hegel-rust (`Machine::steps`), hegel-go (`WithStatefulStepCount`), and
|
|
27
|
+
hegel-java (`Stateful.Options#stepCount`) put the step count on the
|
|
28
|
+
machine runner, with a default of 50. hegel-cpp keeps it in its settings.
|
|
29
|
+
hegel-go, hegel-cpp, and hegel-java sample invariants through the engine.
|
|
30
|
+
hegel-rust and hegel-java name the opt-out `always_run` and `alwaysRun`.
|
|
31
|
+
|
|
32
|
+
## Decision
|
|
33
|
+
|
|
34
|
+
`Hegel::Stateful.run(machine, tc, step_count: 50)` takes the step count,
|
|
35
|
+
and `Hegel.test` no longer takes `stateful_step_count:`. The keyword name
|
|
36
|
+
follows the engine's argument; 50 follows the three implementations that
|
|
37
|
+
moved it.
|
|
38
|
+
|
|
39
|
+
`invariant(name, always_run: true)` checks an invariant at every round.
|
|
40
|
+
Without it, the engine samples the invariant between rounds. Every
|
|
41
|
+
invariant runs before the first rule and after the last.
|
|
42
|
+
|
|
43
|
+
The machine is sequential: one group, equal weights, a concurrency of 1,
|
|
44
|
+
and worker index 0. This binding drives the engine from one thread, and a
|
|
45
|
+
concurrent machine needs one worker thread per drawn level of concurrency.
|
|
46
|
+
Rule weights stay out until a need for them shows up.
|
|
47
|
+
|
|
48
|
+
Keeping `stateful_step_count:` on `Hegel.test` was refused. It would carry
|
|
49
|
+
a value through the runner and the test case to a call that the stateful
|
|
50
|
+
module alone makes, and three of the four implementations that moved it put
|
|
51
|
+
it on the runner.
|
|
52
|
+
|
|
53
|
+
## Consequences
|
|
54
|
+
|
|
55
|
+
A caller who passed `stateful_step_count:` passes `step_count:` to
|
|
56
|
+
`Hegel::Stateful.run` instead. An invariant that a test relied on to run
|
|
57
|
+
after every rule needs `always_run: true`. A machine whose invariants are
|
|
58
|
+
sampled finds a broken invariant at the next sampled round, or at the
|
|
59
|
+
final check, which the shrinker then moves toward the step that broke it.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 0022: Keep microsecond times over a nanosecond engine
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
libhegel 0.36.0 changed `hegel_time_t` from a microsecond field to a
|
|
10
|
+
nanosecond field, and `hegel_generate_time` and `hegel_generate_datetime`
|
|
11
|
+
now draw whole nanoseconds. The struct kept its layout, so a binding that
|
|
12
|
+
does not change compiles and binds as before, and then reads nanoseconds as
|
|
13
|
+
microseconds.
|
|
14
|
+
|
|
15
|
+
`times` returns a `"HH:MM:SS.ffffff"` String and `datetimes` a `Time`, each
|
|
16
|
+
with microsecond precision. The other implementations disagree on what a
|
|
17
|
+
drawn time shows: hegel-rust prints nine digits whenever the nanosecond is
|
|
18
|
+
not zero, hegel-typescript prints six digits for a whole microsecond and
|
|
19
|
+
nine otherwise, and hegel-cpp truncates to microseconds. hegel-go and
|
|
20
|
+
hegel-java return their languages' typed values.
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
`times` and `datetimes` keep microsecond precision. A lower bound's
|
|
25
|
+
microsecond becomes its first nanosecond, and an upper bound's its last,
|
|
26
|
+
so the engine draws over every nanosecond inside the bounds. The drawn
|
|
27
|
+
nanosecond rounds down to a microsecond, so every microsecond in range
|
|
28
|
+
stays reachable, both bounds included. The engine does not draw
|
|
29
|
+
nanoseconds uniformly, so the microseconds are not equally likely either:
|
|
30
|
+
measured against libhegel 0.45.0, two runs of 2000 draws over four
|
|
31
|
+
microseconds gave 1437, 284, 162, and 117, and 1428, 296, 153, and 123.
|
|
32
|
+
|
|
33
|
+
A `datetimes` bound with a fraction of a microsecond narrows to the whole
|
|
34
|
+
microseconds inside the range: the lower bound rounds up and the upper
|
|
35
|
+
rounds down.
|
|
36
|
+
|
|
37
|
+
Moving to nanosecond output was refused while the implementations do not
|
|
38
|
+
agree on its format. A change to it belongs upstream first, as ADR 0015
|
|
39
|
+
requires.
|
|
40
|
+
|
|
41
|
+
## Consequences
|
|
42
|
+
|
|
43
|
+
The public format of `times` and the precision of `datetimes` stay as they
|
|
44
|
+
were. The binding layer speaks nanoseconds, and the conversion lives in
|
|
45
|
+
`Hegel::Generators.first_nanosecond` and `last_nanosecond`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# 0023: Leave unset settings to the engine's profile
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted. Narrows the `nil` rule in the Consequences of
|
|
6
|
+
[ADR 0009](0009-turn-the-example-database-on-with-a-key.md).
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
A `Hegel.test` keyword left `nil` calls no setter, so the engine's value
|
|
11
|
+
applies. Up to libhegel 0.32.5, that value was a fixed default.
|
|
12
|
+
|
|
13
|
+
libhegel 0.40.0 moved defaults into named profiles that the engine
|
|
14
|
+
resolves. `hegel_settings_new` resolves the default profile: one named by
|
|
15
|
+
`hegel_set_default_profile`, `HEGEL_DEFAULT_PROFILE`, or a `hegel.toml`;
|
|
16
|
+
otherwise `ci` on a CI server and `development` elsewhere. libhegel 0.43.4
|
|
17
|
+
then applies `HEGEL_TEST_CASES`, `HEGEL_DATABASE`, `HEGEL_SEED`, and the
|
|
18
|
+
other `HEGEL_*` variables on top. The shipped `ci` profile derandomizes the
|
|
19
|
+
run, turns the example database off, and suppresses the TooSlow health
|
|
20
|
+
check. `hegel_settings_new` can now fail, for a malformed `hegel.toml` or
|
|
21
|
+
`HEGEL_*` variable.
|
|
22
|
+
|
|
23
|
+
hegel-rust builds each run's settings from the reserved `base` profile and
|
|
24
|
+
overwrites every field from its own `Settings` value, which it resolved from
|
|
25
|
+
the default profile earlier. hegel-go, hegel-cpp, hegel-java, and
|
|
26
|
+
hegel-typescript call `hegel_settings_new`.
|
|
27
|
+
|
|
28
|
+
## Decision
|
|
29
|
+
|
|
30
|
+
The binding keeps calling `hegel_settings_new` and keeps the `nil` rule.
|
|
31
|
+
A keyword left `nil` now takes its value from the resolved profile and the
|
|
32
|
+
`HEGEL_*` variables. A keyword the caller passes still wins, because its
|
|
33
|
+
setter runs after the engine resolved the profile. A failure of
|
|
34
|
+
`hegel_settings_new` raises `Hegel::Error` with the engine's message.
|
|
35
|
+
|
|
36
|
+
`report_multiple_failures:` still defaults to `false` and is always
|
|
37
|
+
passed, so a profile cannot turn a re-raised exception into a count.
|
|
38
|
+
|
|
39
|
+
Starting from `base` as hegel-rust does was refused. Ruby has no settings
|
|
40
|
+
object to resolve a profile into, so the binding would have to read every
|
|
41
|
+
field back through the `hegel_settings_get_*` calls to reproduce what
|
|
42
|
+
`hegel_settings_new` already does.
|
|
43
|
+
|
|
44
|
+
## Consequences
|
|
45
|
+
|
|
46
|
+
A project can shape every run through `hegel.toml` or `HEGEL_*` variables
|
|
47
|
+
without changing a call. On a CI server, a run with `database_key:` and no
|
|
48
|
+
`database:` stores nothing, because the `ci` profile turns the database
|
|
49
|
+
off; passing `database:` turns it back on. A run on CI is derandomized
|
|
50
|
+
unless it passes `derandomize: false`.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# 0024: Release when a merge changes the gem version
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted. Replaces the tag push that started the Release workflow.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
A release took three steps from the maintainer: merge the pull request that
|
|
10
|
+
prepares the version, push the tag `v<version>` on the merged commit, and
|
|
11
|
+
approve the `release` environment. The tag carried no decision of its own.
|
|
12
|
+
The version in `lib/hegel/version.rb` already said what to release, and the
|
|
13
|
+
approval was the real gate.
|
|
14
|
+
|
|
15
|
+
A daily routine now opens a pull request for each new libhegel release, with
|
|
16
|
+
a "Prepare <version>" commit. The maintainer's part should be the decision,
|
|
17
|
+
not the steps around it.
|
|
18
|
+
|
|
19
|
+
GitHub does not start a workflow from a push that a workflow makes with its
|
|
20
|
+
own `GITHUB_TOKEN`. A workflow that pushed the tag after a merge would
|
|
21
|
+
therefore not start a tag-triggered Release workflow.
|
|
22
|
+
|
|
23
|
+
## Decision
|
|
24
|
+
|
|
25
|
+
The Release workflow starts on a push to `main` that changes
|
|
26
|
+
`lib/hegel/version.rb`. Its first job reads the version and looks for the tag
|
|
27
|
+
`v<version>`. When the tag exists, as after a revert to a released version,
|
|
28
|
+
the run stops there. Otherwise the publish job waits for the `release`
|
|
29
|
+
environment approval, then builds and pushes the gems from the merged
|
|
30
|
+
commit. The last job creates the tag on that commit and the GitHub release
|
|
31
|
+
together, with `gh release create --target`.
|
|
32
|
+
|
|
33
|
+
The tag is created last, after the gems are out. A rejected approval then
|
|
34
|
+
leaves no tag behind, and rerunning the workflow run releases the same
|
|
35
|
+
commit later.
|
|
36
|
+
|
|
37
|
+
Two alternatives were refused. A workflow that only pushed the tag needs a
|
|
38
|
+
personal access token or a GitHub App token to start the tag-triggered
|
|
39
|
+
workflow, which is a long-lived secret for one step. Creating the tag before
|
|
40
|
+
the approval leaves a tag for a version that may never ship, and the check
|
|
41
|
+
job would then skip that version for good.
|
|
42
|
+
|
|
43
|
+
## Consequences
|
|
44
|
+
|
|
45
|
+
A release is one merge and one approval. Merge a pull request that changes
|
|
46
|
+
the gem version only when that version should ship.
|
|
47
|
+
|
|
48
|
+
A tag pushed by hand no longer starts a release. The workflow's own run on
|
|
49
|
+
`main` is the place to retry one.
|
data/docs/architecture.md
CHANGED
|
@@ -15,6 +15,9 @@ wrapping the engine's whole draw surface, and the run loop.
|
|
|
15
15
|
What exists beyond that: the failure report, which names drawn values by
|
|
16
16
|
reading the caller's own source, interleaves them with whatever `tc.note`
|
|
17
17
|
recorded, and prints a blob that `Hegel.test(reproduce_failure:)` replays.
|
|
18
|
+
The report comes from the cases the engine stamps for capture during the
|
|
19
|
+
run; see
|
|
20
|
+
[ADR 0019](adr/0019-report-failures-from-the-cases-the-engine-stamps.md).
|
|
18
21
|
The generator layer covers twenty-five generators composing through `map`
|
|
19
22
|
and `filter`, reachable bare through `Hegel::Syntax::Methods`. A test case
|
|
20
23
|
can discard itself with `tc.assume` or `tc.reject`. A run can be shaped by
|
|
@@ -27,9 +30,11 @@ database keywords.
|
|
|
27
30
|
`Hegel::Stateful.run` drives a `Hegel::StateMachine`'s rules and invariants,
|
|
28
31
|
drawing values an earlier rule produced back out of a
|
|
29
32
|
`Hegel::Stateful::Pool`. [ADR 0010](adr/0010-declare-stateful-rules-with-a-class-macro.md)
|
|
30
|
-
decides how a machine declares its rules
|
|
33
|
+
decides how a machine declares its rules,
|
|
31
34
|
[ADR 0011](adr/0011-let-the-test-case-own-every-pool-drawn-from-it.md) who
|
|
32
|
-
frees a pool
|
|
35
|
+
frees a pool, and
|
|
36
|
+
[ADR 0021](adr/0021-run-a-state-machine-in-rounds-with-its-own-step-count.md)
|
|
37
|
+
how its rounds, step count, and invariants run.
|
|
33
38
|
|
|
34
39
|
Every feature this binding set out to cover now has a Ruby surface. What
|
|
35
40
|
remains open is not a feature but a measurement, and
|
|
@@ -116,9 +121,9 @@ the other. See
|
|
|
116
121
|
## A rule's own control flow
|
|
117
122
|
|
|
118
123
|
Inside a stateful rule, `tc.assume(false)` means something narrower than it
|
|
119
|
-
does in a test body. `Hegel::Stateful` catches it
|
|
120
|
-
was rejected so
|
|
121
|
-
the test case continues. `Hegel::Runner.classify` never sees it, and no case
|
|
124
|
+
does in a test body. `Hegel::Stateful` catches it and tells libhegel the
|
|
125
|
+
rule was rejected, so its round does not count toward the step budget, and
|
|
126
|
+
the machine starts another round; the test case continues. `Hegel::Runner.classify` never sees it, and no case
|
|
122
127
|
is discarded.
|
|
123
128
|
|
|
124
129
|
A rule is also the one place in this library where a process-ending
|
|
@@ -168,7 +173,7 @@ fields.
|
|
|
168
173
|
|
|
169
174
|
A drawn string arrives as a pointer and a length, is not NUL-terminated,
|
|
170
175
|
and can hold interior NUL bytes, because the drawn alphabet can include
|
|
171
|
-
U+0000. Read by length. Measured against 0.
|
|
176
|
+
U+0000. Read by length. Measured against 0.45.0: an alphabet pinned to
|
|
172
177
|
U+0000 produces a three-byte draw that reads as `""` when taken up to the
|
|
173
178
|
first NUL and as three NUL bytes when taken by its reported length. Every
|
|
174
179
|
string observed reports `valid_encoding?` after `force_encoding(UTF-8)`.
|
data/lib/hegel/draw_name.rb
CHANGED
|
@@ -116,7 +116,9 @@ module Hegel
|
|
|
116
116
|
def encloses?(outer, inner)
|
|
117
117
|
return false if outer.equal?(inner)
|
|
118
118
|
|
|
119
|
-
outer
|
|
119
|
+
# An outer assignment's start offset never equals an inner one's, since
|
|
120
|
+
# the outer node begins with the name first.
|
|
121
|
+
outer.location.start_offset <= inner.location.start_offset && # mutineer:disable-line comparison
|
|
120
122
|
inner.location.end_offset <= outer.location.end_offset
|
|
121
123
|
end
|
|
122
124
|
end
|
data/lib/hegel/generator.rb
CHANGED
|
@@ -18,10 +18,43 @@ module Hegel
|
|
|
18
18
|
raise NotImplementedError, "#{self.class} must implement #do_draw"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# The span label naming this generator's draws to the engine, which
|
|
22
|
+
# treats two spans with one label as draws of one generator when it
|
|
23
|
+
# shrinks. A generator built from others overrides this and combines
|
|
24
|
+
# its own class label with theirs, so that arrays of integers and
|
|
25
|
+
# arrays of strings do not share a label.
|
|
26
|
+
def label
|
|
27
|
+
self.class.label
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The class name, prefixed with this binding's name as hegel.h asks, to
|
|
31
|
+
# keep clear of the engine's own hegel.<kind> labels.
|
|
32
|
+
def self.label
|
|
33
|
+
LibHegel.label_from_name("hegel-ruby.#{name}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Combines this generator's class label with the labels of +parts+, the
|
|
37
|
+
# generators it draws from. A compound generator calls this once, when it
|
|
38
|
+
# is built, and keeps the result: computing it on every draw slowed a
|
|
39
|
+
# run of nested generators by more than twenty times, and keeping it
|
|
40
|
+
# from the first draw would write to a generator a caller may have
|
|
41
|
+
# frozen.
|
|
42
|
+
def combined_label(*parts)
|
|
43
|
+
LibHegel.label_combine([self.class.label, *parts.map { |part| Generator.label_of(part) }])
|
|
44
|
+
end
|
|
45
|
+
private :combined_label
|
|
46
|
+
|
|
47
|
+
# +part+'s label, or 0 for a part that is not a generator. Such a part
|
|
48
|
+
# fails when it is drawn, where a generator validates its arguments, so
|
|
49
|
+
# building the generator around it must not fail first.
|
|
50
|
+
def self.label_of(part)
|
|
51
|
+
part.respond_to?(:label) ? part.label : 0
|
|
52
|
+
end
|
|
53
|
+
|
|
21
54
|
# Returns a new Generator whose #do_draw runs +block+ on the value this
|
|
22
|
-
# generator drew,
|
|
23
|
-
#
|
|
24
|
-
#
|
|
55
|
+
# generator drew, inside one span so the shrinker treats the source draw
|
|
56
|
+
# and the transform as one unit (see hegel-rust's Mapped,
|
|
57
|
+
# src/generators/generators.rs).
|
|
25
58
|
def map(&block)
|
|
26
59
|
Mapped.new(self, block)
|
|
27
60
|
end
|
|
@@ -39,13 +72,15 @@ module Hegel
|
|
|
39
72
|
# generators.rb.
|
|
40
73
|
class Mapped < Generator
|
|
41
74
|
def initialize(source, block)
|
|
42
|
-
super()
|
|
43
75
|
@source = source
|
|
44
76
|
@block = block
|
|
77
|
+
@label = combined_label(@source)
|
|
45
78
|
end
|
|
46
79
|
|
|
80
|
+
attr_reader :label
|
|
81
|
+
|
|
47
82
|
def do_draw(tc)
|
|
48
|
-
tc.start_span(
|
|
83
|
+
tc.start_span(label)
|
|
49
84
|
@block.call(@source.do_draw(tc))
|
|
50
85
|
ensure
|
|
51
86
|
# discard is always false here: a span is only ever marked
|
|
@@ -73,16 +108,22 @@ module Hegel
|
|
|
73
108
|
MAX_ATTEMPTS = 3
|
|
74
109
|
|
|
75
110
|
def initialize(source, block)
|
|
76
|
-
super()
|
|
77
111
|
@source = source
|
|
78
112
|
@block = block
|
|
113
|
+
@label = combined_label(@source)
|
|
79
114
|
end
|
|
80
115
|
|
|
116
|
+
attr_reader :label
|
|
117
|
+
|
|
81
118
|
def do_draw(tc)
|
|
82
119
|
MAX_ATTEMPTS.times do
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
120
|
+
# Each variable is set again before it is read, or stays nil, which
|
|
121
|
+
# acts like false (!nil == !false).
|
|
122
|
+
# The initializer also shows intent: discard the span when an
|
|
123
|
+
# exception skips the reassignment.
|
|
124
|
+
accepted = false # mutineer:disable-line statement_removal
|
|
125
|
+
value = nil # mutineer:disable-line statement_removal, boolean_literal
|
|
126
|
+
tc.start_span(label)
|
|
86
127
|
begin
|
|
87
128
|
value = @source.do_draw(tc)
|
|
88
129
|
accepted = @block.call(value)
|