| 1 | package check |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | "amadan.net/rastrillo/fiesta" |
| 11 | "amadan.net/rastrillo/fiesta/example" |
| 12 | ) |
| 13 | |
| 14 | // The seeded fixtures: the pages rastrillo's browser drives built by |
| 15 | // hand at the seed commit, written out once under testdata/fixtures/ |
| 16 | // and listed in index.json by the file, builder and case they came |
| 17 | // from. A drive ported here reads its page through fixture rather |
| 18 | // than retyping it, so the page it drives is the page the rastrillo |
| 19 | // drive drove. |
| 20 | |
| 21 | // fixtureRow is one entry of testdata/fixtures/index.json. A row |
| 22 | // names either a path under testdata/fixtures/ or an example state |
| 23 | // that carries the same markup; the two reference rows have no path. |
| 24 | type fixtureRow struct { |
| 25 | File string `json:"file"` |
| 26 | Builder string `json:"builder"` |
| 27 | Case string `json:"case"` |
| 28 | Path string `json:"path"` |
| 29 | Kind string `json:"kind"` |
| 30 | Request string `json:"request"` |
| 31 | Example string `json:"example"` |
| 32 | Note string `json:"note"` |
| 33 | } |
| 34 | |
| 35 | // fixtureDir is where the index and the files live, under check/. |
| 36 | const fixtureDir = "testdata/fixtures" |
| 37 | |
| 38 | // fixtureIndex reads index.json. |
| 39 | func fixtureIndex(t *testing.T) []fixtureRow { |
| 40 | t.Helper() |
| 41 | b, err := os.ReadFile(filepath.Join(fixtureDir, "index.json")) |
| 42 | if err != nil { |
| 43 | t.Fatal(err) |
| 44 | } |
| 45 | var rows []fixtureRow |
| 46 | if err := json.Unmarshal(b, &rows); err != nil { |
| 47 | t.Fatalf("%s/index.json: %v", fixtureDir, err) |
| 48 | } |
| 49 | return rows |
| 50 | } |
| 51 | |
| 52 | // fixture returns the contents of the fixture file the index lists for |
| 53 | // the {file, builder, case} row: the body or the whole document, as |
| 54 | // the row's kind says, exactly as the rastrillo builder wrote it. It fails |
| 55 | // the test when the row is missing, when the row names an example |
| 56 | // rather than a path, or when the file is absent. |
| 57 | func fixture(t *testing.T, file, builder, kase string) string { |
| 58 | t.Helper() |
| 59 | for _, r := range fixtureIndex(t) { |
| 60 | if r.File != file || r.Builder != builder || r.Case != kase { |
| 61 | continue |
| 62 | } |
| 63 | if r.Path == "" { |
| 64 | t.Fatalf("fixture %s %s %q has no path (it names example %q); read it through the example package", file, builder, kase, r.Example) |
| 65 | } |
| 66 | b, err := os.ReadFile(filepath.Join(fixtureDir, filepath.FromSlash(r.Path))) |
| 67 | if err != nil { |
| 68 | t.Fatalf("fixture %s %s %q: %v", file, builder, kase, err) |
| 69 | } |
| 70 | return string(b) |
| 71 | } |
| 72 | t.Fatalf("fixture %s %s %q is not in %s/index.json", file, builder, kase, fixtureDir) |
| 73 | return "" |
| 74 | } |
| 75 | |
| 76 | // fixtureExample returns the markup of the example state the index |
| 77 | // names for the {file, builder, case} row: the two rows whose rastrillo |
| 78 | // page was a styleguide sample carry "example/state" instead of a |
| 79 | // path, and read through the example package. It fails the test when |
| 80 | // the row is missing, when the row names a path rather than an |
| 81 | // example, or when the state cannot be read. |
| 82 | func fixtureExample(t *testing.T, file, builder, kase string) string { |
| 83 | t.Helper() |
| 84 | for _, r := range fixtureIndex(t) { |
| 85 | if r.File != file || r.Builder != builder || r.Case != kase { |
| 86 | continue |
| 87 | } |
| 88 | if r.Example == "" { |
| 89 | t.Fatalf("fixture %s %s %q names a path (%s), not an example; read it through fixture", file, builder, kase, r.Path) |
| 90 | } |
| 91 | name, state, ok := strings.Cut(r.Example, "/") |
| 92 | if !ok { |
| 93 | t.Fatalf("fixture %s %s %q names example %q, which is not example/state", file, builder, kase, r.Example) |
| 94 | } |
| 95 | markup, err := example.Markup(fiesta.FS(), name, state) |
| 96 | if err != nil { |
| 97 | t.Fatalf("fixture %s %s %q: %v", file, builder, kase, err) |
| 98 | } |
| 99 | return markup |
| 100 | } |
| 101 | t.Fatalf("fixture %s %s %q is not in %s/index.json", file, builder, kase, fixtureDir) |
| 102 | return "" |
| 103 | } |
| 104 | |
| 105 | // TestEveryFixtureRowResolves: every row that names a path names a |
| 106 | // file that exists and is not empty, and the rows without a path are |
| 107 | // the example references, which name an example. The count is floored |
| 108 | // at 100 so an index that lost its rows could not pass by listing |
| 109 | // nothing. |
| 110 | func TestEveryFixtureRowResolves(t *testing.T) { |
| 111 | rows := fixtureIndex(t) |
| 112 | if len(rows) < 100 { |
| 113 | t.Fatalf("%s/index.json lists %d rows; the seed wrote 137", fixtureDir, len(rows)) |
| 114 | } |
| 115 | seen := map[[3]string]bool{} |
| 116 | for _, r := range rows { |
| 117 | key := [3]string{r.File, r.Builder, r.Case} |
| 118 | if seen[key] { |
| 119 | t.Errorf("%s %s %q is listed twice", r.File, r.Builder, r.Case) |
| 120 | } |
| 121 | seen[key] = true |
| 122 | if r.Path == "" { |
| 123 | if r.Example == "" { |
| 124 | t.Errorf("%s %s %q has neither a path nor an example", r.File, r.Builder, r.Case) |
| 125 | } |
| 126 | continue |
| 127 | } |
| 128 | info, err := os.Stat(filepath.Join(fixtureDir, filepath.FromSlash(r.Path))) |
| 129 | if err != nil { |
| 130 | t.Errorf("%s %s %q: %v", r.File, r.Builder, r.Case, err) |
| 131 | continue |
| 132 | } |
| 133 | if info.Size() == 0 { |
| 134 | t.Errorf("%s %s %q: %s is empty", r.File, r.Builder, r.Case, r.Path) |
| 135 | } |
| 136 | if got := fixture(t, r.File, r.Builder, r.Case); int64(len(got)) != info.Size() { |
| 137 | t.Errorf("%s %s %q: fixture returned %d bytes of a %d-byte file", r.File, r.Builder, r.Case, len(got), info.Size()) |
| 138 | } |
| 139 | } |
| 140 | } |
| 141 | |