001/*
002 * Units of Measurement TCK
003 * Copyright © 2005-2019, 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 tech.units.tck.TCKValidationException;
058
059import javax.inject.Singleton;
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 1.4, July 7, 2019
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     * @deprecated use the simplified version on top of Reflections.org where possible
219     */
220    public static void testHasPublicMethod(String section, Class<?> type, Class<?> returnType, String name, Class<?>... paramTypes) {
221        Class<?> current = type;
222        while (current != null) {
223            for (Method m : current.getDeclaredMethods()) {
224                if (returnType.equals(returnType) && m.getName().equals(name) && ((m.getModifiers() & PUBLIC) != 0)
225                        && Arrays.equals(m.getParameterTypes(), paramTypes)) {
226                    return;
227                }
228            }
229            current = current.getSuperclass();
230        }
231        throw new TCKValidationException(section + ": Class must implement method " + name + '(' + Arrays.toString(paramTypes) + "): "
232                + returnType.getName() + ", but does not: " + type.getName());
233    }
234
235    @SuppressWarnings("rawtypes")
236        private static final List<Class> PRIMITIVE_CLASSES = Collections
237            .unmodifiableList(Arrays.asList(new Class[] { Object.class, Number.class, Enum.class }));
238
239    /**
240     * 
241     * @param section the section of the specification
242     * @param type the data type
243     * @param trySuperclassFirst if tht super class if available should be tested first
244     * @param returnType the expected return type
245     * @param name the name of the method
246     * @param paramTypes types of parameters
247     */
248    public static void testHasPublicMethod(String section, Class<?> type, boolean trySuperclassFirst, Class<?> returnType, String name,
249            Class<?>... paramTypes) {
250        if (trySuperclassFirst && type.getSuperclass() != null) {
251            if (PRIMITIVE_CLASSES.contains(type.getSuperclass())) {
252                testHasPublicMethod(section, type, returnType, name, paramTypes);
253            } else {
254                testHasPublicMethod(section, type.getSuperclass(), returnType, name, paramTypes);
255            }
256        } else {
257            testHasPublicMethod(section, type, returnType, name, paramTypes);
258        }
259    }
260
261    /**
262     * Tests if the given type has a public method with the given signature.
263     * 
264     * @param section
265     *            the section of the spec under test
266     * @param type
267     *            the type to be checked.
268     * @param name
269     *            the method name
270     * @param hasParameters
271     *            the method has parameters.
272     * @throws TCKValidationException
273     *             if test fails.
274     */
275    @SuppressWarnings({ "unchecked" })
276    public static void testHasPublicMethod(String section, Class<?> type, String name, boolean hasParameters) {
277        Set<Method> getters;
278        if (hasParameters) {
279            getters = getAllMethods(type, withModifier(PUBLIC), withName(name));
280        } else {
281            getters = getAllMethods(type, withModifier(PUBLIC), withName(name), withParametersCount(0));
282        }
283        assertThat(getters.size(), greaterThanOrEqualTo(1)); // interface plus
284        // at least one implementation
285    }
286
287    /**
288     * @param section the section of the specification
289     * @param type the data type
290     * @param name the name of the method
291     */
292    public static void testHasPublicMethod(String section, Class<?> type, String name) {
293        testHasPublicMethod(section, type, name, false);
294    }
295
296    /**
297     * Tests if the given type has a public static method with the given signature.
298     * 
299     * @param section
300     *            the section of the spec under test
301     * @param type
302     *            the type to be checked.
303     * @param returnType
304     *            the method return type.
305     * @param name
306     *            the method name
307     * @param paramTypes
308     *            the parameter types.
309     * @throws TCKValidationException
310     *             if test fails.
311     */
312    @SuppressWarnings("rawtypes")
313    static void testHasPublicStaticMethod(String section, Class type, Class returnType, String name, Class... paramTypes) {
314        Class current = type;
315        while (current != null) {
316            for (Method m : current.getDeclaredMethods()) {
317                if (returnType.equals(returnType) && m.getName().equals(name) && ((m.getModifiers() & PUBLIC) != 0)
318                        && ((m.getModifiers() & Modifier.STATIC) != 0) && Arrays.equals(m.getParameterTypes(), paramTypes)) {
319                    return;
320                }
321            }
322            current = current.getSuperclass();
323        }
324        throw new TCKValidationException(section + ": Class must implement method " + name + '(' + Arrays.toString(paramTypes) + "): "
325                + returnType.getName() + ", but does not: " + type.getName());
326    }
327
328    /**
329     * Tests if the given type has not a public method with the given signature.
330     * 
331     * @param section
332     *            the section of the spec under test
333     * @param type
334     *            the type to be checked.
335     * @param returnType
336     *            the method return type.
337     * @param name
338     *            the method name
339     * @param paramTypes
340     *            the parameter types.
341     * @throws TCKValidationException
342     *             if test fails.
343     */
344    @SuppressWarnings("rawtypes")
345        public static void testHasNotPublicMethod(String section, Class type, Class returnType, String name, Class... paramTypes) {
346        Class current = type;
347        while (current != null) {
348            for (Method m : current.getDeclaredMethods()) {
349                if (returnType.equals(returnType) && m.getName().equals(name) && Arrays.equals(m.getParameterTypes(), paramTypes)) {
350                    throw new TCKValidationException(section + ": Class must NOT implement method " + name + '(' + Arrays.toString(paramTypes) + "): "
351                            + returnType.getName() + ", but does: " + type.getName());
352                }
353            }
354            current = current.getSuperclass();
355        }
356    }
357
358    /**
359     * Checks the returned value, when calling a given method.
360     * 
361     * @param section
362     *            the section of the spec under test
363     * @param value
364     *            the expected value
365     * @param methodName
366     *            the target method name
367     * @param instance
368     *            the instance to call
369     * @throws NoSuchMethodException if no method with the given name exists.
370     * @throws SecurityException if a security problem occurs.
371     * @throws IllegalAccessException if the method may not be called, e.g. due to security constraints.
372     * @throws IllegalArgumentException if a wrong or inappropriate argument was provided.
373     * @throws InvocationTargetException if an exception thrown by an invoked method or constructor.
374     * @throws TCKValidationException
375     *             if test fails.
376     */
377    public static void assertValue(String section, Object value, String methodName, Object instance)
378            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
379        Method m = instance.getClass().getDeclaredMethod(methodName);
380        Assert.assertEquals(value, m.invoke(instance), section + ": " + m.getName() + '(' + instance + ") returned invalid value:");
381    }
382
383    static boolean testHasPublicStaticMethodOpt(String section, @SuppressWarnings("rawtypes") Class type, @SuppressWarnings("rawtypes") Class returnType, String methodName, @SuppressWarnings("rawtypes") Class... paramTypes) {
384        try {
385            testHasPublicStaticMethod(section, type, returnType, methodName, paramTypes);
386            return true;
387        } catch (Exception e) {
388            warnings.append(section).append(": Recommendation failed: Missing method [public static ").append(methodName).append('(')
389                    .append(Arrays.toString(paramTypes)).append("):").append(returnType.getName()).append("] on: ").append(type.getName())
390                    .append("\n");
391            return false;
392        }
393    }
394
395    /**
396     * Test for immutability (optional recommendation), writes a warning if not given.
397     * 
398     * @param section
399     *            the section of the spec under test
400     * @param type
401     *            the type to be checked.
402     * @return true, if the instance is probably immutable.
403     */
404    public static boolean testImmutableOpt(String section, @SuppressWarnings("rawtypes") Class type) {
405        try {
406            testImmutable(section, type);
407            return true;
408        } catch (Exception e) {
409            warnings.append(section).append(": Recommendation failed: Class should be immutable: ").append(type.getName()).append(", details: ")
410                    .append(e.getMessage()).append("\n");
411            return false;
412        }
413    }
414
415    /**
416     * Test for serializable (optional recommendation), writes a warning if not given.
417     * 
418     * @param section
419     *            the section of the spec under test
420     * @param type
421     *            the type to be checked.
422     * @return true, if the type is probably serializable.
423     */
424    public static boolean testSerializableOpt(String section, @SuppressWarnings("rawtypes") Class type) {
425        try {
426            testSerializable(section, type);
427            return true;
428        } catch (Exception e) {
429            warnings.append(section).append(": Recommendation failed: Class should be serializable: ").append(type.getName()).append(", details: ")
430                    .append(e.getMessage()).append("\n");
431            return false;
432        }
433    }
434
435    /**
436     * Test for serializable (optional recommendation), writes a warning if not given.
437     * 
438     * @param section
439     *            the section of the spec under test
440     * @param instance
441     *            the object to be checked.
442     * @return true, if the instance is probably serializable.
443     */
444    public static boolean testSerializableOpt(String section, Object instance) {
445        try {
446            testSerializable(section, instance);
447            return true;
448        } catch (Exception e) {
449            warnings.append(section).append(": Recommendation failed: Class is serializable, but serialization failed: ")
450                    .append(instance.getClass().getName()).append("\n");
451            return false;
452        }
453    }
454
455    static void resetWarnings() {
456        warnings.setLength(0);
457    }
458
459    static String getWarnings() {
460        return warnings.toString();
461    }
462}