View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.collections4;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertTrue;
22  
23  import java.io.ByteArrayInputStream;
24  import java.io.ByteArrayOutputStream;
25  import java.io.File;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.ObjectInputStream;
30  import java.io.ObjectOutputStream;
31  import java.io.OutputStream;
32  import java.io.Serializable;
33  import java.nio.file.Files;
34  import java.nio.file.Paths;
35  
36  import org.junit.jupiter.api.Test;
37  
38  /**
39   * Tests {@link Object}.
40   * <p>
41   * To use, simply extend this class, and implement
42   * the {@link #makeObject()} method.
43   * <p>
44   * If your {@link Object} fails one of these tests by design,
45   * you may still use this base set of cases.  Simply override the
46   * test case (method) your {@link Object} fails.
47   */
48  public abstract class AbstractObjectTest extends BulkTest {
49  
50      /** Current major release for Collections */
51      public static final int COLLECTIONS_MAJOR_VERSION = 4;
52  
53      protected String getCanonicalEmptyCollectionName(final Object object) {
54          final StringBuilder retval = new StringBuilder();
55          retval.append(TEST_DATA_PATH);
56          String colName = object.getClass().getName();
57          colName = colName.substring(colName.lastIndexOf(".") + 1);
58          retval.append(colName);
59          retval.append(".emptyCollection.version");
60          retval.append(getCompatibilityVersion());
61          retval.append(".obj");
62          return retval.toString();
63      }
64  
65      protected String getCanonicalFullCollectionName(final Object object) {
66          final StringBuilder retval = new StringBuilder();
67          retval.append(TEST_DATA_PATH);
68          String colName = object.getClass().getName();
69          colName = colName.substring(colName.lastIndexOf(".") + 1);
70          retval.append(colName);
71          retval.append(".fullCollection.version");
72          retval.append(getCompatibilityVersion());
73          retval.append(".obj");
74          return retval.toString();
75      }
76  
77      // protected implementation
78      /**
79       * Gets the version of Collections that this object tries to
80       * maintain serialization compatibility with. Defaults to 4, due to
81       * the package change to collections4 introduced in version 4.
82       *
83       * This constant makes it possible for TestMap (and other subclasses,
84       * if necessary) to automatically check SCM for a versionX copy of a
85       * Serialized object, so we can make sure that compatibility is maintained.
86       * See, for example, TestMap.getCanonicalFullMapName(Map map).
87       * Subclasses can override this variable, indicating compatibility
88       * with earlier Collections versions.
89       *
90       * @return The version, or {@code null} if this object shouldn't be
91       * tested for compatibility with previous versions.
92       */
93      public String getCompatibilityVersion() {
94          return "4";
95      }
96  
97      /**
98       * Returns true to indicate that the collection supports equals() comparisons.
99       * This implementation returns true;
100      */
101     public boolean isEqualsCheckable() {
102         return true;
103     }
104 
105     /**
106      * Is serialization testing supported.
107      * Default is true.
108      */
109     public boolean isTestSerialization() {
110         return true;
111     }
112 
113     /**
114      * Implement this method to return the object to test.
115      *
116      * @return the object to test
117      */
118     public abstract Object makeObject();
119 
120     /**
121      * Read a Serialized or Externalized Object from bytes.
122      * Useful for verifying serialization in memory.
123      *
124      * @param b byte array containing a serialized Object
125      * @return Object contained in the bytes
126      * @throws IOException
127      * @throws ClassNotFoundException
128      */
129     protected Object readExternalFormFromBytes(final byte[] b) throws IOException, ClassNotFoundException {
130         final ByteArrayInputStream stream = new ByteArrayInputStream(b);
131         return readExternalFormFromStream(stream);
132     }
133 
134     /**
135      * Reads a Serialized or Externalized Object from disk.
136      * Useful for creating compatibility tests between
137      * different SCM versions of the same class
138      *
139      * @param path path to the serialized Object
140      * @return the Object at the given path
141      * @throws IOException
142      * @throws ClassNotFoundException
143      */
144     protected Object readExternalFormFromDisk(final String path) throws IOException, ClassNotFoundException {
145         try (InputStream stream = Files.newInputStream(Paths.get(path))) {
146             return readExternalFormFromStream(stream);
147         }
148     }
149 
150     // private implementation
151     private Object readExternalFormFromStream(final InputStream stream) throws IOException, ClassNotFoundException {
152         final ObjectInputStream oStream = new ObjectInputStream(stream);
153         return oStream.readObject();
154     }
155 
156     protected Object serializeDeserialize(final Object obj) throws Exception {
157         final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
158         final ObjectOutputStream out = new ObjectOutputStream(buffer);
159         out.writeObject(obj);
160         out.close();
161 
162         final ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray()));
163         final Object dest = in.readObject();
164         in.close();
165 
166         return dest;
167     }
168 
169     protected boolean skipSerializedCanonicalTests() {
170         return Boolean.getBoolean("org.apache.commons.collections:with-clover");
171     }
172 
173     /**
174      * Override this method if a subclass is testing an object
175      * that cannot serialize an "empty" Collection.
176      * (for example Comparators have no contents)
177      *
178      * @return true
179      */
180     public boolean supportsEmptyCollections() {
181         return true;
182     }
183 
184     /**
185      * Override this method if a subclass is testing an object
186      * that cannot serialize a "full" Collection.
187      * (for example Comparators have no contents)
188      *
189      * @return true
190      */
191     public boolean supportsFullCollections() {
192         return true;
193     }
194 
195     /**
196      * Tests serialization by comparing against a previously stored version in SCM.
197      * If the test object is serializable, confirm that a canonical form exists.
198      */
199     @Test
200     public void testCanonicalEmptyCollectionExists() {
201         if (supportsEmptyCollections() && isTestSerialization() && !skipSerializedCanonicalTests()) {
202             final Object object = makeObject();
203             if (object instanceof Serializable) {
204                 final String name = getCanonicalEmptyCollectionName(object);
205                 assertTrue(
206                         new File(name).exists(),
207                         "Canonical empty collection (" + name + ") is not in SCM");
208             }
209         }
210     }
211 
212     /**
213      * Tests serialization by comparing against a previously stored version in SCM.
214      * If the test object is serializable, confirm that a canonical form exists.
215      */
216     @Test
217     public void testCanonicalFullCollectionExists() {
218         if (supportsFullCollections() && isTestSerialization() && !skipSerializedCanonicalTests()) {
219             final Object object = makeObject();
220             if (object instanceof Serializable) {
221                 final String name = getCanonicalFullCollectionName(object);
222                 assertTrue(
223                         new File(name).exists(),
224                         "Canonical full collection (" + name + ") is not in SCM");
225             }
226         }
227     }
228 
229     @Test
230     public void testEqualsNull() {
231         final Object obj = makeObject();
232         assertFalse(obj.equals(null)); // make sure this doesn't throw NPE either
233     }
234 
235     @Test
236     public void testObjectEqualsSelf() {
237         final Object obj = makeObject();
238         assertEquals(obj, obj, "A Object should equal itself");
239     }
240 
241     @Test
242     public void testObjectHashCodeEqualsContract() {
243         final Object obj1 = makeObject();
244         if (obj1.equals(obj1)) {
245             assertEquals(
246                     obj1.hashCode(), obj1.hashCode(),
247                     "[1] When two objects are equal, their hashCodes should be also.");
248         }
249         final Object obj2 = makeObject();
250         if (obj1.equals(obj2)) {
251             assertEquals(
252                     obj1.hashCode(), obj2.hashCode(),
253                     "[2] When two objects are equal, their hashCodes should be also.");
254             assertEquals(obj2, obj1, "When obj1.equals(obj2) is true, then obj2.equals(obj1) should also be true");
255         }
256     }
257 
258     @Test
259     public void testObjectHashCodeEqualsSelfHashCode() {
260         final Object obj = makeObject();
261         assertEquals(obj.hashCode(), obj.hashCode(), "hashCode should be repeatable");
262     }
263 
264     @Test
265     public void testSerializeDeserializeThenCompare() throws Exception {
266         final Object obj = makeObject();
267         if (obj instanceof Serializable && isTestSerialization()) {
268             final Object dest = serializeDeserialize(obj);
269             if (isEqualsCheckable()) {
270                 assertEquals(obj, dest, "obj != deserialize(serialize(obj))");
271             }
272         }
273     }
274 
275     /**
276      * Sanity check method, makes sure that any Serializable
277      * class can be serialized and de-serialized in memory,
278      * using the handy makeObject() method
279      *
280      * @throws IOException
281      * @throws ClassNotFoundException
282      */
283     @Test
284     public void testSimpleSerialization() throws Exception {
285         final Object o = makeObject();
286         if (o instanceof Serializable && isTestSerialization()) {
287             final byte[] object = writeExternalFormToBytes((Serializable) o);
288             readExternalFormFromBytes(object);
289         }
290     }
291 
292     /**
293      * Converts a Serializable or Externalizable object to
294      * bytes.  Useful for in-memory tests of serialization
295      *
296      * @param o Object to convert to bytes
297      * @return serialized form of the Object
298      * @throws IOException
299      */
300     protected byte[] writeExternalFormToBytes(final Serializable o) throws IOException {
301         final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
302         writeExternalFormToStream(o, byteStream);
303         return byteStream.toByteArray();
304     }
305 
306     /**
307      * Writes a Serializable or Externalizable object as
308      * a file at the given path.  NOT USEFUL as part
309      * of a unit test; this is just a utility method
310      * for creating disk-based objects in SCM that can become
311      * the basis for compatibility tests using
312      * readExternalFormFromDisk(String path)
313      *
314      * @param o Object to serialize
315      * @param path path to write the serialized Object
316      * @throws IOException
317      */
318     protected void writeExternalFormToDisk(final Serializable o, final String path) throws IOException {
319         try (FileOutputStream fileStream = new FileOutputStream(path)) {
320             writeExternalFormToStream(o, fileStream);
321         }
322     }
323 
324     private void writeExternalFormToStream(final Serializable o, final OutputStream stream) throws IOException {
325         final ObjectOutputStream oStream = new ObjectOutputStream(stream);
326         oStream.writeObject(o);
327     }
328 
329 }