{% skip_file if flag?(:wasm32) %}

require "spec"
require "process"
require "./spec_helper"
require "../support/env"
require "../support/wait_for"

private def exit_code_command(code)
  {% if flag?(:win32) %}
    {"cmd.exe", {"/c", "exit #{code}"}}
  {% else %}
    {"/bin/sh", {"-c", "exit #{code}"}}
  {% end %}
end

private def shell_command(command)
  {% if flag?(:win32) %}
    {"cmd.exe", {"/c", command}}
  {% else %}
    {"/bin/sh", {"-c", command}}
  {% end %}
end

private def stdin_to_stdout_command
  {% if flag?(:win32) %}
    {"powershell.exe", {"-C", "$Input"}}
  {% else %}
    {"/bin/cat", [] of String}
  {% end %}
end

private def stdin_to_stderr_command(status = 0)
  {% if flag?(:win32) %}
    {"powershell.exe", {"-C", "[Console]::OpenStandardInput().CopyTo([Console]::OpenStandardError()); exit #{status}"}}
  {% else %}
    {"/bin/sh", {"-c", "cat 1>&2; exit #{status}"}}
  {% end %}
end

private def print_env_command
  {% if flag?(:win32) %}
    # cmd adds these by itself, clear them out before printing.
    shell_command("set COMSPEC=& set PATHEXT=& set PROMPT=& set PROCESSOR_ARCHITECTURE=& set")
  {% else %}
    {"env", [] of String}
  {% end %}
end

private def standing_command
  {% if flag?(:win32) %}
    {"cmd.exe"}
  {% else %}
    {"yes"}
  {% end %}
end

private def path_search_command
  {% if flag?(:win32) %}
    {"cmd.exe"}
  {% else %}
    {"true"}
  {% end %}
end

private def newline
  {% if flag?(:win32) %}
    "\r\n"
  {% else %}
    "\n"
  {% end %}
end

private def to_ary(tuple)
  [tuple[0]].concat(tuple[1])
end

# interpreted code doesn't receive SIGCHLD for `#wait` to work (#12241)
{% if flag?(:interpreted) && !flag?(:win32) %}
  pending Process
  {% skip_file %}
{% end %}

describe Process do
  describe ".new (args)" do
    it "raises if args is empty" do
      expect_raises(File::NotFoundError, "Error executing process: No command") do
        Process.new([] of String)
      end
    end

    it "raises if args[0] is empty" do
      expect_raises(IO::Error, /Error executing process: '(""|\\"\\")?'/) do
        Process.new([""] of String)
      end
    end

    it "raises if command doesn't exist" do
      expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do
        Process.new(["foobarbaz"])
      end
    end

    it "raises for long path" do
      expect_raises(File::NotFoundError, "Error executing process: 'aaaaaaa") do
        Process.new(["a" * 1000])
      end
    end

    it "accepts nilable string for `chdir` (#13767)" do
      expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do
        Process.new(["foobarbaz"], chdir: nil.as(String?))
      end
    end

    it "raises if command is a file path" do
      with_tempfile("crystal-spec-run") do |path|
        File.touch path
        expect_raises({% if flag?(:win32) %} File::BadExecutableError {% else %} File::AccessDeniedError {% end %}, "Error executing process: '#{path.inspect_unquoted}'") do
          Process.new([path])
        end
      end
    end

    it "raises if command is a dir path" do
      with_tempfile("crystal-spec-run") do |path|
        Dir.mkdir path
        expect_raises(File::AccessDeniedError, "Error executing process: '#{path.inspect_unquoted}'") do
          Process.new([path])
        end
      end
    end

    it "raises if command is a file's subpath" do
      with_tempfile("crystal-spec-run") do |path|
        File.touch path
        command = File.join(path, "foo")
        expect_raises(IO::Error, "Error executing process: '#{command.inspect_unquoted}'") do
          Process.new([command])
        end
      end
    end

    it "doesn't break if process is collected before completion", tags: %w[slow] do
      200.times { Process.new(to_ary(exit_code_command(0))) }

      # run the GC multiple times to unmap as much memory as possible
      10.times { GC.collect }

      # the processes above have now been queued after completion; if this last
      # one finishes at all, nothing was broken by the GC
      Process.run(*exit_code_command(0))
    end

    it "accepts tuple args" do
      Process.new({path_search_command[0]}).wait.success?.should be_true
    end
  end

  describe ".new (command + args)" do
    it "raises if command doesn't exist" do
      expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do
        Process.new("foobarbaz")
      end
    end

    it "raises for long path" do
      expect_raises(File::NotFoundError, "Error executing process: 'aaaaaaa") do
        Process.new("a" * 1000)
      end
    end

    it "accepts nilable string for `chdir` (#13767)" do
      expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do
        Process.new("foobarbaz", chdir: nil.as(String?))
      end
    end

    it "raises if command is a file path" do
      with_tempfile("crystal-spec-run") do |path|
        File.touch path
        expect_raises({% if flag?(:win32) %} File::BadExecutableError {% else %} File::AccessDeniedError {% end %}, "Error executing process: '#{path.inspect_unquoted}'") do
          Process.new(path)
        end
      end
    end

    it "raises if command is a dir path" do
      with_tempfile("crystal-spec-run") do |path|
        Dir.mkdir path
        expect_raises(File::AccessDeniedError, "Error executing process: '#{path.inspect_unquoted}'") do
          Process.new(path)
        end
      end
    end

    it "raises if command is a file's subpath" do
      with_tempfile("crystal-spec-run") do |path|
        File.touch path
        command = File.join(path, "foo")
        expect_raises(IO::Error, "Error executing process: '#{command.inspect_unquoted}'") do
          Process.new(command)
        end
      end
    end

    it "doesn't break if process is collected before completion", tags: %w[slow] do
      200.times { Process.new(*exit_code_command(0)) }

      # run the GC multiple times to unmap as much memory as possible
      10.times { GC.collect }

      # the processes above have now been queued after completion; if this last
      # one finishes at all, nothing was broken by the GC
      Process.run(*exit_code_command(0))
    end
  end

  describe "#wait" do
    it "successful exit code" do
      process = Process.new(*exit_code_command(0))
      process.wait.exit_code.should eq(0)
    end

    it "unsuccessful exit code" do
      process = Process.new(*exit_code_command(1))
      process.wait.exit_code.should eq(1)
    end
  end

  describe ".run?(args)" do
    it "waits for successful process" do
      status = Process.run?(to_ary(exit_code_command(0))).should be_a(Process::Status)
      status.exit_code.should eq(0)
    end

    it "waits for unsuccessful process" do
      status = Process.run?(to_ary(exit_code_command(1))).should be_a(Process::Status)
      status.exit_code.should eq(1)
    end

    it "returns nil if args[0] is empty" do
      Process.run?([""] of String).should be_nil
    end

    it "returns nil command doesn't exist" do
      Process.run?(["foobarbaz"]).should be_nil
    end

    it "returns nil for long path" do
      Process.run?(["a" * 1000]).should be_nil
    end

    it "returns nil if command is a file path" do
      with_tempfile("crystal-spec-run") do |path|
        File.touch path
        Process.run?([path]).should be_nil
      end
    end

    it "returns nil if command is a dir path" do
      with_tempfile("crystal-spec-run") do |path|
        Dir.mkdir path
        Process.run?([path]).should be_nil
      end
    end

    it "returns nil if command is a file's subpath" do
      with_tempfile("crystal-spec-run") do |path|
        File.touch path
        command = File.join(path, "foo")
        Process.run?([command]).should be_nil
      end
    end

    it "accepts tuple args" do
      Process.run({path_search_command[0]}).success?.should be_true
    end
  end

  describe ".run" do
    it "waits for the process" do
      Process.run(to_ary(exit_code_command(0))).exit_code.should eq(0)
    end
  end

  describe ".run(args, &)" do
    it "waits for the process" do
      Process.run(to_ary(exit_code_command(0))) { }[0].exit_code.should eq(0)
    end

    it "returns block result" do
      Process.run(to_ary(exit_code_command(0))) { 42 }[1].should eq 42
    end
  end

  describe ".run(command, args)" do
    it "waits for the process" do
      Process.run(*exit_code_command(0)).exit_code.should eq(0)
    end

    it "runs true in block" do
      Process.run(*exit_code_command(0)) { }
      $?.exit_code.should eq(0)
    end

    it "receives arguments in array" do
      command, args = exit_code_command(123)
      Process.run(command, args.to_a).exit_code.should eq(123)
    end

    it "receives arguments in tuple" do
      command, args = exit_code_command(123)
      Process.run(command, args.as(Tuple)).exit_code.should eq(123)
    end

    it "redirects output to /dev/null" do
      # This doesn't test anything but no output should be seen while running tests
      command, args = {% if flag?(:win32) %}
                        {"cmd.exe", {"/c", "dir"}}
                      {% else %}
                        {"/bin/ls", [] of String}
                      {% end %}
      Process.run(command, args, output: Process::Redirect::Close).exit_code.should eq(0)
    end

    it "gets output" do
      value = Process.run(*shell_command("echo hello")) do |proc|
        proc.output.gets_to_end
      end
      value.should eq("hello#{newline}")
    end

    it "sends input in IO" do
      value = Process.run(*stdin_to_stdout_command, input: IO::Memory.new("hello")) do |proc|
        proc.input?.should be_nil
        proc.output.gets_to_end
      end
      value.chomp.should eq("hello")
    end

    it "sends output to IO" do
      output = IO::Memory.new
      Process.run(*shell_command("echo hello"), output: output)
      output.to_s.should eq("hello#{newline}")
    end

    it "sends error to IO" do
      error = IO::Memory.new
      Process.run(*shell_command("1>&2 echo hello"), error: error)
      error.to_s.should eq("hello#{newline}")
    end

    it "sends long output and error to IO" do
      output = IO::Memory.new
      error = IO::Memory.new
      Process.run(*shell_command("echo #{"." * 8000}"), output: output, error: error)
      output.to_s.should eq("." * 8000 + newline)
      error.to_s.should be_empty
    end

    it "controls process in block" do
      value = Process.run(*stdin_to_stdout_command, error: :inherit) do |proc|
        proc.input.puts "hello"
        proc.input.close
        proc.output.gets_to_end
      end
      value.should eq("hello#{newline}")
    end

    it "closes input after block" do
      Process.run(*stdin_to_stdout_command) { }
      $?.exit_code.should eq(0)
    end

    it "closes output and error after block" do
      reader, writer = IO.pipe
      channel = Channel(Process).new

      spawn do
        Process.run(*stdin_to_stdout_command, input: reader, output: :pipe, error: :pipe) do |process|
          channel.send process
          channel.receive
        end
        channel.close
      end

      process = channel.receive

      process.output.closed?.should be_false
      process.error.closed?.should be_false

      channel.send process

      # Wait a moment for the other fiber to continue and close the IOs
      wait_for { process.output.closed? && process.error.closed? }

      writer.close
      channel.receive?.should be_nil
    end

    it "forwards closed io" do
      closed_io = IO::Memory.new
      closed_io.close
      Process.run(*stdin_to_stdout_command, input: closed_io)
      Process.run(*stdin_to_stdout_command, output: closed_io)
      Process.run(*stdin_to_stdout_command, error: closed_io)
    end

    it "forwards non-blocking file" do
      with_tempfile("non-blocking-process-input.txt", "non-blocking-process-output.txt") do |in_path, out_path|
        File.open(in_path, "w+", blocking: false) do |input|
          File.open(out_path, "w+", blocking: false) do |output|
            input.puts "hello"
            input.rewind
            Process.run(*stdin_to_stdout_command, input: input, output: output)
            output.rewind
            output.gets_to_end.chomp.should eq("hello")
          end
        end
      end
    end

    it "sets working directory with string" do
      parent = File.dirname(Dir.current)
      command = {% if flag?(:win32) %}
                  "cmd.exe /c echo %cd%"
                {% else %}
                  "pwd"
                {% end %}
      value = Process.run(command, shell: true, chdir: parent, output: Process::Redirect::Pipe) do |proc|
        proc.output.gets_to_end
      end
      value.should eq "#{parent}#{newline}"
    end

    it "sets working directory with path" do
      parent = Path.new File.dirname(Dir.current)
      command = {% if flag?(:win32) %}
                  "cmd.exe /c echo %cd%"
                {% else %}
                  "pwd"
                {% end %}
      value = Process.run(command, shell: true, chdir: parent, output: Process::Redirect::Pipe) do |proc|
        proc.output.gets_to_end
      end
      value.should eq "#{parent}#{newline}"
    end

    pending_win32 "disallows passing arguments to nowhere" do
      expect_raises ArgumentError, /args.+@/ do
        Process.run("foo bar", {"baz"}, shell: true)
      end
    end

    pending_win32 "looks up programs in the $PATH with a shell" do
      proc = Process.run(*exit_code_command(0), shell: true, output: Process::Redirect::Close)
      proc.exit_code.should eq(0)
    end

    pending_win32 "allows passing huge argument lists to a shell" do
      proc = Process.new(%(echo "${@}"), {"a", "b"}, shell: true, output: Process::Redirect::Pipe)
      output = proc.output.gets_to_end
      proc.wait
      output.should eq "a b\n"
    end

    pending_win32 "does not run shell code in the argument list" do
      proc = Process.new("echo", {"`echo hi`"}, shell: true, output: Process::Redirect::Pipe)
      output = proc.output.gets_to_end
      proc.wait
      output.should eq "`echo hi`\n"
    end

    describe "does not execute batch files" do
      %w[.bat .Bat .BAT .cmd .cmD .CmD .bat\  .cmd\ ... .bat.\ .].each do |ext|
        it ext do
          with_tempfile "process_run#{ext}" do |path|
            File.write(path, "echo '#{ext}'\n")
            expect_raises {{ flag?(:win32) ? File::BadExecutableError : File::AccessDeniedError }}, "Error executing process" do
              Process.run(path)
            end
          end
        end
      end
    end

    describe "environ" do
      it "clears the environment" do
        value = Process.run(*print_env_command, clear_env: true) do |proc|
          proc.output.gets_to_end
        end
        value.should eq("")
      end

      it "clears and sets an environment variable" do
        value = Process.run(*print_env_command, clear_env: true, env: {"FOO" => "bar"}) do |proc|
          proc.output.gets_to_end
        end
        value.should eq("FOO=bar#{newline}")
      end

      it "sets an environment variable" do
        value = Process.run(*print_env_command, env: {"FOO" => "bar"}) do |proc|
          proc.output.gets_to_end
        end
        value.should match /(*ANYCRLF)^FOO=bar$/m
      end

      it "sets an empty environment variable" do
        value = Process.run(*print_env_command, env: {"FOO" => ""}) do |proc|
          proc.output.gets_to_end
        end
        value.should match /(*ANYCRLF)^FOO=$/m
      end

      it "deletes existing environment variable" do
        with_env("FOO": "bar") do
          value = Process.run(*print_env_command, env: {"FOO" => nil}) do |proc|
            proc.output.gets_to_end
          end
          value.should_not match /(*ANYCRLF)^FOO=/m
        end
      end

      {% if flag?(:win32) %}
        it "deletes existing environment variable case-insensitive" do
          with_env("FOO": "bar") do
            value = Process.run(*print_env_command, env: {"foo" => nil}) do |proc|
              proc.output.gets_to_end
            end
            value.should_not match /(*ANYCRLF)^FOO=/mi
          end
        end
      {% end %}

      it "preserves existing environment variable" do
        with_env("FOO": "bar") do
          value = Process.run(*print_env_command) do |proc|
            proc.output.gets_to_end
          end
          value.should match /(*ANYCRLF)^FOO=bar$/m
        end
      end

      it "preserves and sets an environment variable" do
        with_env("FOO": "bar") do
          value = Process.run(*print_env_command, env: {"FOO2" => "bar2"}) do |proc|
            proc.output.gets_to_end
          end
          value.should match /(*ANYCRLF)^FOO=bar$/m
          value.should match /(*ANYCRLF)^FOO2=bar2$/m
        end
      end

      it "overrides existing environment variable" do
        with_env("FOO": "bar") do
          value = Process.run(*print_env_command, env: {"FOO" => "different"}) do |proc|
            proc.output.gets_to_end
          end
          value.should match /(*ANYCRLF)^FOO=different$/m
        end
      end

      {% if flag?(:win32) %}
        it "overrides existing environment variable case-insensitive" do
          with_env("FOO": "bar") do
            value = Process.run(*print_env_command, env: {"fOo" => "different"}) do |proc|
              proc.output.gets_to_end
            end
            value.should_not match /(*ANYCRLF)^FOO=/m
            value.should match /(*ANYCRLF)^fOo=different$/m
          end
        end
      {% end %}

      it "finds binary in parent `$PATH`, not `env`" do
        Process.run(*print_env_command, env: {"PATH" => ""})
      end

      it "errors on invalid key" do
        expect_raises(ArgumentError, %(Invalid env key "")) do
          Process.run(*print_env_command, env: {"" => "baz"})
        end
        expect_raises(ArgumentError, %(Invalid env key "foo=bar")) do
          Process.run(*print_env_command, env: {"foo=bar" => "baz"})
        end
      end

      it "errors on zero char in key" do
        expect_raises({{ flag?(:win32) }} ? ArgumentError : RuntimeError, "String `key` contains null byte") do
          Process.run(*print_env_command, env: {"foo\0" => "baz"})
        end
      end

      it "errors on zero char in value" do
        expect_raises({{ flag?(:win32) }} ? ArgumentError : RuntimeError, "String `value` contains null byte") do
          Process.run(*print_env_command, env: {"foo" => "baz\0"})
        end
      end
    end

    it "errors with empty command" do
      {% begin %}
        expect_raises({% if flag?(:win32) %} IO::Error, "The parameter is incorrect" {% else %} File::NotFoundError{% end %}) do
          Process.run("")
        end
      {% end %}
    end

    it "errors with too long command" do
      pending! unless {{ flag?(:linux) }}

      path_max = {% if LibC.has_constant?(:PATH_MAX) %}
                   LibC::PATH_MAX
                 {% else %}
                   10_000
                 {% end %}

      expect_raises(IO::Error, /File ?name too long/) do
        Process.run("a" * (path_max + 1))
      end

      # The pathname itself is not too long, but it will be when combined with
      # any path prefix.
      expect_raises(IO::Error, /File ?name too long/) do
        Process.run("a" * path_max)
      end
    end

    describe "$PATH" do
      it "works with unset $PATH" do
        with_env("PATH": nil) do
          Process.run(*path_search_command)
        end
      end

      it "errors with empty $PATH" do
        pending! if {{ flag?(:win32) }}
        with_env("PATH": "") do
          expect_raises(File::NotFoundError) do
            Process.run(*path_search_command)
          end
        end
      end

      it "empty still finds in current directory" do
        pending! unless {{ flag?(:unix) }}

        with_tempfile("crystal-spec-run") do |dir|
          Dir.mkdir dir
          File.write(Path[dir, "foo"], "#!/bin/sh\necho bar")
          File.chmod(Path[dir, "foo"], 0o555)
          if {{ flag?(:darwin) }}
            String.build do |io|
              Process.run("foo", chdir: dir, output: io)
            end.should eq "bar\n"
          else
            expect_raises(File::NotFoundError) do
              Process.run("foo", chdir: dir)
            end
          end
        end
      end

      it "empty path entry means current directory" do
        pending! unless {{ flag?(:unix) }}

        with_tempfile("crystal-spec-run") do |dir|
          Dir.mkdir dir
          File.write(Path[dir, "foo"], "#!/bin/sh\necho bar")
          File.chmod(Path[dir, "foo"], 0o555)
          with_env("PATH": ":") do
            Process.run("foo", chdir: dir)
          end
          with_env("PATH": "::") do
            Process.run("foo", chdir: dir)
          end
          with_env("PATH": "/does/not/exist:") do
            Process.run("foo", chdir: dir)
          end
          with_env("PATH": ":/does/not/exist") do
            Process.run("foo", chdir: dir)
          end
        end
      end

      it "finds path in relative directory" do
        pending! unless {{ flag?(:unix) }}

        with_tempfile("crystal-spec-run") do |dir|
          Dir.mkdir_p Path[dir, "bin"]
          Dir.mkdir_p Path[dir, "empty"]
          File.write(Path[dir, "bin", "foo"], "#!/bin/sh\necho bar")
          File.chmod(Path[dir, "bin", "foo"], 0o555)
          with_env("PATH": "bin") do
            Process.run("foo", chdir: dir)
          end
          with_env("PATH": "empty:bin") do
            Process.run("foo", chdir: dir)
          end
          with_env("PATH": "bin:empty") do
            Process.run("foo", chdir: dir)
          end
          with_env("PATH": "/does/not/exist:bin") do
            Process.run("foo", chdir: dir)
          end
          with_env("PATH": "bin:/does/not/exist") do
            Process.run("foo", chdir: dir)
          end
          with_env("PATH": ":bin") do
            Process.run("foo", chdir: dir)
          end
          with_env("PATH": "::bin") do
            Process.run("foo", chdir: dir)
          end
          with_env("PATH": "/does/not/exist::bin") do
            Process.run("foo", chdir: dir)
          end
          with_env("PATH": "bin:/does/not/exist") do
            Process.run("foo", chdir: dir)
          end
        end
      end

      context "with shell: true" do
        it "errors with nonexist $PATH" do
          pending! unless {{ flag?(:unix) }}
          Process.run(*print_env_command, shell: true, env: {"PATH" => "/does/not/exist"}).success?.should be_false
        end

        it "empty path entry means current directory" do
          pending! unless {{ flag?(:unix) }}

          with_tempfile("crystal-spec-run") do |dir|
            Dir.mkdir dir
            File.write(Path[dir, "foo"], "#!/bin/sh\necho bar")
            File.chmod(Path[dir, "foo"], 0o555)
            Process.run("foo", chdir: dir, shell: true, env: {"PATH" => ":"}).success?.should be_true
            Process.run("foo", chdir: dir, shell: true, env: {"PATH" => "::"}).success?.should be_true
            Process.run("foo", chdir: dir, shell: true, env: {"PATH" => "/does/not/exist:"}).success?.should be_true
            Process.run("foo", chdir: dir, shell: true, env: {"PATH" => ":/does/not/exist"}).success?.should be_true
          end
        end

        it "finds path in relative directory" do
          pending! unless {{ flag?(:unix) }}

          with_tempfile("crystal-spec-run") do |dir|
            Dir.mkdir_p Path[dir, "bin"]
            Dir.mkdir_p Path[dir, "empty"]
            File.write(Path[dir, "bin", "foo"], "#!/bin/sh\necho bar")
            File.chmod(Path[dir, "bin", "foo"], 0o555)
            Process.run("foo", chdir: dir, shell: true, env: {"PATH" => "bin"}).success?.should be_true
            Process.run("foo", chdir: dir, shell: true, env: {"PATH" => "empty:bin"}).success?.should be_true
            Process.run("foo", chdir: dir, shell: true, env: {"PATH" => "bin:empty"}).success?.should be_true
            Process.run("foo", chdir: dir, shell: true, env: {"PATH" => "/does/not/exist:bin"}).success?.should be_true
            Process.run("foo", chdir: dir, shell: true, env: {"PATH" => "bin:/does/not/exist"}).success?.should be_true
            Process.run("foo", chdir: dir, shell: true, env: {"PATH" => ":bin"}).success?.should be_true
            Process.run("foo", chdir: dir, shell: true, env: {"PATH" => "::bin"}).success?.should be_true
            Process.run("foo", chdir: dir, shell: true, env: {"PATH" => "/does/not/exist::bin"}).success?.should be_true
            Process.run("foo", chdir: dir, shell: true, env: {"PATH" => "bin:/does/not/exist"}).success?.should be_true
          end
        end
      end
    end

    it "can link processes together" do
      buffer = IO::Memory.new
      Process.run(*stdin_to_stdout_command) do |cat|
        Process.run(*stdin_to_stdout_command, input: cat.output, output: buffer) do
          1000.times { cat.input.puts "line" }
          cat.close
        end
      end
      buffer.to_s.chomp.lines.size.should eq(1000)
    end
  end

  describe ".capture_result" do
    it "captures stdout" do
      result = Process.capture_result(to_ary(shell_command("echo hello")))
      result.status.success?.should be_true
      result.output?.should eq "hello#{newline}"
      result.error?.should eq ""
    end

    it "captures stdout from stdin" do
      result = Process.capture_result(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello"))
      result.status.success?.should be_true
      result.output.chomp.should eq "hello"
    end

    it "ignores stdout if output is IO" do
      io = IO::Memory.new
      result = Process.capture_result(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello"), output: io)
      result.status.success?.should be_true
      result.output?.should be_nil
      result.error?.should eq ""
      io.to_s.chomp.should eq "hello"
    end

    it "ignores stdout if output is FileDescriptor" do
      reader, writer = IO.pipe
      result = Process.capture_result(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello\n"), output: writer)
      result.status.success?.should be_true
      result.output?.should be_nil
      result.error?.should eq ""
      reader.gets.should eq "hello"
    end

    it "captures stderr" do
      result = Process.capture_result(to_ary(shell_command("1>&2 echo hello")))
      result.status.success?.should be_true
      result.output?.should eq ""
      result.error?.should eq "hello#{newline}"
    end

    it "ignores stderr if error is IO" do
      io = IO::Memory.new
      result = Process.capture_result(to_ary(shell_command("1>&2 echo hello")), error: io)
      result.status.success?.should be_true
      result.output?.should eq ""
      result.error?.should be_nil
      io.to_s.should eq "hello#{newline}"
    end

    it "ignores stderr if error is FileDescriptor" do
      reader, writer = IO.pipe
      result = Process.capture_result(to_ary(shell_command("1>&2 echo hello")), error: writer)
      result.status.success?.should be_true
      result.output?.should eq ""
      result.error?.should be_nil
      reader.gets.should eq "hello"
    end

    it "doesn't capture closed stdout" do
      result = Process.capture_result(to_ary(shell_command("echo hello")), output: :close)
      result.output?.should be_nil
      result.error?.should_not be_nil
    end

    it "doesn't capture closed stderr" do
      # FIXME: Autocasting breaks in the interpreter
      result = Process.capture_result(to_ary(shell_command("1>&2 echo hello")), error: Process::Redirect::Close)
      result.status.success?.should be_true
      result.output?.should eq ""
      result.error?.should be_nil
    end

    it "truncates error output", tags: %w[slow] do
      dashes32 = "-" * (32 << 10)
      input = IO::Memory.new("#{dashes32}X#{dashes32}")
      result = Process.capture_result(to_ary(stdin_to_stderr_command), input: input)
      result.status.success?.should be_true
      result.output?.should eq ""
      error = result.error.should be_a(String)
      error.should contain "\n...omitted 1 bytes...\n"
      error.count("-").should eq(32 << 11)
    end

    it "reports status" do
      Process.capture_result(to_ary(exit_code_command(0))).status.exit_code.should eq(0)
      Process.capture_result(to_ary(exit_code_command(123))).status.exit_code.should eq(123)
    end

    it "raises if process cannot execute" do
      expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do
        Process.capture_result(["foobarbaz"])
      end
    end
  end

  describe ".capture_result?" do
    it "captures stdout" do
      result = Process.capture_result?(to_ary(shell_command("echo hello"))).should be_a(Process::Result)
      result.status.success?.should be_true
      result.output?.should eq "hello#{newline}"
      result.error?.should eq ""
    end

    it "captures stdout from stdin" do
      result = Process.capture_result?(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello")).should be_a(Process::Result)
      result.status.success?.should be_true
      result.output.chomp.should eq "hello"
    end

    it "ignores stdout if output is IO" do
      io = IO::Memory.new
      result = Process.capture_result?(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello"), output: io).should be_a(Process::Result)
      result.status.success?.should be_true
      result.output?.should be_nil
      result.error?.should eq ""
      io.to_s.chomp.should eq "hello"
    end

    it "ignores stdout if output is FileDescriptor" do
      reader, writer = IO.pipe
      result = Process.capture_result?(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello\n"), output: writer).should be_a(Process::Result)
      result.status.success?.should be_true
      result.output?.should be_nil
      result.error?.should eq ""
      reader.gets.should eq "hello"
    end

    it "captures stderr" do
      result = Process.capture_result?(to_ary(shell_command("1>&2 echo hello"))).should be_a(Process::Result)
      result.status.success?.should be_true
      result.output?.should eq ""
      result.error?.should eq "hello#{newline}"
    end

    it "ignores stderr if error is IO" do
      io = IO::Memory.new
      result = Process.capture_result?(to_ary(shell_command("1>&2 echo hello")), error: io).should be_a(Process::Result)
      result.status.success?.should be_true
      result.output?.should eq ""
      result.error?.should be_nil
      io.to_s.should eq "hello#{newline}"
    end

    it "ignores stderr if error is FileDescriptor" do
      reader, writer = IO.pipe
      result = Process.capture_result?(to_ary(shell_command("1>&2 echo hello")), error: writer).should be_a(Process::Result)
      result.status.success?.should be_true
      result.output?.should eq ""
      result.error?.should be_nil
      reader.gets.should eq "hello"
    end

    it "doesn't capture closed stdout" do
      result = Process.capture_result?(to_ary(shell_command("echo hello")), output: :close).should be_a(Process::Result)
      result.output?.should be_nil
      result.error?.should_not be_nil
    end

    it "doesn't capture closed stderr" do
      # FIXME: Autocasting breaks in the interpreter
      result = Process.capture_result?(to_ary(shell_command("1>&2 echo hello")), error: Process::Redirect::Close).should be_a(Process::Result)
      result.status.success?.should be_true
      result.output?.should eq ""
      result.error?.should be_nil
    end

    it "truncates error output", tags: %w[slow] do
      dashes32 = "-" * (32 << 10)
      input = IO::Memory.new("#{dashes32}X#{dashes32}")
      result = Process.capture_result?(to_ary(stdin_to_stderr_command), input: input).should be_a(Process::Result)
      result.status.success?.should be_true
      result.output?.should eq ""
      error = result.error.should be_a(String)
      error.should contain "\n...omitted 1 bytes...\n"
      error.count("-").should eq(32 << 11)
    end

    it "reports status" do
      result = Process.capture_result?(to_ary(exit_code_command(0))).should be_a(Process::Result)
      result.status.exit_code.should eq(0)
      result = Process.capture_result?(to_ary(exit_code_command(123))).should be_a(Process::Result)
      result.status.exit_code.should eq(123)
    end

    it "raises if process cannot execute" do
      Process.capture_result?(["foobarbaz"]).should be_nil
    end
  end

  describe ".capture" do
    it "captures stdout" do
      Process.capture(to_ary(shell_command("echo hello"))).should eq "hello#{newline}"
    end

    it "captures stdout from stdin" do
      Process.capture(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello")).chomp.should eq "hello"
    end

    it "raises on non-zero exit status" do
      error = expect_raises(Process::ExitError, /^Command \[.*exit 1.*\] failed: Process exited with status 1$/) do
        Process.capture(to_ary(exit_code_command(1)))
      end
      error.result.status.exit_code.should eq 1
    end

    it "raises if process cannot execute" do
      expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do
        Process.capture(["foobarbaz"])
      end
    end

    it "captures stderr in error message" do
      error = expect_raises(Process::ExitError) do
        Process.capture(to_ary(stdin_to_stderr_command(status: 1)), input: IO::Memory.new("hello"))
      end
      error.result.error.chomp.should eq "hello"
    end
  end

  describe ".capture?" do
    it "captures stdout" do
      Process.capture?(to_ary(shell_command("echo hello"))).should eq "hello#{newline}"
    end

    it "captures stdout from stdin" do
      Process.capture?(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello")).try(&.chomp).should eq "hello"
    end

    it "returns nil on unsuccessful exit" do
      Process.capture?(to_ary(exit_code_command(1))).should be_nil
    end

    it "raises if process cannot execute" do
      expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do
        Process.capture(["foobarbaz"])
      end
    end
  end

  describe ".on_interrupt" do
    it "compiles" do
      typeof(Process.on_interrupt { })
      typeof(Process.ignore_interrupts!)
      typeof(Process.restore_interrupts!)
    end
  end

  describe ".on_terminate" do
    it "compiles" do
      typeof(Process.on_terminate { })
      typeof(Process.ignore_interrupts!)
      typeof(Process.restore_interrupts!)
    end
  end

  {% unless flag?(:win32) %}
    describe "#signal(Signal::KILL)" do
      it "kills a process" do
        process = Process.new(*standing_command)
        process.signal(Signal::KILL).should be_nil
      ensure
        process.try &.wait
      end

      it "kills many process" do
        process1 = Process.new(*standing_command)
        process2 = Process.new(*standing_command)
        process1.signal(Signal::KILL).should be_nil
        process2.signal(Signal::KILL).should be_nil
      ensure
        process1.try &.wait
        process2.try &.wait
      end
    end
  {% end %}

  it "#terminate" do
    process = Process.new(*standing_command)
    process.exists?.should be_true
    process.terminated?.should be_false

    process.terminate
  ensure
    process.try(&.wait)
  end

  typeof(Process.new(*standing_command).terminate(graceful: false))

  describe ".debugger_present?" do
    it "compiles" do
      typeof(Process.debugger_present?)
    end
  end

  it ".exists?" do
    # On Windows killing a parent process does not reparent its children to
    # another existing process, so the following isn't guaranteed to work
    {% unless flag?(:win32) %}
      # We can't reliably check whether it ever returns false, since we can't predict
      # how PIDs are used by the system, a new process might be spawned in between
      # reaping the one we would spawn and checking for it, using the now available
      # pid.
      Process.exists?(Process.ppid).should be_true
    {% end %}

    process = Process.new(*standing_command)
    process.exists?.should be_true
    process.terminated?.should be_false

    # Kill, zombie now
    process.terminate
    {% if flag?(:win32) %}
      # Windows has no concept of zombie processes
      process.exists?.should be_false
      process.terminated?.should be_true
    {% else %}
      process.exists?.should be_true
      process.terminated?.should be_false
    {% end %}

    # Reap, gone now
    process.wait
    process.exists?.should be_false
    process.terminated?.should be_true
  end

  {% unless flag?(:win32) %}
    it ".pgid" do
      process = Process.new(*standing_command)
      Process.pgid(process.pid).should be_a(Int64)
      process.terminate
      Process.pgid.should eq(Process.pgid(Process.pid))
    ensure
      process.try(&.wait)
    end
  {% end %}

  {% unless flag?(:preview_mt) || flag?(:win32) %}
    describe ".fork" do
      it "executes the new process with exec" do
        with_tempfile("crystal-spec-exec") do |path|
          File.exists?(path).should be_false

          fork = Process.fork do
            Process.exec("/usr/bin/env", {"touch", path})
          end
          fork.wait

          File.exists?(path).should be_true
        end
      end

      typeof(Process.fork)
    end
  {% end %}

  describe ".exec" do
    it "redirects STDIN and STDOUT to files", tags: %w[slow] do
      with_tempfile("crystal-exec-stdin", "crystal-exec-stdout") do |stdin_path, stdout_path|
        File.write(stdin_path, "foobar")

        status, _, _ = compile_and_run_source <<-CRYSTAL
          command = #{stdin_to_stdout_command[0].inspect}
          args = #{stdin_to_stdout_command[1].to_a} of String
          stdin_path = #{stdin_path.inspect}
          stdout_path = #{stdout_path.inspect}
          File.open(stdin_path) do |input|
            File.open(stdout_path, "w") do |output|
              Process.exec(command, args, input: input, output: output)
            end
          end
          CRYSTAL

        status.success?.should be_true
        File.read(stdout_path).chomp.should eq("foobar")
      end
    end

    it "gets error from exec" do
      expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do
        Process.exec("foobarbaz")
      end
    end

    it "raises if chdir doesn't exist" do
      expect_raises(File::NotFoundError, "Error while changing directory: 'doesnotexist'") do
        Process.exec(*exit_code_command(1), chdir: "doesnotexist")
      end
    end

    it "does not change directory if exec fails" do
      with_tempfile("exec_chdir") do |path|
        Dir.mkdir_p(path)
        previous_cwd = Dir.current
        expect_raises(File::NotFoundError, "Error executing process: 'doesnotexist':") do
          Process.exec("doesnotexist", chdir: path)
        end
        Dir.current.should eq previous_cwd
      end
    end
  end

  describe ".chroot" do
    {% if flag?(:unix) && !flag?(:android) %}
      it "raises when unprivileged", tags: %w[slow] do
        status, output, _ = compile_and_run_source <<-'CRYSTAL'
          # Try to drop privileges. Ignoring any errors because dropping is only
          # necessary for a privileged user and it doesn't matter when it fails
          # for an unprivileged one.
          # This particular UID is often attributed to the `nobody` user.
          LibC.setuid(65534)

          begin
            Process.chroot(".")
            puts "FAIL"
          rescue ex : RuntimeError
            puts ex.os_error
          end
        CRYSTAL

        status.success?.should be_true
        output.should eq("EPERM\n")
      end
    {% end %}
  end
end
