blob: cd0b53597635aa0df16809474a0e23a14fce5b3b [file] [log] [blame]
// Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors
// Licensed under the MIT License:
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#include "exception.h"
#include "debug.h"
#include <kj/compat/gtest.h>
#include <stdexcept>
#include <stdint.h>
namespace kj {
namespace _ { // private
namespace {
TEST(Exception, TrimSourceFilename) {
#if _WIN32
EXPECT_TRUE(trimSourceFilename(__FILE__) == "kj/exception-test.c++" ||
trimSourceFilename(__FILE__) == "kj\\exception-test.c++");
#else
EXPECT_EQ(trimSourceFilename(__FILE__), "kj/exception-test.c++");
#endif
}
TEST(Exception, RunCatchingExceptions) {
bool recovered = false;
Maybe<Exception> e = kj::runCatchingExceptions([&]() {
KJ_FAIL_ASSERT("foo") {
break;
}
recovered = true;
});
#if KJ_NO_EXCEPTIONS
EXPECT_TRUE(recovered);
#else
EXPECT_FALSE(recovered);
#endif
KJ_IF_MAYBE(ex, e) {
EXPECT_EQ("foo", ex->getDescription());
} else {
ADD_FAILURE() << "Expected exception";
}
}
#if !KJ_NO_EXCEPTIONS
TEST(Exception, RunCatchingExceptionsStdException) {
Maybe<Exception> e = kj::runCatchingExceptions([&]() {
throw std::logic_error("foo");
});
KJ_IF_MAYBE(ex, e) {
EXPECT_EQ("std::exception: foo", ex->getDescription());
} else {
ADD_FAILURE() << "Expected exception";
}
}
TEST(Exception, RunCatchingExceptionsOtherException) {
Maybe<Exception> e = kj::runCatchingExceptions([&]() {
throw 123;
});
KJ_IF_MAYBE(ex, e) {
#if __GNUC__ && !KJ_NO_RTTI
EXPECT_EQ("unknown non-KJ exception of type: int", ex->getDescription());
#else
EXPECT_EQ("unknown non-KJ exception", ex->getDescription());
#endif
} else {
ADD_FAILURE() << "Expected exception";
}
}
#endif
#if !KJ_NO_EXCEPTIONS
// We skip this test when exceptions are disabled because making it no-exceptions-safe defeats
// the purpose of the test: recoverable exceptions won't throw inside a destructor in the first
// place.
class ThrowingDestructor: public UnwindDetector {
public:
~ThrowingDestructor() noexcept(false) {
catchExceptionsIfUnwinding([]() {
KJ_FAIL_ASSERT("this is a test, not a real bug");
});
}
};
TEST(Exception, UnwindDetector) {
// If no other exception is happening, ThrowingDestructor's destructor throws one.
Maybe<Exception> e = kj::runCatchingExceptions([&]() {
ThrowingDestructor t;
});
KJ_IF_MAYBE(ex, e) {
EXPECT_EQ("this is a test, not a real bug", ex->getDescription());
} else {
ADD_FAILURE() << "Expected exception";
}
// If another exception is happening, ThrowingDestructor's destructor's exception is squelched.
e = kj::runCatchingExceptions([&]() {
ThrowingDestructor t;
KJ_FAIL_ASSERT("baz") {
break;
}
});
KJ_IF_MAYBE(ex, e) {
EXPECT_EQ("baz", ex->getDescription());
} else {
ADD_FAILURE() << "Expected exception";
}
}
#endif
#if !__MINGW32__ // Inexplicably crashes when exception is thrown from constructor.
TEST(Exception, ExceptionCallbackMustBeOnStack) {
KJ_EXPECT_THROW_MESSAGE("must be allocated on the stack", new ExceptionCallback);
}
#endif // !__MINGW32__
#if !KJ_NO_EXCEPTIONS
TEST(Exception, ScopeSuccessFail) {
bool success = false;
bool failure = false;
{
KJ_ON_SCOPE_SUCCESS(success = true);
KJ_ON_SCOPE_FAILURE(failure = true);
EXPECT_FALSE(success);
EXPECT_FALSE(failure);
}
EXPECT_TRUE(success);
EXPECT_FALSE(failure);
success = false;
failure = false;
try {
KJ_ON_SCOPE_SUCCESS(success = true);
KJ_ON_SCOPE_FAILURE(failure = true);
EXPECT_FALSE(success);
EXPECT_FALSE(failure);
throw 1;
} catch (int) {}
EXPECT_FALSE(success);
EXPECT_TRUE(failure);
}
#endif
#if __GNUG__ || defined(__clang__)
kj::String testStackTrace() __attribute__((noinline));
#elif _MSC_VER
__declspec(noinline) kj::String testStackTrace();
#endif
kj::String testStackTrace() {
// getStackTrace() normally skips its immediate caller, so we wrap it in another layer.
return getStackTrace();
}
KJ_TEST("getStackTrace() returns correct line number, not line + 1") {
// Backtraces normally produce the return address of each stack frame, but that's usually the
// address immediately after the one that made the call. As a result, it used to be that stack
// traces often pointed to the line after the one that made a call, which was confusing. This
// checks that this bug is fixed.
//
// This is not a very robust test, because:
// 1) Since symbolic stack traces are not available in many situations (e.g. release builds
// lacking debug symbols, systems where addr2line isn't present, etc.), we only check that
// the stack trace does *not* contain the *wrong* value, rather than checking that it does
// contain the right one.
// 2) This test only detects the problem if the call instruction to testStackTrace() is the
// *last* instruction attributed to its line of code. Whether or not this is true seems to be
// dependent on obscure compiler behavior. For example, below, it could only be the case if
// RVO is applied -- but in my testing, RVO does seem to be applied here. I tried several
// variations involving passing via an output parameter or a global variable rather than
// returning, but found some variations detected the problem and others didn't, essentially
// at random.
auto trace = testStackTrace();
auto wrong = kj::str("exception-test.c++:", __LINE__);
KJ_ASSERT(strstr(trace.cStr(), wrong.cStr()) == nullptr, trace, wrong);
}
#if !KJ_NO_EXCEPTIONS
KJ_TEST("InFlightExceptionIterator works") {
bool caught = false;
try {
KJ_DEFER({
try {
KJ_FAIL_ASSERT("bar");
} catch (const kj::Exception& e) {
InFlightExceptionIterator iter;
KJ_IF_MAYBE(e2, iter.next()) {
KJ_EXPECT(e2 == &e, e2->getDescription());
} else {
KJ_FAIL_EXPECT("missing first exception");
}
KJ_IF_MAYBE(e2, iter.next()) {
KJ_EXPECT(e2->getDescription() == "foo", e2->getDescription());
} else {
KJ_FAIL_EXPECT("missing second exception");
}
KJ_EXPECT(iter.next() == nullptr, "more than two exceptions");
caught = true;
}
});
KJ_FAIL_ASSERT("foo");
} catch (const kj::Exception& e) {
// expected
}
KJ_EXPECT(caught);
}
#endif
KJ_TEST("computeRelativeTrace") {
auto testCase = [](uint expectedPrefix,
ArrayPtr<const uintptr_t> trace, ArrayPtr<const uintptr_t> relativeTo) {
auto tracePtr = KJ_MAP(x, trace) { return (void*)x; };
auto relativeToPtr = KJ_MAP(x, relativeTo) { return (void*)x; };
auto result = computeRelativeTrace(tracePtr, relativeToPtr);
KJ_EXPECT(result.begin() == tracePtr.begin());
KJ_EXPECT(result.size() == expectedPrefix, trace, relativeTo, result);
};
testCase(8,
{1, 2, 3, 4, 5, 6, 7, 8},
{8, 7, 6, 5, 4, 3, 2, 1});
testCase(5,
{1, 2, 3, 4, 5, 6, 7, 8},
{8, 7, 6, 5, 5, 6, 7, 8});
testCase(5,
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
{8, 7, 6, 5, 5, 6, 7, 8});
testCase(5,
{1, 2, 3, 4, 5, 6, 7, 8, 6, 7, 8},
{8, 7, 6, 5, 5, 6, 7, 8});
testCase(9,
{1, 2, 3, 4, 5, 6, 7, 8, 5, 5, 6, 7, 8},
{8, 7, 6, 5, 5, 6, 7, 8});
testCase(5,
{1, 2, 3, 4, 5, 5, 6, 7, 8, 5, 6, 7, 8},
{8, 7, 6, 5, 5, 6, 7, 8});
testCase(5,
{1, 2, 3, 4, 5, 6, 7, 8},
{8, 7, 6, 5, 5, 6, 7, 8, 7, 8});
testCase(5,
{1, 2, 3, 4, 5, 6, 7, 8},
{8, 7, 6, 5, 6, 7, 8, 7, 8});
}
} // namespace
} // namespace _ (private)
} // namespace kj