The Process methods that are redirection-like should not discard the exit code of the input.

Only piping should do that.  This addresses an inconsistency with Fork, where using the CustomOutput
OutputStrategy makes the exit code always zero.
This commit is contained in:
Mark Harrah 2013-09-19 12:38:10 -04:00
parent 62137f708f
commit 8883ab324b
3 changed files with 69 additions and 26 deletions

View File

@ -80,7 +80,7 @@ trait SourcePartialBuilder extends NotNull
* argument is call-by-name, so the stream is recreated, written, and closed each
* time this process is executed. */
def #>(out: => OutputStream): ProcessBuilder = #> (new OutputStreamBuilder(out))
def #>(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(toSource, b, false)
def #>(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(toSource, b, false, ExitCodes.firstIfNonzero)
private def toFile(f: File, append: Boolean) = #> (new FileOutput(f, append))
def cat = toSource
protected def toSource: ProcessBuilder
@ -95,7 +95,7 @@ trait SinkPartialBuilder extends NotNull
* argument is call-by-name, so the stream is recreated, read, and closed each
* time this process is executed. */
def #<(in: => InputStream): ProcessBuilder = #< (new InputStreamBuilder(in))
def #<(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(b, toSink, false)
def #<(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(b, toSink, false, ExitCodes.firstIfNonzero)
protected def toSink: ProcessBuilder
}
@ -174,7 +174,9 @@ trait ProcessBuilder extends SourcePartialBuilder with SinkPartialBuilder
def #&& (other: ProcessBuilder): ProcessBuilder
/** Constructs a command that runs this command first and then `other` if this command does not succeed.*/
def #|| (other: ProcessBuilder): ProcessBuilder
/** Constructs a command that will run this command and pipes the output to `other`. `other` must be a simple command.*/
/** Constructs a command that will run this command and pipes the output to `other`.
* `other` must be a simple command.
* The exit code will be that of `other` regardless of whether this command succeeds. */
def #| (other: ProcessBuilder): ProcessBuilder
/** Constructs a command that will run this command and then `other`. The exit code will be the exit code of `other`.*/
def ### (other: ProcessBuilder): ProcessBuilder

View File

@ -114,6 +114,10 @@ object BasicIO
def inheritInput(connect: Boolean) = { p: JProcessBuilder => if (connect) InheritInput(p) else false }
}
private[sbt] object ExitCodes {
def ignoreFirst: (Int, Int) => Int = (a,b) => b
def firstIfNonzero: (Int, Int) => Int = (a,b) => if(a != 0) a else b
}
private abstract class AbstractProcessBuilder extends ProcessBuilder with SinkPartialBuilder with SourcePartialBuilder
@ -123,7 +127,7 @@ private abstract class AbstractProcessBuilder extends ProcessBuilder with SinkPa
def #|(other: ProcessBuilder): ProcessBuilder =
{
require(other.canPipeTo, "Piping to multiple processes is not supported.")
new PipedProcessBuilder(this, other, false)
new PipedProcessBuilder(this, other, false, exitCode = ExitCodes.ignoreFirst)
}
def ###(other: ProcessBuilder): ProcessBuilder = new SequenceProcessBuilder(this, other)
@ -181,7 +185,7 @@ private[sbt] class FileBuilder(base: File) extends FilePartialBuilder with SinkP
def #<<(f: File): ProcessBuilder = #<<(new FileInput(f))
def #<<(u: URL): ProcessBuilder = #<<(new URLInput(u))
def #<<(s: => InputStream): ProcessBuilder = #<<(new InputStreamBuilder(s))
def #<<(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(b, new FileOutput(base, true), false)
def #<<(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(b, new FileOutput(base, true), false, ExitCodes.firstIfNonzero)
}
private abstract class BasicBuilder extends AbstractProcessBuilder
@ -235,9 +239,9 @@ private abstract class SequentialProcessBuilder(a: ProcessBuilder, b: ProcessBui
checkNotThis(b)
override def toString = " ( " + a + " " + operatorString + " " + b + " ) "
}
private class PipedProcessBuilder(first: ProcessBuilder, second: ProcessBuilder, toError: Boolean) extends SequentialProcessBuilder(first, second, if(toError) "#|!" else "#|")
private class PipedProcessBuilder(first: ProcessBuilder, second: ProcessBuilder, toError: Boolean, exitCode: (Int,Int) => Int) extends SequentialProcessBuilder(first, second, if(toError) "#|!" else "#|")
{
override def createProcess(io: ProcessIO) = new PipedProcesses(first, second, io, toError)
override def createProcess(io: ProcessIO) = new PipedProcesses(first, second, io, toError, exitCode)
}
private class AndProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "#&&")
{
@ -274,7 +278,7 @@ private class OrProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) ext
private class ProcessSequence(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) extends SequentialProcess(a, b, io, ignore => true)
private class PipedProcesses(a: ProcessBuilder, b: ProcessBuilder, defaultIO: ProcessIO, toError: Boolean) extends CompoundProcess
private class PipedProcesses(a: ProcessBuilder, b: ProcessBuilder, defaultIO: ProcessIO, toError: Boolean, exitCode: (Int, Int) => Int) extends CompoundProcess
{
protected[this] override def runAndExitValue() =
{
@ -302,11 +306,11 @@ private class PipedProcesses(a: ProcessBuilder, b: ProcessBuilder, defaultIO: Pr
try
{
runInterruptible {
first.exitValue
val firstResult = first.exitValue
currentSource.put(None)
currentSink.put(None)
val result = second.exitValue
result
val secondResult = second.exitValue
exitCode(firstResult, secondResult)
} {
first.destroy()
second.destroy()

View File

@ -6,7 +6,7 @@ import Prop._
import Process._
private[this] object ProcessSpecification extends Properties("Process I/O")
object ProcessSpecification extends Properties("Process I/O")
{
implicit val exitCodeArb: Arbitrary[Array[Byte]] = Arbitrary(Gen.choose(0, 10) flatMap { size => Gen.resize(size, Arbitrary.arbArray[Byte].arbitrary) })
@ -15,8 +15,11 @@ private[this] object ProcessSpecification extends Properties("Process I/O")
property("#|| correct") = forAll( (exitCodes: Array[Byte]) => checkBinary(exitCodes)(_ #|| _)(_ || _))
property("### correct") = forAll( (exitCodes: Array[Byte]) => checkBinary(exitCodes)(_ ### _)( (x,latest) => latest))*/
property("Pipe to output file") = forAll( (data: Array[Byte]) => checkFileOut(data))
property("Pipe to input file") = forAll( (data: Array[Byte]) => checkFileIn(data))
property("Pipe from input file") = forAll( (data: Array[Byte]) => checkFileIn(data))
property("Pipe to process") = forAll( (data: Array[Byte]) => checkPipe(data))
property("Pipe to process ignores input exit code") = forAll( (data: Array[Byte], code: Byte) => checkPipeExit(data, code))
property("Pipe from input file to bad process preserves correct exit code.") = forAll( (data: Array[Byte], code: Byte) => checkFileInExit(data, code))
property("Pipe to output file from bad process preserves correct exit code.") = forAll( (data: Array[Byte], code: Byte) => checkFileOutExit(data, code))
private def checkBinary(codes: Array[Byte])(reduceProcesses: (ProcessBuilder, ProcessBuilder) => ProcessBuilder)(reduceExit: (Boolean, Boolean) => Boolean) =
{
@ -55,29 +58,63 @@ private[this] object ProcessSpecification extends Properties("Process I/O")
temporaryFile #> catCommand #| catCommand #> temporaryFile2
}
}
private def checkPipeExit(data: Array[Byte], code: Byte) =
withTempFiles { (a,b) =>
IO.write(a, data)
val catCommand = process("sbt.cat")
val exitCommand = process(s"sbt.exit $code")
val exit = (a #> exitCommand #| catCommand #> b).!
(s"Exit code: $exit") |:
(s"Output file length: ${b.length}") |:
(exit == 0) &&
(b.length == 0)
}
private def checkFileOutExit(data: Array[Byte], exitCode: Byte) =
withTempFiles { (a,b) =>
IO.write(a, data)
val code = unsigned(exitCode)
val command = process(s"sbt.exit $code")
val exit = (a #> command #> b).!
(s"Exit code: $exit, expected: $code") |:
(s"Output file length: ${b.length}") |:
(exit == code) &&
(b.length == 0)
}
private def checkFileInExit(data: Array[Byte], exitCode: Byte) =
withTempFiles { (a,b) =>
IO.write(a, data)
val code = unsigned(exitCode)
val command = process(s"sbt.exit $code")
val exit = (a #> command).!
(s"Exit code: $exit, expected: $code") |:
(exit == code)
}
private def temp() = File.createTempFile("sbt", "")
private def withData(data: Array[Byte])(f: (File, File) => ProcessBuilder) =
withTempFiles { (a, b) =>
IO.write(a, data)
val process = f(a, b)
( process ! ) == 0 && sameFiles(a, b)
}
private def sameFiles(a: File, b: File) =
IO.readBytes(a) sameElements IO.readBytes(b)
private def withTempFiles[T](f: (File, File) => T): T =
{
val temporaryFile1 = temp()
val temporaryFile2 = temp()
try
{
IO.write(temporaryFile1, data)
val process = f(temporaryFile1, temporaryFile2)
( process ! ) == 0 &&
{
val b1 = IO.readBytes(temporaryFile1)
val b2 = IO.readBytes(temporaryFile2)
b1 sameElements b2
}
}
try f(temporaryFile1, temporaryFile2)
finally
{
temporaryFile1.delete()
temporaryFile2.delete()
}
}
private def unsigned(b: Byte): Int = ((b: Int) +256) % 256
}
private def unsigned(b: Int): Int = ((b: Int) +256) % 256
private def unsigned(b: Byte): Int = unsigned(b: Int)
private def process(command: String) =
{
val ignore = echo // just for the compile dependency so that this test is rerun when TestedProcess.scala changes, not used otherwise