/* * Copyright 2013 Guidewire Software, Inc. */ package gw.test; import gw.lang.reflect.IAnnotationInfo; import gw.lang.reflect.IConstructorInfo; import gw.lang.reflect.IHasJavaClass; import gw.lang.reflect.IMethodInfo; import gw.lang.reflect.IType; import gw.lang.reflect.Modifier; import gw.lang.reflect.TypeSystem; import gw.lang.reflect.java.IJavaType; import gw.lang.reflect.java.JavaTypes; import gw.testharness.IncludeInTestResults; import gw.testharness.KnownBreak; import gw.testharness.KnownBreakQualifier; import gw.util.GosuExceptionUtil; import gw.util.GosuObjectUtil; import gw.util.GosuStringUtil; import gw.util.Predicate; import junit.framework.TestCase; import junit.framework.TestResult; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; public abstract class TestClass extends TestCase implements ITestWithMetadata { private String _pkgName; private String _className; private TestExecutionManager _executionManager; private static final ThreadLocal<TestExecutionManager> THREAD_LOCAL_EXECUTION_MANAGER = new ThreadLocal<TestExecutionManager>(); private List<TestMetadata> _metadata = new ArrayList<TestMetadata>(); private boolean _doNotRun; private boolean _knownBreak; private static final Map<String, Integer> _numberOfInstancesCreatedByTypeName = new HashMap<String, Integer>(); private boolean _isGosuTest; protected TestClass() { super(); initInternalData(); } protected TestClass(String s) { super(s); initInternalData(); } // This is a bit hacky: some subclasses might need to delay the call // to the initInternalData() method until after the class performed some additional work protected TestClass(boolean shouldInit) { if (shouldInit) { initInternalData(); } } // This is a bit hacky: some subclasses might need to delay the call // to the init() method until after the class performed some additional work protected TestClass(String s, boolean shouldInit) { super(s); if (shouldInit) { initInternalData(); } } public boolean isGosuTest() { return _isGosuTest; } public void setGosuTest(boolean gosuTest) { _isGosuTest = gosuTest; } protected void initInternalData() { String fullName = getFullClassNameInternal(); int lastDot = fullName.lastIndexOf("."); _pkgName = fullName.substring(0, lastDot).replace("_proxy_.", ""); _className = fullName.substring(lastDot + 1, fullName.length()).replace('$', '.'); // When running from an IDE, we could be running just one method out of a class, or we could be running // the entire class. Since we have no way to get ahold of the suite that the IDE has created, the only // way for us to tell how many methods we're running out of a given class is to track how many instances // of each class are created. We can then use that to determine when to run the afterClass() hook. Integer numberOfInstances = _numberOfInstancesCreatedByTypeName.get(getTypeName()); if (numberOfInstances == null) { _numberOfInstancesCreatedByTypeName.put(getTypeName(), 1); } else { _numberOfInstancesCreatedByTypeName.put(getTypeName(), numberOfInstances + 1); } } protected String getFullClassNameInternal() { return getClass().getName(); } public static Integer getNumberOfInstancesOfTestClassCreated(String typeName) { return _numberOfInstancesCreatedByTypeName.get(typeName); } @Override protected final void setUp() throws Exception { super.setUp(); if(!getType().getName().endsWith("Test")) { throw new IllegalStateException("All subclasses of TestClass must have a name that ends with \"Test\""); } if ( _executionManager == null || _executionManager.assertionsMustBeEnabled()) { try { assert false; throw new IllegalStateException("Assertions must be enabled for tests to be run properly."); } catch (AssertionError ae) { //ignore } } } public void setExecutionManager(TestExecutionManager executionManager) { _executionManager = executionManager; } @Override protected final void tearDown() throws Exception { super.tearDown(); } public void beforeTestClass(){ } public void beforeTestMethod(){ } public void afterTestMethod(Throwable possibleException){ } public void afterTestClass(){ } @Override public void run(TestResult result) { getExecutionManager().runTestClass(this, result); } void reallyRun(TestResult result) { super.run(result); } @Override public void runBare() throws Throwable { getExecutionManager().runTestClassBare(this); } public void reallyRunBare() throws Throwable { initMetadata(getName()); super.runBare(); } @Override public String toString() { return this.getName() + "(" + getTypeName() + ")"; } @Override public void setName(String name) { super.setName(name); } @Override public String getName() { return super.getName(); } protected TestExecutionManager getExecutionManager() { if (_executionManager == null) { return getThreadLocalExecutionManager(); } else { return _executionManager; } } //Provides a simple thread local execution manager for all the tests being run //presumably from an IDE environment private TestExecutionManager getThreadLocalExecutionManager() { TestExecutionManager executionManager = THREAD_LOCAL_EXECUTION_MANAGER.get(); if (executionManager == null) { executionManager = new TestExecutionManager(); executionManager.setEnvironment(createDefaultEnvironment()); THREAD_LOCAL_EXECUTION_MANAGER.set(executionManager); // TODO - AHK - Set up the default classpath? } return executionManager; } public TestEnvironment createDefaultEnvironment() { return new TestEnvironment(); } @Override protected final void runTest() throws Throwable { // TODO - AHK - More properly log this, rather than using System.out if (_knownBreak) { System.out.println("**** Test method " + getName() + " is marked as a known break. Run tests with -Dgw.tests.skip.knownbreak=true to skip known breaks."); } if (_doNotRun) { System.out.println("*** Skipping test method " + getName() + ", as it's marked @Disabled, @ManualTest, or @InProgress"); } else if (_knownBreak && skipKnownBreakTests()) { System.out.println("*** Skipping test method " + getName() + ", as it's marked @KnownBreak and the gw.tests.skip.knownbreak system parameter is set to true."); } else { doRunTest( getName() ); } } private static Boolean _skipKnownBreakTests = null; private static boolean skipKnownBreakTests() { // And no, I don't really care about thread-safety here if (_skipKnownBreakTests == null) { String propValue = System.getProperty("gw.tests.skip.knownbreak"); if (propValue != null) { _skipKnownBreakTests = Boolean.valueOf(propValue); } else { _skipKnownBreakTests = false; } } return _skipKnownBreakTests; } protected void doRunTest( String name ) throws Throwable { IType type = getType(); Method runMethod; if( type instanceof IJavaType && ((IHasJavaClass) getType()).getBackingClass() == null ) { // Handle case where we are getting IJavaClassInfo from source (getBackingClass() returns null) ClassLoader cl = type.getTypeLoader().getModule().getModuleTypeLoader().getDefaultTypeLoader().getGosuClassLoader().getActualLoader(); Class<?> testClass = Class.forName( type.getName(), true, cl ); runMethod = testClass.getMethod( name ); } else { runMethod = ((IHasJavaClass) getType()).getBackingClass().getMethod( name ); } if (runMethod == null) { fail("Method \"" + name + "\" not found"); } if (!Modifier.isPublic(runMethod.getModifiers())) { fail("Method \"" + name + "\" should be public"); } try { runMethod.invoke(this); } catch( InvocationTargetException e ) { throw GosuExceptionUtil.forceThrow( e.getTargetException() ); } // IMethodInfo runMethod = null; // runMethod = getType().getTypeInfo().getMethod( name ); // if (runMethod == null) { // fail("Method \"" + name + "\" not found"); // } // if (!runMethod.isPublic()) { // fail("Method \"" + name + "\" should be public"); // } // runMethod.getCallHandler().handleCall(this); } public IType getType() { return TypeSystem.getFromObject(this); } public String getTypeName() { return _pkgName + "." + _className; } //================================================================ // Utility Methods //================================================================ public String getClassName() { return _className; } public String getPackageName() { return _pkgName; } //================================================================ // Assertion extensions //================================================================ public interface EqualityTester { boolean equals(Object expected, Object got); } public static void assertArrayEquals(Object[] expected, Object[] got) { assertArrayEquals(expected, got, new EqualityTester() { @Override public boolean equals(Object expected, Object got) { if (expected != null && got != null && expected.getClass().isArray() && got.getClass().isArray() && Array.getLength(expected) == Array.getLength(got)) { int length = Array.getLength(expected); for (int i = 0; i < length; i++) { if (!equals(Array.get(expected, i), Array.get(got, i))) { return false; } } return true; } return GosuObjectUtil.equals(expected, got); } }); } /** * Compare two byte arrays, first the size then each byte. * @param expected * @param actual */ public static void assertArrayEquals(String message, byte[] expected, byte[] actual) { if (expected.length != actual.length) { fail(message+" - expected array length of "+expected.length+" but got "+actual.length); for (int i=0; i<expected.length; i++) { assertEquals(message, expected[i], actual[i]); } } } /** * Verifies that all elements in the first array are present in the second * array and match the elements in the first array. Uses EqualityUtil to * determine equality and is order-insensitive. * * @param expected the expected result (reference) * @param got the obtained result (what to compare against the reference) */ public static void assertArrayEquals(Object[] expected, Object[] got, EqualityTester tester) { if (expected == null) { if (got == null) { return; } else { fail("Expected null, got non-null"); } } else { if (got == null) { fail("Expected non-null, got null"); } } boolean[] expectedFound = makeFoundArray(expected.length); boolean[] gotFound = makeFoundArray(got.length); for (int i = 0; i < expected.length; i++) { Object expectedObject = expected[i]; for (int j = 0; j < got.length; j++) { if (tester.equals(expectedObject, got[j]) && !gotFound[j]) { expectedFound[i] = true; gotFound[j] = true; break; } } } if (!allTrue(expectedFound) || !allTrue(gotFound)) { StringBuffer sb = new StringBuffer(); sb.append("\nExpected:\n"); appendFoundStatus(sb, expected, expectedFound); sb.append("\nGot:\n"); appendFoundStatus(sb, got, gotFound); fail(sb.toString()); } } private static void appendFoundStatus(StringBuffer /*INOUT*/ sb, Object[] expected, boolean[] expectedFound) { sb.append("[\n"); for (int i = 0; i < expected.length; i++) { Object o = expected[i]; sb.append(expectedFound[i] ? " " : "! "); // "! " means we didn't find it sb.append(o); sb.append("\n"); } sb.append("]\n"); } private static boolean[] makeFoundArray(int length) { boolean[] found = new boolean[length]; for (int i = 0; i < found.length; i++) { found[i] = false; } return found; } private static boolean allTrue(boolean[] booleans) { for (boolean b : booleans) { if (!b) { return false; } } return true; } // TODO - AHK - This should be using the other variant public static void assertArrayEquals(String message, Object[] o1, Object[] o2) { boolean equals = false; if(o1.length == o2.length){ equals = true; for (int i = 0; i < o1.length; i++) { if(!GosuObjectUtil.equals(o1[i], o2[i]) ){ equals = false; break; } } } assertTrue(message + " Arrays were not equal. Expected \n[" + GosuStringUtil.join(o1, ",") + "] but found \n[" + GosuStringUtil.join(o2, ",") + "]", equals); } public static void assertSetsEqual(Set o1, Set o2) { boolean equals = GosuObjectUtil.equals( o1, o2 ); assertTrue( "Sets were not equal. Expected \n[" + GosuStringUtil.join( o1, "," ) + "] but found \n[" + GosuStringUtil.join( o2, "," ) + "]", equals ); } public static void assertCollectionEquals(Collection o1, Collection o2) { assertIterableEqualsIgnoreOrder(o1, o2); } public static void assertListEquals(List o1, List o2) { assertIterableEquals(o1, o2, "Lists"); } public static void assertIterableEquals(Iterable o1, Iterable o2) { assertIterableEquals(makeList(o1), makeList(o2), "Iterables"); } public static void assertCollectionEquals(Collection o1, Collection o2, Comparator c) { assertIterableEquals(o1, o2, c, "Collections"); } public static void assertListEquals(List o1, List o2, Comparator c) { assertIterableEquals(o1, o2, c, "Lists"); } public static void assertIterableEquals(Iterable o1, Iterable o2, Comparator c) { assertIterableEquals(makeList(o1), makeList(o2), c, "Iterables"); } public static void assertIterableEqualsIgnoreOrder(Iterable i1, Iterable i2) { Map count1 = makeHistogram( i1 ); Map count2 = makeHistogram( i2 ); if (!count1.equals(count2)) { assertTrue( "Iterators were not equal ignoring order. Expected [" + GosuStringUtil.join(i1.iterator(), ",") + "] but found [" + GosuStringUtil.join(i2.iterator(), ",") + "]", false); } } public static void assertZero(int i) { assertTrue("Should be zero, but found " + i, i == 0); } public static void assertZero(long i) { assertTrue("Should be zero, but found " + i, i == 0); } public static void assertMatchRegex(String message, String pattern, String result) { assertTrue(message + ": " + pattern + " does not match " + result, result.matches(pattern)); } private static Map makeHistogram(Iterable o1) { HashMap<Object, Integer> hist = new HashMap<Object, Integer>(); if( o1 != null ) { for (Object o : o1) { Integer integer = hist.get(o); if (integer == null) { hist.put(o, 0); } else { hist.put(o, ++integer); } } } return hist; } private static void assertIterableEquals(Iterable i1, Iterable i2, String s) { boolean equals = true; if (i1 == i2) return; Iterator e1 = i1.iterator(); Iterator e2 = i2.iterator(); while (e1.hasNext() && e2.hasNext()) { Object o1 = e1.next(); Object o2 = e2.next(); if (o1 == null) { if (o2 != null) { equals = false; break; } } else if (!o1.equals(o2)) { equals = false; break; } } if (equals) { equals = !(e1.hasNext() || e2.hasNext()); } assertTrue( s + " were not equal. Expected \n[" + GosuStringUtil.join(i1.iterator(), ",") + "] but found \n[" + GosuStringUtil.join(i2.iterator(), ",") + "]", equals); } private static void assertIterableEquals(Iterable i1, Iterable i2, Comparator c, String s) { boolean equals = true; if (i1 == i2) return; Iterator e1 = i1.iterator(); Iterator e2 = i2.iterator(); while (e1.hasNext() && e2.hasNext()) { Object o1 = e1.next(); Object o2 = e2.next(); if (o1 == null) { if (o2 != null) { equals = false; break; } } else if (c.compare(o1, o2) != 0) { equals = false; break; } } if (equals) { equals = !(e1.hasNext() || e2.hasNext()); } assertTrue( s + " were not equal. Expected \n[" + GosuStringUtil.join(i1.iterator(), ",") + "] but found \n[" + GosuStringUtil.join(i2.iterator(), ",") + "]", equals); } private static List makeList(Iterable o1) { ArrayList lst = new ArrayList(); for (Object o : o1) { lst.add(o); } return lst; } public int getTotalNumTestMethods() { List<? extends IMethodInfo> methods = getType().getTypeInfo().getMethods(); int count = 0; for (IMethodInfo method : methods) { if (!method.isStatic() && method.getName().startsWith("test") && method.getParameters().length == 0) { count++; } } return count; } @Override public List<TestMetadata> getMetadata() { return _metadata; } protected void addMetadata(Collection<TestMetadata> metadata) { _metadata.addAll(metadata); for (TestMetadata testMetadata : metadata) { if (testMetadata.shouldNotRunTest()) { _doNotRun = true; } else if (testMetadata.getName().equals(KnownBreak.class.getName())) { _knownBreak = true; } } } public Collection<TestMetadata> createMethodMetadata( String method ) { if (getType() instanceof IJavaType) { // For Java types, we have to do things based on Method instead of MethodInfo, since Java TypeInfo isn't currently // reloadable, but the DCEVM means that Java classes themselves are try { Method testMethod = ((IJavaType) getType()).getBackingClass().getMethod(method); return createMetadata(testMethod.getAnnotations()); } catch (NoSuchMethodException e) { throw new IllegalStateException( "Method not found: " + getType().getDisplayName() + "." + method); } } else { IMethodInfo testMethod = getType().getTypeInfo().getMethod(method); if(testMethod == null) { throw new IllegalStateException( "Method not found: " + getType().getDisplayName() + "." + method); } return createMetadata(testMethod.getAnnotations()); } } public Collection<TestMetadata> createClassMetadata() { if (getType() instanceof IJavaType) { return createMetadata(((IJavaType) getType()).getBackingClass().getAnnotations()); } else { return createMetadata(getType().getTypeInfo().getAnnotations()); } } protected Collection<TestMetadata> createMetadata(List<IAnnotationInfo> annotationInfos) { Map<Class<? extends Annotation>, TestMetadata> map = new HashMap<Class<? extends Annotation>, TestMetadata>(); for (IAnnotationInfo ai : annotationInfos) { if (isMetaAnnotationInfo(ai)) { Annotation a = (Annotation) ai.getInstance(); map.put(a.annotationType(), new TestMetadata(a)); } } if (map.containsKey(KnownBreak.class)) { for (IAnnotationInfo ai : annotationInfos) { if (isKnownBreakQualifier(ai)) { Annotation a = (Annotation) ai.getInstance(); Predicate<? super Annotation> qualifierPredicate; try { qualifierPredicate = a.annotationType().getAnnotation(KnownBreakQualifier.class).value().newInstance(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } if (!qualifierPredicate.evaluate(a)) { map.remove(KnownBreak.class); break; } } } } return map.values(); } private boolean isKnownBreakQualifier(IAnnotationInfo ai) { boolean isQualifier = false; for (IAnnotationInfo a : ai.getType().getTypeInfo().getAnnotations()) { if (a.getName().equals(KnownBreakQualifier.class.getName())) { isQualifier = true; break; } } return isQualifier; } protected boolean isMetaAnnotationInfo(IAnnotationInfo ai) { boolean isMetadata = false; for (IAnnotationInfo a : ai.getType().getTypeInfo().getAnnotations()) { if (a.getName().equals(IncludeInTestResults.class.getName())) { isMetadata = true; break; } } return isMetadata; } protected Collection<TestMetadata> createMetadata(Annotation[] annotations) { Map<Class<? extends Annotation>, TestMetadata> map = new HashMap<Class<? extends Annotation>, TestMetadata>(); for (Annotation a : annotations) { if (isMetaAnnotation(a)) { map.put(a.annotationType(), new TestMetadata(a)); } } if (map.containsKey(KnownBreak.class)) { for (Annotation a : annotations) { if (isKnownBreakQualifier(a)) { Predicate<? super Annotation> qualifierPredicate; try { qualifierPredicate = a.annotationType().getAnnotation(KnownBreakQualifier.class).value().newInstance(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } if (!qualifierPredicate.evaluate(a)) { map.remove(KnownBreak.class); break; } } } } return map.values(); } private boolean isKnownBreakQualifier(Annotation a) { return a.annotationType().getAnnotation(KnownBreakQualifier.class) != null; } protected boolean isMetaAnnotation(Annotation ai) { boolean isMetadata = false; for (Annotation a : ai.annotationType().getAnnotations()) { if (a instanceof IncludeInTestResults) { isMetadata = true; break; } } return isMetadata; } public void initMetadata( String method ) { addMetadata( createClassMetadata() ); addMetadata( createMethodMetadata( method ) ); } public static void assertCausesException( Runnable r, Class<? extends Throwable> c ) { try { r.run(); } catch( Throwable t ) { if( c.isAssignableFrom( t.getClass() ) ) { return; } else { fail( "Expecting exception of type " + c + ", but got exception of type " + t.getClass() ); } } fail( "No exception was thrown when executing " + r + ". Expected exception of type " + c ); } public static TestClass createTestClass(IType testType) { IConstructorInfo noArgConstructor = testType.getTypeInfo().getConstructor(); if (noArgConstructor != null) { return (TestClass) noArgConstructor.getConstructor().newInstance(); } else { IConstructorInfo oneArgConstructor = testType.getTypeInfo().getConstructor(JavaTypes.STRING()); if (oneArgConstructor != null) { return (TestClass) oneArgConstructor.getConstructor().newInstance("temp"); } else { throw new IllegalArgumentException("Type " + testType.getName() + " does not have either a no-arg constructor or a one-arg constructor that takes a String"); } } } public static <T extends TestClass>junit.framework.Test _suite(Class<T> clazz) { return TestClassHelper.createTestSuite(clazz, TestSpec.extractTestMethods(clazz)); } }