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  }