Add negative conditions to search queries + tests + better error messages by including filename:line in error messages

This commit is contained in:
David Dworken 2022-04-11 22:36:52 -07:00
parent 970e5d75db
commit 5325fc75ae
4 changed files with 112 additions and 33 deletions

View File

@ -8,6 +8,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"regexp" "regexp"
"runtime"
"strings" "strings"
"testing" "testing"
@ -18,7 +19,8 @@ import (
func RunInteractiveBashCommands(t *testing.T, script string) string { func RunInteractiveBashCommands(t *testing.T, script string) string {
out, err := RunInteractiveBashCommandsWithoutStrictMode(t, "set -emo pipefail\n"+script) out, err := RunInteractiveBashCommandsWithoutStrictMode(t, "set -emo pipefail\n"+script)
if err != nil { if err != nil {
t.Fatalf("error when running command: %v", err) _, filename, line, _ := runtime.Caller(1)
t.Fatalf("error when running command at %s:%d: %v", filename, line, err)
} }
return out return out
} }
@ -262,6 +264,7 @@ func TestAdvancedQuery(t *testing.T) {
notacommand notacommand
cd /tmp/ cd /tmp/
echo querybydir echo querybydir
hishtory disable
`) `)
// Query based on cwd // Query based on cwd
@ -367,6 +370,48 @@ func TestAdvancedQuery(t *testing.T) {
if strings.Count(out, "\n") != 2 { if strings.Count(out, "\n") != 2 {
t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out) t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out)
} }
// Test filtering out a search item
out = RunInteractiveBashCommands(t, `hishtory query`)
if !strings.Contains(out, "cmd_with_diff_hostname_and_username") {
t.Fatalf("hishtory query doesn't contain expected result, out=%#v", out)
}
out = RunInteractiveBashCommands(t, `hishtory query -cmd_with_diff_hostname_and_username`)
if strings.Contains(out, "cmd_with_diff_hostname_and_username") {
t.Fatalf("hishtory query contains unexpected result, out=%#v", out)
}
out = RunInteractiveBashCommands(t, `hishtory query -echo`)
if strings.Contains(out, "echo") {
t.Fatalf("hishtory query contains unexpected result, out=%#v", out)
}
if strings.Count(out, "\n") != 7 {
t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out)
}
// Test filtering out with an atom
out = RunInteractiveBashCommands(t, `hishtory query -hostname:otherhostname`)
if strings.Contains(out, "cmd_with_diff_hostname_and_username") {
t.Fatalf("hishtory query contains unexpected result, out=%#v", out)
}
out = RunInteractiveBashCommands(t, `hishtory query -user:otheruser`)
if strings.Contains(out, "cmd_with_diff_hostname_and_username") {
t.Fatalf("hishtory query contains unexpected result, out=%#v", out)
}
out = RunInteractiveBashCommands(t, `hishtory query -exit_code:0`)
if strings.Count(out, "\n") != 3 {
t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out)
}
// Test filtering out a search item that also looks like it could be a search for a flag
entry = data.MakeFakeHistoryEntry("foo -echo")
manuallySubmitHistoryEntry(t, userSecret, entry)
out = RunInteractiveBashCommands(t, `hishtory query -echo`)
if strings.Contains(out, "echo") {
t.Fatalf("hishtory query contains unexpected result, out=%#v", out)
}
if strings.Count(out, "\n") != 7 {
t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out)
}
} }
func TestUpdate(t *testing.T) { func TestUpdate(t *testing.T) {

View File

@ -157,40 +157,32 @@ func Search(db *gorm.DB, query string, limit int) ([]*HistoryEntry, error) {
} }
tx := db.Where("true") tx := db.Where("true")
for _, token := range tokens { for _, token := range tokens {
if strings.HasPrefix(token, "-") {
if strings.Contains(token, ":") { if strings.Contains(token, ":") {
splitToken := strings.SplitN(token, ":", 2) query, val, err := parseAtomizedToken(token[1:])
field := splitToken[0]
val := splitToken[1]
switch field {
case "user":
tx = tx.Where("local_username = ?", val)
case "hostname":
tx = tx.Where("instr(hostname, ?) > 0", val)
case "cwd":
// TODO: Can I make this support querying via ~/ too?
tx = tx.Where("instr(current_working_directory, ?) > 0", val)
case "exit_code":
tx = tx.Where("exit_code = ?", val)
case "before":
t, err := parseTimeGenerously(val)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse before:%s as a timestamp: %v", val, err) return nil, err
} }
tx = tx.Where("CAST(strftime(\"%s\",start_time) AS INTEGER) < ?", t.Unix()) tx = tx.Where("NOT "+query, val)
case "after":
t, err := parseTimeGenerously(val)
if err != nil {
return nil, fmt.Errorf("failed to parse after:%s as a timestamp: %v", val, err)
}
tx = tx.Where("CAST(strftime(\"%s\",start_time) AS INTEGER) > ?", t.Unix())
default:
panic("TODO: probably return an error?")
}
} else if strings.HasPrefix(token, "-") {
panic("TODO(ddworken): Implement -foo as filtering out foo")
} else { } else {
wildcardedToken := "%" + token + "%" query, v1, v2, v3, err := parseNonAtomizedToken(token[1:])
tx = tx.Where("(command LIKE ? OR hostname LIKE ? OR current_working_directory LIKE ?)", wildcardedToken, wildcardedToken, wildcardedToken) if err != nil {
return nil, err
}
tx = tx.Where("NOT "+query, v1, v2, v3)
}
} else if strings.Contains(token, ":") {
query, val, err := parseAtomizedToken(token)
if err != nil {
return nil, err
}
tx = tx.Where(query, val)
} else {
query, v1, v2, v3, err := parseNonAtomizedToken(token)
if err != nil {
return nil, err
}
tx = tx.Where(query, v1, v2, v3)
} }
} }
tx = tx.Order("end_time DESC") tx = tx.Order("end_time DESC")
@ -205,6 +197,42 @@ func Search(db *gorm.DB, query string, limit int) ([]*HistoryEntry, error) {
return historyEntries, nil return historyEntries, nil
} }
func parseNonAtomizedToken(token string) (string, interface{}, interface{}, interface{}, error) {
wildcardedToken := "%" + token + "%"
return "(command LIKE ? OR hostname LIKE ? OR current_working_directory LIKE ?)", wildcardedToken, wildcardedToken, wildcardedToken, nil
}
func parseAtomizedToken(token string) (string, interface{}, error) {
splitToken := strings.SplitN(token, ":", 2)
field := splitToken[0]
val := splitToken[1]
switch field {
case "user":
return "(local_username = ?)", val, nil
case "hostname":
return "(instr(hostname, ?) > 0)", val, nil
case "cwd":
// TODO: Can I make this support querying via ~/ too?
return "(instr(current_working_directory, ?) > 0)", val, nil
case "exit_code":
return "(exit_code = ?)", val, nil
case "before":
t, err := parseTimeGenerously(val)
if err != nil {
return "", nil, fmt.Errorf("failed to parse before:%s as a timestamp: %v", val, err)
}
return "(CAST(strftime(\"%s\",start_time) AS INTEGER) < ?)", t.Unix(), nil
case "after":
t, err := parseTimeGenerously(val)
if err != nil {
return "", nil, fmt.Errorf("failed to parse after:%s as a timestamp: %v", val, err)
}
return "(CAST(strftime(\"%s\",start_time) AS INTEGER) > ?)", t.Unix(), nil
default:
return "", nil, fmt.Errorf("search query contains unknown search atom %s", field)
}
}
func tokenize(query string) ([]string, error) { func tokenize(query string) ([]string, error) {
if query == "" { if query == "" {
return []string{}, nil return []string{}, nil

View File

@ -2,7 +2,6 @@ package lib
import ( import (
"bytes" "bytes"
_ "embed" // for embedding config.sh
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -14,11 +13,14 @@ import (
"os/user" "os/user"
"path" "path"
"path/filepath" "path/filepath"
"runtime"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
"time" "time"
_ "embed" // for embedding config.sh
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
@ -276,7 +278,8 @@ func Disable() error {
func CheckFatalError(err error) { func CheckFatalError(err error) {
if err != nil { if err != nil {
log.Fatalf("hishtory fatal error: %v", err) _, filename, line, _ := runtime.Caller(1)
log.Fatalf("hishtory fatal error at %s:%d: %v", filename, line, err)
} }
} }

View File

@ -6,6 +6,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"runtime"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -98,12 +99,14 @@ func RunTestServer(t *testing.T) func() {
func Check(t *testing.T, err error) { func Check(t *testing.T, err error) {
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %v", err) _, filename, line, _ := runtime.Caller(1)
t.Fatalf("Unexpected error at %s:%d: %v", filename, line, err)
} }
} }
func CheckWithInfo(t *testing.T, err error, additionalInfo string) { func CheckWithInfo(t *testing.T, err error, additionalInfo string) {
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %v! Additional info: %v", err, additionalInfo) _, filename, line, _ := runtime.Caller(1)
t.Fatalf("Unexpected error: %v at %s:%d! Additional info: %v", err, filename, line, additionalInfo)
} }
} }