mirror of
https://github.com/walles/moar.git
synced 2024-11-22 21:50:43 +03:00
505 lines
15 KiB
Go
505 lines
15 KiB
Go
package m
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alecthomas/chroma/v2"
|
|
"github.com/alecthomas/chroma/v2/formatters"
|
|
"github.com/alecthomas/chroma/v2/lexers"
|
|
"github.com/alecthomas/chroma/v2/styles"
|
|
"github.com/walles/moar/m/linenumbers"
|
|
"gotest.tools/v3/assert"
|
|
)
|
|
|
|
//revive:disable:empty-block
|
|
|
|
const samplesDir = "../sample-files"
|
|
|
|
func testGetLineCount(t *testing.T, reader *Reader) {
|
|
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]
|
|
wcLineCount, 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
|
|
wcLineCount = 1
|
|
} else if strings.HasSuffix(*reader.name, "/two-lines-no-trailing-newline.txt") {
|
|
// "wc -l" thinks this file contains one line
|
|
wcLineCount = 2
|
|
}
|
|
|
|
if reader.GetLineCount() != wcLineCount {
|
|
t.Errorf("Got %d lines from the reader but %d lines from wc -l: <%s>",
|
|
reader.GetLineCount(), wcLineCount, *reader.name)
|
|
}
|
|
|
|
countLinesCount, err := countLines(*reader.name)
|
|
assert.NilError(t, err)
|
|
if countLinesCount != uint64(wcLineCount) {
|
|
t.Errorf("Got %d lines from wc -l, but %d lines from our countLines() function", wcLineCount, countLinesCount)
|
|
}
|
|
}
|
|
|
|
func testGetLines(t *testing.T, reader *Reader) {
|
|
lines, _ := reader.GetLines(linenumbers.LineNumber{}, 10)
|
|
if len(lines.lines) > 10 {
|
|
t.Errorf("Asked for 10 lines, got too many: %d", len(lines.lines))
|
|
}
|
|
|
|
if len(lines.lines) < 10 {
|
|
// No good plan for how to test short files, more than just
|
|
// querying them, which we just did
|
|
return
|
|
}
|
|
|
|
// Test clipping at the end
|
|
lines, _ = reader.GetLines(linenumbers.LineNumberMax(), 10)
|
|
if len(lines.lines) != 10 {
|
|
t.Errorf("Asked for 10 lines but got %d", len(lines.lines))
|
|
return
|
|
}
|
|
|
|
startOfLastSection := lines.firstLine
|
|
lines, _ = reader.GetLines(startOfLastSection, 10)
|
|
if lines.firstLine != startOfLastSection {
|
|
t.Errorf("Expected start line %d when asking for the last 10 lines, got %s",
|
|
startOfLastSection, lines.firstLine.Format())
|
|
return
|
|
}
|
|
if len(lines.lines) != 10 {
|
|
t.Errorf("Expected 10 lines when asking for the last 10 lines, got %d",
|
|
len(lines.lines))
|
|
return
|
|
}
|
|
|
|
lines, _ = reader.GetLines(startOfLastSection.NonWrappingAdd(1), 10)
|
|
if lines.firstLine != startOfLastSection {
|
|
t.Errorf("Expected start line %d when asking for the last+1 10 lines, got %s",
|
|
startOfLastSection, lines.firstLine.Format())
|
|
return
|
|
}
|
|
if len(lines.lines) != 10 {
|
|
t.Errorf("Expected 10 lines when asking for the last+1 10 lines, got %d",
|
|
len(lines.lines))
|
|
return
|
|
}
|
|
|
|
lines, _ = reader.GetLines(startOfLastSection.NonWrappingAdd(-1), 10)
|
|
if lines.firstLine != startOfLastSection.NonWrappingAdd(-1) {
|
|
t.Errorf("Expected start line %d when asking for the last-1 10 lines, got %s",
|
|
startOfLastSection, lines.firstLine.Format())
|
|
return
|
|
}
|
|
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 getTestFiles(t *testing.T) []string {
|
|
files, err := os.ReadDir(samplesDir)
|
|
assert.NilError(t, err)
|
|
|
|
var filenames []string
|
|
for _, file := range files {
|
|
filenames = append(filenames, path.Join(samplesDir, file.Name()))
|
|
}
|
|
|
|
return filenames
|
|
}
|
|
|
|
// Wait for reader to finish reading and highlighting. Used by tests.
|
|
func (r *Reader) _wait() error {
|
|
// Wait for our goroutine to finish
|
|
//revive:disable-next-line:empty-block
|
|
for !r.done.Load() {
|
|
}
|
|
//revive:disable-next-line:empty-block
|
|
for !r.highlightingDone.Load() {
|
|
}
|
|
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
return r.err
|
|
}
|
|
|
|
func TestGetLines(t *testing.T) {
|
|
for _, file := range getTestFiles(t) {
|
|
reader, err := NewReaderFromFilename(file, *styles.Get("native"), formatters.TTY16m, nil)
|
|
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
|
|
}
|
|
|
|
t.Run(file, func(t *testing.T) {
|
|
testGetLines(t, reader)
|
|
testGetLineCount(t, reader)
|
|
testHighlightingLineCount(t, file)
|
|
})
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
if strings.HasSuffix(filenameWithPath, ".zst") {
|
|
return
|
|
}
|
|
if strings.HasSuffix(filenameWithPath, ".zstd") {
|
|
return
|
|
}
|
|
|
|
// Load the unformatted file
|
|
rawBytes, err := os.ReadFile(filenameWithPath)
|
|
assert.NilError(t, 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++
|
|
}
|
|
|
|
// Then load the same file using one of our Readers
|
|
reader, err := NewReaderFromFilename(filenameWithPath, *styles.Get("native"), formatters.TTY16m, nil)
|
|
assert.NilError(t, err)
|
|
err = reader._wait()
|
|
assert.NilError(t, err)
|
|
|
|
highlightedLinesCount := reader.GetLineCount()
|
|
assert.Equal(t, rawLinesCount, highlightedLinesCount)
|
|
}
|
|
|
|
func TestGetLongLine(t *testing.T) {
|
|
file := "../sample-files/very-long-line.txt"
|
|
reader, err := NewReaderFromFilename(file, *styles.Get("native"), formatters.TTY16m, nil)
|
|
assert.NilError(t, err)
|
|
assert.NilError(t, reader._wait())
|
|
|
|
lines, overflow := reader.GetLines(linenumbers.LineNumber{}, 5)
|
|
assert.Equal(t, lines.firstLine, linenumbers.LineNumber{})
|
|
assert.Equal(t, len(lines.lines), 1)
|
|
|
|
// This fits because we got all (one) input lines. Given the line length the
|
|
// line is unlikely to fit on screen, but that's not what this didFit is
|
|
// about.
|
|
assert.Equal(t, overflow, didFit)
|
|
|
|
line := lines.lines[0]
|
|
assert.Assert(t, strings.HasPrefix(line.Plain(nil), "1 2 3 4"), "<%s>", line)
|
|
assert.Assert(t, strings.HasSuffix(line.Plain(nil), "0123456789"), line)
|
|
|
|
assert.Equal(t, len(line.Plain(nil)), 100021)
|
|
}
|
|
|
|
func getReaderWithLineCount(totalLines int) *Reader {
|
|
return NewReaderFromText("", strings.Repeat("x\n", totalLines))
|
|
}
|
|
|
|
func testStatusText(t *testing.T, fromLine linenumbers.LineNumber, toLine linenumbers.LineNumber, totalLines int, expected string) {
|
|
testMe := getReaderWithLineCount(totalLines)
|
|
linesRequested := fromLine.CountLinesTo(toLine)
|
|
lines, _ := testMe.GetLines(fromLine, linesRequested)
|
|
statusText := lines.statusText
|
|
assert.Equal(t, statusText, expected)
|
|
}
|
|
|
|
func TestStatusText(t *testing.T) {
|
|
testStatusText(t, linenumbers.LineNumber{}, linenumbers.LineNumberFromOneBased(10), 20, "20 lines 50%")
|
|
testStatusText(t, linenumbers.LineNumber{}, linenumbers.LineNumberFromOneBased(5), 5, "5 lines 100%")
|
|
testStatusText(t,
|
|
linenumbers.LineNumberFromOneBased(998),
|
|
linenumbers.LineNumberFromOneBased(999),
|
|
1000,
|
|
"1000 lines 99%")
|
|
|
|
testStatusText(t, linenumbers.LineNumber{}, linenumbers.LineNumber{}, 0, "<empty>")
|
|
testStatusText(t, linenumbers.LineNumber{}, linenumbers.LineNumber{}, 1, "1 line 100%")
|
|
|
|
// Test with filename
|
|
testMe, err := NewReaderFromFilename(samplesDir+"/empty", *styles.Get("native"), formatters.TTY16m, nil)
|
|
assert.NilError(t, err)
|
|
assert.NilError(t, testMe._wait())
|
|
|
|
line, overflow := testMe.GetLines(linenumbers.LineNumber{}, 0)
|
|
if line.lines != nil {
|
|
t.Error("line.lines is should have been nil when reading from an empty stream")
|
|
}
|
|
assert.Equal(t, line.statusText, "empty: <empty>")
|
|
assert.Equal(t, overflow, didFit) // Empty always fits
|
|
}
|
|
|
|
func testCompressedFile(t *testing.T, filename string) {
|
|
filenameWithPath := path.Join(samplesDir, filename)
|
|
reader, e := NewReaderFromFilename(filenameWithPath, *styles.Get("native"), formatters.TTY16m, nil)
|
|
if e != nil {
|
|
t.Errorf("Error opening file <%s>: %s", filenameWithPath, e.Error())
|
|
panic(e)
|
|
}
|
|
assert.NilError(t, reader._wait())
|
|
|
|
lines, _ := reader.GetLines(linenumbers.LineNumber{}, 5)
|
|
assert.Equal(t, lines.lines[0].Plain(nil), "This is a compressed file", "%s", filename)
|
|
}
|
|
|
|
func TestCompressedFiles(t *testing.T) {
|
|
testCompressedFile(t, "compressed.txt.gz")
|
|
testCompressedFile(t, "compressed.txt.bz2")
|
|
testCompressedFile(t, "compressed.txt.xz")
|
|
testCompressedFile(t, "compressed.txt.zst")
|
|
testCompressedFile(t, "compressed.txt.zstd")
|
|
}
|
|
|
|
func TestReadFileDoneNoHighlighting(t *testing.T) {
|
|
testMe, err := NewReaderFromFilename(samplesDir+"/empty",
|
|
*styles.Get("Native"), formatters.TTY, nil)
|
|
assert.NilError(t, err)
|
|
|
|
assert.NilError(t, testMe._wait())
|
|
}
|
|
|
|
func TestReadFileDoneYesHighlighting(t *testing.T) {
|
|
testMe, err := NewReaderFromFilename("reader_test.go",
|
|
*styles.Get("Native"), formatters.TTY, nil)
|
|
assert.NilError(t, err)
|
|
|
|
assert.NilError(t, testMe._wait())
|
|
}
|
|
|
|
func TestReadStreamDoneNoHighlighting(t *testing.T) {
|
|
testMe := NewReaderFromStream("", strings.NewReader("Johan"), chroma.Style{}, nil, nil)
|
|
|
|
assert.NilError(t, testMe._wait())
|
|
}
|
|
|
|
func TestReadStreamDoneYesHighlighting(t *testing.T) {
|
|
testMe := NewReaderFromStream("",
|
|
strings.NewReader("Johan"),
|
|
*styles.Get("Native"), formatters.TTY, lexers.EmacsLisp)
|
|
|
|
assert.NilError(t, testMe._wait())
|
|
}
|
|
|
|
func TestReadTextDone(t *testing.T) {
|
|
testMe := NewReaderFromText("", "Johan")
|
|
|
|
assert.NilError(t, testMe._wait())
|
|
}
|
|
|
|
// If people keep appending to the currently opened file we should display those
|
|
// changes.
|
|
func TestReadUpdatingFile(t *testing.T) {
|
|
// Make a temp file containing one line of text, ending with a newline
|
|
file, err := os.CreateTemp("", "moar-TestReadUpdatingFile-*.txt")
|
|
assert.NilError(t, err)
|
|
defer os.Remove(file.Name())
|
|
|
|
const firstLineString = "First line\n"
|
|
_, err = file.WriteString(firstLineString)
|
|
assert.NilError(t, err)
|
|
|
|
// Start a reader on that file
|
|
testMe, err := NewReaderFromFilename(file.Name(), *styles.Get("native"), formatters.TTY16m, nil)
|
|
assert.NilError(t, err)
|
|
|
|
// Wait for the reader to finish reading
|
|
assert.NilError(t, testMe._wait())
|
|
assert.Equal(t, len([]byte(firstLineString)), int(testMe.bytesCount))
|
|
|
|
// Verify we got the single line
|
|
allLines, _ := testMe.GetLines(linenumbers.LineNumber{}, 10)
|
|
assert.Equal(t, len(allLines.lines), 1)
|
|
assert.Equal(t, testMe.GetLineCount(), 1)
|
|
assert.Equal(t, allLines.lines[0].Plain(nil), "First line")
|
|
|
|
// Append a line to the file
|
|
const secondLineString = "Second line\n"
|
|
_, err = file.WriteString(secondLineString)
|
|
assert.NilError(t, err)
|
|
|
|
// Give the reader some time to react
|
|
for i := 0; i < 20; i++ {
|
|
allLines, _ = testMe.GetLines(linenumbers.LineNumber{}, 10)
|
|
if len(allLines.lines) == 2 {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// Verify we got the two lines
|
|
allLines, _ = testMe.GetLines(linenumbers.LineNumber{}, 10)
|
|
assert.Equal(t, len(allLines.lines), 2, "Expected two lines after adding a second one, got %d", len(allLines.lines))
|
|
assert.Equal(t, testMe.GetLineCount(), 2)
|
|
assert.Equal(t, allLines.lines[0].Plain(nil), "First line")
|
|
assert.Equal(t, allLines.lines[1].Plain(nil), "Second line")
|
|
|
|
assert.Equal(t, int(testMe.bytesCount), len([]byte(firstLineString+secondLineString)))
|
|
}
|
|
|
|
// If people keep appending to the currently opened file we should display those
|
|
// changes.
|
|
//
|
|
// This test verifies it with an initially empty file.
|
|
func TestReadUpdatingFile_InitiallyEmpty(t *testing.T) {
|
|
// Make a temp file containing one line of text, ending with a newline
|
|
file, err := os.CreateTemp("", "moar-TestReadUpdatingFile_NoNewlineAtEOF-*.txt")
|
|
assert.NilError(t, err)
|
|
defer os.Remove(file.Name())
|
|
|
|
// Start a reader on that file
|
|
testMe, err := NewReaderFromFilename(file.Name(), *styles.Get("native"), formatters.TTY16m, nil)
|
|
assert.NilError(t, err)
|
|
|
|
// Wait for the reader to finish reading
|
|
assert.NilError(t, testMe._wait())
|
|
|
|
// Verify no lines
|
|
allLines, _ := testMe.GetLines(linenumbers.LineNumber{}, 10)
|
|
assert.Equal(t, len(allLines.lines), 0)
|
|
assert.Equal(t, testMe.GetLineCount(), 0)
|
|
|
|
// Append a line to the file
|
|
_, err = file.WriteString("Text\n")
|
|
assert.NilError(t, err)
|
|
|
|
// Give the reader some time to react
|
|
for i := 0; i < 20; i++ {
|
|
allLines, _ = testMe.GetLines(linenumbers.LineNumber{}, 10)
|
|
if len(allLines.lines) == 1 {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// Verify we got the two lines
|
|
allLines, _ = testMe.GetLines(linenumbers.LineNumber{}, 10)
|
|
assert.Equal(t, len(allLines.lines), 1, "Expected one line after adding one, got %d", len(allLines.lines))
|
|
assert.Equal(t, testMe.GetLineCount(), 1)
|
|
assert.Equal(t, allLines.lines[0].Plain(nil), "Text")
|
|
}
|
|
|
|
// NOTE: Consider testing file tailing with the first part not ending in a
|
|
// linefeed. In this case, the last line should become longer when new bytes
|
|
// show up.
|
|
|
|
// NOTE: Consider testing file tailing with the first part ending ending in the
|
|
// middle of an UTF-8 character. In this case, the new bytes should complete the
|
|
// UTF-8 character so it looks great again.
|
|
|
|
// How long does it take to read a file?
|
|
//
|
|
// This can be slow due to highlighting.
|
|
//
|
|
// Run with: go test -run='^$' -bench=. . ./...
|
|
func BenchmarkReaderDone(b *testing.B) {
|
|
filename := "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, *styles.Get("native"), formatters.TTY16m, nil)
|
|
assert.NilError(b, err)
|
|
|
|
assert.NilError(b, readMe._wait())
|
|
assert.NilError(b, readMe.err)
|
|
}
|
|
}
|
|
|
|
// Try loading a large file
|
|
func BenchmarkReadLargeFile(b *testing.B) {
|
|
// Try loading a file this large
|
|
const largeSizeBytes = 35_000_000
|
|
|
|
// First, create it from something...
|
|
inputFilename := "pager.go"
|
|
contents, err := os.ReadFile(inputFilename)
|
|
assert.NilError(b, err)
|
|
|
|
testdir := b.TempDir()
|
|
largeFileName := testdir + "/large-file"
|
|
largeFile, err := os.Create(largeFileName)
|
|
assert.NilError(b, err)
|
|
|
|
totalBytesWritten := 0
|
|
for totalBytesWritten < largeSizeBytes {
|
|
written, err := largeFile.Write(contents)
|
|
assert.NilError(b, err)
|
|
|
|
totalBytesWritten += written
|
|
}
|
|
err = largeFile.Close()
|
|
assert.NilError(b, err)
|
|
|
|
b.ResetTimer()
|
|
for n := 0; n < b.N; n++ {
|
|
readMe, err := NewReaderFromFilename(largeFileName, *styles.Get("native"), formatters.TTY16m, nil)
|
|
assert.NilError(b, err)
|
|
|
|
assert.NilError(b, readMe._wait())
|
|
assert.NilError(b, readMe.err)
|
|
}
|
|
}
|
|
|
|
// Count lines in pager.go
|
|
func BenchmarkCountLines(b *testing.B) {
|
|
// First, get some sample lines...
|
|
inputFilename := "pager.go"
|
|
contents, err := os.ReadFile(inputFilename)
|
|
assert.NilError(b, err)
|
|
|
|
testdir := b.TempDir()
|
|
countFileName := testdir + "/count-file"
|
|
countFile, err := os.Create(countFileName)
|
|
assert.NilError(b, err)
|
|
|
|
// 1000x makes this take about 12ms on my machine right now. Before 1000x
|
|
// the numbers fluctuated much more.
|
|
for n := 0; n < b.N*1000; n++ {
|
|
_, err := countFile.Write(contents)
|
|
assert.NilError(b, err)
|
|
}
|
|
err = countFile.Close()
|
|
assert.NilError(b, err)
|
|
|
|
b.ResetTimer()
|
|
_, err = countLines(countFileName)
|
|
assert.NilError(b, err)
|
|
}
|