001/*
002 * Copyright (c) 2012, 2013, Werner Keil, Credit Suisse (Anatole Tresch). Licensed under the Apache
003 * License, Version 2.0 (the "License"); you may not use this file except in compliance with the
004 * License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
005 * Unless required by applicable law or agreed to in writing, software distributed under the License
006 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
007 * or implied. See the License for the specific language governing permissions and limitations under
008 * the License. Contributors: Anatole Tresch - initial version.
009 */
010package org.javamoney.tck;
011
012import junit.framework.Assert;
013import org.mutabilitydetector.unittesting.AllowedReason;
014import org.mutabilitydetector.unittesting.MutabilityAssert;
015import org.mutabilitydetector.unittesting.MutabilityMatchers;
016import org.testng.AssertJUnit;
017
018import javax.money.CurrencyUnit;
019import javax.money.Monetary;
020import javax.money.MonetaryAmount;
021import javax.money.MonetaryAmountFactory;
022import javax.money.MonetaryAmountFactoryQuery;
023import javax.money.MonetaryAmountFactoryQueryBuilder;
024import javax.money.MonetaryException;
025import javax.money.MonetaryOperator;
026import javax.money.MonetaryQuery;
027import javax.money.NumberValue;
028import java.io.ByteArrayOutputStream;
029import java.io.ObjectOutputStream;
030import java.io.Serializable;
031import java.lang.reflect.InvocationTargetException;
032import java.lang.reflect.Method;
033import java.lang.reflect.Modifier;
034import java.math.BigDecimal;
035import java.math.MathContext;
036import java.util.Arrays;
037import java.util.Currency;
038import java.util.Random;
039
040/**
041 * Test Utility class.
042 */
043public final class TestUtils {
044    /**
045     * Warnings collected.
046     */
047    private static final StringBuffer WARNINGS = new StringBuffer();
048
049    private TestUtils() {
050    }
051
052
053    /**
054     * Creates a new number with the given precision.
055     *
056     * @param precision the precision
057     * @return a corresponding number.
058     */
059    public static BigDecimal createNumberWithPrecision(int precision) {
060        if (precision == 0) {
061            precision = new Random().nextInt(100);
062        }
063        StringBuilder b = new StringBuilder(precision + 1);
064        for (int i = 0; i < precision; i++) {
065            b.append(String.valueOf(i % 10));
066        }
067        return new BigDecimal(b.toString(), MathContext.UNLIMITED);
068    }
069
070    /**
071     * Creates a corresponding number with the required scale.
072     *
073     * @param scale the target scale.
074     * @return a corresponding number.
075     */
076    public static BigDecimal createNumberWithScale(int scale) {
077        StringBuilder b = new StringBuilder(scale + 2);
078        b.append("9.");
079        for (int i = 0; i < scale; i++) {
080            b.append(String.valueOf(i % 10));
081        }
082        return new BigDecimal(b.toString(), MathContext.UNLIMITED);
083    }
084
085    /**
086     * Tests the given class being serializable.
087     *
088     * @param section the section of the spec under test
089     * @param c       the class to be checked.
090     * @throws org.javamoney.tck.TCKValidationException if test fails.
091     */
092    public static void testSerializable(String section, Class c) {
093        if (!Serializable.class.isAssignableFrom(c)) {
094            throw new TCKValidationException(section + ": Class must be serializable: " + c.getName());
095        }
096    }
097
098    /**
099     * Tests the given class being immutable.
100     *
101     * @param section the section of the spec under test
102     * @param c       the class to be checked.
103     * @throws org.javamoney.tck.TCKValidationException if test fails.
104     */
105    public static void testImmutable(String section, Class c) {
106        try {
107            MutabilityAssert.assertInstancesOf(c, MutabilityMatchers.areImmutable(), AllowedReason
108                            .provided(Currency.class, MonetaryAmount.class,
109                                    CurrencyUnit.class, NumberValue.class,
110                                    MonetaryOperator.class, MonetaryQuery.class)
111                            .areAlsoImmutable(), AllowedReason.allowingForSubclassing(),
112                    AllowedReason.allowingNonFinalFields());
113        } catch (Exception e) {
114            throw new TCKValidationException(section + ": Class is not immutable: " + c.getName(), e);
115        }
116    }
117
118    /**
119     * Tests the given object being (effectively) serializable by serializing it.
120     *
121     * @param section the section of the spec under test
122     * @param o       the object to be checked.
123     * @throws org.javamoney.tck.TCKValidationException if test fails.
124     */
125    public static void testSerializable(String section, Object o) {
126        if (!(o instanceof Serializable)) {
127            throw new TCKValidationException(section + ": Class must be serializable: " + o.getClass().getName());
128        }
129        try (
130                ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream())) {
131            oos.writeObject(o);
132        } catch (Exception e) {
133            throw new TCKValidationException(
134                    "Class must be serializable, but serialization failed: " + o.getClass().getName(), e);
135        }
136    }
137
138    /**
139     * Tests the given class implements a given interface.
140     *
141     * @param section the section of the spec under test
142     * @param type    the type to be checked.
143     * @param iface   the interface to be checked for.
144     * @throws org.javamoney.tck.TCKValidationException if test fails.
145     */
146    public static void testImplementsInterface(String section, Class type, Class iface) {
147        for (Class ifa : type.getInterfaces()) {
148            if (ifa.equals(iface)) {
149                return;
150            }
151        }
152        Assert.fail(section + ": Class must implement " + iface.getName() + ", but does not: " + type.getName());
153    }
154
155    /**
156     * Tests if the given type has a public method with the given signature.
157     * @param section the section of the spec under test
158     * @param type the type to be checked.
159     * @param returnType the method return type.
160     * @param name the method name
161     * @param paramTypes the parametr types.
162     *                   @throws org.javamoney.tck.TCKValidationException if test fails.
163     */
164    public static void testHasPublicMethod(String section, Class type, Class returnType, String name,
165                                           Class... paramTypes) {
166        Class current = type;
167        while (current != null) {
168            for (Method m : current.getDeclaredMethods()) {
169                if (returnType.equals(returnType) &&
170                        m.getName().equals(name) &&
171                        ((m.getModifiers() & Modifier.PUBLIC) != 0) &&
172                        Arrays.equals(m.getParameterTypes(), paramTypes)) {
173                    return;
174                }
175            }
176            current = current.getSuperclass();
177        }
178        throw new TCKValidationException(
179                section + ": Class must implement method " + name + '(' + Arrays.toString(paramTypes) + "): " +
180                        returnType.getName() + ", but does not: " + type.getName());
181    }
182
183    /**
184     * Tests if the given type has a public static method with the given signature.
185     * @param section the section of the spec under test
186     * @param type the type to be checked.
187     * @param returnType the method return type.
188     * @param name the method name
189     * @param paramTypes the parametr types.
190     *                   @throws org.javamoney.tck.TCKValidationException if test fails.
191     */
192    public static void testHasPublicStaticMethod(String section, Class type, Class returnType, String name,
193                                                 Class... paramTypes) {
194        Class current = type;
195        while (current != null) {
196            for (Method m : current.getDeclaredMethods()) {
197                if (returnType.equals(returnType) &&
198                        m.getName().equals(name) &&
199                        ((m.getModifiers() & Modifier.PUBLIC) != 0) &&
200                        ((m.getModifiers() & Modifier.STATIC) != 0) &&
201                        Arrays.equals(m.getParameterTypes(), paramTypes)) {
202                    return;
203                }
204            }
205            current = current.getSuperclass();
206        }
207        throw new TCKValidationException(
208                section + ": Class must implement method " + name + '(' + Arrays.toString(paramTypes) + "): " +
209                        returnType.getName() + ", but does not: " + type.getName());
210    }
211
212    /**
213     * Tests if the given type has not a public method with the given signature.
214     * @param section the section of the spec under test
215     * @param type the type to be checked.
216     * @param returnType the method return type.
217     * @param name the method name
218     * @param paramTypes the parametr types.
219     *                   @throws org.javamoney.tck.TCKValidationException if test fails.
220     */
221    public static void testHasNotPublicMethod(String section, Class type, Class returnType, String name,
222                                              Class... paramTypes) {
223        Class current = type;
224        while (current != null) {
225            for (Method m : current.getDeclaredMethods()) {
226                if (returnType.equals(returnType) &&
227                        m.getName().equals(name) &&
228                        Arrays.equals(m.getParameterTypes(), paramTypes)) {
229                    throw new TCKValidationException(
230                            section + ": Class must NOT implement method " + name + '(' + Arrays.toString(paramTypes) +
231                                    "): " + returnType.getName() + ", but does: " + type.getName());
232                }
233            }
234            current = current.getSuperclass();
235        }
236    }
237
238    /**
239     * Tests if the given type is comparable.
240     * @param section the section of the spec under test
241     * @param type the type to be checked.
242     *             @throws org.javamoney.tck.TCKValidationException if test fails.
243     */
244    public static void testComparable(String section, Class type) {
245        testImplementsInterface(section, type, Comparable.class);
246    }
247
248    /**
249     * Checks the returned value, when calling a given method.
250     * @param section the section of the spec under test
251     * @param value the expected value
252     * @param methodName the target method name
253     * @param instance the instance to call
254     * @throws NoSuchMethodException
255     * @throws SecurityException
256     * @throws IllegalAccessException
257     * @throws IllegalArgumentException
258     * @throws InvocationTargetException
259     * @throws org.javamoney.tck.TCKValidationException if test fails.
260     */
261    public static void assertValue(String section, Object value, String methodName, Object instance)
262            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException,
263            InvocationTargetException {
264        Method m = instance.getClass().getDeclaredMethod(methodName);
265        Assert.assertEquals(section + ": " + m.getName() + '(' + instance + ") returned invalid value:", value,
266                m.invoke(instance));
267    }
268
269    /**
270     * Test for immutability (optional recommendation), writes a warning if not given.
271     * @param section the section of the spec under test
272     * @param type the type to be checked.
273     * @return true, if the instance is probably mutable.
274     */
275    public static boolean testImmutableOpt(String section, Class type) {
276        try {
277            testImmutable(section, type);
278            return true;
279        } catch (Exception e) {
280            WARNINGS.append(section).append(": Recommendation failed: Class should be immutable: ")
281                    .append(type.getName()).append(", details: ").append(e.getMessage()).append("\n");
282            return false;
283        }
284    }
285    /**
286     * Test for serializable (optional recommendation), writes a warning if not given.
287     * @param section the section of the spec under test
288     * @param type the type to be checked.
289     * @return true, if the instance is probably mutable.
290     */
291    public static boolean testSerializableOpt(String section, Class type) {
292        try {
293            testSerializable(section, type);
294            return true;
295        } catch (Exception e) {
296            WARNINGS.append(section).append(": Recommendation failed: Class should be serializable: ")
297                    .append(type.getName()).append(", details: ").append(e.getMessage()).append("\n");
298            return false;
299        }
300    }
301
302    /**
303     * Tests if instance has a pipublic static method.
304     * @param section the section of the spec under test
305     * @param type the type to be checked.
306     * @param returnType the method return type.
307     * @param methodName  the target method name
308     * @param paramTypes the parametr types.
309     * @return true, if test succeeded.
310     */
311    public static boolean testHasPublicStaticMethodOpt(String section, Class type, Class returnType, String methodName,
312                                                       Class... paramTypes) {
313        try {
314            testHasPublicStaticMethod(section, type, returnType, methodName, paramTypes);
315            return true;
316        } catch (Exception e) {
317            WARNINGS.append(section).append(": Recommendation failed: Missing method [public static ")
318                    .append(methodName).append('(').append(Arrays.toString(paramTypes)).append("):")
319                    .append(returnType.getName()).append("] on: ").append(type.getName()).append("\n");
320            return false;
321        }
322    }
323
324    /**
325     * Tests if an instance is effectively serializable.
326     * @param section the section of the spec under test
327     * @param instance the instance to call
328     * @return true, if test succeded.
329     */
330    public static boolean testSerializableOpt(String section, Object instance) {
331        try {
332            testSerializable(section, instance);
333            return true;
334        } catch (Exception e) {
335            WARNINGS.append(section)
336                    .append(": Recommendation failed: Class is serializable, but serialization failed: ")
337                    .append(instance.getClass().getName()).append("\n");
338            return false;
339        }
340    }
341
342    /**
343     * Reset all collected WARNINGS.
344     */
345    public static void resetWarnings() {
346        WARNINGS.setLength(0);
347    }
348
349    /**
350     * Get the collected WARNINGS.
351     * @return
352     */
353    public static String getWarnings() {
354        return WARNINGS.toString();
355    }
356
357    /**
358     * Creates an amount with the given scale.
359     * @param scale the target scale
360     * @return the new amount.
361     */
362    public static MonetaryAmount createAmountWithScale(int scale) {
363        MonetaryAmountFactoryQuery tgtContext = MonetaryAmountFactoryQueryBuilder.of().setMaxScale(scale).build();
364        MonetaryAmountFactory<?> exceedingFactory;
365        try {
366            exceedingFactory = Monetary.getAmountFactory(tgtContext);
367            AssertJUnit.assertNotNull(exceedingFactory);
368            MonetaryAmountFactory<? extends MonetaryAmount> bigFactory =
369                    Monetary.getAmountFactory(exceedingFactory.getAmountType());
370            return bigFactory.setCurrency("CHF").setNumber(createNumberWithScale(scale)).create();
371        } catch (MonetaryException e) {
372            return null;
373        }
374    }
375
376    /**
377     * Creates an amount with the given precision.
378     * @param precision the target precision
379     * @return a corresponding amount.
380     */
381    public static MonetaryAmount createAmountWithPrecision(int precision) {
382        MonetaryAmountFactoryQuery tgtContext = MonetaryAmountFactoryQueryBuilder.of().setPrecision(precision).build();
383        MonetaryAmountFactory<?> exceedingFactory;
384        try {
385            exceedingFactory = Monetary.getAmountFactory(tgtContext);
386            AssertJUnit.assertNotNull(exceedingFactory);
387            MonetaryAmountFactory<? extends MonetaryAmount> bigFactory =
388                    Monetary.getAmountFactory(exceedingFactory.getAmountType());
389            return bigFactory.setCurrency("CHF").setNumber(createNumberWithPrecision(precision)).create();
390        } catch (MonetaryException e) {
391            return null;
392        }
393    }
394}