aboutsummaryrefslogtreecommitdiff
path: root/android/guava-tests/test/com/google/common/collect/WriteReplaceOverridesTest.java
blob: bf10f5f75d14126b0bff00c8daa47a7b5b2fee7c (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
/*
 * Copyright (C) 2023 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.collect;

import static com.google.common.truth.Truth.assertWithMessage;
import static java.lang.reflect.Modifier.PRIVATE;
import static java.lang.reflect.Modifier.PROTECTED;
import static java.lang.reflect.Modifier.PUBLIC;
import static java.util.Arrays.asList;

import com.google.common.base.Optional;
import com.google.common.reflect.ClassPath;
import com.google.common.reflect.ClassPath.ClassInfo;
import com.google.common.reflect.TypeToken;
import java.lang.reflect.Method;
import junit.framework.TestCase;

/**
 * Tests that all package-private {@code writeReplace} methods are overridden in any existing
 * subclasses. Without such overrides, optimizers might put a {@code writeReplace}-containing class
 * and its subclass in different packages, causing the serialization system to fail to invoke {@code
 * writeReplace} when serializing an instance of the subclass. For an example of this problem, see
 * b/310253115.
 */
public class WriteReplaceOverridesTest extends TestCase {
  private static final ImmutableSet<String> GUAVA_PACKAGES =
      FluentIterable.of(
              "base",
              "cache",
              "collect",
              "escape",
              "eventbus",
              "graph",
              "hash",
              "html",
              "io",
              "math",
              "net",
              "primitives",
              "reflect",
              "util.concurrent",
              "xml")
          .transform("com.google.common."::concat)
          .toSet();

  public void testClassesHaveOverrides() throws Exception {
    for (ClassInfo info : ClassPath.from(getClass().getClassLoader()).getAllClasses()) {
      if (!GUAVA_PACKAGES.contains(info.getPackageName())) {
        continue;
      }
      if (info.getName().endsWith("GwtSerializationDependencies")) {
        continue; // These classes exist only for the GWT compiler, not to be used.
      }
      if (
      /*
       * At least one of the classes nested inside TypeResolverTest triggers a bug under older JDKs:
       * https://bugs.openjdk.org/browse/JDK-8215328 -> https://bugs.openjdk.org/browse/JDK-8215470
       * https://github.com/google/guava/blob/4f12c5891a7adedbaa1d99fc9f77d8cc4e9da206/guava-tests/test/com/google/common/reflect/TypeResolverTest.java#L201
       */
      info.getName().contains("TypeResolverTest")
          /*
           * And at least one of the classes inside TypeTokenTest ends up with a null value in
           * TypeMappingIntrospector.mappings. That happens only under older JDKs, too, so it may
           * well be a JDK bug.
           */
          || info.getName().contains("TypeTokenTest")
      /*
       * Luckily, we don't care about analyzing tests at all. We'd skip them all if we could do so
       * trivially, but it's enough to skip these ones.
       */
      ) {
        continue;
      }
      Class<?> clazz = info.load();
      try {
        Method unused = clazz.getDeclaredMethod("writeReplace");
        continue; // It overrides writeReplace, so it's safe.
      } catch (NoSuchMethodException e) {
        // This is a class whose supertypes we want to examine. We'll do that below.
      }
      Optional<Class<?>> supersWithPackagePrivateWriteReplace =
          FluentIterable.from(TypeToken.of(clazz).getTypes())
              .transform(TypeToken::getRawType)
              .transformAndConcat(c -> asList(c.getDeclaredMethods()))
              .firstMatch(
                  m ->
                      m.getName().equals("writeReplace")
                          && m.getParameterTypes().length == 0
                          // Only package-private methods are a problem.
                          && (m.getModifiers() & (PUBLIC | PROTECTED | PRIVATE)) == 0)
              .transform(Method::getDeclaringClass);
      if (!supersWithPackagePrivateWriteReplace.isPresent()) {
        continue;
      }
      assertWithMessage(
              "To help optimizers, any class that inherits a package-private writeReplace() method"
                  + " should override that method.\n"
                  + "(An override that delegates to the supermethod is fine.)\n"
                  + "%s has no such override despite inheriting writeReplace() from %s",
              clazz.getName(), supersWithPackagePrivateWriteReplace.get().getName())
          .fail();
    }
  }
}