Index: E:/Source/OpenSource/ccnet-svn-trunk/project/core/tasks/ExecutableTask.cs =================================================================== --- E:/Source/OpenSource/ccnet-svn-trunk/project/core/tasks/ExecutableTask.cs (revision 3684) +++ E:/Source/OpenSource/ccnet-svn-trunk/project/core/tasks/ExecutableTask.cs (working copy) @@ -38,6 +38,37 @@ [ReflectorProperty("buildArgs", Required = false)] public string BuildArgs = string.Empty; + private string successExitCodesString = string.Empty; + + private int[] successExitCodes = null; + + [ReflectorProperty("successExitCodes", Required = false)] + public string SuccessExitCodes + { + get { return successExitCodesString; } + + set + { + successExitCodesString = value; + + string[] codes = successExitCodesString.Split(','); + + if (codes.Length != 0) + { + successExitCodes = new int[codes.Length]; + + for (int i = 0; i < codes.Length; ++i) + { + successExitCodes[i] = Int32.Parse(codes[i]); + } + } + else + { + successExitCodes = null; + } + } + } + /// /// Gets and sets the maximum number of seconds that the build may take. If the build process takes longer than /// this period, it will be killed. Specify this value as zero to disable process timeouts. @@ -52,11 +83,13 @@ public void Run(IIntegrationResult result) { Util.ListenerFile.WriteInfo(result.ListenerFile, - string.Format("Executing {0}", Executable)); + string.Format("Executing {0}", Executable)); + ProcessInfo processInfo = NewProcessInfoFrom(result); - ProcessResult processResult = AttemptToExecute(NewProcessInfoFrom(result), ProcessMonitor.GetProcessMonitorByProject(result.ProjectName)); - if (!StringUtil.IsWhitespace(processResult.StandardOutput + processResult.StandardError)) + ProcessResult processResult = AttemptToExecute(processInfo, ProcessMonitor.GetProcessMonitorByProject(result.ProjectName)); + + if (!StringUtil.IsWhitespace(processResult.StandardOutput + processResult.StandardError)) { // The executable produced some output. We need to transform it into an XML build report // fragment so the rest of CC.Net can process it. @@ -64,8 +97,9 @@ MakeBuildResult(processResult.StandardOutput, ""), MakeBuildResult(processResult.StandardError, "Error"), processResult.ExitCode, - processResult.TimedOut - ); + processResult.TimedOut, + processResult.Failed); + processResult = newResult; } result.AddTaskResult(new ProcessTaskResult(processResult)); @@ -80,13 +114,14 @@ private ProcessInfo NewProcessInfoFrom(IIntegrationResult result) { - ProcessInfo info = new ProcessInfo(Executable, BuildArgs, BaseDirectory(result)); + ProcessInfo info = new ProcessInfo(Executable, BuildArgs, BaseDirectory(result), successExitCodes); info.TimeOut = BuildTimeoutSeconds*1000; IDictionary properties = result.IntegrationProperties; foreach (string key in properties.Keys) { info.EnvironmentVariables[key] = Convert(properties[key]); } + return info; } Index: E:/Source/OpenSource/ccnet-svn-trunk/project/core/util/ProcessExecutor.cs =================================================================== --- E:/Source/OpenSource/ccnet-svn-trunk/project/core/util/ProcessExecutor.cs (revision 3684) +++ E:/Source/OpenSource/ccnet-svn-trunk/project/core/util/ProcessExecutor.cs (working copy) @@ -88,23 +88,34 @@ // Process must be started before StandardOutput and StandardError streams are accessible using (ProcessReader standardOutput = new ProcessReader(process.StandardOutput), standardError = new ProcessReader(process.StandardError)) { + bool failed = true; + try { WriteToStandardInput(); WaitForProcessToExit(); + + if (processInfo.ProcessSuccessful(process.ExitCode)) + { + failed = false; + } } finally { // Guarantee that process will be killed if it has not exited cleanly if (! process.HasExited) { + failed = true; + Kill(); } // Read in the remainder of the redirected streams standardOutput.WaitForExit(); standardError.WaitForExit(); } - return new ProcessResult(standardOutput.Output, standardError.Output, process.ExitCode, hasTimedOut); + + + return new ProcessResult(standardOutput.Output, standardError.Output, process.ExitCode, hasTimedOut, failed); } } Index: E:/Source/OpenSource/ccnet-svn-trunk/project/core/util/ProcessInfo.cs =================================================================== --- E:/Source/OpenSource/ccnet-svn-trunk/project/core/util/ProcessInfo.cs (revision 3684) +++ E:/Source/OpenSource/ccnet-svn-trunk/project/core/util/ProcessInfo.cs (working copy) @@ -14,13 +14,18 @@ private string standardInputContent = null; private int timeout = DefaultTimeout; + private int[] successExitCodes = null; + public ProcessInfo(string filename) : this(filename, null) {} - public ProcessInfo(string filename, string arguments) : this(filename, arguments, null) + public ProcessInfo(string filename, string arguments) : this(filename, arguments, null, null) {} - public ProcessInfo(string filename, string arguments, string workingDirectory) + public ProcessInfo(string filename, string arguments, string workingDirectory) : this(filename, arguments, workingDirectory, null) + {} + + public ProcessInfo(string filename, string arguments, string workingDirectory, int[] successExitCodes) { startInfo.FileName = StringUtil.StripQuotes(filename); startInfo.Arguments = arguments; @@ -31,6 +36,7 @@ startInfo.RedirectStandardError = true; startInfo.RedirectStandardInput = false; RepathExecutableIfItIsInWorkingDirectory(); + this.successExitCodes = successExitCodes; } private void RepathExecutableIfItIsInWorkingDirectory() @@ -50,6 +56,26 @@ get { return startInfo.EnvironmentVariables; } } + public bool ProcessSuccessful( + int exitCode) + { + if (successExitCodes != null) + { + foreach (int successCode in successExitCodes) + { + if (successCode == exitCode) + { + return true; + } + } + return false; + } + else + { + return 0 == exitCode; + } + } + public string FileName { get { return startInfo.FileName; } Index: E:/Source/OpenSource/ccnet-svn-trunk/project/core/util/ProcessResult.cs =================================================================== --- E:/Source/OpenSource/ccnet-svn-trunk/project/core/util/ProcessResult.cs (revision 3684) +++ E:/Source/OpenSource/ccnet-svn-trunk/project/core/util/ProcessResult.cs (working copy) @@ -15,13 +15,20 @@ private readonly string standardError; private readonly int exitCode; private readonly bool timedOut; + private readonly bool failed; public ProcessResult(string standardOutput, string standardError, int errorCode, bool timedOut) + : this(standardOutput, standardError, errorCode, timedOut, errorCode != SUCCESSFUL_EXIT_CODE) { + } + + public ProcessResult(string standardOutput, string standardError, int errorCode, bool timedOut, bool failed) + { this.standardOutput = (standardOutput == null ? "" : standardOutput); this.standardError = (standardError == null ? "" : standardError); this.exitCode = errorCode; this.timedOut = timedOut; + this.failed = failed; } public string StandardOutput @@ -44,13 +51,9 @@ get { return timedOut; } } - /// - /// A non-zero exit code is the best indication of a process' success or failure. Not all applications adhere to this, however. - /// Applications may write to stderr even if the process succeeds. - /// public bool Failed { - get { return exitCode != SUCCESSFUL_EXIT_CODE; } + get { return failed; } } public bool HasErrorOutput Index: E:/Source/OpenSource/ccnet-svn-trunk/project/UnitTests/Core/Tasks/ExecutableTaskTest.cs =================================================================== --- E:/Source/OpenSource/ccnet-svn-trunk/project/UnitTests/Core/Tasks/ExecutableTaskTest.cs (revision 3684) +++ E:/Source/OpenSource/ccnet-svn-trunk/project/UnitTests/Core/Tasks/ExecutableTaskTest.cs (working copy) @@ -37,6 +37,7 @@ C:\ myarg1 myarg2 123 + 0,1,3,5 "; task = (ExecutableTask) NetReflector.Read(xml); @@ -44,6 +45,7 @@ Assert.AreEqual("mybatchfile.bat", task.Executable); Assert.AreEqual(123, task.BuildTimeoutSeconds); Assert.AreEqual("myarg1 myarg2", task.BuildArgs); + Assert.AreEqual("0,1,3,5", task.SuccessExitCodes); Verify(); } @@ -59,7 +61,8 @@ Assert.AreEqual("mybatchfile.bat", task.Executable); Assert.AreEqual(600, task.BuildTimeoutSeconds); Assert.AreEqual("", task.BuildArgs); - Verify(); + Assert.AreEqual("", task.SuccessExitCodes); + Verify(); } [Test] @@ -205,5 +208,48 @@ + System.Environment.NewLine, result.TaskOutput); Verify(); } + + [Test] + public void ShouldParseValidSuccessExitCodes() + { + task.SuccessExitCodes = "0,1,3,5"; + + task.SuccessExitCodes = "300,500,-1"; + } + + [Test] + [ExpectedException(typeof(System.FormatException))] + public void ShouldThrowExceptionOnInvalidSuccessExitCodes() + { + task.SuccessExitCodes = "0, 1, GOOD"; + } + + [Test] + public void ShouldPassSuccessExitCodesToProcessExecutor() + { + CollectingConstraint constraint = new CollectingConstraint(); + mockProcessExecutor.ExpectAndReturn("Execute", SuccessfulProcessResult(), new object[] { constraint, new IsAnything() }); + + IntegrationResult result = (IntegrationResult)IntegrationResult(); + result.Label = "1.0"; + result.BuildCondition = BuildCondition.ForceBuild; + result.WorkingDirectory = @"c:\workingdir\"; + result.ArtifactDirectory = @"c:\artifactdir\"; + + task.SuccessExitCodes = "0,1,3,5"; + task.Run(result); + + ProcessInfo info = (ProcessInfo)constraint.Parameter; + + Assert.IsTrue(info.ProcessSuccessful(0)); + Assert.IsTrue(info.ProcessSuccessful(1)); + Assert.IsFalse(info.ProcessSuccessful(2)); + Assert.IsTrue(info.ProcessSuccessful(3)); + Assert.IsFalse(info.ProcessSuccessful(4)); + Assert.IsTrue(info.ProcessSuccessful(5)); + Assert.IsFalse(info.ProcessSuccessful(6)); + + Verify(); + } } } Index: E:/Source/OpenSource/ccnet-svn-trunk/project/UnitTests/Core/Util/ProcessInfoTest.cs =================================================================== --- E:/Source/OpenSource/ccnet-svn-trunk/project/UnitTests/Core/Util/ProcessInfoTest.cs (revision 3684) +++ E:/Source/OpenSource/ccnet-svn-trunk/project/UnitTests/Core/Util/ProcessInfoTest.cs (working copy) @@ -46,5 +46,35 @@ Assert.AreEqual(@"c:\nant\nant.exe", info.FileName); Assert.AreEqual(Path.GetTempPath(), info.WorkingDirectory); } + + [Test] + public void ProcessSuccessIsDeterminedBySuccessExitCodes() + { + int[] successExitCodes = { 1, 3, 5 }; + + ProcessInfo info = new ProcessInfo(@"""c:\nant\nant.exe""", null, string.Format(@"""{0}""", Path.GetTempPath()), successExitCodes); + + Assert.IsFalse(info.ProcessSuccessful(0)); + Assert.IsTrue(info.ProcessSuccessful(1)); + Assert.IsFalse(info.ProcessSuccessful(2)); + Assert.IsTrue(info.ProcessSuccessful(3)); + Assert.IsFalse(info.ProcessSuccessful(4)); + Assert.IsTrue(info.ProcessSuccessful(5)); + Assert.IsFalse(info.ProcessSuccessful(6)); + } + + [Test] + public void ProcessSuccessRequiresZeroExitCode() + { + int[] successExitCodes = { 1, 3, 5 }; + + ProcessInfo info = new ProcessInfo(@"""c:\nant\nant.exe""", null, string.Format(@"""{0}""", Path.GetTempPath())); + + Assert.IsTrue(info.ProcessSuccessful(0)); + Assert.IsFalse(info.ProcessSuccessful(1)); + Assert.IsFalse(info.ProcessSuccessful(2)); + Assert.IsFalse(info.ProcessSuccessful(-1)); + } + } } Index: E:/Source/OpenSource/ccnet-svn-trunk/project/UnitTests/Core/Util/ProcessExecutorTest.cs =================================================================== --- E:/Source/OpenSource/ccnet-svn-trunk/project/UnitTests/Core/Util/ProcessExecutorTest.cs (revision 3684) +++ E:/Source/OpenSource/ccnet-svn-trunk/project/UnitTests/Core/Util/ProcessExecutorTest.cs (working copy) @@ -138,6 +138,40 @@ executor.Kill(); } + [Test] + public void ProcessInfoDeterminesSuccessOfProcess() + { + int[] successExitCodes = { 1, 3, 5 }; + + ProcessInfo processInfo1 = new ProcessInfo("cmd.exe", "/C @echo Hello World & exit 1", null, successExitCodes); + + ProcessResult result1 = executor.Execute(processInfo1); + Assert.AreEqual("Hello World", result1.StandardOutput.Trim()); + Assert.AreEqual(1, result1.ExitCode, "Process did not exit successfully"); + AssertFalse("process should not return an error", result1.Failed); + + ProcessInfo processInfo2 = new ProcessInfo("cmd.exe", "/C @echo Hello World & exit 3", null, successExitCodes); + + ProcessResult result2 = executor.Execute(processInfo2); + Assert.AreEqual("Hello World", result2.StandardOutput.Trim()); + Assert.AreEqual(3, result2.ExitCode, "Process did not exit successfully"); + AssertFalse("process should not return an error", result2.Failed); + + ProcessInfo processInfo3 = new ProcessInfo("cmd.exe", "/C @echo Hello World & exit 5", null, successExitCodes); + + ProcessResult result3 = executor.Execute(processInfo3); + Assert.AreEqual("Hello World", result3.StandardOutput.Trim()); + Assert.AreEqual(5, result3.ExitCode, "Process did not exit successfully"); + AssertFalse("process should not return an error", result3.Failed); + + ProcessInfo processInfo4 = new ProcessInfo("cmd.exe", "/C @echo Hello World", null, successExitCodes); + + ProcessResult result4 = executor.Execute(processInfo4); + Assert.AreEqual("Hello World", result4.StandardOutput.Trim()); + Assert.AreEqual(ProcessResult.SUCCESSFUL_EXIT_CODE, result4.ExitCode, "Process did not exit successfully"); + Assert.IsTrue(result4.Failed, "process should return an error"); + } + private static void AssertProcessExitsSuccessfully(ProcessResult result) { Assert.AreEqual(ProcessResult.SUCCESSFUL_EXIT_CODE, result.ExitCode, "Process did not exit successfully");