From 9b47f2a2f4c2997ae51a573e60485b8d8c0775d2 Mon Sep 17 00:00:00 2001
From: Sadayuki Furuhashi <frsyuki@gmail.com>
Date: Wed, 9 Aug 2017 22:40:34 -0700
Subject: [PATCH 01/21] Add add support for Timestamp type

---
 .../core/MessageExtensionFormatException.java |  38 +++
 .../java/org/msgpack/core/MessagePack.java    |   2 +
 .../java/org/msgpack/core/MessagePacker.java  | 118 +++++++++
 .../org/msgpack/core/MessageUnpacker.java     |  86 ++++++-
 .../value/ImmutableTimestampValue.java        |  26 ++
 .../org/msgpack/value/TimestampValue.java     |  38 +++
 .../main/java/org/msgpack/value/Value.java    |   1 +
 .../java/org/msgpack/value/ValueFactory.java  |  11 +
 .../impl/ImmutableTimestampValueImpl.java     | 226 ++++++++++++++++++
 9 files changed, 544 insertions(+), 2 deletions(-)
 create mode 100644 msgpack-core/src/main/java/org/msgpack/core/MessageExtensionFormatException.java
 create mode 100644 msgpack-core/src/main/java/org/msgpack/value/ImmutableTimestampValue.java
 create mode 100644 msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java
 create mode 100644 msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java

diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessageExtensionFormatException.java b/msgpack-core/src/main/java/org/msgpack/core/MessageExtensionFormatException.java
new file mode 100644
index 000000000..cf9e55ee4
--- /dev/null
+++ b/msgpack-core/src/main/java/org/msgpack/core/MessageExtensionFormatException.java
@@ -0,0 +1,38 @@
+//
+// MessagePack for Java
+//
+//    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 org.msgpack.core;
+
+/**
+ * Thrown when format of an extension type is invalid
+ */
+public class MessageExtensionFormatException
+        extends MessageFormatException
+{
+    public MessageExtensionFormatException(Throwable e)
+    {
+        super(e);
+    }
+
+    public MessageExtensionFormatException(String message)
+    {
+        super(message);
+    }
+
+    public MessageExtensionFormatException(String message, Throwable cause)
+    {
+        super(message, cause);
+    }
+}
diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessagePack.java b/msgpack-core/src/main/java/org/msgpack/core/MessagePack.java
index ed8b1e405..edd449b34 100644
--- a/msgpack-core/src/main/java/org/msgpack/core/MessagePack.java
+++ b/msgpack-core/src/main/java/org/msgpack/core/MessagePack.java
@@ -165,6 +165,8 @@ public static final boolean isFixedRaw(byte b)
         public static final byte MAP32 = (byte) 0xdf;
 
         public static final byte NEGFIXINT_PREFIX = (byte) 0xe0;
+
+        public static final byte EXT_TIMESTAMP = (byte) -1;
     }
 
     private MessagePack()
diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java
index 6837f9f72..3ee99a573 100644
--- a/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java
+++ b/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java
@@ -32,6 +32,8 @@
 import java.nio.charset.CharsetEncoder;
 import java.nio.charset.CoderResult;
 import java.nio.charset.CodingErrorAction;
+import java.time.Instant;
+import java.util.Date;
 
 import static org.msgpack.core.MessagePack.Code.ARRAY16;
 import static org.msgpack.core.MessagePack.Code.ARRAY32;
@@ -41,6 +43,7 @@
 import static org.msgpack.core.MessagePack.Code.EXT16;
 import static org.msgpack.core.MessagePack.Code.EXT32;
 import static org.msgpack.core.MessagePack.Code.EXT8;
+import static org.msgpack.core.MessagePack.Code.EXT_TIMESTAMP;
 import static org.msgpack.core.MessagePack.Code.FALSE;
 import static org.msgpack.core.MessagePack.Code.FIXARRAY_PREFIX;
 import static org.msgpack.core.MessagePack.Code.FIXEXT1;
@@ -798,6 +801,121 @@ else if (s.length() < (1 << 16)) {
         return this;
     }
 
+    /**
+     * Writes a Timestamp value.
+     *
+     * <p>
+     * This method writes a timestamp value using timestamp format family.
+     *
+     * @param date the timestamp to be written
+     * @return this
+     * @throws IOException when underlying output throws IOException
+     */
+    public MessagePacker packTimestamp(Date date)
+            throws IOException
+    {
+        long epochMilli = date.getTime();
+        long sec = Math.floorDiv(epochMilli, 1000L);
+        int nsec = ((int) (epochMilli - sec * 1000L)) * 1000;  // 0 <= nsec < 1,000,000,000 < 2^30
+        return packTimestampImpl(sec, nsec);
+    }
+
+    /**
+     * Writes a Timestamp value.
+     *
+     * <p>
+     * This method writes a timestamp value using timestamp format family.
+     *
+     * @param instant the timestamp to be written
+     * @return this
+     * @throws IOException when underlying output throws IOException
+     */
+    public MessagePacker packTimestamp(Instant instant)
+            throws IOException
+    {
+        return packTimestampImpl(instant.getEpochSecond(), instant.getNano());
+    }
+
+    /**
+     * Writes a Timestamp value.
+     *
+     * <p>
+     * This method writes a timestamp value using timestamp format family.
+     *
+     * @param epochSecond the number of seconds from 1970-01-01T00:00:00Z
+     * @param nanoAdjustment the nanosecond adjustment to the number of seconds, positive or negative
+     * @return this
+     * @throws IOException when underlying output throws IOException
+     * @throws ArithmeticException when epochSecond plus nanoAdjustment in seconds exceeds the range of long
+     */
+    public MessagePacker packTimestampEpochSecond(long epochSecond, int nanoAdjustment)
+            throws IOException, ArithmeticException
+    {
+        long sec = Math.addExact(epochSecond, Math.floorDiv(nanoAdjustment, 1000000000L));
+        int nsec = (int) Math.floorMod(nanoAdjustment, 1000000000L);
+        return packTimestampImpl(sec, nsec);
+    }
+
+    private MessagePacker packTimestampImpl(long sec, int nsec)
+            throws IOException
+    {
+        if (sec >>> 34 == 0) {
+            // sec can be serialized in 34 bits.
+            long data64 = (nsec << 34) | sec;
+            if ((data64 & 0xffffffff00000000L) == 0L) {
+                // sec can be serialized in 32 bits and nsec is 0.
+                // use timestamp 32
+                writeTimestamp32((int) sec);
+            }
+            else {
+                // sec exceeded 32 bits or nsec is not 0.
+                // use timestamp 64
+                writeTimestamp64(data64);
+            }
+        }
+        else {
+            // use timestamp 96 format
+            writeTimestamp96(sec, nsec);
+        }
+        return this;
+    }
+
+    private void writeTimestamp32(int sec)
+            throws IOException
+    {
+        // timestamp 32 in fixext 4
+        ensureCapacity(6);
+        buffer.putByte(position++, FIXEXT4);
+        buffer.putByte(position++, EXT_TIMESTAMP);
+        buffer.putInt(position, sec);
+        position += 4;
+    }
+
+    private void writeTimestamp64(long data64)
+            throws IOException
+    {
+        // timestamp 64 in fixext 8
+        ensureCapacity(10);
+        buffer.putByte(position++, FIXEXT8);
+        buffer.putByte(position++, EXT_TIMESTAMP);
+        buffer.putLong(position, data64);
+        position += 8;
+    }
+
+    private void writeTimestamp96(long sec, int nsec)
+            throws IOException
+    {
+        // timestamp 96 in ext 8
+        ensureCapacity(15);
+        buffer.putByte(position++, EXT8);
+        buffer.putByte(position++, (byte) 12);  // length of nsec and sec
+        buffer.putByte(position++, EXT_TIMESTAMP);
+        buffer.putInt(position, nsec);
+        position += 4;
+        buffer.putLong(position, sec);
+        position += 8;
+    }
+
     /**
      * Writes header of an Array value.
      * <p>
diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
index 6d0d57ab3..937d9682c 100644
--- a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
+++ b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
@@ -32,7 +32,14 @@
 import java.nio.charset.CharsetDecoder;
 import java.nio.charset.CoderResult;
 import java.nio.charset.CodingErrorAction;
-
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.msgpack.core.MessagePack.Code.EXT_TIMESTAMP;
 import static org.msgpack.core.Preconditions.checkNotNull;
 
 /**
@@ -595,6 +602,12 @@ private static MessagePackException unexpected(String expected, byte b)
         }
     }
 
+    private static MessagePackException unexpectedExtension(String expected, int expectedType, int actualType)
+    {
+        return new MessageTypeException(String.format("Expected extension type %s (%d), but got extension type %d",
+                    expected, expectedType, actualType));
+    }
+
     public ImmutableValue unpackValue()
             throws IOException
     {
@@ -643,7 +656,12 @@ public ImmutableValue unpackValue()
             }
             case EXTENSION: {
                 ExtensionTypeHeader extHeader = unpackExtensionTypeHeader();
-                return ValueFactory.newExtension(extHeader.getType(), readPayload(extHeader.getLength()));
+                switch (extHeader.getType()) {
+                case EXT_TIMESTAMP:
+                    return ValueFactory.newTimestamp(readPayload(extHeader.getLength()));
+                default:
+                    return ValueFactory.newExtension(extHeader.getType(), readPayload(extHeader.getLength()));
+                }
             }
             default:
                 throw new MessageNeverUsedFormatException("Unknown value type");
@@ -1257,6 +1275,70 @@ private String decodeStringFastPath(int length)
         }
     }
 
+    public Instant unpackInstant()
+            throws IOException
+    {
+        ExtensionTypeHeader ext = unpackExtensionTypeHeader();
+        if (ext.getType() != EXT_TIMESTAMP) {
+            throw unexpectedExtension("Timestamp", EXT_TIMESTAMP, ext.getType());
+        }
+        switch (ext.getLength()) {
+            case 4: {
+                int u32 = readInt();
+                return Instant.ofEpochSecond(u32);
+            }
+            case 8: {
+                long data64 = readLong();
+                int nsec = (int) (data64 >>> 34);
+                long sec = data64 & 0x00000003ffffffffL;
+                return Instant.ofEpochSecond(sec, nsec);
+            }
+            case 12: {
+                int nsec = readInt();
+                long sec = readLong();
+                return Instant.ofEpochSecond(sec, nsec);
+            }
+            default:
+                throw new MessageExtensionFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes",
+                            EXT_TIMESTAMP, ext.getLength()));
+        }
+    }
+
+    public Date unpackDate()
+            throws IOException
+    {
+        return new Date(unpackTimestampMillis());
+    }
+
+    public long unpackTimestampMillis()
+            throws IOException
+    {
+        ExtensionTypeHeader ext = unpackExtensionTypeHeader();
+        if (ext.getType() != EXT_TIMESTAMP) {
+            throw unexpectedExtension("Timestamp", EXT_TIMESTAMP, ext.getType());
+        }
+        switch (ext.getLength()) {
+            case 4: {
+                int u32 = readInt();
+                return (u32 & 0xffffffffL) * 1000L;
+            }
+            case 8: {
+                long data64 = readLong();
+                int nsec = (int) (data64 >>> 34);
+                long sec = data64 & 0x00000003ffffffffL;
+                return sec * 1000L + nsec / 1000L;
+            }
+            case 12: {
+                int nsec = readInt();
+                long sec = readLong();
+                return sec * 1000L + nsec / 1000L;
+            }
+            default:
+                throw new MessageExtensionFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes",
+                            EXT_TIMESTAMP, ext.getLength()));
+        }
+    }
+
     /**
      * Reads header of an array.
      *
diff --git a/msgpack-core/src/main/java/org/msgpack/value/ImmutableTimestampValue.java b/msgpack-core/src/main/java/org/msgpack/value/ImmutableTimestampValue.java
new file mode 100644
index 000000000..bd4a901bb
--- /dev/null
+++ b/msgpack-core/src/main/java/org/msgpack/value/ImmutableTimestampValue.java
@@ -0,0 +1,26 @@
+//
+// MessagePack for Java
+//
+//    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 org.msgpack.value;
+
+/**
+ * Immutable representation of MessagePack's Timestamp type.
+ *
+ * @see org.msgpack.value.TimestampValue
+ */
+public interface ImmutableTimestampValue
+        extends TimestampValue, ImmutableValue
+{
+}
diff --git a/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java b/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java
new file mode 100644
index 000000000..caae015a1
--- /dev/null
+++ b/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java
@@ -0,0 +1,38 @@
+//
+// MessagePack for Java
+//
+//    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 org.msgpack.value;
+
+import java.time.Instant;
+import java.util.Date;
+
+/**
+ * Representation of MessagePack's Timestamp type.
+ *
+ * MessagePack's Timestamp type can represent a timestamp.
+ */
+public interface TimestampValue
+        extends ExtensionValue
+{
+    long getEpochSecond();
+
+    int getNano();
+
+    long toEpochMilli();
+
+    Instant toInstant();
+
+    Date toDate();
+}
diff --git a/msgpack-core/src/main/java/org/msgpack/value/Value.java b/msgpack-core/src/main/java/org/msgpack/value/Value.java
index 546dfbbf4..a95fb8d3e 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/Value.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/Value.java
@@ -16,6 +16,7 @@
 package org.msgpack.value;
 
 import org.msgpack.core.MessagePacker;
+import org.msgpack.core.MessageTypeCastException;
 
 import java.io.IOException;
 
diff --git a/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java b/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java
index 21a4f85dd..8ee733c02 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java
@@ -25,6 +25,7 @@
 import org.msgpack.value.impl.ImmutableMapValueImpl;
 import org.msgpack.value.impl.ImmutableNilValueImpl;
 import org.msgpack.value.impl.ImmutableStringValueImpl;
+import org.msgpack.value.impl.ImmutableTimestampValueImpl;
 
 import java.math.BigInteger;
 import java.util.AbstractMap;
@@ -295,4 +296,14 @@ public static ImmutableExtensionValue newExtension(byte type, byte[] data)
     {
         return new ImmutableExtensionValueImpl(type, data);
     }
+
+    public static ImmutableTimestampValue newTimestamp(byte[] data)
+    {
+        return new ImmutableTimestampValueImpl(data);
+    }
+
+    public static ImmutableTimestampValue newTimestamp(long epochSecond, int nanoAdjustment)
+    {
+        return new ImmutableTimestampValueImpl(epochSecond, nanoAdjustment);
+    }
 }
diff --git a/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java b/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java
new file mode 100644
index 000000000..082dd37b4
--- /dev/null
+++ b/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java
@@ -0,0 +1,226 @@
+//
+// MessagePack for Java
+//
+//    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 org.msgpack.value.impl;
+
+import org.msgpack.core.MessageExtensionFormatException;
+import org.msgpack.core.MessagePacker;
+import org.msgpack.core.buffer.MessageBuffer;
+import org.msgpack.value.ExtensionValue;
+import org.msgpack.value.ImmutableExtensionValue;
+import org.msgpack.value.ImmutableTimestampValue;
+import org.msgpack.value.TimestampValue;
+import org.msgpack.value.Value;
+import org.msgpack.value.ValueType;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Date;
+
+import static org.msgpack.core.MessagePack.Code.EXT_TIMESTAMP;
+
+/**
+ * {@code ImmutableTimestampValueImpl} Implements {@code ImmutableTimestampValue} using a {@code byte} and a {@code byte[]} fields.
+ *
+ * @see TimestampValue
+ */
+public class ImmutableTimestampValueImpl
+        extends AbstractImmutableValue
+        implements ImmutableExtensionValue, ImmutableTimestampValue
+{
+    private final long sec;
+
+    private final int nsec;
+
+    private byte[] data;
+
+    public ImmutableTimestampValueImpl(long epochSecond, int nanoAdjustment)
+    {
+        this.sec = Math.addExact(epochSecond, Math.floorDiv(nanoAdjustment, 1000000000L));
+        this.nsec = (int) Math.floorMod(nanoAdjustment, 1000000000L);
+        this.data = null;
+    }
+
+    public ImmutableTimestampValueImpl(byte[] data)
+    {
+        // See MessageUnpacker.unpackInstant
+        this.data = data;
+        switch (data.length) {
+            case 4: {
+                this.sec = MessageBuffer.wrap(data).getInt(0);
+                this.nsec = 0;
+                break;
+            }
+            case 8: {
+                long data64 = MessageBuffer.wrap(data).getLong(0);
+                this.nsec = (int) (data64 >>> 34);
+                this.sec = data64 & 0x00000003ffffffffL;
+                break;
+            }
+            case 12: {
+                MessageBuffer buffer = MessageBuffer.wrap(data);
+                this.nsec = buffer.getInt(0);
+                this.sec = buffer.getLong(4);
+                break;
+            }
+            default:
+                throw new MessageExtensionFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes",
+                            EXT_TIMESTAMP, data.length));
+        }
+    }
+
+    @Override
+    public byte getType()
+    {
+        return EXT_TIMESTAMP;
+    }
+
+    @Override
+    public ValueType getValueType()
+    {
+        // Note: Future version should return ValueType.TIMESTAMP instead.
+        return ValueType.EXTENSION;
+    }
+
+    @Override
+    public ImmutableTimestampValue immutableValue()
+    {
+        return this;
+    }
+
+    @Override
+    public ImmutableExtensionValue asExtensionValue()
+    {
+        return this;
+    }
+
+    @Override
+    public byte[] getData()
+    {
+        if (data == null) {
+            // See MessagePacker.packTimestampImpl
+            byte[] bytes;
+            if (sec >>> 34 == 0) {
+                long data64 = (nsec << 34) | sec;
+                if ((data64 & 0xffffffff00000000L) == 0L) {
+                    bytes = new byte[4];
+                    MessageBuffer.wrap(bytes).putInt(0, (int) sec);
+                }
+                else {
+                    bytes = new byte[8];
+                    MessageBuffer.wrap(bytes).putLong(0, data64);
+                }
+            }
+            else {
+                bytes = new byte[12];
+                MessageBuffer buffer = MessageBuffer.wrap(bytes);
+                buffer.putInt(0, nsec);
+                buffer.putLong(4, sec);
+            }
+            data = bytes;
+        }
+        return data;
+    }
+
+    @Override
+    public long getEpochSecond()
+    {
+        return sec;
+    }
+
+    @Override
+    public int getNano()
+    {
+        return nsec;
+    }
+
+    @Override
+    public long toEpochMilli()
+    {
+        return Math.addExact(Math.multiplyExact(getEpochSecond(), 1000L), getNano() / 1000000L);
+    }
+
+    @Override
+    public Instant toInstant()
+    {
+        return Instant.ofEpochSecond(getEpochSecond(), getNano());
+    }
+
+    @Override
+    public Date toDate()
+    {
+        return new Date(toEpochMilli());
+    }
+
+    @Override
+    public void writeTo(MessagePacker packer)
+            throws IOException
+    {
+        packer.packTimestampEpochSecond(sec, nsec);
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        // Implements same behavior with ImmutableExtensionValueImpl.
+        if (o == this) {
+            return true;
+        }
+        if (!(o instanceof Value)) {
+            return false;
+        }
+        Value v = (Value) o;
+
+        if (!v.isExtensionValue()) {
+            return false;
+        }
+        ExtensionValue ev = v.asExtensionValue();
+
+        // Here should use isTimestampValue and asTimestampValue instead. However, because
+        // adding these methods to Value interface can't keep backward compatibility without
+        // using "default" keyword since Java 7, here uses instanceof of and cast instead.
+        if (ev instanceof TimestampValue) {
+            TimestampValue tv = (TimestampValue) ev;
+            return getEpochSecond() == tv.getEpochSecond() && getNano() == tv.getNano();
+        }
+        else {
+            return EXT_TIMESTAMP == ev.getType() && Arrays.equals(getData(), ev.getData());
+        }
+    }
+
+    @Override
+    public int hashCode()
+    {
+        // Implements same behavior with ImmutableExtensionValueImpl.
+        int hash = 31 + EXT_TIMESTAMP;
+        for (byte e : getData()) {
+            hash = 31 * hash + e;
+        }
+        return hash;
+    }
+
+    @Override
+    public String toJson()
+    {
+        return "\"" + toInstant().toString() + "\"";
+    }
+
+    @Override
+    public String toString()
+    {
+        return toInstant().toString();
+    }
+}

From 8a9d84e218cc75a01e3d63605165161b1754a9cf Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 16:04:24 -0700
Subject: [PATCH 02/21] Add timestamp tests

---
 .../core/MessageExtensionFormatException.java | 38 -------------------
 .../org/msgpack/core/MessageUnpacker.java     | 17 ++-------
 .../org/msgpack/core/MessagePackTest.scala    | 27 ++++++++++++-
 3 files changed, 29 insertions(+), 53 deletions(-)
 delete mode 100644 msgpack-core/src/main/java/org/msgpack/core/MessageExtensionFormatException.java

diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessageExtensionFormatException.java b/msgpack-core/src/main/java/org/msgpack/core/MessageExtensionFormatException.java
deleted file mode 100644
index cf9e55ee4..000000000
--- a/msgpack-core/src/main/java/org/msgpack/core/MessageExtensionFormatException.java
+++ /dev/null
@@ -1,38 +0,0 @@
-//
-// MessagePack for Java
-//
-//    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 org.msgpack.core;
-
-/**
- * Thrown when format of an extension type is invalid
- */
-public class MessageExtensionFormatException
-        extends MessageFormatException
-{
-    public MessageExtensionFormatException(Throwable e)
-    {
-        super(e);
-    }
-
-    public MessageExtensionFormatException(String message)
-    {
-        super(message);
-    }
-
-    public MessageExtensionFormatException(String message, Throwable cause)
-    {
-        super(message, cause);
-    }
-}
diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
index 937d9682c..a0f26735f 100644
--- a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
+++ b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
@@ -33,11 +33,6 @@
 import java.nio.charset.CoderResult;
 import java.nio.charset.CodingErrorAction;
 import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 
 import static org.msgpack.core.MessagePack.Code.EXT_TIMESTAMP;
 import static org.msgpack.core.Preconditions.checkNotNull;
@@ -1275,7 +1270,7 @@ private String decodeStringFastPath(int length)
         }
     }
 
-    public Instant unpackInstant()
+    public Instant unpackTimestamp()
             throws IOException
     {
         ExtensionTypeHeader ext = unpackExtensionTypeHeader();
@@ -1299,17 +1294,11 @@ public Instant unpackInstant()
                 return Instant.ofEpochSecond(sec, nsec);
             }
             default:
-                throw new MessageExtensionFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes",
+                throw new MessageFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes",
                             EXT_TIMESTAMP, ext.getLength()));
         }
     }
 
-    public Date unpackDate()
-            throws IOException
-    {
-        return new Date(unpackTimestampMillis());
-    }
-
     public long unpackTimestampMillis()
             throws IOException
     {
@@ -1334,7 +1323,7 @@ public long unpackTimestampMillis()
                 return sec * 1000L + nsec / 1000L;
             }
             default:
-                throw new MessageExtensionFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes",
+                throw new MessageFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes",
                             EXT_TIMESTAMP, ext.getLength()));
         }
     }
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
index d2a209bb0..f6d81ff4f 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
@@ -22,9 +22,10 @@ import java.nio.charset.{CodingErrorAction, UnmappableCharacterException}
 import org.msgpack.core.MessagePack.Code
 import org.msgpack.core.MessagePack.{PackerConfig, UnpackerConfig}
 import org.msgpack.value.{Value, Variable}
-import org.scalacheck.Arbitrary
+import org.scalacheck.{Arbitrary, Gen}
 import org.scalacheck.Prop.{forAll, propBoolean}
 
+import java.time.Instant
 import scala.util.Random
 
 /**
@@ -583,6 +584,30 @@ class MessagePackTest extends MessagePackSpec {
       )
     }
 
+    "pack/unpack timestamp values" in {
+      val posLong = Gen.chooseNum[Long](-31557014167219200L, 31556889864403199L)
+      val posInt  = Gen.chooseNum(0, 1000000000 - 1) // NANOS_PER_SECOND
+      forAll(posLong, posInt) { (second: Long, nano: Int) =>
+        val v = Instant.ofEpochSecond(second, nano)
+        check(v, { _.packTimestamp(v) }, { _.unpackTimestamp() })
+      }
+      val secLessThan34bits = Gen.chooseNum[Long](0, 1L << 34)
+      forAll(secLessThan34bits, posInt) { (second: Long, nano: Int) =>
+        val v = Instant.ofEpochSecond(second, nano)
+        check(v, _.packTimestamp(v), _.unpackTimestamp())
+      }
+
+      // Corner cases for u
+      // sign uint32 nanoseq (out of int32 range)
+      for (v <- Seq(
+             Instant.ofEpochSecond(Instant.now().getEpochSecond, 123456789L),
+             Instant.ofEpochSecond(-1302749144L, 0), // 1928-09-19T21:14:16Z
+             Instant.ofEpochSecond(-747359729L, 0), // 1946-04-27T00:04:31Z
+             Instant.ofEpochSecond(4257387427L, 0) // 2104-11-29T07:37:07Z
+           )) {
+        check(v, _.packTimestamp(v), _.unpackTimestamp())
+      }
+    }
   }
 
   "MessagePack.PackerConfig" should {

From ad6fb2de9433c585cd6c9e998a19b08503c68726 Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 16:12:33 -0700
Subject: [PATCH 03/21] Remove unpackTimestampMillis

---
 .../org/msgpack/core/MessageUnpacker.java     | 35 ++-----------------
 1 file changed, 3 insertions(+), 32 deletions(-)

diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
index a0f26735f..05f87692e 100644
--- a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
+++ b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
@@ -1279,7 +1279,7 @@ public Instant unpackTimestamp()
         }
         switch (ext.getLength()) {
             case 4: {
-                int u32 = readInt();
+                long u32 = readInt() & 0xffffffffL;
                 return Instant.ofEpochSecond(u32);
             }
             case 8: {
@@ -1289,38 +1289,9 @@ public Instant unpackTimestamp()
                 return Instant.ofEpochSecond(sec, nsec);
             }
             case 12: {
-                int nsec = readInt();
-                long sec = readLong();
-                return Instant.ofEpochSecond(sec, nsec);
-            }
-            default:
-                throw new MessageFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes",
-                            EXT_TIMESTAMP, ext.getLength()));
-        }
-    }
-
-    public long unpackTimestampMillis()
-            throws IOException
-    {
-        ExtensionTypeHeader ext = unpackExtensionTypeHeader();
-        if (ext.getType() != EXT_TIMESTAMP) {
-            throw unexpectedExtension("Timestamp", EXT_TIMESTAMP, ext.getType());
-        }
-        switch (ext.getLength()) {
-            case 4: {
-                int u32 = readInt();
-                return (u32 & 0xffffffffL) * 1000L;
-            }
-            case 8: {
-                long data64 = readLong();
-                int nsec = (int) (data64 >>> 34);
-                long sec = data64 & 0x00000003ffffffffL;
-                return sec * 1000L + nsec / 1000L;
-            }
-            case 12: {
-                int nsec = readInt();
+                long nsecU32 = readInt() & 0xffffffffL;
                 long sec = readLong();
-                return sec * 1000L + nsec / 1000L;
+                return Instant.ofEpochSecond(sec, nsecU32);
             }
             default:
                 throw new MessageFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes",

From 1bb5af76cf9d6623863ac4786203028b6d712f11 Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 16:47:45 -0700
Subject: [PATCH 04/21] Code cleanup

---
 .../java/org/msgpack/core/MessagePacker.java  | 22 +-----
 .../org/msgpack/core/MessageUnpacker.java     | 10 ++-
 .../org/msgpack/value/TimestampValue.java     |  5 +-
 .../java/org/msgpack/value/ValueFactory.java  |  7 +-
 .../impl/ImmutableTimestampValueImpl.java     | 70 ++++---------------
 5 files changed, 29 insertions(+), 85 deletions(-)

diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java
index 3ee99a573..6431b73ad 100644
--- a/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java
+++ b/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java
@@ -33,7 +33,6 @@
 import java.nio.charset.CoderResult;
 import java.nio.charset.CodingErrorAction;
 import java.time.Instant;
-import java.util.Date;
 
 import static org.msgpack.core.MessagePack.Code.ARRAY16;
 import static org.msgpack.core.MessagePack.Code.ARRAY32;
@@ -801,25 +800,6 @@ else if (s.length() < (1 << 16)) {
         return this;
     }
 
-    /**
-     * Writes a Timestamp value.
-     *
-     * <p>
-     * This method writes a timestamp value using timestamp format family.
-     *
-     * @param date the timestamp to be written
-     * @return this
-     * @throws IOException when underlying output throws IOException
-     */
-    public MessagePacker packTimestamp(Date date)
-            throws IOException
-    {
-        long epochMilli = date.getTime();
-        long sec = Math.floorDiv(epochMilli, 1000L);
-        int nsec = ((int) (epochMilli - sec * 1000L)) * 1000;  // 0 <= nsec < 1,000,000,000 < 2^30
-        return packTimestampImpl(sec, nsec);
-    }
-
     /**
      * Writes a Timestamp value.
      *
@@ -848,7 +828,7 @@ public MessagePacker packTimestamp(Instant instant)
      * @throws IOException when underlying output throws IOException
      * @throws ArithmeticException when epochSecond plus nanoAdjustment in seconds exceeds the range of long
      */
-    public MessagePacker packTimestampEpochSecond(long epochSecond, int nanoAdjustment)
+    public MessagePacker packTimestamp(long epochSecond, int nanoAdjustment)
             throws IOException, ArithmeticException
     {
         long sec = Math.addExact(epochSecond, Math.floorDiv(nanoAdjustment, 1000000000L));
diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
index 05f87692e..95db2dd28 100644
--- a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
+++ b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
@@ -653,7 +653,7 @@ public ImmutableValue unpackValue()
                 ExtensionTypeHeader extHeader = unpackExtensionTypeHeader();
                 switch (extHeader.getType()) {
                 case EXT_TIMESTAMP:
-                    return ValueFactory.newTimestamp(readPayload(extHeader.getLength()));
+                    return ValueFactory.newTimestamp(unpackTimestamp(extHeader));
                 default:
                     return ValueFactory.newExtension(extHeader.getType(), readPayload(extHeader.getLength()));
                 }
@@ -720,6 +720,7 @@ public Variable unpackValue(Variable var)
             }
             case EXTENSION: {
                 ExtensionTypeHeader extHeader = unpackExtensionTypeHeader();
+                // TODO Set TimestampValue
                 var.setExtensionValue(extHeader.getType(), readPayload(extHeader.getLength()));
                 return var;
             }
@@ -1274,6 +1275,11 @@ public Instant unpackTimestamp()
             throws IOException
     {
         ExtensionTypeHeader ext = unpackExtensionTypeHeader();
+        return unpackTimestamp(ext);
+    }
+
+    private Instant unpackTimestamp(ExtensionTypeHeader ext) throws IOException
+    {
         if (ext.getType() != EXT_TIMESTAMP) {
             throw unexpectedExtension("Timestamp", EXT_TIMESTAMP, ext.getType());
         }
@@ -1295,7 +1301,7 @@ public Instant unpackTimestamp()
             }
             default:
                 throw new MessageFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes",
-                            EXT_TIMESTAMP, ext.getLength()));
+                        EXT_TIMESTAMP, ext.getLength()));
         }
     }
 
diff --git a/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java b/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java
index caae015a1..8be7d7338 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java
@@ -16,7 +16,6 @@
 package org.msgpack.value;
 
 import java.time.Instant;
-import java.util.Date;
 
 /**
  * Representation of MessagePack's Timestamp type.
@@ -30,9 +29,7 @@ public interface TimestampValue
 
     int getNano();
 
-    long toEpochMilli();
+    long toEpochMillis();
 
     Instant toInstant();
-
-    Date toDate();
 }
diff --git a/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java b/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java
index 8ee733c02..031679f2c 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java
@@ -28,6 +28,7 @@
 import org.msgpack.value.impl.ImmutableTimestampValueImpl;
 
 import java.math.BigInteger;
+import java.time.Instant;
 import java.util.AbstractMap;
 import java.util.Arrays;
 import java.util.LinkedHashMap;
@@ -297,13 +298,13 @@ public static ImmutableExtensionValue newExtension(byte type, byte[] data)
         return new ImmutableExtensionValueImpl(type, data);
     }
 
-    public static ImmutableTimestampValue newTimestamp(byte[] data)
+    public static ImmutableTimestampValue newTimestamp(Instant timestamp)
     {
-        return new ImmutableTimestampValueImpl(data);
+        return new ImmutableTimestampValueImpl(timestamp);
     }
 
     public static ImmutableTimestampValue newTimestamp(long epochSecond, int nanoAdjustment)
     {
-        return new ImmutableTimestampValueImpl(epochSecond, nanoAdjustment);
+        return newTimestamp(Instant.ofEpochSecond(epochSecond, nanoAdjustment));
     }
 }
diff --git a/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java b/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java
index 082dd37b4..ca620adc4 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java
@@ -15,7 +15,6 @@
 //
 package org.msgpack.value.impl;
 
-import org.msgpack.core.MessageExtensionFormatException;
 import org.msgpack.core.MessagePacker;
 import org.msgpack.core.buffer.MessageBuffer;
 import org.msgpack.value.ExtensionValue;
@@ -28,7 +27,6 @@
 import java.io.IOException;
 import java.time.Instant;
 import java.util.Arrays;
-import java.util.Date;
 
 import static org.msgpack.core.MessagePack.Code.EXT_TIMESTAMP;
 
@@ -41,45 +39,12 @@ public class ImmutableTimestampValueImpl
         extends AbstractImmutableValue
         implements ImmutableExtensionValue, ImmutableTimestampValue
 {
-    private final long sec;
-
-    private final int nsec;
-
+    private final Instant instant;
     private byte[] data;
 
-    public ImmutableTimestampValueImpl(long epochSecond, int nanoAdjustment)
+    public ImmutableTimestampValueImpl(Instant timestamp)
     {
-        this.sec = Math.addExact(epochSecond, Math.floorDiv(nanoAdjustment, 1000000000L));
-        this.nsec = (int) Math.floorMod(nanoAdjustment, 1000000000L);
-        this.data = null;
-    }
-
-    public ImmutableTimestampValueImpl(byte[] data)
-    {
-        // See MessageUnpacker.unpackInstant
-        this.data = data;
-        switch (data.length) {
-            case 4: {
-                this.sec = MessageBuffer.wrap(data).getInt(0);
-                this.nsec = 0;
-                break;
-            }
-            case 8: {
-                long data64 = MessageBuffer.wrap(data).getLong(0);
-                this.nsec = (int) (data64 >>> 34);
-                this.sec = data64 & 0x00000003ffffffffL;
-                break;
-            }
-            case 12: {
-                MessageBuffer buffer = MessageBuffer.wrap(data);
-                this.nsec = buffer.getInt(0);
-                this.sec = buffer.getLong(4);
-                break;
-            }
-            default:
-                throw new MessageExtensionFormatException(String.format("Timestamp extension type (%d) expects 4, 8, or 12 bytes of payload but got %d bytes",
-                            EXT_TIMESTAMP, data.length));
-        }
+        this.instant = timestamp;
     }
 
     @Override
@@ -113,6 +78,8 @@ public byte[] getData()
         if (data == null) {
             // See MessagePacker.packTimestampImpl
             byte[] bytes;
+            long sec = getEpochSecond();
+            int nsec = getNano();
             if (sec >>> 34 == 0) {
                 long data64 = (nsec << 34) | sec;
                 if ((data64 & 0xffffffff00000000L) == 0L) {
@@ -138,38 +105,32 @@ public byte[] getData()
     @Override
     public long getEpochSecond()
     {
-        return sec;
+        return instant.getEpochSecond();
     }
 
     @Override
     public int getNano()
     {
-        return nsec;
+        return instant.getNano();
     }
 
     @Override
-    public long toEpochMilli()
+    public long toEpochMillis()
     {
-        return Math.addExact(Math.multiplyExact(getEpochSecond(), 1000L), getNano() / 1000000L);
+        return instant.toEpochMilli();
     }
 
     @Override
     public Instant toInstant()
     {
-        return Instant.ofEpochSecond(getEpochSecond(), getNano());
-    }
-
-    @Override
-    public Date toDate()
-    {
-        return new Date(toEpochMilli());
+        return instant;
     }
 
     @Override
     public void writeTo(MessagePacker packer)
             throws IOException
     {
-        packer.packTimestampEpochSecond(sec, nsec);
+        packer.packTimestamp(instant);
     }
 
     @Override
@@ -194,7 +155,7 @@ public boolean equals(Object o)
         // using "default" keyword since Java 7, here uses instanceof of and cast instead.
         if (ev instanceof TimestampValue) {
             TimestampValue tv = (TimestampValue) ev;
-            return getEpochSecond() == tv.getEpochSecond() && getNano() == tv.getNano();
+            return instant.equals(tv.toInstant());
         }
         else {
             return EXT_TIMESTAMP == ev.getType() && Arrays.equals(getData(), ev.getData());
@@ -205,10 +166,9 @@ public boolean equals(Object o)
     public int hashCode()
     {
         // Implements same behavior with ImmutableExtensionValueImpl.
-        int hash = 31 + EXT_TIMESTAMP;
-        for (byte e : getData()) {
-            hash = 31 * hash + e;
-        }
+        int hash = EXT_TIMESTAMP;
+        hash *= 31;
+        hash = instant.hashCode();
         return hash;
     }
 

From b4c1448ff98844f0a7ff8d211e64f6961af73aa3 Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 16:59:18 -0700
Subject: [PATCH 05/21] Fix timestamp pack overflow

---
 .../main/java/org/msgpack/core/MessagePacker.java | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java
index 6431b73ad..f3c3773b0 100644
--- a/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java
+++ b/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java
@@ -813,9 +813,11 @@ else if (s.length() < (1 << 16)) {
     public MessagePacker packTimestamp(Instant instant)
             throws IOException
     {
-        return packTimestampImpl(instant.getEpochSecond(), instant.getNano());
+        return packTimestamp(instant.getEpochSecond(), instant.getNano());
     }
 
+    private static final long NANOS_PER_SECOND = 1000000000L;
+
     /**
      * Writes a Timestamp value.
      *
@@ -831,14 +833,9 @@ public MessagePacker packTimestamp(Instant instant)
     public MessagePacker packTimestamp(long epochSecond, int nanoAdjustment)
             throws IOException, ArithmeticException
     {
-        long sec = Math.addExact(epochSecond, Math.floorDiv(nanoAdjustment, 1000000000L));
-        int nsec = (int) Math.floorMod(nanoAdjustment, 1000000000L);
-        return packTimestampImpl(sec, nsec);
-    }
+        long sec = Math.addExact(epochSecond, Math.floorDiv(nanoAdjustment, NANOS_PER_SECOND));
+        long nsec = Math.floorMod((long) nanoAdjustment, NANOS_PER_SECOND);
 
-    private MessagePacker packTimestampImpl(long sec, int nsec)
-            throws IOException
-    {
         if (sec >>> 34 == 0) {
             // sec can be serialized in 34 bits.
             long data64 = (nsec << 34) | sec;
@@ -855,7 +852,7 @@ private MessagePacker packTimestampImpl(long sec, int nsec)
         }
         else {
             // use timestamp 96 format
-            writeTimestamp96(sec, nsec);
+            writeTimestamp96(sec, (int) nsec);
         }
         return this;
     }

From 5c97c462959b134de9bdaf332e68cd7b933aa6a3 Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 20:10:49 -0700
Subject: [PATCH 06/21] Add timestamp value support

---
 .../java/org/msgpack/core/MessagePacker.java  |  14 ++-
 .../org/msgpack/value/ImmutableValue.java     |   3 +
 .../main/java/org/msgpack/value/Value.java    |  17 +++
 .../java/org/msgpack/value/ValueFactory.java  |   5 +
 .../main/java/org/msgpack/value/Variable.java | 112 +++++++++++++++++-
 .../value/impl/AbstractImmutableValue.java    |  13 ++
 .../impl/ImmutableTimestampValueImpl.java     |   6 +
 .../org/msgpack/core/MessagePackTest.scala    |  17 +++
 .../org/msgpack/value/ValueFactoryTest.scala  |   8 +-
 9 files changed, 192 insertions(+), 3 deletions(-)

diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java
index f3c3773b0..4cf789d9f 100644
--- a/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java
+++ b/msgpack-core/src/main/java/org/msgpack/core/MessagePacker.java
@@ -807,7 +807,7 @@ else if (s.length() < (1 << 16)) {
      * This method writes a timestamp value using timestamp format family.
      *
      * @param instant the timestamp to be written
-     * @return this
+     * @return this packer
      * @throws IOException when underlying output throws IOException
      */
     public MessagePacker packTimestamp(Instant instant)
@@ -816,6 +816,18 @@ public MessagePacker packTimestamp(Instant instant)
         return packTimestamp(instant.getEpochSecond(), instant.getNano());
     }
 
+    /**
+     * Writes a Timesamp value using a millisecond value (e.g., System.currentTimeMillis())
+     * @param millis the millisecond value
+     * @return this packer
+     * @throws IOException when underlying output throws IOException
+     */
+    public MessagePacker packTimestamp(long millis)
+            throws IOException
+    {
+        return packTimestamp(Instant.ofEpochMilli(millis));
+    }
+
     private static final long NANOS_PER_SECOND = 1000000000L;
 
     /**
diff --git a/msgpack-core/src/main/java/org/msgpack/value/ImmutableValue.java b/msgpack-core/src/main/java/org/msgpack/value/ImmutableValue.java
index 88798a13d..f85c69bac 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/ImmutableValue.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/ImmutableValue.java
@@ -47,4 +47,7 @@ public interface ImmutableValue
 
     @Override
     public ImmutableStringValue asStringValue();
+
+    @Override
+    public ImmutableTimestampValue asTimestampValue();
 }
diff --git a/msgpack-core/src/main/java/org/msgpack/value/Value.java b/msgpack-core/src/main/java/org/msgpack/value/Value.java
index a95fb8d3e..a3d1ac365 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/Value.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/Value.java
@@ -181,6 +181,14 @@ public interface Value
      */
     boolean isExtensionValue();
 
+    /**
+     * Returns true if the type of this value is Timestamp.
+     *
+     * If this method returns true, {@code asTimestamp} never throws exceptions.
+     * Note that you can't use <code>instanceof</code> or cast <code>((MapValue) thisValue)</code> to check type of a value because type of a mutable value is variable.
+     */
+    boolean isTimestampValue();
+
     /**
      * Returns the value as {@code NilValue}. Otherwise throws {@code MessageTypeCastException}.
      *
@@ -281,6 +289,15 @@ public interface Value
      */
     ExtensionValue asExtensionValue();
 
+    /**
+     * Returns the value as {@code TimestampValue}. Otherwise throws {@code MessageTypeCastException}.
+     *
+     * Note that you can't use <code>instanceof</code> or cast <code>((TimestampValue) thisValue)</code> to check type of a value because type of a mutable value is variable.
+     *
+     * @throws MessageTypeCastException If type of this value is not Map.
+     */
+    TimestampValue asTimestampValue();
+
     /**
      * Serializes the value using the specified {@code MessagePacker}
      *
diff --git a/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java b/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java
index 031679f2c..dc1e28da8 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/ValueFactory.java
@@ -303,6 +303,11 @@ public static ImmutableTimestampValue newTimestamp(Instant timestamp)
         return new ImmutableTimestampValueImpl(timestamp);
     }
 
+    public static ImmutableTimestampValue newTimestamp(long millis)
+    {
+        return newTimestamp(Instant.ofEpochMilli(millis));
+    }
+
     public static ImmutableTimestampValue newTimestamp(long epochSecond, int nanoAdjustment)
     {
         return newTimestamp(Instant.ofEpochSecond(epochSecond, nanoAdjustment));
diff --git a/msgpack-core/src/main/java/org/msgpack/value/Variable.java b/msgpack-core/src/main/java/org/msgpack/value/Variable.java
index 28295fe1c..ae88170c7 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/Variable.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/Variable.java
@@ -30,6 +30,7 @@
 import java.nio.charset.CharacterCodingException;
 import java.nio.charset.CharsetDecoder;
 import java.nio.charset.CodingErrorAction;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Iterator;
@@ -109,6 +110,12 @@ public boolean isExtensionValue()
             return getValueType().isExtensionType();
         }
 
+        @Override
+        public boolean isTimestampValue()
+        {
+            return false;
+        }
+
         @Override
         public NilValue asNilValue()
         {
@@ -175,6 +182,12 @@ public ExtensionValue asExtensionValue()
             throw new MessageTypeCastException();
         }
 
+        @Override
+        public TimestampValue asTimestampValue()
+        {
+            throw new MessageTypeCastException();
+        }
+
         @Override
         public boolean equals(Object obj)
         {
@@ -211,7 +224,8 @@ public static enum Type
         RAW_STRING(ValueType.STRING),
         LIST(ValueType.ARRAY),
         MAP(ValueType.MAP),
-        EXTENSION(ValueType.EXTENSION);
+        EXTENSION(ValueType.EXTENSION),
+        TIMESTAMP(ValueType.EXTENSION);
 
         private final ValueType valueType;
 
@@ -235,6 +249,7 @@ public ValueType getValueType()
     private final ArrayValueAccessor arrayAccessor = new ArrayValueAccessor();
     private final MapValueAccessor mapAccessor = new MapValueAccessor();
     private final ExtensionValueAccessor extensionAccessor = new ExtensionValueAccessor();
+    private final TimestampValueAccessor timestampAccessor = new TimestampValueAccessor();
 
     private Type type;
 
@@ -1031,6 +1046,86 @@ public void writeTo(MessagePacker pk)
         }
     }
 
+    public Variable setTimestampValue(Instant timestamp)
+    {
+        this.type = Type.TIMESTAMP;
+        this.accessor = timestampAccessor;
+        this.objectValue = ValueFactory.newTimestamp(timestamp);
+        return this;
+    }
+
+    private class TimestampValueAccessor
+            extends AbstractValueAccessor
+            implements TimestampValue
+    {
+        @Override
+        public boolean isTimestampValue()
+        {
+            return true;
+        }
+
+        @Override
+        public ValueType getValueType()
+        {
+            return ValueType.EXTENSION;
+        }
+
+        @Override
+        public TimestampValue asTimestampValue()
+        {
+            return this;
+        }
+
+        @Override
+        public ImmutableTimestampValue immutableValue()
+        {
+            return (ImmutableTimestampValue) objectValue;
+        }
+
+        @Override
+        public byte getType()
+        {
+            return ((ImmutableTimestampValue) objectValue).getType();
+        }
+
+        @Override
+        public byte[] getData()
+        {
+            return ((ImmutableTimestampValue) objectValue).getData();
+        }
+
+        @Override
+        public void writeTo(MessagePacker pk)
+                throws IOException
+        {
+            ((ImmutableTimestampValue) objectValue).writeTo(pk);
+        }
+
+        @Override
+        public long getEpochSecond()
+        {
+            return ((ImmutableTimestampValue) objectValue).getEpochSecond();
+        }
+
+        @Override
+        public int getNano()
+        {
+            return ((ImmutableTimestampValue) objectValue).getNano();
+        }
+
+        @Override
+        public long toEpochMillis()
+        {
+            return ((ImmutableTimestampValue) objectValue).toEpochMillis();
+        }
+
+        @Override
+        public Instant toInstant()
+        {
+            return ((ImmutableTimestampValue) objectValue).toInstant();
+        }
+    }
+
     ////
     // Value
     //
@@ -1144,6 +1239,12 @@ public boolean isExtensionValue()
         return getValueType().isExtensionType();
     }
 
+    @Override
+    public boolean isTimestampValue()
+    {
+        return this.type == Type.TIMESTAMP;
+    }
+
     @Override
     public NilValue asNilValue()
     {
@@ -1242,4 +1343,13 @@ public ExtensionValue asExtensionValue()
         }
         return (ExtensionValue) accessor;
     }
+
+    @Override
+    public TimestampValue asTimestampValue()
+    {
+        if (!isTimestampValue()) {
+            throw new MessageTypeCastException();
+        }
+        return (TimestampValue) accessor;
+    }
 }
diff --git a/msgpack-core/src/main/java/org/msgpack/value/impl/AbstractImmutableValue.java b/msgpack-core/src/main/java/org/msgpack/value/impl/AbstractImmutableValue.java
index 1dae99cf2..18fcd2753 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/impl/AbstractImmutableValue.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/impl/AbstractImmutableValue.java
@@ -27,6 +27,7 @@
 import org.msgpack.value.ImmutableNumberValue;
 import org.msgpack.value.ImmutableRawValue;
 import org.msgpack.value.ImmutableStringValue;
+import org.msgpack.value.ImmutableTimestampValue;
 import org.msgpack.value.ImmutableValue;
 
 abstract class AbstractImmutableValue
@@ -98,6 +99,12 @@ public boolean isExtensionValue()
         return getValueType().isExtensionType();
     }
 
+    @Override
+    public boolean isTimestampValue()
+    {
+        return false;
+    }
+
     @Override
     public ImmutableNilValue asNilValue()
     {
@@ -163,4 +170,10 @@ public ImmutableExtensionValue asExtensionValue()
     {
         throw new MessageTypeCastException();
     }
+
+    @Override
+    public ImmutableTimestampValue asTimestampValue()
+    {
+        throw new MessageTypeCastException();
+    }
 }
diff --git a/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java b/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java
index ca620adc4..47ede337c 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java
@@ -47,6 +47,12 @@ public ImmutableTimestampValueImpl(Instant timestamp)
         this.instant = timestamp;
     }
 
+    @Override
+    public boolean isTimestampValue()
+    {
+        return true;
+    }
+
     @Override
     public byte getType()
     {
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
index f6d81ff4f..f85d8552a 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
@@ -591,11 +591,20 @@ class MessagePackTest extends MessagePackSpec {
         val v = Instant.ofEpochSecond(second, nano)
         check(v, { _.packTimestamp(v) }, { _.unpackTimestamp() })
       }
+      // Using different insterfaces
+      forAll(posLong, posInt) { (second: Long, nano: Int) =>
+        val v = Instant.ofEpochSecond(second, nano)
+        check(v, { _.packTimestamp(second, nano) }, { _.unpackTimestamp() })
+      }
       val secLessThan34bits = Gen.chooseNum[Long](0, 1L << 34)
       forAll(secLessThan34bits, posInt) { (second: Long, nano: Int) =>
         val v = Instant.ofEpochSecond(second, nano)
         check(v, _.packTimestamp(v), _.unpackTimestamp())
       }
+      forAll(secLessThan34bits, posInt) { (second: Long, nano: Int) =>
+        val v = Instant.ofEpochSecond(second, nano)
+        check(v, _.packTimestamp(second, nano), _.unpackTimestamp())
+      }
 
       // Corner cases for u
       // sign uint32 nanoseq (out of int32 range)
@@ -608,6 +617,14 @@ class MessagePackTest extends MessagePackSpec {
         check(v, _.packTimestamp(v), _.unpackTimestamp())
       }
     }
+
+    "pack/unpack timestamp in millis" in {
+      val posLong = Gen.chooseNum[Long](-31557014167219200L, 31556889864403199L)
+      forAll(posLong) { (millis: Long) =>
+        val v = Instant.ofEpochMilli(millis)
+        check(v, { _.packTimestamp(millis) }, { _.unpackTimestamp() })
+      }
+    }
   }
 
   "MessagePack.PackerConfig" should {
diff --git a/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala
index c9bc8f8f0..8014af35e 100644
--- a/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala
@@ -35,7 +35,8 @@ class ValueFactoryTest extends MessagePackSpec {
               isMap: Boolean = false,
               isExtension: Boolean = false,
               isRaw: Boolean = false,
-              isNumber: Boolean = false): Boolean = {
+              isNumber: Boolean = false,
+              isTimestamp: Boolean = false): Boolean = {
     v.isNilValue shouldBe isNil
     v.isBooleanValue shouldBe isBoolean
     v.isIntegerValue shouldBe isInteger
@@ -47,6 +48,7 @@ class ValueFactoryTest extends MessagePackSpec {
     v.isExtensionValue shouldBe isExtension
     v.isRawValue shouldBe isRaw
     v.isNumberValue shouldBe isNumber
+    v.isTimestampValue shouldBe isTimestamp
     true
   }
 
@@ -74,6 +76,10 @@ class ValueFactoryTest extends MessagePackSpec {
       forAll { (v: Array[Byte]) =>
         isValid(ValueFactory.newExtension(0, v), expected = ValueType.EXTENSION, isExtension = true, isRaw = false)
       }
+
+      forAll { (millis: Long) =>
+        isValid(ValueFactory.newTimestamp(millis), expected = ValueType.EXTENSION, isExtension = true, isTimestamp = true)
+      }
     }
   }
 }

From 4beb154a8675c467fc5f4ab20ffbc1020af55271 Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 20:15:38 -0700
Subject: [PATCH 07/21] Support timestamp in unpackValue

---
 .../src/main/java/org/msgpack/core/MessageUnpacker.java   | 8 ++++++--
 .../test/scala/org/msgpack/value/ValueFactoryTest.scala   | 1 -
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
index 95db2dd28..4534c803a 100644
--- a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
+++ b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
@@ -720,8 +720,12 @@ public Variable unpackValue(Variable var)
             }
             case EXTENSION: {
                 ExtensionTypeHeader extHeader = unpackExtensionTypeHeader();
-                // TODO Set TimestampValue
-                var.setExtensionValue(extHeader.getType(), readPayload(extHeader.getLength()));
+                if (extHeader.getType() == EXT_TIMESTAMP) {
+                    var.setTimestampValue(unpackTimestamp(extHeader));
+                }
+                else {
+                    var.setExtensionValue(extHeader.getType(), readPayload(extHeader.getLength()));
+                }
                 return var;
             }
             default:
diff --git a/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala
index 8014af35e..8a76b6feb 100644
--- a/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala
@@ -76,7 +76,6 @@ class ValueFactoryTest extends MessagePackSpec {
       forAll { (v: Array[Byte]) =>
         isValid(ValueFactory.newExtension(0, v), expected = ValueType.EXTENSION, isExtension = true, isRaw = false)
       }
-
       forAll { (millis: Long) =>
         isValid(ValueFactory.newTimestamp(millis), expected = ValueType.EXTENSION, isExtension = true, isTimestamp = true)
       }

From a566e76b83172cf600f38098ebe2a2f9204e95ec Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 20:21:43 -0700
Subject: [PATCH 08/21] Add type cast

---
 .../org/msgpack/value/impl/ImmutableTimestampValueImpl.java | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java b/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java
index 47ede337c..227c85d0b 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/impl/ImmutableTimestampValueImpl.java
@@ -78,6 +78,12 @@ public ImmutableExtensionValue asExtensionValue()
         return this;
     }
 
+    @Override
+    public ImmutableTimestampValue asTimestampValue()
+    {
+        return this;
+    }
+
     @Override
     public byte[] getData()
     {

From d8bb62d12ad97395df3925290ae3ed775f933578 Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 22:43:01 -0700
Subject: [PATCH 09/21] Use AirSpec for integration with ScalaCheck

---
 build.sbt                                     |  15 +-
 .../java/org/msgpack/value/ValueType.java     |   6 +
 .../org/msgpack/core/MessagePackSpec.scala    |   1 -
 .../org/msgpack/core/MessagePackTest.scala    |   5 +-
 .../org/msgpack/value/ValueFactoryTest.scala  |  45 +--
 .../org/msgpack/value/VariableTest.scala      | 285 ++++++++++++++++++
 6 files changed, 327 insertions(+), 30 deletions(-)
 create mode 100644 msgpack-core/src/test/scala/org/msgpack/value/VariableTest.scala

diff --git a/build.sbt b/build.sbt
index a97c20f7b..68b7e3c31 100644
--- a/build.sbt
+++ b/build.sbt
@@ -71,15 +71,18 @@ lazy val msgpackCore = Project(id = "msgpack-core", base = file("msgpack-core"))
       "org.msgpack.value",
       "org.msgpack.value.impl"
     ),
+    testFrameworks += new TestFramework("wvlet.airspec.Framework"),
     libraryDependencies ++= Seq(
       // msgpack-core should have no external dependencies
       junitInterface,
-      "org.scalatest"     %% "scalatest"    % "3.2.8"  % "test",
-      "org.scalacheck"    %% "scalacheck"   % "1.15.4" % "test",
-      "org.xerial"        %% "xerial-core"  % "3.6.0"  % "test",
-      "org.msgpack"       % "msgpack"       % "0.6.12" % "test",
-      "commons-codec"     % "commons-codec" % "1.12"   % "test",
-      "com.typesafe.akka" %% "akka-actor"   % "2.5.23" % "test"
+      "org.scalatest"          %% "scalatest"               % "3.2.8"  % "test",
+      "org.scalacheck"         %% "scalacheck"              % "1.15.4" % "test",
+      "org.xerial"             %% "xerial-core"             % "3.6.0"  % "test",
+      "org.msgpack"            % "msgpack"                  % "0.6.12" % "test",
+      "commons-codec"          % "commons-codec"            % "1.12"   % "test",
+      "com.typesafe.akka"      %% "akka-actor"              % "2.5.23" % "test",
+      "org.wvlet.airframe"     %% "airspec"                 % "20.4.1" % "test",
+      "org.scala-lang.modules" %% "scala-collection-compat" % "2.4.3"  % "test"
     )
   )
 
diff --git a/msgpack-core/src/main/java/org/msgpack/value/ValueType.java b/msgpack-core/src/main/java/org/msgpack/value/ValueType.java
index b06478402..8eeb957b3 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/ValueType.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/ValueType.java
@@ -36,6 +36,12 @@ public enum ValueType
     MAP(false, false),
     EXTENSION(false, false);
 
+    /**
+     * Design note: We do not add Timestamp as a ValueType here because
+     * detecting Timestamp values requires reading 1-3 bytes ahead while the other
+     * value types can be determined just by reading the first one byte.
+     */
+
     private final boolean numberType;
     private final boolean rawType;
 
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
index ae1f5ae45..a13b18480 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
@@ -18,7 +18,6 @@ package org.msgpack.core
 import java.io.ByteArrayOutputStream
 import org.scalatest._
 import org.scalatest.matchers.should.Matchers
-import org.scalatest.prop.TableDrivenPropertyChecks
 import org.scalatest.wordspec.AnyWordSpec
 import xerial.core.log.{LogLevel, Logger}
 import xerial.core.util.{TimeReport, Timer}
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
index f85d8552a..8abb3cad6 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
@@ -606,10 +606,9 @@ class MessagePackTest extends MessagePackSpec {
         check(v, _.packTimestamp(second, nano), _.unpackTimestamp())
       }
 
-      // Corner cases for u
-      // sign uint32 nanoseq (out of int32 range)
+      // Corner-cases around uint32 boundaries
       for (v <- Seq(
-             Instant.ofEpochSecond(Instant.now().getEpochSecond, 123456789L),
+             Instant.ofEpochSecond(Instant.now().getEpochSecond, 123456789L), // uint32 nanoseq (out of int32 range)
              Instant.ofEpochSecond(-1302749144L, 0), // 1928-09-19T21:14:16Z
              Instant.ofEpochSecond(-747359729L, 0), // 1946-04-27T00:04:31Z
              Instant.ofEpochSecond(4257387427L, 0) // 2104-11-29T07:37:07Z
diff --git a/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala
index 8a76b6feb..fc9cd49c3 100644
--- a/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala
@@ -15,28 +15,28 @@
 //
 package org.msgpack.value
 
-import org.msgpack.core.MessagePackSpec
-import org.scalacheck.Prop.forAll
+import wvlet.airspec.AirSpec
+import wvlet.airspec.spi.PropertyCheck
 
 /**
   *
   */
-class ValueFactoryTest extends MessagePackSpec {
+class ValueFactoryTest extends AirSpec with PropertyCheck {
 
-  def isValid(v: Value,
-              expected: ValueType,
-              isNil: Boolean = false,
-              isBoolean: Boolean = false,
-              isInteger: Boolean = false,
-              isString: Boolean = false,
-              isFloat: Boolean = false,
-              isBinary: Boolean = false,
-              isArray: Boolean = false,
-              isMap: Boolean = false,
-              isExtension: Boolean = false,
-              isRaw: Boolean = false,
-              isNumber: Boolean = false,
-              isTimestamp: Boolean = false): Boolean = {
+  private def isValid(v: Value,
+                      expected: ValueType,
+                      isNil: Boolean = false,
+                      isBoolean: Boolean = false,
+                      isInteger: Boolean = false,
+                      isString: Boolean = false,
+                      isFloat: Boolean = false,
+                      isBinary: Boolean = false,
+                      isArray: Boolean = false,
+                      isMap: Boolean = false,
+                      isExtension: Boolean = false,
+                      isRaw: Boolean = false,
+                      isNumber: Boolean = false,
+                      isTimestamp: Boolean = false): Boolean = {
     v.isNilValue shouldBe isNil
     v.isBooleanValue shouldBe isBoolean
     v.isIntegerValue shouldBe isInteger
@@ -52,9 +52,8 @@ class ValueFactoryTest extends MessagePackSpec {
     true
   }
 
-  "ValueFactory" should {
-
-    "create valid type values" in {
+  test("ValueFactory") {
+    test("create valid type values") {
       isValid(ValueFactory.newNil(), expected = ValueType.NIL, isNil = true)
       forAll { (v: Boolean) =>
         isValid(ValueFactory.newBoolean(v), expected = ValueType.BOOLEAN, isBoolean = true)
@@ -79,6 +78,12 @@ class ValueFactoryTest extends MessagePackSpec {
       forAll { (millis: Long) =>
         isValid(ValueFactory.newTimestamp(millis), expected = ValueType.EXTENSION, isExtension = true, isTimestamp = true)
       }
+      forAll { (millis: Long) =>
+        isValid(ValueFactory.newTimestamp(millis), expected = ValueType.EXTENSION, isExtension = true, isTimestamp = true)
+      }
+      forAll { (sec: Long, nano: Int) =>
+        isValid(ValueFactory.newTimestamp(sec, nano), expected = ValueType.EXTENSION, isExtension = true, isTimestamp = true)
+      }
     }
   }
 }
diff --git a/msgpack-core/src/test/scala/org/msgpack/value/VariableTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/VariableTest.scala
new file mode 100644
index 000000000..d3c45abb7
--- /dev/null
+++ b/msgpack-core/src/test/scala/org/msgpack/value/VariableTest.scala
@@ -0,0 +1,285 @@
+//
+// MessagePack for Java
+//
+//    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 org.msgpack.value
+
+import org.msgpack.core.{MessagePack, MessagePacker, MessageTypeCastException}
+import wvlet.airspec.AirSpec
+import wvlet.airspec.spi.PropertyCheck
+
+import java.time.Instant
+import java.util
+import scala.jdk.CollectionConverters._
+
+/**
+  *
+  */
+class VariableTest extends AirSpec with PropertyCheck {
+  private def check(pack: MessagePacker => Unit, checker: Variable => Unit): Unit = {
+    val packer = MessagePack.newDefaultBufferPacker()
+    pack(packer)
+    val msgpack = packer.toByteArray
+    packer.close()
+    val v        = new Variable()
+    val unpacker = MessagePack.newDefaultUnpacker(msgpack)
+    unpacker.unpackValue(v)
+    checker(v)
+    unpacker.close()
+  }
+
+  private def validateValue[V <: Value](
+      v: V,
+      asNil: Boolean = false,
+      asBoolean: Boolean = false,
+      asInteger: Boolean = false,
+      asFloat: Boolean = false,
+      asBinary: Boolean = false,
+      asString: Boolean = false,
+      asArray: Boolean = false,
+      asMap: Boolean = false,
+      asExtension: Boolean = false,
+      asTimestamp: Boolean = false
+  ): V = {
+    v.isNilValue shouldBe asNil
+    v.isBooleanValue shouldBe asBoolean
+    v.isIntegerValue shouldBe asInteger
+    v.isNumberValue shouldBe asInteger | asFloat
+    v.isFloatValue shouldBe asFloat
+    v.isRawValue shouldBe asBinary | asString
+    v.isBinaryValue shouldBe asBinary
+    v.isStringValue shouldBe asString
+    v.isArrayValue shouldBe asArray
+    v.isMapValue shouldBe asMap
+    v.isExtensionValue shouldBe asExtension | asTimestamp
+    v.isTimestampValue shouldBe asTimestamp
+
+    if (asNil) {
+      v.getValueType shouldBe ValueType.NIL
+    } else {
+      intercept[MessageTypeCastException] {
+        v.asNilValue()
+      }
+    }
+
+    if (asBoolean) {
+      v.getValueType shouldBe ValueType.BOOLEAN
+    } else {
+      intercept[MessageTypeCastException] {
+        v.asBooleanValue()
+      }
+    }
+
+    if (asInteger) {
+      v.getValueType shouldBe ValueType.INTEGER
+    } else {
+      intercept[MessageTypeCastException] {
+        v.asIntegerValue()
+      }
+    }
+
+    if (asFloat) {
+      v.getValueType shouldBe ValueType.FLOAT
+    } else {
+      intercept[MessageTypeCastException] {
+        v.asFloatValue()
+      }
+    }
+
+    if (asBinary | asString) {
+      v.asRawValue()
+    } else {
+      intercept[MessageTypeCastException] {
+        v.asRawValue()
+      }
+    }
+
+    if (asBinary) {
+      v.getValueType shouldBe ValueType.BINARY
+    } else {
+      intercept[MessageTypeCastException] {
+        v.asBinaryValue()
+      }
+    }
+
+    if (asString) {
+      v.getValueType shouldBe ValueType.STRING
+    } else {
+      intercept[MessageTypeCastException] {
+        v.asStringValue()
+      }
+    }
+
+    if (asArray) {
+      v.getValueType shouldBe ValueType.ARRAY
+    } else {
+      intercept[MessageTypeCastException] {
+        v.asArrayValue()
+      }
+    }
+
+    if (asMap) {
+      v.getValueType shouldBe ValueType.MAP
+    } else {
+      intercept[MessageTypeCastException] {
+        v.asMapValue()
+      }
+    }
+
+    if (asExtension) {
+      v.getValueType shouldBe ValueType.EXTENSION
+    } else {
+      intercept[MessageTypeCastException] {
+        v.asExtensionValue()
+      }
+    }
+
+    if (asTimestamp) {
+      v.getValueType shouldBe ValueType.EXTENSION
+    } else {
+      intercept[MessageTypeCastException] {
+        v.asTimestampValue()
+      }
+    }
+
+    v
+  }
+
+  test("Variable") {
+    test("read nil") {
+      check(
+        _.packNil,
+        checker = { v =>
+          val iv = validateValue(v.asNilValue(), asNil = true)
+          iv.toJson shouldBe "null"
+        }
+      )
+    }
+
+    test("read integers") {
+      forAll { i: Int =>
+        check(
+          _.packInt(i),
+          checker = { v =>
+            val iv = validateValue(v.asIntegerValue(), asInteger = true)
+            iv.asInt() shouldBe i
+            iv.asLong() shouldBe i.toLong
+          }
+        )
+      }
+    }
+
+    test("read double") {
+      forAll { x: Double =>
+        check(
+          _.packDouble(x),
+          checker = { v =>
+            val iv = validateValue(v.asFloatValue(), asFloat = true)
+          //iv.toDouble shouldBe v
+          //iv.toFloat shouldBe x.toFloat
+          }
+        )
+      }
+    }
+
+    test("read boolean") {
+      forAll { x: Boolean =>
+        check(
+          _.packBoolean(x),
+          checker = { v =>
+            val iv = validateValue(v.asBooleanValue(), asBoolean = true)
+            iv.getBoolean shouldBe x
+          }
+        )
+      }
+    }
+
+    test("read binary") {
+      forAll { x: Array[Byte] =>
+        check(
+          { packer =>
+            packer.packBinaryHeader(x.length); packer.addPayload(x)
+          },
+          checker = { v =>
+            val iv = validateValue(v.asBinaryValue(), asBinary = true)
+            util.Arrays.equals(iv.asByteArray(), x)
+          }
+        )
+      }
+    }
+
+    test("read string") {
+      forAll { x: String =>
+        check(
+          _.packString(x),
+          checker = { v =>
+            val iv = validateValue(v.asStringValue(), asString = true)
+            iv.asString() shouldBe x
+          }
+        )
+      }
+    }
+
+    test("read array") {
+      forAll { x: Seq[Int] =>
+        check(
+          { packer =>
+            packer.packArrayHeader(x.size)
+            x.foreach { packer.packInt(_) }
+          },
+          checker = { v =>
+            val iv  = validateValue(v.asArrayValue(), asArray = true)
+            val lst = iv.list().asScala.map(_.asIntegerValue().toInt)
+            lst shouldBe x
+          }
+        )
+      }
+    }
+
+    test("read map") {
+      forAll { x: Seq[Int] =>
+        // Generate map with unique keys
+        val map = x.zipWithIndex.map { case (x, i) => (s"key-${i}", x) }
+        check(
+          { packer =>
+            packer.packMapHeader(map.size)
+            map.foreach { x =>
+              packer.packString(x._1)
+              packer.packInt(x._2)
+            }
+          },
+          checker = { v =>
+            val iv  = validateValue(v.asMapValue(), asMap = true)
+            val lst = iv.map().asScala.map(p => (p._1.asStringValue().asString(), p._2.asIntegerValue().asInt())).toSeq
+            lst.sortBy(_._1) shouldBe map.sortBy(_._1)
+          }
+        )
+      }
+    }
+
+    test("read timestamps") {
+      forAll { millis: Long =>
+        val i = Instant.ofEpochMilli(millis)
+        check(
+          _.packTimestamp(i),
+          checker = { v =>
+            val ts = validateValue(v.asTimestampValue(), asTimestamp = true)
+            ts.isTimestampValue shouldBe true
+            ts.toInstant shouldBe i
+          }
+        )
+      }
+    }
+  }
+}

From 28c7843b3aa737f5975b0ece142d9bdfbdcf2b2f Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 22:47:01 -0700
Subject: [PATCH 10/21] Fix ValueFactoryTest

---
 .../org/msgpack/value/ValueFactoryTest.scala  | 39 ++++++++++++++++---
 1 file changed, 34 insertions(+), 5 deletions(-)

diff --git a/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala
index fc9cd49c3..b667ddfcf 100644
--- a/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala
@@ -15,6 +15,7 @@
 //
 package org.msgpack.value
 
+import org.scalacheck.Gen
 import wvlet.airspec.AirSpec
 import wvlet.airspec.spi.PropertyCheck
 
@@ -53,35 +54,63 @@ class ValueFactoryTest extends AirSpec with PropertyCheck {
   }
 
   test("ValueFactory") {
-    test("create valid type values") {
+    test("nil") {
       isValid(ValueFactory.newNil(), expected = ValueType.NIL, isNil = true)
+    }
+
+    test("boolean") {
       forAll { (v: Boolean) =>
         isValid(ValueFactory.newBoolean(v), expected = ValueType.BOOLEAN, isBoolean = true)
       }
+    }
+
+    test("int") {
       forAll { (v: Int) =>
         isValid(ValueFactory.newInteger(v), expected = ValueType.INTEGER, isInteger = true, isNumber = true)
       }
+    }
+
+    test("float") {
       forAll { (v: Float) =>
         isValid(ValueFactory.newFloat(v), expected = ValueType.FLOAT, isFloat = true, isNumber = true)
       }
+    }
+    test("string") {
       forAll { (v: String) =>
         isValid(ValueFactory.newString(v), expected = ValueType.STRING, isString = true, isRaw = true)
       }
+    }
+
+    test("array") {
       forAll { (v: Array[Byte]) =>
         isValid(ValueFactory.newBinary(v), expected = ValueType.BINARY, isBinary = true, isRaw = true)
       }
+    }
+
+    test("empty array") {
       isValid(ValueFactory.emptyArray(), expected = ValueType.ARRAY, isArray = true)
+    }
+
+    test("empty map") {
       isValid(ValueFactory.emptyMap(), expected = ValueType.MAP, isMap = true)
+    }
+
+    test("ext") {
       forAll { (v: Array[Byte]) =>
         isValid(ValueFactory.newExtension(0, v), expected = ValueType.EXTENSION, isExtension = true, isRaw = false)
       }
+    }
+
+    test("timestamp") {
       forAll { (millis: Long) =>
         isValid(ValueFactory.newTimestamp(millis), expected = ValueType.EXTENSION, isExtension = true, isTimestamp = true)
       }
-      forAll { (millis: Long) =>
-        isValid(ValueFactory.newTimestamp(millis), expected = ValueType.EXTENSION, isExtension = true, isTimestamp = true)
-      }
-      forAll { (sec: Long, nano: Int) =>
+    }
+
+    test("timestamp sec/nano") {
+      val posLong = Gen.chooseNum[Long](-31557014167219200L, 31556889864403199L)
+      val posInt  = Gen.chooseNum(0, 1000000000 - 1) // NANOS_PER_SECOND
+      forAll(posLong, posInt) { (sec: Long, nano: Int) =>
         isValid(ValueFactory.newTimestamp(sec, nano), expected = ValueType.EXTENSION, isExtension = true, isTimestamp = true)
       }
     }

From c20c76dcdc2e3d9f0ef1b830718f1190f3cdf492 Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 22:56:45 -0700
Subject: [PATCH 11/21] Use AirSpec for property tests

---
 .../org/msgpack/core/MessagePackSpec.scala    |   6 +-
 .../org/msgpack/core/MessagePackTest.scala    | 964 +++++++++---------
 .../msgpack/core/MessageUnpackerTest.scala    |   3 +-
 3 files changed, 488 insertions(+), 485 deletions(-)

diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
index a13b18480..68779bf57 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
@@ -24,12 +24,14 @@ import xerial.core.util.{TimeReport, Timer}
 
 import scala.language.implicitConversions
 
+object MessagePackSpec {
+  def toHex(arr: Array[Byte]) = arr.map(x => f"$x%02x").mkString(" ")
+}
+
 trait MessagePackSpec extends AnyWordSpec with Matchers with GivenWhenThen with OptionValues with BeforeAndAfter with Benchmark with Logger {
 
   implicit def toTag(s: String): Tag = Tag(s)
 
-  def toHex(arr: Array[Byte]) = arr.map(x => f"$x%02x").mkString(" ")
-
   def createMessagePackData(f: MessagePacker => Unit): Array[Byte] = {
     val b      = new ByteArrayOutputStream()
     val packer = MessagePack.newDefaultPacker(b)
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
index 8abb3cad6..5bc84661b 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
@@ -21,9 +21,13 @@ import java.nio.CharBuffer
 import java.nio.charset.{CodingErrorAction, UnmappableCharacterException}
 import org.msgpack.core.MessagePack.Code
 import org.msgpack.core.MessagePack.{PackerConfig, UnpackerConfig}
+import org.msgpack.core.MessagePackSpec.toHex
 import org.msgpack.value.{Value, Variable}
 import org.scalacheck.{Arbitrary, Gen}
 import org.scalacheck.Prop.{forAll, propBoolean}
+import wvlet.airspec.AirSpec
+import wvlet.airspec.spi.PropertyCheck
+import wvlet.log.io.Timer
 
 import java.time.Instant
 import scala.util.Random
@@ -31,13 +35,13 @@ import scala.util.Random
 /**
   * Created on 2014/05/07.
   */
-class MessagePackTest extends MessagePackSpec {
+class MessagePackTest extends AirSpec with PropertyCheck with Timer {
 
-  def isValidUTF8(s: String) = {
+  private def isValidUTF8(s: String) = {
     MessagePack.UTF8.newEncoder().canEncode(s)
   }
 
-  def containsUnmappableCharacter(s: String): Boolean = {
+  private def containsUnmappableCharacter(s: String): Boolean = {
     try {
       MessagePack.UTF8
         .newEncoder()
@@ -51,589 +55,585 @@ class MessagePackTest extends MessagePackSpec {
     }
   }
 
-  "MessagePack" should {
+  test("clone packer config") {
+    val config = new PackerConfig()
+      .withBufferSize(10)
+      .withBufferFlushThreshold(32 * 1024)
+      .withSmallStringOptimizationThreshold(142)
+    val copy = config.clone()
 
-    "clone packer config" in {
-      val config = new PackerConfig()
-        .withBufferSize(10)
-        .withBufferFlushThreshold(32 * 1024)
-        .withSmallStringOptimizationThreshold(142)
-      val copy = config.clone()
+    copy shouldBe config
+  }
 
-      copy shouldBe config
-    }
+  test("clone unpacker config") {
+    val config = new UnpackerConfig()
+      .withBufferSize(1)
+      .withActionOnMalformedString(CodingErrorAction.IGNORE)
+      .withActionOnUnmappableString(CodingErrorAction.REPORT)
+      .withAllowReadingBinaryAsString(false)
+      .withStringDecoderBufferSize(34)
+      .withStringSizeLimit(4324)
+
+    val copy = config.clone()
+    copy shouldBe config
+  }
 
-    "clone unpacker config" in {
-      val config = new UnpackerConfig()
-        .withBufferSize(1)
-        .withActionOnMalformedString(CodingErrorAction.IGNORE)
-        .withActionOnUnmappableString(CodingErrorAction.REPORT)
-        .withAllowReadingBinaryAsString(false)
-        .withStringDecoderBufferSize(34)
-        .withStringSizeLimit(4324)
+  test("detect fixint values") {
 
-      val copy = config.clone()
-      copy shouldBe config
+    for (i <- 0 until 0x79) {
+      Code.isPosFixInt(i.toByte) shouldBe true
     }
 
-    "detect fixint values" in {
-
-      for (i <- 0 until 0x79) {
-        Code.isPosFixInt(i.toByte) shouldBe true
-      }
-
-      for (i <- 0x80 until 0xFF) {
-        Code.isPosFixInt(i.toByte) shouldBe false
-      }
+    for (i <- 0x80 until 0xFF) {
+      Code.isPosFixInt(i.toByte) shouldBe false
     }
+  }
 
-    "detect fixarray values" in {
-      val packer = MessagePack.newDefaultBufferPacker()
-      packer.packArrayHeader(0)
-      packer.close
-      val bytes = packer.toByteArray
-      MessagePack.newDefaultUnpacker(bytes).unpackArrayHeader() shouldBe 0
-      try {
-        MessagePack.newDefaultUnpacker(bytes).unpackMapHeader()
-        fail("Shouldn't reach here")
-      } catch {
-        case e: MessageTypeException => // OK
-      }
+  test("detect fixarray values") {
+    val packer = MessagePack.newDefaultBufferPacker()
+    packer.packArrayHeader(0)
+    packer.close
+    val bytes = packer.toByteArray
+    MessagePack.newDefaultUnpacker(bytes).unpackArrayHeader() shouldBe 0
+    try {
+      MessagePack.newDefaultUnpacker(bytes).unpackMapHeader()
+      fail("Shouldn't reach here")
+    } catch {
+      case e: MessageTypeException => // OK
     }
+  }
 
-    "detect fixmap values" in {
-      val packer = MessagePack.newDefaultBufferPacker()
-      packer.packMapHeader(0)
-      packer.close
-      val bytes = packer.toByteArray
-      MessagePack.newDefaultUnpacker(bytes).unpackMapHeader() shouldBe 0
-      try {
-        MessagePack.newDefaultUnpacker(bytes).unpackArrayHeader()
-        fail("Shouldn't reach here")
-      } catch {
-        case e: MessageTypeException => // OK
-      }
+  test("detect fixmap values") {
+    val packer = MessagePack.newDefaultBufferPacker()
+    packer.packMapHeader(0)
+    packer.close
+    val bytes = packer.toByteArray
+    MessagePack.newDefaultUnpacker(bytes).unpackMapHeader() shouldBe 0
+    try {
+      MessagePack.newDefaultUnpacker(bytes).unpackArrayHeader()
+      fail("Shouldn't reach here")
+    } catch {
+      case e: MessageTypeException => // OK
     }
+  }
 
-    "detect fixint quickly" in {
+  test("detect fixint quickly") {
 
-      val N   = 100000
-      val idx = (0 until N).map(x => Random.nextInt(256).toByte).toArray[Byte]
+    val N   = 100000
+    val idx = (0 until N).map(x => Random.nextInt(256).toByte).toArray[Byte]
 
-      time("check fixint", repeat = 100) {
+    time("check fixint", repeat = 100) {
 
-        block("mask") {
-          var i     = 0
-          var count = 0
-          while (i < N) {
-            if ((idx(i) & Code.POSFIXINT_MASK) == 0) {
-              count += 1
-            }
-            i += 1
+      block("mask") {
+        var i     = 0
+        var count = 0
+        while (i < N) {
+          if ((idx(i) & Code.POSFIXINT_MASK) == 0) {
+            count += 1
           }
+          i += 1
         }
+      }
 
-        block("mask in func") {
-          var i     = 0
-          var count = 0
-          while (i < N) {
-            if (Code.isPosFixInt(idx(i))) {
-              count += 1
-            }
-            i += 1
+      block("mask in func") {
+        var i     = 0
+        var count = 0
+        while (i < N) {
+          if (Code.isPosFixInt(idx(i))) {
+            count += 1
           }
+          i += 1
         }
+      }
 
-        block("shift cmp") {
-          var i     = 0
-          var count = 0
-          while (i < N) {
-            if ((idx(i) >>> 7) == 0) {
-              count += 1
-            }
-            i += 1
+      block("shift cmp") {
+        var i     = 0
+        var count = 0
+        while (i < N) {
+          if ((idx(i) >>> 7) == 0) {
+            count += 1
           }
-
+          i += 1
         }
 
       }
 
     }
 
-    "detect neg fix int values" in {
-
-      for (i <- 0 until 0xe0) {
-        Code.isNegFixInt(i.toByte) shouldBe false
-      }
+  }
 
-      for (i <- 0xe0 until 0xFF) {
-        Code.isNegFixInt(i.toByte) shouldBe true
-      }
+  test("detect neg fix int values") {
 
+    for (i <- 0 until 0xe0) {
+      Code.isNegFixInt(i.toByte) shouldBe false
     }
 
-    def check[A](
-        v: A,
-        pack: MessagePacker => Unit,
-        unpack: MessageUnpacker => A,
-        packerConfig: PackerConfig = new PackerConfig(),
-        unpackerConfig: UnpackerConfig = new UnpackerConfig()
-    ): Boolean = {
-      var b: Array[Byte] = null
-      try {
-        val bs     = new ByteArrayOutputStream()
-        val packer = packerConfig.newPacker(bs)
-        pack(packer)
-        packer.close()
-
-        b = bs.toByteArray
-
-        val unpacker = unpackerConfig.newUnpacker(b)
-        val ret      = unpack(unpacker)
-        ret shouldBe v
-        true
-      } catch {
-        case e: Exception =>
-          warn(e.getMessage)
-          if (b != null) {
-            warn(s"packed data (size:${b.length}): ${toHex(b)}")
-          }
-          throw e
-      }
+    for (i <- 0xe0 until 0xFF) {
+      Code.isNegFixInt(i.toByte) shouldBe true
     }
 
-    def checkException[A](
-        v: A,
-        pack: MessagePacker => Unit,
-        unpack: MessageUnpacker => A,
-        packerConfig: PackerConfig = new PackerConfig(),
-        unpaackerConfig: UnpackerConfig = new UnpackerConfig()
-    ): Unit = {
-      var b: Array[Byte] = null
-      val bs             = new ByteArrayOutputStream()
-      val packer         = packerConfig.newPacker(bs)
+  }
+
+  private def check[A](
+      v: A,
+      pack: MessagePacker => Unit,
+      unpack: MessageUnpacker => A,
+      packerConfig: PackerConfig = new PackerConfig(),
+      unpackerConfig: UnpackerConfig = new UnpackerConfig()
+  ): Boolean = {
+    var b: Array[Byte] = null
+    try {
+      val bs     = new ByteArrayOutputStream()
+      val packer = packerConfig.newPacker(bs)
       pack(packer)
       packer.close()
 
       b = bs.toByteArray
 
-      val unpacker = unpaackerConfig.newUnpacker(b)
+      val unpacker = unpackerConfig.newUnpacker(b)
       val ret      = unpack(unpacker)
-
-      fail("cannot not reach here")
+      ret shouldBe v
+      true
+    } catch {
+      case e: Exception =>
+        warn(e.getMessage)
+        if (b != null) {
+          warn(s"packed data (size:${b.length}): ${toHex(b)}")
+        }
+        throw e
     }
+  }
 
-    def checkOverflow[A](v: A, pack: MessagePacker => Unit, unpack: MessageUnpacker => A) {
-      try {
-        checkException[A](v, pack, unpack)
-      } catch {
-        case e: MessageIntegerOverflowException => // OK
-      }
-    }
+  private def checkException[A](
+      v: A,
+      pack: MessagePacker => Unit,
+      unpack: MessageUnpacker => A,
+      packerConfig: PackerConfig = new PackerConfig(),
+      unpaackerConfig: UnpackerConfig = new UnpackerConfig()
+  ): Unit = {
+    var b: Array[Byte] = null
+    val bs             = new ByteArrayOutputStream()
+    val packer         = packerConfig.newPacker(bs)
+    pack(packer)
+    packer.close()
+
+    b = bs.toByteArray
+
+    val unpacker = unpaackerConfig.newUnpacker(b)
+    val ret      = unpack(unpacker)
+
+    fail("cannot not reach here")
+  }
 
-    "pack/unpack primitive values" taggedAs ("prim") in {
-      forAll { (v: Boolean) =>
-        check(v, _.packBoolean(v), _.unpackBoolean)
-      }
-      forAll { (v: Byte) =>
-        check(v, _.packByte(v), _.unpackByte)
-      }
-      forAll { (v: Short) =>
-        check(v, _.packShort(v), _.unpackShort)
-      }
-      forAll { (v: Int) =>
-        check(v, _.packInt(v), _.unpackInt)
-      }
-      forAll { (v: Float) =>
-        check(v, _.packFloat(v), _.unpackFloat)
-      }
-      forAll { (v: Long) =>
-        check(v, _.packLong(v), _.unpackLong)
-      }
-      forAll { (v: Double) =>
-        check(v, _.packDouble(v), _.unpackDouble)
-      }
-      check(null, _.packNil, { unpacker =>
-        unpacker.unpackNil(); null
-      })
+  private def checkOverflow[A](v: A, pack: MessagePacker => Unit, unpack: MessageUnpacker => A) {
+    try {
+      checkException[A](v, pack, unpack)
+    } catch {
+      case e: MessageIntegerOverflowException => // OK
     }
+  }
 
-    "skipping a nil value" taggedAs ("try") in {
-      check(true, _.packNil, _.tryUnpackNil)
-      check(false, { packer =>
-        packer.packString("val")
-      }, { unpacker =>
-        unpacker.tryUnpackNil()
-      })
-      check("val", { packer =>
-        packer.packString("val")
-      }, { unpacker =>
-        unpacker.tryUnpackNil(); unpacker.unpackString()
-      })
-      check("val", { packer =>
-        packer.packNil(); packer.packString("val")
-      }, { unpacker =>
-        unpacker.tryUnpackNil(); unpacker.unpackString()
-      })
-      try {
-        checkException(null, { _ =>
-          }, _.tryUnpackNil)
-      } catch {
-        case e: MessageInsufficientBufferException => // OK
-      }
+  test("pack/unpack primitive values") {
+    forAll { (v: Boolean) =>
+      check(v, _.packBoolean(v), _.unpackBoolean)
     }
-
-    "pack/unpack integer values" taggedAs ("int") in {
-      val sampleData = Seq[Long](Int.MinValue.toLong -
-                                   10,
-                                 -65535,
-                                 -8191,
-                                 -1024,
-                                 -255,
-                                 -127,
-                                 -63,
-                                 -31,
-                                 -15,
-                                 -7,
-                                 -3,
-                                 -1,
-                                 0,
-                                 2,
-                                 4,
-                                 8,
-                                 16,
-                                 32,
-                                 64,
-                                 128,
-                                 256,
-                                 1024,
-                                 8192,
-                                 65536,
-                                 Int.MaxValue.toLong + 10)
-      for (v <- sampleData) {
-        check(v, _.packLong(v), _.unpackLong)
-
-        if (v.isValidInt) {
-          val vi = v.toInt
-          check(vi, _.packInt(vi), _.unpackInt)
-        } else {
-          checkOverflow(v, _.packLong(v), _.unpackInt)
-        }
-
-        if (v.isValidShort) {
-          val vi = v.toShort
-          check(vi, _.packShort(vi), _.unpackShort)
-        } else {
-          checkOverflow(v, _.packLong(v), _.unpackShort)
-        }
-
-        if (v.isValidByte) {
-          val vi = v.toByte
-          check(vi, _.packByte(vi), _.unpackByte)
-        } else {
-          checkOverflow(v, _.packLong(v), _.unpackByte)
-        }
-
-      }
-
+    forAll { (v: Byte) =>
+      check(v, _.packByte(v), _.unpackByte)
     }
-
-    "pack/unpack BigInteger" taggedAs ("bi") in {
-      forAll { (a: Long) =>
-        val v = BigInteger.valueOf(a)
-        check(v, _.packBigInteger(v), _.unpackBigInteger)
-      }
-
-      for (bi <- Seq(BigInteger.valueOf(Long.MaxValue).add(BigInteger.valueOf(1)))) {
-        check(bi, _.packBigInteger(bi), _.unpackBigInteger())
-      }
-
-      for (bi <- Seq(BigInteger.valueOf(Long.MaxValue).shiftLeft(10))) {
-        try {
-          checkException(bi, _.packBigInteger(bi), _.unpackBigInteger())
-          fail("cannot reach here")
-        } catch {
-          case e: IllegalArgumentException => // OK
-        }
-      }
-
+    forAll { (v: Short) =>
+      check(v, _.packShort(v), _.unpackShort)
     }
-
-    "pack/unpack strings" taggedAs ("string") in {
-      val utf8Strings = Arbitrary.arbitrary[String].suchThat(isValidUTF8 _)
-      utf8Strings.map { v =>
-        check(v, _.packString(v), _.unpackString)
-      }
+    forAll { (v: Int) =>
+      check(v, _.packInt(v), _.unpackInt)
     }
-
-    "pack/unpack large strings" taggedAs ("large-string") in {
-      // Large string
-      val strLen = Seq(1000, 2000, 10000, 50000, 100000, 500000)
-      for (l <- strLen) {
-        val v: String =
-          Iterator.continually(Random.nextString(l * 10)).find(isValidUTF8).get
-        check(v, _.packString(v), _.unpackString)
-      }
+    forAll { (v: Float) =>
+      check(v, _.packFloat(v), _.unpackFloat)
+    }
+    forAll { (v: Long) =>
+      check(v, _.packLong(v), _.unpackLong)
+    }
+    forAll { (v: Double) =>
+      check(v, _.packDouble(v), _.unpackDouble)
     }
+    check(null, _.packNil, { unpacker =>
+      unpacker.unpackNil(); null
+    })
+  }
 
-    "report errors when packing/unpacking malformed strings" taggedAs ("malformed") in {
-      // TODO produce malformed utf-8 strings in Java8"
-      pending
-      // Create 100 malformed UTF8 Strings
-      val r = new Random(0)
-      val malformedStrings = Iterator
-        .continually {
-          val b = new Array[Byte](10)
-          r.nextBytes(b)
-          b
-        }
-        .filter(b => !isValidUTF8(new String(b)))
-        .take(100)
-
-      for (malformedBytes <- malformedStrings) {
-        // Pack tests
-        val malformed = new String(malformedBytes)
-        try {
-          checkException(malformed, _.packString(malformed), _.unpackString())
-        } catch {
-          case e: MessageStringCodingException => // OK
-        }
+  test("skipping a nil value") {
+    check(true, _.packNil, _.tryUnpackNil)
+    check(false, { packer =>
+      packer.packString("val")
+    }, { unpacker =>
+      unpacker.tryUnpackNil()
+    })
+    check("val", { packer =>
+      packer.packString("val")
+    }, { unpacker =>
+      unpacker.tryUnpackNil(); unpacker.unpackString()
+    })
+    check("val", { packer =>
+      packer.packNil(); packer.packString("val")
+    }, { unpacker =>
+      unpacker.tryUnpackNil(); unpacker.unpackString()
+    })
+    try {
+      checkException(null, { _ =>
+        }, _.tryUnpackNil)
+    } catch {
+      case e: MessageInsufficientBufferException => // OK
+    }
+  }
 
-        try {
-          checkException(malformed, { packer =>
-            packer.packRawStringHeader(malformedBytes.length)
-            packer.writePayload(malformedBytes)
-          }, _.unpackString())
-        } catch {
-          case e: MessageStringCodingException => // OK
-        }
+  test("pack/unpack integer values") {
+    val sampleData = Seq[Long](Int.MinValue.toLong -
+                                 10,
+                               -65535,
+                               -8191,
+                               -1024,
+                               -255,
+                               -127,
+                               -63,
+                               -31,
+                               -15,
+                               -7,
+                               -3,
+                               -1,
+                               0,
+                               2,
+                               4,
+                               8,
+                               16,
+                               32,
+                               64,
+                               128,
+                               256,
+                               1024,
+                               8192,
+                               65536,
+                               Int.MaxValue.toLong + 10)
+    for (v <- sampleData) {
+      check(v, _.packLong(v), _.unpackLong)
+
+      if (v.isValidInt) {
+        val vi = v.toInt
+        check(vi, _.packInt(vi), _.unpackInt)
+      } else {
+        checkOverflow(v, _.packLong(v), _.unpackInt)
+      }
+
+      if (v.isValidShort) {
+        val vi = v.toShort
+        check(vi, _.packShort(vi), _.unpackShort)
+      } else {
+        checkOverflow(v, _.packLong(v), _.unpackShort)
+      }
+
+      if (v.isValidByte) {
+        val vi = v.toByte
+        check(vi, _.packByte(vi), _.unpackByte)
+      } else {
+        checkOverflow(v, _.packLong(v), _.unpackByte)
       }
+
     }
 
-    "report errors when packing/unpacking strings that contain unmappable characters" taggedAs ("unmap") in {
+  }
 
-      val unmappable = Array[Byte](0xfc.toByte, 0x0a.toByte)
-      //val unmappableChar = Array[Char](new Character(0xfc0a).toChar)
+  test("pack/unpack BigInteger") {
+    forAll { (a: Long) =>
+      val v = BigInteger.valueOf(a)
+      check(v, _.packBigInteger(v), _.unpackBigInteger)
+    }
 
-      // Report error on unmappable character
-      val unpackerConfig = new UnpackerConfig()
-        .withActionOnMalformedString(CodingErrorAction.REPORT)
-        .withActionOnUnmappableString(CodingErrorAction.REPORT)
+    for (bi <- Seq(BigInteger.valueOf(Long.MaxValue).add(BigInteger.valueOf(1)))) {
+      check(bi, _.packBigInteger(bi), _.unpackBigInteger())
+    }
 
-      for (bytes <- Seq(unmappable)) {
-        When("unpacking")
-        try {
-          checkException(bytes, { packer =>
-            packer.packRawStringHeader(bytes.length)
-            packer.writePayload(bytes)
-          }, _.unpackString(), new PackerConfig(), unpackerConfig)
-        } catch {
-          case e: MessageStringCodingException => // OK
-        }
+    for (bi <- Seq(BigInteger.valueOf(Long.MaxValue).shiftLeft(10))) {
+      try {
+        checkException(bi, _.packBigInteger(bi), _.unpackBigInteger())
+        fail("cannot reach here")
+      } catch {
+        case e: IllegalArgumentException => // OK
       }
     }
 
-    "pack/unpack binary" taggedAs ("binary") in {
-      forAll { (v: Array[Byte]) =>
-        check(
-          v, { packer =>
-            packer.packBinaryHeader(v.length); packer.writePayload(v)
-          }, { unpacker =>
-            val len = unpacker.unpackBinaryHeader()
-            val out = new Array[Byte](len)
-            unpacker.readPayload(out, 0, len)
-            out
-          }
-        )
-      }
+  }
 
-      val len = Seq(1000, 2000, 10000, 50000, 100000, 500000)
-      for (l <- len) {
-        val v = new Array[Byte](l)
-        Random.nextBytes(v)
-        check(
-          v, { packer =>
-            packer.packBinaryHeader(v.length); packer.writePayload(v)
-          }, { unpacker =>
-            val len = unpacker.unpackBinaryHeader()
-            val out = new Array[Byte](len)
-            unpacker.readPayload(out, 0, len)
-            out
-          }
-        )
-      }
+  test("pack/unpack strings") {
+    val utf8Strings = Arbitrary.arbitrary[String].suchThat(isValidUTF8 _)
+    utf8Strings.map { v =>
+      check(v, _.packString(v), _.unpackString)
     }
+  }
 
-    val testHeaderLength = Seq(1, 2, 4, 8, 16, 17, 32, 64, 255, 256, 1000, 2000, 10000, 50000, 100000, 500000)
-
-    "pack/unpack arrays" taggedAs ("array") in {
-      forAll { (v: Array[Int]) =>
-        check(
-          v, { packer =>
-            packer.packArrayHeader(v.length)
-            v.map(packer.packInt(_))
-          }, { unpacker =>
-            val len = unpacker.unpackArrayHeader()
-            val out = new Array[Int](len)
-            for (i <- 0 until v.length) {
-              out(i) = unpacker.unpackInt
-            }
-            out
-          }
-        )
-      }
+  test("pack/unpack large strings") {
+    // Large string
+    val strLen = Seq(1000, 2000, 10000, 50000, 100000, 500000)
+    for (l <- strLen) {
+      val v: String =
+        Iterator.continually(Random.nextString(l * 10)).find(isValidUTF8).get
+      check(v, _.packString(v), _.unpackString)
+    }
+  }
 
-      for (l <- testHeaderLength) {
-        check(l, _.packArrayHeader(l), _.unpackArrayHeader())
+  test("report errors when packing/unpacking malformed strings") {
+    // TODO produce malformed utf-8 strings in Java8"
+    pending
+    // Create 100 malformed UTF8 Strings
+    val r = new Random(0)
+    val malformedStrings = Iterator
+      .continually {
+        val b = new Array[Byte](10)
+        r.nextBytes(b)
+        b
+      }
+      .filter(b => !isValidUTF8(new String(b)))
+      .take(100)
+
+    for (malformedBytes <- malformedStrings) {
+      // Pack tests
+      val malformed = new String(malformedBytes)
+      try {
+        checkException(malformed, _.packString(malformed), _.unpackString())
+      } catch {
+        case e: MessageStringCodingException => // OK
       }
 
       try {
-        checkException(0, _.packArrayHeader(-1), _.unpackArrayHeader)
+        checkException(malformed, { packer =>
+          packer.packRawStringHeader(malformedBytes.length)
+          packer.writePayload(malformedBytes)
+        }, _.unpackString())
       } catch {
-        case e: IllegalArgumentException => // OK
+        case e: MessageStringCodingException => // OK
       }
-
     }
+  }
 
-    "pack/unpack maps" taggedAs ("map") in {
-      forAll { (v: Array[Int]) =>
-        val m = v.map(i => (i, i.toString))
-
-        check(
-          m, { packer =>
-            packer.packMapHeader(v.length)
-            m.map {
-              case (k: Int, v: String) =>
-                packer.packInt(k)
-                packer.packString(v)
-            }
-          }, { unpacker =>
-            val len = unpacker.unpackMapHeader()
-            val b   = Seq.newBuilder[(Int, String)]
-            for (i <- 0 until len) {
-              b += ((unpacker.unpackInt, unpacker.unpackString))
-            }
-            b.result
-          }
-        )
-      }
+  test("report errors when packing/unpacking strings that contain unmappable characters") {
 
-      for (l <- testHeaderLength) {
-        check(l, _.packMapHeader(l), _.unpackMapHeader())
-      }
+    val unmappable = Array[Byte](0xfc.toByte, 0x0a.toByte)
+    //val unmappableChar = Array[Char](new Character(0xfc0a).toChar)
 
+    // Report error on unmappable character
+    val unpackerConfig = new UnpackerConfig()
+      .withActionOnMalformedString(CodingErrorAction.REPORT)
+      .withActionOnUnmappableString(CodingErrorAction.REPORT)
+
+    for (bytes <- Seq(unmappable)) {
       try {
-        checkException(0, _.packMapHeader(-1), _.unpackMapHeader)
+        checkException(bytes, { packer =>
+          packer.packRawStringHeader(bytes.length)
+          packer.writePayload(bytes)
+        }, _.unpackString(), new PackerConfig(), unpackerConfig)
       } catch {
-        case e: IllegalArgumentException => // OK
+        case e: MessageStringCodingException => // OK
       }
+    }
+  }
 
+  test("pack/unpack binary") {
+    forAll { (v: Array[Byte]) =>
+      check(
+        v, { packer =>
+          packer.packBinaryHeader(v.length); packer.writePayload(v)
+        }, { unpacker =>
+          val len = unpacker.unpackBinaryHeader()
+          val out = new Array[Byte](len)
+          unpacker.readPayload(out, 0, len)
+          out
+        }
+      )
     }
 
-    "pack/unpack extension types" taggedAs ("ext") in {
-      forAll { (dataLen: Int, tpe: Byte) =>
-        val l = Math.abs(dataLen)
-        l >= 0 ==> {
-          val ext =
-            new ExtensionTypeHeader(ExtensionTypeHeader.checkedCastToByte(tpe), l)
-          check(ext, _.packExtensionTypeHeader(ext.getType, ext.getLength), _.unpackExtensionTypeHeader())
+    val len = Seq(1000, 2000, 10000, 50000, 100000, 500000)
+    for (l <- len) {
+      val v = new Array[Byte](l)
+      Random.nextBytes(v)
+      check(
+        v, { packer =>
+          packer.packBinaryHeader(v.length); packer.writePayload(v)
+        }, { unpacker =>
+          val len = unpacker.unpackBinaryHeader()
+          val out = new Array[Byte](len)
+          unpacker.readPayload(out, 0, len)
+          out
         }
-      }
+      )
+    }
+  }
 
-      for (l <- testHeaderLength) {
-        val ext = new ExtensionTypeHeader(ExtensionTypeHeader.checkedCastToByte(Random.nextInt(128)), l)
-        check(ext, _.packExtensionTypeHeader(ext.getType, ext.getLength), _.unpackExtensionTypeHeader())
-      }
+  val testHeaderLength = Seq(1, 2, 4, 8, 16, 17, 32, 64, 255, 256, 1000, 2000, 10000, 50000, 100000, 500000)
 
+  test("pack/unpack arrays") {
+    forAll { (v: Array[Int]) =>
+      check(
+        v, { packer =>
+          packer.packArrayHeader(v.length)
+          v.map(packer.packInt(_))
+        }, { unpacker =>
+          val len = unpacker.unpackArrayHeader()
+          val out = new Array[Int](len)
+          for (i <- 0 until v.length) {
+            out(i) = unpacker.unpackInt
+          }
+          out
+        }
+      )
+    }
+
+    for (l <- testHeaderLength) {
+      check(l, _.packArrayHeader(l), _.unpackArrayHeader())
+    }
+
+    try {
+      checkException(0, _.packArrayHeader(-1), _.unpackArrayHeader)
+    } catch {
+      case e: IllegalArgumentException => // OK
     }
 
-    "pack/unpack maps in lists" in {
-      val aMap = List(Map("f" -> "x"))
+  }
+
+  test("pack/unpack maps") {
+    forAll { (v: Array[Int]) =>
+      val m = v.map(i => (i, i.toString)).toSeq
 
       check(
-        aMap, { packer =>
-          packer.packArrayHeader(aMap.size)
-          for (m <- aMap) {
-            packer.packMapHeader(m.size)
-            for ((k, v) <- m) {
-              packer.packString(k)
+        m, { packer =>
+          packer.packMapHeader(v.length)
+          m.map {
+            case (k: Int, v: String) =>
+              packer.packInt(k)
               packer.packString(v)
-            }
           }
         }, { unpacker =>
-          val v = new Variable()
-          unpacker.unpackValue(v)
-          import scala.collection.JavaConverters._
-          v.asArrayValue().asScala
-            .map { m =>
-              val mv  = m.asMapValue()
-              val kvs = mv.getKeyValueArray
-
-              kvs
-                .grouped(2)
-                .map({ kvp: Array[Value] =>
-                  val k = kvp(0)
-                  val v = kvp(1)
-
-                  (k.asStringValue().asString, v.asStringValue().asString)
-                })
-                .toMap
-            }
-            .toList
+          val len = unpacker.unpackMapHeader()
+          val b   = Seq.newBuilder[(Int, String)]
+          for (i <- 0 until len) {
+            b += ((unpacker.unpackInt, unpacker.unpackString))
+          }
+          b.result
         }
       )
     }
 
-    "pack/unpack timestamp values" in {
-      val posLong = Gen.chooseNum[Long](-31557014167219200L, 31556889864403199L)
-      val posInt  = Gen.chooseNum(0, 1000000000 - 1) // NANOS_PER_SECOND
-      forAll(posLong, posInt) { (second: Long, nano: Int) =>
-        val v = Instant.ofEpochSecond(second, nano)
-        check(v, { _.packTimestamp(v) }, { _.unpackTimestamp() })
-      }
-      // Using different insterfaces
-      forAll(posLong, posInt) { (second: Long, nano: Int) =>
-        val v = Instant.ofEpochSecond(second, nano)
-        check(v, { _.packTimestamp(second, nano) }, { _.unpackTimestamp() })
-      }
-      val secLessThan34bits = Gen.chooseNum[Long](0, 1L << 34)
-      forAll(secLessThan34bits, posInt) { (second: Long, nano: Int) =>
-        val v = Instant.ofEpochSecond(second, nano)
-        check(v, _.packTimestamp(v), _.unpackTimestamp())
-      }
-      forAll(secLessThan34bits, posInt) { (second: Long, nano: Int) =>
-        val v = Instant.ofEpochSecond(second, nano)
-        check(v, _.packTimestamp(second, nano), _.unpackTimestamp())
-      }
+    for (l <- testHeaderLength) {
+      check(l, _.packMapHeader(l), _.unpackMapHeader())
+    }
+
+    try {
+      checkException(0, _.packMapHeader(-1), _.unpackMapHeader)
+    } catch {
+      case e: IllegalArgumentException => // OK
+    }
+
+  }
 
-      // Corner-cases around uint32 boundaries
-      for (v <- Seq(
-             Instant.ofEpochSecond(Instant.now().getEpochSecond, 123456789L), // uint32 nanoseq (out of int32 range)
-             Instant.ofEpochSecond(-1302749144L, 0), // 1928-09-19T21:14:16Z
-             Instant.ofEpochSecond(-747359729L, 0), // 1946-04-27T00:04:31Z
-             Instant.ofEpochSecond(4257387427L, 0) // 2104-11-29T07:37:07Z
-           )) {
-        check(v, _.packTimestamp(v), _.unpackTimestamp())
+  test("pack/unpack extension types") {
+    forAll { (dataLen: Int, tpe: Byte) =>
+      val l = Math.abs(dataLen)
+      l >= 0 ==> {
+        val ext =
+          new ExtensionTypeHeader(ExtensionTypeHeader.checkedCastToByte(tpe), l)
+        check(ext, _.packExtensionTypeHeader(ext.getType, ext.getLength), _.unpackExtensionTypeHeader())
       }
     }
 
-    "pack/unpack timestamp in millis" in {
-      val posLong = Gen.chooseNum[Long](-31557014167219200L, 31556889864403199L)
-      forAll(posLong) { (millis: Long) =>
-        val v = Instant.ofEpochMilli(millis)
-        check(v, { _.packTimestamp(millis) }, { _.unpackTimestamp() })
+    for (l <- testHeaderLength) {
+      val ext = new ExtensionTypeHeader(ExtensionTypeHeader.checkedCastToByte(Random.nextInt(128)), l)
+      check(ext, _.packExtensionTypeHeader(ext.getType, ext.getLength), _.unpackExtensionTypeHeader())
+    }
+
+  }
+
+  test("pack/unpack maps in lists") {
+    val aMap = List(Map("f" -> "x"))
+
+    check(
+      aMap, { packer =>
+        packer.packArrayHeader(aMap.size)
+        for (m <- aMap) {
+          packer.packMapHeader(m.size)
+          for ((k, v) <- m) {
+            packer.packString(k)
+            packer.packString(v)
+          }
+        }
+      }, { unpacker =>
+        val v = new Variable()
+        unpacker.unpackValue(v)
+        import scala.collection.JavaConverters._
+        v.asArrayValue().asScala
+          .map { m =>
+            val mv  = m.asMapValue()
+            val kvs = mv.getKeyValueArray
+
+            kvs
+              .grouped(2)
+              .map({ kvp: Array[Value] =>
+                val k = kvp(0)
+                val v = kvp(1)
+
+                (k.asStringValue().asString, v.asStringValue().asString)
+              })
+              .toMap
+          }
+          .toList
       }
+    )
+  }
+
+  test("pack/unpack timestamp values") {
+    val posLong = Gen.chooseNum[Long](-31557014167219200L, 31556889864403199L)
+    val posInt  = Gen.chooseNum(0, 1000000000 - 1) // NANOS_PER_SECOND
+    forAll(posLong, posInt) { (second: Long, nano: Int) =>
+      val v = Instant.ofEpochSecond(second, nano)
+      check(v, { _.packTimestamp(v) }, { _.unpackTimestamp() })
+    }
+    // Using different insterfaces
+    forAll(posLong, posInt) { (second: Long, nano: Int) =>
+      val v = Instant.ofEpochSecond(second, nano)
+      check(v, { _.packTimestamp(second, nano) }, { _.unpackTimestamp() })
+    }
+    val secLessThan34bits = Gen.chooseNum[Long](0, 1L << 34)
+    forAll(secLessThan34bits, posInt) { (second: Long, nano: Int) =>
+      val v = Instant.ofEpochSecond(second, nano)
+      check(v, _.packTimestamp(v), _.unpackTimestamp())
+    }
+    forAll(secLessThan34bits, posInt) { (second: Long, nano: Int) =>
+      val v = Instant.ofEpochSecond(second, nano)
+      check(v, _.packTimestamp(second, nano), _.unpackTimestamp())
+    }
+
+    // Corner-cases around uint32 boundaries
+    for (v <- Seq(
+           Instant.ofEpochSecond(Instant.now().getEpochSecond, 123456789L), // uint32 nanoseq (out of int32 range)
+           Instant.ofEpochSecond(-1302749144L, 0), // 1928-09-19T21:14:16Z
+           Instant.ofEpochSecond(-747359729L, 0), // 1946-04-27T00:04:31Z
+           Instant.ofEpochSecond(4257387427L, 0) // 2104-11-29T07:37:07Z
+         )) {
+      check(v, _.packTimestamp(v), _.unpackTimestamp())
+    }
+  }
+
+  test("pack/unpack timestamp in millis") {
+    val posLong = Gen.chooseNum[Long](-31557014167219200L, 31556889864403199L)
+    forAll(posLong) { (millis: Long) =>
+      val v = Instant.ofEpochMilli(millis)
+      check(v, { _.packTimestamp(millis) }, { _.unpackTimestamp() })
     }
   }
 
-  "MessagePack.PackerConfig" should {
-    "be immutable" in {
+  test("MessagePack.PackerConfig") {
+    test("should be immutable") {
       val a = new MessagePack.PackerConfig()
       val b = a.withBufferSize(64 * 1024)
       a.equals(b) shouldBe false
     }
 
-    "implement equals" in {
+    test("should implement equals") {
       val a = new MessagePack.PackerConfig()
       val b = new MessagePack.PackerConfig()
       a.equals(b) shouldBe true
@@ -643,14 +643,14 @@ class MessagePackTest extends MessagePackSpec {
     }
   }
 
-  "MessagePack.UnpackerConfig" should {
-    "be immutable" in {
+  test("MessagePack.UnpackerConfig") {
+    test("should be immutable") {
       val a = new MessagePack.UnpackerConfig()
       val b = a.withBufferSize(64 * 1024)
       a.equals(b) shouldBe false
     }
 
-    "implement equals" in {
+    test("implement equals") {
       val a = new MessagePack.UnpackerConfig()
       val b = new MessagePack.UnpackerConfig()
       a.equals(b) shouldBe true
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
index c2c738515..449bcc97e 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
@@ -15,10 +15,11 @@
 //
 package org.msgpack.core
 
+import org.msgpack.core.MessagePackSpec.toHex
+
 import java.io._
 import java.nio.ByteBuffer
 import java.util.Collections
-
 import org.msgpack.core.buffer._
 import org.msgpack.value.ValueType
 import xerial.core.io.IOUtil._

From 1e4c7a30c99fadc5928946a64ca0003e9c4d5434 Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 23:06:14 -0700
Subject: [PATCH 12/21] Migrate to AirSpec

---
 .../msgpack/core/InvalidDataReadTest.scala    |  29 +++--
 .../org/msgpack/core/MessagePackSpec.scala    |  12 +-
 .../msgpack/core/MessageUnpackerTest.scala    | 123 +++++++++---------
 .../msgpack/core/buffer/ByteStringTest.scala  |   1 +
 .../scala/org/msgpack/value/ValueTest.scala   |  20 +--
 5 files changed, 97 insertions(+), 88 deletions(-)

diff --git a/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala
index 4950da82a..42e06793f 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala
@@ -1,23 +1,24 @@
 package org.msgpack.core
 
+import org.msgpack.core.MessagePackSpec.createMessagePackData
+
 /**
- *
- */
+  *
+  */
 class InvalidDataReadTest extends MessagePackSpec {
 
   "Reading long EXT32" in {
-      // Prepare an EXT32 data with 2GB (Int.MaxValue size) payload for testing the behavior of MessageUnpacker.skipValue()
-      // Actually preparing 2GB of data, however, is too much for CI, so we create only the header part.
-      val msgpack = createMessagePackData(p => p.packExtensionTypeHeader(MessagePack.Code.EXT32, Int.MaxValue))
-      val u = MessagePack.newDefaultUnpacker(msgpack)
-      try {
-        // This error will be thrown after reading the header as the input has no EXT32 body
-        intercept[MessageInsufficientBufferException] {
-          u.skipValue()
-        }
-      }
-      finally  {
-        u.close()
+    // Prepare an EXT32 data with 2GB (Int.MaxValue size) payload for testing the behavior of MessageUnpacker.skipValue()
+    // Actually preparing 2GB of data, however, is too much for CI, so we create only the header part.
+    val msgpack = createMessagePackData(p => p.packExtensionTypeHeader(MessagePack.Code.EXT32, Int.MaxValue))
+    val u       = MessagePack.newDefaultUnpacker(msgpack)
+    try {
+      // This error will be thrown after reading the header as the input has no EXT32 body
+      intercept[MessageInsufficientBufferException] {
+        u.skipValue()
       }
+    } finally {
+      u.close()
+    }
   }
 }
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
index 68779bf57..06edd4a0a 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
@@ -26,12 +26,6 @@ import scala.language.implicitConversions
 
 object MessagePackSpec {
   def toHex(arr: Array[Byte]) = arr.map(x => f"$x%02x").mkString(" ")
-}
-
-trait MessagePackSpec extends AnyWordSpec with Matchers with GivenWhenThen with OptionValues with BeforeAndAfter with Benchmark with Logger {
-
-  implicit def toTag(s: String): Tag = Tag(s)
-
   def createMessagePackData(f: MessagePacker => Unit): Array[Byte] = {
     val b      = new ByteArrayOutputStream()
     val packer = MessagePack.newDefaultPacker(b)
@@ -41,6 +35,12 @@ trait MessagePackSpec extends AnyWordSpec with Matchers with GivenWhenThen with
   }
 }
 
+trait MessagePackSpec extends AnyWordSpec with Matchers with GivenWhenThen with OptionValues with BeforeAndAfter with Benchmark with Logger {
+
+  implicit def toTag(s: String): Tag = Tag(s)
+
+}
+
 trait Benchmark extends Timer {
 
   val numWarmUpRuns = 10
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
index 449bcc97e..665792286 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
@@ -15,13 +15,16 @@
 //
 package org.msgpack.core
 
-import org.msgpack.core.MessagePackSpec.toHex
+import org.msgpack.core.MessagePackSpec.{createMessagePackData, toHex}
 
 import java.io._
 import java.nio.ByteBuffer
 import java.util.Collections
 import org.msgpack.core.buffer._
 import org.msgpack.value.ValueType
+import wvlet.airspec.AirSpec
+import wvlet.log.LogSupport
+import wvlet.log.io.Timer
 import xerial.core.io.IOUtil._
 
 import scala.collection.JavaConverters._
@@ -46,10 +49,10 @@ object MessageUnpackerTest {
 
 import MessageUnpackerTest._
 
-class MessageUnpackerTest extends MessagePackSpec {
+class MessageUnpackerTest extends AirSpec with Timer {
 
-  val universal = MessageBuffer.allocate(0).isInstanceOf[MessageBufferU]
-  def testData: Array[Byte] = {
+  private val universal = MessageBuffer.allocate(0).isInstanceOf[MessageBufferU]
+  private def testData: Array[Byte] = {
     val out    = new ByteArrayOutputStream()
     val packer = MessagePack.newDefaultPacker(out)
 
@@ -69,9 +72,9 @@ class MessageUnpackerTest extends MessagePackSpec {
     arr
   }
 
-  val intSeq = (for (i <- 0 until 100) yield Random.nextInt()).toArray[Int]
+  private val intSeq = (for (i <- 0 until 100) yield Random.nextInt()).toArray[Int]
 
-  def testData2: Array[Byte] = {
+  private def testData2: Array[Byte] = {
     val out    = new ByteArrayOutputStream()
     val packer = MessagePack.newDefaultPacker(out);
 
@@ -87,7 +90,7 @@ class MessageUnpackerTest extends MessagePackSpec {
     arr
   }
 
-  def write(packer: MessagePacker, r: Random) {
+  private def write(packer: MessagePacker, r: Random) {
     val tpeIndex = Iterator
       .continually(r.nextInt(MessageFormat.values().length))
       .find(_ != MessageFormat.NEVER_USED.ordinal())
@@ -143,7 +146,7 @@ class MessageUnpackerTest extends MessagePackSpec {
     }
   }
 
-  def testData3(N: Int): Array[Byte] = {
+  private def testData3(N: Int): Array[Byte] = {
 
     val out    = new ByteArrayOutputStream()
     val packer = MessagePack.newDefaultPacker(out)
@@ -161,7 +164,7 @@ class MessageUnpackerTest extends MessagePackSpec {
     arr
   }
 
-  def readValue(unpacker: MessageUnpacker) {
+  private def readValue(unpacker: MessageUnpacker) {
     val f = unpacker.getNextFormat()
     f.getValueType match {
       case ValueType.ARRAY =>
@@ -182,7 +185,7 @@ class MessageUnpackerTest extends MessagePackSpec {
     }
   }
 
-  def createTempFile = {
+  private def createTempFile = {
     val f = File.createTempFile("msgpackTest", "msgpack")
     f.deleteOnExit
     val p = MessagePack.newDefaultPacker(new FileOutputStream(f))
@@ -191,12 +194,12 @@ class MessageUnpackerTest extends MessagePackSpec {
     f
   }
 
-  def checkFile(u: MessageUnpacker) = {
+  private def checkFile(u: MessageUnpacker) = {
     u.unpackInt shouldBe 99
     u.hasNext shouldBe false
   }
 
-  def unpackers(data: Array[Byte]): Seq[MessageUnpacker] = {
+  private def unpackers(data: Array[Byte]): Seq[MessageUnpacker] = {
     val bb = ByteBuffer.allocate(data.length)
     val db = ByteBuffer.allocateDirect(data.length)
     bb.put(data).flip()
@@ -211,7 +214,7 @@ class MessageUnpackerTest extends MessagePackSpec {
     builder.result()
   }
 
-  def unpackerCollectionWithVariousBuffers(data: Array[Byte], chunkSize: Int): Seq[MessageUnpacker] = {
+  private def unpackerCollectionWithVariousBuffers(data: Array[Byte], chunkSize: Int): Seq[MessageUnpacker] = {
     val seqBytes         = Seq.newBuilder[MessageBufferInput]
     val seqByteBuffers   = Seq.newBuilder[MessageBufferInput]
     val seqDirectBuffers = Seq.newBuilder[MessageBufferInput]
@@ -239,9 +242,9 @@ class MessageUnpackerTest extends MessagePackSpec {
     builder.result()
   }
 
-  "MessageUnpacker" should {
+  test("MessageUnpacker") {
 
-    "parse message packed data" taggedAs ("unpack") in {
+    test("parse message packed data") {
       val arr = testData
 
       for (unpacker <- unpackers(arr)) {
@@ -256,7 +259,7 @@ class MessageUnpackerTest extends MessagePackSpec {
       }
     }
 
-    "skip reading values" in {
+    test("skip reading values") {
 
       for (unpacker <- unpackers(testData)) {
         var skipCount = 0
@@ -270,7 +273,7 @@ class MessageUnpackerTest extends MessagePackSpec {
       }
     }
 
-    "compare skip performance" taggedAs ("skip") in {
+    test("compare skip performance") {
       val N    = 10000
       val data = testData3(N)
 
@@ -298,7 +301,7 @@ class MessageUnpackerTest extends MessagePackSpec {
 
     }
 
-    "parse int data" in {
+    test("parse int data") {
 
       debug(intSeq.mkString(", "))
 
@@ -326,9 +329,8 @@ class MessageUnpackerTest extends MessagePackSpec {
 
     }
 
-    "read data at the buffer boundary" taggedAs ("boundary") in {
-
-      trait SplitTest {
+    test("read data at the buffer boundary") {
+      trait SplitTest extends LogSupport {
         val data: Array[Byte]
         def run {
           for (unpacker <- unpackers(data)) {
@@ -363,7 +365,7 @@ class MessageUnpackerTest extends MessagePackSpec {
       new SplitTest { val data = testData3(30) }.run
     }
 
-    "read integer at MessageBuffer boundaries" taggedAs ("integer-buffer-boundary") in {
+    test("read integer at MessageBuffer boundaries") {
       val packer = MessagePack.newDefaultBufferPacker()
       (0 until 1170).foreach { i =>
         packer.packLong(0x0011223344556677L)
@@ -386,7 +388,7 @@ class MessageUnpackerTest extends MessagePackSpec {
       }
     }
 
-    "read string at MessageBuffer boundaries" taggedAs ("string-buffer-boundary") in {
+    test("read string at MessageBuffer boundaries") {
       val packer = MessagePack.newDefaultBufferPacker()
       (0 until 1170).foreach { i =>
         packer.packString("hello world")
@@ -409,7 +411,7 @@ class MessageUnpackerTest extends MessagePackSpec {
       }
     }
 
-    "be faster than msgpack-v6 skip" taggedAs ("cmp-skip") in {
+    test("be faster than msgpack-v6 skip") {
 
       trait Fixture {
         val unpacker: MessageUnpacker
@@ -467,15 +469,16 @@ class MessageUnpackerTest extends MessagePackSpec {
         }
       }
 
-      t("v7-array").averageWithoutMinMax should be <= t("v6").averageWithoutMinMax
-      t("v7-array-buffer").averageWithoutMinMax should be <= t("v6").averageWithoutMinMax
-      if (!universal)
-        t("v7-direct-buffer").averageWithoutMinMax should be <= t("v6").averageWithoutMinMax
+      t("v7-array").averageWithoutMinMax <= t("v6").averageWithoutMinMax shouldBe true
+      t("v7-array-buffer").averageWithoutMinMax <= t("v6").averageWithoutMinMax shouldBe true
+      if (!universal) {
+        t("v7-direct-buffer").averageWithoutMinMax <= t("v6").averageWithoutMinMax shouldBe true
+      }
     }
 
     import org.msgpack.`type`.{ValueType => ValueTypeV6}
 
-    "be faster than msgpack-v6 read value" taggedAs ("cmp-unpack") in {
+    test("be faster than msgpack-v6 read value") {
 
       def readValueV6(unpacker: org.msgpack.unpacker.MessagePackUnpacker) {
         val vt = unpacker.getNextType()
@@ -599,12 +602,12 @@ class MessageUnpackerTest extends MessagePackSpec {
       if (t("v7-array-buffer").averageWithoutMinMax > t("v6").averageWithoutMinMax) {
         warn(s"v7-array-buffer ${t("v7-array-buffer").averageWithoutMinMax} is slower than v6 ${t("v6").averageWithoutMinMax}")
       }
-      if (!universal)
-        t("v7-direct-buffer").averageWithoutMinMax should be <= t("v6").averageWithoutMinMax
-
+      if (!universal) {
+        t("v7-direct-buffer").averageWithoutMinMax <= t("v6").averageWithoutMinMax shouldBe true
+      }
     }
 
-    "be faster for reading binary than v6" taggedAs ("cmp-binary") in {
+    test("be faster for reading binary than v6") {
 
       val bos    = new ByteArrayOutputStream()
       val packer = MessagePack.newDefaultPacker(bos)
@@ -703,37 +706,37 @@ class MessageUnpackerTest extends MessagePackSpec {
       }
     }
 
-    "read payload as a reference" taggedAs ("ref") in {
+    test("read payload as a reference") {
 
       val dataSizes =
         Seq(0, 1, 5, 8, 16, 32, 128, 256, 1024, 2000, 10000, 100000)
 
       for (s <- dataSizes) {
-        When(f"data size is $s%,d")
-        val data = new Array[Byte](s)
-        Random.nextBytes(data)
-        val b      = new ByteArrayOutputStream()
-        val packer = MessagePack.newDefaultPacker(b)
-        packer.packBinaryHeader(s)
-        packer.writePayload(data)
-        packer.close()
-
-        for (unpacker <- unpackers(b.toByteArray)) {
-          val len = unpacker.unpackBinaryHeader()
-          len shouldBe s
-          val ref = unpacker.readPayloadAsReference(len)
-          unpacker.close()
-          ref.size() shouldBe s
-          val stored = new Array[Byte](len)
-          ref.getBytes(0, stored, 0, len)
+        test(f"data size is $s%,d") {
+          val data = new Array[Byte](s)
+          Random.nextBytes(data)
+          val b      = new ByteArrayOutputStream()
+          val packer = MessagePack.newDefaultPacker(b)
+          packer.packBinaryHeader(s)
+          packer.writePayload(data)
+          packer.close()
+
+          for (unpacker <- unpackers(b.toByteArray)) {
+            val len = unpacker.unpackBinaryHeader()
+            len shouldBe s
+            val ref = unpacker.readPayloadAsReference(len)
+            unpacker.close()
+            ref.size() shouldBe s
+            val stored = new Array[Byte](len)
+            ref.getBytes(0, stored, 0, len)
 
-          stored shouldBe data
+            stored shouldBe data
+          }
         }
       }
-
     }
 
-    "reset the internal states" taggedAs ("reset") in {
+    test("reset the internal states") {
 
       val data = intSeq
       val b    = createMessagePackData(packer => data foreach packer.packInt)
@@ -770,7 +773,7 @@ class MessageUnpackerTest extends MessagePackSpec {
 
     }
 
-    "improve the performance via reset method" taggedAs ("reset-arr") in {
+    test("improve the performance via reset method") {
 
       val out    = new ByteArrayOutputStream
       val packer = MessagePack.newDefaultPacker(out)
@@ -822,7 +825,7 @@ class MessageUnpackerTest extends MessagePackSpec {
       // t("reuse-array-input").averageWithoutMinMax should be <= t("no-buffer-reset").averageWithoutMinMax
     }
 
-    "reset ChannelBufferInput" in {
+    test("reset ChannelBufferInput") {
       val f0 = createTempFile
       val u  = MessagePack.newDefaultUnpacker(new FileInputStream(f0).getChannel)
       checkFile(u)
@@ -834,7 +837,7 @@ class MessageUnpackerTest extends MessagePackSpec {
       u.close
     }
 
-    "reset InputStreamBufferInput" in {
+    test("reset InputStreamBufferInput") {
       val f0 = createTempFile
       val u  = MessagePack.newDefaultUnpacker(new FileInputStream(f0))
       checkFile(u)
@@ -846,7 +849,7 @@ class MessageUnpackerTest extends MessagePackSpec {
       u.close
     }
 
-    "unpack large string data" taggedAs ("large-string") in {
+    test("unpack large string data") {
       def createLargeData(stringLength: Int): Array[Byte] = {
         val out    = new ByteArrayOutputStream()
         val packer = MessagePack.newDefaultPacker(out)
@@ -875,7 +878,7 @@ class MessageUnpackerTest extends MessagePackSpec {
       }
     }
 
-    "unpack string crossing end of buffer" in {
+    test("unpack string crossing end of buffer") {
       def check(expected: String, strLen: Int) = {
         val bytes = new Array[Byte](strLen)
         val out   = new ByteArrayOutputStream
@@ -911,7 +914,7 @@ class MessageUnpackerTest extends MessagePackSpec {
       }
     }
 
-    "read value length at buffer boundary" taggedAs ("number-boundary") in {
+    test("read value length at buffer boundary") {
       val input = new SplitMessageBufferInput(
         Array(Array[Byte](MessagePack.Code.STR16),
               Array[Byte](0x00),
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/ByteStringTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/ByteStringTest.scala
index ed79ef6ab..9a7286270 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/ByteStringTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/ByteStringTest.scala
@@ -16,6 +16,7 @@
 package org.msgpack.core.buffer
 
 import akka.util.ByteString
+import org.msgpack.core.MessagePackSpec.createMessagePackData
 import org.msgpack.core.{MessagePack, MessagePackSpec, MessageUnpacker}
 
 class ByteStringTest extends MessagePackSpec {
diff --git a/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala
index eeb663bc6..3194c7366 100644
--- a/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala
@@ -15,31 +15,35 @@
 //
 package org.msgpack.value
 
+import org.msgpack.core.MessagePackSpec.createMessagePackData
+
 import java.math.BigInteger
 import org.msgpack.core._
 import org.scalacheck.Prop.{forAll, propBoolean}
+import wvlet.airspec.AirSpec
+import wvlet.airspec.spi.PropertyCheck
 
 import scala.util.parsing.json.JSON
 
-class ValueTest extends MessagePackSpec {
-  def checkSuccinctType(pack: MessagePacker => Unit, expectedAtMost: MessageFormat): Boolean = {
+class ValueTest extends AirSpec with PropertyCheck {
+  private def checkSuccinctType(pack: MessagePacker => Unit, expectedAtMost: MessageFormat): Boolean = {
     val b  = createMessagePackData(pack)
     val v1 = MessagePack.newDefaultUnpacker(b).unpackValue()
     val mf = v1.asIntegerValue().mostSuccinctMessageFormat()
     mf.getValueType shouldBe ValueType.INTEGER
-    mf.ordinal() shouldBe <=(expectedAtMost.ordinal())
+    mf.ordinal() <= expectedAtMost.ordinal() shouldBe true
 
     val v2 = new Variable
     MessagePack.newDefaultUnpacker(b).unpackValue(v2)
     val mf2 = v2.asIntegerValue().mostSuccinctMessageFormat()
     mf2.getValueType shouldBe ValueType.INTEGER
-    mf2.ordinal() shouldBe <=(expectedAtMost.ordinal())
+    mf2.ordinal() <= expectedAtMost.ordinal() shouldBe true
 
     true
   }
 
-  "Value" should {
-    "tell most succinct integer type" in {
+  test("Value") {
+    test("tell most succinct integer type") {
       forAll { (v: Byte) =>
         checkSuccinctType(_.packByte(v), MessageFormat.INT8)
       }
@@ -63,7 +67,7 @@ class ValueTest extends MessagePackSpec {
       }
     }
 
-    "produce json strings" in {
+    test("produce json strings") {
 
       import ValueFactory._
 
@@ -108,7 +112,7 @@ class ValueTest extends MessagePackSpec {
 
     }
 
-    "check appropriate range for integers" in {
+    test("check appropriate range for integers") {
       import ValueFactory._
       import java.lang.Byte
       import java.lang.Short

From 2be647bdd7bf1d5110fe6fe1bf137a7d90391ba7 Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 23:24:05 -0700
Subject: [PATCH 13/21] Migrate to AirSpec

---
 .../msgpack/core/InvalidDataReadTest.scala    |   2 +-
 .../core/MessageBufferPackerTest.scala        |   6 +-
 .../org/msgpack/core/MessageFormatTest.scala  |   6 +-
 .../org/msgpack/core/MessagePackSpec.scala    |  23 +-
 .../org/msgpack/core/MessagePackTest.scala    |   2 +-
 .../org/msgpack/core/MessagePackerTest.scala  |  43 +-
 .../msgpack/core/buffer/ByteStringTest.scala  |  19 +-
 .../core/buffer/MessageBufferInputTest.scala  |  53 +--
 .../core/buffer/MessageBufferOutputTest.scala |  24 +-
 .../core/buffer/MessageBufferTest.scala       | 369 +++++++++---------
 .../core/example/MessagePackExampleTest.scala |  10 +-
 .../value/RawStringValueImplTest.scala        |  18 +-
 .../scala/org/msgpack/value/ValueTest.scala   |   2 +-
 .../org/msgpack/value/ValueTypeTest.scala     |  90 ++---
 14 files changed, 328 insertions(+), 339 deletions(-)

diff --git a/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala
index 42e06793f..45c2c12f4 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala
@@ -7,7 +7,7 @@ import org.msgpack.core.MessagePackSpec.createMessagePackData
   */
 class InvalidDataReadTest extends MessagePackSpec {
 
-  "Reading long EXT32" in {
+  test("Reading long EXT32") {
     // Prepare an EXT32 data with 2GB (Int.MaxValue size) payload for testing the behavior of MessageUnpacker.skipValue()
     // Actually preparing 2GB of data, however, is too much for CI, so we create only the header part.
     val msgpack = createMessagePackData(p => p.packExtensionTypeHeader(MessagePack.Code.EXT32, Int.MaxValue))
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala
index 2194e42ea..3c0c1344a 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala
@@ -21,8 +21,8 @@ import java.util.Arrays
 import org.msgpack.value.ValueFactory._
 
 class MessageBufferPackerTest extends MessagePackSpec {
-  "MessageBufferPacker" should {
-    "be equivalent to ByteArrayOutputStream" in {
+  test("MessageBufferPacker") {
+    test("be equivalent to ByteArrayOutputStream") {
       val packer1 = MessagePack.newDefaultBufferPacker
       packer1.packValue(newMap(newString("a"), newInteger(1), newString("b"), newString("s")))
 
@@ -34,7 +34,7 @@ class MessageBufferPackerTest extends MessagePackSpec {
       packer1.toByteArray shouldBe stream.toByteArray
     }
 
-    "clear unflushed" in {
+    test("clear unflushed") {
       val packer = MessagePack.newDefaultBufferPacker
       packer.packInt(1);
       packer.clear();
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala
index e7e9a4c36..742958845 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala
@@ -25,8 +25,8 @@ import scala.util.Random
   * Created on 2014/05/07.
   */
 class MessageFormatTest extends MessagePackSpec {
-  "MessageFormat" should {
-    "cover all byte codes" in {
+  test("MessageFormat") {
+    test("cover all byte codes") {
       def checkV(b: Byte, tpe: ValueType) {
         try MessageFormat.valueOf(b).getValueType shouldBe tpe
         catch {
@@ -102,7 +102,7 @@ class MessageFormatTest extends MessagePackSpec {
       }
     }
 
-    "improve the valueOf performance" in {
+    test("improve the valueOf performance") {
       val N   = 1000000
       val idx = (0 until N).map(x => Random.nextInt(256).toByte).toArray[Byte]
 
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
index 06edd4a0a..63d71d5dd 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
@@ -16,11 +16,9 @@
 package org.msgpack.core
 
 import java.io.ByteArrayOutputStream
-import org.scalatest._
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.wordspec.AnyWordSpec
-import xerial.core.log.{LogLevel, Logger}
-import xerial.core.util.{TimeReport, Timer}
+import wvlet.airspec.AirSpec
+import wvlet.log.LogLevel
+import wvlet.log.io.{TimeReport, Timer}
 
 import scala.language.implicitConversions
 
@@ -35,27 +33,22 @@ object MessagePackSpec {
   }
 }
 
-trait MessagePackSpec extends AnyWordSpec with Matchers with GivenWhenThen with OptionValues with BeforeAndAfter with Benchmark with Logger {
-
-  implicit def toTag(s: String): Tag = Tag(s)
-
-}
+trait MessagePackSpec extends AirSpec with Benchmark {}
 
 trait Benchmark extends Timer {
+  private val numWarmUpRuns = 10
 
-  val numWarmUpRuns = 10
-
-  override protected def time[A](blockName: String, logLevel: LogLevel, repeat: Int)(f: => A): TimeReport = {
+  override protected def time[A](blockName: String, logLevel: LogLevel = LogLevel.INFO, repeat: Int = 1, blockRepeat: Int = 1)(f: => A): TimeReport = {
     super.time(blockName, logLevel = LogLevel.INFO, repeat)(f)
   }
 
-  override protected def block[A](name: String, repeat: Int)(f: => A): TimeReport = {
+  override protected def block[A](name: String)(f: => A): TimeReport = {
     var i = 0
     while (i < numWarmUpRuns) {
       f
       i += 1
     }
 
-    super.block(name, repeat)(f)
+    super.block(name)(f)
   }
 }
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
index 5bc84661b..3cc9d1675 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
@@ -24,7 +24,7 @@ import org.msgpack.core.MessagePack.{PackerConfig, UnpackerConfig}
 import org.msgpack.core.MessagePackSpec.toHex
 import org.msgpack.value.{Value, Variable}
 import org.scalacheck.{Arbitrary, Gen}
-import org.scalacheck.Prop.{forAll, propBoolean}
+import org.scalacheck.Prop.propBoolean
 import wvlet.airspec.AirSpec
 import wvlet.airspec.spi.PropertyCheck
 import wvlet.log.io.Timer
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala
index 5024963fd..65f60751f 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala
@@ -29,7 +29,7 @@ import scala.util.Random
   */
 class MessagePackerTest extends MessagePackSpec {
 
-  def verifyIntSeq(answer: Array[Int], packed: Array[Byte]) {
+  private def verifyIntSeq(answer: Array[Int], packed: Array[Byte]) {
     val unpacker = MessagePack.newDefaultUnpacker(packed)
     val b        = Array.newBuilder[Int]
     while (unpacker.hasNext) {
@@ -40,27 +40,27 @@ class MessagePackerTest extends MessagePackSpec {
     result shouldBe answer
   }
 
-  def createTempFile = {
+  private def createTempFile = {
     val f = File.createTempFile("msgpackTest", "msgpack")
     f.deleteOnExit
     f
   }
 
-  def createTempFileWithOutputStream = {
+  private def createTempFileWithOutputStream = {
     val f   = createTempFile
     val out = new FileOutputStream(f)
     (f, out)
   }
 
-  def createTempFileWithChannel = {
+  private def createTempFileWithChannel = {
     val (f, out) = createTempFileWithOutputStream
     val ch       = out.getChannel
     (f, ch)
   }
 
-  "MessagePacker" should {
+  test("MessagePacker") {
 
-    "reset the internal states" in {
+    test("reset the internal states") {
       val intSeq = (0 until 100).map(i => Random.nextInt).toArray
 
       val b      = new ByteArrayOutputStream
@@ -86,8 +86,7 @@ class MessagePackerTest extends MessagePackSpec {
       verifyIntSeq(intSeq3, b3.toByteArray)
     }
 
-    "improve the performance via reset method" taggedAs ("reset") in {
-
+    test("improve the performance via reset method") {
       val N = 1000
       val t = time("packer", repeat = 10) {
         block("no-buffer-reset") {
@@ -119,10 +118,10 @@ class MessagePackerTest extends MessagePackSpec {
         }
       }
 
-      t("buffer-reset").averageWithoutMinMax should be <= t("no-buffer-reset").averageWithoutMinMax
+      t("buffer-reset").averageWithoutMinMax <= t("no-buffer-reset").averageWithoutMinMax shouldBe true
     }
 
-    "pack larger string array than byte buf" taggedAs ("larger-string-array-than-byte-buf") in {
+    test("pack larger string array than byte buf") {
       // Based on https://github.com/msgpack/msgpack-java/issues/154
 
       def test(bufferSize: Int, stringSize: Int): Boolean = {
@@ -148,7 +147,7 @@ class MessagePackerTest extends MessagePackSpec {
       }
     }
 
-    "reset OutputStreamBufferOutput" in {
+    test("reset OutputStreamBufferOutput") {
       val (f0, out0) = createTempFileWithOutputStream
       val packer     = MessagePack.newDefaultPacker(out0)
       packer.packInt(99)
@@ -178,7 +177,7 @@ class MessagePackerTest extends MessagePackSpec {
       up1.close
     }
 
-    "reset ChannelBufferOutput" in {
+    test("reset ChannelBufferOutput") {
       val (f0, out0) = createTempFileWithChannel
       val packer     = MessagePack.newDefaultPacker(out0)
       packer.packInt(99)
@@ -208,7 +207,7 @@ class MessagePackerTest extends MessagePackSpec {
       up1.close
     }
 
-    "pack a lot of String within expected time" in {
+    test("pack a lot of String within expected time") {
       val count = 20000
 
       def measureDuration(outputStream: java.io.OutputStream) = {
@@ -231,11 +230,11 @@ class MessagePackerTest extends MessagePackSpec {
           measureDuration(fileOutput)
         }
       }
-      t("file-output-stream").averageWithoutMinMax shouldBe <(t("byte-array-output-stream").averageWithoutMinMax * 5)
+      t("file-output-stream").averageWithoutMinMax < (t("byte-array-output-stream").averageWithoutMinMax * 5) shouldBe true
     }
   }
 
-  "compute totalWrittenBytes" in {
+  test("compute totalWrittenBytes") {
     val out = new ByteArrayOutputStream
     val packerTotalWrittenBytes =
       IOUtil.withResource(MessagePack.newDefaultPacker(out)) { packer =>
@@ -254,7 +253,7 @@ class MessagePackerTest extends MessagePackSpec {
     out.toByteArray.length shouldBe packerTotalWrittenBytes
   }
 
-  "support read-only buffer" taggedAs ("read-only") in {
+  test("support read-only buffer") {
     val payload = Array[Byte](1)
     val out     = new ByteArrayOutputStream()
     val packer = MessagePack
@@ -264,7 +263,7 @@ class MessagePackerTest extends MessagePackSpec {
       .close()
   }
 
-  "pack small string with STR8" in {
+  test("pack small string with STR8") {
     val packer = new PackerConfig().newBufferPacker()
     packer.packString("Hello. This is a string longer than 32 characters!")
     val b = packer.toByteArray
@@ -274,7 +273,7 @@ class MessagePackerTest extends MessagePackSpec {
     f shouldBe MessageFormat.STR8
   }
 
-  "be able to disable STR8 for backward compatibility" in {
+  test("be able to disable STR8 for backward compatibility") {
     val config = new PackerConfig()
       .withStr8FormatSupport(false)
 
@@ -285,7 +284,7 @@ class MessagePackerTest extends MessagePackSpec {
     f shouldBe MessageFormat.STR16
   }
 
-  "be able to disable STR8 when using CharsetEncoder" in {
+  test("be able to disable STR8 when using CharsetEncoder") {
     val config = new PackerConfig()
       .withStr8FormatSupport(false)
       .withSmallStringOptimizationThreshold(0) // Disable small string optimization
@@ -294,19 +293,19 @@ class MessagePackerTest extends MessagePackSpec {
     packer.packString("small string")
     val unpacker = MessagePack.newDefaultUnpacker(packer.toByteArray)
     val f        = unpacker.getNextFormat
-    f shouldNot be(MessageFormat.STR8)
+    f shouldNotBe MessageFormat.STR8
     val s = unpacker.unpackString()
     s shouldBe "small string"
   }
 
-  "write raw binary" taggedAs ("raw-binary") in {
+  test("write raw binary") {
     val packer = new MessagePack.PackerConfig().newBufferPacker()
     val msg =
       Array[Byte](-127, -92, 116, 121, 112, 101, -92, 112, 105, 110, 103)
     packer.writePayload(msg)
   }
 
-  "append raw binary" taggedAs ("append-raw-binary") in {
+  test("append raw binary") {
     val packer = new MessagePack.PackerConfig().newBufferPacker()
     val msg =
       Array[Byte](-127, -92, 116, 121, 112, 101, -92, 112, 105, 110, 103)
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/ByteStringTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/ByteStringTest.scala
index 9a7286270..42872fc44 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/ByteStringTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/ByteStringTest.scala
@@ -16,15 +16,16 @@
 package org.msgpack.core.buffer
 
 import akka.util.ByteString
+import org.msgpack.core.MessagePack
 import org.msgpack.core.MessagePackSpec.createMessagePackData
-import org.msgpack.core.{MessagePack, MessagePackSpec, MessageUnpacker}
+import wvlet.airspec.AirSpec
 
-class ByteStringTest extends MessagePackSpec {
+class ByteStringTest extends AirSpec {
 
-  val unpackedString = "foo"
-  val byteString     = ByteString(createMessagePackData(_.packString(unpackedString)))
+  private val unpackedString = "foo"
+  private val byteString     = ByteString(createMessagePackData(_.packString(unpackedString)))
 
-  def unpackString(messageBuffer: MessageBuffer) = {
+  private def unpackString(messageBuffer: MessageBuffer) = {
     val input = new MessageBufferInput {
 
       private var isRead = false
@@ -42,12 +43,14 @@ class ByteStringTest extends MessagePackSpec {
     MessagePack.newDefaultUnpacker(input).unpackString()
   }
 
-  "Unpacking a ByteString's ByteBuffer" should {
-    "fail with a regular MessageBuffer" in {
+  test("Unpacking a ByteString's ByteBuffer") {
+    test("fail with a regular MessageBuffer") {
 
       // can't demonstrate with new ByteBufferInput(byteString.asByteBuffer)
       // as Travis tests run with JDK6 that picks up MessageBufferU
-      a[RuntimeException] shouldBe thrownBy(unpackString(new MessageBuffer(byteString.asByteBuffer)))
+      intercept[RuntimeException] {
+        unpackString(new MessageBuffer(byteString.asByteBuffer))
+      }
     }
   }
 }
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala
index 060e436a1..f6dfafd5a 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala
@@ -29,21 +29,21 @@ import scala.util.Random
 
 class MessageBufferInputTest extends MessagePackSpec {
 
-  val targetInputSize =
+  private val targetInputSize =
     Seq(0, 10, 500, 1000, 2000, 4000, 8000, 10000, 30000, 50000, 100000)
 
-  def testData(size: Int) = {
+  private def testData(size: Int) = {
     //debug(s"test data size: ${size}")
     val b = new Array[Byte](size)
     Random.nextBytes(b)
     b
   }
 
-  def testDataSet = {
+  private def testDataSet = {
     targetInputSize.map(testData)
   }
 
-  def runTest(factory: Array[Byte] => MessageBufferInput) {
+  private def runTest(factory: Array[Byte] => MessageBufferInput) {
     for (b <- testDataSet) {
       checkInputData(b, factory(b))
     }
@@ -74,30 +74,31 @@ class MessageBufferInputTest extends MessagePackSpec {
     }
   }
 
-  def checkInputData(inputData: Array[Byte], in: MessageBufferInput) {
-    When(s"input data size = ${inputData.length}")
-    var cursor = 0
-    for (m <- Iterator.continually(in.next).takeWhile(_ != null)) {
-      m.toByteArray() shouldBe inputData.slice(cursor, cursor + m.size())
-      cursor += m.size()
+  private def checkInputData(inputData: Array[Byte], in: MessageBufferInput) {
+    test(s"When input data size = ${inputData.length}") {
+      var cursor = 0
+      for (m <- Iterator.continually(in.next).takeWhile(_ != null)) {
+        m.toByteArray() shouldBe inputData.slice(cursor, cursor + m.size())
+        cursor += m.size()
+      }
+      cursor shouldBe inputData.length
     }
-    cursor shouldBe inputData.length
   }
 
-  "MessageBufferInput" should {
-    "support byte arrays" in {
+  test("MessageBufferInput") {
+    test("support byte arrays") {
       runTest(new ArrayBufferInput(_))
     }
 
-    "support ByteBuffers" in {
+    test("support ByteBuffers") {
       runTest(b => new ByteBufferInput(b.toByteBuffer))
     }
 
-    "support InputStreams" taggedAs ("is") in {
+    test("support InputStreams") {
       runTest(b => new InputStreamBufferInput(new GZIPInputStream(new ByteArrayInputStream(b.compress))))
     }
 
-    "support file input channel" taggedAs ("fc") in {
+    test("support file input channel") {
       runTest { b =>
         val tmp = b.saveToTmpFile
         try {
@@ -110,13 +111,13 @@ class MessageBufferInputTest extends MessagePackSpec {
     }
   }
 
-  def createTempFile = {
+  private def createTempFile = {
     val f = File.createTempFile("msgpackTest", "msgpack")
     f.deleteOnExit
     f
   }
 
-  def createTempFileWithInputStream = {
+  private def createTempFileWithInputStream = {
     val f   = createTempFile
     val out = new FileOutputStream(f)
     MessagePack.newDefaultPacker(out).packInt(42).close
@@ -124,19 +125,19 @@ class MessageBufferInputTest extends MessagePackSpec {
     (f, in)
   }
 
-  def createTempFileWithChannel = {
+  private def createTempFileWithChannel = {
     val (f, in) = createTempFileWithInputStream
     val ch      = in.getChannel
     (f, ch)
   }
 
-  def readInt(buf: MessageBufferInput): Int = {
+  private def readInt(buf: MessageBufferInput): Int = {
     val unpacker = MessagePack.newDefaultUnpacker(buf)
     unpacker.unpackInt
   }
 
-  "InputStreamBufferInput" should {
-    "reset buffer" in {
+  test("InputStreamBufferInput") {
+    test("reset buffer") {
       val (f0, in0) = createTempFileWithInputStream
       val buf       = new InputStreamBufferInput(in0)
       readInt(buf) shouldBe 42
@@ -146,7 +147,7 @@ class MessageBufferInputTest extends MessagePackSpec {
       readInt(buf) shouldBe 42
     }
 
-    "be non-blocking" taggedAs ("non-blocking") in {
+    test("be non-blocking") {
 
       withResource(new PipedOutputStream()) { pipedOutputStream =>
         withResource(new PipedInputStream()) { pipedInputStream =>
@@ -173,8 +174,8 @@ class MessageBufferInputTest extends MessagePackSpec {
     }
   }
 
-  "ChannelBufferInput" should {
-    "reset buffer" in {
+  test("ChannelBufferInput") {
+    test("reset buffer") {
       val (f0, in0) = createTempFileWithChannel
       val buf       = new ChannelBufferInput(in0)
       readInt(buf) shouldBe 42
@@ -184,7 +185,7 @@ class MessageBufferInputTest extends MessagePackSpec {
       readInt(buf) shouldBe 42
     }
 
-    "unpack without blocking" in {
+    test("unpack without blocking") {
       val server =
         ServerSocketChannel.open.bind(new InetSocketAddress("localhost", 0))
       val executorService = Executors.newCachedThreadPool
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferOutputTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferOutputTest.scala
index e048e1ba1..35186c57e 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferOutputTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferOutputTest.scala
@@ -21,56 +21,56 @@ import org.msgpack.core.MessagePackSpec
 
 class MessageBufferOutputTest extends MessagePackSpec {
 
-  def createTempFile = {
+  private def createTempFile = {
     val f = File.createTempFile("msgpackTest", "msgpack")
     f.deleteOnExit
     f
   }
 
-  def createTempFileWithOutputStream = {
+  private def createTempFileWithOutputStream = {
     val f   = createTempFile
     val out = new FileOutputStream(f)
     (f, out)
   }
 
-  def createTempFileWithChannel = {
+  private def createTempFileWithChannel = {
     val (f, out) = createTempFileWithOutputStream
     val ch       = out.getChannel
     (f, ch)
   }
 
-  def writeIntToBuf(buf: MessageBufferOutput) = {
+  private def writeIntToBuf(buf: MessageBufferOutput) = {
     val mb0 = buf.next(8)
     mb0.putInt(0, 42)
     buf.writeBuffer(4)
     buf.close
   }
 
-  "OutputStreamBufferOutput" should {
-    "reset buffer" in {
+  test("OutputStreamBufferOutput") {
+    test("reset buffer") {
       val (f0, out0) = createTempFileWithOutputStream
       val buf        = new OutputStreamBufferOutput(out0)
       writeIntToBuf(buf)
-      f0.length.toInt should be > 0
+      f0.length.toInt > 0 shouldBe true
 
       val (f1, out1) = createTempFileWithOutputStream
       buf.reset(out1)
       writeIntToBuf(buf)
-      f1.length.toInt should be > 0
+      f1.length.toInt > 0 shouldBe true
     }
   }
 
-  "ChannelBufferOutput" should {
-    "reset buffer" in {
+  test("ChannelBufferOutput") {
+    test("reset buffer") {
       val (f0, ch0) = createTempFileWithChannel
       val buf       = new ChannelBufferOutput(ch0)
       writeIntToBuf(buf)
-      f0.length.toInt should be > 0
+      f0.length.toInt >= 0 shouldBe true
 
       val (f1, ch1) = createTempFileWithChannel
       buf.reset(ch1)
       writeIntToBuf(buf)
-      f1.length.toInt should be > 0
+      f1.length.toInt > 0 shouldBe true
     }
   }
 }
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferTest.scala
index f0f66b4af..1dee5706d 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferTest.scala
@@ -26,235 +26,234 @@ import scala.util.Random
   */
 class MessageBufferTest extends MessagePackSpec {
 
-  "MessageBuffer" should {
+  private val universal = MessageBuffer.allocate(0).isInstanceOf[MessageBufferU]
 
-    val universal = MessageBuffer.allocate(0).isInstanceOf[MessageBufferU]
-    "check buffer type" in {
-      val b = MessageBuffer.allocate(0)
-      info(s"MessageBuffer type: ${b.getClass.getName}")
-    }
+  test("check buffer type") {
+    val b = MessageBuffer.allocate(0)
+    info(s"MessageBuffer type: ${b.getClass.getName}")
+  }
 
-    "wrap byte array considering position and remaining values" taggedAs ("wrap-ba") in {
-      val d  = Array[Byte](10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
-      val mb = MessageBuffer.wrap(d, 2, 2)
-      mb.getByte(0) shouldBe 12
-      mb.size() shouldBe 2
-    }
+  test("wrap byte array considering position and remaining values") {
+    val d  = Array[Byte](10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
+    val mb = MessageBuffer.wrap(d, 2, 2)
+    mb.getByte(0) shouldBe 12
+    mb.size() shouldBe 2
+  }
 
-    "wrap ByteBuffer considering position and remaining values" taggedAs ("wrap-bb") in {
-      val d      = Array[Byte](10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
-      val subset = ByteBuffer.wrap(d, 2, 2)
-      val mb     = MessageBuffer.wrap(subset)
-      mb.getByte(0) shouldBe 12
-      mb.size() shouldBe 2
-    }
+  test("wrap ByteBuffer considering position and remaining values") {
+    val d      = Array[Byte](10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
+    val subset = ByteBuffer.wrap(d, 2, 2)
+    val mb     = MessageBuffer.wrap(subset)
+    mb.getByte(0) shouldBe 12
+    mb.size() shouldBe 2
+  }
+
+  test("have better performance than ByteBuffer") {
 
-    "have better performance than ByteBuffer" in {
+    val N = 1000000
+    val M = 64 * 1024 * 1024
 
-      val N = 1000000
-      val M = 64 * 1024 * 1024
+    val ub = MessageBuffer.allocate(M)
+    val ud =
+      if (universal) MessageBuffer.wrap(ByteBuffer.allocate(M))
+      else MessageBuffer.wrap(ByteBuffer.allocateDirect(M))
+    val hb = ByteBuffer.allocate(M)
+    val db = ByteBuffer.allocateDirect(M)
 
-      val ub = MessageBuffer.allocate(M)
-      val ud =
-        if (universal) MessageBuffer.wrap(ByteBuffer.allocate(M))
-        else MessageBuffer.wrap(ByteBuffer.allocateDirect(M))
-      val hb = ByteBuffer.allocate(M)
-      val db = ByteBuffer.allocateDirect(M)
+    def bench(f: Int => Unit) {
+      var i = 0
+      while (i < N) {
+        f((i * 4) % M)
+        i += 1
+      }
+    }
 
-      def bench(f: Int => Unit) {
+    val r  = new Random(0)
+    val rs = new Array[Int](N)
+    (0 until N).map(i => rs(i) = r.nextInt(N))
+    def randomBench(f: Int => Unit) {
+      var i = 0
+      while (i < N) {
+        f((rs(i) * 4) % M)
+        i += 1
+      }
+    }
+
+    val rep = 3
+    info(f"Reading buffers (of size:${M}%,d) ${N}%,d x $rep times")
+    time("sequential getInt", repeat = rep) {
+      block("unsafe array") {
         var i = 0
         while (i < N) {
-          f((i * 4) % M)
+          ub.getInt((i * 4) % M)
           i += 1
         }
       }
 
-      val r  = new Random(0)
-      val rs = new Array[Int](N)
-      (0 until N).map(i => rs(i) = r.nextInt(N))
-      def randomBench(f: Int => Unit) {
+      block("unsafe direct") {
         var i = 0
         while (i < N) {
-          f((rs(i) * 4) % M)
+          ud.getInt((i * 4) % M)
           i += 1
         }
       }
 
-      val rep = 3
-      info(f"Reading buffers (of size:${M}%,d) ${N}%,d x $rep times")
-      time("sequential getInt", repeat = rep) {
-        block("unsafe array") {
-          var i = 0
-          while (i < N) {
-            ub.getInt((i * 4) % M)
-            i += 1
-          }
-        }
-
-        block("unsafe direct") {
-          var i = 0
-          while (i < N) {
-            ud.getInt((i * 4) % M)
-            i += 1
-          }
-        }
-
-        block("allocate") {
-          var i = 0
-          while (i < N) {
-            hb.getInt((i * 4) % M)
-            i += 1
-          }
+      block("allocate") {
+        var i = 0
+        while (i < N) {
+          hb.getInt((i * 4) % M)
+          i += 1
         }
+      }
 
-        block("allocateDirect") {
-          var i = 0
-          while (i < N) {
-            db.getInt((i * 4) % M)
-            i += 1
-          }
+      block("allocateDirect") {
+        var i = 0
+        while (i < N) {
+          db.getInt((i * 4) % M)
+          i += 1
         }
       }
+    }
 
-      time("random getInt", repeat = rep) {
-        block("unsafe array") {
-          var i = 0
-          while (i < N) {
-            ub.getInt((rs(i) * 4) % M)
-            i += 1
-          }
+    time("random getInt", repeat = rep) {
+      block("unsafe array") {
+        var i = 0
+        while (i < N) {
+          ub.getInt((rs(i) * 4) % M)
+          i += 1
         }
+      }
 
-        block("unsafe direct") {
-          var i = 0
-          while (i < N) {
-            ud.getInt((rs(i) * 4) % M)
-            i += 1
-          }
+      block("unsafe direct") {
+        var i = 0
+        while (i < N) {
+          ud.getInt((rs(i) * 4) % M)
+          i += 1
         }
+      }
 
-        block("allocate") {
-          var i = 0
-          while (i < N) {
-            hb.getInt((rs(i) * 4) % M)
-            i += 1
-          }
+      block("allocate") {
+        var i = 0
+        while (i < N) {
+          hb.getInt((rs(i) * 4) % M)
+          i += 1
         }
+      }
 
-        block("allocateDirect") {
-          var i = 0
-          while (i < N) {
-            db.getInt((rs(i) * 4) % M)
-            i += 1
-          }
+      block("allocateDirect") {
+        var i = 0
+        while (i < N) {
+          db.getInt((rs(i) * 4) % M)
+          i += 1
         }
       }
     }
-    val builder = Seq.newBuilder[MessageBuffer]
-    builder += MessageBuffer.allocate(10)
-    builder += MessageBuffer.wrap(ByteBuffer.allocate(10))
-    if (!universal) builder += MessageBuffer.wrap(ByteBuffer.allocateDirect(10))
-    val buffers = builder.result()
-
-    "convert to ByteBuffer" in {
-      for (t <- buffers) {
-        val bb = t.sliceAsByteBuffer
-        bb.position() shouldBe 0
-        bb.limit() shouldBe 10
-        bb.capacity shouldBe 10
-      }
+  }
+
+  private val builder = Seq.newBuilder[MessageBuffer]
+  builder += MessageBuffer.allocate(10)
+  builder += MessageBuffer.wrap(ByteBuffer.allocate(10))
+  if (!universal) builder += MessageBuffer.wrap(ByteBuffer.allocateDirect(10))
+  private val buffers = builder.result()
+
+  test("convert to ByteBuffer") {
+    for (t <- buffers) {
+      val bb = t.sliceAsByteBuffer
+      bb.position() shouldBe 0
+      bb.limit() shouldBe 10
+      bb.capacity shouldBe 10
     }
+  }
 
-    "put ByteBuffer on itself" in {
-      for (t <- buffers) {
-        val b        = Array[Byte](0x02, 0x03)
-        val srcArray = ByteBuffer.wrap(b)
-        val srcHeap  = ByteBuffer.allocate(b.length)
-        srcHeap.put(b).flip
-        val srcOffHeap = ByteBuffer.allocateDirect(b.length)
-        srcOffHeap.put(b).flip
-
-        for (src <- Seq(srcArray, srcHeap, srcOffHeap)) {
-          // Write header bytes
-          val header = Array[Byte](0x00, 0x01)
-          t.putBytes(0, header, 0, header.length)
-          // Write src after the header
-          t.putByteBuffer(header.length, src, header.length)
-
-          t.getByte(0) shouldBe 0x00
-          t.getByte(1) shouldBe 0x01
-          t.getByte(2) shouldBe 0x02
-          t.getByte(3) shouldBe 0x03
-        }
+  test("put ByteBuffer on itself") {
+    for (t <- buffers) {
+      val b        = Array[Byte](0x02, 0x03)
+      val srcArray = ByteBuffer.wrap(b)
+      val srcHeap  = ByteBuffer.allocate(b.length)
+      srcHeap.put(b).flip
+      val srcOffHeap = ByteBuffer.allocateDirect(b.length)
+      srcOffHeap.put(b).flip
+
+      for (src <- Seq(srcArray, srcHeap, srcOffHeap)) {
+        // Write header bytes
+        val header = Array[Byte](0x00, 0x01)
+        t.putBytes(0, header, 0, header.length)
+        // Write src after the header
+        t.putByteBuffer(header.length, src, header.length)
+
+        t.getByte(0) shouldBe 0x00
+        t.getByte(1) shouldBe 0x01
+        t.getByte(2) shouldBe 0x02
+        t.getByte(3) shouldBe 0x03
       }
     }
+  }
 
-    "put MessageBuffer on itself" in {
-      for (t <- buffers) {
-        val b        = Array[Byte](0x02, 0x03)
-        val srcArray = ByteBuffer.wrap(b)
-        val srcHeap  = ByteBuffer.allocate(b.length)
-        srcHeap.put(b).flip
-        val srcOffHeap = ByteBuffer.allocateDirect(b.length)
-        srcOffHeap.put(b).flip
-        val builder = Seq.newBuilder[ByteBuffer]
-        builder ++= Seq(srcArray, srcHeap)
-        if (!universal) builder += srcOffHeap
-
-        for (src <- builder.result().map(d => MessageBuffer.wrap(d))) {
-          // Write header bytes
-          val header = Array[Byte](0x00, 0x01)
-          t.putBytes(0, header, 0, header.length)
-          // Write src after the header
-          t.putMessageBuffer(header.length, src, 0, header.length)
-
-          t.getByte(0) shouldBe 0x00
-          t.getByte(1) shouldBe 0x01
-          t.getByte(2) shouldBe 0x02
-          t.getByte(3) shouldBe 0x03
-        }
+  test("put MessageBuffer on itself") {
+    for (t <- buffers) {
+      val b        = Array[Byte](0x02, 0x03)
+      val srcArray = ByteBuffer.wrap(b)
+      val srcHeap  = ByteBuffer.allocate(b.length)
+      srcHeap.put(b).flip
+      val srcOffHeap = ByteBuffer.allocateDirect(b.length)
+      srcOffHeap.put(b).flip
+      val builder = Seq.newBuilder[ByteBuffer]
+      builder ++= Seq(srcArray, srcHeap)
+      if (!universal) builder += srcOffHeap
+
+      for (src <- builder.result().map(d => MessageBuffer.wrap(d))) {
+        // Write header bytes
+        val header = Array[Byte](0x00, 0x01)
+        t.putBytes(0, header, 0, header.length)
+        // Write src after the header
+        t.putMessageBuffer(header.length, src, 0, header.length)
+
+        t.getByte(0) shouldBe 0x00
+        t.getByte(1) shouldBe 0x01
+        t.getByte(2) shouldBe 0x02
+        t.getByte(3) shouldBe 0x03
       }
     }
+  }
 
-    "copy sliced buffer" in {
-      def prepareBytes: Array[Byte] = {
-        Array[Byte](0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07)
-      }
+  test("copy sliced buffer") {
+    def prepareBytes: Array[Byte] = {
+      Array[Byte](0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07)
+    }
 
-      def prepareDirectBuffer: ByteBuffer = {
-        val directBuffer = ByteBuffer.allocateDirect(prepareBytes.length)
-        directBuffer.put(prepareBytes)
-        directBuffer.flip
-        directBuffer
-      }
+    def prepareDirectBuffer: ByteBuffer = {
+      val directBuffer = ByteBuffer.allocateDirect(prepareBytes.length)
+      directBuffer.put(prepareBytes)
+      directBuffer.flip
+      directBuffer
+    }
 
-      def checkSliceAndCopyTo(srcBuffer: MessageBuffer, dstBuffer: MessageBuffer) = {
-        val sliced = srcBuffer.slice(2, 5)
-
-        sliced.size() shouldBe 5
-        sliced.getByte(0) shouldBe 0x02
-        sliced.getByte(1) shouldBe 0x03
-        sliced.getByte(2) shouldBe 0x04
-        sliced.getByte(3) shouldBe 0x05
-        sliced.getByte(4) shouldBe 0x06
-
-        sliced.copyTo(3, dstBuffer, 1, 2) // copy 0x05 and 0x06 to dstBuffer[1] and [2]
-
-        dstBuffer.getByte(0) shouldBe 0x00
-        dstBuffer.getByte(1) shouldBe 0x05 // copied by sliced.getByte(3)
-        dstBuffer.getByte(2) shouldBe 0x06 // copied by sliced.getByte(4)
-        dstBuffer.getByte(3) shouldBe 0x03
-        dstBuffer.getByte(4) shouldBe 0x04
-        dstBuffer.getByte(5) shouldBe 0x05
-        dstBuffer.getByte(6) shouldBe 0x06
-        dstBuffer.getByte(7) shouldBe 0x07
-      }
+    def checkSliceAndCopyTo(srcBuffer: MessageBuffer, dstBuffer: MessageBuffer) = {
+      val sliced = srcBuffer.slice(2, 5)
+
+      sliced.size() shouldBe 5
+      sliced.getByte(0) shouldBe 0x02
+      sliced.getByte(1) shouldBe 0x03
+      sliced.getByte(2) shouldBe 0x04
+      sliced.getByte(3) shouldBe 0x05
+      sliced.getByte(4) shouldBe 0x06
+
+      sliced.copyTo(3, dstBuffer, 1, 2) // copy 0x05 and 0x06 to dstBuffer[1] and [2]
+
+      dstBuffer.getByte(0) shouldBe 0x00
+      dstBuffer.getByte(1) shouldBe 0x05 // copied by sliced.getByte(3)
+      dstBuffer.getByte(2) shouldBe 0x06 // copied by sliced.getByte(4)
+      dstBuffer.getByte(3) shouldBe 0x03
+      dstBuffer.getByte(4) shouldBe 0x04
+      dstBuffer.getByte(5) shouldBe 0x05
+      dstBuffer.getByte(6) shouldBe 0x06
+      dstBuffer.getByte(7) shouldBe 0x07
+    }
 
-      checkSliceAndCopyTo(MessageBuffer.wrap(prepareBytes), MessageBuffer.wrap(prepareBytes))
-      checkSliceAndCopyTo(MessageBuffer.wrap(ByteBuffer.wrap(prepareBytes)), MessageBuffer.wrap(ByteBuffer.wrap(prepareBytes)))
-      if (!universal) {
-        checkSliceAndCopyTo(MessageBuffer.wrap(prepareDirectBuffer), MessageBuffer.wrap(prepareDirectBuffer))
-      }
+    checkSliceAndCopyTo(MessageBuffer.wrap(prepareBytes), MessageBuffer.wrap(prepareBytes))
+    checkSliceAndCopyTo(MessageBuffer.wrap(ByteBuffer.wrap(prepareBytes)), MessageBuffer.wrap(ByteBuffer.wrap(prepareBytes)))
+    if (!universal) {
+      checkSliceAndCopyTo(MessageBuffer.wrap(prepareDirectBuffer), MessageBuffer.wrap(prepareDirectBuffer))
     }
   }
 }
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/example/MessagePackExampleTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/example/MessagePackExampleTest.scala
index cbbfd8751..edd8a90a2 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/example/MessagePackExampleTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/example/MessagePackExampleTest.scala
@@ -22,21 +22,21 @@ import org.msgpack.core.MessagePackSpec
   */
 class MessagePackExampleTest extends MessagePackSpec {
 
-  "example" should {
+  test("example") {
 
-    "have basic usage" in {
+    test("have basic usage") {
       MessagePackExample.basicUsage()
     }
 
-    "have packer usage" in {
+    test("have packer usage") {
       MessagePackExample.packer()
     }
 
-    "have file read/write example" in {
+    test("have file read/write example") {
       MessagePackExample.readAndWriteFile();
     }
 
-    "have configuration example" in {
+    test("have configuration example") {
       MessagePackExample.configuration();
     }
   }
diff --git a/msgpack-core/src/test/scala/org/msgpack/value/RawStringValueImplTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/RawStringValueImplTest.scala
index 7de9d6c6f..394a733de 100644
--- a/msgpack-core/src/test/scala/org/msgpack/value/RawStringValueImplTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/value/RawStringValueImplTest.scala
@@ -19,16 +19,14 @@ import org.msgpack.core.MessagePackSpec
 
 class RawStringValueImplTest extends MessagePackSpec {
 
-  "StringValue" should {
-    "return the same hash code if they are equal" in {
-      val str = "a"
-      val a1  = ValueFactory.newString(str.getBytes("UTF-8"))
-      val a2  = ValueFactory.newString(str)
+  test("return the same hash code if they are equal") {
+    val str = "a"
+    val a1  = ValueFactory.newString(str.getBytes("UTF-8"))
+    val a2  = ValueFactory.newString(str)
 
-      a1.shouldEqual(a2)
-      a1.hashCode.shouldEqual(a2.hashCode)
-      a2.shouldEqual(a1)
-      a2.hashCode.shouldEqual(a1.hashCode)
-    }
+    a1 shouldBe a2
+    a1.hashCode shouldBe a2.hashCode
+    a2 shouldBe a1
+    a2.hashCode shouldBe a1.hashCode
   }
 }
diff --git a/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala
index 3194c7366..ce70ec4e9 100644
--- a/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala
@@ -19,7 +19,7 @@ import org.msgpack.core.MessagePackSpec.createMessagePackData
 
 import java.math.BigInteger
 import org.msgpack.core._
-import org.scalacheck.Prop.{forAll, propBoolean}
+import org.scalacheck.Prop.propBoolean
 import wvlet.airspec.AirSpec
 import wvlet.airspec.spi.PropertyCheck
 
diff --git a/msgpack-core/src/test/scala/org/msgpack/value/ValueTypeTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/ValueTypeTest.scala
index 445eda32c..00ebaafc8 100644
--- a/msgpack-core/src/test/scala/org/msgpack/value/ValueTypeTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/value/ValueTypeTest.scala
@@ -23,66 +23,62 @@ import org.msgpack.core.{MessageFormat, MessageFormatException, MessagePackSpec}
   */
 class ValueTypeTest extends MessagePackSpec {
 
-  "ValueType" should {
-
-    "lookup ValueType from a byte value" taggedAs ("code") in {
-
-      def check(b: Byte, tpe: ValueType) {
-        MessageFormat.valueOf(b).getValueType shouldBe tpe
-      }
+  test("lookup ValueType from a byte value") {
+    def check(b: Byte, tpe: ValueType) {
+      MessageFormat.valueOf(b).getValueType shouldBe tpe
+    }
 
-      for (i <- 0 until 0x7f) {
-        check(i.toByte, ValueType.INTEGER)
-      }
+    for (i <- 0 until 0x7f) {
+      check(i.toByte, ValueType.INTEGER)
+    }
 
-      for (i <- 0x80 until 0x8f) {
-        check(i.toByte, ValueType.MAP)
-      }
+    for (i <- 0x80 until 0x8f) {
+      check(i.toByte, ValueType.MAP)
+    }
 
-      for (i <- 0x90 until 0x9f) {
-        check(i.toByte, ValueType.ARRAY)
-      }
+    for (i <- 0x90 until 0x9f) {
+      check(i.toByte, ValueType.ARRAY)
+    }
 
-      check(NIL, ValueType.NIL)
+    check(NIL, ValueType.NIL)
 
-      try {
-        MessageFormat.valueOf(NEVER_USED).getValueType
-        fail("NEVER_USED type should not have ValueType")
-      } catch {
-        case e: MessageFormatException =>
-        // OK
-      }
+    try {
+      MessageFormat.valueOf(NEVER_USED).getValueType
+      fail("NEVER_USED type should not have ValueType")
+    } catch {
+      case e: MessageFormatException =>
+      // OK
+    }
 
-      check(TRUE, ValueType.BOOLEAN)
-      check(FALSE, ValueType.BOOLEAN)
+    check(TRUE, ValueType.BOOLEAN)
+    check(FALSE, ValueType.BOOLEAN)
 
-      for (t <- Seq(BIN8, BIN16, BIN32)) {
-        check(t, ValueType.BINARY)
-      }
+    for (t <- Seq(BIN8, BIN16, BIN32)) {
+      check(t, ValueType.BINARY)
+    }
 
-      for (t <- Seq(FIXEXT1, FIXEXT2, FIXEXT4, FIXEXT8, FIXEXT16, EXT8, EXT16, EXT32)) {
-        check(t, ValueType.EXTENSION)
-      }
+    for (t <- Seq(FIXEXT1, FIXEXT2, FIXEXT4, FIXEXT8, FIXEXT16, EXT8, EXT16, EXT32)) {
+      check(t, ValueType.EXTENSION)
+    }
 
-      for (t <- Seq(INT8, INT16, INT32, INT64, UINT8, UINT16, UINT32, UINT64)) {
-        check(t, ValueType.INTEGER)
-      }
+    for (t <- Seq(INT8, INT16, INT32, INT64, UINT8, UINT16, UINT32, UINT64)) {
+      check(t, ValueType.INTEGER)
+    }
 
-      for (t <- Seq(STR8, STR16, STR32)) {
-        check(t, ValueType.STRING)
-      }
+    for (t <- Seq(STR8, STR16, STR32)) {
+      check(t, ValueType.STRING)
+    }
 
-      for (t <- Seq(FLOAT32, FLOAT64)) {
-        check(t, ValueType.FLOAT)
-      }
+    for (t <- Seq(FLOAT32, FLOAT64)) {
+      check(t, ValueType.FLOAT)
+    }
 
-      for (t <- Seq(ARRAY16, ARRAY32)) {
-        check(t, ValueType.ARRAY)
-      }
+    for (t <- Seq(ARRAY16, ARRAY32)) {
+      check(t, ValueType.ARRAY)
+    }
 
-      for (i <- 0xe0 until 0xff) {
-        check(i.toByte, ValueType.INTEGER)
-      }
+    for (i <- 0xe0 until 0xff) {
+      check(i.toByte, ValueType.INTEGER)
     }
   }
 }

From 111fe8e21e515caee0b8d39a096240578545a6ca Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 23:27:35 -0700
Subject: [PATCH 14/21] Fix test

---
 .../org/msgpack/core/MessageBufferPackerTest.scala   | 12 ++++++------
 .../scala/org/msgpack/core/MessageUnpackerTest.scala |  4 +---
 2 files changed, 7 insertions(+), 9 deletions(-)

diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala
index 3c0c1344a..139badba8 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala
@@ -36,15 +36,15 @@ class MessageBufferPackerTest extends MessagePackSpec {
 
     test("clear unflushed") {
       val packer = MessagePack.newDefaultBufferPacker
-      packer.packInt(1);
-      packer.clear();
-      packer.packInt(2);
+      packer.packInt(1)
+      packer.clear()
+      packer.packInt(2)
 
-      packer.toByteArray shouldBe Array(2)
+      packer.toByteArray shouldBe Array[Byte](2)
       val buffer = packer.toBufferList().get(0)
-      buffer.toByteArray() shouldBe Array(2)
+      buffer.toByteArray() shouldBe Array[Byte](2)
       val array = Arrays.copyOf(buffer.sliceAsByteBuffer().array(), buffer.size())
-      array shouldBe Array(2)
+      array shouldBe Array[Byte](2)
     }
 
   }
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
index 665792286..6d69cc5cc 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
@@ -302,7 +302,6 @@ class MessageUnpackerTest extends AirSpec with Timer {
     }
 
     test("parse int data") {
-
       debug(intSeq.mkString(", "))
 
       for (unpacker <- unpackers(testData2)) {
@@ -323,10 +322,9 @@ class MessageUnpackerTest extends AirSpec with Timer {
           }
         }
 
-        ib.result shouldBe intSeq
+        ib.result shouldBe intSeq.toSeq
         unpacker.getTotalReadBytes shouldBe testData2.length
       }
-
     }
 
     test("read data at the buffer boundary") {

From 7d67105f67a4a60f003a7c21958f1f1662aeff3a Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 23:33:23 -0700
Subject: [PATCH 15/21] Use plain AirSpec

---
 .../org/msgpack/core/InvalidDataReadTest.scala   |  3 ++-
 .../msgpack/core/MessageBufferPackerTest.scala   |  4 ++--
 .../org/msgpack/core/MessageFormatTest.scala     |  3 ++-
 .../scala/org/msgpack/core/MessagePackSpec.scala |  5 +----
 .../scala/org/msgpack/core/MessagePackTest.scala | 16 +++++++---------
 .../org/msgpack/core/MessagePackerTest.scala     |  8 ++++----
 .../org/msgpack/core/MessageUnpackerTest.scala   | 13 +++++--------
 .../core/buffer/MessageBufferInputTest.scala     |  6 +++---
 .../core/buffer/MessageBufferOutputTest.scala    |  6 +++---
 .../msgpack/core/buffer/MessageBufferTest.scala  |  8 ++++----
 .../core/example/MessagePackExampleTest.scala    |  4 ++--
 .../msgpack/value/RawStringValueImplTest.scala   |  4 ++--
 .../scala/org/msgpack/value/ValueTypeTest.scala  |  5 +++--
 13 files changed, 40 insertions(+), 45 deletions(-)

diff --git a/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala
index 45c2c12f4..4e39ff85c 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala
@@ -1,11 +1,12 @@
 package org.msgpack.core
 
 import org.msgpack.core.MessagePackSpec.createMessagePackData
+import wvlet.airspec.AirSpec
 
 /**
   *
   */
-class InvalidDataReadTest extends MessagePackSpec {
+class InvalidDataReadTest extends AirSpec {
 
   test("Reading long EXT32") {
     // Prepare an EXT32 data with 2GB (Int.MaxValue size) payload for testing the behavior of MessageUnpacker.skipValue()
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala
index 139badba8..58b29f435 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala
@@ -17,10 +17,10 @@ package org.msgpack.core
 
 import java.io.ByteArrayOutputStream
 import java.util.Arrays
-
 import org.msgpack.value.ValueFactory._
+import wvlet.airspec.AirSpec
 
-class MessageBufferPackerTest extends MessagePackSpec {
+class MessageBufferPackerTest extends AirSpec {
   test("MessageBufferPacker") {
     test("be equivalent to ByteArrayOutputStream") {
       val packer1 = MessagePack.newDefaultBufferPacker
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala
index 742958845..3299f4069 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala
@@ -18,13 +18,14 @@ package org.msgpack.core
 import org.msgpack.core.MessagePack.Code
 import org.msgpack.value.ValueType
 import org.scalatest.exceptions.TestFailedException
+import wvlet.airspec.AirSpec
 
 import scala.util.Random
 
 /**
   * Created on 2014/05/07.
   */
-class MessageFormatTest extends MessagePackSpec {
+class MessageFormatTest extends AirSpec with Benchmark {
   test("MessageFormat") {
     test("cover all byte codes") {
       def checkV(b: Byte, tpe: ValueType) {
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
index 63d71d5dd..c3df7b259 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
@@ -15,11 +15,10 @@
 //
 package org.msgpack.core
 
-import java.io.ByteArrayOutputStream
-import wvlet.airspec.AirSpec
 import wvlet.log.LogLevel
 import wvlet.log.io.{TimeReport, Timer}
 
+import java.io.ByteArrayOutputStream
 import scala.language.implicitConversions
 
 object MessagePackSpec {
@@ -33,8 +32,6 @@ object MessagePackSpec {
   }
 }
 
-trait MessagePackSpec extends AirSpec with Benchmark {}
-
 trait Benchmark extends Timer {
   private val numWarmUpRuns = 10
 
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
index 3cc9d1675..f60702ed8 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala
@@ -15,27 +15,25 @@
 //
 package org.msgpack.core
 
-import java.io.ByteArrayOutputStream
-import java.math.BigInteger
-import java.nio.CharBuffer
-import java.nio.charset.{CodingErrorAction, UnmappableCharacterException}
-import org.msgpack.core.MessagePack.Code
-import org.msgpack.core.MessagePack.{PackerConfig, UnpackerConfig}
+import org.msgpack.core.MessagePack.{Code, PackerConfig, UnpackerConfig}
 import org.msgpack.core.MessagePackSpec.toHex
 import org.msgpack.value.{Value, Variable}
-import org.scalacheck.{Arbitrary, Gen}
 import org.scalacheck.Prop.propBoolean
+import org.scalacheck.{Arbitrary, Gen}
 import wvlet.airspec.AirSpec
 import wvlet.airspec.spi.PropertyCheck
-import wvlet.log.io.Timer
 
+import java.io.ByteArrayOutputStream
+import java.math.BigInteger
+import java.nio.CharBuffer
+import java.nio.charset.{CodingErrorAction, UnmappableCharacterException}
 import java.time.Instant
 import scala.util.Random
 
 /**
   * Created on 2014/05/07.
   */
-class MessagePackTest extends AirSpec with PropertyCheck with Timer {
+class MessagePackTest extends AirSpec with PropertyCheck with Benchmark {
 
   private def isValidUTF8(s: String) = {
     MessagePack.UTF8.newEncoder().canEncode(s)
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala
index 65f60751f..61891b90e 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala
@@ -15,19 +15,19 @@
 //
 package org.msgpack.core
 
-import java.io.{ByteArrayOutputStream, File, FileInputStream, FileOutputStream}
-
-import org.msgpack.core.MessagePack.{UnpackerConfig, PackerConfig}
+import org.msgpack.core.MessagePack.PackerConfig
 import org.msgpack.core.buffer.{ChannelBufferOutput, OutputStreamBufferOutput}
 import org.msgpack.value.ValueFactory
+import wvlet.airspec.AirSpec
 import xerial.core.io.IOUtil
 
+import java.io.{ByteArrayOutputStream, File, FileInputStream, FileOutputStream}
 import scala.util.Random
 
 /**
   *
   */
-class MessagePackerTest extends MessagePackSpec {
+class MessagePackerTest extends AirSpec with Benchmark {
 
   private def verifyIntSeq(answer: Array[Int], packed: Array[Byte]) {
     val unpacker = MessagePack.newDefaultUnpacker(packed)
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
index 6d69cc5cc..443730722 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
@@ -16,17 +16,15 @@
 package org.msgpack.core
 
 import org.msgpack.core.MessagePackSpec.{createMessagePackData, toHex}
-
-import java.io._
-import java.nio.ByteBuffer
-import java.util.Collections
 import org.msgpack.core.buffer._
 import org.msgpack.value.ValueType
 import wvlet.airspec.AirSpec
 import wvlet.log.LogSupport
-import wvlet.log.io.Timer
 import xerial.core.io.IOUtil._
 
+import java.io._
+import java.nio.ByteBuffer
+import java.util.Collections
 import scala.collection.JavaConverters._
 import scala.util.Random
 
@@ -47,9 +45,9 @@ object MessageUnpackerTest {
   }
 }
 
-import MessageUnpackerTest._
+import org.msgpack.core.MessageUnpackerTest._
 
-class MessageUnpackerTest extends AirSpec with Timer {
+class MessageUnpackerTest extends AirSpec with Benchmark {
 
   private val universal = MessageBuffer.allocate(0).isInstanceOf[MessageBufferU]
   private def testData: Array[Byte] = {
@@ -435,7 +433,6 @@ class MessageUnpackerTest extends AirSpec with Timer {
 
       val t = time("skip performance", repeat = N) {
         block("v6") {
-          import org.msgpack.`type`.{ValueType => ValueTypeV6}
           val v6       = new org.msgpack.MessagePack()
           val unpacker = new org.msgpack.unpacker.MessagePackUnpacker(v6, new ByteArrayInputStream(data))
           var count    = 0
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala
index f6dfafd5a..0dff634cf 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala
@@ -16,18 +16,18 @@
 package org.msgpack.core.buffer
 
 import java.io._
-import java.net.{InetSocketAddress}
+import java.net.InetSocketAddress
 import java.nio.ByteBuffer
 import java.nio.channels.{ServerSocketChannel, SocketChannel}
 import java.util.concurrent.{Callable, Executors, TimeUnit}
 import java.util.zip.{GZIPInputStream, GZIPOutputStream}
-
 import org.msgpack.core.{MessagePack, MessagePackSpec}
+import wvlet.airspec.AirSpec
 import xerial.core.io.IOUtil._
 
 import scala.util.Random
 
-class MessageBufferInputTest extends MessagePackSpec {
+class MessageBufferInputTest extends AirSpec {
 
   private val targetInputSize =
     Seq(0, 10, 500, 1000, 2000, 4000, 8000, 10000, 30000, 50000, 100000)
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferOutputTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferOutputTest.scala
index 35186c57e..ea9cde57e 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferOutputTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferOutputTest.scala
@@ -15,11 +15,11 @@
 //
 package org.msgpack.core.buffer
 
-import java.io._
+import wvlet.airspec.AirSpec
 
-import org.msgpack.core.MessagePackSpec
+import java.io._
 
-class MessageBufferOutputTest extends MessagePackSpec {
+class MessageBufferOutputTest extends AirSpec {
 
   private def createTempFile = {
     val f = File.createTempFile("msgpackTest", "msgpack")
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferTest.scala
index 1dee5706d..4bb9c69da 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferTest.scala
@@ -15,16 +15,16 @@
 //
 package org.msgpack.core.buffer
 
-import java.nio.ByteBuffer
-
-import org.msgpack.core.MessagePackSpec
+import org.msgpack.core.Benchmark
+import wvlet.airspec.AirSpec
 
+import java.nio.ByteBuffer
 import scala.util.Random
 
 /**
   * Created on 2014/05/01.
   */
-class MessageBufferTest extends MessagePackSpec {
+class MessageBufferTest extends AirSpec with Benchmark {
 
   private val universal = MessageBuffer.allocate(0).isInstanceOf[MessageBufferU]
 
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/example/MessagePackExampleTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/example/MessagePackExampleTest.scala
index edd8a90a2..05dfa6e65 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/example/MessagePackExampleTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/example/MessagePackExampleTest.scala
@@ -15,12 +15,12 @@
 //
 package org.msgpack.core.example
 
-import org.msgpack.core.MessagePackSpec
+import wvlet.airspec.AirSpec
 
 /**
   *
   */
-class MessagePackExampleTest extends MessagePackSpec {
+class MessagePackExampleTest extends AirSpec {
 
   test("example") {
 
diff --git a/msgpack-core/src/test/scala/org/msgpack/value/RawStringValueImplTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/RawStringValueImplTest.scala
index 394a733de..fb340553c 100644
--- a/msgpack-core/src/test/scala/org/msgpack/value/RawStringValueImplTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/value/RawStringValueImplTest.scala
@@ -15,9 +15,9 @@
 //
 package org.msgpack.value
 
-import org.msgpack.core.MessagePackSpec
+import wvlet.airspec.AirSpec
 
-class RawStringValueImplTest extends MessagePackSpec {
+class RawStringValueImplTest extends AirSpec {
 
   test("return the same hash code if they are equal") {
     val str = "a"
diff --git a/msgpack-core/src/test/scala/org/msgpack/value/ValueTypeTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/ValueTypeTest.scala
index 00ebaafc8..4627faba4 100644
--- a/msgpack-core/src/test/scala/org/msgpack/value/ValueTypeTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/value/ValueTypeTest.scala
@@ -16,12 +16,13 @@
 package org.msgpack.value
 
 import org.msgpack.core.MessagePack.Code._
-import org.msgpack.core.{MessageFormat, MessageFormatException, MessagePackSpec}
+import org.msgpack.core.{MessageFormat, MessageFormatException}
+import wvlet.airspec.AirSpec
 
 /**
   * Created on 2014/05/06.
   */
-class ValueTypeTest extends MessagePackSpec {
+class ValueTypeTest extends AirSpec {
 
   test("lookup ValueType from a byte value") {
     def check(b: Byte, tpe: ValueType) {

From 6c0f303ee674100b358ef5f96a26a04fbad8cd8d Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 23:35:52 -0700
Subject: [PATCH 16/21] Remove scalatest

---
 README.md                                                     | 2 +-
 build.sbt                                                     | 3 +--
 .../src/test/scala/org/msgpack/core/MessageFormatTest.scala   | 4 ++--
 3 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index 2a6571ad3..1a7c1f439 100644
--- a/README.md
+++ b/README.md
@@ -70,7 +70,7 @@ Here is a list of sbt commands for daily development:
 > ~test:compile                            # Compile both source and test codes
 > ~test                                    # Run tests upon source code change
 > ~testOnly *MessagePackTest               # Run tests in the specified class
-> ~testOnly *MessagePackTest -- -n prim    # Run the test tagged as "prim"
+> ~testOnly *MessagePackTest -- (pattern)  # Run tests matching the pattern 
 > project msgpack-core                     # Focus on a specific project
 > package                                  # Create a jar file in the target folder of each project
 > findbugs                                 # Produce findbugs report in target/findbugs
diff --git a/build.sbt b/build.sbt
index 68b7e3c31..d653f8a2e 100644
--- a/build.sbt
+++ b/build.sbt
@@ -75,13 +75,12 @@ lazy val msgpackCore = Project(id = "msgpack-core", base = file("msgpack-core"))
     libraryDependencies ++= Seq(
       // msgpack-core should have no external dependencies
       junitInterface,
-      "org.scalatest"          %% "scalatest"               % "3.2.8"  % "test",
+      "org.wvlet.airframe"     %% "airspec"                 % "20.4.1" % "test",
       "org.scalacheck"         %% "scalacheck"              % "1.15.4" % "test",
       "org.xerial"             %% "xerial-core"             % "3.6.0"  % "test",
       "org.msgpack"            % "msgpack"                  % "0.6.12" % "test",
       "commons-codec"          % "commons-codec"            % "1.12"   % "test",
       "com.typesafe.akka"      %% "akka-actor"              % "2.5.23" % "test",
-      "org.wvlet.airframe"     %% "airspec"                 % "20.4.1" % "test",
       "org.scala-lang.modules" %% "scala-collection-compat" % "2.4.3"  % "test"
     )
   )
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala
index 3299f4069..5f3447617 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala
@@ -17,8 +17,8 @@ package org.msgpack.core
 
 import org.msgpack.core.MessagePack.Code
 import org.msgpack.value.ValueType
-import org.scalatest.exceptions.TestFailedException
 import wvlet.airspec.AirSpec
+import wvlet.airspec.spi.AirSpecException
 
 import scala.util.Random
 
@@ -31,7 +31,7 @@ class MessageFormatTest extends AirSpec with Benchmark {
       def checkV(b: Byte, tpe: ValueType) {
         try MessageFormat.valueOf(b).getValueType shouldBe tpe
         catch {
-          case e: TestFailedException =>
+          case e: AirSpecException =>
             error(f"Failure when looking at byte ${b}%02x")
             throw e
         }

From 81a71cdf5f44a6374bccc69bf85d1a255dc8fc94 Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 23:45:48 -0700
Subject: [PATCH 17/21] Remove xerial-core dependency

---
 build.sbt                                        | 16 +++++++++-------
 .../scala/org/msgpack/core/MessagePackSpec.scala |  1 -
 .../org/msgpack/core/MessagePackerTest.scala     |  8 ++++----
 .../org/msgpack/core/MessageUnpackerTest.scala   |  2 +-
 .../core/buffer/MessageBufferInputTest.scala     |  8 ++++----
 .../test/scala/org/msgpack/value/ValueTest.scala |  9 ++++-----
 6 files changed, 22 insertions(+), 22 deletions(-)

diff --git a/build.sbt b/build.sbt
index d653f8a2e..344f248f6 100644
--- a/build.sbt
+++ b/build.sbt
@@ -5,6 +5,8 @@ Global / concurrentRestrictions := Seq(
   Tags.limit(Tags.Test, 1)
 )
 
+val AIRFRAME_VERSION = "20.4.1"
+
 // Use dynamic snapshot version strings for non tagged versions
 ThisBuild / dynverSonatypeSnapshots := true
 // Use coursier friendly version separator
@@ -75,13 +77,13 @@ lazy val msgpackCore = Project(id = "msgpack-core", base = file("msgpack-core"))
     libraryDependencies ++= Seq(
       // msgpack-core should have no external dependencies
       junitInterface,
-      "org.wvlet.airframe"     %% "airspec"                 % "20.4.1" % "test",
-      "org.scalacheck"         %% "scalacheck"              % "1.15.4" % "test",
-      "org.xerial"             %% "xerial-core"             % "3.6.0"  % "test",
-      "org.msgpack"            % "msgpack"                  % "0.6.12" % "test",
-      "commons-codec"          % "commons-codec"            % "1.12"   % "test",
-      "com.typesafe.akka"      %% "akka-actor"              % "2.5.23" % "test",
-      "org.scala-lang.modules" %% "scala-collection-compat" % "2.4.3"  % "test"
+      "org.wvlet.airframe"     %% "airframe-json"           % AIRFRAME_VERSION % "test",
+      "org.wvlet.airframe"     %% "airspec"                 % AIRFRAME_VERSION % "test",
+      "org.scalacheck"         %% "scalacheck"              % "1.15.4"         % "test",
+      "org.msgpack"            % "msgpack"                  % "0.6.12"         % "test",
+      "commons-codec"          % "commons-codec"            % "1.12"           % "test",
+      "com.typesafe.akka"      %% "akka-actor"              % "2.5.23"         % "test",
+      "org.scala-lang.modules" %% "scala-collection-compat" % "2.4.3"          % "test"
     )
   )
 
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
index c3df7b259..dee315cd9 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala
@@ -19,7 +19,6 @@ import wvlet.log.LogLevel
 import wvlet.log.io.{TimeReport, Timer}
 
 import java.io.ByteArrayOutputStream
-import scala.language.implicitConversions
 
 object MessagePackSpec {
   def toHex(arr: Array[Byte]) = arr.map(x => f"$x%02x").mkString(" ")
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala
index 61891b90e..096a811b4 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala
@@ -19,7 +19,7 @@ import org.msgpack.core.MessagePack.PackerConfig
 import org.msgpack.core.buffer.{ChannelBufferOutput, OutputStreamBufferOutput}
 import org.msgpack.value.ValueFactory
 import wvlet.airspec.AirSpec
-import xerial.core.io.IOUtil
+import wvlet.log.io.IOUtil.withResource
 
 import java.io.{ByteArrayOutputStream, File, FileInputStream, FileOutputStream}
 import scala.util.Random
@@ -91,7 +91,7 @@ class MessagePackerTest extends AirSpec with Benchmark {
       val t = time("packer", repeat = 10) {
         block("no-buffer-reset") {
           val out = new ByteArrayOutputStream
-          IOUtil.withResource(MessagePack.newDefaultPacker(out)) { packer =>
+          withResource(MessagePack.newDefaultPacker(out)) { packer =>
             for (i <- 0 until N) {
               val outputStream = new ByteArrayOutputStream()
               packer
@@ -104,7 +104,7 @@ class MessagePackerTest extends AirSpec with Benchmark {
 
         block("buffer-reset") {
           val out = new ByteArrayOutputStream
-          IOUtil.withResource(MessagePack.newDefaultPacker(out)) { packer =>
+          withResource(MessagePack.newDefaultPacker(out)) { packer =>
             val bufferOut =
               new OutputStreamBufferOutput(new ByteArrayOutputStream())
             for (i <- 0 until N) {
@@ -237,7 +237,7 @@ class MessagePackerTest extends AirSpec with Benchmark {
   test("compute totalWrittenBytes") {
     val out = new ByteArrayOutputStream
     val packerTotalWrittenBytes =
-      IOUtil.withResource(MessagePack.newDefaultPacker(out)) { packer =>
+      withResource(MessagePack.newDefaultPacker(out)) { packer =>
         packer
           .packByte(0) // 1
           .packBoolean(true) // 1
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
index 443730722..5ae597f27 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala
@@ -20,7 +20,7 @@ import org.msgpack.core.buffer._
 import org.msgpack.value.ValueType
 import wvlet.airspec.AirSpec
 import wvlet.log.LogSupport
-import xerial.core.io.IOUtil._
+import wvlet.log.io.IOUtil.withResource
 
 import java.io._
 import java.nio.ByteBuffer
diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala
index 0dff634cf..4cc4a98a7 100644
--- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala
@@ -15,16 +15,16 @@
 //
 package org.msgpack.core.buffer
 
+import org.msgpack.core.MessagePack
+import wvlet.airspec.AirSpec
+import wvlet.log.io.IOUtil.withResource
+
 import java.io._
 import java.net.InetSocketAddress
 import java.nio.ByteBuffer
 import java.nio.channels.{ServerSocketChannel, SocketChannel}
 import java.util.concurrent.{Callable, Executors, TimeUnit}
 import java.util.zip.{GZIPInputStream, GZIPOutputStream}
-import org.msgpack.core.{MessagePack, MessagePackSpec}
-import wvlet.airspec.AirSpec
-import xerial.core.io.IOUtil._
-
 import scala.util.Random
 
 class MessageBufferInputTest extends AirSpec {
diff --git a/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala
index ce70ec4e9..83cbde6bf 100644
--- a/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala
@@ -20,11 +20,10 @@ import org.msgpack.core.MessagePackSpec.createMessagePackData
 import java.math.BigInteger
 import org.msgpack.core._
 import org.scalacheck.Prop.propBoolean
+import wvlet.airframe.json.JSON
 import wvlet.airspec.AirSpec
 import wvlet.airspec.spi.PropertyCheck
 
-import scala.util.parsing.json.JSON
-
 class ValueTest extends AirSpec with PropertyCheck {
   private def checkSuccinctType(pack: MessagePacker => Unit, expectedAtMost: MessageFormat): Boolean = {
     val b  = createMessagePackData(pack)
@@ -98,9 +97,9 @@ class ValueTest extends AirSpec with PropertyCheck {
         .put(newString("address"), newArray(newString("xxx-xxxx"), newString("yyy-yyyy")))
         .put(newString("name"), newString("mitsu"))
         .build()
-      val i1 = JSON.parseFull(m.toJson)
-      val i2 = JSON.parseFull(m.toString) // expect json value
-      val a1 = JSON.parseFull("""{"id":1001,"name":"mitsu","address":["xxx-xxxx","yyy-yyyy"]}""")
+      val i1 = JSON.parse(m.toJson)
+      val i2 = JSON.parse(m.toString) // expect json value
+      val a1 = JSON.parse("""{"id":1001,"name":"mitsu","address":["xxx-xxxx","yyy-yyyy"]}""")
       // Equals as JSON map
       i1 shouldBe a1
       i2 shouldBe a1

From 5e83c4aa723f5d97a81a230f29058bb9dac3356a Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Tue, 11 May 2021 23:49:31 -0700
Subject: [PATCH 18/21] Remove unnecessary dependencies

---
 build.sbt | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/build.sbt b/build.sbt
index 344f248f6..eae3cd51a 100644
--- a/build.sbt
+++ b/build.sbt
@@ -77,13 +77,15 @@ lazy val msgpackCore = Project(id = "msgpack-core", base = file("msgpack-core"))
     libraryDependencies ++= Seq(
       // msgpack-core should have no external dependencies
       junitInterface,
-      "org.wvlet.airframe"     %% "airframe-json"           % AIRFRAME_VERSION % "test",
-      "org.wvlet.airframe"     %% "airspec"                 % AIRFRAME_VERSION % "test",
-      "org.scalacheck"         %% "scalacheck"              % "1.15.4"         % "test",
-      "org.msgpack"            % "msgpack"                  % "0.6.12"         % "test",
-      "commons-codec"          % "commons-codec"            % "1.12"           % "test",
-      "com.typesafe.akka"      %% "akka-actor"              % "2.5.23"         % "test",
-      "org.scala-lang.modules" %% "scala-collection-compat" % "2.4.3"          % "test"
+      "org.wvlet.airframe" %% "airframe-json" % AIRFRAME_VERSION % "test",
+      "org.wvlet.airframe" %% "airspec"       % AIRFRAME_VERSION % "test",
+      // Add property testing support with forAll methods
+      "org.scalacheck" %% "scalacheck" % "1.15.4" % "test",
+      // For performance comparison with msgpack v6
+      "org.msgpack" % "msgpack" % "0.6.12" % "test",
+      // For integration test with Akka
+      "com.typesafe.akka"      %% "akka-actor"              % "2.5.23" % "test",
+      "org.scala-lang.modules" %% "scala-collection-compat" % "2.4.3"  % "test"
     )
   )
 

From f7d1d5a617b5de54906bf82312877e546af31aea Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Wed, 12 May 2021 00:07:32 -0700
Subject: [PATCH 19/21] Variable.writeTo roundtrip tests

---
 .../org/msgpack/value/VariableTest.scala      | 25 +++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/msgpack-core/src/test/scala/org/msgpack/value/VariableTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/VariableTest.scala
index d3c45abb7..67ea2efef 100644
--- a/msgpack-core/src/test/scala/org/msgpack/value/VariableTest.scala
+++ b/msgpack-core/src/test/scala/org/msgpack/value/VariableTest.scala
@@ -39,6 +39,20 @@ class VariableTest extends AirSpec with PropertyCheck {
     unpacker.close()
   }
 
+  /**
+    * Test Value -> MsgPack -> Value
+    */
+  private def roundTrip(v: Value): Unit = {
+    val packer = MessagePack.newDefaultBufferPacker()
+    v.writeTo(packer)
+    val msgpack  = packer.toByteArray
+    val unpacker = MessagePack.newDefaultUnpacker(msgpack)
+    val v1       = unpacker.unpackValue()
+    unpacker.close()
+    v shouldBe v1
+    v.immutableValue() shouldBe v1
+  }
+
   private def validateValue[V <: Value](
       v: V,
       asNil: Boolean = false,
@@ -67,6 +81,7 @@ class VariableTest extends AirSpec with PropertyCheck {
 
     if (asNil) {
       v.getValueType shouldBe ValueType.NIL
+      roundTrip(v)
     } else {
       intercept[MessageTypeCastException] {
         v.asNilValue()
@@ -75,6 +90,7 @@ class VariableTest extends AirSpec with PropertyCheck {
 
     if (asBoolean) {
       v.getValueType shouldBe ValueType.BOOLEAN
+      roundTrip(v)
     } else {
       intercept[MessageTypeCastException] {
         v.asBooleanValue()
@@ -83,6 +99,7 @@ class VariableTest extends AirSpec with PropertyCheck {
 
     if (asInteger) {
       v.getValueType shouldBe ValueType.INTEGER
+      roundTrip(v)
     } else {
       intercept[MessageTypeCastException] {
         v.asIntegerValue()
@@ -91,6 +108,7 @@ class VariableTest extends AirSpec with PropertyCheck {
 
     if (asFloat) {
       v.getValueType shouldBe ValueType.FLOAT
+      roundTrip(v)
     } else {
       intercept[MessageTypeCastException] {
         v.asFloatValue()
@@ -99,6 +117,7 @@ class VariableTest extends AirSpec with PropertyCheck {
 
     if (asBinary | asString) {
       v.asRawValue()
+      roundTrip(v)
     } else {
       intercept[MessageTypeCastException] {
         v.asRawValue()
@@ -107,6 +126,7 @@ class VariableTest extends AirSpec with PropertyCheck {
 
     if (asBinary) {
       v.getValueType shouldBe ValueType.BINARY
+      roundTrip(v)
     } else {
       intercept[MessageTypeCastException] {
         v.asBinaryValue()
@@ -115,6 +135,7 @@ class VariableTest extends AirSpec with PropertyCheck {
 
     if (asString) {
       v.getValueType shouldBe ValueType.STRING
+      roundTrip(v)
     } else {
       intercept[MessageTypeCastException] {
         v.asStringValue()
@@ -123,6 +144,7 @@ class VariableTest extends AirSpec with PropertyCheck {
 
     if (asArray) {
       v.getValueType shouldBe ValueType.ARRAY
+      roundTrip(v)
     } else {
       intercept[MessageTypeCastException] {
         v.asArrayValue()
@@ -131,6 +153,7 @@ class VariableTest extends AirSpec with PropertyCheck {
 
     if (asMap) {
       v.getValueType shouldBe ValueType.MAP
+      roundTrip(v)
     } else {
       intercept[MessageTypeCastException] {
         v.asMapValue()
@@ -139,6 +162,7 @@ class VariableTest extends AirSpec with PropertyCheck {
 
     if (asExtension) {
       v.getValueType shouldBe ValueType.EXTENSION
+      roundTrip(v)
     } else {
       intercept[MessageTypeCastException] {
         v.asExtensionValue()
@@ -147,6 +171,7 @@ class VariableTest extends AirSpec with PropertyCheck {
 
     if (asTimestamp) {
       v.getValueType shouldBe ValueType.EXTENSION
+      roundTrip(v)
     } else {
       intercept[MessageTypeCastException] {
         v.asTimestampValue()

From 40304599182f0962f3c66a6c15505deaf104eb72 Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Wed, 12 May 2021 00:10:12 -0700
Subject: [PATCH 20/21] Use consistent code style

---
 .../src/main/java/org/msgpack/core/MessageUnpacker.java    | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
index 4534c803a..5213c3d24 100644
--- a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
+++ b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
@@ -720,10 +720,11 @@ public Variable unpackValue(Variable var)
             }
             case EXTENSION: {
                 ExtensionTypeHeader extHeader = unpackExtensionTypeHeader();
-                if (extHeader.getType() == EXT_TIMESTAMP) {
+                switch (extHeader.getType()) {
+                case EXT_TIMESTAMP:
                     var.setTimestampValue(unpackTimestamp(extHeader));
-                }
-                else {
+                    break;
+                default:
                     var.setExtensionValue(extHeader.getType(), readPayload(extHeader.getLength()));
                 }
                 return var;

From 02e6b86e963c971f3822f4c28dea232f11a13c67 Mon Sep 17 00:00:00 2001
From: "Taro L. Saito" <leo@xerial.org>
Date: Wed, 12 May 2021 00:13:38 -0700
Subject: [PATCH 21/21] Fix comments

---
 .../src/main/java/org/msgpack/core/MessageUnpacker.java      | 5 +++++
 .../src/main/java/org/msgpack/value/TimestampValue.java      | 4 +---
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
index 5213c3d24..b43204beb 100644
--- a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
+++ b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
@@ -1283,6 +1283,9 @@ public Instant unpackTimestamp()
         return unpackTimestamp(ext);
     }
 
+    /**
+     * Internal method that can be used only when the extension type header is already read.
+     */
     private Instant unpackTimestamp(ExtensionTypeHeader ext) throws IOException
     {
         if (ext.getType() != EXT_TIMESTAMP) {
@@ -1290,6 +1293,7 @@ private Instant unpackTimestamp(ExtensionTypeHeader ext) throws IOException
         }
         switch (ext.getLength()) {
             case 4: {
+                // Need to convert Java's int (int32) to uint32
                 long u32 = readInt() & 0xffffffffL;
                 return Instant.ofEpochSecond(u32);
             }
@@ -1300,6 +1304,7 @@ private Instant unpackTimestamp(ExtensionTypeHeader ext) throws IOException
                 return Instant.ofEpochSecond(sec, nsec);
             }
             case 12: {
+                // Need to convert Java's int (int32) to uint32
                 long nsecU32 = readInt() & 0xffffffffL;
                 long sec = readLong();
                 return Instant.ofEpochSecond(sec, nsecU32);
diff --git a/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java b/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java
index 8be7d7338..465579b01 100644
--- a/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java
+++ b/msgpack-core/src/main/java/org/msgpack/value/TimestampValue.java
@@ -18,9 +18,7 @@
 import java.time.Instant;
 
 /**
- * Representation of MessagePack's Timestamp type.
- *
- * MessagePack's Timestamp type can represent a timestamp.
+ * Value representation of MessagePack's Timestamp type.
  */
 public interface TimestampValue
         extends ExtensionValue