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
This commit is contained in:
Adam Pardyl
2019-08-27 02:07:16 +02:00
parent b590aeb8f5
commit fab9ad6ac0
27 changed files with 3149 additions and 0 deletions

View File

@@ -28,3 +28,13 @@ cc_library_static {
"android/bluetooth/smp/enums.proto",
],
}
java_library_host {
name: "windowmanager-log-proto",
srcs: [
"android/server/windowmanagerlog.proto"
],
proto: {
type: "full",
},
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2017 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.
*/
syntax = "proto2";
package com.android.server.wm;
option java_multiple_files = true;
/* represents a single log entry */
message ProtoLogMessage {
/* log statement identifier, created from message string and log level. */
optional fixed32 message_hash = 1;
/* log time, relative to the elapsed system time clock. */
optional fixed64 elapsed_realtime_nanos = 2;
/* string parameters passed to the log call. */
repeated string str_params = 3;
/* integer parameters passed to the log call. */
repeated sint64 sint64_params = 4 [packed=true];
/* floating point parameters passed to the log call. */
repeated double double_params = 5 [packed=true];
/* boolean parameters passed to the log call. */
repeated bool boolean_params = 6 [packed=true];
}
/* represents a log file containing window manager log entries.
Encoded, it should start with 0x9 0x57 0x49 0x4e 0x44 0x4f 0x4c 0x4f 0x47 (.WINDOLOG), such
that they can be easily identified. */
message WindowManagerLogFileProto {
/* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | MagicNumber.MAGIC_NUMBER_L
(this is needed because enums have to be 32 bits and there's no nice way to put 64bit
constants into .proto files. */
enum MagicNumber {
INVALID = 0;
MAGIC_NUMBER_L = 0x444e4957; /* WIND (little-endian ASCII) */
MAGIC_NUMBER_H = 0x474f4c4f; /* OLOG (little-endian ASCII) */
}
/* the magic number header */
optional fixed64 magic_number = 1;
/* log proto version. */
optional string version = 2;
/* offset between real-time clock and elapsed system time clock in miliseconds.
Calculated as: (System.currentTimeMillis() - (SystemClock.elapsedRealtimeNanos() / 1000000) */
optional fixed64 realTimeToElapsedTimeOffsetMillis = 3;
/* log entries */
repeated ProtoLogMessage log = 4;
}

View File

@@ -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",
],
}

View File

@@ -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`.

View File

@@ -0,0 +1,7 @@
{
"presubmit": [
{
"name": "protologtool-tests"
}
]
}

View File

@@ -0,0 +1 @@
Main-class: com.android.protologtool.ProtoLogTool

View File

@@ -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
}
}

View File

@@ -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.")
}
}
}
}

View File

@@ -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"
}

View File

@@ -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
)

View File

@@ -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)
}
}
}
}

View File

@@ -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")
}
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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())
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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)])
}
}

View File

@@ -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))
}
}