package annotate
import (
"bytes"
"errors"
"flag"
"fmt"
"io/ioutil"
"path/filepath"
"sort"
"strings"
"testing"
"text/template"
"time"
"unicode/utf8"
)
var saveExp = flag.Bool("exp", false, "overwrite all expected output files with actual output (returning a failure)")
var match = flag.String("m", "", "only run tests whose name contains this string")
func TestAnnotate(t *testing.T) {
tests := map[string]struct {
input string
anns Annotations
want string
wantErr error
}{
"empty and unannotated": {"", nil, "", nil},
"unannotated": {"a⌘b", nil, "a⌘b", nil},
// The docs say "Annotating an empty byte array always returns an empty
// byte array.", which is arbitrary but makes implementation easier.
"empty annotated": {"", Annotations{{0, 0, []byte("["), []byte("]"), 0}}, "", nil},
"zero-length annotations": {
"aaaa",
Annotations{
{0, 0, []byte(""), []byte(""), 0},
{0, 0, []byte(""), []byte(""), 0},
{2, 2, []byte(""), []byte(""), 0},
},
"aaaa",
nil,
},
"1 annotation": {"a", Annotations{{0, 1, []byte("["), []byte("]"), 0}}, "[a]", nil},
"nested": {
"abc",
Annotations{
{0, 3, []byte("["), []byte("]"), 0},
{1, 2, []byte("<"), []byte(">"), 0},
},
"[ac]",
nil,
},
"nested 1": {
"abcd",
Annotations{
{0, 4, []byte("<1>"), []byte("1>"), 0},
{1, 3, []byte("<2>"), []byte("2>"), 0},
{2, 2, []byte("<3>"), []byte("3>"), 0},
},
"<1>a<2>b<3>3>c2>d1>",
nil,
},
"same range": {
"ab",
Annotations{
{0, 2, []byte("["), []byte("]"), 0},
{0, 2, []byte("<"), []byte(">"), 0},
},
"[]",
nil,
},
"same range (with WantInner)": {
"ab",
Annotations{
{0, 2, []byte("["), []byte("]"), 1},
{0, 2, []byte("<"), []byte(">"), 0},
},
"<[ab]>",
nil,
},
"unicode content": {
"abcdef⌘vwxyz",
Annotations{
{6, 9, []byte(""), []byte(""), 0},
{10, 12, []byte(""), []byte(""), 0},
{0, 13, []byte(""), []byte(""), 0},
},
"abcdef⌘vwxyz",
nil,
},
"remainder": {
"xyz",
Annotations{
{0, 2, []byte(""), []byte(""), 0},
{0, 1, []byte(""), []byte(""), 0},
},
"xyz",
nil,
},
// Overlapping
"overlap simple": {
"abc",
Annotations{
{0, 2, []byte(""), []byte(""), 0},
{1, 3, []byte(""), []byte(""), 0},
},
// Without re-opening overlapped annotations, we'd get
// "abc".
"abc",
nil,
},
"overlap simple double": {
"abc",
Annotations{
{0, 2, []byte(""), []byte(""), 0},
{0, 2, []byte(""), []byte(""), 0},
{1, 3, []byte(""), []byte(""), 0},
{1, 3, []byte(""), []byte(""), 0},
},
"abc",
nil,
},
"overlap triple complex": {
"abcd",
Annotations{
{0, 2, []byte(""), []byte(""), 0},
{1, 3, []byte(""), []byte(""), 0},
{2, 4, []byte(""), []byte(""), 0},
},
"abcd",
nil,
},
"overlap same start": {
"abcd",
Annotations{
{0, 2, []byte(""), []byte(""), 0},
{0, 3, []byte(""), []byte(""), 0},
{1, 4, []byte(""), []byte(""), 0},
},
"abcd",
nil,
},
"overlap (infinite loop regression #1)": {
"abcde",
Annotations{
{0, 3, []byte(""), []byte(""), 0},
{1, 5, []byte(""), []byte(""), 0},
{1, 2, []byte(""), []byte(""), 0},
},
"abcde",
nil,
},
// Errors
"start oob": {"a", Annotations{{-1, 1, []byte("<"), []byte(">"), 0}}, "", ErrStartOutOfBounds},
"start oob (multiple)": {
"a",
Annotations{
{-3, 1, []byte("1"), []byte(""), 0},
{-3, 1, []byte("2"), []byte(""), 0},
{-1, 1, []byte("3"), []byte(""), 0},
},
"123a",
ErrStartOutOfBounds,
},
"end oob": {"a", Annotations{{0, 3, []byte("<"), []byte(">"), 0}}, "", ErrEndOutOfBounds},
"end oob (multiple)": {
"ab",
Annotations{
{0, 3, []byte(""), []byte("1"), 0},
{1, 3, []byte(""), []byte("2"), 0},
{0, 5, []byte(""), []byte("3"), 0},
},
"ab213",
ErrEndOutOfBounds,
},
}
for label, test := range tests {
if *match != "" && !strings.Contains(label, *match) {
continue
}
sort.Sort(Annotations(test.anns))
got, err := Annotate([]byte(test.input), test.anns, nil)
if err != test.wantErr {
if test.wantErr == nil {
t.Errorf("%s: Annotate: %s", label, err)
} else {
t.Errorf("%s: Annotate: got error %v, want %v", label, err, test.wantErr)
}
}
if string(got) != test.want {
t.Errorf("%s: Annotate:\ngot %q\nwant %q", label, got, test.want)
continue
}
}
}
func TestAnnotate_Files(t *testing.T) {
annsByFile := map[string]Annotations{
"hello_world.txt": {
{0, 5, []byte(""), []byte(""), 0},
{7, 12, []byte(""), []byte(""), 0},
},
"adjacent.txt": {
{0, 3, []byte(""), []byte(""), 0},
{3, 6, []byte(""), []byte(""), 0},
},
"nested_0.txt": {
{0, 4, []byte("<1>"), []byte("1>"), 0},
{1, 3, []byte("<2>"), []byte("2>"), 0},
},
"nested_2.txt": {
{0, 2, []byte("<1>"), []byte("1>"), 0},
{2, 4, []byte("<2>"), []byte("2>"), 0},
{4, 6, []byte("<3>"), []byte("3>"), 0},
{7, 8, []byte("<4>"), []byte("4>"), 0},
},
"html.txt": {
{193, 203, []byte("<1>"), []byte("1>"), 0},
{336, 339, []byte(""), []byte(""), 0},
},
}
dir := "testdata"
tests, err := ioutil.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
for _, test := range tests {
name := test.Name()
if !strings.Contains(name, *match) {
continue
}
if strings.HasSuffix(name, ".html") {
continue
}
path := filepath.Join(dir, name)
input, err := ioutil.ReadFile(path)
if err != nil {
t.Fatal(err)
continue
}
anns := annsByFile[name]
sort.Sort(anns)
got, err := Annotate(input, anns, template.HTMLEscape)
if err != nil {
t.Errorf("%s: Annotate: %s", name, err)
continue
}
expPath := path + ".html"
if *saveExp {
err = ioutil.WriteFile(expPath, got, 0700)
if err != nil {
t.Fatal(err)
}
continue
}
want, err := ioutil.ReadFile(expPath)
if err != nil {
t.Fatal(err)
}
want = bytes.TrimSpace(want)
got = bytes.TrimSpace(got)
if !bytes.Equal(want, got) {
t.Errorf("%s: want %q, got %q", name, want, got)
continue
}
}
if *saveExp {
t.Fatal("overwrote all expected output files with actual output (run tests again without -exp)")
}
}
func makeFakeData(size1, size2 int) ([]byte, Annotations) {
input := []byte(strings.Repeat(strings.Repeat("a", size1)+"⌘", size2))
inputLength := utf8.RuneCount(input)
n := len(input)/2 - (size1+1)/2
anns := make(Annotations, n)
for i := 0; i < n; i++ {
if i%2 == 0 {
anns[i] = &Annotation{Start: 2 * i, End: 2*i + 1}
} else {
anns[i] = &Annotation{Start: 2*i - 50, End: 2*i + 50}
if anns[i].Start < 0 {
anns[i].Start = 0
anns[i].End = i
}
if anns[i].End >= inputLength {
anns[i].End = inputLength
}
}
anns[i].Left = []byte("L") //[]byte(strings.Repeat("L", i%20))
anns[i].Right = []byte("R") //[]byte(strings.Repeat("R", i%20))
anns[i].WantInner = i % 5
}
sort.Sort(anns)
return input, anns
}
func TestAnnotate_GeneratedData(t *testing.T) {
input, anns := makeFakeData(1, 15)
fail := func(err error) {
annStrs := make([]string, len(anns))
for i, a := range anns {
annStrs[i] = fmt.Sprintf("%v", a)
}
t.Fatalf("Annotate: %s\n\nInput was:\n%q\n\nAnnotations:\n%s", err, input, strings.Join(annStrs, "\n"))
}
tm := time.NewTimer(time.Millisecond * 500)
done := make(chan error)
go func() {
_, err := Annotate(input, anns, nil)
done <- err
}()
select {
case <-tm.C:
fail(errors.New("timed out (is there an infinite loop?)"))
case err := <-done:
if err != nil {
fail(err)
}
}
}
func BenchmarkAnnotate(b *testing.B) {
input, anns := makeFakeData(99, 20)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Annotate(input, anns, nil)
if err != nil {
b.Fatal(err)
}
}
}