source: josm/trunk/test/unit/org/openstreetmap/josm/TestUtils.java@ 18109

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

fix #21172 - Add method to create geometric distances from a point (patch by taylor.smock)

  • Property svn:eol-style set to native
File size: 25.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm;
3
4import static org.junit.jupiter.api.Assertions.assertArrayEquals;
5import static org.junit.jupiter.api.Assertions.assertEquals;
6import static org.junit.jupiter.api.Assertions.assertTrue;
7import static org.junit.jupiter.api.Assertions.fail;
8import static org.junit.jupiter.api.Assumptions.assumeFalse;
9
10import java.awt.Component;
11import java.awt.Container;
12import java.awt.Graphics2D;
13import java.io.File;
14import java.io.FileInputStream;
15import java.io.IOException;
16import java.io.InputStream;
17import java.lang.reflect.Field;
18import java.lang.reflect.Method;
19import java.security.AccessController;
20import java.security.PrivilegedAction;
21import java.time.Instant;
22import java.time.ZoneOffset;
23import java.time.format.DateTimeFormatter;
24import java.time.temporal.Temporal;
25import java.util.ArrayList;
26import java.util.Arrays;
27import java.util.Collection;
28import java.util.Comparator;
29import java.util.List;
30import java.util.Objects;
31import java.util.Set;
32import java.util.concurrent.ExecutionException;
33import java.util.concurrent.ThreadPoolExecutor;
34import java.util.concurrent.atomic.AtomicInteger;
35import java.util.function.Function;
36import java.util.stream.Collectors;
37import java.util.stream.IntStream;
38import java.util.stream.Stream;
39
40import org.junit.jupiter.api.Assertions;
41import org.openstreetmap.josm.command.Command;
42import org.openstreetmap.josm.data.osm.DataSet;
43import org.openstreetmap.josm.data.osm.Node;
44import org.openstreetmap.josm.data.osm.OsmPrimitive;
45import org.openstreetmap.josm.data.osm.OsmUtils;
46import org.openstreetmap.josm.data.osm.Relation;
47import org.openstreetmap.josm.data.osm.RelationMember;
48import org.openstreetmap.josm.data.osm.Way;
49import org.openstreetmap.josm.gui.MainApplication;
50import org.openstreetmap.josm.gui.progress.AbstractProgressMonitor;
51import org.openstreetmap.josm.gui.progress.CancelHandler;
52import org.openstreetmap.josm.gui.progress.ProgressMonitor;
53import org.openstreetmap.josm.gui.progress.ProgressTaskId;
54import org.openstreetmap.josm.gui.util.GuiHelper;
55import org.openstreetmap.josm.io.Compression;
56import org.openstreetmap.josm.testutils.FakeGraphics;
57import org.openstreetmap.josm.testutils.mockers.JOptionPaneSimpleMocker;
58import org.openstreetmap.josm.testutils.mockers.WindowMocker;
59import org.openstreetmap.josm.tools.JosmRuntimeException;
60import org.openstreetmap.josm.tools.ReflectionUtils;
61import org.openstreetmap.josm.tools.Utils;
62import org.openstreetmap.josm.tools.WikiReader;
63
64import com.github.tomakehurst.wiremock.WireMockServer;
65import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
66
67import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
68import io.github.classgraph.ClassGraph;
69import io.github.classgraph.ClassInfoList;
70import io.github.classgraph.ScanResult;
71import mockit.integration.TestRunnerDecorator;
72
73/**
74 * Various utils, useful for unit tests.
75 */
76public final class TestUtils {
77
78 private TestUtils() {
79 // Hide constructor for utility classes
80 }
81
82 /**
83 * Returns the path to test data root directory.
84 * @return path to test data root directory
85 */
86 public static String getTestDataRoot() {
87 String testDataRoot = System.getProperty("josm.test.data");
88 if (testDataRoot == null || testDataRoot.isEmpty()) {
89 testDataRoot = "test/data";
90 System.out.println("System property josm.test.data is not set, using '" + testDataRoot + "'");
91 }
92 return testDataRoot.endsWith("/") ? testDataRoot : testDataRoot + "/";
93 }
94
95 /**
96 * Gets path to test data directory for given ticket id.
97 * @param ticketid Ticket numeric identifier
98 * @return path to test data directory for given ticket id
99 */
100 public static String getRegressionDataDir(int ticketid) {
101 return TestUtils.getTestDataRoot() + "/regress/" + ticketid;
102 }
103
104 /**
105 * Gets path to given file in test data directory for given ticket id.
106 * @param ticketid Ticket numeric identifier
107 * @param filename File name
108 * @return path to given file in test data directory for given ticket id
109 */
110 public static String getRegressionDataFile(int ticketid, String filename) {
111 return getRegressionDataDir(ticketid) + '/' + filename;
112 }
113
114 /**
115 * Gets input stream to given file in test data directory for given ticket id.
116 * @param ticketid Ticket numeric identifier
117 * @param filename File name
118 * @return path to given file in test data directory for given ticket id
119 * @throws IOException if any I/O error occurs
120 */
121 public static InputStream getRegressionDataStream(int ticketid, String filename) throws IOException {
122 return Compression.getUncompressedFileInputStream(new File(getRegressionDataDir(ticketid), filename));
123 }
124
125 /**
126 * Checks that the given Comparator respects its contract on the given table.
127 * @param <T> type of elements
128 * @param comparator The comparator to test
129 * @param array The array sorted for test purpose
130 */
131 @SuppressFBWarnings(value = "RV_NEGATING_RESULT_OF_COMPARETO")
132 public static <T> void checkComparableContract(Comparator<T> comparator, T[] array) {
133 System.out.println("Validating Comparable contract on array of "+array.length+" elements");
134 // Check each compare possibility
135 for (int i = 0; i < array.length; i++) {
136 T r1 = array[i];
137 for (int j = i; j < array.length; j++) {
138 T r2 = array[j];
139 int a = comparator.compare(r1, r2);
140 int b = comparator.compare(r2, r1);
141 if (i == j || a == b) {
142 if (a != 0 || b != 0) {
143 fail(getFailMessage(r1, r2, a, b));
144 }
145 } else {
146 if (a != -b) {
147 fail(getFailMessage(r1, r2, a, b));
148 }
149 }
150 for (int k = j; k < array.length; k++) {
151 T r3 = array[k];
152 int c = comparator.compare(r1, r3);
153 int d = comparator.compare(r2, r3);
154 if (a > 0 && d > 0) {
155 if (c <= 0) {
156 fail(getFailMessage(r1, r2, r3, a, b, c, d));
157 }
158 } else if (a == 0 && d == 0) {
159 if (c != 0) {
160 fail(getFailMessage(r1, r2, r3, a, b, c, d));
161 }
162 } else if (a < 0 && d < 0) {
163 if (c >= 0) {
164 fail(getFailMessage(r1, r2, r3, a, b, c, d));
165 }
166 }
167 }
168 }
169 }
170 // Sort relation array
171 Arrays.sort(array, comparator);
172 }
173
174 /**
175 * Create a test matrix for parameterized tests.
176 * <br />
177 * <b>WARNING:</b> This can quickly become <i>very</i> large (this is combinatorial,
178 * so the returned {@link Stream} length is the size of the object collections multiplied by each other.
179 * So if you have three lists of size 3, 4, and 5, the stream size would be {@code 3 * 4 * 5} or 60 elements.
180 * <br />
181 * Generally speaking, you should avoid putting expected values into the test matrix.
182 *
183 * @param objectCollections The collections of objects. May include/provide {@code null}.
184 * @return The object arrays to be used as arguments. Note: The returned stream might not be thread-safe.
185 */
186 public static Stream<Object[]> createTestMatrix(List<?>... objectCollections) {
187 // Create the original object arrays
188 final AtomicInteger size = new AtomicInteger(1);
189 Stream.of(objectCollections).mapToInt(Collection::size).forEach(i -> size.set(size.get() * i));
190 final List<Object[]> testMatrix = new ArrayList<>(size.get());
191 final int[] indexes = IntStream.range(0, objectCollections.length).map(i -> 0).toArray();
192
193 // It is important to make a new object array each time (we modify them)
194 return IntStream.range(0, size.get()).mapToObj(index -> new Object[objectCollections.length]).peek(args -> {
195 // Just in case someone tries to make this parallel, synchronize on indexes to avoid most issues.
196 synchronized (indexes) {
197 // Set the args
198 for (int listIndex = 0; listIndex < objectCollections.length; listIndex++) {
199 args[listIndex] = objectCollections[listIndex].get(indexes[listIndex]);
200 }
201 // Increment indexes
202 for (int listIndex = 0; listIndex < objectCollections.length; listIndex++) {
203 indexes[listIndex] = indexes[listIndex] + 1;
204 if (indexes[listIndex] >= objectCollections[listIndex].size()) {
205 indexes[listIndex] = 0;
206 } else {
207 break;
208 }
209 }
210 }
211 });
212 }
213
214 private static <T> String getFailMessage(T o1, T o2, int a, int b) {
215 return new StringBuilder("Compared\no1: ").append(o1).append("\no2: ")
216 .append(o2).append("\ngave: ").append(a).append("/").append(b)
217 .toString();
218 }
219
220 private static <T> String getFailMessage(T o1, T o2, T o3, int a, int b, int c, int d) {
221 return new StringBuilder(getFailMessage(o1, o2, a, b))
222 .append("\nCompared\no1: ").append(o1).append("\no3: ").append(o3).append("\ngave: ").append(c)
223 .append("\nCompared\no2: ").append(o2).append("\no3: ").append(o3).append("\ngave: ").append(d)
224 .toString();
225 }
226
227 /**
228 * Returns a private field value.
229 * @param obj object
230 * @param fieldName private field name
231 * @return private field value
232 * @throws ReflectiveOperationException if a reflection operation error occurs
233 */
234 public static Object getPrivateField(Object obj, String fieldName) throws ReflectiveOperationException {
235 return getPrivateField(obj.getClass(), obj, fieldName);
236 }
237
238 /**
239 * Returns a private field value.
240 * @param cls object class
241 * @param obj object
242 * @param fieldName private field name
243 * @return private field value
244 * @throws ReflectiveOperationException if a reflection operation error occurs
245 */
246 public static Object getPrivateField(Class<?> cls, Object obj, String fieldName) throws ReflectiveOperationException {
247 Field f = cls.getDeclaredField(fieldName);
248 AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
249 f.setAccessible(true);
250 return null;
251 });
252 return f.get(obj);
253 }
254
255 /**
256 * Sets a private field value.
257 * @param obj object
258 * @param fieldName private field name
259 * @param value replacement value
260 * @throws ReflectiveOperationException if a reflection operation error occurs
261 */
262 public static void setPrivateField(
263 final Object obj,
264 final String fieldName,
265 final Object value
266 ) throws ReflectiveOperationException {
267 setPrivateField(obj.getClass(), obj, fieldName, value);
268 }
269
270 /**
271 * Sets a private field value.
272 * @param cls object class
273 * @param obj object
274 * @param fieldName private field name
275 * @param value replacement value
276 * @throws ReflectiveOperationException if a reflection operation error occurs
277 */
278 public static void setPrivateField(
279 final Class<?> cls,
280 final Object obj,
281 final String fieldName,
282 final Object value
283 ) throws ReflectiveOperationException {
284 Field f = cls.getDeclaredField(fieldName);
285 AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
286 f.setAccessible(true);
287 return null;
288 });
289 f.set(obj, value);
290 }
291
292 /**
293 * Returns a private static field value.
294 * @param cls object class
295 * @param fieldName private field name
296 * @return private field value
297 * @throws ReflectiveOperationException if a reflection operation error occurs
298 */
299 public static Object getPrivateStaticField(Class<?> cls, String fieldName) throws ReflectiveOperationException {
300 Field f = cls.getDeclaredField(fieldName);
301 AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
302 f.setAccessible(true);
303 return null;
304 });
305 return f.get(null);
306 }
307
308 /**
309 * Sets a private static field value.
310 * @param cls object class
311 * @param fieldName private field name
312 * @param value replacement value
313 * @throws ReflectiveOperationException if a reflection operation error occurs
314 */
315 public static void setPrivateStaticField(Class<?> cls, String fieldName, final Object value) throws ReflectiveOperationException {
316 Field f = cls.getDeclaredField(fieldName);
317 AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
318 f.setAccessible(true);
319 return null;
320 });
321 f.set(null, value);
322 }
323
324 /**
325 * Returns an instance of {@link AbstractProgressMonitor} which keeps track of the monitor state,
326 * but does not show the progress.
327 * @return a progress monitor
328 */
329 public static ProgressMonitor newTestProgressMonitor() {
330 return new AbstractProgressMonitor(new CancelHandler()) {
331
332 @Override
333 protected void doBeginTask() {
334 }
335
336 @Override
337 protected void doFinishTask() {
338 }
339
340 @Override
341 protected void doSetIntermediate(boolean value) {
342 }
343
344 @Override
345 protected void doSetTitle(String title) {
346 }
347
348 @Override
349 protected void doSetCustomText(String title) {
350 }
351
352 @Override
353 protected void updateProgress(double value) {
354 }
355
356 @Override
357 public void setProgressTaskId(ProgressTaskId taskId) {
358 }
359
360 @Override
361 public ProgressTaskId getProgressTaskId() {
362 return null;
363 }
364
365 @Override
366 public Component getWindowParent() {
367 return null;
368 }
369 };
370 }
371
372 /**
373 * Returns an instance of {@link Graphics2D}.
374 * @return a mockup graphics instance
375 */
376 public static Graphics2D newGraphics() {
377 return new FakeGraphics();
378 }
379
380 /**
381 * Makes sure the given primitive belongs to a data set.
382 * @param <T> OSM primitive type
383 * @param osm OSM primitive
384 * @return OSM primitive, attached to a new {@code DataSet}
385 */
386 public static <T extends OsmPrimitive> T addFakeDataSet(T osm) {
387 new DataSet(osm);
388 return osm;
389 }
390
391 /**
392 * Creates a new node with the given tags (see {@link OsmUtils#createPrimitive(java.lang.String)})
393 *
394 * @param tags the tags to set
395 * @return a new node
396 */
397 public static Node newNode(String tags) {
398 return (Node) OsmUtils.createPrimitive("node " + tags);
399 }
400
401 /**
402 * Creates a new way with the given tags (see {@link OsmUtils#createPrimitive(java.lang.String)}) and the nodes added
403 *
404 * @param tags the tags to set
405 * @param nodes the nodes to add
406 * @return a new way
407 */
408 public static Way newWay(String tags, Node... nodes) {
409 final Way way = (Way) OsmUtils.createPrimitive("way " + tags);
410 for (Node node : nodes) {
411 way.addNode(node);
412 }
413 return way;
414 }
415
416 /**
417 * Creates a new relation with the given tags (see {@link OsmUtils#createPrimitive(java.lang.String)}) and the members added
418 *
419 * @param tags the tags to set
420 * @param members the members to add
421 * @return a new relation
422 */
423 public static Relation newRelation(String tags, RelationMember... members) {
424 final Relation relation = (Relation) OsmUtils.createPrimitive("relation " + tags);
425 for (RelationMember member : members) {
426 relation.addMember(member);
427 }
428 return relation;
429 }
430
431 /**
432 * Creates a new empty command.
433 * @param ds data set
434 * @return a new empty command
435 */
436 public static Command newCommand(DataSet ds) {
437 return new Command(ds) {
438 @Override
439 public String getDescriptionText() {
440 return "";
441 }
442
443 @Override
444 public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
445 Collection<OsmPrimitive> added) {
446 // Do nothing
447 }
448 };
449 }
450
451 /**
452 * Ensures 100% code coverage for enums.
453 * @param enumClass enum class to cover
454 */
455 public static void superficialEnumCodeCoverage(Class<? extends Enum<?>> enumClass) {
456 try {
457 Method values = enumClass.getMethod("values");
458 Method valueOf = enumClass.getMethod("valueOf", String.class);
459 ReflectionUtils.setObjectsAccessible(values, valueOf);
460 for (Object o : (Object[]) values.invoke(null)) {
461 assertEquals(o, valueOf.invoke(null, ((Enum<?>) o).name()));
462 }
463 } catch (IllegalArgumentException | ReflectiveOperationException | SecurityException e) {
464 throw new JosmRuntimeException(e);
465 }
466 }
467
468 /**
469 * Get a descendant component by name.
470 * @param root The root component to start searching from.
471 * @param name The component name
472 * @return The component with that name or null if it does not exist.
473 * @since 12045
474 */
475 public static Component getComponentByName(Component root, String name) {
476 if (name.equals(root.getName())) {
477 return root;
478 } else if (root instanceof Container) {
479 Container container = (Container) root;
480 return Stream.of(container.getComponents())
481 .map(child -> getComponentByName(child, name))
482 .filter(Objects::nonNull)
483 .findFirst().orElse(null);
484 } else {
485 return null;
486 }
487 }
488
489 /**
490 * Use to assume that EqualsVerifier is working with the current JVM.
491 */
492 @SuppressWarnings("null")
493 public static void assumeWorkingEqualsVerifier() {
494 if (Utils.getJavaVersion() >= 16) {
495 // Byte Buddy often supports new class file versions for current EA releases if its experimental flag is set to true
496 System.setProperty("net.bytebuddy.experimental", "true");
497 }
498 try {
499 // Workaround to https://github.com/jqno/equalsverifier/issues/177
500 // Inspired by https://issues.apache.org/jira/browse/SOLR-11606
501 nl.jqno.equalsverifier.internal.lib.bytebuddy.ClassFileVersion.ofThisVm();
502 } catch (IllegalArgumentException e) {
503 assumeFalse(e != null);
504 }
505 }
506
507 /**
508 * Use to assume that JMockit is working with the current JVM.
509 */
510 @SuppressWarnings("null")
511 public static void assumeWorkingJMockit() {
512 try {
513 // Workaround to https://github.com/jmockit/jmockit1/issues/534
514 // Inspired by https://issues.apache.org/jira/browse/SOLR-11606
515 new WindowMocker();
516 new JOptionPaneSimpleMocker();
517 } catch (UnsupportedOperationException e) {
518 assumeFalse(e != null);
519 } finally {
520 TestRunnerDecorator.cleanUpAllMocks();
521 }
522 }
523
524 /**
525 * Return WireMock server serving files under ticket directory
526 * @param ticketId Ticket numeric identifier
527 * @return WireMock HTTP server on dynamic port
528 */
529 public static WireMockServer getWireMockServer(int ticketId) {
530 return new WireMockServer(
531 WireMockConfiguration.options()
532 .dynamicPort()
533 .usingFilesUnderDirectory(getRegressionDataDir(ticketId))
534 );
535 }
536
537 /**
538 * Return WireMock server
539 * @return WireMock HTTP server on dynamic port
540 */
541 public static WireMockServer getWireMockServer() {
542 return new WireMockServer(
543 WireMockConfiguration.options()
544 .withRootDirectory("test/data")
545 .dynamicPort()
546 );
547 }
548
549 /**
550 * Renders Temporal to RFC 1123 Date Time
551 * @param time to convert
552 * @return string representation according to RFC1123 of time
553 */
554 public static String getHTTPDate(Temporal time) {
555 return DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC).format(time);
556 }
557
558 /**
559 * Renders java time stamp to RFC 1123 Date Time
560 * @param time java timestamp (milliseconds from Epoch)
561 * @return string representation according to RFC1123 of time
562 */
563 public static String getHTTPDate(long time) {
564 return getHTTPDate(Instant.ofEpochMilli(time));
565 }
566
567 /**
568 * Throws AssertionError if contents of both files are not equal
569 * @param fileA File A
570 * @param fileB File B
571 */
572 public static void assertFileContentsEqual(final File fileA, final File fileB) {
573 assertTrue(fileA.exists());
574 assertTrue(fileA.canRead());
575 assertTrue(fileB.exists());
576 assertTrue(fileB.canRead());
577 try {
578 try (
579 FileInputStream streamA = new FileInputStream(fileA);
580 FileInputStream streamB = new FileInputStream(fileB);
581 ) {
582 assertArrayEquals(
583 Utils.readBytesFromStream(streamA),
584 Utils.readBytesFromStream(streamB)
585 );
586 }
587 } catch (IOException e) {
588 throw new RuntimeException(e);
589 }
590 }
591
592 /**
593 * Replaces {@linkplain System#lineSeparator() system dependent line separators} with {@code \n}
594 * and calls {@link Assertions#assertEquals(java.lang.Object, java.lang.Object)}.
595 * @param expected expected value
596 * @param actual the value to check against <code>expected</code>
597 */
598 public static void assertEqualsNewline(String expected, String actual) {
599 assertEquals(expected, actual.replace(System.lineSeparator(), "\n"));
600 }
601
602 /**
603 * Waits until any asynchronous operations launched by the test on the EDT or worker threads have
604 * (almost certainly) completed.
605 */
606 public static void syncEDTAndWorkerThreads() {
607 boolean workerQueueEmpty = false;
608 while (!workerQueueEmpty) {
609 try {
610 // once our own task(s) have made it to the front of their respective queue(s),
611 // they're both executing at the same time and we know there aren't any outstanding
612 // worker tasks, then presumably the only way there could be incomplete operations
613 // is if the EDT had launched a deferred task to run on itself or perhaps set up a
614 // swing timer - neither are particularly common patterns in JOSM (?)
615 //
616 // there shouldn't be a risk of creating a deadlock in doing this as there shouldn't
617 // (...couldn't?) be EDT operations waiting on the results of a worker task.
618 workerQueueEmpty = MainApplication.worker.submit(
619 () -> GuiHelper.runInEDTAndWaitAndReturn(
620 () -> ((ThreadPoolExecutor) MainApplication.worker).getQueue().isEmpty()
621 )
622 ).get();
623 } catch (InterruptedException | ExecutionException e) {
624 // inconclusive - retry...
625 workerQueueEmpty = false;
626 }
627 }
628 }
629
630 /**
631 * Returns all JOSM subtypes of the given class.
632 * @param <T> class
633 * @param superClass class
634 * @return all JOSM subtypes of the given class
635 */
636 public static <T> Set<Class<? extends T>> getJosmSubtypes(Class<T> superClass) {
637 try (ScanResult scan = new ClassGraph().acceptPackages("org.openstreetmap.josm").ignoreClassVisibility().scan()) {
638 Function<String, ClassInfoList> lambda = superClass.isInterface() ? scan::getClassesImplementing : scan::getSubclasses;
639 return lambda.apply(superClass.getName())
640 .asMap().values().stream().map(x -> x.loadClass(superClass)).collect(Collectors.toSet());
641 }
642 }
643
644 /**
645 * Determines if OSM DEV_API credential have been provided. Required for functional tests.
646 * @return {@code true} if {@code osm.username} and {@code osm.password} have been defined on the command line
647 */
648 public static boolean areCredentialsProvided() {
649 return Utils.getSystemProperty("osm.username") != null && Utils.getSystemProperty("osm.password") != null;
650 }
651
652 /**
653 * Returns the ignored error messages listed on
654 * <a href="https://josm.openstreetmap.de/wiki/IntegrationTestIgnores">JOSM wiki</a> for a given test.
655 * @param integrationTest The integration test class
656 * @return the ignored error messages listed on JOSM wiki for this test.
657 * @throws IOException in case of I/O error
658 */
659 public static List<String> getIgnoredErrorMessages(Class<?> integrationTest) throws IOException {
660 return Arrays.stream(new WikiReader()
661 .read("https://josm.openstreetmap.de/wiki/IntegrationTestIgnores?format=txt").split("\\n", -1))
662 .filter(s -> s.startsWith("|| " + integrationTest.getSimpleName() + " ||"))
663 .map(s -> s.substring(s.indexOf("{{{") + 3, s.indexOf("}}}")))
664 .collect(Collectors.toList());
665 }
666}
Note: See TracBrowser for help on using the repository browser.