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

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

see #15229 - deprecate Main.pref

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.Preferences;
29import org.openstreetmap.josm.data.UserIdentityManager;
30import org.openstreetmap.josm.data.Version;
31import org.openstreetmap.josm.data.osm.User;
32import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
33import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
34import org.openstreetmap.josm.data.preferences.JosmUrls;
35import org.openstreetmap.josm.data.projection.ProjectionRegistry;
36import org.openstreetmap.josm.data.projection.Projections;
37import org.openstreetmap.josm.gui.MainApplication;
38import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
39import org.openstreetmap.josm.gui.oauth.OAuthAuthorizationWizard;
40import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
41import org.openstreetmap.josm.gui.util.GuiHelper;
42import org.openstreetmap.josm.io.CertificateAmendment;
43import org.openstreetmap.josm.io.OsmApi;
44import org.openstreetmap.josm.io.OsmApiInitializationException;
45import org.openstreetmap.josm.io.OsmConnection;
46import org.openstreetmap.josm.io.OsmTransferCanceledException;
47import org.openstreetmap.josm.spi.preferences.Config;
48import org.openstreetmap.josm.spi.preferences.Setting;
49import org.openstreetmap.josm.testutils.mockers.EDTAssertionMocker;
50import org.openstreetmap.josm.testutils.mockers.WindowlessMapViewStateMocker;
51import org.openstreetmap.josm.testutils.mockers.WindowlessNavigatableComponentMocker;
52import org.openstreetmap.josm.tools.I18n;
53import org.openstreetmap.josm.tools.JosmRuntimeException;
54import org.openstreetmap.josm.tools.Logging;
55import org.openstreetmap.josm.tools.MemoryManagerTest;
56import org.openstreetmap.josm.tools.RightAndLefthandTraffic;
57import org.openstreetmap.josm.tools.Territories;
58import org.openstreetmap.josm.tools.date.DateUtils;
59
60import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
61
62/**
63 * This class runs a test in an environment that resembles the one used by the JOSM main application.
64 * <p>
65 * The environment is reset before every test. You can specify the components to which you need access using the methods of this class.
66 * For example, invoking {@link #preferences()} gives you access to the (default) preferences.
67 *
68 * @author Michael Zangl
69 */
70public class JOSMTestRules implements TestRule {
71 private int timeout = isDebugMode() ? -1 : 10 * 1000;
72 private TemporaryFolder josmHome;
73 private boolean usePreferences = false;
74 private APIType useAPI = APIType.NONE;
75 private String i18n = null;
76 private TileSourceRule tileSourceRule;
77 private String assumeRevisionString;
78 private Version originalVersion;
79 private Runnable mapViewStateMockingRunnable;
80 private Runnable navigableComponentMockingRunnable;
81 private Runnable edtAssertionMockingRunnable;
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 * @deprecated Not needed anymore
153 */
154 @Deprecated
155 public JOSMTestRules platform() {
156 return this;
157 }
158
159 /**
160 * Mock this test's assumed JOSM version (as reported by {@link Version}).
161 * @param revisionProperties mock contents of JOSM's {@code REVISION} properties file
162 * @return this instance, for easy chaining
163 */
164 public JOSMTestRules assumeRevision(final String revisionProperties) {
165 this.assumeRevisionString = revisionProperties;
166 return this;
167 }
168
169 /**
170 * Enable the dev.openstreetmap.org API for this test.
171 * @return this instance, for easy chaining
172 */
173 public JOSMTestRules devAPI() {
174 preferences();
175 useAPI = APIType.DEV;
176 return this;
177 }
178
179 /**
180 * Use the {@link FakeOsmApi} for testing.
181 * @return this instance, for easy chaining
182 */
183 public JOSMTestRules fakeAPI() {
184 useAPI = APIType.FAKE;
185 return this;
186 }
187
188 /**
189 * Set up default projection (Mercator)
190 * @return this instance, for easy chaining
191 */
192 public JOSMTestRules projection() {
193 useProjection = true;
194 return this;
195 }
196
197 /**
198 * Set up loading of NTV2 grit shift files to support projections that need them.
199 * @return this instance, for easy chaining
200 */
201 public JOSMTestRules projectionNadGrids() {
202 useProjectionNadGrids = true;
203 return this;
204 }
205
206 /**
207 * Set up HTTPS certificates
208 * @return this instance, for easy chaining
209 */
210 public JOSMTestRules https() {
211 useHttps = 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 Preferences pref = Preferences.main();
422 Config.setPreferencesInstance(pref);
423 Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance());
424 Config.setUrlsProvider(JosmUrls.getInstance());
425 // All tests use the same timezone.
426 TimeZone.setDefault(DateUtils.UTC);
427
428 // Force log handers to reacquire reference to (junit's fake) stdout/stderr
429 for (Handler handler : Logging.getLogger().getHandlers()) {
430 if (handler instanceof Logging.ReacquiringConsoleHandler) {
431 handler.flush();
432 ((Logging.ReacquiringConsoleHandler) handler).reacquireOutputStream();
433 }
434 }
435 // Set log level to info
436 Logging.setLogLevel(Logging.LEVEL_INFO);
437
438 // Assume anonymous user
439 UserIdentityManager.getInstance().setAnonymous();
440 User.clearUserMap();
441 // Setup callbacks
442 DeleteCommand.setDeletionCallback(DeleteAction.defaultDeletionCallback);
443 OsmConnection.setOAuthAccessTokenFetcher(OAuthAuthorizationWizard::obtainAccessToken);
444
445 // Set up i18n
446 if (i18n != null) {
447 I18n.set(i18n);
448 }
449
450 // Add preferences
451 if (usePreferences) {
452 @SuppressWarnings("unchecked")
453 final Map<String, Setting<?>> defaultsMap = (Map<String, Setting<?>>) TestUtils.getPrivateField(pref, "defaultsMap");
454 defaultsMap.clear();
455 pref.resetToInitialState();
456 pref.enableSaveOnPut(false);
457 // No pref init -> that would only create the preferences file.
458 // We force the use of a wrong API server, just in case anyone attempts an upload
459 Config.getPref().put("osm-server.url", "http://invalid");
460 }
461
462 if (useHttps) {
463 try {
464 CertificateAmendment.addMissingCertificates();
465 } catch (IOException | GeneralSecurityException ex) {
466 throw new JosmRuntimeException(ex);
467 }
468 }
469
470 if (useProjection) {
471 ProjectionRegistry.setProjection(Projections.getProjectionByCode("EPSG:3857")); // Mercator
472 }
473
474 if (useProjectionNadGrids) {
475 MainApplication.setupNadGridSources();
476 }
477
478 // Set API
479 if (useAPI == APIType.DEV) {
480 Config.getPref().put("osm-server.url", "https://api06.dev.openstreetmap.org/api");
481 } else if (useAPI == APIType.FAKE) {
482 FakeOsmApi api = FakeOsmApi.getInstance();
483 Config.getPref().put("osm-server.url", api.getServerUrl());
484 }
485
486 // Initialize API
487 if (useAPI != APIType.NONE) {
488 try {
489 OsmApi.getOsmApi().initialize(null);
490 } catch (OsmTransferCanceledException | OsmApiInitializationException e) {
491 throw new InitializationError(e);
492 }
493 }
494
495 if (useMapStyles) {
496 // Reset the map paint styles.
497 MapPaintStyles.readFromPreferences();
498 }
499
500 if (usePresets) {
501 // Reset the presets.
502 TaggingPresets.readFromPreferences();
503 }
504
505 if (territories) {
506 Territories.initialize();
507 }
508
509 if (rlTraffic) {
510 RightAndLefthandTraffic.initialize();
511 }
512
513 if (this.edtAssertionMockingRunnable != null) {
514 this.edtAssertionMockingRunnable.run();
515 }
516
517 if (commands) {
518 // TODO: Implement a more selective version of this once Main is restructured.
519 JOSMFixture.createUnitTestFixture().init(true);
520 } else {
521 if (main) {
522 // apply mockers to MapViewState and NavigableComponent whether we're headless or not
523 // as we generally don't create the josm main window even in non-headless mode.
524 if (this.mapViewStateMockingRunnable != null) {
525 this.mapViewStateMockingRunnable.run();
526 }
527 if (this.navigableComponentMockingRunnable != null) {
528 this.navigableComponentMockingRunnable.run();
529 }
530
531 new MainApplication();
532 JOSMFixture.initContentPane();
533 JOSMFixture.initMainPanel(true);
534 JOSMFixture.initToolbar();
535 JOSMFixture.initMainMenu();
536 }
537 }
538 }
539
540 /**
541 * Clean up what test not using these test rules may have broken.
542 */
543 @SuppressFBWarnings("DM_GC")
544 private void cleanUpFromJosmFixture() {
545 MemoryManagerTest.resetState(true);
546 cleanLayerEnvironment();
547 Preferences.main().resetToInitialState();
548 System.gc();
549 }
550
551 /**
552 * Cleans the Layer manager and the SelectionEventManager.
553 * You don't need to call this during tests, the test environment will do it for you.
554 * @since 12070
555 */
556 public static void cleanLayerEnvironment() {
557 // Get the instance before cleaning - this ensures that it is initialized.
558 SelectionEventManager eventManager = SelectionEventManager.getInstance();
559 MainApplication.getLayerManager().resetState();
560 eventManager.resetState();
561 }
562
563 /**
564 * @return TileSourceRule which is automatically started by this rule
565 */
566 public TileSourceRule getTileSourceRule() {
567 return this.tileSourceRule;
568 }
569
570 /**
571 * Clean up after running a test
572 * @throws ReflectiveOperationException if a reflective access error occurs
573 */
574 @SuppressFBWarnings("DM_GC")
575 protected void after() throws ReflectiveOperationException {
576 // Sync AWT Thread
577 GuiHelper.runInEDTAndWait(() -> { });
578 // Sync worker thread
579 final boolean[] queueEmpty = {false};
580 MainApplication.worker.submit(() -> queueEmpty[0] = true);
581 Awaitility.await().forever().until(() -> queueEmpty[0]);
582 // Remove all layers
583 cleanLayerEnvironment();
584 MemoryManagerTest.resetState(allowMemoryManagerLeaks);
585
586 // TODO: Remove global listeners and other global state.
587 ProjectionRegistry.clearProjectionChangeListeners();
588 Preferences.main().resetToInitialState();
589
590 if (this.assumeRevisionString != null && this.originalVersion != null) {
591 TestUtils.setPrivateStaticField(Version.class, "instance", this.originalVersion);
592 }
593
594 Window[] windows = Window.getWindows();
595 if (windows.length != 0) {
596 Logging.info(
597 "Attempting to close {0} windows left open by tests: {1}",
598 windows.length,
599 Arrays.toString(windows)
600 );
601 }
602 GuiHelper.runInEDTAndWait(() -> {
603 for (Window window : windows) {
604 window.dispatchEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSING));
605 window.dispose();
606 }
607 });
608
609 // Parts of JOSM uses weak references - destroy them.
610 System.gc();
611 }
612
613 private final class CreateJosmEnvironment extends Statement {
614 private final Statement base;
615
616 private CreateJosmEnvironment(Statement base) {
617 this.base = base;
618 }
619
620 @Override
621 public void evaluate() throws Throwable {
622 before();
623 try {
624 base.evaluate();
625 } finally {
626 after();
627 }
628 }
629 }
630
631 enum APIType {
632 NONE, FAKE, DEV
633 }
634
635 /**
636 * The junit timeout statement has problems when switchting timezones. This one does not.
637 * @author Michael Zangl
638 */
639 private static class FailOnTimeoutStatement extends Statement {
640
641 private int timeout;
642 private Statement original;
643
644 FailOnTimeoutStatement(Statement original, int timeout) {
645 this.original = original;
646 this.timeout = timeout;
647 }
648
649 @Override
650 public void evaluate() throws Throwable {
651 TimeoutThread thread = new TimeoutThread(original);
652 thread.setDaemon(true);
653 thread.start();
654 thread.join(timeout);
655 thread.interrupt();
656 if (!thread.isDone) {
657 Throwable exception = thread.getExecutionException();
658 if (exception != null) {
659 throw exception;
660 } else {
661 Logging.debug("Thread state at timeout: {0}", Thread.getAllStackTraces());
662 throw new Exception(MessageFormat.format("Test timed out after {0}ms", timeout));
663 }
664 }
665 }
666 }
667
668 private static final class TimeoutThread extends Thread {
669 public boolean isDone;
670 private Statement original;
671 private Throwable exceptionCaught;
672
673 private TimeoutThread(Statement original) {
674 super("Timeout runner");
675 this.original = original;
676 }
677
678 public Throwable getExecutionException() {
679 return exceptionCaught;
680 }
681
682 @Override
683 public void run() {
684 try {
685 original.evaluate();
686 isDone = true;
687 } catch (Throwable e) {
688 exceptionCaught = e;
689 }
690 }
691 }
692
693 private boolean isDebugMode() {
694 return java.lang.management.ManagementFactory.getRuntimeMXBean().
695 getInputArguments().toString().indexOf("-agentlib:jdwp") > 0;
696 }
697}
Note: See TracBrowser for help on using the repository browser.