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}