aboutsummaryrefslogtreecommitdiff
path: root/compiler/test/dotty/tools/dotc/vulpix/RunnerOrchestration.scala
blob: 8a5d16b83959724516096d89d3a79bf8d2d9dd39 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package dotty
package tools
package dotc
package vulpix

import java.io.{
  File => JFile,
  InputStream, ObjectInputStream,
  OutputStream, ObjectOutputStream
}
import java.util.concurrent.TimeoutException

import scala.concurrent.duration.Duration
import scala.concurrent.{ Await, Future }
import scala.concurrent.ExecutionContext.Implicits.global
import scala.collection.mutable

trait RunnerOrchestration {

  /** The maximum amount of active runners, which contain a child JVM */
  val numberOfSlaves: Int

  /** The maximum duration the child process is allowed to consume before
   *  getting destroyed
   */
  val maxDuration: Duration

  /** Destroy and respawn process after each test */
  val safeMode: Boolean

  /** Running a `Test` class's main method from the specified `dir` */
  def runMain(dir: JFile): Status = monitor.runMain(dir)

  private[this] val monitor = new RunnerMonitor

  private class RunnerMonitor {

    def runMain(dir: JFile): Status = withRunner(_.runMain(dir))

    private class Runner(private var process: Process) {
      private[this] val ois = new ObjectInputStream(process.getInputStream)
      private[this] val oos = new ObjectOutputStream(process.getOutputStream)

      def kill(): Unit = {
        if (process ne null) process.destroy()
        process = null
      }

      def runMain(dir: JFile): Status = {
        assert(process ne null,
          "Runner was killed and then reused without setting a new process")

        // Makes the encapsulating RunnerMonitor spawn a new runner
        def respawn(): Unit = {
          process.destroy()
          process = createProcess
        }

        // pass file to running process
        oos.writeObject(dir)

        // Create a future reading the object:
        val readObject = Future(ois.readObject().asInstanceOf[Status])

        // Await result for `maxDuration` and then timout and destroy the
        // process:
        val status =
          try Await.result(readObject, maxDuration)
          catch { case _: TimeoutException => { Timeout } }

        // Handle failure of the VM:
        status match {
          case _ if safeMode => respawn()
          case status: Failure => respawn()
          case Timeout => respawn()
          case _ => ()
        }

        // return run status:
        status
      }
    }

    private def createProcess: Process = ???

    private[this] val allRunners =
      List.fill(numberOfSlaves)(new Runner(createProcess))

    private[this] val freeRunners = mutable.Queue(allRunners: _*)
    private[this] val busyRunners = mutable.Set.empty[Runner]

    private def getRunner(): Runner = synchronized {
      while (freeRunners.isEmpty) wait()

      val runner = freeRunners.dequeue()
      busyRunners += runner

      notify()
      runner
    }

    private def freeRunner(runner: Runner): Unit = synchronized {
      freeRunners.enqueue(runner)
      busyRunners -= runner
      notify()
    }

    private def withRunner[T](op: Runner => T): T = {
      val runner = getRunner()
      val result = op(runner)
      freeRunner(runner)
      result
    }

    private def killAll(): Unit = allRunners.foreach(_.kill())

    // On shutdown, we need to kill all runners:
    sys.addShutdownHook(killAll())
    // If for some reason the test runner (i.e. sbt) doesn't kill the VM, we
    // need to clean up ourselves.
    SummaryReport.addCleanup(killAll)
  }
}