hishtory/client/lib/lib_test.go
David Dworken 0023c72636
Quote initial commands to make it possible to easily use hishtory to find matching entries for already typed commands that contain flags (#251)
* Quote initial commands to make it possible to easily use hishtory to find matching entries for already typed commands that contain flags

* Add test for quoting dashes

* Fix test failures

* More test fixes

* Update goldens

* Update goldens

* Update goldens

* Fix race condition

* Fix test harness bug by swapping to splitn

* Update goldens

* Update golden

* Update test
2024-10-20 12:22:29 -07:00

340 lines
12 KiB
Go

package lib
import (
"os"
"reflect"
"testing"
"time"
"github.com/ddworken/hishtory/client/data"
"github.com/ddworken/hishtory/client/hctx"
"github.com/ddworken/hishtory/shared"
"github.com/ddworken/hishtory/shared/testutils"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
// Set env variable
defer testutils.BackupAndRestoreEnv("HISHTORY_TEST")()
os.Setenv("HISHTORY_TEST", "1")
m.Run()
}
func requireEntriesEqual(t *testing.T, expected, actual data.HistoryEntry) {
require.Equal(t, normalizeEntryTimezone(expected), normalizeEntryTimezone(actual))
}
func TestPersist(t *testing.T) {
defer testutils.BackupAndRestore(t)()
require.NoError(t, hctx.InitConfig())
db := hctx.GetDb(hctx.MakeContext())
entry := testutils.MakeFakeHistoryEntry("ls ~/")
require.NoError(t, db.Create(entry).Error)
var historyEntries []*data.HistoryEntry
result := db.Find(&historyEntries)
require.NoError(t, result.Error)
if len(historyEntries) != 1 {
t.Fatalf("DB has %d entries, expected 1!", len(historyEntries))
}
dbEntry := historyEntries[0]
requireEntriesEqual(t, entry, *dbEntry)
}
func TestSearch(t *testing.T) {
defer testutils.BackupAndRestore(t)()
require.NoError(t, hctx.InitConfig())
ctx := hctx.MakeContext()
db := hctx.GetDb(ctx)
// Insert data
entry1 := testutils.MakeFakeHistoryEntry("ls /foo")
require.NoError(t, db.Create(entry1).Error)
entry2 := testutils.MakeFakeHistoryEntry("ls /bar")
require.NoError(t, db.Create(entry2).Error)
// Search for data
results, err := Search(ctx, db, "ls", 5)
require.NoError(t, err)
if len(results) != 2 {
t.Fatalf("Search() returned %d results, expected 2, results=%#v", len(results), results)
}
requireEntriesEqual(t, entry2, *results[0])
requireEntriesEqual(t, entry1, *results[1])
// Search but exclude bar
results, err = Search(ctx, db, "ls -bar", 5)
require.NoError(t, err)
if len(results) != 1 {
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
}
// Search but exclude foo
results, err = Search(ctx, db, "ls -foo", 5)
require.NoError(t, err)
if len(results) != 1 {
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
}
// Search but include / also
results, err = Search(ctx, db, "ls /", 5)
require.NoError(t, err)
if len(results) != 2 {
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
}
// Search but exclude slash
results, err = Search(ctx, db, "ls -/", 5)
require.NoError(t, err)
if len(results) != 0 {
t.Fatalf("Search() returned %d results, expected 0, results=%#v", len(results), results)
}
// Tests for escaping
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("ls -baz")).Error)
results, err = Search(ctx, db, "ls", 5)
require.NoError(t, err)
if len(results) != 3 {
t.Fatalf("Search() returned %d results, expected 3, results=%#v", len(results), results)
}
results, err = Search(ctx, db, "ls -baz", 5)
require.NoError(t, err)
if len(results) != 2 {
t.Fatalf("Search() returned %d results, expected 2, results=%#v", len(results), results)
}
results, err = Search(ctx, db, "ls \\-baz", 5)
require.NoError(t, err)
if len(results) != 1 {
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
}
// A malformed search query, but we should just ignore the dash since this is a common enough thing
results, err = Search(ctx, db, "ls -", 5)
require.NoError(t, err)
if len(results) != 3 {
t.Fatalf("Search() returned %d results, expected 3, results=%#v", len(results), results)
}
// A search for an entry containing a backslash
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("echo '\\'")).Error)
results, err = Search(ctx, db, "\\\\", 5)
require.NoError(t, err)
if len(results) != 1 {
t.Fatalf("Search() returned %d results, expected 3, results=%#v", len(results), results)
}
}
func TestChunks(t *testing.T) {
testcases := []struct {
input []int
chunkSize int
output [][]int
}{
{[]int{1, 2, 3, 4, 5}, 2, [][]int{{1, 2}, {3, 4}, {5}}},
{[]int{1, 2, 3, 4, 5}, 3, [][]int{{1, 2, 3}, {4, 5}}},
{[]int{1, 2, 3, 4, 5}, 1, [][]int{{1}, {2}, {3}, {4}, {5}}},
{[]int{1, 2, 3, 4, 5}, 4, [][]int{{1, 2, 3, 4}, {5}}},
}
for _, tc := range testcases {
actual := shared.Chunks(tc.input, tc.chunkSize)
if !reflect.DeepEqual(actual, tc.output) {
t.Fatal("chunks failure")
}
}
}
func TestZshWeirdness(t *testing.T) {
testcases := []struct {
input string
output string
}{
{": 1666062975:0;bash", "bash"},
{": 16660:0;ls", "ls"},
{"ls", "ls"},
{"0", "0"},
{"hgffddxsdsrzsz xddfgdxfdv gdfc ghcvhgfcfg vgv", "hgffddxsdsrzsz xddfgdxfdv gdfc ghcvhgfcfg vgv"},
}
for _, tc := range testcases {
actual := stripZshWeirdness(tc.input)
if !reflect.DeepEqual(actual, tc.output) {
t.Fatalf("weirdness failure for %#v", tc.input)
}
}
}
func TestParseTimeGenerously(t *testing.T) {
ts, err := parseTimeGenerously("2006-01-02T15:04:00-08:00")
require.NoError(t, err)
if ts.Unix() != 1136243040 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02 T15:04:00 -08:00")
require.NoError(t, err)
if ts.Unix() != 1136243040 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02_T15:04:00_-08:00")
require.NoError(t, err)
if ts.Unix() != 1136243040 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02T15:04:00")
require.NoError(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02_T15:04:00")
require.NoError(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02_15:04:00")
require.NoError(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02T15:04")
require.NoError(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02_15:04")
require.NoError(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02")
require.NoError(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 0 || ts.Minute() != 0 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("1693163976")
require.NoError(t, err)
if ts.Year() != 2023 || ts.Month() != time.August || ts.Day() != 27 || ts.Hour() != 12 || ts.Minute() != 19 || ts.Second() != 36 {
t.Fatalf("parsed time incorrectly: %d %s", ts.Unix(), ts.GoString())
}
}
func TestUnescape(t *testing.T) {
testcases := []struct {
input string
output string
}{
{"f bar", "f bar"},
{"f \\bar", "f bar"},
{"f\\:bar", "f:bar"},
{"f\\:bar\\", "f:bar"},
{"\\f\\:bar\\", "f:bar"},
{"", ""},
{"\\", ""},
{"\\\\", "\\"},
}
for _, tc := range testcases {
actual := unescape(tc.input)
if !reflect.DeepEqual(actual, tc.output) {
t.Fatalf("unescape failure for %#v, actual=%#v", tc.input, actual)
}
}
}
func TestContainsUnescaped(t *testing.T) {
testcases := []struct {
input string
token string
expected bool
}{
{"f bar", "f", true},
{"f bar", "f bar", true},
{"f bar", "f r", false},
{"f bar", "f ", true},
{"foo:bar", ":", true},
{"foo:bar", "-", false},
{"foo\\:bar", ":", false},
{"foo\\-bar", "-", false},
{"foo\\-bar", "foo", true},
{"foo\\-bar", "bar", true},
{"foo\\-bar", "a", true},
}
for _, tc := range testcases {
actual := containsUnescaped(tc.input, tc.token)
if !reflect.DeepEqual(actual, tc.expected) {
t.Fatalf("failure for containsUnescaped(%#v, %#v), actual=%#v", tc.input, tc.token, actual)
}
}
}
func TestSplitEscaped(t *testing.T) {
testcases := []struct {
input string
char rune
limit int
expected []string
}{
// Basic tests
{"foo bar", ' ', 2, []string{"foo", "bar"}},
{"foo bar baz", ' ', 2, []string{"foo", "bar baz"}},
{"foo bar baz", ' ', 3, []string{"foo", "bar", "baz"}},
{"foo bar baz", ' ', 1, []string{"foo bar baz"}},
{"foo bar baz", ' ', -1, []string{"foo", "bar", "baz"}},
// Tests for escaping
{"foo\\ bar baz", ' ', -1, []string{"foo bar", "baz"}},
{"foo\\bar baz", ' ', -1, []string{"foobar", "baz"}},
{"foo\\bar baz foob", ' ', 2, []string{"foobar", "baz foob"}},
{"foo\\ bar\\ baz", ' ', -1, []string{"foo bar baz"}},
{"foo\\ bar\\ baz", ' ', -1, []string{"foo bar ", "baz"}},
// Tests for single quotes
{"'foo bar'", ' ', -1, []string{"foo bar"}},
{"'foo bar' ' '", ' ', -1, []string{"foo bar", " "}},
{"'foo bar baz' and", ' ', -1, []string{"foo bar baz", "and"}},
{"'foo bar baz", ' ', -1, []string{"'foo", "bar", "baz"}},
{"'foo bar baz\\''", ' ', -1, []string{"foo bar baz'"}},
{"cwd:'foo bar :baz\\''", ':', -1, []string{"cwd", "foo bar :baz'"}},
{"cwd:'foo bar :baz\\''", ' ', -1, []string{"cwd:foo bar \\:baz'"}},
// Tests for double quotes
{"\"foo bar\"", ' ', -1, []string{"foo bar"}},
{"\"foo bar\" \" \"", ' ', -1, []string{"foo bar", " "}},
{"\"foo bar baz\" and", ' ', -1, []string{"foo bar baz", "and"}},
{"\"foo bar baz", ' ', -1, []string{"\"foo", "bar", "baz"}},
{"\"foo bar baz\\\"\"", ' ', -1, []string{"foo bar baz\""}},
{"cwd:\"foo bar :baz\\\"\"", ':', -1, []string{"cwd", "foo bar :baz\""}},
{"cwd:\"foo bar :baz\\\"\"", ' ', -1, []string{"cwd:foo bar \\:baz\""}},
// Tests for complex quotes
{"\"foo'bar\"", ' ', -1, []string{"foo'bar"}},
{"'foo\"bar'", ' ', -1, []string{"foo\"bar"}},
{"\"foo'bar", ' ', -1, []string{"\"foo'bar"}},
{"'foo\"bar", ' ', -1, []string{"'foo\"bar"}},
{"\"foo'\\\"bar\"", ' ', -1, []string{"foo'\"bar"}},
{"'foo\"\\'bar'", ' ', -1, []string{"foo\"'bar"}},
{"''", ' ', -1, []string{""}},
{"\"\"", ' ', -1, []string{""}},
{"\\\"", ' ', -1, []string{"\""}},
{"\\'", ' ', -1, []string{"'"}},
// Tests the behavior of quotes with
{"it's", ' ', -1, []string{"it's"}},
{"'foo bar", ' ', -1, []string{"'foo", "bar"}},
// Tests for various complex/interesting escaping
{"ls \\-foo", ' ', -1, []string{"ls", "\\-foo"}},
{"ls \\-foo \\a \\\\", ' ', -1, []string{"ls", "\\-foo", "a", "\\\\"}},
{"foo:bar", ':', -1, []string{"foo", "bar"}},
{"foo:bar", ' ', -1, []string{"foo:bar"}},
{"foo\\:bar", ':', -1, []string{"foo\\:bar"}},
{"foo\\:bar", ' ', -1, []string{"foo\\:bar"}},
// Tests for quoting colons
{"foo:bar", ' ', -1, []string{"foo:bar"}},
{"'foo:bar'", ' ', -1, []string{"foo\\:bar"}},
{"\"foo:bar\"", ' ', -1, []string{"foo\\:bar"}},
// Tests for quoting dashes
{"'-foo'", ' ', -1, []string{"\\-foo"}},
{"'--foo'", ' ', -1, []string{"\\--foo"}},
{"bar '--foo'", ' ', -1, []string{"bar", "\\--foo"}},
{"bar 'foo-baz'", ' ', -1, []string{"bar", "foo-baz"}},
}
for _, tc := range testcases {
actual := splitEscaped(tc.input, tc.char, tc.limit)
if !reflect.DeepEqual(actual, tc.expected) {
t.Fatalf("failure for splitEscaped(%#v, %#v, %#v), actual=%#v", tc.input, string(tc.char), tc.limit, actual)
}
}
}