-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
Copy pathParallelTesting.scala
1864 lines (1597 loc) · 78 KB
/
ParallelTesting.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package dotty
package tools
package vulpix
import scala.language.unsafeNulls
import java.io.{File => JFile, IOException, PrintStream, ByteArrayOutputStream}
import java.lang.System.{lineSeparator => EOL}
import java.lang.management.ManagementFactory
import java.net.URL
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
import java.nio.file.{Files, NoSuchFileException, Path, Paths}
import java.nio.charset.{Charset, StandardCharsets}
import java.text.SimpleDateFormat
import java.util.{HashMap, Timer, TimerTask}
import java.util.concurrent.{TimeUnit, TimeoutException, Executors => JExecutors}
import scala.collection.mutable
import scala.io.{Codec, Source}
import scala.jdk.CollectionConverters.*
import scala.util.{Random, Try, Failure => TryFailure, Success => TrySuccess, Using}
import scala.util.control.NonFatal
import scala.util.matching.Regex
import scala.collection.mutable.ListBuffer
import dotc.{Compiler, Driver}
import dotc.core.Contexts.*
import dotc.decompiler
import dotc.report
import dotc.interfaces.Diagnostic.ERROR
import dotc.reporting.{Reporter, TestReporter}
import dotc.reporting.Diagnostic
import dotc.config.Config
import dotc.util.{DiffUtil, SourceFile, SourcePosition, Spans, NoSourcePosition}
import io.AbstractFile
import dotty.tools.vulpix.TestConfiguration.defaultOptions
/** A parallel testing suite whose goal is to integrate nicely with JUnit
*
* This trait can be mixed in to offer parallel testing to compile runs. When
* using this, you should be running your JUnit tests **sequentially**, as the
* test suite itself runs with a high level of concurrency.
*/
trait ParallelTesting extends RunnerOrchestration { self =>
import ParallelTesting._
/** If the running environment supports an interactive terminal, each `Test`
* will be run with a progress bar and real time feedback
*/
def isInteractive: Boolean
/** A list of strings which is used to filter which tests to run, if `Nil` will run
* all tests. All absolute paths that contain any of the substrings in `testFilter`
* will be run
*/
def testFilter: List[String]
/** Tests should override the checkfiles with the current output */
def updateCheckFiles: Boolean
/** Contains a list of failed tests to run, if list is empty no tests will run */
def failedTests: Option[List[String]]
protected def testPlatform: TestPlatform = TestPlatform.JVM
/** A test source whose files or directory of files is to be compiled
* in a specific way defined by the `Test`
*/
sealed trait TestSource { self =>
def name: String
def outDir: JFile
def flags: TestFlags
def sourceFiles: Array[JFile]
def checkFile: Option[JFile]
def runClassPath: String = outDir.getPath + JFile.pathSeparator + flags.runClassPath
def title: String = self match {
case self: JointCompilationSource =>
if (self.files.length > 1) name
else self.files.head.getPath
case self: SeparateCompilationSource =>
self.dir.getPath
}
/** Adds the flags specified in `newFlags0` if they do not already exist */
def withFlags(newFlags0: String*) = {
val newFlags = newFlags0.toArray
if (!flags.options.containsSlice(newFlags)) self match {
case self: JointCompilationSource =>
self.copy(flags = flags.and(newFlags*))
case self: SeparateCompilationSource =>
self.copy(flags = flags.and(newFlags*))
}
else self
}
def withoutFlags(flags1: String*): TestSource = self match {
case self: JointCompilationSource =>
self.copy(flags = flags.without(flags1*))
case self: SeparateCompilationSource =>
self.copy(flags = flags.without(flags1*))
}
lazy val allToolArgs: ToolArgs =
toolArgsFor(sourceFiles.toList.map(_.toPath), getCharsetFromEncodingOpt(flags))
/** Generate the instructions to redo the test from the command line */
def buildInstructions(errors: Int, warnings: Int): String = {
val sb = new StringBuilder
val maxLen = 80
var lineLen = 0
val delimiter = " "
sb.append(
s"""|
|Test '$title' compiled with $errors error(s) and $warnings warning(s),
|the test can be reproduced by running from SBT (prefix it with ./bin/ if you
|want to run from the command line):""".stripMargin
)
sb.append("\n\nscalac ")
flags.all.foreach { arg =>
if (lineLen > maxLen) {
sb.append(delimiter)
lineLen = 4
}
sb.append(arg)
lineLen += arg.length
sb += ' '
}
self match {
case source: JointCompilationSource => {
source.sourceFiles.map(_.getPath).foreach { path =>
sb.append(delimiter)
sb += '\''
sb.append(path)
sb += '\''
sb += ' '
}
sb.toString + "\n\n"
}
case self: SeparateCompilationSource => { // TODO: this won't work when using other versions of compiler
val command = sb.toString
val fsb = new StringBuilder(command)
self.compilationGroups.foreach { (_, files) =>
files.map(_.getPath).foreach { path =>
fsb.append(delimiter)
lineLen = 8
fsb += '\''
fsb.append(path)
fsb += '\''
fsb += ' '
}
fsb.append("\n\n")
fsb.append(command)
}
fsb.toString + "\n\n"
}
}
}
final override def toString: String = sourceFiles match {
case Array(f) => f.getPath
case _ => outDir.getPath.stripPrefix(defaultOutputDir).stripPrefix(name).stripPrefix("/")
}
}
private sealed trait FromTastyCompilationMode
private case object NotFromTasty extends FromTastyCompilationMode
private case object FromTasty extends FromTastyCompilationMode
private case object FromBestEffortTasty extends FromTastyCompilationMode
private case class WithBestEffortTasty(bestEffortDir: JFile) extends FromTastyCompilationMode
/** A group of files that may all be compiled together, with the same flags
* and output directory
*/
private case class JointCompilationSource(
name: String,
files: Array[JFile],
flags: TestFlags,
outDir: JFile,
fromTasty: FromTastyCompilationMode = NotFromTasty,
decompilation: Boolean = false
) extends TestSource {
def sourceFiles: Array[JFile] = files.filter(isSourceFile)
def checkFile: Option[JFile] =
sourceFiles.map(f => new JFile(f.getPath.replaceFirst("\\.(scala|java)$", ".check")))
.find(_.exists())
}
/** A test source whose files will be compiled separately according to their
* suffix `_X`
*/
case class SeparateCompilationSource(
name: String,
dir: JFile,
flags: TestFlags,
outDir: JFile
) extends TestSource {
case class Group(ordinal: Int, compiler: String)
lazy val compilationGroups: List[(Group, Array[JFile])] =
val Compiler = """c([\d\.]+)""".r
val Ordinal = """(\d+)""".r
def groupFor(file: JFile): Group =
val groupSuffix = file.getName.dropWhile(_ != '_').stripSuffix(".scala").stripSuffix(".java")
val groupSuffixParts = groupSuffix.split("_")
val ordinal = groupSuffixParts.collectFirst { case Ordinal(n) => n.toInt }.getOrElse(Int.MinValue)
val compiler = groupSuffixParts.collectFirst { case Compiler(c) => c }.getOrElse("")
Group(ordinal, compiler)
dir.listFiles
.filter(isSourceFile)
.groupBy(groupFor)
.toList
.sortBy { (g, _) => (g.ordinal, g.compiler) }
.map { (g, f) => (g, f.sorted) }
def sourceFiles = compilationGroups.map(_._2).flatten.toArray
def checkFile: Option[JFile] =
val platform =
if allToolArgs.getOrElse(ToolName.Target, Nil).nonEmpty then s".$testPlatform"
else ""
Some(new JFile(dir.getPath + platform + ".check")).filter(_.exists)
}
protected def shouldSkipTestSource(testSource: TestSource): Boolean = false
protected def shouldReRun(testSource: TestSource): Boolean =
failedTests.forall(rerun => testSource match {
case JointCompilationSource(_, files, _, _, _, _) =>
rerun.exists(filter => files.exists(file => file.getPath.contains(filter)))
case SeparateCompilationSource(_, dir, _, _) =>
rerun.exists(dir.getPath.contains)
})
protected trait CompilationLogic { this: Test =>
def suppressErrors = false
/**
* Compiles the test source.
* @return The reporters containing the results of all the compilation runs for this test source.
*/
private final def compileTestSource(testSource: TestSource): Try[List[TestReporter]] =
Try(testSource match {
case testSource @ JointCompilationSource(name, files, flags, outDir, fromTasty, decompilation) =>
val reporter = fromTasty match
case NotFromTasty => compile(testSource.sourceFiles, flags, outDir)
case FromTasty => compileFromTasty(flags, outDir)
case FromBestEffortTasty => compileFromBestEffortTasty(flags, outDir)
case WithBestEffortTasty(bestEffortDir) => compileWithBestEffortTasty(testSource.sourceFiles, bestEffortDir, flags, outDir)
List(reporter)
case testSource @ SeparateCompilationSource(_, dir, flags, outDir) =>
testSource.compilationGroups.map { (group, files) =>
if group.compiler.isEmpty then
compile(files, flags, outDir)
else
compileWithOtherCompiler(group.compiler, files, flags, outDir)
}
})
final def countErrorsAndWarnings(reporters: Seq[TestReporter]): (Int, Int) =
reporters.foldLeft((0, 0)) { case ((err, warn), r) => (err + r.errorCount, warn + r.warningCount) }
final def countErrors (reporters: Seq[TestReporter]) = countErrorsAndWarnings(reporters)._1
final def countWarnings(reporters: Seq[TestReporter]) = countErrorsAndWarnings(reporters)._2
final def reporterFailed(r: TestReporter) = r.errorCount > 0
/**
* Checks if the given actual lines are the same as the ones in the check file.
* If not, fails the test.
*/
final def diffTest(testSource: TestSource, checkFile: JFile, actual: List[String], reporters: Seq[TestReporter], logger: LoggedRunnable) = {
for (msg <- FileDiff.check(testSource.title, actual, checkFile.getPath)) {
if (updateCheckFiles) {
FileDiff.dump(checkFile.toPath.toString, actual)
echo("Updated checkfile: " + checkFile.getPath)
} else {
onFailure(testSource, reporters, logger, Some(msg))
val outFile = checkFile.toPath.resolveSibling(s"${checkFile.toPath.getFileName}.out").toString
FileDiff.dump(outFile, actual)
echo(FileDiff.diffMessage(checkFile.getPath, outFile))
}
}
}
/** Entry point: runs the test */
final def encapsulatedCompilation(testSource: TestSource) = new LoggedRunnable { self =>
def checkTestSource(): Unit = tryCompile(testSource) {
registerStart
val reportersOrCrash = compileTestSource(testSource)
onComplete(testSource, reportersOrCrash, self)
registerCompletion()
}
}
/** This callback is executed once the compilation of this test source finished */
private final def onComplete(testSource: TestSource, reportersOrCrash: Try[Seq[TestReporter]], logger: LoggedRunnable): Unit =
try
reportersOrCrash match
case TryFailure(exn) => onFailure(testSource, Nil, logger, Some(s"Fatal compiler crash when compiling: ${testSource.title}:\n${exn.getMessage}${exn.getStackTrace.map("\n\tat " + _).mkString}"))
case TrySuccess(reporters) if !reporters.exists(_.skipped) =>
maybeFailureMessage(testSource, reporters) match {
case Some(msg) => onFailure(testSource, reporters, logger, Option(msg).filter(_.nonEmpty))
case None => onSuccess(testSource, reporters, logger)
}
case _ =>
catch case ex: Throwable =>
echo(s"Exception thrown onComplete (probably by a reporter) in $testSource: ${ex.getClass}")
Try(ex.printStackTrace())
.recover{ _ =>
val trace = ex.getStackTrace.map(_.toString) // compute this first in case getStackTrace throws an exception
echo(s"${ex.getClass.getName} message could not be printed due to an exception while computing the message.")
if trace.nonEmpty then trace.foreach(echo) else echo(s"${ex.getClass.getName} stack trace is empty.")
}
.getOrElse(echo(s"${ex.getClass.getName} stack trace could not be printed due to an exception while printing the stack trace."))
failTestSource(testSource)
/**
* Based on the reporters obtained after the compilation, determines if this test has failed.
* If it has, returns a Some with an error message. Otherwise, returns None.
* As the conditions of failure are different for different test types, this method should be
* overridden by the concrete implementations of this trait.
*/
def maybeFailureMessage(testSource: TestSource, reporters: Seq[TestReporter]): Option[String] =
if (reporters.exists(reporterFailed)) Some(s"Compilation failed for: '${testSource.title}'")
else None
/**
* If the test has compiled successfully, this callback will be called. You can still fail the test from this callback.
*/
def onSuccess(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable): Unit = ()
/**
* If the test failed to compile or the compiler crashed, this callback will be called.
*/
def onFailure(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable, message: Option[String]): Unit = {
message.foreach(echo)
reporters.filter(reporterFailed).foreach(logger.logReporterContents)
logBuildInstructions(testSource, reporters)
failTestSource(testSource)
}
}
/** Each `Test` takes the `testSources` and performs the compilation and assertions
* according to the implementing class "neg", "run" or "pos".
*/
protected class Test(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit val summaryReport: SummaryReporting) extends CompilationLogic { test =>
import summaryReport._
protected final val realStdout = System.out
protected final val realStderr = System.err
/** A runnable that logs its contents in a buffer */
trait LoggedRunnable extends Runnable {
/** Instances of `LoggedRunnable` implement this method instead of the
* `run` method
*/
def checkTestSource(): Unit
private val logBuffer = mutable.ArrayBuffer.empty[String]
def log(msg: String): Unit = logBuffer.append(msg)
def logReporterContents(reporter: TestReporter): Unit =
reporter.messages.foreach(log)
def echo(msg: String): Unit = {
log(msg)
test.echo(msg)
}
final def run(): Unit = {
checkTestSource()
summaryReport.echoToLog(logBuffer.iterator)
}
}
/** All testSources left after filtering out */
private val filteredSources =
val filteredByName =
if (testFilter.isEmpty) testSources
else testSources.filter {
case JointCompilationSource(_, files, _, _, _, _) =>
testFilter.exists(filter => files.exists(file => file.getPath.contains(filter)))
case SeparateCompilationSource(_, dir, _, _) =>
testFilter.exists(dir.getPath.contains)
}
filteredByName.filterNot(shouldSkipTestSource(_)).filter(shouldReRun(_))
/** Total amount of test sources being compiled by this test */
val sourceCount = filteredSources.length
private var testSourcesStarted = 0
private var _testSourcesCompleted = 0
private def testSourcesCompleted: Int = _testSourcesCompleted
protected final def registerStart = synchronized:
testSourcesStarted += 1
/** Complete the current compilation with the amount of errors encountered */
protected final def registerCompletion() = synchronized {
_testSourcesCompleted += 1
}
sealed trait Failure
case class JavaCompilationFailure(reason: String) extends Failure
case class TimeoutFailure(title: String) extends Failure
case object Generic extends Failure
private var _failures = Set.empty[Failure]
private var _failureCount = 0
/** Fail the current test */
protected final def fail(failure: Failure = Generic): Unit = synchronized {
_failures = _failures + failure
_failureCount = _failureCount + 1
}
def didFail: Boolean = _failureCount != 0
/** A set of the different failures */
def failureReasons: Set[Failure] = _failures
/** Number of failed tests */
def failureCount: Int = _failureCount
private var _skipCount = 0
protected final def registerSkip(): Unit = synchronized { _skipCount += 1 }
def skipCount: Int = _skipCount
protected def logBuildInstructions(testSource: TestSource, reporters: Seq[TestReporter]) = {
val (errCount, warnCount) = countErrorsAndWarnings(reporters)
val errorMsg = testSource.buildInstructions(errCount, warnCount)
addFailureInstruction(errorMsg)
}
/** Instructions on how to reproduce failed test source compilations */
private val reproduceInstructions = mutable.ArrayBuffer.empty[String]
protected final def addFailureInstruction(ins: String): Unit =
synchronized { reproduceInstructions.append(ins) }
/** The test sources that failed according to the implementing subclass */
private val failedTestSources = mutable.ArrayBuffer.empty[FailedTestInfo]
protected final def failTestSource(testSource: TestSource, reason: Failure = Generic) = synchronized {
val extra = reason match {
case TimeoutFailure(title) => s", test '$title' timed out"
case JavaCompilationFailure(msg) => s", java test sources failed to compile with: \n$msg"
case Generic => ""
}
failedTestSources.append(FailedTestInfo(testSource.title, s" failed" + extra))
fail(reason)
}
/** Prints to `System.err` if we're not suppressing all output */
protected def echo(msg: String): Unit = if (!suppressAllOutput) {
// pad right so that output is at least as large as progress bar line
val paddingRight = " " * math.max(0, 80 - msg.length)
realStderr.println(msg + paddingRight)
}
/** Print a progress bar for the current `Test` */
private def updateProgressMonitor(start: Long): Unit =
if testSourcesCompleted < sourceCount && !isUserDebugging then
realStdout.print(s"\r${makeProgressBar(start)}")
private def finishProgressMonitor(start: Long): Unit =
realStdout.println(s"\r${makeProgressBar(start)}")
private def makeProgressBar(start: Long): String =
val tCompiled = testSourcesCompleted
val timestamp = (System.currentTimeMillis - start) / 1000
val progress = (tCompiled.toDouble / sourceCount * 40).toInt
val past = "=" * math.max(progress - 1, 0)
val curr = if progress > 0 then ">" else ""
val next = " " * (40 - progress)
s"[$past$curr$next] completed ($tCompiled/$sourceCount, ${testSourcesStarted} started, $failureCount failed, ${timestamp}s)"
/** Wrapper function to make sure that the compiler itself did not crash -
* if it did, the test should automatically fail.
*/
protected def tryCompile(testSource: TestSource)(op: => Unit): Unit =
try op
catch
case e: Throwable =>
// if an exception is thrown during compilation, the complete test
// run should fail
failTestSource(testSource)
e.printStackTrace()
registerCompletion()
throw e
protected def compile(files0: Array[JFile], flags0: TestFlags, targetDir: JFile): TestReporter = {
import scala.util.Properties.*
def flattenFiles(f: JFile): Array[JFile] =
if (f.isDirectory) f.listFiles.flatMap(flattenFiles)
else Array(f)
val files: Array[JFile] = files0.flatMap(flattenFiles)
val (platformFiles, toolArgs) =
platformAndToolArgsFor(files.toList.map(_.toPath), getCharsetFromEncodingOpt(flags0))
val spec = raw"(\d+)(\+)?".r
val testIsFiltered = toolArgs.get(ToolName.Test) match
case Some("-jvm" :: spec(n, more) :: Nil) =>
if more == "+" then isJavaAtLeast(n) else javaSpecVersion == n
case Some(args) => throw new IllegalStateException(args.mkString("unknown test option: ", ", ", ""))
case None => true
def scalacOptions = toolArgs.getOrElse(ToolName.Scalac, Nil)
def javacOptions = toolArgs.getOrElse(ToolName.Javac, Nil)
val flags = flags0
.and(scalacOptions*)
.and("-d", targetDir.getPath)
.withClasspath(targetDir.getPath)
def compileWithJavac(fs: Array[String]) = if (fs.nonEmpty) {
val fullArgs = Array(
"-encoding", StandardCharsets.UTF_8.name,
) ++ flags.javacFlags ++ javacOptions++ fs
val process = Runtime.getRuntime.exec("javac" +: fullArgs)
val output = Source.fromInputStream(process.getErrorStream).mkString
if process.waitFor() != 0 then Some(output)
else None
} else None
val reporter = mkReporter
val driver =
if (times == 1) new Driver
else new Driver {
private def ntimes(n: Int)(op: Int => Reporter): Reporter =
(1 to n).foldLeft(emptyReporter) ((_, i) => op(i))
override def doCompile(comp: Compiler, files: List[AbstractFile])(using Context) =
ntimes(times) { run =>
val start = System.nanoTime()
val rep = super.doCompile(comp, files)
report.echo(s"\ntime run $run: ${(System.nanoTime - start) / 1000000}ms")
rep
}
}
val allArgs = flags.all
if testIsFiltered then
// If a test contains a Java file that cannot be parsed by Dotty's Java source parser, its
// name must contain the string "JAVA_ONLY".
val dottyFiles = files.filterNot(_.getName.contains("JAVA_ONLY")).map(_.getPath)
val dottyFiles0 =
if platformFiles.isEmpty then dottyFiles
else
val excludedFiles = platformFiles
.collect { case (plat, files) if plat != testPlatform => files }
.flatten
.toSet
dottyFiles.filterNot(excludedFiles)
driver.process(allArgs ++ dottyFiles0, reporter = reporter)
// todo a better mechanism than ONLY. test: -scala-only?
val javaFiles = files.filter(_.getName.endsWith(".java")).filterNot(_.getName.contains("SCALA_ONLY")).map(_.getPath)
val javaErrors = compileWithJavac(javaFiles)
if (javaErrors.isDefined) {
echo(s"\njava compilation failed: \n${ javaErrors.get }")
fail(failure = JavaCompilationFailure(javaErrors.get))
}
else
registerSkip()
reporter.setSkip()
end if
reporter
}
private def parseErrors(errorsText: String, compilerVersion: String, pageWidth: Int) =
val errorPattern = """^.*Error: (.*\.scala):(\d+):(\d+).*""".r
val brokenClassPattern = """^class file (.*) is broken.*""".r
val warnPattern = """^.*Warning: (.*\.scala):(\d+):(\d+).*""".r
val summaryPattern = """\d+ (?:warning|error)s? found""".r
val indent = " "
var diagnostics = List.empty[Diagnostic.Error]
def barLine(start: Boolean) = s"$indent${if start then "╭" else "╰"}${"┄" * pageWidth}${if start then "╮" else "╯"}\n"
def errorLine(line: String) = s"$indent┆${String.format(s"%-${pageWidth}s", stripAnsi(line))}┆\n"
def stripAnsi(str: String): String = str.replaceAll("\u001b\\[\\d+m", "")
def addToLast(str: String): Unit =
diagnostics match
case head :: tail =>
diagnostics = Diagnostic.Error(s"${head.msg.message}$str", head.pos) :: tail
case Nil =>
var inError = false
for line <- errorsText.linesIterator do
line match
case error @ warnPattern(filePath, line, column) =>
inError = false
case error @ errorPattern(filePath, line, column) =>
inError = true
val lineNum = line.toInt
val columnNum = column.toInt
val abstractFile = AbstractFile.getFile(filePath)
val sourceFile = SourceFile(abstractFile, Codec.UTF8)
val offset = sourceFile.lineToOffset(lineNum - 1) + columnNum - 1
val span = Spans.Span(offset)
val sourcePos = SourcePosition(sourceFile, span)
addToLast(barLine(start = false))
diagnostics ::= Diagnostic.Error(s"Compilation of $filePath with Scala $compilerVersion failed at line: $line, column: $column.\nFull error output:\n${barLine(start = true)}${errorLine(error)}", sourcePos)
case error @ brokenClassPattern(filePath) =>
inError = true
diagnostics ::= Diagnostic.Error(s"$error\nFull error output:\n${barLine(start = true)}${errorLine(error)}", NoSourcePosition)
case summaryPattern() => // Ignored
case line if inError => addToLast(errorLine(line))
case _ =>
addToLast(barLine(start = false))
diagnostics.reverse
protected def compileWithOtherCompiler(compiler: String, files: Array[JFile], flags: TestFlags, targetDir: JFile): TestReporter =
def artifactClasspath(organizationName: String, moduleName: String) =
import coursier._
val dep = Dependency(
Module(
Organization(organizationName),
ModuleName(moduleName),
attributes = Map.empty
),
version = compiler
)
Fetch()
.addDependencies(dep)
.run()
.mkString(JFile.pathSeparator)
val pageWidth = TestConfiguration.pageWidth - 20
val fileArgs = files.map(_.getAbsolutePath)
def scala2Command(): Array[String] = {
assert(!flags.options.contains("-scalajs"),
"Compilation tests with Scala.js on Scala 2 are not supported.\nThis test can be skipped using the `// scalajs: --skip` tag")
val stdlibClasspath = artifactClasspath("org.scala-lang", "scala-library")
val scalacClasspath = artifactClasspath("org.scala-lang", "scala-compiler")
val flagsArgs = flags
.copy(options = Array.empty, defaultClassPath = stdlibClasspath)
.withClasspath(targetDir.getPath)
.and("-d", targetDir.getPath)
.all
val scalacCommand = Array("java", "-cp", scalacClasspath, "scala.tools.nsc.Main")
scalacCommand ++ flagsArgs ++ fileArgs
}
def scala3Command(): Array[String] = {
val stdlibClasspath = artifactClasspath("org.scala-lang", "scala3-library_3")
val scalacClasspath = artifactClasspath("org.scala-lang", "scala3-compiler_3")
val flagsArgs = flags
.copy(defaultClassPath = stdlibClasspath)
.withClasspath(targetDir.getPath)
.and("-d", targetDir.getPath)
.and("-pagewidth", pageWidth.toString)
.all
val scalacCommand = Array("java", "-cp", scalacClasspath, "dotty.tools.dotc.Main")
scalacCommand ++ flagsArgs ++ fileArgs
}
val command = if compiler.startsWith("2") then scala2Command() else scala3Command()
val process = Runtime.getRuntime.exec(command)
val reporter = mkReporter
val errorsText = Source.fromInputStream(process.getErrorStream).mkString
if process.waitFor() != 0 then
val diagnostics = parseErrors(errorsText, compiler, pageWidth)
diagnostics.foreach { diag =>
val context = (new ContextBase).initialCtx
reporter.report(diag)(using context)
}
reporter
protected def compileFromBestEffortTasty(flags0: TestFlags, targetDir: JFile): TestReporter = {
val classes = flattenFiles(targetDir).filter(isBestEffortTastyFile).map(_.toString)
val flags = flags0 and "-from-tasty" and "-Ywith-best-effort-tasty"
val reporter = mkReporter
val driver = new Driver
driver.process(flags.all ++ classes, reporter = reporter)
reporter
}
protected def compileWithBestEffortTasty(files0: Array[JFile], bestEffortDir: JFile, flags0: TestFlags, targetDir: JFile): TestReporter = {
val flags = flags0
.and("-Ywith-best-effort-tasty")
.and("-d", targetDir.getPath)
val reporter = mkReporter
val driver = new Driver
val args = Array("-classpath", flags.defaultClassPath + JFile.pathSeparator + bestEffortDir.toString) ++ flags.options
driver.process(args ++ files0.map(_.toString), reporter = reporter)
reporter
}
protected def compileFromTasty(flags0: TestFlags, targetDir: JFile): TestReporter = {
val tastyOutput = new JFile(targetDir.getPath + "_from-tasty")
tastyOutput.mkdir()
val flags = flags0 and ("-d", tastyOutput.getPath) and "-from-tasty"
val classes = flattenFiles(targetDir).filter(isTastyFile).map(_.toString)
val reporter = mkReporter
val driver = new Driver
driver.process(flags.all ++ classes, reporter = reporter)
reporter
}
private def mkLogLevel = if suppressErrors || suppressAllOutput then ERROR + 1 else ERROR
private def mkReporter = TestReporter.reporter(realStdout, logLevel = mkLogLevel)
protected def diffCheckfile(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable) =
testSource.checkFile.foreach(diffTest(testSource, _, reporterOutputLines(reporters), reporters, logger))
private def reporterOutputLines(reporters: Seq[TestReporter]): List[String] =
reporters.flatMap(_.consoleOutput.split("\n")).toList
private[ParallelTesting] def executeTestSuite(): this.type = {
assert(testSourcesCompleted == 0, "not allowed to re-use a `CompileRun`")
if filteredSources.nonEmpty then
val pool = JExecutors.newWorkStealingPool(threadLimit.getOrElse(Runtime.getRuntime.availableProcessors()))
val timer = new Timer()
val logProgress = isInteractive && !suppressAllOutput
val start = System.currentTimeMillis()
if logProgress then
timer.schedule((() => updateProgressMonitor(start)): TimerTask, 100/*ms*/, 200/*ms*/)
val eventualResults = for target <- filteredSources yield
pool.submit(encapsulatedCompilation(target))
pool.shutdown()
if !pool.awaitTermination(20, TimeUnit.MINUTES) then
val remaining = new ListBuffer[TestSource]
for (src, res) <- filteredSources.lazyZip(eventualResults) do
if !res.isDone then
remaining += src
pool.shutdownNow()
System.setOut(realStdout)
System.setErr(realStderr)
throw new TimeoutException(s"Compiling targets timed out, remaining targets: ${remaining.mkString(", ")}")
end if
for fut <- eventualResults do
try fut.get()
catch case ex: Exception =>
System.err.println(ex.getMessage)
ex.printStackTrace()
if logProgress then
timer.cancel()
finishProgressMonitor(start)
if didFail then
reportFailed()
failedTestSources.toSet.foreach(addFailedTest)
reproduceInstructions.foreach(addReproduceInstruction)
else reportPassed()
else echo {
testFilter match
case _ :: _ => s"""No files matched "${testFilter.mkString(",")}" in test"""
case _ => "No tests available under target - erroneous test?"
}
this
}
/** Returns all files in directory or the file if not a directory */
private def flattenFiles(f: JFile): Array[JFile] =
if (f.isDirectory) f.listFiles.flatMap(flattenFiles)
else Array(f)
}
private final class PosTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting)
extends Test(testSources, times, threadLimit, suppressAllOutput)
private final class WarnTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting)
extends Test(testSources, times, threadLimit, suppressAllOutput):
override def suppressErrors = true
override def onSuccess(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable): Unit =
diffCheckfile(testSource, reporters, logger)
override def maybeFailureMessage(testSource: TestSource, reporters: Seq[TestReporter]): Option[String] =
lazy val (expected, expCount) = getWarnMapAndExpectedCount(testSource.sourceFiles.toIndexedSeq)
lazy val obtCount = reporters.foldLeft(0)(_ + _.warningCount)
lazy val (unfulfilled, unexpected) = getMissingExpectedWarnings(expected, diagnostics.iterator)
lazy val diagnostics = reporters.flatMap(_.diagnostics.toSeq.sortBy(_.pos.line))
lazy val messages = diagnostics.map(d => s" at ${d.pos.line + 1}: ${d.message}")
def showLines(title: String, lines: Seq[String]) = if lines.isEmpty then "" else lines.mkString(s"$title\n", "\n", "")
def hasMissingAnnotations = unfulfilled.nonEmpty || unexpected.nonEmpty
def showDiagnostics = showLines("-> following the diagnostics:", messages)
Option:
if reporters.exists(_.errorCount > 0) then
s"""Compilation failed for: ${testSource.title}
|$showDiagnostics
|""".stripMargin.trim.linesIterator.mkString("\n", "\n", "")
else if expCount != obtCount then
s"""|Wrong number of warnings encountered when compiling $testSource
|expected: $expCount, actual: $obtCount
|${showLines("Unfulfilled expectations:", unfulfilled)}
|${showLines("Unexpected warnings:", unexpected)}
|$showDiagnostics
|""".stripMargin.trim.linesIterator.mkString("\n", "\n", "")
else if hasMissingAnnotations then
s"""|Warnings found on incorrect row numbers when compiling $testSource
|${showLines("Unfulfilled expectations:", unfulfilled)}
|${showLines("Unexpected warnings:", unexpected)}
|$showDiagnostics
|""".stripMargin.trim.linesIterator.mkString("\n", "\n", "")
else if !expected.isEmpty then s"\nExpected warnings(s) have {<warning position>=<unreported warning>}: $expected"
else null
end maybeFailureMessage
def getWarnMapAndExpectedCount(files: Seq[JFile]): (HashMap[String, Integer], Int) =
val comment = raw"//(?: *)(nopos-)?warn".r
val map = HashMap[String, Integer]()
var count = 0
def bump(key: String): Unit =
map.get(key) match
case null => map.put(key, 1)
case n => map.put(key, n+1)
count += 1
for file <- files if isSourceFile(file) do
Using.resource(Source.fromFile(file, StandardCharsets.UTF_8.name)) { source =>
source.getLines.zipWithIndex.foreach: (line, lineNbr) =>
comment.findAllMatchIn(line).foreach:
case comment("nopos-") => bump("nopos")
case _ => bump(s"${file.getPath}:${lineNbr+1}")
}
end for
(map, count)
// return unfulfilled expected warnings and unexpected diagnostics
def getMissingExpectedWarnings(expected: HashMap[String, Integer], reporterWarnings: Iterator[Diagnostic]): (List[String], List[String]) =
val unexpected = ListBuffer.empty[String]
def relativize(path: String): String = path.split(JFile.separatorChar).dropWhile(_ != "tests").mkString(JFile.separator)
def seenAt(key: String): Boolean =
expected.get(key) match
case null => false
case 1 => expected.remove(key); true
case n => expected.put(key, n - 1); true
def sawDiagnostic(d: Diagnostic): Unit =
val srcpos = d.pos.nonInlined
if srcpos.exists then
val key = s"${relativize(srcpos.source.file.toString())}:${srcpos.line + 1}"
if !seenAt(key) then unexpected += key
else
if !seenAt("nopos") then unexpected += relativize(srcpos.source.file.toString)
reporterWarnings.foreach(sawDiagnostic)
val splitter = raw"(?:[^:]*):(\d+)".r
val unfulfilled = expected.asScala.keys.toList.sortBy { case splitter(n) => n.toInt case _ => -1 }
(unfulfilled, unexpected.toList)
end getMissingExpectedWarnings
end WarnTest
private final class RewriteTest(testSources: List[TestSource], checkFiles: Map[JFile, JFile], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting)
extends Test(testSources, times, threadLimit, suppressAllOutput) {
private def verifyOutput(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable) = {
testSource.sourceFiles.foreach { file =>
if checkFiles.contains(file) then
val checkFile = checkFiles(file)
val actual = {
val source = Source.fromFile(file, StandardCharsets.UTF_8.name)
try source.getLines().toList
finally source.close()
}
diffTest(testSource, checkFile, actual, reporters, logger)
}
// check that the rewritten code compiles
new CompilationTest(testSource).checkCompile()
}
override def onSuccess(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable) =
verifyOutput(testSource, reporters, logger)
}
protected class RunTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting)
extends Test(testSources, times, threadLimit, suppressAllOutput) {
private var didAddNoRunWarning = false
protected def addNoRunWarning() = if (!didAddNoRunWarning) {
didAddNoRunWarning = true
summaryReport.addStartingMessage {
"""|WARNING
|-------
|Run and debug tests were only compiled, not run - this is due to the `dotty.tests.norun`
|property being set
|""".stripMargin
}
}
private def verifyOutput(checkFile: Option[JFile], dir: JFile, testSource: TestSource, warnings: Int, reporters: Seq[TestReporter], logger: LoggedRunnable) = {
if Properties.testsNoRun then addNoRunWarning()
else runMain(testSource.runClassPath, testSource.allToolArgs) match {
case Success(output) => checkFile match {
case Some(file) if file.exists => diffTest(testSource, file, output.linesIterator.toList, reporters, logger)
case _ =>
}
case Failure(output) =>
if output == "" then
echo(s"Test '${testSource.title}' failed with no output")
else
echo(s"Test '${testSource.title}' failed with output:")
echo(output)
failTestSource(testSource)
case Timeout =>
echo("failed because test " + testSource.title + " timed out")
failTestSource(testSource, TimeoutFailure(testSource.title))
}
}
override def onSuccess(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable) =
verifyOutput(testSource.checkFile, testSource.outDir, testSource, countWarnings(reporters), reporters, logger)
}
private final class NegTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting)
extends Test(testSources, times, threadLimit, suppressAllOutput) {
override def suppressErrors = true
override def maybeFailureMessage(testSource: TestSource, reporters: Seq[TestReporter]): Option[String] =
lazy val (errorMap, expectedErrors) = getErrorMapAndExpectedCount(testSource.sourceFiles.toIndexedSeq)
lazy val actualErrors = reporters.foldLeft(0)(_ + _.errorCount)
lazy val (expected, unexpected) = getMissingExpectedErrors(errorMap, reporters.iterator.flatMap(_.errors))
def hasMissingAnnotations = expected.nonEmpty || unexpected.nonEmpty
def showErrors = "-> following the errors:\n" +
reporters.flatMap(_.allErrors.sortBy(_.pos.line).map(e => s"${e.pos.line + 1}: ${e.message}")).mkString(" at ", "\n at ", "")
Option {
if actualErrors == 0 then s"\nNo errors found when compiling neg test $testSource"
else if expectedErrors == 0 then s"\nNo errors expected/defined in $testSource -- use // error or // nopos-error"
else if expectedErrors != actualErrors then
s"""|Wrong number of errors encountered when compiling $testSource
|expected: $expectedErrors, actual: $actualErrors
|${expected.mkString("Unfulfilled expectations:\n", "\n", "")}
|${unexpected.mkString("Unexpected errors:\n", "\n", "")}
|$showErrors
|""".stripMargin.trim.linesIterator.mkString("\n", "\n", "")
else if hasMissingAnnotations then s"\nErrors found on incorrect row numbers when compiling $testSource\n$showErrors"
else if !errorMap.isEmpty then s"\nExpected error(s) have {<error position>=<unreported error>}: $errorMap"
else null
}
end maybeFailureMessage
override def onSuccess(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable): Unit =
diffCheckfile(testSource, reporters, logger)
// In neg-tests we allow two or three types of error annotations.
// Normally, `// error` must be annotated on the correct line number.
// `// nopos-error` allows for an error reported without a position.
// `// anypos-error` allows for an error reported with a position that can't be annotated in the check file.
//
// We collect these in a map `"file:row" -> numberOfErrors`, for
// nopos and anypos errors we save them in `"file" -> numberOfNoPosErrors`
def getErrorMapAndExpectedCount(files: Seq[JFile]): (HashMap[String, Integer], Int) =
val comment = raw"//( *)(nopos-|anypos-)?error".r
val errorMap = new HashMap[String, Integer]()
var expectedErrors = 0
def bump(key: String): Unit =
errorMap.get(key) match
case null => errorMap.put(key, 1)
case n => errorMap.put(key, n+1)
expectedErrors += 1
files.filter(isSourceFile).foreach { file =>
Using(Source.fromFile(file, StandardCharsets.UTF_8.name)) { source =>
source.getLines.zipWithIndex.foreach { case (line, lineNbr) =>
comment.findAllMatchIn(line).foreach { m =>
m.group(2) match
case prefix if m.group(1).isEmpty =>
val what = Option(prefix).getOrElse("")
echo(s"Warning: ${file.getCanonicalPath}:${lineNbr}: found `//${what}error` but expected `// ${what}error`, skipping comment")
case "nopos-" => bump("nopos")
case "anypos-" => bump("anypos")
case _ => bump(s"${file.getPath}:${lineNbr+1}")
}
}
}.get