14
14
* limitations under the License.
15
15
*/
16
16
17
- import java.io.File
18
- import java.io.IOException
19
- import java.io.LineNumberReader
20
- import java.io.Reader
17
+ import java.io.*
21
18
22
- val DIRECTIVE_START = " <!--- "
23
- val DIRECTIVE_END = " -->"
19
+ const val DIRECTIVE_START = " <!--- "
20
+ const val DIRECTIVE_END = " -->"
24
21
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"
29
27
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"
33
29
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"
36
33
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"
38
48
39
49
val API_REF_REGEX = Regex (" (^|[ \\ ]])\\ [([A-Za-z0-9_.]+)\\ ]($|[^\\ [\\ (])" )
40
50
@@ -53,6 +63,9 @@ fun knit(markdownFileName: String) {
53
63
var knitRegex: Regex ? = null
54
64
val includes = arrayListOf<Include >()
55
65
val code = arrayListOf<String >()
66
+ val test = arrayListOf<String >()
67
+ var testOut: PrintWriter ? = null
68
+ var lastPgk: String? = null
56
69
val files = mutableSetOf<String >()
57
70
val allApiRefs = arrayListOf<ApiRef >()
58
71
val remainingApiRefNames = mutableSetOf<String >()
@@ -72,39 +85,59 @@ fun knit(markdownFileName: String) {
72
85
when (directive?.name) {
73
86
TOC_DIRECTIVE -> {
74
87
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" }
76
89
require(markdownPart == MarkdownPart .PRE_TOC ) { " Only one TOC directive is supported" }
77
90
markdownPart = MarkdownPart .TOC
78
91
}
79
92
KNIT_DIRECTIVE -> {
80
93
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" }
82
95
require(knitRegex == null ) { " Only one KNIT directive is supported" }
83
96
knitRegex = Regex (" \\ ((" + directive.param + " )\\ )" )
84
97
continue @mainLoop
85
98
}
86
99
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" }
88
101
val include = Include (Regex (directive.param))
89
102
if (directive.singleLine) {
90
103
include.lines + = code
91
104
code.clear()
92
105
} 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)
98
107
}
99
108
includes + = include
100
109
continue @mainLoop
101
110
}
102
111
CLEAR_DIRECTIVE -> {
103
112
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" }
105
114
code.clear()
106
115
continue @mainLoop
107
116
}
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
+ }
108
141
SITE_ROOT_DIRECTIVE -> {
109
142
requireSingleLine(directive)
110
143
siteRoot = directive.param
@@ -132,12 +165,14 @@ fun knit(markdownFileName: String) {
132
165
}
133
166
}
134
167
if (inLine.startsWith(CODE_START )) {
168
+ require(test.isEmpty()) { " Previous test was not emitted with $TEST_DIRECTIVE " }
135
169
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)
141
176
continue @mainLoop
142
177
}
143
178
if (inLine.startsWith(SECTION_START ) && markdownPart == MarkdownPart .POST_TOC ) {
@@ -160,12 +195,10 @@ fun knit(markdownFileName: String) {
160
195
for (include in includes) {
161
196
val includeMatch = include.regex.matchEntire(fileName) ? : continue
162
197
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
169
202
}
170
203
}
171
204
outLines + = code
@@ -176,6 +209,8 @@ fun knit(markdownFileName: String) {
176
209
}
177
210
}
178
211
}
212
+ // close test output
213
+ closeTestOut(testOut)
179
214
// update markdown file with toc
180
215
val newLines = buildList<String > {
181
216
addAll(markdown.preTocText)
@@ -195,6 +230,77 @@ fun knit(markdownFileName: String) {
195
230
}
196
231
}
197
232
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
+
198
304
private inline fun <T > buildList (block : ArrayList <T >.() -> Unit ): List <T > {
199
305
val result = arrayListOf<T >()
200
306
result.block()
0 commit comments