ProtoLog: Add end-to-end test

Adds a test that verifies we compute the correct hash end-to-end, i.e. including
the actual path logic, which previously caused a divergence between the viewer config
and the generated code.

Test: atest protologtool-tests
Change-Id: Ia2a414322aa6ccdaedda7f9754d903ec984902d6
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
index dda1311..99a26dc 100644
--- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
@@ -24,6 +24,7 @@
 import java.io.File
 import java.io.FileInputStream
 import java.io.FileOutputStream
+import java.io.OutputStream
 import java.util.concurrent.ExecutorService
 import java.util.concurrent.Executors
 import java.util.jar.JarOutputStream
@@ -42,9 +43,10 @@
     }
 
     private fun processClasses(command: CommandOptions) {
-        val groups = ProtoLogGroupReader()
-                .loadFromJar(command.protoLogGroupsJarArg, command.protoLogGroupsClassNameArg)
-        val out = FileOutputStream(command.outputSourceJarArg)
+        val groups = injector.readLogGroups(
+                command.protoLogGroupsJarArg,
+                command.protoLogGroupsClassNameArg)
+        val out = injector.fileOutputStream(command.outputSourceJarArg)
         val outJar = JarOutputStream(out)
         val processor = ProtoLogCallProcessor(command.protoLogClassNameArg,
                 command.protoLogGroupsClassNameArg, groups)
@@ -56,7 +58,7 @@
                 val transformer = SourceTransformer(command.protoLogImplClassNameArg,
                         command.protoLogCacheClassNameArg, processor)
                 val file = File(path)
-                val text = file.readText()
+                val text = injector.readText(file)
                 val outSrc = try {
                     val code = tryParse(text, path)
                     if (containsProtoLogText(text, command.protoLogClassNameArg)) {
@@ -67,7 +69,7 @@
                 } catch (ex: ParsingException) {
                     // If we cannot parse this file, skip it (and log why). Compilation will fail
                     // in a subsequent build step.
-                    println("\n${ex.message}\n")
+                    injector.reportParseError(ex)
                     text
                 }
                 path to outSrc
@@ -142,8 +144,9 @@
     }
 
     private fun viewerConf(command: CommandOptions) {
-        val groups = ProtoLogGroupReader()
-                .loadFromJar(command.protoLogGroupsJarArg, command.protoLogGroupsClassNameArg)
+        val groups = injector.readLogGroups(
+                command.protoLogGroupsJarArg,
+                command.protoLogGroupsClassNameArg)
         val processor = ProtoLogCallProcessor(command.protoLogClassNameArg,
                 command.protoLogGroupsClassNameArg, groups)
         val builder = ViewerConfigBuilder(processor)
@@ -153,7 +156,7 @@
         command.javaSourceArgs.map { path ->
             executor.submitCallable {
                 val file = File(path)
-                val text = file.readText()
+                val text = injector.readText(file)
                 if (containsProtoLogText(text, command.protoLogClassNameArg)) {
                     try {
                         val code = tryParse(text, path)
@@ -161,7 +164,7 @@
                     } catch (ex: ParsingException) {
                         // If we cannot parse this file, skip it (and log why). Compilation will fail
                         // in a subsequent build step.
-                        println("\n${ex.message}\n")
+                        injector.reportParseError(ex)
                         null
                     }
                 } else {
@@ -174,7 +177,7 @@
 
         executor.shutdown()
 
-        val out = FileOutputStream(command.viewerConfigJsonArg)
+        val out = injector.fileOutputStream(command.viewerConfigJsonArg)
         out.write(builder.build().toByteArray())
         out.close()
     }
@@ -194,18 +197,9 @@
 
     @JvmStatic
     fun main(args: Array<String>) {
-        StaticJavaParser.setConfiguration(ParserConfiguration().apply {
-            setLanguageLevel(ParserConfiguration.LanguageLevel.RAW)
-            setAttributeComments(false)
-        })
-
         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)
-            }
+            invoke(command)
         } catch (ex: InvalidCommandException) {
             println("\n${ex.message}\n")
             showHelpAndExit()
@@ -214,6 +208,36 @@
             exitProcess(1)
         }
     }
+
+    fun invoke(command: CommandOptions) {
+        StaticJavaParser.setConfiguration(ParserConfiguration().apply {
+            setLanguageLevel(ParserConfiguration.LanguageLevel.RAW)
+            setAttributeComments(false)
+        })
+
+        when (command.command) {
+            CommandOptions.TRANSFORM_CALLS_CMD -> processClasses(command)
+            CommandOptions.GENERATE_CONFIG_CMD -> viewerConf(command)
+            CommandOptions.READ_LOG_CMD -> read(command)
+        }
+    }
+
+    var injector = object : Injector {
+        override fun fileOutputStream(file: String) = FileOutputStream(file)
+        override fun readText(file: File) = file.readText()
+        override fun readLogGroups(jarPath: String, className: String) =
+                ProtoLogGroupReader().loadFromJar(jarPath, className)
+        override fun reportParseError(ex: ParsingException) {
+            println("\n${ex.message}\n")
+        }
+    }
+
+    interface Injector {
+        fun fileOutputStream(file: String): OutputStream
+        fun readText(file: File): String
+        fun readLogGroups(jarPath: String, className: String): Map<String, LogGroup>
+        fun reportParseError(ex: ParsingException)
+    }
 }
 
 private fun <T> ExecutorService.submitCallable(f: () -> T) = submit(f)
diff --git a/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
new file mode 100644
index 0000000..dd8a0b1
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import org.junit.Assert
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.OutputStream
+import java.util.jar.JarInputStream
+
+class EndToEndTest {
+
+    @Test
+    fun e2e_transform() {
+        val output = run(
+                src = "frameworks/base/org/example/Example.java" to """
+                    package org.example;
+                    import com.android.server.protolog.common.ProtoLog;
+                    import static com.android.server.wm.ProtoLogGroup.GROUP;
+
+                    class Example {
+                        void method() {
+                            String argString = "hello";
+                            int argInt = 123;
+                            ProtoLog.d(GROUP, "Example: %s %d", argString, argInt);
+                        }
+                    }
+                """.trimIndent(),
+                logGroup = LogGroup("GROUP", true, false, "TAG_GROUP"),
+                commandOptions = CommandOptions(arrayOf("transform-protolog-calls",
+                        "--protolog-class", "com.android.server.protolog.common.ProtoLog",
+                        "--protolog-impl-class", "com.android.server.protolog.ProtoLogImpl",
+                        "--protolog-cache-class",
+                        "com.android.server.protolog.ProtoLog${"\$\$"}Cache",
+                        "--loggroups-class", "com.android.server.wm.ProtoLogGroup",
+                        "--loggroups-jar", "not_required.jar",
+                        "--output-srcjar", "out.srcjar",
+                        "frameworks/base/org/example/Example.java"))
+        )
+        val outSrcJar = assertLoadSrcJar(output, "out.srcjar")
+        assertTrue(" 2066303299," in outSrcJar["frameworks/base/org/example/Example.java"]!!)
+    }
+
+    @Test
+    fun e2e_viewerConfig() {
+        val output = run(
+                src = "frameworks/base/org/example/Example.java" to """
+                    package org.example;
+                    import com.android.server.protolog.common.ProtoLog;
+                    import static com.android.server.wm.ProtoLogGroup.GROUP;
+
+                    class Example {
+                        void method() {
+                            String argString = "hello";
+                            int argInt = 123;
+                            ProtoLog.d(GROUP, "Example: %s %d", argString, argInt);
+                        }
+                    }
+                """.trimIndent(),
+                logGroup = LogGroup("GROUP", true, false, "TAG_GROUP"),
+                commandOptions = CommandOptions(arrayOf("generate-viewer-config",
+                        "--protolog-class", "com.android.server.protolog.common.ProtoLog",
+                        "--loggroups-class", "com.android.server.wm.ProtoLogGroup",
+                        "--loggroups-jar", "not_required.jar",
+                        "--viewer-conf", "out.json",
+                        "frameworks/base/org/example/Example.java"))
+        )
+        val viewerConfigJson = assertLoadText(output, "out.json")
+        assertTrue("\"2066303299\"" in viewerConfigJson)
+    }
+
+    private fun assertLoadSrcJar(
+        outputs: Map<String, ByteArray>,
+        path: String
+    ): Map<String, String> {
+        val out = outputs[path] ?: fail("$path not in outputs (${outputs.keys})")
+
+        val sources = mutableMapOf<String, String>()
+        JarInputStream(ByteArrayInputStream(out)).use { jarStream ->
+            var entry = jarStream.nextJarEntry
+            while (entry != null) {
+                if (entry.name.endsWith(".java")) {
+                    sources[entry.name] = jarStream.reader().readText()
+                }
+                entry = jarStream.nextJarEntry
+            }
+        }
+        return sources
+    }
+
+    private fun assertLoadText(outputs: Map<String, ByteArray>, path: String): String {
+        val out = outputs[path] ?: fail("$path not in outputs (${outputs.keys})")
+        return out.toString(Charsets.UTF_8)
+    }
+
+    fun run(
+        src: Pair<String, String>,
+        logGroup: LogGroup,
+        commandOptions: CommandOptions
+    ): Map<String, ByteArray> {
+        val outputs = mutableMapOf<String, ByteArrayOutputStream>()
+
+        ProtoLogTool.injector = object : ProtoLogTool.Injector {
+            override fun fileOutputStream(file: String): OutputStream =
+                    ByteArrayOutputStream().also { outputs[file] = it }
+
+            override fun readText(file: File): String {
+                if (file.path == src.first) {
+                    return src.second
+                }
+                throw FileNotFoundException("expected: ${src.first}, but was $file")
+            }
+
+            override fun readLogGroups(jarPath: String, className: String) = mapOf(
+                    logGroup.name to logGroup)
+
+            override fun reportParseError(ex: ParsingException) = throw AssertionError(ex)
+        }
+
+        ProtoLogTool.invoke(commandOptions)
+
+        return outputs.mapValues { it.value.toByteArray() }
+    }
+
+    fun fail(message: String): Nothing = Assert.fail(message) as Nothing
+}
diff --git a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
index 6f5955c..4f2be328 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
@@ -186,7 +186,7 @@
             invocation.arguments[0] as CompilationUnit
         }
 
-        val out = sourceJarWriter.processClass(TEST_CODE, PATH, code)
+        val out = sourceJarWriter.processClass(TEST_CODE, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
         val ifStmts = code.findAll(IfStmt::class.java)
@@ -228,7 +228,7 @@
             invocation.arguments[0] as CompilationUnit
         }
 
-        val out = sourceJarWriter.processClass(TEST_CODE_MULTICALLS, PATH, code)
+        val out = sourceJarWriter.processClass(TEST_CODE_MULTICALLS, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
         val ifStmts = code.findAll(IfStmt::class.java)
@@ -266,7 +266,7 @@
             invocation.arguments[0] as CompilationUnit
         }
 
-        val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, code)
+        val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
         val ifStmts = code.findAll(IfStmt::class.java)
@@ -303,7 +303,7 @@
             invocation.arguments[0] as CompilationUnit
         }
 
-        val out = sourceJarWriter.processClass(TEST_CODE_NO_PARAMS, PATH, code)
+        val out = sourceJarWriter.processClass(TEST_CODE_NO_PARAMS, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
         val ifStmts = code.findAll(IfStmt::class.java)
@@ -337,7 +337,7 @@
             invocation.arguments[0] as CompilationUnit
         }
 
-        val out = sourceJarWriter.processClass(TEST_CODE, PATH, code)
+        val out = sourceJarWriter.processClass(TEST_CODE, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
         val ifStmts = code.findAll(IfStmt::class.java)
@@ -375,7 +375,7 @@
             invocation.arguments[0] as CompilationUnit
         }
 
-        val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, code)
+        val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
         val ifStmts = code.findAll(IfStmt::class.java)
@@ -413,7 +413,7 @@
             invocation.arguments[0] as CompilationUnit
         }
 
-        val out = sourceJarWriter.processClass(TEST_CODE, PATH, code)
+        val out = sourceJarWriter.processClass(TEST_CODE, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
         val ifStmts = code.findAll(IfStmt::class.java)
@@ -439,7 +439,7 @@
             invocation.arguments[0] as CompilationUnit
         }
 
-        val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, code)
+        val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
         val ifStmts = code.findAll(IfStmt::class.java)