diff options
Diffstat (limited to 'tools')
47 files changed, 3713 insertions, 177 deletions
diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index 7ffa5ffc09fe..137fbd671865 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -246,6 +246,36 @@ class ValueBodyPrinter : public ConstValueVisitor { Printer* printer_; }; +std::string OverlayablePoliciesToString(OverlayableItem::PolicyFlags policies) { + static const std::map<OverlayableItem::PolicyFlags, std::string> kFlagToString = { + {OverlayableItem::kPublic, "public"}, + {OverlayableItem::kSystem, "system"}, + {OverlayableItem::kVendor, "vendor"}, + {OverlayableItem::kProduct, "product"}, + {OverlayableItem::kSignature, "signature"}, + {OverlayableItem::kOdm, "odm"}, + {OverlayableItem::kOem, "oem"}, + }; + std::string str; + for (auto const& policy : kFlagToString) { + if ((policies & policy.first) != policy.first) { + continue; + } + if (!str.empty()) { + str.append("|"); + } + str.append(policy.second); + policies &= ~policy.first; + } + if (policies != 0) { + if (!str.empty()) { + str.append("|"); + } + str.append(StringPrintf("0x%08x", policies)); + } + return !str.empty() ? str : "none"; +} + } // namespace void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& options, @@ -312,6 +342,10 @@ void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& break; } + if (entry->overlayable_item) { + printer->Print(" OVERLAYABLE"); + } + printer->Println(); if (options.show_values) { @@ -525,4 +559,62 @@ void Debug::DumpXml(const xml::XmlResource& doc, Printer* printer) { doc.root->Accept(&xml_visitor); } +struct DumpOverlayableEntry { + std::string overlayable_section; + std::string policy_subsection; + std::string resource_name; +}; + +void Debug::DumpOverlayable(const ResourceTable& table, text::Printer* printer) { + std::vector<DumpOverlayableEntry> items; + for (const auto& package : table.packages) { + for (const auto& type : package->types) { + for (const auto& entry : type->entries) { + if (entry->overlayable_item) { + const auto& overlayable_item = entry->overlayable_item.value(); + const auto overlayable_section = StringPrintf(R"(name="%s" actor="%s")", + overlayable_item.overlayable->name.c_str(), + overlayable_item.overlayable->actor.c_str()); + const auto policy_subsection = StringPrintf(R"(policies="%s")", + OverlayablePoliciesToString(overlayable_item.policies).c_str()); + const auto value = + StringPrintf("%s/%s", to_string(type->type).data(), entry->name.c_str()); + items.push_back(DumpOverlayableEntry{overlayable_section, policy_subsection, value}); + } + } + } + } + + std::sort(items.begin(), items.end(), + [](const DumpOverlayableEntry& a, const DumpOverlayableEntry& b) { + if (a.overlayable_section != b.overlayable_section) { + return a.overlayable_section < b.overlayable_section; + } + if (a.policy_subsection != b.policy_subsection) { + return a.policy_subsection < b.policy_subsection; + } + return a.resource_name < b.resource_name; + }); + + std::string last_overlayable_section; + std::string last_policy_subsection; + for (const auto& item : items) { + if (last_overlayable_section != item.overlayable_section) { + printer->Println(item.overlayable_section); + last_overlayable_section = item.overlayable_section; + } + if (last_policy_subsection != item.policy_subsection) { + printer->Indent(); + printer->Println(item.policy_subsection); + last_policy_subsection = item.policy_subsection; + printer->Undent(); + } + printer->Indent(); + printer->Indent(); + printer->Println(item.resource_name); + printer->Undent(); + printer->Undent(); + } +} + } // namespace aapt diff --git a/tools/aapt2/Debug.h b/tools/aapt2/Debug.h index a43197cacf7b..9443d606d7e5 100644 --- a/tools/aapt2/Debug.h +++ b/tools/aapt2/Debug.h @@ -39,6 +39,7 @@ struct Debug { static void DumpHex(const void* data, size_t len); static void DumpXml(const xml::XmlResource& doc, text::Printer* printer); static void DumpResStringPool(const android::ResStringPool* pool, text::Printer* printer); + static void DumpOverlayable(const ResourceTable& table, text::Printer* printer); }; } // namespace aapt diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp index 213bdd2372ec..8a43bb4ede35 100644 --- a/tools/aapt2/Main.cpp +++ b/tools/aapt2/Main.cpp @@ -169,12 +169,12 @@ int MainImpl(int argc, char** argv) { aapt::text::Printer printer(&fout); aapt::StdErrDiagnostics diagnostics; - auto main_command = new aapt::MainCommand(&printer, &diagnostics); + aapt::MainCommand main_command(&printer, &diagnostics); // Add the daemon subcommand here so it cannot be called while executing the daemon - main_command->AddOptionalSubcommand( + main_command.AddOptionalSubcommand( aapt::util::make_unique<aapt::DaemonCommand>(&fout, &diagnostics)); - return main_command->Execute(args, &std::cerr); + return main_command.Execute(args, &std::cerr); } int main(int argc, char** argv) { diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index c7ac438dacfb..d50b1de335fb 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -740,7 +740,6 @@ int CompileCommand::Action(const std::vector<std::string>& args) { } std::unique_ptr<io::IFileCollection> file_collection; - std::unique_ptr<IArchiveWriter> archive_writer; // Collect the resources files to compile if (options_.res_dir && options_.res_zip) { @@ -761,8 +760,6 @@ int CompileCommand::Action(const std::vector<std::string>& args) { context.GetDiagnostics()->Error(DiagMessage(options_.res_dir.value()) << err); return 1; } - - archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path); } else if (options_.res_zip) { if (!args.empty()) { context.GetDiagnostics()->Error(DiagMessage() << "files given but --zip specified"); @@ -777,8 +774,6 @@ int CompileCommand::Action(const std::vector<std::string>& args) { context.GetDiagnostics()->Error(DiagMessage(options_.res_zip.value()) << err); return 1; } - - archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path); } else { auto collection = util::make_unique<io::FileCollection>(); @@ -791,7 +786,14 @@ int CompileCommand::Action(const std::vector<std::string>& args) { } file_collection = std::move(collection); + } + + std::unique_ptr<IArchiveWriter> archive_writer; + file::FileType output_file_type = file::GetFileType(options_.output_path); + if (output_file_type == file::FileType::kDirectory) { archive_writer = CreateDirectoryArchiveWriter(context.GetDiagnostics(), options_.output_path); + } else { + archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path); } if (!archive_writer) { diff --git a/tools/aapt2/cmd/Dump.cpp b/tools/aapt2/cmd/Dump.cpp index 429aff1ff594..3982d12f6036 100644 --- a/tools/aapt2/cmd/Dump.cpp +++ b/tools/aapt2/cmd/Dump.cpp @@ -394,6 +394,17 @@ int DumpXmlTreeCommand::Dump(LoadedApk* apk) { return 0; } +int DumpOverlayableCommand::Dump(LoadedApk* apk) { + ResourceTable* table = apk->GetResourceTable(); + if (!table) { + GetDiagnostics()->Error(DiagMessage() << "Failed to retrieve resource table"); + return 1; + } + + Debug::DumpOverlayable(*table, GetPrinter()); + return 0; +} + const char DumpBadgerCommand::kBadgerData[2925] = { 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 95, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, diff --git a/tools/aapt2/cmd/Dump.h b/tools/aapt2/cmd/Dump.h index 7ded9bcf8470..cd51f7a7718c 100644 --- a/tools/aapt2/cmd/Dump.h +++ b/tools/aapt2/cmd/Dump.h @@ -240,6 +240,16 @@ class DumpXmlTreeCommand : public DumpApkCommand { std::vector<std::string> files_; }; +class DumpOverlayableCommand : public DumpApkCommand { + public: + explicit DumpOverlayableCommand(text::Printer* printer, IDiagnostics* diag) + : DumpApkCommand("overlayable", printer, diag) { + SetDescription("Print the <overlayable> resources of an APK."); + } + + int Dump(LoadedApk* apk) override; +}; + /** The default dump command. Performs no action because a subcommand is required. */ class DumpCommand : public Command { public: @@ -255,8 +265,8 @@ class DumpCommand : public Command { AddOptionalSubcommand(util::make_unique<DumpTableCommand>(printer, diag_)); AddOptionalSubcommand(util::make_unique<DumpXmlStringsCommand>(printer, diag_)); AddOptionalSubcommand(util::make_unique<DumpXmlTreeCommand>(printer, diag_)); + AddOptionalSubcommand(util::make_unique<DumpOverlayableCommand>(printer, diag_)); AddOptionalSubcommand(util::make_unique<DumpBadgerCommand>(printer), /* hidden */ true); - // TODO(b/120609160): Add aapt2 overlayable dump command } int Action(const std::vector<std::string>& args) override { diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 5e06818d7a13..e36668e5a043 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -53,9 +53,9 @@ using ::android::ConfigDescription; using ::android::ResTable_config; using ::android::StringPiece; using ::android::base::ReadFileToString; -using ::android::base::WriteStringToFile; using ::android::base::StringAppendF; using ::android::base::StringPrintf; +using ::android::base::WriteStringToFile; namespace aapt { @@ -300,29 +300,7 @@ class Optimizer { OptimizeContext* context_; }; -bool ExtractObfuscationWhitelistFromConfig(const std::string& path, OptimizeContext* context, - OptimizeOptions* options) { - std::string contents; - if (!ReadFileToString(path, &contents, true)) { - context->GetDiagnostics()->Error(DiagMessage() - << "failed to parse whitelist from config file: " << path); - return false; - } - for (StringPiece resource_name : util::Tokenize(contents, ',')) { - options->table_flattener_options.whitelisted_resources.insert( - resource_name.to_string()); - } - return true; -} - -bool ExtractConfig(const std::string& path, OptimizeContext* context, - OptimizeOptions* options) { - std::string content; - if (!android::base::ReadFileToString(path, &content, true /*follow_symlinks*/)) { - context->GetDiagnostics()->Error(DiagMessage(path) << "failed reading whitelist"); - return false; - } - +bool ParseConfig(const std::string& content, IAaptContext* context, OptimizeOptions* options) { size_t line_no = 0; for (StringPiece line : util::Tokenize(content, '\n')) { line_no++; @@ -351,15 +329,24 @@ bool ExtractConfig(const std::string& path, OptimizeContext* context, for (StringPiece directive : util::Tokenize(directives, ',')) { if (directive == "remove") { options->resources_blacklist.insert(resource_name.ToResourceName()); - } else if (directive == "no_obfuscate") { - options->table_flattener_options.whitelisted_resources.insert( - resource_name.entry.to_string()); + } else if (directive == "no_collapse" || directive == "no_obfuscate") { + options->table_flattener_options.name_collapse_exemptions.insert( + resource_name.ToResourceName()); } } } return true; } +bool ExtractConfig(const std::string& path, IAaptContext* context, OptimizeOptions* options) { + std::string content; + if (!android::base::ReadFileToString(path, &content, true /*follow_symlinks*/)) { + context->GetDiagnostics()->Error(DiagMessage(path) << "failed reading config file"); + return false; + } + return ParseConfig(content, context, options); +} + bool ExtractAppDataFromManifest(OptimizeContext* context, const LoadedApk* apk, OptimizeOptions* out_options) { const xml::XmlResource* manifest = apk->GetManifest(); @@ -467,15 +454,6 @@ int OptimizeCommand::Action(const std::vector<std::string>& args) { } } - if (options_.table_flattener_options.collapse_key_stringpool) { - if (whitelist_path_) { - std::string& path = whitelist_path_.value(); - if (!ExtractObfuscationWhitelistFromConfig(path, &context, &options_)) { - return 1; - } - } - } - if (resources_config_path_) { std::string& path = resources_config_path_.value(); if (!ExtractConfig(path, &context, &options_)) { diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index 0be7dad18380..5070ccc8afbf 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -78,10 +78,6 @@ class OptimizeCommand : public Command { "All the resources that would be unused on devices of the given densities will be \n" "removed from the APK.", &target_densities_); - AddOptionalFlag("--whitelist-path", - "Path to the whitelist.cfg file containing whitelisted resources \n" - "whose names should not be altered in final resource tables.", - &whitelist_path_); AddOptionalFlag("--resources-config-path", "Path to the resources.cfg file containing the list of resources and \n" "directives to each resource. \n" @@ -104,11 +100,13 @@ class OptimizeCommand : public Command { "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.", &options_.table_flattener_options.use_sparse_entries); - AddOptionalSwitch("--enable-resource-obfuscation", - "Enables obfuscation of key string pool to single value", + AddOptionalSwitch("--collapse-resource-names", + "Collapses resource names to a single value in the key string pool. Resources can \n" + "be exempted using the \"no_collapse\" directive in a file specified by " + "--resources-config-path.", &options_.table_flattener_options.collapse_key_stringpool); - AddOptionalSwitch("--enable-resource-path-shortening", - "Enables shortening of the path of the resources inside the APK.", + AddOptionalSwitch("--shorten-resource-paths", + "Shortens the paths of resources inside the APK.", &options_.shorten_resource_paths); AddOptionalFlag("--resource-path-shortening-map", "Path to output the map of old resource paths to shortened paths.", @@ -125,7 +123,6 @@ class OptimizeCommand : public Command { const std::string &file_path); Maybe<std::string> config_path_; - Maybe<std::string> whitelist_path_; Maybe<std::string> resources_config_path_; Maybe<std::string> target_densities_; std::vector<std::string> configs_; diff --git a/tools/aapt2/cmd/Optimize_test.cpp b/tools/aapt2/cmd/Optimize_test.cpp new file mode 100644 index 000000000000..ac681e85b3d6 --- /dev/null +++ b/tools/aapt2/cmd/Optimize_test.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Optimize.h" + +#include "AppInfo.h" +#include "Diagnostics.h" +#include "LoadedApk.h" +#include "Resource.h" +#include "test/Test.h" + +using testing::Contains; +using testing::Eq; + +namespace aapt { + +bool ParseConfig(const std::string&, IAaptContext*, OptimizeOptions*); + +using OptimizeTest = CommandTestFixture; + +TEST_F(OptimizeTest, ParseConfigWithNoCollapseExemptions) { + const std::string& content = R"( +string/foo#no_collapse +dimen/bar#no_collapse +)"; + aapt::test::Context context; + OptimizeOptions options; + ParseConfig(content, &context, &options); + + const std::set<ResourceName>& name_collapse_exemptions = + options.table_flattener_options.name_collapse_exemptions; + + ASSERT_THAT(name_collapse_exemptions.size(), Eq(2)); + EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kString, "foo"))); + EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kDimen, "bar"))); +} + +TEST_F(OptimizeTest, ParseConfigWithNoObfuscateExemptions) { + const std::string& content = R"( +string/foo#no_obfuscate +dimen/bar#no_obfuscate +)"; + aapt::test::Context context; + OptimizeOptions options; + ParseConfig(content, &context, &options); + + const std::set<ResourceName>& name_collapse_exemptions = + options.table_flattener_options.name_collapse_exemptions; + + ASSERT_THAT(name_collapse_exemptions.size(), Eq(2)); + EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kString, "foo"))); + EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kDimen, "bar"))); +} + +} // namespace aapt diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index b9321174100b..58e232c33985 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -228,14 +228,15 @@ class PackageFlattener { public: PackageFlattener(IAaptContext* context, ResourceTablePackage* package, const std::map<size_t, std::string>* shared_libs, bool use_sparse_entries, - bool collapse_key_stringpool, const std::set<std::string>& whitelisted_resources) + bool collapse_key_stringpool, + const std::set<ResourceName>& name_collapse_exemptions) : context_(context), diag_(context->GetDiagnostics()), package_(package), shared_libs_(shared_libs), use_sparse_entries_(use_sparse_entries), collapse_key_stringpool_(collapse_key_stringpool), - whitelisted_resources_(whitelisted_resources) { + name_collapse_exemptions_(name_collapse_exemptions) { } bool FlattenPackage(BigBuffer* buffer) { @@ -652,11 +653,12 @@ class PackageFlattener { for (ResourceEntry* entry : sorted_entries) { uint32_t local_key_index; + ResourceName resource_name({}, type->type, entry->name); if (!collapse_key_stringpool_ || - whitelisted_resources_.find(entry->name) != whitelisted_resources_.end()) { + name_collapse_exemptions_.find(resource_name) != name_collapse_exemptions_.end()) { local_key_index = (uint32_t)key_pool_.MakeRef(entry->name).index(); } else { - // resource isn't whitelisted, add it as obfuscated value + // resource isn't exempt from collapse, add it as obfuscated value local_key_index = (uint32_t)key_pool_.MakeRef(obfuscated_resource_name).index(); } // Group values by configuration. @@ -712,7 +714,7 @@ class PackageFlattener { StringPool type_pool_; StringPool key_pool_; bool collapse_key_stringpool_; - const std::set<std::string>& whitelisted_resources_; + const std::set<ResourceName>& name_collapse_exemptions_; }; } // namespace @@ -760,7 +762,7 @@ bool TableFlattener::Consume(IAaptContext* context, ResourceTable* table) { PackageFlattener flattener(context, package.get(), &table->included_packages_, options_.use_sparse_entries, options_.collapse_key_stringpool, - options_.whitelisted_resources); + options_.name_collapse_exemptions); if (!flattener.FlattenPackage(&package_buffer)) { return false; } diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h index 73c17295556b..4360db190146 100644 --- a/tools/aapt2/format/binary/TableFlattener.h +++ b/tools/aapt2/format/binary/TableFlattener.h @@ -19,6 +19,7 @@ #include "android-base/macros.h" +#include "Resource.h" #include "ResourceTable.h" #include "process/IResourceTableConsumer.h" #include "util/BigBuffer.h" @@ -41,8 +42,8 @@ struct TableFlattenerOptions { // have name indices that point to this single value bool collapse_key_stringpool = false; - // Set of whitelisted resource names to avoid altering in key stringpool - std::set<std::string> whitelisted_resources; + // Set of resources to avoid collapsing to a single entry in key stringpool. + std::set<ResourceName> name_collapse_exemptions; // Map from original resource paths to shortened resource paths. std::map<std::string, std::string> shortened_path_map; diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index a9409235e07a..8fbdd7f27041 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -518,7 +518,7 @@ TEST_F(TableFlattenerTest, LongSharedLibraryPackageNameIsIllegal) { ASSERT_FALSE(Flatten(context.get(), {}, table.get(), &result)); } -TEST_F(TableFlattenerTest, ObfuscatingResourceNamesNoWhitelistSucceeds) { +TEST_F(TableFlattenerTest, ObfuscatingResourceNamesNoNameCollapseExemptionsSucceeds) { std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() .SetPackageId("com.app.test", 0x7f) @@ -572,7 +572,7 @@ TEST_F(TableFlattenerTest, ObfuscatingResourceNamesNoWhitelistSucceeds) { ResourceId(0x7f050000), {}, Res_value::TYPE_STRING, (uint32_t)idx, 0u)); } -TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithWhitelistSucceeds) { +TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithNameCollapseExemptionsSucceeds) { std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() .SetPackageId("com.app.test", 0x7f) @@ -591,21 +591,22 @@ TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithWhitelistSucceeds) { TableFlattenerOptions options; options.collapse_key_stringpool = true; - options.whitelisted_resources.insert("test"); - options.whitelisted_resources.insert("three"); + options.name_collapse_exemptions.insert(ResourceName({}, ResourceType::kId, "one")); + options.name_collapse_exemptions.insert(ResourceName({}, ResourceType::kString, "test")); ResTable res_table; ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table)); - EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated", + EXPECT_TRUE(Exists(&res_table, "com.app.test:id/one", ResourceId(0x7f020000), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u)); EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated", ResourceId(0x7f020001), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u)); - EXPECT_TRUE(Exists(&res_table, "com.app.test:id/three", ResourceId(0x7f020002), {}, - Res_value::TYPE_REFERENCE, 0x7f020000u, 0u)); + EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated", + ResourceId(0x7f020002), {}, Res_value::TYPE_REFERENCE, 0x7f020000u, 0u)); + // Note that this resource is also named "one", but it's a different type, so gets obfuscated. EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated", ResourceId(0x7f030000), {}, Res_value::TYPE_INT_DEC, 1u, ResTable_config::CONFIG_VERSION)); diff --git a/tools/aapt2/link/XmlReferenceLinker.cpp b/tools/aapt2/link/XmlReferenceLinker.cpp index f3be483ab728..c3c16b92f712 100644 --- a/tools/aapt2/link/XmlReferenceLinker.cpp +++ b/tools/aapt2/link/XmlReferenceLinker.cpp @@ -83,6 +83,15 @@ class XmlVisitor : public xml::PackageAwareVisitor { Attribute default_attribute(android::ResTable_map::TYPE_ANY); default_attribute.SetWeak(true); + // The default orientation of gradients in android Q is different than previous android + // versions. Set the android:angle attribute to "0" to ensure that the default gradient + // orientation will remain left-to-right in android Q. + if (el->name == "gradient" && context_->GetMinSdkVersion() <= SDK_Q) { + if (!el->FindAttribute(xml::kSchemaAndroid, "angle")) { + el->attributes.push_back(xml::Attribute{xml::kSchemaAndroid, "angle", "0"}); + } + } + const Source source = source_.WithLine(el->line_number); for (xml::Attribute& attr : el->attributes) { // If the attribute has no namespace, interpret values as if diff --git a/tools/aapt2/link/XmlReferenceLinker_test.cpp b/tools/aapt2/link/XmlReferenceLinker_test.cpp index ef99355e5b5f..0ce2e50d6e44 100644 --- a/tools/aapt2/link/XmlReferenceLinker_test.cpp +++ b/tools/aapt2/link/XmlReferenceLinker_test.cpp @@ -47,6 +47,8 @@ class XmlReferenceLinkerTest : public ::testing::Test { test::AttributeBuilder() .SetTypeMask(android::ResTable_map::TYPE_STRING) .Build()) + .AddPublicSymbol("android:attr/angle", ResourceId(0x01010004), + test::AttributeBuilder().Build()) // Add one real symbol that was introduces in v21 .AddPublicSymbol("android:attr/colorAccent", ResourceId(0x01010435), @@ -75,7 +77,7 @@ class XmlReferenceLinkerTest : public ::testing::Test { } protected: - std::unique_ptr<IAaptContext> context_; + std::unique_ptr<test::Context> context_; }; TEST_F(XmlReferenceLinkerTest, LinkBasicAttributes) { @@ -254,4 +256,63 @@ TEST_F(XmlReferenceLinkerTest, LinkViewWithLocalPackageAndAliasOfTheSameName) { EXPECT_EQ(make_value(ResourceId(0x7f030000)), ref->id); } + +TEST_F(XmlReferenceLinkerTest, AddAngleOnGradientForAndroidQ) { + std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <gradient />)"); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.Consume(context_.get(), doc.get())); + + xml::Element* gradient_el = doc->root.get(); + ASSERT_THAT(gradient_el, NotNull()); + + xml::Attribute* xml_attr = gradient_el->FindAttribute(xml::kSchemaAndroid, "angle"); + ASSERT_THAT(xml_attr, NotNull()); + ASSERT_TRUE(xml_attr->compiled_attribute); + EXPECT_EQ(make_value(ResourceId(0x01010004)), xml_attr->compiled_attribute.value().id); + + BinaryPrimitive* value = ValueCast<BinaryPrimitive>(xml_attr->compiled_value.get()); + ASSERT_THAT(value, NotNull()); + EXPECT_EQ(value->value.dataType, android::Res_value::TYPE_INT_DEC); + EXPECT_EQ(value->value.data, 0U); +} + +TEST_F(XmlReferenceLinkerTest, DoNotOverwriteAngleOnGradientForAndroidQ) { + std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:angle="90"/>)"); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.Consume(context_.get(), doc.get())); + + xml::Element* gradient_el = doc->root.get(); + ASSERT_THAT(gradient_el, NotNull()); + + xml::Attribute* xml_attr = gradient_el->FindAttribute(xml::kSchemaAndroid, "angle"); + ASSERT_THAT(xml_attr, NotNull()); + ASSERT_TRUE(xml_attr->compiled_attribute); + EXPECT_EQ(make_value(ResourceId(0x01010004)), xml_attr->compiled_attribute.value().id); + + BinaryPrimitive* value = ValueCast<BinaryPrimitive>(xml_attr->compiled_value.get()); + ASSERT_THAT(value, NotNull()); + EXPECT_EQ(value->value.dataType, android::Res_value::TYPE_INT_DEC); + EXPECT_EQ(value->value.data, 90U); +} + +TEST_F(XmlReferenceLinkerTest, DoNotOverwriteAngleOnGradientForPostAndroidQ) { + std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <gradient xmlns:android="http://schemas.android.com/apk/res/android" />)"); + context_->SetMinSdkVersion(30); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.Consume(context_.get(), doc.get())); + + xml::Element* gradient_el = doc->root.get(); + ASSERT_THAT(gradient_el, NotNull()); + + xml::Attribute* xml_attr = gradient_el->FindAttribute(xml::kSchemaAndroid, "angle"); + ASSERT_THAT(xml_attr, IsNull()); +} + } // namespace aapt diff --git a/tools/aapt2/test/Context.h b/tools/aapt2/test/Context.h index 7e10a59df877..553c43e6c469 100644 --- a/tools/aapt2/test/Context.h +++ b/tools/aapt2/test/Context.h @@ -81,6 +81,10 @@ class Context : public IAaptContext { return min_sdk_version_; } + void SetMinSdkVersion(int min_sdk_version) { + min_sdk_version_ = min_sdk_version; + } + const std::set<std::string>& GetSplitNameDependencies() override { return split_name_dependencies_; } diff --git a/tools/codegen/src/com/android/codegen/ClassInfo.kt b/tools/codegen/src/com/android/codegen/ClassInfo.kt index 5061be2091e5..92da9dab863b 100644 --- a/tools/codegen/src/com/android/codegen/ClassInfo.kt +++ b/tools/codegen/src/com/android/codegen/ClassInfo.kt @@ -38,6 +38,11 @@ open class ClassInfo(val sourceLines: List<String>) { val superInterfaces = (fileAst.types[0] as ClassOrInterfaceDeclaration) .implementedTypes.map { it.asString() } + val superClass = run { + val superClasses = (fileAst.types[0] as ClassOrInterfaceDeclaration).extendedTypes + if (superClasses.isNonEmpty) superClasses[0] else null + } + val ClassName = classAst.nameAsString private val genericArgsAst = classAst.typeParameters val genericArgs = if (genericArgsAst.isEmpty()) "" else { diff --git a/tools/codegen/src/com/android/codegen/ClassPrinter.kt b/tools/codegen/src/com/android/codegen/ClassPrinter.kt index 1f0d4b8a7ec9..bd72d9e7ec21 100644 --- a/tools/codegen/src/com/android/codegen/ClassPrinter.kt +++ b/tools/codegen/src/com/android/codegen/ClassPrinter.kt @@ -1,6 +1,7 @@ package com.android.codegen import com.github.javaparser.ast.Modifier +import com.github.javaparser.ast.body.CallableDeclaration import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration import com.github.javaparser.ast.body.TypeDeclaration import com.github.javaparser.ast.expr.* @@ -37,6 +38,7 @@ class ClassPrinter( val GeneratedMember by lazy { classRef("com.android.internal.util.DataClass.Generated.Member") } val Parcelling by lazy { classRef("com.android.internal.util.Parcelling") } val Parcelable by lazy { classRef("android.os.Parcelable") } + val Parcel by lazy { classRef("android.os.Parcel") } val UnsupportedAppUsage by lazy { classRef("android.annotation.UnsupportedAppUsage") } init { @@ -354,7 +356,9 @@ class ClassPrinter( } fun hasMethod(name: String, vararg argTypes: String): Boolean { - return classAst.methods.any { + val members: List<CallableDeclaration<*>> = + if (name == ClassName) classAst.constructors else classAst.methods + return members.any { it.name.asString() == name && it.parameters.map { it.type.asString() } == argTypes.toList() } @@ -365,6 +369,10 @@ class ClassPrinter( .mapIndexed { i, node -> FieldInfo(index = i, fieldAst = node, classInfo = this) } .filter { hasMethod("lazyInit${it.NameUpperCamel}") } + val extendsParcelableClass by lazy { + Parcelable !in superInterfaces && superClass != null + } + init { val builderFactoryOverride = classAst.methods.find { it.isStatic && it.nameAsString == "builder" diff --git a/tools/codegen/src/com/android/codegen/Generators.kt b/tools/codegen/src/com/android/codegen/Generators.kt index 914e475cfe41..5a95676c1dc8 100644 --- a/tools/codegen/src/com/android/codegen/Generators.kt +++ b/tools/codegen/src/com/android/codegen/Generators.kt @@ -341,7 +341,7 @@ private fun ClassPrinter.generateBuilderSetters(visibility: String) { } } - if (Type.contains("Map<")) { + if (FieldClass.endsWith("Map") && FieldInnerType != null) { generateBuilderMethod( name = adderName, defVisibility = visibility, @@ -422,6 +422,10 @@ fun ClassPrinter.generateParcelable() { +"// void parcelFieldName(Parcel dest, int flags) { ... }" +"" + if (extendsParcelableClass) { + +"super.writeToParcel(dest, flags);\n" + } + if (booleanFields.isNotEmpty() || nullableFields.isNotEmpty()) { +"$flagStorageType flg = 0;" booleanFields.forEachApply { @@ -463,6 +467,123 @@ fun ClassPrinter.generateParcelable() { +"" } + if (!hasMethod(ClassName, Parcel)) { + val visibility = if (classAst.isFinal) "/* package-private */" else "protected" + + +"/** @hide */" + +"@SuppressWarnings({\"unchecked\", \"RedundantCast\"})" + +GENERATED_MEMBER_HEADER + "$visibility $ClassName($Parcel in) {" { + +"// You can override field unparcelling by defining methods like:" + +"// static FieldType unparcelFieldName(Parcel in) { ... }" + +"" + + if (extendsParcelableClass) { + +"super(in);\n" + } + + if (booleanFields.isNotEmpty() || nullableFields.isNotEmpty()) { + +"$flagStorageType flg = in.read$FlagStorageType();" + } + booleanFields.forEachApply { + +"$Type $_name = (flg & $fieldBit) != 0;" + } + nonBooleanFields.forEachApply { + + // Handle customized parceling + val customParcellingMethod = "unparcel$NameUpperCamel" + if (hasMethod(customParcellingMethod, Parcel)) { + +"$Type $_name = $customParcellingMethod(in);" + } else if (customParcellingClass != null) { + +"$Type $_name = $sParcelling.unparcel(in);" + } else if (hasAnnotation("@$DataClassEnum")) { + val ordinal = "${_name}Ordinal" + +"int $ordinal = in.readInt();" + +"$Type $_name = $ordinal < 0 ? null : $FieldClass.values()[$ordinal];" + } else { + val methodArgs = mutableListOf<String>() + + // Create container if any + val containerInitExpr = when { + FieldClass.endsWith("Map") -> "new $LinkedHashMap<>()" + FieldClass == "List" || FieldClass == "ArrayList" -> + "new ${classRef("java.util.ArrayList")}<>()" + else -> "" + } + val passContainer = containerInitExpr.isNotEmpty() + + // nullcheck + + // "FieldType fieldName = (FieldType)" + if (passContainer) { + methodArgs.add(_name) + !"$Type $_name = " + if (mayBeNull) { + +"null;" + !"if ((flg & $fieldBit) != 0) {" + pushIndent() + +"" + !"$_name = " + } + +"$containerInitExpr;" + } else { + !"$Type $_name = " + if (mayBeNull) !"(flg & $fieldBit) == 0 ? null : " + if (ParcelMethodsSuffix == "StrongInterface") { + !"$FieldClass.Stub.asInterface(" + } else if (Type !in PRIMITIVE_TYPES + "String" + "Bundle" && + (!isArray || FieldInnerType !in PRIMITIVE_TYPES + "String") && + ParcelMethodsSuffix != "Parcelable") { + !"($FieldClass) " + } + } + + // Determine method args + when { + ParcelMethodsSuffix == "Parcelable" -> + methodArgs += "$FieldClass.class.getClassLoader()" + ParcelMethodsSuffix == "SparseArray" -> + methodArgs += "$FieldInnerClass.class.getClassLoader()" + ParcelMethodsSuffix == "TypedObject" -> + methodArgs += "$FieldClass.CREATOR" + ParcelMethodsSuffix == "TypedArray" -> + methodArgs += "$FieldInnerClass.CREATOR" + ParcelMethodsSuffix == "Map" -> + methodArgs += "${fieldTypeGenegicArgs[1].substringBefore("<")}.class.getClassLoader()" + ParcelMethodsSuffix.startsWith("Parcelable") + || (isList || isArray) + && FieldInnerType !in PRIMITIVE_TYPES + "String" -> + methodArgs += "$FieldInnerClass.class.getClassLoader()" + } + + // ...in.readFieldType(args...); + when { + ParcelMethodsSuffix == "StrongInterface" -> !"in.readStrongBinder" + isArray -> !"in.create$ParcelMethodsSuffix" + else -> !"in.read$ParcelMethodsSuffix" + } + !"(${methodArgs.joinToString(", ")})" + if (ParcelMethodsSuffix == "StrongInterface") !")" + +";" + + // Cleanup if passContainer + if (passContainer && mayBeNull) { + popIndent() + rmEmptyLine() + +"\n}" + } + } + } + + +"" + fields.forEachApply { + !"this." + generateSetFrom(_name) + } + + generateOnConstructedCallback() + } + } + if (classAst.fields.none { it.variables[0].nameAsString == "CREATOR" }) { val Creator = classRef("android.os.Parcelable.Creator") @@ -477,104 +598,8 @@ fun ClassPrinter.generateParcelable() { } +"@Override" - +"@SuppressWarnings({\"unchecked\", \"RedundantCast\"})" "public $ClassName createFromParcel($Parcel in)" { - +"// You can override field unparcelling by defining methods like:" - +"// static FieldType unparcelFieldName(Parcel in) { ... }" - +"" - if (booleanFields.isNotEmpty() || nullableFields.isNotEmpty()) { - +"$flagStorageType flg = in.read$FlagStorageType();" - } - booleanFields.forEachApply { - +"$Type $_name = (flg & $fieldBit) != 0;" - } - nonBooleanFields.forEachApply { - - // Handle customized parceling - val customParcellingMethod = "unparcel$NameUpperCamel" - if (hasMethod(customParcellingMethod, Parcel)) { - +"$Type $_name = $customParcellingMethod(in);" - } else if (customParcellingClass != null) { - +"$Type $_name = $sParcelling.unparcel(in);" - } else if (hasAnnotation("@$DataClassEnum")) { - val ordinal = "${_name}Ordinal" - +"int $ordinal = in.readInt();" - +"$Type $_name = $ordinal < 0 ? null : $FieldClass.values()[$ordinal];" - } else { - val methodArgs = mutableListOf<String>() - - // Create container if any - val containerInitExpr = when { - FieldClass.endsWith("Map") -> "new $LinkedHashMap<>()" - FieldClass == "List" || FieldClass == "ArrayList" -> - "new ${classRef("java.util.ArrayList")}<>()" - else -> "" - } - val passContainer = containerInitExpr.isNotEmpty() - - // nullcheck + - // "FieldType fieldName = (FieldType)" - if (passContainer) { - methodArgs.add(_name) - !"$Type $_name = " - if (mayBeNull) { - +"null;" - !"if ((flg & $fieldBit) != 0) {" - pushIndent() - +"" - !"$_name = " - } - +"$containerInitExpr;" - } else { - !"$Type $_name = " - if (mayBeNull) !"(flg & $fieldBit) == 0 ? null : " - if (ParcelMethodsSuffix == "StrongInterface") { - !"$FieldClass.Stub.asInterface(" - } else if (Type !in PRIMITIVE_TYPES + "String" + "Bundle" && - (!isArray || FieldInnerType !in PRIMITIVE_TYPES + "String") && - ParcelMethodsSuffix != "Parcelable") { - !"($Type) " - } - } - - // Determine method args - when { - ParcelMethodsSuffix == "Parcelable" -> - methodArgs += "$FieldClass.class.getClassLoader()" - ParcelMethodsSuffix == "TypedObject" -> - methodArgs += "$FieldClass.CREATOR" - ParcelMethodsSuffix == "TypedArray" -> - methodArgs += "$FieldInnerClass.CREATOR" - ParcelMethodsSuffix.startsWith("Parcelable") - || FieldClass == "Map" - || (isList || isArray) - && FieldInnerType !in PRIMITIVE_TYPES + "String" -> - methodArgs += "$FieldInnerClass.class.getClassLoader()" - } - - // ...in.readFieldType(args...); - when { - ParcelMethodsSuffix == "StrongInterface" -> !"in.readStrongBinder" - isArray -> !"in.create$ParcelMethodsSuffix" - else -> !"in.read$ParcelMethodsSuffix" - } - !"(${methodArgs.joinToString(", ")})" - if (ParcelMethodsSuffix == "StrongInterface") !")" - +";" - - // Cleanup if passContainer - if (passContainer && mayBeNull) { - popIndent() - rmEmptyLine() - +"\n}" - } - } - } - "return new $ClassType(" { - fields.forEachTrimmingTrailingComma { - +"$_name," - } - } + ";" + +"return new $ClassName(in);" } rmEmptyLine() } + ";" diff --git a/tools/codegen/src/com/android/codegen/Main.kt b/tools/codegen/src/com/android/codegen/Main.kt index 0f932f3c34e1..039f7b2fc627 100755 --- a/tools/codegen/src/com/android/codegen/Main.kt +++ b/tools/codegen/src/com/android/codegen/Main.kt @@ -95,7 +95,13 @@ In addition, for any field mMyField(or myField) of type FieldType you can define you can use with final fields. Version: $CODEGEN_VERSION -Questions? Feedback? Contact: eugenesusla@ + +Questions? Feedback? +Contact: eugenesusla@ +Bug/feature request: http://go/codegen-bug + +Slides: http://go/android-codegen +In-depth example: http://go/SampleDataClass """ fun main(args: Array<String>) { @@ -107,7 +113,7 @@ fun main(args: Array<String>) { println(CODEGEN_VERSION) System.exit(0) } - val file = File(args.last()) + val file = File(args.last()).absoluteFile val sourceLinesNoClosingBrace = file.readLines().dropLastWhile { it.startsWith("}") || it.all(Char::isWhitespace) } @@ -132,11 +138,11 @@ fun main(args: Array<String>) { // $GENERATED_WARNING_PREFIX v$CODEGEN_VERSION. // // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code // // To regenerate run: // $ $cliExecutable ${cliArgs.dropLast(1).joinToString("") { "$it " }}$fileEscaped - // - // CHECKSTYLE:OFF Generated code + """ if (FeatureFlag.CONST_DEFS()) generateConstDefs() @@ -146,8 +152,7 @@ fun main(args: Array<String>) { generateConstructor("public") } else if (FeatureFlag.BUILDER() || FeatureFlag.COPY_CONSTRUCTOR() - || FeatureFlag.WITHERS() - || FeatureFlag.PARCELABLE()) { + || FeatureFlag.WITHERS()) { generateConstructor("/* package-private */") } if (FeatureFlag.COPY_CONSTRUCTOR()) generateCopyConstructor() diff --git a/tools/codegen/src/com/android/codegen/SharedConstants.kt b/tools/codegen/src/com/android/codegen/SharedConstants.kt index 7d50ad10de00..a36f2c838787 100644 --- a/tools/codegen/src/com/android/codegen/SharedConstants.kt +++ b/tools/codegen/src/com/android/codegen/SharedConstants.kt @@ -1,7 +1,7 @@ package com.android.codegen const val CODEGEN_NAME = "codegen" -const val CODEGEN_VERSION = "1.0.0" +const val CODEGEN_VERSION = "1.0.5" const val CANONICAL_BUILDER_CLASS = "Builder" const val BASE_BUILDER_CLASS = "BaseBuilder" diff --git a/tools/protologtool/Android.bp b/tools/protologtool/Android.bp new file mode 100644 index 000000000000..d1a86c245dec --- /dev/null +++ b/tools/protologtool/Android.bp @@ -0,0 +1,33 @@ +java_library_host { + name: "protologtool-lib", + srcs: [ + "src/com/android/protolog/tool/**/*.kt", + ], + static_libs: [ + "protolog-common", + "javaparser", + "protolog-proto", + "jsonlib", + ], +} + +java_binary_host { + name: "protologtool", + manifest: "manifest.txt", + static_libs: [ + "protologtool-lib", + ], +} + +java_test_host { + name: "protologtool-tests", + test_suites: ["general-tests"], + srcs: [ + "tests/**/*.kt", + ], + static_libs: [ + "protologtool-lib", + "junit", + "mockito", + ], +} diff --git a/tools/protologtool/README.md b/tools/protologtool/README.md new file mode 100644 index 000000000000..ba639570f88c --- /dev/null +++ b/tools/protologtool/README.md @@ -0,0 +1,119 @@ +# ProtoLogTool + +Code transformation tool and viewer for ProtoLog. + +## What does it do? + +ProtoLogTool incorporates three different modes of operation: + +### Code transformation + +Command: `protologtool transform-protolog-calls + --protolog-class <protolog class name> + --protolog-impl-class <protolog implementation class name> + --loggroups-class <protolog groups class name> + --loggroups-jar <config jar path> + --output-srcjar <output.srcjar> + [<input.java>]` + +In this mode ProtoLogTool transforms every ProtoLog logging call in form of: +```java +ProtoLog.x(ProtoLogGroup.GROUP_NAME, "Format string %d %s", value1, value2); +``` +into: +```java +if (ProtoLogImpl.isEnabled(GROUP_NAME)) { + int protoLogParam0 = value1; + String protoLogParam1 = String.valueOf(value2); + ProtoLogImpl.x(ProtoLogGroup.GROUP_NAME, 123456, 0b0100, "Format string %d %s or null", protoLogParam0, protoLogParam1); +} +``` +where `ProtoLog`, `ProtoLogImpl` and `ProtoLogGroup` are the classes provided as arguments + (can be imported, static imported or full path, wildcard imports are not allowed) and, `x` is the + logging method. The transformation is done on the source level. A hash is generated from the format + string, log level and log group name and inserted after the `ProtoLogGroup` argument. After the hash + we insert a bitmask specifying the types of logged parameters. The format string is replaced + by `null` if `ProtoLogGroup.GROUP_NAME.isLogToLogcat()` returns false. If `ProtoLogGroup.GROUP_NAME.isEnabled()` + returns false the log statement is removed entirely from the resultant code. The real generated code is inlined + and a number of new line characters is added as to preserve line numbering in file. + +Input is provided as a list of java source file names. Transformed source is saved to a single +source jar file. The ProtoLogGroup class with all dependencies should be provided as a compiled +jar file (config.jar). + +### Viewer config generation + +Command: `generate-viewer-config + --protolog-class <protolog class name> + --loggroups-class <protolog groups class name> + --loggroups-jar <config jar path> + --viewer-conf <viewer.json> + [<input.java>]` + +This command is similar in it's syntax to the previous one, only instead of creating a processed source jar +it writes a viewer configuration file with following schema: +```json +{ + "version": "1.0.0", + "messages": { + "123456": { + "message": "Format string %d %s", + "level": "ERROR", + "group": "GROUP_NAME", + "at": "com\/android\/server\/example\/Class.java" + } + }, + "groups": { + "GROUP_NAME": { + "tag": "TestLog" + } + } +} + +``` + +### Binary log viewing + +Command: `read-log --viewer-conf <viewer.json> <wm_log.pb>` + +Reads the binary ProtoLog log file and outputs a human-readable LogCat-like text log. + +## What is ProtoLog? + +ProtoLog is a generic logging system created for the WindowManager project. It allows both binary and text logging +and is tunable in runtime. It consists of 3 different submodules: +* logging system built-in the Android app, +* log viewer for reading binary logs, +* a code processing tool. + +ProtoLog is designed to reduce both application size (and by that memory usage) and amount of resources needed +for logging. This is achieved by replacing log message strings with their hashes and only loading to memory/writing +full log messages when necessary. + +### Text logging + +For text-based logs Android LogCat is used as a backend. Message strings are loaded from a viewer config +located on the device when needed. + +### Binary logging + +Binary logs are saved as Protocol Buffers file. They can be read using the ProtoLog tool or specialised +viewer like Winscope. + +## How to use ProtoLog? + +### Adding a new logging group or log statement + +To add a new ProtoLogGroup simple create a new enum ProtoLogGroup member with desired parameters. + +To add a new logging statement just add a new call to ProtoLog.x where x is a log level. + +After doing any changes to logging groups or statements you should build the project and follow instructions printed by the tool. + +## How to change settings on device in runtime? +Use the `adb shell su root cmd window logging` command. To get help just type +`adb shell su root cmd window logging help`. + + + + diff --git a/tools/protologtool/TEST_MAPPING b/tools/protologtool/TEST_MAPPING new file mode 100644 index 000000000000..52b12dc26be9 --- /dev/null +++ b/tools/protologtool/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "protologtool-tests" + } + ] +} diff --git a/tools/protologtool/manifest.txt b/tools/protologtool/manifest.txt new file mode 100644 index 000000000000..cabebd51a2fa --- /dev/null +++ b/tools/protologtool/manifest.txt @@ -0,0 +1 @@ +Main-class: com.android.protolog.tool.ProtoLogTool diff --git a/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt new file mode 100644 index 000000000000..a52c8042582b --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.ImportDeclaration +import com.github.javaparser.ast.expr.BinaryExpr +import com.github.javaparser.ast.expr.Expression +import com.github.javaparser.ast.expr.StringLiteralExpr + +object CodeUtils { + /** + * Returns a stable hash of a string. + * We reimplement String::hashCode() for readability reasons. + */ + fun hash(position: String, messageString: String, logLevel: LogLevel, logGroup: LogGroup): Int { + return (position + messageString + logLevel.name + logGroup.name) + .map { c -> c.toInt() }.reduce { h, c -> h * 31 + c } + } + + fun checkWildcardStaticImported(code: CompilationUnit, className: String, fileName: String) { + code.findAll(ImportDeclaration::class.java) + .forEach { im -> + if (im.isStatic && im.isAsterisk && im.name.toString() == className) { + throw IllegalImportException("Wildcard static imports of $className " + + "methods are not supported.", ParsingContext(fileName, im)) + } + } + } + + fun isClassImportedOrSamePackage(code: CompilationUnit, className: String): Boolean { + val packageName = className.substringBeforeLast('.') + return code.packageDeclaration.isPresent && + code.packageDeclaration.get().nameAsString == packageName || + code.findAll(ImportDeclaration::class.java) + .any { im -> + !im.isStatic && + ((!im.isAsterisk && im.name.toString() == className) || + (im.isAsterisk && im.name.toString() == packageName)) + } + } + + fun staticallyImportedMethods(code: CompilationUnit, className: String): Set<String> { + return code.findAll(ImportDeclaration::class.java) + .filter { im -> + im.isStatic && + im.name.toString().substringBeforeLast('.') == className + } + .map { im -> im.name.toString().substringAfterLast('.') }.toSet() + } + + fun concatMultilineString(expr: Expression, context: ParsingContext): String { + return when (expr) { + is StringLiteralExpr -> expr.asString() + is BinaryExpr -> when { + expr.operator == BinaryExpr.Operator.PLUS -> + concatMultilineString(expr.left, context) + + concatMultilineString(expr.right, context) + else -> throw InvalidProtoLogCallException( + "expected a string literal " + + "or concatenation of string literals, got: $expr", context) + } + else -> throw InvalidProtoLogCallException("expected a string literal " + + "or concatenation of string literals, got: $expr", context) + } + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt new file mode 100644 index 000000000000..3dfa4d216cc2 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import java.util.regex.Pattern + +class CommandOptions(args: Array<String>) { + companion object { + const val TRANSFORM_CALLS_CMD = "transform-protolog-calls" + const val GENERATE_CONFIG_CMD = "generate-viewer-config" + const val READ_LOG_CMD = "read-log" + private val commands = setOf(TRANSFORM_CALLS_CMD, GENERATE_CONFIG_CMD, READ_LOG_CMD) + + private const val PROTOLOG_CLASS_PARAM = "--protolog-class" + private const val PROTOLOGIMPL_CLASS_PARAM = "--protolog-impl-class" + private const val PROTOLOGGROUP_CLASS_PARAM = "--loggroups-class" + private const val PROTOLOGGROUP_JAR_PARAM = "--loggroups-jar" + private const val VIEWER_CONFIG_JSON_PARAM = "--viewer-conf" + private const val OUTPUT_SOURCE_JAR_PARAM = "--output-srcjar" + private val parameters = setOf(PROTOLOG_CLASS_PARAM, PROTOLOGIMPL_CLASS_PARAM, + PROTOLOGGROUP_CLASS_PARAM, PROTOLOGGROUP_JAR_PARAM, VIEWER_CONFIG_JSON_PARAM, + OUTPUT_SOURCE_JAR_PARAM) + + val USAGE = """ + Usage: ${Constants.NAME} <command> [<args>] + Available commands: + + $TRANSFORM_CALLS_CMD $PROTOLOG_CLASS_PARAM <class name> $PROTOLOGIMPL_CLASS_PARAM + <class name> $PROTOLOGGROUP_CLASS_PARAM <class name> $PROTOLOGGROUP_JAR_PARAM + <config.jar> $OUTPUT_SOURCE_JAR_PARAM <output.srcjar> [<input.java>] + - processes java files replacing stub calls with logging code. + + $GENERATE_CONFIG_CMD $PROTOLOG_CLASS_PARAM <class name> $PROTOLOGGROUP_CLASS_PARAM + <class name> $PROTOLOGGROUP_JAR_PARAM <config.jar> $VIEWER_CONFIG_JSON_PARAM + <viewer.json> [<input.java>] + - creates viewer config file from given java files. + + $READ_LOG_CMD $VIEWER_CONFIG_JSON_PARAM <viewer.json> <wm_log.pb> + - translates a binary log to a readable format. + """.trimIndent() + + private fun validateClassName(name: String): String { + if (!Pattern.matches("^([a-z]+[A-Za-z0-9]*\\.)+([A-Za-z0-9]+)$", name)) { + throw InvalidCommandException("Invalid class name $name") + } + return name + } + + private fun getParam(paramName: String, params: Map<String, String>): String { + if (!params.containsKey(paramName)) { + throw InvalidCommandException("Param $paramName required") + } + return params.getValue(paramName) + } + + private fun validateNotSpecified(paramName: String, params: Map<String, String>): String { + if (params.containsKey(paramName)) { + throw InvalidCommandException("Unsupported param $paramName") + } + return "" + } + + private fun validateJarName(name: String): String { + if (!name.endsWith(".jar")) { + throw InvalidCommandException("Jar file required, got $name instead") + } + return name + } + + private fun validateSrcJarName(name: String): String { + if (!name.endsWith(".srcjar")) { + throw InvalidCommandException("Source jar file required, got $name instead") + } + return name + } + + private fun validateJSONName(name: String): String { + if (!name.endsWith(".json")) { + throw InvalidCommandException("Json file required, got $name instead") + } + return name + } + + private fun validateJavaInputList(list: List<String>): List<String> { + if (list.isEmpty()) { + throw InvalidCommandException("No java source input files") + } + list.forEach { name -> + if (!name.endsWith(".java")) { + throw InvalidCommandException("Not a java source file $name") + } + } + return list + } + + private fun validateLogInputList(list: List<String>): String { + if (list.isEmpty()) { + throw InvalidCommandException("No log input file") + } + if (list.size > 1) { + throw InvalidCommandException("Only one log input file allowed") + } + return list[0] + } + } + + val protoLogClassNameArg: String + val protoLogGroupsClassNameArg: String + val protoLogImplClassNameArg: String + val protoLogGroupsJarArg: String + val viewerConfigJsonArg: String + val outputSourceJarArg: String + val logProtofileArg: String + val javaSourceArgs: List<String> + val command: String + + init { + if (args.isEmpty()) { + throw InvalidCommandException("No command specified.") + } + command = args[0] + if (command !in commands) { + throw InvalidCommandException("Unknown command.") + } + + val params: MutableMap<String, String> = mutableMapOf() + val inputFiles: MutableList<String> = mutableListOf() + + var idx = 1 + while (idx < args.size) { + if (args[idx].startsWith("--")) { + if (idx + 1 >= args.size) { + throw InvalidCommandException("No value for ${args[idx]}") + } + if (args[idx] !in parameters) { + throw InvalidCommandException("Unknown parameter ${args[idx]}") + } + if (args[idx + 1].startsWith("--")) { + throw InvalidCommandException("No value for ${args[idx]}") + } + if (params.containsKey(args[idx])) { + throw InvalidCommandException("Duplicated parameter ${args[idx]}") + } + params[args[idx]] = args[idx + 1] + idx += 2 + } else { + inputFiles.add(args[idx]) + idx += 1 + } + } + + when (command) { + TRANSFORM_CALLS_CMD -> { + protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params)) + protoLogGroupsClassNameArg = validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM, + params)) + protoLogImplClassNameArg = validateClassName(getParam(PROTOLOGIMPL_CLASS_PARAM, + params)) + protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params)) + viewerConfigJsonArg = validateNotSpecified(VIEWER_CONFIG_JSON_PARAM, params) + outputSourceJarArg = validateSrcJarName(getParam(OUTPUT_SOURCE_JAR_PARAM, params)) + javaSourceArgs = validateJavaInputList(inputFiles) + logProtofileArg = "" + } + GENERATE_CONFIG_CMD -> { + protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params)) + protoLogGroupsClassNameArg = validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM, + params)) + protoLogImplClassNameArg = validateNotSpecified(PROTOLOGIMPL_CLASS_PARAM, params) + protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params)) + viewerConfigJsonArg = validateJSONName(getParam(VIEWER_CONFIG_JSON_PARAM, params)) + outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params) + javaSourceArgs = validateJavaInputList(inputFiles) + logProtofileArg = "" + } + READ_LOG_CMD -> { + protoLogClassNameArg = validateNotSpecified(PROTOLOG_CLASS_PARAM, params) + protoLogGroupsClassNameArg = validateNotSpecified(PROTOLOGGROUP_CLASS_PARAM, params) + protoLogImplClassNameArg = validateNotSpecified(PROTOLOGIMPL_CLASS_PARAM, params) + protoLogGroupsJarArg = validateNotSpecified(PROTOLOGGROUP_JAR_PARAM, params) + viewerConfigJsonArg = validateJSONName(getParam(VIEWER_CONFIG_JSON_PARAM, params)) + outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params) + javaSourceArgs = listOf() + logProtofileArg = validateLogInputList(inputFiles) + } + else -> { + throw InvalidCommandException("Unknown command.") + } + } + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/Constants.kt b/tools/protologtool/src/com/android/protolog/tool/Constants.kt new file mode 100644 index 000000000000..aa3e00f2f4db --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/Constants.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +object Constants { + const val NAME = "protologtool" + const val VERSION = "1.0.0" + const val IS_ENABLED_METHOD = "isEnabled" + const val ENUM_VALUES_METHOD = "values" +} diff --git a/tools/protologtool/src/com/android/protolog/tool/LogGroup.kt b/tools/protologtool/src/com/android/protolog/tool/LogGroup.kt new file mode 100644 index 000000000000..587f7b9db016 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/LogGroup.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +data class LogGroup( + val name: String, + val enabled: Boolean, + val textEnabled: Boolean, + val tag: String +) diff --git a/tools/protologtool/src/com/android/protolog/tool/LogLevel.kt b/tools/protologtool/src/com/android/protolog/tool/LogLevel.kt new file mode 100644 index 000000000000..e88f0f8231bd --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/LogLevel.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.ast.Node + +enum class LogLevel { + DEBUG, VERBOSE, INFO, WARN, ERROR, WTF; + + companion object { + fun getLevelForMethodName(name: String, node: Node, context: ParsingContext): LogLevel { + return when (name) { + "d" -> DEBUG + "v" -> VERBOSE + "i" -> INFO + "w" -> WARN + "e" -> ERROR + "wtf" -> WTF + else -> + throw InvalidProtoLogCallException("Unknown log level $name in $node", context) + } + } + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/LogParser.kt b/tools/protologtool/src/com/android/protolog/tool/LogParser.kt new file mode 100644 index 000000000000..a59038fc99a0 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/LogParser.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.json.stream.JsonReader +import com.android.server.protolog.common.InvalidFormatStringException +import com.android.server.protolog.common.LogDataType +import com.android.server.protolog.ProtoLogMessage +import com.android.server.protolog.ProtoLogFileProto +import java.io.BufferedReader +import java.io.InputStream +import java.io.InputStreamReader +import java.io.PrintStream +import java.lang.Exception +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Implements a simple parser/viewer for binary ProtoLog logs. + * A binary log is translated into Android "LogCat"-like text log. + */ +class LogParser(private val configParser: ViewerConfigParser) { + companion object { + private val dateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) + private val magicNumber = + ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or + ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_L.number.toLong() + } + + private fun printTime(time: Long, offset: Long, ps: PrintStream) { + ps.print(dateFormat.format(Date(time / 1000000 + offset)) + " ") + } + + private fun printFormatted( + protoLogMessage: ProtoLogMessage, + configEntry: ViewerConfigParser.ConfigEntry, + ps: PrintStream + ) { + val strParmIt = protoLogMessage.strParamsList.iterator() + val longParamsIt = protoLogMessage.sint64ParamsList.iterator() + val doubleParamsIt = protoLogMessage.doubleParamsList.iterator() + val boolParamsIt = protoLogMessage.booleanParamsList.iterator() + val args = mutableListOf<Any>() + val format = configEntry.messageString + val argTypes = LogDataType.parseFormatString(format) + try { + argTypes.forEach { + when (it) { + LogDataType.BOOLEAN -> args.add(boolParamsIt.next()) + LogDataType.LONG -> args.add(longParamsIt.next()) + LogDataType.DOUBLE -> args.add(doubleParamsIt.next()) + LogDataType.STRING -> args.add(strParmIt.next()) + null -> throw NullPointerException() + } + } + } catch (ex: NoSuchElementException) { + throw InvalidFormatStringException("Invalid format string in config", ex) + } + if (strParmIt.hasNext() || longParamsIt.hasNext() || + doubleParamsIt.hasNext() || boolParamsIt.hasNext()) { + throw RuntimeException("Invalid format string in config - no enough matchers") + } + val formatted = format.format(*(args.toTypedArray())) + ps.print("${configEntry.level} ${configEntry.tag}: $formatted\n") + } + + private fun printUnformatted(protoLogMessage: ProtoLogMessage, ps: PrintStream, tag: String) { + ps.println("$tag: ${protoLogMessage.messageHash} - ${protoLogMessage.strParamsList}" + + " ${protoLogMessage.sint64ParamsList} ${protoLogMessage.doubleParamsList}" + + " ${protoLogMessage.booleanParamsList}") + } + + fun parse(protoLogInput: InputStream, jsonConfigInput: InputStream, ps: PrintStream) { + val jsonReader = JsonReader(BufferedReader(InputStreamReader(jsonConfigInput))) + val config = configParser.parseConfig(jsonReader) + val protoLog = ProtoLogFileProto.parseFrom(protoLogInput) + + if (protoLog.magicNumber != magicNumber) { + throw InvalidInputException("ProtoLog file magic number is invalid.") + } + if (protoLog.version != Constants.VERSION) { + throw InvalidInputException("ProtoLog file version not supported by this tool," + + " log version ${protoLog.version}, viewer version ${Constants.VERSION}") + } + + protoLog.logList.forEach { log -> + printTime(log.elapsedRealtimeNanos, protoLog.realTimeToElapsedTimeOffsetMillis, ps) + if (log.messageHash !in config) { + printUnformatted(log, ps, "UNKNOWN") + } else { + val conf = config.getValue(log.messageHash) + try { + printFormatted(log, conf, ps) + } catch (ex: Exception) { + printUnformatted(log, ps, "INVALID") + } + } + } + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ParsingContext.kt b/tools/protologtool/src/com/android/protolog/tool/ParsingContext.kt new file mode 100644 index 000000000000..c6aedfc3093e --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ParsingContext.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.ast.Node + +data class ParsingContext(val filePath: String, val lineNumber: Int) { + constructor(filePath: String, node: Node) + : this(filePath, if (node.range.isPresent) node.range.get().begin.line else -1) + + constructor() : this("", -1) +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt new file mode 100644 index 000000000000..2181cf680f6c --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.expr.Expression +import com.github.javaparser.ast.expr.FieldAccessExpr +import com.github.javaparser.ast.expr.MethodCallExpr +import com.github.javaparser.ast.expr.NameExpr + +/** + * Helper class for visiting all ProtoLog calls. + * For every valid call in the given {@code CompilationUnit} a {@code ProtoLogCallVisitor} callback + * is executed. + */ +open class ProtoLogCallProcessor( + private val protoLogClassName: String, + private val protoLogGroupClassName: String, + private val groupMap: Map<String, LogGroup> +) { + private val protoLogSimpleClassName = protoLogClassName.substringAfterLast('.') + private val protoLogGroupSimpleClassName = protoLogGroupClassName.substringAfterLast('.') + + private fun getLogGroupName( + expr: Expression, + isClassImported: Boolean, + staticImports: Set<String>, + fileName: String + ): String { + val context = ParsingContext(fileName, expr) + return when (expr) { + is NameExpr -> when { + expr.nameAsString in staticImports -> expr.nameAsString + else -> + throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup: $expr", + context) + } + is FieldAccessExpr -> when { + expr.scope.toString() == protoLogGroupClassName + || isClassImported && + expr.scope.toString() == protoLogGroupSimpleClassName -> expr.nameAsString + else -> + throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup: $expr", + context) + } + else -> throw InvalidProtoLogCallException("Invalid group argument " + + "- must be ProtoLogGroup enum member reference: $expr", context) + } + } + + private fun isProtoCall( + call: MethodCallExpr, + isLogClassImported: Boolean, + staticLogImports: Collection<String> + ): Boolean { + return call.scope.isPresent && call.scope.get().toString() == protoLogClassName || + isLogClassImported && call.scope.isPresent && + call.scope.get().toString() == protoLogSimpleClassName || + !call.scope.isPresent && staticLogImports.contains(call.name.toString()) + } + + open fun process(code: CompilationUnit, callVisitor: ProtoLogCallVisitor?, fileName: String): + CompilationUnit { + CodeUtils.checkWildcardStaticImported(code, protoLogClassName, fileName) + CodeUtils.checkWildcardStaticImported(code, protoLogGroupClassName, fileName) + + val isLogClassImported = CodeUtils.isClassImportedOrSamePackage(code, protoLogClassName) + val staticLogImports = CodeUtils.staticallyImportedMethods(code, protoLogClassName) + val isGroupClassImported = CodeUtils.isClassImportedOrSamePackage(code, + protoLogGroupClassName) + val staticGroupImports = CodeUtils.staticallyImportedMethods(code, protoLogGroupClassName) + + code.findAll(MethodCallExpr::class.java) + .filter { call -> + isProtoCall(call, isLogClassImported, staticLogImports) + }.forEach { call -> + val context = ParsingContext(fileName, call) + if (call.arguments.size < 2) { + throw InvalidProtoLogCallException("Method signature does not match " + + "any ProtoLog method: $call", context) + } + + val messageString = CodeUtils.concatMultilineString(call.getArgument(1), + context) + val groupNameArg = call.getArgument(0) + val groupName = + getLogGroupName(groupNameArg, isGroupClassImported, + staticGroupImports, fileName) + if (groupName !in groupMap) { + throw InvalidProtoLogCallException("Unknown group argument " + + "- not a ProtoLogGroup enum member: $call", context) + } + + callVisitor?.processCall(call, messageString, LogLevel.getLevelForMethodName( + call.name.toString(), call, context), groupMap.getValue(groupName)) + } + return code + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt new file mode 100644 index 000000000000..aa58b69d61cb --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.ast.expr.MethodCallExpr + +interface ProtoLogCallVisitor { + fun processCall(call: MethodCallExpr, messageString: String, level: LogLevel, group: LogGroup) +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogGroupReader.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogGroupReader.kt new file mode 100644 index 000000000000..75493b6427cb --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogGroupReader.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.protolog.tool.Constants.ENUM_VALUES_METHOD +import com.android.server.protolog.common.IProtoLogGroup +import java.io.File +import java.net.URLClassLoader + +class ProtoLogGroupReader { + private fun getClassloaderForJar(jarPath: String): ClassLoader { + val jarFile = File(jarPath) + val url = jarFile.toURI().toURL() + return URLClassLoader(arrayOf(url), ProtoLogGroupReader::class.java.classLoader) + } + + private fun getEnumValues(clazz: Class<*>): List<IProtoLogGroup> { + val valuesMethod = clazz.getMethod(ENUM_VALUES_METHOD) + @Suppress("UNCHECKED_CAST") + return (valuesMethod.invoke(null) as Array<IProtoLogGroup>).toList() + } + + fun loadFromJar(jarPath: String, className: String): Map<String, LogGroup> { + try { + val classLoader = getClassloaderForJar(jarPath) + val clazz = classLoader.loadClass(className) + val values = getEnumValues(clazz) + return values.map { group -> + group.name() to + LogGroup(group.name(), group.isEnabled, group.isLogToLogcat, group.tag) + }.toMap() + } catch (ex: ReflectiveOperationException) { + throw RuntimeException("Unable to load ProtoLogGroup enum class", ex) + } + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt new file mode 100644 index 000000000000..70ac0bee59b3 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.protolog.tool.CommandOptions.Companion.USAGE +import com.github.javaparser.ParseProblemException +import com.github.javaparser.StaticJavaParser +import com.github.javaparser.ast.CompilationUnit +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.jar.JarOutputStream +import java.util.zip.ZipEntry +import kotlin.system.exitProcess + +object ProtoLogTool { + private fun showHelpAndExit() { + println(USAGE) + exitProcess(-1) + } + + private fun containsProtoLogText(source: String, protoLogClassName: String): Boolean { + val protoLogSimpleClassName = protoLogClassName.substringAfterLast('.') + return source.contains(protoLogSimpleClassName) + } + + private fun processClasses(command: CommandOptions) { + val groups = ProtoLogGroupReader() + .loadFromJar(command.protoLogGroupsJarArg, command.protoLogGroupsClassNameArg) + val out = FileOutputStream(command.outputSourceJarArg) + val outJar = JarOutputStream(out) + val processor = ProtoLogCallProcessor(command.protoLogClassNameArg, + command.protoLogGroupsClassNameArg, groups) + val transformer = SourceTransformer(command.protoLogImplClassNameArg, processor) + + command.javaSourceArgs.forEach { path -> + val file = File(path) + val text = file.readText() + val newPath = path + val outSrc = try { + val code = tryParse(text, path) + if (containsProtoLogText(text, command.protoLogClassNameArg)) { + transformer.processClass(text, newPath, code) + } else { + text + } + } catch (ex: ParsingException) { + // If we cannot parse this file, skip it (and log why). Compilation will fail + // in a subsequent build step. + println("\n${ex.message}\n") + text + } + outJar.putNextEntry(ZipEntry(newPath)) + outJar.write(outSrc.toByteArray()) + outJar.closeEntry() + } + + outJar.close() + out.close() + } + + private fun tryParse(code: String, fileName: String): CompilationUnit { + try { + return StaticJavaParser.parse(code) + } catch (ex: ParseProblemException) { + val problem = ex.problems.first() + throw ParsingException("Java parsing erro" + + "r: ${problem.verboseMessage}", + ParsingContext(fileName, problem.location.orElse(null) + ?.begin?.range?.orElse(null)?.begin?.line + ?: 0)) + } + } + + private fun viewerConf(command: CommandOptions) { + val groups = ProtoLogGroupReader() + .loadFromJar(command.protoLogGroupsJarArg, command.protoLogGroupsClassNameArg) + val processor = ProtoLogCallProcessor(command.protoLogClassNameArg, + command.protoLogGroupsClassNameArg, groups) + val builder = ViewerConfigBuilder(processor) + command.javaSourceArgs.forEach { path -> + val file = File(path) + val text = file.readText() + if (containsProtoLogText(text, command.protoLogClassNameArg)) { + try { + val code = tryParse(text, path) + val pack = if (code.packageDeclaration.isPresent) code.packageDeclaration + .get().nameAsString else "" + val newPath = pack.replace('.', '/') + '/' + file.name + builder.processClass(code, newPath) + } catch (ex: ParsingException) { + // If we cannot parse this file, skip it (and log why). Compilation will fail + // in a subsequent build step. + println("\n${ex.message}\n") + } + } + } + val out = FileOutputStream(command.viewerConfigJsonArg) + out.write(builder.build().toByteArray()) + out.close() + } + + private fun read(command: CommandOptions) { + LogParser(ViewerConfigParser()) + .parse(FileInputStream(command.logProtofileArg), + FileInputStream(command.viewerConfigJsonArg), System.out) + } + + @JvmStatic + fun main(args: Array<String>) { + try { + val command = CommandOptions(args) + when (command.command) { + CommandOptions.TRANSFORM_CALLS_CMD -> processClasses(command) + CommandOptions.GENERATE_CONFIG_CMD -> viewerConf(command) + CommandOptions.READ_LOG_CMD -> read(command) + } + } catch (ex: InvalidCommandException) { + println("\n${ex.message}\n") + showHelpAndExit() + } catch (ex: CodeProcessingException) { + println("\n${ex.message}\n") + exitProcess(1) + } + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt new file mode 100644 index 000000000000..00fd038079f9 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.protolog.tool.Constants.IS_ENABLED_METHOD +import com.android.server.protolog.common.LogDataType +import com.github.javaparser.StaticJavaParser +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.NodeList +import com.github.javaparser.ast.body.VariableDeclarator +import com.github.javaparser.ast.expr.BooleanLiteralExpr +import com.github.javaparser.ast.expr.CastExpr +import com.github.javaparser.ast.expr.Expression +import com.github.javaparser.ast.expr.FieldAccessExpr +import com.github.javaparser.ast.expr.IntegerLiteralExpr +import com.github.javaparser.ast.expr.MethodCallExpr +import com.github.javaparser.ast.expr.NameExpr +import com.github.javaparser.ast.expr.NullLiteralExpr +import com.github.javaparser.ast.expr.SimpleName +import com.github.javaparser.ast.expr.TypeExpr +import com.github.javaparser.ast.expr.VariableDeclarationExpr +import com.github.javaparser.ast.stmt.BlockStmt +import com.github.javaparser.ast.stmt.ExpressionStmt +import com.github.javaparser.ast.stmt.IfStmt +import com.github.javaparser.ast.type.ArrayType +import com.github.javaparser.ast.type.ClassOrInterfaceType +import com.github.javaparser.ast.type.PrimitiveType +import com.github.javaparser.ast.type.Type +import com.github.javaparser.printer.PrettyPrinter +import com.github.javaparser.printer.PrettyPrinterConfiguration +import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter + +class SourceTransformer( + protoLogImplClassName: String, + private val protoLogCallProcessor: ProtoLogCallProcessor +) : ProtoLogCallVisitor { + override fun processCall( + call: MethodCallExpr, + messageString: String, + level: LogLevel, + group: LogGroup + ) { + // Input format: ProtoLog.e(GROUP, "msg %d", arg) + if (!call.parentNode.isPresent) { + // Should never happen + throw RuntimeException("Unable to process log call $call " + + "- no parent node in AST") + } + if (call.parentNode.get() !is ExpressionStmt) { + // Should never happen + throw RuntimeException("Unable to process log call $call " + + "- parent node in AST is not an ExpressionStmt") + } + val parentStmt = call.parentNode.get() as ExpressionStmt + if (!parentStmt.parentNode.isPresent) { + // Should never happen + throw RuntimeException("Unable to process log call $call " + + "- no grandparent node in AST") + } + val ifStmt: IfStmt + if (group.enabled) { + val hash = CodeUtils.hash(fileName, messageString, level, group) + val newCall = call.clone() + if (!group.textEnabled) { + // Remove message string if text logging is not enabled by default. + // Out: ProtoLog.e(GROUP, null, arg) + newCall.arguments[1].replace(NameExpr("null")) + } + // Insert message string hash as a second argument. + // Out: ProtoLog.e(GROUP, 1234, null, arg) + newCall.arguments.add(1, IntegerLiteralExpr(hash)) + val argTypes = LogDataType.parseFormatString(messageString) + val typeMask = LogDataType.logDataTypesToBitMask(argTypes) + // Insert bitmap representing which Number parameters are to be considered as + // floating point numbers. + // Out: ProtoLog.e(GROUP, 1234, 0, null, arg) + newCall.arguments.add(2, IntegerLiteralExpr(typeMask)) + // Replace call to a stub method with an actual implementation. + // Out: com.android.server.protolog.ProtoLogImpl.e(GROUP, 1234, null, arg) + newCall.setScope(protoLogImplClassNode) + // Create a call to ProtoLogImpl.isEnabled(GROUP) + // Out: com.android.server.protolog.ProtoLogImpl.isEnabled(GROUP) + val isLogEnabled = MethodCallExpr(protoLogImplClassNode, IS_ENABLED_METHOD, + NodeList<Expression>(newCall.arguments[0].clone())) + if (argTypes.size != call.arguments.size - 2) { + throw InvalidProtoLogCallException( + "Number of arguments (${argTypes.size} does not mach format" + + " string in: $call", ParsingContext(fileName, call)) + } + val blockStmt = BlockStmt() + if (argTypes.isNotEmpty()) { + // Assign every argument to a variable to check its type in compile time + // (this is assignment is optimized-out by dex tool, there is no runtime impact)/ + // Out: long protoLogParam0 = arg + argTypes.forEachIndexed { idx, type -> + val varName = "protoLogParam$idx" + val declaration = VariableDeclarator(getASTTypeForDataType(type), varName, + getConversionForType(type)(newCall.arguments[idx + 4].clone())) + blockStmt.addStatement(ExpressionStmt(VariableDeclarationExpr(declaration))) + newCall.setArgument(idx + 4, NameExpr(SimpleName(varName))) + } + } else { + // Assign (Object[])null as the vararg parameter to prevent allocating an empty + // object array. + val nullArray = CastExpr(ArrayType(objectType), NullLiteralExpr()) + newCall.addArgument(nullArray) + } + blockStmt.addStatement(ExpressionStmt(newCall)) + // Create an IF-statement with the previously created condition. + // Out: if (com.android.server.protolog.ProtoLogImpl.isEnabled(GROUP)) { + // long protoLogParam0 = arg; + // com.android.server.protolog.ProtoLogImpl.e(GROUP, 1234, 0, null, protoLogParam0); + // } + ifStmt = IfStmt(isLogEnabled, blockStmt, null) + } else { + // Surround with if (false). + val newCall = parentStmt.clone() + ifStmt = IfStmt(BooleanLiteralExpr(false), BlockStmt(NodeList(newCall)), null) + newCall.setBlockComment(" ${group.name} is disabled ") + } + // Inline the new statement. + val printedIfStmt = inlinePrinter.print(ifStmt) + // Append blank lines to preserve line numbering in file (to allow debugging) + val newLines = LexicalPreservingPrinter.print(parentStmt).count { c -> c == '\n' } + val newStmt = printedIfStmt.substringBeforeLast('}') + ("\n".repeat(newLines)) + '}' + // pre-workaround code, see explanation below + /* + val inlinedIfStmt = StaticJavaParser.parseStatement(newStmt) + LexicalPreservingPrinter.setup(inlinedIfStmt) + // Replace the original call. + if (!parentStmt.replace(inlinedIfStmt)) { + // Should never happen + throw RuntimeException("Unable to process log call $call " + + "- unable to replace the call.") + } + */ + /** Workaround for a bug in JavaParser (AST tree invalid after replacing a node when using + * LexicalPreservingPrinter (https://github.com/javaparser/javaparser/issues/2290). + * Replace the code below with the one commended-out above one the issue is resolved. */ + if (!parentStmt.range.isPresent) { + // Should never happen + throw RuntimeException("Unable to process log call $call " + + "- unable to replace the call.") + } + val range = parentStmt.range.get() + val begin = range.begin.line - 1 + val oldLines = processedCode.subList(begin, range.end.line) + val oldCode = oldLines.joinToString("\n") + val newCode = oldCode.replaceRange( + offsets[begin] + range.begin.column - 1, + oldCode.length - oldLines.lastOrNull()!!.length + + range.end.column + offsets[range.end.line - 1], newStmt) + newCode.split("\n").forEachIndexed { idx, line -> + offsets[begin + idx] += line.length - processedCode[begin + idx].length + processedCode[begin + idx] = line + } + } + + private val inlinePrinter: PrettyPrinter + private val objectType = StaticJavaParser.parseClassOrInterfaceType("Object") + + init { + val config = PrettyPrinterConfiguration() + config.endOfLineCharacter = " " + config.indentSize = 0 + config.tabWidth = 1 + inlinePrinter = PrettyPrinter(config) + } + + companion object { + private val stringType: ClassOrInterfaceType = + StaticJavaParser.parseClassOrInterfaceType("String") + + fun getASTTypeForDataType(type: Int): Type { + return when (type) { + LogDataType.STRING -> stringType.clone() + LogDataType.LONG -> PrimitiveType.longType() + LogDataType.DOUBLE -> PrimitiveType.doubleType() + LogDataType.BOOLEAN -> PrimitiveType.booleanType() + else -> { + // Should never happen. + throw RuntimeException("Invalid LogDataType") + } + } + } + + fun getConversionForType(type: Int): (Expression) -> Expression { + return when (type) { + LogDataType.STRING -> { expr -> + MethodCallExpr(TypeExpr(StaticJavaParser.parseClassOrInterfaceType("String")), + SimpleName("valueOf"), NodeList(expr)) + } + else -> { expr -> expr } + } + } + } + + private val protoLogImplClassNode = + StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogImplClassName) + private var processedCode: MutableList<String> = mutableListOf() + private var offsets: IntArray = IntArray(0) + private var fileName: String = "" + + fun processClass( + code: String, + path: String, + compilationUnit: CompilationUnit = + StaticJavaParser.parse(code) + ): String { + fileName = path + processedCode = code.split('\n').toMutableList() + offsets = IntArray(processedCode.size) + LexicalPreservingPrinter.setup(compilationUnit) + protoLogCallProcessor.process(compilationUnit, this, fileName) + // return LexicalPreservingPrinter.print(compilationUnit) + return processedCode.joinToString("\n") + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt new file mode 100644 index 000000000000..941455a24ec8 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.json.stream.JsonWriter +import com.github.javaparser.ast.CompilationUnit +import com.android.protolog.tool.Constants.VERSION +import com.github.javaparser.ast.expr.MethodCallExpr +import java.io.StringWriter + +class ViewerConfigBuilder( + private val protoLogCallVisitor: ProtoLogCallProcessor +) : ProtoLogCallVisitor { + override fun processCall( + call: MethodCallExpr, + messageString: String, + level: LogLevel, + group: LogGroup + ) { + if (group.enabled) { + val position = fileName + val key = CodeUtils.hash(position, messageString, level, group) + if (statements.containsKey(key)) { + if (statements[key] != LogCall(messageString, level, group, position)) { + throw HashCollisionException( + "Please modify the log message \"$messageString\" " + + "or \"${statements[key]}\" - their hashes are equal.", + ParsingContext(fileName, call)) + } + } else { + groups.add(group) + statements[key] = LogCall(messageString, level, group, position) + call.range.isPresent + } + } + } + + private val statements: MutableMap<Int, LogCall> = mutableMapOf() + private val groups: MutableSet<LogGroup> = mutableSetOf() + private var fileName: String = "" + + fun processClass(unit: CompilationUnit, fileName: String) { + this.fileName = fileName + protoLogCallVisitor.process(unit, this, fileName) + } + + fun build(): String { + val stringWriter = StringWriter() + val writer = JsonWriter(stringWriter) + writer.setIndent(" ") + writer.beginObject() + writer.name("version") + writer.value(VERSION) + writer.name("messages") + writer.beginObject() + statements.toSortedMap().forEach { (key, value) -> + writer.name(key.toString()) + writer.beginObject() + writer.name("message") + writer.value(value.messageString) + writer.name("level") + writer.value(value.logLevel.name) + writer.name("group") + writer.value(value.logGroup.name) + writer.name("at") + writer.value(value.position) + writer.endObject() + } + writer.endObject() + writer.name("groups") + writer.beginObject() + groups.toSortedSet(Comparator { o1, o2 -> o1.name.compareTo(o2.name) }).forEach { group -> + writer.name(group.name) + writer.beginObject() + writer.name("tag") + writer.value(group.tag) + writer.endObject() + } + writer.endObject() + writer.endObject() + stringWriter.buffer.append('\n') + return stringWriter.toString() + } + + data class LogCall( + val messageString: String, + val logLevel: LogLevel, + val logGroup: LogGroup, + val position: String + ) +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt new file mode 100644 index 000000000000..7278db0094e6 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.json.stream.JsonReader + +open class ViewerConfigParser { + data class MessageEntry( + val messageString: String, + val level: String, + val groupName: String + ) + + fun parseMessage(jsonReader: JsonReader): MessageEntry { + jsonReader.beginObject() + var message: String? = null + var level: String? = null + var groupName: String? = null + while (jsonReader.hasNext()) { + when (jsonReader.nextName()) { + "message" -> message = jsonReader.nextString() + "level" -> level = jsonReader.nextString() + "group" -> groupName = jsonReader.nextString() + else -> jsonReader.skipValue() + } + } + jsonReader.endObject() + if (message.isNullOrBlank() || level.isNullOrBlank() || groupName.isNullOrBlank()) { + throw InvalidViewerConfigException("Invalid message entry in viewer config") + } + return MessageEntry(message, level, groupName) + } + + data class GroupEntry(val tag: String) + + fun parseGroup(jsonReader: JsonReader): GroupEntry { + jsonReader.beginObject() + var tag: String? = null + while (jsonReader.hasNext()) { + when (jsonReader.nextName()) { + "tag" -> tag = jsonReader.nextString() + else -> jsonReader.skipValue() + } + } + jsonReader.endObject() + if (tag.isNullOrBlank()) { + throw InvalidViewerConfigException("Invalid group entry in viewer config") + } + return GroupEntry(tag) + } + + fun parseMessages(jsonReader: JsonReader): Map<Int, MessageEntry> { + val config: MutableMap<Int, MessageEntry> = mutableMapOf() + jsonReader.beginObject() + while (jsonReader.hasNext()) { + val key = jsonReader.nextName() + val hash = key.toIntOrNull() + ?: throw InvalidViewerConfigException("Invalid key in messages viewer config") + config[hash] = parseMessage(jsonReader) + } + jsonReader.endObject() + return config + } + + fun parseGroups(jsonReader: JsonReader): Map<String, GroupEntry> { + val config: MutableMap<String, GroupEntry> = mutableMapOf() + jsonReader.beginObject() + while (jsonReader.hasNext()) { + val key = jsonReader.nextName() + config[key] = parseGroup(jsonReader) + } + jsonReader.endObject() + return config + } + + data class ConfigEntry(val messageString: String, val level: String, val tag: String) + + open fun parseConfig(jsonReader: JsonReader): Map<Int, ConfigEntry> { + var messages: Map<Int, MessageEntry>? = null + var groups: Map<String, GroupEntry>? = null + var version: String? = null + + jsonReader.beginObject() + while (jsonReader.hasNext()) { + when (jsonReader.nextName()) { + "messages" -> messages = parseMessages(jsonReader) + "groups" -> groups = parseGroups(jsonReader) + "version" -> version = jsonReader.nextString() + + else -> jsonReader.skipValue() + } + } + jsonReader.endObject() + if (messages == null || groups == null || version == null) { + throw InvalidViewerConfigException("Invalid config - definitions missing") + } + if (version != Constants.VERSION) { + throw InvalidViewerConfigException("Viewer config version not supported by this tool," + + " config version $version, viewer version ${Constants.VERSION}") + } + return messages.map { msg -> + msg.key to ConfigEntry( + msg.value.messageString, msg.value.level, groups[msg.value.groupName]?.tag + ?: throw InvalidViewerConfigException( + "Group definition missing for ${msg.value.groupName}")) + }.toMap() + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/exceptions.kt b/tools/protologtool/src/com/android/protolog/tool/exceptions.kt new file mode 100644 index 000000000000..ae00df123353 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/exceptions.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import java.lang.Exception + +open class CodeProcessingException(message: String, context: ParsingContext) + : Exception("Code processing error in ${context.filePath}:${context.lineNumber}:\n" + + " $message") + +class HashCollisionException(message: String, context: ParsingContext) : + CodeProcessingException(message, context) + +class IllegalImportException(message: String, context: ParsingContext) : + CodeProcessingException("Illegal import: $message", context) + +class InvalidProtoLogCallException(message: String, context: ParsingContext) + : CodeProcessingException("InvalidProtoLogCall: $message", context) + +class ParsingException(message: String, context: ParsingContext) + : CodeProcessingException(message, context) + +class InvalidViewerConfigException(message: String) : Exception(message) + +class InvalidInputException(message: String) : Exception(message) + +class InvalidCommandException(message: String) : Exception(message) diff --git a/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt b/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt new file mode 100644 index 000000000000..b916f8f00a68 --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.StaticJavaParser +import com.github.javaparser.ast.expr.BinaryExpr +import com.github.javaparser.ast.expr.StringLiteralExpr +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class CodeUtilsTest { + @Test + fun hash() { + assertEquals(-1259556708, CodeUtils.hash("Test.java:50", "test", + LogLevel.DEBUG, LogGroup("test", true, true, "TAG"))) + } + + @Test + fun hash_changeLocation() { + assertEquals(15793504, CodeUtils.hash("Test.java:10", "test2", + LogLevel.DEBUG, LogGroup("test", true, true, "TAG"))) + } + + @Test + fun hash_changeLevel() { + assertEquals(-731772463, CodeUtils.hash("Test.java:50", "test", + LogLevel.ERROR, LogGroup("test", true, true, "TAG"))) + } + + @Test + fun hash_changeMessage() { + assertEquals(-2026343204, CodeUtils.hash("Test.java:50", "test2", + LogLevel.DEBUG, LogGroup("test", true, true, "TAG"))) + } + + @Test + fun hash_changeGroup() { + assertEquals(1607870166, CodeUtils.hash("Test.java:50", "test2", + LogLevel.DEBUG, LogGroup("test2", true, true, "TAG"))) + } + + @Test(expected = IllegalImportException::class) + fun checkWildcardStaticImported_true() { + val code = """package org.example.test; + import static org.example.Test.*; + """ + CodeUtils.checkWildcardStaticImported( + StaticJavaParser.parse(code), "org.example.Test", "") + } + + @Test + fun checkWildcardStaticImported_notStatic() { + val code = """package org.example.test; + import org.example.Test.*; + """ + CodeUtils.checkWildcardStaticImported( + StaticJavaParser.parse(code), "org.example.Test", "") + } + + @Test + fun checkWildcardStaticImported_differentClass() { + val code = """package org.example.test; + import static org.example.Test2.*; + """ + CodeUtils.checkWildcardStaticImported( + StaticJavaParser.parse(code), "org.example.Test", "") + } + + @Test + fun checkWildcardStaticImported_notWildcard() { + val code = """package org.example.test; + import org.example.Test.test; + """ + CodeUtils.checkWildcardStaticImported( + StaticJavaParser.parse(code), "org.example.Test", "") + } + + @Test + fun isClassImportedOrSamePackage_imported() { + val code = """package org.example.test; + import org.example.Test; + """ + assertTrue(CodeUtils.isClassImportedOrSamePackage( + StaticJavaParser.parse(code), "org.example.Test")) + } + + @Test + fun isClassImportedOrSamePackage_samePackage() { + val code = """package org.example.test; + """ + assertTrue(CodeUtils.isClassImportedOrSamePackage( + StaticJavaParser.parse(code), "org.example.test.Test")) + } + + @Test + fun isClassImportedOrSamePackage_false() { + val code = """package org.example.test; + import org.example.Test; + """ + assertFalse(CodeUtils.isClassImportedOrSamePackage( + StaticJavaParser.parse(code), "org.example.Test2")) + } + + @Test + fun staticallyImportedMethods_ab() { + val code = """ + import static org.example.Test.a; + import static org.example.Test.b; + """ + val imported = CodeUtils.staticallyImportedMethods(StaticJavaParser.parse(code), + "org.example.Test") + assertTrue(imported.containsAll(listOf("a", "b"))) + assertEquals(2, imported.size) + } + + @Test + fun staticallyImportedMethods_differentClass() { + val code = """ + import static org.example.Test.a; + import static org.example.Test2.b; + """ + val imported = CodeUtils.staticallyImportedMethods(StaticJavaParser.parse(code), + "org.example.Test") + assertTrue(imported.containsAll(listOf("a"))) + assertEquals(1, imported.size) + } + + @Test + fun staticallyImportedMethods_notStatic() { + val code = """ + import static org.example.Test.a; + import org.example.Test.b; + """ + val imported = CodeUtils.staticallyImportedMethods(StaticJavaParser.parse(code), + "org.example.Test") + assertTrue(imported.containsAll(listOf("a"))) + assertEquals(1, imported.size) + } + + @Test + fun concatMultilineString_single() { + val str = StringLiteralExpr("test") + val out = CodeUtils.concatMultilineString(str, ParsingContext()) + assertEquals("test", out) + } + + @Test + fun concatMultilineString_double() { + val str = """ + "test" + "abc" + """ + val code = StaticJavaParser.parseExpression<BinaryExpr>(str) + val out = CodeUtils.concatMultilineString(code, ParsingContext()) + assertEquals("testabc", out) + } + + @Test + fun concatMultilineString_multiple() { + val str = """ + "test" + "abc" + "1234" + "test" + """ + val code = StaticJavaParser.parseExpression<BinaryExpr>(str) + val out = CodeUtils.concatMultilineString(code, ParsingContext()) + assertEquals("testabc1234test", out) + } +} diff --git a/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt b/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt new file mode 100644 index 000000000000..615712e10bcf --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import org.junit.Assert.assertEquals +import org.junit.Test + +class CommandOptionsTest { + companion object { + val TEST_JAVA_SRC = listOf( + "frameworks/base/services/core/java/com/android/server/wm/" + + "AccessibilityController.java", + "frameworks/base/services/core/java/com/android/server/wm/ActivityDisplay.java", + "frameworks/base/services/core/java/com/android/server/wm/" + + "ActivityMetricsLaunchObserver.java" + ) + private const val TEST_PROTOLOG_CLASS = "com.android.server.wm.ProtoLog" + private const val TEST_PROTOLOGIMPL_CLASS = "com.android.server.wm.ProtoLogImpl" + private const val TEST_PROTOLOGGROUP_CLASS = "com.android.server.wm.ProtoLogGroup" + private const val TEST_PROTOLOGGROUP_JAR = "out/soong/.intermediates/frameworks/base/" + + "services/core/services.core.wm.protologgroups/android_common/javac/" + + "services.core.wm.protologgroups.jar" + private const val TEST_SRC_JAR = "out/soong/.temp/sbox175955373/" + + "services.core.wm.protolog.srcjar" + private const val TEST_VIEWER_JSON = "out/soong/.temp/sbox175955373/" + + "services.core.wm.protolog.json" + private const val TEST_LOG = "./test_log.pb" + } + + @Test(expected = InvalidCommandException::class) + fun noCommand() { + CommandOptions(arrayOf()) + } + + @Test(expected = InvalidCommandException::class) + fun invalidCommand() { + val testLine = "invalid" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test + fun transformClasses() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + val cmd = CommandOptions(testLine.split(' ').toTypedArray()) + assertEquals(CommandOptions.TRANSFORM_CALLS_CMD, cmd.command) + assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg) + assertEquals(TEST_PROTOLOGIMPL_CLASS, cmd.protoLogImplClassNameArg) + assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg) + assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg) + assertEquals(TEST_SRC_JAR, cmd.outputSourceJarArg) + assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noProtoLogClass() { + val testLine = "transform-protolog-calls " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noProtoLogImplClass() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noProtoLogGroupClass() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noProtoLogGroupJar() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noOutJar() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + TEST_JAVA_SRC.joinToString(" ") + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noJavaInput() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_invalidProtoLogClass() { + val testLine = "transform-protolog-calls --protolog-class invalid " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_invalidProtoLogImplClass() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class invalid " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_invalidProtoLogGroupClass() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--loggroups-class invalid " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_invalidProtoLogGroupJar() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar invalid.txt " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_invalidOutJar() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar invalid.db ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_invalidJavaInput() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR invalid.py" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_unknownParam() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--unknown test --protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noValue() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test + fun generateConfig() { + val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--viewer-conf $TEST_VIEWER_JSON ${TEST_JAVA_SRC.joinToString(" ")}" + val cmd = CommandOptions(testLine.split(' ').toTypedArray()) + assertEquals(CommandOptions.GENERATE_CONFIG_CMD, cmd.command) + assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg) + assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg) + assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg) + assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigJsonArg) + assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs) + } + + @Test(expected = InvalidCommandException::class) + fun generateConfig_noViewerConfig() { + val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + TEST_JAVA_SRC.joinToString(" ") + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun generateConfig_invalidViewerConfig() { + val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--viewer-conf invalid.yaml ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test + fun readLog() { + val testLine = "read-log --viewer-conf $TEST_VIEWER_JSON $TEST_LOG" + val cmd = CommandOptions(testLine.split(' ').toTypedArray()) + assertEquals(CommandOptions.READ_LOG_CMD, cmd.command) + assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigJsonArg) + assertEquals(TEST_LOG, cmd.logProtofileArg) + } +} diff --git a/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt b/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt new file mode 100644 index 000000000000..04a3bfa499d8 --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.json.stream.JsonReader +import com.android.server.protolog.ProtoLogMessage +import com.android.server.protolog.ProtoLogFileProto +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.mock +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.io.PrintStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class LogParserTest { + private val configParser: ViewerConfigParser = mock(ViewerConfigParser::class.java) + private val parser = LogParser(configParser) + private var config: MutableMap<Int, ViewerConfigParser.ConfigEntry> = mutableMapOf() + private var outStream: OutputStream = ByteArrayOutputStream() + private var printStream: PrintStream = PrintStream(outStream) + private val dateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) + + @Before + fun init() { + Mockito.`when`(configParser.parseConfig(any(JsonReader::class.java))).thenReturn(config) + } + + private fun <T> any(type: Class<T>): T = Mockito.any<T>(type) + + private fun getConfigDummyStream(): InputStream { + return "".byteInputStream() + } + + private fun buildProtoInput(logBuilder: ProtoLogFileProto.Builder): InputStream { + logBuilder.setVersion(Constants.VERSION) + logBuilder.magicNumber = + ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or + ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_L.number.toLong() + return logBuilder.build().toByteArray().inputStream() + } + + private fun testDate(timeMS: Long): String { + return dateFormat.format(Date(timeMS)) + } + + @Test + fun parse() { + config[70933285] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b", + "ERROR", "WindowManager") + + val logBuilder = ProtoLogFileProto.newBuilder() + val logMessageBuilder = ProtoLogMessage.newBuilder() + logMessageBuilder + .setMessageHash(70933285) + .setElapsedRealtimeNanos(0) + .addBooleanParams(true) + logBuilder.addLog(logMessageBuilder.build()) + + parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream) + + assertEquals("${testDate(0)} ERROR WindowManager: Test completed successfully: true\n", + outStream.toString()) + } + + @Test + fun parse_formatting() { + config[123] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b %d %% %o" + + " %x %e %g %s %f", "ERROR", "WindowManager") + + val logBuilder = ProtoLogFileProto.newBuilder() + val logMessageBuilder = ProtoLogMessage.newBuilder() + logMessageBuilder + .setMessageHash(123) + .setElapsedRealtimeNanos(0) + .addBooleanParams(true) + .addAllSint64Params(listOf(1000, 20000, 300000)) + .addAllDoubleParams(listOf(0.1, 0.00001, 1000.1)) + .addStrParams("test") + logBuilder.addLog(logMessageBuilder.build()) + + parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream) + + assertEquals("${testDate(0)} ERROR WindowManager: Test completed successfully: " + + "true 1000 % 47040 493e0 1.000000e-01 1.00000e-05 test 1000.100000\n", + outStream.toString()) + } + + @Test + fun parse_invalidParamsTooMany() { + config[123] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b %d %% %o", + "ERROR", "WindowManager") + + val logBuilder = ProtoLogFileProto.newBuilder() + val logMessageBuilder = ProtoLogMessage.newBuilder() + logMessageBuilder + .setMessageHash(123) + .setElapsedRealtimeNanos(0) + .addBooleanParams(true) + .addAllSint64Params(listOf(1000, 20000, 300000)) + .addAllDoubleParams(listOf(0.1, 0.00001, 1000.1)) + .addStrParams("test") + logBuilder.addLog(logMessageBuilder.build()) + + parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream) + + assertEquals("${testDate(0)} INVALID: 123 - [test] [1000, 20000, 300000] " + + "[0.1, 1.0E-5, 1000.1] [true]\n", outStream.toString()) + } + + @Test + fun parse_invalidParamsNotEnough() { + config[123] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b %d %% %o" + + " %x %e %g %s %f", "ERROR", "WindowManager") + + val logBuilder = ProtoLogFileProto.newBuilder() + val logMessageBuilder = ProtoLogMessage.newBuilder() + logMessageBuilder + .setMessageHash(123) + .setElapsedRealtimeNanos(0) + .addBooleanParams(true) + .addStrParams("test") + logBuilder.addLog(logMessageBuilder.build()) + + parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream) + + assertEquals("${testDate(0)} INVALID: 123 - [test] [] [] [true]\n", + outStream.toString()) + } + + @Test(expected = InvalidInputException::class) + fun parse_invalidMagicNumber() { + val logBuilder = ProtoLogFileProto.newBuilder() + logBuilder.setVersion(Constants.VERSION) + logBuilder.magicNumber = 0 + val stream = logBuilder.build().toByteArray().inputStream() + + parser.parse(stream, getConfigDummyStream(), printStream) + } + + @Test(expected = InvalidInputException::class) + fun parse_invalidVersion() { + val logBuilder = ProtoLogFileProto.newBuilder() + logBuilder.setVersion("invalid") + logBuilder.magicNumber = + ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or + ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_L.number.toLong() + val stream = logBuilder.build().toByteArray().inputStream() + + parser.parse(stream, getConfigDummyStream(), printStream) + } + + @Test + fun parse_noConfig() { + val logBuilder = ProtoLogFileProto.newBuilder() + val logMessageBuilder = ProtoLogMessage.newBuilder() + logMessageBuilder + .setMessageHash(70933285) + .setElapsedRealtimeNanos(0) + .addBooleanParams(true) + logBuilder.addLog(logMessageBuilder.build()) + + parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream) + + assertEquals("${testDate(0)} UNKNOWN: 70933285 - [] [] [] [true]\n", + outStream.toString()) + } +} diff --git a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt new file mode 100644 index 000000000000..97f67a0a3fdb --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.StaticJavaParser +import com.github.javaparser.ast.expr.MethodCallExpr +import org.junit.Assert.assertEquals +import org.junit.Test + +class ProtoLogCallProcessorTest { + private data class LogCall( + val call: MethodCallExpr, + val messageString: String, + val level: LogLevel, + val group: LogGroup + ) + + private val groupMap: MutableMap<String, LogGroup> = mutableMapOf() + private val calls: MutableList<LogCall> = mutableListOf() + private val visitor = ProtoLogCallProcessor("org.example.ProtoLog", "org.example.ProtoLogGroup", + groupMap) + private val processor = object : ProtoLogCallVisitor { + override fun processCall( + call: MethodCallExpr, + messageString: String, + level: LogLevel, + group: LogGroup + ) { + calls.add(LogCall(call, messageString, level, group)) + } + } + + private fun checkCalls() { + assertEquals(1, calls.size) + val c = calls[0] + assertEquals("test %b", c.messageString) + assertEquals(groupMap["TEST"], c.group) + assertEquals(LogLevel.DEBUG, c.level) + } + + @Test + fun process_samePackage() { + val code = """ + package org.example; + + class Test { + void test() { + ProtoLog.d(ProtoLogGroup.TEST, "test %b", true); + ProtoLog.e(ProtoLogGroup.ERROR, "error %d", 1); + } + } + """ + groupMap["TEST"] = LogGroup("TEST", true, false, "WindowManager") + groupMap["ERROR"] = LogGroup("ERROR", true, true, "WindowManagerERROR") + visitor.process(StaticJavaParser.parse(code), processor, "") + assertEquals(2, calls.size) + var c = calls[0] + assertEquals("test %b", c.messageString) + assertEquals(groupMap["TEST"], c.group) + assertEquals(LogLevel.DEBUG, c.level) + c = calls[1] + assertEquals("error %d", c.messageString) + assertEquals(groupMap["ERROR"], c.group) + assertEquals(LogLevel.ERROR, c.level) + } + + @Test + fun process_imported() { + val code = """ + package org.example2; + + import org.example.ProtoLog; + import org.example.ProtoLogGroup; + + class Test { + void test() { + ProtoLog.d(ProtoLogGroup.TEST, "test %b", true); + } + } + """ + groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager") + visitor.process(StaticJavaParser.parse(code), processor, "") + checkCalls() + } + + @Test + fun process_importedStatic() { + val code = """ + package org.example2; + + import static org.example.ProtoLog.d; + import static org.example.ProtoLogGroup.TEST; + + class Test { + void test() { + d(TEST, "test %b", true); + } + } + """ + groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager") + visitor.process(StaticJavaParser.parse(code), processor, "") + checkCalls() + } + + @Test(expected = InvalidProtoLogCallException::class) + fun process_groupNotImported() { + val code = """ + package org.example2; + + import org.example.ProtoLog; + + class Test { + void test() { + ProtoLog.d(ProtoLogGroup.TEST, "test %b", true); + } + } + """ + groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager") + visitor.process(StaticJavaParser.parse(code), processor, "") + } + + @Test + fun process_protoLogNotImported() { + val code = """ + package org.example2; + + import org.example.ProtoLogGroup; + + class Test { + void test() { + ProtoLog.d(ProtoLogGroup.TEST, "test %b", true); + } + } + """ + groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager") + visitor.process(StaticJavaParser.parse(code), processor, "") + assertEquals(0, calls.size) + } + + @Test(expected = InvalidProtoLogCallException::class) + fun process_unknownGroup() { + val code = """ + package org.example; + + class Test { + void test() { + ProtoLog.d(ProtoLogGroup.TEST, "test %b", true); + } + } + """ + visitor.process(StaticJavaParser.parse(code), processor, "") + } + + @Test(expected = InvalidProtoLogCallException::class) + fun process_staticGroup() { + val code = """ + package org.example; + + class Test { + void test() { + ProtoLog.d(TEST, "test %b", true); + } + } + """ + visitor.process(StaticJavaParser.parse(code), processor, "") + } + + @Test(expected = InvalidProtoLogCallException::class) + fun process_badGroup() { + val code = """ + package org.example; + + class Test { + void test() { + ProtoLog.d(0, "test %b", true); + } + } + """ + visitor.process(StaticJavaParser.parse(code), processor, "") + } + + @Test(expected = InvalidProtoLogCallException::class) + fun process_invalidSignature() { + val code = """ + package org.example; + + class Test { + void test() { + ProtoLog.d("test"); + } + } + """ + visitor.process(StaticJavaParser.parse(code), processor, "") + } + + @Test + fun process_disabled() { + // Disabled groups are also processed. + val code = """ + package org.example; + + class Test { + void test() { + ProtoLog.d(ProtoLogGroup.TEST, "test %b", true); + } + } + """ + groupMap["TEST"] = LogGroup("TEST", false, true, "WindowManager") + visitor.process(StaticJavaParser.parse(code), processor, "") + checkCalls() + } +} diff --git a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt new file mode 100644 index 000000000000..e746300646c8 --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt @@ -0,0 +1,456 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.StaticJavaParser +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.expr.MethodCallExpr +import com.github.javaparser.ast.stmt.IfStmt +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Test +import org.mockito.Mockito + +class SourceTransformerTest { + companion object { + private const val PROTO_LOG_IMPL_PATH = "org.example.ProtoLogImpl" + + /* ktlint-disable max-line-length */ + private val TEST_CODE = """ + package org.example; + + class Test { + void test() { + ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); + } + } + """.trimIndent() + + private val TEST_CODE_MULTILINE = """ + package org.example; + + class Test { + void test() { + ProtoLog.w(TEST_GROUP, "test %d %f " + + "abc %s\n test", 100, + 0.1, "test"); + } + } + """.trimIndent() + + private val TEST_CODE_MULTICALLS = """ + package org.example; + + class Test { + void test() { + ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); + ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); + } + } + """.trimIndent() + + private val TEST_CODE_NO_PARAMS = """ + package org.example; + + class Test { + void test() { + ProtoLog.w(TEST_GROUP, "test"); + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_TEXT_ENABLED = """ + package org.example; + + class Test { + void test() { + if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_MULTILINE_TEXT_ENABLED = """ + package org.example; + + class Test { + void test() { + if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, 1780316587, 9, "test %d %f " + "abc %s\n test", protoLogParam0, protoLogParam1, protoLogParam2); + + } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_MULTICALL_TEXT_ENABLED = """ + package org.example; + + class Test { + void test() { + if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); } /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); } + if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_NO_PARAMS = """ + package org.example; + + class Test { + void test() { + if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { org.example.ProtoLogImpl.w(TEST_GROUP, -1741986185, 0, "test", (Object[]) null); } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_TEXT_DISABLED = """ + package org.example; + + class Test { + void test() { + if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, null, protoLogParam0, protoLogParam1); } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_MULTILINE_TEXT_DISABLED = """ + package org.example; + + class Test { + void test() { + if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, 1780316587, 9, null, protoLogParam0, protoLogParam1, protoLogParam2); + + } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_DISABLED = """ + package org.example; + + class Test { + void test() { + if (false) { /* TEST_GROUP is disabled */ ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_MULTILINE_DISABLED = """ + package org.example; + + class Test { + void test() { + if (false) { /* TEST_GROUP is disabled */ ProtoLog.w(TEST_GROUP, "test %d %f " + "abc %s\n test", 100, 0.1, "test"); + + } + } + } + """.trimIndent() + /* ktlint-enable max-line-length */ + + private const val PATH = "com.example.Test.java" + } + + private val processor: ProtoLogCallProcessor = Mockito.mock(ProtoLogCallProcessor::class.java) + private val implPath = "org.example.ProtoLogImpl" + private val sourceJarWriter = SourceTransformer(implPath, processor) + + private fun <T> any(type: Class<T>): T = Mockito.any<T>(type) + + @Test + fun processClass_textEnabled() { + var code = StaticJavaParser.parse(TEST_CODE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f", + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("$implPath.${Constants.IS_ENABLED_METHOD}(TEST_GROUP)", + ifStmt.condition.toString()) + assertFalse(ifStmt.elseStmt.isPresent) + assertEquals(3, ifStmt.thenStmt.childNodes.size) + val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr + assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString()) + assertEquals("w", methodCall.name.asString()) + assertEquals(6, methodCall.arguments.size) + assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) + assertEquals("1698911065", methodCall.arguments[1].toString()) + assertEquals(0b1001.toString(), methodCall.arguments[2].toString()) + assertEquals("\"test %d %f\"", methodCall.arguments[3].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) + assertEquals(TRANSFORMED_CODE_TEXT_ENABLED, out) + } + + @Test + fun processClass_textEnabledMulticalls() { + var code = StaticJavaParser.parse(TEST_CODE_MULTICALLS) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + val calls = code.findAll(MethodCallExpr::class.java) + visitor.processCall(calls[0], "test %d %f", + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST")) + visitor.processCall(calls[1], "test %d %f", + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST")) + visitor.processCall(calls[2], "test %d %f", + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE_MULTICALLS, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(3, ifStmts.size) + val ifStmt = ifStmts[1] + assertEquals("$implPath.${Constants.IS_ENABLED_METHOD}(TEST_GROUP)", + ifStmt.condition.toString()) + assertFalse(ifStmt.elseStmt.isPresent) + assertEquals(3, ifStmt.thenStmt.childNodes.size) + val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr + assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString()) + assertEquals("w", methodCall.name.asString()) + assertEquals(6, methodCall.arguments.size) + assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) + assertEquals("1698911065", methodCall.arguments[1].toString()) + assertEquals(0b1001.toString(), methodCall.arguments[2].toString()) + assertEquals("\"test %d %f\"", methodCall.arguments[3].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) + assertEquals(TRANSFORMED_CODE_MULTICALL_TEXT_ENABLED, out) + } + + @Test + fun processClass_textEnabledMultiline() { + var code = StaticJavaParser.parse(TEST_CODE_MULTILINE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], + "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP", + true, true, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("$implPath.${Constants.IS_ENABLED_METHOD}(TEST_GROUP)", + ifStmt.condition.toString()) + assertFalse(ifStmt.elseStmt.isPresent) + assertEquals(4, ifStmt.thenStmt.childNodes.size) + val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[1] as MethodCallExpr + assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString()) + assertEquals("w", methodCall.name.asString()) + assertEquals(7, methodCall.arguments.size) + assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) + assertEquals("1780316587", methodCall.arguments[1].toString()) + assertEquals(0b001001.toString(), methodCall.arguments[2].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) + assertEquals("protoLogParam2", methodCall.arguments[6].toString()) + assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_ENABLED, out) + } + + @Test + fun processClass_noParams() { + var code = StaticJavaParser.parse(TEST_CODE_NO_PARAMS) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test", + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE_NO_PARAMS, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("$implPath.${Constants.IS_ENABLED_METHOD}(TEST_GROUP)", + ifStmt.condition.toString()) + assertFalse(ifStmt.elseStmt.isPresent) + assertEquals(1, ifStmt.thenStmt.childNodes.size) + val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr + assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString()) + assertEquals("w", methodCall.name.asString()) + assertEquals(5, methodCall.arguments.size) + assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) + assertEquals("-1741986185", methodCall.arguments[1].toString()) + assertEquals(0.toString(), methodCall.arguments[2].toString()) + assertEquals(TRANSFORMED_CODE_NO_PARAMS, out) + } + + @Test + fun processClass_textDisabled() { + var code = StaticJavaParser.parse(TEST_CODE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f", + LogLevel.WARN, LogGroup("TEST_GROUP", true, false, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("$implPath.${Constants.IS_ENABLED_METHOD}(TEST_GROUP)", + ifStmt.condition.toString()) + assertFalse(ifStmt.elseStmt.isPresent) + assertEquals(3, ifStmt.thenStmt.childNodes.size) + val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr + assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString()) + assertEquals("w", methodCall.name.asString()) + assertEquals(6, methodCall.arguments.size) + assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) + assertEquals("1698911065", methodCall.arguments[1].toString()) + assertEquals(0b1001.toString(), methodCall.arguments[2].toString()) + assertEquals("null", methodCall.arguments[3].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) + assertEquals(TRANSFORMED_CODE_TEXT_DISABLED, out) + } + + @Test + fun processClass_textDisabledMultiline() { + var code = StaticJavaParser.parse(TEST_CODE_MULTILINE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], + "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP", + true, false, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("$implPath.${Constants.IS_ENABLED_METHOD}(TEST_GROUP)", + ifStmt.condition.toString()) + assertFalse(ifStmt.elseStmt.isPresent) + assertEquals(4, ifStmt.thenStmt.childNodes.size) + val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[1] as MethodCallExpr + assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString()) + assertEquals("w", methodCall.name.asString()) + assertEquals(7, methodCall.arguments.size) + assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) + assertEquals("1780316587", methodCall.arguments[1].toString()) + assertEquals(0b001001.toString(), methodCall.arguments[2].toString()) + assertEquals("null", methodCall.arguments[3].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) + assertEquals("protoLogParam2", methodCall.arguments[6].toString()) + assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_DISABLED, out) + } + + @Test + fun processClass_disabled() { + var code = StaticJavaParser.parse(TEST_CODE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f", + LogLevel.WARN, LogGroup("TEST_GROUP", false, true, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("false", ifStmt.condition.toString()) + assertEquals(TRANSFORMED_CODE_DISABLED, out) + } + + @Test + fun processClass_disabledMultiline() { + var code = StaticJavaParser.parse(TEST_CODE_MULTILINE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], + "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP", + false, true, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("false", ifStmt.condition.toString()) + assertEquals(TRANSFORMED_CODE_MULTILINE_DISABLED, out) + } +} diff --git a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt new file mode 100644 index 000000000000..2b6abcdee7ed --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.json.stream.JsonReader +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.expr.MethodCallExpr +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.Mockito +import java.io.StringReader + +class ViewerConfigBuilderTest { + companion object { + private val TAG1 = "WM_TEST" + private val TAG2 = "WM_DEBUG" + private val TEST1 = ViewerConfigParser.ConfigEntry("test1", LogLevel.INFO.name, TAG1) + private val TEST2 = ViewerConfigParser.ConfigEntry("test2", LogLevel.DEBUG.name, TAG2) + private val TEST3 = ViewerConfigParser.ConfigEntry("test3", LogLevel.ERROR.name, TAG2) + private val GROUP1 = LogGroup("TEST_GROUP", true, true, TAG1) + private val GROUP2 = LogGroup("DEBUG_GROUP", true, true, TAG2) + private val GROUP3 = LogGroup("DEBUG_GROUP", true, true, TAG2) + private const val PATH = "/tmp/test.java" + } + + private val processor: ProtoLogCallProcessor = Mockito.mock(ProtoLogCallProcessor::class.java) + private val configBuilder = ViewerConfigBuilder(processor) + private val dummyCompilationUnit = CompilationUnit() + + private fun <T> any(type: Class<T>): T = Mockito.any<T>(type) + + private fun parseConfig(json: String): Map<Int, ViewerConfigParser.ConfigEntry> { + return ViewerConfigParser().parseConfig(JsonReader(StringReader(json))) + } + + @Test + fun processClass() { + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO, + GROUP1) + visitor.processCall(MethodCallExpr(), TEST2.messageString, LogLevel.DEBUG, + GROUP2) + visitor.processCall(MethodCallExpr(), TEST3.messageString, LogLevel.ERROR, + GROUP3) + + invocation.arguments[0] as CompilationUnit + } + + configBuilder.processClass(dummyCompilationUnit, PATH) + + val parsedConfig = parseConfig(configBuilder.build()) + assertEquals(3, parsedConfig.size) + assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH, + TEST1.messageString, LogLevel.INFO, GROUP1)]) + assertEquals(TEST2, parsedConfig[CodeUtils.hash(PATH, TEST2.messageString, + LogLevel.DEBUG, GROUP2)]) + assertEquals(TEST3, parsedConfig[CodeUtils.hash(PATH, TEST3.messageString, + LogLevel.ERROR, GROUP3)]) + } + + @Test + fun processClass_nonUnique() { + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO, + GROUP1) + visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO, + GROUP1) + visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO, + GROUP1) + + invocation.arguments[0] as CompilationUnit + } + + configBuilder.processClass(dummyCompilationUnit, PATH) + + val parsedConfig = parseConfig(configBuilder.build()) + assertEquals(1, parsedConfig.size) + assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH, TEST1.messageString, + LogLevel.INFO, GROUP1)]) + } + + @Test + fun processClass_disabled() { + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO, + GROUP1) + visitor.processCall(MethodCallExpr(), TEST2.messageString, LogLevel.DEBUG, + LogGroup("DEBUG_GROUP", false, true, TAG2)) + visitor.processCall(MethodCallExpr(), TEST3.messageString, LogLevel.ERROR, + LogGroup("DEBUG_GROUP", true, false, TAG2)) + + invocation.arguments[0] as CompilationUnit + } + + configBuilder.processClass(dummyCompilationUnit, PATH) + + val parsedConfig = parseConfig(configBuilder.build()) + assertEquals(2, parsedConfig.size) + assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH, TEST1.messageString, + LogLevel.INFO, GROUP1)]) + assertEquals(TEST3, parsedConfig[CodeUtils.hash(PATH, TEST3.messageString, + LogLevel.ERROR, LogGroup("DEBUG_GROUP", true, false, TAG2))]) + } +} diff --git a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigParserTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigParserTest.kt new file mode 100644 index 000000000000..dc3ef7c57b35 --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigParserTest.kt @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.json.stream.JsonReader +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.StringReader + +class ViewerConfigParserTest { + private val parser = ViewerConfigParser() + + private fun getJSONReader(str: String): JsonReader { + return JsonReader(StringReader(str)) + } + + @Test + fun parseMessage() { + val json = """ + { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + """ + val msg = parser.parseMessage(getJSONReader(json)) + assertEquals("Test completed successfully: %b", msg.messageString) + assertEquals("ERROR", msg.level) + assertEquals("GENERIC_WM", msg.groupName) + } + + @Test + fun parseMessage_reorder() { + val json = """ + { + "group": "GENERIC_WM", + "level": "ERROR", + "message": "Test completed successfully: %b" + } + """ + val msg = parser.parseMessage(getJSONReader(json)) + assertEquals("Test completed successfully: %b", msg.messageString) + assertEquals("ERROR", msg.level) + assertEquals("GENERIC_WM", msg.groupName) + } + + @Test + fun parseMessage_unknownEntry() { + val json = """ + { + "unknown": "unknown entries should not block parsing", + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + """ + val msg = parser.parseMessage(getJSONReader(json)) + assertEquals("Test completed successfully: %b", msg.messageString) + assertEquals("ERROR", msg.level) + assertEquals("GENERIC_WM", msg.groupName) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseMessage_noMessage() { + val json = """ + { + "level": "ERROR", + "group": "GENERIC_WM" + } + """ + parser.parseMessage(getJSONReader(json)) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseMessage_noLevel() { + val json = """ + { + "message": "Test completed successfully: %b", + "group": "GENERIC_WM" + } + """ + parser.parseMessage(getJSONReader(json)) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseMessage_noGroup() { + val json = """ + { + "message": "Test completed successfully: %b", + "level": "ERROR" + } + """ + parser.parseMessage(getJSONReader(json)) + } + + @Test + fun parseGroup() { + val json = """ + { + "tag": "WindowManager" + } + """ + val group = parser.parseGroup(getJSONReader(json)) + assertEquals("WindowManager", group.tag) + } + + @Test + fun parseGroup_unknownEntry() { + val json = """ + { + "unknown": "unknown entries should not block parsing", + "tag": "WindowManager" + } + """ + val group = parser.parseGroup(getJSONReader(json)) + assertEquals("WindowManager", group.tag) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseGroup_noTag() { + val json = """ + { + } + """ + parser.parseGroup(getJSONReader(json)) + } + + @Test + fun parseMessages() { + val json = """ + { + "70933285": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + }, + "1792430067": { + "message": "Attempted to add window to a display that does not exist: %d. Aborting.", + "level": "WARN", + "group": "ERROR_WM" + } + } + """ + val messages = parser.parseMessages(getJSONReader(json)) + assertEquals(2, messages.size) + val msg1 = + ViewerConfigParser.MessageEntry("Test completed successfully: %b", + "ERROR", "GENERIC_WM") + val msg2 = + ViewerConfigParser.MessageEntry("Attempted to add window to a display that " + + "does not exist: %d. Aborting.", "WARN", "ERROR_WM") + + assertEquals(msg1, messages[70933285]) + assertEquals(msg2, messages[1792430067]) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseMessages_invalidHash() { + val json = """ + { + "invalid": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + } + """ + parser.parseMessages(getJSONReader(json)) + } + + @Test + fun parseGroups() { + val json = """ + { + "GENERIC_WM": { + "tag": "WindowManager" + }, + "ERROR_WM": { + "tag": "WindowManagerError" + } + } + """ + val groups = parser.parseGroups(getJSONReader(json)) + assertEquals(2, groups.size) + val grp1 = ViewerConfigParser.GroupEntry("WindowManager") + val grp2 = ViewerConfigParser.GroupEntry("WindowManagerError") + assertEquals(grp1, groups["GENERIC_WM"]) + assertEquals(grp2, groups["ERROR_WM"]) + } + + @Test + fun parseConfig() { + val json = """ + { + "version": "${Constants.VERSION}", + "messages": { + "70933285": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + }, + "groups": { + "GENERIC_WM": { + "tag": "WindowManager" + } + } + } + """ + val config = parser.parseConfig(getJSONReader(json)) + assertEquals(1, config.size) + val cfg1 = ViewerConfigParser.ConfigEntry("Test completed successfully: %b", + "ERROR", "WindowManager") + assertEquals(cfg1, config[70933285]) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseConfig_invalidVersion() { + val json = """ + { + "version": "invalid", + "messages": { + "70933285": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + }, + "groups": { + "GENERIC_WM": { + "tag": "WindowManager" + } + } + } + """ + parser.parseConfig(getJSONReader(json)) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseConfig_noVersion() { + val json = """ + { + "messages": { + "70933285": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + }, + "groups": { + "GENERIC_WM": { + "tag": "WindowManager" + } + } + } + """ + parser.parseConfig(getJSONReader(json)) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseConfig_noMessages() { + val json = """ + { + "version": "${Constants.VERSION}", + "groups": { + "GENERIC_WM": { + "tag": "WindowManager" + } + } + } + """ + parser.parseConfig(getJSONReader(json)) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseConfig_noGroups() { + val json = """ + { + "version": "${Constants.VERSION}", + "messages": { + "70933285": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + } + } + """ + parser.parseConfig(getJSONReader(json)) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseConfig_missingGroup() { + val json = """ + { + "version": "${Constants.VERSION}", + "messages": { + "70933285": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + }, + "groups": { + "ERROR_WM": { + "tag": "WindowManager" + } + } + } + """ + parser.parseConfig(getJSONReader(json)) + } +} diff --git a/tools/validatekeymaps/Main.cpp b/tools/validatekeymaps/Main.cpp index 56a242f1daaf..5ac9dfd2a557 100644 --- a/tools/validatekeymaps/Main.cpp +++ b/tools/validatekeymaps/Main.cpp @@ -137,7 +137,6 @@ static bool validateFile(const char* filename) { } } - log("No errors.\n\n"); return true; } |