diff --git a/.gitignore b/.gitignore index 98616f19..9cd08e81 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ maze_output vendor -features/fixtures/testbuild \ No newline at end of file +features/fixtures/testbuild + +# ignore the gemfile to prevent testing against stale versions +Gemfile.lock diff --git a/Gemfile b/Gemfile index 2971d8a3..ad46a955 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source 'https://rubygems.org' -gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', branch: 'v1' +gem "bugsnag-maze-runner", "~> 9.9" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 5ff8a517..46596335 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,55 +1,155 @@ -GIT - remote: https://github.com/bugsnag/maze-runner - revision: 7377529a77eb7585afc66cd2080fcdc4eea3306a - branch: v1 - specs: - bugsnag-maze-runner (1.1.0) - cucumber (~> 3.1.0) - cucumber-expressions (= 5.0.15) - minitest (~> 5.0) - os (~> 1.0.0) - rack (~> 2.0.0) - rake (~> 12.3.3) - test-unit (~> 3.2.0) - GEM remote: https://rubygems.org/ specs: - backports (3.21.0) - builder (3.2.4) - cucumber (3.1.0) - builder (>= 2.1.2) - cucumber-core (~> 3.1.0) - cucumber-expressions (~> 5.0.4) - cucumber-wire (~> 0.0.1) - diff-lcs (~> 1.3) - gherkin (~> 5.0) - multi_json (>= 1.7.5, < 2.0) - multi_test (>= 0.1.2) - cucumber-core (3.1.0) - backports (>= 3.8.0) - cucumber-tag_expressions (~> 1.1.0) - gherkin (>= 5.0.0) - cucumber-expressions (5.0.15) - cucumber-tag_expressions (1.1.1) - cucumber-wire (0.0.1) - diff-lcs (1.4.4) - gherkin (5.1.0) - minitest (5.14.4) - multi_json (1.15.0) + appium_lib (12.0.1) + appium_lib_core (~> 5.0) + nokogiri (~> 1.8, >= 1.8.1) + tomlrb (>= 1.1, < 3.0) + appium_lib_core (5.4.0) + faye-websocket (~> 0.11.0) + selenium-webdriver (~> 4.2, < 4.6) + bugsnag (6.27.1) + concurrent-ruby (~> 1.0) + bugsnag-maze-runner (9.12.0) + appium_lib (~> 12.0.0) + appium_lib_core (~> 5.4.0) + bugsnag (~> 6.24) + cucumber (~> 7.1) + cucumber-expressions (~> 6.0.0) + curb (~> 1.0.5) + dogstatsd-ruby (~> 5.5.0) + json_schemer (~> 0.2.24) + optimist (~> 3.0.1) + os (~> 1.0.0) + rack (~> 2.2) + rake (~> 12.3.3) + rubyzip (~> 2.3.2) + selenium-webdriver (~> 4.0) + test-unit (~> 3.5.2) + webrick (~> 1.7.0) + builder (3.3.0) + childprocess (4.1.0) + concurrent-ruby (1.3.3) + cucumber (7.1.0) + builder (~> 3.2, >= 3.2.4) + cucumber-core (~> 10.1, >= 10.1.0) + cucumber-create-meta (~> 6.0, >= 6.0.1) + cucumber-cucumber-expressions (~> 14.0, >= 14.0.0) + cucumber-gherkin (~> 22.0, >= 22.0.0) + cucumber-html-formatter (~> 17.0, >= 17.0.0) + cucumber-messages (~> 17.1, >= 17.1.1) + cucumber-wire (~> 6.2, >= 6.2.0) + diff-lcs (~> 1.4, >= 1.4.4) + mime-types (~> 3.3, >= 3.3.1) + multi_test (~> 0.1, >= 0.1.2) + sys-uname (~> 1.2, >= 1.2.2) + cucumber-core (10.1.1) + cucumber-gherkin (~> 22.0, >= 22.0.0) + cucumber-messages (~> 17.1, >= 17.1.1) + cucumber-tag-expressions (~> 4.1, >= 4.1.0) + cucumber-create-meta (6.0.4) + cucumber-messages (~> 17.1, >= 17.1.1) + sys-uname (~> 1.2, >= 1.2.2) + cucumber-cucumber-expressions (14.0.0) + cucumber-expressions (6.0.1) + cucumber-gherkin (22.0.0) + cucumber-messages (~> 17.1, >= 17.1.1) + cucumber-html-formatter (17.0.0) + cucumber-messages (~> 17.1, >= 17.1.0) + cucumber-messages (17.1.1) + cucumber-tag-expressions (4.1.0) + cucumber-wire (6.2.1) + cucumber-core (~> 10.1, >= 10.1.0) + cucumber-cucumber-expressions (~> 14.0, >= 14.0.0) + curb (1.0.5) + diff-lcs (1.5.1) + dogstatsd-ruby (5.5.0) + ecma-re-validator (0.4.0) + regexp_parser (~> 2.2) + eventmachine (1.2.7) + faye-websocket (0.11.3) + eventmachine (>= 0.12.0) + websocket-driver (>= 0.5.1) + ffi (1.17.0-aarch64-linux-gnu) + ffi (1.17.0-aarch64-linux-musl) + ffi (1.17.0-arm-linux-gnu) + ffi (1.17.0-arm-linux-musl) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86-linux-gnu) + ffi (1.17.0-x86-linux-musl) + ffi (1.17.0-x86_64-darwin) + ffi (1.17.0-x86_64-linux-gnu) + ffi (1.17.0-x86_64-linux-musl) + hana (1.3.7) + json_schemer (0.2.25) + ecma-re-validator (~> 0.3) + hana (~> 1.3) + regexp_parser (~> 2.0) + simpleidn (~> 0.2) + uri_template (~> 0.7) + mime-types (3.5.2) + mime-types-data (~> 3.2015) + mime-types-data (3.2024.0806) multi_test (0.1.2) + nokogiri (1.16.7-aarch64-linux) + racc (~> 1.4) + nokogiri (1.16.7-arm-linux) + racc (~> 1.4) + nokogiri (1.16.7-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.7-x86-linux) + racc (~> 1.4) + nokogiri (1.16.7-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.7-x86_64-linux) + racc (~> 1.4) + optimist (3.0.1) os (1.0.1) - power_assert (2.0.0) - rack (2.0.9.3) + power_assert (2.0.3) + racc (1.8.1) + rack (2.2.9) rake (12.3.3) - test-unit (3.2.9) + regexp_parser (2.9.2) + rexml (3.3.4) + strscan + rubyzip (2.3.2) + selenium-webdriver (4.5.0) + childprocess (>= 0.5, < 5.0) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + simpleidn (0.2.3) + strscan (3.1.0) + sys-uname (1.3.0) + ffi (~> 1.1) + test-unit (3.5.9) power_assert + tomlrb (2.0.3) + uri_template (0.7.0) + webrick (1.7.0) + websocket (1.2.11) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) PLATFORMS - ruby + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86-linux + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES - bugsnag-maze-runner! + bugsnag-maze-runner (~> 9.9) BUNDLED WITH - 2.1.4 + 2.5.17 diff --git a/features/apptype.feature b/features/apptype.feature index efe897fc..f03d3fd0 100644 --- a/features/apptype.feature +++ b/features/apptype.feature @@ -1,22 +1,18 @@ Feature: Configuring app type Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - And I configure the bugsnag endpoint - And I set environment variable "APP_TYPE" to "background-queue" - And I have built the service "app" + Given I set environment variable "APP_TYPE" to "background-queue" Scenario: An error report contains the configured app type when running a go app Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" - When I run the go service "app" with the test case "handled" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run HandledScenario + And I wait to receive an error And the event "app.type" equals "background-queue" Scenario: An session report contains the configured app type when running a go app Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" - When I run the go service "app" with the test case "session" - Then I wait to receive a request after the start up session - And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - And the payload field "app.type" equals "background-queue" - + When I start the service "app" + And I run SendSessionScenario + And I wait to receive a session + And the session payload field "app.type" equals "background-queue" diff --git a/features/appversion.feature b/features/appversion.feature index 805bf940..ca552806 100644 --- a/features/appversion.feature +++ b/features/appversion.feature @@ -1,21 +1,18 @@ Feature: Configuring app version Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - And I configure the bugsnag endpoint And I set environment variable "APP_VERSION" to "3.1.2" - And I have built the service "app" Scenario: An error report contains the configured app type when running a go app Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" - When I run the go service "app" with the test case "handled" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run HandledScenario + And I wait to receive an error And the event "app.version" equals "3.1.2" -Scenario: An session report contains the configured app type when running a go app +Scenario: A session report contains the configured app type when running a go app Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" - When I run the go service "app" with the test case "session" - Then I wait to receive a request after the start up session - And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - And the payload field "app.version" equals "3.1.2" + When I start the service "app" + And I run SendSessionScenario + And I wait to receive a session + And the session payload field "app.version" equals "3.1.2" diff --git a/features/autonotify.feature b/features/autonotify.feature index 9a79dcfa..34eb39a6 100644 --- a/features/autonotify.feature +++ b/features/autonotify.feature @@ -1,13 +1,8 @@ Feature: Using auto notify -Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - And I configure the bugsnag endpoint - And I have built the service "app" - Scenario: An error report is sent when an AutoNotified crash occurs which later gets recovered - When I run the go service "app" with the test case "autonotify" - Then I wait for 3 seconds - And the request 1 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - And the exception "errorClass" equals "*errors.errorString" for request 1 - And the exception "message" equals "Go routine killed with auto notify" for request 1 + When I start the service "app" + And I run AutonotifyPanicScenario + And I wait to receive an error + And the exception "errorClass" equals "Error" + And the exception "message" equals "Go routine killed with auto notify" diff --git a/features/endpoint.feature b/features/endpoint.feature deleted file mode 100644 index 803f6f4b..00000000 --- a/features/endpoint.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: Configuring endpoint - -Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - And I configure the bugsnag endpoint - And I have built the service "app" - -Scenario: An error report is sent successfully using the notify endpoint only - When I run the go service "app" with the test case "endpoint-notify" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - -Scenario: Configuring Bugsnag will panic if the sessions endpoint is configured without the notify endpoint - When I run the go service "app" with the test case "endpoint-session" - And I wait for 3 second - Then I should receive no requests diff --git a/features/fixtures/app/Dockerfile b/features/fixtures/app/Dockerfile index 2533d28d..ae79fd90 100644 --- a/features/fixtures/app/Dockerfile +++ b/features/fixtures/app/Dockerfile @@ -5,8 +5,9 @@ RUN apk update && apk upgrade && apk add git bash ENV GOPATH /app -COPY testbuild /app/src/github.com/bugsnag/bugsnag-go -WORKDIR /app/src/github.com/bugsnag/bugsnag-go/v2 +COPY features /app/src/features +COPY v2 /app/src/v2 +WORKDIR /app/src/v2 # Ensure subsequent steps are re-run if the GO_VERSION variable changes ARG GO_VERSION @@ -20,15 +21,12 @@ RUN if [[ $(echo -e "1.11\n$GO_VERSION\n1.16" | sort -V | head -2 | tail -1) == go install ./...; \ fi -# Copy test scenarios -COPY ./app /app/src/test -WORKDIR /app/src/test +WORKDIR /app/src/features/fixtures/app # Create app module - avoid locking bugsnag dep by not checking it in # Skip on old versions of Go which pre-date modules RUN if [[ $GO_VERSION != '1.11' && $GO_VERSION != '1.12' ]]; then \ go mod init && go mod tidy; \ + echo "replace github.com/bugsnag/bugsnag-go/v2 => /app/src/v2" >> go.mod; \ + go mod tidy; \ fi - -RUN chmod +x run.sh -CMD ["/app/src/test/run.sh"] diff --git a/features/fixtures/app/command.go b/features/fixtures/app/command.go new file mode 100644 index 00000000..8c45c601 --- /dev/null +++ b/features/fixtures/app/command.go @@ -0,0 +1,42 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +const DEFAULT_MAZE_ADDRESS = "http://localhost:9339" + +type Command struct { + Action string `json:"action,omitempty"` + ScenarioName string `json:"scenario_name,omitempty"` + APIKey string `json:"api_key,omitempty"` + NotifyEndpoint string `json:"notify_endpoint,omitempty"` + SessionsEndpoint string `json:"sessions_endpoint,omitempty"` + UUID string `json:"uuid,omitempty"` + RunUUID string `json:"run_uuid,omitempty"` +} + +func GetCommand(mazeAddress string) Command { + var command Command + mazeURL := fmt.Sprintf("%+v/command", mazeAddress) + client := http.Client{Timeout: 2 * time.Second} + res, err := client.Get(mazeURL) + if err != nil { + fmt.Printf("[Bugsnag] Error while receiving command: %+v\n", err) + return command + } + + if res != nil { + err = json.NewDecoder(res.Body).Decode(&command) + res.Body.Close() + if err != nil { + fmt.Printf("[Bugsnag] Error while decoding command: %+v\n", err) + return command + } + } + + return command +} diff --git a/features/fixtures/app/configure.go b/features/fixtures/app/configure.go new file mode 100644 index 00000000..3dcb4aca --- /dev/null +++ b/features/fixtures/app/configure.go @@ -0,0 +1,47 @@ +package main + +import ( + "os" + "strconv" + "strings" + + bugsnag "github.com/bugsnag/bugsnag-go/v2" +) + +func ConfigureBugsnag() bugsnag.Configuration { + config := bugsnag.Configuration{ + APIKey: os.Getenv("API_KEY"), + AppVersion: os.Getenv("APP_VERSION"), + AppType: os.Getenv("APP_TYPE"), + Hostname: os.Getenv("HOSTNAME"), + } + + if notifyReleaseStages := os.Getenv("NOTIFY_RELEASE_STAGES"); notifyReleaseStages != "" { + config.NotifyReleaseStages = strings.Split(notifyReleaseStages, ",") + } + + if releaseStage := os.Getenv("RELEASE_STAGE"); releaseStage != "" { + config.ReleaseStage = releaseStage + } + + if filters := os.Getenv("PARAMS_FILTERS"); filters != "" { + config.ParamsFilters = []string{filters} + } + + sync, err := strconv.ParseBool(os.Getenv("SYNCHRONOUS")) + if err == nil { + config.Synchronous = sync + } + + acs, err := strconv.ParseBool(os.Getenv("AUTO_CAPTURE_SESSIONS")) + if err == nil { + config.AutoCaptureSessions = acs + } + + config.Endpoints = bugsnag.Endpoints{ + Notify: os.Getenv("BUGSNAG_NOTIFY_ENDPOINT"), + Sessions: os.Getenv("BUGSNAG_SESSIONS_ENDPOINT"), + } + + return config +} diff --git a/features/fixtures/app/handled_scenario.go b/features/fixtures/app/handled_scenario.go new file mode 100644 index 00000000..81fad8b6 --- /dev/null +++ b/features/fixtures/app/handled_scenario.go @@ -0,0 +1,103 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/bugsnag/bugsnag-go/v2" +) + +func HandledErrorScenario() (bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + if _, err := os.Open("nonexistent_file.txt"); err != nil { + if errClass := os.Getenv("ERROR_CLASS"); errClass != "" { + bugsnag.Notify(err, bugsnag.ErrorClass{Name: errClass}) + } else { + bugsnag.Notify(err) + } + } + } + return config, scenarioFunc +} + +func MultipleHandledErrorsScenario() (bugsnag.Configuration, func()) { + //Make the order of the below predictable + config := bugsnag.Configuration{Synchronous: true} + + scenarioFunc := func() { + ctx := bugsnag.StartSession(context.Background()) + bugsnag.Notify(fmt.Errorf("oops"), ctx) + bugsnag.Notify(fmt.Errorf("oops"), ctx) + } + return config, scenarioFunc +} + +func NestedHandledErrorScenario() (bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + if err := Login("token " + os.Getenv("API_KEY")); err != nil { + bugsnag.Notify(NewCustomErr("terminate process", err)) + } else { + i := len(os.Getenv("API_KEY")) + // Some nonsense to avoid inlining checkValue + if val, err := CheckValue(i); err != nil { + fmt.Printf("err: %v, val: %d", err, val) + } + if val, err := CheckValue(i - 46); err != nil { + fmt.Printf("err: %v, val: %d", err, val) + } + + log.Fatalf("This test is broken - no error was generated.") + } + } + return config, scenarioFunc +} + +func HandledCallbackErrorScenario() (bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + bugsnag.Notify(fmt.Errorf("inadequent Prep Error"), func(event *bugsnag.Event) { + event.Context = "nonfatal.go:14" + event.Severity = bugsnag.SeverityInfo + + event.Stacktrace[1].File = ">insertion<" + event.Stacktrace[1].LineNumber = 0 + }) + } + return config, scenarioFunc +} + +func HandledToUnhandledScenario() (bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + bugsnag.Notify(fmt.Errorf("unknown event"), func(event *bugsnag.Event) { + event.Unhandled = true + event.Severity = bugsnag.SeverityError + }) + } + return config, scenarioFunc +} + +func OnBeforeNotifyScenario() (bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + bugsnag.OnBeforeNotify( + func(event *bugsnag.Event, config *bugsnag.Configuration) error { + if event.Message == "ignore this error" { + return fmt.Errorf("not sending errors to ignore") + } + // continue notifying as normal + if event.Message == "change error message" { + event.Message = "Error message was changed" + } + return nil + }) + bugsnag.Notify(fmt.Errorf("ignore this error")) + bugsnag.Notify(fmt.Errorf("don't ignore this error")) + bugsnag.Notify(fmt.Errorf("change error message")) + } + return config, scenarioFunc +} diff --git a/features/fixtures/app/main.go b/features/fixtures/app/main.go index 2751a643..c19f0714 100644 --- a/features/fixtures/app/main.go +++ b/features/fixtures/app/main.go @@ -2,327 +2,64 @@ package main import ( "context" - "flag" "fmt" - "log" "os" - "runtime" - "strconv" - "strings" + "os/signal" + "syscall" "time" - bugsnag "github.com/bugsnag/bugsnag-go/v2" + "github.com/bugsnag/bugsnag-go/v2" ) -func configureBasicBugsnag(testcase string) { - config := bugsnag.Configuration{ - APIKey: os.Getenv("API_KEY"), - AppVersion: os.Getenv("APP_VERSION"), - AppType: os.Getenv("APP_TYPE"), - Hostname: os.Getenv("HOSTNAME"), - } - - if notifyReleaseStages := os.Getenv("NOTIFY_RELEASE_STAGES"); notifyReleaseStages != "" { - config.NotifyReleaseStages = strings.Split(notifyReleaseStages, ",") - } - - if releaseStage := os.Getenv("RELEASE_STAGE"); releaseStage != "" { - config.ReleaseStage = releaseStage - } - - if filters := os.Getenv("PARAMS_FILTERS"); filters != "" { - config.ParamsFilters = []string{filters} - } - - sync, err := strconv.ParseBool(os.Getenv("SYNCHRONOUS")) - if err == nil { - config.Synchronous = sync - } - - acs, err := strconv.ParseBool(os.Getenv("AUTO_CAPTURE_SESSIONS")) - if err == nil { - config.AutoCaptureSessions = acs - } - - switch testcase { - case "endpoint-notify": - config.Endpoints = bugsnag.Endpoints{Notify: os.Getenv("BUGSNAG_ENDPOINT")} - case "endpoint-session": - config.Endpoints = bugsnag.Endpoints{Sessions: os.Getenv("BUGSNAG_ENDPOINT")} - default: - config.Endpoints = bugsnag.Endpoints{ - Notify: os.Getenv("BUGSNAG_ENDPOINT"), - Sessions: os.Getenv("BUGSNAG_ENDPOINT"), - } - } - bugsnag.Configure(config) - - time.Sleep(200 * time.Millisecond) - // Increase publish rate for testing - bugsnag.DefaultSessionPublishInterval = time.Millisecond * 100 +var scenariosMap = map[string] func()(bugsnag.Configuration, func()){ + "UnhandledScenario": UnhandledCrashScenario, + "HandledScenario": HandledErrorScenario, + "MultipleUnhandledScenario": MultipleUnhandledErrorsScenario, + "MultipleHandledScenario": MultipleHandledErrorsScenario, + "NestedErrorScenario": NestedHandledErrorScenario, + "MetadataScenario": MetadataScenario, + "FilteredMetadataScenario": FilteredMetadataScenario, + "HandledCallbackErrorScenario": HandledCallbackErrorScenario, + "SendSessionScenario": SendSessionScenario, + "HandledToUnhandledScenario": HandledToUnhandledScenario, + "SetUserScenario": SetUserScenario, + "RecoverAfterPanicScenario": RecoverAfterPanicScenario, + "AutonotifyPanicScenario": AutonotifyPanicScenario, + "SessionAndErrorScenario": SessionAndErrorScenario, + "OnBeforeNotifyScenario": OnBeforeNotifyScenario, } func main() { + // Listening to the OS Signals + ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + ticker := time.NewTicker(1 * time.Second) - test := flag.String("test", "handled", "what the app should send, either handled, unhandled, session, autonotify") - flag.Parse() - - configureBasicBugsnag(*test) - time.Sleep(100 * time.Millisecond) // Ensure tests are less flaky by ensuring the start-up session gets sent - - switch *test { - case "unhandled": - unhandledCrash() - case "handled", "endpoint-legacy", "endpoint-notify", "endpoint-session": - handledError() - case "handled-with-callback": - handledCallbackError() - case "session": - session() - case "autonotify": - autonotify() - case "metadata": - metadata() - case "onbeforenotify": - onBeforeNotify() - case "filtered": - filtered() - case "recover": - dontDie() - case "session-and-error": - sessionAndError() - case "send-and-exit": - sendAndExit() - case "user": - user() - case "multiple-handled": - multipleHandled() - case "multiple-unhandled": - multipleUnhandled() - case "make-unhandled-with-callback": - handledToUnhandled() - case "nested-error": - nestedHandledError() - default: - log.Println("Not a valid test flag: " + *test) - os.Exit(1) - } - -} - -func multipleHandled() { - //Make the order of the below predictable - bugsnag.Configure(bugsnag.Configuration{Synchronous: true}) - - ctx := bugsnag.StartSession(context.Background()) - bugsnag.Notify(fmt.Errorf("oops"), ctx) - bugsnag.Notify(fmt.Errorf("oops"), ctx) -} - -func multipleUnhandled() { - //Make the order of the below predictable - notifier := bugsnag.New(bugsnag.Configuration{Synchronous: true}) - notifier.FlushSessionsOnRepanic(false) - - ctx := bugsnag.StartSession(context.Background()) - defer func() { recover() }() - defer notifier.AutoNotify(ctx) - defer notifier.AutoNotify(ctx) - panic("oops") -} - -//go:noinline -func unhandledCrash() { - // Invalid type assertion, will panic - func(a interface{}) string { - return a.(string) - }(struct{}{}) -} - -func handledError() { - if _, err := os.Open("nonexistent_file.txt"); err != nil { - if errClass := os.Getenv("ERROR_CLASS"); errClass != "" { - bugsnag.Notify(err, bugsnag.ErrorClass{Name: errClass}) - } else { - bugsnag.Notify(err) + addr := os.Getenv("DEFAULT_MAZE_ADDRESS") + if (addr == "") { + addr = DEFAULT_MAZE_ADDRESS } - } - // Give some time for the error to be sent before exiting - time.Sleep(200 * time.Millisecond) -} - -func session() { - bugsnag.StartSession(context.Background()) - - // Give some time for the session to be sent before exiting - time.Sleep(200 * time.Millisecond) -} - -func autonotify() { - go func() { - defer bugsnag.AutoNotify() - panic("Go routine killed with auto notify") - }() - - // Give enough time for the panic to happen - time.Sleep(100 * time.Millisecond) -} - -func metadata() { - customerData := map[string]string{"Name": "Joe Bloggs", "Age": "21"} - bugsnag.Notify(fmt.Errorf("oops"), bugsnag.MetaData{ - "Scheme": { - "Customer": customerData, - "Level": "Blue", - }, - }) - time.Sleep(200 * time.Millisecond) -} - -func filtered() { - bugsnag.Notify(fmt.Errorf("oops"), bugsnag.MetaData{ - "Account": { - "Name": "Company XYZ", - "Price(dollars)": "1 Million", - }, - }) - time.Sleep(200 * time.Millisecond) -} - -func onBeforeNotify() { - bugsnag.OnBeforeNotify( - func(event *bugsnag.Event, config *bugsnag.Configuration) error { - if event.Message == "Ignore this error" { - return fmt.Errorf("not sending errors to ignore") + + for { + select { + case <-ticker.C: + fmt.Println("[Bugsnag] Get command") + command := GetCommand(DEFAULT_MAZE_ADDRESS) + fmt.Printf("[Bugsnag] Received command: %+v\n", command) + + if command.Action == "run-scenario" { + prepareScenarioFunc, ok := scenariosMap[command.ScenarioName] + if ok { + config, scenarioFunc := prepareScenarioFunc() + bugsnag.Configure(config) + scenarioFunc() + } + } + case <-ctx.Done(): + fmt.Println("[Bugsnag] Context is done, closing") + ticker.Stop() + return } - // continue notifying as normal - if event.Message == "Change error message" { - event.Message = "Error message was changed" - } - return nil - }) - bugsnag.Notify(fmt.Errorf("Ignore this error")) - time.Sleep(100 * time.Millisecond) - bugsnag.Notify(fmt.Errorf("Don't ignore this error")) - time.Sleep(100 * time.Millisecond) - bugsnag.Notify(fmt.Errorf("Change error message")) - time.Sleep(100 * time.Millisecond) -} - -func dontDie() { - go func() { - defer bugsnag.Recover() - panic("Go routine killed but recovered") - }() - time.Sleep(100 * time.Millisecond) -} - -func sessionAndError() { - ctx := bugsnag.StartSession(context.Background()) - bugsnag.Notify(fmt.Errorf("oops"), ctx) - - time.Sleep(200 * time.Millisecond) -} - -func sendAndExit() { - bugsnag.Notify(fmt.Errorf("oops")) -} - -func user() { - bugsnag.Notify(fmt.Errorf("oops"), bugsnag.User{ - Id: "test-user-id", - Name: "test-user-name", - Email: "test-user-email", - }) - - time.Sleep(200 * time.Millisecond) -} - -func handledCallbackError() { - bugsnag.Notify(fmt.Errorf("Inadequent Prep Error"), func(event *bugsnag.Event) { - event.Context = "nonfatal.go:14" - event.Severity = bugsnag.SeverityInfo - - event.Stacktrace[1].File = ">insertion<" - event.Stacktrace[1].LineNumber = 0 - }) - // Give some time for the error to be sent before exiting - time.Sleep(200 * time.Millisecond) -} - -func handledToUnhandled() { - bugsnag.Notify(fmt.Errorf("unknown event"), func(event *bugsnag.Event) { - event.Unhandled = true - event.Severity = bugsnag.SeverityError - }) - // Give some time for the error to be sent before exiting - time.Sleep(200 * time.Millisecond) -} - -type customErr struct { - msg string - cause error - callers []uintptr -} - -func newCustomErr(msg string, cause error) error { - callers := make([]uintptr, 8) - runtime.Callers(2, callers) - return customErr{ - msg: msg, - cause: cause, - callers: callers, - } -} - -func (err customErr) Error() string { - return err.msg -} - -func (err customErr) Unwrap() error { - return err.cause -} - -func (err customErr) Callers() []uintptr { - return err.callers -} - -func nestedHandledError() { - if err := login("token " + os.Getenv("API_KEY")); err != nil { - bugsnag.Notify(newCustomErr("terminate process", err)) - // Give some time for the error to be sent before exiting - time.Sleep(200 * time.Millisecond) - } else { - i := len(os.Getenv("API_KEY")) - // Some nonsense to avoid inlining checkValue - if val, err := checkValue(i); err != nil { - fmt.Printf("err: %v, val: %d", err, val) - } - if val, err := checkValue(i - 46); err != nil { - fmt.Printf("err: %v, val: %d", err, val) } - - log.Fatalf("This test is broken - no error was generated.") - } -} - -func login(token string) error { - val, err := checkValue(len(token) * -1) - if err != nil { - return newCustomErr("login failed", err) - } - fmt.Printf("val: %d", val) - return nil } -func checkValue(i int) (int, error) { - if i < 0 { - return 0, newCustomErr("invalid token", nil) - } else if i%2 == 0 { - return i / 2, nil - } else if i < 9 { - return i * 3, nil - } - return i * 4, nil -} diff --git a/features/fixtures/app/metadata_scenario.go b/features/fixtures/app/metadata_scenario.go new file mode 100644 index 00000000..f5c191c6 --- /dev/null +++ b/features/fixtures/app/metadata_scenario.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + + "github.com/bugsnag/bugsnag-go/v2" +) + +func MetadataScenario() (bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + customerData := map[string]string{"Name": "Joe Bloggs", "Age": "21"} + bugsnag.Notify(fmt.Errorf("oops"), bugsnag.MetaData{ + "Scheme": { + "Customer": customerData, + "Level": "Blue", + }, + }) + } + return config, scenarioFunc +} + +func FilteredMetadataScenario() (bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + bugsnag.Notify(fmt.Errorf("oops"), bugsnag.MetaData{ + "Account": { + "Name": "Company XYZ", + "Price(dollars)": "1 Million", + }, + }) + } + return config, scenarioFunc +} diff --git a/features/fixtures/app/panic_scenario.go b/features/fixtures/app/panic_scenario.go new file mode 100644 index 00000000..6f278a8b --- /dev/null +++ b/features/fixtures/app/panic_scenario.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/bugsnag/bugsnag-go/v2" +) + +func AutonotifyPanicScenario()(bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + defer bugsnag.AutoNotify() + panic("Go routine killed with auto notify") + } + + return config, scenarioFunc +} + +func RecoverAfterPanicScenario() (bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + defer bugsnag.Recover() + panic("Go routine killed but recovered") + } + return config, scenarioFunc +} \ No newline at end of file diff --git a/features/fixtures/app/run.sh b/features/fixtures/app/run.sh deleted file mode 100755 index 3a98cd7e..00000000 --- a/features/fixtures/app/run.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -# SIGTERM or SIGINT trapped (likely SIGTERM from docker), pass it onto app -# process -function _term_or_init { - kill -TERM "$APP_PID" 2>/dev/null - wait $APP_PID -} - -# The bugsnag notifier monitor process needs at least 300ms, in order to ensure -# that it can send its notify -function _exit { - sleep 1 -} - -trap _term_or_init SIGTERM SIGINT -trap _exit EXIT - -PROC="${@:1}" -$PROC & - -# Wait on the app process to ensure that this script is able to trap the SIGTERM -# signal -APP_PID=$! -wait $APP_PID diff --git a/features/fixtures/app/session_scenario.go b/features/fixtures/app/session_scenario.go new file mode 100644 index 00000000..1bf08b5d --- /dev/null +++ b/features/fixtures/app/session_scenario.go @@ -0,0 +1,25 @@ +package main + +import ( + "context" + "fmt" + + "github.com/bugsnag/bugsnag-go/v2" +) + +func SendSessionScenario() (bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + bugsnag.StartSession(context.Background()) + } + return config, scenarioFunc +} + +func SessionAndErrorScenario() (bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + ctx := bugsnag.StartSession(context.Background()) + bugsnag.Notify(fmt.Errorf("oops"), ctx) + } + return config, scenarioFunc +} diff --git a/features/fixtures/app/unhandled_scenario.go b/features/fixtures/app/unhandled_scenario.go new file mode 100644 index 00000000..eb6ba7dd --- /dev/null +++ b/features/fixtures/app/unhandled_scenario.go @@ -0,0 +1,35 @@ +package main + +import ( + "context" + + "github.com/bugsnag/bugsnag-go/v2" +) + +//go:noinline +func UnhandledCrashScenario() (bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + // Invalid type assertion, will panic + func(a interface{}) string { + return a.(string) + }(struct{}{}) + } + return config, scenarioFunc +} + +func MultipleUnhandledErrorsScenario() (bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + //Make the order of the below predictable + notifier := bugsnag.New(bugsnag.Configuration{Synchronous: true}) + notifier.FlushSessionsOnRepanic(false) + + ctx := bugsnag.StartSession(context.Background()) + defer func() { recover() }() + defer notifier.AutoNotify(ctx) + defer notifier.AutoNotify(ctx) + panic("oops") + } + return config, scenarioFunc +} diff --git a/features/fixtures/app/user_scenario.go b/features/fixtures/app/user_scenario.go new file mode 100644 index 00000000..d4e3422d --- /dev/null +++ b/features/fixtures/app/user_scenario.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + + "github.com/bugsnag/bugsnag-go/v2" +) + +func SetUserScenario() (bugsnag.Configuration, func()) { + config := bugsnag.Configuration{} + scenarioFunc := func() { + bugsnag.Notify(fmt.Errorf("oops"), bugsnag.User{ + Id: "test-user-id", + Name: "test-user-name", + Email: "test-user-email", + }) + } + + return config, scenarioFunc +} diff --git a/features/fixtures/app/utils.go b/features/fixtures/app/utils.go new file mode 100644 index 00000000..cdc4936c --- /dev/null +++ b/features/fixtures/app/utils.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "runtime" +) + +type CustomErr struct { + msg string + cause error + callers []uintptr +} + +func NewCustomErr(msg string, cause error) error { + callers := make([]uintptr, 8) + runtime.Callers(2, callers) + return CustomErr{ + msg: msg, + cause: cause, + callers: callers, + } +} + +func (err CustomErr) Error() string { + return err.msg +} + +func (err CustomErr) Unwrap() error { + return err.cause +} + +func (err CustomErr) Callers() []uintptr { + return err.callers +} + +func Login(token string) error { + val, err := CheckValue(len(token) * -1) + if err != nil { + return NewCustomErr("login failed", err) + } + fmt.Printf("val: %d", val) + return nil +} + +func CheckValue(i int) (int, error) { + if i < 0 { + return 0, NewCustomErr("invalid token", nil) + } else if i%2 == 0 { + return i / 2, nil + } else if i < 9 { + return i * 3, nil + } + + return i * 4, nil +} \ No newline at end of file diff --git a/features/fixtures/docker-compose.yml b/features/fixtures/docker-compose.yml index 6bc2f8df..f24a2cb6 100644 --- a/features/fixtures/docker-compose.yml +++ b/features/fixtures/docker-compose.yml @@ -1,12 +1,12 @@ -version: '3.4' services: app: build: - context: . - dockerfile: app/Dockerfile + context: ../../ + dockerfile: ./features/fixtures/app/Dockerfile args: - GO_VERSION environment: + - DEFAULT_MAZE_ADDRESS - API_KEY - ERROR_CLASS - BUGSNAG_ENDPOINT @@ -22,6 +22,7 @@ services: - BUGSNAG_SOURCE_ROOT - BUGSNAG_PROJECT_PACKAGES restart: "no" + command: go run . autoconfigure: build: diff --git a/features/handled.feature b/features/handled.feature index 2555e34e..b114c4b5 100644 --- a/features/handled.feature +++ b/features/handled.feature @@ -1,61 +1,58 @@ Feature: Plain handled errors Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - Given I set environment variable "BUGSNAG_SOURCE_ROOT" to the app directory - And I configure the bugsnag endpoint - And I have built the service "app" + Given I set environment variable "BUGSNAG_SOURCE_ROOT" to "app/src/test/" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" Scenario: A handled error sends a report - When I run the go service "app" with the test case "handled" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run HandledScenario + And I wait to receive an error And the event "unhandled" is false And the event "severity" equals "warning" - And the event "severityReason.type" equals "handledError" - And the exception is a PathError for request 0 + And the event "severityReason.type" equals "handledException" + And the exception "errorClass" equals "Error" And the "file" of stack frame 0 equals "main.go" Scenario: A handled error sends a report with a custom name Given I set environment variable "ERROR_CLASS" to "MyCustomErrorClass" - When I run the go service "app" with the test case "handled" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run HandledScenario + And I wait to receive an error And the event "unhandled" is false And the event "severity" equals "warning" - And the event "severityReason.type" equals "handledError" + And the event "severityReason.type" equals "handledException" And the exception "errorClass" equals "MyCustomErrorClass" And the "file" of stack frame 0 equals "main.go" Scenario: Sending an event using a callback to modify report contents - When I run the go service "app" with the test case "handled-with-callback" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run HandledCallbackErrorScenario + And I wait to receive an error And the event "unhandled" is false And the event "severity" equals "info" And the event "severityReason.type" equals "userCallbackSetSeverity" And the event "context" equals "nonfatal.go:14" And the "file" of stack frame 0 equals "main.go" - And stack frame 0 contains a local function spanning 242 to 248 + And the "lineNumber" of stack frame 0 equals 242 And the "file" of stack frame 1 equals ">insertion<" And the "lineNumber" of stack frame 1 equals 0 Scenario: Marking an error as unhandled in a callback - When I run the go service "app" with the test case "make-unhandled-with-callback" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run HandledToUnhandledScenario + And I wait to receive an error And the event "unhandled" is true And the event "severity" equals "error" And the event "severityReason.type" equals "userCallbackSetSeverity" And the event "severityReason.unhandledOverridden" is true And the "file" of stack frame 0 equals "main.go" - And stack frame 0 contains a local function spanning 254 to 257 + And the "lineNumber" of stack frame 0 equals 254 Scenario: Unwrapping the causes of a handled error - When I run the go service "app" with the test case "nested-error" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run NestedErrorScenario + And I wait to receive an error And the event "unhandled" is false And the event "severity" equals "warning" And the event "exceptions.0.message" equals "terminate process" diff --git a/features/hostname.feature b/features/hostname.feature index 14c72c90..658f4be0 100644 --- a/features/hostname.feature +++ b/features/hostname.feature @@ -1,22 +1,17 @@ Feature: Configuring hostname -Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - And I configure the bugsnag endpoint - And I have built the service "app" - Scenario: An error report contains the configured hostname Given I set environment variable "HOSTNAME" to "server-1a" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" - When I run the go service "app" with the test case "handled" - And I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run HandledScenario + And I wait to receive an error And the event "device.hostname" equals "server-1a" Scenario: An session report contains the configured hostname Given I set environment variable "HOSTNAME" to "server-1a" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" - When I run the go service "app" with the test case "session" - And I wait to receive a request after the start up session - And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - And the payload field "device.hostname" equals "server-1a" + When I start the service "app" + And I run SendSessionScenario + And I wait to receive a session + And the session payload field "device.hostname" equals "server-1a" diff --git a/features/metadata.feature b/features/metadata.feature index 94a8bbf6..42d13750 100644 --- a/features/metadata.feature +++ b/features/metadata.feature @@ -1,15 +1,10 @@ Feature: Sending meta data -Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" - And I configure the bugsnag endpoint - And I have built the service "app" - Scenario: An error report contains custom meta data - When I run the go service "app" with the test case "metadata" - And I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" + And I start the service "app" + And I run MetadataScenario + And I wait to receive an error And the event "metaData.Scheme.Customer.Name" equals "Joe Bloggs" And the event "metaData.Scheme.Customer.Age" equals "21" And the event "metaData.Scheme.Level" equals "Blue" diff --git a/features/multieventsession.feature b/features/multieventsession.feature index e9e4dbc1..141ffd65 100644 --- a/features/multieventsession.feature +++ b/features/multieventsession.feature @@ -1,23 +1,18 @@ Feature: Reporting multiple handled and unhandled errors in the same session Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - And I configure the bugsnag endpoint - And I have built the service "app" - And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" + Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" Scenario: Handled errors know about previous reported handled errors - When I run the go service "app" with the test case "multiple-handled" - And I wait to receive 2 requests - And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - And the request 1 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run MultipleHandledScenario + And I wait to receive 2 errors And the event handled sessions count equals 1 for request 0 And the event handled sessions count equals 2 for request 1 Scenario: Unhandled errors know about previous reported handled errors - When I run the go service "app" with the test case "multiple-unhandled" - And I wait to receive 2 requests - And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - And the request 1 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run MultipleUnhandledScenario + And I wait to receive 2 errors And the event unhandled sessions count equals 1 for request 0 And the event unhandled sessions count equals 2 for request 1 diff --git a/features/onbeforenotify.feature b/features/onbeforenotify.feature index 7b4b332f..a3886615 100644 --- a/features/onbeforenotify.feature +++ b/features/onbeforenotify.feature @@ -1,14 +1,9 @@ Feature: Configuring on before notify -Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - And I configure the bugsnag endpoint - And I have built the service "app" - Scenario: Send three bugsnags and use on before notify to drop one and modify the message of another - When I run the go service "app" with the test case "onbeforenotify" - Then I wait to receive 2 requests after the start up session - And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - And the exception "message" equals "Don't ignore this error" for request 0 - And the request 1 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - And the exception "message" equals "Error message was changed" for request 1 + When I start the service "app" + And I run OnBeforeNotifyScenario + And I wait to receive 2 errors + And the exception "message" equals "Don't ignore this error" + And I discard the oldest error + And the exception "message" equals "Error message was changed" diff --git a/features/plain_features/panics.feature b/features/panics.feature similarity index 100% rename from features/plain_features/panics.feature rename to features/panics.feature diff --git a/features/paramfilters.feature b/features/paramfilters.feature index d1b714b8..68ebe550 100644 --- a/features/paramfilters.feature +++ b/features/paramfilters.feature @@ -1,29 +1,23 @@ Feature: Configuring param filters -Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" - And I configure the bugsnag endpoint - And I have built the service "app" - Scenario: An error report containing meta data is not filtered when the param filters are set but do not match Given I set environment variable "PARAMS_FILTERS" to "Name" - When I run the go service "app" with the test case "filtered" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run FilteredMetadataScenario + And I wait to receive an error And the event "metaData.Account.Price(dollars)" equals "1 Million" Scenario: An error report containing meta data is filtered when the param filters are set and completely match Given I set environment variable "PARAMS_FILTERS" to "Price(dollars)" - When I run the go service "app" with the test case "filtered" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run FilteredMetadataScenario + And I wait to receive an error And the event "metaData.Account.Price(dollars)" equals "[FILTERED]" Scenario: An error report containing meta data is filtered when the param filters are set and partially match Given I set environment variable "PARAMS_FILTERS" to "Price" - When I run the go service "app" with the test case "filtered" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run FilteredMetadataScenario + And I wait to receive an error And the event "metaData.Account.Price(dollars)" equals "[FILTERED]" diff --git a/features/recover.feature b/features/recover.feature index cbdb1f55..35bbf850 100644 --- a/features/recover.feature +++ b/features/recover.feature @@ -1,14 +1,8 @@ Feature: Using recover -Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" - And I configure the bugsnag endpoint - And I have built the service "app" - Scenario: An error report is sent when a go routine crashes but recovers - When I run the go service "app" with the test case "recover" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - And the exception "errorClass" equals "*errors.errorString" + When I start the service "app" + And I run RecoverAfterPanicScenario + And I wait to receive an error + And the exception "errorClass" equals "Error" And the exception "message" equals "Go routine killed but recovered" diff --git a/features/releasestage.feature b/features/releasestage.feature index 6b16ee48..e856e771 100644 --- a/features/releasestage.feature +++ b/features/releasestage.feature @@ -1,71 +1,66 @@ Feature: Configuring release stages and notify release stages -Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - And I configure the bugsnag endpoint - And I have built the service "app" - Scenario: An error report is sent when release stage matches notify release stages Given I set environment variable "NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" And I set environment variable "RELEASE_STAGE" to "stage2" - When I run the go service "app" with the test case "handled" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run HandledScenario + And I wait to receive an error And the event "app.releaseStage" equals "stage2" Scenario: An error report is sent when no notify release stages are specified Given I set environment variable "RELEASE_STAGE" to "stage2" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" - When I run the go service "app" with the test case "handled" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run HandledScenario + And I wait to receive an error And the event "app.releaseStage" equals "stage2" Scenario: An error report is sent regardless of notify release stages if release stage is not set Given I set environment variable "NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" - When I run the go service "app" with the test case "handled" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run HandledScenario + And I wait to receive an error Scenario: An error report is not sent if the release stage does not match the notify release stages Given I set environment variable "NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" And I set environment variable "RELEASE_STAGE" to "stage4" - When I run the go service "app" with the test case "handled" - And I wait for 3 second - Then I should receive no requests + When I start the service "app" + And I run HandledScenario + And I should receive no errors Scenario: An session report is sent when release stage matches notify release stages Given I set environment variable "NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" And I set environment variable "RELEASE_STAGE" to "stage2" - When I run the go service "app" with the test case "session" - Then I wait to receive a request after the start up session - And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - And the payload field "app.releaseStage" equals "stage2" + When I start the service "app" + And I run SendSessionScenario + And I wait to receive a session + And the session payload field "app.releaseStage" equals "stage2" Scenario: An session report is sent when no notify release stages are specified Given I set environment variable "RELEASE_STAGE" to "stage2" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" - When I run the go service "app" with the test case "session" - Then I wait to receive a request after the start up session - And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - And the payload field "app.releaseStage" equals "stage2" + When I start the service "app" + And I run SendSessionScenario + And I wait to receive a session + And the session payload field "app.releaseStage" equals "stage2" Scenario: An session report is sent regardless of notify release stages if release stage is not set Given I set environment variable "NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" - When I run the go service "app" with the test case "session" - Then I wait to receive a request after the start up session - And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run SendSessionScenario + And I wait to receive a session Scenario: An session report is not sent if the release stage does not match the notify release stages Given I set environment variable "NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" And I set environment variable "RELEASE_STAGE" to "stage4" - When I run the go service "app" with the test case "session" - And I wait for 3 second - Then I should receive no requests + When I start the service "app" + And I run SendSessionScenario + And I should receive no sessions diff --git a/features/sessioncontext.feature b/features/sessioncontext.feature index 0b986395..c0d276ee 100644 --- a/features/sessioncontext.feature +++ b/features/sessioncontext.feature @@ -1,15 +1,9 @@ Feature: Session data inside an error report using a session context -Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - And I configure the bugsnag endpoint - And I have built the service "app" - Scenario: An error report contains a session count when part of a session - When I run the go service "app" with the test case "session-and-error" - Then I wait to receive 2 requests after the start up session - And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - And the request 1 is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run SessionAndErrorScenario + Then I wait to receive 2 errors And the event handled sessions count equals 1 for request 0 And the event unhandled sessions count equals 0 for request 0 And the number of sessions started equals 1 for request 1 diff --git a/features/steps/go_steps.rb b/features/steps/go_steps.rb index 16a0e307..bd9fd916 100644 --- a/features/steps/go_steps.rb +++ b/features/steps/go_steps.rb @@ -1,122 +1,87 @@ -require 'net/http' +require 'os' -Given("I set environment variable {string} to the app directory") do |key| - step("I set environment variable \"#{key}\" to \"/app/src/test/\"") +When('I run {string}') do |scenario_name| + execute_command 'run-scenario', scenario_name end -Given("I set environment variable {string} to the notify endpoint") do |key| - step("I set environment variable \"#{key}\" to \"http://#{current_ip}:#{MOCK_API_PORT}\"") +When('I configure the maze endpoint') do + steps %( + When I set environment variable "DEFAULT_MAZE_ADDRESS" to "http://#{local_ip}:9339" + ) end -Given("I set environment variable {string} to the sessions endpoint") do |key| - # they're the same picture dot gif - # split them out for the future endpoint splitting work - step("I set environment variable \"#{key}\" to \"http://#{current_ip}:#{MOCK_API_PORT}\"") -end +def execute_command(action, scenario_name = '') + address = $address ? $address : "#{local_ip}:9339" -Then(/^the request(?: (\d+))? is a valid error report with api key "(.*)"$/) do |request_index, api_key| - request_index ||= 0 - steps %Q{ - And the request #{request_index} is valid for the error reporting API - And the "bugsnag-api-key" header equals "#{api_key}" for request #{request_index} - And the payload field "apiKey" equals "#{api_key}" for request #{request_index} + command = { + action: action, + scenario_name: scenario_name, + notify_endpoint: "http://#{address}/notify", + sessions_endpoint: "http://#{address}/sessions", + api_key: $api_key, } -end -Then(/^the exception is a PathError for request (\d+)$/) do |request_index| - body = find_request(request_index)[:body] - error_class = body["events"][0]["exceptions"][0]["errorClass"] - if ['1.11', '1.12', '1.13', '1.14', '1.15'].include? ENV['GO_VERSION'] - assert_equal(error_class, '*os.PathError') - else - assert_equal(error_class, '*fs.PathError') - end -end + $logger.debug("Queuing command: #{command}") + Maze::Server.commands.add command -Then(/^the request(?: (\d+))? is a valid session report with api key "(.*)"$/) do |request_index, api_key| - request_index ||= 0 - steps %Q{ - And the request #{request_index} is valid for the session tracking API - And the "bugsnag-api-key" header equals "#{api_key}" for request #{request_index} - } + # Ensure fixture has read the command + count = 900 + sleep 0.1 until Maze::Server.commands.remaining.empty? || (count -= 1) < 1 + raise 'Test fixture did not GET /command' unless Maze::Server.commands.remaining.empty? end -Then(/^the event unhandled sessions count equals (\d+) for request (\d+)$/) do |count, request_index| - step "the payload field \"events.0.session.events.unhandled\" equals #{count} for request #{request_index}" -end - -Then(/^the event handled sessions count equals (\d+) for request (\d+)$/) do |count, request_index| - step "the payload field \"events.0.session.events.handled\" equals #{count} for request #{request_index}" -end - -Then(/^the number of sessions started equals (\d+) for request (\d+)$/) do |count, request_index| - step "the payload field \"sessionCounts.0.sessionsStarted\" equals #{count} for request #{request_index}" -end - -When("I run the go service {string} with the test case {string}") do |service, testcase| - run_service_with_command(service, "./run.sh go run . -test=\"#{testcase}\"") -end - -Then(/^I wait to receive a request after the start up session$/) do - step "I wait to receive 1 requests after the start up session" -end - -Then(/^I wait to receive (\d+) requests after the start up session?$/) do |request_count| - max_attempts = 50 - attempts = 0 - start_up_message_received = false - start_up_message_removed = false - received = false - until (attempts >= max_attempts) || received - attempts += 1 - start_up_message_received ||= (stored_requests.size == 1) - if start_up_message_received && !start_up_message_removed - step 'the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"' - stored_requests.shift - start_up_message_removed = true - next - end - received = (stored_requests.size == request_count) - sleep 0.2 - end - raise "Requests not received in 10s (received #{stored_requests.size})" unless received - # Wait an extra second to ensure there are no further requests - sleep 1 - assert_equal(request_count, stored_requests.size, "#{stored_requests.size} requests received") -end - -Then(/^(\d+) requests? (?:was|were) received$/) do |request_count| - sleep 1 - assert_equal(request_count, stored_requests.size, "#{stored_requests.size} requests received") -end - -Then("the in-project frames of the stacktrace are:") do |table| - body = find_request(0)[:body] - stacktrace = body["events"][0]["exceptions"][0]["stacktrace"] - found = 0 # counts matching frames and ensures ordering is correct - expected = table.hashes.length - stacktrace.each do |frame| - if found < expected and frame["inProject"] and - frame["file"] == table.hashes[found]["file"] and - frame["method"] == table.hashes[found]["method"] - found = found + 1 - end - end - assert_equal(found, expected, "expected #{expected} matching frames but found #{found}. stacktrace:\n#{stacktrace}") -end - -Then("stack frame {int} contains a local function spanning {int} to {int}") do |frame, val, old_val| - # Old versions of Go put the line number on the end of the function - if ['1.7', '1.8'].include? ENV['GO_VERSION'] - step "the \"lineNumber\" of stack frame #{frame} equals #{old_val}" +def local_ip + if OS.mac? + 'host.docker.internal' else - step "the \"lineNumber\" of stack frame #{frame} equals #{val}" + ip_addr = `ifconfig | grep -Eo 'inet (addr:)?([0-9]*\\\.){3}[0-9]*' | grep -v '127.0.0.1'` + ip_list = /((?:[0-9]*\.){3}[0-9]*)/.match(ip_addr) + ip_list.captures.first end end -Then("the exception {string} is one of:") do |key, table| - body = find_request(0)[:body] - exception = body["events"][0]["exceptions"][0] - options = table.raw.flatten - assert(options.include?(exception[key]), "expected '#{key}' to be one of #{options}") -end +# Then(/^the exception is a PathError for request (\d+)$/) do |request_index| +# body = find_request(request_index)[:body] +# error_class = body["events"][0]["exceptions"][0]["errorClass"] +# if ['1.11', '1.12', '1.13', '1.14', '1.15'].include? ENV['GO_VERSION'] +# assert_equal(error_class, '*os.PathError') +# else +# assert_equal(error_class, '*fs.PathError') +# end +# end +# # +# Then(/^the event unhandled sessions count equals (\d+) for request (\d+)$/) do |count, request_index| +# step "the payload field \"events.0.session.events.unhandled\" equals #{count} for request #{request_index}" +# end +# +# Then(/^the event handled sessions count equals (\d+) for request (\d+)$/) do |count, request_index| +# step "the payload field \"events.0.session.events.handled\" equals #{count} for request #{request_index}" +# end +# +# Then(/^the number of sessions started equals (\d+) for request (\d+)$/) do |count, request_index| +# step "the payload field \"sessionCounts.0.sessionsStarted\" equals #{count} for request #{request_index}" +# end +# +# Then("the in-project frames of the stacktrace are:") do |table| +# body = find_request(0)[:body] +# stacktrace = body["events"][0]["exceptions"][0]["stacktrace"] +# found = 0 # counts matching frames and ensures ordering is correct +# expected = table.hashes.length +# stacktrace.each do |frame| +# if found < expected and frame["inProject"] and +# frame["file"] == table.hashes[found]["file"] and +# frame["method"] == table.hashes[found]["method"] +# found = found + 1 +# end +# end +# assert_equal(found, expected, "expected #{expected} matching frames but found #{found}. stacktrace:\n#{stacktrace}") +# end +# +# +# Then("the exception {string} is one of:") do |key, table| +# body = find_request(0)[:body] +# exception = body["events"][0]["exceptions"][0] +# options = table.raw.flatten +# assert(options.include?(exception[key]), "expected '#{key}' to be one of #{options}") +# end +# \ No newline at end of file diff --git a/features/support/env.rb b/features/support/env.rb index 3e70280e..8c279b6d 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1,16 +1,6 @@ -require 'fileutils' -require 'socket' -require 'timeout' - -testBuildFolder = 'features/fixtures/testbuild' - -FileUtils.rm_rf(testBuildFolder) -Dir.mkdir testBuildFolder - -# Copy the existing dir -`find . -name '*.go' -o -name 'go.sum' -o -name 'go.mod' \ - -not -path "./examples/*" \ - -not -path "./testutil/*" \ - -not -path "./v2/testutil/*" \ - -not -path "./features/*" \ - -not -name '*_test.go' | cpio -pdm #{testBuildFolder}` +Before do + $address = nil + steps %( + When I configure the maze endpoint + ) +end diff --git a/features/synchronous.feature b/features/synchronous.feature deleted file mode 100644 index 0a132c48..00000000 --- a/features/synchronous.feature +++ /dev/null @@ -1,20 +0,0 @@ -Feature: Configuring synchronous flag - -Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" - And I configure the bugsnag endpoint - And I have built the service "app" - -Scenario: An error report is sent asynchrously but exits immediately so is not sent - Given I set environment variable "SYNCHRONOUS" to "false" - When I run the go service "app" with the test case "send-and-exit" - And I wait for 3 second - Then I should receive no requests - -Scenario: An error report is report synchronously so it will send before exiting - Given I set environment variable "SYNCHRONOUS" to "true" - When I run the go service "app" with the test case "send-and-exit" - Then I wait to receive 1 requests - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" - diff --git a/features/user.feature b/features/user.feature index 49040ac1..e9a41424 100644 --- a/features/user.feature +++ b/features/user.feature @@ -1,18 +1,12 @@ Feature: Sending user data -Background: - Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" - Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" - And I configure the bugsnag endpoint - And I have built the service "app" - Scenario: An error report contains custom user data - Given I set environment variable "USER_ID" to "test-user-id" + And I set environment variable "USER_ID" to "test-user-id" And I set environment variable "USER_NAME" to "test-user-name" And I set environment variable "USER_EMAIL" to "test-user-email" - When I run the go service "app" with the test case "user" - Then I wait to receive a request - And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" + When I start the service "app" + And I run SetUserScenario + And I wait to receive an error And the event "user.id" equals "test-user-id" And the event "user.name" equals "test-user-name" And the event "user.email" equals "test-user-email"