/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.avro;

import static org.apache.avro.SchemaCompatibility.checkReaderWriterCompatibility;
import static org.junit.Assert.assertEquals;

import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.avro.Schema.Field;
import org.apache.avro.SchemaCompatibility.SchemaCompatibilityType;
import org.apache.avro.SchemaCompatibility.SchemaPairCompatibility;
import org.apache.avro.generic.GenericData.EnumSymbol;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.io.DatumReader;
import org.apache.avro.io.DatumWriter;
import org.apache.avro.io.Decoder;
import org.apache.avro.io.DecoderFactory;
import org.apache.avro.io.Encoder;
import org.apache.avro.io.EncoderFactory;
import org.apache.avro.util.Utf8;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Unit-tests for SchemaCompatibility. */
public class TestSchemaCompatibility {
  private static final Logger LOG = LoggerFactory.getLogger(TestSchemaCompatibility.class);

  // -----------------------------------------------------------------------------------------------

  private static final Schema NULL_SCHEMA = Schema.create(Schema.Type.NULL);
  private static final Schema BOOLEAN_SCHEMA = Schema.create(Schema.Type.BOOLEAN);
  private static final Schema INT_SCHEMA = Schema.create(Schema.Type.INT);
  private static final Schema LONG_SCHEMA = Schema.create(Schema.Type.LONG);
  private static final Schema FLOAT_SCHEMA = Schema.create(Schema.Type.FLOAT);
  private static final Schema DOUBLE_SCHEMA = Schema.create(Schema.Type.DOUBLE);
  private static final Schema STRING_SCHEMA = Schema.create(Schema.Type.STRING);
  private static final Schema BYTES_SCHEMA = Schema.create(Schema.Type.BYTES);

  private static final Schema INT_ARRAY_SCHEMA = Schema.createArray(INT_SCHEMA);
  private static final Schema LONG_ARRAY_SCHEMA = Schema.createArray(LONG_SCHEMA);
  private static final Schema STRING_ARRAY_SCHEMA = Schema.createArray(STRING_SCHEMA);

  private static final Schema INT_MAP_SCHEMA = Schema.createMap(INT_SCHEMA);
  private static final Schema LONG_MAP_SCHEMA = Schema.createMap(LONG_SCHEMA);
  private static final Schema STRING_MAP_SCHEMA = Schema.createMap(STRING_SCHEMA);

  private static final Schema ENUM1_AB_SCHEMA =
      Schema.createEnum("Enum1", null, null, list("A", "B"));
  private static final Schema ENUM1_ABC_SCHEMA =
      Schema.createEnum("Enum1", null, null, list("A", "B", "C"));
  private static final Schema ENUM1_BC_SCHEMA =
      Schema.createEnum("Enum1", null, null, list("B", "C"));
  private static final Schema ENUM2_AB_SCHEMA =
      Schema.createEnum("Enum2", null, null, list("A", "B"));

  private static final Schema EMPTY_UNION_SCHEMA =
      Schema.createUnion(new ArrayList<Schema>());
  private static final Schema NULL_UNION_SCHEMA =
      Schema.createUnion(list(NULL_SCHEMA));
  private static final Schema INT_UNION_SCHEMA =
      Schema.createUnion(list(INT_SCHEMA));
  private static final Schema LONG_UNION_SCHEMA =
      Schema.createUnion(list(LONG_SCHEMA));
  private static final Schema STRING_UNION_SCHEMA =
      Schema.createUnion(list(STRING_SCHEMA));
  private static final Schema INT_STRING_UNION_SCHEMA =
      Schema.createUnion(list(INT_SCHEMA, STRING_SCHEMA));
  private static final Schema STRING_INT_UNION_SCHEMA =
      Schema.createUnion(list(STRING_SCHEMA, INT_SCHEMA));

  // Non recursive records:
  private static final Schema EMPTY_RECORD1 =
      Schema.createRecord("Record1", null, null, false);
  private static final Schema EMPTY_RECORD2 =
      Schema.createRecord("Record2", null, null, false);
  private static final Schema A_INT_RECORD1 =
      Schema.createRecord("Record1", null, null, false);
  private static final Schema A_LONG_RECORD1 =
      Schema.createRecord("Record1", null, null, false);
  private static final Schema A_INT_B_INT_RECORD1 =
      Schema.createRecord("Record1", null, null, false);
  private static final Schema A_DINT_RECORD1 =  // DTYPE means TYPE with default value
      Schema.createRecord("Record1", null, null, false);
  private static final Schema A_INT_B_DINT_RECORD1 =
      Schema.createRecord("Record1", null, null, false);
  private static final Schema A_DINT_B_DINT_RECORD1 =
      Schema.createRecord("Record1", null, null, false);
  static {
    EMPTY_RECORD1.setFields(Collections.<Field>emptyList());
    EMPTY_RECORD2.setFields(Collections.<Field>emptyList());
    A_INT_RECORD1.setFields(list(
        new Field("a", INT_SCHEMA, null, null)));
    A_LONG_RECORD1.setFields(list(
        new Field("a", LONG_SCHEMA, null, null)));
    A_INT_B_INT_RECORD1.setFields(list(
        new Field("a", INT_SCHEMA, null, null),
        new Field("b", INT_SCHEMA, null, null)));
    A_DINT_RECORD1.setFields(list(
        new Field("a", INT_SCHEMA, null, 0)));
    A_INT_B_DINT_RECORD1.setFields(list(
        new Field("a", INT_SCHEMA, null, null),
        new Field("b", INT_SCHEMA, null, 0)));
    A_DINT_B_DINT_RECORD1.setFields(list(
        new Field("a", INT_SCHEMA, null, 0),
        new Field("b", INT_SCHEMA, null, 0)));
  }

  // Recursive records
  private static final Schema INT_LIST_RECORD =
      Schema.createRecord("List", null, null, false);
  private static final Schema LONG_LIST_RECORD =
      Schema.createRecord("List", null, null, false);
  static {
    INT_LIST_RECORD.setFields(list(
        new Field("head", INT_SCHEMA, null, null),
        new Field("tail", INT_LIST_RECORD, null, null)));
    LONG_LIST_RECORD.setFields(list(
        new Field("head", LONG_SCHEMA, null, null),
        new Field("tail", LONG_LIST_RECORD, null, null)));
  }

  // -----------------------------------------------------------------------------------------------

  /** Reader/writer schema pair. */
  private static final class ReaderWriter {
    private final Schema mReader;
    private final Schema mWriter;

    public ReaderWriter(final Schema reader, final Schema writer) {
      mReader = reader;
      mWriter = writer;
    }

    public Schema getReader() {
      return mReader;
    }

    public Schema getWriter() {
      return mWriter;
    }
  }

  // -----------------------------------------------------------------------------------------------

  private static final Schema WRITER_SCHEMA = Schema.createRecord(list(
      new Schema.Field("oldfield1", INT_SCHEMA, null, null),
      new Schema.Field("oldfield2", STRING_SCHEMA, null, null)));

  @Test
  public void testValidateSchemaPairMissingField() throws Exception {
    final List<Schema.Field> readerFields = list(
        new Schema.Field("oldfield1", INT_SCHEMA, null, null));
    final Schema reader = Schema.createRecord(readerFields);
    final SchemaCompatibility.SchemaPairCompatibility expectedResult =
        new SchemaCompatibility.SchemaPairCompatibility(
            SchemaCompatibility.SchemaCompatibilityType.COMPATIBLE,
            reader,
            WRITER_SCHEMA,
            SchemaCompatibility.READER_WRITER_COMPATIBLE_MESSAGE);

    // Test omitting a field.
    assertEquals(expectedResult, checkReaderWriterCompatibility(reader, WRITER_SCHEMA));
  }

  @Test
  public void testValidateSchemaPairMissingSecondField() throws Exception {
    final List<Schema.Field> readerFields = list(
        new Schema.Field("oldfield2", STRING_SCHEMA, null, null));
    final Schema reader = Schema.createRecord(readerFields);
    final SchemaCompatibility.SchemaPairCompatibility expectedResult =
        new SchemaCompatibility.SchemaPairCompatibility(
            SchemaCompatibility.SchemaCompatibilityType.COMPATIBLE,
            reader,
            WRITER_SCHEMA,
            SchemaCompatibility.READER_WRITER_COMPATIBLE_MESSAGE);

    // Test omitting other field.
    assertEquals(expectedResult, checkReaderWriterCompatibility(reader, WRITER_SCHEMA));
  }

  @Test
  public void testValidateSchemaPairAllFields() throws Exception {
    final List<Schema.Field> readerFields = list(
        new Schema.Field("oldfield1", INT_SCHEMA, null, null),
        new Schema.Field("oldfield2", STRING_SCHEMA, null, null));
    final Schema reader = Schema.createRecord(readerFields);
    final SchemaCompatibility.SchemaPairCompatibility expectedResult =
        new SchemaCompatibility.SchemaPairCompatibility(
            SchemaCompatibility.SchemaCompatibilityType.COMPATIBLE,
            reader,
            WRITER_SCHEMA,
            SchemaCompatibility.READER_WRITER_COMPATIBLE_MESSAGE);

    // Test with all fields.
    assertEquals(expectedResult, checkReaderWriterCompatibility(reader, WRITER_SCHEMA));
  }

  @Test
  public void testValidateSchemaNewFieldWithDefault() throws Exception {
    final List<Schema.Field> readerFields = list(
        new Schema.Field("oldfield1", INT_SCHEMA, null, null),
        new Schema.Field("newfield1", INT_SCHEMA, null, 42));
    final Schema reader = Schema.createRecord(readerFields);
    final SchemaCompatibility.SchemaPairCompatibility expectedResult =
        new SchemaCompatibility.SchemaPairCompatibility(
            SchemaCompatibility.SchemaCompatibilityType.COMPATIBLE,
            reader,
            WRITER_SCHEMA,
            SchemaCompatibility.READER_WRITER_COMPATIBLE_MESSAGE);

    // Test new field with default value.
    assertEquals(expectedResult, checkReaderWriterCompatibility(reader, WRITER_SCHEMA));
  }

  @Test
  public void testValidateSchemaNewField() throws Exception {
    final List<Schema.Field> readerFields = list(
        new Schema.Field("oldfield1", INT_SCHEMA, null, null),
        new Schema.Field("newfield1", INT_SCHEMA, null, null));
    final Schema reader = Schema.createRecord(readerFields);
    final SchemaCompatibility.SchemaPairCompatibility expectedResult =
        new SchemaCompatibility.SchemaPairCompatibility(
            SchemaCompatibility.SchemaCompatibilityType.INCOMPATIBLE,
            reader,
            WRITER_SCHEMA,
            String.format(
                "Data encoded using writer schema:%n%s%n"
                + "will or may fail to decode using reader schema:%n%s%n",
                WRITER_SCHEMA.toString(true),
                reader.toString(true)));

    // Test new field without default value.
    assertEquals(expectedResult, checkReaderWriterCompatibility(reader, WRITER_SCHEMA));
  }

  @Test
  public void testValidateArrayWriterSchema() throws Exception {
    final Schema validReader = Schema.createArray(STRING_SCHEMA);
    final Schema invalidReader = Schema.createMap(STRING_SCHEMA);
    final SchemaCompatibility.SchemaPairCompatibility validResult =
        new SchemaCompatibility.SchemaPairCompatibility(
            SchemaCompatibility.SchemaCompatibilityType.COMPATIBLE,
            validReader,
            STRING_ARRAY_SCHEMA,
            SchemaCompatibility.READER_WRITER_COMPATIBLE_MESSAGE);
    final SchemaCompatibility.SchemaPairCompatibility invalidResult =
        new SchemaCompatibility.SchemaPairCompatibility(
            SchemaCompatibility.SchemaCompatibilityType.INCOMPATIBLE,
            invalidReader,
            STRING_ARRAY_SCHEMA,
            String.format(
                "Data encoded using writer schema:%n%s%n"
                + "will or may fail to decode using reader schema:%n%s%n",
                STRING_ARRAY_SCHEMA.toString(true),
                invalidReader.toString(true)));

    assertEquals(
        validResult,
        checkReaderWriterCompatibility(validReader, STRING_ARRAY_SCHEMA));
    assertEquals(
        invalidResult,
        checkReaderWriterCompatibility(invalidReader, STRING_ARRAY_SCHEMA));
  }

  @Test
  public void testValidatePrimitiveWriterSchema() throws Exception {
    final Schema validReader = Schema.create(Schema.Type.STRING);
    final SchemaCompatibility.SchemaPairCompatibility validResult =
        new SchemaCompatibility.SchemaPairCompatibility(
            SchemaCompatibility.SchemaCompatibilityType.COMPATIBLE,
            validReader,
            STRING_SCHEMA,
            SchemaCompatibility.READER_WRITER_COMPATIBLE_MESSAGE);
    final SchemaCompatibility.SchemaPairCompatibility invalidResult =
        new SchemaCompatibility.SchemaPairCompatibility(
            SchemaCompatibility.SchemaCompatibilityType.INCOMPATIBLE,
            INT_SCHEMA,
            STRING_SCHEMA,
            String.format(
                "Data encoded using writer schema:%n%s%n"
                + "will or may fail to decode using reader schema:%n%s%n",
                STRING_SCHEMA.toString(true),
                INT_SCHEMA.toString(true)));

    assertEquals(
        validResult,
        checkReaderWriterCompatibility(validReader, STRING_SCHEMA));
    assertEquals(
        invalidResult,
        checkReaderWriterCompatibility(INT_SCHEMA, STRING_SCHEMA));
  }

  /** Reader union schema must contain all writer union branches. */
  @Test
  public void testUnionReaderWriterSubsetIncompatibility() {
    final Schema unionWriter = Schema.createUnion(list(INT_SCHEMA, STRING_SCHEMA));
    final Schema unionReader = Schema.createUnion(list(STRING_SCHEMA));
    final SchemaPairCompatibility result =
        checkReaderWriterCompatibility(unionReader, unionWriter);
    assertEquals(SchemaCompatibilityType.INCOMPATIBLE, result.getType());
  }

  // -----------------------------------------------------------------------------------------------

  /** Collection of reader/writer schema pair that are compatible. */
  public static final List<ReaderWriter> COMPATIBLE_READER_WRITER_TEST_CASES = list(
      new ReaderWriter(BOOLEAN_SCHEMA, BOOLEAN_SCHEMA),

      new ReaderWriter(INT_SCHEMA, INT_SCHEMA),

      new ReaderWriter(LONG_SCHEMA, INT_SCHEMA),
      new ReaderWriter(LONG_SCHEMA, LONG_SCHEMA),

      // Avro spec says INT/LONG can be promoted to FLOAT/DOUBLE.
      // This is arguable as this causes a loss of precision.
      new ReaderWriter(FLOAT_SCHEMA, INT_SCHEMA),
      new ReaderWriter(FLOAT_SCHEMA, LONG_SCHEMA),
      new ReaderWriter(DOUBLE_SCHEMA, LONG_SCHEMA),

      new ReaderWriter(DOUBLE_SCHEMA, INT_SCHEMA),
      new ReaderWriter(DOUBLE_SCHEMA, FLOAT_SCHEMA),

      new ReaderWriter(STRING_SCHEMA, STRING_SCHEMA),

      new ReaderWriter(BYTES_SCHEMA, BYTES_SCHEMA),

      new ReaderWriter(INT_ARRAY_SCHEMA, INT_ARRAY_SCHEMA),
      new ReaderWriter(LONG_ARRAY_SCHEMA, INT_ARRAY_SCHEMA),
      new ReaderWriter(INT_MAP_SCHEMA, INT_MAP_SCHEMA),
      new ReaderWriter(LONG_MAP_SCHEMA, INT_MAP_SCHEMA),

      new ReaderWriter(ENUM1_AB_SCHEMA, ENUM1_AB_SCHEMA),
      new ReaderWriter(ENUM1_ABC_SCHEMA, ENUM1_AB_SCHEMA),
      
      // String-to/from-bytes, introduced in Avro 1.7.7
      new ReaderWriter(STRING_SCHEMA, BYTES_SCHEMA),
      new ReaderWriter(BYTES_SCHEMA, STRING_SCHEMA),

      // Tests involving unions:
      new ReaderWriter(EMPTY_UNION_SCHEMA, EMPTY_UNION_SCHEMA),
      new ReaderWriter(INT_UNION_SCHEMA, INT_UNION_SCHEMA),
      new ReaderWriter(INT_STRING_UNION_SCHEMA, STRING_INT_UNION_SCHEMA),
      new ReaderWriter(INT_UNION_SCHEMA, EMPTY_UNION_SCHEMA),
      new ReaderWriter(LONG_UNION_SCHEMA, INT_UNION_SCHEMA),

      // Special case of singleton unions:
      new ReaderWriter(INT_UNION_SCHEMA, INT_SCHEMA),
      new ReaderWriter(INT_SCHEMA, INT_UNION_SCHEMA),

      // Tests involving records:
      new ReaderWriter(EMPTY_RECORD1, EMPTY_RECORD1),
      new ReaderWriter(EMPTY_RECORD1, A_INT_RECORD1),

      new ReaderWriter(A_INT_RECORD1, A_INT_RECORD1),
      new ReaderWriter(A_DINT_RECORD1, A_INT_RECORD1),
      new ReaderWriter(A_DINT_RECORD1, A_DINT_RECORD1),
      new ReaderWriter(A_INT_RECORD1, A_DINT_RECORD1),

      new ReaderWriter(A_LONG_RECORD1, A_INT_RECORD1),

      new ReaderWriter(A_INT_RECORD1, A_INT_B_INT_RECORD1),
      new ReaderWriter(A_DINT_RECORD1, A_INT_B_INT_RECORD1),

      new ReaderWriter(A_INT_B_DINT_RECORD1, A_INT_RECORD1),
      new ReaderWriter(A_DINT_B_DINT_RECORD1, EMPTY_RECORD1),
      new ReaderWriter(A_DINT_B_DINT_RECORD1, A_INT_RECORD1),
      new ReaderWriter(A_INT_B_INT_RECORD1, A_DINT_B_DINT_RECORD1),

      new ReaderWriter(INT_LIST_RECORD, INT_LIST_RECORD),
      new ReaderWriter(LONG_LIST_RECORD, LONG_LIST_RECORD),
      new ReaderWriter(LONG_LIST_RECORD, INT_LIST_RECORD),

      new ReaderWriter(NULL_SCHEMA, NULL_SCHEMA)
  );

  // -----------------------------------------------------------------------------------------------

  /** Collection of reader/writer schema pair that are incompatible. */
  public static final List<ReaderWriter> INCOMPATIBLE_READER_WRITER_TEST_CASES = list(
      new ReaderWriter(NULL_SCHEMA, INT_SCHEMA),
      new ReaderWriter(NULL_SCHEMA, LONG_SCHEMA),

      new ReaderWriter(BOOLEAN_SCHEMA, INT_SCHEMA),

      new ReaderWriter(INT_SCHEMA, NULL_SCHEMA),
      new ReaderWriter(INT_SCHEMA, BOOLEAN_SCHEMA),
      new ReaderWriter(INT_SCHEMA, LONG_SCHEMA),
      new ReaderWriter(INT_SCHEMA, FLOAT_SCHEMA),
      new ReaderWriter(INT_SCHEMA, DOUBLE_SCHEMA),

      new ReaderWriter(LONG_SCHEMA, FLOAT_SCHEMA),
      new ReaderWriter(LONG_SCHEMA, DOUBLE_SCHEMA),

      new ReaderWriter(FLOAT_SCHEMA, DOUBLE_SCHEMA),

      new ReaderWriter(STRING_SCHEMA, BOOLEAN_SCHEMA),
      new ReaderWriter(STRING_SCHEMA, INT_SCHEMA),

      new ReaderWriter(BYTES_SCHEMA, NULL_SCHEMA),
      new ReaderWriter(BYTES_SCHEMA, INT_SCHEMA),

      new ReaderWriter(INT_ARRAY_SCHEMA, LONG_ARRAY_SCHEMA),
      new ReaderWriter(INT_MAP_SCHEMA, INT_ARRAY_SCHEMA),
      new ReaderWriter(INT_ARRAY_SCHEMA, INT_MAP_SCHEMA),
      new ReaderWriter(INT_MAP_SCHEMA, LONG_MAP_SCHEMA),

      new ReaderWriter(ENUM1_AB_SCHEMA, ENUM1_ABC_SCHEMA),
      new ReaderWriter(ENUM1_BC_SCHEMA, ENUM1_ABC_SCHEMA),

      new ReaderWriter(ENUM1_AB_SCHEMA, ENUM2_AB_SCHEMA),
      new ReaderWriter(INT_SCHEMA, ENUM2_AB_SCHEMA),
      new ReaderWriter(ENUM2_AB_SCHEMA, INT_SCHEMA),

      // Tests involving unions:
      new ReaderWriter(INT_UNION_SCHEMA, INT_STRING_UNION_SCHEMA),
      new ReaderWriter(STRING_UNION_SCHEMA, INT_STRING_UNION_SCHEMA),

      new ReaderWriter(EMPTY_RECORD2, EMPTY_RECORD1),
      new ReaderWriter(A_INT_RECORD1, EMPTY_RECORD1),
      new ReaderWriter(A_INT_B_DINT_RECORD1, EMPTY_RECORD1),

      new ReaderWriter(INT_LIST_RECORD, LONG_LIST_RECORD),

      // Last check:
      new ReaderWriter(NULL_SCHEMA, INT_SCHEMA)
  );

  // -----------------------------------------------------------------------------------------------

  /** Tests reader/writer compatibility validation. */
  @Test
  public void testReaderWriterCompatibility() {
    for (ReaderWriter readerWriter : COMPATIBLE_READER_WRITER_TEST_CASES) {
      final Schema reader = readerWriter.getReader();
      final Schema writer = readerWriter.getWriter();
      LOG.debug("Testing compatibility of reader {} with writer {}.", reader, writer);
      final SchemaPairCompatibility result =
          checkReaderWriterCompatibility(reader, writer);
      assertEquals(String.format(
          "Expecting reader %s to be compatible with writer %s, but tested incompatible.",
          reader, writer),
          SchemaCompatibilityType.COMPATIBLE, result.getType());
    }
  }

  /** Tests the reader/writer incompatibility validation. */
  @Test
  public void testReaderWriterIncompatibility() {
    for (ReaderWriter readerWriter : INCOMPATIBLE_READER_WRITER_TEST_CASES) {
      final Schema reader = readerWriter.getReader();
      final Schema writer = readerWriter.getWriter();
      LOG.debug("Testing incompatibility of reader {} with writer {}.", reader, writer);
      final SchemaPairCompatibility result =
          checkReaderWriterCompatibility(reader, writer);
      assertEquals(String.format(
          "Expecting reader %s to be incompatible with writer %s, but tested compatible.",
          reader, writer),
          SchemaCompatibilityType.INCOMPATIBLE, result.getType());
    }
  }

  // -----------------------------------------------------------------------------------------------

  /**
   * Descriptor for a test case that encodes a datum according to a given writer schema,
   * then decodes it according to reader schema and validates the decoded value.
   */
  private static final class DecodingTestCase {
    /** Writer schema used to encode the datum. */
    private final Schema mWriterSchema;

    /** Datum to encode according to the specified writer schema. */
    private final Object mDatum;

    /** Reader schema used to decode the datum encoded using the writer schema. */
    private final Schema mReaderSchema;

    /** Expected datum value when using the reader schema to decode from the writer schema. */
    private final Object mDecodedDatum;

    public DecodingTestCase(
        final Schema writerSchema,
        final Object datum,
        final Schema readerSchema,
        final Object decoded) {
      mWriterSchema = writerSchema;
      mDatum = datum;
      mReaderSchema = readerSchema;
      mDecodedDatum = decoded;
    }

    public Schema getReaderSchema() {
      return mReaderSchema;
    }

    public Schema getWriterSchema() {
      return mWriterSchema;
    }

    public Object getDatum() {
      return mDatum;
    }

    public Object getDecodedDatum() {
      return mDecodedDatum;
    }
  }

  // -----------------------------------------------------------------------------------------------

  public static final List<DecodingTestCase> DECODING_COMPATIBILITY_TEST_CASES = list(
      new DecodingTestCase(INT_SCHEMA, 1, INT_SCHEMA, 1),
      new DecodingTestCase(INT_SCHEMA, 1, LONG_SCHEMA, 1L),
      new DecodingTestCase(INT_SCHEMA, 1, FLOAT_SCHEMA, 1.0f),
      new DecodingTestCase(INT_SCHEMA, 1, DOUBLE_SCHEMA, 1.0d),

      // This is currently accepted but causes a precision loss:
      // IEEE 754 floats have 24 bits signed mantissa
      new DecodingTestCase(INT_SCHEMA, (1 << 24) + 1, FLOAT_SCHEMA, (float) ((1 << 24) + 1)),

      // new DecodingTestCase(LONG_SCHEMA, 1L, INT_SCHEMA, 1),  // should work in best-effort!

      new DecodingTestCase(
          ENUM1_AB_SCHEMA, new EnumSymbol(ENUM1_AB_SCHEMA, "A"),
          ENUM1_ABC_SCHEMA, new EnumSymbol(ENUM1_ABC_SCHEMA, "A")),

      new DecodingTestCase(
          ENUM1_ABC_SCHEMA, new EnumSymbol(ENUM1_ABC_SCHEMA, "A"),
          ENUM1_AB_SCHEMA, new EnumSymbol(ENUM1_AB_SCHEMA, "A")),

      new DecodingTestCase(
          ENUM1_ABC_SCHEMA, new EnumSymbol(ENUM1_ABC_SCHEMA, "B"),
          ENUM1_BC_SCHEMA, new EnumSymbol(ENUM1_BC_SCHEMA, "B")),

      new DecodingTestCase(
          INT_STRING_UNION_SCHEMA, "the string",
          STRING_SCHEMA, new Utf8("the string")),

      new DecodingTestCase(
          INT_STRING_UNION_SCHEMA, "the string",
          STRING_UNION_SCHEMA, new Utf8("the string"))
);

  /** Tests the reader/writer compatibility at decoding time. */
  @Test
  public void testReaderWriterDecodingCompatibility() throws Exception {
    for (DecodingTestCase testCase : DECODING_COMPATIBILITY_TEST_CASES) {
      final Schema readerSchema = testCase.getReaderSchema();
      final Schema writerSchema = testCase.getWriterSchema();
      final Object datum = testCase.getDatum();
      final Object expectedDecodedDatum = testCase.getDecodedDatum();

      LOG.debug(
          "Testing incompatibility of reader {} with writer {}.",
          readerSchema, writerSchema);

      LOG.debug("Encode datum {} with writer {}.", datum, writerSchema);
      final ByteArrayOutputStream baos = new ByteArrayOutputStream();
      final Encoder encoder = EncoderFactory.get().binaryEncoder(baos, null);
      final DatumWriter<Object> datumWriter = new GenericDatumWriter<Object>(writerSchema);
      datumWriter.write(datum, encoder);
      encoder.flush();

      LOG.debug(
          "Decode datum {} whose writer is {} with reader {}.",
          new Object[]{datum, writerSchema, readerSchema});
      final byte[] bytes = baos.toByteArray();
      final Decoder decoder = DecoderFactory.get().resolvingDecoder(
          writerSchema, readerSchema,
          DecoderFactory.get().binaryDecoder(bytes, null));
      final DatumReader<Object> datumReader = new GenericDatumReader<Object>(readerSchema);
      final Object decodedDatum = datumReader.read(null, decoder);

      assertEquals(String.format(
          "Expecting decoded value %s when decoding value %s whose writer schema is %s "
          + "using reader schema %s, but value was %s.",
          expectedDecodedDatum, datum, writerSchema, readerSchema, decodedDatum),
          expectedDecodedDatum, decodedDatum);
    }
  }

  /** Borrowed from the Guava library. */
  private static <E> ArrayList<E> list(E... elements) {
    final ArrayList<E> list = new ArrayList<E>();
    Collections.addAll(list, elements);
    return list;
  }

}
