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

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

see #15229 - deprecate all Main methods related to projections. New ProjectionRegistry class

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