001/*
002 * Units of Measurement TCK
003 * Copyright © 2005-2020, Jean-Marie Dautelle, Werner Keil, Otavio Santana.
004 *
005 * All rights reserved.
006 *
007 * Redistribution and use in source and binary forms, with or without modification,
008 * are permitted provided that the following conditions are met:
009 *
010 * 1. Redistributions of source code must retain the above copyright notice,
011 *    this list of conditions and the following disclaimer.
012 *
013 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions
014 *    and the following disclaimer in the documentation and/or other materials provided with the distribution.
015 *
016 * 3. Neither the name of JSR-385 nor the names of its contributors may be used to endorse or promote products
017 *    derived from this software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
020 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
021 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
022 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
023 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
026 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
028 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package tech.units.tck.util;
031
032import static java.lang.reflect.Modifier.PUBLIC;
033import static org.hamcrest.MatcherAssert.assertThat;
034import static org.hamcrest.number.OrderingComparison.greaterThanOrEqualTo;
035import static org.reflections.ReflectionUtils.getAllMethods;
036import static org.reflections.ReflectionUtils.withModifier;
037import static org.reflections.ReflectionUtils.withName;
038import static org.reflections.ReflectionUtils.withParametersCount;
039
040import java.io.ByteArrayOutputStream;
041import java.io.ObjectOutputStream;
042import java.io.Serializable;
043import java.lang.reflect.InvocationTargetException;
044import java.lang.reflect.Method;
045import java.lang.reflect.Modifier;
046import java.util.Arrays;
047import java.util.Collections;
048import java.util.List;
049import java.util.Random;
050import java.util.Set;
051
052import org.mutabilitydetector.unittesting.AllowedReason;
053import org.mutabilitydetector.unittesting.MutabilityAssert;
054import org.mutabilitydetector.unittesting.MutabilityMatchers;
055import org.testng.Assert;
056
057import jakarta.inject.Singleton;
058import tech.units.tck.TCKValidationException;
059
060import javax.measure.*;
061import javax.measure.spi.*;
062
063/**
064 * Test utilities used in the JSR 385 TCK.
065 *
066 * @author <a href="mailto:[email protected]">Werner Keil</a>
067 * @version 2.2, November 15, 2020
068 * @since 1.0
069 */
070@Singleton
071public class TestUtils {
072
073    /**
074     * Name of the system property to pass the desired profile
075     */
076    public static final String SYS_PROPERTY_PROFILE = "tech.units.tck.profile";
077
078    /**
079     * Name of the system property to override the default output directory
080     */
081    public static final String SYS_PROPERTY_OUTPUT_DIR = "tech.units.tck.outputDir";
082
083    /**
084     * Name of the system property to override the default report file
085     */
086    public static final String SYS_PROPERTY_REPORT_FILE = "tech.units.tck.reportFile";
087
088    /**
089     * Name of the system property to set the <code>verbose</code> flag
090     */
091    public static final String SYS_PROPERTY_VERBOSE = "tech.units.tck.verbose";   
092    
093    private static final StringBuilder warnings = new StringBuilder();
094
095    /**
096     * This class should not be instantiated
097     */
098    private TestUtils() {
099    }
100
101    static Number createNumberWithPrecision(QuantityFactory<?> f, int precision) {
102        if (precision == 0) {
103            precision = new Random().nextInt(100);
104        }
105        StringBuilder b = new StringBuilder(precision + 1);
106        for (int i = 0; i < precision; i++) {
107            b.append(String.valueOf(i % 10));
108        }
109        return new Double(b.toString());
110    }
111
112    static Number createNumberWithScale(QuantityFactory<?> f, int scale) {
113        StringBuilder b = new StringBuilder(scale + 2);
114        b.append("9.");
115        for (int i = 0; i < scale; i++) {
116            b.append(String.valueOf(i % 10));
117        }
118        return new Double(b.toString());
119    }
120
121    /**
122     * Tests the given object being (effectively) serializable by serializing it.
123     *
124     * @param section
125     *            the section of the spec under test
126     * @param type
127     *            the type to be checked.
128     * @throws TCKValidationException
129     *             if the test fails.
130     */
131    public static void testSerializable(String section, Class<?> type) {
132        if (!Serializable.class.isAssignableFrom(type)) {
133            throw new TCKValidationException(section + ": Class must be serializable: " + type.getName());
134        }
135    }
136
137    /**
138     * Tests the given class being serializable.
139     *
140     * @param section
141     *            the section of the spec under test
142     * @param type
143     *            the type to be checked.
144     * @throws TCKValidationException
145     *             if test fails.
146     */
147    public static void testImmutable(String section, Class<?> type) {
148        try {
149            MutabilityAssert.assertInstancesOf(type, MutabilityMatchers.areImmutable(),
150                    AllowedReason.provided(Dimension.class, Quantity.class, Unit.class, UnitConverter.class).areAlsoImmutable(),
151                    AllowedReason.allowingForSubclassing(), AllowedReason.allowingNonFinalFields());
152        } catch (Exception e) {
153            throw new TCKValidationException(section + ": Class is not immutable: " + type.getName(), e);
154        }
155    }
156
157    /**
158     * Tests the given object being (effectively) serializable by serializing it.
159     *
160     * @param section
161     *            the section of the spec under test
162     * @param o
163     *            the object to be checked.
164     * @throws TCKValidationException
165     *             if test fails.
166     */
167    public static void testSerializable(String section, Object o) {
168        if (!(o instanceof Serializable)) {
169            throw new TCKValidationException(section + ": Class must be serializable: " + o.getClass().getName());
170        }
171        try (ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream())) {
172            oos.writeObject(o);
173        } catch (Exception e) {
174            throw new TCKValidationException("Class must be serializable, but serialization failed: " + o.getClass().getName(), e);
175        }
176    }
177
178    /**
179     * Tests the given class implements a given interface.
180     *
181     * @param section
182     *            the section of the spec under test
183     * @param type
184     *            the type to be checked.
185     * @param iface
186     *            the interface to be checked for.
187     * Triggers Assert#fail
188     *             if test fails.
189     */
190    public static void testImplementsInterface(String section, Class<?> type, Class<?> iface) {
191        for (Class<?> ifa : type.getInterfaces()) {
192            if (ifa.equals(iface)) {
193                return;
194            }
195        }
196        Assert.fail(section + ": Class must implement " + iface.getName() + ", but does not: " + type.getName());
197    }
198
199    /**
200     * Tests if the given type is comparable.
201     * 
202     * @param section
203     *            the section of the spec under test
204     * @param type
205     *            the type to be checked.
206     */
207    public static void testComparable(String section, Class<?> type) {
208        testImplementsInterface(section, type, Comparable.class);
209    }
210
211    /**
212     * 
213     * @param section the section of the specification
214     * @param type the type to be checked.
215     * @param returnType the expected return type
216     * @param name the name of the method
217     * @param paramTypes the types of parameters if available
218     */
219    public static void testHasPublicMethod(String section, Class<?> type, Class<?> returnType, String name, Class<?>... paramTypes) {
220        Class<?> current = type;
221        while (current != null) {
222            for (Method m : current.getDeclaredMethods()) {
223                if (returnType.equals(returnType) && m.getName().equals(name) && ((m.getModifiers() & PUBLIC) != 0)
224                        && Arrays.equals(m.getParameterTypes(), paramTypes)) {
225                    return;
226                }
227            }
228            current = current.getSuperclass();
229        }
230        throw new TCKValidationException(section + ": Class must implement method " + name + '(' + Arrays.toString(paramTypes) + "): "
231                + returnType.getName() + ", but does not: " + type.getName());
232    }
233
234    @SuppressWarnings("rawtypes")
235        private static final List<Class> PRIMITIVE_CLASSES = Collections
236            .unmodifiableList(Arrays.asList(new Class[] { Object.class, Number.class, Enum.class }));
237
238    /**
239     * 
240     * @param section the section of the specification
241     * @param type the data type
242     * @param trySuperclassFirst if tht super class if available should be tested first
243     * @param returnType the expected return type
244     * @param name the name of the method
245     * @param paramTypes types of parameters
246     */
247    public static void testHasPublicMethod(String section, Class<?> type, boolean trySuperclassFirst, Class<?> returnType, String name,
248            Class<?>... paramTypes) {
249        if (trySuperclassFirst && type.getSuperclass() != null) {
250            if (PRIMITIVE_CLASSES.contains(type.getSuperclass())) {
251                testHasPublicMethod(section, type, returnType, name, paramTypes);
252            } else {
253                testHasPublicMethod(section, type.getSuperclass(), returnType, name, paramTypes);
254            }
255        } else {
256            testHasPublicMethod(section, type, returnType, name, paramTypes);
257        }
258    }
259
260    /**
261     * Tests if the given type has a public method with the given signature.
262     * 
263     * @param section
264     *            the section of the spec under test
265     * @param type
266     *            the type to be checked.
267     * @param name
268     *            the method name
269     * @param hasParameters
270     *            the method has parameters.
271     * @throws TCKValidationException
272     *             if test fails.
273     */
274    @SuppressWarnings({ "unchecked" })
275    public static void testHasPublicMethod(String section, Class<?> type, String name, boolean hasParameters) {
276        Set<Method> getters;
277        if (hasParameters) {
278            getters = getAllMethods(type, withModifier(PUBLIC), withName(name));
279        } else {
280            getters = getAllMethods(type, withModifier(PUBLIC), withName(name), withParametersCount(0));
281        }
282        assertThat(getters.size(), greaterThanOrEqualTo(1)); // interface plus
283        // at least one implementation
284    }
285
286    /**
287     * @param section the section of the specification
288     * @param type the data type
289     * @param name the name of the method
290     */
291    public static void testHasPublicMethod(String section, Class<?> type, String name) {
292        testHasPublicMethod(section, type, name, false);
293    }
294
295    /**
296     * Tests if the given type has a public static method with the given signature.
297     * 
298     * @param section
299     *            the section of the spec under test
300     * @param type
301     *            the type to be checked.
302     * @param returnType
303     *            the method return type.
304     * @param name
305     *            the method name
306     * @param paramTypes
307     *            the parameter types.
308     * @throws TCKValidationException
309     *             if test fails.
310     */
311    @SuppressWarnings("rawtypes")
312    static void testHasPublicStaticMethod(String section, Class type, Class returnType, String name, Class... paramTypes) {
313        Class current = type;
314        while (current != null) {
315            for (Method m : current.getDeclaredMethods()) {
316                if (returnType.equals(returnType) && m.getName().equals(name) && ((m.getModifiers() & PUBLIC) != 0)
317                        && ((m.getModifiers() & Modifier.STATIC) != 0) && Arrays.equals(m.getParameterTypes(), paramTypes)) {
318                    return;
319                }
320            }
321            current = current.getSuperclass();
322        }
323        throw new TCKValidationException(section + ": Class must implement method " + name + '(' + Arrays.toString(paramTypes) + "): "
324                + returnType.getName() + ", but does not: " + type.getName());
325    }
326
327    /**
328     * Tests if the given type has not a public method with the given signature.
329     * 
330     * @param section
331     *            the section of the spec under test
332     * @param type
333     *            the type to be checked.
334     * @param returnType
335     *            the method return type.
336     * @param name
337     *            the method name
338     * @param paramTypes
339     *            the parameter types.
340     * @throws TCKValidationException
341     *             if test fails.
342     */
343    @SuppressWarnings("rawtypes")
344        public static void testHasNotPublicMethod(String section, Class<?> type, Class<?> returnType, String name, Class<?>... paramTypes) {
345        Class current = type;
346        while (current != null) {
347            for (Method m : current.getDeclaredMethods()) {
348                if (returnType.equals(returnType) && m.getName().equals(name) && Arrays.equals(m.getParameterTypes(), paramTypes)) {
349                    throw new TCKValidationException(section + ": Class must NOT implement method " + name + '(' + Arrays.toString(paramTypes) + "): "
350                            + returnType.getName() + ", but does: " + type.getName());
351                }
352            }
353            current = current.getSuperclass();
354        }
355    }
356
357    /**
358     * Checks the returned value, when calling a given method.
359     * 
360     * @param section
361     *            the section of the spec under test
362     * @param value
363     *            the expected value
364     * @param methodName
365     *            the target method name
366     * @param instance
367     *            the instance to call
368     * @throws NoSuchMethodException if no method with the given name exists.
369     * @throws SecurityException if a security problem occurs.
370     * @throws IllegalAccessException if the method may not be called, e.g. due to security constraints.
371     * @throws IllegalArgumentException if a wrong or inappropriate argument was provided.
372     * @throws InvocationTargetException if an exception thrown by an invoked method or constructor.
373     * @throws TCKValidationException
374     *             if test fails.
375     */
376    public static void assertValue(String section, Object value, String methodName, Object instance)
377            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
378        Method m = instance.getClass().getDeclaredMethod(methodName);
379        Assert.assertEquals(value, m.invoke(instance), section + ": " + m.getName() + '(' + instance + ") returned invalid value:");
380    }
381
382    static boolean testHasPublicStaticMethodOpt(String section, @SuppressWarnings("rawtypes") Class type, @SuppressWarnings("rawtypes") Class returnType, String methodName, @SuppressWarnings("rawtypes") Class... paramTypes) {
383        try {
384            testHasPublicStaticMethod(section, type, returnType, methodName, paramTypes);
385            return true;
386        } catch (Exception e) {
387            warnings.append(section).append(": Recommendation failed: Missing method [public static ").append(methodName).append('(')
388                    .append(Arrays.toString(paramTypes)).append("):").append(returnType.getName()).append("] on: ").append(type.getName())
389                    .append("\n");
390            return false;
391        }
392    }
393
394    /**
395     * Test for immutability (optional recommendation), writes a warning if not given.
396     * 
397     * @param section
398     *            the section of the spec under test
399     * @param type
400     *            the type to be checked.
401     * @return true, if the instance is probably immutable.
402     */
403    public static boolean testImmutableOpt(String section, @SuppressWarnings("rawtypes") Class type) {
404        try {
405            testImmutable(section, type);
406            return true;
407        } catch (Exception e) {
408            warnings.append(section).append(": Recommendation failed: Class should be immutable: ").append(type.getName()).append(", details: ")
409                    .append(e.getMessage()).append("\n");
410            return false;
411        }
412    }
413
414    /**
415     * Test for serializable (optional recommendation), writes a warning if not given.
416     * 
417     * @param section
418     *            the section of the spec under test
419     * @param type
420     *            the type to be checked.
421     * @return true, if the type is probably serializable.
422     */
423    public static boolean testSerializableOpt(String section, @SuppressWarnings("rawtypes") Class type) {
424        try {
425            testSerializable(section, type);
426            return true;
427        } catch (Exception e) {
428            warnings.append(section).append(": Recommendation failed: Class should be serializable: ").append(type.getName()).append(", details: ")
429                    .append(e.getMessage()).append("\n");
430            return false;
431        }
432    }
433
434    /**
435     * Test for serializable (optional recommendation), writes a warning if not given.
436     * 
437     * @param section
438     *            the section of the spec under test
439     * @param instance
440     *            the object to be checked.
441     * @return true, if the instance is probably serializable.
442     */
443    public static boolean testSerializableOpt(String section, Object instance) {
444        try {
445            testSerializable(section, instance);
446            return true;
447        } catch (Exception e) {
448            warnings.append(section).append(": Recommendation failed: Class is serializable, but serialization failed: ")
449                    .append(instance.getClass().getName()).append("\n");
450            return false;
451        }
452    }
453
454    static void resetWarnings() {
455        warnings.setLength(0);
456    }
457
458    static String getWarnings() {
459        return warnings.toString();
460    }
461}