aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/scala/org/perl8/test/ExternalTest.scala
blob: 7855aa82ec4670688bbce5547ff25e0301a419f4 (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
package com.iinteractive.test

import scala.concurrent.Await
import scala.concurrent.duration.Duration
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.concurrent.Future._
import scala.annotation.tailrec

/** Runs an external process which emits TAP, and parses it as a test.
  *
  * This test class can be used if you have existing tests that you would like
  * to be able to include in a test suite using this framework. You just need
  * to write a test class for each external test that looks like this:
  *
  * {{{
  * class MyTest1 extends ExternalTest("perl", "t/basic.t")
  * }}}
  *
  * This will run your external process, and use its TAP stream as its output.
  * This will allow it to, for instance, be a part of the test suite that runs
  * via `sbt test`. As with any other test class, its stdout and stderr will
  * be sent to `Console.out` and `Console.err`, where they can be overridden
  * as needed.
  */
class ExternalTest (cmdLine: String*) extends Test {
  protected def runTests (raw: Boolean): Int = {
    val processBuilder = new ProcessBuilder(cmdLine: _*)

    // Ensure that if stdout and stderr are both pointing to the same place (a
    // terminal or file or something) that they remain synchronized. This is
    // only possible if the endpoint they are streaming to is the same place
    // underneath, with no extra processing in between. (This is safe because
    // stdout is typically line-buffered, and we only ever output a line at a
    // time when writing TAP, so in theory, buffering of the underlying file
    // descriptor shouldn't make a difference here)
    if (Console.out eq System.out) {
      processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT)
    }
    if (Console.err eq System.err) {
      processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT)
    }

    val process = processBuilder.start

    val streams = Seq(
      Console.out -> process.getInputStream,
      Console.err -> process.getErrorStream
    )

    val listeners = streams.map { case (out, in) =>
      Future {
        val buf = new Array[Byte](1024)

        @tailrec
        def read {
          val bytes = in.read(buf)
          if (bytes >= 0) {
            out.print(new String(buf.take(bytes)))
            read
          }
        }

        read
        true
      }
    }

    val exitCode = process.waitFor
    Await.ready(Future.sequence(listeners), Duration.Inf)
    exitCode
  }
}