# Contains the main implementation of the Contract class. # :main:Contract require 'test/unit/testcase' require 'test/unit/testresult' require 'test/unit/testsuite' require 'contract/exception' require 'contract/overrides' require 'contract/assertions' require 'contract/integration' # Represents a contract between Objects as a collection of test cases. # Objects are said to fulfill a contract if all test cases suceed. This # is useful for ensuring that Objects your code is getting behave in a # way that you expect them to behave so you can fail early or execute # different logic for Objects with different interfaces. # # The tests of the test suite will be run on a copy of the tested Object # so you can safely test its behavior without having to fear data loss. # By default Contracts obtain deep copies of Objects by serializing and # unserializing them with Ruby's +Marshal+ functionality. This will work # in most cases but can fail for Objects containing unserializable parts # like Procs, Files or Sockets. In those cases it is currently of your # responsibility to provide a fitting implementation by overwriting the # Contract.deep_copy method. In the future the contract library might # provide different implementations of it via Ruby's mixin mechanism. class Contract < Test::Unit::TestCase id = %q$Id: contract.rb 120 2005-02-11 20:39:14Z flgr $ current_version = id.split(" ")[2] unless defined?(Version) # The Version of the contract library you are using as String of the # 1.2.3 form where the digits stand for release, major and minor # version respectively. Version = "0.1.1" end # Returns true if the given object fulfills this contract. # This is useful for implementing dispatching mechanisms where you # want to hit different code branches based on whether an Object has # one or another interface. def self.fulfilled_by?(object) self.test(object).nil? end class << self # You can use contracts in +case+ ... +when+ statements or in # Module#signature checks. For example: # case obj # when Numeric then obj + 1 # when ListContract then obj + [1] # end alias :=== :fulfilled_by? end # Enforces that object implements this contract. If it does not an # Exception will be raised. This is useful for example useful when you # need to ensure that the arguments given to a method fulfill a given # contract. # # Note that using Module#enforce is a higher-level way of checking # arguments and return values for the conformance of a given type. You # might however still want to use Contract.enforce directly when you # need more flexibility. def self.enforce(object) reason = self.test(object) raise reason if reason end # Tests whether the given Object fulfils this contract. # # Note: This will return the first reason for the Object not fulfilling # the contract or +nil+ in case it fulfills it. def self.test(object, return_all = false) reasons = [] result = Test::Unit::TestResult.new result.add_listener(Test::Unit::TestResult::FAULT) do |fault| reason = Contract.fault_to_exception(fault, object, self) return reason unless return_all reasons << reason end self.suite.run(result, deep_copy(object)) return reasons unless result.passed? end # Same as Contract.test, but will return all reasons for the Object not # fulfilling the contract in an Array or nil in case of fulfillment. # (as an Array of Exceptions) or +nil+ in the case it does fulfill it. def self.test_all(object) test(object, true) end # This method is used internally for getting a copy of Objects that # the contract is checked against. By default it uses Ruby's +Marshal+ # functionality for obtaining a copy, but this can fail if the Object # contains unserializable parts like Procs, Files or Sockets. It is # currently your responsibility to provide a fitting implementation # of this by overwriting the method in case the default implementation # does not work for you. In the future the contract library might offer # different implementations for this via Ruby's mixin mechanism. def self.deep_copy(object) Marshal.load(Marshal.dump(object)) end # Fulfilling this Contract (via Module#fulfills) implies that the Object # is automatically compatible with the specified mixins which will then # be included automatically. For example the Enumerable relationship # could be expressed like this: # # class EnumerableContract < Contract # provides :each # implies Enumerable # end def self.implies(*mixins) mixins.each do |mixin| if not mixin.is_a?(Module) then raise(TypeError, "wrong argument type #{mixin.class} for " + "#{mixin.inspect} (expected Module)") end end @implications ||= Array.new @implications += mixins end class << self # Returns all implications of a given contract that were stated via # calling Contract.implies. attr_reader :implications end def self.implications() # :nodoc: @implications ||= Array.new ancestors[1 .. -1].inject(@implications) do |result, ancestor| if ancestor.respond_to?(:implications) then ancestor.implications + result else result end end.uniq end end