mocks.cr icon indicating copy to clipboard operation
mocks.cr copied to clipboard

Is it possible to mock File.each_line with a block?

Open ndbroadbent opened this issue 6 years ago • 1 comments

Hello,

I'm using Crystal 0.31.1.

I'm trying to mock a call File.each_line(path), and I wasn't sure if mocks.cr supports this? Here's an example of what I'm trying to do:

class TestClass
  @filename : String
  @contents : String

  def initialize(@filename : String)
    @contents = ""
  end

  def process
    @contents = String.build do |io|
      File.each_line(@filename) do |line|
        io << line
      end
    end
  end
end

And I would like to write a spec like this:

require "./spec_helper"

Spectator.describe TestClass do
  it "reads the file lines into memory" do
    # Sorry I don't think this line is correct, but I'm not sure how to do it.
    file_mock = Mocks.class_double(File)
    allow(file_mock).to receive(
      self.each_line("example_path")
    ).and_yield(["hey, world!", "how's it hanging", "this is a line in a file"])
    
    instance = TestClass.new("example_path")
    instance.process
    expect(instance.contents[0]).to eq("hey, world!")
  end 
end

Can this be achieved with mocks.cr, or should I be doing something else?

It would also be great if you could add some usage examples for mocking File.read and File.each_line. Thanks!

ndbroadbent avatar Dec 06 '19 06:12 ndbroadbent

Oh cool, this example spec was really helpful: https://github.com/waterlink/mocks.cr/blob/master/spec/mocks/create_module_mock_spec.cr

So I figured out that I can do this in the meantime:

# src/test_class.cr
class TestClass
  getter :contents
  @filename : String
  @contents : String

  def initialize(@filename : String)
    @contents = ""
  end

  def process
    @contents = String.build do |io|
      lines = File.read_lines(@filename)
      lines.each do |line|
        io << line
      end
    end
  end
end
# spec_helper.cr
require "../src/*"
require "spec"
require "spectator"
require "mocks/spec"
# spec/test_class_spec.cr

require "./spec_helper"

Mocks.create_mock File do
  mock self.read_lines(filename, encoding = nil, invalid = nil, chomp = true)
end

Spectator.describe TestClass do
  it "reads the file lines into memory" do
    allow(File).to receive(self.read_lines("example_file")).and_return(
      ["hey, world!", "how's it hanging"])
    instance = TestClass.new("example_path")
    instance.process
    expect(instance.contents[0]).to eq("hey, world!")
  end
end

So I'm making progress! But unfortunately this crashes with an "Arithmetic overflow" in lib/mocks/src/mocks/registry.cr

$ crystal spec
E

Failures:

  1) TestClass reads the file lines into memory
     Error: Arithmetic overflow

       OverflowError: Arithmetic overflow
         lib/mocks/src/mocks/registry.cr:86:24 in 'hash'
         /usr/local/Cellar/crystal/0.31.1/src/hash.cr:893:12 in 'key_hash'
         /usr/local/Cellar/crystal/0.31.1/src/hash.cr:336:5 in 'upsert'
         /usr/local/Cellar/crystal/0.31.1/src/hash.cr:912:5 in '[]='
         lib/mocks/src/mocks/registry.cr:103:9 in 'add'
         lib/mocks/src/mocks/registry.cr:155:9 in 'store_stub'
         lib/mocks/src/mocks/allow.cr:16:7 in 'to'
         spec/process_comments_spec.cr:33:16 in '__temp_46'
         spec/process_comments_spec.cr:30:3 in 'run_instance'
         lib/spectator/src/spectator/runnable_example.cr:74:11 in 'run_example'
         lib/spectator/src/spectator/runnable_example.cr:64:9 in '->'
         lib/spectator/src/spectator/runnable_example.cr:255:3 in 'run_wrapper'
         lib/spectator/src/spectator/runnable_example.cr:44:9 in 'capture_result'
         lib/spectator/src/spectator/runnable_example.cr:12:7 in 'run_impl'
         lib/spectator/src/spectator/example.cr:30:7 in 'run'
         lib/spectator/src/spectator/internals/harness.cr:29:7 in 'run'
         lib/spectator/src/spectator/runner.cr:60:18 in 'run_example'
         lib/spectator/src/spectator/runner.cr:35:9 in 'collect_results'
         lib/spectator/src/spectator/runner.cr:356:5 in 'run'
         lib/spectator/src/spectator.cr:113:5 in 'run'
         lib/spectator/src/spectator.cr:87:29 in '->'
         /usr/local/Cellar/crystal/0.31.1/src/kernel.cr:255:3 in 'run'
         /usr/local/Cellar/crystal/0.31.1/src/crystal/main.cr:47:5 in 'main'
         /usr/local/Cellar/crystal/0.31.1/src/crystal/main.cr:106:3 in 'main'

     # spec/process_comments_spec.cr:30

Finished in 67 microseconds
1 examples, 1 failures, 1 errors, 0 pending

Failed examples:

crystal spec spec/process_comments_spec.cr:30 # TestClass reads the file lines into memory


Finished in 76.04 milliseconds
0 examples, 0 failures, 0 errors, 0 pending

UPDATE:

I added this code to the crashing method:

      def hash
        puts "object_id.hash: #{object_id.hash}"
        puts "args.hash: #{args.hash}"
        puts "object_id.hash * 32: #{object_id.hash * 32}"
        object_id.hash * 32 + args.hash
      end

Output:

object_id.hash: 7261337864297883403
args.hash: 6437199635058613238

(Crashes on object_id.hash * 32.)

UPDATE 2

I sent a PR to prevent this overflow crash with some bit shifts, but not sure if that's the best solution.

ndbroadbent avatar Dec 06 '19 06:12 ndbroadbent