Skip to content

Commit

Permalink
feat(test): improve message for diag assertion failures
Browse files Browse the repository at this point in the history
Before:

    test/test-parse-typescript-class.cpp:86: Failure
    Value of: diagnostics
    Expected:
      Actual: 24-byte object <A8-6B 7F-6F 01-00 00-00 10-38 01-34 01-00 00-00 10-38 01-34 01-00 00-00>, whose element #0 doesn't match, whose .token (14-15) doesn't equal 13-14

After:

    test/test-parse-typescript-class.cpp:86: Failure
    Value of: diagnostics
    Expected: 1 diagnostic: {
      Diag_Unexpected_Token{.token = 13..14},
    }
      Actual: 1 diagnostic: {
      Diag_Unexpected_Token,
    }, whose element #0 doesn't match, whose .token (14-15) doesn't equal 13-14
  • Loading branch information
strager committed Feb 26, 2024
1 parent 6ecf408 commit 7b96e74
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 12 deletions.
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ quick_lint_js_add_library(
quick-lint-js/container/padded-string-debug.cpp
quick-lint-js/container/vector-profiler-debug.cpp
quick-lint-js/diag/diag-debug.cpp
quick-lint-js/diag/diag-list-debug.cpp
quick-lint-js/fe/language-debug.cpp
quick-lint-js/fe/lex-debug.cpp
quick-lint-js/i18n/po-parser-debug.cpp
Expand Down
40 changes: 40 additions & 0 deletions src/quick-lint-js/diag/diag-list-debug.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (C) 2020 Matthew "strager" Glazar
// See end of file for extended copyright information.

#include <ostream>
#include <quick-lint-js/diag/diag-list.h>

namespace quick_lint_js {
std::ostream& operator<<(std::ostream& out, const Diag_List& diags) {
int diag_count = 0;
diags.for_each([&](Diag_Type, const void*) -> void { diag_count += 1; });
out << diag_count << " " << (diag_count == 1 ? "diagnostic" : "diagnostics");
if (diag_count > 0) {
out << ": {\n";
diags.for_each([&](Diag_Type type, const void*) -> void {
out << " " << type << ",\n";
});
out << "}";
}
return out;
}

}

// quick-lint-js finds bugs in JavaScript programs.
// Copyright (C) 2020 Matthew "strager" Glazar
//
// This file is part of quick-lint-js.
//
// quick-lint-js is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// quick-lint-js is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with quick-lint-js. If not, see <https://www.gnu.org/licenses/>.
3 changes: 3 additions & 0 deletions src/quick-lint-js/diag/diag-list.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#pragma once

#include <iosfwd>
#include <quick-lint-js/diag/diagnostic-types.h>
#include <quick-lint-js/port/memory-resource.h>

Expand Down Expand Up @@ -80,6 +81,8 @@ class Diag_List {

friend class Diag_List_Diag_Reporter;
};

std::ostream &operator<<(std::ostream &, const Diag_List &);
}

// quick-lint-js finds bugs in JavaScript programs.
Expand Down
65 changes: 57 additions & 8 deletions test/quick-lint-js/diag-matcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,23 @@ class Diag_Fields_Matcher_Impl_Base
explicit Diag_Fields_Matcher_Impl_Base(State s) : state_(std::move(s)) {}

void DescribeTo(std::ostream *out) const final {
*out << "has type " << this->state_.type;
this->describe_fields_to(out);
}
*out << this->state_.type << "{";

void DescribeNegationTo(std::ostream *out) const final {
*out << "doesn't have type " << this->state_.type;
this->describe_fields_to(out);
bool is_first_field = true;
for (const Field &f : this->state_.fields) {
if (!is_first_field) {
*out << ", ";
}
this->describe_field(f, out);
is_first_field = false;
}

*out << "}";
}

void describe_fields_to(std::ostream *) const {
// TODO(strager)
void DescribeNegationTo(std::ostream *out) const final {
*out << "not ";
this->DescribeTo(out);
}

bool MatchAndExplain(const Diag_Collector::Diag &error,
Expand Down Expand Up @@ -127,6 +133,7 @@ class Diag_Fields_Matcher_Impl_Base
}

protected:
virtual void describe_field(const Field &, std::ostream *out) const = 0;
virtual bool field_matches(const Any_Diag_Pointer &error, const Field &f,
testing::MatchResultListener *listener) const = 0;

Expand All @@ -147,6 +154,48 @@ class Diag_Matcher_2::Impl final
using Base::Diag_Fields_Matcher_Impl_Base;

protected:
void describe_field(const Field &f, std::ostream *out) const override {
*out << "." << f.arg.member_name << " = ";
switch (f.arg.member_type) {
case Diagnostic_Arg_Type::source_code_span:
*out << f.begin_offset << ".." << f.end_offset;
break;

case Diagnostic_Arg_Type::char8:
*out << "'";
// TODO(strager): Escape characters like ' and \ and (null) and (tab).
*out << static_cast<char>(f.character);
*out << "'";
break;

case Diagnostic_Arg_Type::string8_view:
*out << "\"";
// TODO(strager): Escape characters like ' and \ and (null) and (tab).
*out << out_string8(f.string);
*out << "\"";
break;

case Diagnostic_Arg_Type::enum_kind:
*out << "Enum_Kind::";
*out << f.enum_kind;
break;

case Diagnostic_Arg_Type::statement_kind:
*out << "Statement_Kind::";
*out << f.statement_kind;
break;

case Diagnostic_Arg_Type::variable_kind:
*out << "Variable_Kind::";
*out << f.variable_kind;
break;

case Diagnostic_Arg_Type::invalid:
QLJS_UNREACHABLE();
break;
}
}

bool field_matches(const Any_Diag_Pointer &error, const Field &f,
testing::MatchResultListener *listener) const override {
switch (f.arg.member_type) {
Expand Down
4 changes: 4 additions & 0 deletions test/quick-lint-js/diag-matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ class Diag_Matcher_2 {
/*implicit*/ operator testing::Matcher<const Diag_Collector::Diag &>() const;
/*implicit*/ operator testing::Matcher<const Any_Diag_Pointer &>() const;

void DescribeTo(std::ostream *out) const {
return ::testing::Matcher<const Any_Diag_Pointer &>(*this).DescribeTo(out);
}

private:
class Impl;

Expand Down
15 changes: 11 additions & 4 deletions test/quick-lint-js/diagnostic-assertion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -566,10 +566,17 @@ class Diag_List_Matcher_Impl
: error_matchers_(std::move(error_matchers)) {}

void DescribeTo([[maybe_unused]] std::ostream* out) const override {
// FIXME(strager): Do we need to write anything here?
}

void DescribeNegationTo([[maybe_unused]] std::ostream* out) const override {
*out << this->error_matchers_.size() << " "
<< (this->error_matchers_.size() == 1 ? "diagnostic" : "diagnostics");
if (!this->error_matchers_.empty()) {
*out << ": {\n";
for (const Diag_Matcher_2& matcher : this->error_matchers_) {
*out << " ";
matcher.DescribeTo(out);
*out << ",\n";
}
*out << "}";
}
// FIXME(strager): Do we need to write anything here?
}

Expand Down
7 changes: 7 additions & 0 deletions test/quick-lint-js/gtest.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ inline void PrintTo(const std::basic_string_view<char8_t> &s,
}
#endif

template <class Value>
std::string get_matcher_description(::testing::Matcher<const Value &> matcher) {
std::ostringstream ss;
matcher.DescribeTo(&ss);
return ss.str();
}

template <class Value>
std::string get_matcher_message(::testing::Matcher<const Value &> matcher,
const Value &value) {
Expand Down
14 changes: 14 additions & 0 deletions test/test-diag-list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,20 @@ TEST(Test_Diag_List, clear_removes_old_diagnostics) {
});
EXPECT_EQ(count, 1);
}

TEST(Test_Diag_List, pretty_print_one_diag) {
// TODO(strager): Include location information.

Linked_Bump_Allocator memory("test");
{
Diag_List diags(&memory);
Padded_String code(u8"hello"_sv);
diags.add(Diag_Let_With_No_Bindings{.where = span_of(code)});
std::ostringstream ss;
ss << diags;
EXPECT_EQ(ss.str(), "1 diagnostic: {\n Diag_Let_With_No_Bindings,\n}");
}
}
}
}

Expand Down
57 changes: 57 additions & 0 deletions test/test-diagnostic-assertion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,10 @@ TEST_F(Test_Diagnostic_Assertion_2, match_offsets_of_1_field_message) {
EXPECT_EQ(get_matcher_message(matcher, diags),
"whose element #0 doesn't match, whose .continue_statement (1-4) "
"doesn't equal 0-5");
EXPECT_EQ(get_matcher_description(matcher),
"1 diagnostic: {\n "
"Diag_Invalid_Continue{.continue_statement = 0..5},\n"
"}");
}

{
Expand All @@ -858,6 +862,10 @@ TEST_F(Test_Diagnostic_Assertion_2, match_offsets_of_1_field_message) {
EXPECT_EQ(get_matcher_message(matcher, diags),
"whose element #0 doesn't match, whose .break_statement (1-4) "
"doesn't equal 0-5");
EXPECT_EQ(get_matcher_description(matcher),
"1 diagnostic: {\n "
"Diag_Invalid_Break{.break_statement = 0..5},\n"
"}");
}
}

Expand Down Expand Up @@ -900,6 +908,11 @@ TEST_F(Test_Diagnostic_Assertion_2, char8_message) {
EXPECT_EQ(get_matcher_message(matcher, diags),
"whose element #0 doesn't match, whose .where (0-1) equals 0-1 and "
"whose .token ('(') doesn't equal ')'");
EXPECT_EQ(get_matcher_description(matcher),
"1 diagnostic: {\n"
" Diag_Expected_Parenthesis_Around_Do_While_Condition"
"{.where = 0..1, .token = ')'},\n"
"}");
}

TEST_F(Test_Diagnostic_Assertion_2, match_span_and_string8_view) {
Expand Down Expand Up @@ -942,6 +955,50 @@ TEST_F(Test_Diagnostic_Assertion_2, string8_view_message) {
get_matcher_message(matcher, diags),
"whose element #0 doesn't match, whose .characters (0-1) equals 0-1 and "
"whose .rounded_val (\"HELLO\") doesn't equal \"hello\"");
EXPECT_EQ(get_matcher_description(matcher),
"1 diagnostic: {\n"
" Diag_Integer_Literal_Will_Lose_Precision"
"{.characters = 0..1, .rounded_val = \"hello\"},\n"
"}");
}

TEST_F(Test_Diagnostic_Assertion_2, enum_kind_message) {
Padded_String code(u8"hi"_sv);
::testing::Matcher matcher = diagnostics_matcher_2(
&code, {u8"^ Diag_TypeScript_Enum_Value_Must_Be_Constant.expression"_diag
u8"{.declared_enum_kind=Enum_Kind::declare_const_enum}"_diag});
EXPECT_EQ(get_matcher_description(matcher),
"1 diagnostic: {\n"
" Diag_TypeScript_Enum_Value_Must_Be_Constant"
"{.expression = 0..1, "
".declared_enum_kind = Enum_Kind::declare_const_enum},\n"
"}");
}

TEST_F(Test_Diagnostic_Assertion_2, statement_kind_message) {
Padded_String code(u8"hi"_sv);
::testing::Matcher matcher = diagnostics_matcher_2(
&code, {u8"^ Diag_Class_Statement_Not_Allowed_In_Body.expected_body"_diag
u8"{.kind_of_statement=Statement_Kind::do_while_loop}"_diag});
EXPECT_EQ(get_matcher_description(matcher),
"1 diagnostic: {\n "
"Diag_Class_Statement_Not_Allowed_In_Body"
"{.expected_body = 0..1, "
".kind_of_statement = Statement_Kind::do_while_loop},\n"
"}");
}

TEST_F(Test_Diagnostic_Assertion_2, variable_kind_message) {
Padded_String code(u8"hi"_sv);
::testing::Matcher matcher = diagnostics_matcher_2(
&code, {u8"^ Diag_Assignment_To_Const_Variable.declaration"_diag
u8"{.var_kind=Variable_Kind::_const}"_diag});
EXPECT_EQ(get_matcher_description(matcher),
"1 diagnostic: {\n"
" Diag_Assignment_To_Const_Variable"
"{.declaration = 0..1, "
".var_kind = Variable_Kind::_const},\n"
"}");
}

TEST_F(Test_Diagnostic_Assertion_2,
Expand Down

0 comments on commit 7b96e74

Please sign in to comment.