Guards

Overview

The RubySpec project intends to provide a complete and exhaustive specification of the Ruby language and its libraries. Because Ruby is not completely isolated from its platform or execution environment, the spec files may contain guards: conditions placed around a spec or a set of specs to enable or disable them. The guard mechanism is currently implemented only in MSpec, although there should be no issues adding them to RSpec or other implementations. MSpec offers another means of inclusion and exclusion as well: tags and how they work together with guards is discussed in some detail further down.

Our reference implementation is generally the most recent stable release of the official Ruby implementation available from http://ruby-lang.org (often referred to as "MatzRuby" or "MRI.") The set of specs we have should describe the complete behaviour of that release all the way from how the if statement works to the behaviour of core classes like Hash and beyond. The specs are always updated to exactly specify that latest reference implementation as new releases come along. The reference implementation may also be referred to as "Standard," "Reference" or "Target." The current reference implementation version is always listed at Current Standard.

So, generally speaking the purpose of any given spec in our hierarchy is to specify how the Standard behaves for that particular aspect. If you see a plain spec, that spec is exactly the behaviour of the Standard. There are three cases in which guards would be used:

Platform Differences in the Standard

In some cases, MatzRuby exhibits different behaviour based on its execution environment. For example, the Kernel.fork method is not available on Windows and the maximum value of a Fixnum before it is promoted to a Bignum is different between 32-bit and 64-bit platforms. These and other similar cases, such as requiring superuser privileges for a Dir.chroot spec, are surrounded by guards:


  platform_is :wordsize => 32 do
    it "converts numbers to Bignums if they get too big" do
       # 32-bit behaviour goes here
    end
  end 

  platform_is :wordsize => 64 do
    it "converts numbers to Bignums if they get too big" do
      # 64-bit behaviour goes here
    end
  end

Here, clearly enough, a difference is expected between the two different platform wordsizes (notably, the guards should always be outside and around the #it blocks.) The guards are actually not connected to or dependent on eachother in any way; you could leave the 64-bit version out altogether. Typically all guards come in pairs so make sure you always implement both behaviours. In some cases a platform simply exhibits additional behaviour, which allows the spec writer to have an unguarded set of specs for the normal behaviour and then a set that augments that, guarded to only be run on the platforms that support it.

Bugs in the Standard

Sometimes a bug appears or is discovered in MatzRuby, either everywhere or specific to a certain platform or environment (the specs have unearthed dozens of bugs already.) The criteria for this particular guard is fairly stringent: at the very least, a bug must be filed against Ruby on their bug tracker. The bug ID is actually an argument (optional for now) to the #ruby_bug guard. It is advisable to first discuss the potential bug either among your fellow RubySpec developers or on the ruby-core mailing list, however, in order to verify that we are in fact dealing with an actual bug and not simply some unexpected behaviour. One of these guards might look like this:


  ruby_bug "#someidnumber" do
    it "produces 2 when adding 1 to 1" do
      # CORRECT implementation goes here
      (1 + 1).should == 2
    end
  end

The most notable thing here is that the implementation of the spec is what should happen, i.e. the correct expectation. The guard actually only stops this spec from running on MatzRuby: other implementations are expected to implement the correct behaviour. The guards are naturally removed when we move to a new reference implementation that behaves correctly.

Ruby Implementation Differences

If you only work with the specs as they relate to the reference implementation, you will never need to create these types of guards, but you may run into some perusing the existing specs. Broadly speaking all of the different alternative Ruby implementations, Rubinius, JRuby, IronRuby and so on, should exhibit the exact same behaviour as the reference implementation but in practice this is not always the case. There is also a bit of a conflict of interest: these being RubySpecs, we would prefer to not have a large amount of implementation guards littering them.

Tags are the alternative mechanism offered by MSpec for this purpose. They allow the users of the specs to locally exclude some specs based on their own criteria, without touching the specs themselves (presumably they will also have a separate, parallel subset of specs that is considered "correct" behaviour for that implementation in the case at hand.) In the future it is likely that the number of implementation guards will lessen in favour of using tags, but for now the guideline has been that tags are used for transient problems, for example to exclude a spec that the implementation is not capable of running yet. Guards, on the other hand, have been used to indicate a permanent difference. There are also two different types of implementation differences: most often a feature is just not supported in an implementation:


  describe "Kernel.fork" do
    not_supported_on :jruby do
      it "does forky things" do
        # Whatever the spec is
      end
    end
  end

So here, because JRuby does not and probably never will support forking, the fork spec or specs are excluded from running on that implementation. Rubinius would use the symbol :rubinius and so on; the only exception is that the reference implementation is always called just :ruby.

The second case is that an implementation may implement a feature differently from the reference implementation. Rubinius, for example, has a Fixnum whose maximum value before conversion to Bignum is platform-independent, unlike MatzRuby. Here, two guards must be used:


  not_compliant_on :rubinius do
    it "automatically converts from a Fixnum to a Bignum when blah blah" do
      # Spec
    end
  end

  compliant_on :ruby, :jruby do
    platform_is :wordsize => 32 do
      # 32-bit spec
    end

    platform_is :wordsize => 64 do
      # 64-bit spec
    end
  end

The first guard, while perhaps a bit unwieldy, indicates that the contained spec should be run on Rubinius but that it is not in compliance with the reference implementation. Correspondingly, the second one indicates to run the spec on the listed implementations and that the spec is in accordance with the reference implementation. (#compliant_on must currently have the :ruby specified although it is kind of implied already. This is likely to change in the near future.)

Version Guards

Sometimes the behavior of certain methods will change between different Ruby versions (or revisions!). If you're writing specs against several different patchlevels you can use the ruby_version_is guard:


ruby_version_is "" ... "1.9" do
  it "adds two numbers" do
    (1+1).should == 2
  end        
end

ruby_version_is "1.9" do
  it "adds two numbers" do
    (1+1).should == 3
  end        
end

In this case we're assuming that 1 + 1 will equal 2 in every version before 1.9 and that in 1.9 and following versions it will return 3.

The range syntax works just like normal Ranges:


"a".."b"  # => "a" to "b", including "b".
"a"..."b" # => "a" to "b", excluding "b".
"a"       # => covers everything before "a" 

Guard Semantics

The exact semantics always depend on the particular guard in question. Usually they are self-evident but the MSpec Wiki contains a thorough description of all guard types and their uses. Two things are always the same, though: if a guard has multiple parameters, those are combined using OR logic: compliant_on :ruby, :jruby means either one of the two, not both (obviously enough.) If AND logic is desired ("Rubinius on 32-bit platforms"), the guards should be nested instead. The outer guards are naturally evoked before getting to the inner ones.

Also available in: HTML TXT