github.com/neohugo/neohugo@v0.123.8/main_test.go (about) 1 // Copyright 2024 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package main 15 16 import ( 17 "bytes" 18 "encoding/json" 19 "fmt" 20 "io" 21 "io/fs" 22 "log" 23 "net/http" 24 "os" 25 "path/filepath" 26 "regexp" 27 "runtime" 28 "strconv" 29 "strings" 30 "testing" 31 "time" 32 33 "github.com/bep/helpers/envhelpers" 34 "github.com/neohugo/neohugo/commands" 35 "github.com/rogpeppe/go-internal/testscript" 36 ) 37 38 func TestCommands(t *testing.T) { 39 p := commonTestScriptsParam 40 p.Dir = "testscripts/commands" 41 testscript.Run(t, p) 42 } 43 44 // Tests in development can be put in "testscripts/unfinished". 45 // Also see the watch_testscripts.sh script. 46 func TestUnfinished(t *testing.T) { 47 if os.Getenv("CI") != "" { 48 t.Skip("skip unfinished tests on CI") 49 } 50 51 p := commonTestScriptsParam 52 p.Dir = "testscripts/unfinished" 53 // p.UpdateScripts = true 54 55 testscript.Run(t, p) 56 } 57 58 func TestMain(m *testing.M) { 59 os.Exit( 60 testscript.RunMain(m, map[string]func() int{ 61 // The main program. 62 "hugo": func() int { 63 err := commands.Execute(os.Args[1:]) 64 if err != nil { 65 fmt.Fprintln(os.Stderr, err) 66 return 1 67 } 68 return 0 69 }, 70 }), 71 ) 72 } 73 74 // nolint 75 var commonTestScriptsParam = testscript.Params{ 76 Setup: func(env *testscript.Env) error { 77 return testSetupFunc()(env) 78 }, 79 Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ 80 // log prints to stderr. 81 "log": func(ts *testscript.TestScript, neg bool, args []string) { 82 log.Println(args) 83 }, 84 // dostounix converts \r\n to \n. 85 "dostounix": func(ts *testscript.TestScript, neg bool, args []string) { 86 filename := ts.MkAbs(args[0]) 87 b, err := os.ReadFile(filename) 88 if err != nil { 89 ts.Fatalf("%v", err) 90 } 91 b = bytes.Replace(b, []byte("\r\n"), []byte{'\n'}, -1) 92 if err := os.WriteFile(filename, b, 0o666); err != nil { 93 ts.Fatalf("%v", err) 94 } 95 }, 96 // cat prints a file to stdout. 97 "cat": func(ts *testscript.TestScript, neg bool, args []string) { 98 filename := ts.MkAbs(args[0]) 99 b, err := os.ReadFile(filename) 100 if err != nil { 101 ts.Fatalf("%v", err) 102 } 103 fmt.Print(string(b)) 104 }, 105 // sleep sleeps for a second. 106 "sleep": func(ts *testscript.TestScript, neg bool, args []string) { 107 i := 1 108 if len(args) > 0 { 109 var err error 110 i, err = strconv.Atoi(args[0]) 111 if err != nil { 112 i = 1 113 } 114 } 115 time.Sleep(time.Duration(i) * time.Second) 116 }, 117 // ls lists a directory to stdout. 118 "ls": func(ts *testscript.TestScript, neg bool, args []string) { 119 dirname := ts.MkAbs(args[0]) 120 121 dir, err := os.Open(dirname) 122 if err != nil { 123 ts.Fatalf("%v", err) 124 } 125 fis, err := dir.Readdir(-1) 126 if err != nil { 127 ts.Fatalf("%v", err) 128 } 129 if len(fis) == 0 { 130 // To simplify empty dir checks. 131 fmt.Fprintln(ts.Stdout(), "Empty dir") 132 return 133 } 134 for _, fi := range fis { 135 fmt.Fprintf(ts.Stdout(), "%s %04o %s %s\n", fi.Mode(), fi.Mode().Perm(), fi.ModTime().Format(time.RFC3339Nano), fi.Name()) 136 } 137 }, 138 // append appends to a file with a leaading newline. 139 "append": func(ts *testscript.TestScript, neg bool, args []string) { 140 if len(args) < 2 { 141 ts.Fatalf("usage: append FILE TEXT") 142 } 143 144 filename := ts.MkAbs(args[0]) 145 words := args[1:] 146 for i, word := range words { 147 words[i] = strings.Trim(word, "\"") 148 } 149 text := strings.Join(words, " ") 150 151 _, err := os.Stat(filename) 152 if err != nil { 153 if os.IsNotExist(err) { 154 ts.Fatalf("file does not exist: %s", filename) 155 } 156 ts.Fatalf("failed to stat file: %v", err) 157 } 158 159 f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0o644) 160 if err != nil { 161 ts.Fatalf("failed to open file: %v", err) 162 } 163 defer f.Close() 164 165 _, err = f.WriteString("\n" + text) 166 if err != nil { 167 ts.Fatalf("failed to write to file: %v", err) 168 } 169 }, 170 // replace replaces a string in a file. 171 "replace": func(ts *testscript.TestScript, neg bool, args []string) { 172 if len(args) < 3 { 173 ts.Fatalf("usage: replace FILE OLD NEW") 174 } 175 filename := ts.MkAbs(args[0]) 176 oldContent, err := os.ReadFile(filename) 177 if err != nil { 178 ts.Fatalf("failed to read file %v", err) 179 } 180 newContent := bytes.Replace(oldContent, []byte(args[1]), []byte(args[2]), -1) 181 err = os.WriteFile(filename, newContent, 0o644) 182 if err != nil { 183 ts.Fatalf("failed to write file: %v", err) 184 } 185 }, 186 187 // httpget checks that a HTTP resource's body matches (if it compiles as a regexp) or contains all of the strings given as arguments. 188 "httpget": func(ts *testscript.TestScript, neg bool, args []string) { 189 if len(args) < 2 { 190 ts.Fatalf("usage: httpgrep URL STRING...") 191 } 192 193 tryget := func() error { 194 resp, err := http.Get(args[0]) 195 if err != nil { 196 return fmt.Errorf("failed to get URL %q: %v", args[0], err) 197 } 198 199 defer resp.Body.Close() 200 body, err := io.ReadAll(resp.Body) 201 if err != nil { 202 return fmt.Errorf("failed to read response body: %v", err) 203 } 204 for _, s := range args[1:] { 205 re, err := regexp.Compile(s) 206 if err == nil { 207 ok := re.Match(body) 208 if ok != !neg { 209 return fmt.Errorf("response body %q for URL %q does not match %q", body, args[0], s) 210 } 211 } else { 212 ok := bytes.Contains(body, []byte(s)) 213 if ok != !neg { 214 return fmt.Errorf("response body %q for URL %q does not contain %q", body, args[0], s) 215 } 216 } 217 } 218 return nil 219 } 220 221 // The timing on server rebuilds can be a little tricky to get right, 222 // so we try again a few times until the server is ready. 223 // There may be smarter ways to do this, but this works. 224 start := time.Now() 225 for { 226 time.Sleep(200 * time.Millisecond) 227 err := tryget() 228 if err == nil { 229 return 230 } 231 if time.Since(start) > 6*time.Second { 232 ts.Fatalf("timeout waiting for %q: %v", args[0], err) 233 } 234 } 235 }, 236 // checkfile checks that a file exists and is not empty. 237 "checkfile": func(ts *testscript.TestScript, neg bool, args []string) { 238 var readonly, exec bool 239 loop: 240 for len(args) > 0 { 241 switch args[0] { 242 case "-readonly": 243 readonly = true 244 args = args[1:] 245 case "-exec": 246 exec = true 247 args = args[1:] 248 default: 249 break loop 250 } 251 } 252 if len(args) == 0 { 253 ts.Fatalf("usage: checkfile [-readonly] [-exec] file...") 254 } 255 256 for _, filename := range args { 257 filename = ts.MkAbs(filename) 258 fi, err := os.Stat(filename) 259 ok := err == nil != neg 260 if !ok { 261 ts.Fatalf("stat %s: %v", filename, err) 262 } 263 if fi.Size() == 0 { 264 ts.Fatalf("%s is empty", filename) 265 } 266 if readonly && fi.Mode()&0o222 != 0 { 267 ts.Fatalf("%s is writable", filename) 268 } 269 if exec && runtime.GOOS != "windows" && fi.Mode()&0o111 == 0 { 270 ts.Fatalf("%s is not executable", filename) 271 } 272 } 273 }, 274 275 // checkfilecount checks that the number of files in a directory is equal to the given count. 276 "checkfilecount": func(ts *testscript.TestScript, neg bool, args []string) { 277 if len(args) != 2 { 278 ts.Fatalf("usage: checkfilecount count dir") 279 } 280 count, err := strconv.Atoi(args[0]) 281 if err != nil { 282 ts.Fatalf("invalid count: %v", err) 283 } 284 if count < 0 { 285 ts.Fatalf("count must be non-negative") 286 } 287 dir := args[1] 288 dir = ts.MkAbs(dir) 289 290 found := 0 291 292 filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { // nolint 293 if err != nil { 294 return err 295 } 296 if d.IsDir() { 297 return nil 298 } 299 found++ 300 return nil 301 }) 302 303 ok := found == count != neg 304 if !ok { 305 ts.Fatalf("found %d files, want %d", found, count) 306 } 307 }, 308 // waitServer waits for the .ready file to be created by the server. 309 "waitServer": func(ts *testscript.TestScript, neg bool, args []string) { 310 type testInfo struct { 311 BaseURLs []string 312 } 313 314 // The server will write a .ready file when ready. 315 // We wait for that. 316 readyFilename := ts.MkAbs(".ready") 317 limit := time.Now().Add(5 * time.Second) 318 for { 319 _, err := os.Stat(readyFilename) 320 if err != nil { 321 time.Sleep(500 * time.Millisecond) 322 if time.Now().After(limit) { 323 ts.Fatalf("timeout waiting for .ready file") 324 } 325 continue 326 } 327 var info testInfo 328 // Read the .ready file's JSON into info. 329 f, err := os.Open(readyFilename) 330 if err != nil { 331 ts.Fatalf("failed to open .ready file: %v", err) 332 } 333 err = json.NewDecoder(f).Decode(&info) 334 if err != nil { 335 ts.Fatalf("error decoding json: %v", err) 336 } 337 f.Close() 338 339 for i, s := range info.BaseURLs { 340 ts.Setenv(fmt.Sprintf("HUGOTEST_BASEURL_%d", i), s) 341 } 342 343 return 344 } 345 }, 346 "stopServer": func(ts *testscript.TestScript, neg bool, args []string) { 347 baseURL := ts.Getenv("HUGOTEST_BASEURL_0") 348 if baseURL == "" { 349 ts.Fatalf("HUGOTEST_BASEURL_0 not set") 350 } 351 if !strings.HasSuffix(baseURL, "/") { 352 baseURL += "/" 353 } 354 resp, err := http.Head(baseURL + "__stop") 355 if err != nil { 356 ts.Fatalf("failed to shutdown server: %v", err) 357 } 358 resp.Body.Close() 359 // Allow some time for the server to shut down. 360 time.Sleep(2 * time.Second) 361 }, 362 }, 363 } 364 365 // nolint 366 func testSetupFunc() func(env *testscript.Env) error { 367 sourceDir, _ := os.Getwd() 368 return func(env *testscript.Env) error { 369 var keyVals []string 370 keyVals = append(keyVals, "HUGO_TESTRUN", "true") 371 keyVals = append(keyVals, "HUGO_CACHEDIR", filepath.Join(env.WorkDir, "hugocache")) 372 xdghome := filepath.Join(env.WorkDir, "xdgcachehome") 373 keyVals = append(keyVals, "XDG_CACHE_HOME", xdghome) 374 home := filepath.Join(env.WorkDir, "home") 375 keyVals = append(keyVals, "HOME", home) 376 377 if runtime.GOOS == "darwin" { 378 if err := os.MkdirAll(filepath.Join(home, "Library", "Caches"), 0o777); err != nil { 379 return err 380 } 381 } 382 383 if runtime.GOOS == "linux" { 384 if err := os.MkdirAll(xdghome, 0o777); err != nil { 385 return err 386 } 387 } 388 389 keyVals = append(keyVals, "SOURCE", sourceDir) 390 391 goVersion := runtime.Version() 392 393 goVersion = strings.TrimPrefix(goVersion, "go") 394 if strings.HasPrefix(goVersion, "1.20") { 395 // Strip patch version. 396 goVersion = goVersion[:strings.LastIndex(goVersion, ".")] 397 } 398 399 keyVals = append(keyVals, "GOVERSION", goVersion) 400 envhelpers.SetEnvVars(&env.Vars, keyVals...) 401 402 return nil 403 } 404 }