Do cleanup on explorer fuzzing infrastructure. (#2790)

I'm partly doing this because the current setup would be difficult to share with the toolchain. e.g., ProtoToCarbon isn't explorer-specific, but the only way to run it via CLI is the explorer's fuzzverter. I want a separate tool.

This change:

- Adds a //common/fuzzing:proto_to_carbon tool.
  - The rest of fuzzverter is now just //explorer/fuzzing:ast_to_proto.
  - The change simplifies overall handling and removes a LLVM CLI dependency.
- Stops allowing unknown fields in the proto.
  - This has mostly led to forgetting to remove fuzzer inputs that were for removed features.
- Moves more non-explorer-specific bits to //common/fuzzing.
- Cleans up remaining pieces in //explorer/fuzzing
  - Merges the //explorer/fuzzing proto tests, which deduplicates AstToString copies.
    - These tests also had duplicate dependencies, etc -- and all complete in ~6s.
  - Updates and fixes regen_corpus which was previously broken by other changes.
  - Updates the README to reflect changes.
- Removes obsolete proto-fuzzer build configuration (AFAICT this is no longer needed).
This commit is contained in:
Jon Ross-Perkins
2023-04-21 08:23:58 -07:00
committed by GitHub
parent c9f0ef9e2c
commit d12583fc08
21 changed files with 432 additions and 527 deletions
-3
View File
@@ -62,9 +62,6 @@ build:asan --features=asan
# Configuration for enabling LibFuzzer (along with ASan).
build:fuzzer --features=fuzzer
# Proto fuzzer specific configuration.
build:proto-fuzzer --features=proto-fuzzer
# Always allow tests to symbolize themselves with whatever `llvm-symbolize` is
# in the users environment.
build --test_env=ASAN_SYMBOLIZER_PATH
@@ -509,13 +509,6 @@ def _impl(ctx):
)],
)
proto_fuzzer = feature(
name = "proto-fuzzer",
enabled = False,
requires = [feature_set(["nonhost"])],
implies = ["fuzzer"],
)
# With clang 14 and lower, we expect it to be built with libc++ debug
# support. In later LLVM versions, we expect the assertions define to work.
if clang_version and clang_version <= 14:
@@ -851,7 +844,6 @@ def _impl(ctx):
asan,
enable_asan_in_fastbuild,
fuzzer,
proto_fuzzer,
layering_check,
module_maps,
use_module_maps,
+27
View File
@@ -25,6 +25,33 @@ cc_library(
hdrs = ["proto_to_carbon.h"],
deps = [
":carbon_cc_proto",
"//common:error",
"@llvm-project//llvm:Support",
],
)
cc_binary(
name = "proto_to_carbon",
testonly = 1,
srcs = ["proto_to_carbon_main.cpp"],
deps = [
":carbon_cc_proto",
":proto_to_carbon_lib",
"//common:bazel_working_dir",
"//common:error",
"@com_google_protobuf//:protobuf_headers",
"@llvm-project//llvm:Support",
],
)
cc_test(
name = "proto_to_carbon_test",
srcs = ["proto_to_carbon_test.cpp"],
deps = [
":carbon_cc_proto",
":proto_to_carbon_lib",
"//common:error",
"//common:gtest_main",
"@com_google_googletest//:gtest",
],
)
+36 -18
View File
@@ -4,6 +4,8 @@
#include "common/fuzzing/proto_to_carbon.h"
#include <google/protobuf/text_format.h>
#include <string_view>
#include "common/fuzzing/carbon.pb.h"
@@ -966,28 +968,44 @@ static auto DeclarationToCarbon(const Fuzzing::Declaration& declaration,
}
}
static auto ProtoToCarbon(const Fuzzing::CompilationUnit& compilation_unit,
llvm::raw_ostream& out) -> void {
out << "// Generated by proto_to_carbon.\n\n";
out << "package ";
LibraryNameToCarbon(compilation_unit.package_statement(), out);
out << (compilation_unit.is_api() ? " api" : " impl") << ";\n";
if (!compilation_unit.declarations().empty()) {
out << "\n";
for (const auto& declaration : compilation_unit.declarations()) {
DeclarationToCarbon(declaration, out);
out << "\n";
}
}
}
auto ProtoToCarbon(const Fuzzing::CompilationUnit& compilation_unit)
auto ProtoToCarbon(const Fuzzing::Carbon& proto, bool maybe_add_main)
-> std::string {
std::string source;
llvm::raw_string_ostream out(source);
ProtoToCarbon(compilation_unit, out);
out << "// Generated by proto_to_carbon.\n"
"package ";
const auto& unit = proto.compilation_unit();
LibraryNameToCarbon(unit.package_statement(), out);
out << (unit.is_api() ? " api" : " impl") << ";\n\n";
bool has_main = false;
if (!unit.declarations().empty()) {
for (const auto& declaration : unit.declarations()) {
DeclarationToCarbon(declaration, out);
if (declaration.kind_case() == Fuzzing::Declaration::kFunction &&
declaration.function().name().name() == "Main") {
has_main = true;
}
out << "\n";
}
}
if (maybe_add_main && !has_main) {
out << "fn Main() -> i32 { return 0; }\n";
}
return source;
}
auto ParseCarbonTextProto(const std::string& contents)
-> ErrorOr<Fuzzing::Carbon> {
google::protobuf::TextFormat::Parser parser;
Fuzzing::Carbon carbon_proto;
if (!parser.ParseFromString(contents, &carbon_proto)) {
return ErrorBuilder() << "Couldn't parse Carbon text proto";
}
return carbon_proto;
}
} // namespace Carbon
+6 -1
View File
@@ -5,6 +5,7 @@
#ifndef CARBON_COMMON_FUZZING_PROTO_TO_CARBON_H_
#define CARBON_COMMON_FUZZING_PROTO_TO_CARBON_H_
#include "common/error.h"
#include "common/fuzzing/carbon.pb.h"
namespace Carbon {
@@ -14,9 +15,13 @@ namespace Carbon {
// buffer is invalid (like a variable declaration with an empty `name` field).
// This is done to reduce the number of inputs the fuzzer framework generates
// when trying to produce lexically valid source.
auto ProtoToCarbon(const Fuzzing::CompilationUnit& compilation_unit)
auto ProtoToCarbon(const Fuzzing::Carbon& proto, bool maybe_add_main)
-> std::string;
// Parses the textproto into a proto object.
auto ParseCarbonTextProto(const std::string& contents)
-> ErrorOr<Fuzzing::Carbon>;
} // namespace Carbon
#endif // CARBON_COMMON_FUZZING_PROTO_TO_CARBON_H_
+51
View File
@@ -0,0 +1,51 @@
// 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
// To convert a crashing input in text proto to Carbon source:
// `proto_to_carbon <file.textproto>`
#include <google/protobuf/text_format.h>
#include <fstream>
#include <iostream>
#include <sstream>
#include "common/bazel_working_dir.h"
#include "common/error.h"
#include "common/fuzzing/proto_to_carbon.h"
namespace Carbon {
auto Main(int argc, char** argv) -> ErrorOr<Success> {
Carbon::SetWorkingDirForBazel();
if (argc != 2) {
return Error("Syntax: proto_to_carbon <file.textproto>");
}
if (!std::filesystem::is_regular_file(argv[1])) {
return Error("Argument must be a file.");
}
// Read the input file.
std::ifstream proto_file(argv[1]);
std::stringstream buffer;
buffer << proto_file.rdbuf();
proto_file.close();
CARBON_ASSIGN_OR_RETURN(Fuzzing::Carbon proto,
Carbon::ParseCarbonTextProto(buffer.str()));
std::cout << Carbon::ProtoToCarbon(proto, /*maybe_add_main=*/true);
return Success();
}
} // namespace Carbon
auto main(int argc, char** argv) -> int {
auto err = Carbon::Main(argc, argv);
if (!err.ok()) {
std::cerr << err.error().message() << "\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
+73
View File
@@ -0,0 +1,73 @@
// 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
#include "common/fuzzing/proto_to_carbon.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "common/error.h"
#include "common/fuzzing/carbon.pb.h"
namespace Carbon::Testing {
namespace {
TEST(FuzzerUtilTest, ProtoToCarbon) {
const ErrorOr<Fuzzing::Carbon> carbon_proto = ParseCarbonTextProto(R"(
compilation_unit {
package_statement { package_name: "P" }
is_api: true
declarations {
function {
name {
name: "Main"
}
param_pattern {}
return_term {
kind: Expression
type { int_type_literal {} }
}
body {
statements {
return_expression_statement {
expression { int_literal { value: 0 } }
}
}
}
}
}
})");
ASSERT_TRUE(carbon_proto.ok());
static constexpr char SourceCode[] = R"(// Generated by proto_to_carbon.
package P api;
fn Main() -> i32
{
return 0;
}
)";
EXPECT_THAT(ProtoToCarbon(*carbon_proto, /*maybe_add_main=*/false),
testing::Eq(SourceCode));
EXPECT_THAT(ProtoToCarbon(*carbon_proto, /*maybe_add_main=*/true),
testing::Eq(SourceCode));
}
TEST(FuzzerUtilTest, ParseCarbonTextProtoWithUnknownField) {
const ErrorOr<Fuzzing::Carbon> carbon_proto = ParseCarbonTextProto(R"(
compilation_unit {
garbage: "value"
declarations {
choice {
name {
name: "Ch"
}
}
}
})");
ASSERT_FALSE(carbon_proto.ok());
}
} // namespace
} // namespace Carbon::Testing
+19 -66
View File
@@ -16,21 +16,18 @@ cc_library(
],
)
cc_library(
name = "fuzzer_util",
cc_binary(
name = "ast_to_proto",
testonly = 1,
srcs = ["fuzzer_util.cpp"],
hdrs = ["fuzzer_util.h"],
srcs = ["ast_to_proto_main.cpp"],
deps = [
"//common:check",
":ast_to_proto_lib",
"//common:bazel_working_dir",
"//common:error",
"//common/fuzzing:carbon_cc_proto",
"//common/fuzzing:proto_to_carbon_lib",
"//explorer/interpreter:exec_program",
"//explorer/interpreter:trace_stream",
"//explorer/ast",
"//explorer/common:arena",
"//explorer/syntax",
"//explorer/syntax:prelude",
"@bazel_tools//tools/cpp/runfiles",
"@com_google_protobuf//:protobuf_headers",
"@llvm-project//llvm:Support",
],
@@ -50,35 +47,34 @@ cc_test(
deps = [
":ast_to_proto_lib",
"//common/fuzzing:carbon_cc_proto",
"//common/fuzzing:proto_to_carbon_lib",
"//explorer/syntax",
"@com_google_googletest//:gtest",
"@com_google_protobuf//:protobuf_headers",
"@llvm-project//llvm:Support",
],
)
cc_binary(
name = "fuzzverter",
cc_library(
name = "fuzzer_util",
testonly = 1,
srcs = ["fuzzverter.cpp"],
srcs = ["fuzzer_util.cpp"],
hdrs = ["fuzzer_util.h"],
deps = [
":ast_to_proto_lib",
":fuzzer_util",
"//common:check",
"//common:error",
"//common/fuzzing:carbon_cc_proto",
"//explorer/common:error_builders",
"//explorer/common:nonnull",
"//common/fuzzing:proto_to_carbon_lib",
"//explorer/ast",
"//explorer/interpreter:exec_program",
"//explorer/interpreter:trace_stream",
"//explorer/syntax",
"//explorer/syntax:prelude",
"@bazel_tools//tools/cpp/runfiles",
"@com_google_protobuf//:protobuf_headers",
"@llvm-project//llvm:Support",
],
)
filegroup(
name = "fuzzer_corpus_files",
srcs = glob(["fuzzer_corpus/**"]),
)
cc_test(
name = "fuzzer_util_test",
srcs = ["fuzzer_util_test.cpp"],
@@ -88,50 +84,7 @@ cc_test(
deps = [
":fuzzer_util",
"//common:gtest_main",
"@com_google_googletest//:gtest",
"@com_google_protobuf//:protobuf_headers",
"@llvm-project//llvm:Support",
],
)
cc_test(
name = "proto_to_carbon_test",
srcs = ["proto_to_carbon_test.cpp"],
args = [
"$(locations //explorer:standard_libraries)",
"$(locations //explorer/testdata:carbon_files)",
],
data = [
"//explorer:standard_libraries",
"//explorer/testdata:carbon_files",
],
deps = [
":ast_to_proto_lib",
"//common/fuzzing:carbon_cc_proto",
"//common/fuzzing:proto_to_carbon_lib",
"//explorer/syntax",
"@com_google_googletest//:gtest",
"@com_google_protobuf//:protobuf_headers",
"@llvm-project//llvm:Support",
],
)
cc_test(
name = "clone_test",
srcs = ["clone_test.cpp"],
args = [
"$(locations //explorer:standard_libraries)",
"$(locations //explorer/testdata:carbon_files)",
],
data = [
"//explorer:standard_libraries",
"//explorer/testdata:carbon_files",
],
deps = [
":ast_to_proto_lib",
"//common/fuzzing:carbon_cc_proto",
"//explorer/ast",
"//explorer/syntax",
"@com_google_googletest//:gtest",
"@com_google_protobuf//:protobuf_headers",
"@llvm-project//llvm:Support",
+33 -49
View File
@@ -36,44 +36,29 @@ message definition in `common/fuzzing/carbon.proto`.
## Incorporating AST changes into the fuzzer
Fuzzer AST representation in
[carbon.proto](https://github.com/carbon-language/carbon-lang/blob/trunk/common/fuzzing/carbon.proto)
needs to be updated when changes are made to the AST, like adding a new AST node
Fuzzer AST representation in [carbon.proto](/common/fuzzing/carbon.proto) needs
to be updated when changes are made to the AST, like adding a new AST node
classes or changing relevant data members of existing nodes.
There are two unit tests which normally should not require direct changes, as
both tests work off of Carbon test files in
[testdata](https://github.com/carbon-language/carbon-lang/tree/trunk/explorer/testdata).
- [ast_to_proto_test.cpp](https://github.com/carbon-language/carbon-lang/blob/trunk/explorer/fuzzing/ast_to_proto_test.cpp)
is a 'smoke' test which verifies that each field of Carbon proto is
populated at least once after converting all of test Carbon files and
merging the results into a single protocol buffer.
- [proto_to_carbon_test.cpp](https://github.com/carbon-language/carbon-lang/blob/trunk/explorer/fuzzing/proto_to_carbon_test.cpp)
uses a 'roundtrip' approach, by converting each parseable Carbon file to a
proto representation, then back to Carbon source, parsing this source into a
second instance of an AST, and comparing the second AST with the original
AST using `AST::Dump()` method. The goal of the test is to ensure that
`carbon.proto` is able to represent ASTs correctly without information loss.
[ast_to_proto_test](ast_to_proto_test.cpp) normally should not require direct
changes, as tests work off of Carbon test files in
[testdata](/explorer/testdata).
To incorporate AST changes into fuzzing logic:
1. Add appropriate AST information to
[carbon.proto](https://github.com/carbon-language/carbon-lang/blob/trunk/common/fuzzing/carbon.proto).
Use existing similar cases as examples.
[carbon.proto](/common/fuzzing/carbon.proto). Use existing similar cases as
examples.
1. Add logic to populate the proto to
[ast_to_proto.cpp](https://github.com/carbon-language/carbon-lang/blob/trunk/explorer/fuzzing/ast_to_proto.cpp).
2. Modify [proto_to_carbon.cpp](/common/fuzzing/proto_to_carbon.cpp) which
handles printing of a Carbon proto instance as a Carbon source string. For
example, add code to print newly introduced proto fields.
1. Make sure `ast_to_proto_test` passes with the new changes.
3. Add logic to populate the proto to
[ast_to_proto.cpp](/explorer/fuzzing/ast_to_proto.cpp).
1. Modify
[proto_to_carbon.cpp](https://github.com/carbon-language/carbon-lang/blob/trunk/common/fuzzing/proto_to_carbon.cpp)
which handles printing of a Carbon proto instance as a Carbon source string.
For example, add code to print newly introduced proto fields.
1. Make sure `proto_to_carbon_test` passes after the changes.
4. Make sure `bazel test //explorer/fuzzing:ast_to_proto_test` passes with the
new changes.
## Running the fuzzer
@@ -85,36 +70,35 @@ a crash is triggered, or forever in a bug-free program ;).
To run in 'unit test' mode:
```bash
bazel test --config=proto-fuzzer --test_output=all //explorer/fuzzing:explorer_fuzzer
bazel test //explorer/fuzzing:explorer_fuzzer
```
To run in 'fuzzing' mode:
```bash
bazel build --config=proto-fuzzer //explorer/fuzzing:explorer_fuzzer
bazel build --config=fuzzer //explorer/fuzzing:explorer_fuzzer.full_corpus
bazel-bin/explorer/fuzzing/explorer_fuzzer
bazel-bin/explorer/fuzzing/explorer_fuzzer.full_corpus
```
## Investigating a crash
Typically it's going to be easiest to run explorer on directly. You can do this
with:
````bash
# Convert the crash to a source file.
bazel run //common/fuzzing:proto_to_carbon -- /tmp/crash.textproto > crash.carbon
# Run explorer on the crash.
bazel run //explorer -- crash.carbon
```
It's also possible to run the fuzzer on a single input:
```bash
bazel-bin/explorer/fuzzing/explorer_fuzzer /tmp/crash.textproto
```
## Investigating a crash
To reproduce a crash, run the fuzzer on the crashing input as described above.
A separate tool called `fuzzverter` can be used for things like converting a
crashing input to Carbon source code for running `explorer` on the code
directly.
To convert a `Fuzzing::Carbon` text proto to Carbon source:
```bash
bazel-bin/explorer/fuzzing/fuzzverter --mode proto_to_carbon --input /tmp/crash.textproto
```
bazel-bin/explorer/fuzzing/explorer_fuzzer.full_corpus /tmp/crash.textproto
````
## Generating new fuzzer corpus entries
@@ -125,5 +109,5 @@ to be a `Fuzzing::Carbon` text proto.
To generate a text proto from Carbon source:
```bash
bazel-bin/explorer/fuzzing/fuzzverter --mode carbon_to_proto --input /tmp/crash.carbon --output /tmp/crash.textproto
bazel run //explorer/fuzzing:ast_to_proto -- /tmp/crash.carbon > crash.textproto
```
+7 -7
View File
@@ -843,15 +843,15 @@ static auto DeclarationToProto(const Declaration& declaration)
return declaration_proto;
}
auto AstToProto(const AST& ast) -> Fuzzing::CompilationUnit {
Fuzzing::CompilationUnit compilation_unit;
*compilation_unit.mutable_package_statement() =
LibraryNameToProto(ast.package);
compilation_unit.set_is_api(ast.is_api);
auto AstToProto(const AST& ast) -> Fuzzing::Carbon {
Fuzzing::Carbon carbon;
auto* unit = carbon.mutable_compilation_unit();
*unit->mutable_package_statement() = LibraryNameToProto(ast.package);
unit->set_is_api(ast.is_api);
for (const Declaration* declaration : ast.declarations) {
*compilation_unit.add_declarations() = DeclarationToProto(*declaration);
*unit->add_declarations() = DeclarationToProto(*declaration);
}
return compilation_unit;
return carbon;
}
} // namespace Carbon
+1 -1
View File
@@ -11,7 +11,7 @@
namespace Carbon {
// Builds a protobuf representation of `ast`.
auto AstToProto(const AST& ast) -> Fuzzing::CompilationUnit;
auto AstToProto(const AST& ast) -> Fuzzing::Carbon;
} // namespace Carbon
+66
View File
@@ -0,0 +1,66 @@
// 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
// To convert a Carbon file to a text proto:
// `ast_to_proto <file.carbon>`
#include <google/protobuf/text_format.h>
#include <fstream>
#include <iostream>
#include <sstream>
#include "common/bazel_working_dir.h"
#include "common/error.h"
#include "common/fuzzing/carbon.pb.h"
#include "explorer/ast/ast.h"
#include "explorer/common/arena.h"
#include "explorer/fuzzing/ast_to_proto.h"
#include "explorer/syntax/parse.h"
namespace Carbon {
auto Main(int argc, char** argv) -> ErrorOr<Success> {
Carbon::SetWorkingDirForBazel();
if (argc != 2) {
return Error("Syntax: ast_to_proto <file.carbon>");
}
if (!std::filesystem::is_regular_file(argv[1])) {
return Error("Argument must be a file.");
}
// Read the input file.
std::ifstream proto_file(argv[1]);
std::stringstream buffer;
buffer << proto_file.rdbuf();
proto_file.close();
Arena arena;
const ErrorOr<AST> ast = Parse(&arena, argv[1],
/*parser_debug=*/false);
if (!ast.ok()) {
return ErrorBuilder() << "Parsing failed: " << ast.error().message();
}
Fuzzing::Carbon carbon_proto = AstToProto(*ast);
std::string proto_string;
google::protobuf::TextFormat::Printer p;
if (!p.PrintToString(carbon_proto, &proto_string)) {
return Error("Failed to convert to text proto");
}
std::cout << proto_string;
return Success();
}
} // namespace Carbon
auto main(int argc, char** argv) -> int {
auto err = Carbon::Main(argc, argv);
if (!err.ok()) {
std::cerr << err.error().message() << "\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
+96 -21
View File
@@ -6,6 +6,7 @@
#include <gmock/gmock.h>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/util/message_differencer.h>
#include <gtest/gtest.h>
#include <filesystem>
@@ -13,8 +14,8 @@
#include <set>
#include <variant>
#include "common/fuzzing/proto_to_carbon.h"
#include "explorer/syntax/parse.h"
#include "llvm/Support/Error.h"
namespace Carbon::Testing {
namespace {
@@ -26,6 +27,18 @@ using ::google::protobuf::Reflection;
static std::vector<llvm::StringRef>* carbon_files = nullptr;
// Returns a string representation of `ast`.
auto AstToString(const AST& ast) -> std::string {
std::string s;
llvm::raw_string_ostream out(s);
out << "package " << ast.package.package << (ast.is_api ? "api" : "impl")
<< ";\n";
for (auto* declaration : ast.declarations) {
out << *declaration << "\n";
}
return s;
}
// Concatenates message and field names.
auto FieldName(const Descriptor& descriptor, const FieldDescriptor& field)
-> std::string {
@@ -80,35 +93,29 @@ auto CollectUsedFields(const Message& message,
}
}
// Determines which fields in the proto have not been used at all.
auto GetUnusedFields(const Message& message) -> std::set<std::string> {
std::set<std::string> all_messages;
std::set<std::string> all_fields;
CollectAllFields(*message.GetDescriptor(), all_messages, all_fields);
std::set<std::string> used_fields;
CollectUsedFields(message, used_fields);
std::set<std::string> unused_fields;
std::set_difference(all_fields.begin(), all_fields.end(), used_fields.begin(),
used_fields.end(),
std::inserter(unused_fields, unused_fields.begin()));
return unused_fields;
}
// A 'smoke' test to check that each field present in `carbon.proto` is set at
// least once after converting all Carbon test sources to proto representation.
TEST(AstToProtoTest, SetsAllProtoFields) {
Carbon::Fuzzing::CompilationUnit merged_proto;
Fuzzing::Carbon merged_proto;
for (const llvm::StringRef f : *carbon_files) {
Carbon::Arena arena;
const ErrorOr<AST> ast = Carbon::Parse(&arena, f, /*parser_debug=*/false);
Arena arena;
const ErrorOr<AST> ast = Parse(&arena, f, /*parser_debug=*/false);
if (ast.ok()) {
merged_proto.MergeFrom(AstToProto(*ast));
}
}
std::set<std::string> unused_fields = GetUnusedFields(merged_proto);
std::set<std::string> all_messages;
std::set<std::string> all_fields;
CollectAllFields(*Fuzzing::Carbon::GetDescriptor(), all_messages, all_fields);
std::set<std::string> used_fields;
CollectUsedFields(merged_proto, used_fields);
std::set<std::string> unused_fields;
std::set_difference(all_fields.begin(), all_fields.end(), used_fields.begin(),
used_fields.end(),
std::inserter(unused_fields, unused_fields.begin()));
EXPECT_EQ(unused_fields.size(), 0)
<< "Unused fields"
<< std::accumulate(unused_fields.begin(), unused_fields.end(),
@@ -118,6 +125,74 @@ TEST(AstToProtoTest, SetsAllProtoFields) {
});
}
// Ensures that `carbon.proto` is able to represent ASTs correctly without
// information loss by doing round-trip testing of files:
//
// 1) Converts each parseable Carbon file to a proto representation.
// 2) Converts back to Carbon source.
// 3) Parses the source into a second instance of an AST.
// 4) Compares the second AST with the original.
TEST(AstToProtoTest, Roundtrip) {
int parsed_ok_count = 0;
for (const llvm::StringRef f : *carbon_files) {
Arena arena;
const ErrorOr<AST> ast = Parse(&arena, f, /*parser_debug=*/false);
if (ast.ok()) {
++parsed_ok_count;
const std::string source_from_proto =
ProtoToCarbon(AstToProto(*ast), /*maybe_add_main=*/false);
SCOPED_TRACE(testing::Message()
<< "Carbon file: " << f << ", source from proto:\n"
<< source_from_proto);
const ErrorOr<AST> ast_from_proto =
ParseFromString(&arena, f, source_from_proto, /*parser_debug=*/false);
if (ast_from_proto.ok()) {
EXPECT_EQ(AstToString(*ast), AstToString(*ast_from_proto));
} else {
ADD_FAILURE() << "Parse error " << ast_from_proto.error().message();
}
}
}
// Makes sure files were actually processed.
EXPECT_GT(parsed_ok_count, 0);
}
auto CloneAST(Arena& arena, const AST& ast) -> AST {
CloneContext context(&arena);
return {
.package = ast.package,
.is_api = ast.is_api,
.imports = ast.imports,
.declarations = context.Clone(ast.declarations),
.main_call = context.Clone(ast.main_call),
.num_prelude_declarations = ast.num_prelude_declarations,
};
}
// Verifies that an AST and its clone produce identical protos.
TEST(AstToProtoTest, SameProtoAfterClone) {
int parsed_ok_count = 0;
for (const llvm::StringRef f : *carbon_files) {
Arena arena;
const ErrorOr<AST> ast = Parse(&arena, f, /*parser_debug=*/false);
if (ast.ok()) {
++parsed_ok_count;
const AST clone = CloneAST(arena, *ast);
const Fuzzing::Carbon orig_proto = AstToProto(*ast);
const Fuzzing::Carbon clone_proto = AstToProto(clone);
// TODO: Use EqualsProto once it's available.
EXPECT_TRUE(google::protobuf::util::MessageDifferencer::Equals(
orig_proto, clone_proto))
<< "clone produced a different AST. original:\n"
<< AstToString(*ast) << "clone:\n"
<< AstToString(clone);
}
}
// Makes sure files were actually processed.
EXPECT_GT(parsed_ok_count, 0);
}
} // namespace
} // namespace Carbon::Testing
-73
View File
@@ -1,73 +0,0 @@
// 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
#include <gmock/gmock.h>
#include <google/protobuf/util/message_differencer.h>
#include <gtest/gtest.h>
#include "explorer/ast/clone_context.h"
#include "explorer/fuzzing/ast_to_proto.h"
#include "explorer/syntax/parse.h"
namespace Carbon::Testing {
namespace {
static std::vector<llvm::StringRef>* carbon_files = nullptr;
auto CloneAST(Arena& arena, const AST& ast) -> AST {
CloneContext context(&arena);
return {
.package = ast.package,
.is_api = ast.is_api,
.imports = ast.imports,
.declarations = context.Clone(ast.declarations),
.main_call = context.Clone(ast.main_call),
.num_prelude_declarations = ast.num_prelude_declarations,
};
}
// Returns a string representation of `ast`.
auto AstToString(const AST& ast) -> std::string {
std::string s;
llvm::raw_string_ostream out(s);
out << "package " << ast.package.package << (ast.is_api ? "api" : "impl")
<< ";\n";
for (auto* declaration : ast.declarations) {
out << *declaration << "\n";
}
return s;
}
TEST(CloneTest, SameProtoAfterClone) {
int parsed_ok_count = 0;
for (const llvm::StringRef f : *carbon_files) {
Carbon::Arena arena;
const ErrorOr<AST> ast = Carbon::Parse(&arena, f, /*parser_debug=*/false);
if (ast.ok()) {
++parsed_ok_count;
const AST clone = CloneAST(arena, *ast);
const Fuzzing::CompilationUnit orig_proto = AstToProto(*ast);
const Fuzzing::CompilationUnit clone_proto = AstToProto(clone);
// TODO: Use EqualsProto once it's available.
EXPECT_TRUE(google::protobuf::util::MessageDifferencer::Equals(
orig_proto, clone_proto))
<< "clone produced a different AST. original:\n"
<< AstToString(*ast) << "clone:\n"
<< AstToString(clone);
}
}
// Makes sure files were actually processed.
EXPECT_GT(parsed_ok_count, 0);
}
} // namespace
} // namespace Carbon::Testing
auto main(int argc, char** argv) -> int {
::testing::InitGoogleTest(&argc, argv);
// gtest should remove flags, leaving just input files.
std::vector<llvm::StringRef> carbon_files(&argv[1], &argv[argc]);
Carbon::Testing::carbon_files = &carbon_files;
return RUN_ALL_TESTS();
}
+1 -1
View File
@@ -10,5 +10,5 @@
DEFINE_TEXT_PROTO_FUZZER(const Carbon::Fuzzing::Carbon& input) {
// Only verifying it doesn't crash.
(void)Carbon::ParseAndExecute(input.compilation_unit());
(void)Carbon::ParseAndExecute(input);
}
+4 -40
View File
@@ -19,16 +19,7 @@
namespace Carbon {
// Appended to fuzzer-generated Carbon source when the source is missing
// `Main()` definition, to prevent early error return in semantic analysis.
static constexpr char EmptyMain[] = R"(
fn Main() -> i32 {
return 0;
}
)";
auto Internal::GetRunfilesFile(const std::string& file)
-> ErrorOr<std::string> {
auto GetRunfilesFile(const std::string& file) -> ErrorOr<std::string> {
using bazel::tools::cpp::runfiles::Runfiles;
std::string error;
// `Runfiles::Create()` fails if passed an empty `argv0`.
@@ -44,42 +35,15 @@ auto Internal::GetRunfilesFile(const std::string& file)
return full_path;
}
auto ParseCarbonTextProto(const std::string& contents, bool allow_unknown)
-> ErrorOr<Fuzzing::Carbon> {
google::protobuf::TextFormat::Parser parser;
if (allow_unknown) {
parser.AllowUnknownField(true);
parser.AllowUnknownExtension(true);
}
Fuzzing::Carbon carbon_proto;
if (!parser.ParseFromString(contents, &carbon_proto)) {
return ErrorBuilder() << "Couldn't parse Carbon text proto";
}
return carbon_proto;
}
auto ProtoToCarbonWithMain(const Fuzzing::CompilationUnit& compilation_unit)
-> std::string {
const bool has_main = std::any_of(
compilation_unit.declarations().begin(),
compilation_unit.declarations().end(),
[](const Fuzzing::Declaration& decl) {
return decl.kind_case() == Fuzzing::Declaration::kFunction &&
decl.function().name().name() == "Main";
});
return Carbon::ProtoToCarbon(compilation_unit) + (has_main ? "" : EmptyMain);
}
auto ParseAndExecute(const Fuzzing::CompilationUnit& compilation_unit)
-> ErrorOr<int> {
const std::string source = ProtoToCarbonWithMain(compilation_unit);
auto ParseAndExecute(const Fuzzing::Carbon& carbon) -> ErrorOr<int> {
const std::string source = ProtoToCarbon(carbon, /*maybe_add_main=*/true);
Arena arena;
CARBON_ASSIGN_OR_RETURN(AST ast,
ParseFromString(&arena, "Fuzzer.carbon", source,
/*parser_debug=*/false));
const ErrorOr<std::string> prelude_path =
Internal::GetRunfilesFile("carbon/explorer/data/prelude.carbon");
GetRunfilesFile("carbon/explorer/data/prelude.carbon");
// Can't do anything without a prelude, so it's a fatal error.
CARBON_CHECK(prelude_path.ok()) << prelude_path.error();
+2 -15
View File
@@ -7,31 +7,18 @@
#include "common/error.h"
#include "common/fuzzing/carbon.pb.h"
#include "explorer/ast/ast.h"
namespace Carbon {
// Parses text proto with a Carbon message, optionally ignoring unknown fields.
auto ParseCarbonTextProto(const std::string& contents,
bool allow_unknown = true)
-> ErrorOr<Fuzzing::Carbon>;
// Converts `compilation_unit` to Carbon. Adds an default `Main()`
// definition if one is not present in the proto.
auto ProtoToCarbonWithMain(const Fuzzing::CompilationUnit& compilation_unit)
-> std::string;
// Parses and executes a fuzzer-generated program.
// Returns program result if execution was successful.
auto ParseAndExecute(const Fuzzing::CompilationUnit& compilation_unit)
-> ErrorOr<int>;
namespace Internal {
auto ParseAndExecute(const Fuzzing::Carbon& carbon) -> ErrorOr<int>;
// Returns a full path for a file under bazel runfiles.
// Exposed for testing.
auto GetRunfilesFile(const std::string& file) -> ErrorOr<std::string>;
} // namespace Internal
} // namespace Carbon
#endif // CARBON_EXPLORER_FUZZING_FUZZER_UTIL_H_
+4 -24
View File
@@ -9,6 +9,7 @@
#include <fstream>
#include "common/fuzzing/proto_to_carbon.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/raw_ostream.h"
@@ -41,38 +42,17 @@ TEST(FuzzerUtilTest, ParseAndExecute) {
}
})");
ASSERT_TRUE(carbon_proto.ok());
const ErrorOr<int> result = ParseAndExecute(carbon_proto->compilation_unit());
const ErrorOr<int> result = ParseAndExecute(*carbon_proto);
ASSERT_TRUE(result.ok()) << "Execution failed: " << result.error();
EXPECT_EQ(*result, 0);
}
TEST(FuzzerUtilTest, GetRunfilesFile) {
EXPECT_THAT(*Internal::GetRunfilesFile("carbon/explorer/data/prelude.carbon"),
EXPECT_THAT(*GetRunfilesFile("carbon/explorer/data/prelude.carbon"),
testing::EndsWith("/prelude.carbon"));
EXPECT_THAT(Internal::GetRunfilesFile("nonexistent-file").error().message(),
EXPECT_THAT(GetRunfilesFile("nonexistent-file").error().message(),
testing::EndsWith("doesn't exist"));
}
TEST(FuzzerUtilTest, ParseCarbonTextProtoWithUnknownField) {
const ErrorOr<Fuzzing::Carbon> carbon_proto =
ParseCarbonTextProto(R"(
compilation_unit {
garbage: "value"
declarations {
choice {
name {
name: "Ch"
}
}
}
})",
/*allow_unknown=*/true);
ASSERT_TRUE(carbon_proto.ok());
// No EqualsProto in gmock - https://github.com/google/googletest/issues/1761.
EXPECT_EQ(
carbon_proto->compilation_unit().declarations(0).choice().name().name(),
"Ch");
}
} // namespace
} // namespace Carbon::Testing
-125
View File
@@ -1,125 +0,0 @@
// 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
// An utility for converting between fuzzer protos and Carbon sources.
//
// For example, to convert a crashing input in text proto to carbon source:
// `fuzzverter --mode=proto_to_carbon --input file.textproto`
//
// To generate a new text proto from carbon source for seeding the corpus:
// `fuzzverter --mode=carbon_to_proto --input file.carbon`
#include <google/protobuf/text_format.h>
#include <cstdlib>
#include <fstream>
#include <ios>
#include <sstream>
#include "common/error.h"
#include "common/fuzzing/carbon.pb.h"
#include "explorer/common/error_builders.h"
#include "explorer/fuzzing/ast_to_proto.h"
#include "explorer/fuzzing/fuzzer_util.h"
#include "explorer/syntax/parse.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/InitLLVM.h"
namespace Carbon {
namespace cl = llvm::cl;
// Reads a file and returns its contents as a string.
static auto ReadFile(std::string_view file_name) -> ErrorOr<std::string> {
std::ifstream file(file_name, std::ios::in);
if (!file.is_open()) {
return ErrorBuilder() << "Could not open " << file_name << " for reading";
}
std::stringstream ss;
ss << file.rdbuf();
return ss.str();
}
// Writes string `s` to `file_name`.
static auto WriteFile(std::string_view s, std::string_view file_name)
-> ErrorOr<Success> {
std::ofstream file(file_name, std::ios::out);
if (!file.is_open()) {
return ErrorBuilder() << "Could not open " << file_name << " for writing";
}
file << s;
return Success();
}
// Converts text proto to Carbon source.
static auto TextProtoToCarbon(std::string_view input_file_name,
std::string_view output_file_name)
-> ErrorOr<Success> {
CARBON_ASSIGN_OR_RETURN(const std::string input_contents,
ReadFile(input_file_name));
CARBON_ASSIGN_OR_RETURN(const Fuzzing::Carbon carbon_proto,
ParseCarbonTextProto(input_contents));
const std::string carbon_source =
ProtoToCarbonWithMain(carbon_proto.compilation_unit());
return WriteFile(carbon_source, output_file_name);
}
// Converts Carbon source to text proto.
static auto CarbonToTextProto(std::string_view input_file_name,
std::string_view output_file_name)
-> ErrorOr<Success> {
Carbon::Arena arena;
const ErrorOr<AST> ast = Carbon::Parse(&arena, input_file_name,
/*parser_debug=*/false);
if (!ast.ok()) {
return ErrorBuilder() << "Parsing failed: " << ast.error().message();
}
Fuzzing::Carbon carbon_proto;
*carbon_proto.mutable_compilation_unit() = AstToProto(*ast);
std::string proto_string;
google::protobuf::TextFormat::Printer p;
if (!p.PrintToString(carbon_proto, &proto_string)) {
return Error("Failed to convert to text proto");
}
return WriteFile(proto_string, output_file_name);
}
// Command line options for defining input/output format.
enum class ConversionMode { TextProtoToCarbon, CarbonToTextProto };
auto Main(int argc, char* argv[]) -> ErrorOr<Success> {
llvm::InitLLVM init_llvm(argc, argv);
cl::opt<ConversionMode> mode(
"mode", cl::desc("Conversion mode"),
cl::values(
clEnumValN(ConversionMode::TextProtoToCarbon, "proto_to_carbon",
"Convert text proto to Carbon source"),
clEnumValN(ConversionMode::CarbonToTextProto, "carbon_to_proto",
"Convert Carbon source to text proto")),
cl::Required);
cl::opt<std::string> input_file_name("input", cl::desc("<input file>"),
cl::init("/dev/stdin"));
cl::opt<std::string> output_file_name("output", cl::desc("<output file>"),
cl::init("/dev/stdout"));
cl::ParseCommandLineOptions(argc, argv);
switch (mode) {
case ConversionMode::TextProtoToCarbon:
return TextProtoToCarbon(input_file_name, output_file_name);
case ConversionMode::CarbonToTextProto:
return CarbonToTextProto(input_file_name, output_file_name);
}
}
} // namespace Carbon
auto main(int argc, char* argv[]) -> int {
if (const auto result = Carbon::Main(argc, argv); !result.ok()) {
llvm::errs() << result.error().message() << "\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
-64
View File
@@ -1,64 +0,0 @@
// 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
#include "common/fuzzing/proto_to_carbon.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "explorer/fuzzing/ast_to_proto.h"
#include "explorer/syntax/parse.h"
namespace Carbon::Testing {
namespace {
static std::vector<llvm::StringRef>* carbon_files = nullptr;
// Returns a string representation of `ast`.
auto AstToString(const AST& ast) -> std::string {
std::string s;
llvm::raw_string_ostream out(s);
out << "package " << ast.package.package << (ast.is_api ? "api" : "impl")
<< ";\n";
for (auto* declaration : ast.declarations) {
out << *declaration << "\n";
}
return s;
}
TEST(ProtoToCarbonTest, Roundtrip) {
int parsed_ok_count = 0;
for (const llvm::StringRef f : *carbon_files) {
Carbon::Arena arena;
const ErrorOr<AST> ast = Carbon::Parse(&arena, f, /*parser_debug=*/false);
if (ast.ok()) {
++parsed_ok_count;
const std::string source_from_proto = ProtoToCarbon(AstToProto(*ast));
SCOPED_TRACE(testing::Message()
<< "Carbon file: " << f << ", source from proto:\n"
<< source_from_proto);
const ErrorOr<AST> ast_from_proto = Carbon::ParseFromString(
&arena, f, source_from_proto, /*parser_debug=*/false);
if (ast_from_proto.ok()) {
EXPECT_EQ(AstToString(*ast), AstToString(*ast_from_proto));
} else {
ADD_FAILURE() << "Parse error " << ast_from_proto.error().message();
}
}
}
// Makes sure files were actually processed.
EXPECT_GT(parsed_ok_count, 0);
}
} // namespace
} // namespace Carbon::Testing
auto main(int argc, char** argv) -> int {
::testing::InitGoogleTest(&argc, argv);
// gtest should remove flags, leaving just input files.
std::vector<llvm::StringRef> carbon_files(&argv[1], &argv[argc]);
Carbon::Testing::carbon_files = &carbon_files;
return RUN_ALL_TESTS();
}
+6 -11
View File
@@ -35,8 +35,7 @@ def _carbon_to_proto(carbon_file: str) -> str:
"""Converts carbon file to text proto string."""
try:
p = subprocess.run(
"bazel-bin/explorer/fuzzing/fuzzverter --mode carbon_to_proto "
f"--input {carbon_file} --output /dev/stdout",
f"bazel-bin/explorer/fuzzing/ast_to_proto {carbon_file}",
shell=True,
check=True,
stdout=subprocess.PIPE,
@@ -65,12 +64,12 @@ def _write_corpus_files(text_protos: Iterable[str], corpus_dir: str) -> None:
def main() -> None:
os.chdir(os.path.join(os.path.dirname(__file__), "../.."))
print("Building fuzzverter...", flush=True)
print("Building ast_to_proto...", flush=True)
subprocess.check_call(
[
"bazel",
"build",
"//explorer/fuzzing:fuzzverter",
"//explorer/fuzzing:ast_to_proto",
]
)
carbon_sources = _get_files(_TESTDATA, ".carbon")
@@ -95,12 +94,8 @@ def main() -> None:
[
"bazel",
"build",
"--features=fuzzer",
# Workaround for #1208.
"--copt=-U_LIBCPP_DEBUG",
# Workaround for #1173.
"--per_file_copt=llvm/.*@-fno-sanitize=fuzzer",
"//explorer/fuzzing:explorer_fuzzer",
"--config=fuzzer",
"//explorer/fuzzing:explorer_fuzzer.full_corpus",
]
)
@@ -110,7 +105,7 @@ def main() -> None:
)
subprocess.check_call(
[
"bazel-bin/explorer/fuzzing/explorer_fuzzer",
"bazel-bin/explorer/fuzzing/explorer_fuzzer.full_corpus",
"-merge=1",
_FUZZER_CORPUS,
new_corpus_dir,