001/*
002 * ============================================================================
003 * Copyright © 2002-2023 by Thomas Thrien.
004 * All Rights Reserved.
005 * ============================================================================
006 * Licensed to the public under the agreements of the GNU Lesser General Public
007 * License, version 3.0 (the "License"). You may obtain a copy of the License at
008 *
009 *      http://www.gnu.org/licenses/lgpl.html
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, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 * License for the specific language governing permissions and limitations
015 * under the License.
016 */
017
018package org.tquadrat.foundation.testutil;
019
020import static java.lang.String.format;
021import static java.lang.System.err;
022import static java.lang.System.getProperties;
023import static java.lang.System.in;
024import static java.lang.System.out;
025import static java.lang.System.setErr;
026import static java.lang.System.setIn;
027import static java.lang.System.setOut;
028import static java.lang.reflect.Modifier.isFinal;
029import static java.lang.reflect.Modifier.isPrivate;
030import static java.lang.reflect.Modifier.isStatic;
031import static java.net.NetworkInterface.getNetworkInterfaces;
032import static java.util.Arrays.asList;
033import static java.util.Objects.isNull;
034import static java.util.Objects.nonNull;
035import static java.util.Objects.requireNonNull;
036import static java.util.stream.Collectors.joining;
037import static org.apiguardian.api.API.Status.STABLE;
038import static org.junit.jupiter.api.Assertions.fail;
039import static org.tquadrat.foundation.testutil.TestUtils.EMPTY_STRING;
040import static org.tquadrat.foundation.testutil.TestUtils.getLiveThreads;
041import static org.tquadrat.foundation.testutil.TestUtils.isAssertionOn;
042
043import java.io.InputStream;
044import java.io.PrintStream;
045import java.lang.reflect.InvocationTargetException;
046import java.lang.reflect.Method;
047import java.net.SocketException;
048import java.time.ZoneId;
049import java.util.ArrayList;
050import java.util.Collection;
051import java.util.HashSet;
052import java.util.Locale;
053import java.util.Properties;
054import java.util.TimeZone;
055import java.util.concurrent.atomic.AtomicBoolean;
056
057import org.apiguardian.api.API;
058import org.easymock.EasyMockSupport;
059import org.junit.jupiter.api.AfterEach;
060import org.junit.jupiter.api.BeforeAll;
061import org.junit.jupiter.api.BeforeEach;
062
063/**
064 *  A base class for JUnit test classes.
065 *
066 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
067 *  @version $Id: TestBaseClass.java 1100 2024-02-16 23:33:45Z tquadrat $
068 *  @since 0.0.5
069 *
070 *  @UMLGraph.link
071 */
072@SuppressWarnings( {"AbstractClassWithoutAbstractMethods", "UseOfSystemOutOrSystemErr", "AbstractClassExtendsConcreteClass"} )
073@API( status = STABLE, since = "0.0.5" )
074public abstract class TestBaseClass extends EasyMockSupport
075{
076        /*-----------*\
077    ====** Constants **========================================================
078        \*-----------*/
079    /**
080     *  The message for an exception that was not thrown: {@value}.
081     */
082    @SuppressWarnings( "ConstantDeclaredInAbstractClass" )
083    public static final String MSG_ExceptionNotThrown = "Expected Exception '%s' was not thrown";
084
085    /**
086     *  The message for an unexpected exception that was thrown: {@value}.
087     */
088    @SuppressWarnings( "ConstantDeclaredInAbstractClass" )
089    public static final String MSG_ExceptionThrown = "Unexpected Exception '%s' was thrown";
090
091    /**
092     *  The message that another than the expected exception was thrown: {@value}.
093     */
094    @SuppressWarnings( "ConstantDeclaredInAbstractClass" )
095    public static final String MSG_WrongExceptionThrown = "Wrong Exception type; caught '%2$s' but '%1$s' was expected";
096
097        /*------------*\
098    ====** Attributes **=======================================================
099        \*------------*/
100    /**
101     *  The default error output stream.
102     */
103    private PrintStream m_DefaultErrorStream;
104
105    /**
106     *  The default input stream.
107     */
108    private InputStream m_DefaultInputStream;
109
110    /**
111     *  The default
112     *  {@link Locale}.
113     */
114    private Locale m_DefaultLocale;
115
116    /**
117     *  The default output stream.
118     */
119    private PrintStream m_DefaultOutputStream;
120
121    /**
122     *  The default
123     *  {@link TimeZone}.
124     */
125    @SuppressWarnings( "UseOfObsoleteDateTimeApi" )
126    private TimeZone m_DefaultTimeZone;
127
128    /**
129     *  The default
130     *  {@link ZoneId}.
131     */
132    @SuppressWarnings( {"FieldCanBeLocal", "unused"} )
133    private ZoneId m_DefaultZoneId;
134
135    /**
136     *  Threads that are alive before the test is started.
137     */
138    private Collection<Thread> m_LiveThreadsBeforeSetup = null;
139
140    /**
141     *  Flag that controls whether the test regarding additional threads will
142     *  be performed or not.
143     *
144     *  @see #assertThatThereAreNoMoreLiveThreadsThanBeforeSetup()
145     */
146    private boolean m_SkipThreadTest = false;
147
148    /**
149     *  The system properties.
150     */
151    @SuppressWarnings( {"StaticCollection", "StaticVariableMayNotBeInitialized"} )
152    private static Properties m_SystemProperties;
153
154        /*--------------*\
155    ====** Constructors **=====================================================
156        \*--------------*/
157    /**
158     *  Creates a new {@code TestBaseClass} instance.
159     */
160    protected TestBaseClass()
161    {
162        //  Just exists!
163    }   //  TestBaseClass()
164
165        /*---------*\
166    ====** Methods **==========================================================
167        \*---------*/
168    /**
169     *  As the name of the method indicates, it asserts that the JDK assertions
170     *  are enabled.<br>
171     *  <br>The method uses
172     *  {@link TestUtils#isAssertionOn()} to check whether JDK assertion is
173     *  currently activated, meaning that the program was started with the
174     *  command line flags {@code -ea} or {@code -enableassertions}.<br>
175     *  <br>Unfortunately, it is possible to activate assertions for some
176     *  selected packages only (and {@code org.tquadrat.test} may not be
177     *  amongst these), or {@code org.tquadrat.test} is explicitly disabled
178     *  with {@code -da} or {@code -disableassertions} (or – most mean! –
179     *  assertions are <i>only</i> activated for {@code org.tquadrat.test}}.
180     *  Therefore, the outcome of this method will not guarantee for one
181     *  hundred percent that assertion are always thrown where necessary.
182     *
183     *  @see TestUtils#isAssertionOn()
184     */
185    @BeforeAll
186    protected static final void assertThatJDKAssertionAreEnabled()
187    {
188        if( !isAssertionOn() )
189        {
190            fail( "JDK Assertions are not enabled" );
191        }
192    }   //  assertThatJDKAssertionAreEnabled()
193
194    /**
195     *  As the name of the method indicates, it asserts that the system
196     *  properties were not modified.
197     */
198    @AfterEach
199    @SuppressWarnings( "static-method" )
200    protected final void assertThatSystemPropertiesWereNotChanged()
201    {
202        final var currentProperties = getProperties();
203        final Collection<String> names = new HashSet<>( currentProperties.stringPropertyNames() );
204        if( currentProperties.size() < m_SystemProperties.size() )
205        {
206            final Collection<String> storedNames = new HashSet<>( m_SystemProperties.stringPropertyNames() );
207            storedNames.removeAll( names );
208            fail( format( "System Property '%s' was removed", String.join( ", ", storedNames ) ) );
209        }
210
211        String value;
212        for( final var name : names )
213        {
214            value = currentProperties.getProperty( name );
215            if( nonNull( value) )
216            {
217                if( !value.equals( m_SystemProperties.getProperty( name ) ) )
218                {
219                    fail( format( "System Property '%s' was changed", name ) );
220                }
221            }
222            else
223            {
224                if( nonNull( m_SystemProperties.getProperty( name ) ) )
225                {
226                    fail( format( "System Property '%s' was added", name ) );
227                }
228            }
229        }
230    }   //  assertThatSystemPropertiesWereNotChanged()
231
232    /**
233     *  As the name of the method indicates, it asserts that there are no more
234     *  living threads than there had been before setup.
235     */
236    @AfterEach
237    protected final void assertThatThereAreNoMoreLiveThreadsThanBeforeSetup()
238    {
239        if( !m_SkipThreadTest )
240        {
241            if( isNull( m_LiveThreadsBeforeSetup ) )
242            {
243                fail( "'obtainLiveThreads()' was not executed" );
244            }
245
246            final Collection<Thread> liveThreads = new HashSet<>( getLiveThreads() );
247            liveThreads.removeAll( m_LiveThreadsBeforeSetup );
248            if( !liveThreads.isEmpty() )
249            {
250                final var failed = new AtomicBoolean( false );
251                @SuppressWarnings( "OverlyLongLambda" )
252                final var message = liveThreads.stream()
253                    .filter( Thread::isAlive )
254                    .map( t ->
255                    {
256                        failed.set( true );
257                        final var buffer = new StringBuilder( t.getName() )
258                            .append( " (" )
259                            .append( t.threadId() );
260                        if( t.isDaemon() )
261                        {
262                            buffer.append( "/DAEMON" );
263                        }
264                        buffer.append( ")" );
265                        return buffer.toString();
266                    } )
267                    .collect( joining( ", ", "Detected unexpected living threads after test: ", EMPTY_STRING ) );
268                if( failed.get() ) fail( message );
269            }
270        }
271    }   //  assertThatThereAreNoMoreLiveThreadsThanBeforeSetup()
272
273    /**
274     *  Returns the default error stream.
275     *
276     *  @return The default error stream.
277     */
278    protected final PrintStream getDefaultErrorStream() { return m_DefaultErrorStream; }
279
280    /**
281     *  Returns the default output stream.
282     *
283     *  @return The default output stream.
284     */
285    protected final PrintStream getDefaultOutputStream() { return m_DefaultOutputStream; }
286
287    /**
288     *  Checks whether the machine, that runs the current test, has network
289     *  configured.
290     *
291     *  @return {@code true} if the current machine has a network configured,
292     *      {@code false} otherwise.
293     */
294    @SuppressWarnings( "static-method" )
295    protected final boolean hasNetwork()
296    {
297        var retValue = true;
298        try
299        {
300            retValue = getNetworkInterfaces().hasMoreElements();
301        }
302        catch( final SocketException ignored )
303        {
304            retValue = false;
305        }
306
307        //---* Done *----------------------------------------------------------
308        return retValue;
309    }   //  hasNetwork()
310
311    /**
312     *  References to all currently running threads will be stored.
313     *
314     *  @see #m_LiveThreadsBeforeSetup
315     *  @see #assertThatThereAreNoMoreLiveThreadsThanBeforeSetup()
316     */
317    @BeforeEach
318    protected final void obtainLiveThreads()
319    {
320        m_LiveThreadsBeforeSetup = getLiveThreads();
321    }   //  obtainLiveThreads()
322
323    /**
324     *  Resets the default settings for
325     *  {@link Locale},
326     *  {@link java.util.TimeZone},
327     *  and
328     *  {@link java.time.ZoneId}.
329     *
330     *  @see Locale#setDefault(Locale)
331     *  @see TimeZone#setDefault(TimeZone)
332     *  @see ZoneId#systemDefault()
333     */
334    @SuppressWarnings( "UseOfObsoleteDateTimeApi" )
335    @BeforeEach
336    protected final void resetDefaultSettings()
337    {
338        Locale.setDefault( m_DefaultLocale );
339        TimeZone.setDefault( m_DefaultTimeZone );
340    }   //  resetDefaultSettings()
341
342    /**
343     *  Resets the default streams.
344     */
345    @AfterEach
346    protected final void resetDefaultStreams()
347    {
348        setErr( m_DefaultErrorStream );
349        setIn( m_DefaultInputStream );
350        setOut( m_DefaultOutputStream );
351    }   //  resetDefaultStreams()
352
353    /**
354     *  Resets the thread test flag.
355     */
356    @BeforeEach
357    protected final void resetSkipThreadTest() { m_SkipThreadTest = false; }
358
359    /**
360     *  Saves the default settings for
361     *  {@link Locale},
362     *  {@link java.util.TimeZone},
363     *  and
364     *  {@link java.time.ZoneId}.
365     *
366     *  @see Locale#getDefault()
367     *  @see TimeZone#getDefault()
368     *  @see ZoneId#systemDefault()
369     */
370    @SuppressWarnings( "UseOfObsoleteDateTimeApi" )
371    @BeforeEach
372    protected final void saveDefaultSettings()
373    {
374        m_DefaultLocale = Locale.getDefault();
375        m_DefaultTimeZone = TimeZone.getDefault();
376        m_DefaultZoneId = ZoneId.systemDefault();
377    }   //  saveDefaultSettings()
378
379    /**
380     *  Saves the default streams.
381     */
382    @BeforeEach
383    protected final void saveDefaultStreams()
384    {
385        m_DefaultErrorStream = err;
386        m_DefaultInputStream = in;
387        m_DefaultOutputStream = out;
388    }   //  saveDefaultStreams()
389
390    /**
391     *  Sets the thread test flag to {@code true}, disabling that test.
392     */
393    public final void skipThreadTest() { m_SkipThreadTest = true; }
394
395    /**
396     *  Keep the
397     *  {@linkplain System#getProperties() system properties}.
398     */
399    @BeforeAll
400    protected static void storeSystemProperties()
401    {
402        m_SystemProperties = new Properties( getProperties() );
403    }   //  storeSystemProperties()
404
405    /**
406     *  Translates the escape sequences in a String. Refer to
407     *  {@link String#translateEscapes()}
408     *  for the details.<br>
409     *  <br>Use this method to convert the input from CSV files or alike.
410     *
411     *  @param  input   The input String; can be {@code null}.
412     *  @return The processed String; will be {@code null} if the input was
413     *      already {@code null}.
414     *
415     *  @since 0.1.0
416     */
417    @SuppressWarnings( "static-method" )
418    protected final String translateEscapes( final CharSequence input )
419    {
420        final var retValue = isNull( input ) ? null : input.toString().translateEscapes();
421
422        //---* Done *----------------------------------------------------------
423        return retValue;
424    }   //  translateEscapes()
425
426    /**
427     *  Validates whether a class is really a static class.<br>
428     *  <br>A class is <i>static</i> when it has the following characteristics:
429     *  <ul>
430     *  <li>The class is final.</li>
431     *  <li>All methods are static.</li>
432     *  <li>Only the default constructor may exists, this has to be private,
433     *  and it has to throw an
434     *  {@link Error}
435     *  on invocation.</li>
436     *  </ul>
437     *  If a test fails, the result is written to
438     *  {@link System#out}.<br>
439     *  <br>The method will not check for the {@code &#64;UtilityClass}
440     *  annotation as this has only the retention level {@code SOURCE}.
441     *
442     *  @param  candidate   The class to inspect.
443     *  @return {@code true} if the class is in fact static, {@code false}
444     *      otherwise.
445     */
446    @SuppressWarnings( {"ProhibitedExceptionThrown", "RedundantStreamOptionalCall", "OverlyComplexMethod"} )
447    protected final boolean validateAsStaticClass( final Class<?> candidate )
448    {
449        skipThreadTest();
450
451        //---* The class must be final *---------------------------------------
452        var retValue = isFinal( requireNonNull( candidate, "candidate must not be null" ).getModifiers() );
453        if( !retValue )
454        {
455            out.printf( "Class '%s' is not final\n", candidate.getName() );
456        }
457
458        //---* All methods must be static *------------------------------------
459        final Collection<Method> methods = new ArrayList<>( asList( candidate.getMethods() ) );
460        methods.addAll( asList( candidate.getDeclaredMethods() ) );
461        //noinspection SimplifyStreamApiCallChains
462        retValue = retValue && !methods.stream()
463            //---* Eliminate all methods from Object *-------------------------
464            .filter( method -> !method.getDeclaringClass().equals( Object.class ) )
465            //---* Eliminate all static methods *------------------------------
466            .filter( method -> !isStatic( method.getModifiers() ) )
467            //---* Map the remaining methods to their names *------------------
468            .map( Method::toString )
469            //---* Eliminate duplicates *--------------------------------------
470            .distinct()
471            //---* Print all remaining methods *-------------------------------
472            .peek( method -> out.printf( "Method '%2$s' of class '%1$s' is not static\n", candidate.getName(), method ) )
473            /*
474             * This was just added to avoid a shortcut; otherwise, only the
475             * first offending method would be printed.
476             */
477            .sorted()
478            //---* Any entry will match - if any at all *----------------------
479            .anyMatch( $ -> true );
480
481        //---* No public or protected constructors are allowed *---------------
482        var constructors = candidate.getConstructors();
483        if( constructors.length > 0 )
484        {
485            retValue = false;
486            out.printf( "Class '%s' has public constructor\n", candidate.getName() );
487        }
488
489        //---* Only one private constructor is allowed *-----------------------
490        constructors = candidate.getDeclaredConstructors();
491        if( constructors.length != 1 )
492        {
493            retValue = false;
494            out.printf( "Class '%s' defines more than one constructor\n", candidate.getName() );
495        }
496
497        /*
498         * This one and only constructor has to be the default constructor.
499         */
500        final var constructor = constructors [0];
501        if( constructor.getParameterTypes().length != 0 )
502        {
503            retValue = false;
504            out.printf( "Constructor of class '%s' is not the default constructor\n", candidate.getName() );
505        }
506
507        //---* The default constructor has to be private *---------------------
508        if( !isPrivate( constructor.getModifiers() ) )
509        {
510            retValue = false;
511            out.printf( "Constructor of class '%s' is not private\n", candidate.getName() );
512        }
513
514        /*
515         * The constructor has to throw an Error (in fact, a
516         * PrivateConstructorForStaticClassCalledError).
517         */
518        if( retValue )
519        {
520            final var message = "Constructor of class '%s' does not throw a PrivateConstructorForStaticClassCalledError\n";
521
522            /*
523             * To avoid exceptions, this part of the test will be executed
524             * only when the one and only constructor is the default
525             * constructor - meaning the return value is still true.
526             */
527            constructor.setAccessible( true );
528            try
529            {
530                constructor.newInstance();
531                retValue = false;
532                out.printf( message, candidate.getName() );
533            }
534            catch( final InvocationTargetException e )
535            {
536                final var throwable = e.getCause();
537                if( isNull( throwable ) || !throwable.getClass().getName().endsWith( "PrivateConstructorForStaticClassCalledError" ) )
538                {
539                    retValue = false;
540                    out.printf( message, candidate.getName() );
541                }
542            }
543            catch( final InstantiationException | IllegalAccessException | IllegalArgumentException e )
544            {
545                throw new RuntimeException( format( "Instantiation of class '%s' failed", candidate.getName() ), e );
546            }
547        }
548
549        //---* Done *----------------------------------------------------------
550        return retValue;
551    }   //  validateAsStaticClass()
552}
553//  class TestBaseClass
554
555/*
556 *  End of File
557 */