Search code examples
c#.netasynchronousspecflowinvalidoperationexception

InvalidOperationException when running process with Specflow


I get this when I run my Specflow test:

[xUnit.net 00:00:06.30]       System.AggregateException : One or more errors occurred. (Cannot mix synchronous and asynchronous operation on process stream.)
[xUnit.net 00:00:06.30]       ---- System.InvalidOperationException : Cannot mix synchronous and asynchronous operation on process stream.
[xUnit.net 00:00:06.30]       Stack Trace:
[xUnit.net 00:00:06.31]            at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
[xUnit.net 00:00:06.31]            at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
[xUnit.net 00:00:06.31]            at System.Threading.Tasks.Task`1.get_Result()
[xUnit.net 00:00:06.31]         c:\Users\Me\Source\Repos\test_tool\Steps\IntegrationTestStepDefinitions.cs(51,0): at test_tool.Steps.IntegrationTestStepDefinitions.GivenMessagesAreStreamingFromToTopicOnPartition(String binaryFile, String topic, Int32 partition)
[xUnit.net 00:00:06.31]            at TechTalk.SpecFlow.Bindings.BindingInvoker.InvokeBinding(IBinding binding, IContextManager contextManager, Object[] arguments, ITestTracer testTracer, TimeSpan& duration)
[xUnit.net 00:00:06.31]            at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.ExecuteStepMatch(BindingMatch match, Object[] arguments, TimeSpan& duration)
[xUnit.net 00:00:06.31]            at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.ExecuteStep(IContextManager contextManager, StepInstance stepInstance)
[xUnit.net 00:00:06.31]            at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.OnAfterLastStep()
[xUnit.net 00:00:06.31]            at TechTalk.SpecFlow.TestRunner.CollectScenarioErrors()
[xUnit.net 00:00:06.31]            at test_tool.Features.IntegrationTestFeature.ScenarioCleanup()

This is the step(lines 43-53):

        [Given(@"messages are streaming from ""(.*)"" to topic ""(.*)"" on partition (.*)")]
        public void GivenMessagesAreStreamingFromToTopicOnPartition(string binaryFile, string topic, int partition)
        {
            var MessageInjectorOptions = new MessageInjectorOptions();
            _configuration.GetSection("JavaMessageInjector").Bind(MessageInjectorOptions);
            _logger.LogInformation("MessageInjector Command: " + MessageInjectorOptions.MessageInjectorFilename + " " + MessageInjectorOptions.MessageInjectorParameters + " " + binaryFile + " " + topic + " " + partition);
            _output.WriteLine("Console MessageInjector Command: " + MessageInjectorOptions.MessageInjectorFilename + " " + MessageInjectorOptions.MessageInjectorParameters + " " + binaryFile + " " + topic + " " + partition);
            var result = TestHelper.ExecuteShellCommand(MessageInjectorOptions.MessageInjectorFilename, MessageInjectorOptions.MessageInjectorParameters + " " + binaryFile + " " + topic + " " + partition, 300000, true);
            _logger.LogInformation("Output: " + result.Result.Output);
            _logger.LogInformation("MessageInjector Exit Code: " + result.Result.ExitCode + ", Completed: " + result.Result.Completed + " Output: " + result.Result.Output);
        }

This is my ExecuteShellCommand:

public static async Task<ProcessResult> ExecuteShellCommand(string command, string arguments="", int timeout=1000, bool insertWait=false)
{
    var result = new ProcessResult();

    using (var process = new Process())
    {
        process.StartInfo.FileName = command;
        process.StartInfo.Arguments = arguments;
        process.StartInfo.UseShellExecute = false;
        process.StartInfo.RedirectStandardInput = true;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.RedirectStandardError = true;
        process.StartInfo.CreateNoWindow = true;

        var outputBuilder = new StringBuilder();
        var outputCloseEvent = new TaskCompletionSource<bool>();

        process.OutputDataReceived += (s, e) =>
        {
            // The output stream has been closed i.e. the process has terminated
            if (e.Data == null)
            {
                outputCloseEvent.SetResult(true);
            }
            else
            {
                outputBuilder.AppendLine(e.Data);
            }
        };

        var errorBuilder = new StringBuilder();
        var errorCloseEvent = new TaskCompletionSource<bool>();

        process.ErrorDataReceived += (s, e) =>
        {
            // The error stream has been closed i.e. the process has terminated
            if (e.Data == null)
            {
                errorCloseEvent.SetResult(true);
            }
            else
            {
                errorBuilder.AppendLine(e.Data);
            }
        };

        bool isStarted;

        try
        {
            process.OutputDataReceived += new DataReceivedEventHandler(OutputHandler);
            process.ErrorDataReceived += new DataReceivedEventHandler(OutputHandler);
            isStarted = process.Start(); 
            StreamReader reader = process.StandardOutput;
            string output = reader.ReadToEnd();
            result.Output = output;
        }
        catch (Exception error)
        {
            // Usually it occurs when an executable file is not found or is not executable

            result.Completed = true;
            result.ExitCode = -1;
            result.Output = error.Message;

            isStarted = false;
        }

        if (isStarted)
        {
            // Reads the output stream first and then waits because deadlocks are possible
            process.BeginOutputReadLine();
            process.BeginErrorReadLine();

            if (insertWait)
            {
                await Task.Delay(150000);
            }

            // Creates task to wait for process exit using timeout
            var waitForExit = WaitForExitAsync(process, timeout);

            // Create task to wait for process exit and closing all output streams
            var processTask = Task.WhenAll(waitForExit, outputCloseEvent.Task, errorCloseEvent.Task);

            // Waits process completion and then checks it was not completed by timeout
            if (await Task.WhenAny(Task.Delay(timeout), processTask) == processTask && waitForExit.Result)
            {
                result.Completed = true;
                result.ExitCode = process.ExitCode;

                // Adds process output if it was completed with error
                if (process.ExitCode != 0)
                {
                    result.Output = $"{outputBuilder}{errorBuilder}";
                }
            }
            else
            {
                try
                {
                    // Kill hung process
                    process.Kill();
                }
                catch
                {
                }
            }
        }
    }

    return result;
}

I don't understand what the error is about. I assume it's coming from something that's wrong in TestHelper. Is it that I must wait for it to return or how should I resolve this?


Solution

  • You cannot call an async method from a synchronous step definition. You need to use Asynchronous Bindings instead.

    1. Change the step definition method signature to async and return a Task:

      [Given(@"messages are streaming from ""(.*)"" to topic ""(.*)"" on partition (.*)")]
      public async Task GivenMessagesAreStreamingFromToTopicOnPartition(string binaryFile, string topic, int partition)
      {
          // ...
      }
      
    2. Use await when calling TestHelper.ExecuteShellCommand(...):

      var result = await TestHelper.ExecuteShellCommand(MessageInjectorOptions.MessageInjectorFilename, MessageInjectorOptions.MessageInjectorParameters + " " + binaryFile + " " + topic + " " + partition, 300000, true);
      
      _logger.LogInformation("Output: " + result.Output);
      _logger.LogInformation("MessageInjector Exit Code: " + result.ExitCode + ", Completed: " + result.Completed + " Output: " + result.Output);
      

      Be sure to replace all occurrences of result.Result.X with just result.X, since the result variable will now be a ProcessResult object instead of a Task<ProcessResult>.