diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Makefile | 80 | ||||
-rw-r--r-- | README.adoc | 13 | ||||
-rw-r--r-- | go.mod | 8 | ||||
-rw-r--r-- | go.sum | 4 | ||||
-rw-r--r-- | src/glaze.go | 277 | ||||
-rw-r--r-- | src/lib.go | 187 | ||||
-rw-r--r-- | src/main.go (renamed from src/cmd/main.go) | 2 | ||||
-rwxr-xr-x | tests/cli-opts.sh | 46 | ||||
-rw-r--r-- | tests/glaze.go | 36 | ||||
-rwxr-xr-x | tests/integration.sh | 73 | ||||
-rw-r--r-- | tests/lib.sh | 119 | ||||
-rw-r--r-- | tests/lib_test.go | 13 | ||||
-rw-r--r-- | tests/main.go | 7 | ||||
-rw-r--r-- | tests/resources/index.html | 4 | ||||
l--------- | tests/resources/link.txt | 1 | ||||
-rw-r--r-- | tests/resources/original.txt | 10 |
17 files changed, 648 insertions, 235 deletions
@@ -1,2 +1,5 @@ /*.bin +/src/*.a +/src/*.bin +/tests/*.a /tests/*.bin @@ -8,6 +8,7 @@ LANGUAGES = en PREFIX = /usr BINDIR = $(PREFIX)/bin LIBDIR = $(PREFIX)/lib +GOLIBDIR = $(LIBDIR)/go INCLUDEDIR = $(PREFIX)/include SRCDIR = $(PREFIX)/src/$(NAME) SHAREDIR = $(PREFIX)/share @@ -21,7 +22,7 @@ LDLIBS = .SUFFIXES: -.SUFFIXES: .go .bin +.SUFFIXES: .go .a .bin .bin-check @@ -29,17 +30,25 @@ all: include deps.mk +objects = \ + src/$(NAME).a \ + src/main.a \ + tests/$(NAME).a \ + tests/main.a \ + sources = \ - src/lib.go \ - src/cmd/main.go \ + src/$(NAME).go \ + src/main.go \ derived-assets = \ + $(objects) \ + src/main.bin \ + tests/main.bin \ $(NAME).bin \ - tests/lib_test.bin \ side-assets = \ - glaze.socket \ + $(NAME).socket \ @@ -48,22 +57,46 @@ side-assets = \ all: $(derived-assets) -$(NAME).bin: src/lib.go src/cmd/main.go Makefile - go build -o $@ -v src/cmd/main.go +$(objects): Makefile + +src/$(NAME).a: src/$(NAME).go + go tool compile $(GOCFLAGS) -o $@ -p $(*F) $*.go + +tests/$(NAME).a: tests/$(NAME).go src/$(NAME).go + go tool compile $(GOCFLAGS) -o $@ -p $(*F) $*.go src/$(*F).go + +src/main.a: src/main.go src/$(NAME).a +tests/main.a: tests/main.go tests/$(NAME).a +src/main.a tests/main.a: + go tool compile $(GOCFLAGS) -o $@ -I $(@D) $*.go -tests/lib_test.bin: src/lib.go tests/lib_test.go Makefile - go test -c -o $@ -v $*.go +src/main.bin: src/main.a +tests/main.bin: tests/main.a +src/main.bin tests/main.bin: + go tool link $(GOLDFLAGS) -o $@ -L $(@D) $*.a +$(NAME).bin: src/main.bin + ln -fs $? $@ -check-unit: tests/lib_test.bin - ./tests/lib_test.bin + +tests.bin-check = \ + tests/main.bin-check \ + +tests/main.bin-check: tests/main.bin +$(tests.bin-check): + $(EXEC)$*.bin + +check-unit: $(tests.bin-check) integration-tests = \ tests/cli-opts.sh \ + tests/integration.sh \ -$(integration-tests): $(NAME).bin ALWAYS +.PRECIOUS: $(integration-tests) +$(integration-tests): $(NAME).bin +$(integration-tests): ALWAYS sh $@ check-integration: $(integration-tests) @@ -87,27 +120,32 @@ clean: install: all mkdir -p \ '$(DESTDIR)$(BINDIR)' \ + '$(DESTDIR)$(GOLIBDIR)' \ + '$(DESTDIR)$(SRCDIR)' \ cp $(NAME).bin '$(DESTDIR)$(BINDIR)'/$(NAME) - for f in $(sources); do \ - dir='$(DESTDIR)$(SRCDIR)'/"`dirname "$${f#src/}"`"; \ - mkdir -p "$$dir"; \ - cp -P "$$f" "$$dir"; \ - done + cp src/$(NAME).a '$(DESTDIR)$(GOLIBDIR)' + cp $(sources) '$(DESTDIR)$(SRCDIR)' ## Uninstalls from $(DESTDIR)$(PREFIX). This is a perfect mirror ## of the "install" target, and removes *all* that was installed. ## A dedicated test asserts that this is always true. uninstall: rm -rf \ - '$(DESTDIR)$(BINDIR)'/$(NAME) \ - '$(DESTDIR)$(SRCDIR)' \ + '$(DESTDIR)$(BINDIR)'/$(NAME) \ + '$(DESTDIR)$(GOLIBDIR)'/$(NAME).a \ + '$(DESTDIR)$(SRCDIR)' \ + ## Run it locally. run: all - rm -f glaze.socket - ./$(NAME).bin -P '/api/socket*:retcp.socket' -P '*:../papo/src/static/' glaze.socket + rm -f $(NAME).socket + ./$(NAME).bin \ + -P '/fonte/*:src/' \ + -P '/pasta/*:tests/' \ + -P '/api/socket*:retcp.socket' \ + glaze.socket ALWAYS: diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..ca9c5ce --- /dev/null +++ b/README.adoc @@ -0,0 +1,13 @@ +## Run it locally. +run: all + rm -f glaze.socket + ./$(NAME).bin -P '/api/socket*:retcp.socket' -P '*:../papo/src/static/' glaze.socket + +glaze -P 'URL:PATH' + + +setfattr -n user.glaze.directory-listing -v true tests +setfattr -n user.glaze.try-index-html -v true tests + + +add test with directory traversal, and escaping symllinks @@ -1,8 +0,0 @@ -module euandre.org/glaze - -go 1.21.5 - -require ( - github.com/pkg/xattr v0.4.9 // indirect - golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect -) @@ -1,4 +0,0 @@ -github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= -github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= -golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw= -golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/src/glaze.go b/src/glaze.go new file mode 100644 index 0000000..d0bc3e0 --- /dev/null +++ b/src/glaze.go @@ -0,0 +1,277 @@ +package glaze + +import ( + "context" + "errors" + "flag" + "fmt" + "io" + "io/fs" + "net" + "net/http" + "net/url" + "os" + "strings" + "syscall" + + + g "gobang" +) + + + +type patternPath struct {} + +type pathInfo struct { + pattern string + path string + label string + fileHandle *os.File + fileInfo fs.FileInfo +} + + + +const ( + xattrTryIndexHtml = "user.glaze.try-index-html" + xattrDirectoryListing = "user.glaze.directory-listing" + xattrExpectedValue = "true" +) + + + +func httpError(w http.ResponseWriter, code int, err error) { + t := http.StatusText(code) + g.Error( + err.Error(), "http-server-error", + "code", code, + "error", err, + "status-text", t, + ) + http.Error(w, t, code) +} + +func withTrailingSlash(path string) string { + if strings.HasSuffix(path, "/") { + return path + } else { + return path + "/" + } +} + +func adjustPattern(pattern string) string { + if pattern == "*" { + return "/" + } else if strings.HasSuffix(pattern, "*") { + return pattern[0:len(pattern) - 1] + } else { + return pattern + "{$}" + } +} + +func handleSymlink(info pathInfo, w http.ResponseWriter, r *http.Request) error { + linked, err := os.Readlink(info.path) + if err != nil { + return err + } + + // From http.Redirect(): + // + // > If the Content-Type header has not been set, + // > Redirect sets it to "text/html; charset=utf-8" and + // > writes a small HTML body. + // + // 🙄 + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + + http.Redirect(w, r, linked, http.StatusMovedPermanently) + return nil +} + +func handleProxy(info pathInfo, w http.ResponseWriter, r *http.Request) error { + target, err := url.Parse(info.path) + if err != nil { + return err + } + + target.Scheme = "http" + target.Host = "localhost" + httpClient := http.Client { + Transport: &http.Transport { + DialContext: func(_ context.Context, _ string, _ string) (net.Conn, error) { + return net.Dial("unix", info.path) + }, + }, + } + + r.URL.Scheme = target.Scheme + r.URL.Host = target.Host + r.RequestURI = "" + response, err := httpClient.Do(r) + if err != nil { + return err + } + for k, vArr := range response.Header { + for _, v := range vArr { + w.Header().Add(k, v) + } + } + w.WriteHeader(response.StatusCode) + io.Copy(w, response.Body) + return nil +} + +func handleFile(info pathInfo, w http.ResponseWriter, r *http.Request) error { + http.ServeContent(w, r, info.fileInfo.Name(), info.fileInfo.ModTime(), info.fileHandle) + return nil +} + +func getXattr(path string, name string) (string, error) { + data := make([]byte, 256) + i, err := syscall.Getxattr(path, name, data) + fmt.Println("i", i) + if err != nil { + return "", err + } + return string(data[0:i]), nil +} + +func handleDirectoryTarget(info pathInfo, w http.ResponseWriter, r *http.Request) error { + targetPath := withTrailingSlash(info.path) + r.URL.Path + newInfo, fn, err := handlerForDynPath(targetPath) + if err != nil { + return err + } + + if (newInfo.label != "directory") { + return fn(newInfo, w, r) + } + + indexHtmlXattr, err := getXattr(targetPath, xattrTryIndexHtml) + if err == nil && string(indexHtmlXattr) == xattrExpectedValue { + newPath := withTrailingSlash(targetPath) + "index.html" + _, err := os.Open(newPath) + if err == nil { + return fn(newInfo, w, r) + } + } + + dirListXattr, err := getXattr(targetPath, xattrDirectoryListing) + if err == nil && string(dirListXattr) == xattrExpectedValue { + http.FileServer(http.Dir(newInfo.path)).ServeHTTP(w, r) + return nil + } + + http.Error(w, "Forbidden", http.StatusForbidden) + return nil +} + +func handlerFuncFor(mode fs.FileMode) (string, func(pathInfo, http.ResponseWriter, *http.Request) error) { + if mode.Type() == fs.ModeSymlink { + return "symlink", handleSymlink + } else if mode.Type() == fs.ModeSocket { + return "socket", handleProxy + } else if !mode.IsDir() { + return "file", handleFile + } else { + return "directory", handleDirectoryTarget + } +} + +func handlerForDynPath(path string) (pathInfo, func(pathInfo, http.ResponseWriter, *http.Request) error, error) { + var info pathInfo + fileHandle, err := os.Open(path) + if err != nil { + return info, nil, err + } + + fileInfo, err := os.Lstat(path) + if err != nil { + return info, nil, err + } + + label, fn := handlerFuncFor(fileInfo.Mode()) + g.Info("Handler picked", "handler-picked", "label", label) + info = pathInfo { + path: path, + label: label, + fileHandle: fileHandle, + fileInfo: fileInfo, + } + return info, fn, nil +} + +func handlerFor(path string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id := g.NewUUID() + g.Info("in-request", "in-request", "path", path, "id", id) + defer g.Info("in-response", "in-response", "path", path, "id", id) + + info, fn, err := handlerForDynPath(path) + if err != nil { + httpError(w, http.StatusInternalServerError, err) + return + } + + err = fn(info, w, r) + if err != nil { + httpError(w, http.StatusInternalServerError, err) + return + } + }) +} + +func (i *patternPath) String() string { + return "" +} + +func (_ *patternPath) Set(value string) error { + arr := strings.Split(value, ":") + if len(arr) != 2 { + return errors.New("Bad value for path pattern: " + value) + } + + pattern := adjustPattern(arr[0]) + path := arr[1] + + http.Handle(pattern, http.StripPrefix(pattern, handlerFor(path))) + return nil +} + +func parseArgs(args []string) string { + var pat patternPath + fs := flag.NewFlagSet(args[0], flag.ExitOnError) + fs.Var(&pat, "P", "") + fs.Parse(args[1:]) + if fs.NArg() != 1 { + fmt.Fprintf( + os.Stderr, + "Usage: %s [ -P PATTERN:PATH ]... LISTEN.socket\n", + args[0], + ) + os.Exit(2) + } + return fs.Arg(0) +} + +func listen(fromAddr string) net.Listener { + listener, err := net.Listen("unix", fromAddr) + g.FatalIf(err) + g.Info("Started listening", "listen-start", "from-address", fromAddr) + return listener +} + +func start(listener net.Listener) { + server := http.Server {} + err := server.Serve(listener) + g.FatalIf(err) +} + + + +func Main() { + g.Init() + addr := parseArgs(os.Args) + listener := listen(addr) + start(listener) +} diff --git a/src/lib.go b/src/lib.go deleted file mode 100644 index 6b486f5..0000000 --- a/src/lib.go +++ /dev/null @@ -1,187 +0,0 @@ -package glaze - -import ( - "context" - "errors" - "flag" - "io" - "io/fs" - "log" - "net" - "net/http" - "net/url" - "os" - "strings" - - "github.com/pkg/xattr" -) - - - -const TRY_INDEX_HTML_XATTR = "user.glaze.directory-listing" - - -type PatternPath struct {} - -func (i *PatternPath) String() string { - return "FIXME" -} - -func proxyHandler(path string) http.Handler { - target, err := url.Parse(path) - if err != nil { - log.Fatal(err) - } - - target.Scheme = "http" - target.Host = "localhost" - - httpClient := http.Client { - Transport: &http.Transport { - DialContext: func(_ context.Context, _ string, _ string) (net.Conn, error) { - return net.Dial("unix", path) - }, - }, - } - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.URL.Scheme = target.Scheme - r.URL.Host = target.Host - r.RequestURI = "" - - response, err := httpClient.Do(r) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - log.Println(err) - return - } - - for k, vArr := range response.Header { - for _, v := range vArr { - w.Header().Add(k, v) - } - } - w.WriteHeader(response.StatusCode) - io.Copy(w, response.Body) - }) -} - -func fileHandler(path string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fileHandle, err := os.Open(path) - if err != nil { - http.Error(w, "Not Founde", http.StatusNotFound) - return - } - - fileInfo, err := fileHandle.Stat() - if err != nil { - http.Error(w, "Server Errore", http.StatusInternalServerError) - return - } - - http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), fileHandle) - }) -} - -func directoryHandler(path string) http.Handler { - // FIXME: return 301/302 on symlink - return http.FileServer(http.Dir(path)) -} - -func adjustPattern(pattern string) string { - if pattern == "*" { - return "/" - } else if strings.HasSuffix(pattern, "*") { - return pattern[0:len(pattern) - 1] - } else { - return pattern - // FIXME: when on Go 1.22, use: - // return pattern + "{$}" - } -} - -func registerHandler(tag string, pattern string, handler http.Handler) { - http.Handle( - pattern, - logged( - tag, - http.StripPrefix( - pattern, - handler, - ), - ), - ) -} - -func registerPattern(fileInfo fs.FileInfo, pattern string, path string) { - if fileInfo.Mode().Type() == fs.ModeSocket { - registerHandler("proxy", pattern, proxyHandler(path)) - } else if !fileInfo.Mode().IsDir() { - registerHandler("file", pattern, fileHandler(path)) - } else { - registerHandler("directory", pattern, directoryHandler(path)) - - data, err := xattr.Get(path, TRY_INDEX_HTML_XATTR) - if err != nil { - log.Fatal(err) - } - if string(data) == "true" { - indexPattern := pattern - if !strings.HasSuffix(indexPattern, "/") { - indexPattern += "/" - } - indexPattern += "index.html" - registerHandler("index-html-file", indexPattern, fileHandler(path + "/index.html")) - } - } -} - -func (_ *PatternPath) Set(value string) error { - arr := strings.Split(value, ":") - if len(arr) != 2 { - return errors.New("Bad value for path pattern: " + value) - } - - pattern := adjustPattern(arr[0]) - path := arr[1] - - fileInfo, err := os.Stat(path) - if err != nil { - return err - } - - registerPattern(fileInfo, pattern, path) - return nil -} - -func logged(tag string, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Printf("%s: %s %s\n", tag, r.Method, r.URL.Path) - next.ServeHTTP(w, r) - }) -} - -// FIXME: log to json -func Main() { - var myFlags PatternPath - flag.Var(&myFlags, "P", "URL:PATH pattern") - flag.Parse() - - if flag.NArg() != 1 { - flag.Usage() - os.Exit(2) - } - listenPath := flag.Arg(0) - - listener, err := net.Listen("unix", listenPath) - if err != nil { - log.Fatal("Failed to net.listen(): " + err.Error()) - } - - server := http.Server {} - err = server.Serve(listener) - if err != nil { - log.Fatal(err) - } -} diff --git a/src/cmd/main.go b/src/main.go index 8c2fbe3..15f5d79 100644 --- a/src/cmd/main.go +++ b/src/main.go @@ -1,6 +1,6 @@ package main -import "euandre.org/glaze/src" +import "glaze" func main() { glaze.Main() diff --git a/tests/cli-opts.sh b/tests/cli-opts.sh index fcb62ca..96d4726 100755 --- a/tests/cli-opts.sh +++ b/tests/cli-opts.sh @@ -1,4 +1,48 @@ #!/bin/sh set -eu -exit +. tests/lib.sh + + +test_needs_2_arguments() { + testing 'needs 2 arguments' + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR"' EXIT + STATUS=0 + ./binder.bin 1>"$OUT" 2>"$ERR" || STATUS=$? + assert_status 2 + assert_empty_stdout + assert_usage "$ERR" + rm -f "$OUT" "$ERR" + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR"' EXIT + STATUS=0 + ./binder.bin FROM-ADDR 1>"$OUT" 2>"$ERR" || STATUS=$? + assert_status 2 + assert_empty_stdout + assert_usage "$ERR" + rm -f "$OUT" "$ERR" + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR"' EXIT + STATUS=0 + ./binder.bin TO-ADDR 1>"$OUT" 2>"$ERR" || STATUS=$? + assert_status 2 + assert_empty_stdout + assert_usage "$ERR" + rm -f "$OUT" "$ERR" + + test_ok +} + + +exit # FIXME +test_needs_2_arguments diff --git a/tests/glaze.go b/tests/glaze.go new file mode 100644 index 0000000..366efa0 --- /dev/null +++ b/tests/glaze.go @@ -0,0 +1,36 @@ +package glaze + +import ( + g "gobang" +) + + +func test_adjustPattern() { + inputs := []string { + "", + "/", + "/*", + "/abc*", + "/abc/", + "/abc/*", + "abc/*", + } + expected := []string { + "{$}", + "/{$}", + "/", + "/abc", + "/abc/{$}", + "/abc/", + "abc/", + } + + for i, input := range inputs { + g.AssertEqual(adjustPattern(input), expected[i]) + } +} + + +func MainTest() { + test_adjustPattern() +} diff --git a/tests/integration.sh b/tests/integration.sh new file mode 100755 index 0000000..e0a92dc --- /dev/null +++ b/tests/integration.sh @@ -0,0 +1,73 @@ +#!/bin/sh +set -eu + +. tests/lib.sh + + +test_exits_when_upstream_errors() { + testing 'exits when upstream errors' + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR" s1.socket client.txt' EXIT + + rm -f s1.socket + ./binder.bin localhost:1234 s1.socket 1>"$OUT" 2>"$ERR" & + pid=$! + while ! lsof -s TCP:LISTEN -i :1234 > /dev/null; do + true + done + + echo request | socat tcp-connect:localhost:1234 stdio > client.txt + kill $pid + wait + + assert_fgrep_stdout 'listen-start' + assert_fgrep_stdout 'active-connections' + assert_fgrep_stdout 'dial-connection' + assert_empty_stderr + assert_empty_stream 'client.txt' client.txt + rm -f "$OUT" "$ERR" s1.socket client.txt + + test_ok +} + +test_works_from_client_to_server() { + testing 'works from client to server' + + N="$LINENO" + OUT="$(mkstemp)" + ERR="$(mkstemp)" + trap 'rm -f "$OUT" "$ERR" s2.socket client.txt server.txt' EXIT + + rm -f s2.socket + ./binder.bin localhost:1234 s2.socket 1>"$OUT" 2>"$ERR" & + pid=$! + while ! lsof -s TCP:LISTEN -i :1234 > /dev/null; do + true + done + + echo response | socat unix-listen:s2.socket stdio > server.txt & + while [ ! -S s2.socket ]; do + true + done + + echo request | socat tcp-connect:localhost:1234 stdio > client.txt + kill $pid + wait + + assert_fgrep_stdout 'listen-start' + assert_fgrep_stdout 'active-connections' + assert_empty_stderr + assert_fgrep_stream 'client.txt' client.txt 'response' + assert_fgrep_stream 'server.txt' server.txt 'request' + rm -f "$OUT" "$ERR" s2.socket client.txt server.txt + + test_ok +} + + +exit # FIXME +test_exits_when_upstream_errors +test_works_from_client_to_server diff --git a/tests/lib.sh b/tests/lib.sh new file mode 100644 index 0000000..07ecbef --- /dev/null +++ b/tests/lib.sh @@ -0,0 +1,119 @@ +#!/bin/sh + +end="\033[0m" +red="\033[0;31m" +green="\033[0;32m" +yellow="\033[0;33m" + +N= +OUT= +ERR= +STATUS= + +ERROR() { + # shellcheck disable=2059 + printf "${red}ERROR${end}" +} + +print_debug_info() { + # shellcheck disable=2016 + printf 'LINENO: %s\n$OUT: %s\n$ERR: %s\n' \ + "$N" "$OUT" "$ERR" >&2 +} + +assert_status() { + if [ "$STATUS" != "$1" ]; then + printf '\n%s: Bad status.\n\nexpected: %s\ngot: %s\n' \ + "$(ERROR)" "$1" "$STATUS" >&2 + print_debug_info + exit 1 + fi +} + +assert_usage() { + if ! grep -Fq 'Usage' "$1"; then + echo 'Expected to find "Usage" text, it was missing:' >&2 + cat "$1" >&2 + print_debug_info + exit 1 + fi +} + +assert_empty_stream() { + if [ -s "$2" ]; then + FMT='\n%s: Expected %s (%s) to be empty, but has content:\n%s\n' + # shellcheck disable=2059 + printf "$FMT" \ + "$(ERROR)" "$1" "$2" "$(cat "$2")" >&2 + print_debug_info + exit 1 + fi +} + +assert_empty_stdout() { + assert_empty_stream STDOUT "$OUT" +} + +assert_empty_stderr() { + assert_empty_stream STDERR "$ERR" +} + +assert_stream() { + if [ "$(cat "$2")" != "$3" ]; then + printf '\n%s: Bad %s (%s)\n\nexpected: %s\ngot: %s\n' \ + "$(ERROR)" "$1" "$2" "$3" "$(cat "$2")" >&2 + print_debug_info + exit 1 + fi +} + +assert_stdout() { + assert_stream STDOUT "$OUT" "$1" +} + +assert_stderr() { + assert_stream STDERR "$ERR" "$1" +} + +assert_grep_stream() { + if ! grep -qE "$3" "$2"; then + printf '\n%s: Bad %s (%s)\n\ngrepping: %s\nin:\n%s\n' \ + "$(ERROR)" "$1" "$2" "$3" "$(cat "$2")" >&2 + print_debug_info + exit 1 + fi +} + +assert_grep_stdout() { + assert_grep_stream STDOUT "$OUT" "$1" +} + +assert_grep_stderr() { + assert_grep_stream STDERR "$ERR" "$1" +} + +assert_fgrep_stream() { + if ! grep -Fq -- "$3" "$2"; then + printf '\n%s: Bad %s (%s)\n\ngrepping: %s\nin:\n%s\n' \ + "$(ERROR)" "$1" "$2" "$3" "$(cat "$2")" >&2 + print_debug_info + exit 1 + fi +} + +assert_fgrep_stdout() { + assert_fgrep_stream STDOUT "$OUT" "$1" +} + +assert_fgrep_stderr() { + assert_fgrep_stream STDERR "$ERR" "$1" +} + +testing() { + printf "${yellow}testing${end}: %s..." "$1" >&2 +} + +test_ok() { + # shellcheck disable=2059 + printf " ${green}OK${end}.\n" >&2 +} diff --git a/tests/lib_test.go b/tests/lib_test.go deleted file mode 100644 index 31259c6..0000000 --- a/tests/lib_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package lib_test - -import ( - "testing" - - "euandre.org/glaze/src" -) - -func TestPlaceholder(t *testing.T) { - if (&glaze.PatternPath{}).String() != "FIXME" { - t.Fail() - } -} diff --git a/tests/main.go b/tests/main.go new file mode 100644 index 0000000..835ef3a --- /dev/null +++ b/tests/main.go @@ -0,0 +1,7 @@ +package main + +import "glaze" + +func main() { + glaze.MainTest() +} diff --git a/tests/resources/index.html b/tests/resources/index.html new file mode 100644 index 0000000..4a3ce66 --- /dev/null +++ b/tests/resources/index.html @@ -0,0 +1,4 @@ +olar + +oiwjef + diff --git a/tests/resources/link.txt b/tests/resources/link.txt new file mode 120000 index 0000000..44ab045 --- /dev/null +++ b/tests/resources/link.txt @@ -0,0 +1 @@ +original.txt
\ No newline at end of file diff --git a/tests/resources/original.txt b/tests/resources/original.txt new file mode 100644 index 0000000..f00c965 --- /dev/null +++ b/tests/resources/original.txt @@ -0,0 +1,10 @@ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 |