1
1
mirror of https://github.com/walles/moar.git synced 2024-11-30 02:34:13 +03:00
moar/m/reader_test.go

337 lines
8.7 KiB
Go
Raw Normal View History

package m
2019-06-14 07:49:27 +03:00
import (
2019-06-15 18:13:10 +03:00
"io/ioutil"
2019-06-16 22:54:25 +03:00
"math"
"os/exec"
2019-06-14 07:49:27 +03:00
"path"
"runtime"
"strconv"
2019-06-22 00:24:53 +03:00
"strings"
2019-06-15 18:13:10 +03:00
"testing"
2019-06-22 00:24:53 +03:00
"gotest.tools/assert"
2019-06-14 07:49:27 +03:00
)
func testGetLineCount(t *testing.T, reader *Reader) {
2019-07-10 01:21:36 +03:00
if strings.Contains(*reader.name, "compressed") {
// We are no good at counting lines of compressed files, never mind
return
}
cmd := exec.Command("wc", "-l", *reader.name)
output, err := cmd.CombinedOutput()
if err != nil {
t.Error("Error calling wc -l to count lines of", *reader.name, err)
}
wcNumberString := strings.Split(strings.TrimSpace(string(output)), " ")[0]
fileLineCount, err := strconv.Atoi(wcNumberString)
if err != nil {
t.Error("Error counting lines of", *reader.name, err)
}
if strings.HasSuffix(*reader.name, "/line-without-newline.txt") {
// "wc -l" thinks this file contains zero lines
fileLineCount = 1
}
2019-07-10 01:21:36 +03:00
if reader.GetLineCount() != fileLineCount {
t.Errorf("Got %d lines but expected %d: <%s>",
reader.GetLineCount(), fileLineCount, *reader.name)
}
}
func testGetLines(t *testing.T, reader *Reader) {
2019-06-22 00:24:53 +03:00
t.Logf("Testing file: %s...", *reader.name)
lines := reader.GetLines(1, 10)
if len(lines.lines) > 10 {
t.Errorf("Asked for 10 lines, got too many: %d", len(lines.lines))
}
if len(lines.lines) < 10 {
2019-07-10 01:21:36 +03:00
// No good plan for how to test short files, more than just
// querying them, which we just did
return
}
// Test clipping at the end
2019-06-16 22:54:25 +03:00
lines = reader.GetLines(math.MaxInt32, 10)
if len(lines.lines) != 10 {
2019-06-14 07:49:27 +03:00
t.Errorf("Asked for 10 lines but got %d", len(lines.lines))
return
}
2019-06-14 07:59:19 +03:00
startOfLastSection := lines.firstLineOneBased
lines = reader.GetLines(startOfLastSection, 10)
if lines.firstLineOneBased != startOfLastSection {
t.Errorf("Expected start line %d when asking for the last 10 lines, got %d",
startOfLastSection, lines.firstLineOneBased)
return
}
2019-06-16 22:54:25 +03:00
if len(lines.lines) != 10 {
t.Errorf("Expected 10 lines when asking for the last 10 lines, got %d",
len(lines.lines))
return
}
2019-06-14 07:59:19 +03:00
2019-06-15 18:13:10 +03:00
lines = reader.GetLines(startOfLastSection+1, 10)
2019-06-14 07:59:19 +03:00
if lines.firstLineOneBased != startOfLastSection {
t.Errorf("Expected start line %d when asking for the last+1 10 lines, got %d",
startOfLastSection, lines.firstLineOneBased)
return
}
2019-06-16 22:54:25 +03:00
if len(lines.lines) != 10 {
t.Errorf("Expected 10 lines when asking for the last+1 10 lines, got %d",
len(lines.lines))
return
}
2019-06-14 07:59:19 +03:00
2019-06-15 18:13:10 +03:00
lines = reader.GetLines(startOfLastSection-1, 10)
if lines.firstLineOneBased != startOfLastSection-1 {
2019-06-14 07:59:19 +03:00
t.Errorf("Expected start line %d when asking for the last-1 10 lines, got %d",
startOfLastSection, lines.firstLineOneBased)
return
}
2019-06-16 22:54:25 +03:00
if len(lines.lines) != 10 {
t.Errorf("Expected 10 lines when asking for the last-1 10 lines, got %d",
len(lines.lines))
return
}
}
func getSamplesDir() string {
2019-06-14 07:49:27 +03:00
// From: https://coderwall.com/p/_fmbug/go-get-path-to-current-file
_, filename, _, ok := runtime.Caller(0)
if !ok {
panic("Getting current filename failed")
}
2019-06-23 22:30:11 +03:00
return path.Join(path.Dir(filename), "../sample-files")
}
2019-06-14 07:49:27 +03:00
func getTestFiles() []string {
files, err := ioutil.ReadDir(getSamplesDir())
2019-06-15 18:13:10 +03:00
if err != nil {
2019-06-14 07:49:27 +03:00
panic(err)
2019-06-15 18:13:10 +03:00
}
2019-06-14 07:49:27 +03:00
var filenames []string
2019-06-15 18:13:10 +03:00
for _, file := range files {
filenames = append(filenames, "../sample-files/"+file.Name())
2019-06-14 07:49:27 +03:00
}
return filenames
}
func TestGetLines(t *testing.T) {
for _, file := range getTestFiles() {
if strings.HasSuffix(file, ".xz") {
_, err := exec.LookPath("xz")
if err != nil {
t.Log("Not testing xz compressed file, xz not found in $PATH: ", file)
continue
}
}
reader, err := NewReaderFromFilename(file)
if err != nil {
t.Errorf("Error opening file <%s>: %s", file, err.Error())
continue
}
if err := reader._Wait(); err != nil {
t.Errorf("Error reading file <%s>: %s", file, err.Error())
continue
}
testGetLines(t, reader)
testGetLineCount(t, reader)
testHighlightingLineCount(t, file)
}
2019-06-15 18:13:10 +03:00
}
2019-06-22 00:24:53 +03:00
func testHighlightingLineCount(t *testing.T, filenameWithPath string) {
// This won't work on compressed files
if strings.HasSuffix(filenameWithPath, ".xz") {
return
}
if strings.HasSuffix(filenameWithPath, ".bz2") {
return
}
if strings.HasSuffix(filenameWithPath, ".gz") {
return
}
// Load the unformatted file
rawBytes, err := ioutil.ReadFile(filenameWithPath)
if err != nil {
panic(err)
}
rawContents := string(rawBytes)
// Count its lines
rawLinefeedsCount := strings.Count(rawContents, "\n")
rawRunes := []rune(rawContents)
rawFileEndsWithNewline := true // Special case empty files
if len(rawRunes) > 0 {
rawFileEndsWithNewline = rawRunes[len(rawRunes)-1] == '\n'
}
rawLinesCount := rawLinefeedsCount
if !rawFileEndsWithNewline {
rawLinesCount += 1
}
// Then load the same file using one of our Readers
reader, err := NewReaderFromFilename(filenameWithPath)
if err != nil {
panic(err)
}
err = reader._Wait()
if err != nil {
panic(err)
}
highlightedLinesCount := reader.GetLineCount()
assert.Check(t, rawLinesCount == highlightedLinesCount)
}
func TestGetLongLine(t *testing.T) {
file := "../sample-files/very-long-line.txt"
reader, err := NewReaderFromFilename(file)
if err != nil {
panic(err)
}
if err := reader._Wait(); err != nil {
panic(err)
}
lines := reader.GetLines(1, 5)
assert.Equal(t, lines.firstLineOneBased, 1)
assert.Equal(t, len(lines.lines), 1)
line := lines.lines[0]
Parse lines on demand and only once This improves line processing performance by 40%. Fixes #36. diff --git m/ansiTokenizer.go m/ansiTokenizer.go index d991e23..056a227 100644 --- m/ansiTokenizer.go +++ m/ansiTokenizer.go @@ -23,6 +23,44 @@ type Token struct { Style tcell.Style } +// A Line represents a line of text that can / will be paged +type Line struct { + raw *string + plain *string + tokens []Token +} + +// NewLine creates a new Line from a (potentially ANSI / man page formatted) string +func NewLine(raw string) *Line { + return &Line{ + raw: &raw, + plain: nil, + tokens: nil, + } +} + +// Tokens returns a representation of the string split into styled tokens +func (line *Line) Tokens() []Token { + line.parse() + return line.tokens +} + +// Plain returns a plain text representation of the initial string +func (line *Line) Plain() string { + line.parse() + return *line.plain +} + +func (line *Line) parse() { + if line.raw == nil { + // Already done + return + } + + line.tokens, line.plain = tokensFromString(*line.raw) + line.raw = nil +} + // SetManPageFormatFromEnv parses LESS_TERMCAP_xx environment variables and // adapts the moar output accordingly. func SetManPageFormatFromEnv() { diff --git m/pager.go m/pager.go index 412e05b..98efa9a 100644 --- m/pager.go +++ m/pager.go @@ -111,7 +111,7 @@ func NewPager(r *Reader) *Pager { } } -func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line string) { +func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line *Line) { screenWidth, _ := p.screen.Size() prefixLength := 0 @@ -138,7 +138,7 @@ func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNum func createScreenLine( stringIndexAtColumnZero int, screenColumnsCount int, - line string, + line *Line, search *regexp.Regexp, ) []Token { var returnMe []Token @@ -152,14 +152,14 @@ func createScreenLine( searchHitDelta = -1 } - tokens, plainString := tokensFromString(line) - if stringIndexAtColumnZero >= len(tokens) { + if stringIndexAtColumnZero >= len(line.Tokens()) { // Nothing (more) to display, never mind return returnMe } - matchRanges := getMatchRanges(plainString, search) - for _, token := range tokens[stringIndexAtColumnZero:] { + plain := line.Plain() + matchRanges := getMatchRanges(&plain, search) + for _, token := range line.Tokens()[stringIndexAtColumnZero:] { if len(returnMe) >= screenColumnsCount { // We are trying to add a character to the right of the screen. // Indicate that this line continues to the right. @@ -232,7 +232,8 @@ func (p *Pager) _AddLines(spinner string) { // This happens when we're done eofSpinner = "---" } - p._AddLine(nil, 0, screenLineNumber, _EofMarkerFormat+eofSpinner) + spinnerLine := NewLine(_EofMarkerFormat + eofSpinner) + p._AddLine(nil, 0, screenLineNumber, spinnerLine) switch p.mode { case _Searching: @@ -329,8 +330,8 @@ func (p *Pager) _FindFirstHitLineOneBased(firstLineOneBased int, backwards bool) return nil } - _, lineText := tokensFromString(*line) - if p.searchPattern.MatchString(*lineText) { + lineText := line.Plain() + if p.searchPattern.MatchString(lineText) { return &lineNumber } diff --git m/pager_test.go m/pager_test.go index 65fa3c2..ce0f79b 100644 --- m/pager_test.go +++ m/pager_test.go @@ -265,13 +265,15 @@ func assertTokenRangesEqual(t *testing.T, actual []Token, expected []Token) { } func TestCreateScreenLineBase(t *testing.T) { - line := createScreenLine(0, 3, "", nil) - assert.Assert(t, len(line) == 0) + line := NewLine("") + screenLine := createScreenLine(0, 3, line, nil) + assert.Assert(t, len(screenLine) == 0) } func TestCreateScreenLineOverflowRight(t *testing.T) { - line := createScreenLine(0, 3, "012345", nil) - assertTokenRangesEqual(t, line, []Token{ + line := NewLine("012345") + screenLine := createScreenLine(0, 3, line, nil) + assertTokenRangesEqual(t, screenLine, []Token{ createExpectedCell('0', tcell.StyleDefault), createExpectedCell('1', tcell.StyleDefault), createExpectedCell('>', tcell.StyleDefault.Reverse(true)), @@ -279,8 +281,9 @@ func TestCreateScreenLineOverflowRight(t *testing.T) { } func TestCreateScreenLineUnderflowLeft(t *testing.T) { - line := createScreenLine(1, 3, "012", nil) - assertTokenRangesEqual(t, line, []Token{ + line := NewLine("012") + screenLine := createScreenLine(1, 3, line, nil) + assertTokenRangesEqual(t, screenLine, []Token{ createExpectedCell('<', tcell.StyleDefault.Reverse(true)), createExpectedCell('1', tcell.StyleDefault), createExpectedCell('2', tcell.StyleDefault), @@ -293,8 +296,9 @@ func TestCreateScreenLineSearchHit(t *testing.T) { panic(err) } - line := createScreenLine(0, 3, "abc", pattern) - assertTokenRangesEqual(t, line, []Token{ + line := NewLine("abc") + screenLine := createScreenLine(0, 3, line, pattern) + assertTokenRangesEqual(t, screenLine, []Token{ createExpectedCell('a', tcell.StyleDefault), createExpectedCell('b', tcell.StyleDefault.Reverse(true)), createExpectedCell('c', tcell.StyleDefault), @@ -307,8 +311,9 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) { panic(err) } - line := createScreenLine(0, 3, "åäö", pattern) - assertTokenRangesEqual(t, line, []Token{ + line := NewLine("åäö") + screenLine := createScreenLine(0, 3, line, pattern) + assertTokenRangesEqual(t, screenLine, []Token{ createExpectedCell('å', tcell.StyleDefault), createExpectedCell('ä', tcell.StyleDefault.Reverse(true)), createExpectedCell('ö', tcell.StyleDefault), @@ -318,9 +323,10 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) { func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) { pattern := regexp.MustCompile("ä") - line := createScreenLine(1, 4, "ååäö", pattern) + line := NewLine("ååäö") + screenLine := createScreenLine(1, 4, line, pattern) - assertTokenRangesEqual(t, line, []Token{ + assertTokenRangesEqual(t, screenLine, []Token{ createExpectedCell('<', tcell.StyleDefault.Reverse(true)), createExpectedCell('å', tcell.StyleDefault), createExpectedCell('ä', tcell.StyleDefault.Reverse(true)), @@ -331,9 +337,10 @@ func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) { func TestCreateScreenLineScrolled2Utf8SearchHit(t *testing.T) { pattern := regexp.MustCompile("ä") - line := createScreenLine(2, 4, "åååäö", pattern) + line := NewLine("åååäö") + screenLine := createScreenLine(2, 4, line, pattern) - assertTokenRangesEqual(t, line, []Token{ + assertTokenRangesEqual(t, screenLine, []Token{ createExpectedCell('<', tcell.StyleDefault.Reverse(true)), createExpectedCell('å', tcell.StyleDefault), createExpectedCell('ä', tcell.StyleDefault.Reverse(true)), diff --git m/reader.go m/reader.go index 418c4c5..d47b710 100644 --- m/reader.go +++ m/reader.go @@ -29,7 +29,7 @@ import ( // // This package provides query methods for the struct, no peeking!! type Reader struct { - lines []string + lines []*Line name *string lock *sync.Mutex err error @@ -41,7 +41,7 @@ type Reader struct { // Lines contains a number of lines from the reader, plus metadata type Lines struct { - lines []string + lines []*Line // One-based line number of the first line returned firstLineOneBased int @@ -136,7 +136,7 @@ func readStream(stream io.Reader, reader *Reader, fromFilter *exec.Cmd) { } reader.lock.Lock() - reader.lines = append(reader.lines, string(completeLine)) + reader.lines = append(reader.lines, NewLine(string(completeLine))) reader.lock.Unlock() // This is how to do a non-blocking write to a channel: @@ -172,7 +172,7 @@ func NewReaderFromStream(name string, reader io.Reader) *Reader { // If fromFilter is not nil this method will wait() for it, // and effectively takes over ownership for it. func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader { - var lines []string + var lines []*Line var lock = &sync.Mutex{} done := make(chan bool, 1) @@ -201,9 +201,11 @@ func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader { // Moar in the bottom left corner of the screen. func NewReaderFromText(name string, text string) *Reader { noExternalNewlines := strings.Trim(text, "\n") - lines := []string{} + lines := []*Line{} if len(noExternalNewlines) > 0 { - lines = strings.Split(noExternalNewlines, "\n") + for _, line := range strings.Split(noExternalNewlines, "\n") { + lines = append(lines, NewLine(line)) + } } done := make(chan bool, 1) done <- true @@ -380,7 +382,7 @@ func (r *Reader) GetLineCount() int { } // GetLine gets a line. If the requested line number is out of bounds, nil is returned. -func (r *Reader) GetLine(lineNumberOneBased int) *string { +func (r *Reader) GetLine(lineNumberOneBased int) *Line { r.lock.Lock() defer r.lock.Unlock() @@ -390,7 +392,7 @@ func (r *Reader) GetLine(lineNumberOneBased int) *string { if lineNumberOneBased > len(r.lines) { return nil } - return &r.lines[lineNumberOneBased-1] + return r.lines[lineNumberOneBased-1] } // GetLines gets the indicated lines from the input diff --git m/reader_test.go m/reader_test.go index 2ba7326..0e2aed2 100644 --- m/reader_test.go +++ m/reader_test.go @@ -158,8 +158,8 @@ func TestGetLongLine(t *testing.T) { assert.Equal(t, len(lines.lines), 1) line := lines.lines[0] - assert.Assert(t, strings.HasPrefix(line, "1 2 3 4"), "<%s>", line) - assert.Assert(t, strings.HasSuffix(line, "0123456789"), line) + assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line) + assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line) stat, err := os.Stat(file) if err != nil { @@ -168,7 +168,7 @@ func TestGetLongLine(t *testing.T) { fileSize := stat.Size() // The "+1" is because the Reader strips off the ending linefeed - assert.Equal(t, len(line)+1, int(fileSize)) + assert.Equal(t, len(line.Plain())+1, int(fileSize)) } func getReaderWithLineCount(totalLines int) *Reader { @@ -219,7 +219,7 @@ func testCompressedFile(t *testing.T, filename string) { panic(err) } - assert.Equal(t, reader.GetLines(1, 5).lines[0], "This is a compressed file", "%s", filename) + assert.Equal(t, reader.GetLines(1, 5).lines[0].Plain(), "This is a compressed file", "%s", filename) } func TestCompressedFiles(t *testing.T) { Change-Id: Id8671001ec7c1038e2df0b87a45d346a1f1dd663
2021-01-11 12:42:34 +03:00
assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line)
assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line)
assert.Equal(t, len(line.Plain()), 100021)
}
func getReaderWithLineCount(totalLines int) *Reader {
reader := NewReaderFromStream("", strings.NewReader(strings.Repeat("x\n", totalLines)))
if err := reader._Wait(); err != nil {
panic(err)
}
return reader
2019-06-22 00:24:53 +03:00
}
func testStatusText(t *testing.T, fromLine int, toLine int, totalLines int, expected string) {
testMe := getReaderWithLineCount(totalLines)
2019-06-22 00:24:53 +03:00
linesRequested := toLine - fromLine + 1
statusText := testMe.GetLines(fromLine, linesRequested).statusText
assert.Equal(t, statusText, expected)
}
func TestStatusText(t *testing.T) {
testStatusText(t, 1, 10, 20, "1-10/20 50%")
testStatusText(t, 1, 5, 5, "1-5/5 100%")
testStatusText(t, 998, 999, 1000, "998-999/1000 99%")
2019-06-22 00:24:53 +03:00
testStatusText(t, 0, 0, 0, "<empty>")
testStatusText(t, 1, 1, 1, "1-1/1 100%")
2019-06-22 00:24:53 +03:00
// Test with filename
testMe, err := NewReaderFromFilename(getSamplesDir() + "/empty")
2019-06-22 00:24:53 +03:00
if err != nil {
panic(err)
}
if err := testMe._Wait(); err != nil {
panic(err)
}
2019-06-22 00:24:53 +03:00
statusText := testMe.GetLines(0, 0).statusText
assert.Equal(t, statusText, "empty: <empty>")
2019-06-22 00:24:53 +03:00
}
func testCompressedFile(t *testing.T, filename string) {
filenameWithPath := getSamplesDir() + "/" + filename
2019-06-24 22:55:33 +03:00
reader, e := NewReaderFromFilename(filenameWithPath)
2019-06-23 22:30:11 +03:00
if e != nil {
2019-06-24 22:55:33 +03:00
t.Errorf("Error opening file <%s>: %s", filenameWithPath, e.Error())
2019-06-23 22:30:11 +03:00
panic(e)
}
if err := reader._Wait(); err != nil {
panic(err)
}
2019-06-23 22:30:11 +03:00
Parse lines on demand and only once This improves line processing performance by 40%. Fixes #36. diff --git m/ansiTokenizer.go m/ansiTokenizer.go index d991e23..056a227 100644 --- m/ansiTokenizer.go +++ m/ansiTokenizer.go @@ -23,6 +23,44 @@ type Token struct { Style tcell.Style } +// A Line represents a line of text that can / will be paged +type Line struct { + raw *string + plain *string + tokens []Token +} + +// NewLine creates a new Line from a (potentially ANSI / man page formatted) string +func NewLine(raw string) *Line { + return &Line{ + raw: &raw, + plain: nil, + tokens: nil, + } +} + +// Tokens returns a representation of the string split into styled tokens +func (line *Line) Tokens() []Token { + line.parse() + return line.tokens +} + +// Plain returns a plain text representation of the initial string +func (line *Line) Plain() string { + line.parse() + return *line.plain +} + +func (line *Line) parse() { + if line.raw == nil { + // Already done + return + } + + line.tokens, line.plain = tokensFromString(*line.raw) + line.raw = nil +} + // SetManPageFormatFromEnv parses LESS_TERMCAP_xx environment variables and // adapts the moar output accordingly. func SetManPageFormatFromEnv() { diff --git m/pager.go m/pager.go index 412e05b..98efa9a 100644 --- m/pager.go +++ m/pager.go @@ -111,7 +111,7 @@ func NewPager(r *Reader) *Pager { } } -func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line string) { +func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line *Line) { screenWidth, _ := p.screen.Size() prefixLength := 0 @@ -138,7 +138,7 @@ func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNum func createScreenLine( stringIndexAtColumnZero int, screenColumnsCount int, - line string, + line *Line, search *regexp.Regexp, ) []Token { var returnMe []Token @@ -152,14 +152,14 @@ func createScreenLine( searchHitDelta = -1 } - tokens, plainString := tokensFromString(line) - if stringIndexAtColumnZero >= len(tokens) { + if stringIndexAtColumnZero >= len(line.Tokens()) { // Nothing (more) to display, never mind return returnMe } - matchRanges := getMatchRanges(plainString, search) - for _, token := range tokens[stringIndexAtColumnZero:] { + plain := line.Plain() + matchRanges := getMatchRanges(&plain, search) + for _, token := range line.Tokens()[stringIndexAtColumnZero:] { if len(returnMe) >= screenColumnsCount { // We are trying to add a character to the right of the screen. // Indicate that this line continues to the right. @@ -232,7 +232,8 @@ func (p *Pager) _AddLines(spinner string) { // This happens when we're done eofSpinner = "---" } - p._AddLine(nil, 0, screenLineNumber, _EofMarkerFormat+eofSpinner) + spinnerLine := NewLine(_EofMarkerFormat + eofSpinner) + p._AddLine(nil, 0, screenLineNumber, spinnerLine) switch p.mode { case _Searching: @@ -329,8 +330,8 @@ func (p *Pager) _FindFirstHitLineOneBased(firstLineOneBased int, backwards bool) return nil } - _, lineText := tokensFromString(*line) - if p.searchPattern.MatchString(*lineText) { + lineText := line.Plain() + if p.searchPattern.MatchString(lineText) { return &lineNumber } diff --git m/pager_test.go m/pager_test.go index 65fa3c2..ce0f79b 100644 --- m/pager_test.go +++ m/pager_test.go @@ -265,13 +265,15 @@ func assertTokenRangesEqual(t *testing.T, actual []Token, expected []Token) { } func TestCreateScreenLineBase(t *testing.T) { - line := createScreenLine(0, 3, "", nil) - assert.Assert(t, len(line) == 0) + line := NewLine("") + screenLine := createScreenLine(0, 3, line, nil) + assert.Assert(t, len(screenLine) == 0) } func TestCreateScreenLineOverflowRight(t *testing.T) { - line := createScreenLine(0, 3, "012345", nil) - assertTokenRangesEqual(t, line, []Token{ + line := NewLine("012345") + screenLine := createScreenLine(0, 3, line, nil) + assertTokenRangesEqual(t, screenLine, []Token{ createExpectedCell('0', tcell.StyleDefault), createExpectedCell('1', tcell.StyleDefault), createExpectedCell('>', tcell.StyleDefault.Reverse(true)), @@ -279,8 +281,9 @@ func TestCreateScreenLineOverflowRight(t *testing.T) { } func TestCreateScreenLineUnderflowLeft(t *testing.T) { - line := createScreenLine(1, 3, "012", nil) - assertTokenRangesEqual(t, line, []Token{ + line := NewLine("012") + screenLine := createScreenLine(1, 3, line, nil) + assertTokenRangesEqual(t, screenLine, []Token{ createExpectedCell('<', tcell.StyleDefault.Reverse(true)), createExpectedCell('1', tcell.StyleDefault), createExpectedCell('2', tcell.StyleDefault), @@ -293,8 +296,9 @@ func TestCreateScreenLineSearchHit(t *testing.T) { panic(err) } - line := createScreenLine(0, 3, "abc", pattern) - assertTokenRangesEqual(t, line, []Token{ + line := NewLine("abc") + screenLine := createScreenLine(0, 3, line, pattern) + assertTokenRangesEqual(t, screenLine, []Token{ createExpectedCell('a', tcell.StyleDefault), createExpectedCell('b', tcell.StyleDefault.Reverse(true)), createExpectedCell('c', tcell.StyleDefault), @@ -307,8 +311,9 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) { panic(err) } - line := createScreenLine(0, 3, "åäö", pattern) - assertTokenRangesEqual(t, line, []Token{ + line := NewLine("åäö") + screenLine := createScreenLine(0, 3, line, pattern) + assertTokenRangesEqual(t, screenLine, []Token{ createExpectedCell('å', tcell.StyleDefault), createExpectedCell('ä', tcell.StyleDefault.Reverse(true)), createExpectedCell('ö', tcell.StyleDefault), @@ -318,9 +323,10 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) { func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) { pattern := regexp.MustCompile("ä") - line := createScreenLine(1, 4, "ååäö", pattern) + line := NewLine("ååäö") + screenLine := createScreenLine(1, 4, line, pattern) - assertTokenRangesEqual(t, line, []Token{ + assertTokenRangesEqual(t, screenLine, []Token{ createExpectedCell('<', tcell.StyleDefault.Reverse(true)), createExpectedCell('å', tcell.StyleDefault), createExpectedCell('ä', tcell.StyleDefault.Reverse(true)), @@ -331,9 +337,10 @@ func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) { func TestCreateScreenLineScrolled2Utf8SearchHit(t *testing.T) { pattern := regexp.MustCompile("ä") - line := createScreenLine(2, 4, "åååäö", pattern) + line := NewLine("åååäö") + screenLine := createScreenLine(2, 4, line, pattern) - assertTokenRangesEqual(t, line, []Token{ + assertTokenRangesEqual(t, screenLine, []Token{ createExpectedCell('<', tcell.StyleDefault.Reverse(true)), createExpectedCell('å', tcell.StyleDefault), createExpectedCell('ä', tcell.StyleDefault.Reverse(true)), diff --git m/reader.go m/reader.go index 418c4c5..d47b710 100644 --- m/reader.go +++ m/reader.go @@ -29,7 +29,7 @@ import ( // // This package provides query methods for the struct, no peeking!! type Reader struct { - lines []string + lines []*Line name *string lock *sync.Mutex err error @@ -41,7 +41,7 @@ type Reader struct { // Lines contains a number of lines from the reader, plus metadata type Lines struct { - lines []string + lines []*Line // One-based line number of the first line returned firstLineOneBased int @@ -136,7 +136,7 @@ func readStream(stream io.Reader, reader *Reader, fromFilter *exec.Cmd) { } reader.lock.Lock() - reader.lines = append(reader.lines, string(completeLine)) + reader.lines = append(reader.lines, NewLine(string(completeLine))) reader.lock.Unlock() // This is how to do a non-blocking write to a channel: @@ -172,7 +172,7 @@ func NewReaderFromStream(name string, reader io.Reader) *Reader { // If fromFilter is not nil this method will wait() for it, // and effectively takes over ownership for it. func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader { - var lines []string + var lines []*Line var lock = &sync.Mutex{} done := make(chan bool, 1) @@ -201,9 +201,11 @@ func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader { // Moar in the bottom left corner of the screen. func NewReaderFromText(name string, text string) *Reader { noExternalNewlines := strings.Trim(text, "\n") - lines := []string{} + lines := []*Line{} if len(noExternalNewlines) > 0 { - lines = strings.Split(noExternalNewlines, "\n") + for _, line := range strings.Split(noExternalNewlines, "\n") { + lines = append(lines, NewLine(line)) + } } done := make(chan bool, 1) done <- true @@ -380,7 +382,7 @@ func (r *Reader) GetLineCount() int { } // GetLine gets a line. If the requested line number is out of bounds, nil is returned. -func (r *Reader) GetLine(lineNumberOneBased int) *string { +func (r *Reader) GetLine(lineNumberOneBased int) *Line { r.lock.Lock() defer r.lock.Unlock() @@ -390,7 +392,7 @@ func (r *Reader) GetLine(lineNumberOneBased int) *string { if lineNumberOneBased > len(r.lines) { return nil } - return &r.lines[lineNumberOneBased-1] + return r.lines[lineNumberOneBased-1] } // GetLines gets the indicated lines from the input diff --git m/reader_test.go m/reader_test.go index 2ba7326..0e2aed2 100644 --- m/reader_test.go +++ m/reader_test.go @@ -158,8 +158,8 @@ func TestGetLongLine(t *testing.T) { assert.Equal(t, len(lines.lines), 1) line := lines.lines[0] - assert.Assert(t, strings.HasPrefix(line, "1 2 3 4"), "<%s>", line) - assert.Assert(t, strings.HasSuffix(line, "0123456789"), line) + assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line) + assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line) stat, err := os.Stat(file) if err != nil { @@ -168,7 +168,7 @@ func TestGetLongLine(t *testing.T) { fileSize := stat.Size() // The "+1" is because the Reader strips off the ending linefeed - assert.Equal(t, len(line)+1, int(fileSize)) + assert.Equal(t, len(line.Plain())+1, int(fileSize)) } func getReaderWithLineCount(totalLines int) *Reader { @@ -219,7 +219,7 @@ func testCompressedFile(t *testing.T, filename string) { panic(err) } - assert.Equal(t, reader.GetLines(1, 5).lines[0], "This is a compressed file", "%s", filename) + assert.Equal(t, reader.GetLines(1, 5).lines[0].Plain(), "This is a compressed file", "%s", filename) } func TestCompressedFiles(t *testing.T) { Change-Id: Id8671001ec7c1038e2df0b87a45d346a1f1dd663
2021-01-11 12:42:34 +03:00
assert.Equal(t, reader.GetLines(1, 5).lines[0].Plain(), "This is a compressed file", "%s", filename)
2019-06-23 22:30:11 +03:00
}
func TestCompressedFiles(t *testing.T) {
testCompressedFile(t, "compressed.txt.gz")
testCompressedFile(t, "compressed.txt.bz2")
_, err := exec.LookPath("xz")
if err == nil {
testCompressedFile(t, "compressed.txt.xz")
} else {
t.Log("WARNING: xz not found in path, not testing automatic xz decompression")
}
2019-06-23 22:30:11 +03:00
}
2019-06-24 22:55:33 +03:00
2019-06-25 20:44:22 +03:00
func TestFilterNotInstalled(t *testing.T) {
// FIXME: Test what happens if we try to use a filter that is not installed
}
func TestFilterFailure(t *testing.T) {
// FIXME: Test what happens if the filter command fails because of bad command line options
}
func TestFilterPermissionDenied(t *testing.T) {
// FIXME: Test what happens if the filter command fails because it can't access the requested file
}
func TestFilterFileNotFound(t *testing.T) {
2020-03-28 12:10:38 +03:00
// What happens if the filter cannot read its input file?
NonExistentPath := "/does-not-exist"
reader, err := newReaderFromCommand(NonExistentPath, "cat")
2020-03-28 12:10:38 +03:00
// Creating should be fine, it's waiting for it to finish that should fail.
// Feel free to re-evaluate in the future.
assert.Check(t, err == nil)
err = reader._Wait()
assert.Check(t, err != nil)
assert.Check(t, strings.Contains(err.Error(), NonExistentPath), err.Error())
2019-06-25 20:44:22 +03:00
}
func TestFilterNotAFile(t *testing.T) {
// FIXME: Test what happens if the filter command fails because the target is not a file
}
2021-04-22 08:56:10 +03:00
// How long does it take to read a file?
//
// This can be slow due to highlighting.
//
// Run with: go test -bench . ./...
func BenchmarkReaderDone(b *testing.B) {
filename := getSamplesDir() + "/../m/pager.go" // This is our longest .go file
b.ResetTimer()
for n := 0; n < b.N; n++ {
// This is our longest .go file
readMe, err := NewReaderFromFilename(filename)
if err != nil {
panic(err)
}
// Wait for the reader to finish
<-readMe.done
if readMe.err != nil {
panic(readMe.err)
2021-04-22 08:56:10 +03:00
}
}
}