jobpayload 0.1.0 → 0.2.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/CHANGELOG.md +59 -0
- data/README.md +188 -9
- data/lib/jobpayload/cli.rb +40 -0
- data/lib/jobpayload/finding.rb +1 -1
- data/lib/jobpayload/fixture.rb +19 -6
- data/lib/jobpayload/formatter/json.rb +1 -1
- data/lib/jobpayload/secure_fixture_reader.rb +80 -0
- data/lib/jobpayload/snapshot_checker.rb +107 -0
- data/lib/jobpayload/version.rb +1 -1
- data/lib/jobpayload.rb +2 -0
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4412fc4ef1f876b11092e07462d6a46a998c3c063a8b7576ff33dd871a53cf63
|
|
4
|
+
data.tar.gz: 514ada469e5d6ea61b44315f03b0e2bb946204b2c072bb58681adb3af24213e9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0134ac75049008ed572e7d73c1b978441642dce567e945d44071f81ab3decab7be51f33631caf37953fef7839e88c5cb1215e7bcc96589f6d708f049f76fa650
|
|
7
|
+
data.tar.gz: ccd929aba8e3dd698580fb00f6a643b02fcc7b551c8e4f65be6686569e6bb47e93657b40fedc019fde90ffeea48f746b1c7679d3f024a6a5076ba5f0e9063ea6
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,65 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.2.0
|
|
6
|
+
|
|
7
|
+
0.2.0 adds snapshot drift detection. Everything 0.1.0 does is unchanged:
|
|
8
|
+
`snapshot`, `snapshot --update` and `check` behave as before, and the fixture
|
|
9
|
+
schema (`jobpayload_schema: 1`), the JSON output schema (`schema_version: 1`),
|
|
10
|
+
finding codes and exit codes are the same. There are no breaking changes.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `jobpayload snapshot --check`: serializes every snapshot case with the
|
|
15
|
+
current code and compares it with the baseline fixture already stored in
|
|
16
|
+
`--output`, to catch unintended changes to the payloads new code enqueues.
|
|
17
|
+
- **Read-only:** nothing in `--output` is written, created, renamed or
|
|
18
|
+
deleted, not even a missing `--output` directory. Cannot be combined with
|
|
19
|
+
`--update`.
|
|
20
|
+
- **Comparison:** the whole `job` object, as canonical JSON. The current
|
|
21
|
+
payload is normalized as `snapshot` writes it; the stored baseline is
|
|
22
|
+
compared as stored (never re-normalized), so any edit to it, volatile
|
|
23
|
+
metadata included, is reported. `source` metadata, key order, whitespace
|
|
24
|
+
and equivalent JSON spellings are ignored; value types (`1` vs `1.0`) and
|
|
25
|
+
array order are not.
|
|
26
|
+
- **Statuses and exit codes:** `identical` (0), `different` (1), `missing`
|
|
27
|
+
(1) and `invalid` (2); the highest exit status wins (2 > 1 > 0). A
|
|
28
|
+
baseline is `invalid` when it is not valid schema v1 JSON, is a symbolic
|
|
29
|
+
link (never followed), is not a regular file, cannot be read, or holds a
|
|
30
|
+
`job` that `snapshot` could never have written. Fixtures with no matching
|
|
31
|
+
case are ignored.
|
|
32
|
+
- **Output:** deterministic text, or JSON (`schema_version: 1`,
|
|
33
|
+
`"mode": "check"`). It reports whether each baseline differs, not where
|
|
34
|
+
(no field-level diff).
|
|
35
|
+
|
|
36
|
+
### Security
|
|
37
|
+
|
|
38
|
+
- `snapshot --check` reads baselines in a way that resists the fixture's
|
|
39
|
+
directory entry being replaced during the check: after `lstat`, the file
|
|
40
|
+
is opened read-only with `O_NOFOLLOW` and `O_NONBLOCK`, verified with
|
|
41
|
+
`fstat` to be the same regular file (device and inode), and read only from
|
|
42
|
+
that descriptor. An entry swapped for a symlink, FIFO, directory or another
|
|
43
|
+
file, or removed, is `invalid` (exit 2) and never read, so it cannot leak
|
|
44
|
+
file contents into the output, block the check, or make it pass. Platforms
|
|
45
|
+
without these flags fail closed. This protects the final path component
|
|
46
|
+
only; see README "Race-safe baseline reading" for what is not covered.
|
|
47
|
+
The hardening was made before `snapshot --check` was released, so no
|
|
48
|
+
released version is affected. `jobpayload check` is unchanged (it reads by
|
|
49
|
+
path and follows symbolic links, as in 0.1.0).
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- Internal: `JobPayload::Fixture.parse_content` parses fixture bytes the
|
|
54
|
+
caller has already read; `Fixture.parse` keeps its behaviour and shares the
|
|
55
|
+
same validation.
|
|
56
|
+
- Tests and CI (no runtime change): tests for `snapshot --check` and its
|
|
57
|
+
race-safe reads (deterministic lstat/open races, FIFO deadlines, descriptor
|
|
58
|
+
leaks, real-process runs); a macOS smoke job; capability-based skips for
|
|
59
|
+
file names that are not valid UTF-8; and a separate Rails `main` GlobalID
|
|
60
|
+
regression workflow (not a supported version).
|
|
61
|
+
- README: documents `snapshot --check`, its security boundaries and the
|
|
62
|
+
updated version references.
|
|
63
|
+
|
|
5
64
|
## 0.1.0
|
|
6
65
|
|
|
7
66
|
Initial release. The single question v0.1.0 answers is: *can the current
|
data/README.md
CHANGED
|
@@ -16,6 +16,7 @@ application can still be deserialized by the **new** version.
|
|
|
16
16
|
- [Quick start](#quick-start)
|
|
17
17
|
- [Creating snapshot cases](#creating-snapshot-cases)
|
|
18
18
|
- [Generating baseline fixtures](#generating-baseline-fixtures)
|
|
19
|
+
- [Detecting snapshot drift (`snapshot --check`)](#detecting-snapshot-drift-snapshot---check)
|
|
19
20
|
- [Checking old fixtures on new code](#checking-old-fixtures-on-new-code)
|
|
20
21
|
- [Two-way rolling-deploy checking](#two-way-rolling-deploy-checking)
|
|
21
22
|
- [GlobalID / inconclusive semantics](#globalid--inconclusive-semantics)
|
|
@@ -28,6 +29,7 @@ application can still be deserialized by the **new** version.
|
|
|
28
29
|
- [Non-goals](#non-goals)
|
|
29
30
|
- [Known limitations](#known-limitations)
|
|
30
31
|
- [Security / safety notes](#security--safety-notes)
|
|
32
|
+
- [Race-safe baseline reading (`snapshot --check`)](#race-safe-baseline-reading-snapshot---check)
|
|
31
33
|
|
|
32
34
|
## What jobpayload checks
|
|
33
35
|
|
|
@@ -47,7 +49,10 @@ A baseline fixture is compatible with the current application when:
|
|
|
47
49
|
subclasses, nested arrays/hashes and GlobalID lookups, in your real
|
|
48
50
|
(test) environment.
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
`check` checks **backward-read compatibility only**: old payload → new code.
|
|
53
|
+
Since v0.2.0, `snapshot --check` can also detect *drift*: whether the current
|
|
54
|
+
code still serializes each snapshot case exactly as its stored baseline
|
|
55
|
+
(see [Detecting snapshot drift](#detecting-snapshot-drift-snapshot---check)).
|
|
51
56
|
|
|
52
57
|
## Why queued payload compatibility matters
|
|
53
58
|
|
|
@@ -150,6 +155,7 @@ bundle exec jobpayload snapshot [options]
|
|
|
150
155
|
| `--boot PATH` | `config/environment.rb` | File required to boot the app |
|
|
151
156
|
| `--environment NAME` | `test` | Value assigned to `RAILS_ENV` before booting |
|
|
152
157
|
| `--update` | off | Replace existing fixtures whose content changed |
|
|
158
|
+
| `--check` | off | Compare with existing fixtures without writing (see [Detecting snapshot drift](#detecting-snapshot-drift-snapshot---check)) |
|
|
153
159
|
| `--format text\|json` | `text` | Output format |
|
|
154
160
|
|
|
155
161
|
Behaviour:
|
|
@@ -193,7 +199,7 @@ One JSON file per fixture, schema v1:
|
|
|
193
199
|
"name": "billing-money-v1",
|
|
194
200
|
"source": {
|
|
195
201
|
"active_job_version": "8.1.4",
|
|
196
|
-
"jobpayload_version": "0.
|
|
202
|
+
"jobpayload_version": "0.2.0",
|
|
197
203
|
"rails_version": "8.1.4",
|
|
198
204
|
"ruby_version": "3.4.8"
|
|
199
205
|
}
|
|
@@ -213,6 +219,93 @@ One JSON file per fixture, schema v1:
|
|
|
213
219
|
two-space layout (independent of the json gem version), UTF-8, LF line endings,
|
|
214
220
|
a final newline, written atomically (temp file + rename).
|
|
215
221
|
|
|
222
|
+
### Detecting snapshot drift (`snapshot --check`)
|
|
223
|
+
|
|
224
|
+
```sh
|
|
225
|
+
bundle exec jobpayload snapshot --check [options]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`--check` serializes every snapshot case with the current code, exactly as
|
|
229
|
+
`snapshot` would, and compares the result with the fixture already stored in
|
|
230
|
+
`--output`. It answers "did the payload this code writes change?", so an
|
|
231
|
+
unintended change to what new code enqueues fails CI before it ships. It is
|
|
232
|
+
**read-only for the fixtures**: nothing in `--output` is written, replaced,
|
|
233
|
+
created or deleted, not even a missing `--output` directory. (Like `snapshot`,
|
|
234
|
+
it still boots your application and runs your cases file, which may write to
|
|
235
|
+
the test database.) `--check` and `--update` cannot be combined.
|
|
236
|
+
|
|
237
|
+
Only the `job` object is compared, and every key in it counts. The current
|
|
238
|
+
payload is normalized exactly as `snapshot` writes it (fixed `job_id`,
|
|
239
|
+
`enqueued_at` and so on); the stored `job` is compared as stored, so any edit
|
|
240
|
+
to a fixture's `job`, volatile metadata included, is reported as `different`.
|
|
241
|
+
`source` metadata, key order, whitespace and equivalent JSON spellings
|
|
242
|
+
(`"\u00e9"` and `"é"`, `1.0e2` and `100.0`) are ignored. Value types and array
|
|
243
|
+
order are kept: `1` and `1.0`, or `null` and a missing key, are different
|
|
244
|
+
payloads.
|
|
245
|
+
|
|
246
|
+
| Status | Meaning | Exit |
|
|
247
|
+
| --- | --- | --- |
|
|
248
|
+
| `identical` | The stored `job` matches what the current code serializes | 0 |
|
|
249
|
+
| `different` | The payload the current code writes differs from the stored `job` | 1 |
|
|
250
|
+
| `missing` | No fixture file for the case | 1 |
|
|
251
|
+
| `invalid` | The fixture cannot be used: invalid JSON or UTF-8, wrong schema, name mismatch, a symbolic link (never followed, even when it points to a valid fixture), not a regular file (such as a directory or FIFO), replaced or removed while it is being checked, unreadable or inaccessible (for example an unsearchable directory), or a `job` that `snapshot` could never have written (a non-finite number such as `1e400`, or nesting deeper than `snapshot`'s limit of 100) | 2 |
|
|
252
|
+
|
|
253
|
+
The exit status is the highest that applies (`2` > `1` > `0`). Case errors,
|
|
254
|
+
boot errors and usage errors exit 2 as for `snapshot`. Fixtures with no
|
|
255
|
+
matching case are ignored. A `--output` directory that does not exist is not an
|
|
256
|
+
error: every case is reported `missing` (exit 1), so check the path if
|
|
257
|
+
everything is missing. Only the fixture file entry itself must not be a symbolic
|
|
258
|
+
link; `--output` may be one. Baselines are read in a way that
|
|
259
|
+
resists the file being swapped during the check; see
|
|
260
|
+
[Race-safe baseline reading](#race-safe-baseline-reading-snapshot---check).
|
|
261
|
+
|
|
262
|
+
Text output has one line per case, in name order, and a summary. The status
|
|
263
|
+
says whether a fixture differs, not where; compare the files (for example
|
|
264
|
+
with `git diff` after `snapshot --update`) to see the change:
|
|
265
|
+
|
|
266
|
+
```text
|
|
267
|
+
identical billing-legacy-money-v1 test/jobpayload_fixtures/billing-legacy-money-v1.json
|
|
268
|
+
different billing-money-v1 test/jobpayload_fixtures/billing-money-v1.json
|
|
269
|
+
missing scheduled-v1 test/jobpayload_fixtures/scheduled-v1.json
|
|
270
|
+
|
|
271
|
+
3 fixtures: 1 identical, 1 different, 1 missing, 0 invalid
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
With `--format json` the document (schema v1) is:
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"schema_version": 1,
|
|
279
|
+
"tool_version": "0.2.0",
|
|
280
|
+
"mode": "check",
|
|
281
|
+
"status": "fail",
|
|
282
|
+
"summary": {
|
|
283
|
+
"fixtures": 1,
|
|
284
|
+
"identical": 0,
|
|
285
|
+
"different": 1,
|
|
286
|
+
"missing": 0,
|
|
287
|
+
"invalid": 0
|
|
288
|
+
},
|
|
289
|
+
"fixtures": [
|
|
290
|
+
{
|
|
291
|
+
"name": "billing-money-v1",
|
|
292
|
+
"path": "test/jobpayload_fixtures/billing-money-v1.json",
|
|
293
|
+
"status": "different",
|
|
294
|
+
"reason": null
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`status` is `pass`, `fail` or `tool_error` for exit 0, 1 and 2. `reason` is
|
|
301
|
+
set only for `invalid`.
|
|
302
|
+
Text and JSON output are deterministic for the same input.
|
|
303
|
+
|
|
304
|
+
A `different` result is not a compatibility failure by itself: it means the
|
|
305
|
+
new code writes a different payload than the fixture records. Add a new case
|
|
306
|
+
for the new format (keeping the old fixture for `check`), or re-run
|
|
307
|
+
`snapshot --update` if the fixture was meant to follow the code.
|
|
308
|
+
|
|
216
309
|
## Checking old fixtures on new code
|
|
217
310
|
|
|
218
311
|
```sh
|
|
@@ -310,7 +403,8 @@ database (seeds, fixtures, or a boot file) to get a conclusive answer.
|
|
|
310
403
|
|
|
311
404
|
## Finding codes
|
|
312
405
|
|
|
313
|
-
Finding codes are a stable API
|
|
406
|
+
Finding codes are a stable API: 0.2.0 keeps every 0.1.0 code and its meaning
|
|
407
|
+
unchanged.
|
|
314
408
|
|
|
315
409
|
| Code | Name | Kind | Meaning |
|
|
316
410
|
| --- | --- | --- | --- |
|
|
@@ -331,6 +425,10 @@ Finding codes are a stable API for 0.1.x.
|
|
|
331
425
|
|
|
332
426
|
When several apply, the highest priority wins: `2` > `1` > `0`.
|
|
333
427
|
|
|
428
|
+
These are the exit statuses of `check`. `snapshot --check` uses the same
|
|
429
|
+
`2` > `1` > `0` precedence with its own meanings; see
|
|
430
|
+
[Detecting snapshot drift](#detecting-snapshot-drift-snapshot---check).
|
|
431
|
+
|
|
334
432
|
Results are written to **stdout**. Usage, configuration and boot errors are
|
|
335
433
|
written to **stderr** (and nothing is written to stdout in that case).
|
|
336
434
|
jobpayload itself writes nothing else to stdout. While booting and
|
|
@@ -401,6 +499,9 @@ A summary line `N tool error(s)` is added when fixtures had fatal findings.
|
|
|
401
499
|
|
|
402
500
|
## JSON output
|
|
403
501
|
|
|
502
|
+
This section describes `check --format json`. The `snapshot --check` document
|
|
503
|
+
is described in [Detecting snapshot drift](#detecting-snapshot-drift-snapshot---check).
|
|
504
|
+
|
|
404
505
|
```sh
|
|
405
506
|
bundle exec jobpayload check --format json
|
|
406
507
|
```
|
|
@@ -408,7 +509,7 @@ bundle exec jobpayload check --format json
|
|
|
408
509
|
```json
|
|
409
510
|
{
|
|
410
511
|
"schema_version": 1,
|
|
411
|
-
"tool_version": "0.
|
|
512
|
+
"tool_version": "0.2.0",
|
|
412
513
|
"status": "fail",
|
|
413
514
|
"summary": {
|
|
414
515
|
"fixtures": 3,
|
|
@@ -467,7 +568,7 @@ bundle exec jobpayload check --format json
|
|
|
467
568
|
|
|
468
569
|
| Field | Meaning |
|
|
469
570
|
| --- | --- |
|
|
470
|
-
| `schema_version` | JSON output schema version. Always `1` in 0.1.x. |
|
|
571
|
+
| `schema_version` | JSON output schema version. Always `1` in 0.1.x and 0.2.0. |
|
|
471
572
|
| `tool_version` | jobpayload version. |
|
|
472
573
|
| `status` | `"pass"` (exit 0), `"fail"` (exit 1) or `"tool_error"` (exit 2). With `--fail-on-inconclusive`, inconclusive fixtures make it `"fail"`. |
|
|
473
574
|
| `summary.fixtures` | Number of fixture files checked. |
|
|
@@ -487,7 +588,7 @@ Backtraces are never included in JSON output. The same input always produces
|
|
|
487
588
|
byte-identical JSON. Exception messages, fixture file names and paths are
|
|
488
589
|
emitted as valid UTF-8 (invalid bytes become U+FFFD).
|
|
489
590
|
|
|
490
|
-
**Stability policy.** Within `schema_version: 1` (
|
|
591
|
+
**Stability policy.** Within `schema_version: 1` (0.1.x and 0.2.0),
|
|
491
592
|
existing fields are never removed or renamed, and the meaning and allowed
|
|
492
593
|
values of existing fields do not change. New fields may be added, so consumers
|
|
493
594
|
should ignore fields they do not know. Any incompatible change bumps
|
|
@@ -514,13 +615,15 @@ jobs:
|
|
|
514
615
|
bundler-cache: true
|
|
515
616
|
- run: bin/rails db:prepare
|
|
516
617
|
- run: bundle exec jobpayload check
|
|
618
|
+
# Optional: fail when the code changes the payloads it writes.
|
|
619
|
+
- run: bundle exec jobpayload snapshot --check
|
|
517
620
|
```
|
|
518
621
|
|
|
519
622
|
For two-way checking, check out both the base and the head revision (for
|
|
520
623
|
example two `actions/checkout` steps with different `path:` and `ref:`), then
|
|
521
624
|
run `check` twice: base fixtures → head app, and head fixtures → base app, as
|
|
522
625
|
shown in [Two-way rolling-deploy checking](#two-way-rolling-deploy-checking).
|
|
523
|
-
jobpayload
|
|
626
|
+
jobpayload does not orchestrate checkouts itself.
|
|
524
627
|
|
|
525
628
|
## Supported Ruby / Active Job versions
|
|
526
629
|
|
|
@@ -543,7 +646,7 @@ built-in fixture set.
|
|
|
543
646
|
|
|
544
647
|
## Non-goals
|
|
545
648
|
|
|
546
|
-
|
|
649
|
+
jobpayload deliberately does **not**:
|
|
547
650
|
|
|
548
651
|
- execute `perform` or any job business logic, enqueue, or retry jobs
|
|
549
652
|
- check `perform` arity or execution semantics. A successful deserialize does
|
|
@@ -558,6 +661,8 @@ v0.1.0 deliberately does **not**:
|
|
|
558
661
|
- orchestrate forward/two-way compatibility automatically
|
|
559
662
|
- re-serialize deserialized arguments (round-trip / re-enqueue compatibility)
|
|
560
663
|
- provide ignore/suppression lists or a configuration file
|
|
664
|
+
- show field-level (structural) diffs: `snapshot --check` reports only
|
|
665
|
+
whether a baseline differs, not where
|
|
561
666
|
|
|
562
667
|
## Known limitations
|
|
563
668
|
|
|
@@ -567,6 +672,8 @@ v0.1.0 deliberately does **not**:
|
|
|
567
672
|
never writes more than 100). Such a fixture is reported as `AJP900` with a
|
|
568
673
|
`SystemStackError` cause, or the check stops with an internal error; either
|
|
569
674
|
way the exit status is 2, never 0 or 1.
|
|
675
|
+
`snapshot --check` reports a baseline nested deeper than 100 levels as
|
|
676
|
+
`invalid` (exit 2).
|
|
570
677
|
- **GlobalIDs inside custom serializer payloads.** Only a top-level or
|
|
571
678
|
plain-array/hash GlobalID argument can be inconclusive. If a custom
|
|
572
679
|
serializer's own payload contains a GlobalID whose record is missing, the
|
|
@@ -578,6 +685,13 @@ v0.1.0 deliberately does **not**:
|
|
|
578
685
|
[Exit codes](#exit-codes) and can corrupt `--format json` output.
|
|
579
686
|
- **Semantic changes** that still deserialize successfully pass (see
|
|
580
687
|
[Non-goals](#non-goals)).
|
|
688
|
+
- **File names that are not valid UTF-8.** jobpayload passes the exact bytes
|
|
689
|
+
it is given for `--output`, `--fixtures`, `--cases` and `--boot` to the file
|
|
690
|
+
system, and only replaces invalid bytes with U+FFFD in text and JSON output.
|
|
691
|
+
Whether such a name can exist at all depends on the file system: most Linux
|
|
692
|
+
file systems accept any bytes, while macOS (APFS) rejects names that are
|
|
693
|
+
not valid UTF-8, so jobpayload cannot create them there. Ordinary UTF-8
|
|
694
|
+
paths work on every platform.
|
|
581
695
|
- **Database errors during GlobalID lookup** (`ActiveRecord::StatementInvalid`,
|
|
582
696
|
for example a column or table that the current schema no longer has) are
|
|
583
697
|
reported as `AJP900` with exit 2, not as `AJP201`. The run still fails.
|
|
@@ -601,12 +715,55 @@ v0.1.0 deliberately does **not**:
|
|
|
601
715
|
at production databases. Note that a `DATABASE_URL` set in your shell is
|
|
602
716
|
still used by Rails in the `test` environment.
|
|
603
717
|
|
|
718
|
+
### Race-safe baseline reading (`snapshot --check`)
|
|
719
|
+
|
|
720
|
+
`snapshot --check` reads each baseline so that the fixture's directory entry
|
|
721
|
+
cannot be swapped between the type check and the read:
|
|
722
|
+
|
|
723
|
+
1. `lstat` the fixture path. A symbolic link, directory, FIFO or anything else
|
|
724
|
+
that is not a regular file is `invalid`; nothing is opened.
|
|
725
|
+
2. Open it read-only with `O_NOFOLLOW` (a symbolic link put in its place is
|
|
726
|
+
refused instead of followed) and `O_NONBLOCK` (a FIFO put in its place
|
|
727
|
+
cannot block the open waiting for a writer). The open never creates or
|
|
728
|
+
truncates anything.
|
|
729
|
+
3. `fstat` the open descriptor. It must be a regular file with the same device
|
|
730
|
+
and inode numbers `lstat` reported, so a different file renamed into place
|
|
731
|
+
is detected. Only then is anything read.
|
|
732
|
+
4. Read the bytes from that same descriptor (the path is never opened again)
|
|
733
|
+
and parse them.
|
|
734
|
+
|
|
735
|
+
An entry that is replaced, turned into a symlink, FIFO or directory, or removed
|
|
736
|
+
between steps 1 and 2 is reported as `invalid` (exit 2), with a reason such as
|
|
737
|
+
`replaced by a different file after it was checked`; its contents are never
|
|
738
|
+
read, so they cannot leak into the output or make the check pass. If the
|
|
739
|
+
platform lacks `O_NOFOLLOW` or `O_NONBLOCK`, every baseline is `invalid`
|
|
740
|
+
rather than read without them.
|
|
741
|
+
|
|
742
|
+
What this does **not** guarantee:
|
|
743
|
+
|
|
744
|
+
- Only the final path component is protected. Parent directories, including a
|
|
745
|
+
symlinked `--output` directory (which is allowed), are resolved normally and
|
|
746
|
+
are trusted; replacing a parent directory is not detected.
|
|
747
|
+
- The device/inode check pins the file that was opened, not its contents: a
|
|
748
|
+
process that rewrites that same file while it is read is not detected.
|
|
749
|
+
- A hard link is a regular file and is read like any other.
|
|
750
|
+
- Something swapped in after `lstat` is opened before `fstat` can reject it.
|
|
751
|
+
Opening is non-blocking and never makes a terminal the controlling one, but
|
|
752
|
+
opening some device nodes can still have side effects of its own.
|
|
753
|
+
- Inode reuse, network file systems and unusual file systems may weaken these
|
|
754
|
+
checks; they are not a complete guarantee on every file system.
|
|
755
|
+
- It does not make untrusted code safe: your application and cases file still
|
|
756
|
+
run as described above.
|
|
757
|
+
|
|
758
|
+
`jobpayload check` is unchanged: it still reads its fixtures by path and
|
|
759
|
+
follows symbolic links, as in 0.1.0.
|
|
760
|
+
|
|
604
761
|
## Development
|
|
605
762
|
|
|
606
763
|
```sh
|
|
607
764
|
bundle install
|
|
608
765
|
bundle exec rake test # Minitest; randomized order
|
|
609
|
-
bundle exec rake build # pkg/jobpayload-0.
|
|
766
|
+
bundle exec rake build # pkg/jobpayload-0.2.0.gem
|
|
610
767
|
|
|
611
768
|
# Another Active Job version:
|
|
612
769
|
BUNDLE_GEMFILE=gemfiles/activejob_7.2.gemfile bundle install
|
|
@@ -617,6 +774,28 @@ The test suite boots small Rails-free applications from `test/apps` (Active
|
|
|
617
774
|
Job + Active Record on in-memory SQLite) in subprocesses: `v1` writes baseline
|
|
618
775
|
fixtures, `v2_compatible` and `v2_breaking` model later versions of the same app.
|
|
619
776
|
|
|
777
|
+
Tests that need file names which are not valid UTF-8 probe the file system
|
|
778
|
+
of their own temporary directory and are skipped, with the reason, only when
|
|
779
|
+
it rejects such names (as on macOS).
|
|
780
|
+
|
|
781
|
+
CI runs:
|
|
782
|
+
|
|
783
|
+
- **Linux matrix** (`.github/workflows/ci.yml`): the full suite for every
|
|
784
|
+
supported Ruby / Active Job combination above.
|
|
785
|
+
- **macOS smoke test** (same workflow): the full suite on Ruby 3.4 / Active
|
|
786
|
+
Job 8.1.
|
|
787
|
+
- **Rails edge** (`.github/workflows/rails-edge.yml`, on pull requests,
|
|
788
|
+
weekly and on demand): GlobalID classification regression tests
|
|
789
|
+
(`test/edge`) against Rails `main` (`gemfiles/rails_edge.gemfile`), as a
|
|
790
|
+
separate workflow and check. This is an early warning about upstream
|
|
791
|
+
changes, not a supported version; its result is independent of the matrix
|
|
792
|
+
above, and a failure is reported as a failure.
|
|
793
|
+
|
|
794
|
+
```sh
|
|
795
|
+
BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle install
|
|
796
|
+
BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle exec rake test:rails_edge
|
|
797
|
+
```
|
|
798
|
+
|
|
620
799
|
## License
|
|
621
800
|
|
|
622
801
|
MIT. See [LICENSE](LICENSE).
|
data/lib/jobpayload/cli.rb
CHANGED
|
@@ -98,10 +98,13 @@ module JobPayload
|
|
|
98
98
|
opts.on("--output DIR", "Fixture output directory (default: #{DEFAULT_FIXTURES})") { |v| options[:output] = v }
|
|
99
99
|
boot_options(opts, options)
|
|
100
100
|
opts.on("--update", "Replace existing fixtures whose content changed") { options[:update] = true }
|
|
101
|
+
opts.on("--check", "Compare with existing fixtures without writing (exit 1 on drift)") { options[:check] = true }
|
|
101
102
|
format_option(opts, options)
|
|
102
103
|
end
|
|
103
104
|
return EXIT_SUCCESS if parse(parser)
|
|
104
105
|
|
|
106
|
+
raise UsageError, "--check and --update cannot be used together" if options[:check] && options[:update]
|
|
107
|
+
|
|
105
108
|
formatter_name = options[:format]
|
|
106
109
|
raise ConfigurationError, "cases file not found: #{options[:cases]}" unless File.file?(options[:cases])
|
|
107
110
|
if File.exist?(options[:output]) && !File.directory?(options[:output])
|
|
@@ -110,11 +113,20 @@ module JobPayload
|
|
|
110
113
|
|
|
111
114
|
boot(options)
|
|
112
115
|
registry = CaseRegistry.load(options[:cases])
|
|
116
|
+
return snapshot_check(registry, options[:output], formatter_name) if options[:check]
|
|
117
|
+
|
|
113
118
|
entries = Snapshotter.new(registry: registry, output_dir: options[:output], update: options[:update]).call
|
|
114
119
|
@stdout.print(formatter_name == "json" ? snapshot_json(entries) : snapshot_text(entries))
|
|
115
120
|
EXIT_SUCCESS
|
|
116
121
|
end
|
|
117
122
|
|
|
123
|
+
# `snapshot --check`: read-only drift detection against existing fixtures.
|
|
124
|
+
def snapshot_check(registry, fixtures_dir, formatter_name)
|
|
125
|
+
entries = SnapshotChecker.new(registry: registry, fixtures_dir: fixtures_dir).call
|
|
126
|
+
@stdout.print(formatter_name == "json" ? snapshot_check_json(entries) : snapshot_check_text(entries))
|
|
127
|
+
SnapshotChecker.exit_code(entries)
|
|
128
|
+
end
|
|
129
|
+
|
|
118
130
|
def check
|
|
119
131
|
options = {
|
|
120
132
|
fixtures: DEFAULT_FIXTURES, boot: Boot::DEFAULT_PATH, environment: Boot::DEFAULT_ENVIRONMENT,
|
|
@@ -200,6 +212,34 @@ module JobPayload
|
|
|
200
212
|
Canonical.pretty(document)
|
|
201
213
|
end
|
|
202
214
|
|
|
215
|
+
def snapshot_check_text(entries)
|
|
216
|
+
lines = entries.map do |entry|
|
|
217
|
+
line = format("%-9s %s %s", entry.status, entry.name, JobPayload.scrub_utf8(entry.path))
|
|
218
|
+
entry.reason ? "#{line} (#{JobPayload.scrub_utf8(entry.reason)})" : line
|
|
219
|
+
end
|
|
220
|
+
counts = entries.map(&:status).tally
|
|
221
|
+
summary = SnapshotChecker::STATUSES.map { |status| "#{counts.fetch(status, 0)} #{status}" }.join(", ")
|
|
222
|
+
"#{lines.join("\n")}\n\n#{entries.size} fixtures: #{summary}\n"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def snapshot_check_json(entries)
|
|
226
|
+
counts = entries.map(&:status).tally
|
|
227
|
+
document = {
|
|
228
|
+
"schema_version" => 1,
|
|
229
|
+
"tool_version" => VERSION,
|
|
230
|
+
"mode" => "check",
|
|
231
|
+
"status" => SnapshotChecker.status(entries),
|
|
232
|
+
"summary" => { "fixtures" => entries.size }.merge(SnapshotChecker::STATUSES.to_h { |s| [s.to_s, counts.fetch(s, 0)] }),
|
|
233
|
+
"fixtures" => entries.map do |e|
|
|
234
|
+
{
|
|
235
|
+
"name" => e.name, "path" => JobPayload.scrub_utf8(e.path), "status" => e.status.to_s,
|
|
236
|
+
"reason" => e.reason && JobPayload.scrub_utf8(e.reason)
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
}
|
|
240
|
+
Canonical.pretty(document)
|
|
241
|
+
end
|
|
242
|
+
|
|
203
243
|
def usage_error(message)
|
|
204
244
|
@stderr.puts("jobpayload: #{message}")
|
|
205
245
|
@stderr.puts("Run `jobpayload --help` for usage.")
|
data/lib/jobpayload/finding.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module JobPayload
|
|
4
4
|
# One problem found while checking a fixture.
|
|
5
5
|
#
|
|
6
|
-
# Finding codes are a stable API:
|
|
6
|
+
# Finding codes are a stable API: 0.2.0 keeps every 0.1.0 code and meaning.
|
|
7
7
|
Finding = Struct.new(
|
|
8
8
|
:code,
|
|
9
9
|
:fixture,
|
data/lib/jobpayload/fixture.rb
CHANGED
|
@@ -24,9 +24,24 @@ module JobPayload
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def self.parse(path)
|
|
27
|
+
build(path) { File.binread(path) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Like parse, for bytes the caller has already read from +path+ (for
|
|
31
|
+
# example through SecureFixtureReader); +path+ only names the fixture.
|
|
32
|
+
def self.parse_content(path, content)
|
|
33
|
+
build(path) { content }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.build(path)
|
|
27
37
|
basename = File.basename(path, ".json")
|
|
28
38
|
reason = catch(:invalid) do
|
|
29
|
-
|
|
39
|
+
content = begin
|
|
40
|
+
yield
|
|
41
|
+
rescue SystemCallError => e
|
|
42
|
+
invalid!("cannot read file: #{e.message}")
|
|
43
|
+
end
|
|
44
|
+
data = parse_json(content)
|
|
30
45
|
validate!(data, basename)
|
|
31
46
|
return new(name: data["name"], path: path, job: data["job"], source: data["source"])
|
|
32
47
|
end
|
|
@@ -35,8 +50,8 @@ module JobPayload
|
|
|
35
50
|
Invalid.new(JobPayload.scrub_utf8(basename), path, JobPayload.scrub_utf8(reason))
|
|
36
51
|
end
|
|
37
52
|
|
|
38
|
-
def self.parse_json(
|
|
39
|
-
content =
|
|
53
|
+
def self.parse_json(content)
|
|
54
|
+
content = content.dup.force_encoding(Encoding::UTF_8)
|
|
40
55
|
invalid!("file is not valid UTF-8") unless content.valid_encoding?
|
|
41
56
|
data = JSON.parse(content, max_nesting: false)
|
|
42
57
|
# Some json versions decode a lone surrogate escape ("\udc80") into
|
|
@@ -45,8 +60,6 @@ module JobPayload
|
|
|
45
60
|
data
|
|
46
61
|
rescue JSON::ParserError => e
|
|
47
62
|
invalid!("invalid JSON: #{e.message.lines.first.to_s.strip}")
|
|
48
|
-
rescue SystemCallError => e
|
|
49
|
-
invalid!("cannot read file: #{e.message}")
|
|
50
63
|
end
|
|
51
64
|
|
|
52
65
|
# Iterative, so that deeply nested fixtures cannot exhaust the stack here.
|
|
@@ -89,6 +102,6 @@ module JobPayload
|
|
|
89
102
|
def self.invalid!(reason)
|
|
90
103
|
throw :invalid, reason
|
|
91
104
|
end
|
|
92
|
-
private_class_method :parse_json, :valid_strings?, :validate!, :invalid!
|
|
105
|
+
private_class_method :build, :parse_json, :valid_strings?, :validate!, :invalid!
|
|
93
106
|
end
|
|
94
107
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JobPayload
|
|
4
|
+
# Reads a baseline fixture for `snapshot --check` without trusting the path
|
|
5
|
+
# to still name the entry that was checked with lstat.
|
|
6
|
+
#
|
|
7
|
+
# The file is opened read-only with O_NOFOLLOW (a final-component symlink is
|
|
8
|
+
# refused, not followed) and O_NONBLOCK (opening a FIFO never waits for a
|
|
9
|
+
# writer). The open descriptor is then checked with fstat: it must be a
|
|
10
|
+
# regular file with the device and inode lstat reported. Only then is
|
|
11
|
+
# anything read, and only from that descriptor; the path is never reopened.
|
|
12
|
+
#
|
|
13
|
+
# This defends against the fixture's directory entry being replaced between
|
|
14
|
+
# lstat and open. It does not pin parent directories, and it cannot stop a
|
|
15
|
+
# process from rewriting the verified file's contents while it is read.
|
|
16
|
+
module SecureFixtureReader
|
|
17
|
+
# content is set on success, reason otherwise.
|
|
18
|
+
Result = Struct.new(:content, :reason)
|
|
19
|
+
|
|
20
|
+
UNSUPPORTED = "cannot open file safely: this platform lacks O_NOFOLLOW or O_NONBLOCK"
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# +expected+ is the File::Stat from File.lstat(path), already known to be
|
|
25
|
+
# a regular file.
|
|
26
|
+
def read(path, expected)
|
|
27
|
+
flags = open_flags
|
|
28
|
+
return Result.new(nil, UNSUPPORTED) unless flags
|
|
29
|
+
|
|
30
|
+
io = begin
|
|
31
|
+
File.new(path, flags, binmode: true)
|
|
32
|
+
rescue Errno::ENOENT
|
|
33
|
+
return Result.new(nil, "file disappeared after it was checked")
|
|
34
|
+
rescue *symlink_errors
|
|
35
|
+
return Result.new(nil, "replaced by a symbolic link after it was checked (not followed)")
|
|
36
|
+
rescue SystemCallError => e
|
|
37
|
+
return Result.new(nil, "cannot read file: #{e.message}")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
actual = io.stat
|
|
42
|
+
return Result.new(nil, "replaced by something that is not a regular file after it was checked") unless actual.file?
|
|
43
|
+
unless actual.dev == expected.dev && actual.ino == expected.ino
|
|
44
|
+
return Result.new(nil, "replaced by a different file after it was checked")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Result.new(read_verified(io), nil)
|
|
48
|
+
rescue SystemCallError, IOError => e
|
|
49
|
+
Result.new(nil, "cannot read file: #{e.message}")
|
|
50
|
+
ensure
|
|
51
|
+
io.close
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Read-only, no-follow, non-blocking; never creates or truncates. nil when
|
|
56
|
+
# the platform cannot provide that (Ruby defines missing flags as 0), so
|
|
57
|
+
# the caller fails closed instead of falling back to a plain read.
|
|
58
|
+
def open_flags(constants = File::Constants)
|
|
59
|
+
nofollow = constants.const_defined?(:NOFOLLOW) ? constants::NOFOLLOW : 0
|
|
60
|
+
nonblock = constants.const_defined?(:NONBLOCK) ? constants::NONBLOCK : 0
|
|
61
|
+
return nil if nofollow.zero? || nonblock.zero?
|
|
62
|
+
|
|
63
|
+
flags = constants::RDONLY | nofollow | nonblock
|
|
64
|
+
# Never become the controlling terminal if the entry was swapped for one.
|
|
65
|
+
flags |= constants::NOCTTY if constants.const_defined?(:NOCTTY)
|
|
66
|
+
flags
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Errors open(2) reports for a final-component symlink under O_NOFOLLOW:
|
|
70
|
+
# ELOOP on Linux and macOS, EMLINK on FreeBSD, EFTYPE on NetBSD.
|
|
71
|
+
def symlink_errors
|
|
72
|
+
%i[ELOOP EMLINK EFTYPE].filter_map { |name| Errno.const_get(name) if Errno.const_defined?(name) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Only ever called on a descriptor that passed the checks above.
|
|
76
|
+
def read_verified(io)
|
|
77
|
+
io.read
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module JobPayload
|
|
6
|
+
# Compares what the current application serializes for each snapshot case
|
|
7
|
+
# with the fixture already stored for it (`jobpayload snapshot --check`).
|
|
8
|
+
#
|
|
9
|
+
# Read-only: never writes, renames or creates anything. Only the `job`
|
|
10
|
+
# objects are compared, and every key in them counts. The current job is
|
|
11
|
+
# normalized exactly as `snapshot` writes it; the stored job is taken as
|
|
12
|
+
# is, so an edited baseline (including its volatile metadata) is never
|
|
13
|
+
# masked. Both are compared as canonical JSON: `source` metadata, key order
|
|
14
|
+
# and layout are ignored, while value types (1 and 1.0 differ, because
|
|
15
|
+
# Active Job reads them differently) and array order are kept.
|
|
16
|
+
class SnapshotChecker
|
|
17
|
+
# status: :identical, :different, :missing or :invalid; reason is set for
|
|
18
|
+
# :invalid only.
|
|
19
|
+
Entry = Struct.new(:name, :path, :status, :reason)
|
|
20
|
+
|
|
21
|
+
STATUSES = %i[identical different missing invalid].freeze
|
|
22
|
+
|
|
23
|
+
# Overall status => exit status.
|
|
24
|
+
EXIT_CODES = { "pass" => 0, "fail" => 1, "tool_error" => 2 }.freeze
|
|
25
|
+
|
|
26
|
+
def initialize(registry:, fixtures_dir:)
|
|
27
|
+
@registry = registry
|
|
28
|
+
@fixtures_dir = fixtures_dir
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Evaluates every case first (a failing case raises before anything is
|
|
32
|
+
# compared), then checks the fixtures in name order.
|
|
33
|
+
def call
|
|
34
|
+
snapshotter = Snapshotter.new(registry: @registry, output_dir: @fixtures_dir)
|
|
35
|
+
current = @registry.cases.map { |kase| [kase.name, snapshotter.build_document(kase)["job"]] }
|
|
36
|
+
current.map { |name, job| compare(name, job) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# "tool_error" when any baseline is invalid, else "fail" when any differs
|
|
40
|
+
# or is missing, else "pass".
|
|
41
|
+
def self.status(entries)
|
|
42
|
+
statuses = entries.map(&:status)
|
|
43
|
+
return "tool_error" if statuses.include?(:invalid)
|
|
44
|
+
return "fail" if statuses.intersect?(%i[different missing])
|
|
45
|
+
|
|
46
|
+
"pass"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 2 > 1 > 0, from status.
|
|
50
|
+
def self.exit_code(entries)
|
|
51
|
+
EXIT_CODES.fetch(status(entries))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Canonical JSON for a stored job, without normalizing it. Raises
|
|
55
|
+
# ::JSON::JSONError when the job is not something `snapshot` could have
|
|
56
|
+
# written: a non-finite number (1e400 parses to Infinity) or nesting
|
|
57
|
+
# deeper than snapshot's limit of 100.
|
|
58
|
+
def self.canonical_baseline(job)
|
|
59
|
+
::JSON.generate(job)
|
|
60
|
+
Canonical.generate(job)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# +current_job+ is already normalized (Snapshotter#build_document).
|
|
66
|
+
def compare(name, current_job)
|
|
67
|
+
path = File.join(@fixtures_dir, "#{name}.json")
|
|
68
|
+
stat = begin
|
|
69
|
+
File.lstat(path)
|
|
70
|
+
rescue Errno::ENOENT
|
|
71
|
+
return Entry.new(name, path, :missing, nil)
|
|
72
|
+
rescue SystemCallError => e
|
|
73
|
+
# For example an unsearchable fixture directory: not "never generated".
|
|
74
|
+
return invalid(name, path, "cannot access file: #{e.message}")
|
|
75
|
+
end
|
|
76
|
+
# From lstat, so a symlink is never followed, whatever it points to.
|
|
77
|
+
return invalid(name, path, "symbolic link (not followed)") if stat.symlink?
|
|
78
|
+
return invalid(name, path, "not a regular file") unless stat.file?
|
|
79
|
+
|
|
80
|
+
# Opened without following symlinks or blocking, and read only after the
|
|
81
|
+
# open file is confirmed to be the one lstat saw.
|
|
82
|
+
read = SecureFixtureReader.read(path, stat)
|
|
83
|
+
return invalid(name, path, read.reason) if read.reason
|
|
84
|
+
|
|
85
|
+
fixture = begin
|
|
86
|
+
Fixture.parse_content(path, read.content)
|
|
87
|
+
rescue SystemStackError => e
|
|
88
|
+
# Some json versions parse recursively; nesting deep enough to
|
|
89
|
+
# exhaust the stack must not abort the other fixtures.
|
|
90
|
+
return invalid(name, path, "job cannot be canonicalized: #{e.message}")
|
|
91
|
+
end
|
|
92
|
+
return invalid(name, path, fixture.reason) if fixture.is_a?(Fixture::Invalid)
|
|
93
|
+
|
|
94
|
+
baseline = begin
|
|
95
|
+
self.class.canonical_baseline(fixture.job)
|
|
96
|
+
rescue ::JSON::JSONError, ArgumentError, EncodingError, SystemStackError => e
|
|
97
|
+
return invalid(name, path, "job cannot be canonicalized: #{e.message}")
|
|
98
|
+
end
|
|
99
|
+
status = baseline == Canonical.generate(current_job) ? :identical : :different
|
|
100
|
+
Entry.new(name, path, status, nil)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def invalid(name, path, reason)
|
|
104
|
+
Entry.new(name, path, :invalid, reason)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/jobpayload/version.rb
CHANGED
data/lib/jobpayload.rb
CHANGED
|
@@ -15,6 +15,8 @@ module JobPayload
|
|
|
15
15
|
autoload :Boot, "jobpayload/boot"
|
|
16
16
|
autoload :Canonical, "jobpayload/canonical"
|
|
17
17
|
autoload :Snapshotter, "jobpayload/snapshotter"
|
|
18
|
+
autoload :SnapshotChecker, "jobpayload/snapshot_checker"
|
|
19
|
+
autoload :SecureFixtureReader, "jobpayload/secure_fixture_reader"
|
|
18
20
|
autoload :Fixture, "jobpayload/fixture"
|
|
19
21
|
autoload :FixtureLoader, "jobpayload/fixture_loader"
|
|
20
22
|
autoload :Checker, "jobpayload/checker"
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jobpayload
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- cottondesu
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-10-09 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: activejob
|
|
@@ -34,6 +35,7 @@ description: |
|
|
|
34
35
|
deterministic JSON fixtures and checks that the current application can still
|
|
35
36
|
deserialize them (ActiveJob::Base.deserialize and ActiveJob::Arguments.deserialize)
|
|
36
37
|
without ever performing the job.
|
|
38
|
+
email:
|
|
37
39
|
executables:
|
|
38
40
|
- jobpayload
|
|
39
41
|
extensions: []
|
|
@@ -58,6 +60,8 @@ files:
|
|
|
58
60
|
- lib/jobpayload/formatter/json.rb
|
|
59
61
|
- lib/jobpayload/formatter/text.rb
|
|
60
62
|
- lib/jobpayload/result.rb
|
|
63
|
+
- lib/jobpayload/secure_fixture_reader.rb
|
|
64
|
+
- lib/jobpayload/snapshot_checker.rb
|
|
61
65
|
- lib/jobpayload/snapshotter.rb
|
|
62
66
|
- lib/jobpayload/version.rb
|
|
63
67
|
homepage: https://github.com/cottondesu/jobpayload
|
|
@@ -68,6 +72,7 @@ metadata:
|
|
|
68
72
|
source_code_uri: https://github.com/cottondesu/jobpayload
|
|
69
73
|
changelog_uri: https://github.com/cottondesu/jobpayload/blob/main/CHANGELOG.md
|
|
70
74
|
rubygems_mfa_required: 'true'
|
|
75
|
+
post_install_message:
|
|
71
76
|
rdoc_options: []
|
|
72
77
|
require_paths:
|
|
73
78
|
- lib
|
|
@@ -82,7 +87,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
82
87
|
- !ruby/object:Gem::Version
|
|
83
88
|
version: '0'
|
|
84
89
|
requirements: []
|
|
85
|
-
rubygems_version: 3.
|
|
90
|
+
rubygems_version: 3.5.22
|
|
91
|
+
signing_key:
|
|
86
92
|
specification_version: 4
|
|
87
93
|
summary: Checks that previously serialized Active Job payloads still deserialize on
|
|
88
94
|
new code.
|