unittests/VMRuntime/CodeCoverageProfilerTest.cpp (174 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 "TestHelpers.h"
#include "gtest/gtest.h"
#include "hermes/VM/Profiler/CodeCoverageProfiler.h"
#include "llvh/ADT/StringRef.h"
#include "llvh/Support/raw_ostream.h"
#include <algorithm>
#include <future>
#include <unordered_map>
using namespace hermes::vm;
namespace hermes {
namespace unittest {
namespace CodeCoverageTest {
class CodeCoverageProfilerTest : public RuntimeTestFixture {
public:
CodeCoverageProfilerTest() {
CodeCoverageProfiler::enableGlobal();
}
protected:
static CodeCoverageProfiler::FuncInfo getFuncInfo(Handle<JSFunction> func) {
auto bcProvider = func->getCodeBlock()->getRuntimeModule()->getBytecode();
auto functionId = func->getCodeBlock()->getFunctionID();
auto debugInfo = bcProvider->getDebugInfo();
auto debugOffsets = bcProvider->getDebugOffsets(functionId);
if (debugInfo && debugOffsets &&
debugOffsets->sourceLocations != hbc::DebugOffsets::NO_OFFSET) {
if (auto pos = debugInfo->getLocationForAddress(
debugOffsets->sourceLocations, 0 /* opcodeOffset */)) {
auto file = debugInfo->getFilenameByID(pos->filenameId);
auto line = pos->line - 1; // Normalised to zero-based
auto column = pos->column - 1; // Normalised to zero-based
return {line, column, file};
}
}
const uint32_t segmentID = bcProvider->getSegmentID();
const uint32_t funcVirtualOffset =
bcProvider->getVirtualOffsetForFunction(functionId);
const std::string sourceURL = func->getRuntimeModule()->getSourceURL();
return {segmentID, funcVirtualOffset, sourceURL};
}
// Check if the function is executed or not.
static bool isFuncExecuted(
const std::vector<CodeCoverageProfiler::FuncInfo> &executedFuncInfos,
Handle<JSFunction> checkFunc) {
CodeCoverageProfiler::FuncInfo checkFuncInfo = getFuncInfo(checkFunc);
return std::find(
executedFuncInfos.begin(),
executedFuncInfos.end(),
checkFuncInfo) != executedFuncInfos.end();
}
// Whether \p func1 and \p func2 have different FuncInfo
// or not.
static ::testing::AssertionResult hasDifferentInfo(
Handle<JSFunction> func1,
Handle<JSFunction> func2) {
if (getFuncInfo(func1) == getFuncInfo(func2)) {
return ::testing::AssertionFailure()
<< "func1 and func2 has same func info.";
} else {
return ::testing::AssertionSuccess();
}
}
};
TEST_F(CodeCoverageProfilerTest, BasicFunctionUsedUnused) {
hbc::CompileFlags flags;
flags.lazy = false;
CallResult<HermesValue> res = runtime.run(
"function used() {}; function unused() {}; used(); [used, unused];",
"file:///fake.js",
flags);
EXPECT_FALSE(isException(res));
std::unordered_map<std::string, std::vector<CodeCoverageProfiler::FuncInfo>>
executedFuncInfos = CodeCoverageProfiler::getExecutedFunctions();
std::vector<CodeCoverageProfiler::FuncInfo> testRuntimeExecutedFuncInfos =
executedFuncInfos.find(runtime.getHeap().getName())->second;
Handle<JSArray> funcArr = runtime.makeHandle(vmcast<JSArray>(*res));
Handle<JSFunction> funcUsed =
runtime.makeHandle(vmcast<JSFunction>(funcArr->at(runtime, 0)));
Handle<JSFunction> funcUnused =
runtime.makeHandle(vmcast<JSFunction>(funcArr->at(runtime, 1)));
// Used and unused functions should have different info.
EXPECT_TRUE(hasDifferentInfo(funcUsed, funcUnused));
// Global + used.
EXPECT_EQ(testRuntimeExecutedFuncInfos.size(), 2);
EXPECT_TRUE(isFuncExecuted(testRuntimeExecutedFuncInfos, funcUsed));
EXPECT_FALSE(isFuncExecuted(testRuntimeExecutedFuncInfos, funcUnused));
}
// Right now, this just tests that we can simultaneously run two code coverage
// profilers.
TEST_F(CodeCoverageProfilerTest, BasicFunctionUsedUnusedTwoRuntimes) {
auto runtime2 = newRuntime();
GCScope scope{*runtime2};
std::vector<Runtime *> runtimes = {&runtime, runtime2.get()};
std::vector<std::future<PinnedHermesValue>> resFuts;
for (auto *rt : runtimes) {
resFuts.push_back(
std::async(std::launch::async, [this, rt]() -> PinnedHermesValue {
hbc::CompileFlags flags;
flags.lazy = false;
CallResult<HermesValue> res = rt->run(
"function used() {}; function unused() {}; used(); [used, unused];",
"file:///fake.js",
flags);
EXPECT_FALSE(isException(res));
return *res;
}));
}
for (size_t i = 0; i < runtimes.size(); i++) {
Runtime &rt = *runtimes[i];
HermesValue res = resFuts[i].get();
std::vector<CodeCoverageProfiler::FuncInfo> executedFuncInfos =
rt.getCodeCoverageProfiler().getExecutedFunctionsLocal();
Handle<JSArray> funcArr = rt.makeHandle(vmcast<JSArray>(res));
Handle<JSFunction> funcUsed =
rt.makeHandle(vmcast<JSFunction>(funcArr->at(rt, 0)));
Handle<JSFunction> funcUnused =
rt.makeHandle(vmcast<JSFunction>(funcArr->at(rt, 1)));
// Used and unused functions should have different info.
EXPECT_TRUE(hasDifferentInfo(funcUsed, funcUnused));
// Global + used.
EXPECT_EQ(executedFuncInfos.size(), 2);
EXPECT_TRUE(isFuncExecuted(executedFuncInfos, funcUsed));
EXPECT_FALSE(isFuncExecuted(executedFuncInfos, funcUnused));
}
}
TEST_F(CodeCoverageProfilerTest, FunctionsFromMultipleModules) {
hbc::CompileFlags flags;
flags.lazy = false;
CallResult<HermesValue> res1 =
runtime.run("function foo() {}; foo(); foo;", "file:///fake1.js", flags);
EXPECT_FALSE(isException(res1));
Handle<JSFunction> funcFoo = runtime.makeHandle(vmcast<JSFunction>(*res1));
CallResult<HermesValue> res2 = runtime.run(
"\n function bar() {}; function bar() {}; function unused() {}; bar(); [bar, unused];",
"file:///fake2.js",
flags);
EXPECT_FALSE(isException(res2));
std::unordered_map<std::string, std::vector<CodeCoverageProfiler::FuncInfo>>
executedFuncInfos = CodeCoverageProfiler::getExecutedFunctions();
std::vector<CodeCoverageProfiler::FuncInfo> testRuntimeExecutedFuncInfos =
executedFuncInfos.find(runtime.getHeap().getName())->second;
Handle<JSArray> funcArr = runtime.makeHandle(vmcast<JSArray>(*res2));
Handle<JSFunction> funcBar =
runtime.makeHandle(vmcast<JSFunction>(funcArr->at(runtime, 0)));
Handle<JSFunction> funcUnused =
runtime.makeHandle(vmcast<JSFunction>(funcArr->at(runtime, 1)));
// Used and unused functions should have different info.
EXPECT_TRUE(hasDifferentInfo(funcFoo, funcUnused));
EXPECT_TRUE(hasDifferentInfo(funcBar, funcUnused));
EXPECT_EQ(testRuntimeExecutedFuncInfos.size(), 4);
EXPECT_TRUE(isFuncExecuted(testRuntimeExecutedFuncInfos, funcFoo));
EXPECT_TRUE(isFuncExecuted(testRuntimeExecutedFuncInfos, funcBar));
EXPECT_FALSE(isFuncExecuted(testRuntimeExecutedFuncInfos, funcUnused));
}
TEST_F(CodeCoverageProfilerTest, FunctionsFromMultipleDomains) {
hbc::CompileFlags flags;
flags.lazy = false;
CallResult<HermesValue> res = runtime.run(
"var eval1 = eval('const a = 1; function used1() {}; used1(); used1; //# sourceURL=foo1.js'); "
"var eval2 = eval('const b = 1; function unused() {}; function used2() {}; used2(); [used2, unused] //# sourceURL=foo2.js');"
"[eval1, eval2[0], eval2[1]];",
"file:///fake.js",
flags);
EXPECT_FALSE(isException(res));
std::unordered_map<std::string, std::vector<CodeCoverageProfiler::FuncInfo>>
executedFuncInfos = CodeCoverageProfiler::getExecutedFunctions();
std::vector<CodeCoverageProfiler::FuncInfo> testRuntimeExecutedFuncInfos =
executedFuncInfos.find(runtime.getHeap().getName())->second;
Handle<JSArray> funcArr = runtime.makeHandle(vmcast<JSArray>(*res));
Handle<JSFunction> funcUsed1 =
runtime.makeHandle(vmcast<JSFunction>(funcArr->at(runtime, 0)));
Handle<JSFunction> funcUsed2 =
runtime.makeHandle(vmcast<JSFunction>(funcArr->at(runtime, 1)));
Handle<JSFunction> funcUnused =
runtime.makeHandle(vmcast<JSFunction>(funcArr->at(runtime, 2)));
// Used and unused functions should have different info.
EXPECT_TRUE(hasDifferentInfo(funcUsed1, funcUnused));
EXPECT_TRUE(hasDifferentInfo(funcUsed2, funcUnused));
// Global + two eval() code + used1 + used2.
EXPECT_EQ(testRuntimeExecutedFuncInfos.size(), 5);
EXPECT_TRUE(isFuncExecuted(testRuntimeExecutedFuncInfos, funcUsed1));
EXPECT_TRUE(isFuncExecuted(testRuntimeExecutedFuncInfos, funcUsed2));
EXPECT_FALSE(isFuncExecuted(testRuntimeExecutedFuncInfos, funcUnused));
}
} // namespace CodeCoverageTest
} // namespace unittest
} // namespace hermes