source: josm/trunk/test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java@ 14119

Last change on this file since 14119 was 14119, checked in by Don-vip, 6 years ago

see #15229 - deprecate all Main methods returning an URL

File size: 23.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.testutils;
3
4import java.awt.Color;
5import java.awt.Window;
6import java.awt.event.WindowEvent;
7import java.io.ByteArrayInputStream;
8import java.io.File;
9import java.io.IOException;
10import java.security.GeneralSecurityException;
11import java.text.MessageFormat;
12import java.util.Arrays;
13import java.util.Map;
14import java.util.TimeZone;
15import java.util.logging.Handler;
16
17import org.awaitility.Awaitility;
18import org.junit.rules.TemporaryFolder;
19import org.junit.rules.TestRule;
20import org.junit.runner.Description;
21import org.junit.runners.model.InitializationError;
22import org.junit.runners.model.Statement;
23import org.openstreetmap.josm.JOSMFixture;
24import org.openstreetmap.josm.Main;
25import org.openstreetmap.josm.TestUtils;
26import org.openstreetmap.josm.actions.DeleteAction;
27import org.openstreetmap.josm.command.DeleteCommand;
28import org.openstreetmap.josm.data.UserIdentityManager;
29import org.openstreetmap.josm.data.Version;
30import org.openstreetmap.josm.data.osm.User;
31import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
32import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
33import org.openstreetmap.josm.data.preferences.JosmUrls;
34import org.openstreetmap.josm.data.projection.Projections;
35import org.openstreetmap.josm.gui.MainApplication;
36import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
37import org.openstreetmap.josm.gui.oauth.OAuthAuthorizationWizard;
38import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
39import org.openstreetmap.josm.gui.util.GuiHelper;
40import org.openstreetmap.josm.io.CertificateAmendment;
41import org.openstreetmap.josm.io.OsmApi;
42import org.openstreetmap.josm.io.OsmApiInitializationException;
43import org.openstreetmap.josm.io.OsmConnection;
44import org.openstreetmap.josm.io.OsmTransferCanceledException;
45import org.openstreetmap.josm.spi.preferences.Config;
46import org.openstreetmap.josm.spi.preferences.Setting;
47import org.openstreetmap.josm.testutils.mockers.EDTAssertionMocker;
48import org.openstreetmap.josm.testutils.mockers.WindowlessMapViewStateMocker;
49import org.openstreetmap.josm.testutils.mockers.WindowlessNavigatableComponentMocker;
50import org.openstreetmap.josm.tools.I18n;
51import org.openstreetmap.josm.tools.JosmRuntimeException;
52import org.openstreetmap.josm.tools.Logging;
53import org.openstreetmap.josm.tools.MemoryManagerTest;
54import org.openstreetmap.josm.tools.RightAndLefthandTraffic;
55import org.openstreetmap.josm.tools.Territories;
56import org.openstreetmap.josm.tools.date.DateUtils;
57
58import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
59
60/**
61 * This class runs a test in an environment that resembles the one used by the JOSM main application.
62 * <p>
63 * The environment is reset before every test. You can specify the components to which you need access using the methods of this class.
64 * For example, invoking {@link #preferences()} gives you access to the (default) preferences.
65 *
66 * @author Michael Zangl
67 */
68public class JOSMTestRules implements TestRule {
69 private int timeout = isDebugMode() ? -1 : 10 * 1000;
70 private TemporaryFolder josmHome;
71 private boolean usePreferences = false;
72 private APIType useAPI = APIType.NONE;
73 private String i18n = null;
74 private TileSourceRule tileSourceRule;
75 private String assumeRevisionString;
76 private Version originalVersion;
77 private Runnable mapViewStateMockingRunnable;
78 private Runnable navigableComponentMockingRunnable;
79 private Runnable edtAssertionMockingRunnable;
80 private boolean platform;
81 private boolean useProjection;
82 private boolean useProjectionNadGrids;
83 private boolean commands;
84 private boolean allowMemoryManagerLeaks;
85 private boolean useMapStyles;
86 private boolean usePresets;
87 private boolean useHttps;
88 private boolean territories;
89 private boolean rlTraffic;
90 private boolean main;
91
92 /**
93 * Disable the default timeout for this test. Use with care.
94 * @return this instance, for easy chaining
95 */
96 public JOSMTestRules noTimeout() {
97 timeout = -1;
98 return this;
99 }
100
101 /**
102 * Set a timeout for all tests in this class. Local method timeouts may only reduce this timeout.
103 * @param millis The timeout duration in milliseconds.
104 * @return this instance, for easy chaining
105 */
106 public JOSMTestRules timeout(int millis) {
107 timeout = isDebugMode() ? -1 : millis;
108 return this;
109 }
110
111 /**
112 * Enable the use of default preferences.
113 * @return this instance, for easy chaining
114 */
115 public JOSMTestRules preferences() {
116 josmHome();
117 usePreferences = true;
118 return this;
119 }
120
121 /**
122 * Set JOSM home to a valid, empty directory.
123 * @return this instance, for easy chaining
124 */
125 private JOSMTestRules josmHome() {
126 josmHome = new TemporaryFolder();
127 return this;
128 }
129
130 /**
131 * Enables the i18n module for this test in english.
132 * @return this instance, for easy chaining
133 */
134 public JOSMTestRules i18n() {
135 return i18n("en");
136 }
137
138 /**
139 * Enables the i18n module for this test.
140 * @param language The language to use.
141 * @return this instance, for easy chaining
142 */
143 public JOSMTestRules i18n(String language) {
144 i18n = language;
145 return this;
146 }
147
148 /**
149 * Enable {@link Main#platform} global variable.
150 * @return this instance, for easy chaining
151 */
152 public JOSMTestRules platform() {
153 platform = true;
154 return this;
155 }
156
157 /**
158 * Mock this test's assumed JOSM version (as reported by {@link Version}).
159 * @param revisionProperties mock contents of JOSM's {@code REVISION} properties file
160 * @return this instance, for easy chaining
161 */
162 public JOSMTestRules assumeRevision(final String revisionProperties) {
163 this.assumeRevisionString = revisionProperties;
164 return this;
165 }
166
167 /**
168 * Enable the dev.openstreetmap.org API for this test.
169 * @return this instance, for easy chaining
170 */
171 public JOSMTestRules devAPI() {
172 preferences();
173 useAPI = APIType.DEV;
174 return this;
175 }
176
177 /**
178 * Use the {@link FakeOsmApi} for testing.
179 * @return this instance, for easy chaining
180 */
181 public JOSMTestRules fakeAPI() {
182 useAPI = APIType.FAKE;
183 return this;
184 }
185
186 /**
187 * Set up default projection (Mercator)
188 * @return this instance, for easy chaining
189 */
190 public JOSMTestRules projection() {
191 useProjection = true;
192 return this;
193 }
194
195 /**
196 * Set up loading of NTV2 grit shift files to support projections that need them.
197 * @return this instance, for easy chaining
198 */
199 public JOSMTestRules projectionNadGrids() {
200 useProjectionNadGrids = true;
201 return this;
202 }
203
204 /**
205 * Set up HTTPS certificates
206 * @return this instance, for easy chaining
207 */
208 public JOSMTestRules https() {
209 useHttps = true;
210 platform = true;
211 return this;
212 }
213
214 /**
215 * Allow the execution of commands using {@link Main#undoRedo}
216 * @return this instance, for easy chaining
217 */
218 public JOSMTestRules commands() {
219 commands = true;
220 return this;
221 }
222
223 /**
224 * Allow the memory manager to contain items after execution of the test cases.
225 * @return this instance, for easy chaining
226 */
227 public JOSMTestRules memoryManagerLeaks() {
228 allowMemoryManagerLeaks = true;
229 return this;
230 }
231
232 /**
233 * Use map styles in this test.
234 * @return this instance, for easy chaining
235 * @since 11777
236 */
237 public JOSMTestRules mapStyles() {
238 preferences();
239 useMapStyles = true;
240 return this;
241 }
242
243 /**
244 * Use presets in this test.
245 * @return this instance, for easy chaining
246 * @since 12568
247 */
248 public JOSMTestRules presets() {
249 preferences();
250 usePresets = true;
251 return this;
252 }
253
254 /**
255 * Use boundaries dataset in this test.
256 * @return this instance, for easy chaining
257 * @since 12545
258 */
259 public JOSMTestRules territories() {
260 territories = true;
261 return this;
262 }
263
264 /**
265 * Use right and lefthand traffic dataset in this test.
266 * @return this instance, for easy chaining
267 * @since 12556
268 */
269 public JOSMTestRules rlTraffic() {
270 territories();
271 rlTraffic = true;
272 return this;
273 }
274
275 /**
276 * Re-raise AssertionErrors thrown in the EDT where they would have normally been swallowed.
277 * @return this instance, for easy chaining
278 */
279 public JOSMTestRules assertionsInEDT() {
280 return this.assertionsInEDT(EDTAssertionMocker::new);
281 }
282
283 /**
284 * Re-raise AssertionErrors thrown in the EDT where they would have normally been swallowed.
285 * @param edtAssertionMockingRunnable Runnable for initializing this functionality
286 *
287 * @return this instance, for easy chaining
288 */
289 public JOSMTestRules assertionsInEDT(final Runnable edtAssertionMockingRunnable) {
290 this.edtAssertionMockingRunnable = edtAssertionMockingRunnable;
291 return this;
292 }
293
294 /**
295 * Replace imagery sources with a default set of mock tile sources
296 *
297 * @return this instance, for easy chaining
298 */
299 public JOSMTestRules fakeImagery() {
300 return this.fakeImagery(
301 new TileSourceRule(
302 true,
303 true,
304 true,
305 new TileSourceRule.ColorSource(Color.WHITE, "White Tiles", 256),
306 new TileSourceRule.ColorSource(Color.BLACK, "Black Tiles", 256),
307 new TileSourceRule.ColorSource(Color.MAGENTA, "Magenta Tiles", 256),
308 new TileSourceRule.ColorSource(Color.GREEN, "Green Tiles", 256)
309 )
310 );
311 }
312
313 /**
314 * Replace imagery sources with those from specific mock tile server setup
315 * @param tileSourceRule Tile source rule
316 *
317 * @return this instance, for easy chaining
318 */
319 public JOSMTestRules fakeImagery(TileSourceRule tileSourceRule) {
320 this.preferences();
321 this.tileSourceRule = tileSourceRule;
322 return this;
323 }
324
325 /**
326 * Use the {@link Main#main}, {@code Main.contentPanePrivate}, {@code Main.mainPanel},
327 * global variables in this test.
328 * @return this instance, for easy chaining
329 * @since 12557
330 */
331 public JOSMTestRules main() {
332 return this.main(
333 WindowlessMapViewStateMocker::new,
334 WindowlessNavigatableComponentMocker::new
335 );
336 }
337
338 /**
339 * Use the {@link Main#main}, {@code Main.contentPanePrivate}, {@code Main.mainPanel},
340 * global variables in this test.
341 * @param mapViewStateMockingRunnable Runnable to use for mocking out any required parts of
342 * {@link org.openstreetmap.josm.gui.MapViewState}, null to skip.
343 * @param navigableComponentMockingRunnable Runnable to use for mocking out any required parts
344 * of {@link org.openstreetmap.josm.gui.NavigatableComponent}, null to skip.
345 *
346 * @return this instance, for easy chaining
347 */
348 public JOSMTestRules main(
349 final Runnable mapViewStateMockingRunnable,
350 final Runnable navigableComponentMockingRunnable
351 ) {
352 platform();
353 this.main = true;
354 this.mapViewStateMockingRunnable = mapViewStateMockingRunnable;
355 this.navigableComponentMockingRunnable = navigableComponentMockingRunnable;
356 return this;
357 }
358
359 private static class MockVersion extends Version {
360 MockVersion(final String propertiesString) {
361 super.initFromRevisionInfo(
362 new ByteArrayInputStream(propertiesString.getBytes())
363 );
364 }
365 }
366
367 @Override
368 public Statement apply(Statement base, Description description) {
369 Statement statement = base;
370 // counter-intuitively, Statements which need to have their setup routines performed *after* another one need to
371 // be added into the chain *before* that one, so that it ends up on the "inside".
372 if (timeout > 0) {
373 // TODO: new DisableOnDebug(timeout)
374 statement = new FailOnTimeoutStatement(statement, timeout);
375 }
376
377 // this half of TileSourceRule's initialization must happen after josm is set up
378 if (this.tileSourceRule != null) {
379 statement = this.tileSourceRule.applyRegisterLayers(statement, description);
380 }
381
382 statement = new CreateJosmEnvironment(statement);
383 if (josmHome != null) {
384 statement = josmHome.apply(statement, description);
385 }
386
387 // run mock tile server as the outermost Statement (started first) so it can hopefully be initializing in
388 // parallel with other setup
389 if (this.tileSourceRule != null) {
390 statement = this.tileSourceRule.applyRunServer(statement, description);
391 }
392 return statement;
393 }
394
395 /**
396 * Set up before running a test
397 * @throws InitializationError If an error occured while creating the required environment.
398 * @throws ReflectiveOperationException if a reflective access error occurs
399 */
400 protected void before() throws InitializationError, ReflectiveOperationException {
401 cleanUpFromJosmFixture();
402
403 if (this.assumeRevisionString != null) {
404 this.originalVersion = Version.getInstance();
405 final Version replacementVersion = new MockVersion(this.assumeRevisionString);
406 TestUtils.setPrivateStaticField(Version.class, "instance", replacementVersion);
407 }
408
409 // Add JOSM home
410 if (josmHome != null) {
411 try {
412 File home = josmHome.newFolder();
413 System.setProperty("josm.home", home.getAbsolutePath());
414 JosmBaseDirectories.getInstance().clearMemos();
415 } catch (IOException e) {
416 throw new InitializationError(e);
417 }
418 }
419
420 Config.setPreferencesInstance(Main.pref);
421 Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance());
422 Config.setUrlsProvider(JosmUrls.getInstance());
423 // All tests use the same timezone.
424 TimeZone.setDefault(DateUtils.UTC);
425
426 // Force log handers to reacquire reference to (junit's fake) stdout/stderr
427 for (Handler handler : Logging.getLogger().getHandlers()) {
428 if (handler instanceof Logging.ReacquiringConsoleHandler) {
429 handler.flush();
430 ((Logging.ReacquiringConsoleHandler) handler).reacquireOutputStream();
431 }
432 }
433 // Set log level to info
434 Logging.setLogLevel(Logging.LEVEL_INFO);
435
436 // Assume anonymous user
437 UserIdentityManager.getInstance().setAnonymous();
438 User.clearUserMap();
439 // Setup callbacks
440 DeleteCommand.setDeletionCallback(DeleteAction.defaultDeletionCallback);
441 OsmConnection.setOAuthAccessTokenFetcher(OAuthAuthorizationWizard::obtainAccessToken);
442
443 // Set up i18n
444 if (i18n != null) {
445 I18n.set(i18n);
446 }
447
448 // Add preferences
449 if (usePreferences) {
450 @SuppressWarnings("unchecked")
451 final Map<String, Setting<?>> defaultsMap = (Map<String, Setting<?>>) TestUtils.getPrivateField(Main.pref, "defaultsMap");
452 defaultsMap.clear();
453 Main.pref.resetToInitialState();
454 Main.pref.enableSaveOnPut(false);
455 // No pref init -> that would only create the preferences file.
456 // We force the use of a wrong API server, just in case anyone attempts an upload
457 Config.getPref().put("osm-server.url", "http://invalid");
458 }
459
460 // Set Platform
461 if (platform) {
462 Main.determinePlatformHook();
463 }
464
465 if (useHttps) {
466 try {
467 CertificateAmendment.addMissingCertificates();
468 } catch (IOException | GeneralSecurityException ex) {
469 throw new JosmRuntimeException(ex);
470 }
471 }
472
473 if (useProjection) {
474 Main.setProjection(Projections.getProjectionByCode("EPSG:3857")); // Mercator
475 }
476
477 if (useProjectionNadGrids) {
478 MainApplication.setupNadGridSources();
479 }
480
481 // Set API
482 if (useAPI == APIType.DEV) {
483 Config.getPref().put("osm-server.url", "https://api06.dev.openstreetmap.org/api");
484 } else if (useAPI == APIType.FAKE) {
485 FakeOsmApi api = FakeOsmApi.getInstance();
486 Config.getPref().put("osm-server.url", api.getServerUrl());
487 }
488
489 // Initialize API
490 if (useAPI != APIType.NONE) {
491 try {
492 OsmApi.getOsmApi().initialize(null);
493 } catch (OsmTransferCanceledException | OsmApiInitializationException e) {
494 throw new InitializationError(e);
495 }
496 }
497
498 if (useMapStyles) {
499 // Reset the map paint styles.
500 MapPaintStyles.readFromPreferences();
501 }
502
503 if (usePresets) {
504 // Reset the presets.
505 TaggingPresets.readFromPreferences();
506 }
507
508 if (territories) {
509 Territories.initialize();
510 }
511
512 if (rlTraffic) {
513 RightAndLefthandTraffic.initialize();
514 }
515
516 if (this.edtAssertionMockingRunnable != null) {
517 this.edtAssertionMockingRunnable.run();
518 }
519
520 if (commands) {
521 // TODO: Implement a more selective version of this once Main is restructured.
522 JOSMFixture.createUnitTestFixture().init(true);
523 } else {
524 if (main) {
525 // apply mockers to MapViewState and NavigableComponent whether we're headless or not
526 // as we generally don't create the josm main window even in non-headless mode.
527 if (this.mapViewStateMockingRunnable != null) {
528 this.mapViewStateMockingRunnable.run();
529 }
530 if (this.navigableComponentMockingRunnable != null) {
531 this.navigableComponentMockingRunnable.run();
532 }
533
534 new MainApplication();
535 JOSMFixture.initContentPane();
536 JOSMFixture.initMainPanel(true);
537 JOSMFixture.initToolbar();
538 JOSMFixture.initMainMenu();
539 }
540 }
541 }
542
543 /**
544 * Clean up what test not using these test rules may have broken.
545 */
546 @SuppressFBWarnings("DM_GC")
547 private void cleanUpFromJosmFixture() {
548 MemoryManagerTest.resetState(true);
549 cleanLayerEnvironment();
550 Main.pref.resetToInitialState();
551 Main.platform = null;
552 System.gc();
553 }
554
555 /**
556 * Cleans the Layer manager and the SelectionEventManager.
557 * You don't need to call this during tests, the test environment will do it for you.
558 * @since 12070
559 */
560 public static void cleanLayerEnvironment() {
561 // Get the instance before cleaning - this ensures that it is initialized.
562 SelectionEventManager eventManager = SelectionEventManager.getInstance();
563 MainApplication.getLayerManager().resetState();
564 eventManager.resetState();
565 }
566
567 /**
568 * @return TileSourceRule which is automatically started by this rule
569 */
570 public TileSourceRule getTileSourceRule() {
571 return this.tileSourceRule;
572 }
573
574 /**
575 * Clean up after running a test
576 * @throws ReflectiveOperationException if a reflective access error occurs
577 */
578 @SuppressFBWarnings("DM_GC")
579 protected void after() throws ReflectiveOperationException {
580 // Sync AWT Thread
581 GuiHelper.runInEDTAndWait(() -> { });
582 // Sync worker thread
583 final boolean[] queueEmpty = {false};
584 MainApplication.worker.submit(() -> queueEmpty[0] = true);
585 Awaitility.await().forever().until(() -> queueEmpty[0]);
586 // Remove all layers
587 cleanLayerEnvironment();
588 MemoryManagerTest.resetState(allowMemoryManagerLeaks);
589
590 // TODO: Remove global listeners and other global state.
591 Main.clearProjectionChangeListeners();
592 Main.pref.resetToInitialState();
593 Main.platform = null;
594
595 if (this.assumeRevisionString != null && this.originalVersion != null) {
596 TestUtils.setPrivateStaticField(Version.class, "instance", this.originalVersion);
597 }
598
599 Window[] windows = Window.getWindows();
600 if (windows.length != 0) {
601 Logging.info(
602 "Attempting to close {0} windows left open by tests: {1}",
603 windows.length,
604 Arrays.toString(windows)
605 );
606 }
607 GuiHelper.runInEDTAndWait(() -> {
608 for (Window window : windows) {
609 window.dispatchEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSING));
610 window.dispose();
611 }
612 });
613
614 // Parts of JOSM uses weak references - destroy them.
615 System.gc();
616 }
617
618 private final class CreateJosmEnvironment extends Statement {
619 private final Statement base;
620
621 private CreateJosmEnvironment(Statement base) {
622 this.base = base;
623 }
624
625 @Override
626 public void evaluate() throws Throwable {
627 before();
628 try {
629 base.evaluate();
630 } finally {
631 after();
632 }
633 }
634 }
635
636 enum APIType {
637 NONE, FAKE, DEV
638 }
639
640 /**
641 * The junit timeout statement has problems when switchting timezones. This one does not.
642 * @author Michael Zangl
643 */
644 private static class FailOnTimeoutStatement extends Statement {
645
646 private int timeout;
647 private Statement original;
648
649 FailOnTimeoutStatement(Statement original, int timeout) {
650 this.original = original;
651 this.timeout = timeout;
652 }
653
654 @Override
655 public void evaluate() throws Throwable {
656 TimeoutThread thread = new TimeoutThread(original);
657 thread.setDaemon(true);
658 thread.start();
659 thread.join(timeout);
660 thread.interrupt();
661 if (!thread.isDone) {
662 Throwable exception = thread.getExecutionException();
663 if (exception != null) {
664 throw exception;
665 } else {
666 Logging.debug("Thread state at timeout: {0}", Thread.getAllStackTraces());
667 throw new Exception(MessageFormat.format("Test timed out after {0}ms", timeout));
668 }
669 }
670 }
671 }
672
673 private static final class TimeoutThread extends Thread {
674 public boolean isDone;
675 private Statement original;
676 private Throwable exceptionCaught;
677
678 private TimeoutThread(Statement original) {
679 super("Timeout runner");
680 this.original = original;
681 }
682
683 public Throwable getExecutionException() {
684 return exceptionCaught;
685 }
686
687 @Override
688 public void run() {
689 try {
690 original.evaluate();
691 isDone = true;
692 } catch (Throwable e) {
693 exceptionCaught = e;
694 }
695 }
696 }
697
698 private boolean isDebugMode() {
699 return java.lang.management.ManagementFactory.getRuntimeMXBean().
700 getInputArguments().toString().indexOf("-agentlib:jdwp") > 0;
701 }
702}
Note: See TracBrowser for help on using the repository browser.