mirror of https://github.com/sbt/sbt.git
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:
parent
62137f708f
commit
8883ab324b
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue