Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI

on:
push:
branches:
- master
- 'feature/**'
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go: [oldstable, stable]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}

- name: gofmt
run: test -z "$(gofmt -l .)"

- name: vet
run: go vet ./...

- name: test
run: go test -race ./...

- name: fuzz
# A short differential-fuzzing smoke on top of the seed corpus,
# which `go test` above already runs; see FuzzMatchRegexp.
run: go test -fuzz=FuzzMatchRegexp -fuzztime=30s .
54 changes: 54 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Fuzz

on:
schedule:
# Nightly; the odd minute avoids the on-the-hour load spikes.
- cron: '17 3 * * *'
workflow_dispatch:
# Allows manual runs from the Actions tab.

jobs:
fuzz:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: stable

- name: locate fuzz cache
id: cache-dir
run: echo "dir=$(go env GOCACHE)/fuzz" >> "$GITHUB_OUTPUT"

- name: restore corpus
uses: actions/cache/restore@v4
with:
path: ${{ steps.cache-dir.outputs.dir }}
# A fresh key every run plus a prefix restore key: the job always
# starts from the most recent corpus and always saves the evolved
# one, so the coverage exploration accumulates across nights.
key: fuzz-corpus-${{ github.run_id }}
restore-keys: |
fuzz-corpus-

- name: fuzz
run: go test -fuzz=FuzzMatchRegexp -fuzztime=30m .

- name: save corpus
# Save even when the fuzzing found a failure -- especially then.
if: always()
uses: actions/cache/save@v4
with:
path: ${{ steps.cache-dir.outputs.dir }}
key: fuzz-corpus-${{ github.run_id }}

- name: upload crashers
# A found failure is minimized and written into testdata/fuzz/;
# keep it as an artifact so it can be committed as a regression.
if: failure()
uses: actions/upload-artifact@v4
with:
name: crashers
path: testdata/fuzz/
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ glob.iml
*.dot
*.png
*.svg
patterns.txt
*.bench
15 changes: 0 additions & 15 deletions .travis.yml

This file was deleted.

57 changes: 36 additions & 21 deletions bench.sh
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
#! /bin/bash
#!/bin/bash
#
# Compares the benchmarks of the current branch against a git revision:
#
# ./bench.sh v0.2.3 # all the benchmarks
# ./bench.sh master 'Match' # the ones matching a -bench regexp
#
# The results are written to *.bench files in the current directory and
# compared with benchstat (go install golang.org/x/perf/cmd/benchstat@latest).

bench() {
filename="/tmp/$1-$2.bench"
if test -e "${filename}";
then
echo "Already exists ${filename}"
else
backup=`git rev-parse --abbrev-ref HEAD`
git checkout $1
echo -n "Creating ${filename}... "
go test ./... -run=NONE -bench=$2 > "${filename}" -benchmem
echo "OK"
git checkout ${backup}
sleep 5
fi
}
set -eu

prev=$1
what=${2:-.}
curr=$(git rev-parse --abbrev-ref HEAD)
rnd=$(head -c4 </dev/urandom | xxd -p)

file() {
echo "$rnd-$1.bench" | tr "/" "_"
}

to=$1
current=`git rev-parse --abbrev-ref HEAD`
bench() {
local rev=$1
local out
out=$(file "$rev")
if [[ -e "$out" ]]; then
echo "Already exists $out"
return
fi
git checkout -q "$rev"
echo -n "Creating $out... "
go test ./... -run=none -benchmem -bench="$what" >"$out"
echo "OK"
git checkout -q "$curr"
sleep 5
}

bench ${to} $2
bench ${current} $2
bench "$prev"
bench "$curr"

benchcmp $3 "/tmp/${to}-$2.bench" "/tmp/${current}-$2.bench"
benchstat "$(file "$prev")" "$(file "$curr")"
44 changes: 0 additions & 44 deletions cmd/globdraw/main.go

This file was deleted.

103 changes: 73 additions & 30 deletions cmd/globtest/main.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package main

import (
"errors"
"flag"
"fmt"
"github.com/gobwas/glob"
"os"
"strings"
"testing"
"text/tabwriter"
"unicode/utf8"

"github.com/gobwas/glob"
"github.com/gobwas/glob/internal/debug"
)

func benchString(r testing.BenchmarkResult) string {
Expand All @@ -31,52 +35,91 @@ func benchString(r testing.BenchmarkResult) string {
return fmt.Sprintf("%8d\t%s\t%s allocs", r.N, ns, allocs)
}

// pointAt renders the pattern with a pointer at the offset of the syntax
// error, followed by its reason:
//
// {a,b
// ----^ unclosed `{`
func pointAt(pattern string, err *glob.SyntaxError) string {
// The offset is in bytes, while the pointer is drawn in columns.
col := utf8.RuneCountInString(pattern[:min(err.Offset, len(pattern))])
return fmt.Sprintf("%s\n%s^ %s\n\n", pattern, strings.Repeat("-", col), err.Reason)
}

func main() {
pattern := flag.String("p", "", "pattern to draw")
sep := flag.String("s", "", "comma separated list of separators")
fixture := flag.String("f", "", "fixture")
verbose := flag.Bool("v", false, "verbose")
var (
pattern = flag.String("p", "", "pattern to draw")
sep = flag.String("s", "", "comma separated list of separators")
fixture = flag.String("f", "", "fixture")
benchCompile = flag.Bool("bench-compile", false, "benchmark compilation time")
benchMatch = flag.Bool("bench-match", false, "benchmark matching time")
verbose = flag.Bool("v", false, "print the pattern, fixture, compiled matcher tree and result; without it only the exit status tells: 0 on match, 1 otherwise")
)

// Expose testing package flags.
testing.Init()

flag.Parse()

if *pattern == "" {
flag.Usage()
os.Exit(1)
os.Exit(2)
}

var separators []rune
for _, c := range strings.Split(*sep, ",") {
if r, w := utf8.DecodeRuneInString(c); len(c) > w {
fmt.Println("only single charactered separators are allowed")
os.Exit(1)
} else {
separators = append(separators, r)
if len(c) == 0 {
continue
}
r, w := utf8.DecodeRuneInString(c)
if len(c) > w {
fmt.Fprintln(os.Stderr, "only single charactered separators are allowed")
os.Exit(2)
}
separators = append(separators, r)
}

g, err := glob.Compile(*pattern, separators...)
if err != nil {
fmt.Println("could not compile pattern:", err)
os.Exit(1)
fmt.Fprintln(os.Stderr, "could not compile pattern:", err)
var syntaxErr *glob.SyntaxError
if errors.As(err, &syntaxErr) {
fmt.Fprint(os.Stderr, "\n"+pointAt(*pattern, syntaxErr))
}
os.Exit(2)
}
matched := g.Match(*fixture)

if !*verbose {
fmt.Println(g.Match(*fixture))
return
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
if *verbose {
fmt.Fprintf(w, "pattern:\t%s\n", g)
fmt.Fprintf(w, "fixture:\t%s\n", *fixture)
fmt.Fprintf(w, "matchers:\t%s\n", debug.Tree(g))
fmt.Fprintf(w, "result:\t%t\n", matched)
// Flush before the benchmarks: they take a while, and the result
// must not wait for them.
w.Flush()
}

fmt.Printf("result: %t\n", g.Match(*fixture))

cb := testing.Benchmark(func(b *testing.B) {
for i := 0; i < b.N; i++ {
glob.Compile(*pattern, separators...)
}
})
fmt.Println("compile:", benchString(cb))
if *benchCompile {
b := testing.Benchmark(func(b *testing.B) {
for i := 0; i < b.N; i++ {
glob.Compile(*pattern, separators...)
}
})
fmt.Fprintf(w, "compile:\t%s\n", benchString(b))
}
if *benchMatch {
b := testing.Benchmark(func(b *testing.B) {
for i := 0; i < b.N; i++ {
g.Match(*fixture)
}
})
fmt.Fprintf(w, "match:\t%s\n", benchString(b))
}
w.Flush()

mb := testing.Benchmark(func(b *testing.B) {
for i := 0; i < b.N; i++ {
g.Match(*fixture)
}
})
fmt.Println("match: ", benchString(mb))
if !matched {
os.Exit(1)
}
}
Loading
Loading