001/*
002 * Units of Measurement TCK
003 * Copyright © 2005-2023, 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.reflections.ReflectionUtils.getAllMethods;
034import static org.reflections.ReflectionUtils.withModifier;
035import static org.reflections.ReflectionUtils.withName;
036import static org.reflections.ReflectionUtils.withParametersCount;
037
038import static org.testng.Assert.assertEquals;
039import static org.testng.Assert.assertNotNull;
040import static org.testng.Assert.assertTrue;
041import static org.testng.Assert.fail;
042
043import java.io.ByteArrayOutputStream;
044import java.io.ObjectOutputStream;
045import java.io.Serializable;
046import java.lang.reflect.InvocationTargetException;
047import java.lang.reflect.Method;
048import java.lang.reflect.Modifier;
049import java.util.Arrays;
050import java.util.Collections;
051import java.util.List;
052import java.util.Random;
053import java.util.Set;
054
055import jakarta.inject.Singleton;
056import tech.units.tck.TCKValidationException;
057
058import javax.measure.spi.*;
059
060/**
061 * Test utilities used in the JSR 385 TCK.
062 *
063 * @author <a href="mailto:[email protected]">Werner Keil</a>
064 * @version 2.7, October 4, 2023
065 * @since 1.0
066 */
067@Singleton
068public class TestUtils {
069
070    /**
071     * Name of the system property to pass the desired profile
072     */
073    public static final String SYS_PROPERTY_PROFILE = "tech.units.tck.profile";
074
075    /**
076     * Name of the system property to override the default output directory
077     */
078    public static final String SYS_PROPERTY_OUTPUT_DIR = "tech.units.tck.outputDir";
079
080    /**
081     * Name of the system property to override the default report file
082     */
083    public static final String SYS_PROPERTY_REPORT_FILE = "tech.units.tck.reportFile";
084
085    /**
086     * Name of the system property to set the <code>verbose</code> flag
087     */
088    public static final String SYS_PROPERTY_VERBOSE = "tech.units.tck.verbose";
089
090    /**
091     * Number of built-in API prefix types
092     */
093    public static final int NUM_OF_PREFIX_TYPES = 2;
094    
095    /**
096     * Number of binary prefixes
097     */
098    public static final int NUM_OF_BINARY_PREFIXES = 8;
099    
100    /**
101     * Number of metric (SI) prefixes
102     */
103    public static final int NUM_OF_METRIC_PREFIXES = 24;
104    
105    /**
106     * Global message for missing TCK Configuration
107     */
108    public static final String MSG_NO_TCK_CONFIG = "TCK Configuration not available.";
109    
110    private static final StringBuilder warnings = new StringBuilder();
111
112    /**
113     * This class should not be instantiated
114     */
115    private TestUtils() {
116    }
117
118    static Number createNumberWithPrecision(QuantityFactory<?> f, int precision) {
119        if (precision == 0) {
120            precision = new Random().nextInt(100);
121        }
122        StringBuilder b = new StringBuilder(precision + 1);
123        for (int i = 0; i < precision; i++) {
124            b.append(String.valueOf(i % 10));
125        }
126        return Double.valueOf(b.toString());
127    }
128
129    static Number createNumberWithScale(QuantityFactory<?> f, int scale) {
130        StringBuilder b = new StringBuilder(scale + 2);
131        b.append("9.");
132        for (int i = 0; i < scale; i++) {
133            b.append(String.valueOf(i % 10));
134        }
135        return Double.valueOf(b.toString());
136    }
137
138    /**
139     * Tests the given object being {@link Serializable}.
140     *
141     * @param section
142     *            the section of the spec under test
143     * @param type
144     *            the type to be checked.
145     * @throws TCKValidationException
146     *             if the test fails.
147     * 
148     */
149    public static void testSerializable(String section, Class<?> type) {
150        if (!Serializable.class.isAssignableFrom(type)) {
151            throw new TCKValidationException(section + ": Class must be serializable: " + type.getName());
152        }
153    }
154
155    /**
156     * Tests the given object being (effectively) serializable by serializing it.
157     *
158     * @param section
159     *            the section of the spec under test
160     * @param o
161     *            the object to be checked.
162     * @throws TCKValidationException
163     *             if test fails.
164     */
165    public static void testSerializable(String section, Object o) {
166        if (!Serializable.class.isAssignableFrom(o.getClass())) {
167            throw new TCKValidationException(section + ": Class must be serializable: " + o.getClass().getName());
168        }
169        try (ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream())) {
170            oos.writeObject(o);
171        } catch (Exception e) {
172            throw new TCKValidationException("Class should be serializable, but serialization failed: " + o.getClass().getName(), e);
173        }
174    }
175    
176    /**
177     * Test for serializable (optional recommendation), writes a warning if not given.
178     *
179     * @param section
180     *            the section of the spec under test
181     * @param type
182     *            the type to be checked.
183     * @return true, if the type is probably serializable.
184     */
185    public static boolean testSerializableOpt(String section, @SuppressWarnings("rawtypes") Class type) {
186        try {
187            testSerializable(section, type);
188            return true;
189        } catch (Exception e) {
190            warnings.append(section).append(": Recommendation failed: Class should be serializable: ").append(type.getName()).append(", details: ")
191                    .append(e.getMessage()).append("\n");
192            return false;
193        }
194    }
195
196    /**
197     * Test for serializable (optional recommendation), writes a warning if not given.
198     *
199     * @param section
200     *            the section of the spec under test
201     * @param instance
202     *            the object to be checked.
203     * @return true, if the instance is probably serializable.
204     */
205    public static boolean testSerializableOpt(String section, Object instance) {
206        try {
207            testSerializable(section, instance);
208            return true;
209        } catch (Exception e) {
210            warnings.append(section).append(": Recommendation failed: Class is serializable, but serialization failed: ")
211                    .append(instance.getClass().getName()).append("\n");
212            return false;
213        }
214    }
215
216    /**
217     * Tests the given class implements a given interface.
218     *
219     * @param section
220     *            the section of the spec under test
221     * @param type
222     *            the type to be checked.
223     * @param iface
224     *            the interface to be checked for.
225     * Triggers Assert#fail
226     *             if test fails.
227     */
228    public static void testImplementsInterface(String section, Class<?> type, Class<?> iface) {
229        for (Class<?> ifa : type.getInterfaces()) {
230            if (ifa.equals(iface)) {
231                return;
232            }
233        }
234        fail(section + ": Class must implement " + iface.getName() + ", but does not: " + type.getName());
235    }
236
237    /**
238     * Tests if the given type is {@link Comparable}.
239     *
240     * @param section
241     *            the section of the spec under test
242     * @param type
243     *            the type to be checked.
244     * @throws TCKValidationException
245     *             if the test fails.            
246     */
247    public static void testComparable(String section, Class<?> type) {
248        if (!Comparable.class.isAssignableFrom(type)) {
249            throw new TCKValidationException(section + ": Class must be comparable: " + type.getName());
250        }
251    }
252
253    /**
254     *
255     * @param section the section of the specification
256     * @param type the type to be checked.
257     * @param returnType the expected return type
258     * @param name the name of the method
259     * @param paramTypes the types of parameters if available
260     */
261    public static void testHasPublicMethod(String section, Class<?> type, Class<?> returnType, String name, Class<?>... paramTypes) {
262        Class<?> current = type;
263        while (current != null) {
264            for (Method m : current.getDeclaredMethods()) {
265                if (returnType.equals(m.getReturnType()) && m.getName().equals(name) && ((m.getModifiers() & PUBLIC) != 0)
266                        && Arrays.equals(m.getParameterTypes(), paramTypes)) {
267                    return;
268                }
269            }
270            current = current.getSuperclass();
271        }
272        throw new TCKValidationException(section + ": Class must implement method " + name + '(' + Arrays.toString(paramTypes) + "): "
273                + returnType.getName() + ", but does not: " + type.getName());
274    }
275
276    @SuppressWarnings("rawtypes")
277        private static final List<Class> PRIMITIVE_CLASSES = Collections
278            .unmodifiableList(Arrays.asList(new Class[] { Object.class, Number.class, Enum.class }));
279
280    /**
281     *
282     * @param section the section of the specification
283     * @param type the data type
284     * @param trySuperclassFirst if tht super class if available should be tested first
285     * @param returnType the expected return type
286     * @param name the name of the method
287     * @param paramTypes types of parameters
288     */
289    public static void testHasPublicMethod(String section, Class<?> type, boolean trySuperclassFirst, Class<?> returnType, String name,
290            Class<?>... paramTypes) {
291        if (trySuperclassFirst && type.getSuperclass() != null) {
292            if (PRIMITIVE_CLASSES.contains(type.getSuperclass())) {
293                testHasPublicMethod(section, type, returnType, name, paramTypes);
294            } else {
295                testHasPublicMethod(section, type.getSuperclass(), returnType, name, paramTypes);
296            }
297        } else {
298            testHasPublicMethod(section, type, returnType, name, paramTypes);
299        }
300    }
301
302    /**
303     * Tests if the given type has a public method with the given signature.
304     *
305     * @param section
306     *            the section of the spec under test
307     * @param type
308     *            the type to be checked.
309     * @param name
310     *            the method name
311     * @param hasParameters
312     *            the method has parameters.
313     * @throws TCKValidationException
314     *             if test fails.
315     */
316    @SuppressWarnings({ "unchecked" })
317    public static void testHasPublicMethod(String section, Class<?> type, String name, boolean hasParameters) {
318        Set<Method> getters;
319        if (hasParameters) {
320            getters = getAllMethods(type, withModifier(PUBLIC), withName(name));
321        } else {
322            getters = getAllMethods(type, withModifier(PUBLIC), withName(name), withParametersCount(0));
323        }
324        assertNotNull(getters);
325        assertTrue(getters.size() >= 1); // interface plus at least one implementation
326    }
327
328    /**
329     * @param section the section of the specification
330     * @param type the data type
331     * @param name the name of the method
332     */
333    public static void testHasPublicMethod(String section, Class<?> type, String name) {
334        testHasPublicMethod(section, type, name, false);
335    }
336
337    /**
338     * Tests if the given type has a public static method with the given signature.
339     *
340     * @param section
341     *            the section of the spec under test
342     * @param type
343     *            the type to be checked.
344     * @param returnType
345     *            the method return type.
346     * @param name
347     *            the method name
348     * @param paramTypes
349     *            the parameter types.
350     * @throws TCKValidationException
351     *             if test fails.
352     */
353    @SuppressWarnings("rawtypes")
354    static void testHasPublicStaticMethod(String section, Class type, Class returnType, String name, Class... paramTypes) {
355        Class current = type;
356        while (current != null) {
357            for (Method m : current.getDeclaredMethods()) {
358                if (returnType.equals(returnType) && m.getName().equals(name) && ((m.getModifiers() & PUBLIC) != 0)
359                        && ((m.getModifiers() & Modifier.STATIC) != 0) && Arrays.equals(m.getParameterTypes(), paramTypes)) {
360                    return;
361                }
362            }
363            current = current.getSuperclass();
364        }
365        throw new TCKValidationException(section + ": Class must implement method " + name + '(' + Arrays.toString(paramTypes) + "): "
366                + returnType.getName() + ", but does not: " + type.getName());
367    }
368
369    /**
370     * Tests if the given type has not a public method with the given signature.
371     *
372     * @param section
373     *            the section of the spec under test
374     * @param type
375     *            the type to be checked.
376     * @param returnType
377     *            the method return type.
378     * @param name
379     *            the method name
380     * @param paramTypes
381     *            the parameter types.
382     * @throws TCKValidationException
383     *             if test fails.
384     */
385    @SuppressWarnings("rawtypes")
386        public static void testHasNotPublicMethod(String section, Class<?> type, Class<?> returnType, String name, Class<?>... paramTypes) {
387        Class current = type;
388        while (current != null) {
389            for (Method m : current.getDeclaredMethods()) {
390                if (returnType.equals(returnType) && m.getName().equals(name) && Arrays.equals(m.getParameterTypes(), paramTypes)) {
391                    throw new TCKValidationException(section + ": Class must NOT implement method " + name + '(' + Arrays.toString(paramTypes) + "): "
392                            + returnType.getName() + ", but does: " + type.getName());
393                }
394            }
395            current = current.getSuperclass();
396        }
397    }
398
399    /**
400     * Checks the returned value, when calling a given method.
401     *
402     * @param section
403     *            the section of the spec under test
404     * @param value
405     *            the expected value
406     * @param methodName
407     *            the target method name
408     * @param instance
409     *            the instance to call
410     * @throws NoSuchMethodException if no method with the given name exists.
411     * @throws SecurityException if a security problem occurs.
412     * @throws IllegalAccessException if the method may not be called, e.g. due to security constraints.
413     * @throws IllegalArgumentException if a wrong or inappropriate argument was provided.
414     * @throws InvocationTargetException if an exception thrown by an invoked method or constructor.
415     * @throws TCKValidationException
416     *             if test fails.
417     */
418    public static void assertValue(String section, Object value, String methodName, Object instance)
419            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
420        final Method m = instance.getClass().getDeclaredMethod(methodName);
421        assertEquals(m.invoke(instance), value, section + ": " + m.getName() + '(' + instance + ") returned invalid value:");
422    }
423
424    static boolean testHasPublicStaticMethodOpt(String section, @SuppressWarnings("rawtypes") Class type, @SuppressWarnings("rawtypes") Class returnType, String methodName, @SuppressWarnings("rawtypes") Class... paramTypes) {
425        try {
426            testHasPublicStaticMethod(section, type, returnType, methodName, paramTypes);
427            return true;
428        } catch (Exception e) {
429            warnings.append(section).append(": Recommendation failed: Missing method [public static ").append(methodName).append('(')
430                    .append(Arrays.toString(paramTypes)).append("):").append(returnType.getName()).append("] on: ").append(type.getName())
431                    .append("\n");
432            return false;
433        }
434    }
435    
436    /**
437     * Tests the given class being immutable.
438     *
439     * @param section
440     *            the section of the spec under test
441     * @param type
442     *            the type to be checked.
443     * @throws TCKValidationException
444     *             if test fails.
445     * @deprecated This was never used by the TCK, as immutability is highly recommended, but not enforced. MutabilityDetector is also incompatible with the Java Platform Module System and not actively developed in recent years.
446     */
447    public static void testImmutable(String section, Class<?> type) {
448//        try {
449//            MutabilityAssert.assertInstancesOf(type, MutabilityMatchers.areImmutable(),
450//                    AllowedReason.provided(Dimension.class, Quantity.class, Unit.class, UnitConverter.class).areAlsoImmutable(),
451//                    AllowedReason.allowingForSubclassing(), AllowedReason.allowingNonFinalFields());
452//        } catch (Exception e) {
453//            throw new TCKValidationException(section + ": Class is not immutable: " + type.getName(), e);
454//        }
455    }
456
457    /**
458     * Test for immutability (optional recommendation), writes a warning if not given.
459     *
460     * @param section
461     *            the section of the spec under test
462     * @param type
463     *            the type to be checked.
464     * @return true, if the instance is probably immutable.
465     * 
466     * @deprecated This was never used by the TCK, as immutability is highly recommended, but not enforced. MutabilityDetector is also incompatible with the Java Platform Module System and not actively developed in recent years. 
467     */
468    public static boolean testImmutableOpt(String section, @SuppressWarnings("rawtypes") Class type) {
469        try {
470            testImmutable(section, type);
471            return true;
472        } catch (Exception e) {
473            warnings.append(section).append(": Recommendation failed: Class should be immutable: ").append(type.getName()).append(", details: ")
474                    .append(e.getMessage()).append("\n");
475            return false;
476        }
477    }
478
479    static void resetWarnings() {
480        warnings.setLength(0);
481    }
482
483    static String getWarnings() {
484        return warnings.toString();
485    }
486}