unittests/API/SynthTraceTest.cpp (973 lines of code) (raw):

/* * 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 <hermes/SynthTrace.h> #include <hermes/TraceInterpreter.h> #include <hermes/TracingRuntime.h> #include "hermes/Public/RuntimeConfig.h" #include "hermes/VM/VMExperiments.h" #include "llvh/Support/SHA1.h" #include <gmock/gmock.h> #include <gtest/gtest.h> #include <hermes/hermes.h> #include <memory> using namespace facebook::hermes::tracing; using namespace facebook::hermes; namespace jsi = facebook::jsi; namespace { struct SynthTraceTest : public ::testing::Test { std::unique_ptr<TracingHermesRuntime> rt; SynthTrace::TimeSinceStart dummyTime{SynthTrace::TimeSinceStart::zero()}; static std::unique_ptr<TracingHermesRuntime> makeRuntime() { ::hermes::vm::RuntimeConfig config = ::hermes::vm::RuntimeConfig::Builder().withTraceEnabled(true).build(); // We pass "forReplay = true" below, to prevent the TracingHermesRuntime // from interactions it does automatically on non-replay runs. // We don't need those for these tests. return makeTracingHermesRuntime( makeHermesRuntime(config), config, /* traceStream */ nullptr, /* forReplay */ true); } SynthTraceTest() : rt(makeRuntime()) {} template <typename T> void expectEqual( T expected, const SynthTrace::Record &actual, const char *file, int line) { // gtest doesn't know how to convert a T, so change T to its superclass, // Record. const SynthTrace::Record &baseExpected = expected; if (!(expected == dynamic_cast<const T &>(actual))) { ADD_FAILURE_AT(file, line) << "expected is: " << ::testing::PrintToString(baseExpected) << ", actual is: " << ::testing::PrintToString(actual); } } }; #define EXPECT_EQ_RECORD(expected, actual) \ expectEqual(expected, actual, __FILE__, __LINE__) /// @name Synth trace tests /// @{ TEST_F(SynthTraceTest, CreateObject) { SynthTrace::ObjectID objID; { auto obj = jsi::Object(*rt); objID = rt->getUniqueID(obj); } const auto &records = rt->trace().records(); EXPECT_EQ(1, records.size()); EXPECT_EQ_RECORD( SynthTrace::CreateObjectRecord(dummyTime, objID), *records.at(0)); } TEST_F(SynthTraceTest, CallAndReturn) { const std::string code = "function identity(x) { return x; }"; rt->evaluateJavaScript( std::unique_ptr<jsi::StringBuffer>(new jsi::StringBuffer(code)), ""); const SynthTrace::ObjectID globalObjID = rt->getUniqueID(rt->global()); std::string argStr{"foobar"}; // StringCreate0 auto arg = jsi::String::createFromAscii(*rt, argStr); SynthTrace::ObjectID argID = rt->getUniqueID(arg); std::string identityStr{"identity"}; jsi::String identity = jsi::String::createFromAscii(*rt, identityStr); SynthTrace::ObjectID identityID = rt->getUniqueID(identity); auto func = rt->global().getProperty(*rt, identity).asObject(*rt).asFunction(*rt); SynthTrace::ObjectID functionID = rt->getUniqueID(func); auto ret = func.call(*rt, {std::move(arg)}); // Make sure that the return value is correct in case there's some bug in // the function that was called. ASSERT_EQ(argStr, ret.asString(*rt).utf8(*rt)); const auto &records = rt->trace().records(); // The first two records are for executing the JS for "identity". // Then there are two string creations -- one labeled StringCreate0 above, // and the other for the string "identity" in StringCreate1. EXPECT_EQ(7, records.size()); auto gprExpect = SynthTrace::GetPropertyRecord( dummyTime, globalObjID, identityID, #ifdef HERMESVM_API_TRACE_DEBUG identityStr, #endif SynthTrace::encodeObject(functionID)); EXPECT_EQ_RECORD(gprExpect, *records.at(4)); EXPECT_EQ_RECORD( SynthTrace::CallFromNativeRecord( dummyTime, functionID, SynthTrace::encodeUndefined(), {SynthTrace::encodeString(argID)}), *records.at(5)); EXPECT_EQ_RECORD( SynthTrace::ReturnToNativeRecord( dummyTime, SynthTrace::encodeString(argID)), *records.at(6)); } TEST_F(SynthTraceTest, CallToNative) { auto arg = 1; SynthTrace::ObjectID functionID; uint32_t propNameID; { // Define a native function that is called from JS. auto undefined = [](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t argc) -> jsi::Value { // Return the argument that was passed in. if (argc != 1) { throw std::logic_error("Should be exactly one argument"); } return jsi::Value(rt, args[0].asNumber() + 100); }; auto propName = jsi::PropNameID::forAscii(*rt, "foo"); auto func = jsi::Function::createFromHostFunction(*rt, propName, 1, undefined); propNameID = rt->getUniqueID(propName); functionID = rt->getUniqueID(func); auto ret = func.call(*rt, {jsi::Value(arg)}); ASSERT_EQ(arg + 100, ret.asNumber()); } const auto &records = rt->trace().records(); EXPECT_EQ(6, records.size()); // The function is called from native, and is defined in native, so it // trampolines through the VM. EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord(dummyTime, propNameID, "foo", 3), *records.at(0)); auto chfrExpect = SynthTrace::CreateHostFunctionRecord( dummyTime, functionID, propNameID, #ifdef HERMESVM_API_TRACE_DEBUG "foo", #endif 1); EXPECT_EQ_RECORD(chfrExpect, *records.at(1)); EXPECT_EQ_RECORD( SynthTrace::CallFromNativeRecord( dummyTime, functionID, SynthTrace::encodeUndefined(), {SynthTrace::encodeNumber(arg)}), *records.at(2)); EXPECT_EQ_RECORD( SynthTrace::CallToNativeRecord( dummyTime, functionID, SynthTrace::encodeUndefined(), {SynthTrace::encodeNumber(arg)}), *records.at(3)); // Return once from the call from JS to native, and then again for the call // into JS. EXPECT_EQ_RECORD( SynthTrace::ReturnFromNativeRecord( dummyTime, SynthTrace::encodeNumber(arg + 100)), *records.at(4)); EXPECT_EQ_RECORD( SynthTrace::ReturnToNativeRecord( dummyTime, SynthTrace::encodeNumber(arg + 100)), *records.at(5)); } TEST_F(SynthTraceTest, GetProperty) { std::string a{"a"}; std::string b{"b"}; SynthTrace::ObjectID objID; SynthTrace::ObjectID aStringID; SynthTrace::ObjectID aPropID; SynthTrace::ObjectID bPropID; { auto obj = jsi::Object(*rt); objID = rt->getUniqueID(obj); // Property name doesn't matter, just want to record that some property was // requested. auto aStr = jsi::String::createFromAscii(*rt, a); aStringID = rt->getUniqueID(aStr); auto aValue = obj.getProperty(*rt, aStr); ASSERT_TRUE(aValue.isUndefined()); // Now get using a PropNameID created from aStr. auto aProp = jsi::PropNameID::forString(*rt, aStr); aPropID = rt->getUniqueID(aProp); aValue = obj.getProperty(*rt, aProp); ASSERT_TRUE(aValue.isUndefined()); // Now get using a PropNameID created from b. auto bProp = jsi::PropNameID::forAscii(*rt, b); bPropID = rt->getUniqueID(bProp); auto bValue = obj.getProperty(*rt, bProp); ASSERT_TRUE(bValue.isUndefined()); } const auto &records = rt->trace().records(); EXPECT_EQ(7, records.size()); EXPECT_EQ_RECORD( SynthTrace::CreateObjectRecord(dummyTime, objID), *records.at(0)); EXPECT_EQ_RECORD( SynthTrace::CreateStringRecord(dummyTime, aStringID, a.c_str(), 1), *records.at(1)); auto gprExpect0 = SynthTrace::GetPropertyRecord( dummyTime, objID, aStringID, #ifdef HERMESVM_API_TRACE_DEBUG a, #endif SynthTrace::encodeUndefined()); EXPECT_EQ_RECORD(gprExpect0, *records.at(2)); EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord( dummyTime, aPropID, SynthTrace::encodeString(aStringID)), *records.at(3)); auto gprExpect1 = SynthTrace::GetPropertyRecord( dummyTime, objID, aPropID, #ifdef HERMESVM_API_TRACE_DEBUG a, #endif SynthTrace::encodeUndefined()); EXPECT_EQ_RECORD(gprExpect1, *records.at(4)); EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord(dummyTime, bPropID, b.c_str(), 1), *records.at(5)); auto gprExpect2 = SynthTrace::GetPropertyRecord( dummyTime, objID, bPropID, #ifdef HERMESVM_API_TRACE_DEBUG b, #endif SynthTrace::encodeUndefined()); EXPECT_EQ_RECORD(gprExpect2, *records.at(6)); } TEST_F(SynthTraceTest, SetProperty) { std::string a{"a"}; std::string b{"b"}; SynthTrace::ObjectID objID; SynthTrace::ObjectID aStringID; SynthTrace::ObjectID bPropID; { auto obj = jsi::Object(*rt); objID = rt->getUniqueID(obj); auto aStr = jsi::String::createFromAscii(*rt, a); aStringID = rt->getUniqueID(aStr); obj.setProperty(*rt, aStr, 1); // Now set using a PropNameID. auto bProp = jsi::PropNameID::forAscii(*rt, b); bPropID = rt->getUniqueID(bProp); obj.setProperty(*rt, bProp, true); } const auto &records = rt->trace().records(); EXPECT_EQ(5, records.size()); EXPECT_EQ_RECORD( SynthTrace::CreateObjectRecord(dummyTime, objID), *records.at(0)); EXPECT_EQ_RECORD( SynthTrace::CreateStringRecord(dummyTime, aStringID, a.c_str(), 1), *records.at(1)); auto sprExpect0 = SynthTrace::SetPropertyRecord( dummyTime, objID, aStringID, #ifdef HERMESVM_API_TRACE_DEBUG a, #endif SynthTrace::encodeNumber(1)); EXPECT_EQ_RECORD(sprExpect0, *records.at(2)); EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord(dummyTime, bPropID, b.c_str(), 1), *records.at(3)); auto sprExpect1 = SynthTrace::SetPropertyRecord( dummyTime, objID, bPropID, #ifdef HERMESVM_API_TRACE_DEBUG b, #endif SynthTrace::encodeBool(true)); EXPECT_EQ_RECORD(sprExpect1, *records.at(4)); } TEST_F(SynthTraceTest, HasProperty) { std::string a{"a"}; std::string b{"b"}; SynthTrace::ObjectID objID; SynthTrace::ObjectID aStringID; SynthTrace::ObjectID bPropID; { auto obj = jsi::Object(*rt); objID = rt->getUniqueID(obj); auto aStr = jsi::String::createFromAscii(*rt, a); aStringID = rt->getUniqueID(aStr); bool hasA = obj.hasProperty(*rt, aStr); // Whether or not "a" exists is irrelevant in this test. (void)hasA; // Now set using a PropNameID. auto bProp = jsi::PropNameID::forAscii(*rt, b); bPropID = rt->getUniqueID(bProp); bool hasB = obj.hasProperty(*rt, bProp); // Whether or not "b" exists is irrelevant in this test. (void)hasB; } const auto &records = rt->trace().records(); EXPECT_EQ(5, records.size()); EXPECT_EQ_RECORD( SynthTrace::CreateObjectRecord(dummyTime, objID), *records.at(0)); EXPECT_EQ_RECORD( SynthTrace::CreateStringRecord(dummyTime, aStringID, a.c_str(), 1), *records.at(1)); auto hprExpect0 = SynthTrace::HasPropertyRecord( dummyTime, objID, aStringID #ifdef HERMESVM_API_TRACE_DEBUG , a #endif ); EXPECT_EQ_RECORD(hprExpect0, *records.at(2)); EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord(dummyTime, bPropID, b.c_str(), 1), *records.at(3)); auto hprExpect1 = SynthTrace::HasPropertyRecord( dummyTime, objID, bPropID #ifdef HERMESVM_API_TRACE_DEBUG , b #endif ); EXPECT_EQ_RECORD(hprExpect1, *records.at(4)); } TEST_F(SynthTraceTest, GetPropertyNames) { SynthTrace::ObjectID objID; SynthTrace::ObjectID propNamesID; { auto obj = jsi::Object(*rt); objID = rt->getUniqueID(obj); jsi::Array names = obj.getPropertyNames(*rt); propNamesID = rt->getUniqueID(names); } const auto &records = rt->trace().records(); EXPECT_EQ(2, records.size()); EXPECT_EQ_RECORD( SynthTrace::CreateObjectRecord(dummyTime, objID), *records.at(0)); EXPECT_EQ_RECORD( SynthTrace::GetPropertyNamesRecord(dummyTime, objID, propNamesID), *records.at(1)); } TEST_F(SynthTraceTest, CreateArray) { SynthTrace::ObjectID objID; { auto arr = jsi::Array(*rt, 10); objID = rt->getUniqueID(arr); } const auto &records = rt->trace().records(); EXPECT_EQ(1, records.size()); EXPECT_EQ_RECORD( SynthTrace::CreateArrayRecord(dummyTime, objID, 10), *records.at(0)); } TEST_F(SynthTraceTest, ArrayWrite) { SynthTrace::ObjectID objID; { auto arr = jsi::Array(*rt, 10); objID = rt->getUniqueID(arr); arr.setValueAtIndex(*rt, 0, 1); } const auto &records = rt->trace().records(); EXPECT_EQ(2, records.size()); EXPECT_EQ_RECORD( SynthTrace::CreateArrayRecord(dummyTime, objID, 10), *records.at(0)); EXPECT_EQ_RECORD( SynthTrace::ArrayWriteRecord( dummyTime, objID, 0, SynthTrace::encodeNumber(1)), *records.at(1)); } TEST_F(SynthTraceTest, CallObjectGetProp) { // Test to see if the GetPropertyRecord properly associates with the passed // in object. std::string a{"a"}; std::string getObjectPropStr{"getObjectProp"}; SynthTrace::ObjectID objID; SynthTrace::ObjectID aStringID; SynthTrace::ObjectID functionID; uint32_t propNameID; { auto aStr = jsi::String::createFromAscii(*rt, a); aStringID = rt->getUniqueID(aStr); auto getObjectProp = [&aStr]( jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t argc) { if (argc != 1) { throw std::logic_error("Should be exactly one argument"); } args[0].asObject(rt).getProperty(rt, aStr); return jsi::Value(1); }; auto propName = jsi::PropNameID::forAscii(*rt, getObjectPropStr); auto func = jsi::Function::createFromHostFunction(*rt, propName, 1, getObjectProp); auto obj = jsi::Object(*rt); propNameID = rt->getUniqueID(propName); objID = rt->getUniqueID(obj); functionID = rt->getUniqueID(func); auto value = func.call(*rt, obj); // Make sure the right value was returned. ASSERT_EQ(1, value.asNumber()); } const auto &records = rt->trace().records(); EXPECT_EQ(9, records.size()); EXPECT_EQ_RECORD( SynthTrace::CreateStringRecord(dummyTime, aStringID, a.c_str(), 1), *records.at(0)); EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord( dummyTime, propNameID, getObjectPropStr.c_str(), getObjectPropStr.size()), *records.at(1)); // The function was called with one argument, the object. auto chfrExpect = SynthTrace::CreateHostFunctionRecord( dummyTime, functionID, propNameID, #ifdef HERMESVM_API_TRACE_DEBUG "getObjectProp", #endif 1); EXPECT_EQ_RECORD(chfrExpect, *records.at(2)); EXPECT_EQ_RECORD( SynthTrace::CreateObjectRecord(dummyTime, objID), *records.at(3)); EXPECT_EQ_RECORD( SynthTrace::CallFromNativeRecord( dummyTime, functionID, SynthTrace::encodeUndefined(), {SynthTrace::encodeObject(objID)}), *records.at(4)); // The function (which is called from JS into native) reads one property of // the passed in object. EXPECT_EQ_RECORD( SynthTrace::CallToNativeRecord( dummyTime, functionID, SynthTrace::encodeUndefined(), {SynthTrace::encodeObject(objID)}), *records.at(5)); auto gprExpect = SynthTrace::GetPropertyRecord( dummyTime, objID, aStringID, #ifdef HERMESVM_API_TRACE_DEBUG a, #endif SynthTrace::encodeUndefined()); EXPECT_EQ_RECORD(gprExpect, *records.at(6)); // The function returned a number (it also trampolined through JS and back so // there's two returns). EXPECT_EQ_RECORD( SynthTrace::ReturnFromNativeRecord( dummyTime, SynthTrace::encodeNumber(1)), *records.at(7)); EXPECT_EQ_RECORD( SynthTrace::ReturnToNativeRecord(dummyTime, SynthTrace::encodeNumber(1)), *records.at(8)); } TEST_F(SynthTraceTest, DrainMicrotasks) { { rt->drainMicrotasks(); rt->drainMicrotasks(5); } const auto &records = rt->trace().records(); EXPECT_EQ(2, records.size()); EXPECT_EQ_RECORD( SynthTrace::DrainMicrotasksRecord(dummyTime), *records.at(0)); EXPECT_EQ_RECORD( SynthTrace::DrainMicrotasksRecord(dummyTime, 5), *records.at(1)); } TEST_F(SynthTraceTest, HostObjectProxy) { // This allows us to share the constant strings between the outer scope // and the TestHostObject, below. struct ConstStrings { const std::string x{"x"}; const std::string getHappened{"getHappened"}; const std::string setHappened{"setHappened"}; const std::string getPropertyNamesHappened{"getPropertyNamesHappened"}; }; ConstStrings cs; SynthTrace::ObjectID objID; SynthTrace::ObjectID xPropNameID; SynthTrace::ObjectID getHappenedPropNameID; SynthTrace::ObjectID setHappenedPropNameID; SynthTrace::ObjectID getPropertyNamesHappenedPropNameID; auto insertValue = 5; { class TestHostObject : public jsi::HostObject { double xVal; const ConstStrings &cs; public: jsi::PropNameID xPropName; jsi::PropNameID getHappenedPropName; jsi::PropNameID setHappenedPropName; jsi::PropNameID getPropertyNamesHappenedPropName; TestHostObject(jsi::Runtime &rt, const ConstStrings &cs) : xVal(0.0), cs(cs), xPropName(jsi::PropNameID::forAscii(rt, cs.x.c_str())), getHappenedPropName( jsi::PropNameID::forAscii(rt, cs.getHappened.c_str())), setHappenedPropName( jsi::PropNameID::forAscii(rt, cs.setHappened.c_str())), getPropertyNamesHappenedPropName(jsi::PropNameID::forAscii( rt, cs.getPropertyNamesHappened.c_str())) {} jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override { // Do an operation with the runtime, to ensure that it is traced. rt.global().setProperty(rt, getHappenedPropName, jsi::Value(true)); if (jsi::PropNameID::compare(rt, name, xPropName)) { return jsi::Value(xVal); } else { return jsi::Value::undefined(); } } void set( jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) override { // Do an operation with the runtime, to ensure that it is traced. rt.global().setProperty(rt, setHappenedPropName, jsi::Value(true)); if (jsi::PropNameID::compare(rt, name, xPropName)) { xVal = value.asNumber(); } } std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime &rt) override { // Do an operation with the runtime, to ensure that it is traced. rt.global().setProperty( rt, getPropertyNamesHappenedPropName, jsi::Value(true)); // Can't re-use propName due to deleted copy constructor. return jsi::PropNameID::names(rt, cs.x.c_str()); } }; auto tho = std::make_shared<TestHostObject>(*rt, cs); xPropNameID = rt->getUniqueID(tho->xPropName); getHappenedPropNameID = rt->getUniqueID(tho->getHappenedPropName); setHappenedPropNameID = rt->getUniqueID(tho->setHappenedPropName); getPropertyNamesHappenedPropNameID = rt->getUniqueID(tho->getPropertyNamesHappenedPropName); jsi::Object ho = jsi::Object::createFromHostObject(*rt, tho); objID = rt->getUniqueID(ho); // Access the property ASSERT_EQ(0, ho.getProperty(*rt, tho->xPropName).asNumber()); // Write to the property ho.setProperty(*rt, tho->xPropName, jsi::Value(insertValue)); // Check that it was written just in case. ASSERT_EQ(insertValue, ho.getProperty(*rt, tho->xPropName).asNumber()); } const auto &records = rt->trace().records(); auto globID = rt->getUniqueID(rt->global()); EXPECT_EQ(17, records.size()); // Created a proxy host object. EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord( dummyTime, xPropNameID, cs.x.c_str(), cs.x.size()), *records.at(0)); EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord( dummyTime, getHappenedPropNameID, cs.getHappened.c_str(), cs.getHappened.size()), *records.at(1)); EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord( dummyTime, setHappenedPropNameID, cs.setHappened.c_str(), cs.setHappened.size()), *records.at(2)); EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord( dummyTime, getPropertyNamesHappenedPropNameID, cs.getPropertyNamesHappened.c_str(), cs.getPropertyNamesHappened.size()), *records.at(3)); EXPECT_EQ_RECORD( SynthTrace::CreateHostObjectRecord(dummyTime, objID), *records.at(4)); // Called getProperty on the proxy. This first calls getProperty on the proxy, // then on the host object itself. EXPECT_EQ_RECORD( SynthTrace::GetPropertyNativeRecord(dummyTime, objID, xPropNameID, cs.x), *records.at(5)); auto sprExpect0 = SynthTrace::SetPropertyRecord( dummyTime, globID, getHappenedPropNameID, #ifdef HERMESVM_API_TRACE_DEBUG cs.getHappened, #endif SynthTrace::encodeBool(true)); EXPECT_EQ_RECORD(sprExpect0, *records.at(6)); EXPECT_EQ_RECORD( SynthTrace::GetPropertyNativeReturnRecord( dummyTime, SynthTrace::encodeNumber(0)), *records.at(7)); auto gprExpect0 = SynthTrace::GetPropertyRecord( dummyTime, objID, xPropNameID, #ifdef HERMESVM_API_TRACE_DEBUG cs.x, #endif SynthTrace::encodeNumber(0)); EXPECT_EQ_RECORD(gprExpect0, *records.at(8)); // Called setProperty on the proxy. auto sprExpect1 = SynthTrace::SetPropertyRecord( dummyTime, objID, xPropNameID, #ifdef HERMESVM_API_TRACE_DEBUG cs.x, #endif SynthTrace::encodeNumber(insertValue)); EXPECT_EQ_RECORD(sprExpect1, *records.at(9)); EXPECT_EQ_RECORD( SynthTrace::SetPropertyNativeRecord( dummyTime, objID, xPropNameID, cs.x, SynthTrace::encodeNumber(insertValue)), *records.at(10)); auto sprExpect2 = SynthTrace::SetPropertyRecord( dummyTime, globID, setHappenedPropNameID, #ifdef HERMESVM_API_TRACE_DEBUG cs.setHappened, #endif SynthTrace::encodeBool(true)); EXPECT_EQ_RECORD(sprExpect2, *records.at(11)); EXPECT_EQ_RECORD( SynthTrace::SetPropertyNativeReturnRecord(dummyTime), *records.at(12)); // Called getProperty one last time. EXPECT_EQ_RECORD( SynthTrace::GetPropertyNativeRecord(dummyTime, objID, xPropNameID, cs.x), *records.at(13)); auto sprExpect4 = SynthTrace::SetPropertyRecord( dummyTime, globID, getHappenedPropNameID, #ifdef HERMESVM_API_TRACE_DEBUG cs.getHappened, #endif SynthTrace::encodeBool(true)); EXPECT_EQ_RECORD(sprExpect4, *records.at(14)); EXPECT_EQ_RECORD( SynthTrace::GetPropertyNativeReturnRecord( dummyTime, SynthTrace::encodeNumber(insertValue)), *records.at(15)); auto gprExpect1 = SynthTrace::GetPropertyRecord( dummyTime, objID, xPropNameID, #ifdef HERMESVM_API_TRACE_DEBUG cs.x, #endif SynthTrace::encodeNumber(insertValue)); EXPECT_EQ_RECORD(gprExpect1, *records.at(16)); } TEST_F(SynthTraceTest, HostObjectPropertyNamesAreDefs) { const std::string ho{"ho"}; const std::string x{"x"}; const std::string y{"y"}; const std::string xRes{"xRes"}; const std::string yRes{"yRes"}; // This allows us to share the constant strings between the outer scope // and the TestHostObject, below. struct ConstStrings { const std::string o{"o"}; }; ConstStrings cs; SynthTrace::ObjectID oObjID; SynthTrace::ObjectID hoObjID; SynthTrace::ObjectID oPropNameID; SynthTrace::ObjectID hoPropNameID; SynthTrace::ObjectID xResPropNameID; SynthTrace::ObjectID yResPropNameID; hermes::SHA1 codeHash; { class TestHostObject : public jsi::HostObject { public: jsi::PropNameID oPropName; TestHostObject(jsi::Runtime &rt, const ConstStrings &cs) : oPropName(jsi::PropNameID::forAscii(rt, cs.o.c_str())) {} jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override { // Do an operation with the runtime, to ensure that it is traced. auto oObj = rt.global().getProperty(rt, oPropName).asObject(rt); return oObj.getProperty(rt, name); } void set( jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) override { // Do an operation with the runtime, to ensure that it is traced. auto oObj = rt.global().getProperty(rt, oPropName).asObject(rt); oObj.setProperty(rt, name, value); } }; auto tho = std::make_shared<TestHostObject>(*rt, cs); oPropNameID = rt->getUniqueID(tho->oPropName); jsi::Object o{*rt}; oObjID = rt->getUniqueID(o); rt->global().setProperty(*rt, tho->oPropName, o); jsi::Object hoObj = jsi::Object::createFromHostObject(*rt, tho); hoObjID = rt->getUniqueID(hoObj); auto hoPropName = jsi::PropNameID::forAscii(*rt, ho.c_str()); hoPropNameID = rt->getUniqueID(hoPropName); rt->global().setProperty(*rt, hoPropName, hoObj); const std::string code = R"###( o.x = 7; o.y = true; (function () { this.xRes = ho.x; ho.y = false; this.yRes = o.y; }) (); )###"; codeHash = llvh::SHA1::hash(llvh::makeArrayRef( reinterpret_cast<const uint8_t *>(code.data()), code.size())); rt->evaluateJavaScript( std::unique_ptr<jsi::StringBuffer>(new jsi::StringBuffer(code)), ""); auto xResPropName = jsi::PropNameID::forAscii(*rt, xRes.c_str()); xResPropNameID = rt->getUniqueID(xResPropName); auto yResPropName = jsi::PropNameID::forAscii(*rt, yRes.c_str()); yResPropNameID = rt->getUniqueID(yResPropName); // Retrieve the results. ASSERT_EQ(7, rt->global().getProperty(*rt, xResPropName).asNumber()); ASSERT_FALSE(rt->global().getProperty(*rt, yResPropName).getBool()); } const auto &records = rt->trace().records(); auto globID = rt->getUniqueID(rt->global()); EXPECT_EQ(20, records.size()); // Created a proxy host object. EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord( dummyTime, oPropNameID, cs.o.c_str(), cs.o.size()), *records.at(0)); EXPECT_EQ_RECORD( SynthTrace::CreateObjectRecord(dummyTime, oObjID), *records.at(1)); auto sprExpect0 = SynthTrace::SetPropertyRecord( dummyTime, globID, oPropNameID, #ifdef HERMESVM_API_TRACE_DEBUG cs.o, #endif SynthTrace::encodeObject(oObjID)); EXPECT_EQ_RECORD(sprExpect0, *records.at(2)); EXPECT_EQ_RECORD( SynthTrace::CreateHostObjectRecord(dummyTime, hoObjID), *records.at(3)); EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord( dummyTime, hoPropNameID, ho.c_str(), ho.size()), *records.at(4)); auto sprExpect1 = SynthTrace::SetPropertyRecord( dummyTime, globID, hoPropNameID, #ifdef HERMESVM_API_TRACE_DEBUG ho, #endif SynthTrace::encodeObject(hoObjID)); EXPECT_EQ_RECORD(sprExpect1, *records.at(5)); EXPECT_EQ_RECORD( SynthTrace::BeginExecJSRecord(dummyTime, "", codeHash, false), *records.at(6)); // Called getProperty on the host object. // We can't create the expected record, since we don't know the unique ID for // the PropNameID for "x". So test the fields individually. EXPECT_EQ( SynthTrace::RecordType::GetPropertyNative, records.at(7)->getType()); auto rec7AsGPN = dynamic_cast<const SynthTrace::GetPropertyNativeRecord &>(*records.at(7)); EXPECT_EQ(hoObjID, rec7AsGPN.hostObjectID_); uint32_t observedXPropNameUID = rec7AsGPN.propNameID_; EXPECT_EQ(x, rec7AsGPN.propName_); // Now we're in in the body of the HostObject getter. auto gprExpect0 = SynthTrace::GetPropertyRecord( dummyTime, globID, oPropNameID, #ifdef HERMESVM_API_TRACE_DEBUG cs.o, #endif SynthTrace::encodeObject(oObjID)); EXPECT_EQ_RECORD(gprExpect0, *records.at(8)); auto gprExpect1 = SynthTrace::GetPropertyRecord( dummyTime, oObjID, observedXPropNameUID, #ifdef HERMESVM_API_TRACE_DEBUG x, #endif SynthTrace::encodeNumber(7)); EXPECT_EQ_RECORD(gprExpect1, *records.at(9)); EXPECT_EQ_RECORD( SynthTrace::GetPropertyNativeReturnRecord( dummyTime, SynthTrace::encodeNumber(7)), *records.at(10)); // Called setProperty on the host object. // We can't create the expected record, since we don't know the unique ID for // the PropNameID for "y". So test the fields individually. EXPECT_EQ( SynthTrace::RecordType::SetPropertyNative, records.at(11)->getType()); auto rec11AsGPN = dynamic_cast<const SynthTrace::SetPropertyNativeRecord &>( *records.at(11)); EXPECT_EQ(hoObjID, rec11AsGPN.hostObjectID_); uint32_t observedYPropNameUID = rec11AsGPN.propNameID_; EXPECT_EQ(y, rec11AsGPN.propName_); EXPECT_FALSE(rec11AsGPN.value_.getBool()); auto gprExpect2 = SynthTrace::GetPropertyRecord( dummyTime, globID, oPropNameID, #ifdef HERMESVM_API_TRACE_DEBUG cs.o, #endif SynthTrace::encodeObject(oObjID)); EXPECT_EQ_RECORD(gprExpect2, *records.at(12)); auto sprExpect = SynthTrace::SetPropertyRecord( dummyTime, oObjID, observedYPropNameUID, #ifdef HERMESVM_API_TRACE_DEBUG y, #endif SynthTrace::encodeBool(false)); EXPECT_EQ_RECORD(sprExpect, *records.at(13)); EXPECT_EQ_RECORD( SynthTrace::SetPropertyNativeReturnRecord(dummyTime), *records.at(14)); EXPECT_EQ_RECORD( SynthTrace::EndExecJSRecord(dummyTime, SynthTrace::encodeUndefined()), *records.at(15)); EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord( dummyTime, xResPropNameID, xRes.c_str(), xRes.size()), *records.at(16)); EXPECT_EQ_RECORD( SynthTrace::CreatePropNameIDRecord( dummyTime, yResPropNameID, yRes.c_str(), yRes.size()), *records.at(17)); auto gprExpect3 = SynthTrace::GetPropertyRecord( dummyTime, globID, xResPropNameID, #ifdef HERMESVM_API_TRACE_DEBUG xRes, #endif SynthTrace::encodeNumber(7)); EXPECT_EQ_RECORD(gprExpect3, *records.at(18)); auto gprExpect4 = SynthTrace::GetPropertyRecord( dummyTime, globID, yResPropNameID, #ifdef HERMESVM_API_TRACE_DEBUG yRes, #endif SynthTrace::encodeBool(false)); EXPECT_EQ_RECORD(gprExpect4, *records.at(19)); } // These tests fail on Windows. #if defined(EXPECT_DEATH_IF_SUPPORTED) && !defined(_WINDOWS) TEST(SynthTraceDeathTest, HostFunctionThrowsExceptionFails) { auto fn = [] { auto rt = SynthTraceTest::makeRuntime(); // TODO (T28293178) Remove this once exceptions are supported. jsi::Function throwingFunc = jsi::Function::createFromHostFunction( *rt, jsi::PropNameID::forAscii(*rt, "thrower"), 0, [](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value { throw std::runtime_error("Cannot call"); }); throwingFunc.call(*rt); }; EXPECT_DEATH_IF_SUPPORTED(fn(), ""); } TEST(SynthTraceDeathTest, HostObjectThrowsExceptionFails) { // TODO (T28293178) Remove this once exceptions are supported. class ThrowingHostObject : public jsi::HostObject { jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &sym) override { throw std::runtime_error("Cannot get"); } void set( jsi::Runtime &rt, const jsi::PropNameID &sym, const jsi::Value &val) override { throw std::runtime_error("Cannot set"); } }; auto fn = [] { auto rt = SynthTraceTest::makeRuntime(); jsi::Object thro = jsi::Object::createFromHostObject( *rt, std::make_shared<ThrowingHostObject>()); ASSERT_TRUE(thro.isHostObject(*rt)); thro.getProperty(*rt, "foo"); }; EXPECT_DEATH_IF_SUPPORTED(fn(), ""); } #endif /// @} /// @name Synth trace replay tests /// @{ struct SynthTraceReplayTest : public ::testing::Test { ::hermes::vm::RuntimeConfig config; std::string traceResult; std::unique_ptr<TracingHermesRuntime> traceRt; std::unique_ptr<jsi::Runtime> replayRt; SynthTraceReplayTest(::hermes::vm::RuntimeConfig conf) : config(conf), traceRt(makeTracingHermesRuntime( makeHermesRuntime(config), config, /* traceStream */ std::make_unique<llvh::raw_string_ostream>(traceResult), /* forReplay */ true)) {} SynthTraceReplayTest() : SynthTraceReplayTest(::hermes::vm::RuntimeConfig::Builder() .withTraceEnabled(true) .build()) {} void replay() { traceRt.reset(); replayRt = makeHermesRuntime(config); tracing::TraceInterpreter::execFromMemoryBuffer( llvh::MemoryBuffer::getMemBuffer(traceResult), {}, *replayRt, {}); } jsi::Value eval(jsi::Runtime &rt, const char *code) { return rt.global().getPropertyAsFunction(rt, "eval").call(rt, code); } }; TEST_F(SynthTraceReplayTest, CreateObjectReplay) { { auto &rt = *traceRt; auto obj = jsi::Object(rt); obj.setProperty(rt, "bar", 5); rt.global().setProperty(rt, "foo", obj); } replay(); { auto &rt = *replayRt; EXPECT_EQ( rt.global() .getPropertyAsObject(rt, "foo") .getProperty(rt, "bar") .asNumber(), 5); } } TEST_F(SynthTraceReplayTest, SetPropertyReplay) { { auto &rt = *traceRt; auto asciiProp = jsi::PropNameID::forAscii(rt, "a"); auto utf8Prop = jsi::PropNameID::forUtf8(rt, "b"); auto strProp = jsi::PropNameID::forString(rt, jsi::String::createFromAscii(rt, "c")); rt.global().setProperty(rt, "symD", eval(rt, "Symbol('d')")); rt.global().setProperty(rt, "symE", eval(rt, "Symbol('e')")); auto symProp = jsi::PropNameID::forSymbol( rt, rt.global().getProperty(rt, "symE").asSymbol(rt)); jsi::Object x(rt); x.setProperty(rt, asciiProp, "apple"); x.setProperty(rt, utf8Prop, "banana"); x.setProperty(rt, strProp, "coconut"); x.setProperty(rt, symProp, "eggplant"); rt.global().setProperty(rt, "x", x); eval(rt, "x[symD] = 'durian'"); } replay(); { auto &rt = *replayRt; EXPECT_EQ(eval(rt, "x.a").asString(rt).utf8(rt), "apple"); EXPECT_EQ(eval(rt, "x.b").asString(rt).utf8(rt), "banana"); EXPECT_EQ(eval(rt, "x.c").asString(rt).utf8(rt), "coconut"); EXPECT_EQ(eval(rt, "x[symD]").asString(rt).utf8(rt), "durian"); EXPECT_EQ(eval(rt, "x[symE]").asString(rt).utf8(rt), "eggplant"); } } struct JobQueueReplayTest : public SynthTraceReplayTest { JobQueueReplayTest() : SynthTraceReplayTest( ::hermes::vm::RuntimeConfig::Builder() .withTraceEnabled(true) .withEnableHermesInternal(true) .withVMExperimentFlags(::hermes::vm::experiments::JobQueue) .build()) {} }; TEST_F(JobQueueReplayTest, DrainSingleMicrotask) { { auto &rt = *traceRt; eval(rt, "var x = 3; HermesInternal.enqueueJob(function(){x = 4;})"); rt.drainMicrotasks(); } replay(); { auto &rt = *replayRt; EXPECT_EQ(eval(rt, "x").asNumber(), 4); } } TEST_F(JobQueueReplayTest, DrainMultipleMicrotasks) { { auto &rt = *traceRt; eval(rt, R""""( var x = 0; function inc(){ x += 1; } HermesInternal.enqueueJob(inc); HermesInternal.enqueueJob(inc); HermesInternal.enqueueJob(inc); )""""); rt.drainMicrotasks(); } replay(); { auto &rt = *replayRt; EXPECT_EQ(eval(rt, "x").asNumber(), 3); } } /// @} } // namespace