Skip to content

Commit 7b00a92

Browse files
motiz88facebook-github-bot
authored andcommitted
Add CDP-JSI integration tests (#43027)
Summary: Pull Request resolved: #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
1 parent 032208d commit 7b00a92

5 files changed

Lines changed: 432 additions & 0 deletions

File tree

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <folly/executors/QueuedImmediateExecutor.h>
9+
#include <gmock/gmock.h>
10+
#include <gtest/gtest.h>
11+
12+
#include <jsinspector-modern/InspectorInterfaces.h>
13+
#include <jsinspector-modern/PageTarget.h>
14+
15+
#include <memory>
16+
17+
#include "FollyDynamicMatchers.h"
18+
#include "InspectorMocks.h"
19+
#include "UniquePtrFactory.h"
20+
#include "engines/JsiIntegrationTestGenericEngineAdapter.h"
21+
#include "engines/JsiIntegrationTestHermesEngineAdapter.h"
22+
23+
using namespace ::testing;
24+
25+
namespace facebook::react::jsinspector_modern {
26+
27+
namespace {
28+
29+
/**
30+
* A text fixture class for the integration between the modern RN CDP backend
31+
* and a JSI engine, mocking out the rest of RN. For simplicity, everything is
32+
* single-threaded and "async" work is actually done through a queued immediate
33+
* executor ( = run immediately and finish all queued sub-tasks before
34+
* returning).
35+
*
36+
* The main limitation of the simpler threading model is that we can't cover
37+
* breakpoints etc - since pausing during JS execution would prevent the test
38+
* from making progress. Such functionality is better suited for a full RN+CDP
39+
* integration test (using RN's own thread management) as well as for each
40+
* engine's unit tests.
41+
*
42+
* \tparam EngineAdapter An adapter class that implements RuntimeTargetDelegate
43+
* for a particular engine, plus exposes access to a RuntimeExecutor (based on
44+
* the provided folly::Executor) and the corresponding jsi::Runtime.
45+
*/
46+
template <typename EngineAdapter>
47+
class JsiIntegrationPortableTest : public Test, private PageTargetDelegate {
48+
folly::QueuedImmediateExecutor immediateExecutor_;
49+
50+
protected:
51+
JsiIntegrationPortableTest() : engineAdapter_{immediateExecutor_} {
52+
instance_ = &page_->registerInstance(instanceTargetDelegate_);
53+
runtimeTarget_ = &instance_->registerRuntime(
54+
engineAdapter_, engineAdapter_.getRuntimeExecutor());
55+
}
56+
57+
~JsiIntegrationPortableTest() override {
58+
toPage_.reset();
59+
if (runtimeTarget_) {
60+
EXPECT_TRUE(instance_);
61+
instance_->unregisterRuntime(*runtimeTarget_);
62+
runtimeTarget_ = nullptr;
63+
}
64+
if (instance_) {
65+
page_->unregisterInstance(*instance_);
66+
instance_ = nullptr;
67+
}
68+
}
69+
70+
void connect() {
71+
ASSERT_FALSE(toPage_) << "Can only connect once in a JSI integration test.";
72+
toPage_ = page_->connect(
73+
remoteConnections_.make_unique(),
74+
{.integrationName = "JsiIntegrationTest"});
75+
76+
// We'll always get an onDisconnect call when we tear
77+
// down the test. Expect it in order to satisfy the strict mock.
78+
EXPECT_CALL(*remoteConnections_[0], onDisconnect());
79+
}
80+
81+
void reload() {
82+
if (runtimeTarget_) {
83+
ASSERT_TRUE(instance_);
84+
instance_->unregisterRuntime(*runtimeTarget_);
85+
runtimeTarget_ = nullptr;
86+
}
87+
if (instance_) {
88+
page_->unregisterInstance(*instance_);
89+
instance_ = nullptr;
90+
}
91+
instance_ = &page_->registerInstance(instanceTargetDelegate_);
92+
runtimeTarget_ = &instance_->registerRuntime(
93+
engineAdapter_, engineAdapter_.getRuntimeExecutor());
94+
}
95+
96+
MockRemoteConnection& fromPage() {
97+
assert(toPage_);
98+
return *remoteConnections_[0];
99+
}
100+
101+
VoidExecutor inspectorExecutor_ = [this](auto callback) {
102+
immediateExecutor_.add(callback);
103+
};
104+
105+
jsi::Value eval(std::string_view code) {
106+
return engineAdapter_.getRuntime().evaluateJavaScript(
107+
std::make_shared<jsi::StringBuffer>(std::string(code)), "<eval>");
108+
}
109+
110+
std::shared_ptr<PageTarget> page_ =
111+
PageTarget::create(*this, inspectorExecutor_);
112+
InstanceTarget* instance_{};
113+
RuntimeTarget* runtimeTarget_{};
114+
115+
MockInstanceTargetDelegate instanceTargetDelegate_;
116+
EngineAdapter engineAdapter_;
117+
118+
private:
119+
UniquePtrFactory<StrictMock<MockRemoteConnection>> remoteConnections_;
120+
121+
protected:
122+
// NOTE: Needs to be destroyed before page_.
123+
std::unique_ptr<ILocalConnection> toPage_;
124+
125+
private:
126+
// PageTargetDelegate methods
127+
128+
void onReload(const PageReloadRequest& request) override {
129+
(void)request;
130+
reload();
131+
}
132+
};
133+
134+
} // namespace
135+
136+
////////////////////////////////////////////////////////////////////////////////
137+
138+
// Some tests are specific to Hermes's CDP capabilities and some are not.
139+
// We'll use JsiIntegrationHermesTest as a fixture for Hermes-specific tests
140+
// and typed tests for the engine-agnostic ones.
141+
142+
using JsiIntegrationHermesTest =
143+
JsiIntegrationPortableTest<JsiIntegrationTestHermesEngineAdapter>;
144+
145+
/**
146+
* The list of engine adapters for which engine-agnostic tests should pass.
147+
*/
148+
using AllEngines = Types<
149+
JsiIntegrationTestHermesEngineAdapter,
150+
JsiIntegrationTestGenericEngineAdapter>;
151+
152+
TYPED_TEST_SUITE(JsiIntegrationPortableTest, AllEngines);
153+
154+
////////////////////////////////////////////////////////////////////////////////
155+
156+
TYPED_TEST(JsiIntegrationPortableTest, ConnectWithoutCrashing) {
157+
this->connect();
158+
}
159+
160+
TYPED_TEST(JsiIntegrationPortableTest, ErrorOnUnknownMethod) {
161+
this->connect();
162+
163+
EXPECT_CALL(
164+
this->fromPage(),
165+
onMessage(JsonParsed(
166+
AllOf(AtJsonPtr("/id", 1), AtJsonPtr("/error/code", -32601)))))
167+
.RetiresOnSaturation();
168+
169+
this->toPage_->sendMessage(R"({
170+
"id": 1,
171+
"method": "Foobar.unknownMethod"
172+
})");
173+
}
174+
175+
////////////////////////////////////////////////////////////////////////////////
176+
177+
TEST_F(JsiIntegrationHermesTest, EvaluateExpression) {
178+
connect();
179+
180+
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
181+
"id": 1,
182+
"result": {
183+
"result": {
184+
"type": "number",
185+
"value": 42
186+
}
187+
}
188+
})")));
189+
toPage_->sendMessage(R"({
190+
"id": 1,
191+
"method": "Runtime.evaluate",
192+
"params": {"expression": "42"}
193+
})");
194+
}
195+
196+
TEST_F(JsiIntegrationHermesTest, ExecutionContextNotifications) {
197+
connect();
198+
199+
InSequence s;
200+
201+
// NOTE: This is the wrong sequence of responses from Hermes - the
202+
// notification should come before the method response.
203+
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
204+
"id": 1,
205+
"result": {}
206+
})")));
207+
EXPECT_CALL(
208+
fromPage(),
209+
onMessage(JsonParsed(
210+
AllOf(AtJsonPtr("/method", Eq("Runtime.executionContextCreated"))))))
211+
.RetiresOnSaturation();
212+
213+
toPage_->sendMessage(R"({
214+
"id": 1,
215+
"method": "Runtime.enable"
216+
})");
217+
218+
// NOTE: Missing a Runtime.executionContextDestroyed notification here.
219+
220+
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
221+
"method": "Runtime.executionContextsCleared"
222+
})")))
223+
.RetiresOnSaturation();
224+
EXPECT_CALL(
225+
fromPage(),
226+
onMessage(JsonParsed(
227+
AllOf(AtJsonPtr("/method", Eq("Runtime.executionContextCreated"))))))
228+
.RetiresOnSaturation();
229+
// Simulate a reload triggered by the app (not by the debugger).
230+
reload();
231+
232+
// NOTE: Missing a Runtime.executionContextDestroyed notification here.
233+
234+
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
235+
"method": "Runtime.executionContextsCleared"
236+
})")))
237+
.RetiresOnSaturation();
238+
EXPECT_CALL(
239+
fromPage(),
240+
onMessage(JsonParsed(
241+
AllOf(AtJsonPtr("/method", Eq("Runtime.executionContextCreated"))))))
242+
.RetiresOnSaturation();
243+
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
244+
"id": 2,
245+
"result": {}
246+
})")))
247+
.RetiresOnSaturation();
248+
toPage_->sendMessage(R"({
249+
"id": 2,
250+
"method": "Page.reload"
251+
})");
252+
}
253+
254+
} // namespace facebook::react::jsinspector_modern
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <jsinspector-modern/FallbackRuntimeAgentDelegate.h>
9+
10+
#include <folly/executors/QueuedImmediateExecutor.h>
11+
#include <hermes/hermes.h>
12+
13+
#include "JsiIntegrationTestGenericEngineAdapter.h"
14+
15+
using facebook::hermes::makeHermesRuntime;
16+
17+
namespace facebook::react::jsinspector_modern {
18+
19+
JsiIntegrationTestGenericEngineAdapter::JsiIntegrationTestGenericEngineAdapter(
20+
folly::Executor& jsExecutor)
21+
: runtime_{hermes::makeHermesRuntime()}, jsExecutor_{jsExecutor} {}
22+
23+
std::unique_ptr<RuntimeAgentDelegate>
24+
JsiIntegrationTestGenericEngineAdapter::createAgentDelegate(
25+
FrontendChannel frontendChannel,
26+
SessionState& sessionState) {
27+
return std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate>(
28+
new FallbackRuntimeAgentDelegate(
29+
frontendChannel,
30+
sessionState,
31+
"Generic engine (" + runtime_->description() + ")"));
32+
}
33+
34+
jsi::Runtime& JsiIntegrationTestGenericEngineAdapter::getRuntime()
35+
const noexcept {
36+
return *runtime_;
37+
}
38+
39+
RuntimeExecutor JsiIntegrationTestGenericEngineAdapter::getRuntimeExecutor()
40+
const noexcept {
41+
return [&jsExecutor = jsExecutor_, &runtime = getRuntime()](auto fn) {
42+
jsExecutor.add([fn, &runtime]() { fn(runtime); });
43+
};
44+
}
45+
46+
} // namespace facebook::react::jsinspector_modern
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <jsinspector-modern/RuntimeTarget.h>
11+
12+
#include <folly/executors/QueuedImmediateExecutor.h>
13+
#include <jsi/jsi.h>
14+
15+
#include <memory>
16+
17+
namespace facebook::react::jsinspector_modern {
18+
19+
/**
20+
* An engine adapter for JsiIntegrationTest that represents a generic
21+
* JSI-compatible engine, with no engine-specific CDP support. Uses Hermes under
22+
* the hood, without Hermes's CDP support.
23+
*/
24+
class JsiIntegrationTestGenericEngineAdapter : public RuntimeTargetDelegate {
25+
public:
26+
explicit JsiIntegrationTestGenericEngineAdapter(folly::Executor& jsExecutor);
27+
28+
virtual std::unique_ptr<RuntimeAgentDelegate> createAgentDelegate(
29+
FrontendChannel frontendChannel,
30+
SessionState& sessionState) override;
31+
32+
jsi::Runtime& getRuntime() const noexcept;
33+
34+
RuntimeExecutor getRuntimeExecutor() const noexcept;
35+
36+
private:
37+
std::unique_ptr<jsi::Runtime> runtime_;
38+
folly::Executor& jsExecutor_;
39+
};
40+
41+
} // namespace facebook::react::jsinspector_modern
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <folly/executors/QueuedImmediateExecutor.h>
9+
10+
#include <hermes/inspector-modern/chrome/HermesRuntimeAgentDelegate.h>
11+
12+
#include "JsiIntegrationTestHermesEngineAdapter.h"
13+
14+
using facebook::hermes::makeHermesRuntime;
15+
16+
namespace facebook::react::jsinspector_modern {
17+
18+
JsiIntegrationTestHermesEngineAdapter::JsiIntegrationTestHermesEngineAdapter(
19+
folly::Executor& jsExecutor)
20+
: runtime_{hermes::makeHermesRuntime()}, jsExecutor_{jsExecutor} {}
21+
22+
std::unique_ptr<RuntimeAgentDelegate>
23+
JsiIntegrationTestHermesEngineAdapter::createAgentDelegate(
24+
FrontendChannel frontendChannel,
25+
SessionState& sessionState) {
26+
return std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate>(
27+
new HermesRuntimeAgentDelegate(
28+
frontendChannel, sessionState, runtime_, getRuntimeExecutor()));
29+
}
30+
31+
jsi::Runtime& JsiIntegrationTestHermesEngineAdapter::getRuntime()
32+
const noexcept {
33+
return *runtime_;
34+
}
35+
36+
RuntimeExecutor JsiIntegrationTestHermesEngineAdapter::getRuntimeExecutor()
37+
const noexcept {
38+
auto& jsExecutor = jsExecutor_;
39+
return [runtimeWeak = std::weak_ptr(runtime_), &jsExecutor](auto fn) {
40+
jsExecutor.add([runtimeWeak, fn]() {
41+
auto runtime = runtimeWeak.lock();
42+
if (!runtime) {
43+
return;
44+
}
45+
fn(*runtime);
46+
});
47+
};
48+
}
49+
50+
} // namespace facebook::react::jsinspector_modern

0 commit comments

Comments
 (0)