102 lines
5.1 KiB
Go
102 lines
5.1 KiB
Go
package ingest
|
|
|
|
import "testing"
|
|
|
|
// A document that does not identify itself as a build report is REFUSED, and the reason it has to be
|
|
// is what the test name says: `{}` decodes into a report claiming a whole book with no holes,
|
|
// nothing removed and no drift — the most reassuring thing this type can say, produced for free by
|
|
// any process at the engine's path that is not the engine.
|
|
//
|
|
// Mutation caught: dropping the `build_version` presence check in DecodeBuild.
|
|
func TestADocumentWithNoBuildVersionIsNotAReportEvenThoughItDecodesIntoAReassuringOne(t *testing.T) {
|
|
for _, body := range []string{`{}`, `{"complete":true,"total_units":14}`, `{"build_version":""}`} {
|
|
if _, err := DecodeBuild([]byte(body)); err == nil {
|
|
t.Errorf("DecodeBuild(%s) was accepted; a report with no build_version is not a report", body)
|
|
}
|
|
}
|
|
if _, err := DecodeBuild([]byte(`{"build_version":"tm-build-v1"}`)); err != nil {
|
|
t.Errorf("a report that names its shape was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// The value of `build_version` is NOT gated, unlike the manifest's, and the asymmetry is deliberate:
|
|
// a build report is a receipt for a file that already exists on disk, so refusing to read it over a
|
|
// version string would throw away a completed artifact. Pinned because the two decoders sit next to
|
|
// each other and the manifest's rule is the one a reader reaches for first.
|
|
//
|
|
// Mutation caught: adding a `doc.Version != KnownBuildVersion` refusal to DecodeBuild.
|
|
func TestAnUnknownBuildShapeIsStillReadBecauseTheFileAlreadyExists(t *testing.T) {
|
|
rep, err := DecodeBuild([]byte(`{"build_version":"tm-build-v9","complete":true,"book_id":"bk_1"}`))
|
|
if err != nil {
|
|
t.Fatalf("a report from a later engine was refused: %v", err)
|
|
}
|
|
if rep.Version != "tm-build-v9" || !rep.Complete || rep.BookID != "bk_1" {
|
|
t.Fatalf("the report was not read as it stands: %+v", rep)
|
|
}
|
|
}
|
|
|
|
// Every field the door acts on is read, and the allowlist does not quietly drop one. Written as a
|
|
// whole document rather than field by field because the failure this catches is a RENAME on the
|
|
// engine's side, which leaves a zero value that reads as "nothing to report".
|
|
//
|
|
// Mutation caught: removing any of the json tags below from wireBuild.
|
|
func TestTheWholeOfWhatTheDoorActsOnIsRead(t *testing.T) {
|
|
rep, err := DecodeBuild([]byte(`{
|
|
"build_version":"tm-build-v1","book_id":"bk_1","total_units":14,
|
|
"pending_units":1,"withheld_units":2,"incomplete_units":3,"stale_units":4,
|
|
"stale_unknown":true,"ghost_rows":5,"config_drift":true,
|
|
"removed_files":["/a"],"stale_copies":["/b"],"complete":false}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := BuildReport{Version: "tm-build-v1", BookID: "bk_1", Complete: false,
|
|
PendingUnits: 1, WithheldUnits: 2, IncompleteUnits: 3, StaleUnits: 4, GhostRows: 5,
|
|
StaleUnknown: true, ConfigDrift: true,
|
|
RemovedFiles: []string{"/a"}, StaleCopies: []string{"/b"}}
|
|
if rep.Version != want.Version || rep.BookID != want.BookID || rep.Complete != want.Complete ||
|
|
rep.PendingUnits != want.PendingUnits || rep.WithheldUnits != want.WithheldUnits ||
|
|
rep.IncompleteUnits != want.IncompleteUnits || rep.StaleUnits != want.StaleUnits ||
|
|
rep.GhostRows != want.GhostRows || rep.StaleUnknown != want.StaleUnknown ||
|
|
rep.ConfigDrift != want.ConfigDrift || len(rep.RemovedFiles) != 1 || len(rep.StaleCopies) != 1 {
|
|
t.Fatalf("read %+v, want %+v", rep, want)
|
|
}
|
|
}
|
|
|
|
// The door's ratified second duty is to check the REPORT and not only the file (D39.175 п.2,
|
|
// D39.178 п.3), and NeedsOperator is the one place that decides what is worth saying. A clean report
|
|
// says nothing — a line on every export would be a line an operator filters.
|
|
//
|
|
// Mutation caught: dropping any of the four disjuncts, or making it always true.
|
|
func TestOnlyAReportWithSomethingForTheOperatorAsksForALine(t *testing.T) {
|
|
if (BuildReport{Complete: true}).NeedsOperator() {
|
|
t.Error("a clean report asked for an operator's attention")
|
|
}
|
|
for name, rep := range map[string]BuildReport{
|
|
"config drift": {ConfigDrift: true},
|
|
"stale unknown": {StaleUnknown: true},
|
|
"removed files": {RemovedFiles: []string{"/a"}},
|
|
"stale copies": {StaleCopies: []string{"/a"}},
|
|
} {
|
|
if !rep.NeedsOperator() {
|
|
t.Errorf("%s did not reach the operator", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The engine withdrew «nothing was written» as a promise of the refusal band and this side used to
|
|
// repeat it (unified backlog row 246). The tail of that row is this constant: the engine's
|
|
// `exitBookIncomplete` had no name here at all, so `build`'s own refusal class arrived as an
|
|
// unrecognised number.
|
|
//
|
|
// Mutation caught: changing the constant, or letting it fall outside the band.
|
|
func TestTheBuildRefusalHasItsOwnNumberAndItIsInsideTheBand(t *testing.T) {
|
|
if ExitBookIncomplete != 16 {
|
|
t.Errorf("ExitBookIncomplete = %d, want 16 (backend/cmd/tmctl/main.go exitBookIncomplete)", ExitBookIncomplete)
|
|
}
|
|
if !Refused(ExitBookIncomplete) {
|
|
t.Error("the build refusal is outside the refusal band, so a caller would read it as a failure")
|
|
}
|
|
if OutcomeOf(ExitBookIncomplete) != OutcomeRefused {
|
|
t.Errorf("OutcomeOf(16) = %q, want %q", OutcomeOf(ExitBookIncomplete), OutcomeRefused)
|
|
}
|
|
}
|