001/*
002 * Units of Measurement TCK
003 * Copyright © 2005-2017, Jean-Marie Dautelle, Werner Keil, V2COM.
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:units@catmedia.us">Werner Keil</a>
067 * @version 1.0.2, September 10, 2017
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     * @throws 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
214     * @param type
215     * @param returnType
216     * @param name
217     * @param paramTypes
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    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
241     * @param type
242     * @param trySuperclassFirst
243     * @param returnType
244     * @param name
245     * @param paramTypes
246     * @deprecated use the simplified version on top of Reflections.org where possible
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     * 
289     * @param section
290     * @param type
291     * @param name
292     * @deprecated use the simplified version on top of Reflections.org where possible
293     */
294    public static void testHasPublicMethod(String section, Class<?> type, String name) {
295        testHasPublicMethod(section, type, name, false);
296    }
297
298    /**
299     * Tests if the given type has a public static method with the given signature.
300     * 
301     * @param section
302     *            the section of the spec under test
303     * @param type
304     *            the type to be checked.
305     * @param returnType
306     *            the method return type.
307     * @param name
308     *            the method name
309     * @param paramTypes
310     *            the parameter types.
311     * @throws TCKValidationException
312     *             if test fails.
313     */
314    @SuppressWarnings("rawtypes")
315    static void testHasPublicStaticMethod(String section, Class type, Class returnType, String name, Class... paramTypes) {
316        Class current = type;
317        while (current != null) {
318            for (Method m : current.getDeclaredMethods()) {
319                if (returnType.equals(returnType) && m.getName().equals(name) && ((m.getModifiers() & PUBLIC) != 0)
320                        && ((m.getModifiers() & Modifier.STATIC) != 0) && Arrays.equals(m.getParameterTypes(), paramTypes)) {
321                    return;
322                }
323            }
324            current = current.getSuperclass();
325        }
326        throw new TCKValidationException(section + ": Class must implement method " + name + '(' + Arrays.toString(paramTypes) + "): "
327                + returnType.getName() + ", but does not: " + type.getName());
328    }
329
330    /**
331     * Tests if the given type has not a public method with the given signature.
332     * 
333     * @param section
334     *            the section of the spec under test
335     * @param type
336     *            the type to be checked.
337     * @param returnType
338     *            the method return type.
339     * @param name
340     *            the method name
341     * @param paramTypes
342     *            the parameter types.
343     * @throws TCKValidationException
344     *             if test fails.
345     */
346    public static void testHasNotPublicMethod(String section, Class type, Class returnType, String name, Class... paramTypes) {
347        Class current = type;
348        while (current != null) {
349            for (Method m : current.getDeclaredMethods()) {
350                if (returnType.equals(returnType) && m.getName().equals(name) && Arrays.equals(m.getParameterTypes(), paramTypes)) {
351                    throw new TCKValidationException(section + ": Class must NOT implement method " + name + '(' + Arrays.toString(paramTypes) + "): "
352                            + returnType.getName() + ", but does: " + type.getName());
353                }
354            }
355            current = current.getSuperclass();
356        }
357    }
358
359    /**
360     * Checks the returned value, when calling a given method.
361     * 
362     * @param section
363     *            the section of the spec under test
364     * @param value
365     *            the expected value
366     * @param methodName
367     *            the target method name
368     * @param instance
369     *            the instance to call
370     * @throws NoSuchMethodException
371     * @throws SecurityException
372     * @throws IllegalAccessException
373     * @throws IllegalArgumentException
374     * @throws InvocationTargetException
375     * @throws TCKValidationException
376     *             if test fails.
377     */
378    public static void assertValue(String section, Object value, String methodName, Object instance)
379            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
380        Method m = instance.getClass().getDeclaredMethod(methodName);
381        Assert.assertEquals(value, m.invoke(instance), section + ": " + m.getName() + '(' + instance + ") returned invalid value:");
382    }
383
384    static boolean testHasPublicStaticMethodOpt(String section, Class type, Class returnType, String methodName, Class... paramTypes) {
385        try {
386            testHasPublicStaticMethod(section, type, returnType, methodName, paramTypes);
387            return true;
388        } catch (Exception e) {
389            warnings.append(section).append(": Recommendation failed: Missing method [public static ").append(methodName).append('(')
390                    .append(Arrays.toString(paramTypes)).append("):").append(returnType.getName()).append("] on: ").append(type.getName())
391                    .append("\n");
392            return false;
393        }
394    }
395
396    /**
397     * Test for immutability (optional recommendation), writes a warning if not given.
398     * 
399     * @param section
400     *            the section of the spec under test
401     * @param type
402     *            the type to be checked.
403     * @return true, if the instance is probably immutable.
404     */
405    public static boolean testImmutableOpt(String section, Class type) {
406        try {
407            testImmutable(section, type);
408            return true;
409        } catch (Exception e) {
410            warnings.append(section).append(": Recommendation failed: Class should be immutable: ").append(type.getName()).append(", details: ")
411                    .append(e.getMessage()).append("\n");
412            return false;
413        }
414    }
415
416    /**
417     * Test for serializable (optional recommendation), writes a warning if not given.
418     * 
419     * @param section
420     *            the section of the spec under test
421     * @param type
422     *            the type to be checked.
423     * @return true, if the type is probably serializable.
424     */
425    public static boolean testSerializableOpt(String section, Class type) {
426        try {
427            testSerializable(section, type);
428            return true;
429        } catch (Exception e) {
430            warnings.append(section).append(": Recommendation failed: Class should be serializable: ").append(type.getName()).append(", details: ")
431                    .append(e.getMessage()).append("\n");
432            return false;
433        }
434    }
435
436    /**
437     * Test for serializable (optional recommendation), writes a warning if not given.
438     * 
439     * @param section
440     *            the section of the spec under test
441     * @param instance
442     *            the object to be checked.
443     * @return true, if the instance is probably serializable.
444     */
445    public static boolean testSerializableOpt(String section, Object instance) {
446        try {
447            testSerializable(section, instance);
448            return true;
449        } catch (Exception e) {
450            warnings.append(section).append(": Recommendation failed: Class is serializable, but serialization failed: ")
451                    .append(instance.getClass().getName()).append("\n");
452            return false;
453        }
454    }
455
456    static void resetWarnings() {
457        warnings.setLength(0);
458    }
459
460    static String getWarnings() {
461        return warnings.toString();
462    }
463}