1414 * limitations under the License.
1515 */
1616
17- import java.io.File
18- import java.io.IOException
19- import java.io.LineNumberReader
20- import java.io.Reader
17+ import java.io.*
2118
22- val DIRECTIVE_START = " <!--- "
23- val DIRECTIVE_END = " -->"
19+ const val DIRECTIVE_START = " <!--- "
20+ const val DIRECTIVE_END = " -->"
2421
25- val TOC_DIRECTIVE = " TOC"
26- val KNIT_DIRECTIVE = " KNIT"
27- val INCLUDE_DIRECTIVE = " INCLUDE"
28- val CLEAR_DIRECTIVE = " CLEAR"
22+ const val TOC_DIRECTIVE = " TOC"
23+ const val KNIT_DIRECTIVE = " KNIT"
24+ const val INCLUDE_DIRECTIVE = " INCLUDE"
25+ const val CLEAR_DIRECTIVE = " CLEAR"
26+ const val TEST_DIRECTIVE = " TEST"
2927
30- val SITE_ROOT_DIRECTIVE = " SITE_ROOT"
31- val DOCS_ROOT_DIRECTIVE = " DOCS_ROOT"
32- val INDEX_DIRECTIVE = " INDEX"
28+ const val TEST_OUT_DIRECTIVE = " TEST_OUT"
3329
34- val CODE_START = " ```kotlin"
35- val CODE_END = " ```"
30+ const val SITE_ROOT_DIRECTIVE = " SITE_ROOT"
31+ const val DOCS_ROOT_DIRECTIVE = " DOCS_ROOT"
32+ const val INDEX_DIRECTIVE = " INDEX"
3633
37- val SECTION_START = " ##"
34+ const val CODE_START = " ```kotlin"
35+ const val CODE_END = " ```"
36+
37+ const val TEST_START = " ```text"
38+ const val TEST_END = " ```"
39+
40+ const val SECTION_START = " ##"
41+
42+ const val PACKAGE_PREFIX = " package "
43+ const val STARTS_WITH_PREDICATE = " STARTS_WITH"
44+ const val FLEXIBLE_TIME_PREDICATE = " FLEXIBLE_TIME"
45+ const val FLEXIBLE_THREAD_PREDICATE = " FLEXIBLE_THREAD"
46+ const val LINES_START_UNORDERED_PREDICATE = " LINES_START_UNORDERED"
47+ const val LINES_START_PREDICATE = " LINES_START"
3848
3949val API_REF_REGEX = Regex (" (^|[ \\ ]])\\ [([A-Za-z0-9_.]+)\\ ]($|[^\\ [\\ (])" )
4050
@@ -53,6 +63,9 @@ fun knit(markdownFileName: String) {
5363 var knitRegex: Regex ? = null
5464 val includes = arrayListOf<Include >()
5565 val code = arrayListOf<String >()
66+ val test = arrayListOf<String >()
67+ var testOut: PrintWriter ? = null
68+ var lastPgk: String? = null
5669 val files = mutableSetOf<String >()
5770 val allApiRefs = arrayListOf<ApiRef >()
5871 val remainingApiRefNames = mutableSetOf<String >()
@@ -72,39 +85,59 @@ fun knit(markdownFileName: String) {
7285 when (directive?.name) {
7386 TOC_DIRECTIVE -> {
7487 requireSingleLine(directive)
75- require(directive.param.isEmpty()) { " TOC directive must not have parameters" }
88+ require(directive.param.isEmpty()) { " $TOC_DIRECTIVE directive must not have parameters" }
7689 require(markdownPart == MarkdownPart .PRE_TOC ) { " Only one TOC directive is supported" }
7790 markdownPart = MarkdownPart .TOC
7891 }
7992 KNIT_DIRECTIVE -> {
8093 requireSingleLine(directive)
81- require(! directive.param.isEmpty()) { " KNIT directive must include regex parameter" }
94+ require(! directive.param.isEmpty()) { " $KNIT_DIRECTIVE directive must include regex parameter" }
8295 require(knitRegex == null ) { " Only one KNIT directive is supported" }
8396 knitRegex = Regex (" \\ ((" + directive.param + " )\\ )" )
8497 continue @mainLoop
8598 }
8699 INCLUDE_DIRECTIVE -> {
87- require(! directive.param.isEmpty()) { " INCLUDE directive must include regex parameter" }
100+ require(! directive.param.isEmpty()) { " $INCLUDE_DIRECTIVE directive must include regex parameter" }
88101 val include = Include (Regex (directive.param))
89102 if (directive.singleLine) {
90103 include.lines + = code
91104 code.clear()
92105 } else {
93- while (true ) {
94- val includeLine = readLine() ? : break
95- if (includeLine.startsWith(DIRECTIVE_END )) break
96- include.lines + = includeLine
97- }
106+ readUntilTo(DIRECTIVE_END , include.lines)
98107 }
99108 includes + = include
100109 continue @mainLoop
101110 }
102111 CLEAR_DIRECTIVE -> {
103112 requireSingleLine(directive)
104- require(directive.param.isEmpty()) { " CLEAR directive must not have parameters" }
113+ require(directive.param.isEmpty()) { " $CLEAR_DIRECTIVE directive must not have parameters" }
105114 code.clear()
106115 continue @mainLoop
107116 }
117+ TEST_OUT_DIRECTIVE -> {
118+ require(! directive.param.isEmpty()) { " $TEST_OUT_DIRECTIVE directive must include file name parameter" }
119+ val file = File (directive.param)
120+ file.parentFile?.mkdirs()
121+ closeTestOut(testOut)
122+ println (" Writing tests to ${directive.param} " )
123+ testOut = PrintWriter (file)
124+ readUntil(DIRECTIVE_END ).forEach { testOut!! .println (it) }
125+ }
126+ TEST_DIRECTIVE -> {
127+ require(lastPgk != null ) { " '$PACKAGE_PREFIX ' prefix was not found in emitted code" }
128+ require(testOut != null ) { " $TEST_OUT_DIRECTIVE directive was not specified" }
129+ var predicate = directive.param
130+ if (test.isEmpty()) {
131+ if (directive.singleLine) {
132+ require(! predicate.isEmpty()) { " $TEST_OUT_DIRECTIVE must be preceded by $TEST_START block or contain test predicate" }
133+ } else
134+ test + = readUntil(DIRECTIVE_END )
135+ } else {
136+ requireSingleLine(directive)
137+ }
138+ writeTest(testOut!! , lastPgk!! , test, predicate)
139+ test.clear()
140+ }
108141 SITE_ROOT_DIRECTIVE -> {
109142 requireSingleLine(directive)
110143 siteRoot = directive.param
@@ -132,12 +165,14 @@ fun knit(markdownFileName: String) {
132165 }
133166 }
134167 if (inLine.startsWith(CODE_START )) {
168+ require(test.isEmpty()) { " Previous test was not emitted with $TEST_DIRECTIVE " }
135169 code + = " "
136- while (true ) {
137- val codeLine = readLine() ? : break
138- if (codeLine.startsWith(CODE_END )) break
139- code + = codeLine
140- }
170+ readUntilTo(CODE_END , code)
171+ continue @mainLoop
172+ }
173+ if (inLine.startsWith(TEST_START )) {
174+ require(test.isEmpty()) { " Previous test was not emitted with $TEST_DIRECTIVE " }
175+ readUntilTo(TEST_END , test)
141176 continue @mainLoop
142177 }
143178 if (inLine.startsWith(SECTION_START ) && markdownPart == MarkdownPart .POST_TOC ) {
@@ -160,12 +195,10 @@ fun knit(markdownFileName: String) {
160195 for (include in includes) {
161196 val includeMatch = include.regex.matchEntire(fileName) ? : continue
162197 include.lines.forEach { includeLine ->
163- var toOutLine = includeLine
164- for ((id, group) in includeMatch.groups.withIndex()) {
165- if (group != null )
166- toOutLine = toOutLine.replace(" \$\$ $id " , group.value)
167- }
168- outLines + = toOutLine
198+ val line = makeReplacements(includeLine, includeMatch)
199+ if (line.startsWith(PACKAGE_PREFIX ))
200+ lastPgk = line.substring(PACKAGE_PREFIX .length).trim()
201+ outLines + = line
169202 }
170203 }
171204 outLines + = code
@@ -176,6 +209,8 @@ fun knit(markdownFileName: String) {
176209 }
177210 }
178211 }
212+ // close test output
213+ closeTestOut(testOut)
179214 // update markdown file with toc
180215 val newLines = buildList<String > {
181216 addAll(markdown.preTocText)
@@ -195,6 +230,77 @@ fun knit(markdownFileName: String) {
195230 }
196231}
197232
233+ fun writeTest (testOut : PrintWriter , pgk : String , test : List <String >, predicate : String ) {
234+ val funName = buildString {
235+ var cap = true
236+ for (c in pgk) {
237+ if (c == ' .' ) {
238+ cap = true
239+ } else {
240+ append(if (cap) c.toUpperCase() else c)
241+ cap = false
242+ }
243+ }
244+ }
245+ with (testOut) {
246+ println ()
247+ println (" @Test" )
248+ println (" fun test$funName () {" )
249+ print (" test { $pgk .main(emptyArray()) }" )
250+ when (predicate) {
251+ " " -> writeTestLines(" verifyLines" , test)
252+ STARTS_WITH_PREDICATE -> writeTestLines(" verifyLinesStartWith" , test)
253+ FLEXIBLE_TIME_PREDICATE -> writeTestLines(" verifyLinesFlexibleTime" , test)
254+ FLEXIBLE_THREAD_PREDICATE -> writeTestLines(" verifyLinesFlexibleThread" , test)
255+ LINES_START_UNORDERED_PREDICATE -> writeTestLines(" verifyLinesStartUnordered" , test)
256+ LINES_START_PREDICATE -> writeTestLines(" verifyLinesStart" , test)
257+ else -> {
258+ println (" .also { lines ->" )
259+ println (" check($predicate )" )
260+ println (" }" )
261+ }
262+ }
263+ println (" }" )
264+ }
265+ }
266+
267+ private fun PrintWriter.writeTestLines (method : String , test : List <String >) {
268+ println (" .$method (" )
269+ for ((index, testLine) in test.withIndex()) {
270+ val commaOpt = if (index < test.size - 1 ) " ," else " "
271+ val escapedLine = testLine.replace(" \" " , " \\\" " )
272+ println (" \" $escapedLine \" $commaOpt " )
273+ }
274+ println (" )" )
275+ }
276+
277+ private fun makeReplacements (line : String , match : MatchResult ): String {
278+ var result = line
279+ for ((id, group) in match.groups.withIndex()) {
280+ if (group != null )
281+ result = result.replace(" \$\$ $id " , group.value)
282+ }
283+ return result
284+ }
285+
286+ private fun closeTestOut (testOut : PrintWriter ? ) {
287+ if (testOut != null ) {
288+ testOut.println (" }" )
289+ testOut.close()
290+ }
291+ }
292+
293+ private fun MarkdownTextReader.readUntil (marker : String ): List <String > =
294+ arrayListOf<String >().also { readUntilTo(marker, it) }
295+
296+ private fun MarkdownTextReader.readUntilTo (marker : String , list : MutableList <String >) {
297+ while (true ) {
298+ val line = readLine() ? : break
299+ if (line.startsWith(marker)) break
300+ list + = line
301+ }
302+ }
303+
198304private inline fun <T > buildList (block : ArrayList <T >.() -> Unit ): List <T > {
199305 val result = arrayListOf<T >()
200306 result.block()
0 commit comments