From c50228a34821fe9cad2da2e294c9ab3c5d79266b Mon Sep 17 00:00:00 2001
From: Robert Scott <code@humanleg.org.uk>
Date: Tue, 13 Feb 2018 20:59:31 +0000
Subject: [PATCH v2 03/28] testing: add ExtendedDialogMocker, WindowMocker

---
 .../josm/testutils/mockers/BaseDialogMockUp.java   |  50 +++++++
 .../testutils/mockers/ExtendedDialogMocker.java    | 158 +++++++++++++++++++++
 .../josm/testutils/mockers/WindowMocker.java       |  39 +++++
 3 files changed, 247 insertions(+)
 create mode 100644 test/unit/org/openstreetmap/josm/testutils/mockers/BaseDialogMockUp.java
 create mode 100644 test/unit/org/openstreetmap/josm/testutils/mockers/ExtendedDialogMocker.java
 create mode 100644 test/unit/org/openstreetmap/josm/testutils/mockers/WindowMocker.java

diff --git a/test/unit/org/openstreetmap/josm/testutils/mockers/BaseDialogMockUp.java b/test/unit/org/openstreetmap/josm/testutils/mockers/BaseDialogMockUp.java
new file mode 100644
index 000000000..0e2a5bf8e
--- /dev/null
+++ b/test/unit/org/openstreetmap/josm/testutils/mockers/BaseDialogMockUp.java
@@ -0,0 +1,50 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import mockit.MockUp;
+
+/**
+ * Abstract class implementing the few common features of the dialog-mockers which are readily factorable.
+ */
+abstract class BaseDialogMockUp<T> extends MockUp<T> {
+    private final List<Object[]> invocationLog;
+
+    /**
+     * @return an unmodifiable view of the internal invocation log. Each entry is an array of Objects to
+     *     allow for more advanced implementations to be able to express their invocations in their own
+     *     ways. Typically the invocation's "result value" is used as the first element of the array.
+     */
+    public List<Object[]> getInvocationLog() {
+        return this.invocationLog;
+    }
+
+    private final List<Object[]> invocationLogInternal = new ArrayList<Object[]>(4);
+
+    /**
+     * @return the actual (writable) invocation log
+     */
+    protected List<Object[]> getInvocationLogInternal() {
+        return this.invocationLogInternal;
+    }
+
+    private final Map<String, Object> mockResultMap;
+
+    /**
+     * @return mapping to {@link Object}s so response button can be specified by String (label) or Integer
+     *     - sorry, no type safety as java doesn't support union types
+     */
+    public Map<String, Object> getMockResultMap() {
+        return this.mockResultMap;
+    }
+
+    BaseDialogMockUp(final Map<String, Object> mockResultMap) {
+        this.mockResultMap = mockResultMap != null ? mockResultMap : new HashMap<String, Object>(4);
+        this.invocationLog = Collections.unmodifiableList(this.invocationLogInternal);
+    }
+}
diff --git a/test/unit/org/openstreetmap/josm/testutils/mockers/ExtendedDialogMocker.java b/test/unit/org/openstreetmap/josm/testutils/mockers/ExtendedDialogMocker.java
new file mode 100644
index 000000000..f0fb62f29
--- /dev/null
+++ b/test/unit/org/openstreetmap/josm/testutils/mockers/ExtendedDialogMocker.java
@@ -0,0 +1,158 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils;
+
+import static org.junit.Assert.fail;
+
+import java.awt.Component;
+import java.awt.GraphicsEnvironment;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import java.util.WeakHashMap;
+
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.tools.Logging;
+
+import mockit.Deencapsulation;
+import mockit.Invocation;
+import mockit.Mock;
+
+/**
+ * MockUp for {@link ExtendedDialog} allowing a test to pre-seed uses of {@link ExtendedDialog}
+ * with mock "responses". This works best with {@link ExtendedDialog}s which have their contents set
+ * through {@link ExtendedDialog#setContent(String)} as simple strings. In such a case, responses can
+ * be defined through a mapping from content {@link String}s to button indexes ({@link Integer}s) or
+ * button labels ({@link String}s). Example:
+ *
+ * <pre>
+ *      new ExtendedDialogMocker(ImmutableMap.&lt;String, Object&gt;builder()
+ *          .put("JOSM version 8,001 required for plugin baz_plugin.", "Download Plugin")
+ *          .put("JOSM version 7,001 required for plugin dummy_plugin.", "Cancel")
+ *          .put("Are you sure you want to do foo bar?", ExtendedDialog.DialogClosedOtherwise)
+ *          .build()
+ *      );
+ * </pre>
+ * 
+ * Testing examples with more complicated contents would require overriding
+ * {@link #getString(ExtendedDialog)} or even {@link #getMockResult(ExtendedDialog)} with custom logic.
+ * The class is implemented as a number of small methods with the main aim being to allow overriding of
+ * only the parts necessary for a particular case.
+ *
+ * The default {@link #getMockResult(ExtendedDialog)} will raise an
+ * {@link junit.framework.AssertionFailedError} on an {@link ExtendedDialog} activation without a
+ * matching mapping entry or if the named button doesn't exist.
+ *
+ * The public {@link #getMockResultMap()} method returns the modifiable result map to allow for situations
+ * where the desired result might need to be changed mid-test.
+ */
+public class ExtendedDialogMocker extends BaseDialogMockUp<ExtendedDialog> {
+    /**
+     * Because we're unable to add fields to the mocked class, we need to use this external global
+     * mapping to be able to keep a note of the most recently set simple String contents of each
+     * {@link ExtendedDialog} instance - {@link ExtendedDialog} doesn't store this information 
+     * itself, instead converting it directly into the embedded {@link Component}.
+     */
+    protected final Map<ExtendedDialog, String> simpleStringContentMemo = new WeakHashMap<ExtendedDialog, String>();
+
+    /**
+     * Construct an {@link ExtendedDialogMocker} with an empty {@link #mockResultMap}.
+     */
+    public ExtendedDialogMocker() {
+        this(null);
+    }
+
+    /**
+     * Construct an {@link ExtendedDialogMocker} with the provided {@link #mockResultMap}.
+     * @param mockResultMap mapping of {@link ExtendedDialog} string contents to
+     *      result button label or integer index.
+     */
+    public ExtendedDialogMocker(final Map<String, Object> mockResultMap) {
+        super(mockResultMap);
+        if (GraphicsEnvironment.isHeadless()) {
+            new WindowMocker();
+        }
+    }
+
+    protected int getButtonPositionFromLabel(final ExtendedDialog instance, final String label) {
+        final String[] bTexts = Deencapsulation.getField(instance, "bTexts");
+        final int position = Arrays.asList(bTexts).indexOf(label);
+        if (position == -1) {
+            fail("Unable to find button labeled \"" + label + "\". Instead found: " + Arrays.toString(bTexts));
+        }
+        return position;
+    }
+
+    protected String getString(final ExtendedDialog instance) {
+        return Optional.ofNullable(this.simpleStringContentMemo.get(instance))
+            .orElseGet(() -> instance.toString());
+    }
+
+    protected int getMockResult(final ExtendedDialog instance) {
+        final String stringContent = this.getString(instance);
+        final Object result = this.getMockResultMap().get(stringContent);
+
+        if (result == null) {
+            fail(
+                "Unexpected ExtendedDialog content: " + stringContent
+            );
+        } else if (result instanceof Integer) {
+            return (Integer) result;
+        } else if (result instanceof String) {
+            // buttons are numbered with 1-based indexing
+            return 1 + this.getButtonPositionFromLabel(instance, (String) result);
+        }
+
+        throw new IllegalArgumentException(
+            "ExtendedDialog contents mapped to unsupported type of Object: " + result
+        );
+    }
+
+    protected Object[] getInvocationLogEntry(final ExtendedDialog instance, final int mockResult) {
+        return new Object[] {
+            (Integer) mockResult,
+            this.getString(instance),
+            instance.getTitle()
+        };
+    }
+
+    @Mock
+    private void setupDialog(final Invocation invocation) {
+        if (!GraphicsEnvironment.isHeadless()) {
+            invocation.proceed();
+        }
+        // else do nothing - WindowMocker-ed Windows doesn't work well enough for some of the
+        // component constructions
+    }
+
+    @Mock
+    private void setVisible(final Invocation invocation, final boolean value) {
+        if (value == true) {
+            final ExtendedDialog instance = invocation.getInvokedInstance();
+            final int mockResult = this.getMockResult(instance);
+            // TODO check validity of mockResult?
+            Deencapsulation.setField(instance, "result", mockResult);
+            Logging.info(
+                "{0} answering {1} to ExtendedDialog with content {2}",
+                this.getClass().getName(),
+                mockResult,
+                this.getString(instance)
+            );
+            this.getInvocationLogInternal().add(this.getInvocationLogEntry(instance, mockResult));
+        }
+    }
+
+    @Mock
+    private ExtendedDialog setContent(final Invocation invocation, final String message) {
+        final ExtendedDialog retval = invocation.proceed(message);
+        // must set this *after* the regular invocation else that will fall through to
+        // setContent(Component, boolean) which would overwrite it (with null)
+        this.simpleStringContentMemo.put((ExtendedDialog) invocation.getInvokedInstance(), message);
+        return retval;
+    }
+
+    @Mock
+    private ExtendedDialog setContent(final Invocation invocation, final Component content, final boolean placeContentInScrollPane) {
+        this.simpleStringContentMemo.put((ExtendedDialog) invocation.getInvokedInstance(), null);
+        return invocation.proceed(content, placeContentInScrollPane);
+    }
+}
diff --git a/test/unit/org/openstreetmap/josm/testutils/mockers/WindowMocker.java b/test/unit/org/openstreetmap/josm/testutils/mockers/WindowMocker.java
new file mode 100644
index 000000000..64440647a
--- /dev/null
+++ b/test/unit/org/openstreetmap/josm/testutils/mockers/WindowMocker.java
@@ -0,0 +1,39 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils;
+
+import java.awt.Frame;
+import java.awt.GraphicsConfiguration;
+import java.awt.Window;
+
+import mockit.Invocation;
+import mockit.Mock;
+import mockit.MockUp;
+
+/**
+ * MockUp for a {@link Window} which simply (and naively) makes its constructor(s) a no-op. This has
+ * the advantage of removing the isHeadless check. Though if course it also leaves you with
+ * uninintialized objects, and so of course they don't *necessarily* work properly. But often they
+ * work *just enough* to behave how a test needs them to. Exercise left to the reader to discover
+ * the limits here.
+ */
+public class WindowMocker extends MockUp<Window> {
+    @Mock
+    private void $init(final Invocation invocation) {
+    }
+
+    @Mock
+    private void $init(final Invocation invocation, final Window window) {
+    }
+
+    @Mock
+    private void $init(final Invocation invocation, final Frame frame) {
+    }
+
+    @Mock
+    private void $init(final Invocation invocation, final GraphicsConfiguration gc) {
+    }
+
+    @Mock
+    private void $init(final Invocation invocation, final Window window, final GraphicsConfiguration gc) {
+    }
+}
-- 
2.11.0

