mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Add CDP-JSI integration tests (#43027)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/43027 Changelog: [Internal] Adds a test suite for the integration between the modern RN CDP backend and Hermes (plus potentially other JS engines), mocking out the rest of RN. For simplicity, everything is single-threaded and "async" work is actually done through a queued immediate executor ( = run immediately and finish all queued sub-tasks before returning). The main limitation of the simpler threading model is that we can't cover breakpoints etc - since pausing during JS execution would prevent the test from making progress. Such functionality is better suited for a full RN+CDP integration test (using RN's own thread management) as well as for each engine's unit tests. ## Types of tests in this diff * `TEST_F(JsiIntegrationHermesTest, ...)` - tests specific to the Hermes integration. * `TYPED_TEST(JsiIntegrationPortableTest, ...)` - tests that should pass on all engines. * These use gtest's [typed tests](https://google.github.io/googletest/advanced.html#typed-tests) feature. * This is a good fit for testing CDP features that have no strict dependency on Hermes (like the upcoming `Runtime.addBinding` support). **Long term**, aspirationally, all tests should be in this category, covering a consistent baseline of CDP features needed for debugging with any supported engine. * The first "non-Hermes" engine we test against (`GenericEngineAdapter`) is actually Hermes in disguise, minus any Hermes-specific CDP handling. We could conceivably add more engines here, as long as we have the ability to build them (and their JSI bindings) as part of building the tests. bypass-github-export-checks Reviewed By: huntie Differential Revision: D53756996 fbshipit-source-id: fbafb088abd4263ec841bf848185637ec126c6d1
This commit is contained in:
committed by
Facebook GitHub Bot
parent
032208d81a
commit
7b00a92a3b
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#include <folly/executors/QueuedImmediateExecutor.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <jsinspector-modern/InspectorInterfaces.h>
|
||||
#include <jsinspector-modern/PageTarget.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "FollyDynamicMatchers.h"
|
||||
#include "InspectorMocks.h"
|
||||
#include "UniquePtrFactory.h"
|
||||
#include "engines/JsiIntegrationTestGenericEngineAdapter.h"
|
||||
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
|
||||
|
||||
using namespace ::testing;
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* A text fixture class for the integration between the modern RN CDP backend
|
||||
* and a JSI engine, mocking out the rest of RN. For simplicity, everything is
|
||||
* single-threaded and "async" work is actually done through a queued immediate
|
||||
* executor ( = run immediately and finish all queued sub-tasks before
|
||||
* returning).
|
||||
*
|
||||
* The main limitation of the simpler threading model is that we can't cover
|
||||
* breakpoints etc - since pausing during JS execution would prevent the test
|
||||
* from making progress. Such functionality is better suited for a full RN+CDP
|
||||
* integration test (using RN's own thread management) as well as for each
|
||||
* engine's unit tests.
|
||||
*
|
||||
* \tparam EngineAdapter An adapter class that implements RuntimeTargetDelegate
|
||||
* for a particular engine, plus exposes access to a RuntimeExecutor (based on
|
||||
* the provided folly::Executor) and the corresponding jsi::Runtime.
|
||||
*/
|
||||
template <typename EngineAdapter>
|
||||
class JsiIntegrationPortableTest : public Test, private PageTargetDelegate {
|
||||
folly::QueuedImmediateExecutor immediateExecutor_;
|
||||
|
||||
protected:
|
||||
JsiIntegrationPortableTest() : engineAdapter_{immediateExecutor_} {
|
||||
instance_ = &page_->registerInstance(instanceTargetDelegate_);
|
||||
runtimeTarget_ = &instance_->registerRuntime(
|
||||
engineAdapter_, engineAdapter_.getRuntimeExecutor());
|
||||
}
|
||||
|
||||
~JsiIntegrationPortableTest() override {
|
||||
toPage_.reset();
|
||||
if (runtimeTarget_) {
|
||||
EXPECT_TRUE(instance_);
|
||||
instance_->unregisterRuntime(*runtimeTarget_);
|
||||
runtimeTarget_ = nullptr;
|
||||
}
|
||||
if (instance_) {
|
||||
page_->unregisterInstance(*instance_);
|
||||
instance_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void connect() {
|
||||
ASSERT_FALSE(toPage_) << "Can only connect once in a JSI integration test.";
|
||||
toPage_ = page_->connect(
|
||||
remoteConnections_.make_unique(),
|
||||
{.integrationName = "JsiIntegrationTest"});
|
||||
|
||||
// We'll always get an onDisconnect call when we tear
|
||||
// down the test. Expect it in order to satisfy the strict mock.
|
||||
EXPECT_CALL(*remoteConnections_[0], onDisconnect());
|
||||
}
|
||||
|
||||
void reload() {
|
||||
if (runtimeTarget_) {
|
||||
ASSERT_TRUE(instance_);
|
||||
instance_->unregisterRuntime(*runtimeTarget_);
|
||||
runtimeTarget_ = nullptr;
|
||||
}
|
||||
if (instance_) {
|
||||
page_->unregisterInstance(*instance_);
|
||||
instance_ = nullptr;
|
||||
}
|
||||
instance_ = &page_->registerInstance(instanceTargetDelegate_);
|
||||
runtimeTarget_ = &instance_->registerRuntime(
|
||||
engineAdapter_, engineAdapter_.getRuntimeExecutor());
|
||||
}
|
||||
|
||||
MockRemoteConnection& fromPage() {
|
||||
assert(toPage_);
|
||||
return *remoteConnections_[0];
|
||||
}
|
||||
|
||||
VoidExecutor inspectorExecutor_ = [this](auto callback) {
|
||||
immediateExecutor_.add(callback);
|
||||
};
|
||||
|
||||
jsi::Value eval(std::string_view code) {
|
||||
return engineAdapter_.getRuntime().evaluateJavaScript(
|
||||
std::make_shared<jsi::StringBuffer>(std::string(code)), "<eval>");
|
||||
}
|
||||
|
||||
std::shared_ptr<PageTarget> page_ =
|
||||
PageTarget::create(*this, inspectorExecutor_);
|
||||
InstanceTarget* instance_{};
|
||||
RuntimeTarget* runtimeTarget_{};
|
||||
|
||||
MockInstanceTargetDelegate instanceTargetDelegate_;
|
||||
EngineAdapter engineAdapter_;
|
||||
|
||||
private:
|
||||
UniquePtrFactory<StrictMock<MockRemoteConnection>> remoteConnections_;
|
||||
|
||||
protected:
|
||||
// NOTE: Needs to be destroyed before page_.
|
||||
std::unique_ptr<ILocalConnection> toPage_;
|
||||
|
||||
private:
|
||||
// PageTargetDelegate methods
|
||||
|
||||
void onReload(const PageReloadRequest& request) override {
|
||||
(void)request;
|
||||
reload();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Some tests are specific to Hermes's CDP capabilities and some are not.
|
||||
// We'll use JsiIntegrationHermesTest as a fixture for Hermes-specific tests
|
||||
// and typed tests for the engine-agnostic ones.
|
||||
|
||||
using JsiIntegrationHermesTest =
|
||||
JsiIntegrationPortableTest<JsiIntegrationTestHermesEngineAdapter>;
|
||||
|
||||
/**
|
||||
* The list of engine adapters for which engine-agnostic tests should pass.
|
||||
*/
|
||||
using AllEngines = Types<
|
||||
JsiIntegrationTestHermesEngineAdapter,
|
||||
JsiIntegrationTestGenericEngineAdapter>;
|
||||
|
||||
TYPED_TEST_SUITE(JsiIntegrationPortableTest, AllEngines);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
TYPED_TEST(JsiIntegrationPortableTest, ConnectWithoutCrashing) {
|
||||
this->connect();
|
||||
}
|
||||
|
||||
TYPED_TEST(JsiIntegrationPortableTest, ErrorOnUnknownMethod) {
|
||||
this->connect();
|
||||
|
||||
EXPECT_CALL(
|
||||
this->fromPage(),
|
||||
onMessage(JsonParsed(
|
||||
AllOf(AtJsonPtr("/id", 1), AtJsonPtr("/error/code", -32601)))))
|
||||
.RetiresOnSaturation();
|
||||
|
||||
this->toPage_->sendMessage(R"({
|
||||
"id": 1,
|
||||
"method": "Foobar.unknownMethod"
|
||||
})");
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
TEST_F(JsiIntegrationHermesTest, EvaluateExpression) {
|
||||
connect();
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 1,
|
||||
"result": {
|
||||
"result": {
|
||||
"type": "number",
|
||||
"value": 42
|
||||
}
|
||||
}
|
||||
})")));
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {"expression": "42"}
|
||||
})");
|
||||
}
|
||||
|
||||
TEST_F(JsiIntegrationHermesTest, ExecutionContextNotifications) {
|
||||
connect();
|
||||
|
||||
InSequence s;
|
||||
|
||||
// NOTE: This is the wrong sequence of responses from Hermes - the
|
||||
// notification should come before the method response.
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 1,
|
||||
"result": {}
|
||||
})")));
|
||||
EXPECT_CALL(
|
||||
fromPage(),
|
||||
onMessage(JsonParsed(
|
||||
AllOf(AtJsonPtr("/method", Eq("Runtime.executionContextCreated"))))))
|
||||
.RetiresOnSaturation();
|
||||
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 1,
|
||||
"method": "Runtime.enable"
|
||||
})");
|
||||
|
||||
// NOTE: Missing a Runtime.executionContextDestroyed notification here.
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"method": "Runtime.executionContextsCleared"
|
||||
})")))
|
||||
.RetiresOnSaturation();
|
||||
EXPECT_CALL(
|
||||
fromPage(),
|
||||
onMessage(JsonParsed(
|
||||
AllOf(AtJsonPtr("/method", Eq("Runtime.executionContextCreated"))))))
|
||||
.RetiresOnSaturation();
|
||||
// Simulate a reload triggered by the app (not by the debugger).
|
||||
reload();
|
||||
|
||||
// NOTE: Missing a Runtime.executionContextDestroyed notification here.
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"method": "Runtime.executionContextsCleared"
|
||||
})")))
|
||||
.RetiresOnSaturation();
|
||||
EXPECT_CALL(
|
||||
fromPage(),
|
||||
onMessage(JsonParsed(
|
||||
AllOf(AtJsonPtr("/method", Eq("Runtime.executionContextCreated"))))))
|
||||
.RetiresOnSaturation();
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 2,
|
||||
"result": {}
|
||||
})")))
|
||||
.RetiresOnSaturation();
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 2,
|
||||
"method": "Page.reload"
|
||||
})");
|
||||
}
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#include <jsinspector-modern/FallbackRuntimeAgentDelegate.h>
|
||||
|
||||
#include <folly/executors/QueuedImmediateExecutor.h>
|
||||
#include <hermes/hermes.h>
|
||||
|
||||
#include "JsiIntegrationTestGenericEngineAdapter.h"
|
||||
|
||||
using facebook::hermes::makeHermesRuntime;
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
JsiIntegrationTestGenericEngineAdapter::JsiIntegrationTestGenericEngineAdapter(
|
||||
folly::Executor& jsExecutor)
|
||||
: runtime_{hermes::makeHermesRuntime()}, jsExecutor_{jsExecutor} {}
|
||||
|
||||
std::unique_ptr<RuntimeAgentDelegate>
|
||||
JsiIntegrationTestGenericEngineAdapter::createAgentDelegate(
|
||||
FrontendChannel frontendChannel,
|
||||
SessionState& sessionState) {
|
||||
return std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate>(
|
||||
new FallbackRuntimeAgentDelegate(
|
||||
frontendChannel,
|
||||
sessionState,
|
||||
"Generic engine (" + runtime_->description() + ")"));
|
||||
}
|
||||
|
||||
jsi::Runtime& JsiIntegrationTestGenericEngineAdapter::getRuntime()
|
||||
const noexcept {
|
||||
return *runtime_;
|
||||
}
|
||||
|
||||
RuntimeExecutor JsiIntegrationTestGenericEngineAdapter::getRuntimeExecutor()
|
||||
const noexcept {
|
||||
return [&jsExecutor = jsExecutor_, &runtime = getRuntime()](auto fn) {
|
||||
jsExecutor.add([fn, &runtime]() { fn(runtime); });
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jsinspector-modern/RuntimeTarget.h>
|
||||
|
||||
#include <folly/executors/QueuedImmediateExecutor.h>
|
||||
#include <jsi/jsi.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
/**
|
||||
* An engine adapter for JsiIntegrationTest that represents a generic
|
||||
* JSI-compatible engine, with no engine-specific CDP support. Uses Hermes under
|
||||
* the hood, without Hermes's CDP support.
|
||||
*/
|
||||
class JsiIntegrationTestGenericEngineAdapter : public RuntimeTargetDelegate {
|
||||
public:
|
||||
explicit JsiIntegrationTestGenericEngineAdapter(folly::Executor& jsExecutor);
|
||||
|
||||
virtual std::unique_ptr<RuntimeAgentDelegate> createAgentDelegate(
|
||||
FrontendChannel frontendChannel,
|
||||
SessionState& sessionState) override;
|
||||
|
||||
jsi::Runtime& getRuntime() const noexcept;
|
||||
|
||||
RuntimeExecutor getRuntimeExecutor() const noexcept;
|
||||
|
||||
private:
|
||||
std::unique_ptr<jsi::Runtime> runtime_;
|
||||
folly::Executor& jsExecutor_;
|
||||
};
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#include <folly/executors/QueuedImmediateExecutor.h>
|
||||
|
||||
#include <hermes/inspector-modern/chrome/HermesRuntimeAgentDelegate.h>
|
||||
|
||||
#include "JsiIntegrationTestHermesEngineAdapter.h"
|
||||
|
||||
using facebook::hermes::makeHermesRuntime;
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
JsiIntegrationTestHermesEngineAdapter::JsiIntegrationTestHermesEngineAdapter(
|
||||
folly::Executor& jsExecutor)
|
||||
: runtime_{hermes::makeHermesRuntime()}, jsExecutor_{jsExecutor} {}
|
||||
|
||||
std::unique_ptr<RuntimeAgentDelegate>
|
||||
JsiIntegrationTestHermesEngineAdapter::createAgentDelegate(
|
||||
FrontendChannel frontendChannel,
|
||||
SessionState& sessionState) {
|
||||
return std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate>(
|
||||
new HermesRuntimeAgentDelegate(
|
||||
frontendChannel, sessionState, runtime_, getRuntimeExecutor()));
|
||||
}
|
||||
|
||||
jsi::Runtime& JsiIntegrationTestHermesEngineAdapter::getRuntime()
|
||||
const noexcept {
|
||||
return *runtime_;
|
||||
}
|
||||
|
||||
RuntimeExecutor JsiIntegrationTestHermesEngineAdapter::getRuntimeExecutor()
|
||||
const noexcept {
|
||||
auto& jsExecutor = jsExecutor_;
|
||||
return [runtimeWeak = std::weak_ptr(runtime_), &jsExecutor](auto fn) {
|
||||
jsExecutor.add([runtimeWeak, fn]() {
|
||||
auto runtime = runtimeWeak.lock();
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
fn(*runtime);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jsinspector-modern/RuntimeTarget.h>
|
||||
|
||||
#include <folly/executors/QueuedImmediateExecutor.h>
|
||||
#include <hermes/hermes.h>
|
||||
#include <jsi/jsi.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
/**
|
||||
* An engine adapter for JsiIntegrationTest that uses Hermes (and Hermes's
|
||||
* CDP support).
|
||||
*/
|
||||
class JsiIntegrationTestHermesEngineAdapter : public RuntimeTargetDelegate {
|
||||
public:
|
||||
explicit JsiIntegrationTestHermesEngineAdapter(folly::Executor& jsExecutor);
|
||||
|
||||
virtual std::unique_ptr<RuntimeAgentDelegate> createAgentDelegate(
|
||||
FrontendChannel frontendChannel,
|
||||
SessionState& sessionState) override;
|
||||
|
||||
jsi::Runtime& getRuntime() const noexcept;
|
||||
|
||||
RuntimeExecutor getRuntimeExecutor() const noexcept;
|
||||
|
||||
private:
|
||||
std::shared_ptr<facebook::hermes::HermesRuntime> runtime_;
|
||||
folly::Executor& jsExecutor_;
|
||||
};
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
Reference in New Issue
Block a user