unittests/VMRuntime/TestHelpers.h (299 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. */ #ifndef HERMES_UNITTESTS_VMRUNTIME_TESTHELPERS_H #define HERMES_UNITTESTS_VMRUNTIME_TESTHELPERS_H #include "hermes/BCGen/HBC/BytecodeGenerator.h" #include "hermes/BCGen/HBC/BytecodeProviderFromSrc.h" #include "hermes/Public/GCConfig.h" #include "hermes/Public/RuntimeConfig.h" #include "hermes/VM/Callable.h" #include "hermes/VM/CodeBlock.h" #include "hermes/VM/Domain.h" #include "hermes/VM/JSArray.h" #include "hermes/VM/Operations.h" #include "hermes/VM/PointerBase.h" #include "hermes/VM/Runtime.h" #include "hermes/VM/RuntimeModule-inline.h" #include "hermes/VM/StorageProvider.h" #include "hermes/VM/StringPrimitive.h" #include "hermes/VM/StringRefUtils.h" #include "gtest/gtest.h" namespace hermes { namespace vm { // Initial and max heap size constants static constexpr uint32_t kInitHeapSmall = 1 << 8; static constexpr uint32_t kMaxHeapSmall = 1 << 11; static constexpr uint32_t kInitHeapSize = 1 << 16; static constexpr uint32_t kMaxHeapSize = 1 << 19; static constexpr uint32_t kInitHeapLarge = 1 << 20; static constexpr uint32_t kMaxHeapLarge = 1 << 24; static const GCConfig::Builder kTestGCConfigBaseBuilder = GCConfig::Builder() .withSanitizeConfig( vm::GCSanitizeConfig::Builder().withSanitizeRate(0.0).build()) .withShouldRandomizeAllocSpace(false); static const GCConfig kTestGCConfigSmall = GCConfig::Builder(kTestGCConfigBaseBuilder) .withInitHeapSize(kInitHeapSmall) .withMaxHeapSize(kMaxHeapSmall) .build(); static const GCConfig::Builder kTestGCConfigBuilder = GCConfig::Builder(kTestGCConfigBaseBuilder) .withInitHeapSize(kInitHeapSize) .withMaxHeapSize(kMaxHeapSize); static const GCConfig kTestGCConfig = GCConfig::Builder(kTestGCConfigBuilder).build(); static const GCConfig kTestGCConfigLarge = GCConfig::Builder(kTestGCConfigBuilder) .withInitHeapSize(kInitHeapLarge) .withMaxHeapSize(kMaxHeapLarge) .build(); static const RuntimeConfig kTestRTConfigSmallHeap = RuntimeConfig::Builder().withGCConfig(kTestGCConfigSmall).build(); static const RuntimeConfig::Builder kTestRTConfigBuilder = RuntimeConfig::Builder().withGCConfig(kTestGCConfig); static const RuntimeConfig kTestRTConfig = RuntimeConfig::Builder(kTestRTConfigBuilder).build(); static const RuntimeConfig kTestRTConfigLargeHeap = RuntimeConfig::Builder().withGCConfig(kTestGCConfigLarge).build(); template <typename T> ::testing::AssertionResult isException( Runtime &runtime, const CallResult<T> &res) { return isException(runtime, res.getStatus()); } ::testing::AssertionResult isException( Runtime &runtime, ExecutionStatus status); /// A RuntimeTestFixture should be used by any test that requires a Runtime. /// For different heap sizes, use the different subclasses. class RuntimeTestFixtureBase : public ::testing::Test { std::shared_ptr<Runtime> rt; protected: // Convenience accessor that points to rt. Runtime &runtime; RuntimeConfig rtConfig; GCScope gcScope; Handle<Domain> domain; RuntimeTestFixtureBase(const RuntimeConfig &runtimeConfig) : rt(Runtime::create(runtimeConfig)), runtime(*rt), rtConfig(runtimeConfig), gcScope(runtime), domain(runtime.makeHandle(Domain::create(runtime))) {} /// Can't copy due to internal pointer. RuntimeTestFixtureBase(const RuntimeTestFixtureBase &) = delete; template <typename T> ::testing::AssertionResult isException(const CallResult<T> &res) { return isException(res.getStatus()); } ::testing::AssertionResult isException(ExecutionStatus status) { return ::hermes::vm::isException(runtime, status); } std::shared_ptr<Runtime> newRuntime() { return Runtime::create(rtConfig); } }; class RuntimeTestFixture : public RuntimeTestFixtureBase { public: RuntimeTestFixture() : RuntimeTestFixtureBase(kTestRTConfig) {} RuntimeTestFixture(experiments::VMExperimentFlags flags) : RuntimeTestFixtureBase(RuntimeConfig::Builder() .withGCConfig(kTestGCConfig) .withVMExperimentFlags(flags) .build()) {} }; class SmallHeapRuntimeTestFixture : public RuntimeTestFixtureBase { public: SmallHeapRuntimeTestFixture() : RuntimeTestFixtureBase(kTestRTConfigSmallHeap) {} }; class LargeHeapRuntimeTestFixture : public RuntimeTestFixtureBase { public: LargeHeapRuntimeTestFixture() : RuntimeTestFixtureBase(kTestRTConfigLargeHeap) {} }; /// Configuration for the GC which fixes a size -- \p sz -- for the heap, and /// does not permit any growth. Intended only for testing purposes where we /// don't expect or want the heap to grow. /// /// \p builder is an optional parameter representing an initial builder to set /// size parameters upon, providing an opportunity to set other parameters. inline const GCConfig TestGCConfigFixedSize( gcheapsize_t sz, GCConfig::Builder builder = kTestGCConfigBuilder) { return builder.withInitHeapSize(sz).withMaxHeapSize(sz).build(); } /// Assert that execution of `x' didn't throw. #define ASSERT_RETURNED(x) ASSERT_EQ(ExecutionStatus::RETURNED, x) /// Expect that 'x' is a string primitive with value 'str' #define EXPECT_STRINGPRIM(str, x) \ do { \ GCScopeMarkerRAII marker{runtime}; \ Handle<> xHandle{runtime, x}; \ Handle<StringPrimitive> strHandle = \ StringPrimitive::createNoThrow(runtime, str); \ EXPECT_TRUE( \ isSameValue(strHandle.getHermesValue(), xHandle.getHermesValue())); \ } while (0) /// Assert that execution of 'x' didn't throw and returned the expected bool. #define EXPECT_CALLRESULT_BOOL_RAW(B, x) \ do { \ auto res = x; \ ASSERT_RETURNED(res.getStatus()); \ EXPECT_##B(*res); \ } while (0) /// Assert that execution of 'x' didn't throw and returned the expected bool. #define EXPECT_CALLRESULT_BOOL(B, x) \ do { \ auto res = x; \ ASSERT_RETURNED(res.getStatus()); \ EXPECT_##B((*res)->getBool()); \ } while (0) /// Assert that execution of 'x' didn't throw and returned the expected /// CallResult. // Will replace "EXPECT_RETURN_STRING" after the entire refactor. #define EXPECT_CALLRESULT_STRING(str, x) \ do { \ auto res = x; \ ASSERT_RETURNED(res.getStatus()); \ EXPECT_STRINGPRIM(str, res->get()); \ } while (0) /// Assert that execution of 'x' didn't throw and returned undefined. #define EXPECT_CALLRESULT_UNDEFINED(x) \ do { \ auto res = x; \ ASSERT_RETURNED(res.getStatus()); \ EXPECT_TRUE((*res)->isUndefined()); \ } while (0) /// Assert that execution of 'x' didn't throw and returned the expected double. #define EXPECT_CALLRESULT_DOUBLE(d, x) \ do { \ auto res = x; \ ASSERT_RETURNED(res.getStatus()); \ EXPECT_EQ(d, (*res)->getDouble()); \ } while (0) /// Assert that execution of 'x' didn't throw and returned the expected double. #define EXPECT_CALLRESULT_VALUE(v, x) \ do { \ auto res = x; \ ASSERT_RETURNED(res.getStatus()); \ EXPECT_EQ(v, res->get()); \ } while (0) /// Some tests expect out of memory. This may either be fatal, or throw /// exception; parameterize tests over this choice. #ifdef HERMESVM_EXCEPTION_ON_OOM #define EXPECT_OOM(exp) \ { \ bool exThrown = false; \ try { \ exp; \ } catch (const JSOutOfMemoryError &x) { \ exThrown = true; \ } \ EXPECT_TRUE(exThrown); \ } #else #define EXPECT_OOM(exp) EXPECT_DEATH_IF_SUPPORTED({ exp; }, "OOM") #endif /// Get a named value from an object. #define GET_VALUE(objHandle, predefinedId) \ do { \ propRes = JSObject::getNamed_RJS( \ objHandle, \ runtime, \ Predefined::getSymbolID(Predefined::predefinedId)); \ ASSERT_RETURNED(propRes.getStatus()); \ } while (0) /// Get the global object. #define GET_GLOBAL(predefinedId) GET_VALUE(runtime.getGlobal(), predefinedId) inline HermesValue operator"" _hd(long double d) { return HermesValue::encodeDoubleValue(d); } /// A minimal Runtime for GC tests. class DummyRuntime final : public HandleRootOwner, public PointerBase, private GCBase::GCCallbacks { private: GCStorage gcStorage_; public: std::vector<WeakRoot<GCCell> *> weakRoots{}; /// Create a DummyRuntime with the default parameters. static std::shared_ptr<DummyRuntime> create(const GCConfig &gcConfig); /// Use a custom storage provider and/or a custom crash manager. /// \param provider A pointer to a StorageProvider. It *must* use /// StorageProvider::defaultProvider eventually or the test will fail. /// \param crashMgr static std::shared_ptr<DummyRuntime> create( const GCConfig &gcConfig, std::shared_ptr<StorageProvider> provider, std::shared_ptr<CrashManager> crashMgr = std::make_shared<NopCrashManager>()); /// Provide the correct storage provider based on build modes. /// All decorator StorageProviders must wrap the one returned from this /// function. static std::unique_ptr<StorageProvider> defaultProvider(); ~DummyRuntime(); template < typename T, HasFinalizer hasFinalizer = HasFinalizer::No, LongLived longLived = LongLived::No, class... Args> T *makeAFixed(Args &&...args) { return getHeap().makeAFixed<T, hasFinalizer, longLived>( std::forward<Args>(args)...); } template < typename T, HasFinalizer hasFinalizer = HasFinalizer::No, LongLived longLived = LongLived::No, class... Args> T *makeAVariable(uint32_t size, Args &&...args) { return getHeap().makeAVariable<T, hasFinalizer, longLived>( size, std::forward<Args>(args)...); } GC &getHeap() { return *gcStorage_.get(); } void collect(); void markRoots(RootAndSlotAcceptorWithNames &acceptor, bool) override; void markWeakRoots(WeakRootAcceptor &weakAcceptor, bool) override; void markRootsForCompleteMarking( RootAndSlotAcceptorWithNames &acceptor) override; unsigned int getSymbolsEnd() const override { return 0; } void unmarkSymbols() override {} void freeSymbols(const llvh::BitVector &) override {} #ifdef HERMES_SLOW_DEBUG bool isSymbolLive(SymbolID) override { return true; } const void *getStringForSymbol(SymbolID) override { return nullptr; } #endif void printRuntimeGCStats(JSONEmitter &) const override {} void visitIdentifiers( const std::function<void(SymbolID, const StringPrimitive *)> &) override { } std::string convertSymbolToUTF8(SymbolID) override; std::string getCallStackNoAlloc() override { return "<dummy runtime has no call stack>"; } void onGCEvent(GCEventKind, const std::string &) override {} /// It's a unit test, it doesn't care about reporting how much memory it uses. size_t mallocSize() const override { return 0; } const inst::Inst *getCurrentIPSlow() const override { return nullptr; } StackTracesTreeNode *getCurrentStackTracesTreeNode( const inst::Inst *ip) override { return nullptr; } StackTracesTree *getStackTracesTree() override { return nullptr; } private: DummyRuntime( const GCConfig &gcConfig, std::shared_ptr<StorageProvider> storageProvider, std::shared_ptr<CrashManager> crashMgr); }; /// A DummyRuntimeTestFixtureBase should be used by any test that requires a /// DummyRuntime. It takes a GCConfig, which can be /// used to specify heap size using the constants i.e kInitHeapSize. class DummyRuntimeTestFixtureBase : public ::testing::Test { std::shared_ptr<DummyRuntime> rt; protected: // Convenience accessor that points to rt. DummyRuntime &runtime; GCScope gcScope; DummyRuntimeTestFixtureBase(const GCConfig &gcConfig) : rt(DummyRuntime::create(gcConfig)), runtime(*rt), gcScope(runtime) {} /// Can't copy due to internal pointer. DummyRuntimeTestFixtureBase(const DummyRuntimeTestFixtureBase &) = delete; }; // Provide HermesValue & wrappers comparison operators for convenience. /// Compare two HermesValue for bit equality. inline bool operator==(HermesValue a, HermesValue b) { return a.getRaw() == b.getRaw(); } /// Helper function to create a CodeBlock that correspond to a single function /// generated from \p BFG. The generated code block will be part of the \p /// runtimeModule. inline CodeBlock *createCodeBlock( RuntimeModule *runtimeModule, Runtime &, hbc::BytecodeFunctionGenerator *BFG) { std::unique_ptr<hbc::BytecodeModule> BM(new hbc::BytecodeModule(1)); BM->setFunction( 0, BFG->generateBytecodeFunction( Function::DefinitionKind::ES5Function, ValueKind::FunctionKind, true, 0, 0)); runtimeModule->initializeWithoutCJSModulesMayAllocate( hbc::BCProviderFromSrc::createBCProviderFromSrc(std::move(BM))); return runtimeModule->getCodeBlockMayAllocate(0); } } // namespace vm } // namespace hermes #endif // HERMES_UNITTESTS_VMRUNTIME_TESTHELPERS_H