source: josm/trunk/test/unit/org/openstreetmap/josm/testutils/TileSourceRule.java@ 17195

Last change on this file since 17195 was 17195, checked in by simon04, 4 years ago

see #15102 - see #16637 - Use WireMockServer.url()

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