Using spec module is it possible to store what is printed?

I want to store what is printed from the code using puts or p, but I don’t find a good way to store the messages in such a way to be able to trace them back to which test they come from.

Therefore is my question is there some method to be able to store p messages associated with the tests it comes from?

Whats your use case? If possible could consider using the Log module which would then allow you to leverage Log - Crystal 1.7.2.

Otherwise your best bet would prob be to re-define/create your own printing methods that store metadata about it to an array or something and clear it before each test case. E.g.

PRINTS = [] of PrintContext

record PrintContext,
  message : String,
  file : String,
  line : Int32

def puts(message : String, file : String = __FILE__, line : Int32 = __LINE__) : Nil
  PRINTS << PrintContext.new message, file, line

  STDOUT.puts message
end

puts "foo" # => foo

puts "bar" # => bar

pp PRINTS.map &.message # => ["foo", "bar"]
PRINTS.clear
pp PRINTS.map &.message # => []

I work on the test runner for exercism, in some languages you can use a the “print” method to see what the code outputs, here is an example for python:

I want a way there I can store the output assisited with each test, I don’t know what the person using the test runner may end up printing. If I knew what was going to be printed I could find a way, I can also get p to work in a good way but not puts. But I guess I could modify the puts and p method to store the output in a file. I was just wondering if there were a “simplier” way.

Okay, so for this use case you probably want to redirect stdout (maybe also stderr) for the duration of a test into a memory buffer.

It’s possible to reopen the standard streams in order to redirect them, it’s just not very straighforward to implement.
The gist is something like this:

original_stdout = File.open("/dev/null")
original_stdout.reopen(STDOUT)
IO.pipe do |reader, writer|
  STDOUT.reopen(writer)
  begin
    puts "foo"
  ensure
    writer.close
    STDOUT.reopen(original_stdout)
  end
  puts reader.gets_to_end.upcase
end

We recently discussed this on discord: Discord

If you want to capture stderr as well, you need some more plumbing. @watzon iterated on that in Discord and this is what he came up with for his use case:

original_stdout = File.open("/dev/null")
original_stdout.reopen(STDOUT)

original_stderr = File.open("/dev/null")
original_stderr.reopen(STDERR)

begin
  stdout_reader, stdout_writer = IO.pipe
  stderr_reader, stderr_writer = IO.pipe
  STDERR.reopen(stderr_writer)
  STDOUT.reopen(stdout_writer)

  user_response = App.exec(request_data, runtime_response)
  user_stdout = stdout_reader.gets_to_end
  user_stderr = stderr_reader.gets_to_end

  return context.put_status(200).json({
    "response" => user_response,
    "stdout"   => user_stdout,
  })
rescue e : Exception
  error_string = String.build do |str|
    str << user_stderr
    str << e.message
    e.backtrace.each do |line|
      str << line
    end
  end
  return context.put_status(500).json({
    "stdout" => user_stdout,
    "stderr" => error_string.strip,
  })
ensure
  [
    stdout_reader, stdout_writer, stderr_reader,
    stderr_writer, original_stdout, original_stderr,
  ].each(&.try(&.close))
  STDOUT.reopen(original_stdout)
  STDERR.reopen(original_stderr)
end
2 Likes

I don’t know if this is really it. I have no problem getting the data, I want to be able to sort prints to their tests.

Say I have this test file:

describe "something" do
  it "does something" do
    some_method(5)
  end

  it "does something else" do
    some_method(10)
  end
end

And this method:

def some_method(number)
  if number == 5
    p "number is 5"
  elsif number == 10
    p "number is 10"
  end
end

I would like to be able to say:

On test_1 it gave the output: “number is 5” and on test_2 it gave the output: “number is 10”

I have found an “okay” way to be able to get this data and split it when using p, since I can read what is output to the console and then split everywhere it starts with an “F” or “.” but if you use puts and start a print statement with an “F” that would cause some issues.

You can use an around_each hook for that.

https://crystal-lang.org/api/1.7.2/Spec/Methods.html#around_each(&block:Example::Procsy->)-instance-method

Thanks!

Combining both of these methods made it work, I just have to figure out some file system things but that is not crystal related.

Thanks so much for all of the help, the update to the test runner is live:

1 Like