Repository navigation
test: lay out tests by the code they test - #1110
Merged
Merged
Conversation
Four test helpers had two copies with the same body, and one spec was written out twice: - e2e's compilePromotionSpec and compileStreamingSpec, now one compileClean in its helpers_test.go; - annotation's jsonSchemaFromYAML and resolve's schemaFromYAML, now openapitest.SchemaFromYAML, which both packages' tests can import; - archtest's and irtest's repoRoot, now irtest.RepoRoot; - irverify's and pass's opDocNested, now irtest.NestedOverloadDoc; - the CLI's warning spec, which compile_test.go wrote out beside validate_test.go's warnSpec, which it now uses. irtest's RepoRoot takes the working directory's lookup through rootFrom, so its two failures, an unreadable working directory and a walk that reaches the filesystem root, are tested with the package's recording stub; openapitest's SchemaFromYAML is tested with its own.
Fifty-one source files had two test files, x_test.go outside the package and x_internal_test.go inside it, because one file can hold only one package clause. Where the inside half reached a few unexported functions, constants or types, it needed only to see them. In thirteen packages those files are now external tests, merged into the x_test.go beside them or renamed to it, and reach what they need through the package's export_test.go, which names each alias once: twenty files in all. The tests are the same code, qualified by their package; where an alias's exported name is taken, it says what it adds (irtest's CompareGoldenOn and FillOn take the stub testingT, atomicfile's ReplaceWithChmod the injected chmod). auth's nil-requirement test is the one rewritten rather than moved: it reached lowerSecurityRequirement to hand it a nil entry, and LowerSecurityRequirements, which callers use, takes the same entry through its list. Both catch keeping the entry as an empty option and reporting it. A file stays inside its package where it needs an unexported field or method, a generic or a value variable, which no alias carries, or a file that does, and in the six packages whose external tests would need a second API's worth of aliases: schema, operation, bind, irverify, harness and overlay.
Nine test files inside their package sat beside external tests under a plain x_test.go name, so a reader could not tell from the name which side of the package boundary a test was on: the root's budgets and helpers files, harness's order tests, and six in irverify. They take the _internal_test.go spelling the rest of the repository uses for that. The lint exclusion naming budgets_test.go, and the comments that named three of the files, follow. One of those comments is in a conformance spec, whose golden records the spec's hash, so the golden's hash moves with it; the comment now names the function it means, reverseMappings, instead of a file.
schema's tests drove one lowering through a fixture, the lowerer with loweredFor, lowerSpec, lowererOver, newRawLowerer and its document method, and declared it twice, byte for byte, once on each side of the package boundary: in-package tests cannot see helpers an external test file declares, and external tests cannot see unexported ones. It is now declared once, exported, in schema's export_test.go, which both sides compile with: schema.LowerSpec from outside the package, LowerSpec from inside it. Its fields are exported for the same reason. Every use was rewritten by its type information, not its spelling, and the package's tests and subtests are the same before and after. operation had two helpers named lowerServiceSpec doing different things: the in-package one drives LowerService through the fixture, the external one compiles the whole spec. The external one is now compileService.
Sixty-two tests sat in a file named for one source file while their names, and their subjects, were declarations of another. load's source_internal_test.go held twenty tests of hold, which external.go declares, and spellings_internal_test.go five more; pass's validate_refs tests held collectTypeIDs's, which #1102 moved to validate_graphql.go; bind's documents tests held prefix, which defs.go declares. Each moves to the test file of the file declaring its subject, created where none existed: hold's twenty-five tests to external_hold_internal_test.go, with the helpers only they use, and the rest to the bindings, models, unprepared, source, typejudge, losers, nullability, visibility, constraints, owners, code, format, resolve and amplification test files of their packages. Two helpers several load files share, diagLines and response200, go to load's helpers_internal_test.go. What stays is a test named for its package's entry point or for a construct and filed by the subject it exercises through it: the CLI's TestRun_ tests, load's TestResolve_, operation's TestContent_ and TestOperations_, and schema's TestLower_, TestSchema_, TestModel_ and TestAllOf_. Every package's tests and subtests are the same before and after.
Eight test files ran past 1,500 lines, schema's allof_test.go to 3,079, so a reader looking for one construct's tests scrolled past every other's. Each splits into topic files that keep its name as their prefix and so sort beside it: allof into nesting, required, residue, conflicts and narrowing; discriminator into beside_union, mapping and allof; union into oneof and combinators; operation's pathitemref into unmounted, collisions, references and fields, and params into querystring, schema and identity; merge's fold cases into fold_docs, fold_visibility, fold_narrowing and fold_types; and load's unprepared into external and reads, and reached's resolve tests into reached_resolve. Every test file in the repository is now under 1,250 lines. A test goes to the topic its name places it in, and a helper only the tests of one topic use goes with them. Helpers several files use now sit in their package's helpers file, which merge gains for its visibility fixtures, whose comment no longer calls the visibility cases "below". Every declaration is otherwise byte-identical, and the packages' tests and subtests are the same before and after.
A conformance case's assertion is the body TestConformance runs for it, so a failure inside it is most use reported where it happens. Ten case bodies called t.Helper, which sent every failure in them to TestConformance's loop instead: a failure planted in assertSelfURI was reported at conformance_test.go:51, and is now at the line that fails. Eleven assertions that another assertion calls did not call it, so their failures pointed inside the helper rather than at the case using it; they now do.
CONTRIBUTING's "Where a test goes" gains the three rules this change applies. A test is external and reaches what it must of its package's internals through export_test.go, staying inside the package only for an unexported field or method, a generic, a variable it assigns, or a package whose aliases would be a second API. A test is named for the function or type it tests, or for the construct or rule it pins, and asserting helpers call t.Helper except a conformance case's own body. A helper several files share has one home, in the package's helpers file or, across packages, in openapitest or irtest. The root guidance's testing summary points at the section.
Some in-package test files reach nothing that an export_test.go alias cannot carry: all of detect's, spelling's and value's, oas's code and format tests, and every nodeview and sourceindex test file except the one that reads a View's or a Measure's fields. They become external tests that reach what they need through their package's export_test.go, as the rest of the suite does. Three fixtures are used on both sides of their package's boundary, so each is declared once, exported, in export_test.go: nodeview's YAMLDoc and ComponentsDoc, and sourceindex's Decode. Each package's tests and subtests are identical before and after, compared with go test -json, and each package keeps full statement coverage.
Thirty-seven tests sat in the test file of one source file while their names test a declaration of another: value's TestNumericLiteral tests, for one, sat in value_test.go though NumericLiteral is declared in scalar.go. Each moves to the test file of the file that declares its subject: - load: the Open test to external, the Beside and Writes tests to siblings; - value: the NumericLiteral tests to scalar; - nodeview: the IsMergeKey and Repeat tests to a new nodeview_test.go; - oas: the Kinds test to types_table; - lowering: the New test to lowering; - irtest: the Fill test to fill; - census: the Report tests to a new report_test.go; - sourceindex: the Nesting tests, with the two helpers only they use, to sourceindex_test.go, outside the package as that file is; - merge: the PropIndex, WireNameIndex and MergeProperty tests to merge, and the Merger tests to fold; - ir: the TypeCommon, TypeDef and template tests to typedef, and the marker test to a new typedef_internal_test.go, since it calls the unexported marker. e2e's assertOnlyErrorDiags, which helpers_test.go also calls, moves there, and irtest's TestMain comment names golden_test.go, which declares runCompare. Every moved declaration is byte-identical, comment included, except the Nesting tests' qualified references and one comment that no longer calls the round-trip tests "above". Each package's tests and subtests are unchanged, compared with go test -json.
Comparing every test helper's body, whatever its name, finds helpers written more than once. Each one with a home now has one copy: - within a package: irverify's violationCodes and violationPaths, each also written under another name, and reader's resolved-directory fixture, written on both sides of the package boundary and now declared once in export_test.go; - in openapitest, for the compiler's packages: ParamBySource, FirstDiag, PointersOf, HolderSpec and YAMLDocument, which also replaces sourceindex's Decode fixture; - in irtest, for the IR's: DocOf, ModelOf, ScalarOf and RefsTo, the builders both ir's and irfacts' tests wrote, NestedListValue, Encoded, RefusedAsCyclic and DiagCodes. The two wrappers that asserted an error-severity code become calls to openapitest.AssertHasCode, as the rest of the suite makes, and the three helpers that took a BigVal's address become new(ir.BigVal(v)). e2e's namedID, a second copy of its componentID, becomes componentID, the name openapitest's package comment gives that per-package copy. What is still written more than once has no common home. parseFull and compileNullable compile through openapi.New, which openapitest cannot import. componentID, and typeByName over it, spell an ID the idderivations rule allows only in a test file. The remaining file, tree and AST helpers sit in packages that share no test-helper package. Each package's tests and subtests are unchanged except for the fourteen tests of the new shared helpers, and every package keeps full statement coverage.
resolve's locate tests and reader's disk_resolve tests need nothing an alias cannot carry. They stayed inside their packages only because they share fixtures with tests that must stay there. Each shared fixture is now declared once where both sides reach it: resolve's WholeModelAt and reader's VanishWorkingDirectory in export_test.go. locate reaches the walk through the At alias export_test.go already holds, and resolve's indexedDoc, which only resolve_internal_test.go uses, moves there. Each package's tests and subtests are unchanged, compared with go test -json, and each package keeps full statement coverage.
The rules under "Where a test goes" now say what the tree does: - a package's tests stay inside it when moving them out would take more than ten aliases, and a cmd program's tests are all inside it, under plain names, since a main package cannot be imported; - a function a table runs as a case's body leaves t.Helper out, as the canaries do as well as the conformance assertions; - a helper is written once, sitting in one of the test files that share it or, when new, in the package's helpers file, with openapitest and irtest holding what several packages share. A helper neither can hold is written in each package that needs it and says why, as compileNullable's copies now do too.
Planting a fault in each shared helper this change adds found three that its tests let through: - DiagCodes' test listed b, a, b, which reads the same reversed, so it could not tell report order from its reverse; it now lists c, a, b, a; - SchemaFromYAML's check that the library reports no validation error could be deleted unnoticed, since no fixture parses and then fails to validate; a required written as a scalar now does; - encoded and rootFrom could stop marking themselves helpers unnoticed; their failure tests now count the Helper calls, as compareGolden's test already does.
Comparing helpers' bodies up to the names of their parameters and locals, not only as written, finds copies the exact comparison missed: - annotation's parsedMapping, bind's tree and oas's parse each wrote openapitest.YAMLDocument again, and now call it; - e2e's and schema's preservationClaims become openapitest.PreservationClaims; - canary's releaseAnchorNames and sourceindex's clearAnchors, the load phase's anchor clearing restated for tests, become openapitest.ClearAnchors; - irverify's bigVal, a fourth helper taking a BigVal's address, becomes new(ir.BigVal(v)) as the other three did. Each package's tests and subtests are unchanged except for the tests of the two new helpers, and every package keeps full statement coverage.
The helper rule now names what a shared home does not take: a helper that belongs in openapitest or irtest but cannot live there, which is written in each package that needs it with each copy saying why; one outside what either serves, such as a file read or a stub compiler; and one whose body is a single expression, which holds no behaviour for a copy to drift from. Those are the copies a name-blind comparison of every test helper still finds.
Three external tests spelled an unexported bound's value instead of reading it, two of them with a comment that the bound was unexported: schema's discriminator ancestor depth (256) and $ref position hops (64), and operation's multipart $ref hops (16). Each is now aliased in its package's export_test.go, as resolve's MaxRefChain already is, and the tests read the alias. The multipart tests also spelled what follows from the bounds: the chain lengths, the component one past the cut and the warnings' text, for both the hop bound and schema.MaxAllOfFlattenDepth. They derive those too, so a changed bound leaves them testing its new boundary rather than failing on, or quietly passing beside, the old one. The re-entry test kept a 16-link chain that only stayed within the bound while the bound was 16. Each package's tests and subtests are unchanged. With a bound's value planted at another number the tests still pass, and with its comparison planted one off they fail.
The control for the forbidden querystring keywords called itself the control for "both cases above" and spoke of "neither keyword", but three cases precede it, style, explode and allowReserved, and it checks all three.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Test files were hard to navigate after #1109's move, for reasons a scan of the whole suite at
main(ff3fff4) pinned down:x_test.gooutside the package andx_internal_test.goinside it;schema/allof_test.goto 3,078;schema's lowering fixture among them, byte for byte on both sides of the package boundary.This PR fixes each of them and writes the rules down. Every change is to test code, except the shared helpers added to the two test-helper packages,
ir/irtestandcompilers/openapi/internal/openapitest.Internals through
export_test.go. 38 in-package test files in 19 packages become external tests. Each takes its source'sx_test.go, merged into that file where one already existed, and reaches what it needs through the package'sexport_test.go. The tests are the same code, qualified by their package. Where an alias's exported name is taken, its name says what it adds, asirtest.CompareGoldenOn(takes the stubtestingT) andatomicfile.ReplaceWithChmoddo. A fixture both sides of a package use is declared once there, as nodeview'sYAMLDocand reader'sResolvedDirare. auth's nil-requirement test is rewritten instead: it now goes throughLowerSecurityRequirements, the exported function, and still catches the two faults the original caught._internalnames. Nine in-package test files that sat beside external tests under plain names take the_internal_test.gospelling. The lint exclusion and three comments naming them follow.One
schemafixture. TheLowererfixture is declared once, exported, inschema/export_test.go, and both sides of the package use it. All 459 uses were rewritten from type information, not spelling.operation's externallowerServiceSpec, which compiles a whole spec, becomescompileService, so it no longer shares a name with the in-package helper that drivesLowerService.Shared helpers. Comparing every test helper's body, whatever its own name and the names of its parameters and locals, found the copies. Each helper with a home now has one:
compileClean, which replaces two compile helpers; irverify'sviolationCodesandviolationPaths; reader's resolved-directory fixture; and e2e'scomponentID, which it also wrote asnamedID;openapitest, for the compiler's packages:SchemaFromYAML,ParamBySource,FirstDiag,PointersOf,PreservationClaims,HolderSpec,ClearAnchorsandYAMLDocument, which five packages had each written;irtest, for the IR's:RepoRoot,NestedOverloadDoc,NestedListValue,DocOf,ModelOf,ScalarOf,RefsTo,Encoded,RefusedAsCyclicandDiagCodes.Two wrappers of
openapitest.AssertHasCodebecome calls to it, four helpers that took aBigVal's address becomenew(ir.BigVal(v)), and the CLI tests stop writing outwarnSpeca second time.RepoRootandEncodedtake their failures through an internal function, so those are tested too.Restated bounds. Six tests in
schemaandoperationspelled a bound's value, or what follows from it, instead of reading the bound, two of them with a comment that the bound was unexported. Each bound is now aliased in its package'sexport_test.go, asresolve.MaxRefChainalready was, and the tests derive their chain lengths, components and expected messages from it, so a changed bound leaves them testing its new boundary.Misfiled tests. 99 tests move to the test file of the file declaring their subject, with any helper only they use. Among them: load's 25
holdtests go toexternal_hold_internal_test.go, value'sNumericLiteraltests toscalar_test.go,pass'scollectTypeIDstests tovalidate_graphql_test.go, and ir'sTypeCommonand template tests totypedef_test.go.Large files split by topic. The eight files over 1,500 lines split into
x_<topic>_test.gofiles, which sort beside their source. For example,allofbecomesallof_nesting,allof_required,allof_residue,allof_conflictsandallof_narrowing. Every test file is now under 1,250 lines.t.Helperin conformance assertions. Ten conformance case bodies calledt.Helper, so their failures were reported atTestConformance's loop line; they no longer do. Eleven sub-assertions called by other assertions now do.CONTRIBUTING gains three rules under "Where a test goes", each saying what fails when it is missed:
cmdprogram's are all inside it, since amainpackage cannot be imported;t.Helperout;The root guidance's testing summary points at the section.
Deliberately left:
schema,operation,bind,irverify,harnessoroverlay, whose tests would need 16 to 51 aliases each, enough to make a second API. Two of the 43 are new, innodeviewandir'stypedef: tests moved to the file declaring their subject, beside tests of that file that must stay inside.load,merge,annotationandscanwould need 14 to 106 aliases each.TestRun_tests,load'sTestResolve_,operation'sTestContent_andTestOperations_, andschema'sTestLower_. CONTRIBUTING names that convention.parseFullandcompileNullablecompile throughopenapi.New, whichopenapitestcannot import, andcomponentIDspells an ID theidderivationsrule allows only in a test file;openapitestandirtestserve: file reads and writes, AST predicates, package loading and stub compilers;ir.TypeRef{Target: id}ortypeByName, which hold no behaviour for a copy to drift from.Test plan
go test -list '.*' ./...: 3,478 onmain, 3,501 here. Twenty-three are new, the tests of the shared helpers. One is renamed:TestLowerSecurityRequirement_Nilis nowTestLowerSecurityRequirements_DropsANilEntrySilently.go test -json -race ./...: 12,271 passing tests and subtests onmain, fuzz seeds included, against 12,294 here, differing only in those 24 names. FourTestLoad_AFileURIReadsTheFileItNamessubtests are named after a random temp directory, so they differ in name only.go run ./cmd/morphic-covergateover that run's profile: all 15,233 statements covered. The 64 new ones are the shared helpers'.main's, compared withgo test -jsonbefore and after.assertSelfURIwas reported atconformance_test.go:51before thet.Helperchange and is now reported at the line that fails;DiagCodes' test used a list that reads the same reversed, no fixture reachedSchemaFromYAML's validation check, and nothing countedencoded's androotFrom'sHelpercalls.make fmt build vet lint lint-planted fuzz bench-smokeexit 0, with golangci-lint at the pinned version andFUZZTIME=2s.go test -shuffle=on ./...passes in two runs, so no moved test leans on another's order.