test(accounting): content-accounting gate for okf build, written red
Capability loop step 3, no capability. tools/okf_accounting_gate.py asks, per README file type, how many of the elements a SOURCE holds okf build books as carried / pointer / coded rejection, with unaccounted and double-booked both required to be 0. Exit 1 today on rows 2, 3, 4 and 6. The fasit is tools/okf_witness.py (stdlib + pdfplumber + poppler, no package import; tested on the live import graph), committed as tests/fixtures/accounting/*inventory.json over one fixture per type. Measured: no source inventory (0 of 13); two graphics/ files carried through documents AND counted extractor_unknown (50 on R761 under --gate none); a refused document logged "0 carried of 0 found"; R761 refused whole because guard 1.4.0 treats its 71 U+00AD soft hyphens as an invisible carrier (asked of the security repo). The two R761 witnesses agree once STS labels are counted by role, not tag. Report: docs/2026-09-17-innholdsregnskapet-rod-gate.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
bc39e8091f
commit
0b00de4408
26 changed files with 2598 additions and 0 deletions
28
tests/fixtures/README.md
vendored
28
tests/fixtures/README.md
vendored
|
|
@ -175,6 +175,34 @@ The version range that carries this lives in `pyproject.toml`'s
|
|||
`[project.optional-dependencies] extract`, with the same reasoning at the
|
||||
declaration site.
|
||||
|
||||
## The content-accounting fixtures (`accounting/`)
|
||||
|
||||
The fasit side of `tools/okf_accounting_gate.py`. `accounting/corpus/` holds
|
||||
one document per row of README's file-type table (13 of 13) plus a `graphics/`
|
||||
directory next to them that the HTML, STS and markdown documents point at --
|
||||
the layout under which a picture is carried through a document AND booked as a
|
||||
rejected file. `accounting/rejected/` holds one HTML document with a
|
||||
zero-width space in its prose, which the guard refuses at every tier, and the
|
||||
image it points at.
|
||||
|
||||
`inventory.json` and `rejected-inventory.json` are what `tools/okf_witness.py`
|
||||
counts in those two directories, committed as data and regenerated only with
|
||||
that tool:
|
||||
|
||||
```
|
||||
python3 tools/okf_witness.py tests/fixtures/accounting/corpus > tests/fixtures/accounting/inventory.json
|
||||
python3 tools/okf_witness.py tests/fixtures/accounting/rejected > tests/fixtures/accounting/rejected-inventory.json
|
||||
```
|
||||
|
||||
`witness/prosess-84-sts.twin.json` is the STS document written by hand in the
|
||||
publisher's JSON node form (`standardContent`, nodes with `e`/`t`/`x`), so the
|
||||
two STS witnesses can be compared on a fixture as well as on R761. Eight of
|
||||
the thirteen documents are byte copies of fixtures documented above
|
||||
(`image-inbox/`, `k2-office/`, `prisark.xlsx`); the other five
|
||||
(`notat.md`, `logg.txt`, `mengder.csv`, `parametre.json`, `side.htm`) are
|
||||
written here, and `notat.md` carries a fenced `# ...` line that is not a
|
||||
heading.
|
||||
|
||||
## What these fixtures do not cover
|
||||
|
||||
Structured table recovery. Measured on real Vegnormalene, only 45 of 196
|
||||
|
|
|
|||
BIN
tests/fixtures/accounting/corpus/graphics/figur-84-1.png
vendored
Normal file
BIN
tests/fixtures/accounting/corpus/graphics/figur-84-1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 B |
BIN
tests/fixtures/accounting/corpus/graphics/tabell-84-2.png
vendored
Normal file
BIN
tests/fixtures/accounting/corpus/graphics/tabell-84-2.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 122 B |
52
tests/fixtures/accounting/corpus/krav-rikt-tekstformat.rtf
vendored
Normal file
52
tests/fixtures/accounting/corpus/krav-rikt-tekstformat.rtf
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{\rtf1\ansi\ansicpg1252\deff0{\fonttbl{\f0\froman Times New Roman;}}\pard\sa180\b\fs32 Kravspesifikasjon for tunnelbelysning\b0\fs24\par
|
||||
\pard\sa180 Dokumentet samler kravene til belysning i vegtunneler over 500 meter.\par
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Dokumentnummer:\cell \pard\intbl SVV-2026-0417\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Tittel:\cell \pard\intbl Tunnelbelysning i hovedl\u248 ?pet\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Ansvarlig avdeling:\cell \pard\intbl Utbyggingsdivisjonen\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Fagomr\u229 ?de:\cell \pard\intbl Elektro og belysning\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Versjon:\cell \pard\intbl 2.3 godkjent\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Gyldig fra:\cell \pard\intbl 01.03.2026\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Erstatter:\cell \pard\intbl SVV-2024-0188\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Tunnellengde:\cell \pard\intbl 1240 meter\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Dimensjonerende fart:\cell \pard\intbl 80 km/t\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl \u197 ?rsd\u248 ?gntrafikk:\cell \pard\intbl 12400 kj\u248 ?ret\u248 ?y\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Terskelluminans:\cell \pard\intbl 145 candela\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Overgangssone:\cell \pard\intbl Tre trinn nedtrapping\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Innerstrekning:\cell \pard\intbl 3,0 candela\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Utkj\u248 ?ringssone:\cell \pard\intbl Ingen forsterkning\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Fargetemperatur:\cell \pard\intbl 4000 kelvin\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Fargegjengivelse:\cell \pard\intbl Ra st\u248 ?rre enn 70\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl N\u248 ?dbelysning:\cell \pard\intbl 60 minutter drift\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Vedlikeholdsfaktor:\cell \pard\intbl 0,80 beregnet\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl M\u229 ?lemetode:\cell \pard\intbl M\u229 ?ling med luminanskamera\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000
|
||||
\pard\intbl Avviksh\u229 ?ndtering:\cell \pard\intbl S\u248 ?knad om fravik\cell \row
|
||||
\pard\sa180 Luminansmatrise per trafikklasse\par
|
||||
\trowd\trgaph108\cellx3000\cellx6000\cellx9000\cellx12000
|
||||
\pard\intbl Sone\cell \pard\intbl Klasse A\cell \pard\intbl Klasse B\cell \pard\intbl Klasse C\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000\cellx9000\cellx12000
|
||||
\pard\intbl Terskel\cell \pard\intbl 150 cd\cell \pard\intbl 120 cd\cell \pard\intbl 95 cd\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000\cellx9000\cellx12000
|
||||
\pard\intbl Overgang\cell \pard\intbl 45 cd\cell \pard\intbl 36 cd\cell \pard\intbl 28 cd\cell \row
|
||||
\trowd\trgaph108\cellx3000\cellx6000\cellx9000\cellx12000
|
||||
\pard\intbl Indre\cell \pard\intbl 3,5 cd\cell \pard\intbl 3,0 cd\cell \pard\intbl 2,5 cd\cell \row
|
||||
}
|
||||
BIN
tests/fixtures/accounting/corpus/krav-tekstdokument.odt
vendored
Normal file
BIN
tests/fixtures/accounting/corpus/krav-tekstdokument.odt
vendored
Normal file
Binary file not shown.
6
tests/fixtures/accounting/corpus/logg.txt
vendored
Normal file
6
tests/fixtures/accounting/corpus/logg.txt
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
Driftslogg for tunnelen.
|
||||
|
||||
Vask utført i mars.
|
||||
Lys kontrollert i april.
|
||||
|
||||
Ingen avvik registrert.
|
||||
3
tests/fixtures/accounting/corpus/mengder.csv
vendored
Normal file
3
tests/fixtures/accounting/corpus/mengder.csv
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Post,Mengde,Enhet
|
||||
84.1,120,m3
|
||||
84.2,45,m2
|
||||
|
19
tests/fixtures/accounting/corpus/notat.md
vendored
Normal file
19
tests/fixtures/accounting/corpus/notat.md
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Drift av tunneler
|
||||
|
||||
Tunnelen driftes etter håndboka.
|
||||
|
||||
## Vask
|
||||
|
||||
Vask skjer to ganger i året.
|
||||
|
||||
| Sone | Intervall |
|
||||
| --- | --- |
|
||||
| Inngang | 6 mnd |
|
||||
| Hovedløp | 12 mnd |
|
||||
|
||||

|
||||
|
||||
```bash
|
||||
# ikke en overskrift
|
||||
okf build .
|
||||
```
|
||||
1
tests/fixtures/accounting/corpus/parametre.json
vendored
Normal file
1
tests/fixtures/accounting/corpus/parametre.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"tunnel": "Eksempeltunnelen", "lengde_m": 1240, "soner": ["inngang", "hovedlop"], "belysning": {"terskel_cd": 145, "fart_kmt": 80}}
|
||||
BIN
tests/fixtures/accounting/corpus/prisark.xlsx
vendored
Normal file
BIN
tests/fixtures/accounting/corpus/prisark.xlsx
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/accounting/corpus/prosess-84-notat.docx
vendored
Normal file
BIN
tests/fixtures/accounting/corpus/prosess-84-notat.docx
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/accounting/corpus/prosess-84-presentasjon.pptx
vendored
Normal file
BIN
tests/fixtures/accounting/corpus/prosess-84-presentasjon.pptx
vendored
Normal file
Binary file not shown.
18
tests/fixtures/accounting/corpus/prosess-84-sts.xml
vendored
Normal file
18
tests/fixtures/accounting/corpus/prosess-84-sts.xml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<standard xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<front><std-ident><doc-number>R761</doc-number></std-ident></front>
|
||||
<body>
|
||||
<sec>
|
||||
<label>84</label>
|
||||
<title>Konstruksjoner av betong</title>
|
||||
<sec>
|
||||
<label>84.1</label>
|
||||
<title>Toleranser</title>
|
||||
<p>Toleranseklasse er gitt i tabell 84-2.</p>
|
||||
<graphic xlink:href="graphics/tabell-84-2.png"/>
|
||||
<p>Figur 84-1 viser prinsippet.</p>
|
||||
<graphic xlink:href="figur-84-1.png"/>
|
||||
</sec>
|
||||
</sec>
|
||||
</body>
|
||||
</standard>
|
||||
BIN
tests/fixtures/accounting/corpus/prosess-84-tabell.pdf
vendored
Normal file
BIN
tests/fixtures/accounting/corpus/prosess-84-tabell.pdf
vendored
Normal file
Binary file not shown.
14
tests/fixtures/accounting/corpus/prosess-84-web.html
vendored
Normal file
14
tests/fixtures/accounting/corpus/prosess-84-web.html
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html><head><title>Prosess 84</title></head>
|
||||
<body>
|
||||
<h1>84 Konstruksjoner av betong</h1>
|
||||
<p>Toleranseklasse for de enkelte konstruksjonsdeler er gitt i tabell 84-2.</p>
|
||||
<img src="graphics/tabell-84-2.png" alt="Tabell 84-2 Toleranseklasser">
|
||||
<p>Figuren under viser prinsippet.</p>
|
||||
<figure>
|
||||
<img src="graphics/figur-84-1.png" alt="Figur 84-1 Prinsippskisse">
|
||||
<figcaption>Figur 84-1 Prinsippskisse</figcaption>
|
||||
</figure>
|
||||
<p>Og en som ligger et annet sted:</p>
|
||||
<img src="https://example.invalid/ekstern.png" alt="Ekstern figur">
|
||||
</body></html>
|
||||
9
tests/fixtures/accounting/corpus/side.htm
vendored
Normal file
9
tests/fixtures/accounting/corpus/side.htm
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<!doctype html>
|
||||
<html><head><title>Side</title></head>
|
||||
<body>
|
||||
<h1>Belysning</h1>
|
||||
<p>Terskelluminansen er 145 candela.</p>
|
||||
<h2>Soner</h2>
|
||||
<ul><li>Inngang</li><li>Hovedløp</li></ul>
|
||||
<table><tr><th>Sone</th><th>Lengde</th></tr><tr><td>Inngang</td><td>120 m</td></tr></table>
|
||||
</body></html>
|
||||
230
tests/fixtures/accounting/inventory.json
vendored
Normal file
230
tests/fixtures/accounting/inventory.json
vendored
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
{
|
||||
"documents": {
|
||||
"krav-rikt-tekstformat.rtf": {
|
||||
"elements": {
|
||||
"cell": 56,
|
||||
"image": 0,
|
||||
"paragraph": 3,
|
||||
"table_row": 24
|
||||
},
|
||||
"images": [],
|
||||
"suffix": ".rtf",
|
||||
"witness": "rtf control words"
|
||||
},
|
||||
"krav-tekstdokument.odt": {
|
||||
"elements": {
|
||||
"cell": 56,
|
||||
"heading": 1,
|
||||
"image": 0,
|
||||
"list_item": 0,
|
||||
"paragraph": 2,
|
||||
"table": 2
|
||||
},
|
||||
"images": [],
|
||||
"suffix": ".odt",
|
||||
"witness": "odt zip xml"
|
||||
},
|
||||
"logg.txt": {
|
||||
"elements": {
|
||||
"line": 4,
|
||||
"paragraph": 3
|
||||
},
|
||||
"images": [],
|
||||
"suffix": ".txt",
|
||||
"witness": "text lines"
|
||||
},
|
||||
"mengder.csv": {
|
||||
"elements": {
|
||||
"cell": 6,
|
||||
"header_cell": 3,
|
||||
"row": 2
|
||||
},
|
||||
"images": [],
|
||||
"suffix": ".csv",
|
||||
"witness": "csv"
|
||||
},
|
||||
"notat.md": {
|
||||
"elements": {
|
||||
"code_block": 1,
|
||||
"heading": 2,
|
||||
"image": 1,
|
||||
"paragraph": 2,
|
||||
"table": 1,
|
||||
"table_row": 2
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"kind": "local",
|
||||
"ref": "graphics/tabell-84-2.png",
|
||||
"target": "graphics/tabell-84-2.png"
|
||||
}
|
||||
],
|
||||
"suffix": ".md",
|
||||
"witness": "markdown lines"
|
||||
},
|
||||
"parametre.json": {
|
||||
"elements": {
|
||||
"key": 6,
|
||||
"value": 6
|
||||
},
|
||||
"images": [],
|
||||
"suffix": ".json",
|
||||
"witness": "json"
|
||||
},
|
||||
"prisark.xlsx": {
|
||||
"elements": {
|
||||
"cell": 18,
|
||||
"image": 0,
|
||||
"row": 9,
|
||||
"sheet": 2
|
||||
},
|
||||
"images": [],
|
||||
"suffix": ".xlsx",
|
||||
"witness": "xlsx zip xml"
|
||||
},
|
||||
"prosess-84-notat.docx": {
|
||||
"elements": {
|
||||
"cell": 0,
|
||||
"footnote": 0,
|
||||
"heading": 0,
|
||||
"image": 1,
|
||||
"paragraph": 2,
|
||||
"table": 0
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"kind": "embedded",
|
||||
"ref": "",
|
||||
"target": null
|
||||
}
|
||||
],
|
||||
"suffix": ".docx",
|
||||
"witness": "docx zip xml"
|
||||
},
|
||||
"prosess-84-presentasjon.pptx": {
|
||||
"elements": {
|
||||
"cell": 0,
|
||||
"image": 1,
|
||||
"paragraph": 0,
|
||||
"slide": 1,
|
||||
"table": 0,
|
||||
"title": 1
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"kind": "embedded",
|
||||
"ref": "",
|
||||
"target": null
|
||||
}
|
||||
],
|
||||
"suffix": ".pptx",
|
||||
"witness": "pptx zip xml"
|
||||
},
|
||||
"prosess-84-sts.xml": {
|
||||
"elements": {
|
||||
"cell": 0,
|
||||
"footnote": 0,
|
||||
"image": 2,
|
||||
"list_item": 0,
|
||||
"paragraph": 2,
|
||||
"section": 2,
|
||||
"section_label": 2,
|
||||
"table": 0,
|
||||
"table_label": 0,
|
||||
"title": 2
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"kind": "local",
|
||||
"ref": "graphics/tabell-84-2.png",
|
||||
"target": "graphics/tabell-84-2.png"
|
||||
},
|
||||
{
|
||||
"kind": "local",
|
||||
"ref": "figur-84-1.png",
|
||||
"target": "graphics/figur-84-1.png"
|
||||
}
|
||||
],
|
||||
"suffix": ".xml",
|
||||
"witness": "xml.etree"
|
||||
},
|
||||
"prosess-84-tabell.pdf": {
|
||||
"elements": {
|
||||
"image": 2,
|
||||
"page": 1
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"kind": "embedded",
|
||||
"ref": "",
|
||||
"target": null
|
||||
},
|
||||
{
|
||||
"kind": "embedded",
|
||||
"ref": "",
|
||||
"target": null
|
||||
}
|
||||
],
|
||||
"suffix": ".pdf",
|
||||
"witness": "pdfplumber objects"
|
||||
},
|
||||
"prosess-84-web.html": {
|
||||
"elements": {
|
||||
"cell": 0,
|
||||
"heading": 1,
|
||||
"image": 3,
|
||||
"list_item": 0,
|
||||
"paragraph": 3,
|
||||
"table": 0
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"kind": "local",
|
||||
"ref": "graphics/tabell-84-2.png",
|
||||
"target": "graphics/tabell-84-2.png"
|
||||
},
|
||||
{
|
||||
"kind": "local",
|
||||
"ref": "graphics/figur-84-1.png",
|
||||
"target": "graphics/figur-84-1.png"
|
||||
},
|
||||
{
|
||||
"kind": "remote",
|
||||
"ref": "https://example.invalid/ekstern.png",
|
||||
"target": null
|
||||
}
|
||||
],
|
||||
"suffix": ".html",
|
||||
"witness": "html.parser"
|
||||
},
|
||||
"side.htm": {
|
||||
"elements": {
|
||||
"cell": 4,
|
||||
"heading": 2,
|
||||
"image": 0,
|
||||
"list_item": 2,
|
||||
"paragraph": 1,
|
||||
"table": 1
|
||||
},
|
||||
"images": [],
|
||||
"suffix": ".htm",
|
||||
"witness": "html.parser"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"graphics/figur-84-1.png": {
|
||||
"pointed_at_by": [
|
||||
"prosess-84-sts.xml",
|
||||
"prosess-84-web.html"
|
||||
]
|
||||
},
|
||||
"graphics/tabell-84-2.png": {
|
||||
"pointed_at_by": [
|
||||
"notat.md",
|
||||
"prosess-84-sts.xml",
|
||||
"prosess-84-web.html"
|
||||
]
|
||||
}
|
||||
},
|
||||
"witness_version": 1
|
||||
}
|
||||
31
tests/fixtures/accounting/rejected-inventory.json
vendored
Normal file
31
tests/fixtures/accounting/rejected-inventory.json
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"documents": {
|
||||
"avvist.html": {
|
||||
"elements": {
|
||||
"cell": 0,
|
||||
"heading": 1,
|
||||
"image": 1,
|
||||
"list_item": 0,
|
||||
"paragraph": 1,
|
||||
"table": 0
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"kind": "local",
|
||||
"ref": "graphics/figur.png",
|
||||
"target": "graphics/figur.png"
|
||||
}
|
||||
],
|
||||
"suffix": ".html",
|
||||
"witness": "html.parser"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"graphics/figur.png": {
|
||||
"pointed_at_by": [
|
||||
"avvist.html"
|
||||
]
|
||||
}
|
||||
},
|
||||
"witness_version": 1
|
||||
}
|
||||
7
tests/fixtures/accounting/rejected/avvist.html
vendored
Normal file
7
tests/fixtures/accounting/rejected/avvist.html
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<!doctype html>
|
||||
<html><head><title>Avvist</title></head>
|
||||
<body>
|
||||
<h1>Skjult</h1>
|
||||
<p>Denne teksten bærer et usynlig tegn.</p>
|
||||
<img src="graphics/figur.png" alt="Figur 1">
|
||||
</body></html>
|
||||
BIN
tests/fixtures/accounting/rejected/graphics/figur.png
vendored
Normal file
BIN
tests/fixtures/accounting/rejected/graphics/figur.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 B |
216
tests/fixtures/accounting/witness/prosess-84-sts.twin.json
vendored
Normal file
216
tests/fixtures/accounting/witness/prosess-84-sts.twin.json
vendored
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
{
|
||||
"productInfo": {
|
||||
"meta": {
|
||||
"productTitle": "R761 fixture twin"
|
||||
}
|
||||
},
|
||||
"standardContent": {
|
||||
"id": null,
|
||||
"tag": "#document",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "standard",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "front",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "std-ident",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "doc-number",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 2,
|
||||
"t": "R761",
|
||||
"x": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "body",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "sec",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "label",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 2,
|
||||
"t": "84",
|
||||
"x": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "title",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 2,
|
||||
"t": "Konstruksjoner av betong",
|
||||
"x": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "sec",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "label",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 2,
|
||||
"t": "84.1",
|
||||
"x": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "title",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 2,
|
||||
"t": "Toleranser",
|
||||
"x": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "p",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 2,
|
||||
"t": "Toleranseklasse er gitt i tabell 84-2.",
|
||||
"x": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "graphic",
|
||||
"a": {
|
||||
"xlink:href": "graphic_0001.png"
|
||||
},
|
||||
"c": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "p",
|
||||
"a": {},
|
||||
"c": [
|
||||
{
|
||||
"e": 2,
|
||||
"t": "Figur 84-1 viser prinsippet.",
|
||||
"x": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"e": 1,
|
||||
"t": null,
|
||||
"x": {
|
||||
"id": null,
|
||||
"tag": "graphic",
|
||||
"a": {
|
||||
"xlink:href": "graphic_0002.png"
|
||||
},
|
||||
"c": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
425
tests/test_accounting_gate.py
Normal file
425
tests/test_accounting_gate.py
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
"""The content-accounting gate's own tests (`tools/okf_accounting_gate.py`).
|
||||
|
||||
The gate is RED today by design: `okf build` has no source inventory and no
|
||||
per-element accounting. These tests are GREEN and prove three things.
|
||||
|
||||
1. The fasit is independent. `tools/okf_witness.py` imports no
|
||||
`llm_ingestion_okf` module -- checked on the live import graph of a process
|
||||
that ran every witness, with a control that shows the check fires -- and
|
||||
the committed inventories are exactly what the witness counts today, pinned
|
||||
again to hand counts on four documents.
|
||||
2. Every row CAN turn green and CAN turn red, each boundary driven from both
|
||||
sides with synthetic build output (the door the capability must open).
|
||||
3. Run against the real `okf build` at this commit, the gate sees the three
|
||||
defects it was ordered for: no inventory, a file carried AND rejected, and
|
||||
a rejected document logged as "0 carried of 0 found".
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
TOOLS = Path(__file__).resolve().parents[1] / "tools"
|
||||
sys.path.insert(0, str(TOOLS))
|
||||
|
||||
import okf_accounting_gate as gate # noqa: E402
|
||||
import okf_witness as witness # noqa: E402
|
||||
|
||||
TABLE = [
|
||||
".csv",
|
||||
".docx",
|
||||
".htm",
|
||||
".html",
|
||||
".json",
|
||||
".md",
|
||||
".odt",
|
||||
".pdf",
|
||||
".pptx",
|
||||
".rtf",
|
||||
".txt",
|
||||
".xlsx",
|
||||
".xml",
|
||||
]
|
||||
|
||||
|
||||
# --- 1. the fasit ------------------------------------------------------------
|
||||
|
||||
_IMPORT_PROBE = """
|
||||
import sys
|
||||
sys.path.insert(0, {tools!r})
|
||||
{preload}
|
||||
import okf_witness as w
|
||||
from pathlib import Path
|
||||
fixtures = Path({fixtures!r})
|
||||
w.witness_inbox(fixtures / "corpus")
|
||||
w.witness_inbox(fixtures / "rejected")
|
||||
w.count_sts_json((fixtures / "witness" / "prosess-84-sts.twin.json").read_bytes())
|
||||
w.pdf_poppler(fixtures / "corpus" / "prosess-84-tabell.pdf")
|
||||
print(sorted(m for m in sys.modules if m.split(".")[0] == "llm_ingestion_okf"))
|
||||
"""
|
||||
|
||||
|
||||
def _package_modules_after_witness(preload: str) -> list[str]:
|
||||
script = _IMPORT_PROBE.format(tools=str(TOOLS), fixtures=str(gate.FIXTURES), preload=preload)
|
||||
out = subprocess.run(
|
||||
[sys.executable, "-c", script], capture_output=True, text=True, check=True
|
||||
).stdout
|
||||
result: list[str] = json.loads(out.strip().splitlines()[-1].replace("'", '"'))
|
||||
return result
|
||||
|
||||
|
||||
def test_the_witness_imports_no_module_of_the_package_it_judges() -> None:
|
||||
pytest.importorskip("pdfplumber")
|
||||
assert _package_modules_after_witness("") == []
|
||||
|
||||
|
||||
def test_the_import_check_fires_when_the_package_is_loaded() -> None:
|
||||
pytest.importorskip("pdfplumber")
|
||||
assert "llm_ingestion_okf" in _package_modules_after_witness("import llm_ingestion_okf")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("inbox", "committed"),
|
||||
[(gate.CORPUS, gate.INVENTORY), (gate.REJECTED, gate.REJECTED_INVENTORY)],
|
||||
)
|
||||
def test_the_committed_fasit_is_what_the_witness_counts(inbox: Path, committed: Path) -> None:
|
||||
pytest.importorskip("pdfplumber")
|
||||
assert witness.witness_inbox(inbox) == gate.load_inventory(committed)
|
||||
|
||||
|
||||
def test_the_fixture_corpus_covers_every_readme_file_type() -> None:
|
||||
inventory = gate.load_inventory(gate.INVENTORY)
|
||||
assert gate.readme_types() == TABLE
|
||||
assert {entry["suffix"] for entry in inventory["documents"].values()} == set(TABLE)
|
||||
|
||||
|
||||
# Counted by hand from the fixture bytes, not by the witness.
|
||||
HAND_COUNTS = {
|
||||
"notat.md": {
|
||||
"code_block": 1,
|
||||
"heading": 2,
|
||||
"image": 1,
|
||||
"paragraph": 2,
|
||||
"table": 1,
|
||||
"table_row": 2,
|
||||
},
|
||||
"side.htm": {"cell": 4, "heading": 2, "image": 0, "list_item": 2, "paragraph": 1, "table": 1},
|
||||
"krav-rikt-tekstformat.rtf": {"cell": 56, "image": 0, "paragraph": 3, "table_row": 24},
|
||||
"prosess-84-sts.xml": {
|
||||
"cell": 0,
|
||||
"footnote": 0,
|
||||
"image": 2,
|
||||
"list_item": 0,
|
||||
"paragraph": 2,
|
||||
"section": 2,
|
||||
"section_label": 2,
|
||||
"table": 0,
|
||||
"table_label": 0,
|
||||
"title": 2,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", sorted(HAND_COUNTS))
|
||||
def test_the_witness_matches_a_hand_count(name: str) -> None:
|
||||
inventory = witness.witness_file(gate.CORPUS, gate.CORPUS / name)
|
||||
assert inventory.elements == HAND_COUNTS[name]
|
||||
|
||||
|
||||
def test_a_fenced_heading_is_not_a_heading_to_the_witness() -> None:
|
||||
elements, _ = witness.count_markdown("# Real\n\n```bash\n# not one\n```\n")
|
||||
assert elements["heading"] == 1
|
||||
assert elements["code_block"] == 1
|
||||
|
||||
|
||||
def test_the_sts_image_reference_resolves_through_the_graphics_directory() -> None:
|
||||
inventory = witness.witness_file(gate.CORPUS, gate.STS_FIXTURE)
|
||||
assert [ref.target for ref in inventory.images] == [
|
||||
"graphics/tabell-84-2.png",
|
||||
"graphics/figur-84-1.png",
|
||||
]
|
||||
|
||||
|
||||
def test_a_reference_above_the_document_resolves_to_nothing(tmp_path: Path) -> None:
|
||||
(tmp_path / "secret.png").write_bytes(b"x")
|
||||
(tmp_path / "docs").mkdir()
|
||||
document = tmp_path / "docs" / "a.html"
|
||||
assert witness.resolve_local(tmp_path, document, "../secret.png") is None
|
||||
|
||||
|
||||
def test_the_witness_refuses_a_doctype() -> None:
|
||||
with pytest.raises(witness.WitnessRefused):
|
||||
witness.count_sts_xml(b'<!DOCTYPE x [<!ENTITY a "b">]><standard/>')
|
||||
|
||||
|
||||
# --- 2. every row can go both ways -------------------------------------------
|
||||
|
||||
|
||||
def _inventory() -> dict[str, Any]:
|
||||
return {
|
||||
"documents": {
|
||||
"a.md": {"suffix": ".md", "elements": {"heading": 2, "image": 1}, "images": []},
|
||||
},
|
||||
"files": {"graphics/x.png": {"pointed_at_by": ["a.md"]}},
|
||||
}
|
||||
|
||||
|
||||
def _build(
|
||||
*,
|
||||
accounting: dict[str, Any] | None = None,
|
||||
sources: set[str] | None = None,
|
||||
assets: set[str] | None = None,
|
||||
log: str = "",
|
||||
exit_code: int = 0,
|
||||
) -> gate.Build:
|
||||
return gate.Build(
|
||||
exit_code=exit_code,
|
||||
log=log,
|
||||
accounting=accounting,
|
||||
source_files={"a.md"} if sources is None else sources,
|
||||
asset_prefixes=assets or set(),
|
||||
)
|
||||
|
||||
|
||||
def _declared(heading: int = 2, image: int = 1, fate: str = "rejected") -> dict[str, Any]:
|
||||
return {
|
||||
"accounting_version": 1,
|
||||
"documents": [
|
||||
{
|
||||
"source_file": "a.md",
|
||||
"status": "persisted",
|
||||
"code": None,
|
||||
"inventory": {"heading": 2, "image": 1},
|
||||
"fates": {
|
||||
"heading": {"carried": heading},
|
||||
"image": {"pointer": image},
|
||||
},
|
||||
}
|
||||
],
|
||||
"files": [{"source_file": "graphics/x.png", "fate": fate, "code": "extractor_unknown"}],
|
||||
}
|
||||
|
||||
|
||||
def _corpus(tmp_path: Path) -> Path:
|
||||
(tmp_path / "graphics").mkdir()
|
||||
(tmp_path / "graphics" / "x.png").write_bytes(b"png bytes")
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_row1_is_green_when_every_type_has_a_fasit() -> None:
|
||||
inventory = {
|
||||
"documents": {f"f{s}": {"suffix": s, "elements": {}} for s in TABLE},
|
||||
"files": {},
|
||||
}
|
||||
row = gate.row1(TABLE, inventory, inventory)
|
||||
assert (row.k, row.m, row.status) == (13, 13, gate.GREEN)
|
||||
|
||||
|
||||
def test_row1_is_red_when_one_type_lacks_a_fasit() -> None:
|
||||
inventory = {
|
||||
"documents": {f"f{s}": {"suffix": s, "elements": {}} for s in TABLE[:-1]},
|
||||
"files": {},
|
||||
}
|
||||
row = gate.row1(TABLE, inventory, inventory)
|
||||
assert (row.k, row.m, row.status) == (12, 13, gate.RED)
|
||||
|
||||
|
||||
def test_row1_is_red_when_the_committed_fasit_is_stale() -> None:
|
||||
inventory = {"documents": {"f.md": {"suffix": ".md", "elements": {"heading": 1}}}}
|
||||
fresh = {"documents": {"f.md": {"suffix": ".md", "elements": {"heading": 2}}}}
|
||||
row = gate.row1([".md"], inventory, fresh)
|
||||
assert (row.k, row.status) == (0, gate.RED)
|
||||
|
||||
|
||||
def test_row2_is_red_without_the_door() -> None:
|
||||
row = gate.row2([".md"], _inventory(), _build(), door=False)
|
||||
assert (row.k, row.m, row.status) == (0, 1, gate.RED)
|
||||
|
||||
|
||||
def test_row2_is_green_when_the_declared_inventory_equals_the_witness() -> None:
|
||||
row = gate.row2([".md"], _inventory(), _build(accounting=_declared()), door=True)
|
||||
assert (row.k, row.m, row.status) == (1, 1, gate.GREEN)
|
||||
|
||||
|
||||
def test_row2_is_red_when_the_declared_inventory_is_one_off() -> None:
|
||||
declared = _declared()
|
||||
declared["documents"][0]["inventory"]["heading"] = 3
|
||||
row = gate.row2([".md"], _inventory(), _build(accounting=declared), door=True)
|
||||
assert (row.k, row.status) == (0, gate.RED)
|
||||
|
||||
|
||||
def test_row3_is_green_when_every_element_and_file_has_one_fate(tmp_path: Path) -> None:
|
||||
units = gate.account(_inventory(), _build(accounting=_declared()), _corpus(tmp_path))
|
||||
row = gate.row3(units, door=True)
|
||||
assert (row.k, row.m, row.status) == (2, 2, gate.GREEN)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("heading", "u", "d"), [(1, 1, 0), (3, 0, 1)])
|
||||
def test_row3_is_red_one_element_either_side(tmp_path: Path, heading: int, u: int, d: int) -> None:
|
||||
units = gate.account(
|
||||
_inventory(), _build(accounting=_declared(heading=heading)), _corpus(tmp_path)
|
||||
)
|
||||
document = units[0]
|
||||
assert (document.unaccounted, document.double) == (u, d)
|
||||
assert gate.row3(units, door=True).status == gate.RED
|
||||
|
||||
|
||||
def test_row3_is_red_when_no_fate_is_declared(tmp_path: Path) -> None:
|
||||
units = gate.account(_inventory(), _build(), _corpus(tmp_path))
|
||||
assert units[0].unaccounted == 3
|
||||
assert gate.row3(units, door=False).status == gate.RED
|
||||
|
||||
|
||||
def test_a_file_carried_through_a_document_and_rejected_is_double_booked(tmp_path: Path) -> None:
|
||||
corpus = _corpus(tmp_path)
|
||||
carried = {gate._sha12(corpus / "graphics" / "x.png")}
|
||||
units = gate.account(_inventory(), _build(accounting=_declared(), assets=carried), corpus)
|
||||
assert (units[1].unaccounted, units[1].double) == (0, 1)
|
||||
|
||||
|
||||
def test_a_file_carried_through_a_document_and_declared_carried_is_clean(tmp_path: Path) -> None:
|
||||
corpus = _corpus(tmp_path)
|
||||
carried = {gate._sha12(corpus / "graphics" / "x.png")}
|
||||
build = _build(accounting=_declared(fate="carried"), assets=carried)
|
||||
assert gate.account(_inventory(), build, corpus)[1].clean
|
||||
|
||||
|
||||
def test_a_file_declared_carried_without_its_bytes_is_unaccounted(tmp_path: Path) -> None:
|
||||
build = _build(accounting=_declared(fate="carried"))
|
||||
unit = gate.account(_inventory(), build, _corpus(tmp_path))[1]
|
||||
assert (unit.unaccounted, unit.double) == (1, 0)
|
||||
|
||||
|
||||
def test_an_unpointed_file_sharing_bytes_with_a_carried_one_is_not_carried(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
corpus = _corpus(tmp_path)
|
||||
(corpus / "graphics" / "twin.png").write_bytes(b"png bytes")
|
||||
inventory = _inventory()
|
||||
inventory["files"]["graphics/twin.png"] = {"pointed_at_by": []}
|
||||
carried = {gate._sha12(corpus / "graphics" / "x.png")}
|
||||
units = gate.account(inventory, _build(assets=carried), corpus)
|
||||
assert [(u.name, u.double) for u in units[1:]] == [
|
||||
("graphics/twin.png", 0),
|
||||
("graphics/x.png", 1),
|
||||
]
|
||||
|
||||
|
||||
def test_without_the_door_double_booking_is_derived_from_conservation(tmp_path: Path) -> None:
|
||||
corpus = _corpus(tmp_path)
|
||||
carried = {gate._sha12(corpus / "graphics" / "x.png")}
|
||||
assert gate.account(_inventory(), _build(assets=carried), corpus)[1].double == 1
|
||||
assert gate.account(_inventory(), _build(), corpus)[1].clean
|
||||
|
||||
|
||||
_HONEST_LOG = (
|
||||
"* **Images**: 0 carried of 1 found, written to `assets/`.\n"
|
||||
"* a.md: 3 elements found in the source, 0 carried: document rejected `fail_secure`\n"
|
||||
)
|
||||
|
||||
|
||||
def _rejected_inventory() -> dict[str, Any]:
|
||||
inventory = _inventory()
|
||||
inventory["documents"]["a.md"]["images"] = [{"kind": "local"}]
|
||||
return inventory
|
||||
|
||||
|
||||
def test_row4_is_green_when_the_log_names_what_the_rejected_document_held() -> None:
|
||||
row = gate.row4(_rejected_inventory(), _build(sources=set(), log=_HONEST_LOG))
|
||||
assert (row.k, row.m, row.status) == (1, 1, gate.GREEN)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"log",
|
||||
[
|
||||
_HONEST_LOG.replace("0 carried of 1 found", "0 carried of 0 found"),
|
||||
_HONEST_LOG.replace("3 elements", "2 elements"),
|
||||
_HONEST_LOG.replace(" `fail_secure`", ""),
|
||||
],
|
||||
)
|
||||
def test_row4_is_red_when_the_log_understates_the_rejected_document(log: str) -> None:
|
||||
row = gate.row4(_rejected_inventory(), _build(sources=set(), log=log))
|
||||
assert (row.k, row.status) == (0, gate.RED)
|
||||
|
||||
|
||||
def test_row4_cannot_be_green_when_nothing_was_rejected() -> None:
|
||||
row = gate.row4(_rejected_inventory(), _build(log=_HONEST_LOG))
|
||||
assert (row.m, row.status) == (0, gate.RED)
|
||||
|
||||
|
||||
def test_row5_is_green_when_the_witnesses_agree() -> None:
|
||||
row = gate.row5([("pair", gate.compare({"p": 3}, {"p": 3}))], [])
|
||||
assert (row.k, row.m, row.status) == (1, 1, gate.GREEN)
|
||||
|
||||
|
||||
def test_row5_is_red_with_both_numbers_when_they_disagree_by_one() -> None:
|
||||
row = gate.row5([("pair", gate.compare({"p": 3}, {"p": 4}))], [])
|
||||
assert row.status == gate.RED
|
||||
assert row.details == ["pair: p: 3 vs 4"]
|
||||
|
||||
|
||||
def test_row5_is_red_when_a_witness_is_missing() -> None:
|
||||
assert gate.row5([("pair", gate.compare({"p": 3}, None))], []).status == gate.RED
|
||||
|
||||
|
||||
def test_row6_without_its_source_is_red_locally_and_skipped_in_ci(tmp_path: Path) -> None:
|
||||
missing = tmp_path / "absent"
|
||||
local = gate.row6(missing, ci=False)
|
||||
ci = gate.row6(missing, ci=True)
|
||||
assert (local.status, local.fails) == (gate.RED, True)
|
||||
assert (ci.status, ci.fails) == (gate.SKIPPED, False)
|
||||
assert "source missing" in ci.reason
|
||||
|
||||
|
||||
def test_the_proposed_exceptions_are_not_applied() -> None:
|
||||
assert gate.APPROVED_EXCEPTIONS == frozenset()
|
||||
assert "NOT APPROVED" in gate.render([])
|
||||
|
||||
|
||||
def test_bad_usage_exits_two() -> None:
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
gate.main(["--no-such-flag"])
|
||||
assert exc.value.code == 2
|
||||
|
||||
|
||||
# --- 3. the real build at this commit ----------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def real_rows() -> list[gate.Row]:
|
||||
pytest.importorskip("pdfplumber")
|
||||
pytest.importorskip("pypandoc")
|
||||
if gate.door_available():
|
||||
pytest.skip("the accounting door exists; these rows describe the build before it")
|
||||
return gate.evaluate(r761=None, ci=True, consume=False)
|
||||
|
||||
|
||||
def test_the_real_gate_is_red_on_rows_two_three_and_four(real_rows: list[gate.Row]) -> None:
|
||||
assert [r.number for r in real_rows if r.fails] == [2, 3, 4]
|
||||
assert [(r.number, r.status) for r in real_rows if not r.fails] == [
|
||||
(1, gate.GREEN),
|
||||
(5, gate.GREEN),
|
||||
(6, gate.SKIPPED),
|
||||
]
|
||||
|
||||
|
||||
def test_the_real_gate_sees_the_graphics_carried_and_rejected(real_rows: list[gate.Row]) -> None:
|
||||
row3 = real_rows[2]
|
||||
assert "d = 2 double-booked" in row3.reason
|
||||
doubled = [d for d in row3.details if "AND rejected" in d]
|
||||
assert [d.split(":")[0] for d in doubled] == [
|
||||
"file graphics/figur-84-1.png",
|
||||
"file graphics/tabell-84-2.png",
|
||||
]
|
||||
|
||||
|
||||
def test_the_real_gate_sees_the_rejected_document_logged_as_empty(
|
||||
real_rows: list[gate.Row],
|
||||
) -> None:
|
||||
assert "log says 0 carried of 0 found; the source declares 1" in "\n".join(real_rows[3].details)
|
||||
Loading…
Add table
Add a link
Reference in a new issue