diff --git a/Makefile b/Makefile index 682f692..fb201a9 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,6 @@ +forcetest: + go clean -testcache + HISHTORY_TEST=1 go test -p 1 ./... test: HISHTORY_TEST=1 go test -p 1 ./... diff --git a/backend/server/server_test.go b/backend/server/server_test.go index b29703d..c96f0ec 100644 --- a/backend/server/server_test.go +++ b/backend/server/server_test.go @@ -128,7 +128,11 @@ func TestUpdateReleaseVersion(t *testing.T) { t.Fatalf("updateReleaseVersion failed: %v", err) } - // And check that the new value looks reasonable + // If ReleaseVersion is still unknown, skip because we're getting rate limited + if ReleaseVersion == "UNKNOWN" { + t.Skip() + } + // Otherwise, check that the new value looks reasonable if !strings.HasPrefix(ReleaseVersion, "v0.") { t.Fatalf("ReleaseVersion wasn't updated to contain a version: %#v", ReleaseVersion) } @@ -151,6 +155,10 @@ func TestGithubRedirects(t *testing.T) { t.Fatalf("expected endpoint to return redirect") } locationHeader := resp.Header.Get("location") + if strings.Contains(locationHeader, "https://github.com/ddworken/hishtory/releases/download/UNKNOWN") { + // Getting rate limited, skip the test + t.Skip() + } if !strings.Contains(locationHeader, "https://github.com/ddworken/hishtory/releases/download/v") { t.Fatalf("expected location header to point to github") } diff --git a/client/client_test.go b/client/client_test.go index 7cfd31f..40e2c51 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -11,6 +11,9 @@ import ( "runtime" "strings" "testing" + "time" + + "github.com/google/go-cmp/cmp" "github.com/ddworken/hishtory/client/data" "github.com/ddworken/hishtory/shared" @@ -37,7 +40,7 @@ func RunInteractiveBashCommandsWithoutStrictMode(t *testing.T, script string) (s return "", fmt.Errorf("unexpected error when running commands, out=%#v, err=%#v: %v", stdout.String(), stderr.String(), err) } outStr := stdout.String() - if strings.Contains(outStr, "hishtory fatal error:") { + if strings.Contains(outStr, "hishtory fatal error") { t.Fatalf("Ran command, but hishtory had a fatal error! out=%#v", outStr) } return outStr, nil @@ -53,12 +56,6 @@ func TestIntegration(t *testing.T) { } func TestIntegrationWithNewDevice(t *testing.T) { - if os.Getenv("GITHUB_ACTIONS") != "" { - // TODO: debug why these tests fail on github actions, the error message is: - // `bash: cannot set terminal process group (683): Inappropriate ioctl for device\nbash: no job control in this shell` - t.Skip() - } - // Set up defer shared.BackupAndRestore(t)() defer shared.RunTestServer(t)() @@ -73,7 +70,7 @@ func TestIntegrationWithNewDevice(t *testing.T) { installHishtory(t, userSecret) // Querying should show the history from the previous run - out := RunInteractiveBashCommands(t, "hishtory query") + out := hishtoryQuery(t, "") expected := []string{"echo thisisrecorded", "hishtory enable", "echo bar", "echo foo", "ls /foo", "ls /bar", "ls /a"} for _, item := range expected { if !strings.Contains(out, item) { @@ -85,7 +82,7 @@ func TestIntegrationWithNewDevice(t *testing.T) { } RunInteractiveBashCommands(t, "echo mynewcommand") - out = RunInteractiveBashCommands(t, "hishtory query") + out = hishtoryQuery(t, "") if !strings.Contains(out, "echo mynewcommand") { t.Fatalf("output is missing `echo mynewcommand`") } @@ -101,7 +98,7 @@ func TestIntegrationWithNewDevice(t *testing.T) { // Run a command that shouldn't be in the hishtory later on RunInteractiveBashCommands(t, `echo notinthehistory`) - out = RunInteractiveBashCommands(t, "hishtory query") + out = hishtoryQuery(t, "") if !strings.Contains(out, "echo notinthehistory") { t.Fatalf("output is missing `echo notinthehistory`") } @@ -113,7 +110,7 @@ func TestIntegrationWithNewDevice(t *testing.T) { } // Querying should show the history from the previous run - out = RunInteractiveBashCommands(t, "hishtory query") + out = hishtoryQuery(t, "") expected = []string{"echo thisisrecorded", "echo mynewcommand", "hishtory enable", "echo bar", "echo foo", "ls /foo", "ls /bar", "ls /a"} for _, item := range expected { if !strings.Contains(out, item) { @@ -129,7 +126,7 @@ func TestIntegrationWithNewDevice(t *testing.T) { } RunInteractiveBashCommands(t, "echo mynewercommand") - out = RunInteractiveBashCommands(t, "hishtory query") + out = hishtoryQuery(t, "") if !strings.Contains(out, "echo mynewercommand") { t.Fatalf("output is missing `echo mynewercommand`") } @@ -143,23 +140,27 @@ func TestIntegrationWithNewDevice(t *testing.T) { manuallySubmitHistoryEntry(t, userSecret, newEntry) // Now check if that is in there when we do hishtory query - out = RunInteractiveBashCommands(t, `hishtory query`) + out = hishtoryQuery(t, "") if !strings.Contains(out, "othercomputer") { t.Fatalf("hishtory query doesn't contain cmd run on another machine! out=%#v", out) } // Finally, test the export command + waitForBackgroundSavesToComplete(t) out = RunInteractiveBashCommands(t, `hishtory export`) - if out != fmt.Sprintf( - "/tmp/client install\nset -emo pipefail\nset -emo pipefail\nhishtory status\nset -emo pipefail\nhishtory query\nhishtory query\nset -m\nls /a\nls /bar\nls /foo\necho foo\necho bar\nhishtory enable\necho thisisrecorded\nset -emo pipefail\nhishtory query\nset -emo pipefail\nhishtory query foo\n/tmp/client install %s\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mynewcommand\nset -emo pipefail\nhishtory query\nhishtory init %s\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mynewercommand\nset -emo pipefail\nhishtory query\nothercomputer\nset -emo pipefail\nhishtory query\nset -emo pipefail\n", userSecret, userSecret) { - t.Fatalf("hishtory export had unexpected output! out=%#v", out) + if strings.Contains(out, "thisisnotrecorded") { + t.Fatalf("hishtory export contains a command that should not have been recorded, out=%#v", out) + } + expectedOutputWithoutKey := "set -emo pipefail\nhishtory status\nset -emo pipefail\nhishtory query\nset -m\nls /a\nls /bar\nls /foo\necho foo\necho bar\nhishtory enable\necho thisisrecorded\nset -emo pipefail\nhishtory query\nset -emo pipefail\nhishtory query foo\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mynewcommand\nset -emo pipefail\nhishtory query\nhishtory init %s\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mynewercommand\nset -emo pipefail\nhishtory query\nothercomputer\nset -emo pipefail\nhishtory query\nset -emo pipefail\n" + expectedOutput := fmt.Sprintf(expectedOutputWithoutKey, userSecret) + if diff := cmp.Diff(expectedOutput, out); diff != "" { + t.Fatalf("hishtory export mismatch (-expected +got):\n%s", diff) } } func installHishtory(t *testing.T, userSecret string) string { - out := RunInteractiveBashCommands(t, ` - go build -o /tmp/client - /tmp/client install `+userSecret) + out := RunInteractiveBashCommands(t, `go build -o /tmp/client +/tmp/client install `+userSecret) r := regexp.MustCompile(`Setting secret hishtory key to (.*)`) matches := r.FindStringSubmatch(out) if len(matches) != 2 { @@ -180,7 +181,7 @@ func testIntegration(t *testing.T) string { // Test the banner os.Setenv("FORCED_BANNER", "HELLO_FROM_SERVER") - out = RunInteractiveBashCommands(t, `hishtory query`) + out = hishtoryQuery(t, "") if !strings.Contains(out, "HELLO_FROM_SERVER") { t.Fatalf("hishtory query didn't show the banner message! out=%#v", out) } @@ -195,6 +196,7 @@ echo foo echo bar hishtory disable echo thisisnotrecorded +sleep 0.5 hishtory enable echo thisisrecorded`) if err != nil { @@ -205,7 +207,7 @@ echo thisisrecorded`) } // Test querying for all commands - out = RunInteractiveBashCommands(t, "hishtory query") + out = hishtoryQuery(t, "") expected := []string{"echo thisisrecorded", "hishtory enable", "echo bar", "echo foo", "ls /foo", "ls /bar", "ls /a"} for _, item := range expected { if !strings.Contains(out, item) { @@ -219,7 +221,7 @@ echo thisisrecorded`) // } // Test querying for a specific command - out = RunInteractiveBashCommands(t, "hishtory query foo") + out = hishtoryQuery(t, "foo") expected = []string{"echo foo", "ls /foo"} unexpected := []string{"echo thisisrecorded", "hishtory enable", "echo bar", "ls /bar", "ls /a"} for _, item := range expected { @@ -248,29 +250,27 @@ func TestAdvancedQuery(t *testing.T) { userSecret := installHishtory(t, "") // Run some commands we can query for - _, err := RunInteractiveBashCommandsWithoutStrictMode(t, ` - set -m - echo nevershouldappear - notacommand - cd /tmp/ - echo querybydir - hishtory disable - `) + _, err := RunInteractiveBashCommandsWithoutStrictMode(t, `set -m +echo nevershouldappear +notacommand +cd /tmp/ +echo querybydir +hishtory disable`) if err != nil { t.Fatal(err) } // A super basic query just to ensure the basics are working - out := RunInteractiveBashCommands(t, `hishtory query echo`) + out := hishtoryQuery(t, `echo`) if !strings.Contains(out, "echo querybydir") { - t.Fatalf("hishtory query doesn't contain result matching echo, out=%#v", out) + t.Fatalf("hishtory query doesn't contain result matching echo querybydir, out=%#v", out) } if strings.Count(out, "\n") != 3 { t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out) } // Query based on cwd - out = RunInteractiveBashCommands(t, `hishtory query cwd:/tmp`) + out = hishtoryQuery(t, `cwd:/tmp`) if !strings.Contains(out, "echo querybydir") { t.Fatalf("hishtory query doesn't contain result matching cwd:/tmp, out=%#v", out) } @@ -282,7 +282,7 @@ func TestAdvancedQuery(t *testing.T) { } // Query based on cwd without the slash - out = RunInteractiveBashCommands(t, `hishtory query cwd:tmp`) + out = hishtoryQuery(t, `cwd:tmp`) if !strings.Contains(out, "echo querybydir") { t.Fatalf("hishtory query doesn't contain result matching cwd:tmp, out=%#v", out) } @@ -291,7 +291,7 @@ func TestAdvancedQuery(t *testing.T) { } // Query based on cwd and another term - out = RunInteractiveBashCommands(t, `hishtory query cwd:/tmp querybydir`) + out = hishtoryQuery(t, `cwd:/tmp querybydir`) if !strings.Contains(out, "echo querybydir") { t.Fatalf("hishtory query doesn't contain result matching cwd:/tmp, out=%#v", out) } @@ -303,7 +303,7 @@ func TestAdvancedQuery(t *testing.T) { } // Query based on exit_code - out = RunInteractiveBashCommands(t, `hishtory query exit_code:127`) + out = hishtoryQuery(t, `exit_code:127`) if !strings.Contains(out, "notacommand") { t.Fatalf("hishtory query doesn't contain expected result, out=%#v", out) } @@ -312,33 +312,33 @@ func TestAdvancedQuery(t *testing.T) { } // Query based on exit_code and something else that matches nothing - out = RunInteractiveBashCommands(t, `hishtory query exit_code:127 foo`) + out = hishtoryQuery(t, `exit_code:127 foo`) if strings.Count(out, "\n") != 1 { t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out) } // Query based on before: and cwd: - out = RunInteractiveBashCommands(t, `hishtory query before:2025-07-02 cwd:/tmp`) + out = hishtoryQuery(t, `before:2025-07-02 cwd:/tmp`) if strings.Count(out, "\n") != 3 { t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out) } - out = RunInteractiveBashCommands(t, `hishtory query before:2025-07-02 cwd:tmp`) + out = hishtoryQuery(t, `before:2025-07-02 cwd:tmp`) if strings.Count(out, "\n") != 3 { t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out) } - out = RunInteractiveBashCommands(t, `hishtory query before:2025-07-02 cwd:mp`) + out = hishtoryQuery(t, `before:2025-07-02 cwd:mp`) if strings.Count(out, "\n") != 3 { t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out) } // Query based on after: and cwd: - out = RunInteractiveBashCommands(t, `hishtory query after:2020-07-02 cwd:/tmp`) + out = hishtoryQuery(t, `after:2020-07-02 cwd:/tmp`) if strings.Count(out, "\n") != 3 { t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out) } // Query based on after: that returns no results - out = RunInteractiveBashCommands(t, `hishtory query after:2120-07-02 cwd:/tmp`) + out = hishtoryQuery(t, `after:2120-07-02 cwd:/tmp`) if strings.Count(out, "\n") != 1 { t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out) } @@ -350,7 +350,7 @@ func TestAdvancedQuery(t *testing.T) { manuallySubmitHistoryEntry(t, userSecret, entry) // Query based on the username that exists - out = RunInteractiveBashCommands(t, `hishtory query user:otheruser`) + out = hishtoryQuery(t, `user:otheruser`) if !strings.Contains(out, "cmd_with_diff_hostname_and_username") { t.Fatalf("hishtory query doesn't contain expected result, out=%#v", out) } @@ -359,13 +359,13 @@ func TestAdvancedQuery(t *testing.T) { } // Query based on the username that doesn't exist - out = RunInteractiveBashCommands(t, `hishtory query user:noexist`) + out = hishtoryQuery(t, `user:noexist`) if strings.Count(out, "\n") != 1 { t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out) } // Query based on the hostname - out = RunInteractiveBashCommands(t, `hishtory query hostname:otherhostname`) + out = hishtoryQuery(t, `hostname:otherhostname`) if !strings.Contains(out, "cmd_with_diff_hostname_and_username") { t.Fatalf("hishtory query doesn't contain expected result, out=%#v", out) } @@ -374,35 +374,32 @@ func TestAdvancedQuery(t *testing.T) { } // Test filtering out a search item - out = RunInteractiveBashCommands(t, `hishtory query`) + out = hishtoryQuery(t, "") 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`) + out = hishtoryQuery(t, `-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 `) + out = hishtoryQuery(t, `-echo`) if strings.Contains(out, "echo") { t.Fatalf("hishtory query contains unexpected result, out=%#v", out) } - if os.Getenv("GITHUB_ACTIONS") == "" { - // For some reason, this fails on github actions and it only has 6 lines (it is missing the install line) - if strings.Count(out, "\n") != 7 { - t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out) - } + if strings.Count(out, "\n") != 5 { + 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`) + out = hishtoryQuery(t, `-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`) + out = hishtoryQuery(t, `-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`) + out = hishtoryQuery(t, `-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) } @@ -410,7 +407,7 @@ func TestAdvancedQuery(t *testing.T) { // 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 -install`) + out = hishtoryQuery(t, `-echo -install`) if strings.Contains(out, "echo") { t.Fatalf("hishtory query contains unexpected result, out=%#v", out) } @@ -444,6 +441,127 @@ func TestUpdate(t *testing.T) { } } +func TestRepeatedCommandThenQuery(t *testing.T) { + // Set up + defer shared.BackupAndRestore(t)() + defer shared.RunTestServer(t)() + userSecret := installHishtory(t, "") + + // Check the status command + out := RunInteractiveBashCommands(t, `hishtory status`) + if out != fmt.Sprintf("Hishtory: v0.Unknown\nEnabled: true\nSecret Key: %s\nCommit Hash: Unknown\n", userSecret) { + t.Fatalf("status command has unexpected output: %#v", out) + } + + // Run a command many times + for i := 0; i < 25; i++ { + RunInteractiveBashCommands(t, fmt.Sprintf("echo mycommand-%d", i)) + } + + // Check that it shows up correctly + out = hishtoryQuery(t, `mycommand`) + if strings.Count(out, "\n") != 26 { + t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out) + } + if strings.Count(out, "echo mycommand") != 25 { + t.Fatalf("hishtory query has the wrong number of commands=%d, out=%#v", strings.Count(out, "echo mycommand"), out) + } + + RunInteractiveBashCommands(t, `echo mycommand-30 +echo mycommand-31 +echo mycommand-3`) + + out = RunInteractiveBashCommands(t, "hishtory export") + expectedOutput := "set -emo pipefail\nhishtory status\nset -emo pipefail\necho mycommand-0\nset -emo pipefail\necho mycommand-1\nset -emo pipefail\necho mycommand-2\nset -emo pipefail\necho mycommand-3\nset -emo pipefail\necho mycommand-4\nset -emo pipefail\necho mycommand-5\nset -emo pipefail\necho mycommand-6\nset -emo pipefail\necho mycommand-7\nset -emo pipefail\necho mycommand-8\nset -emo pipefail\necho mycommand-9\nset -emo pipefail\necho mycommand-10\nset -emo pipefail\necho mycommand-11\nset -emo pipefail\necho mycommand-12\nset -emo pipefail\necho mycommand-13\nset -emo pipefail\necho mycommand-14\nset -emo pipefail\necho mycommand-15\nset -emo pipefail\necho mycommand-16\nset -emo pipefail\necho mycommand-17\nset -emo pipefail\necho mycommand-18\nset -emo pipefail\necho mycommand-19\nset -emo pipefail\necho mycommand-20\nset -emo pipefail\necho mycommand-21\nset -emo pipefail\necho mycommand-22\nset -emo pipefail\necho mycommand-23\nset -emo pipefail\necho mycommand-24\nset -emo pipefail\nhishtory query mycommand\nset -emo pipefail\necho mycommand-30\necho mycommand-31\necho mycommand-3\nset -emo pipefail\n" + if diff := cmp.Diff(expectedOutput, out); diff != "" { + t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out) + } +} + +func TestRepeatedCommandAndQuery(t *testing.T) { + // Set up + defer shared.BackupAndRestore(t)() + defer shared.RunTestServer(t)() + userSecret := installHishtory(t, "") + + // Check the status command + out := RunInteractiveBashCommands(t, `hishtory status`) + if out != fmt.Sprintf("Hishtory: v0.Unknown\nEnabled: true\nSecret Key: %s\nCommit Hash: Unknown\n", userSecret) { + t.Fatalf("status command has unexpected output: %#v", out) + } + + // Run a command many times + for i := 0; i < 25; i++ { + RunInteractiveBashCommands(t, fmt.Sprintf("echo mycommand-%d", i)) + out = hishtoryQuery(t, fmt.Sprintf("mycommand-%d", i)) + if strings.Count(out, "\n") != 2 { + t.Fatalf("hishtory query #%d has the wrong number of lines=%d, out=%#v", i, strings.Count(out, "\n"), out) + } + if strings.Count(out, "echo mycommand") != 1 { + t.Fatalf("hishtory query #%d has the wrong number of commands=%d, out=%#v", i, strings.Count(out, "echo mycommand"), out) + } + + } +} + +func TestRepeatedEnableDisable(t *testing.T) { + // Set up + defer shared.BackupAndRestore(t)() + defer shared.RunTestServer(t)() + installHishtory(t, "") + + // Run a command many times + for i := 0; i < 25; i++ { + RunInteractiveBashCommands(t, fmt.Sprintf(`echo mycommand-%d +hishtory disable +echo shouldnotshowup +sleep 0.5 +hishtory enable`, i)) + out := hishtoryQuery(t, fmt.Sprintf("mycommand-%d", i)) + if strings.Count(out, "\n") != 2 { + t.Fatalf("hishtory query #%d has the wrong number of lines=%d, out=%#v", i, strings.Count(out, "\n"), out) + } + if strings.Count(out, "echo mycommand") != 1 { + t.Fatalf("hishtory query #%d has the wrong number of commands=%d, out=%#v", i, strings.Count(out, "echo mycommand"), out) + } + out = hishtoryQuery(t, "") + if strings.Contains(out, "shouldnotshowup") { + t.Fatalf("hishtory query contains a result that should not have been recorded, out=%#v", out) + } + } + + out := RunInteractiveBashCommands(t, "hishtory export") + expectedOutput := "set -emo pipefail\necho mycommand-0\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-0\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-1\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-1\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-2\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-2\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-3\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-3\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-4\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-4\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-5\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-5\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-6\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-6\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-7\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-7\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-8\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-8\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-9\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-9\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-10\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-10\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-11\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-11\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-12\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-12\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-13\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-13\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-14\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-14\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-15\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-15\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-16\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-16\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-17\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-17\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-18\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-18\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-19\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-19\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-20\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-20\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-21\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-21\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-22\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-22\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-23\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-23\nset -emo pipefail\nhishtory query\nset -emo pipefail\necho mycommand-24\nhishtory enable\nset -emo pipefail\nhishtory query mycommand-24\nset -emo pipefail\nhishtory query\nset -emo pipefail\n" + if out != expectedOutput { + t.Fatalf("hishtory export has unexpected output=%#v", out) + } +} + +func waitForBackgroundSavesToComplete(t *testing.T) { + for i := 0; i < 20; i++ { + cmd := exec.Command("pidof", "hishtory") + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil && err.Error() != "exit status 1" { + t.Fatalf("failed to check if hishtory was running: %v, stdout=%#v, stderr=%#v", err, stdout.String(), stderr.String()) + } + if !strings.Contains(stdout.String(), "\n") { + // pidof had no output, so hishtory isn't running and we're done waitng + return + } + time.Sleep(50 * time.Millisecond) + } + t.Fatalf("failed to wait until hishtory wasn't running") +} + +func hishtoryQuery(t *testing.T, query string) string { + waitForBackgroundSavesToComplete(t) + return RunInteractiveBashCommands(t, "hishtory query "+query) +} + // TODO: Maybe a dedicated unit test for retrieveAdditionalEntriesFromRemote func manuallySubmitHistoryEntry(t *testing.T, userSecret string, entry data.HistoryEntry) { @@ -457,3 +575,58 @@ func manuallySubmitHistoryEntry(t *testing.T, userSecret string, entry data.Hist t.Fatalf("failed to submit result to backend, status_code=%d", resp.StatusCode) } } + +func TestHishtoryBackgroundSaving(t *testing.T) { + // Setup + defer shared.BackupAndRestore(t)() + defer shared.RunTestServer(t)() + + // Test install with an unset HISHTORY_TEST var so that we save in the background (this is likely to be flakey!) + out := RunInteractiveBashCommands(t, `unset HISHTORY_TEST +go build -o /tmp/client +/tmp/client install`) + r := regexp.MustCompile(`Setting secret hishtory key to (.*)`) + matches := r.FindStringSubmatch(out) + if len(matches) != 2 { + t.Fatalf("Failed to extract userSecret from output: matches=%#v", matches) + } + userSecret := matches[1] + + // Assert that config.sh isn't the dev version + // TODO: do ^ + + // Test the status subcommand + out = RunInteractiveBashCommands(t, `hishtory status`) + if out != fmt.Sprintf("Hishtory: v0.Unknown\nEnabled: true\nSecret Key: %s\nCommit Hash: Unknown\n", userSecret) { + t.Fatalf("status command has unexpected output: %#v", out) + } + + // Test recording commands + out, err := RunInteractiveBashCommandsWithoutStrictMode(t, `set -m +ls /a +echo foo`) + if err != nil { + t.Fatal(err) + } + if out != "foo\n" { + t.Fatalf("unexpected output from running commands: %#v", out) + } + + // Test querying for all commands + out = hishtoryQuery(t, "") + expected := []string{"echo foo", "ls /a"} + for _, item := range expected { + if !strings.Contains(out, item) { + t.Fatalf("output is missing expected item %#v: %#v", item, out) + } + } + + // Test querying for a specific command + out = hishtoryQuery(t, "foo") + if !strings.Contains(out, "echo foo") { + t.Fatalf("output doesn't contain the expected item, out=%#v", out) + } + if strings.Contains(out, "ls /a") { + t.Fatalf("output contains unexpected item, out=%#v", out) + } +} diff --git a/client/lib/lib.go b/client/lib/lib.go index 8675d52..165b11e 100644 --- a/client/lib/lib.go +++ b/client/lib/lib.go @@ -23,6 +23,7 @@ import ( "gorm.io/driver/sqlite" "gorm.io/gorm" + "gorm.io/gorm/logger" "github.com/fatih/color" "github.com/google/uuid" @@ -35,6 +36,9 @@ import ( //go:embed config.sh var ConfigShContents string +//go:embed test_config.sh +var TestConfigShContents string + func getCwd() (string, error) { cwd, err := os.Getwd() if err != nil { @@ -219,7 +223,16 @@ func GetConfig() (ClientConfig, error) { } data, err := os.ReadFile(path.Join(homedir, shared.HISHTORY_PATH, shared.CONFIG_PATH)) if err != nil { - return ClientConfig{}, fmt.Errorf("failed to read config file: %v", err) + files, err := ioutil.ReadDir(path.Join(homedir, shared.HISHTORY_PATH)) + if err != nil { + return ClientConfig{}, fmt.Errorf("failed to read config file (and failed to list too): %v", err) + } + filenames := "" + for _, file := range files { + filenames += file.Name() + filenames += ", " + } + return ClientConfig{}, fmt.Errorf("failed to read config file (files in ~/.hishtory/: %s): %v", filenames, err) } var config ClientConfig err = json.Unmarshal(data, &config) @@ -312,7 +325,11 @@ func Install() error { func configureBashrc(homedir, binaryPath string) error { // Create the file we're going to source in our bashrc. Do this no matter what in case there are updates to it. bashConfigPath := path.Join(filepath.Dir(binaryPath), "config.sh") - err := ioutil.WriteFile(bashConfigPath, []byte(ConfigShContents), 0o644) + configContents := ConfigShContents + if os.Getenv("HISHTORY_TEST") != "" { + configContents = TestConfigShContents + } + err := ioutil.WriteFile(bashConfigPath, []byte(configContents), 0o644) if err != nil { return fmt.Errorf("failed to write config.sh file: %v", err) } @@ -428,7 +445,17 @@ func OpenLocalSqliteDb() (*gorm.DB, error) { if err != nil { return nil, fmt.Errorf("failed to create ~/.hishtory dir: %v", err) } - db, err := gorm.Open(sqlite.Open(path.Join(homedir, shared.HISHTORY_PATH, shared.DB_PATH)), &gorm.Config{SkipDefaultTransaction: true}) + newLogger := logger.New( + // TODO: change this back to warn, but have it go to a file? + log.New(os.Stdout, "\n", log.LstdFlags), + logger.Config{ + SlowThreshold: 200 * time.Millisecond, + LogLevel: logger.Silent, + IgnoreRecordNotFoundError: true, + Colorful: false, + }, + ) + db, err := gorm.Open(sqlite.Open(path.Join(homedir, shared.HISHTORY_PATH, shared.DB_PATH)), &gorm.Config{SkipDefaultTransaction: true, Logger: newLogger}) if err != nil { return nil, fmt.Errorf("failed to connect to the DB: %v", err) } diff --git a/client/lib/test_config.sh b/client/lib/test_config.sh new file mode 100644 index 0000000..1af34c3 --- /dev/null +++ b/client/lib/test_config.sh @@ -0,0 +1,29 @@ +# This script should be sourced inside of .bashrc to integrate bash with hishtory +# This is the same as config.sh, except it doesn't run the save process in the background. This is crucial to making tests reproducible. + +# Implementation of PreCommand and PostCommand based on https://jichu4n.com/posts/debug-trap-and-prompt_command-in-bash/ +function PreCommand() { + if [ -z "$HISHTORY_AT_PROMPT" ]; then + return + fi + unset HISHTORY_AT_PROMPT + + # Run before every command + HISHTORY_START_TIME=`date +%s%N` +} +trap "PreCommand" DEBUG + +HISHTORY_FIRST_PROMPT=1 +function PostCommand() { + EXIT_CODE=$? + HISHTORY_AT_PROMPT=1 + + if [ -n "$HISHTORY_FIRST_PROMPT" ]; then + unset HISHTORY_FIRST_PROMPT + return + fi + + # Run after every prompt + hishtory saveHistoryEntry $EXIT_CODE "`history 1`" $HISHTORY_START_TIME +} +PROMPT_COMMAND="PostCommand" diff --git a/go.mod b/go.mod index 9d7d86b..4f408a6 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( require ( github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect + github.com/google/go-cmp v0.5.7 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.10.1 // indirect github.com/jackc/pgio v1.0.0 // indirect diff --git a/go.sum b/go.sum index aebc8af..a83d433 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -197,6 +199,7 @@ golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/hishtory.go b/hishtory.go index a79a830..e1e95fc 100644 --- a/hishtory.go +++ b/hishtory.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "log" "net/http" "os" "strings" @@ -127,7 +128,9 @@ func displayBannerIfSet() error { func saveHistoryEntry() { config, err := lib.GetConfig() - lib.CheckFatalError(err) + if err != nil { + log.Fatalf("hishtory cannot save an entry because the hishtory config file does not exist, try running `hishtory init` (err=%v)", err) + } if !config.IsEnabled { return } diff --git a/shared/testutils.go b/shared/testutils.go index d191d38..00cc2d8 100644 --- a/shared/testutils.go +++ b/shared/testutils.go @@ -20,6 +20,8 @@ func ResetLocalState(t *testing.T) { _ = os.Remove(path.Join(homedir, HISHTORY_PATH, DB_PATH)) _ = os.Remove(path.Join(homedir, HISHTORY_PATH, CONFIG_PATH)) + _ = os.Remove(path.Join(homedir, HISHTORY_PATH, "hishtory")) + _ = os.Remove(path.Join(homedir, HISHTORY_PATH, "config.sh")) } func BackupAndRestore(t *testing.T) func() { @@ -30,9 +32,13 @@ func BackupAndRestore(t *testing.T) func() { _ = os.Rename(path.Join(homedir, HISHTORY_PATH, DB_PATH), path.Join(homedir, HISHTORY_PATH, DB_PATH+".bak")) _ = os.Rename(path.Join(homedir, HISHTORY_PATH, CONFIG_PATH), path.Join(homedir, HISHTORY_PATH, CONFIG_PATH+".bak")) + _ = os.Rename(path.Join(homedir, HISHTORY_PATH, "hishtory"), path.Join(homedir, HISHTORY_PATH, "hishtory.bak")) + _ = os.Rename(path.Join(homedir, HISHTORY_PATH, "config.sh"), path.Join(homedir, HISHTORY_PATH, "config.sh.bak")) return func() { _ = os.Rename(path.Join(homedir, HISHTORY_PATH, DB_PATH+".bak"), path.Join(homedir, HISHTORY_PATH, DB_PATH)) _ = os.Rename(path.Join(homedir, HISHTORY_PATH, CONFIG_PATH+".bak"), path.Join(homedir, HISHTORY_PATH, CONFIG_PATH)) + _ = os.Rename(path.Join(homedir, HISHTORY_PATH, "hishtory.bak"), path.Join(homedir, HISHTORY_PATH, "hishtory")) + _ = os.Rename(path.Join(homedir, HISHTORY_PATH, "config.sh.bak"), path.Join(homedir, HISHTORY_PATH, "config.sh")) } } @@ -88,7 +94,7 @@ func RunTestServer(t *testing.T) func() { }() return func() { err := cmd.Process.Kill() - if err != nil { + if err != nil && err.Error() != "os: process already finished" { t.Fatalf("failed to kill process: %v", err) } if strings.Contains(stderr.String()+stdout.String(), "failed to") {