Files
sdk/runtime/platform/assert.h
T
Tess Strickland 73a2c6c9a7 [vm] Fix EXPECT_STREQ output when the common prefix and suffix overlap.
Currently, the common prefix and suffix can end up being larger than one
of the two strings, if the other string has extra content that matches
the portion in the overlap. For example, in JSON output, there might be
a missing } in a run of consecutive }s when closing JSON outputs. (Or,
more likely thanks to our infrastructure for generating JSON outputs,
that the expected string has an extra }.) In this case, the returned
mismatches are empty, making it hard to discern what went wrong.

This CL fixes that by limiting the common suffix to be no larger than
the portion of the smaller string after the common prefix.

Also add handling in the case where the two strings are equal (here,
returning the escaped string in the prefix and leaving the other outputs
empty), though this shouldn't happen since this function is only called
when the null-terminated strings are not equal.

TEST=ci (manual testing on while working on CL 450381)

Change-Id: I52140f32bd44d7e31cf6ba97d862a89ac0568f02
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/499221
Reviewed-by: Alexander Markov <alexmarkov@google.com>
Commit-Queue: Tess Strickland <sstrickl@google.com>
2026-04-30 02:34:29 -07:00

489 lines
18 KiB
C++

// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#ifndef RUNTIME_PLATFORM_ASSERT_H_
#define RUNTIME_PLATFORM_ASSERT_H_
#include "platform/globals.h"
#if !defined(DEBUG) && !defined(NDEBUG)
#error neither DEBUG nor NDEBUG defined
#elif defined(DEBUG) && defined(NDEBUG)
#error both DEBUG and NDEBUG defined
#endif
// TODO(5411406): include sstream for now, once we have a Utils::toString()
// implemented for all the primitive types we can replace the usage of
// sstream by Utils::toString()
#if defined(DEBUG) || defined(TESTING)
#include <string.h>
#include <sstream>
#include <string>
#endif
namespace dart {
class DynamicAssertionHelper {
public:
DynamicAssertionHelper(const char* file, int line)
: file_(file), line_(line) {}
protected:
void Print(const char* format,
va_list arguments,
bool will_abort = false) const;
const char* const file_;
const int line_;
DISALLOW_IMPLICIT_CONSTRUCTORS(DynamicAssertionHelper);
};
class Assert : public DynamicAssertionHelper {
public:
Assert(const char* file, int line) : DynamicAssertionHelper(file, line) {}
DART_NORETURN void Fail(const char* format, ...) const PRINTF_ATTRIBUTE(2, 3);
template <typename T>
T NotNull(const T p);
};
class Expect : public DynamicAssertionHelper {
public:
Expect(const char* file, int line) : DynamicAssertionHelper(file, line) {}
void Fail(const char* format, ...) const PRINTF_ATTRIBUTE(2, 3);
#if defined(TESTING)
template <typename E, typename A>
void Equals(const E& expected, const A& actual);
template <typename E, typename A>
void NotEquals(const E& not_expected, const A& actual);
template <typename E, typename A>
void BitEquals(const E& expected, const A& actual);
template <typename E, typename A, typename T>
void FloatEquals(const E& expected, const A& actual, const T& tol);
void StringEquals(const char* expected, const char* actual);
void IsSubstring(const char* needle, const char* haystack);
void IsNotSubstring(const char* needle, const char* haystack);
template <typename E, typename A>
void LessThan(const E& left, const A& right);
template <typename E, typename A>
void LessEqual(const E& left, const A& right);
template <typename E, typename A>
void GreaterThan(const E& left, const A& right);
template <typename E, typename A>
void GreaterEqual(const E& left, const A& right);
template <typename T>
void NotNull(const T p);
template <typename T>
void Null(const T p);
#endif
static bool failed() { return failed_; }
private:
static bool failed_;
};
template <typename T>
T Assert::NotNull(const T p) {
if (p != nullptr) return p;
Fail("expected: not nullptr, found nullptr");
return nullptr;
}
#if defined(TESTING)
// Only allow the expensive (with respect to code size) assertions
// in testing code.
template <typename E, typename A>
void Expect::Equals(const E& expected, const A& actual) {
if (actual == expected) return;
std::ostringstream ess, ass;
ess << expected;
ass << actual;
std::string es = ess.str(), as = ass.str();
Fail("expected: <%s> but was: <%s>", es.c_str(), as.c_str());
}
template <typename E, typename A>
void Expect::NotEquals(const E& not_expected, const A& actual) {
if (actual != not_expected) return;
std::ostringstream ness;
ness << not_expected;
std::string nes = ness.str();
Fail("did not expect: <%s>", nes.c_str());
}
template <typename E, typename A>
void Expect::BitEquals(const E& expected, const A& actual) {
static_assert(sizeof(E) == sizeof(A));
if (memcmp(&expected, &actual, sizeof(E)) == 0) return;
std::ostringstream ess, ass;
ess << expected;
ass << actual;
std::string es = ess.str(), as = ass.str();
Fail("expected: <%s> but was: <%s>", es.c_str(), as.c_str());
}
template <typename E, typename A, typename T>
void Expect::FloatEquals(const E& expected, const A& actual, const T& tol) {
if (((expected - tol) <= actual) && (actual <= (expected + tol))) {
return;
}
std::ostringstream ess, ass, tolss;
ess << expected;
ass << actual;
tolss << tol;
std::string es = ess.str(), as = ass.str(), tols = tolss.str();
Fail("expected: <%s> but was: <%s> (tolerance: <%s>)", es.c_str(), as.c_str(),
tols.c_str());
}
static void Escape(std::string& dst, const char* src, int len) {
if (len == 0) return;
const char* const end = src + len;
for (char c = *src; src < end; src++, c = *src) {
if (c == '\n') {
dst += "\\n\"\n\"";
} else if (c == '\'') {
dst += "\\\'";
} else if (c == '\"') {
dst += "\\\"";
} else if (c == '\\') {
dst += "\\\\";
} else if (c == '\0') {
dst += "\\0";
} else {
dst += c;
}
}
}
// Splits two null-terminated strings into a common prefix and suffix
// and the mismatched parts of the two strings, escaping the outputs.
//
// If the two strings are the same, then an escaped version of
// the string is in the prefix and the other outputs are empty.
//
// If there is no common prefix and suffix or the size of the common
// prefix and suffix combined is small in comparison to the size
// of the two strings, then the prefix and suffix are empty to limit
// the noise in the output of Expect::StringEquals.
static void FindCommonPrefixAndSuffix(const char* expected,
const char* actual,
std::string& prefix,
std::string& expected_mismatch,
std::string& actual_mismatch,
std::string& suffix) {
const int expected_len = strlen(expected);
const int actual_len = strlen(actual);
int prefix_length = 0;
while (expected[prefix_length] == actual[prefix_length]) {
// Either the prefix or actual string is a prefix of the other.
if (expected[prefix_length] == '\0' || actual[prefix_length] == '\0') {
break;
}
prefix_length += 1;
}
if (prefix_length == expected_len && prefix_length == actual_len) {
// The two are equal, so just escape the whole string into the prefix.
Escape(prefix, expected, prefix_length);
return;
}
int suffix_length = 0;
while (expected[(expected_len - 1) - suffix_length] ==
actual[(actual_len - 1) - suffix_length]) {
if (prefix_length + suffix_length == expected_len ||
prefix_length + suffix_length == actual_len) {
// The prefix and suffix cover the entirety of one of the
// two strings, meaning the actual string either has missing
// or extra contents compared to the expected string.
break;
}
suffix_length += 1;
}
// Set a minimum difference required to output a non-empty prefix and suffix,
// so that they are not output if the common prefix and suffix of the expected
// and actual strings is small.
constexpr int kMinDifference = 10;
if (prefix_length + suffix_length < kMinDifference) {
prefix_length = 0;
suffix_length = 0;
}
Escape(prefix, expected, prefix_length);
Escape(expected_mismatch, expected + prefix_length,
expected_len - prefix_length - suffix_length);
Escape(actual_mismatch, actual + prefix_length,
actual_len - prefix_length - suffix_length);
Escape(suffix, expected + expected_len - suffix_length, suffix_length);
}
inline void Expect::StringEquals(const char* expected, const char* actual) {
if (actual == nullptr) {
Fail("expected:\n<\"%s\">\nbut was nullptr", expected);
} else {
if (strcmp(expected, actual) == 0) return;
std::string prefix, expected_mismatch, actual_mismatch, suffix;
FindCommonPrefixAndSuffix(expected, actual, prefix, expected_mismatch,
actual_mismatch, suffix);
// Only print the prefix and/or suffix if non-empty.
if (prefix.empty() && suffix.empty()) {
Fail("expected:\n<\"%s\">\nbut was:\n<\"%s\">", expected_mismatch.c_str(),
actual_mismatch.c_str());
} else if (prefix.empty()) {
Fail(
"substring mismatch:\nexpected:\n <\"%s\">\nbut was:\n "
"<\"%s\">\nsuffix:\n <\"%s\">",
expected_mismatch.c_str(), actual_mismatch.c_str(), suffix.c_str());
} else if (suffix.empty()) {
Fail("prefix:\n <\"%s\">\nexpected:\n <\"%s\">\nbut was:\n <\"%s\">",
prefix.c_str(), expected_mismatch.c_str(), actual_mismatch.c_str());
} else {
Fail(
"prefix:\n <\"%s\">\nexpected:\n <\"%s\">\nbut was:\n "
"<\"%s\">\nsuffix:\n <\"%s\">",
prefix.c_str(), expected_mismatch.c_str(), actual_mismatch.c_str(),
suffix.c_str());
}
}
}
inline void Expect::IsSubstring(const char* needle, const char* haystack) {
if (strstr(haystack, needle) != nullptr) return;
Fail("expected <\"%s\"> to be a substring of <\"%s\">", needle, haystack);
}
inline void Expect::IsNotSubstring(const char* needle, const char* haystack) {
if (strstr(haystack, needle) == nullptr) return;
Fail("expected <\"%s\"> to not be a substring of <\"%s\">", needle, haystack);
}
template <typename E, typename A>
void Expect::LessThan(const E& left, const A& right) {
if (left < right) return;
std::ostringstream ess, ass;
ess << left;
ass << right;
std::string es = ess.str(), as = ass.str();
Fail("expected: %s < %s", es.c_str(), as.c_str());
}
template <typename E, typename A>
void Expect::LessEqual(const E& left, const A& right) {
if (left <= right) return;
std::ostringstream ess, ass;
ess << left;
ass << right;
std::string es = ess.str(), as = ass.str();
Fail("expected: %s <= %s", es.c_str(), as.c_str());
}
template <typename E, typename A>
void Expect::GreaterThan(const E& left, const A& right) {
if (left > right) return;
std::ostringstream ess, ass;
ess << left;
ass << right;
std::string es = ess.str(), as = ass.str();
Fail("expected: %s > %s", es.c_str(), as.c_str());
}
template <typename E, typename A>
void Expect::GreaterEqual(const E& left, const A& right) {
if (left >= right) return;
std::ostringstream ess, ass;
ess << left;
ass << right;
std::string es = ess.str(), as = ass.str();
Fail("expected: %s >= %s", es.c_str(), as.c_str());
}
template <typename T>
void Expect::NotNull(const T p) {
if (p != nullptr) return;
Fail("expected: not nullptr, found nullptr");
}
template <typename T>
void Expect::Null(const T p) {
if (p == nullptr) return;
Fail("expected: nullptr, found not null pointer");
}
#endif
} // namespace dart
#define FATAL(format, ...) \
dart::Assert(__FILE__, __LINE__).Fail(format, ##__VA_ARGS__);
#define UNIMPLEMENTED() FATAL("unimplemented code")
#define UNREACHABLE() FATAL("unreachable code")
#define OUT_OF_MEMORY() FATAL("Out of memory.")
#if defined(DEBUG)
// DEBUG binaries use assertions in the code.
// Note: We wrap the if statement in a do-while so that we get a compile
// error if there is no semicolon after ASSERT(condition). This
// ensures that we get the same behavior on DEBUG and RELEASE builds.
#define ASSERT(cond) \
do { \
if (!(cond)) dart::Assert(__FILE__, __LINE__).Fail("expected: %s", #cond); \
} while (false)
#define ASSERT_EQUAL(actual, expected) \
do { \
if ((expected) != (actual)) { \
const std::string actual_str = std::to_string(actual); \
const std::string expected_str = std::to_string(expected); \
dart::Assert(__FILE__, __LINE__) \
.Fail("expected \"%s\" = %s, actual \"%s\" = %s", #expected, \
expected_str.c_str(), #actual, actual_str.c_str()); \
} \
} while (false)
#define ASSERT_LESS_OR_EQUAL(actual, expected) \
do { \
if ((actual) > (expected)) { \
const std::string actual_str = std::to_string(actual); \
const std::string expected_str = std::to_string(expected); \
dart::Assert(__FILE__, __LINE__) \
.Fail("expected \"%s\" = %s >= actual \"%s\" = %s", #expected, \
expected_str.c_str(), #actual, actual_str.c_str()); \
} \
} while (false)
#define ASSERT_IMPLIES(antecedent, consequent) \
do { \
if (antecedent) { \
ASSERT(consequent); \
} \
} while (false)
// DEBUG_ASSERT allows identifiers in condition to be undeclared in release
// mode.
#define DEBUG_ASSERT(cond) ASSERT(cond)
// Returns 'ptr'; useful for initializer lists:
// class Foo { Foo(int* ptr) : ptr_(ASSERT_NOTNULL(ptr)) ...
#define ASSERT_NOTNULL(ptr) dart::Assert(__FILE__, __LINE__).NotNull((ptr))
#else // if defined(DEBUG)
// In order to avoid variable unused warnings for code that only uses
// a variable in an ASSERT or EXPECT, we make sure to use the macro
// argument.
#define ASSERT(condition) \
do { \
} while (false && (condition))
#define ASSERT_EQUAL(expected, actual) \
do { \
} while (false && ((expected) != (actual)))
#define ASSERT_LESS_OR_EQUAL(expected, actual) \
do { \
} while (false && ((actual) > (expected)))
#define ASSERT_IMPLIES(antecedent, consequent) \
do { \
} while (false && (!(antecedent) || (consequent)))
#define DEBUG_ASSERT(cond)
#define ASSERT_NOTNULL(ptr) (ptr)
#endif // if defined(DEBUG)
#define RELEASE_ASSERT(cond) \
do { \
if (!(cond)) dart::Assert(__FILE__, __LINE__).Fail("expected: %s", #cond); \
} while (false)
#define RELEASE_ASSERT_WITH_MSG(cond, msg) \
do { \
if (!(cond)) { \
dart::Assert(__FILE__, __LINE__).Fail("%s: expected: %s", msg, #cond); \
} \
} while (false)
#define COMPILE_ASSERT(expr) static_assert(expr, "")
#if defined(TESTING)
// EXPECT and FAIL are equivalent to ASSERT and FATAL except that they do not
// cause early termination of the unit test. This allows testing to proceed
// further to be able to report other failures before reporting the overall
// unit tests as failing.
#define EXPECT(condition) \
if (!(condition)) { \
dart::Expect(__FILE__, __LINE__).Fail("expected: %s", #condition); \
}
#define EXPECT_EQ(expected, actual) \
dart::Expect(__FILE__, __LINE__).Equals((expected), (actual))
#define EXPECT_NE(not_expected, actual) \
dart::Expect(__FILE__, __LINE__).NotEquals((not_expected), (actual))
#define EXPECT_BITEQ(expected, actual) \
dart::Expect(__FILE__, __LINE__).BitEquals((expected), (actual))
#define EXPECT_FLOAT_EQ(expected, actual, tol) \
dart::Expect(__FILE__, __LINE__).FloatEquals((expected), (actual), (tol))
#define EXPECT_STREQ(expected, actual) \
dart::Expect(__FILE__, __LINE__).StringEquals((expected), (actual))
#define EXPECT_SUBSTRING(needle, haystack) \
dart::Expect(__FILE__, __LINE__).IsSubstring((needle), (haystack))
#define EXPECT_NOTSUBSTRING(needle, haystack) \
dart::Expect(__FILE__, __LINE__).IsNotSubstring((needle), (haystack))
#define EXPECT_LT(left, right) \
dart::Expect(__FILE__, __LINE__).LessThan((left), (right))
#define EXPECT_LE(left, right) \
dart::Expect(__FILE__, __LINE__).LessEqual((left), (right))
#define EXPECT_GT(left, right) \
dart::Expect(__FILE__, __LINE__).GreaterThan((left), (right))
#define EXPECT_GE(left, right) \
dart::Expect(__FILE__, __LINE__).GreaterEqual((left), (right))
#define EXPECT_NOTNULL(ptr) dart::Expect(__FILE__, __LINE__).NotNull((ptr))
#define EXPECT_NULLPTR(ptr) dart::Expect(__FILE__, __LINE__).Null((ptr))
#define FAIL(format, ...) \
dart::Expect(__FILE__, __LINE__).Fail(format, ##__VA_ARGS__)
#endif // defined(TESTING)
#endif // RUNTIME_PLATFORM_ASSERT_H_