Repository navigation
Expand file tree
/
Copy pathstandalone_runner.php
More file actions
258 lines (231 loc) · 9.52 KB
/
Copy pathstandalone_runner.php
File metadata and controls
258 lines (231 loc) · 9.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Rasuvaeff\PropertyTesting\Classify;
use Rasuvaeff\PropertyTesting\Event\PropertyEvent;
use Rasuvaeff\PropertyTesting\Event\PropertyFinished;
use Rasuvaeff\PropertyTesting\Gen;
use Rasuvaeff\PropertyTesting\PropertyListener;
use Rasuvaeff\PropertyTesting\Runner\CallableTrialExecutor;
use Rasuvaeff\PropertyTesting\Runner\DistributionReport;
use Rasuvaeff\PropertyTesting\Runner\Falsified;
use Rasuvaeff\PropertyTesting\Runner\Passed;
use Rasuvaeff\PropertyTesting\Runner\PathFailed;
use Rasuvaeff\PropertyTesting\Runner\Phase;
use Rasuvaeff\PropertyTesting\Runner\PropertyConfig;
use Rasuvaeff\PropertyTesting\Runner\PropertyDefinition;
use Rasuvaeff\PropertyTesting\Runner\PropertyRunner;
use Rasuvaeff\PropertyTesting\Runner\ShrinkMode;
/**
* This example drives the engine directly — no Testo, no PHPUnit, no test
* framework at all. A custom harness (CLI checker, CI guard, application-side
* validation) builds a PropertyDefinition by hand, executes the body through
* CallableTrialExecutor, and inspects the structured PropertyResult. The
* runner never prints, never exits and never throws for a property outcome:
* how a failure surfaces is entirely up to this script.
*/
$runner = new PropertyRunner();
// A property that holds: string concatenation length is additive.
$holds = new PropertyDefinition(
id: 'standalone::concatLengthIsAdditive',
name: 'concatLengthIsAdditive',
generators: [
'left' => Gen::stringAscii(),
'right' => Gen::stringAscii(),
],
parameterNames: ['left', 'right'],
config: new PropertyConfig(runs: 200, seed: 42),
);
$result = $runner->run($holds, new CallableTrialExecutor(
static function (string $left, string $right): void {
if (strlen($left . $right) !== strlen($left) + strlen($right)) {
throw new RuntimeException('Concatenation lost characters');
}
},
));
echo $result instanceof Passed
? "concatLengthIsAdditive: held for 200 runs\n"
: "concatLengthIsAdditive: FAILED unexpectedly\n";
// derandomize: with no explicit seed the runner would draw a random one, and a
// property that fails for one input in fifty would fail CI only some of the
// time. Derived from the property id instead, the same run always selects the
// same inputs — printed here twice to show it.
foreach ([1, 2] as $attempt) {
$seen = [];
$runner->run(
new PropertyDefinition(
id: 'standalone::derandomised',
name: 'derandomised',
generators: ['value' => Gen::intBetween(0, 1_000_000)],
parameterNames: ['value'],
config: new PropertyConfig(runs: 3, derandomize: true),
),
new CallableTrialExecutor(static function (int $value) use (&$seen): void {
$seen[] = $value;
}),
);
echo sprintf("derandomised run %d: %s\n", $attempt, implode(', ', $seen));
}
// A property that is falsified: "every integer stays below 100". The runner
// shrinks the failing input to the minimal one and reports it as a structured
// Falsified result carrying the usual CounterExample.
$falsified = new PropertyDefinition(
id: 'standalone::everyIntStaysBelowHundred',
name: 'everyIntStaysBelowHundred',
generators: ['value' => Gen::intBetween(0, 10_000)],
parameterNames: ['value'],
config: new PropertyConfig(runs: 200, seed: 42),
);
$result = $runner->run($falsified, new CallableTrialExecutor(
static function (int $value): void {
if ($value >= 100) {
throw new RuntimeException(sprintf('%d is not below 100', $value));
}
},
));
if ($result instanceof Falsified) {
$example = $result->counterExample();
echo "everyIntStaysBelowHundred: falsified as expected\n";
echo sprintf(" seed: %d\n", $example->seed);
echo sprintf(" original: %s\n", var_export($example->originalArguments['value'], true));
echo sprintf(" shrunk: %s (in %d step(s))\n", var_export($example->shrunkArguments['value'], true), $example->shrinkSteps);
} else {
echo "everyIntStaysBelowHundred: held unexpectedly\n";
}
// The same property with the descent switched off: the counterexample is
// reported exactly as generated, with zero shrink steps and zero trials. A
// wall-clock alternative is `shrinkBudgetMs`, which keeps the best candidate
// the descent reached before the budget ran out.
$unshrunk = $runner->run(
new PropertyDefinition(
id: 'standalone::everyIntStaysBelowHundred',
name: 'everyIntStaysBelowHundred',
generators: ['value' => Gen::intBetween(0, 10_000)],
parameterNames: ['value'],
config: new PropertyConfig(runs: 200, seed: 42, shrink: ShrinkMode::Off),
),
new CallableTrialExecutor(static function (int $value): void {
if ($value >= 100) {
throw new RuntimeException(sprintf('%d is not below 100', $value));
}
}),
);
if ($unshrunk instanceof Falsified) {
$example = $unshrunk->counterExample();
echo sprintf(
" with ShrinkMode::Off: %s in %d step(s), %d trial(s)\n",
var_export($example->shrunkArguments['value'], true),
$example->shrinkSteps,
$example->shrinkTrials,
);
}
// The counterexample carries the descent that produced it. Passed back with the
// seed, the runner follows those steps instead of searching for them again: one
// body execution per accepted step instead of one per candidate tried. It does
// not skip the random phase — reaching the failing run still means running the
// runs before it.
if ($result instanceof Falsified) {
$replayed = $runner->run(
new PropertyDefinition(
id: 'standalone::everyIntStaysBelowHundred',
name: 'everyIntStaysBelowHundred',
generators: ['value' => Gen::intBetween(0, 10_000)],
parameterNames: ['value'],
config: new PropertyConfig(runs: 200, seed: 42, path: $result->counterExample()->path),
),
new CallableTrialExecutor(static function (int $value): void {
if ($value >= 100) {
throw new RuntimeException(sprintf('%d is not below 100', $value));
}
}),
);
if ($replayed instanceof Falsified) {
echo sprintf(
" path %s: same shrunk value %s in %d trial(s) instead of %d\n",
$result->counterExample()->path,
var_export($replayed->counterExample()->shrunkArguments['value'], true),
$replayed->counterExample()->shrinkTrials,
$result->counterExample()->shrinkTrials,
);
}
}
// A path that no longer applies is its own outcome, never a silent fresh
// search: this one names a candidate the enumeration does not have.
$stale = $runner->run(
new PropertyDefinition(
id: 'standalone::everyIntStaysBelowHundred',
name: 'everyIntStaysBelowHundred',
generators: ['value' => Gen::intBetween(0, 10_000)],
parameterNames: ['value'],
config: new PropertyConfig(runs: 200, seed: 42, path: 'value:99'),
),
new CallableTrialExecutor(static function (int $value): void {
if ($value >= 100) {
throw new RuntimeException(sprintf('%d is not below 100', $value));
}
}),
);
if ($stale instanceof PathFailed) {
echo sprintf(" stale path: %s\n", $stale->exception->getMessage());
}
// The distribution as data, not as a printed line: a listener reads the report
// off PropertyFinished, which is where CI telemetry would collect it.
$collector = new class implements PropertyListener {
public ?DistributionReport $report = null;
#[\Override]
public function onEvent(PropertyEvent $event): void
{
if ($event instanceof PropertyFinished) {
$this->report = $event->distribution;
}
}
};
$runner->run(
new PropertyDefinition(
id: 'standalone::classified',
name: 'classified',
generators: ['value' => Gen::intBetween(0, 100)],
parameterNames: ['value'],
config: new PropertyConfig(runs: 200, seed: 42),
),
new CallableTrialExecutor(static function (int $value): void {
Classify::label($value % 2 === 0 ? 'even' : 'odd');
Classify::cover($value > 90, 'high', 5.0);
}),
[$collector],
);
foreach ($collector->report?->labels ?? [] as $share) {
echo sprintf(
"classified: %-4s %5.1f%% (%d/%d)%s\n",
$share->label,
$share->percent,
$share->count,
$collector->report?->checks ?? 0,
$share->required === null ? '' : sprintf(' — required %.1f%%, %s', $share->required, $share->meetsRequirement() ? 'met' : 'missed'),
);
}
// Phases are a set: this run performs the pinned examples and nothing else, so
// it completes in one body execution instead of 200. With no random phase the
// statistics report honest zeros rather than the configured run count.
$gate = $runner->run(
new PropertyDefinition(
id: 'standalone::fastGate',
name: 'fastGate',
generators: ['value' => Gen::intBetween(0, 10_000)],
parameterNames: ['value'],
config: new PropertyConfig(runs: 200, seed: 42, phases: [Phase::Examples, Phase::Corpus]),
examples: [[7]],
),
new CallableTrialExecutor(static function (int $value): void {
if ($value >= 100) {
throw new RuntimeException(sprintf('%d is not below 100', $value));
}
}),
);
if ($gate instanceof Passed) {
echo sprintf(
"fastGate: passed with attempts=%d, checks=%d (the random phase never ran)\n",
$gate->statistics->attempts,
$gate->statistics->checks,
);
}