summaryrefslogtreecommitdiffstats
path: root/mozglue/tests/gtest/TestStackWalk.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--mozglue/tests/gtest/TestStackWalk.cpp279
1 files changed, 279 insertions, 0 deletions
diff --git a/mozglue/tests/gtest/TestStackWalk.cpp b/mozglue/tests/gtest/TestStackWalk.cpp
new file mode 100644
index 0000000000..cec21ffc5e
--- /dev/null
+++ b/mozglue/tests/gtest/TestStackWalk.cpp
@@ -0,0 +1,279 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// The header under test.
+#include "mozilla/StackWalk.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+
+#include <algorithm>
+
+#include "gtest/gtest.h"
+
+MOZ_EXPORT bool gStackWalkTesterDummy = true;
+
+struct StackWalkTester;
+
+// Descriptor of the recursive function calls wanted, and for each of them
+// whether to perform tail call optimization or not.
+struct CallInfo {
+ int (*mFunc)(int aDepth, int aLastSkipped, int aIgnored,
+ StackWalkTester& aTester);
+ bool mTailCall;
+
+ bool TailCall() {
+#if defined(__i386__) || defined(MOZ_CODE_COVERAGE)
+ // We can't make tail calls happen on i386 because all arguments to
+ // functions are on the stack, so the stack pointer needs to be updated
+ // before the call and restored after the call, so tail call optimization
+ // never happens.
+ // Similarly, code-coverage flags don't guarantee that tail call
+ // optimization will happen.
+ return false;
+#else
+ return mTailCall;
+#endif
+ }
+};
+
+struct PCRange {
+ void* mStart;
+ void* mEnd;
+};
+
+// PCRange pretty printer for gtest assertions.
+std::ostream& operator<<(std::ostream& aStream, const PCRange& aRange) {
+ aStream << aRange.mStart;
+ aStream << "-";
+ aStream << aRange.mEnd;
+ return aStream;
+}
+
+// Allow to use EXPECT_EQ with a vector of PCRanges and a vector of plain
+// addresses, allowing a more useful output when the test fails, showing
+// both lists.
+bool operator==(const std::vector<PCRange>& aRanges,
+ const std::vector<void*>& aPtrs) {
+ if (aRanges.size() != aPtrs.size()) {
+ return false;
+ }
+ for (size_t i = 0; i < aRanges.size(); i++) {
+ auto range = aRanges[i];
+ auto ptr = reinterpret_cast<uintptr_t>(aPtrs[i]);
+ if (ptr <= reinterpret_cast<uintptr_t>(range.mStart) ||
+ ptr >= reinterpret_cast<uintptr_t>(range.mEnd)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+struct StackWalkTester {
+ // Description of the recursion of functions to perform for the testcase.
+ std::vector<CallInfo> mFuncCalls;
+ // Collection of PCs reported by MozStackWalk.
+ std::vector<void*> mFramePCs;
+ // Collection of PCs expected per what was observed while recursing.
+ std::vector<PCRange> mExpectedFramePCs;
+ // The aFirstFramePC value that will be passed to MozStackWalk.
+ void* mFirstFramePC = nullptr;
+
+ // Callback to be given to the stack walker.
+ // aClosure should point at an instance of this class.
+ static void StackWalkCallback(uint32_t aFrameNumber, void* aPC, void* aSP,
+ void* aClosure) {
+ ASSERT_NE(aClosure, nullptr);
+ StackWalkTester& tester = *reinterpret_cast<StackWalkTester*>(aClosure);
+ tester.mFramePCs.push_back(aPC);
+ EXPECT_EQ(tester.mFramePCs.size(), size_t(aFrameNumber))
+ << "Frame number doesn't match";
+ }
+
+ // Callers of this function get a range of addresses with:
+ // ```
+ // label:
+ // recursion();
+ // AddExpectedPC(&&label);
+ // ```
+ // This intends to record the range from label to the return of AddExpectedPC.
+ // The ideal code would be:
+ // ```
+ // recursion();
+ // label:
+ // AddExpectedPC(&&label);
+ // ```
+ // and we wouldn't need to keep ranges. But while this works fine with Clang,
+ // GCC actually sometimes reorders code such the address received by
+ // AddExpectedPC is the address *before* the recursion.
+ // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=99784 Using a label before the
+ // recursion and CallerPC() from a function call after the recursion makes it
+ // less likely for things to go wrong.
+ MOZ_NEVER_INLINE void AddExpectedPC(void* aPC) {
+ mExpectedFramePCs.push_back({aPC, CallerPC()});
+ }
+
+ // Function intended to be called in sequence for recursion.
+ // CallInfo lists are meant to contain a sequence of IntermediateCallback<1>,
+ // IntermediateCallback<2>, etc.
+ // aDepth is a counter of how deep the recursion has gone so far;
+ // aLastSkipped is the depth of the last frame we want skipped in the
+ // testcase; aIgnored is there to avoid the compiler merging both recursive
+ // function calls, which would prevent tail call optimization happening on one
+ // of them. aTester is the instance of this class for the testcase.
+ template <int Id>
+ MOZ_NEVER_INLINE MOZ_EXPORT static int IntermediateCallback(
+ int aDepth, int aLastSkipped, int aIgnored, StackWalkTester& aTester) {
+ auto& callInfo = aTester.mFuncCalls.at(aDepth + 1);
+ if (aDepth == aLastSkipped) {
+ aTester.mFirstFramePC = CallerPC();
+ }
+ if (aTester.mFuncCalls.at(aDepth).TailCall()) {
+ return callInfo.mFunc(aDepth + 1, aLastSkipped, Id, aTester);
+ // Since we're doing a tail call, we're not expecting this frame appearing
+ // in the trace.
+ }
+ here:
+ callInfo.mFunc(aDepth + 1, aLastSkipped, Id + 1, aTester);
+ aTester.AddExpectedPC(&&here);
+ return 0;
+ }
+
+#if defined(__clang__)
+ __attribute__((no_sanitize("function")))
+#endif
+ MOZ_NEVER_INLINE MOZ_EXPORT static void
+ LeafCallback(int aDepth, int aLastSkipped, int aIgnored,
+ StackWalkTester& aTester) {
+ if (aDepth == aLastSkipped) {
+ aTester.mFirstFramePC = CallerPC();
+ }
+ if (aTester.mFuncCalls.at(aDepth).TailCall()) {
+ // For the same reason that we have the aIgnored argument on these
+ // callbacks, we need to avoid both MozStackWalk calls to be merged by the
+ // compiler, so we use different values of aMaxFrames for that.
+ return MozStackWalk(StackWalkTester::StackWalkCallback,
+ aTester.mFirstFramePC,
+ /*aMaxFrames*/ 19, &aTester);
+ // Since we're doing a tail call, we're not expecting this frame appearing
+ // in the trace.
+ }
+ here:
+ MozStackWalk(StackWalkTester::StackWalkCallback, aTester.mFirstFramePC,
+ /*aMaxFrames*/ 20, &aTester);
+ aTester.AddExpectedPC(&&here);
+ // Because we return nothing from this function, simply returning here would
+ // produce a tail-call optimization, which we explicitly don't want to
+ // happen. So we add a branch that depends on an extern value to prevent
+ // that from happening.
+ MOZ_RELEASE_ASSERT(gStackWalkTesterDummy);
+ }
+
+ explicit StackWalkTester(std::initializer_list<CallInfo> aFuncCalls)
+ : mFuncCalls(aFuncCalls) {}
+
+ // Dump a vector of PCRanges as WalkTheStack would, for test failure output.
+ // Only the end of the range is shown. Not ideal, but
+ // MozFormatCodeAddressDetails only knows to deal with one address at a time.
+ // The full ranges would be printed by EXPECT_EQ anyways.
+ static std::string DumpFrames(std::vector<PCRange>& aFramePCRanges) {
+ std::vector<void*> framePCs;
+ framePCs.reserve(aFramePCRanges.size());
+ for (auto range : aFramePCRanges) {
+ framePCs.push_back(range.mEnd);
+ }
+ return DumpFrames(framePCs);
+ }
+
+ // Dump a vector of addresses as WalkTheStack would, for test failure output.
+ static std::string DumpFrames(std::vector<void*>& aFramePCs) {
+ size_t n = 0;
+ std::string result;
+ for (auto* framePC : aFramePCs) {
+ char buf[1024];
+ MozCodeAddressDetails details;
+ result.append(" ");
+ n++;
+ if (MozDescribeCodeAddress(framePC, &details)) {
+ int length =
+ MozFormatCodeAddressDetails(buf, sizeof(buf), n, framePC, &details);
+ result.append(buf, std::min(length, (int)sizeof(buf) - 1));
+ } else {
+ result.append("MozDescribeCodeAddress failed");
+ }
+ result.append("\n");
+ }
+ return result;
+ }
+
+ // Dump a description of the given test case.
+ static std::string DumpFuncCalls(std::vector<CallInfo>& aFuncCalls) {
+ std::string result;
+ for (auto funcCall : aFuncCalls) {
+ MozCodeAddressDetails details;
+ result.append(" ");
+ if (MozDescribeCodeAddress(reinterpret_cast<void*>(funcCall.mFunc),
+ &details)) {
+ result.append(details.function);
+ if (funcCall.TailCall()) {
+ result.append(" tail call");
+ }
+ } else {
+ result.append("MozDescribeCodeAddress failed");
+ }
+ result.append("\n");
+ }
+ return result;
+ }
+
+ MOZ_EXPORT MOZ_NEVER_INLINE void RunTest(int aLastSkipped) {
+ ASSERT_TRUE(aLastSkipped < (int)mFuncCalls.size());
+ mFramePCs.clear();
+ mExpectedFramePCs.clear();
+ mFirstFramePC = nullptr;
+ auto& callInfo = mFuncCalls.at(0);
+ here:
+ callInfo.mFunc(0, aLastSkipped, 0, *this);
+ AddExpectedPC(&&here);
+ if (aLastSkipped < 0) {
+ aLastSkipped = mFuncCalls.size();
+ }
+ for (int i = (int)mFuncCalls.size() - 1; i >= aLastSkipped; i--) {
+ if (!mFuncCalls.at(i).TailCall()) {
+ mExpectedFramePCs.erase(mExpectedFramePCs.begin());
+ }
+ }
+ mFramePCs.resize(std::min(mExpectedFramePCs.size(), mFramePCs.size()));
+ EXPECT_EQ(mExpectedFramePCs, mFramePCs)
+ << "Expected frames:\n"
+ << DumpFrames(mExpectedFramePCs) << "Found frames:\n"
+ << DumpFrames(mFramePCs)
+ << "Function calls data (last skipped: " << aLastSkipped << "):\n"
+ << DumpFuncCalls(mFuncCalls);
+ }
+};
+
+TEST(TestStackWalk, StackWalk)
+{
+ const auto foo = StackWalkTester::IntermediateCallback<1>;
+ const auto bar = StackWalkTester::IntermediateCallback<2>;
+ const auto qux = StackWalkTester::IntermediateCallback<3>;
+ const auto leaf = reinterpret_cast<int (*)(int, int, int, StackWalkTester&)>(
+ StackWalkTester::LeafCallback);
+
+ const std::initializer_list<CallInfo> tests[] = {
+ {{foo, false}, {bar, false}, {qux, false}, {leaf, false}},
+ {{foo, false}, {bar, true}, {qux, false}, {leaf, false}},
+ {{foo, false}, {bar, false}, {qux, false}, {leaf, true}},
+ {{foo, true}, {bar, false}, {qux, true}, {leaf, true}},
+ };
+ for (auto test : tests) {
+ StackWalkTester tester(test);
+ for (int i = -1; i < (int)test.size(); i++) {
+ tester.RunTest(i);
+ }
+ }
+}