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
@@ -157,8 +163,6 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
157163 if (javaopts.nonEmpty)
158164 nestUI.verbose(s " Found javaopts file ' $argsFile', using options: ' ${javaopts.mkString(" ," )}' " )
159165
160- val testFullPath = testFile.getAbsolutePath
161-
162166 // Note! As this currently functions, suiteRunner.javaOpts must precede argString
163167 // because when an option is repeated to java only the last one wins.
164168 // That means until now all the .javaopts files were being ignored because
@@ -167,30 +171,15 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
167171 //
168172 // debug: Found javaopts file 'files/shootout/message.scala-2.javaopts', using options: '-Xss32k'
169173 // 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
174+ val propertyOpts = propertyOptions(fork = true ).map { case (k, v) => s " -D $k= $v" }
186175
187176 val classpath = joinPaths(extraClasspath ++ testClassPath)
188177
189178 javaCmdPath +: (
190179 (suiteRunner.javaOpts.split(' ' ) ++ extraJavaOptions ++ javaopts).filter(_ != " " ).toList ++ Seq (
191180 " -classpath" ,
192181 join(outDir.toString, classpath)
193- ) ++ propertyOptions ++ Seq (
182+ ) ++ propertyOpts ++ Seq (
194183 " scala.tools.nsc.MainGenericRunner" ,
195184 " -usejavacp" ,
196185 " Test" ,
@@ -199,6 +188,40 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
199188 )
200189 }
201190
191+ def propertyOptions (fork : Boolean ): List [(String , String )] = {
192+ val testFullPath = testFile.getAbsolutePath
193+ val extras = if (nestUI.debug) List (" partest.debug" -> " true" ) else Nil
194+ val immutablePropsToCheck = List [(String , String )](
195+ " file.encoding" -> " UTF-8" ,
196+ " user.language" -> " en" ,
197+ " user.country" -> " US"
198+ )
199+ val immutablePropsForkOnly = List [(String , String )](
200+ " java.library.path" -> logFile.getParentFile.getAbsolutePath,
201+ )
202+ val shared = List (
203+ " partest.output" -> (" " + outDir.getAbsolutePath),
204+ " partest.lib" -> (" " + libraryUnderTest.jfile.getAbsolutePath),
205+ " partest.reflect" -> (" " + reflectUnderTest.jfile.getAbsolutePath),
206+ " partest.comp" -> (" " + compilerUnderTest.jfile.getAbsolutePath),
207+ " partest.cwd" -> (" " + outDir.getParent),
208+ " partest.test-path" -> (" " + testFullPath),
209+ " partest.testname" -> (" " + fileBase),
210+ " javacmd" -> (" " + javaCmdPath),
211+ " javaccmd" -> (" " + javacCmdPath),
212+ ) ++ extras
213+ if (fork) {
214+ immutablePropsToCheck ++ immutablePropsForkOnly ++ shared
215+ } else {
216+ for ((k, requiredValue) <- immutablePropsToCheck) {
217+ val actual = System .getProperty(k)
218+ 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" )
219+ }
220+ shared
221+ }
222+ }
223+
224+
202225 /** Runs command redirecting standard out and
203226 * error out to output file.
204227 */
@@ -235,6 +258,53 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
235258 }
236259 }
237260
261+ def execTestInProcess (classesDir : File , log : File ): Boolean = {
262+ stopwatch.pause()
263+ suiteRunner.synchronized {
264+ stopwatch.start()
265+ def run (): Unit = {
266+ StreamCapture .withExtraProperties(propertyOptions(fork = false ).toMap) {
267+ try {
268+ val out = Files .newOutputStream(log.toPath, StandardOpenOption .APPEND )
269+ try {
270+ val loader = new URLClassLoader (classesDir.toURI.toURL :: Nil , getClass.getClassLoader)
271+ StreamCapture .capturingOutErr(out) {
272+ val cls = loader.loadClass(" Test" )
273+ val main = cls.getDeclaredMethod(" main" , classOf [Array [String ]])
274+ try {
275+ main.invoke(null , Array [String ](" jvm" ))
276+ } catch {
277+ case ite : InvocationTargetException => throw ite.getCause
278+ }
279+ }
280+ } finally {
281+ out.close()
282+ }
283+ } catch {
284+ case t : ControlThrowable => throw t
285+ case t : Throwable =>
286+ // We'll let the checkfile diffing report this failure
287+ Files .write(log.toPath, stackTraceString(t).getBytes(Charset .defaultCharset()), StandardOpenOption .APPEND )
288+ }
289+ }
290+ }
291+
292+ pushTranscript(s " <in process execution of $testIdent> > ${logFile.getName}" )
293+
294+ TrapExit (() => run()) match {
295+ case Left ((status, throwable)) if status != 0 =>
296+ // Files.readAllLines(log.toPath).forEach(println(_))
297+ // val error = new AssertionError(s"System.exit(${status}) was called.")
298+ // error.setStackTrace(throwable.getStackTrace)
299+ setLastState(genFail(" non-zero exit code" ))
300+ false
301+ case _ =>
302+ setLastState(genPass())
303+ true
304+ }
305+ }
306+ }
307+
238308 override def toString = s """ Test( $testIdent, lastState = $lastState) """
239309
240310 // result is unused
@@ -641,9 +711,10 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
641711 (diffIsOk, LogContext (logFile, swr, wr))
642712 }
643713
644- def run (): TestState = {
714+ def run (): ( TestState , Long ) = {
645715 // javac runner, for one, would merely append to an existing log file, so just delete it before we start
646716 logFile.delete()
717+ stopwatch.start()
647718
648719 if (kind == " neg" || (kind endsWith " -neg" )) runNegTest()
649720 else kind match {
@@ -652,10 +723,18 @@ class Runner(val testFile: File, val suiteRunner: SuiteRunner, val nestUI: NestU
652723 case " res" => runResidentTest()
653724 case " scalap" => runScalapTest()
654725 case " script" => runScriptTest()
655- case _ => runTestCommon(execTest(outDir, logFile) && diffIsOk )
726+ case _ => runRunTest( )
656727 }
657728
658- lastState
729+ (lastState, stopwatch.stop)
730+ }
731+
732+ private def runRunTest (): Unit = {
733+ val argsFile = testFile changeExtension " javaopts"
734+ val javaopts = readOptionsFile(argsFile)
735+ val execInProcess = PartestDefaults .execInProcess && javaopts.isEmpty && ! Set (" specialized" , " instrumented" ).contains(testFile.getParentFile.getName)
736+ def exec () = if (execInProcess) execTestInProcess(outDir, logFile) else execTest(outDir, logFile)
737+ runTestCommon(exec() && diffIsOk)
659738 }
660739
661740 private def decompileClass (clazz : Class [_], isPackageObject : Boolean ): String = {
@@ -738,6 +817,8 @@ class SuiteRunner(
738817 // TODO: make this immutable
739818 PathSettings .testSourcePath = testSourcePath
740819
820+ val durations = collection.concurrent.TrieMap [File , Long ]()
821+
741822 def banner = {
742823 val baseDir = fileManager.compilerUnderTest.parent.toString
743824 def relativize (path : String ) = path.replace(baseDir, s " $$ baseDir " ).replace(PathSettings .srcDir.toString, " $sourceDir" )
@@ -759,29 +840,35 @@ class SuiteRunner(
759840 // |Java Classpath: ${sys.props("java.class.path")}
760841 }
761842
762- def onFinishTest (testFile : File , result : TestState , durationMs : Long ): TestState = result
843+ def onFinishTest (testFile : File , result : TestState , durationMs : Long ): TestState = {
844+ durations(testFile) = durationMs
845+ result
846+ }
763847
764848 def runTest (testFile : File ): TestState = {
765849 val start = System .nanoTime()
766850 val runner = new Runner (testFile, this , nestUI)
851+ var stopwatchDuration : Option [Long ] = None
767852
768853 // when option "--failed" is provided execute test only if log
769854 // is present (which means it failed before)
770855 val state =
771856 if (failed && ! runner.logFile.canRead)
772857 runner.genPass()
773858 else {
774- val (state, _ ) =
775- try timed( runner.run() )
859+ val (state, durationMs ) =
860+ try runner.run()
776861 catch {
777862 case t : Throwable => throw new RuntimeException (s " Error running $testFile" , t)
778863 }
779- nestUI.reportTest(state, runner)
864+ stopwatchDuration = Some (durationMs)
865+ nestUI.reportTest(state, runner, durationMs)
780866 runner.cleanup()
781867 state
782868 }
783869 val end = System .nanoTime()
784- onFinishTest(testFile, state, TimeUnit .NANOSECONDS .toMillis(end - start))
870+ val durationMs = stopwatchDuration.getOrElse(TimeUnit .NANOSECONDS .toMillis(end - start))
871+ onFinishTest(testFile, state, durationMs)
785872 }
786873
787874 def runTestsForFiles (kindFiles : Array [File ], kind : String ): Array [TestState ] = {
0 commit comments