[geary/wip/794174-conversation-monitor-max-cpu: 1/3] Add a mock object mixin that can check call expectations on mocks.



commit 98a5e662b121fce9ef25ea32de393e15eafd4651
Author: Michael James Gratton <mike vee net>
Date:   Fri Mar 9 12:03:07 2018 +1100

    Add a mock object mixin that can check call expectations on mocks.
    
    * test/mock-object.vala: Add initial mock object implementation.
    
    * test/test-case.vala; Add some useful high level assertion functions.

 test/CMakeLists.txt   |    1 +
 test/meson.build      |    1 +
 test/mock-object.vala |  243 +++++++++++++++++++++++++++++++++++++++++++++++++
 test/test-case.vala   |  181 ++++++++++++++++++++++++++++++++++++-
 4 files changed, 423 insertions(+), 3 deletions(-)
---
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 40d161b..2d16ff0 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -3,6 +3,7 @@
 # Copyright 2016 Michael Gratton <mike vee net>
 
 set(TEST_LIB_SRC
+  mock-object.vala
   test-case.vala
 )
 
diff --git a/test/meson.build b/test/meson.build
index 86fde3c..7c44a8b 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -1,4 +1,5 @@
 geary_test_lib_sources = [
+  'mock-object.vala',
   'test-case.vala',
 ]
 
diff --git a/test/mock-object.vala b/test/mock-object.vala
new file mode 100644
index 0000000..59b4b56
--- /dev/null
+++ b/test/mock-object.vala
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2018 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+private interface Argument {
+
+    public abstract void assert(Object object) throws Error;
+
+}
+
+private class BoxArgument<T> : Object, Argument {
+
+    private T value;
+
+    internal BoxArgument(T value) {
+        this.value = value;
+    }
+
+    public new void assert(Object object) throws Error {
+        assert_true(
+            object is BoxArgument,
+            "Expected %s value".printf(this.get_type().name())
+        );
+        assert_true(this.value == ((BoxArgument<T>) object).value);
+    }
+
+        }
+
+private class IntArgument : Object, Argument {
+
+    private int value;
+
+    internal IntArgument(int value) {
+        this.value = value;
+    }
+
+    public new void assert(Object object) throws Error {
+        assert_true(object is IntArgument, "Expected int value");
+        assert_int(this.value, ((IntArgument) object).value);
+    }
+
+}
+
+/**
+ * Represents an expected method call on a mock object.
+ *
+ * An instance of this object is returned when calling {@link
+ * Mock.Object.expect_call}, and may be used to further specify
+ * expectations, such that the mock method should throw a specific
+ * error or return a specific value or object.
+ */
+public class ExpectedCall : Object {
+
+
+    public string name { get; private set; }
+    internal Object[]? args;
+    public Error? throw_error { get; private set; default = null; }
+    public Object? return_object { get; private set; default = null; }
+    public Variant? return_value { get; private set; default = null; }
+    public bool was_called { get; private set; default = false; }
+
+
+    internal ExpectedCall(string name, Object[]? args) {
+        this.name = name;
+        this.args = args;
+    }
+
+    public ExpectedCall returns_object(Object value) {
+        this.return_object = value;
+        return this;
+    }
+
+    public ExpectedCall returns_boolean(bool value) {
+        this.return_value = new GLib.Variant.boolean(value);
+        return this;
+    }
+
+    public ExpectedCall @throws(Error err) {
+        this.throw_error = err;
+        return this;
+    }
+
+    internal void called() {
+        this.was_called = true;
+    }
+
+}
+
+
+/**
+ * Denotes a class that is injected into code being tested.
+ *
+ * Mock objects are unit testing fixtures that are used to provide
+ * instances of specific classes or interfaces which are required by
+ * the code being tested. For example, if an object being tested
+ * requires certain objects to be passed in via its constructor or as
+ * arguments of method calls and uses these to implement its
+ * behaviour, mock objects that fulfil these requirements can be used.
+ *
+ * Mock objects provide a means of both ensuring code being tested
+ * makes expected method calls with expected arguments on its
+ * dependencies, and a means of orchestrating the return value and
+ * exceptions raised when these methods are called, if any.
+ *
+ * To specify a specific method should be called on a mock object,
+ * call {@link expect_call} with the name of the method and optionally
+ * the arguments that are expected. The returned {@link ExpectedCall}
+ * object can be used to specify any exception or return values for
+ * the method. After executing the code being tested, call {@link
+ * assert_expectations} to ensure that the actual calls made matched
+ * those expected.
+ */
+public interface MockObject {
+
+
+    public static Object box_arg<T>(T value) {
+        return new BoxArgument<T>(value);
+    }
+
+    public static Object int_arg(int value) {
+        return new IntArgument(value);
+    }
+
+    protected abstract Gee.Queue<ExpectedCall> expected { get; set; }
+
+
+    public ExpectedCall expect_call(string name, Object[]? args = null) {
+        ExpectedCall expected = new ExpectedCall(name, args);
+        this.expected.offer(expected);
+        return expected;
+    }
+
+    public void assert_expectations() throws Error {
+        assert_true(this.expected.is_empty,
+                    "%d expected calls not made".printf(this.expected.size));
+        reset_expectations();
+    }
+
+    public void reset_expectations() {
+        this.expected.clear();
+    }
+
+    protected bool boolean_call(string name, Object[] args, bool default_return)
+        throws Error {
+        ExpectedCall? expected = call_made(name, args);
+
+        bool return_value = default_return;
+        if (expected.return_value != null) {
+            return_value = expected.return_value.get_boolean();
+        }
+        return return_value;
+    }
+
+    protected R object_call<R>(string name, Object[] args, R default_return)
+        throws Error {
+        ExpectedCall? expected = call_made(name, args);
+
+        R? return_object = default_return;
+        if (expected.return_object != null) {
+            return_object = (R) expected.return_object;
+        }
+        return return_object;
+    }
+
+    protected R object_or_throw_call<R>(string name, Object[] args, Error default_error)
+        throws Error {
+        ExpectedCall? expected = call_made(name, args);
+
+        if (expected.return_object != null) {
+            throw default_error;
+        }
+        return expected.return_object;
+    }
+
+    protected void void_call(string name, Object[] args) throws Error {
+        call_made(name, args);
+    }
+
+    private ExpectedCall? call_made(string name, Object[] args) throws Error {
+        assert_false(this.expected.is_empty, "Unexpected call: %s".printf(name));
+
+        ExpectedCall expected = this.expected.poll();
+        assert_string(expected.name, name, "Unexpected call");
+        if (expected.args != null) {
+            assert_args(expected.args, args, "Call %s".printf(name));
+        }
+
+        expected.called();
+
+        if (expected.throw_error != null) {
+            throw expected.throw_error;
+        }
+
+        return expected;
+    }
+
+    private void assert_args(Object[]? expected_args, Object[]? actual_args, string context)
+        throws Error {
+        int args = 0;
+        foreach (Object expected in expected_args) {
+            if (args >= actual_args.length) {
+                break;
+            }
+
+            Object actual = actual_args[args];
+            string arg_context = "%s, argument #%d".printf(context, args++);
+
+            if (expected is Argument) {
+                ((Argument) expected).assert(actual);
+            } else if (expected != null) {
+                assert_true(
+                    actual != null,
+                    "%s: Expected %s, actual is null".printf(
+                        arg_context, expected.get_type().name()
+                    )
+                );
+                assert_true(
+                    expected.get_type() == actual.get_type(),
+                    "%s: Expected %s, actual: %s".printf(
+                        arg_context,
+                        expected.get_type().name(),
+                        actual.get_type().name()
+                    )
+                );
+                assert_equal(
+                    expected, actual,
+                    "%s: object value".printf(arg_context)
+                );
+            } else {
+
+            }
+        }
+
+        assert_int(
+            expected_args.length, actual_args.length,
+            "%s: argument list length".printf(context)
+        );
+    }
+
+}
diff --git a/test/test-case.vala b/test/test-case.vala
index b17ce89..78502d2 100644
--- a/test/test-case.vala
+++ b/test/test-case.vala
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2009 Julien Peeters
- * Copyright (C) 2017 Michael Gratton
+ * Copyright (C) 2017-2018 Michael Gratton <mike vee net>
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -17,12 +17,142 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
  *
  * Author:
- *     Julien Peeters <contact julienpeeters fr>
- *     Michael Gratton <mike vee net>
+ *  Julien Peeters <contact julienpeeters fr>
+ *  Michael Gratton <mike vee net>
  */
 
+
+public void assert_null(Object? actual, string? context = null)
+    throws Error {
+    if (actual != null) {
+        print_assert(context ?? "Object is non-null", null);
+        assert_not_reached();
+    }
+}
+
+public void assert_non_null(Object? actual, string? context = null)
+    throws Error {
+    if (actual == null) {
+        print_assert(context ?? "Object is null", null);
+        assert_not_reached();
+    }
+}
+
+public void assert_equal(Object expected, Object? actual, string? context = null)
+    throws Error {
+    if (expected != actual) {
+        print_assert(context ?? "Objects are not equal", null);
+        assert_not_reached();
+    }
+}
+
+public void assert_string(string expected, string? actual, string? context = null)
+    throws Error {
+    if (expected != actual) {
+        string a = expected;
+        if (a.length > 20) {
+            a = a[0:15] + "…";
+        }
+        string b = actual;
+        if (b.length > 20) {
+            b = b[0:15] + "…";
+        }
+        print_assert("Expected: \"%s\", was: \"%s\"".printf(a, b), context);
+        assert_not_reached();
+    }
+}
+
+public void assert_int(int expected, int actual, string? context = null)
+    throws Error {
+    if (expected != actual) {
+        print_assert("Expected: %d, was: %d".printf(expected, actual), context);
+        assert_not_reached();
+    }
+}
+
+public void assert_true(bool condition, string? context = null)
+    throws Error {
+    if (!condition) {
+        print_assert(context ?? "Expected true", null);
+        assert_not_reached();
+    }
+}
+
+public void assert_false(bool condition, string? context = null)
+    throws Error {
+    if (condition) {
+        print_assert(context ?? "Expected false", null);
+        assert_not_reached();
+    }
+}
+
+public void assert_error(Error expected, Error? actual, string? context = null) {
+    bool failed = false;
+    if (actual == null) {
+        print_assert(
+            "Expected error: %s %i, was null".printf(
+                expected.domain.to_string(), expected.code
+            ),
+            context
+        );
+        failed = true;
+    } else if (expected.domain != actual.domain ||
+               expected.code != actual.code) {
+        print_assert(
+            "Expected error: %s %i, was actually %s %i: %s".printf(
+                expected.domain.to_string(),
+                expected.code,
+                actual.domain.to_string(),
+                actual.code,
+                actual.message
+            ),
+            context
+        );
+        failed = true;
+    }
+
+    if (failed) {
+        assert_not_reached();
+    }
+}
+
+// XXX this shadows GLib.assert_no_error since that doesn't work
+public void assert_no_error(Error? err, string? context = null) {
+    if (err != null) {
+        print_assert(
+            "Unexpected error: %s %i: %s".printf(
+                err.domain.to_string(),
+                err.code,
+                err.message
+            ),
+            context
+        );
+        assert_not_reached();
+    }
+}
+
+private inline void print_assert(string message, string? context) {
+    string output = message;
+    if (context != null) {
+        output = "%s: %s".printf(context, output);
+    }
+    GLib.stderr.puts(output);
+    GLib.stderr.putc('\n');
+}
+
 public abstract class TestCase : Object {
 
+
+    private class SignalWaiter : Object {
+
+        public bool was_fired = false;
+
+        public void @callback(Object source) {
+            was_fired = true;
+        }
+    }
+
+
     protected MainContext main_loop = MainContext.default();
 
        private GLib.TestSuite suite;
@@ -75,6 +205,51 @@ public abstract class TestCase : Object {
         return result;
     }
 
+    /**
+     * Waits for a mock object's call to be completed.
+     *
+     * This method busy waits on the test's main loop until either
+     * until {@link ExpectedCall.was_called} is true, or until the
+     * given timeout in seconds has occurred.
+     *
+     * Returns //true// if the call was made, or //false// if the
+     * timeout was reached.
+     */
+    protected bool wait_for_call(ExpectedCall call, double timeout = 1.0) {
+        GLib.Timer timer = new GLib.Timer();
+        timer.start();
+        while (!call.was_called && timer.elapsed() < timeout) {
+            this.main_loop.iteration(false);
+        }
+        return call.was_called;
+    }
+
+    /**
+     * Waits for an object's signal to be fired.
+     *
+     * This method busy waits on the test's main loop until either
+     * until the object emits the named signal, or until the given
+     * timeout in seconds has occurred.
+     *
+     * Returns //true// if the signal was fired, or //false// if the
+     * timeout was reached.
+     */
+    protected bool wait_for_signal(Object source, string name, double timeout = 0.5) {
+        SignalWaiter handler = new SignalWaiter();
+        ulong id = GLib.Signal.connect_swapped(
+            source, name, (GLib.Callback) handler.callback, handler
+        );
+
+        GLib.Timer timer = new GLib.Timer();
+        timer.start();
+        while (!handler.was_fired && timer.elapsed() < timeout) {
+            this.main_loop.iteration(false);
+        }
+
+        source.disconnect(id);
+        return handler.was_fired;
+    }
+
        private class Adaptor {
 
                public string name { get; private set; }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]