aboutsummaryrefslogtreecommitdiffstats
path: root/guava-testlib/src/com/google/common/testing/AbstractPackageSanityTests.java
blob: 3e13fa0c6ba5f111bc952495fba5ed30e1107c42 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
/*
 * Copyright (C) 2012 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.testing;

import static com.google.common.base.Predicates.and;
import static com.google.common.base.Predicates.not;
import static com.google.common.testing.AbstractPackageSanityTests.Chopper.suffix;

import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.reflect.ClassPath;
import com.google.common.testing.NullPointerTester.Visibility;

import junit.framework.AssertionFailedError;
import junit.framework.TestCase;

import org.junit.Test;

import java.io.IOException;
import java.io.Serializable;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Automatically runs sanity checks against top level classes in the same package of the test that
 * extends {@code AbstractPackageSanityTests}. Currently sanity checks include {@link
 * NullPointerTester}, {@link EqualsTester} and {@link SerializableTester}. For example: <pre>
 * public class PackageSanityTests extends AbstractPackageSanityTests {}
 * </pre>
 *
 * <p>Note that only top-level classes with either a non-private constructor or a non-private static
 * factory method to construct instances can have their instance methods checked. For example: <pre>
 * public class Address {
 *   private final String city;
 *   private final String state;
 *   private final String zipcode;
 *
 *   public Address(String city, String state, String zipcode) {...}
 *
 *   {@literal @Override} public boolean equals(Object obj) {...}
 *   {@literal @Override} public int hashCode() {...}
 *   ...
 * }
 * </pre>
 * No cascading checks are performed against the return values of methods unless the method is a
 * static factory method. Neither are semantics of mutation methods such as {@code
 * someList.add(obj)} checked. For more detailed discussion of supported and unsupported cases, see
 * {@link #testEquals}, {@link #testNulls} and {@link #testSerializable}.
 *
 * <p>For testing against the returned instances from a static factory class, such as <pre>
 * interface Book {...}
 * public class Books {
 *   public static Book hardcover(String title) {...}
 *   public static Book paperback(String title) {...}
 * }
 * </pre>
 * please use {@link ClassSanityTester#forAllPublicStaticMethods}.
 *
 * <p>This class incurs IO because it scans the classpath and reads classpath resources.
 *
 * @author Ben Yu
 * @since 14.0
 */
@Beta
// TODO: Switch to JUnit 4 and use @Parameterized and @BeforeClass
public abstract class AbstractPackageSanityTests extends TestCase {

  /* The names of the expected method that tests null checks. */
  private static final ImmutableList<String> NULL_TEST_METHOD_NAMES = ImmutableList.of(
      "testNulls", "testNull",
      "testNullPointers", "testNullPointer",
      "testNullPointerExceptions", "testNullPointerException");

  /* The names of the expected method that tests serializable. */
  private static final ImmutableList<String> SERIALIZABLE_TEST_METHOD_NAMES = ImmutableList.of(
      "testSerializable", "testSerialization",
      "testEqualsAndSerializable", "testEqualsAndSerialization");

  /* The names of the expected method that tests equals. */
  private static final ImmutableList<String> EQUALS_TEST_METHOD_NAMES = ImmutableList.of(
      "testEquals", "testEqualsAndHashCode",
      "testEqualsAndSerializable", "testEqualsAndSerialization",
      "testEquality");

  private static final Chopper TEST_SUFFIX =
      suffix("Test")
          .or(suffix("Tests"))
          .or(suffix("TestCase"))
          .or(suffix("TestSuite"));

  private final Logger logger = Logger.getLogger(getClass().getName());
  private final ClassSanityTester tester = new ClassSanityTester();
  private Visibility visibility = Visibility.PACKAGE;
  private Predicate<Class<?>> classFilter = new Predicate<Class<?>>() {
    @Override public boolean apply(Class<?> cls) {
      return visibility.isVisible(cls.getModifiers());
    }
  };

  /**
   * Restricts the sanity tests for public API only. By default, package-private API are also
   * covered.
   */
  protected final void publicApiOnly() {
    visibility = Visibility.PUBLIC;
  }

  /**
   * Tests all top-level public {@link Serializable} classes in the package. For a serializable
   * Class {@code C}:
   * <ul>
   * <li>If {@code C} explicitly implements {@link Object#equals}, the deserialized instance will be
   *     checked to be equal to the instance before serialization.
   * <li>If {@code C} doesn't explicitly implement {@code equals} but instead inherits it from a
   *     superclass, no equality check is done on the deserialized instance because it's not clear
   *     whether the author intended for the class to be a value type.
   * <li>If a constructor or factory method takes a parameter whose type is interface, a dynamic
   *     proxy will be passed to the method. It's possible that the method body expects an instance
   *     method of the passed-in proxy to be of a certain value yet the proxy isn't aware of the
   *     assumption, in which case the equality check before and after serialization will fail.
   * <li>If the constructor or factory method takes a parameter that {@link
   *     AbstractPackageSanityTests} doesn't know how to construct, the test will fail.
   * <li>If there is no public constructor or public static factory method declared by {@code C},
   *     {@code C} is skipped for serialization test, even if it implements {@link Serializable}.
   * <li>Serialization test is not performed on method return values unless the method is a public
   *     static factory method whose return type is {@code C} or {@code C}'s subtype.
   * </ul>
   *
   * In all cases, if {@code C} needs custom logic for testing serialization, you can add an
   * explicit {@code testSerializable()} test in the corresponding {@code CTest} class, and {@code
   * C} will be excluded from automated serialization test performed by this method.
   */
  @Test
  public void testSerializable() throws Exception {
    // TODO: when we use @BeforeClass, we can pay the cost of class path scanning only once.
    for (Class<?> classToTest
        : findClassesToTest(loadClassesInPackage(), SERIALIZABLE_TEST_METHOD_NAMES)) {
      if (Serializable.class.isAssignableFrom(classToTest)) {
        try {
          Object instance = tester.instantiate(classToTest);
          if (instance != null) {
            if (isEqualsDefined(classToTest)) {
              SerializableTester.reserializeAndAssert(instance);
            } else {
              SerializableTester.reserialize(instance);
            }
          }
        } catch (Throwable e) {
          throw sanityError(classToTest, SERIALIZABLE_TEST_METHOD_NAMES, "serializable test", e);
        }
      }
    }
  }

  /**
   * Performs {@link NullPointerTester} checks for all top-level public classes in the package. For
   * a class {@code C}
   * <ul>
   * <li>All public static methods are checked such that passing null for any parameter that's not
   *     annotated with {@link javax.annotation.Nullable} should throw {@link NullPointerException}.
   * <li>If there is any public constructor or public static factory method declared by the class,
   *     all public instance methods will be checked too using the instance created by invoking the
   *     constructor or static factory method.
   * <li>If the constructor or factory method used to construct instance takes a parameter that
   *     {@link AbstractPackageSanityTests} doesn't know how to construct, the test will fail.
   * <li>If there is no public constructor or public static factory method declared by {@code C},
   *     instance methods are skipped for nulls test.
   * <li>Nulls test is not performed on method return values unless the method is a public static
   *     factory method whose return type is {@code C} or {@code C}'s subtype.
   * </ul>
   *
   * In all cases, if {@code C} needs custom logic for testing nulls, you can add an explicit {@code
   * testNulls()} test in the corresponding {@code CTest} class, and {@code C} will be excluded from
   * the automated null tests performed by this method.
   */
  @Test
  public void testNulls() throws Exception {
    for (Class<?> classToTest
        : findClassesToTest(loadClassesInPackage(), NULL_TEST_METHOD_NAMES)) {
      try {
        tester.doTestNulls(classToTest, visibility);
      } catch (Throwable e) {
        throw sanityError(classToTest, NULL_TEST_METHOD_NAMES, "nulls test", e);
      }
    }
  }

  /**
   * Tests {@code equals()} and {@code hashCode()} implementations for every top-level public class
   * in the package, that explicitly implements {@link Object#equals}. For a class {@code C}:
   * <ul>
   * <li>The public constructor or public static factory method with the most parameters is used to
   *     construct the sample instances. In case of tie, the candidate constructors or factories are
   *     tried one after another until one can be used to construct sample instances.
   * <li>For the constructor or static factory method used to construct instances, it's checked that
   *     when equal parameters are passed, the result instance should also be equal; and vice versa.
   * <li>Inequality check is not performed against state mutation methods such as {@link List#add},
   *     or functional update methods such as {@link com.google.common.base.Joiner#skipNulls}.
   * <li>If the constructor or factory method used to construct instance takes a parameter that
   *     {@link AbstractPackageSanityTests} doesn't know how to construct, the test will fail.
   * <li>If there is no public constructor or public static factory method declared by {@code C},
   *     {@code C} is skipped for equality test.
   * <li>Equality test is not performed on method return values unless the method is a public static
   *     factory method whose return type is {@code C} or {@code C}'s subtype.
   * </ul>
   *
   * In all cases, if {@code C} needs custom logic for testing {@code equals()}, you can add an
   * explicit {@code testEquals()} test in the corresponding {@code CTest} class, and {@code C} will
   * be excluded from the automated {@code equals} test performed by this method.
   */
  @Test
  public void testEquals() throws Exception {
    for (Class<?> classToTest
        : findClassesToTest(loadClassesInPackage(), EQUALS_TEST_METHOD_NAMES)) {
      if (!classToTest.isEnum() && isEqualsDefined(classToTest)) {
        try {
          tester.doTestEquals(classToTest);
        } catch (Throwable e) {
          throw sanityError(classToTest, EQUALS_TEST_METHOD_NAMES, "equals test", e);
        }
      }
    }
  }

  /**
   * Sets the default value for {@code type}, when dummy value for a parameter of the same type
   * needs to be created in order to invoke a method or constructor. The default value isn't used in
   * testing {@link Object#equals} because more than one sample instances are needed for testing
   * inequality.
   */
  protected final <T> void setDefault(Class<T> type, T value) {
    tester.setDefault(type, value);
  }

  /** Specifies that classes that satisfy the given predicate aren't tested for sanity. */
  protected final void ignoreClasses(Predicate<? super Class<?>> condition) {
    this.classFilter = and(this.classFilter, not(condition));
  }

  private static AssertionFailedError sanityError(
      Class<?> cls, List<String> explicitTestNames, String description, Throwable e) {
    String message = String.format(
        "Error in automated %s of %s\n"
            + "If the class is better tested explicitly, you can add %s() to %sTest",
        description, cls, explicitTestNames.get(0), cls.getName());
    AssertionFailedError error = new AssertionFailedError(message);
    error.initCause(e);
    return error;
  }

  /**
   * Finds the classes not ending with a test suffix and not covered by an explicit test
   * whose name is {@code explicitTestName}.
   */
  @VisibleForTesting List<Class<?>> findClassesToTest(
      Iterable<? extends Class<?>> classes, Iterable<String> explicitTestNames) {
    // "a.b.Foo" -> a.b.Foo.class
    TreeMap<String, Class<?>> classMap = Maps.newTreeMap();
    for (Class<?> cls : classes) {
      classMap.put(cls.getName(), cls);
    }
    // Foo.class -> [FooTest.class, FooTests.class, FooTestSuite.class, ...]
    Multimap<Class<?>, Class<?>> testClasses = HashMultimap.create();
    LinkedHashSet<Class<?>> candidateClasses = Sets.newLinkedHashSet();
    for (Class<?> cls : classes) {
      Optional<String> testedClassName = TEST_SUFFIX.chop(cls.getName());
      if (testedClassName.isPresent()) {
        Class<?> testedClass = classMap.get(testedClassName.get());
        if (testedClass != null) {
          testClasses.put(testedClass, cls);
        }
      } else {
        candidateClasses.add(cls);
      }
    }
    List<Class<?>> result = Lists.newArrayList();
    NEXT_CANDIDATE: for (Class<?> candidate : Iterables.filter(candidateClasses, classFilter)) {
      for (Class<?> testClass : testClasses.get(candidate)) {
        if (hasTest(testClass, explicitTestNames)) {
          // covered by explicit test
          continue NEXT_CANDIDATE;
        }
      }
      result.add(candidate);
    }
    return result;
  }

  private List<Class<?>> loadClassesInPackage() throws IOException {
    List<Class<?>> classes = Lists.newArrayList();
    String packageName = getClass().getPackage().getName();
    for (ClassPath.ClassInfo classInfo 
        : ClassPath.from(getClass().getClassLoader()).getTopLevelClasses(packageName)) {
      Class<?> cls;
      try {
        cls = classInfo.load();
      } catch (NoClassDefFoundError e) {
        // In case there were linking problems, this is probably not a class we care to test anyway.
        logger.log(Level.SEVERE, "Cannot load class " + classInfo + ", skipping...", e);
        continue;
      }
      if (!cls.isInterface()) {
        classes.add(cls);
      }
    }
    return classes;
  }

  private static boolean hasTest(Class<?> testClass, Iterable<String> testNames) {
    for (String testName : testNames) {
      try {
        testClass.getMethod(testName);
        return true;
      } catch (NoSuchMethodException e) {
        continue;
      }
    }
    return false;
  }

  private static boolean isEqualsDefined(Class<?> cls) {
    try {
      return !cls.getDeclaredMethod("equals", Object.class).isSynthetic();
    } catch (NoSuchMethodException e) {
      return false;
    }
  }

  static abstract class Chopper {

    final Chopper or(final Chopper you) {
      final Chopper i = this;
      return new Chopper() {
        @Override Optional<String> chop(String str) {
          return i.chop(str).or(you.chop(str));
        }
      };
    }

    abstract Optional<String> chop(String str);

    static Chopper suffix(final String suffix) {
      return new Chopper() {
        @Override Optional<String> chop(String str) {
          if (str.endsWith(suffix)) {
            return Optional.of(str.substring(0, str.length() - suffix.length()));
          } else {
            return Optional.absent();
          }
        }
      };
    }
  }
}