Files
carbon-lang/toolchain/testing/coverage_helper.h
T
Chandler Carruth 2c9739799b Render diagnostics through the terminal library (#7683)
Diagnostic rendering was part of the data model: `Loc::FormatLocation`
and
`Loc::FormatSnippet` wrote plain text to a stream, so no capability,
color, or
terminal width could reach them.

A diagnostic is now one message with labels, contexts, and location
information
attached, and `Diagnostics::Renderer` draws it into a
`Terminal::Buffer`. The
design is in `toolchain/docs/diagnostics_rendering.md` (#7668),
authoring in
`toolchain/docs/diagnostics.md`.

Before:

```
example.carbon:6:3: error: 1 argument passed to function expecting 0 arguments
  Run0(1);
  ^~~~~~~
example.carbon:3:1: note: calling function declared here
fn Run0() {}
^~~~~~~~~~~
```

After:

```
error: 1 argument passed to function expecting 0 arguments
       ╭─┤ example.carbon:6:3
       ┆
     3 │ fn Run0() {}
       · ─────┬─────
       ·      ╰──┤ calling function declared here
       ┆
->   6 │   Run0(1);
       ·   ───────
       ╰────────────
```

`--no-diagnostic-snippets` gives a compact form, `example.carbon:6:3-9`.
File
tests use it; the rendering tests under `toolchain/diagnostics/testdata`
ask for
snippets.

C++ interop diagnostics are data now rather than text Clang drew:
`SemIR::ClangSourceLocId` names a `clang::SourceRange`, and a Clang
location is
marked as the point it names.

`StreamConsumer` moves to a `:renderer` target that `:emitter` does not
depend
on, keeping `common/terminal` out of `consumer.h`. Capabilities come
from the
file behind the error stream, so tests get plain ASCII.

`renderer_fuzzer` draws diagnostics built from fuzzer bytes with every
capability combination, since a shape the renderer doesn't expect must
never be
why the compiler dies while reporting a problem.

TODOs mark the follow-ups: line weights and crossing lines, wrapping
that keeps
a quoted type whole, and ordering diagnostics that share a byte offset.

Also: `IncorrectExtension`'s hint moves into its message, since its note
had no
location of its own, and `strip_noise` in `check_diagnostics.py` blanks
multi-line `#define`s instead of deleting them.

Assisted-by: Claude Code
2026-10-15 06:08:57 +00:00

97 lines
3.2 KiB
C++

// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#ifndef CARBON_TOOLCHAIN_TESTING_COVERAGE_HELPER_H_
#define CARBON_TOOLCHAIN_TESTING_COVERAGE_HELPER_H_
#include <gtest/gtest.h>
#include <fstream>
#include <string>
#include "common/find.h"
#include "common/set.h"
#include "llvm/ADT/StringExtras.h"
#include "re2/re2.h"
namespace Carbon::Testing {
// Looks for kinds that aren't covered by a file_test in the manifest path.
// Kinds are identified by the provided regular expression kind_pattern.
//
// should_be_covered should return false when a kind is either untestable or not
// yet tested.
//
// `allow_unlisted_matches` says that the pattern matches names other than
// kinds, so a match that isn't one is not a stale name to report. Pass it only
// where those other names have a check of their own, since without it a
// misspelled kind in a test would go unnoticed. Diagnostics pass it because a
// line can name a label instead, which `check_diagnostics.py` checks.
//
// TODO: Switch `kind_pattern` to `llvm::StringLiteral` if
// `llvm::StringLiteral::c_str` is added.
template <typename KindT>
auto TestKindCoverage(const std::string& manifest_path,
const char* kind_pattern, llvm::ArrayRef<KindT> kinds,
llvm::ArrayRef<KindT> untested_kinds,
bool allow_unlisted_matches = false) {
std::ifstream manifest_in(manifest_path.c_str());
ASSERT_TRUE(manifest_in.good());
RE2 kind_re(kind_pattern);
ASSERT_TRUE(kind_re.ok()) << kind_re.error();
Set<std::string> covered_kinds;
std::string test_filename;
while (std::getline(manifest_in, test_filename)) {
std::ifstream test_in(test_filename);
ASSERT_TRUE(test_in.good());
std::string line;
while (std::getline(test_in, line)) {
std::string kind;
if (RE2::PartialMatch(line, kind_re, &kind)) {
covered_kinds.Insert(kind);
}
}
}
llvm::SmallVector<llvm::StringRef> missing_kinds;
for (auto kind : kinds) {
if (Contains(untested_kinds, kind)) {
EXPECT_FALSE(covered_kinds.Erase(kind.name()))
<< "Kind " << kind
<< " has coverage even though none was expected. If this has "
"changed, update the coverage test.";
continue;
}
if (!covered_kinds.Erase(kind.name())) {
missing_kinds.push_back(kind.name());
}
}
constexpr llvm::StringLiteral Bullet = "\n - ";
llvm::sort(missing_kinds);
EXPECT_TRUE(missing_kinds.empty()) << "Some kinds have no tests:" << Bullet
<< llvm::join(missing_kinds, Bullet);
if (allow_unlisted_matches) {
return;
}
llvm::SmallVector<std::string> unexpected_matches;
for (const std::string& match : covered_kinds.entries()) {
unexpected_matches.push_back(match);
}
llvm::sort(unexpected_matches);
EXPECT_TRUE(unexpected_matches.empty())
<< "Matched things that aren't in the kind list:" << Bullet
<< llvm::join(unexpected_matches, Bullet);
}
} // namespace Carbon::Testing
#endif // CARBON_TOOLCHAIN_TESTING_COVERAGE_HELPER_H_