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 * argument is call-by-name, so the stream is recreated, written, and closed each
* time this process is executed. */ * time this process is executed. */
def #>(out: => OutputStream): ProcessBuilder = #> (new OutputStreamBuilder(out)) 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)) private def toFile(f: File, append: Boolean) = #> (new FileOutput(f, append))
def cat = toSource def cat = toSource
protected def toSource: ProcessBuilder 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 * argument is call-by-name, so the stream is recreated, read, and closed each
* time this process is executed. */ * time this process is executed. */
def #<(in: => InputStream): ProcessBuilder = #< (new InputStreamBuilder(in)) 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 protected def toSink: ProcessBuilder
} }
@ -174,7 +174,9 @@ trait ProcessBuilder extends SourcePartialBuilder with SinkPartialBuilder
def #&& (other: ProcessBuilder): ProcessBuilder def #&& (other: ProcessBuilder): ProcessBuilder
/** Constructs a command that runs this command first and then `other` if this command does not succeed.*/ /** Constructs a command that runs this command first and then `other` if this command does not succeed.*/
def #|| (other: ProcessBuilder): ProcessBuilder 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 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`.*/ /** 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 def ### (other: ProcessBuilder): ProcessBuilder

View File

@ -114,6 +114,10 @@ object BasicIO
def inheritInput(connect: Boolean) = { p: JProcessBuilder => if (connect) InheritInput(p) else false } 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 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 = def #|(other: ProcessBuilder): ProcessBuilder =
{ {
require(other.canPipeTo, "Piping to multiple processes is not supported.") 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) 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 #<<(f: File): ProcessBuilder = #<<(new FileInput(f))
def #<<(u: URL): ProcessBuilder = #<<(new URLInput(u)) def #<<(u: URL): ProcessBuilder = #<<(new URLInput(u))
def #<<(s: => InputStream): ProcessBuilder = #<<(new InputStreamBuilder(s)) 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 private abstract class BasicBuilder extends AbstractProcessBuilder
@ -235,9 +239,9 @@ private abstract class SequentialProcessBuilder(a: ProcessBuilder, b: ProcessBui
checkNotThis(b) checkNotThis(b)
override def toString = " ( " + a + " " + operatorString + " " + 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, "#&&") 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 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() = protected[this] override def runAndExitValue() =
{ {
@ -302,11 +306,11 @@ private class PipedProcesses(a: ProcessBuilder, b: ProcessBuilder, defaultIO: Pr
try try
{ {
runInterruptible { runInterruptible {
first.exitValue val firstResult = first.exitValue
currentSource.put(None) currentSource.put(None)
currentSink.put(None) currentSink.put(None)
val result = second.exitValue val secondResult = second.exitValue
result exitCode(firstResult, secondResult)
} { } {
first.destroy() first.destroy()
second.destroy() second.destroy()

View File

@ -6,7 +6,7 @@ import Prop._
import Process._ 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) }) 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)(_ #|| _)(_ || _))
property("### correct") = forAll( (exitCodes: Array[Byte]) => checkBinary(exitCodes)(_ ### _)( (x,latest) => latest))*/ 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 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") = 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) = 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 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 temp() = File.createTempFile("sbt", "")
private def withData(data: Array[Byte])(f: (File, File) => ProcessBuilder) = 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 temporaryFile1 = temp()
val temporaryFile2 = temp() val temporaryFile2 = temp()
try try f(temporaryFile1, temporaryFile2)
{
IO.write(temporaryFile1, data)
val process = f(temporaryFile1, temporaryFile2)
( process ! ) == 0 &&
{
val b1 = IO.readBytes(temporaryFile1)
val b2 = IO.readBytes(temporaryFile2)
b1 sameElements b2
}
}
finally finally
{ {
temporaryFile1.delete() temporaryFile1.delete()
temporaryFile2.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) = 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 val ignore = echo // just for the compile dependency so that this test is rerun when TestedProcess.scala changes, not used otherwise