diff options
Diffstat (limited to 'src/partest-alternative')
31 files changed, 4005 insertions, 0 deletions
diff --git a/src/partest-alternative/README b/src/partest-alternative/README new file mode 100644 index 0000000000..c7673fe2f8 --- /dev/null +++ b/src/partest-alternative/README @@ -0,0 +1,50 @@ +If you're looking for something to read, I suggest running ../test/partest +with no arguments, which at this moment prints this: + +Usage: partest [<options>] [<test> <test> ...] + <test>: a path to a test designator, typically a .scala file or a directory. + Examples: files/pos/test1.scala, files/res/bug785 + + Test categories: + --all run all tests (default, unless no options given) + --pos Compile files that are expected to build + --neg Compile files that are expected to fail + --run Test JVM backend + --jvm Test JVM backend + --res Run resident compiler scenarii + --buildmanager Run Build Manager scenarii + --scalacheck Run Scalacheck tests + --script Run script files + --shootout Run shootout tests + --scalap Run scalap tests + + Test "smart" categories: + --grep run all tests with a source file containing <expr> + --failed run all tests which failed on the last run + + Specifying paths and additional flags, ~ means repository root: + --rootdir path from ~ to partest (default: test) + --builddir path from ~ to test build (default: build/pack) + --srcdir path from --rootdir to sources (default: files) + --javaopts flags to java on all runs (overrides JAVA_OPTS) + --scalacopts flags to scalac on all tests (overrides SCALAC_OPTS) + --pack alias for --builddir build/pack + --quick alias for --builddir build/quick + + Options influencing output: + --trace show the individual steps taken by each test + --show-diff show diff between log and check file + --show-log show log on failures + --dry-run do not run tests, only show their traces. + --terse be less verbose (almost silent except for failures) + --verbose be more verbose (additive with --trace) + --debug maximum debugging output + --ansi print output in color + + Other options: + --timeout Timeout in seconds + --cleanup delete all stale files and dirs before run + --nocleanup do not delete any logfiles or object dirs + --stats collect and print statistics about the tests + --validate examine test filesystem for inconsistencies + --version print version diff --git a/src/partest-alternative/scala/tools/partest/Actions.scala b/src/partest-alternative/scala/tools/partest/Actions.scala new file mode 100644 index 0000000000..cb60152b71 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Actions.scala @@ -0,0 +1,231 @@ +/* __ *\ +** ________ ___ / / ___ Scala Parallel Testing ** +** / __/ __// _ | / / / _ | (c) 2007-2010, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +package scala.tools +package partest + +import util._ +import nsc.io._ + +trait Actions { + partest: Universe => + + class TestSequence(val actions: List[TestStep]) extends AbsTestSequence { + } + + implicit def createSequence(xs: List[TestStep]) = new TestSequence(xs) + + trait ExecSupport { + self: TestEntity => + + def execEnv: Map[String, String] = { + val map = assembleEnvironment() + val cwd = execCwd.toList map ("CWD" -> _.path) + + map ++ cwd + } + def execCwd = if (commandFile.isFile) Some(sourcesDir) else None + + def runExec(args: List[String]): Boolean = { + val cmd = fromArgs(args) + + if (isVerbose) { + trace("runExec: " + execEnv.mkString("ENV(", "\n", "\n)")) + execCwd foreach (x => trace("CWD(" + x + ")")) + } + + trace("runExec: " + cmd) + isDryRun || execAndLog(cmd) + } + + /** Runs <code>command</code> redirecting standard out and + * error out to <code>output</code> file. + */ + private def runCommandOld(command: String, output: java.io.File): Int = { + import java.io._ + import nest.StreamAppender + + // NestUI.verbose("running command:\n"+command) + val proc = Runtime.getRuntime.exec(command) + val in = proc.getInputStream + val err = proc.getErrorStream + val writer = new PrintWriter(new FileWriter(output), true) + val inApp = StreamAppender(in, writer) + val errApp = StreamAppender(err, writer) + val async = new Thread(errApp) + async.start() + inApp.run() + async.join() + writer.close() + + try proc.exitValue() + catch { case _: IllegalThreadStateException => 0 } + } + + /** Exec a process to run a command. Assumes 0 exit value is success. + * Of necessity, also treats no available exit value as success. + */ + protected def execAndLog(cmd: String): Boolean = { + runCommandOld(cmd, logFile.jfile) == 0 + + // var proc: Process = null + // + // val result = interruptMeIn(cmd, testTimeout) { + // loggingResult { + // proc = Process.exec(toArgs(cmd), execEnv, execCwd.orNull, true) + // proc.slurp() + // } + // proc != null && (proc.waitFor() == 0) + // } + // result getOrElse { + // warning("Process never terminated: '%s'" format cmd) + // if (proc != null) + // proc.destroy() + // + // false + // } + } + } + + trait ScriptableTest { + self: TestEntity => + + /** Translates a line from a .cmds file into a teststep. + */ + def customTestStep(line: String): TestStep = { + trace("customTestStep: " + line) + val (cmd, rest) = line span (x => !Character.isWhitespace(x)) + def qualify(name: String) = sourcesDir / name path + val args = toArgs(rest) map qualify + def fail: TestStep = (_: TestEntity) => error("Parse error: did not understand '%s'" format line) + + val f: TestEntity => Boolean = cmd match { + case "scalac" => _ scalac args + case "javac" => _ javac args + case "scala" => _ runScala args + case _ => fail + } + f + } + } + + trait CompilableTest extends CompileExecSupport { + self: TestEntity => + + def sourceFiles = location.walk collect { case f: File if isJavaOrScala(f) => f } toList + def allSources = sourceFiles map (_.path) + def scalaSources = sourceFiles filter isScala map (_.path) + def javaSources = sourceFiles filter isJava map (_.path) + + /** If there are mixed java and scala files, the standard compilation + * sequence is: + * + * scalac with all files + * javac with only java files + * scalac with only scala files + * + * This should be expanded to encompass other strategies so we know how + * well they're working or not working - notably, it would be very useful + * to know exactly when and how two-pass compilation fails. + */ + def compile() = { + trace("compile: " + sourceFiles) + + def compileJava() = javac(javaSources) + def compileScala() = scalac(scalaSources) + def compileAll() = scalac(allSources) + def compileMixed() = compileAll() && compileJava() && compileScala() + + if (scalaSources.nonEmpty && javaSources.nonEmpty) compileMixed() + else compileScala() + } + } + + trait DiffableTest { + self: TestEntity => + + def checkFile: File = withExtension("check").toFile + def checkFileRequired = + returning(checkFile.isFile)(res => if (!res) warnAndLog("A checkFile at '%s' is mandatory.\n" format checkFile.path)) + + lazy val sourceFileNames = sourceFiles map (_.name) + + /** Given the difficulty of verifying that any selective approach works + * everywhere, the algorithm now is to look for the name of any known + * source file for this test, and if seen, remove all the non-whitespace + * preceding it. (Paths with whitespace don't work anyway.) This should + * wipe out all slashes, backslashes, C:\, cygwin/windows differences, + * and whatever else makes a simple diff not simple. + * + * The log and check file are both transformed, which I don't think is + * correct -- only the log should be -- but doing it this way until I + * can clarify martin's comments in #3283. + */ + def normalizePaths(s: String) = + sourceFileNames.foldLeft(s)((res, name) => res.replaceAll("""\S+\Q%s\E""" format name, name)) + + /** The default cleanup normalizes paths relative to sourcesDir, + * absorbs line terminator differences by going to lines and back, + * and trims leading or trailing whitespace. + */ + def diffCleanup(f: File) = safeLines(f) map normalizePaths mkString "\n" trim + + /** diffFiles requires actual Files as arguments but the output we want + * is the post-processed versions of log/check, so we resort to tempfiles. + */ + lazy val diffOutput = { + if (!checkFile.exists) "" else { + val input = diffCleanup(checkFile) + val output = diffCleanup(logFile) + def asFile(s: String) = returning(File.makeTemp("partest-diff"))(_ writeAll s) + + if (input == output) "" + else diffFiles(asFile(input), asFile(output)) + } + } + private def checkTraceName = tracePath(checkFile) + private def logTraceName = tracePath(logFile) + private def isDiffConfirmed = checkFile.exists && (diffOutput == "") + + private def sendTraceMsg() { + def result = + if (isDryRun) "" + else if (isDiffConfirmed) " [passed]" + else if (checkFile.exists) " [failed]" + else " [unchecked]" + + trace("diff %s %s%s".format(checkTraceName, logTraceName, result)) + } + + /** If optional is true, a missing check file is considered + * a successful diff. Necessary since many categories use + * checkfiles in an ad hoc manner. + */ + def runDiff() = { + sendTraceMsg() + + def updateCheck = ( + isUpdateCheck && { + val formatStr = "** diff %s %s: " + ( + if (checkFile.exists) "failed, updating '%s' and marking as passed." + else if (diffOutput == "") "not creating checkFile at '%s' as there is no output." + else "was unchecked, creating '%s' for future tests." + ) + "\n" + + normal(formatStr.format(checkTraceName, logTraceName, checkFile.path)) + if (diffOutput != "") normal(diffOutput) + + checkFile.writeAll(diffCleanup(logFile), "\n") + true + } + ) + + isDryRun || isDiffConfirmed || (updateCheck || !checkFile.exists) + } + } +} diff --git a/src/partest-alternative/scala/tools/partest/Alarms.scala b/src/partest-alternative/scala/tools/partest/Alarms.scala new file mode 100644 index 0000000000..f38d8d6268 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Alarms.scala @@ -0,0 +1,86 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools +package partest + +import java.util.{ Timer, TimerTask } + +trait Alarms { + self: Universe => + + def interruptMeIn[T](debugMsg: String, seconds: Int)(body: => T): Option[T] = { + val thisThread = currentThread + val alarm = new SimpleAlarm(seconds * 1000) set thisThread.interrupt() + debug("interruptMeIn(%d) '%s'".format(seconds, debugMsg)) + + try { Some(body) } + catch { case _: InterruptedException => debug("Received interrupted exception.") ; None } + finally { debug("Cancelling interruptMeIn '%s'" format debugMsg) ; alarm.cancel() ; Thread.interrupted() } + } + + case class AlarmerAction(secs: Int, action: () => Unit) extends Runnable { + override def run() = action() + } + + /** Set any number of alarms up with tuples of the form: + * seconds to alarm -> Function0[Unit] to execute + */ + class Alarmer(alarms: AlarmerAction*) { + import java.util.concurrent._ + + val exec = Executors.newSingleThreadScheduledExecutor() + alarms foreach (x => exec.schedule(x, x.secs, TimeUnit.SECONDS)) + exec.shutdown() + + def cancelAll() = exec.shutdownNow() + } + + class SimpleAlarm(timeout: Long) { + private val alarm = new Timer + + /** Start a timer, running the given body if it goes off. + */ + def set(body: => Unit) = returning(new TimerTask { def run() = body })(alarm.schedule(_, timeout)) + + /** Cancel the timer. + */ + def cancel() = alarm.cancel() + } + + trait TestAlarms { + test: TestEntity => + + private def warning1 = AlarmerAction(testWarning, () => warning( + """|I've been waiting %s seconds for this to complete: + | %s + |It may be stuck, or if not, it should be broken into smaller tests. + |""".stripMargin.format(testWarning, test)) + ) + private def warning2 = AlarmerAction(testWarning * 2, () => warning( + """|Now I've been waiting %s seconds for this to complete: + | %s + |If partest seems hung it would be a good place to look. + |""".stripMargin.format(testWarning * 2, test)) + ) + + def startAlarms(onTimeout: => Unit) = + if (isNoAlarms) new Alarmer() // for alarm debugging + else new Alarmer(Seq(warning1, warning2, AlarmerAction(testTimeout, () => onTimeout)): _*) + } + + // Thread.setDefaultUncaughtExceptionHandler(new UncaughtException) + // class UncaughtException extends Thread.UncaughtExceptionHandler { + // def uncaughtException(t: Thread, e: Throwable) { + // Console.println("Uncaught in %s: %s".format(t, e)) + // } + // } + // + // lazy val logger = File("/tmp/partest.log").bufferedWriter() + // def flog(msg: String) = logger synchronized { + // logger write (msg + "\n") + // logger.flush() + // } +} diff --git a/src/partest-alternative/scala/tools/partest/BuildContributors.scala b/src/partest-alternative/scala/tools/partest/BuildContributors.scala new file mode 100644 index 0000000000..64c7e07bc3 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/BuildContributors.scala @@ -0,0 +1,102 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + */ + +package scala.tools +package partest + +import nsc.io._ +import nsc.util.ClassPath + +trait BuildContributors { + universe: Universe => + + /** A trait mixed into types which contribute a portion of the values. + * The basic mechanism is the TestBuild, TestCategory, and TestEntity + * can each contribute to each value. They are assembled at the last + * moment by the ContributorAssembler (presently the TestEntity.) + */ + trait BuildContributor { + def javaFlags: List[String] + def scalacFlags: List[String] + def classpathPaths: List[Path] + def buildProperties: List[(String, Any)] + def buildEnvironment: Map[String, String] + } + + trait ContributorAssembler { + def contributors: List[BuildContributor] + def assemble[T](what: BuildContributor => List[T]): List[T] = contributors flatMap what + + /** !!! This will need work if we want to achieve real composability, + * but it can wait for the demand. + */ + def assembleScalacArgs(args: List[String]) = assemble(_.scalacFlags) ++ args + def assembleJavaArgs(args: List[String]) = assemble(_.javaFlags) ++ args + def assembleProperties() = assemble(_.buildProperties) + def assembleClasspaths(paths: List[Path]) = assemble(_.classpathPaths) ++ paths + def assembleEnvironment() = assemble(_.buildEnvironment.toList).toMap + + def createClasspathString() = ClassPath fromPaths (assembleClasspaths(Nil) : _*) + def createPropertyString() = assembleProperties() map { case (k, v) => "-D%s=%s".format(k, v.toString) } + } + + trait BuildContribution extends BuildContributor { + self: TestBuild => + + /** The base classpath and system properties. + * !!! TODO - this should adjust itself depending on the build + * being tested, because pack and quick at least need different jars. + */ + def classpathPaths = List[Path](library, compiler, partest, fjbg) ++ forkJoinPath + def buildProperties = List( + "scala.home" -> testBuildDir, + "partest.lib" -> library, // used in jvm/inner + "java.awt.headless" -> true, + "user.language" -> "en", + "user.country" -> "US", + "partest.debug" -> isDebug, + "partest.verbose" -> isVerbose + // Disabled because there are no natives tests. + // "java.library.path" -> srcLibDir + ) + def javaFlags: List[String] = toArgs(javaOpts) + def scalacFlags: List[String] = toArgs(scalacOpts) + + /** We put the build being tested's /bin directory in the front of the + * path so the scripts and such written to execute "scala" will use this + * build and not whatever happens to be on their path. + */ + private def modifiedPath = ClassPath.join(scalaBin.path, Properties.envOrElse("PATH", "")) + def buildEnvironment = Map("PATH" -> modifiedPath) + } + + trait CategoryContribution extends BuildContributor { + self: DirBasedCategory => + + /** Category-wide classpath additions placed in <category>/lib. */ + private def libContents = root / "lib" ifDirectory (_.list.toList) + + def classpathPaths = libContents getOrElse Nil + def buildProperties = Nil + def javaFlags = Nil + def scalacFlags = Nil + def buildEnvironment = Map() + } + + trait TestContribution extends BuildContributor with ContributorAssembler { + self: TestEntity => + + def jarsInTestDir = location.walk collect { case f: File if f hasExtension "jar" => f } toList + + def contributors = List(build, category, self) + def javaFlags = safeArgs(javaOptsFile) + def scalacFlags = safeArgs(scalaOptsFile) + def classpathPaths = jarsInTestDir :+ outDir + def buildProperties = List( + "partest.output" -> outDir.toAbsolute, // used in jvm/inner + "partest.cwd" -> outDir.parent.toAbsolute // used in shootout tests + ) + def buildEnvironment = Map("JAVA_OPTS" -> fromArgs(assembleJavaArgs(Nil))) + } +}
\ No newline at end of file diff --git a/src/partest-alternative/scala/tools/partest/Categories.scala b/src/partest-alternative/scala/tools/partest/Categories.scala new file mode 100644 index 0000000000..172cca74b4 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Categories.scala @@ -0,0 +1,70 @@ +/* __ *\ +** ________ ___ / / ___ Scala Parallel Testing ** +** / __/ __// _ | / / / _ | (c) 2007-2010, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +package scala.tools +package partest + +import nsc.Settings +import nsc.io._ +import nsc.util.{ ClassPath } + +trait Categories { + self: Universe => + + trait TestCategory extends AbsTestCategory { + def kind: String + def startMessage: String = "Executing test group" + def testSequence: TestSequence + + class TestSettings(entity: TestEntity, error: String => Unit) extends Settings(error) { + def this(entity: TestEntity) = this(entity, Console println _) + + deprecation.value = false + encoding.value = "ISO-8859-1" + classpath.value = entity.testClasspath + outdir.value = entity.outDir.path + } + + def createSettings(entity: TestEntity): TestSettings = new TestSettings(entity) + def createTest(location: Path): TestEntity = + if (location.isFile) TestFile(this, location.toFile) + else if (location.isDirectory) TestDirectory(this, location.toDirectory) + else error("Failed to create test at '%s'" format location) + + /** Category test identification. + */ + def denotesTestFile(p: Path) = p.isFile && (p hasExtension "scala") + def denotesTestDir(p: Path) = p.isDirectory && !ignorePath(p) + def denotesTest(p: Path) = denotesTestDir(p) || denotesTestFile(p) + + /** This should verify that all necessary files are present. + * By default it delegates to denotesTest. + */ + def denotesValidTest(p: Path) = denotesTest(p) + } + + abstract class DirBasedCategory(val kind: String) extends TestCategory with CategoryContribution { + lazy val root = Directory(src / kind).normalize + def enumerate = root.list filter denotesTest map createTest toList + + /** Standard actions. These can be overridden either on the + * Category level or by individual tests. + */ + def compile: TestStep = (_: TestEntity).compile() + def checkFileRequired: TestStep = (_: TestEntity).checkFileRequired + def diff: TestStep = (_: TestEntity).diff() + def run: TestStep = (_: TestEntity).run() + def exec: TestStep = (_: TestEntity).exec() + + /** Combinators. + */ + def not(f: TestStep): TestStep = !f(_: TestEntity) + + override def toString = kind + } +}
\ No newline at end of file diff --git a/src/partest-alternative/scala/tools/partest/Compilable.scala b/src/partest-alternative/scala/tools/partest/Compilable.scala new file mode 100644 index 0000000000..ddaa277842 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Compilable.scala @@ -0,0 +1,106 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + */ + +package scala.tools +package partest + +import scala.tools.nsc.io._ +import scala.tools.nsc.{ Global, Settings, CompilerCommand, FatalError } +import scala.tools.nsc.util.{ ClassPath } +import scala.tools.nsc.reporters.{ Reporter, ConsoleReporter } + +trait PartestCompilation { + self: Universe => + + trait CompileExecSupport extends ExecSupport { + self: TestEntity => + + def javacpArg = "-classpath " + testClasspath + def scalacpArg = "-usejavacp" + + /** Not used, requires tools.jar. + */ + // def javacInternal(args: List[String]) = { + // import com.sun.tools.javac.Main + // Main.compile(args.toArray, logWriter) + // } + + def javac(args: List[String]): Boolean = { + val allArgString = fromArgs(javacpArg :: javacOpts :: args) + + // javac -d outdir -classpath <basepath> <files> + val cmd = "%s -d %s %s".format(javacCmd, outDir, allArgString) + def traceMsg = + if (isVerbose) cmd + else "%s -d %s %s".format(tracePath(Path(javacCmd)), tracePath(outDir), fromArgs(args)) + + trace(traceMsg) + + isDryRun || execAndLog(cmd) + } + + def scalac(args: List[String]): Boolean = { + val allArgs = assembleScalacArgs(args) + val (global, files) = newGlobal(allArgs) + def nonFileArgs = if (isVerbose) global.settings.recreateArgs else assembleScalacArgs(Nil) + def traceArgs = fromArgs(nonFileArgs ++ (files map tracePath)) + def traceMsg = "scalac " + traceArgs + + trace(traceMsg) + isDryRun || global.partestCompile(files, true) + } + + /** Actually running the test, post compilation. + * Normally args will be List("Test", "jvm"), main class and arg to it. + */ + def runScala(args: List[String]): Boolean = { + val scalaRunnerClass = "scala.tools.nsc.MainGenericRunner" + + // java $JAVA_OPTS <javaopts> -classpath <cp> + val javaCmdAndOptions = javaCmd +: assembleJavaArgs(List(javacpArg)) + // MainGenericRunner -usejavacp <scalacopts> Test jvm + val scalaCmdAndOptions = List(scalaRunnerClass, scalacpArg) ++ assembleScalacArgs(args) + // Assembled + val cmd = fromArgs(javaCmdAndOptions ++ createPropertyString() ++ scalaCmdAndOptions) + + def traceMsg = if (isVerbose) cmd else fromArgs(javaCmd :: args) + trace("runScala: " + traceMsg) + + isDryRun || execAndLog(cmd) + } + + def newReporter(settings: Settings) = new ConsoleReporter(settings, Console.in, logWriter) + + class PartestGlobal(settings: Settings, val creporter: ConsoleReporter) extends Global(settings, creporter) { + def partestCompile(files: List[String], printSummary: Boolean): Boolean = { + try { new Run compile files } + catch { + case FatalError(msg) => creporter.error(null, "fatal error: " + msg) + case ae: AssertionError => creporter.error(null, ""+ae) + case te: TypeError => creporter.error(null, ""+te) + case ex => + creporter.error(null, ""+ex) + throw ex + } + + if (printSummary) + creporter.printSummary + + creporter.flush() + !creporter.hasErrors + } + } + + def newGlobal(args: List[String]): (PartestGlobal, List[String]) = { + val settings = category createSettings self + val command = new CompilerCommand(args, settings) + val reporter = newReporter(settings) + + if (!command.ok) + debug("Error parsing arguments: '%s'".format(args mkString ", ")) + + (new PartestGlobal(command.settings, reporter), command.files) + } + } +} diff --git a/src/partest-alternative/scala/tools/partest/Config.scala b/src/partest-alternative/scala/tools/partest/Config.scala new file mode 100644 index 0000000000..288a3034e9 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Config.scala @@ -0,0 +1,129 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + */ + +package scala.tools +package partest + +import io._ +import nsc.io._ +import Properties._ + +trait Config { + universe: Universe => + + lazy val src = absolutize(srcDir).toDirectory + lazy val build = new TestBuild() + + def javaHomeEnv = envOrElse("JAVA_HOME", null) + def javaCmd = envOrElse("JAVACMD", "java") + def javacCmd = Option(javaHomeEnv) map (x => Path(x) / "bin" / "javac" path) getOrElse "javac" + + /** Values related to actors. The timeouts are in seconds. On a dry + * run we only allocate one worker so the output isn't interspersed. + */ + def workerTimeout = 3600 // 1 hour, probably overly generous + def numWorkers = if (isDryRun) 1 else propOrElse("partest.actors", "8").toInt + def expectedErrors = propOrElse("partest.errors", "0").toInt + def poolSize = (wrapAccessControl(propOrNone("actors.corePoolSize")) getOrElse "16").toInt + + def allScalaFiles = src.deepFiles filter (_ hasExtension "scala") + def allObjDirs = src.deepDirs filter (_ hasExtension "obj") + def allLogFiles = src.deepFiles filter (_ hasExtension "log") + def allClassFiles = src.deepFiles filter (_ hasExtension "class") + + class TestBuild() extends BuildContribution { + import nsc.util.ClassPath + + /** Scala core libs. + */ + val library = pathForComponent("library") + val compiler = pathForComponent("compiler") + val partest = pathForComponent("partest") + val scalap = pathForComponent("scalap", "%s.jar") + + /** Scala supplementary libs - these are not all needed for all build targets, + * and some of them are copied inside other jars in later targets. However quick + * for instance cannot be run without some of these. + */ + val fjbg = pathForLibrary("fjbg") + val msil = pathForLibrary("msil") + val forkjoin = pathForLibrary("forkjoin") + val scalacheck = pathForLibrary("scalacheck") + + /** Other interesting paths. + */ + val scalaBin = testBuildDir / "bin" + + /** A hack for now to get quick running. + */ + def needsForkJoin = { + val loader = nsc.util.ScalaClassLoader.fromURLs(List(library.toURL)) + val fjMarker = "scala.concurrent.forkjoin.ForkJoinTask" + val clazz = loader.tryToLoadClass(fjMarker) + + if (clazz.isDefined) debug("Loaded ForkJoinTask OK, don't need jar.") + else debug("Could not load ForkJoinTask, putting jar on classpath.") + + clazz.isEmpty + } + lazy val forkJoinPath: List[Path] = if (needsForkJoin) List(forkjoin) else Nil + + /** Internal **/ + private def repo = partestDir.parent.normalize + // XXX - is this needed? Where? + // + // private val pluginOptionString = "-Xplugin:" + // private def updatedPluginPath(options: String): String = { + // val (pluginArgs, rest) = toArgs(options) partition (_ startsWith pluginOptionString) + // // join all plugin paths as one classpath + // val pluginPaths = ClassPath.join(pluginArgs map (_ stripPrefix pluginOptionString): _*) + // // map all paths to absolute + // val newPath = ClassPath.map(pluginPaths, x => absolutize(x).path) + // // recreate option + // val pluginOption = if (newPath == "") None else Some(pluginOptionString + newPath) + // + // fromArgs(rest ::: pluginOption.toList) + // } + + private def pathForComponent(what: String, jarFormat: String = "scala-%s.jar"): Path = { + def asDir = testBuildDir / "classes" / what + def asJar = testBuildDir / "lib" / jarFormat.format(what) + + if (asDir.isDirectory) asDir + else if (asJar.isFile) asJar + else "" + } + private def pathForLibrary(what: String) = File(repo / "lib" / (what + ".jar")) + } + + def printConfigBanner() = { + debug("Java VM started with arguments: '%s'" format fromArgs(Process.javaVmArguments)) + debug("System Properties:\n" + util.allPropertiesString()) + + normal(configBanner()) + } + + /** Treat an access control failure as None. */ + private def wrapAccessControl[T](body: => Option[T]): Option[T] = + try body catch { case _: java.security.AccessControlException => None } + + private def configBanner() = { + val javaBin = Path(javaHome) / "bin" + val javaInfoString = "%s (build %s, %s)".format(javaVmName, javaVmVersion, javaVmInfo) + + List( + "Scala compiler classes in: " + testBuildDir, + "Scala version is: " + nsc.Properties.versionMsg, + "Scalac options are: " + universe.scalacOpts, + "Java binaries in: " + javaBin, + "Java runtime is: " + javaInfoString, + "Java runtime options: " + (Process.javaVmArguments mkString " "), + "Javac options are: " + universe.javacOpts, + "Java options are: " + universe.javaOpts, + "Source directory is: " + src, + "Selected categories: " + (selectedCategories mkString " "), + "" + ) mkString "\n" + } +} diff --git a/src/partest-alternative/scala/tools/partest/Dispatcher.scala b/src/partest-alternative/scala/tools/partest/Dispatcher.scala new file mode 100644 index 0000000000..2a9d99ab60 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Dispatcher.scala @@ -0,0 +1,162 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + * @author Philipp Haller + */ + +package scala.tools +package partest + +import scala.tools.nsc.io._ +import scala.actors.{ Actor, TIMEOUT } +import scala.actors.Actor._ +import scala.collection.immutable +import scala.util.control.Exception.ultimately + +/** The machinery for concurrent execution of tests. Each Worker + * is given a bundle of tests, which it runs sequentially and then + * sends a report back to the dispatcher. + */ +trait Dispatcher { + partest: Universe => + + /** The public entry point. The given filter narrows down the list of + * tests to run. + */ + def runSelection(categories: List[TestCategory], filt: TestEntity => Boolean = _ => true): CombinedTestResults = { + // Setting scala.home informs tests where to obtain their jars. + setProp("scala.home", testBuildDir.path) + + val allTests = allCategories flatMap (_.enumerate) + val selected = allTests filter filt + val groups = selected groupBy (_.category) + val count = selected.size + + if (count == 0) return CombinedTestResults(0, 0, 0, Nil) + else if (count == allTests.size) verbose("Running all %d tests." format count) + else verbose("Running %d/%d tests: %s".format(count, allTests.size, toStringTrunc(selected map (_.label) mkString ", "))) + + allCategories collect { case x if groups contains x => runCategory(x, groups(x)) } reduceLeft (_ ++ _) + } + + private def parallelizeTests(tests: List[TestEntity]): immutable.Map[TestEntity, TestResult] = { + // propagate verbosity + if (isDebug) scala.actors.Debug.level = 3 + + // "If elected, I guarantee a slice of tests for every worker!" + val groups = tests grouped ((tests.size / numWorkers) + 1) toList + + // "Workers, line up for assignments!" + val workers = + for ((slice, workerNum) <- groups.zipWithIndex) yield { + returning(new Worker(workerNum)) { worker => + worker.start() + worker ! TestsToRun(slice) + } + } + + normal("Started %d workers with ~%d tests each.\n".format(groups.size, groups.head.size)) + + /** Listening for news from the proletariat. + */ + (workers map { w => + receiveWithin(workerTimeout * 1000) { + case ResultsOfRun(resultMap) => resultMap + case TIMEOUT => + warning("Worker %d timed out." format w.workerNum) + // mark all the worker's tests as having timed out - should be hard to miss + // immutable.Map[TestEntity, TestResult]() + groups(w.workerNum) map (x => (x -> new Timeout(x))) toMap + } + }) reduceLeft (_ ++ _) + } + + private def runCategory(category: TestCategory, tests: List[TestEntity]): CombinedTestResults = { + val kind = category.kind + normal("%s (%s tests in %s)\n".format(category.startMessage, tests.size, category)) + + val (milliSeconds, resultMap) = timed2(parallelizeTests(tests)) + val (passed, failed) = resultsToStatistics(resultMap mapValues (_.state)) + val failures = resultMap.values filterNot (_.passed) toList + + CombinedTestResults(passed, failed, milliSeconds, failures) + } + + /** A Worker is given a bundle of tests and runs them all sequentially. + */ + class Worker(val workerNum: Int) extends Actor { + def act() { + react { case TestsToRun(tests) => + val master = sender + runTests(tests)(results => master ! ResultsOfRun(results)) + } + } + + /** Runs the tests. Passes the result Map to onCompletion when done. + */ + private def runTests(tests: List[TestEntity])(onCompletion: immutable.Map[TestEntity, TestResult] => Unit) { + var results = new immutable.HashMap[TestEntity, TestResult] // maps tests to results + val numberOfTests = tests.size + val testIterator = tests.iterator + def processed = results.size + def isComplete = testIterator.isEmpty + + def atThreshold(num: Double) = { + require(num >= 0 && num <= 1.0) + ((processed - 1).toDouble / numberOfTests <= num) && (processed.toDouble / numberOfTests >= num) + } + + def extraMessage = { + // for now quiet for normal people + if (isVerbose || isTrace || isDebug) { + if (isComplete) "(#%d 100%%)" format workerNum + else if (isVerbose) "(#%d %d/%d)".format(workerNum, processed, numberOfTests) + else if (isTrace && atThreshold(0.5)) "(#%d 50%%)" format workerNum + else "" + } + else "" + } + + def countAndReport(result: TestResult) { + val TestResult(test, state) = result + // refuse to count an entity twice + if (results contains test) + return warning("Received duplicate result for %s: was %s, now %s".format(test, results(test), state)) + + // increment the counter for this result state + results += (test -> result) + + // show on screen + if (isDryRun) normal("\n") // blank line between dry run traces + else result show extraMessage + + // remove log if successful + if (result.passed) + test.deleteLog() + + // Respond to master if this Worker is complete + if (isComplete) + onCompletion(results) + } + + Actor.loopWhile(testIterator.hasNext) { + val parent = self + // pick a test and set some alarms + val test = testIterator.next + val alarmer = test startAlarms (parent ! new Timeout(test)) + + actor { + ultimately(alarmer.cancelAll()) { + // Calling isSuccess forces the lazy val "process" inside the test, running it. + val res = test.isSuccess + // Cancel the alarms and alert the media. + parent ! TestResult(test, res) + } + } + + react { + case x: TestResult => countAndReport(x) + } + } + } + } +}
\ No newline at end of file diff --git a/src/partest-alternative/scala/tools/partest/Entities.scala b/src/partest-alternative/scala/tools/partest/Entities.scala new file mode 100644 index 0000000000..bea505b594 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Entities.scala @@ -0,0 +1,74 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + * @author Philipp Haller + */ + +package scala.tools +package partest + +import nsc.io._ + +trait Entities { + self: Universe => + + abstract class TestEntity extends AbsTestEntity + with TestContribution + with TestHousekeeping + with TestAlarms + with EntityLogging + with CompilableTest + with ScriptableTest + with DiffableTest { + def location: Path + def category: TestCategory + + lazy val label = location.stripExtension + lazy val testClasspath = returning(createClasspathString())(x => vtrace("testClasspath: " + x)) + + /** Was this test successful? Calling this for the first time forces + * lazy val "process" which actually runs the test. + */ + def isSuccess = process + + /** Some standard files, which may or may not be present. + */ + def scalaOptsFile = withExtension("flags").toFile // opts to scalac + def javaOptsFile = withExtension("javaopts").toFile // opts to java (but not javac) + def commandFile = withExtension("cmds").toFile // sequence of commands to execute + def logFile = withExtension("log").toFile // collected output + + /** Some standard directories. + */ + def outDir = withExtension("obj").toDirectory // output dir, e.g. files/pos/t14.obj + def categoryDir = location.parent.normalize // category dir, e.g. files/pos/ + def sourcesDir = location ifDirectory (_.normalize) getOrElse categoryDir + + /** Standard arguments for run, exec, diff. + */ + def argumentsToRun = List("Test", "jvm") + def argumentsToExec = List(location.path) + + /** Using a .cmds file for a custom test sequence. + */ + def commandList = safeLines(commandFile) + def testSequence = + if (commandFile.isFile && commandList.nonEmpty) commandList map customTestStep + else category.testSequence + + def run() = runScala(argumentsToRun) + def exec() = runExec(argumentsToExec) + def diff() = runDiff() // checkFile, logFile + + /** The memoized result of the test run. + */ + private lazy val process = { + val outcome = runWrappers(testSequence.actions forall (f => f(this))) + + // an empty outcome means we've been interrupted and are shutting down. + outcome getOrElse false + } + } + + case class TestDirectory(category: TestCategory, location: Directory) extends TestEntity { } + case class TestFile(category: TestCategory, location: File) extends TestEntity { } +} diff --git a/src/partest-alternative/scala/tools/partest/Housekeeping.scala b/src/partest-alternative/scala/tools/partest/Housekeeping.scala new file mode 100644 index 0000000000..a624ca8adb --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Housekeeping.scala @@ -0,0 +1,187 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + */ + +package scala.tools +package partest + +import scala.util.control.Exception.catching +import util._ +import nsc.io._ +import Process.runtime +import Properties._ + +/** An agglomeration of code which is low on thrills. Hopefully + * it operates so quietly in the background that you never have to + * look at this file. + */ +trait Housekeeping { + self: Universe => + + /** Orderly shutdown on ctrl-C. */ + @volatile private var _shuttingDown = false + protected def setShuttingDown() = { + /** Whatever we want to do as shutdown begins goes here. */ + if (!_shuttingDown) { + warning("Received shutdown signal, partest is cleaning up...\n") + _shuttingDown = true + } + } + def isShuttingDown = _shuttingDown + + /** Execute some code with a shutdown hook in place. This is + * motivated by the desire not to leave the filesystem full of + * junk when someone ctrl-Cs a test run. + */ + def withShutdownHook[T](hook: => Unit)(body: => T): Option[T] = + /** Java doesn't like it if you keep adding and removing shutdown + * hooks after shutdown has begun, so we trap the failure. + */ + catching(classOf[IllegalStateException]) opt { + val t = new Thread() { + override def run() = { + setShuttingDown() + hook + } + } + runtime addShutdownHook t + + try body + finally runtime removeShutdownHook t + } + + /** Search for a directory, possibly given only a name, by starting + * at the current dir and walking upward looking for it at each level. + */ + protected def searchForDir(name: String): Directory = { + val result = Path(name) ifDirectory (x => x.normalize) orElse { + val cwd = Directory.Current getOrElse error("user.dir property not set") + val dirs = cwd :: cwd.parents map (_ / name) + + Path onlyDirs dirs map (_.normalize) headOption + } + + result getOrElse error("Fatal: could not find directory '%s'" format name) + } + + /** Paths we ignore for most purposes. + */ + def ignorePath(x: Path) = { + (x.name startsWith ".") || + (x.isDirectory && ((x.name == "lib") || x.hasExtension("obj", "svn"))) + } + /** Make a possibly relative path absolute using partestDir as the base. + */ + def absolutize(path: String) = Path(path) toAbsoluteWithRoot partestDir + + /** Go on a deleting binge. + */ + def cleanupAll() { + if (isNoCleanup) + return + + val (dirCount, fileCount) = (cleanupObjDirs(), cleanupLogs() + cleanupJunk()) + if (dirCount + fileCount > 0) + normal("Cleaned up %d directories and %d files.\n".format(dirCount, fileCount)) + } + + def cleanupObjDirs() = countTrue(allObjDirs collect { case x if x.exists => x.deleteRecursively() }) + def cleanupJunk() = countTrue(allClassFiles collect { case x if x.exists => x.delete() }) + def cleanupLogs() = countTrue(allLogFiles collect { case x if x.exists => x.delete() }) + + /** Look through every file in the partest directory and ask around + * to make sure someone knows him. Complain about strangers. + */ + def validateAll() { + def denotesTest(p: Path) = allCategories exists (_ denotesTest p) + def isMSILcheck(p: Path) = p.name endsWith "-msil.check" + + def analyzeCategory(cat: DirBasedCategory) = { + val allTests = cat.enumerate + val otherPaths = cat.root walkFilter (x => !ignorePath(x)) filterNot (cat denotesTest _) filterNot isMSILcheck toList + val count = otherPaths.size + + println("Validating %d non-test paths in %s.".format(count, cat.kind)) + + for (path <- otherPaths) { + (allTests find (_ acknowledges path)) match { + case Some(test) => if (isVerbose) println(" OK: '%s' is claimed by '%s'".format(path, test.label)) + case _ => println(">> Unknown path '%s'" format path) + } + } + } + + allCategories collect { case x: DirBasedCategory => analyzeCategory(x) } + } + + trait TestHousekeeping { + self: TestEntity => + + /** Calculating derived files. Given a test like + * files/run/foo.scala or files/run/foo/ + * This creates paths like foo.check, foo.flags, etc. + */ + def withExtension(extension: String) = categoryDir / "%s.%s".format(label, extension) + + /** True for a path if this test acknowledges it belongs to this test. + * Overridden by some categories. + */ + def acknowledges(path: Path): Boolean = { + val loc = location.normalize + val knownPaths = List(scalaOptsFile, javaOptsFile, commandFile, logFile, checkFile) ++ jarsInTestDir + def isContainedSource = location.isDirectory && isJavaOrScala(path) && (path.normalize startsWith loc) + + (knownPaths exists (_ isSame path)) || isContainedSource + } + + /** This test "responds to" this String. This could mean anything -- it's a + * way of specifying ad-hoc collections of tests to exercise only a subset of tests. + * At present it looks for the given String in all the test sources. + */ + def respondsToString(str: String) = containsString(str) + def containsString(str: String) = { + debug("Checking %s for \"%s\"".format(sourceFiles mkString ", ", str)) + sourceFiles map safeSlurp exists (_ contains str) + } + + def possiblyTimed[T](body: => T): T = { + if (isStats) timed(recordTestTiming(label, _))(body) + else body + } + + private def prepareForTestRun() = { + // make sure we have a clean slate + deleteLog(force = true) + if (outDir.exists) + outDir.deleteRecursively() + + // recreate object dir + outDir createDirectory true + } + def deleteOutDir() = outDir.deleteRecursively() + def deleteShutdownHook() = { debug("Shutdown hook deleting " + outDir) ; deleteOutDir() } + + protected def runWrappers[T](body: => T): Option[T] = { + prepareForTestRun() + + withShutdownHook(deleteShutdownHook()) { + loggingOutAndErr { + val result = possiblyTimed { body } + if (!isNoCleanup) + deleteOutDir() + + result + } + } + } + + override def toString = location.path + override def equals(other: Any) = other match { + case x: TestEntity => location.normalize == x.location.normalize + case _ => false + } + override def hashCode = location.normalize.hashCode + } + + private def countTrue(f: => Iterator[Boolean]) = f filter (_ == true) length +}
\ No newline at end of file diff --git a/src/partest-alternative/scala/tools/partest/Partest.scala b/src/partest-alternative/scala/tools/partest/Partest.scala new file mode 100644 index 0000000000..b3fe9a98ef --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Partest.scala @@ -0,0 +1,81 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + */ + +package scala.tools +package partest + +import nsc.io._ +import nsc.util._ +import category.AllCategories + +/** Global object for a Partest run. It is completely configured by the list + * of arguments passed to the constructor (although there are a few properties + * and environment variables which can influence matters.) See PartestSpec.scala + * for the complete list. + */ +class Partest(args: List[String]) extends { + val parsed = PartestSpec(args: _*) +} with Universe with PartestSpec with cmd.Instance with AllCategories { + + if (parsed.propertyArgs.nonEmpty) + debug("Partest property args: " + fromArgs(parsed.propertyArgs)) + + debug("Partest created with args: " + fromArgs(args)) + + def helpMsg = PartestSpec.helpMsg + + // The abstract values from Universe. + lazy val testBuildDir = searchForDir(buildDir) + lazy val partestDir = searchForDir(rootDir) + lazy val allCategories = List(Pos, Neg, Run, Jvm, Res, Shootout, Scalap, Scalacheck, BuildManager, Script) + lazy val selectedCategories = if (isAllImplied) allCategories else specifiedCats + + def specifiedTests = parsed.residualArgs map (x => Path(x).normalize) + def specifiedKinds = testKinds filter (x => isSet(x) || (runSets contains x)) + def specifiedCats = specifiedKinds flatMap (x => allCategories find (_.kind == x)) + def isAllImplied = isAll || (specifiedTests.isEmpty && specifiedKinds.isEmpty) + + /** Assembles a filter based on command line options which restrict the test set + * --grep limits to only matching tests + * --failed limits to only recently failed tests (log file is present) + * --<category> limits to only the given tests and categories (but --all overrides) + * path/to/Test limits to only the given tests and categories + */ + lazy val filter = { + def indivFilter(test: TestEntity) = specifiedTests contains test.location.normalize + def categoryFilter(test: TestEntity) = specifiedCats contains test.category + def indivOrCat(test: TestEntity) = isAllImplied || indivFilter(test) || categoryFilter(test) // combines previous two + + def failedFilter(test: TestEntity) = !isFailed || (test.logFile exists) + def grepFilter(test: TestEntity) = grepExpr.isEmpty || (test containsString grepExpr.get) + def combinedFilter(x: TestEntity) = indivOrCat(x) && failedFilter(x) && grepFilter(x) // combines previous three + + combinedFilter _ + } + + def launchTestSuite() = { + def onTimeout() = { + warning("Partest test run timed out after " + timeout + " seconds.\n") + System.exit(-1) + } + val alarm = new Alarmer(AlarmerAction(timeout, () => onTimeout())) + + try runSelection(selectedCategories, filter) + finally alarm.cancelAll() + } +} + +object Partest { + def fromBuild(dir: String, args: String*): Partest = apply("--builddir" +: dir +: args: _*) + def apply(args: String*): Partest = new Partest(args.toList) + + // builds without partest jars won't actually work + def starr() = fromBuild("") + def locker() = fromBuild("build/locker") + def quick() = fromBuild("build/quick") + def pack() = fromBuild("build/pack") + def strap() = fromBuild("build/strap") + def dist() = fromBuild("dists/latest") +} + diff --git a/src/partest-alternative/scala/tools/partest/PartestSpec.scala b/src/partest-alternative/scala/tools/partest/PartestSpec.scala new file mode 100644 index 0000000000..c25119b3af --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/PartestSpec.scala @@ -0,0 +1,104 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools +package partest + +import nsc.io._ +import cmd._ + +/** This takes advantage of bits of scala goodness to fully define a command + * line program with a minimum of duplicated code. When the specification object + * is created, the vals are evaluated in order and each of them side effects + * a private accumulator. What emerges is a full list of the valid unary + * and binary arguments, as well as autogenerated help. + */ +trait PartestSpec extends Spec with Meta.StdOpts with Interpolation { + def referenceSpec = PartestSpec + def programInfo = Spec.Info("partest", "", "scala.tools.partest.Runner") + private val kind = new Spec.Accumulator[String]() + protected def testKinds = kind.get + + private implicit val tokenizeString = FromString.ArgumentsFromString // String => List[String] + + help(""" + |# Pro Tip! Instant bash completion: `partest --bash` (note backticks) + |Usage: partest [<options>] [<test> <test> ...] + | <test>: a path to a test designator, typically a .scala file or a directory. + | Examples: files/pos/test1.scala, files/res/bug785 + | + | Test categories:""".stripMargin) + + val isAll = ("all" / "run all tests (default, unless no options given)" --?) + (kind("pos") / "Compile files that are expected to build" --?) + (kind("neg") / "Compile files that are expected to fail" --?) + (kind("run") / "Test JVM backend" --?) + (kind("jvm") / "Test JVM backend" --?) + (kind("res") / "Run resident compiler scenarii" --?) + (kind("buildmanager") / "Run Build Manager scenarii" --?) + (kind("scalacheck") / "Run Scalacheck tests" --?) + (kind("script") / "Run script files" --?) + (kind("shootout") / "Run shootout tests" --?) + (kind("scalap") / "Run scalap tests" --?) + + heading ("""Test "smart" categories:""") + val grepExpr = "grep" / "run all tests with a source file containing <expr>" --| + val isFailed = "failed" / "run all tests which failed on the last run" --? + + heading ("Specifying paths and additional flags, ~ means repository root:") + + val rootDir = "rootdir" / "path from ~ to partest" defaultTo "test" + val buildDir = "builddir" / "path from ~ to test build" defaultTo "build/pack" + val srcDir = "srcdir" / "path from --rootdir to sources" defaultTo "files" + val javaOpts = "javaopts" / "flags to java on all runs" defaultToEnv "JAVA_OPTS" + val javacOpts = "javacopts" / "flags to javac on all runs" defaultToEnv "JAVAC_OPTS" + val scalacOpts = "scalacopts" / "flags to scalac on all tests" defaultToEnv "SCALAC_OPTS" + + "pack" / "" expandTo ("--builddir", "build/pack") + "quick" / "" expandTo ("--builddir", "build/quick") + + heading ("Options influencing output:") + val isTrace = "trace" / "show the individual steps taken by each test" --? + val isShowDiff = "show-diff" / "show diff between log and check file" --? + val isShowLog = "show-log" / "show log on failures" --? + val isDryRun = "dry-run" / "do not run tests, only show their traces." --? + val isTerse = "terse" / "be less verbose (almost silent except for failures)" --? + val isVerbose = "verbose" / "be more verbose (additive with --trace)" --? + val isDebug = "debug" / "maximum debugging output" --? + val isAnsi = "ansi" / "print output in color" --? + + heading ("Other options:") + val timeout = "timeout" / "Overall timeout in seconds" defaultTo 7200 + val testWarning = "test-warning" / "Test warning in seconds" defaultTo 90 + val testTimeout = "test-timeout" / "Test timeout in seconds" defaultTo 900 + val isCleanup = "cleanup" / "delete all stale files and dirs before run" --? + val isNoCleanup = "nocleanup" / "do not delete any logfiles or object dirs" --? + val isStats = "stats" / "collect and print statistics about the tests" --? + val isValidate = "validate" / "examine test filesystem for inconsistencies" --? + val isUpdateCheck = "update-check" / "overwrite checkFile if diff fails" --? + + "version" / "print version" --> runAndExit(println(Properties.versionMsg)) + + // no help for anything below this line - secret options + // mostly intended for property configuration. + val runSets = ("runsets" --^) getOrElse Nil + val isNoAlarms = "noalarms" --? + val isInsideAnt = "is-in-ant" --? +} + +object PartestSpec extends PartestSpec with Property { + lazy val propMapper = new PropertyMapper(PartestSpec) { + override def isPassThrough(key: String) = key == "partest.options" + } + + type ThisCommandLine = PartestCommandLine + class PartestCommandLine(args: List[String]) extends SpecCommandLine(args) { + override def errorFn(msg: String) = printAndExit("Error: " + msg) + + def propertyArgs = PartestSpec.propertyArgs + } + + override def creator(args: List[String]): PartestCommandLine = new PartestCommandLine(args) +} diff --git a/src/partest-alternative/scala/tools/partest/Properties.scala b/src/partest-alternative/scala/tools/partest/Properties.scala new file mode 100644 index 0000000000..4eeb0359ec --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Properties.scala @@ -0,0 +1,18 @@ +/* __ *\ +** ________ ___ / / ___ Scala Parallel Testing ** +** / __/ __// _ | / / / _ | (c) 2007-2010, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +// $Id$ + +package scala.tools +package partest + +/** Loads partest.properties from the jar. */ +object Properties extends scala.util.PropertiesTrait { + protected def propCategory = "partest" + protected def pickJarBasedOn = classOf[Application] +} diff --git a/src/partest-alternative/scala/tools/partest/Results.scala b/src/partest-alternative/scala/tools/partest/Results.scala new file mode 100644 index 0000000000..5d0e300136 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Results.scala @@ -0,0 +1,121 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + */ + +package scala.tools +package partest + +import scala.collection.immutable + +trait Results { + self: Universe => + + /** A collection of tests for a Worker. + */ + case class TestsToRun(entities: List[TestEntity]) + + /** The response from a Worker who has been given TestsToRun. + */ + case class ResultsOfRun(results: immutable.Map[TestEntity, TestResult]) + + /** The result of a single test. (0: OK, 1: FAILED, 2: TIMEOUT) + */ + sealed abstract class TestResult(val state: Int, val description: String) { + def entity: TestEntity + + def passed = state == 0 + def colorize(s: String): String + def show(msg: String) = + if (!isShuttingDown) + showResult(colorize(description), msg) + + private def outputPrefix = if (isInsideAnt) "" else markNormal("partest: ") + private def name = src relativize entity.location // e.g. "neg/test.scala" + private def showResult(status: String, extraMsg: String) = + normal(outputPrefix + "[...]/%-40s [%s] %s\n".format(name, status, extraMsg)) + + override def equals(other: Any) = other match { + case x: TestResult => entity == x.entity + case _ => false + } + override def hashCode = entity.hashCode + override def toString = "%s [%s]".format(entity, description) + } + + class Success(val entity: TestEntity) extends TestResult(0, " OK ") { + def colorize(s: String) = markSuccess(s) + override def show(msg: String) = if (!isTerse) super.show(msg) + } + class Failure(val entity: TestEntity) extends TestResult(1, " FAILED ") { + def colorize(s: String) = markFailure(s) + + override def show(msg: String) = { + super.show(msg) + + if (isShowDiff || isTrace) + normal(entity.diffOutput) + + if (isShowLog || isTrace) + normal(toStringTrunc(entity.failureMessage(), 1600)) + } + override def toString = List(super.toString, toStringTrunc(entity.failureMessage(), 400)) mkString "\n" + } + class Timeout(val entity: TestEntity) extends TestResult(2, "TIME OUT") { + def colorize(s: String) = markFailure(s) + } + + object TestResult { + def apply(entity: TestEntity, success: Boolean) = + if (success) new Success(entity) + else new Failure(entity) + + def apply(entity: TestEntity, state: Int) = state match { + case 0 => new Success(entity) + case 1 => new Failure(entity) + case 2 => new Timeout(entity) + } + def unapply(x: Any) = x match { + case x: TestResult => Some((x.entity, x.state)) + case _ => None + } + } + + /** The combined results of any number of tests. + */ + case class CombinedTestResults( + passed: Int, + failed: Int, + elapsedMilliseconds: Long, + failures: List[TestResult] + ) { + // housekeeping + val elapsedSecs = elapsedMilliseconds / 1000 + val elapsedMins = elapsedSecs / 60 + val elapsedHrs = elapsedMins / 60 + val dispMins = elapsedMins - elapsedHrs * 60 + val dispSecs = elapsedSecs - elapsedMins * 60 + + def total = passed + failed + def hasFailures = failed > 0 + def exitCode = if (expectedErrors == failed) 0 else 1 + + def ++(x: CombinedTestResults) = CombinedTestResults( + passed + x.passed, + failed + x.failed, + elapsedMilliseconds + x.elapsedMilliseconds, + failures ::: x.failures + ) + + def elapsedString = "%02d:%02d:%02d".format(elapsedHrs, dispMins, dispSecs) + def failuresString = { + if (failures.isEmpty) "" + else "Summary of failures:" :: failures mkString ("\n", "\n", "") + } + + override def toString = + if (total == 0) "There were no tests to run." + else if (isDryRun) "%d tests would be run." format total + else if (hasFailures) "%d of %d tests failed (elapsed time: %s)".format(failed, total, elapsedString) + failuresString + else "All %d tests were successful (elapsed time: %s)".format(total, elapsedString) + } +}
\ No newline at end of file diff --git a/src/partest-alternative/scala/tools/partest/Runner.scala b/src/partest-alternative/scala/tools/partest/Runner.scala new file mode 100644 index 0000000000..1a28e60896 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Runner.scala @@ -0,0 +1,36 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + * @author Philipp Haller + */ + +package scala.tools +package partest + +import nsc.io._ + +object Runner { + def main(args: Array[String]) { + val runner = Partest(args: _*) + import runner._ + + if (args.isEmpty) return println(helpMsg) + if (isValidate) return validateAll() + + printConfigBanner() + + if (isCleanup) + cleanupAll() + + val result = launchTestSuite() + val exitCode = result.exitCode + val message = "\n" + result + "\n" + + if (exitCode == 0) success(message) + else failure(message) + + if (isStats) + showTestStatistics() + + System exit exitCode + } +} diff --git a/src/partest-alternative/scala/tools/partest/Statistics.scala b/src/partest-alternative/scala/tools/partest/Statistics.scala new file mode 100644 index 0000000000..2ea3c6e8f0 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Statistics.scala @@ -0,0 +1,46 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + * @author Philipp Haller + */ + +package scala.tools +package partest + +import scala.collection.mutable.HashMap + +trait Statistics { + /** Only collected when --stats is given. */ + lazy val testStatistics = new HashMap[String, Long] + + /** Given function and block of code, evaluates code block, + * calls function with milliseconds elapsed, and returns block result. + */ + def timed[T](f: Long => Unit)(body: => T): T = { + val start = System.currentTimeMillis + val result = body + val end = System.currentTimeMillis + + f(end - start) + result + } + /** Times body and returns both values. + */ + def timed2[T](body: => T): (Long, T) = { + var milliSeconds = 0L + val result = timed(x => milliSeconds = x)(body) + + (milliSeconds, result) + } + + def resultsToStatistics(results: Iterable[(_, Int)]): (Int, Int) = + (results partition (_._2 == 0)) match { + case (winners, losers) => (winners.size, losers.size) + } + + def recordTestTiming(name: String, milliseconds: Long) = + synchronized { testStatistics(name) = milliseconds } + + def showTestStatistics() { + testStatistics.toList sortBy (-_._2) foreach { case (k, v) => println("%s: %.2f seconds".format(k, (v.toDouble / 1000))) } + } +} diff --git a/src/partest-alternative/scala/tools/partest/Universe.scala b/src/partest-alternative/scala/tools/partest/Universe.scala new file mode 100644 index 0000000000..942fc1a8be --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/Universe.scala @@ -0,0 +1,96 @@ +/* __ *\ +** ________ ___ / / ___ Scala Parallel Testing ** +** / __/ __// _ | / / / _ | (c) 2007-2010, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +package scala.tools +package partest + +import nsc.io._ +import category.AllCategories +import io.Logging + +/** The high level view of the partest infrastructure. + */ +abstract class Universe + extends Entities + with BuildContributors + with Logging + with Dispatcher + with Statistics + with Housekeeping + with Results + with PartestCompilation + with PartestSpec + with Config + with Alarms + with Actions + with Categories { + + /** The abstract values from which all else is derived. */ + def partestDir: Directory + def testBuildDir: Directory + def allCategories: List[TestCategory] + def selectedCategories: List[TestCategory] + + /** Some plausibly abstract types. */ + type TestBuild <: BuildContributor // e.g. quick, pack + type TestCategory <: AbsTestCategory // e.g. pos, neg, run + type TestEntity <: AbsTestEntity // e.g. files/pos/test25.scala + type TestSequence <: AbsTestSequence // e.g. compile, run, diff + + /** Although TestStep isn't much more than Function1 right now, + * it exists this way so it can become more capable. + */ + implicit def f1ToTestStep(f: TestEntity => Boolean): TestStep = + new TestStep { def apply(test: TestEntity) = f(test) } + + abstract class TestStep extends (TestEntity => Boolean) { + def apply(test: TestEntity): Boolean + } + + /** An umbrella category of tests, such as "pos" or "run". + */ + trait AbsTestCategory extends BuildContributor { + type TestSettings + + def kind: String + def testSequence: TestSequence + def denotesTest(location: Path): Boolean + + def createTest(location: Path): TestEntity + def createSettings(entity: TestEntity): TestSettings + def enumerate: List[TestEntity] + } + + /** A single test. It may involve multiple files, but only a + * single path is used to designate it. + */ + trait AbsTestEntity extends BuildContributor { + def category: TestCategory + def location: Path + def onException(x: Throwable): Unit + def testClasspath: String + + /** Most tests will use the sequence defined by the category, + * but the test can override and define a custom sequence. + */ + def testSequence: TestSequence + + /** True if this test recognizes the given path as a piece of it. + * For validation purposes. + */ + def acknowledges(path: Path): Boolean + } + + /** Every TestEntity is partly characterized by a series of actions + * which are applied to the TestEntity in the given order. The test + * passes if all those actions return true, fails otherwise. + */ + trait AbsTestSequence { + def actions: List[TestStep] + } +}
\ No newline at end of file diff --git a/src/partest-alternative/scala/tools/partest/ant/JavaTask.scala b/src/partest-alternative/scala/tools/partest/ant/JavaTask.scala new file mode 100644 index 0000000000..6740554dd8 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/ant/JavaTask.scala @@ -0,0 +1,57 @@ +/* __ *\ +** ________ ___ / / ___ Scala Parallel Testing ** +** / __/ __// _ | / / / _ | (c) 2007-2010, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +package scala.tools +package partest +package ant + +import org.apache.tools.ant.Task +import org.apache.tools.ant.taskdefs.Java +import org.apache.tools.ant.types.Environment + +import scala.tools.nsc.io._ +import scala.tools.nsc.util.ClassPath +import cmd.Spec._ + +class JavaTask extends Java { + override def getTaskName() = "partest" + private val scalaRunnerClass = "scala.tools.nsc.MainGenericRunner" + private val partestRunnerClass = "scala.tools.partest.Runner" + def defaultJvmArgs = "-Xms64M -Xmx768M -Xss768K -XX:MaxPermSize=96M" + + protected def rootDir = prop("partest.rootdir") getOrElse (baseDir / "test").path + protected def partestJVMArgs = prop("partest.jvm.args") getOrElse defaultJvmArgs + protected def runnerArgs = List("-usejavacp", partestRunnerClass, "--javaopts", partestJVMArgs) + + private def baseDir = Directory(getProject.getBaseDir) + private def prop(s: String) = Option(getProject getProperty s) + private def jvmline(s: String) = returning(createJvmarg())(_ setLine s) + private def addArg(s: String) = returning(createArg())(_ setValue s) + + private def newKeyValue(key: String, value: String) = + returning(new Environment.Variable)(x => { x setKey key ; x setValue value }) + + def setDefaults() { + setFork(true) + setFailonerror(true) + getProject.setSystemProperties() + setClassname(scalaRunnerClass) + addSysproperty(newKeyValue("partest.is-in-ant", "true")) + jvmline(partestJVMArgs) + runnerArgs foreach addArg + + // do we want basedir or rootDir to be the cwd? + // setDir(Path(rootDir).jfile) + } + + override def init() = { + super.init() + setDefaults() + } +} + diff --git a/src/partest-alternative/scala/tools/partest/antlib.xml b/src/partest-alternative/scala/tools/partest/antlib.xml new file mode 100644 index 0000000000..af36f11368 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/antlib.xml @@ -0,0 +1,3 @@ +<antlib> + <taskdef name="partest" classname="scala.tools.partest.ant.JavaTask"/> +</antlib> diff --git a/src/partest-alternative/scala/tools/partest/category/AllCategories.scala b/src/partest-alternative/scala/tools/partest/category/AllCategories.scala new file mode 100644 index 0000000000..953f80324b --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/category/AllCategories.scala @@ -0,0 +1,20 @@ +/* __ *\ +** ________ ___ / / ___ Scala Parallel Testing ** +** / __/ __// _ | / / / _ | (c) 2007-2010, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +package scala.tools +package partest +package category + +trait AllCategories extends Compiler with Analysis with Runner { + self: Universe => + + object Pos extends DirBasedCategory("pos") { lazy val testSequence: TestSequence = List(compile) } + object Neg extends DirBasedCategory("neg") { lazy val testSequence: TestSequence = List(checkFileRequired, not(compile), diff) } + object Run extends DirBasedCategory("run") { lazy val testSequence: TestSequence = List(compile, run, diff) } + object Jvm extends DirBasedCategory("jvm") { lazy val testSequence: TestSequence = List(compile, run, diff) } +} diff --git a/src/partest-alternative/scala/tools/partest/category/Analysis.scala b/src/partest-alternative/scala/tools/partest/category/Analysis.scala new file mode 100644 index 0000000000..2c6c208ee5 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/category/Analysis.scala @@ -0,0 +1,64 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + */ + +package scala.tools +package partest +package category + +import java.lang.{ ClassLoader => JavaClassLoader } +import java.net.URL +import nsc.util.ScalaClassLoader +import nsc.io._ + +class PartestClassLoader(urls: Array[URL], parent: JavaClassLoader) extends ScalaClassLoader.URLClassLoader(urls, parent) { + def this(urls: Array[URL]) = this(urls, null) + def bytes(path: String) = findBytesForClassName(path) + def singleton(path: String) = tryToInitializeClass(path).get getField "MODULE$" get null + + /** Calls a method in an object via reflection. + */ + def apply[T](className: String, methodName: String)(args: Any*): T = { + def fail = error("Reflection failed on %s.%s".format(className, methodName)) + val clazz = tryToLoadClass(className) getOrElse fail + val obj = singleton(className) + val m = clazz.getMethods find (x => x.getName == methodName && x.getParameterTypes.size == args.size) getOrElse fail + + m.invoke(obj, args map (_.asInstanceOf[AnyRef]): _*).asInstanceOf[T] + } +} + +trait Analysis { + self: Universe => + + object Scalap extends DirBasedCategory("scalap") { + val testSequence: TestSequence = List(checkFileRequired, compile, run, diff) + override def denotesTest(p: Path) = p.isDirectory && (p.toDirectory.files exists (_.name == "result.test")) + override def createTest(location: Path) = new ScalapTest(location) + + class ScalapTest(val location: Path) extends TestEntity { + val category = Scalap + val scalapMain = "scala.tools.scalap.Main$" + val scalapMethod = "decompileScala" + + override def classpathPaths = super.classpathPaths :+ build.scalap + override def checkFile = File(location / "result.test") + + private def runnerURLs = build.classpathPaths ::: classpathPaths map (_.toURL) + private def createClassLoader = new PartestClassLoader(runnerURLs.toArray, this.getClass.getClassLoader) + + val isPackageObject = containsString("package object") + val suffix = if (isPackageObject) ".package" else "" + val className = location.name.capitalize + suffix + + override def run() = loggingResult { + def loader = createClassLoader + def bytes = loader.bytes(className) + + trace("scalap %s".format(className)) + if (isDryRun) "" + else loader[String](scalapMain, scalapMethod)(bytes, isPackageObject) + } + } + } +} diff --git a/src/partest-alternative/scala/tools/partest/category/Compiler.scala b/src/partest-alternative/scala/tools/partest/category/Compiler.scala new file mode 100644 index 0000000000..49775d5031 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/category/Compiler.scala @@ -0,0 +1,140 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + */ + +package scala.tools +package partest +package category + +import nsc.io._ +import nsc.reporters._ +import nsc.{ Settings, CompilerCommand } +import scala.tools.nsc.interactive.RefinedBuildManager +import util.copyPath + +trait Compiler { + self: Universe => + + /** Resident Compiler. + * $SCALAC -d dir.obj -Xresident -sourcepath . "$@" + */ + object Res extends DirBasedCategory("res") { + lazy val testSequence: TestSequence = List(checkFileRequired, compile, diff) + + override def denotesTest(p: Path) = p.isDirectory && resFile(p).isFile + override def createTest(location: Path) = new ResidentTest(location.toDirectory) + + override def createSettings(entity: TestEntity): TestSettings = + returning(super.createSettings(entity)) { settings => + settings.resident.value = true + settings.sourcepath.value = entity.sourcesDir.path + } + + class ResidentTest(val location: Directory) extends TestEntity { + val category = Res + override def sourcesDir = categoryDir + + override def acknowledges(p: Path) = + super.acknowledges(p) || (resFile(location) isSame p) + + private def residentCompilerCommands = safeLines(resFile(location)) + private def compileResident(global: PartestGlobal, lines: List[String]) = { + def printPrompt = global inform "nsc> " + val results = + lines map { line => + printPrompt + trace("compile " + line) + isDryRun || global.partestCompile(toArgs(line) map (categoryDir / _ path), false) + } + + printPrompt + + /** Note - some res tests are really "neg" style tests, so we can't + * use the return value of the compile. The diff catches failures. + */ + true // results forall (_ == true) + } + + override def compile() = compileResident(newGlobal(Nil)._1, residentCompilerCommands) + } + private[Res] def resFile(p: Path) = p.toFile addExtension "res" + } + + object BuildManager extends DirBasedCategory("buildmanager") { + lazy val testSequence: TestSequence = List(checkFileRequired, compile, diff) + override def denotesTest(p: Path) = p.isDirectory && testFile(p).isFile + override def createTest(location: Path) = new BuildManagerTest(location.toDirectory) + + override def createSettings(entity: TestEntity): TestSettings = + returning[TestSettings](super.createSettings(entity)) { settings => + settings.Ybuildmanagerdebug.value = true + settings.sourcepath.value = entity.sourcesDir.path + } + + class PartestBuildManager(settings: Settings, val reporter: ConsoleReporter) extends RefinedBuildManager(settings) { + def errorFn(msg: String) = Console println msg + + override protected def newCompiler(newSettings: Settings) = + new BuilderGlobal(newSettings, reporter) + + private def filesToSet(pre: String, fs: List[String]): Set[AbstractFile] = + fs flatMap (s => Option(AbstractFile getFile (Path(settings.sourcepath.value) / s path))) toSet + + def buildManagerCompile(line: String): Boolean = { + val prompt = "builder > " + reporter printMessage (prompt + line) + val command = new CompilerCommand(toArgs(line), settings) + val files = filesToSet(settings.sourcepath.value, command.files) + + update(files, Set.empty) + true + } + } + + private[BuildManager] def testFile(p: Path) = (p / p.name addExtension "test").toFile + + class BuildManagerTest(val location: Directory) extends TestEntity { + val category = BuildManager + + override def sourcesDir = outDir + override def sourceFiles = Path onlyFiles (location walkFilter (_ != changesDir) filter isJavaOrScala toList) + override def checkFile = File(location / location.name addExtension "check") + + override def acknowledges(p: Path) = super.acknowledges(p) || (p isSame testFile(location)) + + def buildManagerCommands = safeLines(testFile(location)) + def changesDir = Directory(location / (location.name + ".changes")) + + override def compile() = { + val settings = createSettings(this) + val pbm = new PartestBuildManager(settings, newReporter(settings)) + + // copy files + for (source <- sourceFiles) { + val target = outDir / (location.normalize relativize source) + copyPath(source, target.toFile) + } + + def runUpdate(line: String) = { + val Array(srcName, replacement) = line split "=>" + copyPath(File(changesDir / replacement), File(outDir / srcName)) + } + + def sendCommand(line: String): Boolean = { + val compileRegex = """^>>compile (.*)$""".r + val updateRegex = """^>>update\s+(.*)""".r + trace("send: " + (line drop 2)) + + isDryRun || (line match { + case compileRegex(xs) => pbm.buildManagerCompile(xs) + case updateRegex(line) => runUpdate(line) + }) + } + + // send each line to the build manager + buildManagerCommands forall sendCommand + } + } + } +} + diff --git a/src/partest-alternative/scala/tools/partest/category/Runner.scala b/src/partest-alternative/scala/tools/partest/category/Runner.scala new file mode 100644 index 0000000000..10bf5794a9 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/category/Runner.scala @@ -0,0 +1,108 @@ +/* __ *\ +** ________ ___ / / ___ Scala Parallel Testing ** +** / __/ __// _ | / / / _ | (c) 2007-2010, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +package scala.tools +package partest +package category + +import nsc.io._ + +trait Runner { + self: Universe => + + /** Shootout. + */ + object Shootout extends DirBasedCategory("shootout") { + lazy val testSequence: TestSequence = List(compile, run, diff) + + override def denotesTest(p: Path) = isScala(p) && runner(p).isFile + override def createTest(location: Path) = new ShootoutTest(location.toFile) + + class ShootoutTest(val location: File) extends TestEntity { + val category = Shootout + // The files in shootout are very free form, so acknowledge anything close. + override def acknowledges(p: Path) = + (p.parent.normalize isSame Shootout.root) && (p.name startsWith label) + + private def generated = File(outDir / "test.scala") + private def runnerFile = runner(location) + override def sourceFiles = List(generated) + + override def compile() = { + trace("generate %s from %s, %s".format(tracePath(generated), tracePath(location), tracePath(runnerFile))) + // generate source file (even on dry run, we need the path) + generated.writeAll(location.slurp(), runnerFile.slurp()) + + // compile generated file + super.compile() + } + } + + private[Shootout] def runner(p: Path) = p addExtension "runner" toFile + } + + object Scalacheck extends DirBasedCategory("scalacheck") { + lazy val testSequence: TestSequence = List(compile, run) + override def createTest(location: Path) = new ScalacheckTest(location) + + class ScalacheckTest(val location: Path) extends TestEntity { + val category = Scalacheck + + import build.{ scalacheck, forkjoin } + import org.scalacheck.Properties + import org.scalacheck.Test.{ checkProperties, defaultParams, Result } + + override def classpathPaths = super.classpathPaths ::: List(scalacheck, forkjoin) + private def arrayURLs = Array(scalacheck, outDir) map (_.toURL) + + /** For reasons I'm not entirely clear on, I've written all this + * to avoid a source dependency on scalacheck. + */ + class ScalacheckClassLoader extends PartestClassLoader(arrayURLs, this.getClass.getClassLoader) { + type ScalacheckResult = { def passed: Boolean } + + def propCallback(name: String, passed: Int, discarded: Int): Unit = () + def testCallback(name: String, result: AnyRef): Unit = () + + val test = singleton("Test$") + val params = apply[AnyRef]("org.scalacheck.Test$", "defaultParams")() + val result = apply[Seq[(String, AnyRef)]]("org.scalacheck.Test$", "checkProperties")(test, params, propCallback _, testCallback _) + + def allResults() = + for ((prop, res) <- result) yield { + ScalacheckTest.this.trace("%s: %s".format(prop, res)) + res.asInstanceOf[ScalacheckResult].passed + } + + def check() = allResults forall (_ == true) + } + + override def run() = { + trace("scalacheck runs via classloader with: %s".format(arrayURLs mkString ", ")) + isDryRun || (new ScalacheckClassLoader check) + } + } + } + + object Script extends DirBasedCategory("script") { + val testSequence: TestSequence = List(exec, diff) + override def createTest(location: Path) = new ScriptTest(location) + + class ScriptTest(val location: Path) extends TestEntity { + val category = Script + val scriptFile = if (location.isDirectory) location / (label + ".scala") else location + val argsFile = withExtension("args").toFile + def batFile = scriptFile changeExtension "bat" + def script = if (Properties.isWin) batFile else scriptFile + + override def acknowledges(p: Path) = super.acknowledges(p) || (List(argsFile, batFile) exists (_ isSame p)) + override def execCwd = Some(sourcesDir) + override def argumentsToExec = script.path :: safeArgs(argsFile) + } + } +}
\ No newline at end of file diff --git a/src/partest-alternative/scala/tools/partest/io/ANSIWriter.scala b/src/partest-alternative/scala/tools/partest/io/ANSIWriter.scala new file mode 100644 index 0000000000..0ddcd97a5f --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/io/ANSIWriter.scala @@ -0,0 +1,58 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + * @author Philipp Haller + */ + +package scala.tools +package partest +package io + +import java.io.{ Writer, PrintWriter, OutputStream, OutputStreamWriter } + +object ANSIWriter { + val NONE = 0 + val SOME = 1 + val MANY = 2 + + def apply(isAnsi: Boolean) = if (isAnsi) MANY else NONE +} +import ANSIWriter._ + +class ANSIWriter(writer: Writer) extends PrintWriter(writer, true) { + def this(out: OutputStream) = this(new OutputStreamWriter(out)) + def colorful: Int = NONE + + protected val manyColors = List( + Console.BOLD + Console.BLACK, + Console.BOLD + Console.GREEN, + Console.BOLD + Console.RED, + Console.BOLD + Console.YELLOW, + Console.RESET + ) + protected val someColors = List( + Console.BOLD + Console.BLACK, + Console.RESET, + Console.BOLD + Console.BLACK, + Console.BOLD + Console.BLACK, + Console.RESET + ) + protected val noColors = List("", "", "", "", "") + + lazy val List(_outline, _success, _failure, _warning, _default) = colorful match { + case NONE => noColors + case SOME => someColors + case MANY => manyColors + case _ => noColors + } + + private def wrprint(msg: String): Unit = synchronized { + print(msg) + flush() + } + + def outline(msg: String) = wrprint(_outline + msg + _default) + def success(msg: String) = wrprint(_success + msg + _default) + def failure(msg: String) = wrprint(_failure + msg + _default) + def warning(msg: String) = wrprint(_warning + msg + _default) + def normal(msg: String) = wrprint(_default + msg) +} diff --git a/src/partest-alternative/scala/tools/partest/io/Diff.java b/src/partest-alternative/scala/tools/partest/io/Diff.java new file mode 100644 index 0000000000..c7a3d42f30 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/io/Diff.java @@ -0,0 +1,874 @@ +// $Id$ + +package scala.tools.partest.io; + +import java.util.Hashtable; + +/** A class to compare IndexedSeqs of objects. The result of comparison + is a list of <code>change</code> objects which form an + edit script. The objects compared are traditionally lines + of text from two files. Comparison options such as "ignore + whitespace" are implemented by modifying the <code>equals</code> + and <code>hashcode</code> methods for the objects compared. +<p> + The basic algorithm is described in: </br> + "An O(ND) Difference Algorithm and its Variations", Eugene Myers, + Algorithmica Vol. 1 No. 2, 1986, p 251. +<p> + This class outputs different results from GNU diff 1.15 on some + inputs. Our results are actually better (smaller change list, smaller + total size of changes), but it would be nice to know why. Perhaps + there is a memory overwrite bug in GNU diff 1.15. + + @author Stuart D. Gathman, translated from GNU diff 1.15 + Copyright (C) 2000 Business Management Systems, Inc. +<p> + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 1, or (at your option) + any later version. +<p> + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +<p> + You should have received a copy of the <a href=COPYING.txt> + GNU General Public License</a> + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + */ + +public class Diff { + + /** Prepare to find differences between two arrays. Each element of + the arrays is translated to an "equivalence number" based on + the result of <code>equals</code>. The original Object arrays + are no longer needed for computing the differences. They will + be needed again later to print the results of the comparison as + an edit script, if desired. + */ + public Diff(Object[] a,Object[] b) { + Hashtable h = new Hashtable(a.length + b.length); + filevec[0] = new file_data(a,h); + filevec[1] = new file_data(b,h); + } + + /** 1 more than the maximum equivalence value used for this or its + sibling file. */ + private int equiv_max = 1; + + /** When set to true, the comparison uses a heuristic to speed it up. + With this heuristic, for files with a constant small density + of changes, the algorithm is linear in the file size. */ + public boolean heuristic = false; + + /** When set to true, the algorithm returns a guarranteed minimal + set of changes. This makes things slower, sometimes much slower. */ + public boolean no_discards = false; + + private int[] xvec, yvec; /* IndexedSeqs being compared. */ + private int[] fdiag; /* IndexedSeq, indexed by diagonal, containing + the X coordinate of the point furthest + along the given diagonal in the forward + search of the edit matrix. */ + private int[] bdiag; /* IndexedSeq, indexed by diagonal, containing + the X coordinate of the point furthest + along the given diagonal in the backward + search of the edit matrix. */ + private int fdiagoff, bdiagoff; + private final file_data[] filevec = new file_data[2]; + private int cost; + + /** Find the midpoint of the shortest edit script for a specified + portion of the two files. + + We scan from the beginnings of the files, and simultaneously from the ends, + doing a breadth-first search through the space of edit-sequence. + When the two searches meet, we have found the midpoint of the shortest + edit sequence. + + The value returned is the number of the diagonal on which the midpoint lies. + The diagonal number equals the number of inserted lines minus the number + of deleted lines (counting only lines before the midpoint). + The edit cost is stored into COST; this is the total number of + lines inserted or deleted (counting only lines before the midpoint). + + This function assumes that the first lines of the specified portions + of the two files do not match, and likewise that the last lines do not + match. The caller must trim matching lines from the beginning and end + of the portions it is going to specify. + + Note that if we return the "wrong" diagonal value, or if + the value of bdiag at that diagonal is "wrong", + the worst this can do is cause suboptimal diff output. + It cannot cause incorrect diff output. */ + + private int diag (int xoff, int xlim, int yoff, int ylim) { + final int[] fd = fdiag; // Give the compiler a chance. + final int[] bd = bdiag; // Additional help for the compiler. + final int[] xv = xvec; // Still more help for the compiler. + final int[] yv = yvec; // And more and more . . . + final int dmin = xoff - ylim; // Minimum valid diagonal. + final int dmax = xlim - yoff; // Maximum valid diagonal. + final int fmid = xoff - yoff; // Center diagonal of top-down search. + final int bmid = xlim - ylim; // Center diagonal of bottom-up search. + int fmin = fmid, fmax = fmid; // Limits of top-down search. + int bmin = bmid, bmax = bmid; // Limits of bottom-up search. + /* True if southeast corner is on an odd + diagonal with respect to the northwest. */ + final boolean odd = (fmid - bmid & 1) != 0; + + fd[fdiagoff + fmid] = xoff; + bd[bdiagoff + bmid] = xlim; + + for (int c = 1;; ++c) + { + int d; /* Active diagonal. */ + boolean big_snake = false; + + /* Extend the top-down search by an edit step in each diagonal. */ + if (fmin > dmin) + fd[fdiagoff + --fmin - 1] = -1; + else + ++fmin; + if (fmax < dmax) + fd[fdiagoff + ++fmax + 1] = -1; + else + --fmax; + for (d = fmax; d >= fmin; d -= 2) + { + int x, y, oldx, tlo = fd[fdiagoff + d - 1], thi = fd[fdiagoff + d + 1]; + + if (tlo >= thi) + x = tlo + 1; + else + x = thi; + oldx = x; + y = x - d; + while (x < xlim && y < ylim && xv[x] == yv[y]) { + ++x; ++y; + } + if (x - oldx > 20) + big_snake = true; + fd[fdiagoff + d] = x; + if (odd && bmin <= d && d <= bmax && bd[bdiagoff + d] <= fd[fdiagoff + d]) + { + cost = 2 * c - 1; + return d; + } + } + + /* Similar extend the bottom-up search. */ + if (bmin > dmin) + bd[bdiagoff + --bmin - 1] = Integer.MAX_VALUE; + else + ++bmin; + if (bmax < dmax) + bd[bdiagoff + ++bmax + 1] = Integer.MAX_VALUE; + else + --bmax; + for (d = bmax; d >= bmin; d -= 2) + { + int x, y, oldx, tlo = bd[bdiagoff + d - 1], thi = bd[bdiagoff + d + 1]; + + if (tlo < thi) + x = tlo; + else + x = thi - 1; + oldx = x; + y = x - d; + while (x > xoff && y > yoff && xv[x - 1] == yv[y - 1]) { + --x; --y; + } + if (oldx - x > 20) + big_snake = true; + bd[bdiagoff + d] = x; + if (!odd && fmin <= d && d <= fmax && bd[bdiagoff + d] <= fd[fdiagoff + d]) + { + cost = 2 * c; + return d; + } + } + + /* Heuristic: check occasionally for a diagonal that has made + lots of progress compared with the edit distance. + If we have any such, find the one that has made the most + progress and return it as if it had succeeded. + + With this heuristic, for files with a constant small density + of changes, the algorithm is linear in the file size. */ + + if (c > 200 && big_snake && heuristic) + { + int best = 0; + int bestpos = -1; + + for (d = fmax; d >= fmin; d -= 2) + { + int dd = d - fmid; + if ((fd[fdiagoff + d] - xoff)*2 - dd > 12 * (c + (dd > 0 ? dd : -dd))) + { + if (fd[fdiagoff + d] * 2 - dd > best + && fd[fdiagoff + d] - xoff > 20 + && fd[fdiagoff + d] - d - yoff > 20) + { + int k; + int x = fd[fdiagoff + d]; + + /* We have a good enough best diagonal; + now insist that it end with a significant snake. */ + for (k = 1; k <= 20; k++) + if (xvec[x - k] != yvec[x - d - k]) + break; + + if (k == 21) + { + best = fd[fdiagoff + d] * 2 - dd; + bestpos = d; + } + } + } + } + if (best > 0) + { + cost = 2 * c - 1; + return bestpos; + } + + best = 0; + for (d = bmax; d >= bmin; d -= 2) + { + int dd = d - bmid; + if ((xlim - bd[bdiagoff + d])*2 + dd > 12 * (c + (dd > 0 ? dd : -dd))) + { + if ((xlim - bd[bdiagoff + d]) * 2 + dd > best + && xlim - bd[bdiagoff + d] > 20 + && ylim - (bd[bdiagoff + d] - d) > 20) + { + /* We have a good enough best diagonal; + now insist that it end with a significant snake. */ + int k; + int x = bd[bdiagoff + d]; + + for (k = 0; k < 20; k++) + if (xvec[x + k] != yvec[x - d + k]) + break; + if (k == 20) + { + best = (xlim - bd[bdiagoff + d]) * 2 + dd; + bestpos = d; + } + } + } + } + if (best > 0) + { + cost = 2 * c - 1; + return bestpos; + } + } + } + } + + /** Compare in detail contiguous subsequences of the two files + which are known, as a whole, to match each other. + + The results are recorded in the IndexedSeqs filevec[N].changed_flag, by + storing a 1 in the element for each line that is an insertion or deletion. + + The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1. + + Note that XLIM, YLIM are exclusive bounds. + All line numbers are origin-0 and discarded lines are not counted. */ + + private void compareseq (int xoff, int xlim, int yoff, int ylim) { + /* Slide down the bottom initial diagonal. */ + while (xoff < xlim && yoff < ylim && xvec[xoff] == yvec[yoff]) { + ++xoff; ++yoff; + } + /* Slide up the top initial diagonal. */ + while (xlim > xoff && ylim > yoff && xvec[xlim - 1] == yvec[ylim - 1]) { + --xlim; --ylim; + } + + /* Handle simple cases. */ + if (xoff == xlim) + while (yoff < ylim) + filevec[1].changed_flag[1+filevec[1].realindexes[yoff++]] = true; + else if (yoff == ylim) + while (xoff < xlim) + filevec[0].changed_flag[1+filevec[0].realindexes[xoff++]] = true; + else + { + /* Find a point of correspondence in the middle of the files. */ + + int d = diag (xoff, xlim, yoff, ylim); + int c = cost; + int f = fdiag[fdiagoff + d]; + int b = bdiag[bdiagoff + d]; + + if (c == 1) + { + /* This should be impossible, because it implies that + one of the two subsequences is empty, + and that case was handled above without calling `diag'. + Let's verify that this is true. */ + throw new IllegalArgumentException("Empty subsequence"); + } + else + { + /* Use that point to split this problem into two subproblems. */ + compareseq (xoff, b, yoff, b - d); + /* This used to use f instead of b, + but that is incorrect! + It is not necessarily the case that diagonal d + has a snake from b to f. */ + compareseq (b, xlim, b - d, ylim); + } + } + } + + /** Discard lines from one file that have no matches in the other file. + */ + + private void discard_confusing_lines() { + filevec[0].discard_confusing_lines(filevec[1]); + filevec[1].discard_confusing_lines(filevec[0]); + } + + private boolean inhibit = false; + + /** Adjust inserts/deletes of blank lines to join changes + as much as possible. + */ + + private void shift_boundaries() { + if (inhibit) + return; + filevec[0].shift_boundaries(filevec[1]); + filevec[1].shift_boundaries(filevec[0]); + } + + public interface ScriptBuilder { + /** Scan the tables of which lines are inserted and deleted, + producing an edit script. + @param changed0 true for lines in first file which do not match 2nd + @param len0 number of lines in first file + @param changed1 true for lines in 2nd file which do not match 1st + @param len1 number of lines in 2nd file + @return a linked list of changes - or null + */ + public change build_script( + boolean[] changed0,int len0, + boolean[] changed1,int len1 + ); + } + + /** Scan the tables of which lines are inserted and deleted, + producing an edit script in reverse order. */ + + static class ReverseScript implements ScriptBuilder { + public change build_script( + final boolean[] changed0,int len0, + final boolean[] changed1,int len1) + { + change script = null; + int i0 = 0, i1 = 0; + while (i0 < len0 || i1 < len1) { + if (changed0[1+i0] || changed1[1+i1]) { + int line0 = i0, line1 = i1; + + /* Find # lines changed here in each file. */ + while (changed0[1+i0]) ++i0; + while (changed1[1+i1]) ++i1; + + /* Record this change. */ + script = new change(line0, line1, i0 - line0, i1 - line1, script); + } + + /* We have reached lines in the two files that match each other. */ + i0++; i1++; + } + + return script; + } + } + + static class ForwardScript implements ScriptBuilder { + /** Scan the tables of which lines are inserted and deleted, + producing an edit script in forward order. */ + public change build_script( + final boolean[] changed0,int len0, + final boolean[] changed1,int len1) + { + change script = null; + int i0 = len0, i1 = len1; + + while (i0 >= 0 || i1 >= 0) + { + if (changed0[i0] || changed1[i1]) + { + int line0 = i0, line1 = i1; + + /* Find # lines changed here in each file. */ + while (changed0[i0]) --i0; + while (changed1[i1]) --i1; + + /* Record this change. */ + script = new change(i0, i1, line0 - i0, line1 - i1, script); + } + + /* We have reached lines in the two files that match each other. */ + i0--; i1--; + } + + return script; + } + } + + /** Standard ScriptBuilders. */ + public final static ScriptBuilder + forwardScript = new ForwardScript(), + reverseScript = new ReverseScript(); + + /* Report the differences of two files. DEPTH is the current directory + depth. */ + public final change diff_2(final boolean reverse) { + return diff(reverse ? reverseScript : forwardScript); + } + + /** Get the results of comparison as an edit script. The script + is described by a list of changes. The standard ScriptBuilder + implementations provide for forward and reverse edit scripts. + Alternate implementations could, for instance, list common elements + instead of differences. + @param bld an object to build the script from change flags + @return the head of a list of changes + */ + public change diff(final ScriptBuilder bld) { + + /* Some lines are obviously insertions or deletions + because they don't match anything. Detect them now, + and avoid even thinking about them in the main comparison algorithm. */ + + discard_confusing_lines (); + + /* Now do the main comparison algorithm, considering just the + undiscarded lines. */ + + xvec = filevec[0].undiscarded; + yvec = filevec[1].undiscarded; + + int diags = + filevec[0].nondiscarded_lines + filevec[1].nondiscarded_lines + 3; + fdiag = new int[diags]; + fdiagoff = filevec[1].nondiscarded_lines + 1; + bdiag = new int[diags]; + bdiagoff = filevec[1].nondiscarded_lines + 1; + + compareseq (0, filevec[0].nondiscarded_lines, + 0, filevec[1].nondiscarded_lines); + fdiag = null; + bdiag = null; + + /* Modify the results slightly to make them prettier + in cases where that can validly be done. */ + + shift_boundaries (); + + /* Get the results of comparison in the form of a chain + of `struct change's -- an edit script. */ + return bld.build_script( + filevec[0].changed_flag, + filevec[0].buffered_lines, + filevec[1].changed_flag, + filevec[1].buffered_lines + ); + + } + + /** The result of comparison is an "edit script": a chain of change objects. + Each change represents one place where some lines are deleted + and some are inserted. + + LINE0 and LINE1 are the first affected lines in the two files (origin 0). + DELETED is the number of lines deleted here from file 0. + INSERTED is the number of lines inserted here in file 1. + + If DELETED is 0 then LINE0 is the number of the line before + which the insertion was done; vice versa for INSERTED and LINE1. */ + + public static class change { + /** Previous or next edit command. */ + public change link; + /** # lines of file 1 changed here. */ + public final int inserted; + /** # lines of file 0 changed here. */ + public final int deleted; + /** Line number of 1st deleted line. */ + public final int line0; + /** Line number of 1st inserted line. */ + public final int line1; + + /** Cons an additional entry onto the front of an edit script OLD. + LINE0 and LINE1 are the first affected lines in the two files (origin 0). + DELETED is the number of lines deleted here from file 0. + INSERTED is the number of lines inserted here in file 1. + + If DELETED is 0 then LINE0 is the number of the line before + which the insertion was done; vice versa for INSERTED and LINE1. */ + public change(int line0, int line1, int deleted, int inserted, change old) { + this.line0 = line0; + this.line1 = line1; + this.inserted = inserted; + this.deleted = deleted; + this.link = old; + //System.err.println(line0+","+line1+","+inserted+","+deleted); + } + } + + /** Data on one input file being compared. + */ + + class file_data { + + /** Allocate changed array for the results of comparison. */ + void clear() { + /* Allocate a flag for each line of each file, saying whether that line + is an insertion or deletion. + Allocate an extra element, always zero, at each end of each IndexedSeq. + */ + changed_flag = new boolean[buffered_lines + 2]; + } + + /** Return equiv_count[I] as the number of lines in this file + that fall in equivalence class I. + @return the array of equivalence class counts. + */ + int[] equivCount() { + int[] equiv_count = new int[equiv_max]; + for (int i = 0; i < buffered_lines; ++i) + ++equiv_count[equivs[i]]; + return equiv_count; + } + + /** Discard lines that have no matches in another file. + + A line which is discarded will not be considered by the actual + comparison algorithm; it will be as if that line were not in the file. + The file's `realindexes' table maps virtual line numbers + (which don't count the discarded lines) into real line numbers; + this is how the actual comparison algorithm produces results + that are comprehensible when the discarded lines are counted. +<p> + When we discard a line, we also mark it as a deletion or insertion + so that it will be printed in the output. + @param f the other file + */ + void discard_confusing_lines(file_data f) { + clear(); + /* Set up table of which lines are going to be discarded. */ + final byte[] discarded = discardable(f.equivCount()); + + /* Don't really discard the provisional lines except when they occur + in a run of discardables, with nonprovisionals at the beginning + and end. */ + filterDiscards(discarded); + + /* Actually discard the lines. */ + discard(discarded); + } + + /** Mark to be discarded each line that matches no line of another file. + If a line matches many lines, mark it as provisionally discardable. + @see equivCount() + @param counts The count of each equivalence number for the other file. + @return 0=nondiscardable, 1=discardable or 2=provisionally discardable + for each line + */ + + private byte[] discardable(final int[] counts) { + final int end = buffered_lines; + final byte[] discards = new byte[end]; + final int[] equivs = this.equivs; + int many = 5; + int tem = end / 64; + + /* Multiply MANY by approximate square root of number of lines. + That is the threshold for provisionally discardable lines. */ + while ((tem = tem >> 2) > 0) + many *= 2; + + for (int i = 0; i < end; i++) + { + int nmatch; + if (equivs[i] == 0) + continue; + nmatch = counts[equivs[i]]; + if (nmatch == 0) + discards[i] = 1; + else if (nmatch > many) + discards[i] = 2; + } + return discards; + } + + /** Don't really discard the provisional lines except when they occur + in a run of discardables, with nonprovisionals at the beginning + and end. */ + + private void filterDiscards(final byte[] discards) { + final int end = buffered_lines; + + for (int i = 0; i < end; i++) + { + /* Cancel provisional discards not in middle of run of discards. */ + if (discards[i] == 2) + discards[i] = 0; + else if (discards[i] != 0) + { + /* We have found a nonprovisional discard. */ + int j; + int length; + int provisional = 0; + + /* Find end of this run of discardable lines. + Count how many are provisionally discardable. */ + for (j = i; j < end; j++) + { + if (discards[j] == 0) + break; + if (discards[j] == 2) + ++provisional; + } + + /* Cancel provisional discards at end, and shrink the run. */ + while (j > i && discards[j - 1] == 2) { + discards[--j] = 0; --provisional; + } + + /* Now we have the length of a run of discardable lines + whose first and last are not provisional. */ + length = j - i; + + /* If 1/4 of the lines in the run are provisional, + cancel discarding of all provisional lines in the run. */ + if (provisional * 4 > length) + { + while (j > i) + if (discards[--j] == 2) + discards[j] = 0; + } + else + { + int consec; + int minimum = 1; + int tem = length / 4; + + /* MINIMUM is approximate square root of LENGTH/4. + A subrun of two or more provisionals can stand + when LENGTH is at least 16. + A subrun of 4 or more can stand when LENGTH >= 64. */ + while ((tem = tem >> 2) > 0) + minimum *= 2; + minimum++; + + /* Cancel any subrun of MINIMUM or more provisionals + within the larger run. */ + for (j = 0, consec = 0; j < length; j++) + if (discards[i + j] != 2) + consec = 0; + else if (minimum == ++consec) + /* Back up to start of subrun, to cancel it all. */ + j -= consec; + else if (minimum < consec) + discards[i + j] = 0; + + /* Scan from beginning of run + until we find 3 or more nonprovisionals in a row + or until the first nonprovisional at least 8 lines in. + Until that point, cancel any provisionals. */ + for (j = 0, consec = 0; j < length; j++) + { + if (j >= 8 && discards[i + j] == 1) + break; + if (discards[i + j] == 2) { + consec = 0; discards[i + j] = 0; + } + else if (discards[i + j] == 0) + consec = 0; + else + consec++; + if (consec == 3) + break; + } + + /* I advances to the last line of the run. */ + i += length - 1; + + /* Same thing, from end. */ + for (j = 0, consec = 0; j < length; j++) + { + if (j >= 8 && discards[i - j] == 1) + break; + if (discards[i - j] == 2) { + consec = 0; discards[i - j] = 0; + } + else if (discards[i - j] == 0) + consec = 0; + else + consec++; + if (consec == 3) + break; + } + } + } + } + } + + /** Actually discard the lines. + @param discards flags lines to be discarded + */ + private void discard(final byte[] discards) { + final int end = buffered_lines; + int j = 0; + for (int i = 0; i < end; ++i) + if (no_discards || discards[i] == 0) + { + undiscarded[j] = equivs[i]; + realindexes[j++] = i; + } + else + changed_flag[1+i] = true; + nondiscarded_lines = j; + } + + file_data(Object[] data,Hashtable h) { + buffered_lines = data.length; + + equivs = new int[buffered_lines]; + undiscarded = new int[buffered_lines]; + realindexes = new int[buffered_lines]; + + for (int i = 0; i < data.length; ++i) { + Integer ir = (Integer)h.get(data[i]); + if (ir == null) + h.put(data[i],new Integer(equivs[i] = equiv_max++)); + else + equivs[i] = ir.intValue(); + } + } + + /** Adjust inserts/deletes of blank lines to join changes + as much as possible. + + We do something when a run of changed lines include a blank + line at one end and have an excluded blank line at the other. + We are free to choose which blank line is included. + `compareseq' always chooses the one at the beginning, + but usually it is cleaner to consider the following blank line + to be the "change". The only exception is if the preceding blank line + would join this change to other changes. + @param f the file being compared against + */ + + void shift_boundaries(file_data f) { + final boolean[] changed = changed_flag; + final boolean[] other_changed = f.changed_flag; + int i = 0; + int j = 0; + int i_end = buffered_lines; + int preceding = -1; + int other_preceding = -1; + + for (;;) + { + int start, end, other_start; + + /* Scan forwards to find beginning of another run of changes. + Also keep track of the corresponding point in the other file. */ + + while (i < i_end && !changed[1+i]) + { + while (other_changed[1+j++]) + /* Non-corresponding lines in the other file + will count as the preceding batch of changes. */ + other_preceding = j; + i++; + } + + if (i == i_end) + break; + + start = i; + other_start = j; + + for (;;) + { + /* Now find the end of this run of changes. */ + + while (i < i_end && changed[1+i]) i++; + end = i; + + /* If the first changed line matches the following unchanged one, + and this run does not follow right after a previous run, + and there are no lines deleted from the other file here, + then classify the first changed line as unchanged + and the following line as changed in its place. */ + + /* You might ask, how could this run follow right after another? + Only because the previous run was shifted here. */ + + if (end != i_end + && equivs[start] == equivs[end] + && !other_changed[1+j] + && end != i_end + && !((preceding >= 0 && start == preceding) + || (other_preceding >= 0 + && other_start == other_preceding))) + { + changed[1+end++] = true; + changed[1+start++] = false; + ++i; + /* Since one line-that-matches is now before this run + instead of after, we must advance in the other file + to keep in synch. */ + ++j; + } + else + break; + } + + preceding = i; + other_preceding = j; + } + } + + /** Number of elements (lines) in this file. */ + final int buffered_lines; + + /** IndexedSeq, indexed by line number, containing an equivalence code for + each line. It is this IndexedSeq that is actually compared with that + of another file to generate differences. */ + private final int[] equivs; + + /** IndexedSeq, like the previous one except that + the elements for discarded lines have been squeezed out. */ + final int[] undiscarded; + + /** IndexedSeq mapping virtual line numbers (not counting discarded lines) + to real ones (counting those lines). Both are origin-0. */ + final int[] realindexes; + + /** Total number of nondiscarded lines. */ + int nondiscarded_lines; + + /** Array, indexed by real origin-1 line number, + containing true for a line that is an insertion or a deletion. + The results of comparison are stored here. */ + boolean[] changed_flag; + + } +} diff --git a/src/partest-alternative/scala/tools/partest/io/DiffPrint.java b/src/partest-alternative/scala/tools/partest/io/DiffPrint.java new file mode 100644 index 0000000000..2b2ad93ec7 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/io/DiffPrint.java @@ -0,0 +1,607 @@ +// $Id$ + +package scala.tools.partest.io; + +import java.io.*; +import java.util.Vector; +import java.util.Date; +//import com.objectspace.jgl.predicates.UnaryPredicate; + +interface UnaryPredicate { + boolean execute(Object obj); +} + +/** A simple framework for printing change lists produced by <code>Diff</code>. + @see bmsi.util.Diff + @author Stuart D. Gathman + Copyright (C) 2000 Business Management Systems, Inc. +<p> + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 1, or (at your option) + any later version. +<p> + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +<p> + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ +public class DiffPrint { + /** A Base class for printing edit scripts produced by Diff. + This class divides the change list into "hunks", and calls + <code>print_hunk</code> for each hunk. Various utility methods + are provided as well. + */ + public static abstract class Base { + protected Base(Object[] a,Object[] b, Writer w) { + outfile = new PrintWriter(w); + file0 = a; + file1 = b; + } + /** Set to ignore certain kinds of lines when printing + an edit script. For example, ignoring blank lines or comments. + */ + protected UnaryPredicate ignore = null; + + /** Set to the lines of the files being compared. + */ + protected Object[] file0, file1; + + /** Divide SCRIPT into pieces by calling HUNKFUN and + print each piece with PRINTFUN. + Both functions take one arg, an edit script. + + PRINTFUN takes a subscript which belongs together (with a null + link at the end) and prints it. */ + public void print_script(Diff.change script) { + Diff.change next = script; + + while (next != null) + { + Diff.change t, end; + + /* Find a set of changes that belong together. */ + t = next; + end = hunkfun(next); + + /* Disconnect them from the rest of the changes, + making them a hunk, and remember the rest for next iteration. */ + next = end.link; + end.link = null; + //if (DEBUG) + // debug_script(t); + + /* Print this hunk. */ + print_hunk(t); + + /* Reconnect the script so it will all be freed properly. */ + end.link = next; + } + outfile.flush(); + } + + /** Called with the tail of the script + and returns the last link that belongs together with the start + of the tail. */ + + protected Diff.change hunkfun(Diff.change hunk) { + return hunk; + } + + protected int first0, last0, first1, last1, deletes, inserts; + protected PrintWriter outfile; + + /** Look at a hunk of edit script and report the range of lines in each file + that it applies to. HUNK is the start of the hunk, which is a chain + of `struct change'. The first and last line numbers of file 0 are stored + in *FIRST0 and *LAST0, and likewise for file 1 in *FIRST1 and *LAST1. + Note that these are internal line numbers that count from 0. + + If no lines from file 0 are deleted, then FIRST0 is LAST0+1. + + Also set *DELETES nonzero if any lines of file 0 are deleted + and set *INSERTS nonzero if any lines of file 1 are inserted. + If only ignorable lines are inserted or deleted, both are + set to 0. */ + + protected void analyze_hunk(Diff.change hunk) { + int f0, l0 = 0, f1, l1 = 0, show_from = 0, show_to = 0; + int i; + Diff.change next; + boolean nontrivial = (ignore == null); + + show_from = show_to = 0; + + f0 = hunk.line0; + f1 = hunk.line1; + + for (next = hunk; next != null; next = next.link) + { + l0 = next.line0 + next.deleted - 1; + l1 = next.line1 + next.inserted - 1; + show_from += next.deleted; + show_to += next.inserted; + for (i = next.line0; i <= l0 && ! nontrivial; i++) + if (!ignore.execute(file0[i])) + nontrivial = true; + for (i = next.line1; i <= l1 && ! nontrivial; i++) + if (!ignore.execute(file1[i])) + nontrivial = true; + } + + first0 = f0; + last0 = l0; + first1 = f1; + last1 = l1; + + /* If all inserted or deleted lines are ignorable, + tell the caller to ignore this hunk. */ + + if (!nontrivial) + show_from = show_to = 0; + + deletes = show_from; + inserts = show_to; + } + + /** Print the script header which identifies the files compared. */ + protected void print_header(String filea, String fileb) { } + + protected abstract void print_hunk(Diff.change hunk); + + protected void print_1_line(String pre,Object linbuf) { + outfile.println(pre + linbuf.toString()); + } + + /** Print a pair of line numbers with SEPCHAR, translated for file FILE. + If the two numbers are identical, print just one number. + + Args A and B are internal line numbers. + We print the translated (real) line numbers. */ + + protected void print_number_range (char sepchar, int a, int b) { + /* Note: we can have B < A in the case of a range of no lines. + In this case, we should print the line number before the range, + which is B. */ + if (++b > ++a) + outfile.print("" + a + sepchar + b); + else + outfile.print(b); + } + + public static char change_letter(int inserts, int deletes) { + if (inserts == 0) + return 'd'; + else if (deletes == 0) + return 'a'; + else + return 'c'; + } + } + + /** Print a change list in the standard diff format. + */ + public static class NormalPrint extends Base { + + public NormalPrint(Object[] a,Object[] b, Writer w) { + super(a,b,w); + } + + /** Print a hunk of a normal diff. + This is a contiguous portion of a complete edit script, + describing changes in consecutive lines. */ + + protected void print_hunk (Diff.change hunk) { + + /* Determine range of line numbers involved in each file. */ + analyze_hunk(hunk); + if (deletes == 0 && inserts == 0) + return; + + /* Print out the line number header for this hunk */ + print_number_range (',', first0, last0); + outfile.print(change_letter(inserts, deletes)); + print_number_range (',', first1, last1); + outfile.println(); + + /* Print the lines that the first file has. */ + if (deletes != 0) + for (int i = first0; i <= last0; i++) + print_1_line ("< ", file0[i]); + + if (inserts != 0 && deletes != 0) + outfile.println("---"); + + /* Print the lines that the second file has. */ + if (inserts != 0) + for (int i = first1; i <= last1; i++) + print_1_line ("> ", file1[i]); + } + } + + /** Prints an edit script in a format suitable for input to <code>ed</code>. + The edit script must be generated with the reverse option to + be useful as actual <code>ed</code> input. + */ + public static class EdPrint extends Base { + + public EdPrint(Object[] a,Object[] b, Writer w) { + super(a,b,w); + } + + /** Print a hunk of an ed diff */ + protected void print_hunk(Diff.change hunk) { + + /* Determine range of line numbers involved in each file. */ + analyze_hunk (hunk); + if (deletes == 0 && inserts == 0) + return; + + /* Print out the line number header for this hunk */ + print_number_range (',', first0, last0); + outfile.println(change_letter(inserts, deletes)); + + /* Print new/changed lines from second file, if needed */ + if (inserts != 0) + { + boolean inserting = true; + for (int i = first1; i <= last1; i++) + { + /* Resume the insert, if we stopped. */ + if (! inserting) + outfile.println(i - first1 + first0 + "a"); + inserting = true; + + /* If the file's line is just a dot, it would confuse `ed'. + So output it with a double dot, and set the flag LEADING_DOT + so that we will output another ed-command later + to change the double dot into a single dot. */ + + if (".".equals(file1[i])) + { + outfile.println(".."); + outfile.println("."); + /* Now change that double dot to the desired single dot. */ + outfile.println(i - first1 + first0 + 1 + "s/^\\.\\././"); + inserting = false; + } + else + /* Line is not `.', so output it unmodified. */ + print_1_line ("", file1[i]); + } + + /* End insert mode, if we are still in it. */ + if (inserting) + outfile.println("."); + } + } + } + + /** Prints an edit script in context diff format. This and its + 'unified' variation is used for source code patches. + */ + public static class ContextPrint extends Base { + + protected int context = 3; + + public ContextPrint(Object[] a,Object[] b, Writer w) { + super(a,b,w); + } + + protected void print_context_label (String mark, File inf, String label) { + if (label != null) + outfile.println(mark + ' ' + label); + else if (inf.lastModified() > 0) + // FIXME: use DateFormat to get precise format needed. + outfile.println( + mark + ' ' + inf.getPath() + '\t' + new Date(inf.lastModified()) + ); + else + /* Don't pretend that standard input is ancient. */ + outfile.println(mark + ' ' + inf.getPath()); + } + + public void print_header(String filea,String fileb) { + print_context_label ("***", new File(filea), filea); + print_context_label ("---", new File(fileb), fileb); + } + + /** If function_regexp defined, search for start of function. */ + private String find_function(Object[] lines, int start) { + return null; + } + + protected void print_function(Object[] file,int start) { + String function = find_function (file0, first0); + if (function != null) { + outfile.print(" "); + outfile.print( + (function.length() < 40) ? function : function.substring(0,40) + ); + } + } + + protected void print_hunk(Diff.change hunk) { + + /* Determine range of line numbers involved in each file. */ + + analyze_hunk (hunk); + + if (deletes == 0 && inserts == 0) + return; + + /* Include a context's width before and after. */ + + first0 = Math.max(first0 - context, 0); + first1 = Math.max(first1 - context, 0); + last0 = Math.min(last0 + context, file0.length - 1); + last1 = Math.min(last1 + context, file1.length - 1); + + + outfile.print("***************"); + + /* If we looked for and found a function this is part of, + include its name in the header of the diff section. */ + print_function (file0, first0); + + outfile.println(); + outfile.print("*** "); + print_number_range (',', first0, last0); + outfile.println(" ****"); + + if (deletes != 0) { + Diff.change next = hunk; + + for (int i = first0; i <= last0; i++) { + /* Skip past changes that apply (in file 0) + only to lines before line I. */ + + while (next != null && next.line0 + next.deleted <= i) + next = next.link; + + /* Compute the marking for line I. */ + + String prefix = " "; + if (next != null && next.line0 <= i) + /* The change NEXT covers this line. + If lines were inserted here in file 1, this is "changed". + Otherwise it is "deleted". */ + prefix = (next.inserted > 0) ? "!" : "-"; + + print_1_line (prefix, file0[i]); + } + } + + outfile.print("--- "); + print_number_range (',', first1, last1); + outfile.println(" ----"); + + if (inserts != 0) { + Diff.change next = hunk; + + for (int i = first1; i <= last1; i++) { + /* Skip past changes that apply (in file 1) + only to lines before line I. */ + + while (next != null && next.line1 + next.inserted <= i) + next = next.link; + + /* Compute the marking for line I. */ + + String prefix = " "; + if (next != null && next.line1 <= i) + /* The change NEXT covers this line. + If lines were deleted here in file 0, this is "changed". + Otherwise it is "inserted". */ + prefix = (next.deleted > 0) ? "!" : "+"; + + print_1_line (prefix, file1[i]); + } + } + } + } + + /** Prints an edit script in context diff format. This and its + 'unified' variation is used for source code patches. + */ + public static class UnifiedPrint extends ContextPrint { + + public UnifiedPrint(Object[] a,Object[] b, Writer w) { + super(a,b,w); + } + + public void print_header(String filea,String fileb) { + print_context_label ("---", new File(filea), filea); + print_context_label ("+++", new File(fileb), fileb); + } + + private void print_number_range (int a, int b) { + //translate_range (file, a, b, &trans_a, &trans_b); + + /* Note: we can have B < A in the case of a range of no lines. + In this case, we should print the line number before the range, + which is B. */ + if (b < a) + outfile.print(b + ",0"); + else + super.print_number_range(',',a,b); + } + + protected void print_hunk(Diff.change hunk) { + /* Determine range of line numbers involved in each file. */ + analyze_hunk (hunk); + + if (deletes == 0 && inserts == 0) + return; + + /* Include a context's width before and after. */ + + first0 = Math.max(first0 - context, 0); + first1 = Math.max(first1 - context, 0); + last0 = Math.min(last0 + context, file0.length - 1); + last1 = Math.min(last1 + context, file1.length - 1); + + + + outfile.print("@@ -"); + print_number_range (first0, last0); + outfile.print(" +"); + print_number_range (first1, last1); + outfile.print(" @@"); + + /* If we looked for and found a function this is part of, + include its name in the header of the diff section. */ + print_function(file0,first0); + + outfile.println(); + + Diff.change next = hunk; + int i = first0; + int j = first1; + + while (i <= last0 || j <= last1) { + + /* If the line isn't a difference, output the context from file 0. */ + + if (next == null || i < next.line0) { + outfile.print(' '); + print_1_line ("", file0[i++]); + j++; + } + else { + /* For each difference, first output the deleted part. */ + + int k = next.deleted; + while (k-- > 0) { + outfile.print('-'); + print_1_line ("", file0[i++]); + } + + /* Then output the inserted part. */ + + k = next.inserted; + while (k-- > 0) { + outfile.print('+'); + print_1_line ("", file1[j++]); + } + + /* We're done with this hunk, so on to the next! */ + + next = next.link; + } + } + } + } + + + /** Read a text file into an array of String. This provides basic diff + functionality. A more advanced diff utility will use specialized + objects to represent the text lines, with options to, for example, + convert sequences of whitespace to a single space for comparison + purposes. + */ + static String[] slurp(String file) throws IOException { + BufferedReader rdr = new BufferedReader(new FileReader(file)); + Vector s = new Vector(); + for (;;) { + String line = rdr.readLine(); + if (line == null) break; + s.addElement(line); + } + String[] a = new String[s.size()]; + s.copyInto(a); + return a; + } + + public static void main(String[] argv) throws IOException { + String filea = argv[argv.length - 2]; + String fileb = argv[argv.length - 1]; + String[] a = slurp(filea); + String[] b = slurp(fileb); + Diff d = new Diff(a,b); + char style = 'n'; + for (int i = 0; i < argv.length - 2; ++i) { + String f = argv[i]; + if (f.startsWith("-")) { + for (int j = 1; j < f.length(); ++j) { + switch (f.charAt(j)) { + case 'e': // Ed style + style = 'e'; break; + case 'c': // Context diff + style = 'c'; break; + case 'u': + style = 'u'; break; + } + } + } + } + boolean reverse = style == 'e'; + Diff.change script = d.diff_2(reverse); + if (script == null) + System.err.println("No differences"); + else { + Base p; + Writer w = new OutputStreamWriter(System.out); + switch (style) { + case 'e': + p = new EdPrint(a,b,w); break; + case 'c': + p = new ContextPrint(a,b,w); break; + case 'u': + p = new UnifiedPrint(a,b,w); break; + default: + p = new NormalPrint(a,b,w); + } + p.print_header(filea,fileb); + p.print_script(script); + } + } + + public static void doDiff(String[] argv, Writer w) throws IOException { + String filea = argv[argv.length - 2]; + String fileb = argv[argv.length - 1]; + String[] a = slurp(filea); + String[] b = slurp(fileb); + Diff d = new Diff(a,b); + char style = 'n'; + for (int i = 0; i < argv.length - 2; ++i) { + String f = argv[i]; + if (f.startsWith("-")) { + for (int j = 1; j < f.length(); ++j) { + switch (f.charAt(j)) { + case 'e': // Ed style + style = 'e'; break; + case 'c': // Context diff + style = 'c'; break; + case 'u': + style = 'u'; break; + } + } + } + } + boolean reverse = style == 'e'; + Diff.change script = d.diff_2(reverse); + if (script == null) + w.write("No differences\n"); + else { + Base p; + switch (style) { + case 'e': + p = new EdPrint(a,b,w); break; + case 'c': + p = new ContextPrint(a,b,w); break; + case 'u': + p = new UnifiedPrint(a,b,w); break; + default: + p = new NormalPrint(a,b,w); + } + p.print_header(filea,fileb); + p.print_script(script); + } + } + +} diff --git a/src/partest-alternative/scala/tools/partest/io/JUnitReport.scala b/src/partest-alternative/scala/tools/partest/io/JUnitReport.scala new file mode 100644 index 0000000000..63ae200020 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/io/JUnitReport.scala @@ -0,0 +1,38 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + */ + +package scala.tools +package partest +package io + +/** This is disabled for the moment but I can fix it up if anyone + * is using it. + */ +class JUnitReport { + // create JUnit Report xml files if directory was specified + // def junitReport(dir: Directory) = { + // dir.mkdir() + // val report = testReport(set.kind, results, succs, fails) + // XML.save("%s/%s.xml".format(d.toAbsolute.path, set.kind), report) + // } + + // def oneResult(res: (TestEntity, Int)) = + // <testcase name={res._1.path}>{ + // res._2 match { + // case 0 => scala.xml.NodeSeq.Empty + // case 1 => <failure message="Test failed"/> + // case 2 => <failure message="Test timed out"/> + // } + // }</testcase> + // + // def testReport(kind: String, results: Iterable[(TestEntity, Int)], succs: Int, fails: Int) = { + // <testsuite name={kind} tests={(succs + fails).toString} failures={fails.toString}> + // <properties/> + // { + // results.map(oneResult(_)) + // } + // </testsuite> + // } + // +}
\ No newline at end of file diff --git a/src/partest-alternative/scala/tools/partest/io/Logging.scala b/src/partest-alternative/scala/tools/partest/io/Logging.scala new file mode 100644 index 0000000000..52239ffb2c --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/io/Logging.scala @@ -0,0 +1,137 @@ +package scala.tools +package partest +package io + +import java.io.{ StringWriter, PrintWriter, Writer } +import scala.tools.nsc.io._ +import scala.util.control.ControlThrowable + +trait Logging { + universe: Universe => + + class PartestANSIWriter extends ANSIWriter(Console.out) { + override def colorful: Int = ANSIWriter(universe.isAnsi) + private def printIf(cond: Boolean, msg: String) = + if (cond) { outline("debug: ") ; println(msg) } + + val verbose = printIf(isVerbose || isDebug, _: String) + val debug = printIf(isDebug, _: String) + } + + lazy val NestUI = new PartestANSIWriter() + + import NestUI.{ _outline, _success, _failure, _warning, _default } + + def markOutline(msg: String) = _outline + msg + _default + def markSuccess(msg: String) = _success + msg + _default + def markFailure(msg: String) = _failure + msg + _default + def markWarning(msg: String) = _warning + msg + _default + def markNormal(msg: String) = _default + msg + + def outline(msg: String) = NestUI outline msg + def success(msg: String) = NestUI success msg + def failure(msg: String) = NestUI failure msg + def warning(msg: String) = NestUI warning msg + def normal(msg: String) = NestUI normal msg + + def verbose(msg: String) = NestUI verbose msg + def debug(msg: String) = NestUI debug msg + + trait EntityLogging { + self: TestEntity => + + lazy val logWriter = new LogWriter(logFile) + + /** Redirect stdout and stderr to logFile, run body, return result. + */ + def loggingOutAndErr[T](body: => T): T = { + val log = logFile.printStream(append = true) + + try Console.withOut(log) { + Console.withErr(log) { + body + } + } + finally log.close() + } + + /** What to print in a failure summary. + */ + def failureMessage() = if (diffOutput != "") diffOutput else safeSlurp(logFile) + + /** For tracing. Outputs a line describing the next action. tracePath + * is a path wrapper which prints name or full path depending on verbosity. + */ + def trace(msg: String) = if (isTrace || isDryRun) System.err.println(">> [%s] %s".format(label, msg)) + + def tracePath(path: Path): String = if (isVerbose) path.path else path.name + def tracePath(path: String): String = tracePath(Path(path)) + + /** v == verbose. + */ + def vtrace(msg: String) = if (isVerbose) trace(msg) + + /** Run body, writes result to logFile. Any throwable is + * caught, stringified, and written to the log. + */ + def loggingResult(body: => String) = + try returning(true)(_ => logFile writeAll body) + catch { + case x: ControlThrowable => throw x + case x: InterruptedException => debug(this + " received interrupt, failing.\n") ; false + case x: Throwable => logException(x) + } + + def throwableToString(x: Throwable): String = { + val w = new StringWriter + x.printStackTrace(new PrintWriter(w)) + w.toString + } + + def warnAndLog(str: String) = { + warning(toStringTrunc(str, 800)) + logWriter append str + } + + def warnAndLogException(msg: String, ex: Throwable) = + warnAndLog(msg + throwableToString(ex)) + + def deleteLog(force: Boolean = false) = + if (universe.isNoCleanup && !force) debug("Not cleaning up " + logFile) + else logFile.deleteIfExists() + + def onException(x: Throwable) { logException(x) } + def logException(x: Throwable) = { + val msg = throwableToString(x) + if (!isTerse) + normal(msg) + + logWriter append msg + false + } + } + + /** A writer which doesn't create the file until a write comes in. + */ + class LazilyCreatedWriter(log: File) extends Writer { + @volatile private var isCreated = false + private lazy val underlying = { + isCreated = true + log.bufferedWriter() + } + + def flush() = if (isCreated) underlying.flush() + def close() = if (isCreated) underlying.close() + def write(chars: Array[Char], off: Int, len: Int) = { + underlying.write(chars, off, len) + underlying.flush() + } + } + + class LogWriter(log: File) extends PrintWriter(new LazilyCreatedWriter(log), true) { + override def print(s: String) = { + super.print(s) + flush() + } + } +}
\ No newline at end of file diff --git a/src/partest-alternative/scala/tools/partest/nest/StreamAppender.scala b/src/partest-alternative/scala/tools/partest/nest/StreamAppender.scala new file mode 100644 index 0000000000..8cebcf1685 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/nest/StreamAppender.scala @@ -0,0 +1,94 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + * @author Philipp Haller + */ + +// $Id$ + +package scala.tools.partest +package nest + +import java.io._ + +object StreamAppender { + def wrapIn(in: InputStream): BufferedReader = new BufferedReader(new InputStreamReader(in)) + def wrapIn(reader: Reader): BufferedReader = new BufferedReader(reader) + def wrapIn(str: String): BufferedReader = new BufferedReader(new StringReader(str)) + + def wrapOut(out: OutputStream): PrintWriter = new PrintWriter(new OutputStreamWriter(out), true) + def wrapOut(writer: Writer): PrintWriter = new PrintWriter(writer, true) + def wrapOut(): PrintWriter = wrapOut(new StringWriter) + + def apply(reader: BufferedReader, writer: Writer): StreamAppender = + new StreamAppender(reader, wrapOut(writer)) + + def apply(reader: Reader, writer: Writer): StreamAppender = + apply(wrapIn(reader), writer) + + def apply(in: InputStream, writer: Writer): StreamAppender = + apply(wrapIn(in), writer) + + def apply(str: String, writer: Writer): StreamAppender = + apply(wrapIn(str), writer) + + def apply(in: File, out: File): StreamAppender = + apply(new FileReader(in), new FileWriter(out)) + + def appendToString(in1: InputStream, in2: InputStream): String = { + val swriter1 = new StringWriter + val swriter2 = new StringWriter + val app1 = StreamAppender(wrapIn(in1), swriter1) + val app2 = StreamAppender(wrapIn(in2), swriter2) + + val async = new Thread(app2) + async.start() + app1.run() + async.join() + swriter1.toString + swriter2.toString + } +/* + private def inParallel(t1: Runnable, t2: Runnable, t3: Runnable) { + val thr1 = new Thread(t1) + val thr2 = new Thread(t2) + thr1.start() + thr2.start() + t3.run() + thr1.join() + thr2.join() + } +*/ + private def inParallel(t1: Runnable, t2: Runnable) { + val thr = new Thread(t2) + thr.start() + t1.run() + thr.join() + } + + def concat(in: InputStream, err: InputStream, out: OutputStream) = new Runnable { + override def run() { + val outWriter = wrapOut(out) + val inApp = StreamAppender(in, outWriter) + + val errStringWriter = new StringWriter + val errApp = StreamAppender(wrapIn(err), errStringWriter) + + inParallel(inApp, errApp) + + // append error string to out + StreamAppender(errStringWriter.toString, outWriter).run() + } + } +} + +class StreamAppender(reader: BufferedReader, writer: PrintWriter) extends Runnable { + override def run() = runAndMap(identity) + private def lines() = Iterator continually reader.readLine() takeWhile (_ != null) + def closeAll() = { + reader.close() + writer.close() + } + + def runAndMap(f: String => String) = + try lines() map f foreach (writer println _) + catch { case e: IOException => e.printStackTrace() } +} diff --git a/src/partest-alternative/scala/tools/partest/package.scala b/src/partest-alternative/scala/tools/partest/package.scala new file mode 100644 index 0000000000..f6d216e379 --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/package.scala @@ -0,0 +1,45 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + */ + +package scala.tools + +import nsc.io.{ File, Path, Process, Directory } +import java.nio.charset.CharacterCodingException + +package object partest { + /** The CharacterCodingExceptions are thrown at least on windows trying + * to read a file like script/utf-8.scala + */ + private[partest] def safeSlurp(f: File) = + try if (f.exists) f.slurp() else "" + catch { case _: CharacterCodingException => "" } + + private[partest] def safeLines(f: File) = safeSlurp(f) split """\r\n|\r|\n""" toList + private[partest] def safeArgs(f: File) = toArgs(safeSlurp(f)) + private[partest] def isJava(f: Path) = f.isFile && (f hasExtension "java") + private[partest] def isScala(f: Path) = f.isFile && (f hasExtension "scala") + private[partest] def isJavaOrScala(f: Path) = isJava(f) || isScala(f) + + private[partest] def toArgs(line: String) = cmd toArgs line + private[partest] def fromArgs(args: List[String]) = cmd fromArgs args + + /** Strings, argument lists, etc. */ + + private[partest] def fromAnyArgs(args: List[Any]) = args mkString " " // separate to avoid accidents + private[partest] def toStringTrunc(x: Any, max: Int = 240) = { + val s = x.toString + if (s.length < max) s + else (s take max) + " [...]" + } + private[partest] def setProp(k: String, v: String) = scala.util.Properties.setProp(k, v) + + /** Pretty self explanatory. */ + def printAndExit(msg: String): Unit = { + println(msg) + exit(1) + } + + /** Apply a function and return the passed value */ + def returning[T](x: T)(f: T => Unit): T = { f(x) ; x } +}
\ No newline at end of file diff --git a/src/partest-alternative/scala/tools/partest/util/package.scala b/src/partest-alternative/scala/tools/partest/util/package.scala new file mode 100644 index 0000000000..bc5470ba5d --- /dev/null +++ b/src/partest-alternative/scala/tools/partest/util/package.scala @@ -0,0 +1,61 @@ +/* NEST (New Scala Test) + * Copyright 2007-2010 LAMP/EPFL + */ + +package scala.tools +package partest + +import java.util.{ Timer, TimerTask } +import java.io.StringWriter +import nsc.io._ + +/** Misc code still looking for a good home. + */ +package object util { + + def allPropertiesString() = javaHashtableToString(System.getProperties) + + private def javaHashtableToString(table: java.util.Hashtable[_,_]) = { + import collection.JavaConversions._ + (table.toList map { case (k, v) => "%s -> %s\n".format(k, v) }).sorted mkString + } + + def filesToSet(pre: String, fs: List[String]): Set[AbstractFile] = + fs flatMap (x => Option(AbstractFile getFile (Path(pre) / x).path)) toSet + + /** Copies one Path to another Path, trying to be sensible when one or the + * other is a Directory. Returns true if it believes it succeeded. + */ + def copyPath(from: Path, to: Path): Boolean = { + if (!to.parent.isDirectory) + to.parent.createDirectory(force = true) + + def copyDir = { + val sub = to / from.name createDirectory true + from.toDirectory.list forall (x => copyPath(x, sub)) + } + (from.isDirectory, to.isDirectory) match { + case (true, true) => copyDir + case (true, false) => false + case (false, true) => from.toFile copyTo (to / from.name) + case (false, false) => from.toFile copyTo to + } + } + + /** + * Compares two files using a Java implementation of the GNU diff + * available at http://www.bmsi.com/java/#diff. + * + * @param f1 the first file to be compared + * @param f2 the second file to be compared + * @return the text difference between the compared files + */ + def diffFiles(f1: File, f2: File): String = { + val diffWriter = new StringWriter + val args = Array(f1.toAbsolute.path, f2.toAbsolute.path) + + io.DiffPrint.doDiff(args, diffWriter) + val result = diffWriter.toString + if (result == "No differences") "" else result + } +} |