Persistent expectations

An RSpec example usually consists of some code to set up the situation being tested, and some code called an expectation that defines the expected outcome. As discussed in the Pins page, to write an expectation that the main output pin is driving high you could write:

expect(main_output).to be_driving_high

This is fine, but it is not a great way to test firmware because (unless you put it in a loop or method) it only runs once, at one particular cycle of the simulation. It will not catch any accidental glitches on the main output pin that occur at a later time.

RPicSim helps to address this by adding a new feature to RSpec examples called “persistent expectations”. Persistent expectations are implemented in the module RPicSim::RSpec::PersistentExpectations, which is part of RPicSim's RSpec integration.

Usage

To set a persistent expectation, call the expecting method inside an RSpec example or a before/after hook:

expecting main_output => be_driving_high

The argument to expecting is a hash with the objects being tested as the keys, and the matchers they are being tested against as the values. You can specify multiple persistent expectations on different objects:

expecting main_output => be_driving_high, error_output => be_drving_low

You can not specify multiple persistent expectations that apply to the same object. If you specify a persistent expectation for an object that already had one, the latest one you specify will override the previous one.

To remove a persistent expectation, specify a matcher of nil:

expecting main_output => nil

If expecting is given a block, expectations will only be valid for the duration of the block:

# Verify that the main output stays high for 10 cycles.
expecting main_output => be_driving_high do
  # The expectation will be checked within the block.
  run_cycles 10
end
# The expectation will not be checked here.

The persistent expectations will not be checked immediately when they are added, but they will be checked after every step of the simulation. You can also check them at any time by calling check_expecations inside your RSPec example.

Persistent expectations, when combined with RSpec's satisfy matcher, are very powerful. If counter is a variable in your simulation, you could use this code to ensure that counter never goes above 120:

expecting counter => satisfy { |c| c.value <= 120 }

Persistent expectations are implemented in a straightforward way: the expectations are stored in a hash that is an instance variable of the RSpec example, and the expectations are checked after every step via a hook that is registered with RPicSim::Sim#every_step when the simulation is started.

Example

The following RSpec example tests that the main output pin is held low (after giving the device some time to start up), but then it goes high after the main input goes high:

it "mirrors the main input onto the main output pin" do
  run_cycles 120    # Give the device time to start up.

  expecting main_output => be_driving_low
  run_cycles 800

  main_input.set true

  # Turn off the persistent expectation temporarily to give the device
  # time to detect the change in the input.
  expecting main_output => nil
  run_cycles 200

  expecting main_output => be_driving_high
  run_cycles 800
end

In the above example, we removed the persistent expectation on main_output temporarily because the device was in a transitionary period and we didn't know exactly when the transition would happen. We chose to stop monitoring the pin for the duration of the transition and then start monitoring it later, at which point we expect the pin to be in its new state. We can rewrite that using block arguments instead of explicitly clearing the expectation:

it "mirrors the main input onto the main output pin" do
  run_cycles 120    # Give the device time to start up.

  expecting main_output => be_driving_low do
    run_cycles 800
  end

  main_input.set true

  # Give the device time to detect the change in the input.
  run_cycles 200

  expecting main_output => be_driving_high do
    run_cycles 800
  end
end

If you need to repeat this patten many times in your tests, you might consider adding a method in your spec_helper.rb to help you do it:

def transition(opts={})
  opts = opts.dup
  cycles = opts.delete(:cycles) || 50
  opts.keys.each { |k| expectations.delete k }
  run_cycles cycles
  expectations.merge! opts
  check_expectations
end

Then the test above could become:

it "mirrors the main input onto the main output pin" do
  run_cycles 120    # Give the device time to start up.

  expecting main_output => be_driving_low
  run_cycles 800

  main_input.set true

  transition main_output => be_driving_high
  run_cycles 800
end

The transition method above does not check anything about the main output during the transition time, so unfortunately it might miss any glitches that happen during that time. Also, it is not very general. For these reasons, it has not been integrated into the RPicSim code and you will need to copy it to your spec_helper.rb file yourself if you want to use it.