diff options
author | Adam Pardyl <apardyl@google.com> | 2019-08-27 02:07:16 +0200 |
---|---|---|
committer | Adam Pardyl <apardyl@google.com> | 2019-09-06 16:03:26 +0200 |
commit | fab9ad6ac0da3ad2dff2f7fc1b87c143daab392f (patch) | |
tree | 97261f3809a8d679be12549f255f8005e862001a /tools/protologtool | |
parent | b590aeb8f51a96c5832482f0c09c63168cd0cdc4 (diff) |
Implement the ProtoLogTool
Implemented the ProtoLog code processing, viewer config generation
and binary log viewer tool.
Design doc: http://go/windowmanager-log2proto
Bug:
Test: atest protologtool-tests
Change-Id: Iff889944a6c381eb8a5b9b637b6bcd38ec60a245
Diffstat (limited to 'tools/protologtool')
25 files changed, 3078 insertions, 0 deletions
diff --git a/tools/protologtool/Android.bp b/tools/protologtool/Android.bp new file mode 100644 index 000000000000..a86c226c2179 --- /dev/null +++ b/tools/protologtool/Android.bp @@ -0,0 +1,28 @@ +java_binary_host { + name: "protologtool", + manifest: "manifest.txt", + srcs: [ + "src/**/*.kt", + ], + static_libs: [ + "javaparser", + "windowmanager-log-proto", + "jsonlib", + ], +} + +java_test_host { + name: "protologtool-tests", + test_suites: ["general-tests"], + srcs: [ + "src/**/*.kt", + "tests/**/*.kt", + ], + static_libs: [ + "javaparser", + "windowmanager-log-proto", + "jsonlib", + "junit", + "mockito", + ], +} diff --git a/tools/protologtool/README.md b/tools/protologtool/README.md new file mode 100644 index 000000000000..3439357af598 --- /dev/null +++ b/tools/protologtool/README.md @@ -0,0 +1,106 @@ +# ProtoLogTool + +Code transformation tool and viewer for ProtoLog. + +## What does it do? + +ProtoLogTool incorporates three different modes of operation: + +### Code transformation + +Command: `process <protolog class path> <protolog implementation class path> + <protolog groups class path> <config.jar> [<input.java>] <output.srcjar>` + +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 (GROUP_NAME.isLogToAny()) { + ProtoLogImpl.x(ProtoLogGroup.GROUP_NAME, 123456, "Format string %d %s or null", value1, value2); +} +``` +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 and log level and inserted after the `ProtoLogGroup` argument. 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. + +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: `viewerconf <protolog class path> <protolog implementation class path +<protolog groups class path> <config.jar> [<input.java>] <output.json>` + +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" + }, + }, + "groups": { + "GROUP_NAME": { + "tag": "TestLog" + } + } +} + +``` + +### Binary log viewing + +Command: `read <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 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 run `make update-protolog` to update +viewer configuration saved in the code repository. + +## 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..f5e53c450f2a --- /dev/null +++ b/tools/protologtool/manifest.txt @@ -0,0 +1 @@ +Main-class: com.android.protologtool.ProtoLogTool diff --git a/tools/protologtool/src/com/android/protologtool/CodeUtils.kt b/tools/protologtool/src/com/android/protologtool/CodeUtils.kt new file mode 100644 index 000000000000..facca6290c91 --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/CodeUtils.kt @@ -0,0 +1,135 @@ +/* + * 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.protologtool + +import com.github.javaparser.StaticJavaParser +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.ImportDeclaration +import com.github.javaparser.ast.NodeList +import com.github.javaparser.ast.expr.BinaryExpr +import com.github.javaparser.ast.expr.Expression +import com.github.javaparser.ast.expr.MethodCallExpr +import com.github.javaparser.ast.expr.SimpleName +import com.github.javaparser.ast.expr.StringLiteralExpr +import com.github.javaparser.ast.expr.TypeExpr +import com.github.javaparser.ast.type.PrimitiveType +import com.github.javaparser.ast.type.Type + +object CodeUtils { + /** + * Returns a stable hash of a string. + * We reimplement String::hashCode() for readability reasons. + */ + fun hash(str: String, level: LogLevel): Int { + return (level.name + str).map { c -> c.toInt() }.reduce { h, c -> h * 31 + c } + } + + fun isWildcardStaticImported(code: CompilationUnit, className: String): Boolean { + return code.findAll(ImportDeclaration::class.java) + .any { im -> im.isStatic && im.isAsterisk && im.name.toString() == className } + } + + 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): String { + return when (expr) { + is StringLiteralExpr -> expr.asString() + is BinaryExpr -> when { + expr.operator == BinaryExpr.Operator.PLUS -> + concatMultilineString(expr.left) + concatMultilineString(expr.right) + else -> throw InvalidProtoLogCallException( + "messageString must be a string literal " + + "or concatenation of string literals.", expr) + } + else -> throw InvalidProtoLogCallException("messageString must be a string literal " + + "or concatenation of string literals.", expr) + } + } + + enum class LogDataTypes( + val type: Type, + val toType: (Expression) -> Expression = { expr -> expr } + ) { + // When adding new LogDataType make sure to update {@code logDataTypesToBitMask} accordingly + STRING(StaticJavaParser.parseClassOrInterfaceType("String"), + { expr -> + MethodCallExpr(TypeExpr(StaticJavaParser.parseClassOrInterfaceType("String")), + SimpleName("valueOf"), NodeList(expr)) + }), + LONG(PrimitiveType.longType()), + DOUBLE(PrimitiveType.doubleType()), + BOOLEAN(PrimitiveType.booleanType()); + } + + fun parseFormatString(messageString: String): List<LogDataTypes> { + val types = mutableListOf<LogDataTypes>() + var i = 0 + while (i < messageString.length) { + if (messageString[i] == '%') { + if (i + 1 >= messageString.length) { + throw InvalidFormatStringException("Invalid format string in config") + } + when (messageString[i + 1]) { + 'b' -> types.add(CodeUtils.LogDataTypes.BOOLEAN) + 'd', 'o', 'x' -> types.add(CodeUtils.LogDataTypes.LONG) + 'f', 'e', 'g' -> types.add(CodeUtils.LogDataTypes.DOUBLE) + 's' -> types.add(CodeUtils.LogDataTypes.STRING) + '%' -> { + } + else -> throw InvalidFormatStringException("Invalid format string field" + + " %${messageString[i + 1]}") + } + i += 2 + } else { + i += 1 + } + } + return types + } + + fun logDataTypesToBitMask(types: List<LogDataTypes>): Int { + if (types.size > 16) { + throw InvalidFormatStringException("Too many log call parameters " + + "- max 16 parameters supported") + } + var mask = 0 + types.forEachIndexed { idx, type -> + val x = LogDataTypes.values().indexOf(type) + mask = mask or (x shl (idx * 2)) + } + return mask + } +} diff --git a/tools/protologtool/src/com/android/protologtool/CommandOptions.kt b/tools/protologtool/src/com/android/protologtool/CommandOptions.kt new file mode 100644 index 000000000000..df49e1566fbc --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/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.protologtool + +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/protologtool/Constants.kt b/tools/protologtool/src/com/android/protologtool/Constants.kt new file mode 100644 index 000000000000..2ccfc4d20182 --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/Constants.kt @@ -0,0 +1,27 @@ +/* + * 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.protologtool + +object Constants { + const val NAME = "protologtool" + const val VERSION = "1.0.0" + const val IS_ENABLED_METHOD = "isEnabled" + const val IS_LOG_TO_LOGCAT_METHOD = "isLogToLogcat" + const val IS_LOG_TO_ANY_METHOD = "isLogToAny" + const val GET_TAG_METHOD = "getTag" + const val ENUM_VALUES_METHOD = "values" +} diff --git a/tools/protologtool/src/com/android/protologtool/LogGroup.kt b/tools/protologtool/src/com/android/protologtool/LogGroup.kt new file mode 100644 index 000000000000..42a37a26e08a --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/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.protologtool + +data class LogGroup( + val name: String, + val enabled: Boolean, + val textEnabled: Boolean, + val tag: String +) diff --git a/tools/protologtool/src/com/android/protologtool/LogLevel.kt b/tools/protologtool/src/com/android/protologtool/LogLevel.kt new file mode 100644 index 000000000000..dc29557ef440 --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/LogLevel.kt @@ -0,0 +1,37 @@ +/* + * 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.protologtool + +import com.github.javaparser.ast.Node + +enum class LogLevel { + DEBUG, VERBOSE, INFO, WARN, ERROR, WTF; + + companion object { + fun getLevelForMethodName(name: String, node: Node): LogLevel { + return when (name) { + "d" -> DEBUG + "v" -> VERBOSE + "i" -> INFO + "w" -> WARN + "e" -> ERROR + "wtf" -> WTF + else -> throw InvalidProtoLogCallException("Unknown log level $name", node) + } + } + } +} diff --git a/tools/protologtool/src/com/android/protologtool/LogParser.kt b/tools/protologtool/src/com/android/protologtool/LogParser.kt new file mode 100644 index 000000000000..4d0eb0e4a705 --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/LogParser.kt @@ -0,0 +1,112 @@ +/* + * 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.protologtool + +import com.android.json.stream.JsonReader +import com.android.server.wm.ProtoLogMessage +import com.android.server.wm.WindowManagerLogFileProto +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 = + WindowManagerLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or + WindowManagerLogFileProto.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 = CodeUtils.parseFormatString(format) + try { + argTypes.forEach { + when (it) { + CodeUtils.LogDataTypes.BOOLEAN -> args.add(boolParamsIt.next()) + CodeUtils.LogDataTypes.LONG -> args.add(longParamsIt.next()) + CodeUtils.LogDataTypes.DOUBLE -> args.add(doubleParamsIt.next()) + CodeUtils.LogDataTypes.STRING -> args.add(strParmIt.next()) + } + } + } 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 = WindowManagerLogFileProto.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/protologtool/ProtoLogCallProcessor.kt b/tools/protologtool/src/com/android/protologtool/ProtoLogCallProcessor.kt new file mode 100644 index 000000000000..29d8ae5c6694 --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/ProtoLogCallProcessor.kt @@ -0,0 +1,108 @@ +/* + * 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.protologtool + +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> + ): String { + return when (expr) { + is NameExpr -> when { + expr.nameAsString in staticImports -> expr.nameAsString + else -> + throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup", expr) + } + is FieldAccessExpr -> when { + expr.scope.toString() == protoLogGroupClassName + || isClassImported && + expr.scope.toString() == protoLogGroupSimpleClassName -> expr.nameAsString + else -> + throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup", expr) + } + else -> throw InvalidProtoLogCallException("Invalid group argument " + + "- must be ProtoLogGroup enum member reference", expr) + } + } + + 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?): CompilationUnit { + if (CodeUtils.isWildcardStaticImported(code, protoLogClassName) || + CodeUtils.isWildcardStaticImported(code, protoLogGroupClassName)) { + throw IllegalImportException("Wildcard static imports of $protoLogClassName " + + "and $protoLogGroupClassName methods are not supported.") + } + + 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 -> + if (call.arguments.size < 2) { + throw InvalidProtoLogCallException("Method signature does not match " + + "any ProtoLog method.", call) + } + + val messageString = CodeUtils.concatMultilineString(call.getArgument(1)) + val groupNameArg = call.getArgument(0) + val groupName = + getLogGroupName(groupNameArg, isGroupClassImported, staticGroupImports) + if (groupName !in groupMap) { + throw InvalidProtoLogCallException("Unknown group argument " + + "- not a ProtoLogGroup enum member", call) + } + + callVisitor?.processCall(call, messageString, LogLevel.getLevelForMethodName( + call.name.toString(), call), groupMap.getValue(groupName)) + } + return code + } +} diff --git a/tools/protologtool/src/com/android/protologtool/ProtoLogCallVisitor.kt b/tools/protologtool/src/com/android/protologtool/ProtoLogCallVisitor.kt new file mode 100644 index 000000000000..42a75f8cc22f --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/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.protologtool + +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/protologtool/ProtoLogGroupReader.kt b/tools/protologtool/src/com/android/protologtool/ProtoLogGroupReader.kt new file mode 100644 index 000000000000..664c8a6506b2 --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/ProtoLogGroupReader.kt @@ -0,0 +1,60 @@ +/* + * 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.protologtool + +import com.android.protologtool.Constants.ENUM_VALUES_METHOD +import com.android.protologtool.Constants.GET_TAG_METHOD +import com.android.protologtool.Constants.IS_ENABLED_METHOD +import com.android.protologtool.Constants.IS_LOG_TO_LOGCAT_METHOD +import java.io.File +import java.lang.RuntimeException +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<Enum<*>> { + val valuesMethod = clazz.getMethod(ENUM_VALUES_METHOD) + @Suppress("UNCHECKED_CAST") + return (valuesMethod.invoke(null) as Array<Enum<*>>).toList() + } + + private fun getLogGroupFromEnumValue(group: Any, clazz: Class<*>): LogGroup { + val enabled = clazz.getMethod(IS_ENABLED_METHOD).invoke(group) as Boolean + val textEnabled = clazz.getMethod(IS_LOG_TO_LOGCAT_METHOD).invoke(group) as Boolean + val tag = clazz.getMethod(GET_TAG_METHOD).invoke(group) as String + val name = (group as Enum<*>).name + return LogGroup(name, enabled, textEnabled, tag) + } + + 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 getLogGroupFromEnumValue(group, clazz) + }.toMap() + } catch (ex: ReflectiveOperationException) { + throw RuntimeException("Unable to load ProtoLogGroup enum class", ex) + } + } +} diff --git a/tools/protologtool/src/com/android/protologtool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protologtool/ProtoLogTool.kt new file mode 100644 index 000000000000..485a0479cbd9 --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/ProtoLogTool.kt @@ -0,0 +1,94 @@ +/* + * 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.protologtool + +import com.android.protologtool.CommandOptions.Companion.USAGE +import com.github.javaparser.StaticJavaParser +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 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 code = StaticJavaParser.parse(file) + val outSrc = transformer.processClass(code) + val pack = if (code.packageDeclaration.isPresent) code.packageDeclaration + .get().nameAsString else "" + val newPath = pack.replace('.', '/') + '/' + file.name + outJar.putNextEntry(ZipEntry(newPath)) + outJar.write(outSrc.toByteArray()) + outJar.closeEntry() + } + + outJar.close() + out.close() + } + + 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) + builder.processClass(StaticJavaParser.parse(file)) + } + val out = FileOutputStream(command.viewerConfigJsonArg) + out.write(builder.build().toByteArray()) + out.close() + } + + 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(ex.message) + showHelpAndExit() + } + } +} diff --git a/tools/protologtool/src/com/android/protologtool/SourceTransformer.kt b/tools/protologtool/src/com/android/protologtool/SourceTransformer.kt new file mode 100644 index 000000000000..319a8170dca8 --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/SourceTransformer.kt @@ -0,0 +1,162 @@ +/* + * 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.protologtool + +import com.android.protologtool.Constants.IS_LOG_TO_ANY_METHOD +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.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.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.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(messageString, level) + 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 = CodeUtils.parseFormatString(messageString) + val typeMask = CodeUtils.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.wm.ProtoLogImpl.e(GROUP, 1234, null, arg) + newCall.setScope(protoLogImplClassNode) + // Create a call to GROUP.isLogAny() + // Out: GROUP.isLogAny() + val isLogAnyExpr = MethodCallExpr(newCall.arguments[0].clone(), + SimpleName(IS_LOG_TO_ANY_METHOD)) + if (argTypes.size != call.arguments.size - 2) { + throw InvalidProtoLogCallException( + "Number of arguments does not mach format string", 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(type.type, varName, + type.toType(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 (GROUP.isLogAny()) { + // long protoLogParam0 = arg; + // com.android.server.wm.ProtoLogImpl.e(GROUP, 1234, 0, null, protoLogParam0); + // } + ifStmt = IfStmt(isLogAnyExpr, 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)) + '}' + 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.") + } + } + + 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) + } + + private val protoLogImplClassNode = + StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogImplClassName) + + fun processClass(compilationUnit: CompilationUnit): String { + LexicalPreservingPrinter.setup(compilationUnit) + protoLogCallProcessor.process(compilationUnit, this) + return LexicalPreservingPrinter.print(compilationUnit) + } +} diff --git a/tools/protologtool/src/com/android/protologtool/ViewerConfigBuilder.kt b/tools/protologtool/src/com/android/protologtool/ViewerConfigBuilder.kt new file mode 100644 index 000000000000..8ce9a49c0302 --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/ViewerConfigBuilder.kt @@ -0,0 +1,91 @@ +/* + * 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.protologtool + +import com.android.json.stream.JsonWriter +import com.github.javaparser.ast.CompilationUnit +import com.android.protologtool.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 key = CodeUtils.hash(messageString, level) + if (statements.containsKey(key)) { + if (statements[key] != Triple(messageString, level, group)) { + throw HashCollisionException( + "Please modify the log message \"$messageString\" " + + "or \"${statements[key]}\" - their hashes are equal.") + } + } else { + groups.add(group) + statements[key] = Triple(messageString, level, group) + } + } + } + + private val statements: MutableMap<Int, Triple<String, LogLevel, LogGroup>> = mutableMapOf() + private val groups: MutableSet<LogGroup> = mutableSetOf() + + fun processClass(unit: CompilationUnit) { + protoLogCallVisitor.process(unit, this) + } + + 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.first) + writer.name("level") + writer.value(value.second.name) + writer.name("group") + writer.value(value.third.name) + 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() + } +} diff --git a/tools/protologtool/src/com/android/protologtool/ViewerConfigParser.kt b/tools/protologtool/src/com/android/protologtool/ViewerConfigParser.kt new file mode 100644 index 000000000000..69cf92d4d228 --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/ViewerConfigParser.kt @@ -0,0 +1,125 @@ +/* + * 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.protologtool + +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()) { + val key = jsonReader.nextName() + when (key) { + "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()) { + val key = jsonReader.nextName() + when (key) { + "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()) { + val key = jsonReader.nextName() + when (key) { + "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/protologtool/exceptions.kt b/tools/protologtool/src/com/android/protologtool/exceptions.kt new file mode 100644 index 000000000000..2199785a335b --- /dev/null +++ b/tools/protologtool/src/com/android/protologtool/exceptions.kt @@ -0,0 +1,44 @@ +/* + * 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.protologtool + +import com.github.javaparser.ast.Node +import java.lang.Exception +import java.lang.RuntimeException + +class HashCollisionException(message: String) : RuntimeException(message) + +class IllegalImportException(message: String) : Exception(message) + +class InvalidProtoLogCallException(message: String, node: Node) + : RuntimeException("$message\nAt: $node") + +class InvalidViewerConfigException : Exception { + constructor(message: String) : super(message) + + constructor(message: String, ex: Exception) : super(message, ex) +} + +class InvalidFormatStringException : Exception { + constructor(message: String) : super(message) + + constructor(message: String, ex: Exception) : super(message, ex) +} + +class InvalidInputException(message: String) : Exception(message) + +class InvalidCommandException(message: String) : Exception(message) diff --git a/tools/protologtool/tests/com/android/protologtool/CodeUtilsTest.kt b/tools/protologtool/tests/com/android/protologtool/CodeUtilsTest.kt new file mode 100644 index 000000000000..82daa736e1bc --- /dev/null +++ b/tools/protologtool/tests/com/android/protologtool/CodeUtilsTest.kt @@ -0,0 +1,206 @@ +/* + * 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.protologtool + +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(-1704685243, CodeUtils.hash("test", LogLevel.DEBUG)) + } + + @Test + fun hash_changeLevel() { + assertEquals(-1176900998, CodeUtils.hash("test", LogLevel.ERROR)) + } + + @Test + fun hash_changeMessage() { + assertEquals(-1305634931, CodeUtils.hash("test2", LogLevel.DEBUG)) + } + + @Test + fun isWildcardStaticImported_true() { + val code = """package org.example.test; + import static org.example.Test.*; + """ + assertTrue(CodeUtils.isWildcardStaticImported( + StaticJavaParser.parse(code), "org.example.Test")) + } + + @Test + fun isWildcardStaticImported_notStatic() { + val code = """package org.example.test; + import org.example.Test.*; + """ + assertFalse(CodeUtils.isWildcardStaticImported( + StaticJavaParser.parse(code), "org.example.Test")) + } + + @Test + fun isWildcardStaticImported_differentClass() { + val code = """package org.example.test; + import static org.example.Test2.*; + """ + assertFalse(CodeUtils.isWildcardStaticImported( + StaticJavaParser.parse(code), "org.example.Test")) + } + + @Test + fun isWildcardStaticImported_notWildcard() { + val code = """package org.example.test; + import org.example.Test.test; + """ + assertFalse(CodeUtils.isWildcardStaticImported( + 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) + assertEquals("test", out) + } + + @Test + fun concatMultilineString_double() { + val str = """ + "test" + "abc" + """ + val code = StaticJavaParser.parseExpression<BinaryExpr>(str) + val out = CodeUtils.concatMultilineString(code) + assertEquals("testabc", out) + } + + @Test + fun concatMultilineString_multiple() { + val str = """ + "test" + "abc" + "1234" + "test" + """ + val code = StaticJavaParser.parseExpression<BinaryExpr>(str) + val out = CodeUtils.concatMultilineString(code) + assertEquals("testabc1234test", out) + } + + @Test + fun parseFormatString() { + val str = "%b %d %o %x %f %e %g %s %%" + val out = CodeUtils.parseFormatString(str) + assertEquals(listOf( + CodeUtils.LogDataTypes.BOOLEAN, + CodeUtils.LogDataTypes.LONG, + CodeUtils.LogDataTypes.LONG, + CodeUtils.LogDataTypes.LONG, + CodeUtils.LogDataTypes.DOUBLE, + CodeUtils.LogDataTypes.DOUBLE, + CodeUtils.LogDataTypes.DOUBLE, + CodeUtils.LogDataTypes.STRING + ), out) + } + + @Test(expected = InvalidFormatStringException::class) + fun parseFormatString_invalid() { + val str = "%q" + CodeUtils.parseFormatString(str) + } + + @Test + fun logDataTypesToBitMask() { + val types = listOf(CodeUtils.LogDataTypes.STRING, CodeUtils.LogDataTypes.DOUBLE, + CodeUtils.LogDataTypes.LONG, CodeUtils.LogDataTypes.BOOLEAN) + val mask = CodeUtils.logDataTypesToBitMask(types) + assertEquals(0b11011000, mask) + } + + @Test(expected = InvalidFormatStringException::class) + fun logDataTypesToBitMask_toManyParams() { + val types = mutableListOf<CodeUtils.LogDataTypes>() + for (i in 0..16) { + types.add(CodeUtils.LogDataTypes.STRING) + } + CodeUtils.logDataTypesToBitMask(types) + } +} diff --git a/tools/protologtool/tests/com/android/protologtool/CommandOptionsTest.kt b/tools/protologtool/tests/com/android/protologtool/CommandOptionsTest.kt new file mode 100644 index 000000000000..c1cd473574c2 --- /dev/null +++ b/tools/protologtool/tests/com/android/protologtool/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.protologtool + +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/protologtool/LogParserTest.kt b/tools/protologtool/tests/com/android/protologtool/LogParserTest.kt new file mode 100644 index 000000000000..7106ea6fa168 --- /dev/null +++ b/tools/protologtool/tests/com/android/protologtool/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.protologtool + +import com.android.json.stream.JsonReader +import com.android.server.wm.ProtoLogMessage +import com.android.server.wm.WindowManagerLogFileProto +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: WindowManagerLogFileProto.Builder): InputStream { + logBuilder.setVersion(Constants.VERSION) + logBuilder.magicNumber = + WindowManagerLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or + WindowManagerLogFileProto.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 = WindowManagerLogFileProto.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 = WindowManagerLogFileProto.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 = WindowManagerLogFileProto.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 = WindowManagerLogFileProto.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 = WindowManagerLogFileProto.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 = WindowManagerLogFileProto.newBuilder() + logBuilder.setVersion("invalid") + logBuilder.magicNumber = + WindowManagerLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or + WindowManagerLogFileProto.MagicNumber.MAGIC_NUMBER_L.number.toLong() + val stream = logBuilder.build().toByteArray().inputStream() + + parser.parse(stream, getConfigDummyStream(), printStream) + } + + @Test + fun parse_noConfig() { + val logBuilder = WindowManagerLogFileProto.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/protologtool/ProtoLogCallProcessorTest.kt b/tools/protologtool/tests/com/android/protologtool/ProtoLogCallProcessorTest.kt new file mode 100644 index 000000000000..dcb1f7fe3366 --- /dev/null +++ b/tools/protologtool/tests/com/android/protologtool/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.protologtool + +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/protologtool/SourceTransformerTest.kt b/tools/protologtool/tests/com/android/protologtool/SourceTransformerTest.kt new file mode 100644 index 000000000000..7b8dd9a73fa9 --- /dev/null +++ b/tools/protologtool/tests/com/android/protologtool/SourceTransformerTest.kt @@ -0,0 +1,373 @@ +/* + * 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.protologtool + +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" + 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_NO_PARAMS = """ + package org.example; + + class Test { + void test() { + ProtoLog.w(TEST_GROUP, "test"); + } + } + """.trimIndent() + + /* ktlint-disable max-line-length */ + private val TRANSFORMED_CODE_TEXT_ENABLED = """ + package org.example; + + class Test { + void test() { + if (TEST_GROUP.isLogToAny()) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 835524026, 9, "test %d %f", protoLogParam0, protoLogParam1); } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_MULTILINE_TEXT_ENABLED = """ + package org.example; + + class Test { + void test() { + if (TEST_GROUP.isLogToAny()) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -986393606, 9, "test %d %f " + "abc %s\n test", protoLogParam0, protoLogParam1, protoLogParam2); + + } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_NO_PARAMS = """ + package org.example; + + class Test { + void test() { + if (TEST_GROUP.isLogToAny()) { org.example.ProtoLogImpl.w(TEST_GROUP, 1282022424, 0, "test", (Object[]) null); } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_TEXT_DISABLED = """ + package org.example; + + class Test { + void test() { + if (TEST_GROUP.isLogToAny()) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 835524026, 9, null, protoLogParam0, protoLogParam1); } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_MULTILINE_TEXT_DISABLED = """ + package org.example; + + class Test { + void test() { + if (TEST_GROUP.isLogToAny()) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -986393606, 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 val processor: ProtoLogCallProcessor = Mockito.mock(ProtoLogCallProcessor::class.java) + private val sourceJarWriter = SourceTransformer("org.example.ProtoLogImpl", processor) + + private fun <T> any(type: Class<T>): T = Mockito.any<T>(type) + + @Test + fun processClass_textEnabled() { + val code = StaticJavaParser.parse(TEST_CODE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::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(code) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("TEST_GROUP.${Constants.IS_LOG_TO_ANY_METHOD}()", + 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("835524026", 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_textEnabledMultiline() { + val code = StaticJavaParser.parse(TEST_CODE_MULTILINE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::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(code) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("TEST_GROUP.${Constants.IS_LOG_TO_ANY_METHOD}()", + 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("-986393606", 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() { + val code = StaticJavaParser.parse(TEST_CODE_NO_PARAMS) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::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(code) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("TEST_GROUP.${Constants.IS_LOG_TO_ANY_METHOD}()", + 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("1282022424", methodCall.arguments[1].toString()) + assertEquals(0.toString(), methodCall.arguments[2].toString()) + assertEquals(TRANSFORMED_CODE_NO_PARAMS, out) + } + + @Test + fun processClass_textDisabled() { + val code = StaticJavaParser.parse(TEST_CODE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::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(code) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("TEST_GROUP.${Constants.IS_LOG_TO_ANY_METHOD}()", + 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("835524026", 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() { + val code = StaticJavaParser.parse(TEST_CODE_MULTILINE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::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(code) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("TEST_GROUP.${Constants.IS_LOG_TO_ANY_METHOD}()", + 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("-986393606", 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() { + val code = StaticJavaParser.parse(TEST_CODE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::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(code) + + 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() { + val code = StaticJavaParser.parse(TEST_CODE_MULTILINE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::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(code) + + 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/protologtool/ViewerConfigBuilderTest.kt b/tools/protologtool/tests/com/android/protologtool/ViewerConfigBuilderTest.kt new file mode 100644 index 000000000000..53d2e8b0f4fa --- /dev/null +++ b/tools/protologtool/tests/com/android/protologtool/ViewerConfigBuilderTest.kt @@ -0,0 +1,120 @@ +/* + * 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.protologtool + +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 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))).thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO, + LogGroup("TEST_GROUP", true, true, TAG1)) + visitor.processCall(MethodCallExpr(), TEST2.messageString, LogLevel.DEBUG, + LogGroup("DEBUG_GROUP", true, true, TAG2)) + visitor.processCall(MethodCallExpr(), TEST3.messageString, LogLevel.ERROR, + LogGroup("DEBUG_GROUP", true, true, TAG2)) + + invocation.arguments[0] as CompilationUnit + } + + configBuilder.processClass(dummyCompilationUnit) + + val parsedConfig = parseConfig(configBuilder.build()) + assertEquals(3, parsedConfig.size) + assertEquals(TEST1, parsedConfig[CodeUtils.hash(TEST1.messageString, + LogLevel.INFO)]) + assertEquals(TEST2, parsedConfig[CodeUtils.hash(TEST2.messageString, + LogLevel.DEBUG)]) + assertEquals(TEST3, parsedConfig[CodeUtils.hash(TEST3.messageString, + LogLevel.ERROR)]) + } + + @Test + fun processClass_nonUnique() { + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java))).thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO, + LogGroup("TEST_GROUP", true, true, TAG1)) + visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO, + LogGroup("TEST_GROUP", true, true, TAG1)) + visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO, + LogGroup("TEST_GROUP", true, true, TAG1)) + + invocation.arguments[0] as CompilationUnit + } + + configBuilder.processClass(dummyCompilationUnit) + + val parsedConfig = parseConfig(configBuilder.build()) + assertEquals(1, parsedConfig.size) + assertEquals(TEST1, parsedConfig[CodeUtils.hash(TEST1.messageString, LogLevel.INFO)]) + } + + @Test + fun processClass_disabled() { + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java))).thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO, + LogGroup("TEST_GROUP", true, true, TAG1)) + 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) + + val parsedConfig = parseConfig(configBuilder.build()) + assertEquals(2, parsedConfig.size) + assertEquals(TEST1, parsedConfig[CodeUtils.hash(TEST1.messageString, LogLevel.INFO)]) + assertEquals(TEST3, parsedConfig[CodeUtils.hash(TEST3.messageString, LogLevel.ERROR)]) + } +} diff --git a/tools/protologtool/tests/com/android/protologtool/ViewerConfigParserTest.kt b/tools/protologtool/tests/com/android/protologtool/ViewerConfigParserTest.kt new file mode 100644 index 000000000000..c0cea733eadd --- /dev/null +++ b/tools/protologtool/tests/com/android/protologtool/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.protologtool + +import com.android.json.stream.JsonReader +import org.junit.Test +import java.io.StringReader +import org.junit.Assert.assertEquals + +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" + } + } + } + """ + val config = parser.parseConfig(getJSONReader(json)) + } +} |