| // Protocol Buffers - Google's data interchange format |
| // Copyright 2008 Google Inc. All rights reserved. |
| // |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file or at |
| // https://developers.google.com/open-source/licenses/bsd |
| |
| // Author: kenton@google.com (Kenton Varda) |
| |
| #include "google/protobuf/compiler/mock_code_generator.h" |
| |
| #include <stdlib.h> |
| |
| #include <cstdint> |
| #include <iostream> |
| #include <memory> |
| #include <ostream> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "google/protobuf/testing/file.h" |
| #include "google/protobuf/testing/file.h" |
| #include "google/protobuf/descriptor.pb.h" |
| #include <gtest/gtest.h> |
| #include "absl/log/absl_check.h" |
| #include "absl/log/absl_log.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_join.h" |
| #include "absl/strings/str_replace.h" |
| #include "absl/strings/str_split.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/strings/strip.h" |
| #include "absl/strings/substitute.h" |
| #include "google/protobuf/compiler/plugin.pb.h" |
| #include "google/protobuf/descriptor.h" |
| #include "google/protobuf/descriptor_visitor.h" |
| #include "google/protobuf/io/printer.h" |
| #include "google/protobuf/io/zero_copy_stream.h" |
| #include "google/protobuf/text_format.h" |
| #include "google/protobuf/unittest_features.pb.h" |
| |
| #ifdef major |
| #undef major |
| #endif |
| #ifdef minor |
| #undef minor |
| #endif |
| |
| namespace google { |
| namespace protobuf { |
| namespace compiler { |
| namespace { |
| |
| // Returns the list of the names of files in all_files in the form of a |
| // comma-separated string. |
| std::string CommaSeparatedList( |
| const std::vector<const FileDescriptor*>& all_files) { |
| std::vector<absl::string_view> names; |
| for (size_t i = 0; i < all_files.size(); i++) { |
| names.push_back(all_files[i]->name()); |
| } |
| return absl::StrJoin(names, ","); |
| } |
| |
| static constexpr absl::string_view kFirstInsertionPointName = |
| "first_mock_insertion_point"; |
| static constexpr absl::string_view kSecondInsertionPointName = |
| "second_mock_insertion_point"; |
| static constexpr absl::string_view kFirstInsertionPoint = |
| "# @@protoc_insertion_point(first_mock_insertion_point) is here\n"; |
| static constexpr absl::string_view kSecondInsertionPoint = |
| " # @@protoc_insertion_point(second_mock_insertion_point) is here\n"; |
| |
| absl::string_view GetTestCase() { |
| const char* c_key = getenv("TEST_CASE"); |
| if (c_key == nullptr) { |
| // In Windows, setting 'TEST_CASE=' is equivalent to unsetting |
| // and therefore c_key can be nullptr |
| return ""; |
| } |
| return c_key; |
| } |
| |
| } // namespace |
| |
| MockCodeGenerator::MockCodeGenerator(absl::string_view name) : name_(name) { |
| absl::string_view key = GetTestCase(); |
| if (key == "no_editions") { |
| suppressed_features_ |= CodeGenerator::FEATURE_SUPPORTS_EDITIONS; |
| } else if (key == "invalid_features") { |
| feature_extensions_ = {nullptr}; |
| } else if (key == "no_feature_defaults") { |
| feature_extensions_ = {}; |
| } else if (key == "high_maximum") { |
| maximum_edition_ = Edition::EDITION_99997_TEST_ONLY; |
| } else if (key == "low_minimum") { |
| maximum_edition_ = Edition::EDITION_1_TEST_ONLY; |
| } |
| } |
| |
| MockCodeGenerator::~MockCodeGenerator() = default; |
| |
| uint64_t MockCodeGenerator::GetSupportedFeatures() const { |
| uint64_t all_features = CodeGenerator::FEATURE_PROTO3_OPTIONAL | |
| CodeGenerator::FEATURE_SUPPORTS_EDITIONS; |
| return all_features & ~suppressed_features_; |
| } |
| |
| void MockCodeGenerator::SuppressFeatures(uint64_t features) { |
| suppressed_features_ = features; |
| } |
| |
| void MockCodeGenerator::ExpectGenerated( |
| absl::string_view name, absl::string_view parameter, |
| absl::string_view insertions, absl::string_view file, |
| absl::string_view first_message_name, |
| absl::string_view first_parsed_file_name, |
| absl::string_view output_directory) { |
| std::string content; |
| ABSL_CHECK_OK(File::GetContents( |
| absl::StrCat(output_directory, "/", GetOutputFileName(name, file)), |
| &content, true)); |
| |
| std::vector<std::string> lines = |
| absl::StrSplit(content, '\n', absl::SkipEmpty()); |
| |
| while (!lines.empty() && lines.back().empty()) { |
| lines.pop_back(); |
| } |
| for (size_t i = 0; i < lines.size(); i++) { |
| absl::StrAppend(&lines[i], "\n"); |
| } |
| |
| std::vector<std::string> insertion_list; |
| if (!insertions.empty()) { |
| insertion_list = absl::StrSplit(insertions, ',', absl::SkipEmpty()); |
| } |
| |
| EXPECT_EQ(lines.size(), 3 + insertion_list.size() * 2); |
| EXPECT_EQ(GetOutputFileContent(name, parameter, file, first_parsed_file_name, |
| first_message_name), |
| lines[0]); |
| |
| EXPECT_EQ(kFirstInsertionPoint, lines[1 + insertion_list.size()]); |
| EXPECT_EQ(kSecondInsertionPoint, lines[2 + insertion_list.size() * 2]); |
| |
| for (size_t i = 0; i < insertion_list.size(); i++) { |
| EXPECT_EQ(GetOutputFileContent(insertion_list[i], "first_insert", file, |
| file, first_message_name), |
| lines[1 + i]); |
| // Second insertion point is indented, so the inserted text should |
| // automatically be indented too. |
| EXPECT_EQ(absl::StrCat( |
| " ", GetOutputFileContent(insertion_list[i], "second_insert", |
| file, file, first_message_name)), |
| lines[2 + insertion_list.size() + i]); |
| } |
| } |
| |
| namespace { |
| void CheckSingleAnnotation(absl::string_view expected_file, |
| absl::string_view expected_text, |
| absl::string_view file_content, |
| const GeneratedCodeInfo::Annotation& annotation) { |
| EXPECT_EQ(expected_file, annotation.source_file()); |
| ASSERT_GE(file_content.size(), annotation.begin()); |
| ASSERT_GE(file_content.size(), annotation.end()); |
| ASSERT_LE(annotation.begin(), annotation.end()); |
| EXPECT_EQ(expected_text.size(), annotation.end() - annotation.begin()); |
| EXPECT_EQ(expected_text, |
| file_content.substr(annotation.begin(), expected_text.size())); |
| } |
| } // anonymous namespace |
| |
| void MockCodeGenerator::CheckGeneratedAnnotations( |
| absl::string_view name, absl::string_view file, |
| absl::string_view output_directory) { |
| std::string file_content; |
| ABSL_CHECK_OK(File::GetContents( |
| absl::StrCat(output_directory, "/", GetOutputFileName(name, file)), |
| &file_content, true)); |
| std::string meta_content; |
| ABSL_CHECK_OK( |
| File::GetContents(absl::StrCat(output_directory, "/", |
| GetOutputFileName(name, file), ".pb.meta"), |
| &meta_content, true)); |
| GeneratedCodeInfo annotations; |
| ABSL_CHECK(TextFormat::ParseFromString(meta_content, &annotations)); |
| ASSERT_EQ(7, annotations.annotation_size()); |
| |
| CheckSingleAnnotation("first_annotation", "first", file_content, |
| annotations.annotation(0)); |
| CheckSingleAnnotation("first_path", |
| "test_generator: first_insert,\n foo.proto,\n " |
| "MockCodeGenerator_Annotate,\n foo.proto\n", |
| file_content, annotations.annotation(1)); |
| CheckSingleAnnotation("first_path", |
| "test_plugin: first_insert,\n foo.proto,\n " |
| "MockCodeGenerator_Annotate,\n foo.proto\n", |
| file_content, annotations.annotation(2)); |
| CheckSingleAnnotation("second_annotation", "second", file_content, |
| annotations.annotation(3)); |
| // This annotated text has changed because it was inserted at an indented |
| // insertion point. |
| CheckSingleAnnotation("second_path", |
| "test_generator: second_insert,\n foo.proto,\n " |
| "MockCodeGenerator_Annotate,\n foo.proto\n", |
| file_content, annotations.annotation(4)); |
| CheckSingleAnnotation("second_path", |
| "test_plugin: second_insert,\n foo.proto,\n " |
| "MockCodeGenerator_Annotate,\n foo.proto\n", |
| file_content, annotations.annotation(5)); |
| CheckSingleAnnotation("third_annotation", "third", file_content, |
| annotations.annotation(6)); |
| } |
| |
| bool MockCodeGenerator::Generate(const FileDescriptor* file, |
| const std::string& parameter, |
| GeneratorContext* context, |
| std::string* error) const { |
| // Override minimum/maximum after generating the pool to simulate a plugin |
| // that "works" but doesn't advertise support of the current edition. |
| absl::string_view test_case = GetTestCase(); |
| if (test_case == "high_minimum") { |
| minimum_edition_ = Edition::EDITION_99997_TEST_ONLY; |
| } else if (test_case == "low_maximum") { |
| maximum_edition_ = Edition::EDITION_PROTO2; |
| } |
| |
| if (GetEdition(*file) >= Edition::EDITION_2023 && |
| (suppressed_features_ & CodeGenerator::FEATURE_SUPPORTS_EDITIONS) == 0) { |
| internal::VisitDescriptors(*file, [&](const auto& descriptor) { |
| const FeatureSet& features = GetResolvedSourceFeatures(descriptor); |
| ABSL_CHECK(features.HasExtension(pb::test)) |
| << "Test features were not resolved properly"; |
| ABSL_CHECK(features.GetExtension(pb::test).has_file_feature()) |
| << "Test features were not resolved properly"; |
| ABSL_CHECK(features.GetExtension(pb::test).has_source_feature()) |
| << "Test features were not resolved properly"; |
| }); |
| } |
| |
| bool annotate = false; |
| for (int i = 0; i < file->message_type_count(); i++) { |
| if (absl::StartsWith(file->message_type(i)->name(), "MockCodeGenerator_")) { |
| absl::string_view command = absl::StripPrefix( |
| file->message_type(i)->name(), "MockCodeGenerator_"); |
| if (command == "Error") { |
| *error = "Saw message type MockCodeGenerator_Error."; |
| return false; |
| } |
| if (command == "Exit") { |
| std::cerr << "Saw message type MockCodeGenerator_Exit." << std::endl; |
| exit(123); |
| } |
| ABSL_CHECK(command != "Abort") |
| << "Saw message type MockCodeGenerator_Abort."; |
| if (command == "HasSourceCodeInfo") { |
| FileDescriptorProto file_descriptor_proto; |
| file->CopySourceCodeInfoTo(&file_descriptor_proto); |
| bool has_source_code_info = |
| file_descriptor_proto.has_source_code_info() && |
| file_descriptor_proto.source_code_info().location_size() > 0; |
| ABSL_LOG(FATAL) |
| << "Saw message type MockCodeGenerator_HasSourceCodeInfo: " |
| << has_source_code_info << "."; |
| } else if (command == "HasJsonName") { |
| FieldDescriptorProto field_descriptor_proto; |
| file->message_type(i)->field(0)->CopyTo(&field_descriptor_proto); |
| ABSL_LOG(FATAL) << "Saw json_name: " |
| << field_descriptor_proto.has_json_name(); |
| } else if (command == "Annotate") { |
| annotate = true; |
| } else if (command == "ShowVersionNumber") { |
| Version compiler_version; |
| context->GetCompilerVersion(&compiler_version); |
| ABSL_LOG(FATAL) << "Saw compiler_version: " |
| << compiler_version.major() * 1000000 + |
| compiler_version.minor() * 1000 + |
| compiler_version.patch() |
| << " " << compiler_version.suffix(); |
| } else { |
| ABSL_LOG(FATAL) << "Unknown MockCodeGenerator command: " << command; |
| } |
| } |
| } |
| |
| bool insert_endlines = absl::StartsWith(parameter, "insert_endlines="); |
| if (insert_endlines || absl::StartsWith(parameter, "insert=")) { |
| std::vector<std::string> insert_into = absl::StrSplit( |
| absl::StripPrefix(parameter, |
| insert_endlines ? "insert_endlines=" : "insert="), |
| ',', absl::SkipEmpty()); |
| |
| for (size_t i = 0; i < insert_into.size(); i++) { |
| { |
| google::protobuf::GeneratedCodeInfo info; |
| std::string content = |
| GetOutputFileContent(name_, "first_insert", file, context); |
| if (insert_endlines) { |
| absl::StrReplaceAll({{",", ",\n"}}, &content); |
| } |
| if (annotate) { |
| auto* annotation = info.add_annotation(); |
| annotation->set_begin(0); |
| annotation->set_end(content.size()); |
| annotation->set_source_file("first_path"); |
| } |
| std::unique_ptr<io::ZeroCopyOutputStream> output( |
| context->OpenForInsertWithGeneratedCodeInfo( |
| GetOutputFileName(insert_into[i], file), |
| std::string(kFirstInsertionPointName), info)); |
| io::Printer printer(output.get(), '$'); |
| printer.PrintRaw(content); |
| if (printer.failed()) { |
| *error = "MockCodeGenerator detected write error."; |
| return false; |
| } |
| } |
| |
| { |
| google::protobuf::GeneratedCodeInfo info; |
| std::string content = |
| GetOutputFileContent(name_, "second_insert", file, context); |
| if (insert_endlines) { |
| absl::StrReplaceAll({{",", ",\n"}}, &content); |
| } |
| if (annotate) { |
| auto* annotation = info.add_annotation(); |
| annotation->set_begin(0); |
| annotation->set_end(content.size()); |
| annotation->set_source_file("second_path"); |
| } |
| std::unique_ptr<io::ZeroCopyOutputStream> output( |
| context->OpenForInsertWithGeneratedCodeInfo( |
| GetOutputFileName(insert_into[i], file), |
| std::string(kSecondInsertionPointName), info)); |
| io::Printer printer(output.get(), '$'); |
| printer.PrintRaw(content); |
| if (printer.failed()) { |
| *error = "MockCodeGenerator detected write error."; |
| return false; |
| } |
| } |
| } |
| } else { |
| std::unique_ptr<io::ZeroCopyOutputStream> output( |
| context->Open(GetOutputFileName(name_, file))); |
| |
| GeneratedCodeInfo annotations; |
| io::AnnotationProtoCollector<GeneratedCodeInfo> annotation_collector( |
| &annotations); |
| io::Printer printer(output.get(), '$', |
| annotate ? &annotation_collector : nullptr); |
| printer.PrintRaw(GetOutputFileContent(name_, parameter, file, context)); |
| std::string annotate_suffix = "_annotation"; |
| if (annotate) { |
| printer.Print("$p$\n", "p", "first"); |
| printer.Annotate("p", absl::StrCat("first", annotate_suffix)); |
| } |
| printer.PrintRaw(kFirstInsertionPoint); |
| if (annotate) { |
| printer.Print("$p$\n", "p", "second"); |
| printer.Annotate("p", absl::StrCat("second", annotate_suffix)); |
| } |
| printer.PrintRaw(kSecondInsertionPoint); |
| if (annotate) { |
| printer.Print("$p$\n", "p", "third"); |
| printer.Annotate("p", absl::StrCat("third", annotate_suffix)); |
| } |
| |
| if (printer.failed()) { |
| *error = "MockCodeGenerator detected write error."; |
| return false; |
| } |
| if (annotate) { |
| std::unique_ptr<io::ZeroCopyOutputStream> meta_output(context->Open( |
| absl::StrCat(GetOutputFileName(name_, file), ".pb.meta"))); |
| if (!TextFormat::Print(annotations, meta_output.get())) { |
| *error = "MockCodeGenerator couldn't write .pb.meta"; |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| std::string MockCodeGenerator::GetOutputFileName( |
| absl::string_view generator_name, const FileDescriptor* file) { |
| return GetOutputFileName(generator_name, file->name()); |
| } |
| |
| std::string MockCodeGenerator::GetOutputFileName( |
| absl::string_view generator_name, absl::string_view file) { |
| return absl::StrCat(file, ".MockCodeGenerator.", generator_name); |
| } |
| |
| std::string MockCodeGenerator::GetOutputFileContent( |
| absl::string_view generator_name, absl::string_view parameter, |
| const FileDescriptor* file, GeneratorContext* context) { |
| std::vector<const FileDescriptor*> all_files; |
| context->ListParsedFiles(&all_files); |
| return GetOutputFileContent( |
| generator_name, parameter, file->name(), CommaSeparatedList(all_files), |
| file->message_type_count() > 0 ? file->message_type(0)->name() |
| : "(none)"); |
| } |
| |
| std::string MockCodeGenerator::GetOutputFileContent( |
| absl::string_view generator_name, absl::string_view parameter, |
| absl::string_view file, absl::string_view parsed_file_list, |
| absl::string_view first_message_name) { |
| return absl::Substitute("$0: $1, $2, $3, $4\n", generator_name, parameter, |
| file, first_message_name, parsed_file_list); |
| } |
| |
| } // namespace compiler |
| } // namespace protobuf |
| } // namespace google |