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

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

    From d956bef2fd59091c4b970e948a893c2599c1bfd0 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             | 310 +++++++++++++++++++++
     1 file changed, 310 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..43af754ac
    - +  
     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        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 abstract static 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            // CHECKSTYLE.OFF: EmptyBlock
     138            } catch (IOException e) {
     139                // I don't see how this would be possible writing to a ByteArrayOutputStream
     140            }
     141            // CHECKSTYLE.ON: EmptyBlock
     142            return outputStream.toByteArray();
     143        }
     144
     145        @Override
     146        public MappingBuilder getMappingBuilder() {
     147            return WireMock.get(WireMock.urlMatching(String.format("/%h/(\\d+)/(\\d+)/(\\d+)\\.png", this.hashCode())));
     148        }
     149
     150        @Override
     151        public ImageryInfo getImageryInfo(int port) {
     152            return new ImageryInfo(
     153                this.label,
     154                String.format("tms[20]:http://localhost:%d/%h/{z}/{x}/{y}.png", port, this.hashCode()),
     155                "tms",
     156                (String) null,
     157                (String) null
     158            );
     159        }
     160
     161        @Override
     162        public String getLabel() {
     163            return this.label;
     164        }
     165    }
     166
     167    public final List<ConstSource> sourcesList;
     168    public final boolean clearLayerList;
     169    public final boolean clearSlippyMapSources;
     170    public final boolean registerInLayerList;
     171
     172    /**
     173     * Construct a TileSourceRule for use with a JUnit test.
     174     *
     175     * This variant will not make any attempt to register the sources' existence with any JOSM subsystems, so is safe
     176     * for direct application to a JUnit test.
     177     *
     178     * @param sources tile sources to serve from this mock server
     179     */
     180    public TileSourceRule(ConstSource... sources) {
     181        this(false, false, false, sources);
     182    }
     183
     184    /**
     185     * Construct a TileSourceRule for use with a JUnit test.
     186     *
     187     * The three boolean parameters control whether to perform various steps registering the tile sources with parts
     188     * of JOSM's internals as part of the setup process. It is advised to only enable any of these if it can be ensured
     189     * that this rule will have its setup routine executed *after* the relevant parts of JOSM have been set up, e.g.
     190     * when handled by {@link org.openstreetmap.josm.testutils.JOSMTestRules#fakeImagery}.
     191     *
     192     * @param clearLayerList whether to clear ImageryLayerInfo's layer list of any pre-existing entries
     193     * @param clearSlippyMapSources whether to clear SlippyMapBBoxChooser's stubborn fallback Mapnik TileSource
     194     * @param registerInLayerList whether to add sources to ImageryLayerInfo's layer list
     195     * @param sources tile sources to serve from this mock server
     196     */
     197    public TileSourceRule(
     198        boolean clearLayerList,
     199        boolean clearSlippyMapSources,
     200        boolean registerInLayerList,
     201        ConstSource... sources
     202    ) {
     203        super(options().dynamicPort());
     204        this.clearLayerList = clearLayerList;
     205        this.clearSlippyMapSources = clearSlippyMapSources;
     206        this.registerInLayerList = registerInLayerList;
     207
     208        // set up a stub target for the early request hack
     209        this.stubFor(WireMock.get(
     210            WireMock.urlMatching("/_poke")
     211        ).willReturn(
     212            WireMock.aResponse().withStatus(200).withBody("ow.")
     213        ));
     214
     215        this.sourcesList = Collections.unmodifiableList(Arrays.asList(sources));
     216        for (ConstSource source : this.sourcesList) {
     217            this.stubFor(source.getMappingBuilder().willReturn(source.getResponseDefinitionBuilder()));
     218        }
     219    }
     220
     221    /**
     222     * A junit-rule {@code apply} method exposed separately to allow a chaining rule to put this much earlier in
     223     * the test's initialization routine. The idea being to allow WireMock's web server to be starting up while other
     224     * necessary initialization is taking place.
     225     * See {@link org.junit.rules.TestRule#apply} for arguments.
     226     */
     227    public Statement applyRunServer(Statement base, Description description) {
     228        return super.apply(new Statement() {
     229            @Override
     230            public void evaluate() throws Throwable {
     231                try {
     232                    // a hack to circumvent a WireMock bug concerning delayed server startup. sending an early request
     233                    // to the mock server seems to prompt it to start earlier (though this request itself is not
     234                    // expected to succeed). see https://github.com/tomakehurst/wiremock/issues/97
     235                    (new java.net.URL(String.format("http://localhost:%d/_poke", TileSourceRule.this.port()))).getContent();
     236                // CHECKSTYLE.OFF: EmptyBlock
     237                } catch (Throwable t) {
     238                    // don't care
     239                }
     240                // CHECKSTYLE.ON: EmptyBlock
     241                base.evaluate();
     242            }
     243        }, description);
     244    }
     245
     246    /**
     247     * A junit-rule {@code apply} method exposed separately, containing initialization steps which can only be performed
     248     * once more of josm's environment has been set up.
     249     * See {@link org.junit.rules.TestRule#apply} for arguments.
     250     */
     251    public Statement applyRegisterLayers(Statement base, Description description) {
     252        if (this.registerInLayerList || this.clearLayerList) {
     253            return new Statement() {
     254                @Override
     255                @SuppressWarnings("unchecked")
     256                public void evaluate() throws Throwable {
     257                    List<SlippyMapBBoxChooser.TileSourceProvider> slippyMapProviders = null;
     258                    SlippyMapBBoxChooser.TileSourceProvider slippyMapDefaultProvider = null;
     259                    List<ImageryInfo> originalImageryInfoList = null;
     260                    if (TileSourceRule.this.clearSlippyMapSources) {
     261                        try {
     262                            slippyMapProviders = (List<SlippyMapBBoxChooser.TileSourceProvider>) getPrivateStaticField(
     263                                SlippyMapBBoxChooser.class,
     264                                "providers"
     265                            );
     266                            // pop this off the beginning of the list, keep for later
     267                            slippyMapDefaultProvider = slippyMapProviders.remove(0);
     268                        } catch (ReflectiveOperationException e) {
     269                            Logging.warn("Failed to remove default SlippyMapBBoxChooser TileSourceProvider");
     270                        }
     271                    }
     272
     273                    if (TileSourceRule.this.clearLayerList) {
     274                        originalImageryInfoList = ImageryLayerInfo.instance.getLayers();
     275                        ImageryLayerInfo.instance.clear();
     276                    }
     277                    if (TileSourceRule.this.registerInLayerList) {
     278                        for (ConstSource source : TileSourceRule.this.sourcesList) {
     279                            ImageryLayerInfo.addLayer(source.getImageryInfo(TileSourceRule.this.port()));
     280                        }
     281                    }
     282
     283                    try {
     284                        base.evaluate();
     285                    } finally {
     286                        // clean up to original state
     287                        if (slippyMapDefaultProvider != null) {
     288                            slippyMapProviders.add(0, slippyMapDefaultProvider);
     289                        }
     290                        if (originalImageryInfoList != null) {
     291                            ImageryLayerInfo.instance.clear();
     292                            ImageryLayerInfo.addLayers(originalImageryInfoList);
     293                        }
     294                    }
     295                }
     296            };
     297        } else {
     298            return base;
     299        }
     300    }
     301
     302    /**
     303     * A standard implementation of apply which simply calls both sub- {@code apply} methods, {@link #applyRunServer}
     304     * and {@link applyRegisterLayers}. Called when used as a standard junit rule.
     305     */
     306    @Override
     307    public Statement apply(Statement base, Description description) {
     308        return this.applyRunServer(this.applyRegisterLayers(base, description), description);
     309    }
     310}