package dotty.tools.dotc import repl.TestREPL import core.Contexts._ import interfaces.Diagnostic.ERROR import reporting._ import diagnostic.MessageContainer import util.SourcePosition import config.CompilerCommand import dotty.tools.io.PlainFile import scala.collection.mutable.ListBuffer import scala.reflect.io.{ Path, Directory, File => SFile, AbstractFile } import scala.annotation.tailrec import java.io.{ RandomAccessFile, File => JFile } /** Legacy compiler tests that run single threaded */ abstract class CompilerTest { /** Override with output dir of test so it can be patched. Partest expects * classes to be in partest-generated/[kind]/[testname]-[kind].obj/ */ val defaultOutputDir: String /** Override to filter out tests that should not be run by partest. */ def partestableFile(prefix: String, fileName: String, extension: String, args: List[String]) = true def partestableDir(prefix: String, dirName: String, args: List[String]) = true def partestableList(testName: String, files: List[String], args: List[String]) = true /** Always run with JUnit. */ def compileLine(cmdLine: String)(implicit defaultOptions: List[String]): Unit = compileArgs(cmdLine.split("\n"), Nil) /** Compiles the given code file. * * @param prefix the parent directory (including separator at the end) * @param fileName the filename, by default without extension * @param args arguments to the compiler * @param extension the file extension, .scala by default * @param defaultOptions more arguments to the compiler */ def compileFile(prefix: String, fileName: String, args: List[String] = Nil, extension: String = ".scala", runTest: Boolean = false) (implicit defaultOptions: List[String]): Unit = { val filePath = s"$prefix$fileName$extension" val expErrors = expectedErrors(filePath) if (runTest) log(s"WARNING: run tests can only be run by partest, JUnit just verifies compilation: $prefix$fileName$extension") if (args.contains("-rewrite")) { val file = new PlainFile(filePath) val data = file.toByteArray // compile with rewrite compileArgs((filePath :: args).toArray, expErrors) // compile again, check that file now compiles without -language:Scala2 val plainArgs = args.filter(arg => arg != "-rewrite" && arg != "-language:Scala2") compileFile(prefix, fileName, plainArgs, extension, runTest) // restore original test file val out = file.output out.write(data) out.close() } else compileArgs((filePath :: args).toArray, expErrors) } def runFile(prefix: String, fileName: String, args: List[String] = Nil, extension: String = ".scala") (implicit defaultOptions: List[String]): Unit = { compileFile(prefix, fileName, args, extension, true) } def findJarFromRuntime(partialName: String): String = { val urls = ClassLoader.getSystemClassLoader.asInstanceOf[java.net.URLClassLoader].getURLs.map(_.getFile.toString) urls.find(_.contains(partialName)).getOrElse { throw new java.io.FileNotFoundException( s"""Unable to locate $partialName on classpath:\n${urls.toList.mkString("\n")}""" ) } } private def compileWithJavac( fs: Array[String], args: Array[String] )(implicit defaultOptions: List[String]): Boolean = { val scalaLib = findJarFromRuntime("scala-library") val fullArgs = Array( "javac", "-classpath", s".:$scalaLib" ) ++ args ++ defaultOptions.dropWhile("-d" != _).take(2) ++ fs Runtime.getRuntime.exec(fullArgs).waitFor() == 0 } /** Compiles the code files in the given directory together. If args starts * with "-deep", all files in subdirectories (and so on) are included. */ def compileDir(prefix: String, dirName: String, args: List[String] = Nil, runTest: Boolean = false) (implicit defaultOptions: List[String]): Unit = { def computeFilePathsAndExpErrors = { val dir = Directory(prefix + dirName) val (files, normArgs) = args match { case "-deep" :: args1 => (dir.deepFiles, args1) case _ => (dir.files, args) } val (filePaths, javaFilePaths) = files .toArray.map(_.toString) .foldLeft((Array.empty[String], Array.empty[String])) { case (acc @ (fp, jfp), name) => if (name endsWith ".scala") (name +: fp, jfp) else if (name endsWith ".java") (fp, name +: jfp) else (fp, jfp) } val expErrors = expectedErrors(filePaths.toList) (filePaths, javaFilePaths, normArgs, expErrors) } if (runTest) log(s"WARNING: run tests can only be run by partest, JUnit just verifies compilation: $prefix$dirName") val (filePaths, javaFilePaths, normArgs, expErrors) = computeFilePathsAndExpErrors compileWithJavac(javaFilePaths, Array.empty) // javac needs to run first on dotty-library compileArgs(javaFilePaths ++ filePaths ++ normArgs, expErrors) } def runDir(prefix: String, dirName: String, args: List[String] = Nil) (implicit defaultOptions: List[String]): Unit = compileDir(prefix, dirName, args, true) /** Compiles each source in the directory path separately by calling * compileFile resp. compileDir. */ def compileFiles(path: String, args: List[String] = Nil, verbose: Boolean = true, runTest: Boolean = false, compileSubDirs: Boolean = true)(implicit defaultOptions: List[String]): Unit = { val dir = Directory(path) val fileNames = dir.files.toArray.map(_.jfile.getName).filter(name => (name endsWith ".scala") || (name endsWith ".java")) for (name <- fileNames) { if (verbose) log(s"testing $path$name") compileFile(path, name, args, "", runTest) } if (compileSubDirs) for (subdir <- dir.dirs) { if (verbose) log(s"testing $subdir") compileDir(path, subdir.jfile.getName, args, runTest) } } def runFiles(path: String, args: List[String] = Nil, verbose: Boolean = true) (implicit defaultOptions: List[String]): Unit = compileFiles(path, args, verbose, true) /** Compiles the given list of code files. */ def compileList(testName: String, files: List[String], args: List[String] = Nil) (implicit defaultOptions: List[String]): Unit = { val expErrors = expectedErrors(files) compileArgs((files ++ args).toArray, expErrors) } // ========== HELPERS ============= private def expectedErrors(filePaths: List[String]): List[ErrorsInFile] = if (filePaths.exists(isNegTest(_))) filePaths.map(getErrors(_)) else Nil private def expectedErrors(filePath: String): List[ErrorsInFile] = expectedErrors(List(filePath)) private def isNegTest(testPath: String) = testPath.contains("/neg/") private def compileArgs(args: Array[String], expectedErrorsPerFile: List[ErrorsInFile]) (implicit defaultOptions: List[String]): Unit = { val allArgs = args ++ defaultOptions val verbose = allArgs.contains("-verbose") //println(s"""all args: ${allArgs.mkString("\n")}""") val processor = if (allArgs.exists(_.startsWith("#"))) Bench else Main val storeReporter = new Reporter with UniqueMessagePositions with HideNonSensicalMessages { private val consoleReporter = new ConsoleReporter() private val innerStoreReporter = new StoreReporter(consoleReporter) def doReport(m: MessageContainer)(implicit ctx: Context): Unit = { if (m.level == ERROR || verbose) { innerStoreReporter.flush() consoleReporter.doReport(m) } else if (errorCount > 0) consoleReporter.doReport(m) else innerStoreReporter.doReport(m) } } val reporter = processor.process(allArgs, storeReporter) val nerrors = reporter.errorCount val xerrors = (expectedErrorsPerFile map {_.totalErrors}).sum def expectedErrorFiles = expectedErrorsPerFile.collect{ case er if er.totalErrors > 0 => er.fileName } assert(nerrors == xerrors, s"""Wrong # of errors. Expected: $xerrors, found: $nerrors |Files with expected errors: $expectedErrorFiles |errors: """.stripMargin) // NEG TEST if (xerrors > 0) { val errorLines = reporter.allErrors.map(_.pos) // reporter didn't record as many errors as its errorCount says assert(errorLines.length == nerrors, s"Not enough errors recorded.") // Some compiler errors have an associated source position. Each error // needs to correspond to a "// error" marker on that line in the source // file and vice versa. // Other compiler errors don't have an associated source position. Their // number should correspond to the total count of "// nopos-error" // markers in all files val (errorsByFile, errorsWithoutPos) = errorLines.groupBy(_.source.file).toList.partition(_._1.toString != "") // check errors with source position val foundErrorsPerFile = errorsByFile.map({ case (fileName, errorList) => val posErrorLinesToNr = errorList.groupBy(_.line).toList.map({ case (line, list) => (line, list.length) }).sortBy(_._1) ErrorsInFile(fileName.toString, 0, posErrorLinesToNr) }) val expectedErrorsPerFileZeroed = expectedErrorsPerFile.map({ case ErrorsInFile(fileName, _, posErrorLinesToNr) => ErrorsInFile(fileName.toString, 0, posErrorLinesToNr) }) checkErrorsWithPosition(expectedErrorsPerFileZeroed, foundErrorsPerFile) // check errors without source position val expectedNoPos = expectedErrorsPerFile.map(_.noposErrorNr).sum val foundNoPos = errorsWithoutPos.map(_._2.length).sum assert(foundNoPos == expectedNoPos, s"Wrong # of errors without source position. Expected (all files): $expectedNoPos, found (compiler): $foundNoPos") } } // ========== NEG TEST HELPERS ============= /** Captures the number of nopos-errors in the given file and the number of * errors with a position, represented as a tuple of source line and number * of errors on that line. */ case class ErrorsInFile(fileName: String, noposErrorNr: Int, posErrorLinesToNr: List[(Int, Int)]) { def totalErrors = noposErrorNr + posErrorLinesToNr.map(_._2).sum } /** Extracts the errors expected for the given neg test file. */ def getErrors(fileName: String): ErrorsInFile = { val content = SFile(fileName).slurp val (line, rest) = content.span(_ != '\n') @tailrec def checkLine(line: String, rest: String, index: Int, noposAcc: Int, posAcc: List[(Int, Int)]): ErrorsInFile = { val posErrors = "// ?error".r.findAllIn(line).length val newPosAcc = if (posErrors > 0) (index, posErrors) :: posAcc else posAcc val newNoPosAcc = noposAcc + "// ?nopos-error".r.findAllIn(line).length val (newLine, newRest) = rest.span(_ != '\n') if (newRest.isEmpty) ErrorsInFile(fileName.toString, newNoPosAcc, newPosAcc.reverse) else checkLine(newLine, newRest.tail, index + 1, newNoPosAcc, newPosAcc) // skip leading '\n' } checkLine(line, rest.tail, 0, 0, Nil) // skip leading '\n' } /** Asserts that the expected and found number of errors correspond, and * otherwise throws an error with the filename, plus optionally a line * number if available. */ def errorMsg(fileName: String, lineNumber: Option[Int], exp: Int, found: Int) = { val i = lineNumber.map({ i => ":" + (i + 1) }).getOrElse("") assert(found == exp, s"Wrong # of errors for $fileName$i. Expected (file): $exp, found (compiler): $found") } /** Compares the expected with the found errors and creates a nice error * message if they don't agree. */ def checkErrorsWithPosition(expected: List[ErrorsInFile], found: List[ErrorsInFile]): Unit = { // create nice error messages expected.diff(found) match { case Nil => // nothing missing case ErrorsInFile(fileName, _, expectedLines) :: xs => found.find(_.fileName == fileName) match { case None => // expected some errors, but none found for this file errorMsg(fileName, None, expectedLines.map(_._2).sum, 0) case Some(ErrorsInFile(_,_,foundLines)) => // found wrong number/location of markers for this file compareLines(fileName, expectedLines, foundLines) } } found.diff(expected) match { case Nil => // nothing missing case ErrorsInFile(fileName, _, foundLines) :: xs => expected.find(_.fileName == fileName) match { case None => // found some errors, but none expected for this file errorMsg(fileName, None, 0, foundLines.map(_._2).sum) case Some(ErrorsInFile(_,_,expectedLines)) => // found wrong number/location of markers for this file compareLines(fileName, expectedLines, foundLines) } } } /** Gives an error message for one line where the expected number of errors and * the number of compiler errors differ. */ def compareLines(fileName: String, expectedLines: List[(Int, Int)], foundLines: List[(Int, Int)]) = { expectedLines foreach{ case (line, expNr) => foundLines.find(_._1 == line) match { case Some((_, `expNr`)) => // this line is ok case Some((_, foundNr)) => errorMsg(fileName, Some(line), expNr, foundNr) case None => println(s"expected lines = $expectedLines%, %") println(s"found lines = $foundLines%, %") errorMsg(fileName, Some(line), expNr, 0) } } foundLines foreach { case (line, foundNr) => expectedLines.find(_._1 == line) match { case Some((_, `foundNr`)) => // this line is ok case Some((_, expNr)) => errorMsg(fileName, Some(line), expNr, foundNr) case None => errorMsg(fileName, Some(line), 0, foundNr) } } } // ========== PARTEST HELPERS ============= // In particular, don't copy flags from scalac tests private val extensionsToCopy = scala.collection.immutable.HashSet("scala", "java") /** Determines what kind of test to run. */ private def testKind(prefixDir: String, runTest: Boolean) = { if (runTest) "run" else if (isNegTest(prefixDir)) "neg" else if (prefixDir.endsWith("run" + JFile.separator)) { log("WARNING: test is being run as pos test despite being in a run directory. " + "Use runFile/runDir instead of compileFile/compileDir to do a run test") "pos" } else "pos" } /** The three possibilities: no generated sources exist yet, the same sources * exist already, different sources exist. */ object Difference extends Enumeration { type Difference = Value val NotExists, ExistsSame, ExistsDifferent = Value } import Difference._ /** Recursively copy over source files and directories, excluding extensions * that aren't in extensionsToCopy. */ private def recCopyFiles(sourceFile: Path, dest: Path): Unit = { @tailrec def copyfile(file: SFile, bytewise: Boolean): Unit = { if (bytewise) { val in = file.inputStream() val out = SFile(dest).outputStream() val buffer = new Array[Byte](1024) @tailrec def loop(available: Int):Unit = { if (available < 0) {()} else { out.write(buffer, 0, available) val read = in.read(buffer) loop(read) } } loop(0) in.close() out.close() } else { try { SFile(dest)(scala.io.Codec.UTF8).writeAll((s"/* !!!!! WARNING: DO NOT MODIFY. Original is at: $file !!!!! */").replace("\\", "/"), file.slurp("UTF-8")) } catch { case unmappable: java.nio.charset.MalformedInputException => copyfile(file, true) //there are bytes that can't be mapped with UTF-8. Bail and just do a straight byte-wise copy without the warning header. } } } processFileDir(sourceFile, { sf => if (extensionsToCopy.contains(sf.extension)) { dest.parent.jfile.mkdirs copyfile(sf, false) } else { log(s"WARNING: ignoring $sf") } }, { sdir => dest.jfile.mkdirs sdir.list.foreach(path => recCopyFiles(path, dest / path.name)) }, Some("DPCompilerTest.recCopyFiles: sourceFile not found: " + sourceFile)) } /** Reads the existing files for the given test source if any. */ private def getExisting(dest: Path): ExistingFiles = { val content: Option[Option[String]] = processFileDir(dest, f => try Some(f.slurp("UTF8")) catch {case io: java.io.IOException => Some(io.toString())}, d => Some("")) if (content.isDefined && content.get.isDefined) { val flags = (dest changeExtension "flags").toFile.safeSlurp val nerr = (dest changeExtension "nerr").toFile.safeSlurp ExistingFiles(content.get, flags, nerr) } else ExistingFiles() } /** Encapsulates existing generated test files. */ case class ExistingFiles(genSrc: Option[String] = None, flags: Option[String] = None, nerr: Option[String] = None) { def isDifferent(sourceFile: JFile, otherFlags: List[String], otherNerr: String): Difference = { if (!genSrc.isDefined) { NotExists } else { val source = processFileDir(sourceFile, { f => try Some(f.slurp("UTF8")) catch {case _: java.io.IOException => None} }, { d => Some("") }, Some("DPCompilerTest sourceFile doesn't exist: " + sourceFile)).get if (source == genSrc) { nerr match { case Some(n) if (n != otherNerr) => ExistsDifferent case None if (otherNerr != "0") => ExistsDifferent case _ if (flags.map(_ == otherFlags.mkString(" ")).getOrElse(otherFlags.isEmpty)) => ExistsSame case _ => ExistsDifferent } } else ExistsDifferent } } } import scala.util.matching.Regex val nrFinder = """(.*_v)(\d+)""".r /** Changes the version number suffix in the name (without extension). */ private def replaceVersion(name: String, nr: Int): Option[String] = { val nrString = nr.toString name match { case nrFinder(prefix, `nrString`) => Some(prefix + (nr + 1)) case _ if nr != 0 => None case _ => Some(name + "_v1") } } /** Returns None if the given path doesn't exist, otherwise returns Some of * applying either processFile or processDir, depending on what the path * refers to in the file system. If failMsgOnNone is defined, this function * asserts that the file exists using the provided message. */ private def processFileDir[T](input: Path, processFile: SFile => T, processDir: Directory => T, failMsgOnNone: Option[String] = None): Option[T] = { val res = input.ifFile(f => processFile(f)).orElse(input.ifDirectory(d => processDir(d))) (failMsgOnNone, res) match { case (Some(msg), None) => assert(false, msg); None case _ => res } } /** Write either to console */ private def log(msg: String) = println(msg) }