• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) Tor Norbye.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 @file:Suppress("PropertyName", "PrivatePropertyName")
18 
19 package com.facebook.ktfmt.kdoc
20 
21 import com.google.common.truth.Truth.assertThat
22 import java.io.BufferedReader
23 import java.io.File
24 
25 /**
26  * Verifies that two KDoc comment strings render to the same HTML documentation using Dokka. This is
27  * used by the test infrastructure to make sure that the transformations we're allowing are not
28  * changing the appearance of the documentation.
29  *
30  * Unfortunately, just diffing HTML strings isn't always enough, because dokka will preserve some
31  * text formatting which is immaterial to the HTML appearance. Therefore, if you've also installed
32  * Pandoc, it will use that to generate a text rendering of the HTML which is then used for diffing
33  * instead. (Even this isn't fullproof because pandoc also preserves some details that should not
34  * matter). Text rendering does drop a lot of markup (such as bold and italics) so it would be
35  * better to compare in some other format, such as PDF, but unfortunately, the PDF rendering doesn't
36  * appear to be stable; rendering the same document twice yields a binary diff.
37  *
38  * Dokka no longer provides a fat/shadow jar; instead you have to download a bunch of different
39  * dependencies. Therefore, for convenience this is set up to point to an AndroidX checkout, which
40  * has all the prebuilts. Point the below to AndroidX and the rest should work.
41  */
42 class DokkaVerifier(private val tempFolder: File) {
43   // Configuration parameters
44   // Checkout of https://github.com/androidx/androidx
45   private val ANDROIDX_HOME: String? = null
46 
47   // Optional install of pandoc, e.g. "/opt/homebrew/bin/pandoc"
48   private val PANDOC: String? = null
49 
50   // JDK install
51   private val JAVA_HOME: String? = System.getenv("JAVA_HOME") ?: System.getProperty("java.home")
52 
verifynull53   fun verify(before: String, after: String) {
54     JAVA_HOME ?: return
55     ANDROIDX_HOME ?: return
56 
57     val androidx = File(ANDROIDX_HOME)
58     if (!androidx.isDirectory) {
59       return
60     }
61 
62     val prebuilts = File(androidx, "prebuilts")
63     if (!prebuilts.isDirectory) {
64       println("AndroidX prebuilts not found; not verifying with Dokka")
65     }
66     val cli = find(prebuilts, "org.jetbrains.dokka", "dokka-cli")
67     val analysis = find(prebuilts, "org.jetbrains.dokka", "dokka-analysis")
68     val base = find(prebuilts, "org.jetbrains.dokka", "dokka-base")
69     val compiler = find(prebuilts, "org.jetbrains.dokka", "kotlin-analysis-compiler")
70     val intellij = find(prebuilts, "org.jetbrains.dokka", "kotlin-analysis-intellij")
71     val coroutines = find(prebuilts, "org.jetbrains.kotlinx", "kotlinx-coroutines-core")
72     val html = find(prebuilts, "org.jetbrains.kotlinx", "kotlinx-html-jvm")
73     val freemarker = find(prebuilts, "org.freemarker", "freemarker")
74 
75     val src = File(tempFolder, "src")
76     val out = File(tempFolder, "dokka")
77     src.mkdirs()
78     out.mkdirs()
79 
80     val beforeFile = File(src, "before.kt")
81     beforeFile.writeText("${before.split("\n").joinToString("\n") { it.trim() }}\nclass Before\n")
82 
83     val afterFile = File(src, "after.kt")
84     afterFile.writeText("${after.split("\n").joinToString("\n") { it.trim() }}\nclass After\n")
85 
86     val args = mutableListOf<String>()
87     args.add(File(JAVA_HOME, "bin/java").path)
88     args.add("-jar")
89     args.add(cli.path)
90     args.add("-pluginsClasspath")
91     val pathSeparator =
92         ";" // instead of File.pathSeparator as would have been reasonable (e.g. : on Unix)
93     val path =
94         listOf(analysis, base, compiler, intellij, coroutines, html, freemarker).joinToString(
95             pathSeparator) {
96               it.path
97             }
98     args.add(path)
99     args.add("-sourceSet")
100     args.add("-src $src") // (nested parameter within -sourceSet)
101     args.add("-outputDir")
102     args.add(out.path)
103     executeProcess(args)
104 
105     fun getHtml(file: File): String {
106       val rendered = file.readText()
107       val begin = rendered.indexOf("<div class=\"copy-popup-wrapper popup-to-left\">")
108       val end = rendered.indexOf("<div class=\"tabbedcontent\">", begin)
109       return rendered.substring(begin, end).replace(Regex(" +"), " ").replace(">", ">\n")
110     }
111 
112     fun getText(file: File): String? {
113       return if (PANDOC != null) {
114         val pandocFile = File(PANDOC)
115         if (!pandocFile.isFile) {
116           error("Cannot execute $pandocFile")
117         }
118         val outFile = File(out, "text.text")
119         executeProcess(listOf(PANDOC, file.path, "-o", outFile.path))
120         val rendered = outFile.readText()
121 
122         val begin = rendered.indexOf("[]{.copy-popup-icon}Content copied to clipboard")
123         val end = rendered.indexOf("::: tabbedcontent", begin)
124         rendered.substring(begin, end).replace(Regex(" +"), " ").replace(">", ">\n")
125       } else {
126         null
127       }
128     }
129 
130     val indexBefore = File("$out/root/[root]/-before/index.html")
131     val beforeContents = getHtml(indexBefore)
132     val indexAfter = File("$out/root/[root]/-after/index.html")
133     val afterContents = getHtml(indexAfter)
134     if (beforeContents != afterContents) {
135       val beforeText = getText(indexBefore)
136       val afterText = getText(indexAfter)
137       if (beforeText != null && afterText != null) {
138         assertThat(beforeText).isEqualTo(afterText)
139         return
140       }
141 
142       assertThat(beforeContents).isEqualTo(afterContents)
143     }
144   }
145 
findnull146   private fun find(prebuilts: File, group: String, artifact: String): File {
147     val versionDir = File(prebuilts, "androidx/external/${group.replace('.','/')}/$artifact")
148     val versions =
149         versionDir.listFiles().filter { it.name.first().isDigit() }.sortedByDescending { it.name }
150     for (version in versions.map { it.name }) {
151       val jar = File(versionDir, "$version/$artifact-$version.jar")
152       if (jar.isFile) {
153         return jar
154       }
155     }
156     error("Could not find a valid jar file for $group:$artifact")
157   }
158 
executeProcessnull159   private fun executeProcess(args: List<String>) {
160     var input: BufferedReader? = null
161     var error: BufferedReader? = null
162     try {
163       val process = Runtime.getRuntime().exec(args.toTypedArray())
164       input = process.inputStream.bufferedReader()
165       error = process.errorStream.bufferedReader()
166       val exitVal = process.waitFor()
167       if (exitVal != 0) {
168         val sb = StringBuilder()
169         sb.append("Failed to execute process\n")
170         sb.append("Command args:\n")
171         for (arg in args) {
172           sb.append("  ").append(arg).append("\n")
173         }
174         sb.append("Standard output:\n")
175         var line: String?
176         while (input.readLine().also { line = it } != null) {
177           sb.append(line).append("\n")
178         }
179         sb.append("Error output:\n")
180         while (error.readLine().also { line = it } != null) {
181           sb.append(line).append("\n")
182         }
183         error(sb.toString())
184       }
185     } catch (t: Throwable) {
186       val sb = StringBuilder()
187       for (arg in args) {
188         sb.append("  ").append(arg).append("\n")
189       }
190       t.printStackTrace()
191       error("Could not run process:\n$sb")
192     } finally {
193       input?.close()
194       error?.close()
195     }
196   }
197 }
198