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

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

    From ebdb627f3997444a061f7fc02c6d71ec7f6271ec 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             | 305 +++++++++++++++++++++
     1 file changed, 305 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..ef9a98bb3
    - +  
     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
     206        // set up a stub target for the early request hack
     207        this.stubFor(WireMock.get(
     208            WireMock.urlMatching("/_poke")
     209        ).willReturn(
     210            WireMock.aResponse().withStatus(200).withBody("ow.")
     211        ));
     212
     213        this.sourcesList = Collections.unmodifiableList(Arrays.asList(sources));
     214        for (ConstSource source : this.sourcesList) {
     215            this.stubFor(source.getMappingBuilder().willReturn(source.getResponseDefinitionBuilder()));
     216        }
     217    }
     218
     219    /**
     220     * A junit-rule {@code apply} method exposed separately to allow a chaining rule to put this much earlier in
     221     * the test's initialization routine. The idea being to allow WireMock's web server to be starting up while other
     222     * necessary initialization is taking place.
     223     * See {@link org.junit.rules.TestRule#apply} for arguments.
     224     */
     225    public Statement applyRunServer(Statement base, Description description) {
     226        return super.apply(new Statement() {
     227            @Override
     228            public void evaluate() throws Throwable {
     229                try {
     230                    // a hack to circumvent a WireMock bug concerning delayed server startup. sending an early request
     231                    // to the mock server seems to prompt it to start earlier (though this request itself is not
     232                    // expected to succeed). see https://github.com/tomakehurst/wiremock/issues/97
     233                    (new java.net.URL(String.format("http://localhost:%d/_poke", TileSourceRule.this.port()))).getContent();
     234                } catch (Throwable t) {
     235                }
     236                base.evaluate();
     237            }
     238        }, description);
     239    }
     240
     241    /**
     242     * A junit-rule {@code apply} method exposed separately, containing initialization steps which can only be performed
     243     * once more of josm's environment has been set up.
     244     * See {@link org.junit.rules.TestRule#apply} for arguments.
     245     */
     246    public Statement applyRegisterLayers(Statement base, Description description) {
     247        if (this.registerInLayerList || this.clearLayerList) {
     248            return new Statement() {
     249                @Override
     250                @SuppressWarnings("unchecked")
     251                public void evaluate() throws Throwable {
     252                    List<SlippyMapBBoxChooser.TileSourceProvider> slippyMapProviders = null;
     253                    SlippyMapBBoxChooser.TileSourceProvider slippyMapDefaultProvider = null;
     254                    List<ImageryInfo> originalImageryInfoList = null;
     255                    if (TileSourceRule.this.clearSlippyMapSources) {
     256                        try {
     257                            slippyMapProviders = (List<SlippyMapBBoxChooser.TileSourceProvider>)getPrivateStaticField(
     258                                SlippyMapBBoxChooser.class,
     259                                "providers"
     260                            );
     261                            // pop this off the beginning of the list, keep for later
     262                            slippyMapDefaultProvider = slippyMapProviders.remove(0);
     263                        } catch (ReflectiveOperationException e) {
     264                            Logging.warn("Failed to remove default SlippyMapBBoxChooser TileSourceProvider");
     265                        }
     266                    }
     267
     268                    if (TileSourceRule.this.clearLayerList) {
     269                        originalImageryInfoList = ImageryLayerInfo.instance.getLayers();
     270                        ImageryLayerInfo.instance.clear();
     271                    }
     272                    if (TileSourceRule.this.registerInLayerList) {
     273                        for (ConstSource source : TileSourceRule.this.sourcesList){
     274                            ImageryLayerInfo.addLayer(source.getImageryInfo(TileSourceRule.this.port()));
     275                        }
     276                    }
     277
     278                    try {
     279                        base.evaluate();
     280                    } finally {
     281                        // clean up to original state
     282                        if (slippyMapDefaultProvider != null) {
     283                            slippyMapProviders.add(0, slippyMapDefaultProvider);
     284                        }
     285                        if (originalImageryInfoList != null) {
     286                            ImageryLayerInfo.instance.clear();
     287                            ImageryLayerInfo.addLayers(originalImageryInfoList);
     288                        }
     289                    }
     290                }
     291            };
     292        } else {
     293            return base;
     294        }
     295    }
     296
     297    /**
     298     * A standard implementation of apply which simply calls both sub- {@code apply} methods, {@link #applyRunServer}
     299     * and {@link applyRegisterLayers}. Called when used as a standard junit rule.
     300     */
     301    @Override
     302    public Statement apply(Statement base, Description description) {
     303        return this.applyRunServer(this.applyRegisterLayers(base, description), description);
     304    }
     305}