diff --git a/src/main/java/com/amazon/ion/impl/IonTypeID.java b/src/main/java/com/amazon/ion/impl/IonTypeID.java index f7a0c6f7d7..2ddcedca20 100644 --- a/src/main/java/com/amazon/ion/impl/IonTypeID.java +++ b/src/main/java/com/amazon/ion/impl/IonTypeID.java @@ -236,7 +236,7 @@ private IonTypeID(byte id, int minorVersion) { (upperNibble == 0xD && lowerNibble >= 0x2) || id == DELIMITED_STRUCT || id == VARIABLE_LENGTH_INLINE_SYMBOL - || id == VARIABLE_LENGTH_STRUCT_WITH_FLEXSYMS + || id == VARIABLE_LENGTH_STRUCT_WITH_FLEX_SYMS || id == ANNOTATIONS_1_FLEX_SYM || id == ANNOTATIONS_2_FLEX_SYM || id == ANNOTATIONS_MANY_FLEX_SYM @@ -292,7 +292,7 @@ private IonTypeID(byte id, int minorVersion) { if (id == DELIMITED_END_MARKER) { type = null; length = 0; - } else if (id == DELIMITED_STRUCT || id == VARIABLE_LENGTH_STRUCT_WITH_SIDS || id == VARIABLE_LENGTH_STRUCT_WITH_FLEXSYMS) { + } else if (id == DELIMITED_STRUCT || id == VARIABLE_LENGTH_STRUCT_WITH_SIDS || id == VARIABLE_LENGTH_STRUCT_WITH_FLEX_SYMS) { type = IonType.STRUCT; } else if (id == VARIABLE_LENGTH_INTEGER) { type = IonType.INT; diff --git a/src/main/java/com/amazon/ion/impl/IonWriter_1_1.kt b/src/main/java/com/amazon/ion/impl/IonWriter_1_1.kt index b83856a42e..29c4d9c90c 100644 --- a/src/main/java/com/amazon/ion/impl/IonWriter_1_1.kt +++ b/src/main/java/com/amazon/ion/impl/IonWriter_1_1.kt @@ -114,11 +114,10 @@ interface IonWriter_1_1 { /** * Steps into a Struct. * - * The [delimited] and [useFlexSym] parameters are suggestions. Implementations may ignore these parameters if they - * are not relevant for that particular implementation. All implementations must document their specific behavior - * for this method. + * The [delimited] parameter is a suggestion. Implementations may ignore it if it is not relevant for that + * particular implementation. All implementations must document their specific behavior for this method. */ - fun stepInStruct(delimited: Boolean, useFlexSym: Boolean) + fun stepInStruct(delimited: Boolean) /** * Steps into a stream. diff --git a/src/main/java/com/amazon/ion/impl/bin/FlexInt.kt b/src/main/java/com/amazon/ion/impl/bin/FlexInt.kt index 98ca9e2bda..61ea43559e 100644 --- a/src/main/java/com/amazon/ion/impl/bin/FlexInt.kt +++ b/src/main/java/com/amazon/ion/impl/bin/FlexInt.kt @@ -13,6 +13,11 @@ import java.math.BigInteger */ object FlexInt { + /** + * A byte representing zero, encoded as a FlexInt (or FlexUInt). + */ + const val ZERO: Byte = 0x01 + /** Determine the length of FlexUInt for the provided value. */ @JvmStatic fun flexUIntLength(value: Long): Int { diff --git a/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt b/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt index 005b1eacfc..171adf4115 100644 --- a/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt +++ b/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt @@ -5,6 +5,7 @@ package com.amazon.ion.impl.bin import com.amazon.ion.* import com.amazon.ion.impl.* import com.amazon.ion.impl.bin.IonRawBinaryWriter_1_1.ContainerType.* +import com.amazon.ion.impl.bin.Ion_1_1_Constants.* import com.amazon.ion.impl.bin.utf8.* import com.amazon.ion.util.* import java.io.ByteArrayOutputStream @@ -43,15 +44,20 @@ class IonRawBinaryWriter_1_1 internal constructor( private data class ContainerInfo( var type: ContainerType? = null, var isDelimited: Boolean = false, + var usesFlexSym: Boolean = false, var position: Long = -1, var length: Long = 0, // TODO: Test if performance is better with an Object Reference or an index into the PatchPoint queue. var patchPoint: PatchPoint? = null, ) { - fun clear() { - type = null - isDelimited = false - position = -1 + /** + * Clears this [ContainerInfo] of old data and initializes it with the given new data. + */ + fun reset(type: ContainerType? = null, position: Long = -1, isDelimited: Boolean = false) { + this.type = type + this.isDelimited = isDelimited + this.position = position + usesFlexSym = false length = 0 patchPoint = null } @@ -88,7 +94,7 @@ class IonRawBinaryWriter_1_1 internal constructor( private val patchPoints = _Private_RecyclingQueue(512) { PatchPoint() } private val containerStack = _Private_RecyclingStack(8) { ContainerInfo() } - private var currentContainer: ContainerInfo = containerStack.push { it.type = Top } + private var currentContainer: ContainerInfo = containerStack.push { it.reset(Top, 0L) } override fun finish() { if (closed) return @@ -210,6 +216,8 @@ class IonRawBinaryWriter_1_1 internal constructor( */ private inline fun openValue(valueWriterExpression: () -> Unit) { + if (isInStruct()) confirm(hasFieldName) { "Values in a struct must have a field name." } + // Start at 1, assuming there's an annotations OpCode byte. // We'll clear this if there are no annotations. var annotationsTotalLength = 1L @@ -265,11 +273,7 @@ class IonRawBinaryWriter_1_1 internal constructor( * Writes 3 or more annotations for SIDs or FlexSyms */ private fun writeManyAnnotations(): Long { - currentContainer = containerStack.push { - it.clear() - it.type = Annotations - it.position = buffer.position() - } + currentContainer = containerStack.push { it.reset(Annotations, position = buffer.position()) } if (annotationFlexSymFlag == FLEX_SYMS_REQUIRED) { buffer.writeByte(OpCodes.ANNOTATIONS_MANY_FLEX_SYM) buffer.reserve(ANNOTATIONS_LENGTH_PREFIX_ALLOCATION_SIZE) @@ -310,11 +314,35 @@ class IonRawBinaryWriter_1_1 internal constructor( } override fun writeFieldName(sid: Int) { - TODO("Not implemented yet.") + confirm(currentContainer.type == Struct) { "Can only write a field name inside of a struct." } + if (sid == 0 && !currentContainer.usesFlexSym) switchToFlexSym() + + currentContainer.length += if (currentContainer.usesFlexSym) { + buffer.writeFlexSym(sid) + } else { + buffer.writeFlexUInt(sid) + } + hasFieldName = true } override fun writeFieldName(text: CharSequence) { - TODO("Not implemented yet.") + confirm(currentContainer.type == Struct) { "Can only write a field name inside of a struct." } + if (!currentContainer.usesFlexSym) switchToFlexSym() + + currentContainer.length += buffer.writeFlexSym(utf8StringEncoder.encode(text.toString())) + hasFieldName = true + } + + private fun switchToFlexSym() { + // To switch, we need to insert the sid-to-flexsym switch marker, unless... + // if no fields have been written yet, we can just switch the op code of the struct. + if (currentContainer.length == 0L) { + buffer.writeUInt8At(currentContainer.position, OpCodes.VARIABLE_LENGTH_STRUCT_WITH_FLEX_SYMS.toLong()) + } else { + buffer.writeByte(SID_TO_FLEX_SYM_SWITCH_MARKER) + currentContainer.length += 1 + } + currentContainer.usesFlexSym = true } override fun writeNull() = writeScalar { IonEncoder_1_1.writeNullValue(buffer, IonType.NULL) } @@ -385,12 +413,7 @@ class IonRawBinaryWriter_1_1 internal constructor( override fun stepInList(delimited: Boolean) { openValue { - currentContainer = containerStack.push { - it.clear() - it.type = List - it.position = buffer.position() - it.isDelimited = delimited - } + currentContainer = containerStack.push { it.reset(List, buffer.position(), delimited) } if (delimited) { buffer.writeByte(OpCodes.DELIMITED_LIST) } else { @@ -401,11 +424,28 @@ class IonRawBinaryWriter_1_1 internal constructor( } override fun stepInSExp(delimited: Boolean) { - TODO("Not yet implemented") + openValue { + currentContainer = containerStack.push { it.reset(SExp, buffer.position(), delimited) } + if (delimited) { + buffer.writeByte(OpCodes.DELIMITED_SEXP) + } else { + buffer.writeByte(OpCodes.VARIABLE_LENGTH_SEXP) + buffer.reserve(lengthPrefixPreallocation) + } + } } - override fun stepInStruct(delimited: Boolean, useFlexSym: Boolean) { - TODO("Not yet implemented") + override fun stepInStruct(delimited: Boolean) { + openValue { + currentContainer = containerStack.push { it.reset(Struct, buffer.position(), delimited) } + if (delimited) { + buffer.writeByte(OpCodes.DELIMITED_STRUCT) + currentContainer.usesFlexSym = true + } else { + buffer.writeByte(OpCodes.VARIABLE_LENGTH_STRUCT_WITH_SIDS) + buffer.reserve(lengthPrefixPreallocation) + } + } } override fun stepInEExp(name: CharSequence) { @@ -430,24 +470,32 @@ class IonRawBinaryWriter_1_1 internal constructor( // Write closing delimiter if we're in a delimited container. // Update length prefix if we're in a prefixed container. if (currentContainer.isDelimited) { + if (isInStruct()) { + // Need a 0 FlexInt before the end delimiter + buffer.writeByte(FlexInt.ZERO) + thisContainerTotalLength += 1 + } thisContainerTotalLength += 1 // For the end marker buffer.writeByte(OpCodes.DELIMITED_END_MARKER) } else { // currentContainer.type is non-null for any initialized ContainerInfo when (currentContainer.type.assumeNotNull()) { - List -> { - // TODO: Possibly extract this so it can be reused for the other length-prefixed container types + List, SExp, Struct -> { val contentLength = currentContainer.length - if (contentLength <= 0xF) { + if (contentLength <= 0xF && !currentContainer.usesFlexSym) { // Clean up any unused space that was pre-allocated. buffer.shiftBytesLeft(currentContainer.length.toInt(), lengthPrefixPreallocation) - buffer.writeUInt8At(currentContainer.position, OpCodes.LIST_ZERO_LENGTH + contentLength) + val zeroLengthOpCode = when (currentContainer.type) { + List -> OpCodes.LIST_ZERO_LENGTH + SExp -> OpCodes.SEXP_ZERO_LENGTH + Struct -> OpCodes.STRUCT_SID_ZERO_LENGTH + else -> TODO("Unreachable") + } + buffer.writeUInt8At(currentContainer.position, zeroLengthOpCode + contentLength) } else { thisContainerTotalLength += writeCurrentContainerLength(lengthPrefixPreallocation) } } - SExp -> TODO() - Struct -> TODO() Macro -> TODO() Stream -> TODO() Annotations -> TODO("Unreachable.") diff --git a/src/main/java/com/amazon/ion/impl/bin/Ion_1_1_Constants.java b/src/main/java/com/amazon/ion/impl/bin/Ion_1_1_Constants.java index 7aa507e8ae..c50bbd7a45 100644 --- a/src/main/java/com/amazon/ion/impl/bin/Ion_1_1_Constants.java +++ b/src/main/java/com/amazon/ion/impl/bin/Ion_1_1_Constants.java @@ -13,6 +13,9 @@ private Ion_1_1_Constants() {} static final int FIRST_2_BYTE_SYMBOL_ADDRESS = 256; static final int FIRST_MANY_BYTE_SYMBOL_ADDRESS = 65792; + + static final byte SID_TO_FLEX_SYM_SWITCH_MARKER = 0; + public static final int MAX_NANOSECONDS = 999999999; public static final int NANOSECOND_SCALE = 9; public static final int MAX_MICROSECONDS = 999999; diff --git a/src/main/java/com/amazon/ion/impl/bin/OpCodes.java b/src/main/java/com/amazon/ion/impl/bin/OpCodes.java index 35fe3ad65f..9544c65ba4 100644 --- a/src/main/java/com/amazon/ion/impl/bin/OpCodes.java +++ b/src/main/java/com/amazon/ion/impl/bin/OpCodes.java @@ -34,10 +34,23 @@ private OpCodes() {} // 0x7D-0x7F Reserved public static final byte STRING_ZERO_LENGTH = (byte) 0x80; + // 0x81-0x8F are additional lengths of strings. public static final byte INLINE_SYMBOL_ZERO_LENGTH = (byte) 0x90; + // 0x91-0x9F are additional lengths of symbols. public static final byte LIST_ZERO_LENGTH = (byte) 0xA0; + // 0xA1-0xAF are additional lengths of lists. + + public static final byte SEXP_ZERO_LENGTH = (byte) 0xB0; + // 0xB1-0xBF are additional lengths of sexps. + + public static final byte STRUCT_SID_ZERO_LENGTH = (byte) 0xC0; + // 0xC1 Reserved + // 0xC2-0xCF are additional lengths of structs. + + // 0xD0-0xDF + // See https://github.com/amazon-ion/ion-docs/issues/292 public static final byte SYMBOL_ADDRESS_1_BYTE = (byte) 0xE1; public static final byte SYMBOL_ADDRESS_2_BYTES = (byte) 0xE2; @@ -68,7 +81,7 @@ private OpCodes() {} public static final byte VARIABLE_LENGTH_LIST = (byte) 0xFA; public static final byte VARIABLE_LENGTH_SEXP = (byte) 0xFB; public static final byte VARIABLE_LENGTH_STRUCT_WITH_SIDS = (byte) 0xFC; - public static final byte VARIABLE_LENGTH_STRUCT_WITH_FLEXSYMS = (byte) 0xFD; + public static final byte VARIABLE_LENGTH_STRUCT_WITH_FLEX_SYMS = (byte) 0xFD; public static final byte VARIABLE_LENGTH_BLOB = (byte) 0xFE; public static final byte VARIABLE_LENGTH_CLOB = (byte) 0xFF; } diff --git a/src/main/java/com/amazon/ion/impl/bin/WriteBuffer.java b/src/main/java/com/amazon/ion/impl/bin/WriteBuffer.java index 30a717e74e..1801a8125f 100644 --- a/src/main/java/com/amazon/ion/impl/bin/WriteBuffer.java +++ b/src/main/java/com/amazon/ion/impl/bin/WriteBuffer.java @@ -1529,7 +1529,7 @@ public int writeFlexSym(int sid) { if (sid != 0) { return writeFlexInt(sid); } else { - writeByte((byte) 0x01); + writeByte(FlexInt.ZERO); writeByte((byte) 0x90); return 2; } @@ -1540,7 +1540,7 @@ public int writeFlexSym(int sid) { */ public int writeFlexSym(Utf8StringEncoder.Result text) { if (text.getEncodedLength() == 0) { - writeByte((byte) 0x01); + writeByte(FlexInt.ZERO); writeByte((byte) 0x80); return 2; } else { diff --git a/src/test/java/com/amazon/ion/impl/bin/IonRawBinaryWriterTest_1_1.kt b/src/test/java/com/amazon/ion/impl/bin/IonRawBinaryWriterTest_1_1.kt index 4ae3b03c75..0a74acb5ad 100644 --- a/src/test/java/com/amazon/ion/impl/bin/IonRawBinaryWriterTest_1_1.kt +++ b/src/test/java/com/amazon/ion/impl/bin/IonRawBinaryWriterTest_1_1.kt @@ -27,7 +27,13 @@ class IonRawBinaryWriterTest_1_1 { return baos.toByteArray().joinToString(" ") { it.toHexString(HexFormat.UpperCase) } } + /** + * @param hexBytes a string containing white-space delimited pairs of hex digits representing the expected output. + * The string may contain multiple lines. Anything after a `|` character on a line is ignored, so + * you can use `|` to add comments. + */ private inline fun assertWriterOutputEquals(hexBytes: String, autoClose: Boolean = true, block: IonRawBinaryWriter_1_1.() -> Unit) { + val hexBytes = hexBytes.replace(Regex("\\s+(\\|.*\\n)?\\s+"), " ").trim().uppercase() assertEquals(hexBytes, writeAsHexString(autoClose, block)) } @@ -201,7 +207,7 @@ class IonRawBinaryWriterTest_1_1 { @Test fun `write a variable-length prefixed list`() { - assertWriterOutputEquals("FA 21${" 5E".repeat(16)}") { + assertWriterOutputEquals("FA 21 ${" 5E".repeat(16)}") { stepInList(false) repeat(16) { writeBool(true) } stepOut() @@ -211,7 +217,7 @@ class IonRawBinaryWriterTest_1_1 { @Test fun `write a prefixed list that is so long it requires patch points`() { - assertWriterOutputEquals("FA 02 02${" 5E".repeat(128)}") { + assertWriterOutputEquals("FA 02 02 ${" 5E".repeat(128)}") { stepInList(false) repeat(128) { writeBool(true) } stepOut() @@ -245,6 +251,374 @@ class IonRawBinaryWriterTest_1_1 { } } + @Test + fun `write a delimited sexp`() { + assertWriterOutputEquals("F2 5E 5F F0") { + stepInSExp(true) + writeBool(true) + writeBool(false) + stepOut() + } + } + + @Test + fun `write a prefixed sexp`() { + assertWriterOutputEquals("B2 5E 5F") { + stepInSExp(false) + writeBool(true) + writeBool(false) + stepOut() + } + } + + @Test + fun `write a variable-length prefixed sexp`() { + assertWriterOutputEquals("FB 21 ${" 5E".repeat(16)}") { + stepInSExp(false) + repeat(16) { writeBool(true) } + stepOut() + finish() + } + } + + @Test + fun `write a prefixed sexp that is so long it requires patch points`() { + assertWriterOutputEquals("FB 02 02 ${" 5E".repeat(128)}") { + stepInSExp(false) + repeat(128) { writeBool(true) } + stepOut() + } + } + + @Test + fun `write multiple nested prefixed sexps`() { + assertWriterOutputEquals("B4 B3 B2 B1 B0") { + repeat(5) { stepInSExp(false) } + repeat(5) { stepOut() } + } + } + + @Test + fun `write multiple nested delimited sexps`() { + assertWriterOutputEquals("F2 F2 F2 F2 F0 F0 F0 F0") { + repeat(4) { stepInSExp(true) } + repeat(4) { stepOut() } + } + } + + @Test + fun `write multiple nested delimited and prefixed sexps`() { + assertWriterOutputEquals("F2 B9 F2 B6 F2 B3 F2 B0 F0 F0 F0 F0") { + repeat(4) { + stepInSExp(true) + stepInSExp(false) + } + repeat(8) { stepOut() } + } + } + + @Test + fun `write a prefixed struct`() { + assertWriterOutputEquals( + """ + C4 | Struct Length = 4 + 17 | SID 11 + 5E | true + 19 | SID 12 + 5F | false + """ + ) { + stepInStruct(false) + writeFieldName(11) + writeBool(true) + writeFieldName(12) + writeBool(false) + stepOut() + } + } + + @Test + fun `write a variable length prefixed struct`() { + assertWriterOutputEquals( + """ + FC | Variable Length SID Struct + 21 | Length = 16 + ${"17 5E ".repeat(8)} + """ + ) { + stepInStruct(false) + repeat(8) { + writeFieldName(11) + writeBool(true) + } + stepOut() + } + } + + @Test + fun `write a struct so long it requires patch points`() { + assertWriterOutputEquals( + """ + FC | Variable Length SID Struct + 02 02 | Length = 128 + ${"17 5E ".repeat(64)} + """ + ) { + stepInStruct(false) + repeat(64) { + writeFieldName(11) + writeBool(true) + } + stepOut() + } + } + + @Test + fun `write multiple nested prefixed structs`() { + assertWriterOutputEquals( + """ + C8 | Struct Length = 8 + 17 | SID 11 + C6 | Struct Length = 6 + 17 | SID 11 + C4 | Struct Length = 4 + 17 | SID 11 + C2 | Struct Length = 2 + 17 | SID 11 + C0 | Struct Length = 0 + """ + ) { + stepInStruct(false) + repeat(4) { + writeFieldName(11) + stepInStruct(false) + } + repeat(5) { + stepOut() + } + } + } + + @Test + fun `write multiple nested delimited structs`() { + assertWriterOutputEquals( + """ + F3 | Begin delimited struct + 17 | FlexSym SID 11 + F3 | Begin delimited struct + 17 F3 17 F3 17 F3 | etc. + 01 F0 | End delimited struct + 01 F0 01 F0 01 F0 01 F0 | etc. + """ + ) { + stepInStruct(true) + repeat(4) { + writeFieldName(11) + stepInStruct(true) + } + repeat(5) { + stepOut() + } + } + } + + @Test + fun `write empty prefixed struct`() { + assertWriterOutputEquals("C0") { + stepInStruct(false) + stepOut() + } + } + + @Test + fun `write delimited struct`() { + assertWriterOutputEquals( + """ + F3 | Begin delimited struct + 17 | SID 11 + 5E | true + FB 66 6F 6F | FlexSym 'foo' + 5E | true + 02 01 | FlexSym SID 64 + 5E | true + 01 F0 | End delimited struct + """ + ) { + stepInStruct(true) + writeFieldName(11) + writeBool(true) + writeFieldName("foo") + writeBool(true) + writeFieldName(64) + writeBool(true) + stepOut() + } + } + + @Test + fun `write empty delimited struct`() { + assertWriterOutputEquals( + """ + F3 | Begin delimited struct + 01 F0 | End delimited struct + """ + ) { + stepInStruct(true) + stepOut() + } + } + + @Test + fun `write prefixed struct with a single flex sym field`() { + assertWriterOutputEquals( + """ + FD | Variable length FlexSym Struct + 0B | Length = 5 + FB 66 6F 6F | FlexSym 'foo' + 5E | true + """ + ) { + stepInStruct(false) + writeFieldName("foo") + writeBool(true) + stepOut() + } + } + + @Test + fun `write prefixed struct with multiple fields and flex syms`() { + assertWriterOutputEquals( + """ + FD | Variable length FlexSym Struct + 1F | Length = 15 + FB 66 6F 6F | FlexSym 'foo' + 5E | true + FB 62 61 72 | FlexSym 'bar' + 5E | true + FB 62 61 7A | FlexSym 'baz' + 5E | true + """ + ) { + stepInStruct(false) + writeFieldName("foo") + writeBool(true) + writeFieldName("bar") + writeBool(true) + writeFieldName("baz") + writeBool(true) + stepOut() + } + } + + @Test + fun `write prefixed struct that starts with sids and switches partway through to use flex syms`() { + assertWriterOutputEquals( + """ + FC | Variable length SID Struct + 17 | Length = 11 + 81 | SID 64 + 5E | true + 00 | switch to FlexSym encoding + FB 66 6F 6F | FlexSym 'foo' + 5E | true + 02 01 | FlexSym SID 64 + 5E | true + """ + ) { + stepInStruct(false) + writeFieldName(64) + writeBool(true) + writeFieldName("foo") + writeBool(true) + writeFieldName(64) + writeBool(true) + stepOut() + } + } + + @Test + fun `write prefixed struct with sid 0`() { + assertWriterOutputEquals( + """ + FD | Variable length FlexSym Struct + 07 | Length = 3 + 01 90 | FlexSym SID 0 + 5E | true + """ + ) { + stepInStruct(false) + writeFieldName(0) + writeBool(true) + stepOut() + } + } + + @Test + fun `write prefixed struct with sid 0 after another value`() { + assertWriterOutputEquals( + """ + FC | Variable length SID struct + 17 | Length = FlexUInt 11 + 03 | SID 1 + 5E | true + 00 | switch to FlexSym encoding + 01 90 | FlexSym SID 0 + 5E | true + 05 | FlexSym SID 2 + 5E | true + 01 90 | FlexSym SID 0 + 5E | true + """ + ) { + stepInStruct(false) + writeFieldName(1) + writeBool(true) + writeFieldName(0) + writeBool(true) + writeFieldName(2) + writeBool(true) + writeFieldName(0) + writeBool(true) + stepOut() + } + } + + @Test + fun `writing a value in a struct with no field name should throw an exception`() { + assertWriterThrows { + stepInStruct(true) + writeBool(true) + } + assertWriterThrows { + stepInStruct(false) + writeBool(true) + } + } + + @Test + fun `calling writeFieldName outside of a struct should throw an exception`() { + assertWriterThrows { + writeFieldName(12) + } + assertWriterThrows { + writeFieldName("foo") + } + } + + @Test + fun `calling stepOut with a dangling field name should throw an exception`() { + assertWriterThrows { + stepInStruct(false) + writeFieldName(12) + stepOut() + } + assertWriterThrows { + stepInStruct(true) + writeFieldName("foo") + stepOut() + } + } + @Test fun `writeAnnotations with empty int array should write no annotations`() { assertWriterOutputEquals("5E") {