@@ -6,8 +6,10 @@ import scala.util.matching.Regex
6
6
import scala .io .StdIn .readLine
7
7
import fansi .Color .{Red , Blue , Green }
8
8
import java .security .SecureRandom
9
+ import scala .util .Random
9
10
10
11
val SnippetBlock = """ *```[^ ]+ title=([\w\d\.\-\/_]+) *""" .r
12
+ val CompileBlock = """ *``` *(\w+) +(compile|fail) *(?:title=([\w\d\.\-\/_]+))? *""" .r
11
13
val CodeBlockEnds = """ *``` *""" .r
12
14
val BashCommand = """ *```bash *(fail)? *""" .r
13
15
val CheckBlock = """ *\<\!-- Expected(-regex)?: *""" .r
@@ -18,6 +20,7 @@ case class Options(
18
20
files : Seq [String ] = Nil ,
19
21
dest : Option [os.Path ] = None ,
20
22
stopAtFailure : Boolean = false ,
23
+ statusFile : Option [os.Path ] = None ,
21
24
step : Boolean = false
22
25
)
23
26
@@ -34,9 +37,13 @@ enum Commands:
34
37
cmd.mkString(prefix, " " , " " )
35
38
case Write (name, _, _) =>
36
39
name
40
+ case Compile (_, _, _, _) =>
41
+ " compile snippet"
37
42
}
38
43
39
44
case Write (fileName : String , lines : Seq [String ], context : Context )
45
+
46
+ case Compile (fileName : String , lines : Seq [String ], context : Context , shouldFail : Boolean )
40
47
case Run (scriptLines : Seq [String ], shouldFail : Boolean , context : Context )
41
48
case Check (patterns : Seq [String ], regex : Boolean , context : Context )
42
49
case Clear (context : Context )
@@ -75,6 +82,10 @@ def parse(content: Seq[String], currentCommands: Seq[Commands], context: Context
75
82
case SnippetBlock (name) :: tail =>
76
83
parseMultiline(tail, Commands .Write (name, _, context))
77
84
85
+ case CompileBlock (name, status, fileName) :: tail =>
86
+ val file = Option (fileName).getOrElse(" snippet_" + Random .nextInt(1000 ) + " ." + name)
87
+ parseMultiline(tail, Commands .Compile (file, _, context, status == " fail" ))
88
+
78
89
case BashCommand (failGroup) :: tail =>
79
90
parseMultiline(tail, Commands .Run (_, failGroup != null , context))
80
91
@@ -104,7 +115,7 @@ def checkPath(options: Options)(path: os.Path): Seq[TestCase] =
104
115
toCheck.toList.flatMap(checkPath(options))
105
116
catch
106
117
case e @ FailedCheck (line, file, text) =>
107
- println(Red ( e.getMessage) )
118
+ println(Console . RED + e.getMessage + Console . RESET )
108
119
Seq (TestCase (path.relativeTo(os.pwd), Some (e.getMessage)))
109
120
case e : Throwable =>
110
121
val short = s " Unexpected exception ${e.getClass.getName}"
@@ -161,38 +172,54 @@ def checkFile(file: os.Path, options: Options): Unit =
161
172
def runCommand (cmd : Commands , log : String => Unit ) =
162
173
given Context = cmd.context
163
174
175
+ def writeFile (file : os.Path , code : Seq [String ], c : Context ) =
176
+ val (prefixLines, codeLines) =
177
+ code match
178
+ case shbang :: tail if shbang.startsWith(" #!" ) =>
179
+ List (shbang + " \n " ) -> tail
180
+ case other =>
181
+ Nil -> other
182
+
183
+ codeLines.foreach(log)
184
+
185
+ val prefix =
186
+ if ! shouldAlignContent(file) then prefixLines.mkString(" " )
187
+ else prefixLines.mkString(" " , " " , s " $fakeLineMarker\n " * c.line)
188
+
189
+ os.write.over(file, code.mkString(prefix, " \n " , " " ), createFolders = true )
190
+
191
+ def run (cmd : os.proc): Int =
192
+ val res = cmd.call(cwd = out, mergeErrIntoOut = true , check = false )
193
+
194
+ log(res.out.text())
195
+
196
+ lastOutput = res.out.text()
197
+ res.exitCode
198
+
164
199
cmd match
165
200
case Commands .Run (cmds, shouldFail, _) =>
166
201
val script = out / " .scala-build" / " run.sh"
167
202
os.write.over(script, mkBashScript(cmds), createFolders = true )
168
203
os.perms.set(script, " rwxr-xr-x" )
169
- val res = os.proc(script).call(cwd = out, mergeErrIntoOut = true , check = false )
170
-
171
- log(res.out.text())
172
204
205
+ val exitCode = run(os.proc(script))
173
206
if shouldFail then
174
- check(res. exitCode != 0 , s " Commands should fail. " )
207
+ check(exitCode != 0 , s " Commands should fail. " )
175
208
else
176
- check(res.exitCode == 0 , s " Commands failed. " )
177
-
178
- lastOutput = res.out.text()
209
+ check(exitCode == 0 , s " Commands failed. " )
179
210
180
211
case Commands .Write (name, code, c) =>
181
- val (prefixLines, codeLines) =
182
- code match
183
- case shbang :: tail if shbang.startsWith(" #!" ) =>
184
- List (shbang + " \n " ) -> tail
185
- case other =>
186
- Nil -> other
187
-
188
- val file = out / os.RelPath (name)
189
- codeLines.foreach(log)
212
+ writeFile(out / os.RelPath (name), code, c)
190
213
191
- val prefix =
192
- if ! shouldAlignContent(file) then prefixLines.mkString( " " )
193
- else prefixLines.mkString( " " , " " , s " $fakeLineMarker \n " * c.line )
214
+ case Commands . Compile (name, code, c, shouldFail) =>
215
+ val dest = out / " .snippets " / name
216
+ writeFile(dest, code, c )
194
217
195
- os.write.over(file, code.mkString(prefix, " \n " , " " ), createFolders = true )
218
+ val exitCode = run(os.proc(" scala-cli" , " compile" , dest))
219
+ if shouldFail then
220
+ check(exitCode != 0 , s " Compilation should fail. " )
221
+ else
222
+ check(exitCode == 0 , s " Compilation failed. " )
196
223
197
224
case Commands .Check (patterns, regex, line) =>
198
225
check(lastOutput != null , " No output stored from previous commands" )
@@ -202,7 +229,7 @@ def checkFile(file: os.Path, options: Options): Unit =
202
229
patterns.foreach { pattern =>
203
230
val regex = pattern.r
204
231
check(
205
- lines.exists(regex.matches ),
232
+ lines.exists(regex.findFirstIn(_).nonEmpty ),
206
233
s " Regex: $pattern, does not matches any line in: \n $lastOutput"
207
234
)
208
235
}
@@ -281,14 +308,20 @@ def checkFile(file: os.Path, options: Options): Unit =
281
308
282
309
os.list(out).filter(_.toString.endsWith(" .scala" )).foreach(p => os.copy.into(p, exampleDir))
283
310
311
+ def asPath (pathStr : String ): os.Path =
312
+ os.FilePath (pathStr) match
313
+ case p : os.Path => p
314
+ case s : os.SubPath => os.pwd / s
315
+ case r : os.RelPath => os.pwd / r
316
+
284
317
@ main def check (args : String * ) =
285
- def processFiles (dest : Options ) =
286
- val paths = dest .files.map { str =>
287
- val path = os.pwd / os. RelPath (str)
318
+ def processFiles (options : Options ) =
319
+ val paths = options .files.map { str =>
320
+ val path = asPath (str)
288
321
assert(os.exists(path), s " Provided path $str does not exists in ${os.pwd}" )
289
322
path
290
323
}
291
- val testCases = paths.flatMap(checkPath(dest ))
324
+ val testCases = paths.flatMap(checkPath(options ))
292
325
val (failed, ok) = testCases.partition(_.failure.nonEmpty)
293
326
if testCases.size > 1 then
294
327
if ok.nonEmpty then
@@ -303,20 +336,35 @@ def checkFile(file: os.Path, options: Options): Unit =
303
336
println(" " )
304
337
sys.exit(1 )
305
338
339
+ options.statusFile.foreach { file =>
340
+ os.write.over(file, s " Test completed: \n ${testCases.map(_.path).mkString(" \n " )}" )
341
+ }
342
+
343
+ case class PathParameter (name : String ):
344
+ def unapply (args : Seq [String ]): Option [(os.Path , Seq [String ])] = args.match
345
+ case `name` :: param :: tail =>
346
+ if param.startsWith(" --" ) then
347
+ println(s " Please provide file name not an option: $param" )
348
+ sys.exit(1 )
349
+ Some ((asPath(param), tail))
350
+ case `name` :: Nil =>
351
+ println(Red (s " Expected an argument after `-- $name` parameter " ))
352
+ sys.exit(1 )
353
+ case _ => None
354
+
355
+ val Dest = PathParameter (" --dest" )
356
+ val StatusFile = PathParameter (" --status-file" )
357
+
306
358
def parseArgs (args : Seq [String ], options : Options ): Options = args match
307
359
case Nil => options
308
360
case " --step" :: rest =>
309
361
parseArgs(rest, options.copy(step = true ))
310
362
case " --stopAtFailure" :: rest =>
311
363
parseArgs(rest, options.copy(stopAtFailure = true ))
312
- case " --dest" :: dest :: rest =>
313
- if dest.startsWith(" --" ) then
314
- println(s " Please provide file name not an option: $dest" )
315
-
316
- parseArgs(rest, options.copy(dest = Some (os.pwd / dest)))
317
- case " --dest" :: Nil =>
318
- println(Red (" Exptected a destanation after `--dest` parameter" ))
319
- sys.exit(1 )
364
+ case Dest (dest, rest) =>
365
+ parseArgs(rest, options.copy(dest = Some (dest)))
366
+ case StatusFile (file, rest) =>
367
+ parseArgs(rest, options.copy(statusFile = Some (file)))
320
368
case path :: rest => parseArgs(rest, options.copy(files = options.files :+ path))
321
369
322
370
processFiles(parseArgs(args, Options ()))
0 commit comments