From ddd287f54b973180b1c88f0b783bee9a498d5b2b Mon Sep 17 00:00:00 2001 From: infogulch Date: Sun, 24 Mar 2024 22:11:09 -0500 Subject: [PATCH] Remove embedded structs in config; fix new test script --- .github/workflows/ci.yaml | 6 +- .gitignore | 2 +- build.go | 10 ++-- cmd/main.go | 4 +- config.go | 57 ++++++++----------- go.mod | 1 - go.sum | 2 - handlers.go | 2 +- instance.go | 14 ++--- test/go.mod | 5 ++ test/go.sum | 2 + test/test.go | 115 +++++++++++++++++++++++++++++++------- 12 files changed, 144 insertions(+), 76 deletions(-) create mode 100644 test/go.mod create mode 100644 test/go.sum diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9b1d5ad..29e3951 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,11 +18,11 @@ jobs: - name: Build run: go build -v ./... - - name: Test + - name: Run Go Tests run: go test -v ./... - - name: Run Hurl Tests - run: ./test/exec.sh + - name: Run Integration Tests + run: go run ./test - name: Build binaries for all platforms run: .github/workflows/release.sh diff --git a/.gitignore b/.gitignore index 9dfe782..f6777bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ go.work* xtemplate -xtemplate.exe +xtemplate.* caddy dist diff --git a/build.go b/build.go index a671342..f786140 100644 --- a/build.go +++ b/build.go @@ -43,7 +43,7 @@ var extensionContentTypes = map[string]string{ func (x *Instance) addStaticFileHandler(path_ string) error { // Open and stat the file - fsfile, err := x.config.Template.FS.Open(path_) + fsfile, err := x.config.FS.Open(path_) if err != nil { return fmt.Errorf("failed to open static file '%s': %w", path_, err) } @@ -115,7 +115,7 @@ func (x *Instance) addStaticFileHandler(path_ string) error { file.encodings = []encodingInfo{{encoding: encoding, path: path_, size: size, modtime: stat.ModTime()}} pattern := "GET " + identityPath - handler := staticFileHandler(x.config.Template.FS, file) + handler := staticFileHandler(x.config.FS, file) if err = catch("add handler to servemux", func() { x.router.HandleFunc(pattern, handler) }); err != nil { return err } @@ -150,7 +150,7 @@ func catch(description string, fn func()) (err error) { var routeMatcher *regexp.Regexp = regexp.MustCompile("^(GET|POST|PUT|PATCH|DELETE|SSE) (.*)$") func (x *Instance) addTemplateHandler(path_ string, minify *minify.M) error { - content, err := fs.ReadFile(x.config.Template.FS, path_) + content, err := fs.ReadFile(x.config.FS, path_) if err != nil { return fmt.Errorf("could not read template file '%s': %v", path_, err) } @@ -163,7 +163,7 @@ func (x *Instance) addTemplateHandler(path_ string, minify *minify.M) error { path_ = path.Clean("/" + path_) // parse each template file manually to have more control over its final // names in the template namespace. - newtemplates, err := parse.Parse(path_, string(content), x.config.Template.Delimiters.Left, x.config.Template.Delimiters.Right, x.funcs, buliltinsSkeleton) + newtemplates, err := parse.Parse(path_, string(content), x.config.LDelim, x.config.RDelim, x.funcs, buliltinsSkeleton) if err != nil { return fmt.Errorf("could not parse template file '%s': %v", path_, err) } @@ -189,7 +189,7 @@ func (x *Instance) addTemplateHandler(path_ string, minify *minify.M) error { continue } // strip the extension from the handled path - routePath := strings.TrimSuffix(path_, x.config.Template.TemplateExtension) + routePath := strings.TrimSuffix(path_, x.config.TemplateExtension) // files named 'index' handle requests to the directory if path.Base(routePath) == "index" { routePath = path.Dir(routePath) diff --git a/cmd/main.go b/cmd/main.go index 6ade5fd..41acf19 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -47,7 +47,7 @@ func Main(overrides ...xtemplate.ConfigOverride) { } if args.WatchTemplates { - args.Watch = append(args.Watch, args.Config.Template.Path) + args.Watch = append(args.Watch, args.Config.TemplatesDir) } if len(args.Watch) != 0 { _, err := watch.Watch(args.Watch, 200*time.Millisecond, log.WithGroup("fswatch"), func() bool { @@ -60,5 +60,5 @@ func Main(overrides ...xtemplate.ConfigOverride) { } } - log.Info("server stopped", server.Serve(args.Listen)) + log.Info("server stopped", slog.Any("exit", server.Serve(args.Listen))) } diff --git a/config.go b/config.go index 8b97e7e..0eee7ab 100644 --- a/config.go +++ b/config.go @@ -16,30 +16,21 @@ func New() (c *Config) { } type Config struct { - // Control where and how templates are loaded. - Template struct { - // The FS to load templates from. Overrides Path if not nil. - FS fs.FS `json:"-" arg:"-"` - - // The path to the templates directory. - Path string `json:"path,omitempty" arg:"-t,--template-dir"` - - // File extension to search for to find template files. Default `.html`. - TemplateExtension string `json:"template_extension,omitempty" arg:"--template-ext"` - - // The template action delimiters, default "{{" and "}}". - Delimiters struct { - Left string `json:"left,omitempty" arg:"--ldelim"` - Right string `json:"right,omitempty" arg:"--rdelim"` - } `json:"delimiters,omitempty"` - - // Minify html templates as they're loaded. - // - // > Minification is the process of removing bytes from a file (such as - // whitespace) without changing its output and therefore shrinking its - // size and speeding up transmission over the internet - Minify bool `json:"minify,omitempty" arg:"--minify"` - } `json:"template,omitempty" arg:"-"` + // The FS to load templates from. Overrides Path if not nil. + FS fs.FS `json:"-" arg:"-"` + + // The path to the templates directory. + TemplatesDir string `json:"templates_dir,omitempty" arg:"-t,--template-dir" default:"templates"` + + // File extension to search for to find template files. Default `.html`. + TemplateExtension string `json:"template_extension,omitempty" arg:"--template-ext" default:".html"` + + // The template action delimiters, default "{{" and "}}". + LDelim string `json:"left,omitempty" arg:"--ldelim" default:"{{"` + RDelim string `json:"right,omitempty" arg:"--rdelim" default:"}}"` + + // Minify html templates at load time. + Minify bool `json:"minify,omitempty" arg:"-m,--minify" default:"true"` Dot []DotConfig `json:"dot_config" arg:"-c,--dot-config,separate"` @@ -53,20 +44,20 @@ type Config struct { // FillDefaults sets default values for unset fields func (config *Config) Defaults() *Config { - if config.Template.Path == "" { - config.Template.Path = "templates" + if config.TemplatesDir == "" { + config.TemplatesDir = "templates" } - if config.Template.TemplateExtension == "" { - config.Template.TemplateExtension = ".html" + if config.TemplateExtension == "" { + config.TemplateExtension = ".html" } - if config.Template.Delimiters.Left == "" { - config.Template.Delimiters.Left = "{{" + if config.LDelim == "" { + config.LDelim = "{{" } - if config.Template.Delimiters.Right == "" { - config.Template.Delimiters.Right = "}}" + if config.RDelim == "" { + config.RDelim = "}}" } return config @@ -76,7 +67,7 @@ type ConfigOverride func(*Config) func WithTemplateFS(fs fs.FS) ConfigOverride { return func(c *Config) { - c.Template.FS = fs + c.FS = fs } } diff --git a/go.mod b/go.mod index 08f8fa3..46398c0 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,6 @@ require ( ) require ( - github.com/Hellseher/go-shellquote v0.0.0-20240324000151-06aa0e50b601 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect diff --git a/go.sum b/go.sum index 77e1514..92429b0 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/Hellseher/go-shellquote v0.0.0-20240324000151-06aa0e50b601 h1:qip+zkEa+Gwd/M0r6/mLk3Y4tcBCnzVhF1pNK8IzlQM= -github.com/Hellseher/go-shellquote v0.0.0-20240324000151-06aa0e50b601/go.mod h1:t4xAP6TrFpgvp/U7szp27rYrXXVu9yn3WhNylBvnfb8= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= diff --git a/handlers.go b/handlers.go index c023b7d..1e33b49 100644 --- a/handlers.go +++ b/handlers.go @@ -94,7 +94,7 @@ func staticFileHandler(fs fs.FS, fileinfo *fileInfo) http.HandlerFunc { // If the request provides a hash, check that it matches. If not, we don't have that file. queryhash := r.URL.Query().Get("hash") - if queryhash != "" && queryhash == fileinfo.hash { + if queryhash != "" && queryhash != fileinfo.hash { log.LogAttrs(r.Context(), slog.LevelDebug, "request for file with wrong hash query parameter", slog.String("expected", fileinfo.hash), slog.String("queryhash", queryhash)) http.NotFound(w, r) return diff --git a/instance.go b/instance.go index 7691557..fd0ed37 100644 --- a/instance.go +++ b/instance.go @@ -80,8 +80,8 @@ func (config Config) Instance() (*Instance, error) { inst.config.Logger = inst.config.Logger.With(slog.Int64("instance", inst.id)) inst.config.Logger.Info("initializing") - if inst.config.Template.FS == nil { - inst.config.Template.FS = os.DirFS(inst.config.Template.Path) + if inst.config.FS == nil { + inst.config.FS = os.DirFS(inst.config.TemplatesDir) } { @@ -95,15 +95,15 @@ func (config Config) Instance() (*Instance, error) { inst.files = make(map[string]*fileInfo) inst.router = http.NewServeMux() - inst.templates = template.New(".").Delims(inst.config.Template.Delimiters.Left, inst.config.Template.Delimiters.Right).Funcs(inst.funcs) + inst.templates = template.New(".").Delims(inst.config.LDelim, inst.config.RDelim).Funcs(inst.funcs) var m *minify.M - if config.Template.Minify { + if config.Minify { m = minify.New() m.Add("text/css", &css.Minifier{}) m.Add("image/svg+xml", &svg.Minifier{}) m.Add("text/html", &html.Minifier{ - TemplateDelims: [...]string{inst.config.Template.Delimiters.Left, inst.config.Template.Delimiters.Right}, + TemplateDelims: [...]string{inst.config.LDelim, inst.config.RDelim}, }) m.AddRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), &js.Minifier{}) } @@ -111,11 +111,11 @@ func (config Config) Instance() (*Instance, error) { inst.bufferDot = makeDot(slices.Concat([]DotConfig{{"X", instanceDotProvider{inst}}, {"Req", requestDotProvider{}}}, config.Dot, []DotConfig{{"Resp", responseDotProvider{}}})) inst.flusherDot = makeDot(slices.Concat([]DotConfig{{"X", instanceDotProvider{inst}}, {"Req", requestDotProvider{}}}, config.Dot, []DotConfig{{"Flush", flushDotProvider{}}})) - if err := fs.WalkDir(inst.config.Template.FS, ".", func(path string, d fs.DirEntry, err error) error { + if err := fs.WalkDir(inst.config.FS, ".", func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return err } - if ext := filepath.Ext(path); ext == inst.config.Template.TemplateExtension { + if ext := filepath.Ext(path); ext == inst.config.TemplateExtension { err = inst.addTemplateHandler(path, m) } else { err = inst.addStaticFileHandler(path) diff --git a/test/go.mod b/test/go.mod new file mode 100644 index 0000000..ce1ef80 --- /dev/null +++ b/test/go.mod @@ -0,0 +1,5 @@ +module github.com/infogulch/xtemplate/test + +go 1.22.1 + +require github.com/Hellseher/go-shellquote v0.0.0-20240324000151-06aa0e50b601 diff --git a/test/go.sum b/test/go.sum new file mode 100644 index 0000000..11b0a3b --- /dev/null +++ b/test/go.sum @@ -0,0 +1,2 @@ +github.com/Hellseher/go-shellquote v0.0.0-20240324000151-06aa0e50b601 h1:qip+zkEa+Gwd/M0r6/mLk3Y4tcBCnzVhF1pNK8IzlQM= +github.com/Hellseher/go-shellquote v0.0.0-20240324000151-06aa0e50b601/go.mod h1:t4xAP6TrFpgvp/U7szp27rYrXXVu9yn3WhNylBvnfb8= diff --git a/test/test.go b/test/test.go index bbe46d1..f81d80b 100644 --- a/test/test.go +++ b/test/test.go @@ -5,33 +5,91 @@ import ( "io/fs" "os" "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + "time" "github.com/Hellseher/go-shellquote" ) func main() { - log := try(os.Create("xtemplate.log"))("open log file") - log.Truncate(0) - - argStr := `--loglevel -4 -c DB:sql:sqlite:file:test.sqlite -c FS:fs:./context` - args := try(shellquote.Split(argStr))("split args") - xtemplate := exec.Command("go", append([]string{"run", "../cmd"}, args...)...) - xtemplate.Stdout = log - xtemplate.Stderr = log - try0(xtemplate.Start(), "start xtemplate") - defer try0(xtemplate.Process.Kill(), "kill xtemplate") - go func() { - try0(xtemplate.Wait(), "wait for xtemplate") - os.Exit(1) + defer func() { + if err := recover(); err != nil { + fmt.Printf("exiting because: %v\n", err) + } }() - files := try(fs.Glob(os.DirFS("."), "tests/*.hurl"))("glob files") - argStr = "--continue-on-error --test --report-html report" - args = try(shellquote.Split(argStr))("split args") - hurl := exec.Command("hurl", append(args, files...)...) - hurl.Stdout = os.Stdout - hurl.Stderr = os.Stderr - hurl.Run() + _, file, _, _ := runtime.Caller(0) + testdir := filepath.Dir(file) + + if len(os.Args) > 1 { + switch os.Args[1] { + case "hurl": + goto hurl + } + } + + // Build xtemplate + { + args := split(`go build -o xtemplate ../cmd`) + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdout + cmd.Dir = testdir + try0(cmd.Run(), "go build") + fmt.Println("~ Build ~") + } + + // Run xtemplate, wait until its ready, exit test if it fails early + { + args := split(`./xtemplate --loglevel -4 -c DB:sql:sqlite:file:test.sqlite -c FS:fs:./context`) + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = testdir + + logpath := filepath.Join(testdir, "xtemplate.log") + log := try(os.Create(logpath))("open log file") + try(log.Seek(0, 0))("seek to beginning") + defer log.Close() + cmd.Stdout = log + cmd.Stderr = log + + try0(cmd.Start(), "start xtemplate") + defer kill(cmd) + + go func() { + try0(cmd.Wait(), "wait for xtemplate") + time.Sleep(time.Second) + panic("xtemplate exited") + }() + + waitUntilFileContainsString(logpath, "starting server") + + fmt.Println("~ Run xtemplate ~") + } + +hurl: + { + files := try(fs.Glob(os.DirFS(testdir), "tests/*.hurl"))("glob files") + args := split("hurl --continue-on-error --test --report-html report") + cmd := exec.Command(args[0], append(args[1:], files...)...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Dir = testdir + defer kill(cmd) + try0(cmd.Run(), "run hurl") + fmt.Println("~ Run hurl ~") + } +} + +func split(a string) []string { return try(shellquote.Split(a))("split args") } + +func kill(c *exec.Cmd) { + err := c.Process.Kill() + if err != nil && err != os.ErrProcessDone { + panic(fmt.Sprintf("failed to kill %s: %v", c.Path, err)) + } } func try[T any](t T, err error) func(string) T { @@ -45,6 +103,21 @@ func try[T any](t T, err error) func(string) T { func try0(err error, desc string) { if err != nil { - panic(fmt.Sprintf(desc, err)) + panic(fmt.Sprintf("failed to %s: %v\n", desc, err)) } } + +func waitUntilFileContainsString(filename string, needle string) { + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + for { + if strings.Contains(string(try(os.ReadFile(filename))("read file")), needle) { + wg.Done() + break + } + time.Sleep(10 * time.Millisecond) + } + }() + wg.Wait() +}