Skip to content

Commit

Permalink
fixup! Catch panic from bbolt open connection
Browse files Browse the repository at this point in the history
  • Loading branch information
gandarez committed Aug 9, 2023
1 parent 55bdc90 commit 3b281ff
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 53 deletions.
48 changes: 28 additions & 20 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/wakatime/wakatime-cli/pkg/log"
"github.com/wakatime/wakatime-cli/pkg/offline"
"github.com/wakatime/wakatime-cli/pkg/vipertools"
"github.com/wakatime/wakatime-cli/pkg/wakaerror"

"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand Down Expand Up @@ -259,7 +260,9 @@ func RunCmdWithOfflineSync(v *viper.Viper, verbose bool, sendDiagsOnErrors bool,
os.Exit(exitCode)
}

os.Exit(runCmd(v, verbose, sendDiagsOnErrors, offlinesync.Run))
exitCode = runCmd(v, verbose, sendDiagsOnErrors, offlinesync.Run)

os.Exit(exitCode)
}

// runCmd contains the main logic of RunCmd.
Expand Down Expand Up @@ -297,8 +300,17 @@ func runCmd(v *viper.Viper, verbose bool, sendDiagsOnErrors bool, cmd cmdFn) int

// run command
exitCode, err := cmd(v)
if err != nil && (verbose || canLogError(err)) {
log.Errorf("failed to run command: %s", err)
// nolint:nestif
if err != nil {
if errwaka, ok := err.(wakaerror.Error); ok {
sendDiagsOnErrors = sendDiagsOnErrors || errwaka.SendDiagsOnErrors()
// if verbose is not set, use the value from the error
verbose = verbose || errwaka.ShouldLogError()
}

if verbose {
log.Errorf("failed to run command: %s", err)
}

resetLogs()

Expand Down Expand Up @@ -330,31 +342,31 @@ func saveHeartbeats(v *viper.Viper) {

func sendDiagnostics(v *viper.Viper, d diagnostics) error {
paramAPI, err := params.LoadAPIParams(v)
// prevent sending diags for api key errors
if err != nil && !errors.As(err, &api.ErrAuth{}) {
return fmt.Errorf("failed to load API parameters: %s", err)
}

c, err := cmdapi.NewClient(paramAPI)
if err != nil {
// Prevent sending diags for api key errors.
if !errors.As(err, &api.ErrAuth{}) {
return fmt.Errorf("failed to load API parameters: %s", err)
}
log.Warnf("failed to initialize api client: %s", err)

// Prevent sending diags for api connection errors.
if !errors.As(err, &api.ErrBackoff{}) {
return fmt.Errorf("failed to load API parameters: %s", err)
// try without authencation
c, err = cmdapi.NewClientWithoutAuth(paramAPI)
if err != nil {
return fmt.Errorf("failed to initialize api client without auth: %s", err)
}
}

c, err := cmdapi.NewClientWithoutAuth(paramAPI)
if err != nil {
return fmt.Errorf("failed to initialize api client: %s", err)
}
// foce disable ssl verification
api.WithDisableSSLVerify()(c)

diagnostics := []diagnostic.Diagnostic{
diagnostic.Error(d.OriginalError),
diagnostic.Logs(d.Logs),
diagnostic.Stack(d.Stack),
}

api.WithDisableSSLVerify()(c)

err = c.SendDiagnostics(paramAPI.Plugin, d.Panicked, diagnostics...)
if err != nil {
return fmt.Errorf("failed to send diagnostics to the API: %s", err)
Expand All @@ -377,7 +389,3 @@ func captureLogs(dest io.Writer) func() {
log.SetOutput(logOutput)
}
}

func canLogError(err error) bool {
return !errors.As(err, &api.ErrBackoff{})
}
17 changes: 10 additions & 7 deletions cmd/run_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func TestRunCmd_ErrOfflineEnqueue(t *testing.T) {
router.HandleFunc("/plugins/errors", func(w http.ResponseWriter, req *http.Request) {
// check request
assert.Equal(t, http.MethodPost, req.Method)
assert.Nil(t, req.Header["Authorization"])
assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"])
assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"])

expectedBodyTpl, err := os.ReadFile("testdata/diagnostics_request_template.json")
Expand All @@ -63,19 +63,22 @@ func TestRunCmd_ErrOfflineEnqueue(t *testing.T) {
require.NoError(t, err)

var diagnostics struct {
Platform string `json:"platform"`
Architecture string `json:"architecture"`
CliVersion string `json:"cli_version"`
Editor string `json:"editor"`
Logs string `json:"logs"`
Stack string `json:"stacktrace"`
Architecture string `json:"architecture"`
CliVersion string `json:"cli_version"`
Editor string `json:"editor"`
Logs string `json:"logs"`
OriginalError string `json:"error_message"`
Platform string `json:"platform"`
Plugin string `json:"plugin"`
Stack string `json:"stacktrace"`
}

err = json.Unmarshal(body, &diagnostics)
require.NoError(t, err)

expectedBodyStr := fmt.Sprintf(
string(expectedBodyTpl),
jsonEscape(t, diagnostics.OriginalError),
jsonEscape(t, diagnostics.Logs),
jsonEscape(t, diagnostics.Stack),
)
Expand Down
98 changes: 95 additions & 3 deletions cmd/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/wakatime/wakatime-cli/cmd"
"github.com/wakatime/wakatime-cli/pkg/offline"
"github.com/wakatime/wakatime-cli/pkg/version"

"github.com/spf13/viper"
Expand Down Expand Up @@ -172,7 +173,7 @@ func TestRunCmd_SendDiagnostics_Error(t *testing.T) {

// check request
assert.Equal(t, http.MethodPost, req.Method)
assert.Nil(t, req.Header["Authorization"])
assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"])
assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"])

expectedBodyTpl, err := os.ReadFile("testdata/diagnostics_request_template.json")
Expand Down Expand Up @@ -263,7 +264,7 @@ func TestRunCmd_SendDiagnostics_Panic(t *testing.T) {

// check request
assert.Equal(t, http.MethodPost, req.Method)
assert.Nil(t, req.Header["Authorization"])
assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"])
assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"])

expectedBodyTpl, err := os.ReadFile("testdata/diagnostics_request_panic_template.json")
Expand Down Expand Up @@ -355,7 +356,7 @@ func TestRunCmd_SendDiagnostics_NoLogs_Panic(t *testing.T) {

// check request
assert.Equal(t, http.MethodPost, req.Method)
assert.Nil(t, req.Header["Authorization"])
assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"])
assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"])

expectedBodyTpl, err := os.ReadFile("testdata/diagnostics_request_panic_no_logs_template.json")
Expand Down Expand Up @@ -404,6 +405,97 @@ func TestRunCmd_SendDiagnostics_NoLogs_Panic(t *testing.T) {
assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond)
}

func TestRunCmd_SendDiagnostics_WakaError(t *testing.T) {
// this is exclusively run in subprocess
if os.Getenv("TEST_RUN") == "1" {
version.OS = "some os"
version.Arch = "some architecture"
version.Version = "some version"

tmpDir := t.TempDir()

offlineQueueFile, err := os.CreateTemp(tmpDir, "")
require.NoError(t, err)

logFile, err := os.CreateTemp(tmpDir, "")
require.NoError(t, err)

v := viper.New()
v.Set("api-url", os.Getenv("TEST_SERVER_URL"))
v.Set("entity", "/path/to/file")
v.Set("key", "00000000-0000-4000-8000-000000000000")
v.Set("log-file", logFile.Name())
v.Set("log-to-stdout", true)
v.Set("offline-queue-file", offlineQueueFile.Name())
v.Set("plugin", "vim")

cmd.RunCmd(v, false, false, func(v *viper.Viper) (int, error) {
return 42, offline.ErrOpenDB{Err: errors.New("fail")}
})

return
}

testServerURL, router, tearDown := setupTestServer()
defer tearDown()

var numCalls int

router.HandleFunc("/plugins/errors", func(w http.ResponseWriter, req *http.Request) {
numCalls++

// check request
assert.Equal(t, http.MethodPost, req.Method)
assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"])
assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"])

expectedBodyTpl, err := os.ReadFile("testdata/diagnostics_request_template.json")
require.NoError(t, err)

body, err := io.ReadAll(req.Body)
require.NoError(t, err)

var diagnostics struct {
Architecture string `json:"architecture"`
CliVersion string `json:"cli_version"`
Logs string `json:"logs"`
OriginalError string `json:"error_message"`
Platform string `json:"platform"`
Plugin string `json:"plugin"`
Stack string `json:"stacktrace"`
}

err = json.Unmarshal(body, &diagnostics)
require.NoError(t, err)

expectedBodyStr := fmt.Sprintf(
string(expectedBodyTpl),
jsonEscape(t, diagnostics.OriginalError),
jsonEscape(t, diagnostics.Logs),
jsonEscape(t, diagnostics.Stack),
)

assert.JSONEq(t, expectedBodyStr, string(body))

// send response
w.WriteHeader(http.StatusCreated)
})

// run command in another runner, to effectively test os.Exit()
cmd := exec.Command(os.Args[0], "-test.run=TestRunCmd_SendDiagnostics_WakaError") // nolint:gosec
cmd.Env = append(os.Environ(), "TEST_RUN=1")
cmd.Env = append(cmd.Env, fmt.Sprintf("TEST_SERVER_URL=%s", testServerURL))

err := cmd.Run()

e, ok := err.(*exec.ExitError)
require.True(t, ok)

assert.Equal(t, 42, e.ExitCode())

assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond)
}

func TestRunCmdWithOfflineSync(t *testing.T) {
// this is exclusively run in subprocess
if os.Getenv("TEST_RUN") == "1" {
Expand Down
13 changes: 13 additions & 0 deletions pkg/api/api_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package api_test

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
)

func setupTestServer() (string, *http.ServeMux, func()) {
Expand All @@ -11,3 +15,12 @@ func setupTestServer() (string, *http.ServeMux, func()) {

return srv.URL, router, func() { srv.Close() }
}

func jsonEscape(t *testing.T, i string) string {
b, err := json.Marshal(i)
require.NoError(t, err)

s := string(b)

return s[1 : len(s)-1]
}
34 changes: 28 additions & 6 deletions pkg/api/diagnostic_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package api_test

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
Expand All @@ -26,25 +28,45 @@ func TestClient_SendDiagnostics(t *testing.T) {

// check method and headers
assert.Equal(t, http.MethodPost, req.Method)
assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"])
assert.Nil(t, req.Header["Authorization"])
assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"])

// check body
expectedBody, err := os.ReadFile("testdata/diagnostics_request.json")
expectedBodyTpl, err := os.ReadFile("testdata/diagnostics_request_template.json")
require.NoError(t, err)

body, err := io.ReadAll(req.Body)
require.NoError(t, err)

assert.JSONEq(t, string(expectedBody), string(body))
var diagnostics struct {
Architecture string `json:"architecture"`
CliVersion string `json:"cli_version"`
Logs string `json:"logs"`
OriginalError string `json:"error_message"`
Platform string `json:"platform"`
Plugin string `json:"plugin"`
Stack string `json:"stacktrace"`
}

err = json.Unmarshal(body, &diagnostics)
require.NoError(t, err)

expectedBodyStr := fmt.Sprintf(
string(expectedBodyTpl),
jsonEscape(t, diagnostics.OriginalError),
jsonEscape(t, diagnostics.Logs),
jsonEscape(t, diagnostics.Stack),
)

assert.JSONEq(t, expectedBodyStr, string(body))

// write response
w.WriteHeader(http.StatusCreated)
})

version.OS = "linux"
version.Arch = "amd64"
version.Version = "<local-build>"
version.OS = "some os"
version.Arch = "some architecture"
version.Version = "some version"

diagnostics := []diagnostic.Diagnostic{
diagnostic.Error("some error"),
Expand Down
Loading

0 comments on commit 3b281ff

Please sign in to comment.