Bug 1724183 - Add ExtensionTest::AssertMatchInternal to provide the logic to be shared between AssertThrows and AssertRejects. r=baku,sfink
authorLuca Greco <lgreco@mozilla.com>
Tue, 12 Oct 2021 18:32:27 +0000
changeset 595558 1db9a2d4ac46e10ea2e91b75c307288116d034ea
parent 595557 211c00af6c724814c5028dd2dab509c3ece50d05
child 595559 e573ec34a4b467378ccf448b4d6387076a8a098b
push id38872
push usermalexandru@mozilla.com
push dateWed, 13 Oct 2021 03:44:20 +0000
treeherdermozilla-central@9b9f8bfe2625 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku, sfink
bugs1724183
milestone95.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1724183 - Add ExtensionTest::AssertMatchInternal to provide the logic to be shared between AssertThrows and AssertRejects. r=baku,sfink This patch introduce a new AssertMatchInternal to ExtensionTest, this method isn't exposed to js callers through the webidl definition, it is meant to be used internally by the AssertThrows and AssertRejects methods (which are instead both methods part of the ExtensionTest webidl). This patch also introduces small changes to ExtensionAPIRequestForwarder and RequestWorkerRunnable, to allow AssertMatchInternal to accept as an optional parameter a serialized caller stack trace (to be sent as part of the APIRequest forwarded from the worker thread to the main thread), which is needed for AssertRejects (to include the strack trace retrieved when AssertRejects is called, and then after the promise parameter has been rejected or resolved to send it as part of the forwarded aPI request). Differential Revision: https://phabricator.services.mozilla.com/D122033
toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.cpp
toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.h
toolkit/components/extensions/webidl-api/ExtensionTest.cpp
toolkit/components/extensions/webidl-api/ExtensionTest.h
--- a/toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.cpp
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.cpp
@@ -92,29 +92,40 @@ ExtensionAPIRequestForwarder::APIRequest
         do_ImportModule("resource://gre/modules/ExtensionProcessScript.jsm",
                         "ExtensionAPIRequestHandler");
     MOZ_RELEASE_ASSERT(sAPIRequestHandler);
     ClearOnShutdown(&sAPIRequestHandler);
   }
   return *sAPIRequestHandler;
 }
 
+void ExtensionAPIRequestForwarder::SetSerializedCallerStack(
+    UniquePtr<dom::SerializedStackHolder> aCallerStack) {
+  MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
+  MOZ_ASSERT(mStackHolder.isNothing());
+  mStackHolder = Some(std::move(aCallerStack));
+}
+
 void ExtensionAPIRequestForwarder::Run(nsIGlobalObject* aGlobal, JSContext* aCx,
                                        const dom::Sequence<JS::Value>& aArgs,
                                        ExtensionEventListener* aListener,
                                        JS::MutableHandleValue aRetVal,
                                        ErrorResult& aRv) {
   MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
 
   dom::WorkerPrivate* workerPrivate = dom::GetCurrentThreadWorkerPrivate();
   MOZ_ASSERT(workerPrivate);
 
   RefPtr<RequestWorkerRunnable> runnable =
       new RequestWorkerRunnable(workerPrivate, this);
 
+  if (mStackHolder.isSome()) {
+    runnable->SetSerializedCallerStack(mStackHolder.extract());
+  }
+
   RefPtr<dom::Promise> domPromise;
 
   IgnoredErrorResult rv;
 
   switch (mRequestType) {
     case APIRequestType::CALL_FUNCTION_ASYNC:
       domPromise = dom::Promise::Create(aGlobal, rv);
       if (NS_WARN_IF(rv.Failed())) {
@@ -294,17 +305,20 @@ void RequestWorkerRunnable::Init(nsIGlob
 
   IgnoredErrorResult rv;
   SerializeArgs(aCx, aArgs, rv);
   if (NS_WARN_IF(rv.Failed())) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return;
   }
 
-  SerializeCallerStack(aCx);
+  if (!mStackHolder.isSome()) {
+    SerializeCallerStack(aCx);
+  }
+
   mEventListener = aListener;
 }
 
 void RequestWorkerRunnable::Init(nsIGlobalObject* aGlobal, JSContext* aCx,
                                  const dom::Sequence<JS::Value>& aArgs,
                                  const RefPtr<dom::Promise>& aPromiseRetval,
                                  ErrorResult& aRv) {
   // Custom callbacks needed to make the PromiseWorkerProxy instance to
@@ -321,27 +335,35 @@ void RequestWorkerRunnable::Init(nsIGlob
     return;
   }
 
   mPromiseProxy = dom::PromiseWorkerProxy::Create(
       mWorkerPrivate, aPromiseRetval,
       &kExtensionAPIRequestStructuredCloneCallbacks);
 }
 
+void RequestWorkerRunnable::SetSerializedCallerStack(
+    UniquePtr<dom::SerializedStackHolder> aCallerStack) {
+  MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
+  MOZ_ASSERT(mStackHolder.isNothing());
+  mStackHolder = Some(std::move(aCallerStack));
+}
+
 void RequestWorkerRunnable::SerializeCallerStack(JSContext* aCx) {
   MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
   MOZ_ASSERT(mStackHolder.isNothing());
   mStackHolder = Some(dom::GetCurrentStack(aCx));
 }
 
 void RequestWorkerRunnable::DeserializeCallerStack(
     JSContext* aCx, JS::MutableHandleValue aRetval) {
   MOZ_ASSERT(NS_IsMainThread());
   if (mStackHolder.isSome()) {
     JS::RootedObject savedFrame(aCx, mStackHolder->get()->ReadStack(aCx));
+    MOZ_ASSERT(savedFrame);
     aRetval.set(JS::ObjectValue(*savedFrame));
     mStackHolder = Nothing();
   }
 }
 
 void RequestWorkerRunnable::SerializeArgs(JSContext* aCx,
                                           const dom::Sequence<JS::Value>& aArgs,
                                           ErrorResult& aRv) {
--- a/toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.h
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.h
@@ -81,27 +81,31 @@ class ExtensionAPIRequestForwarder {
 
   void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
            const dom::Sequence<JS::Value>& aArgs,
            const RefPtr<dom::Promise>& aPromiseRetval, ErrorResult& aRv);
 
   void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
            JS::MutableHandleValue aRetVal, ErrorResult& aRv);
 
+  void SetSerializedCallerStack(
+      UniquePtr<dom::SerializedStackHolder> aCallerStack);
+
  protected:
   virtual ~ExtensionAPIRequestForwarder() = default;
 
  private:
   already_AddRefed<ExtensionAPIRequest> CreateAPIRequest(
       nsIGlobalObject* aGlobal, JSContext* aCx,
       const dom::Sequence<JS::Value>& aArgs, ExtensionEventListener* aListener,
       ErrorResult& aRv);
 
   APIRequestType mRequestType;
   ExtensionAPIRequestTarget mRequestTarget;
+  Maybe<UniquePtr<dom::SerializedStackHolder>> mStackHolder;
 };
 
 /*
  * This runnable is used internally by ExtensionAPIRequestForwader class
  * to call the JS privileged code that handle the API requests originated
  * from the WebIDL bindings instantiated in a worker thread.
  *
  * The runnable is meant to block the worker thread until we get a result
@@ -118,16 +122,19 @@ class ExtensionAPIRequestForwarder {
 class RequestWorkerRunnable : public dom::WorkerMainThreadRunnable {
  public:
   using APIRequestType = mozIExtensionAPIRequest::RequestType;
   using APIResultType = mozIExtensionAPIRequestResult::ResultType;
 
   RequestWorkerRunnable(dom::WorkerPrivate* aWorkerPrivate,
                         ExtensionAPIRequestForwarder* aOuterAPIRequest);
 
+  void SetSerializedCallerStack(
+      UniquePtr<dom::SerializedStackHolder> aCallerStack);
+
   /**
    * Init a request runnable for AddListener and RemoveListener API requests
    * (which do have an event callback callback and do not expect any return
    * value).
    */
   void Init(nsIGlobalObject* aGlobal, JSContext* aCx,
             const dom::Sequence<JS::Value>& aArgs,
             ExtensionEventListener* aListener, ErrorResult& aRv);
--- a/toolkit/components/extensions/webidl-api/ExtensionTest.cpp
+++ b/toolkit/components/extensions/webidl-api/ExtensionTest.cpp
@@ -1,19 +1,21 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "ExtensionTest.h"
 #include "ExtensionEventManager.h"
 
-#include "js/Equality.h"  // JS::StrictlyEqual
+#include "js/Equality.h"            // JS::StrictlyEqual
+#include "js/PropertyAndElement.h"  // JS_GetProperty
 #include "mozilla/dom/ExtensionTestBinding.h"
 #include "nsIGlobalObject.h"
+#include "js/RegExp.h"
 
 namespace mozilla {
 namespace extensions {
 
 bool IsInAutomation(JSContext* aCx, JSObject* aGlobal) {
   return NS_IsMainThread()
              ? xpc::IsInAutomation()
              : dom::WorkerGlobalScope::IsInAutomation(aCx, aGlobal);
@@ -103,16 +105,166 @@ void ExtensionTest::CallWebExtMethodAsse
                  !args.AppendElement(messageVal, fallible))) {
     ThrowUnexpectedError(aCx, aRv);
     return;
   }
 
   CallWebExtMethodNoReturn(aCx, aApiMethod, args, aRv);
 }
 
+MOZ_CAN_RUN_SCRIPT bool ExtensionTest::AssertMatchInternal(
+    JSContext* aCx, const JS::HandleValue aActualValue,
+    const JS::HandleValue aExpectedMatchValue, const nsAString& aMessagePre,
+    const dom::Optional<nsAString>& aMessage,
+    UniquePtr<dom::SerializedStackHolder> aSerializedCallerStack,
+    ErrorResult& aRv) {
+  // Stringify the actual value, if the expected value is a regexp or a string
+  // then it will be used as part of the matching assertion, otherwise it is
+  // still interpolated in the assertion message.
+  JS::Rooted<JSString*> actualToString(aCx, JS::ToString(aCx, aActualValue));
+  NS_ENSURE_TRUE(actualToString, false);
+  nsAutoJSString actualString;
+  NS_ENSURE_TRUE(actualString.init(aCx, actualToString), false);
+
+  bool matched = false;
+
+  if (aExpectedMatchValue.isObject()) {
+    JS::RootedObject expectedMatchObj(aCx, &aExpectedMatchValue.toObject());
+
+    bool isRegexp;
+    NS_ENSURE_TRUE(JS::ObjectIsRegExp(aCx, expectedMatchObj, &isRegexp), false);
+
+    if (isRegexp) {
+      // Expected value is a regexp, test if the stringified actual value does
+      // match.
+      nsString input(actualString);
+      size_t index = 0;
+      JS::RootedValue rxResult(aCx);
+      NS_ENSURE_TRUE(JS::ExecuteRegExpNoStatics(
+                         aCx, expectedMatchObj, input.BeginWriting(),
+                         actualString.Length(), &index, true, &rxResult),
+                     false);
+      matched = !rxResult.isNull();
+    } else if (JS::IsCallable(expectedMatchObj) &&
+               !JS::IsConstructor(expectedMatchObj)) {
+      // Expected value is a matcher function, execute it with the value as a
+      // parameter:
+      //
+      // - if the matcher function throws, steal the exception to re-raise it
+      //   to the extension code that called the assertion method, but
+      //   continue to still report the assertion as failed to the WebExtensions
+      //   internals.
+      //
+      // - if the function return a falsey value, the assertion should fail and
+      //   no exception is raised to the extension code that called the
+      //   assertion
+      JS::Rooted<JS::Value> retval(aCx);
+      aRv.MightThrowJSException();
+      if (!JS::Call(aCx, JS::UndefinedHandleValue, expectedMatchObj,
+                    JS::HandleValueArray(aActualValue), &retval)) {
+        aRv.StealExceptionFromJSContext(aCx);
+        matched = false;
+      } else {
+        matched = JS::ToBoolean(retval);
+      }
+    } else if (JS::IsConstructor(expectedMatchObj)) {
+      // Expected value is a constructor, test if the actual value is an
+      // instanceof the expected constructor.
+      NS_ENSURE_TRUE(
+          JS::InstanceofOperator(aCx, expectedMatchObj, aActualValue, &matched),
+          false);
+    } else {
+      // Fallback to strict equal for any other js object type we don't expect.
+      NS_ENSURE_TRUE(
+          JS::StrictlyEqual(aCx, aActualValue, aExpectedMatchValue, &matched),
+          false);
+    }
+  } else if (aExpectedMatchValue.isString()) {
+    // Expected value is a string, assertion should fail if the expected string
+    // isn't equal to the stringified actual value.
+    JS::Rooted<JSString*> expectedToString(
+        aCx, JS::ToString(aCx, aExpectedMatchValue));
+    NS_ENSURE_TRUE(expectedToString, false);
+
+    nsAutoJSString expectedString;
+    NS_ENSURE_TRUE(expectedString.init(aCx, expectedToString), false);
+
+    // If actual is an object and it has a message property that is a string,
+    // then we want to use that message string as the string to compare the
+    // expected one with.
+    //
+    // This is needed mainly to match the current JS implementation.
+    //
+    // TODO(Bug 1731094): as a low priority follow up, we may want to reconsider
+    // and compare the entire stringified error (which is also often a common
+    // behavior in many third party JS test frameworks).
+    JS::RootedValue messageVal(aCx);
+    if (aActualValue.isObject()) {
+      JS::Rooted<JSObject*> actualValueObj(aCx, &aActualValue.toObject());
+
+      if (!JS_GetProperty(aCx, actualValueObj, "message", &messageVal)) {
+        // GetProperty may raise an exception, in that case we steal the
+        // exception to re-raise it to the caller, but continue to still report
+        // the assertion as failed to the WebExtensions internals.
+        aRv.StealExceptionFromJSContext(aCx);
+        matched = false;
+      }
+
+      if (messageVal.isString()) {
+        actualToString.set(messageVal.toString());
+        NS_ENSURE_TRUE(actualString.init(aCx, actualToString), false);
+      }
+    }
+    matched = expectedString.Equals(actualString);
+  } else {
+    // Fallback to strict equal for any other js value type we don't expect.
+    NS_ENSURE_TRUE(
+        JS::StrictlyEqual(aCx, aActualValue, aExpectedMatchValue, &matched),
+        false);
+  }
+
+  // Convert the expected value to a source string, to be interpolated
+  // in the assertion message.
+  JS::Rooted<JSString*> expectedToSource(
+      aCx, JS_ValueToSource(aCx, aExpectedMatchValue));
+  NS_ENSURE_TRUE(expectedToSource, false);
+  nsAutoJSString expectedSource;
+  NS_ENSURE_TRUE(expectedSource.init(aCx, expectedToSource), false);
+
+  nsString message;
+  message.AppendPrintf("%s to match '%s', got '%s'",
+                       NS_ConvertUTF16toUTF8(aMessagePre).get(),
+                       NS_ConvertUTF16toUTF8(expectedSource).get(),
+                       NS_ConvertUTF16toUTF8(actualString).get());
+  if (aMessage.WasPassed()) {
+    message.AppendPrintf(": %s", NS_ConvertUTF16toUTF8(aMessage.Value()).get());
+  }
+
+  // Complete the assertion by forwarding the boolean result and the
+  // interpolated assertion message to the test.assertTrue API method on the
+  // main thread.
+  dom::Sequence<JS::Value> assertTrueArgs;
+  JS::Rooted<JS::Value> arg0(aCx);
+  JS::Rooted<JS::Value> arg1(aCx);
+  NS_ENSURE_FALSE(!dom::ToJSValue(aCx, matched, &arg0) ||
+                      !dom::ToJSValue(aCx, message, &arg1) ||
+                      !assertTrueArgs.AppendElement(arg0, fallible) ||
+                      !assertTrueArgs.AppendElement(arg1, fallible),
+                  false);
+
+  auto request = CallFunctionNoReturn(u"assertTrue"_ns);
+  IgnoredErrorResult erv;
+  if (aSerializedCallerStack) {
+    request->SetSerializedCallerStack(std::move(aSerializedCallerStack));
+  }
+  request->Run(GetGlobalObject(), aCx, assertTrueArgs, erv);
+  NS_ENSURE_FALSE(erv.Failed(), false);
+  return true;
+}
+
 ExtensionEventManager* ExtensionTest::OnMessage() {
   if (!mOnMessageEventMgr) {
     mOnMessageEventMgr = CreateEventManager(u"onMessage"_ns);
   }
 
   return mOnMessageEventMgr;
 }
 
--- a/toolkit/components/extensions/webidl-api/ExtensionTest.h
+++ b/toolkit/components/extensions/webidl-api/ExtensionTest.h
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_extensions_ExtensionTest_h
 #define mozilla_extensions_ExtensionTest_h
 
 #include "js/TypeDecls.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/SerializedStackHolder.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsCOMPtr.h"
 #include "nsISupports.h"
 #include "nsWrapperCache.h"
 
 #include "ExtensionAPIBase.h"
 #include "ExtensionBrowser.h"
 
@@ -57,16 +58,23 @@ class ExtensionTest final : public nsISu
   static bool IsAllowed(JSContext* aCx, JSObject* aGlobal);
 
   nsIGlobalObject* GetParentObject() const;
 
   void CallWebExtMethodAssertEq(JSContext* aCx, const nsAString& aApiMethod,
                                 const dom::Sequence<JS::Value>& aArgs,
                                 ErrorResult& aRv);
 
+  MOZ_CAN_RUN_SCRIPT bool AssertMatchInternal(
+      JSContext* aCx, const JS::HandleValue aActualValue,
+      const JS::HandleValue aExpectedMatchValue, const nsAString& aMessagePre,
+      const dom::Optional<nsAString>& aMessage,
+      UniquePtr<dom::SerializedStackHolder> aSerializedCallerStack,
+      ErrorResult& aRv);
+
   ExtensionEventManager* OnMessage();
 
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ExtensionTest)
 };
 
 }  // namespace extensions
 }  // namespace mozilla