Skip to content
This repository was archived by the owner on Sep 8, 2022. It is now read-only.

Commit fbeb205

Browse files
authored
Merge pull request #99 from retronym/ticket/75
Add a mode to reuse JVM for test execution
2 parents d8c59ed + 09d2dcc commit fbeb205

File tree

7 files changed

+247
-72
lines changed

7 files changed

+247
-72
lines changed

src/main/scala/scala/tools/partest/PartestDefaults.scala

+11-1
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,18 @@ object PartestDefaults {
1515

1616
def testBuild = prop("partest.build")
1717
def errorCount = prop("partest.errors") map (_.toInt) getOrElse 0
18-
def numThreads = prop("partest.threads") map (_.toInt) getOrElse runtime.availableProcessors
18+
def numThreads = math.max(1, prop("partest.threads") map (_.toInt) getOrElse runtime.availableProcessors)
19+
def execInProcess: Boolean = {
20+
val prop = java.lang.Boolean.getBoolean("partest.exec.in.process")
21+
if (prop && numThreads > 1) warningMessage
22+
prop
23+
}
24+
private lazy val warningMessage: Unit = {
25+
println("Note: test execution will be non-parallel under -Dpartest.exec.in.process")
26+
}
27+
1928
def waitTime = Duration(prop("partest.timeout") getOrElse "4 hours")
29+
def printDurationThreshold = java.lang.Integer.getInteger("partest.print.duration.threshold.ms", 5000)
2030

2131
//def timeout = "1200000" // per-test timeout
2232

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package scala.tools.partest.nest
2+
3+
import java.io.FileDescriptor
4+
import java.net.InetAddress
5+
import java.security.Permission
6+
7+
class DelegatingSecurityManager(delegate: SecurityManager) extends SecurityManager {
8+
override def checkExit(status: Int): Unit = if (delegate ne null) delegate.checkExit(status)
9+
override def checkPermission(perm: Permission): Unit = if (delegate ne null) delegate.checkPermission(perm)
10+
override def checkPermission(perm: Permission, context: AnyRef): Unit = if (delegate ne null) delegate.checkPermission(perm, context)
11+
override def checkExec(cmd: String): Unit = if (delegate ne null) delegate.checkExec(cmd)
12+
override def checkWrite(file: String): Unit = if (delegate ne null) delegate.checkWrite(file)
13+
override def checkDelete(file: String): Unit = if (delegate ne null) delegate.checkDelete(file)
14+
override def checkRead(file: String): Unit = if (delegate ne null) delegate.checkRead(file)
15+
override def checkRead(file: String, context: scala.Any): Unit = if (delegate ne null) delegate.checkRead(file, context)
16+
override def checkPropertyAccess(key: String): Unit = if (delegate ne null) delegate.checkPropertyAccess(key)
17+
override def checkAccept(host: String, port: Int): Unit = if (delegate ne null) delegate.checkAccept(host, port)
18+
override def checkWrite(fd: FileDescriptor): Unit = if (delegate ne null) delegate.checkWrite(fd)
19+
override def checkPrintJobAccess(): Unit = if (delegate ne null) delegate.checkPrintJobAccess()
20+
override def checkMulticast(maddr: InetAddress): Unit = if (delegate ne null) delegate.checkMulticast(maddr)
21+
override def checkSetFactory(): Unit = if (delegate ne null) delegate.checkSetFactory()
22+
override def checkLink(lib: String): Unit = if (delegate ne null) delegate.checkLink(lib)
23+
override def checkSecurityAccess(target: String): Unit = if (delegate ne null) delegate.checkSecurityAccess(target)
24+
override def checkListen(port: Int): Unit = if (delegate ne null) delegate.checkListen(port)
25+
override def checkAccess(t: Thread): Unit = if (delegate ne null) delegate.checkAccess(t)
26+
override def checkAccess(g: ThreadGroup): Unit = if (delegate ne null) delegate.checkAccess(g)
27+
override def checkCreateClassLoader(): Unit = if (delegate ne null) delegate.checkCreateClassLoader()
28+
override def checkPackageDefinition(pkg: String): Unit = if (delegate ne null) delegate.checkPackageDefinition(pkg)
29+
override def checkConnect(host: String, port: Int): Unit = if (delegate ne null) delegate.checkConnect(host, port)
30+
override def checkConnect(host: String, port: Int, context: scala.Any): Unit = if (delegate ne null) delegate.checkConnect(host, port, context)
31+
override def checkPackageAccess(pkg: String): Unit = if (delegate ne null) delegate.checkPackageAccess(pkg)
32+
override def checkPropertiesAccess(): Unit = if (delegate ne null) delegate.checkPropertiesAccess()
33+
override def checkRead(fd: FileDescriptor): Unit = if (delegate ne null) delegate.checkRead(fd)
34+
}

src/main/scala/scala/tools/partest/nest/NestUI.scala

+5-4
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class NestUI(val verbose: Boolean = false, val debug: Boolean = false, val terse
5252
}
5353
}
5454

55-
def statusLine(state: TestState) = {
55+
def statusLine(state: TestState, durationMs: Long) = {
5656
import state._
5757
import TestState._
5858
val colorizer = state match {
@@ -62,10 +62,11 @@ class NestUI(val verbose: Boolean = false, val debug: Boolean = false, val terse
6262
case _ => red
6363
}
6464
val word = bold(colorizer(state.shortStatus))
65-
f"$word $testNumber - $testIdent%-40s$reasonString"
65+
def durationString = if (durationMs > PartestDefaults.printDurationThreshold) f"[duration ${(1.0 * durationMs) / 1000}%.2fs]" else ""
66+
f"$word $testNumber - $testIdent%-40s$reasonString$durationString"
6667
}
6768

68-
def reportTest(state: TestState, info: TestInfo): Unit = {
69+
def reportTest(state: TestState, info: TestInfo, durationMs: Long): Unit = {
6970
if (terse && state.isOk) {
7071
if (dotCount >= DotWidth) {
7172
outline("\n.")
@@ -75,7 +76,7 @@ class NestUI(val verbose: Boolean = false, val debug: Boolean = false, val terse
7576
dotCount += 1
7677
}
7778
} else {
78-
echo(statusLine(state))
79+
echo(statusLine(state, durationMs))
7980
if (!state.isOk) {
8081
def showLog() = if (info.logFile.canRead) {
8182
echo(bold(cyan(s"##### Log file '${info.logFile}' from failed test #####\n")))

src/main/scala/scala/tools/partest/nest/Runner.scala

+120-38
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,29 @@
55
package scala.tools.partest
66
package nest
77

8-
import java.io.{ Console => _, _ }
8+
import java.io.{Console => _, _}
9+
import java.lang.reflect.InvocationTargetException
10+
import java.nio.charset.Charset
11+
import java.nio.file.{Files, StandardOpenOption}
912
import java.util.concurrent.Executors
1013
import java.util.concurrent.TimeUnit
1114
import java.util.concurrent.TimeUnit.NANOSECONDS
15+
1216
import scala.collection.mutable.ListBuffer
1317
import scala.concurrent.duration.Duration
1418
import scala.reflect.internal.FatalError
1519
import scala.reflect.internal.util.ScalaClassLoader
16-
import scala.sys.process.{ Process, ProcessLogger }
17-
import scala.tools.nsc.Properties.{ envOrNone, isWin, javaHome, propOrEmpty, versionMsg, javaVmName, javaVmVersion, javaVmInfo }
18-
import scala.tools.nsc.{ Settings, CompilerCommand, Global }
20+
import scala.sys.process.{Process, ProcessLogger}
21+
import scala.tools.nsc.Properties.{envOrNone, isWin, javaHome, javaVmInfo, javaVmName, javaVmVersion, propOrEmpty, versionMsg}
22+
import scala.tools.nsc.{CompilerCommand, Global, Settings}
1923
import scala.tools.nsc.reporters.ConsoleReporter
2024
import scala.tools.nsc.util.stackTraceString
21-
import scala.util.{ Try, Success, Failure }
25+
import scala.util.{Failure, Success, Try}
2226
import ClassPath.join
23-
import TestState.{ Pass, Fail, Crash, Uninitialized, Updated }
24-
25-
import FileManager.{ compareContents, joinPaths, withTempFile }
27+
import TestState.{Crash, Fail, Pass, Uninitialized, Updated}
28+
import FileManager.{compareContents, joinPaths, withTempFile}
29+
import scala.reflect.internal.util.ScalaClassLoader.URLClassLoader
30+
import scala.util.control.ControlThrowable
2631

2732
trait TestInfo {
2833
/** pos/t1234 */
@@ -53,6 +58,7 @@ trait TestInfo {
5358

5459
/** Run a single test. Rubber meets road. */
5560
class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestUI) extends TestInfo {
61+
private val stopwatch = new Stopwatch()
5662

5763
import suiteRunner.{fileManager => fm, _}
5864
val fileManager = fm
@@ -125,9 +131,7 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
125131
)
126132

127133
pushTranscript(args mkString " ")
128-
val captured = StreamCapture(runCommand(args, logFile))
129-
if (captured.result) genPass() else {
130-
logFile appendAll captured.stderr
134+
if (runCommand(args, logFile)) genPass() else {
131135
genFail("java compilation failed")
132136
}
133137
}
@@ -157,8 +161,6 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
157161
if (javaopts.nonEmpty)
158162
nestUI.verbose(s"Found javaopts file '$argsFile', using options: '${javaopts.mkString(",")}'")
159163

160-
val testFullPath = testFile.getAbsolutePath
161-
162164
// Note! As this currently functions, suiteRunner.javaOpts must precede argString
163165
// because when an option is repeated to java only the last one wins.
164166
// That means until now all the .javaopts files were being ignored because
@@ -167,30 +169,15 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
167169
//
168170
// debug: Found javaopts file 'files/shootout/message.scala-2.javaopts', using options: '-Xss32k'
169171
// debug: java -Xss32k -Xss2m -Xms256M -Xmx1024M -classpath [...]
170-
val extras = if (nestUI.debug) List("-Dpartest.debug=true") else Nil
171-
val propertyOptions = List(
172-
"-Dfile.encoding=UTF-8",
173-
"-Djava.library.path="+logFile.getParentFile.getAbsolutePath,
174-
"-Dpartest.output="+outDir.getAbsolutePath,
175-
"-Dpartest.lib="+libraryUnderTest.getAbsolutePath,
176-
"-Dpartest.reflect="+reflectUnderTest.getAbsolutePath,
177-
"-Dpartest.comp="+compilerUnderTest.getAbsolutePath,
178-
"-Dpartest.cwd="+outDir.getParent,
179-
"-Dpartest.test-path="+testFullPath,
180-
"-Dpartest.testname="+fileBase,
181-
"-Djavacmd="+javaCmdPath,
182-
"-Djavaccmd="+javacCmdPath,
183-
"-Duser.language=en",
184-
"-Duser.country=US"
185-
) ++ extras
172+
val propertyOpts = propertyOptions(fork = true).map { case (k, v) => s"-D$k=$v" }
186173

187174
val classpath = joinPaths(extraClasspath ++ testClassPath)
188175

189176
javaCmdPath +: (
190177
(suiteRunner.javaOpts.split(' ') ++ extraJavaOptions ++ javaopts).filter(_ != "").toList ++ Seq(
191178
"-classpath",
192179
join(outDir.toString, classpath)
193-
) ++ propertyOptions ++ Seq(
180+
) ++ propertyOpts ++ Seq(
194181
"scala.tools.nsc.MainGenericRunner",
195182
"-usejavacp",
196183
"Test",
@@ -199,6 +186,40 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
199186
)
200187
}
201188

189+
def propertyOptions(fork: Boolean): List[(String, String)] = {
190+
val testFullPath = testFile.getAbsolutePath
191+
val extras = if (nestUI.debug) List("partest.debug" -> "true") else Nil
192+
val immutablePropsToCheck = List[(String, String)](
193+
"file.encoding" -> "UTF-8",
194+
"user.language" -> "en",
195+
"user.country" -> "US"
196+
)
197+
val immutablePropsForkOnly = List[(String, String)](
198+
"java.library.path" -> logFile.getParentFile.getAbsolutePath,
199+
)
200+
val shared = List(
201+
"partest.output" -> ("" + outDir.getAbsolutePath),
202+
"partest.lib" -> ("" + libraryUnderTest.jfile.getAbsolutePath),
203+
"partest.reflect" -> ("" + reflectUnderTest.jfile.getAbsolutePath),
204+
"partest.comp" -> ("" + compilerUnderTest.jfile.getAbsolutePath),
205+
"partest.cwd" -> ("" + outDir.getParent),
206+
"partest.test-path" -> ("" + testFullPath),
207+
"partest.testname" -> ("" + fileBase),
208+
"javacmd" -> ("" + javaCmdPath),
209+
"javaccmd" -> ("" + javacCmdPath),
210+
) ++ extras
211+
if (fork) {
212+
immutablePropsToCheck ++ immutablePropsForkOnly ++ shared
213+
} else {
214+
for ((k, requiredValue) <- immutablePropsToCheck) {
215+
val actual = System.getProperty(k)
216+
assert(actual == requiredValue, s"Unable to run test without forking as the current JVM has an incorrect system property. For $k, found $actual, required $requiredValue")
217+
}
218+
shared
219+
}
220+
}
221+
222+
202223
/** Runs command redirecting standard out and
203224
* error out to output file.
204225
*/
@@ -235,6 +256,50 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
235256
}
236257
}
237258

259+
def execTestInProcess(classesDir: File, log: File): Boolean = {
260+
stopwatch.pause()
261+
suiteRunner.synchronized {
262+
stopwatch.start()
263+
def run(): Unit = {
264+
StreamCapture.withExtraProperties(propertyOptions(fork = false).toMap) {
265+
try {
266+
val out = Files.newOutputStream(log.toPath, StandardOpenOption.APPEND)
267+
try {
268+
val loader = new URLClassLoader(classesDir.toURI.toURL :: Nil, getClass.getClassLoader)
269+
StreamCapture.capturingOutErr(out) {
270+
val cls = loader.loadClass("Test")
271+
val main = cls.getDeclaredMethod("main", classOf[Array[String]])
272+
try {
273+
main.invoke(null, Array[String]("jvm"))
274+
} catch {
275+
case ite: InvocationTargetException => throw ite.getCause
276+
}
277+
}
278+
} finally {
279+
out.close()
280+
}
281+
} catch {
282+
case t: ControlThrowable => throw t
283+
case t: Throwable =>
284+
// We'll let the checkfile diffing report this failure
285+
Files.write(log.toPath, stackTraceString(t).getBytes(Charset.defaultCharset()), StandardOpenOption.APPEND)
286+
}
287+
}
288+
}
289+
290+
pushTranscript(s"<in process execution of $testIdent> > ${logFile.getName}")
291+
292+
TrapExit(() => run()) match {
293+
case Left((status, throwable)) if status != 0 =>
294+
setLastState(genFail("non-zero exit code"))
295+
false
296+
case _ =>
297+
setLastState(genPass())
298+
true
299+
}
300+
}
301+
}
302+
238303
override def toString = s"""Test($testIdent, lastState = $lastState)"""
239304

240305
// result is unused
@@ -641,9 +706,10 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
641706
(diffIsOk, LogContext(logFile, swr, wr))
642707
}
643708

644-
def run(): TestState = {
709+
def run(): (TestState, Long) = {
645710
// javac runner, for one, would merely append to an existing log file, so just delete it before we start
646711
logFile.delete()
712+
stopwatch.start()
647713

648714
if (kind == "neg" || (kind endsWith "-neg")) runNegTest()
649715
else kind match {
@@ -652,10 +718,18 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
652718
case "res" => runResidentTest()
653719
case "scalap" => runScalapTest()
654720
case "script" => runScriptTest()
655-
case _ => runTestCommon(execTest(outDir, logFile) && diffIsOk)
721+
case _ => runRunTest()
656722
}
657723

658-
lastState
724+
(lastState, stopwatch.stop)
725+
}
726+
727+
private def runRunTest(): Unit = {
728+
val argsFile = testFile changeExtension "javaopts"
729+
val javaopts = readOptionsFile(argsFile)
730+
val execInProcess = PartestDefaults.execInProcess && javaopts.isEmpty && !Set("specialized", "instrumented").contains(testFile.getParentFile.getName)
731+
def exec() = if (execInProcess) execTestInProcess(outDir, logFile) else execTest(outDir, logFile)
732+
runTestCommon(exec() && diffIsOk)
659733
}
660734

661735
private def decompileClass(clazz: Class[_], isPackageObject: Boolean): String = {
@@ -738,6 +812,8 @@ class SuiteRunner(
738812
// TODO: make this immutable
739813
PathSettings.testSourcePath = testSourcePath
740814

815+
val durations = collection.concurrent.TrieMap[File, Long]()
816+
741817
def banner = {
742818
val baseDir = fileManager.compilerUnderTest.parent.toString
743819
def relativize(path: String) = path.replace(baseDir, s"$$baseDir").replace(PathSettings.srcDir.toString, "$sourceDir")
@@ -759,29 +835,35 @@ class SuiteRunner(
759835
// |Java Classpath: ${sys.props("java.class.path")}
760836
}
761837

762-
def onFinishTest(testFile: File, result: TestState, durationMs: Long): TestState = result
838+
def onFinishTest(testFile: File, result: TestState, durationMs: Long): TestState = {
839+
durations(testFile) = durationMs
840+
result
841+
}
763842

764843
def runTest(testFile: File): TestState = {
765844
val start = System.nanoTime()
766845
val runner = new Runner(testFile, this, nestUI)
846+
var stopwatchDuration: Option[Long] = None
767847

768848
// when option "--failed" is provided execute test only if log
769849
// is present (which means it failed before)
770850
val state =
771851
if (failed && !runner.logFile.canRead)
772852
runner.genPass()
773853
else {
774-
val (state, _) =
775-
try timed(runner.run())
854+
val (state, durationMs) =
855+
try runner.run()
776856
catch {
777857
case t: Throwable => throw new RuntimeException(s"Error running $testFile", t)
778858
}
779-
nestUI.reportTest(state, runner)
859+
stopwatchDuration = Some(durationMs)
860+
nestUI.reportTest(state, runner, durationMs)
780861
runner.cleanup()
781862
state
782863
}
783864
val end = System.nanoTime()
784-
onFinishTest(testFile, state, TimeUnit.NANOSECONDS.toMillis(end - start))
865+
val durationMs = stopwatchDuration.getOrElse(TimeUnit.NANOSECONDS.toMillis(end - start))
866+
onFinishTest(testFile, state, durationMs)
785867
}
786868

787869
def runTestsForFiles(kindFiles: Array[File], kind: String): Array[TestState] = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package scala.tools.partest.nest
2+
3+
/**
4+
* Measured elapsed time between between calls to `start` and `stop`.
5+
* May be `pause`-ed and re-`started` before `stop` is eventually called.
6+
*/
7+
final class Stopwatch {
8+
private var base: Option[Long] = None
9+
private var elapsed = 0L
10+
def pause(): Unit = {
11+
assert(base.isDefined)
12+
val now = System.nanoTime
13+
elapsed += (now - base.get)
14+
base = None
15+
}
16+
def start(): Unit = {
17+
base = Some(System.nanoTime())
18+
}
19+
20+
def stop(): Long = {
21+
pause()
22+
(1.0 * elapsed / 1000 / 1000).toLong
23+
}
24+
}

0 commit comments

Comments
 (0)