Ticket #16010: v2-0003-testing-add-ExtendedDialogMocker-WindowMocker.patch

File v2-0003-testing-add-ExtendedDialogMocker-WindowMocker.patch, 11.4 KB (added by ris, 11 months ago)
  • new file test/unit/org/openstreetmap/josm/testutils/mockers/BaseDialogMockUp.java

    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
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.testutils;
     3
     4import java.util.ArrayList;
     5import java.util.Collections;
     6import java.util.HashMap;
     7import java.util.List;
     8import java.util.Map;
     9
     10import mockit.MockUp;
     11
     12/**
     13 * Abstract class implementing the few common features of the dialog-mockers which are readily factorable.
     14 */
     15abstract class BaseDialogMockUp<T> extends MockUp<T> {
     16    private final List<Object[]> invocationLog;
     17
     18    /**
     19     * @return an unmodifiable view of the internal invocation log. Each entry is an array of Objects to
     20     *     allow for more advanced implementations to be able to express their invocations in their own
     21     *     ways. Typically the invocation's "result value" is used as the first element of the array.
     22     */
     23    public List<Object[]> getInvocationLog() {
     24        return this.invocationLog;
     25    }
     26
     27    private final List<Object[]> invocationLogInternal = new ArrayList<Object[]>(4);
     28
     29    /**
     30     * @return the actual (writable) invocation log
     31     */
     32    protected List<Object[]> getInvocationLogInternal() {
     33        return this.invocationLogInternal;
     34    }
     35
     36    private final Map<String, Object> mockResultMap;
     37
     38    /**
     39     * @return mapping to {@link Object}s so response button can be specified by String (label) or Integer
     40     *     - sorry, no type safety as java doesn't support union types
     41     */
     42    public Map<String, Object> getMockResultMap() {
     43        return this.mockResultMap;
     44    }
     45
     46    BaseDialogMockUp(final Map<String, Object> mockResultMap) {
     47        this.mockResultMap = mockResultMap != null ? mockResultMap : new HashMap<String, Object>(4);
     48        this.invocationLog = Collections.unmodifiableList(this.invocationLogInternal);
     49    }
     50}
  • new file test/unit/org/openstreetmap/josm/testutils/mockers/ExtendedDialogMocker.java

    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
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.testutils;
     3
     4import static org.junit.Assert.fail;
     5
     6import java.awt.Component;
     7import java.awt.GraphicsEnvironment;
     8import java.util.Arrays;
     9import java.util.Map;
     10import java.util.Optional;
     11import java.util.WeakHashMap;
     12
     13import org.openstreetmap.josm.gui.ExtendedDialog;
     14import org.openstreetmap.josm.tools.Logging;
     15
     16import mockit.Deencapsulation;
     17import mockit.Invocation;
     18import mockit.Mock;
     19
     20/**
     21 * MockUp for {@link ExtendedDialog} allowing a test to pre-seed uses of {@link ExtendedDialog}
     22 * with mock "responses". This works best with {@link ExtendedDialog}s which have their contents set
     23 * through {@link ExtendedDialog#setContent(String)} as simple strings. In such a case, responses can
     24 * be defined through a mapping from content {@link String}s to button indexes ({@link Integer}s) or
     25 * button labels ({@link String}s). Example:
     26 *
     27 * <pre>
     28 *      new ExtendedDialogMocker(ImmutableMap.&lt;String, Object&gt;builder()
     29 *          .put("JOSM version 8,001 required for plugin baz_plugin.", "Download Plugin")
     30 *          .put("JOSM version 7,001 required for plugin dummy_plugin.", "Cancel")
     31 *          .put("Are you sure you want to do foo bar?", ExtendedDialog.DialogClosedOtherwise)
     32 *          .build()
     33 *      );
     34 * </pre>
     35 *
     36 * Testing examples with more complicated contents would require overriding
     37 * {@link #getString(ExtendedDialog)} or even {@link #getMockResult(ExtendedDialog)} with custom logic.
     38 * The class is implemented as a number of small methods with the main aim being to allow overriding of
     39 * only the parts necessary for a particular case.
     40 *
     41 * The default {@link #getMockResult(ExtendedDialog)} will raise an
     42 * {@link junit.framework.AssertionFailedError} on an {@link ExtendedDialog} activation without a
     43 * matching mapping entry or if the named button doesn't exist.
     44 *
     45 * The public {@link #getMockResultMap()} method returns the modifiable result map to allow for situations
     46 * where the desired result might need to be changed mid-test.
     47 */
     48public class ExtendedDialogMocker extends BaseDialogMockUp<ExtendedDialog> {
     49    /**
     50     * Because we're unable to add fields to the mocked class, we need to use this external global
     51     * mapping to be able to keep a note of the most recently set simple String contents of each
     52     * {@link ExtendedDialog} instance - {@link ExtendedDialog} doesn't store this information
     53     * itself, instead converting it directly into the embedded {@link Component}.
     54     */
     55    protected final Map<ExtendedDialog, String> simpleStringContentMemo = new WeakHashMap<ExtendedDialog, String>();
     56
     57    /**
     58     * Construct an {@link ExtendedDialogMocker} with an empty {@link #mockResultMap}.
     59     */
     60    public ExtendedDialogMocker() {
     61        this(null);
     62    }
     63
     64    /**
     65     * Construct an {@link ExtendedDialogMocker} with the provided {@link #mockResultMap}.
     66     * @param mockResultMap mapping of {@link ExtendedDialog} string contents to
     67     *      result button label or integer index.
     68     */
     69    public ExtendedDialogMocker(final Map<String, Object> mockResultMap) {
     70        super(mockResultMap);
     71        if (GraphicsEnvironment.isHeadless()) {
     72            new WindowMocker();
     73        }
     74    }
     75
     76    protected int getButtonPositionFromLabel(final ExtendedDialog instance, final String label) {
     77        final String[] bTexts = Deencapsulation.getField(instance, "bTexts");
     78        final int position = Arrays.asList(bTexts).indexOf(label);
     79        if (position == -1) {
     80            fail("Unable to find button labeled \"" + label + "\". Instead found: " + Arrays.toString(bTexts));
     81        }
     82        return position;
     83    }
     84
     85    protected String getString(final ExtendedDialog instance) {
     86        return Optional.ofNullable(this.simpleStringContentMemo.get(instance))
     87            .orElseGet(() -> instance.toString());
     88    }
     89
     90    protected int getMockResult(final ExtendedDialog instance) {
     91        final String stringContent = this.getString(instance);
     92        final Object result = this.getMockResultMap().get(stringContent);
     93
     94        if (result == null) {
     95            fail(
     96                "Unexpected ExtendedDialog content: " + stringContent
     97            );
     98        } else if (result instanceof Integer) {
     99            return (Integer) result;
     100        } else if (result instanceof String) {
     101            // buttons are numbered with 1-based indexing
     102            return 1 + this.getButtonPositionFromLabel(instance, (String) result);
     103        }
     104
     105        throw new IllegalArgumentException(
     106            "ExtendedDialog contents mapped to unsupported type of Object: " + result
     107        );
     108    }
     109
     110    protected Object[] getInvocationLogEntry(final ExtendedDialog instance, final int mockResult) {
     111        return new Object[] {
     112            (Integer) mockResult,
     113            this.getString(instance),
     114            instance.getTitle()
     115        };
     116    }
     117
     118    @Mock
     119    private void setupDialog(final Invocation invocation) {
     120        if (!GraphicsEnvironment.isHeadless()) {
     121            invocation.proceed();
     122        }
     123        // else do nothing - WindowMocker-ed Windows doesn't work well enough for some of the
     124        // component constructions
     125    }
     126
     127    @Mock
     128    private void setVisible(final Invocation invocation, final boolean value) {
     129        if (value == true) {
     130            final ExtendedDialog instance = invocation.getInvokedInstance();
     131            final int mockResult = this.getMockResult(instance);
     132            // TODO check validity of mockResult?
     133            Deencapsulation.setField(instance, "result", mockResult);
     134            Logging.info(
     135                "{0} answering {1} to ExtendedDialog with content {2}",
     136                this.getClass().getName(),
     137                mockResult,
     138                this.getString(instance)
     139            );
     140            this.getInvocationLogInternal().add(this.getInvocationLogEntry(instance, mockResult));
     141        }
     142    }
     143
     144    @Mock
     145    private ExtendedDialog setContent(final Invocation invocation, final String message) {
     146        final ExtendedDialog retval = invocation.proceed(message);
     147        // must set this *after* the regular invocation else that will fall through to
     148        // setContent(Component, boolean) which would overwrite it (with null)
     149        this.simpleStringContentMemo.put((ExtendedDialog) invocation.getInvokedInstance(), message);
     150        return retval;
     151    }
     152
     153    @Mock
     154    private ExtendedDialog setContent(final Invocation invocation, final Component content, final boolean placeContentInScrollPane) {
     155        this.simpleStringContentMemo.put((ExtendedDialog) invocation.getInvokedInstance(), null);
     156        return invocation.proceed(content, placeContentInScrollPane);
     157    }
     158}
  • new file test/unit/org/openstreetmap/josm/testutils/mockers/WindowMocker.java

    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
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.testutils;
     3
     4import java.awt.Frame;
     5import java.awt.GraphicsConfiguration;
     6import java.awt.Window;
     7
     8import mockit.Invocation;
     9import mockit.Mock;
     10import mockit.MockUp;
     11
     12/**
     13 * MockUp for a {@link Window} which simply (and naively) makes its constructor(s) a no-op. This has
     14 * the advantage of removing the isHeadless check. Though if course it also leaves you with
     15 * uninintialized objects, and so of course they don't *necessarily* work properly. But often they
     16 * work *just enough* to behave how a test needs them to. Exercise left to the reader to discover
     17 * the limits here.
     18 */
     19public class WindowMocker extends MockUp<Window> {
     20    @Mock
     21    private void $init(final Invocation invocation) {
     22    }
     23
     24    @Mock
     25    private void $init(final Invocation invocation, final Window window) {
     26    }
     27
     28    @Mock
     29    private void $init(final Invocation invocation, final Frame frame) {
     30    }
     31
     32    @Mock
     33    private void $init(final Invocation invocation, final GraphicsConfiguration gc) {
     34    }
     35
     36    @Mock
     37    private void $init(final Invocation invocation, final Window window, final GraphicsConfiguration gc) {
     38    }
     39}