Skip to content

Commit 7b5e7ae

Browse files
Add Process.capture (#16773)
Implement `Process.capture`, `Process.capture?` and `Process.capture_result` from [RFC 0025](crystal-lang/rfcs#25). This implements the basic capture behaviour, which may be enhanced in the future. In particular, this patch misses truncation of the captured error stream which will be a follow-up (#16774).
1 parent 792d6a8 commit 7b5e7ae

File tree

2 files changed

+399
-0
lines changed

2 files changed

+399
-0
lines changed

spec/std/process_spec.cr

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ private def stdin_to_stdout_command
3030
{% end %}
3131
end
3232

33+
private def stdin_to_stderr_command(status = 0)
34+
{% if flag?(:win32) %}
35+
{"powershell.exe", {"-C", "while ($line = [Console]::In.ReadLine()) { [Console]::Error.WriteLine($line) }; exit #{status}"}}
36+
{% else %}
37+
{"/bin/sh", {"-c", "cat 1>&2; exit #{status}"}}
38+
{% end %}
39+
end
40+
3341
private def print_env_command
3442
{% if flag?(:win32) %}
3543
# cmd adds these by itself, clear them out before printing.
@@ -769,6 +777,244 @@ describe Process do
769777
end
770778
end
771779

780+
describe ".capture_result" do
781+
it "captures stdout" do
782+
result = Process.capture_result(to_ary(shell_command("echo hello")))
783+
result.status.success?.should be_true
784+
result.output?.should eq "hello#{newline}"
785+
result.error?.should eq ""
786+
end
787+
788+
it "captures stdout from stdin" do
789+
result = Process.capture_result(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello"))
790+
result.status.success?.should be_true
791+
result.output.chomp.should eq "hello"
792+
end
793+
794+
it "ignores stdout if output is IO" do
795+
io = IO::Memory.new
796+
result = Process.capture_result(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello"), output: io)
797+
result.status.success?.should be_true
798+
result.output?.should be_nil
799+
result.error?.should eq ""
800+
io.to_s.chomp.should eq "hello"
801+
end
802+
803+
it "ignores stdout if output is FileDescriptor" do
804+
reader, writer = IO.pipe
805+
result = Process.capture_result(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello\n"), output: writer)
806+
result.status.success?.should be_true
807+
result.output?.should be_nil
808+
result.error?.should eq ""
809+
reader.gets.should eq "hello"
810+
end
811+
812+
it "captures stderr" do
813+
result = Process.capture_result(to_ary(shell_command("1>&2 echo hello")))
814+
result.status.success?.should be_true
815+
result.output?.should eq ""
816+
result.error?.should eq "hello#{newline}"
817+
end
818+
819+
it "ignores stderr if error is IO" do
820+
io = IO::Memory.new
821+
result = Process.capture_result(to_ary(shell_command("1>&2 echo hello")), error: io)
822+
result.status.success?.should be_true
823+
result.output?.should eq ""
824+
result.error?.should be_nil
825+
io.to_s.should eq "hello#{newline}"
826+
end
827+
828+
it "ignores stderr if error is FileDescriptor" do
829+
reader, writer = IO.pipe
830+
result = Process.capture_result(to_ary(shell_command("1>&2 echo hello")), error: writer)
831+
result.status.success?.should be_true
832+
result.output?.should eq ""
833+
result.error?.should be_nil
834+
reader.gets.should eq "hello"
835+
end
836+
837+
it "doesn't capture closed stdout" do
838+
result = Process.capture_result(to_ary(shell_command("echo hello")), output: :close)
839+
result.output?.should be_nil
840+
result.error?.should_not be_nil
841+
end
842+
843+
it "doesn't capture closed stderr" do
844+
# FIXME: Autocasting breaks in the interpreter
845+
result = Process.capture_result(to_ary(shell_command("1>&2 echo hello")), error: Process::Redirect::Close)
846+
result.status.success?.should be_true
847+
result.output?.should eq ""
848+
result.error?.should be_nil
849+
end
850+
851+
pending "truncates error output", tags: %w[slow] do
852+
dashes32 = "-" * (32 << 10)
853+
input = IO::Memory.new("#{dashes32}X#{dashes32}")
854+
result = Process.capture_result(to_ary(stdin_to_stderr_command), input: input)
855+
result.status.success?.should be_true
856+
result.output?.should eq ""
857+
error = result.error.should be_a(String)
858+
error.should contain "\n...omitted 1 bytes...\n"
859+
error.count("-").should eq(32 << 11)
860+
end
861+
862+
it "reports status" do
863+
Process.capture_result(to_ary(exit_code_command(0))).status.exit_code.should eq(0)
864+
Process.capture_result(to_ary(exit_code_command(123))).status.exit_code.should eq(123)
865+
end
866+
867+
it "raises if process cannot execute" do
868+
expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do
869+
Process.capture_result(["foobarbaz"])
870+
end
871+
end
872+
end
873+
874+
describe ".capture_result?" do
875+
it "captures stdout" do
876+
result = Process.capture_result?(to_ary(shell_command("echo hello"))).should be_a(Process::Result)
877+
result.status.success?.should be_true
878+
result.output?.should eq "hello#{newline}"
879+
result.error?.should eq ""
880+
end
881+
882+
it "captures stdout from stdin" do
883+
result = Process.capture_result?(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello")).should be_a(Process::Result)
884+
result.status.success?.should be_true
885+
result.output.chomp.should eq "hello"
886+
end
887+
888+
it "ignores stdout if output is IO" do
889+
io = IO::Memory.new
890+
result = Process.capture_result?(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello"), output: io).should be_a(Process::Result)
891+
result.status.success?.should be_true
892+
result.output?.should be_nil
893+
result.error?.should eq ""
894+
io.to_s.chomp.should eq "hello"
895+
end
896+
897+
it "ignores stdout if output is FileDescriptor" do
898+
reader, writer = IO.pipe
899+
result = Process.capture_result?(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello\n"), output: writer).should be_a(Process::Result)
900+
result.status.success?.should be_true
901+
result.output?.should be_nil
902+
result.error?.should eq ""
903+
reader.gets.should eq "hello"
904+
end
905+
906+
it "captures stderr" do
907+
result = Process.capture_result?(to_ary(shell_command("1>&2 echo hello"))).should be_a(Process::Result)
908+
result.status.success?.should be_true
909+
result.output?.should eq ""
910+
result.error?.should eq "hello#{newline}"
911+
end
912+
913+
it "ignores stderr if error is IO" do
914+
io = IO::Memory.new
915+
result = Process.capture_result?(to_ary(shell_command("1>&2 echo hello")), error: io).should be_a(Process::Result)
916+
result.status.success?.should be_true
917+
result.output?.should eq ""
918+
result.error?.should be_nil
919+
io.to_s.should eq "hello#{newline}"
920+
end
921+
922+
it "ignores stderr if error is FileDescriptor" do
923+
reader, writer = IO.pipe
924+
result = Process.capture_result?(to_ary(shell_command("1>&2 echo hello")), error: writer).should be_a(Process::Result)
925+
result.status.success?.should be_true
926+
result.output?.should eq ""
927+
result.error?.should be_nil
928+
reader.gets.should eq "hello"
929+
end
930+
931+
it "doesn't capture closed stdout" do
932+
result = Process.capture_result?(to_ary(shell_command("echo hello")), output: :close).should be_a(Process::Result)
933+
result.output?.should be_nil
934+
result.error?.should_not be_nil
935+
end
936+
937+
it "doesn't capture closed stderr" do
938+
# FIXME: Autocasting breaks in the interpreter
939+
result = Process.capture_result?(to_ary(shell_command("1>&2 echo hello")), error: Process::Redirect::Close).should be_a(Process::Result)
940+
result.status.success?.should be_true
941+
result.output?.should eq ""
942+
result.error?.should be_nil
943+
end
944+
945+
pending "truncates error output", tags: %w[slow] do
946+
dashes32 = "-" * (32 << 10)
947+
input = IO::Memory.new("#{dashes32}X#{dashes32}")
948+
result = Process.capture_result?(to_ary(stdin_to_stderr_command), input: input).should be_a(Process::Result)
949+
result.status.success?.should be_true
950+
result.output?.should eq ""
951+
error = result.error.should be_a(String)
952+
error.should contain "\n...omitted 1 bytes...\n"
953+
error.count("-").should eq(32 << 11)
954+
end
955+
956+
it "reports status" do
957+
result = Process.capture_result?(to_ary(exit_code_command(0))).should be_a(Process::Result)
958+
result.status.exit_code.should eq(0)
959+
result = Process.capture_result?(to_ary(exit_code_command(123))).should be_a(Process::Result)
960+
result.status.exit_code.should eq(123)
961+
end
962+
963+
it "raises if process cannot execute" do
964+
Process.capture_result?(["foobarbaz"]).should be_nil
965+
end
966+
end
967+
968+
describe ".capture" do
969+
it "captures stdout" do
970+
Process.capture(to_ary(shell_command("echo hello"))).should eq "hello#{newline}"
971+
end
972+
973+
it "captures stdout from stdin" do
974+
Process.capture(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello")).chomp.should eq "hello"
975+
end
976+
977+
it "raises on non-zero exit status" do
978+
error = expect_raises(Process::ExitError, /^Command \[.*exit 1.*\] failed: Process exited with status 1$/) do
979+
Process.capture(to_ary(exit_code_command(1)))
980+
end
981+
error.result.status.exit_code.should eq 1
982+
end
983+
984+
it "raises if process cannot execute" do
985+
expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do
986+
Process.capture(["foobarbaz"])
987+
end
988+
end
989+
990+
it "captures stderr in error message" do
991+
error = expect_raises(Process::ExitError) do
992+
Process.capture(to_ary(stdin_to_stderr_command(status: 1)), input: IO::Memory.new("hello"))
993+
end
994+
error.result.error.chomp.should eq "hello"
995+
end
996+
end
997+
998+
describe ".capture?" do
999+
it "captures stdout" do
1000+
Process.capture?(to_ary(shell_command("echo hello"))).should eq "hello#{newline}"
1001+
end
1002+
1003+
it "captures stdout from stdin" do
1004+
Process.capture?(to_ary(stdin_to_stdout_command), input: IO::Memory.new("hello")).try(&.chomp).should eq "hello"
1005+
end
1006+
1007+
it "returns nil on unsuccessful exit" do
1008+
Process.capture?(to_ary(exit_code_command(1))).should be_nil
1009+
end
1010+
1011+
it "raises if process cannot execute" do
1012+
expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do
1013+
Process.capture(["foobarbaz"])
1014+
end
1015+
end
1016+
end
1017+
7721018
describe ".on_interrupt" do
7731019
it "compiles" do
7741020
typeof(Process.on_interrupt { })

0 commit comments

Comments
 (0)