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
18/**
19* @author Alexey V. Varlamov
20* @version $Revision$
21*/
22
23package org.apache.harmony.testframework.serialization;
24
25import dalvik.annotation.BrokenTest;
26import dalvik.annotation.TestLevel;
27import dalvik.annotation.TestTargetClass;
28import dalvik.annotation.TestTargetNew;
29
30import java.io.ByteArrayInputStream;
31import java.io.ByteArrayOutputStream;
32import java.io.File;
33import java.io.FileOutputStream;
34import java.io.IOException;
35import java.io.InputStream;
36import java.io.ObjectInputStream;
37import java.io.ObjectOutputStream;
38import java.io.OutputStream;
39import java.io.Serializable;
40import java.lang.reflect.Method;
41import java.security.Permission;
42import java.security.PermissionCollection;
43import java.util.Collection;
44import java.util.Collections;
45import java.util.HashSet;
46
47import junit.framework.Assert;
48import junit.framework.TestCase;
49
50/**
51 * Framework for serialization testing. Subclasses only need to override
52 * getData() method and, optionally, assertDeserialized() method. The first one
53 * returns array of objects to be de/serialized in tests, and the second
54 * compares reference and deserialized objects (needed only if tested objects do
55 * not provide specific method equals()). <br>
56 * There are two modes of test run: <b>reference generation mode </b> and
57 * <b>testing mode </b>. The actual mode is selected via
58 * <b>&quot;test.mode&quot; </b> system property. The <b>testing mode </b> is
59 * the default mode. <br>
60 * To turn on the <b>reference generation mode </b>, the test.mode property
61 * should be set to value &quot;serial.reference&quot;. In this mode, no testing
62 * is performed but golden files are produced, which contain reference
63 * serialized objects. This mode should be run on a pure
64 * Implementation classes, which are targeted for compartibility. <br>
65 * The location of golden files (in both modes) is controlled via
66 * <b>&quot;RESOURCE_DIR&quot; </b> system property.
67 *
68 */
69@TestTargetClass(Serializable.class)
70public abstract class SerializationTest extends TestCase {
71
72    /**
73     * Property name for the testing mode.
74     */
75    public static final String MODE_KEY = "test.mode";
76
77
78    /**
79     * Testing mode.
80     */
81    public static String mode = System.getProperty(MODE_KEY);
82
83    /**
84     * Reference files generation mode switch.
85     */
86    public static final String SERIAL_REFERENCE_MODE = "serial.reference";
87
88    /**
89     * Key to a system property defining root location of golden files.
90     */
91    public static final String GOLDEN_PATH = "RESOURCE_DIR";
92
93    private static final String outputPath = System.getProperty(GOLDEN_PATH,
94            "src/test/resources/serialization");
95
96    /**
97     * Parameterized c-tor inherited from superclass.
98     */
99    public SerializationTest(String name) {
100        super(name);
101    }
102
103    /**
104     * Default c-tor inherited from superclass.
105     */
106    public SerializationTest() {
107        super();
108    }
109
110    /**
111     * Depending on testing mode, produces golden files or performs testing.
112     */
113    @Override
114    public void runBare() throws Throwable {
115
116        if (mode != null && mode.equals(SERIAL_REFERENCE_MODE)) {
117            produceGoldenFiles();
118        } else {
119            super.runBare();
120        }
121    }
122
123    /**
124     * This is the main working method of this framework. Subclasses must
125     * override it to provide actual objects for testing.
126     *
127     * @return array of objects to be de/serialized in tests.
128     */
129    protected abstract Object[] getData();
130
131    /**
132     * Tests that data objects can be serialized and deserialized without
133     * exceptions, and that deserialization really produces deeply cloned
134     * objects.
135     */
136    @TestTargetNew(
137        level = TestLevel.ADDITIONAL,
138        notes = "",
139        method = "!Serialization",
140        args = {}
141    )
142    public void testSelf() throws Throwable {
143
144        if (this instanceof SerializableAssert) {
145            verifySelf(getData(), (SerializableAssert) this);
146        } else {
147            verifySelf(getData());
148
149        }
150    }
151
152    /**
153     * Tests that data objects can be deserialized from golden files, to verify
154     * compatibility with Reference Implementation.
155     */
156
157    @TestTargetNew(
158        level = TestLevel.ADDITIONAL,
159        notes = "",
160        method = "!Serialization",
161        args = {}
162    )
163    public void testGolden() throws Throwable {
164
165        verifyGolden(this, getData());
166    }
167
168    /**
169     * Returns golden file for an object being tested.
170     *
171     * @param index array index of tested data (as returned by
172     *        {@link #getData() getData()})
173     * @return corresponding golden file
174     */
175    protected File getDataFile(int index) {
176        String name = this.getClass().getName();
177        int dot = name.lastIndexOf(".");
178        String path = name.substring(0, dot).replace('.', File.separatorChar);
179        if (outputPath != null && outputPath.length() != 0) {
180            path = outputPath + File.separator + path;
181        }
182
183        return new File(path, name.substring(dot + 1) + "." + index + ".dat");
184    }
185
186    /**
187     * Working method for files generation mode. Serializes test objects
188     * returned by {@link #getData() getData()}to golden files, each object to
189     * a separate file.
190     *
191     * @throws IOException
192     */
193    protected void produceGoldenFiles() throws IOException {
194
195        String goldenPath = outputPath + File.separatorChar
196                + getClass().getName().replace('.', File.separatorChar)
197                + ".golden.";
198
199        Object[] data = getData();
200        for (int i = 0; i < data.length; i++) {
201
202            File goldenFile = new File(goldenPath + i + ".ser");
203            goldenFile.getParentFile().mkdirs();
204            goldenFile.createNewFile();
205
206            putObjectToStream(data[i], new FileOutputStream(goldenFile));
207        }
208    }
209
210    /**
211     * Serializes specified object to an output stream.
212     */
213    public static void putObjectToStream(Object obj, OutputStream os)
214        throws IOException {
215        ObjectOutputStream oos = new ObjectOutputStream(os);
216        oos.writeObject(obj);
217        oos.flush();
218        oos.close();
219    }
220
221    /**
222     * Deserializes single object from an input stream.
223     */
224    public static Serializable getObjectFromStream(InputStream is) throws IOException,
225        ClassNotFoundException {
226        ObjectInputStream ois = new ObjectInputStream(is);
227        Object result = ois.readObject();
228        ois.close();
229        return (Serializable)result;
230    }
231
232    /**
233     * Interface to compare (de)serialized objects
234     *
235     * Should be implemented if a class under test does not provide specific
236     * equals() method and it's instances should to be compared manually.
237     */
238    public interface SerializableAssert {
239
240        /**
241         * Compares deserialized and reference objects.
242         *
243         * @param initial -
244         *            initial object used for creating serialized form
245         * @param deserialized -
246         *            deserialized object
247         */
248        void assertDeserialized(Serializable initial, Serializable deserialized);
249    }
250
251    // default comparator for a class that has equals(Object) method
252    private final static SerializableAssert DEFAULT_COMPARATOR = new SerializableAssert() {
253        public void assertDeserialized(Serializable initial,
254                Serializable deserialized) {
255
256            Assert.assertEquals(initial, deserialized);
257        }
258    };
259
260    /**
261     * Comparator for verifying that deserialized object is the same as initial.
262     */
263    public final static SerializableAssert SAME_COMPARATOR = new SerializableAssert() {
264        public void assertDeserialized(Serializable initial,
265                Serializable deserialized) {
266
267            Assert.assertSame(initial, deserialized);
268        }
269    };
270
271    /**
272     * Comparator for java.lang.Throwable objects
273     */
274    public final static SerializableAssert THROWABLE_COMPARATOR = new SerializableAssert() {
275        public void assertDeserialized(Serializable initial, Serializable deserialized) {
276
277            Throwable initThr = (Throwable) initial;
278            Throwable dserThr = (Throwable) deserialized;
279
280            // verify class
281            Assert.assertEquals(initThr.getClass(), dserThr.getClass());
282
283            // verify message
284            Assert.assertEquals(initThr.getMessage(), dserThr.getMessage());
285
286            // verify cause
287            if (initThr.getCause() == null) {
288                Assert.assertNull(dserThr.getCause());
289            } else {
290                Assert.assertNotNull(dserThr.getCause());
291
292                THROWABLE_COMPARATOR.assertDeserialized(initThr.getCause(),
293                        dserThr.getCause());
294            }
295        }
296    };
297
298    /**
299     * Comparator for java.security.PermissionCollection objects
300     */
301    public final static SerializableAssert PERMISSION_COLLECTION_COMPARATOR = new SerializableAssert() {
302        public void assertDeserialized(Serializable initial, Serializable deserialized) {
303
304            PermissionCollection initPC = (PermissionCollection) initial;
305            PermissionCollection dserPC = (PermissionCollection) deserialized;
306
307            // verify class
308            Assert.assertEquals(initPC.getClass(), dserPC.getClass());
309
310            // verify 'readOnly' field
311            Assert.assertEquals(initPC.isReadOnly(), dserPC.isReadOnly());
312
313            // verify collection of permissions
314            Collection<Permission> refCollection = new HashSet<Permission>(
315                    Collections.list(initPC.elements()));
316            Collection<Permission> tstCollection = new HashSet<Permission>(
317                    Collections.list(dserPC.elements()));
318
319            Assert.assertEquals(refCollection, tstCollection);
320        }
321    };
322
323    /**
324     * Returns <code>comparator</code> for provided serializable
325     * <code>object</code>.
326     *
327     * The <code>comparator</code> is searched in the following order: <br>-
328     * if <code>test</code> implements SerializableAssert interface then it is
329     * selected as </code>comparator</code>.<br>- if passed <code>object</code>
330     * has class in its classes hierarchy that overrides <code>equals(Object)</code>
331     * method then <code>DEFAULT_COMPARATOR</code> is selected.<br> - the
332     * method tries to select one of known comparators basing on <code>object's</code>
333     * class,for example, if passed <code>object</code> is instance of
334     * java.lang.Throwable then <code>THROWABLE_COMPARATOR</code> is used.<br>-
335     * otherwise RuntimeException is thrown
336     *
337     * @param test -
338     *            test case
339     * @param object -
340     *            object to be compared
341     * @return object's comparator
342     */
343    public static SerializableAssert defineComparator(TestCase test,
344            Object object) throws Exception {
345
346        if (test instanceof SerializableAssert) {
347            return (SerializableAssert) test;
348        }
349
350        Method m = object.getClass().getMethod("equals",
351                new Class[] { Object.class });
352
353        if (m.getDeclaringClass() != Object.class) {
354            // one of classes overrides Object.equals(Object) method
355            // use default comparator
356            return DEFAULT_COMPARATOR;
357        }
358
359        // TODO use generics to detect comparator
360        // instead of 'instanceof' for the first element
361        if (object instanceof java.lang.Throwable) {
362            return THROWABLE_COMPARATOR;
363        } else if (object instanceof java.security.PermissionCollection) {
364            return PERMISSION_COLLECTION_COMPARATOR;
365        }
366
367        throw new RuntimeException("Failed to detect comparator");
368    }
369
370    /**
371     * Verifies that object deserialized from golden file correctly.
372     *
373     * The method invokes <br>
374     * verifyGolden(test, object, defineComparator(test, object));
375     *
376     * @param test -
377     *            test case
378     * @param object -
379     *            to be compared
380     */
381    public static void verifyGolden(TestCase test, Object object)
382            throws Exception {
383
384        verifyGolden(test, object, defineComparator(test, object));
385    }
386
387    /**
388     * Verifies that object deserialized from golden file correctly.
389     *
390     * The method loads "<code>testName</code>.golden.ser" resource file
391     * from "<module root>/src/test/resources/serialization/<code>testPackage</code>"
392     * folder, reads an object from the loaded file and compares it with
393     * <code>object</code> using specified <code>comparator</code>.
394     *
395     * @param test-
396     *            test case
397     * @param object-
398     *            to be compared
399     * @param comparator -
400     *            for comparing (de)serialized objects
401     */
402    public static void verifyGolden(TestCase test, Object object,
403            SerializableAssert comparator) throws Exception {
404
405        Assert.assertNotNull("Null comparator", comparator);
406
407        Serializable deserialized = getObject(test, ".golden.ser");
408
409        comparator.assertDeserialized((Serializable) object, deserialized);
410    }
411
412    /**
413     * Verifies that objects from array deserialized from golden files
414     * correctly.
415     *
416     * The method invokes <br>
417     * verifyGolden(test, objects, defineComparator(test, object[0]));
418     *
419     * @param test -
420     *            test case
421     * @param objects -
422     *            array of objects to be compared
423     */
424    public static void verifyGolden(TestCase test, Object[] objects)
425            throws Exception {
426
427        Assert.assertFalse("Empty array", objects.length == 0);
428        verifyGolden(test, objects, defineComparator(test, objects[0]));
429    }
430
431    /**
432     * Verifies that objects from array deserialized from golden files
433     * correctly.
434     *
435     * The method loads "<code>testName</code>.golden.<code>N</code>.ser"
436     * resource files from "<module root>/src/test/resources/serialization/<code>testPackage</code>"
437     * folder, from each loaded file it reads an object from and compares it
438     * with corresponding object in provided array (i.e. <code>objects[N]</code>)
439     * using specified <code>comparator</code>. (<code>N</code> is index
440     * in object's array.)
441     *
442     * @param test-
443     *            test case
444     * @param objects -
445     *            array of objects to be compared
446     * @param comparator -
447     *            for comparing (de)serialized objects
448     */
449    public static void verifyGolden(TestCase test, Object[] objects,
450            SerializableAssert comparator) throws Exception {
451
452        Assert.assertFalse("Empty array", objects.length == 0);
453        for (int i = 0; i < objects.length; i++) {
454            Serializable deserialized = getObject(test, ".golden." + i + ".ser");
455            comparator.assertDeserialized((Serializable) objects[i],
456                    deserialized);
457        }
458    }
459
460    /**
461     * Verifies that object can be smoothly serialized/deserialized.
462     *
463     * The method invokes <br>
464     * verifySelf(object, defineComparator(null, object));
465     *
466     * @param object -
467     *            to be serialized/deserialized
468     */
469    public static void verifySelf(Object object)
470            throws Exception {
471
472        verifySelf(object, defineComparator(null, object));
473    }
474
475    /**
476     * Verifies that object can be smoothly serialized/deserialized.
477     *
478     * The method serialize/deserialize <code>object</code> and compare it
479     * with initial <code>object</code>.
480     *
481     * @param object -
482     *            object to be serialized/deserialized
483     * @param comparator -
484     *            for comparing serialized/deserialized object with initial
485     *            object
486     */
487    public static void verifySelf(Object object, SerializableAssert comparator)
488            throws Exception {
489
490        Serializable initial = (Serializable) object;
491
492        comparator.assertDeserialized(initial, copySerializable(initial));
493    }
494
495    /**
496     * Verifies that that objects from array can be smoothly
497     * serialized/deserialized.
498     *
499     * The method invokes <br>
500     * verifySelf(objects, defineComparator(null, object[0]));
501     *
502     * @param objects -
503     *            array of objects to be serialized/deserialized
504     */
505    public static void verifySelf(Object[] objects)
506            throws Exception {
507
508        Assert.assertFalse("Empty array", objects.length == 0);
509        verifySelf(objects, defineComparator(null, objects[0]));
510    }
511
512    /**
513     * Verifies that that objects from array can be smoothly
514     * serialized/deserialized.
515     *
516     * The method serialize/deserialize each object in <code>objects</code>
517     * array and compare it with initial object.
518     *
519     * @param objects -
520     *            array of objects to be serialized/deserialized
521     * @param comparator -
522     *            for comparing serialized/deserialized object with initial
523     *            object
524     */
525    public static void verifySelf(Object[] objects, SerializableAssert comparator)
526            throws Exception {
527
528        Assert.assertFalse("Empty array", objects.length == 0);
529        for(Object entry: objects){
530            verifySelf(entry, comparator);
531        }
532    }
533
534    private static Serializable getObject(TestCase test, String toAppend)
535            throws Exception {
536
537        StringBuilder path = new StringBuilder("/serialization");
538
539        path.append(File.separatorChar);
540        path.append(test.getClass().getName().replace('.', File.separatorChar));
541        path.append(toAppend);
542
543        InputStream in = SerializationTest.class
544                .getResourceAsStream(path.toString());
545
546        Assert.assertNotNull("Failed to load serialization resource file: "
547                + path, in);
548
549        return getObjectFromStream(in);
550    }
551
552    /**
553     * Creates golden file.
554     *
555     * The folder for created file is: <code>root + test's package name</code>.
556     * The file name is: <code>test's name + "golden.ser"</code>
557     *
558     * @param root -
559     *            root directory for serialization resource files
560     * @param test -
561     *            test case
562     * @param object -
563     *            object to be serialized
564     * @throws IOException -
565     *             if I/O error
566     */
567    public static void createGoldenFile(String root, TestCase test,
568            Object object) throws IOException {
569
570        String goldenPath = test.getClass().getName().replace('.',
571                File.separatorChar)
572                + ".golden.ser";
573
574        if (root != null) {
575            goldenPath = root + File.separatorChar + goldenPath;
576        }
577
578
579        File goldenFile = new File(goldenPath);
580        goldenFile.getParentFile().mkdirs();
581        goldenFile.createNewFile();
582
583        putObjectToStream(object, new FileOutputStream(goldenFile));
584
585        // don't forget to remove it from test case after using
586        Assert.fail("Generating golden file.\nGolden file name:"
587                + goldenFile.getAbsolutePath());
588    }
589
590    /**
591     * Copies an object by serializing/deserializing it.
592     *
593     * @param initial -
594     *            an object to be copied
595     * @return copy of provided object
596     */
597    public static Serializable copySerializable(Serializable initial)
598            throws IOException, ClassNotFoundException {
599
600        ByteArrayOutputStream out = new ByteArrayOutputStream();
601        putObjectToStream(initial, out);
602        ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
603
604        return getObjectFromStream(in);
605    }
606}
607