~mna/siberian

0fb01de4275c6dff106f3963fd4437a24c0c185f — Martin Angers 2 years ago ded5adc master
hm not sure if debug works as expected
4 files changed, 164 insertions(+), 6 deletions(-)

M parse.go
M parse_test.go
A ring_writer.go
A ring_writer_test.go
M parse.go => parse.go +36 -2
@@ 1,6 1,7 @@
package siberian

import (
	"bytes"
	"fmt"
	"io"
	"reflect"


@@ 19,17 20,50 @@ func Matches(m Matcher, b []byte) bool {
// matches before the farthest non-successful match. If max is -1, all previous
// matches are printed.
func Debug(m Matcher, w io.Writer, max int) Matcher {
	// TODO: if max == -1, then use 2 bytes.Buffer, the current and the one
	// with the farthest failure position. Swap the farthest one when the
	// current fails on a later position.
	// If max >= 0, use two RingWriters and the same logic.
	var (
		curw io.Writer
		maxw io.Writer
	)
	farthest := -1
	return VisitMatcher(m, func(m Matcher) Matcher {
	neww := func() io.Writer {
		if max < 0 {
			return new(bytes.Buffer)
		}
		return newRingWriter(max + 1) // +1 for the failed match
	}
	curw = neww()

	// wrap each Matcher in one that buffers the debugging into the buffers.
	mm := VisitMatcher(m, func(m Matcher) Matcher {
		return MatcherFunc(func(b []byte, p int) int {
			n := m.Match(b, p)
			data := "EOF"
			if p < len(b) {
				data = string(b[p])
			}
			fmt.Fprintf(curw, "%d => %d | %s\n", p, n, data)
			if n < 0 && p > farthest {
				farthest = p
				fmt.Fprintf(w, "!%d: %T\n", p, m)
				maxw = curw
				curw = neww()
			}
			return n
		})
	})

	// and wrap only the top-level one in one that actually prints to w
	// the results if the parsing was not a match.
	return MatcherFunc(func(b []byte, p int) int {
		n := mm.Match(b, p)
		if n < 0 && maxw != nil {
			fmt.Fprintf(w, "%s", maxw)
		}
		return n
	})
}

// VisitContext maintains context during a visit of Matchers. Because

M parse_test.go => parse_test.go +27 -4
@@ 2,6 2,7 @@ package siberian_test

import (
	"bytes"
	"strings"
	"testing"

	"git.sr.ht/~mna/siberian"


@@ 119,8 120,30 @@ func TestVisitMatcher(t *testing.T) {
}

func TestDebug(t *testing.T) {
	var buf bytes.Buffer
	m := siberian.Debug(jsonmatcher.Doc, &buf, -1)
	siberian.Matches(m, []byte("1a"))
	t.Log(buf.String())
	cases := []struct {
		in        string
		max       int
		wantLines int
	}{
		{"null", -1, 0},
		{"nul", 0, 1},
		{"1a", -1, 4},
		{`{"a": "b`, -1, 4},
		{`[1, 2,]`, -1, 4},
		{`1.23a`, -1, 4},
	}
	for _, c := range cases {
		t.Run(c.in, func(t *testing.T) {
			var buf bytes.Buffer
			m := siberian.Debug(jsonmatcher.Doc, &buf, c.max)
			siberian.Matches(m, []byte(c.in))

			content := buf.String()
			gotLines := strings.Count(content, "\n")
			t.Log(content)
			if gotLines != c.wantLines {
				t.Errorf("want %d lines, got %d", c.wantLines, gotLines)
			}
		})
	}
}

A ring_writer.go => ring_writer.go +48 -0
@@ 0,0 1,48 @@
package siberian

import (
	"fmt"
	"strings"
)

// ringWriter is an io.Writer where only the last n calls to Write
// are retained. Writes to it can never fail.
type ringWriter struct {
	buf  [][]byte
	next int
}

// newRingWriter creates a ringWriter ready to use. It panics if n
// is <= 0.
func newRingWriter(n int) *ringWriter {
	if n <= 0 {
		panic(fmt.Sprintf("invalid n value for ringWriter: %d", n))
	}
	return &ringWriter{
		buf: make([][]byte, n),
	}
}

func (w *ringWriter) Write(p []byte) (int, error) {
	s := string(p)
	ix := w.next % len(w.buf)
	w.buf[ix] = []byte(s)
	w.next++
	return len(p), nil
}

func (w *ringWriter) Reset() {
	for i := range w.buf {
		w.buf[i] = nil
	}
	w.next = 0
}

func (w *ringWriter) String() string {
	var buf strings.Builder
	for i := 0; i < len(w.buf); i++ {
		ix := (w.next + i) % len(w.buf)
		buf.Write(w.buf[ix])
	}
	return buf.String()
}

A ring_writer_test.go => ring_writer_test.go +53 -0
@@ 0,0 1,53 @@
package siberian

import (
	"fmt"
	"strings"
	"testing"
)

func TestRingWriter(t *testing.T) {
	cases := []struct {
		writes string // one write per line
		want   string // content joined by a line
	}{
		{
			"", "\n", // because we use Fprintn in the test
		},
		{
			"a", "a\n",
		},
		{
			"a\nb", "a\nb\n",
		},
		{
			"a\nb\nc", "a\nb\nc\n",
		},
		{
			"a\nb\nc\nd", "b\nc\nd\n",
		},
		{
			"a\nb\nc\nd\ne", "c\nd\ne\n",
		},
		{
			"a\nb\nc\nd\ne\nf", "d\ne\nf\n",
		},
		{
			"a\nb\nc\nd\ne\nf\ng", "e\nf\ng\n",
		},
	}
	w := newRingWriter(3)
	for _, c := range cases {
		t.Run(c.writes, func(t *testing.T) {
			w.Reset()

			lines := strings.Split(c.writes, "\n")
			for _, l := range lines {
				fmt.Fprintln(w, l)
			}
			if got := w.String(); c.want != got {
				t.Fatalf("want (%d):\n%s\ngot (%d):\n%s\n", len(c.want), c.want, len(got), got)
			}
		})
	}
}