Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ compilers/ Layer 1 — the Compiler contract and the format-keyed registry. Im
internal/ Layer 1 — that compiler's own packages, each with its own allowlist rather than
the compiler's. The ordering among them is real and enforced, from diag (reaches
only ir) up to operation, document and detect. None may reach the compiler above it.
e2e/ Test files only, so outside the layering: that compiler's end-to-end tests, the
conformance corpus and goldens among them, compiled through its public face.
pass/ Layer 1 — IR → IR passes. Imports ir and ir/irfacts.
emitters/* Layer 2 — imports ir, ir/irfacts + emitter contract; never compiler. (Not built yet.)
engine/ Layer 3 — orchestration; imports everything below. It owns every read, write and
Expand Down Expand Up @@ -205,11 +207,12 @@ These all exist already — extend them rather than building a parallel mechanis
`internal/jsonshape` by `internal/census`; `go test ./cmd/morphic-census` holds the file to a
census of `testdata/`. A case that sets a fact takes its line out, so its change runs `-update`.
- **Lowering specification**: `docs/compilers/openapi.md` is machine-read like the matrix. Each rule
names its evidence, and the `TestLoweringSpec` tests in `compilers/openapi` hold the names to the
code both ways, each listed code to the rule's text, and every `docs/compilers/openapi.md#<key>`
citation to a rule. It, `docs/parser-library.md` and each format's conformance `matrix.md` are
read through `internal/keydoc`, the one reader of the repository's keyed documents, and a test a
Markdown document names must exist (`internal/archtest`).
names its evidence, and the `TestLoweringSpec` tests in `compilers/openapi/e2e` hold the names to
the code both ways, each listed code to the rule's text, and every
`docs/compilers/openapi.md#<key>` citation to a rule. It, `docs/parser-library.md` and each
format's conformance `matrix.md` are read through `internal/keydoc`, the one reader of the
repository's keyed documents, and a test a Markdown document names must exist
(`internal/archtest`).
- **Oracles**: `internal/harness` drives a spec through no-panic → no error diagnostic →
`irverify` invariants → JSON round-trip → determinism → order-invariance, stopping at the first
one that fires. `harness.Check` is the list — read it there rather than trusting this sentence;
Expand Down
40 changes: 33 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,31 @@ Read the documents for your layer, and skip the rest until you need them.
| The IR | `ir-design.md`, `architecture.md` §2.5, `prior-art.md` | `reference-learnings.md` |
| An emitter (none exists yet) | [`emitter-design.md`](docs/emitter-design.md), `architecture.md` §2.3 to §2.5 | `prior-art.md` |

### Where a test goes

- **A unit test sits beside the code it tests,** in its package, in the `_test.go` named after the
file that declares what the test's name says it tests, as `TestTypes_RegisterTakesNoCoordinate`,
of `Types` in `compilers/compile/types.go`, sits in `types_test.go`. Once a file's tests split by
topic, each part keeps that prefix, as `load_entry_internal_test.go` does beside `load.go`. A
scenario several files of a package decide together goes in a file named for the scenario, as
`pass/validate_propids_test.go` is, a check over a whole package in one named for what it checks,
as `ir/nofloat_test.go` is, and in a package of one source file a test file needs no prefix.
*Fails if missed:* `make coverage`, for a test in another package than its code, since coverage
counts only a package's own tests. Nothing fails for a test in the wrong file of its package.
- **An end-to-end test of the OpenAPI compiler goes in `compilers/openapi/e2e`,** in a file named
for the construct or the property it checks: a test of what the compiler does as a whole, which no
one package decides, such as the conformance corpus, the goldens, fuzzing, benchmarks and a
scenario several packages settle together. A test of one package's code stays in that package even
when it compiles a whole spec through `openapi.New` to reach the code, as `internal/schema`'s
`parseFull` tests do, since only there does it count toward that package's coverage. The e2e
package holds no code, so its tests cover nothing, and the code they reach still needs a unit test
beside it; `compilers/openapi` itself holds only the tests of its own files. A helper several of
the package's files share is in its `helpers_test.go`, except the corpus's own reader, which
`conformance_test.go` declares, and a fixture the files of one construct share, which stays with
that construct.
*Fails if missed:* `make coverage`, for code only an end-to-end test reaches. Nothing fails for an
end-to-end test written in the compiler's own package or in one of its internal packages.

## Add a lowering rule to the OpenAPI compiler

A lowering rule says how one OpenAPI construct reaches the IR: the field it fills, what it keeps in
Expand Down Expand Up @@ -74,20 +99,21 @@ A lowering rule says how one OpenAPI construct reaches the IR: the field it fill
`TestDocComments_StayUnderTheCap`, `TestSuppressions_NoDirectiveStandsInTheSource`.
4. **Add a conformance case and a unit test.** Write a minimal spec at
`testdata/conformance/openapi/<name>.yaml` that exercises the rule, add an entry to
`conformanceCases()` in `compilers/openapi/conformance_test.go` naming its assertion, list it
`conformanceCases()` in `compilers/openapi/e2e/conformance_test.go` naming its assertion, list it
under each [`ir-spec-matrix.md`](docs/ir-spec-matrix.md) row it witnesses in
`testdata/conformance/openapi/matrix.md` (none, for a case that pins only a diagnostic), write
the assertion, and name the case in the rule's Cases field. Then write its golden alone, since
the assertion in the `conformance_<family>_test.go` of its construct, and name the case in the
rule's Cases field. Then write its golden alone, since
a run that also reaches `TestConformance_TableNamesEveryCorpusSpec` reads the directory before
the golden exists:

```bash
go test ./compilers/openapi -run '^TestConformance$/^<name>$' -update
go test ./compilers/openapi/e2e -run '^TestConformance$/^<name>$' -update
```

Coverage is counted per package, and the case runs in `compilers/openapi`, so it covers nothing
under `internal/`: the code you wrote there also needs a test in its own package, which the
rule's Tests field then names.
Coverage is counted per package, and the case runs in `compilers/openapi/e2e`, which holds no
code of its own, so it covers nothing: the code you wrote also needs a test in its own package,
which the rule's Tests field then names.
*Fails if missed:* `TestConformance_TableNamesEveryCorpusSpec` for a spec with no entry,
`TestConformance/<name>` for a golden that does not match, `TestLoweringSpec_CasesAreTheCorpus`
for a case no rule names, `TestConformance_EveryRegisteredFormatIsHeldToItsColumn` for a name in
Expand All @@ -97,7 +123,7 @@ A lowering rule says how one OpenAPI construct reaches the IR: the field it fill
5. **Regenerate what the change moves, and read the diff.** Other goldens may change too:

```bash
go test ./compilers/openapi -run 'TestGolden|TestConformance' -update
go test ./compilers/openapi/e2e -run 'TestGolden|TestConformance' -update
git status --short testdata/
```

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,13 +316,13 @@ Run a single test with `go test ./ir -run TestName`. Beyond that:
and review the diff:

```bash
go test ./compilers/openapi -run 'TestGolden|TestConformance' -update
go test ./compilers/openapi/e2e -run 'TestGolden|TestConformance' -update
```

- **Conformance corpus.** Each case in `testdata/conformance/openapi/` is a minimal spec that
witnesses one or more rows of the [spec capability matrix](docs/ir-spec-matrix.md). To add one,
write `<name>.yaml`, add an entry to `conformanceCases()` in
`compilers/openapi/conformance_test.go` naming its assertion, list it under the matrix rows it
`compilers/openapi/e2e/conformance_test.go` naming its assertion, list it under the matrix rows it
witnesses in `testdata/conformance/openapi/matrix.md`, then run the command above to write
`<name>.golden.json`, and name the case in the rule of the
[OpenAPI lowering specification](docs/compilers/openapi.md) it pins. The suite fails on a spec
Expand All @@ -342,7 +342,7 @@ Run a single test with `go test ./ir -run TestName`. Beyond that:
it reads the IR's declarations through the go command.
- **Fuzzing.** `make fuzz` runs every fuzz target for 10s each (`make fuzz FUZZTIME=5m` for a
longer search). One target runs on its own with, for example,
`go test ./compilers/openapi -run '^$' -fuzz '^FuzzCompile$' -fuzztime 1m`.
`go test ./compilers/openapi/e2e -run '^$' -fuzz '^FuzzCompile$' -fuzztime 1m`.

## Design docs

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ func missingParentDest(t *testing.T) string {
}

// readOnlyDirDest returns a writable file inside a read-only directory — the
// shape TestWriteCompiled_ReadOnlyDirFails pins, where the destination itself
// could be rewritten but its directory will not take the temp file.
// shape atomicfile.TestReplace_ReadOnlyDirFails pins, where the destination
// itself could be rewritten but its directory will not take the temp file.
func readOnlyDirDest(t *testing.T) string {
t.Helper()
if os.Geteuid() == 0 {
Expand Down
File renamed without changes.
70 changes: 70 additions & 0 deletions compilers/compile/diags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package compile_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/dexpace/morphic/compilers/compile"
"github.com/dexpace/morphic/ir"
)

// TestDiags_DedupesByFullIdentity pins that the collapse is by identity and
// nothing coarser: two findings differing in any one field both survive.
func TestDiags_DedupesByFullIdentity(t *testing.T) {
t.Parallel()
base := ir.Diagnostic{
Severity: ir.SeverityError,
Code: "x/y",
Message: "m",
Provenance: ir.Provenance{Source: 0, Pointer: "/p"},
}
differs := map[string]func(d *ir.Diagnostic){
"severity": func(d *ir.Diagnostic) { d.Severity = ir.SeverityInfo },
"code": func(d *ir.Diagnostic) { d.Code = "x/z" },
"message": func(d *ir.Diagnostic) { d.Message = "other" },
"source": func(d *ir.Diagnostic) { d.Provenance.Source = 1 },
"pointer": func(d *ir.Diagnostic) { d.Provenance.Pointer = "/q" },
"inferredBy": func(d *ir.Diagnostic) { d.Provenance.Inferred = "guess" },
"position line": func(d *ir.Diagnostic) { d.Provenance.Position.Line = 5 },
"position column": func(d *ir.Diagnostic) { d.Provenance.Position.Column = 5 },
"node": func(d *ir.Diagnostic) { d.Provenance.Node = "op/x" },
}
for name, mutate := range differs {
t.Run(name, func(t *testing.T) {
t.Parallel()
var d compile.Diags
d.Append(base)
d.Append(base)
require.Equal(t, 1, d.Len(), "an identical repeat collapses")

other := base
mutate(&other)
d.Append(other)
assert.Equal(t, 2, d.Len(), "a diagnostic differing in %s is a distinct finding", name)
})
}
}

func TestDiags_AppendAllPreservesFirstSeenOrder(t *testing.T) {
t.Parallel()
mk := func(code string) ir.Diagnostic {
return ir.Diagnostic{Severity: ir.SeverityError, Code: code}
}
var d compile.Diags
d.AppendAll([]ir.Diagnostic{mk("a"), mk("b"), mk("a"), mk("c")})

got := make([]string, 0, d.Len())
for _, x := range d.List() {
got = append(got, x.Code)
}
assert.Equal(t, []string{"a", "b", "c"}, got)
}

func TestDiags_ZeroValueIsUsable(t *testing.T) {
t.Parallel()
var d compile.Diags
assert.Zero(t, d.Len())
assert.Empty(t, d.List())
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,65 +146,6 @@ func TestTypes_RegistryIsTheLiveMap(t *testing.T) {
assert.Contains(t, types.String(), "types: 1")
}

// TestDiags_DedupesByFullIdentity pins that the collapse is by identity and
// nothing coarser: two findings differing in any one field both survive.
func TestDiags_DedupesByFullIdentity(t *testing.T) {
t.Parallel()
base := ir.Diagnostic{
Severity: ir.SeverityError,
Code: "x/y",
Message: "m",
Provenance: ir.Provenance{Source: 0, Pointer: "/p"},
}
differs := map[string]func(d *ir.Diagnostic){
"severity": func(d *ir.Diagnostic) { d.Severity = ir.SeverityInfo },
"code": func(d *ir.Diagnostic) { d.Code = "x/z" },
"message": func(d *ir.Diagnostic) { d.Message = "other" },
"source": func(d *ir.Diagnostic) { d.Provenance.Source = 1 },
"pointer": func(d *ir.Diagnostic) { d.Provenance.Pointer = "/q" },
"inferredBy": func(d *ir.Diagnostic) { d.Provenance.Inferred = "guess" },
"position line": func(d *ir.Diagnostic) { d.Provenance.Position.Line = 5 },
"position column": func(d *ir.Diagnostic) { d.Provenance.Position.Column = 5 },
"node": func(d *ir.Diagnostic) { d.Provenance.Node = "op/x" },
}
for name, mutate := range differs {
t.Run(name, func(t *testing.T) {
t.Parallel()
var d compile.Diags
d.Append(base)
d.Append(base)
require.Equal(t, 1, d.Len(), "an identical repeat collapses")

other := base
mutate(&other)
d.Append(other)
assert.Equal(t, 2, d.Len(), "a diagnostic differing in %s is a distinct finding", name)
})
}
}

func TestDiags_AppendAllPreservesFirstSeenOrder(t *testing.T) {
t.Parallel()
mk := func(code string) ir.Diagnostic {
return ir.Diagnostic{Severity: ir.SeverityError, Code: code}
}
var d compile.Diags
d.AppendAll([]ir.Diagnostic{mk("a"), mk("b"), mk("a"), mk("c")})

got := make([]string, 0, d.Len())
for _, x := range d.List() {
got = append(got, x.Code)
}
assert.Equal(t, []string{"a", "b", "c"}, got)
}

func TestDiags_ZeroValueIsUsable(t *testing.T) {
t.Parallel()
var d compile.Diags
assert.Zero(t, d.Len())
assert.Empty(t, d.List())
}

// TestTypes_RefusesEntriesTheRegistryCannotHold covers the boundary this type
// exists to hold. It owns the registry's invariants, so it is also the one place
// able to manufacture the malformed states irverify and pass.Validate report —
Expand Down
File renamed without changes.
4 changes: 4 additions & 0 deletions compilers/openapi/budgets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import (
"github.com/dexpace/morphic/ir"
)

// amplificationBombFixture is a document whose YAML aliases expand it to many
// times its own size, which no alias budget admits.
const amplificationBombFixture = "../../testdata/openapi/amplification_alias_bomb.yaml"

// enumSpec is a minimal document whose one component declares a string enum of
// n members. The specs these tests need differ only in how large they are, and
// building them here is what keeps a multi-megabyte fixture out of the corpus.
Expand Down
Loading
Loading