55package scala .tools .partest
66package 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 }
912import java .util .concurrent .Executors
1013import java .util .concurrent .TimeUnit
1114import java .util .concurrent .TimeUnit .NANOSECONDS
15+
1216import scala .collection .mutable .ListBuffer
1317import scala .concurrent .duration .Duration
1418import scala .reflect .internal .FatalError
1519import 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 }
1923import scala .tools .nsc .reporters .ConsoleReporter
2024import scala .tools .nsc .util .stackTraceString
21- import scala .util .{ Try , Success , Failure }
25+ import scala .util .{Failure , Success , Try }
2226import 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
2732trait TestInfo {
2833 /** pos/t1234 */
@@ -53,6 +58,7 @@ trait TestInfo {
5358
5459/** Run a single test. Rubber meets road. */
5560class 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 ] = {
0 commit comments