blob: 1beb68d0877c51458cd98e38eb75e53514e69e5f [file] [log] [blame]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package ui
import (
"context"
"io/ioutil"
"path/filepath"
"strings"
"time"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/crash"
"chromiumos/tast/local/cryptohome"
"chromiumos/tast/local/input"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: WebUIJSErrors,
Desc: "Checks that Chrome's WebUI JavaScript Error Reporting works on Chrome OS",
Contacts: []string{"iby@chromium.org", "cros-telemetry@google.com"},
Attr: []string{"group:mainline"},
SoftwareDeps: []string{"chrome", "chrome_internal"},
})
}
// checkJavaScriptError checks that a JavaScript error report was created.
// It confirms that an error report with a JavaScript stack file (.js_stack) was
// created in the crash directories crashDir. It confirms that the meta file
// contains the line "upload_var_error_message=" followed by the
// expectedErrorMessage and that the stack contains the expectedStackEntries in
// the order given.
func checkJavaScriptError(ctx, cleanupCtx context.Context, crashDir, outDir, expectedErrorMessage string,
expectedStackEntries []string) error {
const (
metaRegex = `jserror\.\d{8}\.\d{6}\.\d+\.\d+\.meta`
stackRegex = `jserror\.\d{8}\.\d{6}\.\d+\.\d+\.js_stack`
logRegex = `jserror\.\d{8}\.\d{6}\.\d+\.\d+\.chrome.txt.gz`
expectedType = "upload_var_type=JavascriptError"
expectedPayloadPrefix = "payload="
expectedLogPrefix = "upload_file_chrome.txt="
expectedErrorMessagePrefix = "upload_var_error_message="
)
crashFileMap, err := crash.WaitForCrashFiles(ctx, []string{crashDir}, []string{metaRegex, stackRegex, logRegex})
if err != nil {
return errors.Wrapf(err, "WaitForCrashFiles failed for directory %s", crashDir)
}
defer crash.RemoveAllFiles(cleanupCtx, crashFileMap)
metas := crashFileMap[metaRegex]
stacks := crashFileMap[stackRegex]
logs := crashFileMap[logRegex]
// We should have only generated one error report for each JavaScript error
// test. We don't expect multiple error reports for a single JavaScript error.
// This should be safe since we're only navigating to a single page so
// there shouldn't be outside JavaScript errors. (Any errors from the login
// page would have been removed when we did SetUpCrashTest) Only JavaScript
// errors should produce error reports that start with jserror so we aren't
// vulnerable to unrelated selinux violations or segfaults confusing us.
if len(metas) != 1 || len(stacks) != 1 || len(logs) != 1 {
crash.MoveFilesToOut(ctx, outDir, metas...)
crash.MoveFilesToOut(ctx, outDir, stacks...)
crash.MoveFilesToOut(ctx, outDir, logs...)
return errors.New("multiple JS Errors found")
}
metaContents, err := ioutil.ReadFile(metas[0])
if err != nil {
return errors.Wrap(err, "couldn't read meta file")
}
if !strings.Contains(string(metaContents), expectedErrorMessagePrefix+expectedErrorMessage) {
crash.MoveFilesToOut(ctx, outDir, metas[0])
return errors.Errorf("didn't find expected meta var: %q. Leaving for debugging: %s", expectedErrorMessagePrefix+expectedErrorMessage, metas[0])
}
if !strings.Contains(string(metaContents), expectedType) {
crash.MoveFilesToOut(ctx, outDir, metas[0])
return errors.Errorf("didn't find expected meta var: %q. Leaving for debugging: %s", expectedType, metas[0])
}
expectedPayload := expectedPayloadPrefix + filepath.Base(stacks[0])
if !strings.Contains(string(metaContents), expectedPayload) {
crash.MoveFilesToOut(ctx, outDir, metas[0])
return errors.Errorf("didn't find expected meta payload: %q. Leaving for debugging: %s", expectedPayload, metas[0])
}
expectedLogUpload := expectedLogPrefix + filepath.Base(logs[0])
if !strings.Contains(string(metaContents), expectedLogUpload) {
crash.MoveFilesToOut(ctx, outDir, metas[0])
return errors.Errorf("didn't find expected meta log upload: %q. Leaving for debugging: %s", expectedLogUpload, metas[0])
}
stackContents, err := ioutil.ReadFile(stacks[0])
if err != nil {
return errors.Wrap(err, "couldn't read stack file")
}
previousLocation := -1
for _, expectedStackEntry := range expectedStackEntries {
location := strings.Index(string(stackContents), expectedStackEntry)
if location == -1 {
crash.MoveFilesToOut(ctx, outDir, stacks[0])
return errors.Errorf("didn't find expected text in stack: %q. Leaving for debugging: %s", expectedStackEntry, stacks[0])
}
if location < previousLocation {
return errors.Errorf("stack is out of order; %q too early. Leaving for debugging: %s", expectedStackEntry, stacks[0])
}
previousLocation = location
}
return nil
}
func checkPageLoadError(ctx, cleanupCtx context.Context, crashDir, outDir string) error {
testing.ContextLog(ctx, "Checking for crash files after bad page load")
const (
expectedFunction1 = "logsErrorDuringPageLoadInner"
expectedFunction2 = "logsErrorDuringPageLoadOuter"
expectedErrorMessage = "WebUI JS Error: printing error on page load"
)
if err := checkJavaScriptError(ctx, cleanupCtx, crashDir, outDir, expectedErrorMessage, []string{expectedFunction1, expectedFunction2}); err != nil {
return err
}
return nil
}
func checkLoggedError(ctx, cleanupCtx context.Context, crashDir, outDir string) error {
testing.ContextLog(ctx, "Checking for crash files after JavaScripts logs an error")
// Alt+L presses the "Log Error" button on chrome://webuijserror
kw, err := input.Keyboard(ctx)
if err != nil {
return errors.Wrap(err, "failed to get keyboard handle")
}
defer kw.Close()
if err := kw.Accel(ctx, "Alt+l"); err != nil {
return errors.Wrap(err, "failed to press keys")
}
const (
expectedFunction1 = "logsErrorFromButtonClickInner"
expectedFunction2 = "logsErrorFromButtonClickHandler"
expectedErrorMessage = "WebUI JS Error: printing error on button click"
)
if err := checkJavaScriptError(ctx, cleanupCtx, crashDir, outDir, expectedErrorMessage, []string{expectedFunction1, expectedFunction2}); err != nil {
return err
}
return nil
}
func checkUncaughtExceptionError(ctx, cleanupCtx context.Context, crashDir, outDir string) error {
testing.ContextLog(ctx, "Checking for crash files after JavaScripts throws an unhandled exception")
// Alt+T presses the "Throw Uncaught Error" button on chrome://webuijserror
kw, err := input.Keyboard(ctx)
if err != nil {
return errors.Wrap(err, "failed to get keyboard handle")
}
defer kw.Close()
if err := kw.Accel(ctx, "Alt+t"); err != nil {
return errors.Wrap(err, "failed to press keys")
}
const (
expectedFunction1 = "throwExceptionInner"
expectedFunction2 = "throwExceptionHandler"
expectedErrorMessage = "Uncaught Error: WebUI JS Error: exception button clicked"
)
if err := checkJavaScriptError(ctx, cleanupCtx, crashDir, outDir, expectedErrorMessage, []string{expectedFunction1, expectedFunction2}); err != nil {
return err
}
return nil
}
func checkUnhandledPromiseRejectionError(ctx, cleanupCtx context.Context, crashDir, outDir string) error {
testing.ContextLog(ctx, "Checking for crash files after JavaScripts doesn't handle a promise rejection")
// Alt+P presses the "Unhandled Promise Rejection" button on chrome://webuijserror
kw, err := input.Keyboard(ctx)
if err != nil {
return errors.Wrap(err, "failed to get keyboard handle")
}
defer kw.Close()
if err := kw.Accel(ctx, "Alt+p"); err != nil {
return errors.Wrap(err, "failed to press keys")
}
const (
expectedStack = "No Stack"
expectedErrorMessage = "Uncaught (in promise) WebUI JS Error: The rejector always rejects!"
)
if err := checkJavaScriptError(ctx, cleanupCtx, crashDir, outDir, expectedErrorMessage, []string{expectedStack}); err != nil {
return err
}
return nil
}
func WebUIJSErrors(ctx context.Context, s *testing.State) {
cleanupCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 5*time.Second)
defer cancel()
const vModuleFlags = "--vmodule=chrome_js_error_report_processor=3,web_ui_impl=3,web_ui_main_frame_observer=3,webui_js_error_ui=3"
cr, err := chrome.New(ctx, chrome.EnableFeatures("SendWebUIJavaScriptErrorReports"), chrome.ExtraArgs(vModuleFlags))
if err != nil {
s.Fatal("Chrome login failed: ", err)
}
defer cr.Close(cleanupCtx)
if err := crash.SetUpCrashTest(ctx, crash.WithMockConsent()); err != nil {
s.Fatal("SetUpCrashTest failed: ", err)
}
defer crash.TearDownCrashTest(cleanupCtx)
conn, err := cr.NewConn(ctx, "chrome://webuijserror")
if err != nil {
s.Fatal("Chrome navigation failed: ", err)
}
defer conn.Close()
user := cr.NormalizedUser()
path, err := cryptohome.UserPath(ctx, user)
if err != nil {
s.Fatal("Couldn't get user path: ", err)
}
crashDir := filepath.Join(path, "crash")
if err := checkPageLoadError(ctx, cleanupCtx, crashDir, s.OutDir()); err != nil {
s.Error("checkPageLoadError failed: ", err)
}
if err := checkLoggedError(ctx, cleanupCtx, crashDir, s.OutDir()); err != nil {
s.Error("checkLoggedError failed: ", err)
}
if err := checkUncaughtExceptionError(ctx, cleanupCtx, crashDir, s.OutDir()); err != nil {
s.Error("checkUncaughtExceptionError failed: ", err)
}
if err := checkUnhandledPromiseRejectionError(ctx, cleanupCtx, crashDir, s.OutDir()); err != nil {
s.Error("checkUnhandledPromiseRejectionError failed: ", err)
}
}