// Copyright 2016 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package text import ( "bytes" "fmt" "image" "io" "io/ioutil" "math/rand" "reflect" "sort" "strings" "testing" "unicode/utf8" "golang.org/x/image/font" "golang.org/x/image/math/fixed" ) func min(a, b int) int { if a < b { return a } return b } func readAllText(dst []byte, f *Frame) []byte { for p := f.FirstParagraph(); p != nil; p = p.Next(f) { for l := p.FirstLine(f); l != nil; l = l.Next(f) { for b := l.FirstBox(f); b != nil; b = b.Next(f) { dst = append(dst, b.Text(f)...) } } } return dst } func readAllRunes(dst []rune, rr io.RuneReader) ([]rune, error) { for { r, size, err := rr.ReadRune() if err == io.EOF { return dst, nil } if err != nil { return nil, err } // Check that r and size are consistent. if wantSize := utf8.RuneLen(r); size == wantSize { // OK. } else if r == utf8.RuneError && size == 1 { // Also OK; one byte of invalid UTF-8 was replaced by '\ufffd'. } else { return nil, fmt.Errorf("rune %#x: got size %d, want %d", r, size, wantSize) } dst = append(dst, r) } } func runesEqual(xs, ys []rune) bool { if len(xs) != len(ys) { return false } for i := range xs { if xs[i] != ys[i] { return false } } return true } func allASCII(rs []rune) bool { for _, r := range rs { if r >= 128 { return false } } return true } func runesLen(rs []rune) (n int) { for _, r := range rs { n += utf8.RuneLen(r) } return n } // insert inserts the insert argument into buf at the given offset. // // cap(buf) must be >= len(buf) + len(insert). func insert(buf, insert []byte, offset int) []byte { n := len(insert) buf = buf[:len(buf)+n] copy(buf[offset+n:], buf[offset:]) copy(buf[offset:offset+n], insert) return buf } func rngIntPair(rng *rand.Rand, n int) (x, y int) { x = rng.Intn(n) y = rng.Intn(n) if x > y { x, y = y, x } return x, y } // toyFace implements the font.Face interface by measuring every rune's width // as 1 pixel. type toyFace struct{} func (toyFace) Close() error { return nil } func (toyFace) Glyph(dot fixed.Point26_6, r rune) (image.Rectangle, image.Image, image.Point, fixed.Int26_6, bool) { panic("unimplemented") } func (toyFace) GlyphBounds(r rune) (fixed.Rectangle26_6, fixed.Int26_6, bool) { panic("unimplemented") } func (toyFace) GlyphAdvance(r rune) (fixed.Int26_6, bool) { return fixed.I(1), true } func (toyFace) Kern(r0, r1 rune) fixed.Int26_6 { return 0 } func (toyFace) Metrics() font.Metrics { return font.Metrics{ Ascent: fixed.I(2), Descent: fixed.I(1), } } const toyFaceLineHeight = 3 // iRobot is some text that contains both ASCII and non-ASCII runes. const iRobot = "\"I, Robot\" in Russian is \"Я, робот\".\nIt's about robots.\n" var ( iRobotBytes = []byte(iRobot) iRobotRunes = []rune(iRobot) ) func iRobotFrame(maxWidth int) *Frame { f := new(Frame) f.SetFace(toyFace{}) f.SetMaxWidth(fixed.I(maxWidth)) c := f.NewCaret() c.WriteString(iRobot) c.Close() return f } func TestZeroFrame(t *testing.T) { f := new(Frame) if err := checkInvariants(f); err != nil { t.Fatal(err) } } func TestSeek(t *testing.T) { f := iRobotFrame(10) c := f.NewCaret() defer c.Close() rng := rand.New(rand.NewSource(1)) seen := [1 + len(iRobot)]bool{} for i := 0; i < 10*len(iRobot); i++ { wantPos := int64(rng.Intn(len(iRobot) + 1)) gotPos, err := c.Seek(wantPos, SeekSet) if err != nil { t.Fatalf("i=%d: Seek: %v", i, err) } if gotPos != wantPos { t.Fatalf("i=%d: Seek: got %d, want %d", i, gotPos, wantPos) } seen[gotPos] = true if err := checkInvariants(f); err != nil { t.Fatalf("i=%d: %v", i, err) } gotByte, gotErr := c.ReadByte() wantByte, wantErr := byte(0), io.EOF if gotPos < int64(len(iRobot)) { wantByte, wantErr = iRobot[gotPos], nil } if gotByte != wantByte || gotErr != wantErr { t.Fatalf("i=%d: ReadByte: got %d, %v, want %d, %v", i, gotByte, gotErr, wantByte, wantErr) } } for i, s := range seen { if !s { t.Errorf("randomly generated positions weren't exhaustive: position %d / %d not seen", i, len(iRobot)) } } } func testRead(f *Frame, want string) error { c := f.NewCaret() defer c.Close() asBytes, err := ioutil.ReadAll(c) if err != nil { return fmt.Errorf("ReadAll: %v", err) } if got := string(asBytes); got != want { return fmt.Errorf("Read\ngot: %q\nwant: %q", got, want) } return nil } func TestRead(t *testing.T) { f := iRobotFrame(10) if err := checkInvariants(f); err != nil { t.Fatal(err) } if err := testRead(f, iRobot); err != nil { t.Fatal(err) } } func TestReadByte(t *testing.T) { f := iRobotFrame(10) c := f.NewCaret() defer c.Close() got, want := []byte(nil), []byte(iRobot) for { x, err := c.ReadByte() if err == io.EOF { break } if err != nil { t.Fatalf("ReadByte: %v", err) } if err := checkInvariants(f); err != nil { t.Fatal(err) } got = append(got, x) } if err := checkInvariants(f); err != nil { t.Fatal(err) } if !reflect.DeepEqual(got, want) { t.Fatalf("\ngot: %v\nwant: %v", got, want) } } func TestReadRune(t *testing.T) { f := iRobotFrame(10) c := f.NewCaret() defer c.Close() got, want := []rune(nil), []rune(iRobot) for { r, _, err := c.ReadRune() if err == io.EOF { break } if err != nil { t.Fatalf("ReadRune: %v", err) } if err := checkInvariants(f); err != nil { t.Fatal(err) } got = append(got, r) } if err := checkInvariants(f); err != nil { t.Fatal(err) } if !reflect.DeepEqual(got, want) { t.Fatalf("\ngot: %v\nwant: %v", got, want) } } func TestWrite(t *testing.T) { f := new(Frame) c := f.NewCaret() c.Write([]byte{0xff, 0xfe, 0xfd}) c.WriteByte(0x80) c.WriteRune('\U0001f4a9') c.WriteString("abc\x00") c.Close() if err := checkInvariants(f); err != nil { t.Fatal(err) } got := f.text[:f.len] want := []byte{0xff, 0xfe, 0xfd, 0x80, 0xf0, 0x9f, 0x92, 0xa9, 0x61, 0x62, 0x63, 0x00} if !bytes.Equal(got, want) { t.Fatalf("\ngot % x\nwant % x", got, want) } } func TestManyWrites(t *testing.T) { f := new(Frame) f.SetFace(toyFace{}) f.SetMaxWidth(fixed.I(10)) c := f.NewCaret() defer c.Close() const n, abc16 = 100, "abcdefghijkl\n " rng := rand.New(rand.NewSource(1)) buf := make([]byte, n) for i := range buf { buf[i] = abc16[rng.Intn(len(abc16))] } for i := 0; i < 100; i++ { x, y := rngIntPair(rng, len(buf)+1) c.Write(buf[x:y]) if err := checkInvariants(f); err != nil { t.Fatalf("i=%d: %v", i, err) } } } func TestRandomAccessWrite(t *testing.T) { f := new(Frame) c := f.NewCaret() defer c.Close() rng := rand.New(rand.NewSource(1)) gotBuf := make([]byte, 0, 10000) want := make([]byte, 0, 10000) buf := make([]byte, 10) x := byte(0) for i := 0; i < 100; i++ { n := rng.Intn(len(buf) + 1) for j := range buf[:n] { buf[j] = x x++ } offset := rng.Intn(len(want) + 1) // Insert buf[:n] into the Frame at the given offset. c.Seek(int64(offset), SeekSet) c.Write(buf[:n]) if err := checkInvariants(f); err != nil { t.Fatalf("i=%d: %v", i, err) } // Insert buf[:n] into want at the given offset. want = insert(want, buf[:n], offset) if got := readAllText(gotBuf[:0], f); !bytes.Equal(got, want) { t.Errorf("i=%d:\ngot % x\nwant % x", i, got, want) } } } func TestWriteAtStart(t *testing.T) { f := iRobotFrame(10) c := f.NewCaret() defer c.Close() prefix := "The Truth of the Matter\n(insofaras): " gotBuf := make([]byte, len(prefix)+len(iRobot)) want := make([]byte, len(iRobot), len(prefix)+len(iRobot)) copy(want, iRobot) for i := 0; i < len(prefix); i++ { x := prefix[len(prefix)-1-i] c.Seek(0, SeekSet) c.WriteByte(x) if err := checkInvariants(f); err != nil { t.Fatalf("i=%d: %v", i, err) } want = want[:len(want)+1] copy(want[1:], want) want[0] = x if got := readAllText(gotBuf[:0], f); !bytes.Equal(got, want) { t.Fatalf("i=%d:\ngot % x\nwant % x", i, got, want) } } if nl, nb := f.nUsedLines(), f.nUsedBoxes(); nl == nb { t.Fatalf("before compaction: using %d lines and %d boxes, should be not equal", nl, nb) } f.compactText() if nl, nb := f.nUsedLines(), f.nUsedBoxes(); nl != nb { t.Fatalf("after compaction: using %d lines and %d boxes, should be equal", nl, nb) } } func TestLineCount(t *testing.T) { f := iRobotFrame(0) testCases := []struct { desc string maxWidth int wantLines []string wantLineCounts []int }{{ desc: "infinite maxWidth", maxWidth: 0, wantLines: []string{ `"I, Robot" in Russian is "Я, робот".`, `It's about robots.`, ``, }, wantLineCounts: []int{1, 1, 1}, }, { desc: "finite maxWidth", maxWidth: 10, wantLines: []string{ `"I, Robot"`, `in Russian`, `is "Я,`, `робот".`, `It's about`, `robots.`, ``, }, wantLineCounts: []int{4, 2, 1}, }} for _, tc := range testCases { f.SetMaxWidth(fixed.I(tc.maxWidth)) lines, lineCounts := []string{}, []int{} for p := f.FirstParagraph(); p != nil; p = p.Next(f) { for l := p.FirstLine(f); l != nil; l = l.Next(f) { var line []byte for b := l.FirstBox(f); b != nil; b = b.Next(f) { line = append(line, b.TrimmedText(f)...) } lines = append(lines, string(line)) } lineCounts = append(lineCounts, p.LineCount(f)) } if got, want := f.LineCount(), len(tc.wantLines); got != want { t.Errorf("%s: LineCount: got %d, want %d", tc.desc, got, want) continue } if !reflect.DeepEqual(lines, tc.wantLines) { t.Errorf("%s: lines: got %q, want %q", tc.desc, lines, tc.wantLines) continue } if got, want := f.ParagraphCount(), len(tc.wantLineCounts); got != want { t.Errorf("%s: ParagraphCount: got %d, want %d", tc.desc, got, want) continue } if !reflect.DeepEqual(lineCounts, tc.wantLineCounts) { t.Errorf("%s: lineCounts: got %d, want %d", tc.desc, lineCounts, tc.wantLineCounts) continue } if got, want := f.Height(), toyFaceLineHeight*len(tc.wantLines); got != want { t.Errorf("%s: Height: got %d, want %d", tc.desc, got, want) continue } } } func TestSetMaxWidth(t *testing.T) { f := new(Frame) f.SetFace(toyFace{}) want := "" rng := rand.New(rand.NewSource(1)) for i := 0; i < 100; i++ { if i%20 == 5 { c := f.NewCaret() c.WriteString(iRobot) c.Close() want += iRobot } maxWidth := rng.Intn(20) f.SetMaxWidth(fixed.I(maxWidth)) if err := checkInvariants(f); err != nil { t.Fatalf("i=%d: %v", i, err) } if err := testRead(f, want); err != nil { t.Fatalf("i=%d: %v", i, err) } if want == "" { continue } // Check that a zero (which means infinite) maxWidth corresponds to one // Line per Paragraph. np, nl := f.nUsedParagraphs(), f.nUsedLines() if maxWidth == 0 { if np != nl { t.Fatalf("i=%d: maxWidth=0: got %d paragraphs and %d lines, should be equal", i, np, nl) } } else { if np == nl { t.Fatalf("i=%d: maxWidth=%d: got %d paragraphs and %d lines, should be unequal", i, maxWidth, np, nl) } } } } func TestReadRuneAcrossBoxes(t *testing.T) { rng := rand.New(rand.NewSource(1)) for i := 0; i < 6; i++ { // text is a mixture of valid and invalid UTF-8. text := "ab_\u0100\u0101_\ufffd中文_\xff\xffñ\xff_z_\U0001f4a9_\U0001f600" + strings.Repeat("\xff", i) wantBytes := []byte(text) wantRunes := []rune(text) gotBytesBuf := make([]byte, 0, len(text)) gotRunesBuf := make([]rune, 0, len(text)) for j := 0; j < 100; j++ { f := new(Frame) { c := f.NewCaret() c.WriteString(text) // Split the sole Line into multiple Boxes at random byte offsets, // possibly cutting across multi-byte UTF-8 encoded runes. for { if rng.Intn(8) == 0 { break } c.Seek(int64(rng.Intn(len(text)+1)), SeekSet) c.splitBox(false) } c.Close() if err := checkInvariants(f); err != nil { t.Fatalf("i=%d, j=%d: %v", i, j, err) } if gotBytes := readAllText(gotBytesBuf[:0], f); !bytes.Equal(gotBytes, wantBytes) { t.Fatalf("i=%d, j=%d: bytes\ngot % x\nwant % x", i, j, gotBytes, wantBytes) } } // Test lineReader.ReadRune. { p := f.firstP l := f.paragraphs[p].firstL b := f.lines[l].firstB lr := f.lineReader(b, f.boxes[b].i) gotRunes, err := readAllRunes(gotRunesBuf[:0], lr) if err != nil { t.Fatalf("i=%d, j=%d: lineReader readAllRunes: %v", i, j, err) } if !runesEqual(gotRunes, wantRunes) { t.Fatalf("i=%d, j=%d: lineReader readAllRunes:\ngot %#x\nwant %#x", i, j, gotRunes, wantRunes) } } // Test Caret.ReadRune. { c := f.NewCaret() gotRunes, err := readAllRunes(gotRunesBuf[:0], c) c.Close() if err != nil { t.Fatalf("i=%d, j=%d: Caret readAllRunes: %v", i, j, err) } if !runesEqual(gotRunes, wantRunes) { t.Fatalf("i=%d, j=%d: Caret readAllRunes:\ngot %#x\nwant %#x", i, j, gotRunes, wantRunes) } } } } } func TestDelete(t *testing.T) { rng := rand.New(rand.NewSource(1)) gotBytesBuf := make([]byte, 0, len(iRobot)) wantBytesBuf := make([]byte, 0, len(iRobot)) for i := 0; i < 100; i++ { f := iRobotFrame(rng.Intn(len(iRobot) + 1)) c := f.NewCaret() defer c.Close() wantBytes := append(wantBytesBuf[:0], iRobot...) for j := 0; j < 8; j++ { // Delete the text in the byte range [x, y). x, y := rngIntPair(rng, len(wantBytes)+1) got, want := 0, y-x if rng.Intn(2) == 0 { c.Seek(int64(x), SeekSet) got = c.Delete(Forwards, want) } else { c.Seek(int64(y), SeekSet) got = c.Delete(Backwards, want) } if err := checkInvariants(f); err != nil { t.Fatalf("i=%d, j=%d, x=%d, y=%d: %v", i, j, x, y, err) } if got != want { t.Fatalf("i=%d, j=%d, x=%d, y=%d: Delete: got %d, want %d", i, j, x, y, got, want) } gotBytes := readAllText(gotBytesBuf[:0], f) wantBytes = append(wantBytes[:x], wantBytes[y:]...) if !bytes.Equal(gotBytes, wantBytes) { t.Fatalf("i=%d, j=%d, x=%d, y=%d:\ngot %q\nwant %q", i, j, x, y, gotBytes, wantBytes) } } } } func TestDeleteRunes(t *testing.T) { rng := rand.New(rand.NewSource(1)) gotBytesBuf := make([]byte, 0, len(iRobot)) wantRunesBuf := make([]rune, 0, len(iRobot)) wantBytesBuf := make([]byte, 0, len(iRobot)) for i := 0; i < 100; i++ { f := iRobotFrame(rng.Intn(len(iRobot) + 1)) c := f.NewCaret() defer c.Close() wantRunes := append(wantRunesBuf[:0], iRobotRunes...) wantBytes := append(wantBytesBuf[:0], iRobot...) for j := 0; j < 8; j++ { xR, yR := rngIntPair(rng, len(wantRunes)+1) xB := runesLen(wantRunes[:xR]) yB := runesLen(wantRunes[:yR]) // Delete the text in the byte range [xB, yB). gotR, gotB, wantR, wantB := 0, 0, yR-xR, yB-xB if rng.Intn(2) == 0 { c.Seek(int64(xB), SeekSet) gotR, gotB = c.DeleteRunes(Forwards, wantR) } else { c.Seek(int64(yB), SeekSet) gotR, gotB = c.DeleteRunes(Backwards, wantR) } if err := checkInvariants(f); err != nil { t.Fatalf("i=%d, j=%d, xR=%d, xB=%d, yR=%d, yB=%d: %v", i, j, xR, xB, yR, yB, err) } if gotR != wantR || gotB != wantB { t.Fatalf("i=%d, j=%d, xR=%d, xB=%d, yR=%d, yB=%d: Delete: got %d runes, %d bytes, want %d, %d", i, j, xR, xB, yR, yB, gotR, gotB, wantR, wantB) } gotBytes := readAllText(gotBytesBuf[:0], f) wantRunes = append(wantRunes[:xR], wantRunes[yR:]...) wantBytes = append(wantBytes[:xB], wantBytes[yB:]...) if !bytes.Equal(gotBytes, wantBytes) { t.Fatalf("i=%d, j=%d, xR=%d, xB=%d, yR=%d, yB=%d:\ngot %q\nwant %q", i, j, xR, xB, yR, yB, gotBytes, wantBytes) } } } } func TestDeleteTooMuch(t *testing.T) { if len(iRobot) < 17+15 { t.Fatal("iRobot string is too short") } f := iRobotFrame(10) c := f.NewCaret() defer c.Close() c.Seek(-15, SeekEnd) if got, want := c.Delete(Forwards, 16), 15; got != want { t.Errorf("Delete Forwards: got %d, want %d", got, want) } c.Seek(+17, SeekSet) if got, want := c.Delete(Backwards, 18), 17; got != want { t.Errorf("Delete Backwards: got %d, want %d", got, want) } got := string(readAllText(nil, f)) want := iRobot[17 : len(iRobot)-15] if got != want { t.Errorf("\ngot %q\nwant %q", got, want) } } func TestDeleteRunesTooMuch(t *testing.T) { if len(iRobotRunes) < 30+24 { t.Fatal("iRobotRunes []rune is too short") } prefix := iRobotRunes[:30] suffix := iRobotRunes[len(iRobotRunes)-24:] if allASCII(prefix) { t.Fatal("prefix contains no non-ASCII runes") } if allASCII(suffix) { t.Fatal("suffix contains no non-ASCII runes") } prefixB := runesLen(prefix) suffixB := runesLen(suffix) f := iRobotFrame(10) c := f.NewCaret() defer c.Close() c.Seek(-int64(suffixB), SeekEnd) wantR, wantB := len(suffix), suffixB if gotR, gotB := c.DeleteRunes(Forwards, 25); gotR != wantR || gotB != wantB { t.Errorf("DeleteRunes Forwards: got %d runes, %d bytes, want %d, %d", gotR, gotB, wantR, wantB) } c.Seek(+int64(prefixB), SeekSet) wantR, wantB = len(prefix), prefixB if gotR, gotB := c.DeleteRunes(Backwards, 31); gotR != wantR || gotB != wantB { t.Errorf("DeleteRunes Backwards: got %d runes, %d bytes, want %d, %d", gotR, gotB, wantR, wantB) } got := string(readAllText(nil, f)) want := iRobot[prefixB : len(iRobot)-suffixB] if got != want { t.Errorf("\ngot %q\nwant %q", got, want) } } func TestCompactText(t *testing.T) { f := new(Frame) f.SetFace(toyFace{}) f.SetMaxWidth(fixed.I(80)) c := f.NewCaret() defer c.Close() const n, abc64 = 1024, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\n " rng := rand.New(rand.NewSource(1)) buf := make([]byte, n) for i := range buf { buf[i] = abc64[rng.Intn(len(abc64))] } written, wantLen := 0, 0 for i := 0; written < 131072; i++ { if rng.Intn(10) != 0 { x, y := rngIntPair(rng, len(buf)+1) c.Seek(int64(rng.Intn(f.Len()+1)), SeekSet) c.Write(buf[x:y]) written += y - x wantLen += y - x } if rng.Intn(10) != 0 { x, y := rngIntPair(rng, wantLen+1) c.Seek(int64(x), SeekSet) c.Delete(Forwards, y-x) wantLen -= y - x } // Calculate cached counts, some of the time. if rng.Intn(3) == 0 { f.ParagraphCount() } if rng.Intn(4) == 0 { f.LineCount() } if rng.Intn(5) == 0 { c.Seek(int64(rng.Intn(f.Len()+1)), SeekSet) f.paragraphs[c.p].LineCount(f) } if err := checkInvariants(f); err != nil { t.Fatalf("i=%d: %v", i, err) } if gotLen := f.Len(); gotLen != wantLen { t.Fatalf("i=%d: length: got %d, want %d", i, gotLen, wantLen) } } if len(f.text) == written { t.Fatal("f.text was not compacted") } } func TestMergeIntoOneLine(t *testing.T) { rng := rand.New(rand.NewSource(1)) for i := 0; i < 100; i++ { f := new(Frame) f.SetMaxWidth(fixed.I(100)) if !f.initialized() { f.initialize() } p := f.firstP pp := &f.paragraphs[p] l := pp.firstL ll := &f.lines[l] b := ll.firstB bb := &f.boxes[b] // Make some Lines and Boxes. wantNUsedBoxes := 0 prevIJ := int32(0) emptyRun := true for j := 0; ; j++ { length := rng.Intn(3) bb.i = prevIJ prevIJ += int32(length) bb.j = prevIJ f.len += length if length > 0 && emptyRun { emptyRun = false wantNUsedBoxes++ } if rng.Intn(20) == 0 { break } if rng.Intn(4) == 0 { // Make a new Box on a new Line. l1, realloc := f.newLine() if realloc { ll = &f.lines[l] } ll1 := &f.lines[l1] ll.next = l1 ll1.prev = l l, ll = l1, ll1 ll.firstB, _ = f.newBox() b = ll.firstB bb = &f.boxes[b] } else { // Make a new Box on the same Line. b1, realloc := f.newBox() if realloc { bb = &f.boxes[b] } bb1 := &f.boxes[b1] bb.next = b1 bb1.prev = b b, bb = b1, bb1 } if rng.Intn(5) == 0 { // Put an i/j gap between this run and the previous one. prevIJ += 1 + int32(rng.Intn(5)) emptyRun = true } } // We normally remove all empty Boxes. However, if there is no text // whatsoever, we still need one Box. if f.len == 0 { if wantNUsedBoxes != 0 { t.Fatalf("i=%d: no text: wantNUsedBoxes: got %d, want 0", i, wantNUsedBoxes) } wantNUsedBoxes = 1 } // Make f.text long enough to hold all of the Boxes' i's and j's. f.text = make([]byte, prevIJ) for i := range f.text { f.text[i] = byte('a' + i%16) } // Do the merge. if err := checkSomeInvariants(f, ignoreInvariantEmptyBoxes); err != nil { t.Fatalf("i=%d: before: %v", i, err) } f.mergeIntoOneLine(p) if err := checkInvariants(f); err != nil { t.Fatalf("i=%d: after: %v", i, err) } // Check that there is only one Line in use. if n := f.nUsedLines(); n != 1 { t.Errorf("i=%d: nUsedLines: got %d, want %d", i, n, 1) } // Check the number of Boxes in use. if n := f.nUsedBoxes(); n != wantNUsedBoxes { t.Errorf("i=%d: nUsedBoxes: got %d, want %d", i, n, wantNUsedBoxes) } } } // TestMultipleCarets tests multiple Carets all seeking, reading, writing and // deleting text. func TestMultipleCarets(t *testing.T) { f := new(Frame) f.SetFace(toyFace{}) f.SetMaxWidth(fixed.I(10)) rng := rand.New(rand.NewSource(1)) gotBuf := make([]byte, 0, 10000) want := make([]byte, 0, 10000) positions := map[*Caret]int{} carets := []*Caret{} defer func() { for _, c := range carets { c.Close() } }() for i := 0; i < 1000; i++ { if err := checkInvariants(f); err != nil { t.Fatalf("i=%d: %v", i, err) } if len(carets) == 0 || rng.Intn(20) == 0 { c := f.NewCaret() carets = append(carets, c) positions[c] = 0 continue } caretsIndex := rng.Intn(len(carets)) c := carets[caretsIndex] if rng.Intn(40) == 0 { c.Close() carets[caretsIndex] = carets[len(carets)-1] carets[len(carets)-1] = nil carets = carets[:len(carets)-1] delete(positions, c) continue } // Seek, read, delete or write, with a slight bias to writing. switch rng.Intn(5) { case 0: // Seek. p := rng.Intn(len(want) + 1) c.Seek(int64(p), SeekSet) positions[c] = p case 1: // ReadByte. gotB, gotErr := c.ReadByte() wantB, wantErr := byte(0), io.EOF if p := positions[c]; p < len(want) { wantB, wantErr = want[p], nil positions[c] = p + 1 } if gotB != wantB || gotErr != wantErr { t.Fatalf("i=%d: ReadByte: got %d, %v, want %d, %v", i, gotB, gotErr, wantB, wantErr) } case 2: // Delete. dir := Direction(rng.Intn(2) == 0) nBytes, dBytes := rng.Intn(len(want)), 0 pos, x, y := positions[c], 0, 0 if dir == Forwards { dBytes = min(nBytes, len(want)-pos) x, y = pos, pos+dBytes } else { dBytes = min(nBytes, pos) x, y = pos-dBytes, pos } if d := c.Delete(dir, nBytes); d != dBytes { t.Fatalf("i=%d: Delete: got %d bytes, want %d", i, d, dBytes) } want = append(want[:x], want[y:]...) for _, cc := range carets { if cc == c { positions[cc] = x continue } switch p := positions[cc]; { case p <= x: // No-op. case p <= y: positions[cc] = x default: positions[cc] -= dBytes } } default: // Write. i, j := rngIntPair(rng, len(iRobotBytes)) s := iRobotBytes[i:j] c.Write(s) pos := positions[c] want = insert(want, s, pos) for _, cc := range carets { if cc == c || positions[cc] > pos { positions[cc] += j - i } } } badPos := false for ci, cc := range carets { if int(cc.pos) != positions[cc] { badPos = true t.Errorf("i=%d, ci=%d: position: got %d, want %d", i, ci, cc.pos, positions[cc]) } } if badPos { t.Fatalf("i=%d: there were inconsistent Caret positions", i) } if got := readAllText(gotBuf[:0], f); !bytes.Equal(got, want) { t.Fatalf("i=%d:\ngot % x\nwant % x", i, got, want) } } if err := checkInvariants(f); err != nil { t.Fatal(err) } } type ijRange struct { i, j int32 } type byI []ijRange func (b byI) Len() int { return len(b) } func (b byI) Less(x, y int) bool { return b[x].i < b[y].i } func (b byI) Swap(x, y int) { b[x], b[y] = b[y], b[x] } const ( ignoreInvariantEmptyBoxes = 1 << iota ) func checkInvariants(f *Frame) error { return checkSomeInvariants(f, 0) } func checkSomeInvariants(f *Frame, ignoredInvariants uint32) error { const infinity = 1e6 if !f.initialized() { if !reflect.DeepEqual(*f, Frame{}) { return fmt.Errorf("uninitialized Frame is not zero-valued") } return nil } // Iterate through the Paragraphs, Lines and Boxes. Check that every first // child has no previous sibling, and no child is the first child of // multiple parents. Also check the cached Paragraph and Line counts. nUsedParagraphs, nUsedLines, nUsedBoxes := 0, 0, 0 { firstLines := map[int32]bool{} firstBoxes := map[int32]bool{} p := f.firstP if p == 0 { return fmt.Errorf("firstP is zero") } if x := f.paragraphs[p].prev; x != 0 { return fmt.Errorf("first Paragraph %d's prev: got %d, want 0", p, x) } for ; p != 0; p = f.paragraphs[p].next { l := f.paragraphs[p].firstL if l == 0 { return fmt.Errorf("paragraphs[%d].firstL is zero", p) } if x := f.lines[l].prev; x != 0 { return fmt.Errorf("first Line %d's prev: got %d, want 0", l, x) } if firstLines[l] { return fmt.Errorf("duplicate first Line %d", l) } firstLines[l] = true nUsedLinesThisParagraph := 0 for ; l != 0; l = f.lines[l].next { b := f.lines[l].firstB if b == 0 { return fmt.Errorf("lines[%d].firstB is zero", l) } if x := f.boxes[b].prev; x != 0 { return fmt.Errorf("first Box %d's prev: got %d, want 0", b, x) } if firstBoxes[b] { return fmt.Errorf("duplicate first Box %d", b) } firstBoxes[b] = true for ; b != 0; b = f.boxes[b].next { nUsedBoxes++ if nUsedBoxes >= infinity { return fmt.Errorf("too many used Boxes (infinite loop?)") } } nUsedLines++ nUsedLinesThisParagraph++ } nUsedParagraphs++ if n := int(f.paragraphs[p].cachedLineCountPlus1 - 1); n >= 0 && n != nUsedLinesThisParagraph { return fmt.Errorf("Paragraph cached Line count: got %d, want %d", n, nUsedLinesThisParagraph) } } if n := int(f.cachedLineCountPlus1 - 1); n >= 0 && n != nUsedLines { return fmt.Errorf("Frame cached Line count: got %d, want %d", n, nUsedLines) } if n := int(f.cachedParagraphCountPlus1 - 1); n >= 0 && n != nUsedParagraphs { return fmt.Errorf("Frame cached Paragraph count: got %d, want %d", n, nUsedParagraphs) } } // Check that only the last Box can be empty. if ignoredInvariants&ignoreInvariantEmptyBoxes == 0 { for p := f.firstP; p != 0; { nextP := f.paragraphs[p].next for l := f.paragraphs[p].firstL; l != 0; { nextL := f.lines[l].next for b := f.lines[l].firstB; b != 0; { nextB := f.boxes[b].next emptyBox := f.boxes[b].i == f.boxes[b].j lastBox := nextP == 0 && nextL == 0 && nextB == 0 if emptyBox && !lastBox { return fmt.Errorf("boxes[%d] is empty, but isn't the last Box", b) } b = nextB } l = nextL } p = nextP } } // Check the paragraphs. for p, pp := range f.paragraphs { if p == 0 { if pp != (Paragraph{}) { return fmt.Errorf("paragraphs[0] is a non-zero Paragraph: %v", pp) } continue } if pp.next < 0 || len(f.paragraphs) <= int(pp.next) { return fmt.Errorf("invalid paragraphs[%d].next: got %d, want in [0, %d)", p, pp.next, len(f.paragraphs)) } if len(f.paragraphs) <= int(pp.prev) { return fmt.Errorf("invalid paragraphs[%d].prev: got %d, want in [0, %d)", p, pp.prev, len(f.paragraphs)) } if pp.prev < 0 { // The Paragraph is in the free-list, which is checked separately below. continue } if pp.next != 0 && f.paragraphs[pp.next].prev != int32(p) { return fmt.Errorf("invalid links: paragraphs[%d].next=%d, paragraphs[%d].prev=%d", p, pp.next, pp.next, f.paragraphs[pp.next].prev) } if pp.prev != 0 && f.paragraphs[pp.prev].next != int32(p) { return fmt.Errorf("invalid links: paragraphs[%d].prev=%d, paragraphs[%d].next=%d", p, pp.prev, pp.prev, f.paragraphs[pp.prev].next) } } // Check the paragraphs' free-list. nFreeParagraphs := 0 for p := f.freeP; p != 0; nFreeParagraphs++ { if nFreeParagraphs >= infinity { return fmt.Errorf("Paragraph free-list is too long (infinite loop?)") } if p < 0 || len(f.paragraphs) <= int(p) { return fmt.Errorf("invalid Paragraph free-list index: got %d, want in [0, %d)", p, len(f.paragraphs)) } pp := &f.paragraphs[p] if pp.prev >= 0 { return fmt.Errorf("paragraphs[%d] is an invalid free-list element: %#v", p, *pp) } p = pp.next } // Check the lines. for l, ll := range f.lines { if l == 0 { if ll != (Line{}) { return fmt.Errorf("lines[0] is a non-zero Line: %v", ll) } continue } if ll.next < 0 || len(f.lines) <= int(ll.next) { return fmt.Errorf("invalid lines[%d].next: got %d, want in [0, %d)", l, ll.next, len(f.lines)) } if len(f.lines) <= int(ll.prev) { return fmt.Errorf("invalid lines[%d].prev: got %d, want in [0, %d)", l, ll.prev, len(f.lines)) } if ll.prev < 0 { // The Line is in the free-list, which is checked separately below. continue } if ll.next != 0 && f.lines[ll.next].prev != int32(l) { return fmt.Errorf("invalid links: lines[%d].next=%d, lines[%d].prev=%d", l, ll.next, ll.next, f.lines[ll.next].prev) } if ll.prev != 0 && f.lines[ll.prev].next != int32(l) { return fmt.Errorf("invalid links: lines[%d].prev=%d, lines[%d].next=%d", l, ll.prev, ll.prev, f.lines[ll.prev].next) } } // Check the lines' free-list. nFreeLines := 0 for l := f.freeL; l != 0; nFreeLines++ { if nFreeLines >= infinity { return fmt.Errorf("Line free-list is too long (infinite loop?)") } if l < 0 || len(f.lines) <= int(l) { return fmt.Errorf("invalid Line free-list index: got %d, want in [0, %d)", l, len(f.lines)) } ll := &f.lines[l] if ll.prev >= 0 { return fmt.Errorf("lines[%d] is an invalid free-list element: %#v", l, *ll) } l = ll.next } // Check the boxes. for b, bb := range f.boxes { if b == 0 { if bb != (Box{}) { return fmt.Errorf("boxes[0] is a non-zero Box: %v", bb) } continue } if bb.next < 0 || len(f.boxes) <= int(bb.next) { return fmt.Errorf("invalid boxes[%d].next: got %d, want in [0, %d)", b, bb.next, len(f.boxes)) } if len(f.boxes) <= int(bb.prev) { return fmt.Errorf("invalid boxes[%d].prev: got %d, want in [0, %d)", b, bb.prev, len(f.boxes)) } if bb.prev < 0 { // The Box is in the free-list, which is checked separately below. continue } if bb.next != 0 && f.boxes[bb.next].prev != int32(b) { return fmt.Errorf("invalid links: boxes[%d].next=%d, boxes[%d].prev=%d", b, bb.next, bb.next, f.boxes[bb.next].prev) } if bb.prev != 0 && f.boxes[bb.prev].next != int32(b) { return fmt.Errorf("invalid links: boxes[%d].prev=%d, boxes[%d].next=%d", b, bb.prev, bb.prev, f.boxes[bb.prev].next) } if 0 > bb.i || bb.i > bb.j || bb.j > int32(len(f.text)) { return fmt.Errorf("invalid boxes[%d] i/j range: i=%d, j=%d, len=%d", b, bb.i, bb.j, len(f.text)) } } // Check the boxes' free-list. nFreeBoxes := 0 for b := f.freeB; b != 0; nFreeBoxes++ { if nFreeBoxes >= infinity { return fmt.Errorf("Box free-list is too long (infinite loop?)") } if b < 0 || len(f.boxes) <= int(b) { return fmt.Errorf("invalid Box free-list index: got %d, want in [0, %d)", b, len(f.boxes)) } bb := &f.boxes[b] if bb.i != 0 || bb.j != 0 || bb.prev >= 0 { return fmt.Errorf("boxes[%d] is an invalid free-list element: %#v", b, *bb) } b = bb.next } // Check that the boxes' i:j ranges do not overlap, and their total length // equals f.len. nText, ijRanges := 0, []ijRange{} for _, bb := range f.boxes { if bb.i < bb.j { nText += int(bb.j - bb.i) ijRanges = append(ijRanges, ijRange{i: bb.i, j: bb.j}) } } sort.Sort(byI(ijRanges)) for x := range ijRanges { if x == 0 { continue } if ijRanges[x-1].j > ijRanges[x].i { return fmt.Errorf("overlapping Box i:j ranges: %v and %v", ijRanges[x-1], ijRanges[x]) } } if nText != f.len { return fmt.Errorf("text length: got %d, want %d", nText, f.len) } // Check that every Paragraph, Line and Box, other than the 0th of each, is // either used or free. if len(f.paragraphs) != 1+nUsedParagraphs+nFreeParagraphs { return fmt.Errorf("#paragraphs (%d) != 1 + #used (%d) + #free (%d)", len(f.paragraphs), nUsedParagraphs, nFreeParagraphs) } if len(f.lines) != 1+nUsedLines+nFreeLines { return fmt.Errorf("#lines (%d) != 1 + #used (%d) + #free (%d)", len(f.lines), nUsedLines, nFreeLines) } if len(f.boxes) != 1+nUsedBoxes+nFreeBoxes { return fmt.Errorf("#boxes (%d) != 1 + #used (%d) + #free (%d)", len(f.boxes), nUsedBoxes, nFreeBoxes) } // Check that each Caret's pos is in the Frame's 0:len range. If the // Caret's cached {p,l,b,k} values are valid, also check that the Caret's k // is in its Box's i:j range, and its Box b is in its Line l is in its // Paragraph p. for i, c := range f.carets { if c.pos < 0 || f.len < int(c.pos) { return fmt.Errorf("caret[%d]: pos %d outside range [0, %d]", i, c.pos, f.len) } if c.seqNum != f.seqNum { continue } if c.b < 1 || len(f.boxes) < int(c.b) { return fmt.Errorf("caret[%d]: c.b %d outside range [1, %d]", i, c.b, len(f.boxes)) } bb := &f.boxes[c.b] if c.k < bb.i || bb.j < c.k { return fmt.Errorf("caret[%d]: c.k %d outside range [%d, %d]", i, c.k, bb.i, bb.j) } if c.l < 1 || len(f.lines) < int(c.l) { return fmt.Errorf("caret[%d]: c.l %d outside range [1, %d]", i, c.l, len(f.lines)) } if !f.lines[c.l].contains(f, c.b) { return fmt.Errorf("caret[%d]: Line %d does not contain Box %d", i, c.l, c.b) } if c.p < 1 || len(f.paragraphs) < int(c.p) { return fmt.Errorf("caret[%d]: c.p %d outside range [1, %d]", i, c.p, len(f.paragraphs)) } if !f.paragraphs[c.p].contains(f, c.l) { return fmt.Errorf("caret[%d]: Paragraph %d does not contain Line %d", i, c.p, c.l) } } return nil } func (f *Frame) nUsedParagraphs() (n int) { for p, pp := range f.paragraphs { // The 0th index is a special case. A negative prev means that the // Paragraph is in the free-list. if p != 0 && pp.prev >= 0 { n++ } } return n } func (f *Frame) nUsedLines() (n int) { for l, ll := range f.lines { // The 0th index is a special case. A negative prev means that the // Line is in the free-list. if l != 0 && ll.prev >= 0 { n++ } } return n } func (f *Frame) nUsedBoxes() (n int) { for b, bb := range f.boxes { // The 0th index is a special case. A negative prev means that the // Box is in the free-list. if b != 0 && bb.prev >= 0 { n++ } } return n } func (p *Paragraph) contains(f *Frame, l int32) bool { for x := p.firstL; x != 0; x = f.lines[x].next { if x == l { return true } } return false } func (l *Line) contains(f *Frame, b int32) bool { for x := l.firstB; x != 0; x = f.boxes[x].next { if x == b { return true } } return false } // dump is used for debugging. func dump(w io.Writer, f *Frame) { for p := f.FirstParagraph(); p != nil; p = p.Next(f) { for l := p.FirstLine(f); l != nil; l = l.Next(f) { for b := l.FirstBox(f); b != nil; b = b.Next(f) { fmt.Fprintf(w, "[%s]", b.TrimmedText(f)) } fmt.Fprintln(w) } } }