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}