001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau;
018
019import static java.util.stream.Collectors.*;
020import static org.apache.juneau.commons.utils.ThrowableUtils.*;
021import static org.junit.jupiter.api.Assertions.*;
022
023import java.io.*;
024import java.net.*;
025import java.nio.file.*;
026import java.time.*;
027import java.util.*;
028import java.util.function.*;
029import java.util.stream.*;
030
031import org.apache.juneau.commons.utils.*;
032import org.junit.jupiter.api.*;
033
034/**
035 * Shared, marshall-free test utilities common to Juneau test modules.
036 *
037 * <p>
038 * Provides the stable pure-JDK/commons subset of the per-module {@code TestUtils} classes: exception-message
039 * assertions, equality helpers, stream/reader factories, locale/timezone toggles, and URL construction.  Module-level
040 * {@code TestUtils} classes extend this base and add their own marshall-bound helpers (e.g. {@code assertJson},
041 * {@code json}, {@code validateXml}).
042 *
043 * <p>
044 * This class lives in the {@code juneau-test-utils} module, which depends only on {@code juneau-commons} and JUnit, so
045 * it never introduces a {@code juneau-marshall} dependency onto its consumers.
046 */
047@SuppressWarnings({
048   "java:S5960" // test-support utility shipped as main source; assertions are intentional
049})
050public abstract class BasicTestUtils extends Shorts {
051
052   /** How long {@link #await(BooleanSupplier, Duration)} sleeps between checks of its condition. */
053   private static final long AWAIT_INTERVAL_MS = 25;
054
055   private static final ThreadLocal<TimeZone> SYSTEM_TIME_ZONE = new ThreadLocal<>();
056
057   /** Holds the previous default locale while a test overrides it via {@link #setLocale(Locale)}. */
058   public static final ThreadLocal<Locale> SYSTEM_LOCALE = new ThreadLocal<>();
059
060   /**
061    * Asserts that all the specified values are equal to the first value.
062    *
063    * @param values The values to compare.  Element 0 is the expected value.
064    */
065   public static void assertEqualsAll(Object...values) {
066      for (var i = 1; i < values.length; i++) {
067         assertEquals(values[0], values[i], fs("Elements at index %1$s and %2$s did not match. %1$s=%3$s, %2$s=%4$s", 0, i, r(values[0]), r(values[i])));
068      }
069   }
070
071   /**
072    * Asserts that the specified actual value does not equal any of the specified values.
073    *
074    * @param actual The actual value.  Must not be <jk>null</jk>.
075    * @param values The values that must not match the actual value.
076    */
077   public static void assertNotEqualsAny(Object actual, Object...values) {
078      assertNotNull(actual, "Value was null.");
079      for (var i = 0; i < values.length; i++) {
080         assertNotEquals(values[i], actual, fs("Element at index %s unexpectedly matched.  expected=%s, actual=%s", i, values[i], s(actual)));
081      }
082   }
083
084   /**
085    * Asserts that the specified executable throws an exception whose chained messages contain all the specified substrings.
086    *
087    * @param <T> The expected exception type.
088    * @param expectedType The expected exception type.
089    * @param expectedSubstrings The substrings expected in the exception message chain.
090    * @param executable The code expected to throw.
091    * @return The thrown exception.
092    */
093   public static <T extends Throwable> T assertThrowsWithMessage(Class<T> expectedType, List<String> expectedSubstrings, org.junit.jupiter.api.function.Executable executable) {
094      var exception = Assertions.assertThrows(expectedType, executable);
095      var messages = getMessages(exception);
096      expectedSubstrings.forEach(x -> assertTrue(messages.contains(x), fs("Expected message to contain: %s.\nActual:\n%s", x, messages)));
097      return exception;
098   }
099
100   /**
101    * Asserts that the specified executable throws an exception whose chained messages contain the specified substring.
102    *
103    * @param <T> The expected exception type.
104    * @param expectedType The expected exception type.
105    * @param expectedSubstring The substring expected in the exception message chain.
106    * @param executable The code expected to throw.
107    * @return The thrown exception.
108    */
109   public static <T extends Throwable> T assertThrowsWithMessage(Class<T> expectedType, String expectedSubstring, org.junit.jupiter.api.function.Executable executable) {
110      var exception = Assertions.assertThrows(expectedType, executable);
111      var messages = getMessages(exception);
112      assertTrue(messages.contains(expectedSubstring), fs("Expected message to contain: %s.\nActual:\n%s", expectedSubstring, messages));
113      return exception;
114   }
115
116   /**
117    * Collects the newline-joined message chain of the specified throwable and all its causes.
118    *
119    * @param t The throwable.
120    * @return The joined message chain.
121    */
122   protected static String getMessages(Throwable t) {
123      return Stream.iterate(t, Throwable::getCause).takeWhile(e -> e != null).map(Throwable::getMessage).collect(joining("\n"));
124   }
125
126   /**
127    * Creates an input stream from the specified string.
128    *
129    * @param in The contents of the stream.
130    * @return A new input stream.
131    */
132   public static final ByteArrayInputStream inputStream(String in) {
133      return new ByteArrayInputStream(in.getBytes());
134   }
135
136   /**
137    * Creates a reader from the specified string.
138    *
139    * @param in The contents of the reader.
140    * @return A new reader.
141    */
142   public static final StringReader reader(String in) {
143      return new StringReader(in);
144   }
145
146   /**
147    * Temporarily sets the default system locale to the specified locale.
148    *
149    * @param v The new default locale.
150    */
151   public static final void setLocale(Locale v) {
152      SYSTEM_LOCALE.set(Locale.getDefault());
153      Locale.setDefault(v);
154   }
155
156   /**
157    * Temporarily sets the default system timezone to the specified timezone ID.
158    *
159    * @param v The new default timezone ID.
160    */
161   public static final synchronized void setTimeZone(String v) {
162      SYSTEM_TIME_ZONE.set(TimeZone.getDefault());
163      TimeZone.setDefault(TimeZone.getTimeZone(v));
164   }
165
166   /**
167    * Restores the default system locale saved by {@link #setLocale(Locale)}.
168    */
169   public static final void unsetLocale() {
170      Locale.setDefault(SYSTEM_LOCALE.get());
171   }
172
173   /**
174    * Restores the default system timezone saved by {@link #setTimeZone(String)}.
175    */
176   public static final synchronized void unsetTimeZone() {
177      TimeZone.setDefault(SYSTEM_TIME_ZONE.get());
178      SYSTEM_TIME_ZONE.remove();
179   }
180
181   /**
182    * Constructs a {@link URL} object from a string.
183    *
184    * @param value The URL string.
185    * @return A new {@link URL} object.
186    */
187   public static URL url(String value) {
188      return safe(()->new URI(value).toURL());
189   }
190
191   /**
192    * Polls a condition until it holds or the timeout elapses.
193    *
194    * <p>
195    * For tests that wait on state with no signal to block on, such as a file another process writes.  Use this
196    * instead of a hand-rolled sleep loop.  The condition is checked once more after the last sleep, so one that comes
197    * true right at the deadline still counts.
198    *
199    * @param condition The condition to wait for.  Must not be <jk>null</jk>.
200    * @param timeout How long to wait.  Must not be <jk>null</jk>.
201    * @return <jk>true</jk> once the condition holds, or <jk>false</jk> if it still does not hold after the timeout.
202    * @throws InterruptedException If the thread is interrupted while waiting.
203    */
204   @SuppressWarnings({
205      "java:S2925" // The one shared bounded poll; the state it waits on has no signal to block on.
206   })
207   public static boolean await(BooleanSupplier condition, Duration timeout) throws InterruptedException {
208      var deadline = System.nanoTime() + timeout.toNanos();
209      while (! condition.getAsBoolean()) {
210         var left = deadline - System.nanoTime();
211         if (left <= 0)
212            return false;
213         Thread.sleep(Math.min(AWAIT_INTERVAL_MS, Duration.ofNanos(left).toMillis() + 1));
214      }
215      return true;
216   }
217
218   /**
219    * Waits until a file exists or the timeout elapses.
220    *
221    * @param path The file to wait for.  Must not be <jk>null</jk>.
222    * @param timeout How long to wait.  Must not be <jk>null</jk>.
223    * @return <jk>true</jk> once the file exists, or <jk>false</jk> if it still does not exist after the timeout.
224    * @throws InterruptedException If the thread is interrupted while waiting.
225    * @see #await(BooleanSupplier, Duration)
226    */
227   public static boolean awaitFile(Path path, Duration timeout) throws InterruptedException {
228      return await(() -> Files.exists(path), timeout);
229   }
230
231   /**
232    * Returns an empty array of the specified type.
233    *
234    * @param <T> The component type.
235    * @param t The component type.
236    * @return A new empty array of the specified type.
237    */
238   public static <T> T[] ea(Class<T> t) {
239      return CollectionUtils.array(t, 0);
240   }
241}