Ticket #15508: v2-0002-add-TileSourceRule-a-junit-rule-for-creating-mock-ti.patch

File v2-0002-add-TileSourceRule-a-junit-rule-for-creating-mock-ti.patch, 12.5 KB (added by ris, 6 years ago)
  • new file test/unit/org/openstreetmap/josm/testutils/TileSourceRule.java

    From a916f5e9c0a250e783751aefc0987d7515eaee0e Mon Sep 17 00:00:00 2001
    From: Robert Scott <code@humanleg.org.uk>
    Date: Sat, 21 Oct 2017 11:19:28 +0100
    Subject: [PATCH 2/4] add TileSourceRule: a junit rule for creating mock tile
     servers
    
    ---
     .../josm/testutils/TileSourceRule.java             | 285 +++++++++++++++++++++
     1 file changed, 285 insertions(+)
     create mode 100644 test/unit/org/openstreetmap/josm/testutils/TileSourceRule.java
    
    diff --git a/test/unit/org/openstreetmap/josm/testutils/TileSourceRule.java b/test/unit/org/openstreetmap/josm/testutils/TileSourceRule.java
    new file mode 100644
    index 000000000..7e2314aa8
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.testutils;
     3
     4import java.io.ByteArrayOutputStream;
     5import java.io.IOException;
     6import java.util.Arrays;
     7import java.util.Collections;
     8import java.util.Objects;
     9import java.util.HashMap;
     10import java.util.List;
     11
     12import java.awt.Color;
     13import java.awt.Graphics2D;
     14import java.awt.image.BufferedImage;
     15
     16import javax.imageio.ImageIO;
     17
     18import org.openstreetmap.josm.data.imagery.ImageryInfo;
     19import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
     20import org.openstreetmap.josm.gui.bbox.SlippyMapBBoxChooser;
     21import org.openstreetmap.josm.tools.Logging;
     22
     23import static org.openstreetmap.josm.TestUtils.getPrivateStaticField;
     24
     25import org.junit.runner.Description;
     26import org.junit.runners.model.Statement;
     27import com.github.tomakehurst.wiremock.client.MappingBuilder;
     28import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
     29import com.github.tomakehurst.wiremock.client.WireMock;
     30import com.github.tomakehurst.wiremock.junit.WireMockRule;
     31
     32import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
     33
     34
     35/**
     36 * A JUnit rule, based on {@link WireMockRule} to provide a test with a simple mock tile server serving multiple tile
     37 * sources.
     38 */
     39public class TileSourceRule extends WireMockRule {
     40    private static class ByteArrayWrapper{
     41        // i don't believe you're making me do this, java
     42        public final byte[] byteArray;
     43
     44        public ByteArrayWrapper(byte[] ba) {
     45            this.byteArray = ba;
     46        }
     47    }
     48
     49    // allocation is expensive and many tests may be wanting to set up the same tile sources one after the other, hence
     50    // this cache
     51    public static HashMap<ConstSource, ByteArrayWrapper> constPayloadCache = new HashMap<>();
     52
     53    /**
     54     * Class defining a tile source for TileSourceRule to mock. Due to the way WireMock is designed, it is far more
     55     * straightforward to serve a single image in all tile positions
     56     */
     57    public static abstract class ConstSource {
     58        /**
     59         * method for actually generating the payload body bytes, uncached
     60         */
     61        public abstract byte[] generatePayloadBytes();
     62
     63        /**
     64         * @return a {@link MappingBuilder} representing the request matching properties of this tile source, suitable
     65         * for passing to {@link WireMockRule#stubFor}.
     66         */
     67        public abstract MappingBuilder getMappingBuilder();
     68
     69        /**
     70         * @return text label/name for this source if displayed in JOSM menus
     71         */
     72        public abstract String getLabel();
     73
     74        /**
     75         * @param port the port this WireMock server is running on
     76         * @return {@link ImageryInfo} describing this tile source, as might be submitted to {@link ImageryLayerInfo#add}
     77         */
     78        public abstract ImageryInfo getImageryInfo(int port);
     79
     80        /**
     81         * @return byte array of the payload body for this source, possibly retrieved from a global cache
     82         */
     83        public byte[] getPayloadBytes() {
     84            ByteArrayWrapper payloadWrapper = constPayloadCache.get(this);
     85            if (payloadWrapper == null) {
     86                payloadWrapper = new ByteArrayWrapper(this.generatePayloadBytes());
     87                constPayloadCache.put(this, payloadWrapper);
     88            }
     89            return payloadWrapper.byteArray;
     90        }
     91
     92        /**
     93         * @return a {@link ResponseDefinitionBuilder} embodying the payload of this tile source suitable for
     94         * application to a {@link MappingBuilder}.
     95         */
     96        public ResponseDefinitionBuilder getResponseDefinitionBuilder() {
     97            return WireMock.aResponse().withStatus(200).withHeader("Content-Type", "image/png").withBody(
     98                this.getPayloadBytes()
     99            );
     100        }
     101    }
     102
     103    /**
     104     * A plain color tile source
     105     */
     106    public static class ColorSource extends ConstSource {
     107        protected final Color color;
     108        protected final String label;
     109        protected final int tileSize;
     110
     111        /**
     112         * @param color Color for these tiles
     113         * @param label text label/name for this source if displayed in JOSM menus
     114         * @param tileSize Pixel dimension of tiles (usually 256)
     115         */
     116        public ColorSource(Color color, String label, int tileSize) {
     117            this.color = color;
     118            this.label = label;
     119            this.tileSize = tileSize;
     120        }
     121
     122        @Override
     123        public int hashCode() {
     124            return Objects.hash(this.color, this.label, this.tileSize, this.getClass());
     125        }
     126
     127        @Override
     128        public byte[] generatePayloadBytes() {
     129            BufferedImage image = new BufferedImage(this.tileSize, this.tileSize, BufferedImage.TYPE_INT_RGB);
     130            Graphics2D g = image.createGraphics();
     131            g.setBackground(this.color);
     132            g.clearRect(0, 0, image.getWidth(), image.getHeight());
     133
     134            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
     135            try {
     136                ImageIO.write(image, "png", outputStream);
     137            } catch (IOException e) {
     138                // I don't see how this would be possible writing to a ByteArrayOutputStream
     139            }
     140            return outputStream.toByteArray();
     141        }
     142
     143        @Override
     144        public MappingBuilder getMappingBuilder() {
     145            return WireMock.get(WireMock.urlMatching(String.format("/%h/(\\d+)/(\\d+)/(\\d+)\\.png", this.hashCode())));
     146        }
     147
     148        @Override
     149        public ImageryInfo getImageryInfo(int port) {
     150            return new ImageryInfo(
     151                this.label,
     152                String.format("tms[20]:http://localhost:%d/%h/{z}/{x}/{y}.png", port, this.hashCode()),
     153                "tms",
     154                (String)null,
     155                (String)null
     156            );
     157        }
     158
     159        @Override
     160        public String getLabel() {
     161            return this.label;
     162        }
     163    }
     164
     165    public final List<ConstSource> sourcesList;
     166    public final boolean clearLayerList;
     167    public final boolean clearSlippyMapSources;
     168    public final boolean registerInLayerList;
     169
     170    /**
     171     * Construct a TileSourceRule for use with a JUnit test.
     172     *
     173     * This variant will not make any attempt to register the sources' existence with any JOSM subsystems, so is safe
     174     * for direct application to a JUnit test.
     175     *
     176     * @param sources tile sources to serve from this mock server
     177     */
     178    public TileSourceRule(ConstSource... sources) {
     179        this(false, false, false, sources);
     180    }
     181
     182    /**
     183     * Construct a TileSourceRule for use with a JUnit test.
     184     *
     185     * The three boolean parameters control whether to perform various steps registering the tile sources with parts
     186     * of JOSM's internals as part of the setup process. It is advised to only enable any of these if it can be ensured
     187     * that this rule will have its setup routine executed *after* the relevant parts of JOSM have been set up, e.g.
     188     * when handled by {@link org.openstreetmap.josm.testutils.JOSMTestRules#fakeImagery}.
     189     *
     190     * @param clearLayerList whether to clear ImageryLayerInfo's layer list of any pre-existing entries
     191     * @param clearSlippyMapSources whether to clear SlippyMapBBoxChooser's stubborn fallback Mapnik TileSource
     192     * @param registerInLayerList whether to add sources to ImageryLayerInfo's layer list
     193     * @param sources tile sources to serve from this mock server
     194     */
     195    public TileSourceRule(
     196        boolean clearLayerList,
     197        boolean clearSlippyMapSources,
     198        boolean registerInLayerList,
     199        ConstSource... sources
     200    ) {
     201        super(options().dynamicPort());
     202        this.clearLayerList = clearLayerList;
     203        this.clearSlippyMapSources = clearSlippyMapSources;
     204        this.registerInLayerList = registerInLayerList;
     205        this.sourcesList = Collections.unmodifiableList(Arrays.asList(sources));
     206        for (ConstSource source : this.sourcesList) {
     207            this.stubFor(source.getMappingBuilder().willReturn(source.getResponseDefinitionBuilder()));
     208        }
     209    }
     210
     211    /**
     212     * A junit-rule {@code apply} method exposed separately to allow a chaining rule to put this much earlier in
     213     * the test's initialization routine. The idea being to allow WireMock's web server to be starting up while other
     214     * necessary initialization is taking place.
     215     * See {@link org.junit.rules.TestRule#apply} for arguments.
     216     */
     217    public Statement applyRunServer(Statement base, Description description) {
     218        return super.apply(base, description);
     219    }
     220
     221    /**
     222     * A junit-rule {@code apply} method exposed separately, containing initialization steps which can only be performed
     223     * once more of josm's environment has been set up.
     224     * See {@link org.junit.rules.TestRule#apply} for arguments.
     225     */
     226    public Statement applyRegisterLayers(Statement base, Description description) {
     227        if (this.registerInLayerList || this.clearLayerList) {
     228            return new Statement() {
     229                @Override
     230                @SuppressWarnings("unchecked")
     231                public void evaluate() throws Throwable {
     232                    List<SlippyMapBBoxChooser.TileSourceProvider> slippyMapProviders = null;
     233                    SlippyMapBBoxChooser.TileSourceProvider slippyMapDefaultProvider = null;
     234                    List<ImageryInfo> originalImageryInfoList = null;
     235                    if (TileSourceRule.this.clearSlippyMapSources) {
     236                        try {
     237                            slippyMapProviders = (List<SlippyMapBBoxChooser.TileSourceProvider>)getPrivateStaticField(
     238                                SlippyMapBBoxChooser.class,
     239                                "providers"
     240                            );
     241                            // pop this off the beginning of the list, keep for later
     242                            slippyMapDefaultProvider = slippyMapProviders.remove(0);
     243                        } catch (ReflectiveOperationException e) {
     244                            Logging.warn("Failed to remove default SlippyMapBBoxChooser TileSourceProvider");
     245                        }
     246                    }
     247
     248                    if (TileSourceRule.this.clearLayerList) {
     249                        originalImageryInfoList = ImageryLayerInfo.instance.getLayers();
     250                        ImageryLayerInfo.instance.clear();
     251                    }
     252                    if (TileSourceRule.this.registerInLayerList) {
     253                        for (ConstSource source : TileSourceRule.this.sourcesList){
     254                            ImageryLayerInfo.addLayer(source.getImageryInfo(TileSourceRule.this.port()));
     255                        }
     256                    }
     257
     258                    try {
     259                        base.evaluate();
     260                    } finally {
     261                        // clean up to original state
     262                        if (slippyMapDefaultProvider != null) {
     263                            slippyMapProviders.add(0, slippyMapDefaultProvider);
     264                        }
     265                        if (originalImageryInfoList != null) {
     266                            ImageryLayerInfo.instance.clear();
     267                            ImageryLayerInfo.addLayers(originalImageryInfoList);
     268                        }
     269                    }
     270                }
     271            };
     272        } else {
     273            return base;
     274        }
     275    }
     276
     277    /**
     278     * A standard implementation of apply which simply calls both sub- {@code apply} methods, {@link #applyRunServer}
     279     * and {@link applyRegisterLayers}. Called when used as a standard junit rule.
     280     */
     281    @Override
     282    public Statement apply(Statement base, Description description) {
     283        return this.applyRunServer(this.applyRegisterLayers(base, description), description);
     284    }
     285}