#
# ex:ts=10000
# vim:sw=4:sts=4:tw=0
#
# EekMock - A system for mock objects in Ruby.
# 
# Copyright (C) 2004 Eivind Eklund.  All rights reserved.
# You may use this as you wish, WITH NO WARRANTY WHATSOEVER,
# and on the condition that you keep this disclaimer and credit.
#
# Dependencies: types.rb

#
# Introduction
# ------------
#
# EekMock is my attempt to venture into the space of Mock Object frameworks.
# There are a few goals that I want to accomplish with it:
#
# 1. Avoid the brittleness issues that often come with mock objects by allowing
#    limited expectations.
#
# 2. Bridge the gap between state based testing (which tends to use real
#    objects and thus verify how the objects work together) and "normal" state
#    based testing.
#
# 3. Make it possible to do "as much mocking is you feel like", slowly
#    introducing mocks alongside state based testing.
#
#
# The design of EekMock is different from all other mock frameworks I've seen
# so far (both for Ruby and other languages).  The primary difference is that
# EekMock can work as a PROXY for a real object, enforcing expectations as
# appropriate.  This makes it possible to add a mock inside a normal
# state-based test, and then just add expectations to the test as appropriate.
# It also means that the brittleness-issue should be less relevant than for
# other mock frameworks.
#
# The API of EekMock is primarily inspired by the Java projects EasyMock and
# jMock (which I've not directly worked with).
#
# The specific verification system comes from types.rb, to avoid having to
# rewrite that functionality.  I in general recommend being careful in adding
# type annotations to your actual program; if they are too strict, they hinder
# refactoring.  And since types.rb makes it extremely easy to add type
# annotations, it is very easy to end up with too many of them.
#
# 
#
#
# Example of use as a generic mock
# --------------------------------
#
# Very heavily inspired by a examples from Martin Fowler.
#
# class OrderEasyTester < Test::Unit::TestCase
#     TALISKER = "Talisker"
#     
#     def setup
#         @warehouse_control = EekMock.new(nil)
#         @warehouse_mock =    @warehouse_control.mock
#     end
# 
#     def test_FillingRemovesInventoryIfInStock
#         # setup
#         order = Order.new(TALISKER, 50)
#         
#         # expectations (EasyMock style)
#         @warehouse_control.capture { |capture|
#             capture.has_inventory(TALISKER, 50).retval = true
#             capture.remove(TALISKER, 50)
#         }
#
#         # OR (FIXME Must be implemented, somehow, and the interface made good
#         # for Ruby.  This is just a direct copy of jMock)
#
#         # expectations (jMock style)
#         @warehouse_control.expects(once()).method("has_inventory").
#              with(eq(TALISKER),eq(50)).
#              will(returnValue(true));
#         @warehouse_control.expects(once()).method("remove")
#             .with(eq(TALISKER), eq(50))
#             .after("hasInventory");
# 
#         # execute
#         @warehouse_control.execute { |warehouse_mock|
#             order.fill(warehouse_mock}
#         }
#         
#         # verify - the .verify is implict due to the use of execute above.
#         # if we have several mocks, we can verify each of them instead of
#         # using a block.
#         # @warehouse_control.verify
#         assert(order.filled?)
#     end
# 
#     def test_FillingDoesNotRemoveIfNotEnoughInStock
#         order = Order.new(TALISKER, 51)
# 
#         @warehouse_mock.has_inventory(TALISKER, 51)
#         @warehouse_control.retval = false
#         @warehouse_control.replay
# 
#         order.fill(@warehouse_mock)
# 
#         assert(!order.filled?)
#         @warehouse_control.verify()
#     end
# end
#
#
# Other references
# ----------------
# Mock object testing is a fairly complex business.  There is a tendency to
# create "brittle" tests - tests that fail too often.  EekMock attempts to make
# it easy to create the 'right' mocks - mocks that only fail when they are
# supposed to fail, and passes for irrelevant code changes.
#
# There is a pattern language for using mock objects at
#   http://www.mockobjects.com/MockObjectTestingPatterns.html
# This gives common-sense advice that is somewhat expensive to learn by
# oneself.  I recommend reading through it before starting with mock objects.
#
# Martin Fowler has an article on the difference between Mocks and Stubs; it's
# available from
#   http://www.martinfowler.com/articles/mocksArentStubs.html
#
# Charles Miller has interesting comments on ending up "overtesting" with
# mocks, and how they can get in the way.  See
#   http://fishbowl.pastiche.org/2003/12/16/overly_mocked
#
# A *large* set of patterns for automated testing, including an in-depth
# discussion of Mocks, is available from
#  http://tap.testautomationpatterns.com:8080/index.html
#
#
# Questions for evaluation
# ------------------------
#
# XXX Should there be some way to set up expectations for return values in
# proxying?
#
# XXX Should we be able to proxy return values and calls even if we are doing
# replay?
#
# XXX There should be some way to mix different expectation lists, to be able
# to have each of them define a sequence of calls, but allow various mixes.
# This CAN be done with the tracer, but it is probably better to allow this
# specific case explictly.
#
# XXX Should we allow parameter constraints?  See
# http://www.mockobjects.com/ParameterConstraints.html
# Maybe evaluate the arguments using === ?  Adding a simple
#     class Equals
#         def initalize(arg) @arg = arg end
#         def ===(other) @arg == other end
#     end
# should allow this to work very simply.
#
# XXX Some form of partial order dependency might be useful.
# If we allow creation of several log lists, and can create a dependency
# between the lists, this might work out.
# We might also allow a "group" of statements to be executed in any order (e.g,
# setters).  This could be a log list.
#
# XXX Should we have some sort of automated Dummy object factory that can give
# good error messages if accesses are attempted?  (useful for the fake return
# values, and could be exposed to the user...)
#
#
# "Competing" mock systems for Ruby
# ---------------------------------
#
# http://onestepback.org/software/flexmock/
# http://www.deveiate.org/code/Test-Unit-Mock.html
# Test::Mock in the ruby distribution
#    (from http://www.b13media.com/dev/ruby/mock.html)
# http://www.rubygarden.org/ruby?RubyTestingPatterns/CallRecorder
#

class EekMock
    #
    # Set up mocking of the object "mocked".
    #
    def initialize(mocked)
        @mocked = mocked
        @method_counts = {}
        @method_counts.default = 0
    end
    #
    # Get the mock object for this controller.
    #
    # Depending on state, this can be a logging proxy for "mocked",
    # a capture object for method calls later to be "replayed" (by doing the
    # actual tests), or a replay object.
    #
    def mock
        begin
            @mocked.__is_eek_mocked(self)
        rescue NoMethodError => e
            had_method_missing = false
            class <<@mocked
                instance_methods.each { |method|
                    next if method == "__id__" || method == "__send__" || method == "print"
                    print "Aliasing #{method}\n"
                    alias_method "eek_deep_proxy_#{method}", method
                    if method == "method_missing"
                        had_method_missing = true
                    end
                    undef_method(method)
                }
            end
            raise "Found it" if had_method_missing
            def @mocked.eek_proxied_method_missing(symbol, *args, &block)
                symbol = symbol.to_s
                if symbol == "eek_deep_proxy_missing_method"
                    raise NoMethodError, "Unknown method `#{symbol}' for #{inspect}#{self.class.name}"
                elsif (symbol =~ /^eek_deep_proxy_(.*)/)
                    eek_deep_proxy_missing_method($1, *args, &block)
                else
                    rc = __send__("eek_deep_proxy_#{symbol}", *args, &block)
                    @eek_deep_proxy_controller._register_call(symbol, args)
                    rc
                end
            end
            def @mocked.__is_eek_mocked(controller)
                @eek_deep_proxy_controller = controller
            end
            @mocked.__is_eek_mocked(self)
        end
        @mocked
    end
    # :nodoc:
    # 
    def _register_call(symbol, args)
        @method_counts[symbol.to_s] += 1
    end
    #
    # Start capturing events (method calls) on the mock.
    # These method calls will NOT be proxied to "mocked".
    #
    # The system support several named captures; this can be used to build
    # less brittle mocks.
    #
    def capture(log = "default", &block)
        raise NotImplementedError
    end
    #
    # Start replaying events.  Can only be called after start_capture.
    #
    def replay(log = "default", offset = 0)
        raise NotImplementedError
    end
    #
    # Set the return value for the last call done in a capture sequence.  If
    # the return value for a call is NOT set, the replay will return DUMMY
    # objects that ALWAYS raise if they receive a message (a call).
    #
    def retval=(value)
        raise NotImplementedError
    end
    #
    # Use a block to calculate the retval
    # block { |method_name, method_args, method_block|
    # }
    #
    def retval(&block)
        raise NotImplatementEdrror
    end
    #
    # FIXME Add a doraise call
    # FIXME Handle other aspects of raising
    #

    #
    # Start proxying calls to "mocked" and logging their parameters, return
    # values, and (for debugging) caller traces.
    #
    # The log can be run assertions against after execution.
    # Set log to nil to work as a pure proxy (for use alongside tracers)
    #
    def proxy(log = "default")
        raise NotImplementedError
    end
    # FIXME Name just "state" ?
    #     state  - the present state
    #               :proxy -   proxying
    #               :capture - doing capture (but then you probably shouldn't
    #                          have set the tracer yet)
    #               :replay -  doing replay
    def get_state
        raise NotImplementedError
    end

    # FIXME Add appropriate methods for these
    #     log -     name of the log we are presently working on (or nil if we
    #               are proxying and have no log)
    #     offset -  the offset in the log we are working on

    #
    # Set up tracer block to see all method calls on the mock, and be able to
    # change behaviour.
    # 
    # This works as follows:
    #
    # controller.tracer([<states>]) {
    #    |method,           # The name of the method called
    #     args,             # Array of arguments the method was called with
    #     controller|       # The controller we were called from
    #
    #     # Preamble - verify preconditions etc
    #
    #     # Do the actual call, and get the return value from it
    #     retval = controller.continue
    #
    #     # "Postamble" - can verify various aspects of the return value and
    #     # state (e.g, postconditions)
    # }
    #
    # The tracer can do the following calls:
    # .proxy -  switch to proxying mode
    # .replay - switch to replay of the appropriate log
    # .fail -   consider the method in question to have failed.
    #
    # If .proxy or .replay is called in the preamble, the state switch will be
    # in effect for the call.  If .proxy or .replay is called in the postamble,
    # it will be in effect for the next call.
    #
    def tracer(states = [:proxy, :capture, :replay], &block)
        raise NotImplementedError
    end
    #
    # Get the log data for a particular log key.
    #
    # Format:
    #  [[methodname, args, retval, caller]*]
    #
    # NOTE: It would potentially be very useful to be able to translate this to
    # something that could be run regexps on.  Consider doing that if you see
    # any case where it would be useful; I'm reluctant to make up an API for
    # that until I have experiences with some cases where it actually has been
    # or is useful.  Please feel free to submit your case as base data, too.
    #
    def log(log)
        raise NotImplementedError
    end
    #
    # Create a list of method calls to ignore.  This is useful for query
    # methods.
    #
    def ignore(*methods)
        raise NotImplementedError
    end
    #
    # Verify that we got all the calls we were supposed to.
    #
    def verify
        raise NotImplementedError
    end
    #
    # Report how many times a method was called.
    #
    # FIXME Is this a good interface?  Is this a good name?
    def mock_count(sym)
        @method_counts[sym.to_s]
    end
    #
    # FIXME Stolen text from FlexMock - should be used for re-implementation
    #
    # Handle all messages denoted by sym by calling the given block and passing
    # any parameters to the block. If we know exactly how many calls are to be
    # made to a particular method, we may check that by passing in the number
    # of expected calls as a second paramter.
    #
    def mock_handle(sym, expected_count=nil, &block)
        raise NotImplementedError
    end
    #
    # FIXME Stolen text from FlexMock - should be used for re-implementation
    #
    # Ignore all undefined (missing) method calls. 
    def mock_ignore_missing
        raise NotImplementedError
    end


    #
    # Mock a specific class
    #
    # Should work just like Test::Unit::Mock(klass)
    #
    def self.mock_class(klass)
        raise NotImplementedError
    end
end
