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.assertions;
018
019import static java.util.Collections.*;
020import static org.apache.juneau.common.utils.ThrowableUtils.*;
021import static org.apache.juneau.common.utils.Utils.*;
022
023import java.io.*;
024import java.util.*;
025import java.util.function.*;
026
027import org.apache.juneau.common.utils.*;
028import org.apache.juneau.cp.*;
029import org.apache.juneau.internal.*;
030import org.apache.juneau.serializer.*;
031
032/**
033 * Used for fluent assertion calls against throwables.
034 *
035 * <h5 class='section'>Test Methods:</h5>
036 * <p>
037 * <ul class='javatree'>
038 *    <li class='jc'>{@link FluentObjectAssertion}
039 *    <ul class='javatreec'>
040 *       <li class='jm'>{@link FluentObjectAssertion#isExists() isExists()}
041 *       <li class='jm'>{@link FluentObjectAssertion#is(Object) is(Object)}
042 *       <li class='jm'>{@link FluentObjectAssertion#is(Predicate) is(Predicate)}
043 *       <li class='jm'>{@link FluentObjectAssertion#isNot(Object) isNot(Object)}
044 *       <li class='jm'>{@link FluentObjectAssertion#isAny(Object...) isAny(Object...)}
045 *       <li class='jm'>{@link FluentObjectAssertion#isNotAny(Object...) isNotAny(Object...)}
046 *       <li class='jm'>{@link FluentObjectAssertion#isNull() isNull()}
047 *       <li class='jm'>{@link FluentObjectAssertion#isNotNull() isNotNull()}
048 *       <li class='jm'>{@link FluentObjectAssertion#isString(String) isString(String)}
049 *       <li class='jm'>{@link FluentObjectAssertion#isJson(String) isJson(String)}
050 *       <li class='jm'>{@link FluentObjectAssertion#isSame(Object) isSame(Object)}
051 *       <li class='jm'>{@link FluentObjectAssertion#isSameJsonAs(Object) isSameJsonAs(Object)}
052 *       <li class='jm'>{@link FluentObjectAssertion#isSameSortedJsonAs(Object) isSameSortedJsonAs(Object)}
053 *       <li class='jm'>{@link FluentObjectAssertion#isSameSerializedAs(Object, WriterSerializer) isSameSerializedAs(Object, WriterSerializer)}
054 *       <li class='jm'>{@link FluentObjectAssertion#isType(Class) isType(Class)}
055 *       <li class='jm'>{@link FluentObjectAssertion#isExactType(Class) isExactType(Class)}
056 *    </ul>
057 * </ul>
058 *
059 * <h5 class='section'>Transform Methods:</h5>
060 * <p>
061 * <ul class='javatree'>
062 *    <li class='jc'>{@link FluentThrowableAssertion}
063 *    <ul class='javatreec'>
064 *       <li class='jm'>{@link FluentThrowableAssertion#asMessage() asMessage()}
065 *       <li class='jm'>{@link FluentThrowableAssertion#asMessages() asMessages()}
066 *       <li class='jm'>{@link FluentThrowableAssertion#asLocalizedMessage() asLocalizedMessage()}
067 *       <li class='jm'>{@link FluentThrowableAssertion#asLocalizedMessages() asLocalizedMessages()}
068 *       <li class='jm'>{@link FluentThrowableAssertion#asStackTrace() asStackTrace()}
069 *       <li class='jm'>{@link FluentThrowableAssertion#asCausedBy() asCausedBy()}
070 *       <li class='jm'>{@link FluentThrowableAssertion#asCausedBy(Class) asCausedBy(Class)}
071 *       <li class='jm'>{@link FluentThrowableAssertion#asFind(Class) asFind(Class)}
072 *    </ul>
073 *    <li class='jc'>{@link FluentObjectAssertion}
074 *    <ul class='javatreec'>
075 *       <li class='jm'>{@link FluentObjectAssertion#asString() asString()}
076 *       <li class='jm'>{@link FluentObjectAssertion#asString(WriterSerializer) asString(WriterSerializer)}
077 *       <li class='jm'>{@link FluentObjectAssertion#asString(Function) asString(Function)}
078 *       <li class='jm'>{@link FluentObjectAssertion#asJson() asJson()}
079 *       <li class='jm'>{@link FluentObjectAssertion#asJsonSorted() asJsonSorted()}
080 *       <li class='jm'>{@link FluentObjectAssertion#asTransformed(Function) asApplied(Function)}
081 *       <li class='jm'>{@link FluentObjectAssertion#asAny() asAny()}
082 * </ul>
083 * </ul>
084 *
085 * <h5 class='section'>Configuration Methods:</h5>
086 * <p>
087 * <ul class='javatree'>
088 *    <li class='jc'>{@link Assertion}
089 *    <ul class='javatreec'>
090 *       <li class='jm'>{@link Assertion#setMsg(String, Object...) setMsg(String, Object...)}
091 *       <li class='jm'>{@link Assertion#setOut(PrintStream) setOut(PrintStream)}
092 *       <li class='jm'>{@link Assertion#setSilent() setSilent()}
093 *       <li class='jm'>{@link Assertion#setStdOut() setStdOut()}
094 *       <li class='jm'>{@link Assertion#setThrowable(Class) setThrowable(Class)}
095*  </ul>
096 * </ul>
097 *
098 * <h5 class='section'>See Also:</h5><ul>
099 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauEcosystemOverview">Juneau Ecosystem Overview</a>
100 * </ul>
101 *
102 * @param <T> The throwable type.
103 * @param <R> The return type.
104 */
105public class FluentThrowableAssertion<T extends Throwable,R> extends FluentObjectAssertion<T,R> {
106
107   //-----------------------------------------------------------------------------------------------------------------
108   // Static
109   //-----------------------------------------------------------------------------------------------------------------
110
111   private static final Messages MESSAGES = Messages.of(FluentThrowableAssertion.class, "Messages");
112   private static final String
113      MSG_exceptionWasNotExpectedType = MESSAGES.getString("exceptionWasNotExpectedType"),
114      MSG_exceptionWasNotThrown = MESSAGES.getString("exceptionWasNotThrown"),
115      MSG_causedByExceptionNotExpectedType = MESSAGES.getString("causedByExceptionNotExpectedType");
116
117   //-----------------------------------------------------------------------------------------------------------------
118   // Instance
119   //-----------------------------------------------------------------------------------------------------------------
120
121   /**
122    * Constructor.
123    *
124    * @param value
125    *    The object being tested.
126    *    <br>Can be <jk>null</jk>.
127    * @param returns
128    *    The object to return after a test method is called.
129    *    <br>If <jk>null</jk>, the test method returns this object allowing multiple test method calls to be
130    * used on the same assertion.
131    */
132   public FluentThrowableAssertion(T value, R returns) {
133      this(null, value, returns);
134   }
135
136   /**
137    * Chained constructor.
138    *
139    * <p>
140    * Used when transforming one assertion into another so that the assertion config can be used by the new assertion.
141    *
142    * @param creator
143    *    The assertion that created this assertion.
144    *    <br>Should be <jk>null</jk> if this is the top-level assertion.
145    * @param value
146    *    The object being tested.
147    *    <br>Can be <jk>null</jk>.
148    * @param returns
149    *    The object to return after a test method is called.
150    *    <br>If <jk>null</jk>, the test method returns this object allowing multiple test method calls to be
151    * used on the same assertion.
152    */
153   public FluentThrowableAssertion(Assertion creator, T value, R returns) {
154      super(creator, value, returns);
155   }
156
157   //-----------------------------------------------------------------------------------------------------------------
158   // Transform methods
159   //-----------------------------------------------------------------------------------------------------------------
160
161   @Override /* FluentObjectAssertion */
162   public FluentThrowableAssertion<T,R> asTransformed(Function<T,T> function) {  // NOSONAR - Intentional.
163      return new FluentThrowableAssertion<>(this, function.apply(orElse(null)), returns());
164   }
165
166   /**
167    * Returns an assertion against the throwable message.
168    *
169    * <h5 class='section'>Example:</h5>
170    * <p class='bjava'>
171    *    <jc>// Asserts that the specified method throws an exception
172    * // with 'foobar' somewhere in the messages. </jc>
173    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
174    *       .asMessage()
175    *       .isPattern(<js>".*foobar.*"</js>);
176    * </p>
177    *
178    * @return An assertion against the throwable message.  Never <jk>null</jk>.
179    */
180   public FluentStringAssertion<R> asMessage() {
181      return new FluentStringAssertion<>(this, map(Throwable::getMessage).orElse(null), returns());
182   }
183
184   /**
185    * Returns an assertion against the throwable message and all caused-by messages.
186    *
187    * <h5 class='section'>Example:</h5>
188    * <p class='bjava'>
189    *    <jc>// Asserts that the specified method throws an exception with
190    *    // 'foobar' somewhere in the messages. </jc>
191    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
192    *       .asMessages()
193    *       .isPattern(<js>".*foobar.*"</js>);
194    * </p>
195    *
196    * @return An assertion against the throwable message.  Never <jk>null</jk>.
197    */
198   public FluentListAssertion<String,R> asMessages() {
199      List<String> l = null;
200      Throwable t = orElse(null);
201      if (t != null) {
202         if (t.getCause() == null)
203            l = singletonList(t.getMessage());
204         else {
205            l = list();
206            while (t != null) {
207               l.add(t.getMessage());
208               t = t.getCause();
209            }
210         }
211      }
212      return new FluentListAssertion<>(this, l, returns());
213   }
214
215   /**
216    * Returns an assertion against the throwable localized message.
217    *
218    * <h5 class='section'>Example:</h5>
219    * <p class='bjava'>
220    *    <jc>// Asserts that the specified method throws an exception with
221    *    // 'foobar' somewhere in the localized messages. </jc>
222    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
223    *       .asLocalizedMessage()
224    *       .isPattern(<js>".*foobar.*"</js>);
225    * </p>
226    *
227    * @return An assertion against the throwable localized message.  Never <jk>null</jk>.
228    */
229   public FluentStringAssertion<R> asLocalizedMessage() {
230      return new FluentStringAssertion<>(this, map(Throwable::getLocalizedMessage).orElse(null), returns());
231   }
232
233   /**
234    * Returns an assertion against the throwable message and all caused-by messages.
235    *
236    * <h5 class='section'>Example:</h5>
237    * <p class='bjava'>
238    *    <jc>// Asserts that the specified method throws an exception with
239    *    // 'foobar' somewhere in the messages. </jc>
240    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
241    *       .asLocalizedMessages()
242    *       .isPattern(<js>".*foobar.*"</js>);
243    * </p>
244    *
245    * @return An assertion against the throwable message.  Never <jk>null</jk>.
246    */
247   public FluentListAssertion<String,R> asLocalizedMessages() {
248      List<String> l = null;
249      Throwable t = orElse(null);
250      if (t != null) {
251         if (t.getCause() == null)
252            l = singletonList(t.getMessage());
253         else {
254            l = list();
255            while (t != null) {
256               l.add(t.getLocalizedMessage());
257               t = t.getCause();
258            }
259         }
260      }
261      return new FluentListAssertion<>(this, l, returns());
262   }
263
264   /**
265    * Returns an assertion against the throwable localized message.
266    *
267    * <h5 class='section'>Example:</h5>
268    * <p class='bjava'>
269    *    <jc>// Asserts that the specified method throws an exception with
270    *    // 'foobar' somewhere in the stack trace. </jc>
271    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
272    *       .asStackTrace()
273    *       .isPattern(<js>"foobar"</js>);
274    * </p>
275    *
276    * @return An assertion against the throwable stacktrace.  Never <jk>null</jk>.
277    */
278   public FluentStringListAssertion<R> asStackTrace() {
279      return new FluentStringListAssertion<>(this, valueIsNull() ? null : Arrays.asList(getStackTrace(value())), returns());
280   }
281
282   /**
283    * Returns an assertion against the caused-by throwable.
284    *
285    * <h5 class='section'>Example:</h5>
286    * <p class='bjava'>
287    *    <jc>// Asserts that the specified method throws an exception whose
288    *    // caused-by message contains 'foobar'. </jc>
289    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
290    *       .asCausedBy()
291    *       .asMessage()
292    *       .isPattern(<js>"foobar"</js>);
293    * </p>
294    *
295    * @return An assertion against the caused-by.  Never <jk>null</jk>.
296    */
297   public FluentThrowableAssertion<Throwable,R> asCausedBy() {
298      return asCausedBy(Throwable.class);
299   }
300
301   /**
302    * Returns an assertion against the caused-by throwable.
303    *
304    * <h5 class='section'>Example:</h5>
305    * <p class='bjava'>
306    *    <jc>// Asserts that the specified method throws an exception whose
307    *    // caused-by message contains 'foobar'. </jc>
308    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
309    *       .asCausedBy(RuntimeException.<jk>class</jk>)
310    *       .asMessage()
311    *       .isPattern(<js>"foobar"</js>);
312    * </p>
313    *
314    * @param <X> The throwable type.
315    * @param type The expected exception type.
316    * @return An assertion against the caused-by.  Never <jk>null</jk>.
317    */
318   public <X extends Throwable> FluentThrowableAssertion<X,R> asCausedBy(Class<X> type) {
319      var t = map(Throwable::getCause).orElse(null);
320      if (t == null || type.isInstance(t))
321         return new FluentThrowableAssertion<>(this, type.cast(t), returns());
322      throw error(MSG_causedByExceptionNotExpectedType, type, t.getClass());
323   }
324
325   /**
326    * Returns an assertion against the throwable localized message.
327    *
328    * <h5 class='section'>Example:</h5>
329    * <p class='bjava'>
330    *    <jc>// Asserts that the specified method throws an exception with a
331    *    // caused-by RuntimeException containing 'foobar'</jc>
332    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
333    *       .findCausedBy(RuntimeException.<jk>class</jk>)
334    *       .isExists()
335    *       .asMessage()
336    *       .isPattern(<js>"foobar"</js>);
337    * </p>
338    *
339    * @param <X> The throwable type.
340    * @param throwableClass The class type to search for in the caused-by chain.
341    * @return An assertion against the caused-by throwable.  Never <jk>null</jk>.
342    */
343   public <X extends Throwable> FluentThrowableAssertion<X,R> asFind(Class<X> throwableClass) {
344      Throwable t = orElse(null);
345      while (t != null) {
346         if (throwableClass.isInstance(t))
347            return new FluentThrowableAssertion<>(this, throwableClass.cast(t), returns());
348         t = t.getCause();
349      }
350      return new FluentThrowableAssertion<>(this, (X)null, returns());
351   }
352
353   //-----------------------------------------------------------------------------------------------------------------
354   // Test methods
355   //-----------------------------------------------------------------------------------------------------------------
356
357   /**
358    * Asserts that this throwable is of the specified type.
359    *
360    * <h5 class='section'>Example:</h5>
361    * <p class='bjava'>
362    *    <jc>// Asserts that the specified method throws a RuntimeException. </jc>
363    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
364    *       .isType(RuntimeException.<jk>class</jk>);
365    * </p>
366    *
367    * @param parent The type.
368    * @return The fluent return object.
369    */
370   @Override
371   public R isType(Class<?> parent) {
372      Utils.assertArgNotNull("parent", parent);
373      if (! parent.isInstance(value()))
374         throw error(MSG_exceptionWasNotExpectedType, className(parent), className(value()));
375      return returns();
376   }
377
378   /**
379    * Asserts that this throwable is exactly the specified type.
380    *
381    * <h5 class='section'>Example:</h5>
382    * <p class='bjava'>
383    *    <jc>// Asserts that the specified method throws a RuntimeException. </jc>
384    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar())
385    *       .isExactType(RuntimeException.<jk>class</jk>);
386    * </p>
387    *
388    * @param type The type.
389    * @return The fluent return object.
390    */
391   @Override
392   public R isExactType(Class<?> type) {
393      Utils.assertArgNotNull("type", type);
394      if (type != value().getClass())
395         throw error(MSG_exceptionWasNotExpectedType, className(type), className(value()));
396      return returns();
397   }
398
399   /**
400    * Asserts that this throwable exists.
401    *
402    * <h5 class='section'>Example:</h5>
403    * <p class='bjava'>
404    *    <jc>// Asserts that the specified method throws any exception.</jc>
405    *    ThrowableAssertion.<jsm>assertThrown</jsm>(() -&gt; <jv>foo</jv>.getBar()).isExists();
406    * </p>
407    *
408    * @return The fluent return object.
409    */
410   @Override
411   public R isExists() {
412      if (valueIsNull())
413         throw error(MSG_exceptionWasNotThrown);
414      return returns();
415   }
416
417   //-----------------------------------------------------------------------------------------------------------------
418   // Fluent setters
419   //-----------------------------------------------------------------------------------------------------------------
420   @Override /* Overridden from Assertion */
421   public FluentThrowableAssertion<T,R> setMsg(String msg, Object...args) {
422      super.setMsg(msg, args);
423      return this;
424   }
425
426   @Override /* Overridden from Assertion */
427   public FluentThrowableAssertion<T,R> setOut(PrintStream value) {
428      super.setOut(value);
429      return this;
430   }
431
432   @Override /* Overridden from Assertion */
433   public FluentThrowableAssertion<T,R> setSilent() {
434      super.setSilent();
435      return this;
436   }
437
438   @Override /* Overridden from Assertion */
439   public FluentThrowableAssertion<T,R> setStdOut() {
440      super.setStdOut();
441      return this;
442   }
443
444   @Override /* Overridden from Assertion */
445   public FluentThrowableAssertion<T,R> setThrowable(Class<? extends java.lang.RuntimeException> value) {
446      super.setThrowable(value);
447      return this;
448   }
449   //-----------------------------------------------------------------------------------------------------------------
450   // Utility methods
451   //-----------------------------------------------------------------------------------------------------------------
452
453   @Override
454   protected boolean equals(Object o1, Object o2) {
455      if (o1 instanceof Throwable o1t && o2 instanceof Throwable o2t)
456         return Utils.eq(o1t, o2t, (x,y)->Utils.eq(x.getClass(),y.getClass()) && Utils.eq(x.getMessage(),y.getMessage()));
457      return super.equals(o1, o2);
458   }
459}