| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | "amadan.net/rastrillo/fiesta" |
| 11 | "amadan.net/rastrillo/fiesta/internal/build" |
| 12 | ) |
| 13 | |
| 14 | // fiestaBuild runs `fiesta build -out out` as main would, and returns |
| 15 | // its error. |
| 16 | func fiestaBuild(t *testing.T, out string) error { |
| 17 | t.Helper() |
| 18 | var stderr bytes.Buffer |
| 19 | return run([]string{"build", "-out", out}, &stderr) |
| 20 | } |
| 21 | |
| 22 | // TestBuildRefusesADirectoryItDoesNotOwn: the version directory under |
| 23 | // -out is refused, untouched, when it holds anything and no stamp; |
| 24 | // taken, emptied and rewritten when it holds the stamp; and created |
| 25 | // when it is absent. What it writes is the dist, file for file. |
| 26 | func TestBuildRefusesADirectoryItDoesNotOwn(t *testing.T) { |
| 27 | out := t.TempDir() |
| 28 | root := filepath.Join(out, fiesta.Version) |
| 29 | stray := filepath.Join(root, "notes.txt") |
| 30 | if err := os.MkdirAll(root, 0o755); err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | if err := os.WriteFile(stray, []byte("mine"), 0o644); err != nil { |
| 34 | t.Fatal(err) |
| 35 | } |
| 36 | |
| 37 | err := fiestaBuild(t, out) |
| 38 | if err == nil { |
| 39 | t.Fatalf("fiesta build took %s, which holds a file it did not write and no %s stamp", root, stampName) |
| 40 | } |
| 41 | if !strings.Contains(err.Error(), stampName) { |
| 42 | t.Errorf("the refusal does not name the %s stamp: %v", stampName, err) |
| 43 | } |
| 44 | if b, err := os.ReadFile(stray); err != nil || string(b) != "mine" { |
| 45 | t.Errorf("the refused directory was touched: %s is %q, %v", stray, b, err) |
| 46 | } |
| 47 | if entries, _ := os.ReadDir(root); len(entries) != 1 { |
| 48 | t.Errorf("the refused directory holds %d entries, want the one stray file", len(entries)) |
| 49 | } |
| 50 | |
| 51 | if err := os.WriteFile(filepath.Join(root, stampName), []byte("This directory is written by fiesta build, an earlier run of it.\n"), 0o644); err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | if err := fiestaBuild(t, out); err != nil { |
| 55 | t.Fatalf("fiesta build refused a directory holding its own stamp: %v", err) |
| 56 | } |
| 57 | if _, err := os.Stat(stray); !os.IsNotExist(err) { |
| 58 | t.Errorf("the stray file survived a build into a directory the command owns: %v", err) |
| 59 | } |
| 60 | if b, err := os.ReadFile(filepath.Join(root, stampName)); err != nil || string(b) != stampBody("fiesta build") { |
| 61 | t.Errorf("the stamp was not rewritten: %q, %v", b, err) |
| 62 | } |
| 63 | |
| 64 | dist, err := build.Dist(fiesta.FS(), fiesta.Version) |
| 65 | if err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | if len(dist) == 0 { |
| 69 | t.Fatal("the dist is empty") |
| 70 | } |
| 71 | for name, want := range dist { |
| 72 | got, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(name))) |
| 73 | if err != nil { |
| 74 | t.Errorf("the build did not write %s: %v", name, err) |
| 75 | continue |
| 76 | } |
| 77 | if !bytes.Equal(got, want) { |
| 78 | t.Errorf("%s on disk differs from the dist", name) |
| 79 | } |
| 80 | } |
| 81 | var written int |
| 82 | err = filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error { |
| 83 | if err == nil && !d.IsDir() && filepath.Base(p) != stampName { |
| 84 | written++ |
| 85 | } |
| 86 | return err |
| 87 | }) |
| 88 | if err != nil { |
| 89 | t.Fatal(err) |
| 90 | } |
| 91 | if written != len(dist) { |
| 92 | t.Errorf("%s holds %d files besides the stamp, the dist has %d", root, written, len(dist)) |
| 93 | } |
| 94 | |
| 95 | fresh := filepath.Join(t.TempDir(), "site", "fiesta") |
| 96 | if err := fiestaBuild(t, fresh); err != nil { |
| 97 | t.Fatalf("fiesta build could not create %s: %v", fresh, err) |
| 98 | } |
| 99 | if _, err := os.Stat(filepath.Join(fresh, fiesta.Version, "fiesta.css")); err != nil { |
| 100 | t.Errorf("a build into an absent directory wrote no fiesta.css: %v", err) |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | // TestBuildNeedsItsFlag: no -out is an error that says so, as is a |
| 105 | // command the binary does not have. |
| 106 | func TestBuildNeedsItsFlag(t *testing.T) { |
| 107 | var stderr bytes.Buffer |
| 108 | if err := run([]string{"build"}, &stderr); err == nil || !strings.Contains(err.Error(), "-out") { |
| 109 | t.Errorf("build with no -out: %v", err) |
| 110 | } |
| 111 | if err := run([]string{"paint"}, &stderr); err == nil || !strings.Contains(err.Error(), "paint") { |
| 112 | t.Errorf("an unknown command: %v", err) |
| 113 | } |
| 114 | if err := run(nil, &stderr); err == nil { |
| 115 | t.Error("no command at all passed") |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | // fiestaGallery runs `fiesta gallery -out out` as main would, and |
| 120 | // returns its error. |
| 121 | func fiestaGallery(t *testing.T, out string) error { |
| 122 | t.Helper() |
| 123 | var stderr bytes.Buffer |
| 124 | return run([]string{"gallery", "-out", out}, &stderr) |
| 125 | } |
| 126 | |
| 127 | // TestGalleryRefusesARelease: `fiesta gallery -out dist/<version>`, |
| 128 | // pointed at the directory a build wrote, is refused untouched. The |
| 129 | // stamp there is fiesta build's, and a stamp says whose directory it |
| 130 | // is, not merely that some command once wrote it. |
| 131 | func TestGalleryRefusesARelease(t *testing.T) { |
| 132 | out := t.TempDir() |
| 133 | if err := fiestaBuild(t, out); err != nil { |
| 134 | t.Fatal(err) |
| 135 | } |
| 136 | release := filepath.Join(out, fiesta.Version) |
| 137 | err := fiestaGallery(t, release) |
| 138 | if err == nil { |
| 139 | t.Fatalf("fiesta gallery emptied %s, a release fiesta build wrote", release) |
| 140 | } |
| 141 | for _, want := range []string{release, "fiesta build"} { |
| 142 | if !strings.Contains(err.Error(), want) { |
| 143 | t.Errorf("the refusal does not say %q: %v", want, err) |
| 144 | } |
| 145 | } |
| 146 | if _, err := os.Stat(filepath.Join(release, "fiesta.css")); err != nil { |
| 147 | t.Errorf("the release lost fiesta.css: %v", err) |
| 148 | } |
| 149 | if b, err := os.ReadFile(filepath.Join(release, stampName)); err != nil || string(b) != stampBody("fiesta build") { |
| 150 | t.Errorf("the release's stamp was rewritten: %q, %v", b, err) |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | // TestGalleryRefusesADirectoryHoldingARelease: a gallery directory |
| 155 | // that a build has since written a release inside is refused by the |
| 156 | // next gallery run, naming the release's stamp, and the release is |
| 157 | // left as it was. A stamp below the directory is something else's |
| 158 | // output, whichever command's it is. |
| 159 | func TestGalleryRefusesADirectoryHoldingARelease(t *testing.T) { |
| 160 | out := t.TempDir() |
| 161 | if err := fiestaGallery(t, out); err != nil { |
| 162 | t.Fatal(err) |
| 163 | } |
| 164 | if err := fiestaBuild(t, out); err != nil { |
| 165 | t.Fatal(err) |
| 166 | } |
| 167 | release := filepath.Join(out, fiesta.Version) |
| 168 | err := fiestaGallery(t, out) |
| 169 | if err == nil { |
| 170 | t.Fatalf("fiesta gallery emptied %s, which held the release %s", out, release) |
| 171 | } |
| 172 | if !strings.Contains(err.Error(), filepath.Join(release, stampName)) { |
| 173 | t.Errorf("the refusal does not name %s: %v", filepath.Join(release, stampName), err) |
| 174 | } |
| 175 | if _, err := os.Stat(filepath.Join(release, "fiesta.css")); err != nil { |
| 176 | t.Errorf("the release lost fiesta.css: %v", err) |
| 177 | } |
| 178 | if _, err := os.Stat(filepath.Join(out, "index.html")); err != nil { |
| 179 | t.Errorf("the refused gallery directory was touched: %v", err) |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | // TestGalleryRefusesALinkedDirectoryHoldingARelease: the reviewer's |
| 184 | // route round the check. A symbolic link to a gallery directory that |
| 185 | // a release has been built inside is refused like the directory |
| 186 | // itself, naming the release's stamp by its real path, and the release |
| 187 | // is left as it was. The link is resolved before anything is read, |
| 188 | // walked or emptied, so every step sees the same real directory. |
| 189 | func TestGalleryRefusesALinkedDirectoryHoldingARelease(t *testing.T) { |
| 190 | base := t.TempDir() |
| 191 | site := filepath.Join(base, "site") |
| 192 | if err := fiestaGallery(t, site); err != nil { |
| 193 | t.Fatal(err) |
| 194 | } |
| 195 | if err := fiestaBuild(t, site); err != nil { |
| 196 | t.Fatal(err) |
| 197 | } |
| 198 | link := filepath.Join(base, "link") |
| 199 | if err := os.Symlink(site, link); err != nil { |
| 200 | t.Fatal(err) |
| 201 | } |
| 202 | release := filepath.Join(site, fiesta.Version) |
| 203 | err := fiestaGallery(t, link) |
| 204 | if err == nil { |
| 205 | t.Fatalf("fiesta gallery emptied %s through the link %s, and with it the release %s", site, link, release) |
| 206 | } |
| 207 | real, err2 := filepath.EvalSymlinks(filepath.Join(release, stampName)) |
| 208 | if err2 != nil { |
| 209 | t.Fatalf("the release's stamp is gone: %v", err2) |
| 210 | } |
| 211 | if !strings.Contains(err.Error(), real) { |
| 212 | t.Errorf("the refusal does not name %s: %v", real, err) |
| 213 | } |
| 214 | if _, err := os.Stat(filepath.Join(release, "fiesta.css")); err != nil { |
| 215 | t.Errorf("the release lost fiesta.css: %v", err) |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | // TestGalleryRefusesALinkInsideItsDirectory: a symbolic link inside a |
| 220 | // directory the command owns is refused by name, whatever it points |
| 221 | // at, and what it points at is left alone. The commands never write a |
| 222 | // link, so one inside their output is not theirs; refusing every link |
| 223 | // is the one rule that both never follows a link out of the tree and |
| 224 | // never empties a foreign-stamped directory through one. |
| 225 | func TestGalleryRefusesALinkInsideItsDirectory(t *testing.T) { |
| 226 | base := t.TempDir() |
| 227 | site := filepath.Join(base, "site") |
| 228 | if err := fiestaGallery(t, site); err != nil { |
| 229 | t.Fatal(err) |
| 230 | } |
| 231 | dist := filepath.Join(base, "dist") |
| 232 | if err := fiestaBuild(t, dist); err != nil { |
| 233 | t.Fatal(err) |
| 234 | } |
| 235 | release := filepath.Join(dist, fiesta.Version) |
| 236 | link := filepath.Join(site, "release") |
| 237 | if err := os.Symlink(release, link); err != nil { |
| 238 | t.Fatal(err) |
| 239 | } |
| 240 | err := fiestaGallery(t, site) |
| 241 | if err == nil { |
| 242 | t.Fatalf("fiesta gallery emptied %s with the link %s in it", site, link) |
| 243 | } |
| 244 | if !strings.Contains(err.Error(), link) { |
| 245 | t.Errorf("the refusal does not name the link %s: %v", link, err) |
| 246 | } |
| 247 | if _, err := os.Lstat(link); err != nil { |
| 248 | t.Errorf("the link was removed: %v", err) |
| 249 | } |
| 250 | if _, err := os.Stat(filepath.Join(release, "fiesta.css")); err != nil { |
| 251 | t.Errorf("the release behind the link lost fiesta.css: %v", err) |
| 252 | } |
| 253 | if _, err := os.Stat(filepath.Join(site, "index.html")); err != nil { |
| 254 | t.Errorf("the refused directory was touched: %v", err) |
| 255 | } |
| 256 | } |
| 257 | |