FastDoubleCharBufferBoundaryTest.java

package tools.jackson.core.unittest.write;

import java.io.ByteArrayOutputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;

import org.junit.jupiter.api.Test;

import tools.jackson.core.JsonGenerator;
import tools.jackson.core.JsonToken;
import tools.jackson.core.JsonParser;
import tools.jackson.core.ObjectReadContext;
import tools.jackson.core.ObjectWriteContext;
import tools.jackson.core.PrettyPrinter;
import tools.jackson.core.StreamWriteFeature;
import tools.jackson.core.io.NumberOutput;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.unittest.JacksonCoreTestBase;
import tools.jackson.core.util.DefaultIndenter;
import tools.jackson.core.util.DefaultPrettyPrinter;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Verifies that {@code WriterBasedJsonGenerator} writing floats/doubles directly
 * into its char buffer (with {@link StreamWriteFeature#USE_FAST_DOUBLE_WRITER})
 * flushes correctly when values land on the output buffer boundary.
 */
public class FastDoubleCharBufferBoundaryTest extends JacksonCoreTestBase
{
    private final JsonFactory FAST_FACTORY = JsonFactory.builder()
            .enable(StreamWriteFeature.USE_FAST_DOUBLE_WRITER)
            .build();

    @Test
    void testDoublesAcrossBufferBoundary() throws Exception
    {
        // Enough long values to cross the default char buffer several times
        final double v = -Double.MIN_NORMAL;
        final String expectedOne = NumberOutput.toString(v, true);
        final int count = 2000;

        StringWriter sw = new StringWriter();
        try (JsonGenerator gen = FAST_FACTORY.createGenerator(ObjectWriteContext.empty(), sw)) {
            gen.writeStartArray();
            for (int i = 0; i < count; ++i) {
                gen.writeNumber(v);
            }
            gen.writeEndArray();
        }
        _verify(sw.toString(), expectedOne, count);
    }

    @Test
    void testFloatsAcrossBufferBoundary() throws Exception
    {
        final float v = -Float.MIN_NORMAL;
        final String expectedOne = NumberOutput.toString(v, true);
        final int count = 3000;

        StringWriter sw = new StringWriter();
        try (JsonGenerator gen = FAST_FACTORY.createGenerator(ObjectWriteContext.empty(), sw)) {
            gen.writeStartArray();
            for (int i = 0; i < count; ++i) {
                gen.writeNumber(v);
            }
            gen.writeEndArray();
        }
        _verify(sw.toString(), expectedOne, count);
    }

    @Test
    void testFloatsAcrossBufferBoundaryBytes() throws Exception
    {
        // Same for UTF8JsonGenerator: float-only so its (smaller) flush reserve is what trips
        final float v = -Float.MIN_NORMAL;
        final String expectedOne = NumberOutput.toString(v, true);
        final int count = 3000;

        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try (JsonGenerator gen = FAST_FACTORY.createGenerator(ObjectWriteContext.empty(), bytes)) {
            gen.writeStartArray();
            for (int i = 0; i < count; ++i) {
                gen.writeNumber(v);
            }
            gen.writeEndArray();
        }
        _verify(bytes.toString(StandardCharsets.UTF_8), expectedOne, count);
    }

    @Test
    void testMixedLengthsAcrossBufferBoundary() throws Exception
    {
        // Varying lengths so that boundary is hit at different offsets
        final double[] values = { 1.0, -Double.MIN_NORMAL, 0.1, 123456789.0, 1.0E-300, -2.5 };
        final int count = 4000;
        StringBuilder expected = new StringBuilder("[");
        StringWriter sw = new StringWriter();
        try (JsonGenerator gen = FAST_FACTORY.createGenerator(ObjectWriteContext.empty(), sw)) {
            gen.writeStartArray();
            for (int i = 0; i < count; ++i) {
                double v = values[i % values.length];
                gen.writeNumber(v);
                if (i > 0) {
                    expected.append(',');
                }
                expected.append(NumberOutput.toString(v, true));
            }
            gen.writeEndArray();
        }
        expected.append(']');
        assertEquals(expected.toString(), sw.toString());
    }

    // [core#1704]: PrettyPrinter writes indentation from within _verifyValueWrite(),
    // just before the fast path checks for room; varying the indent width walks the
    // value across every offset relative to the output buffer edge.
    @Test
    void testPrettyPrintedAcrossBufferBoundary() throws Exception
    {
        final double[] values = { -Double.MIN_NORMAL, 1.0, 0.1, 123456789.0, 1.0E-300, -2.5 };
        final int count = 500;

        for (int indentLen = 1; indentLen <= 40; ++indentLen) {
            StringBuilder indent = new StringBuilder();
            for (int i = 0; i < indentLen; ++i) {
                indent.append(' ');
            }
            final PrettyPrinter pp = new DefaultPrettyPrinter()
                    .withArrayIndenter(new DefaultIndenter(indent.toString(), "\n"));
            ObjectWriteContext ctxt = new ObjectWriteContext.Base() {
                @Override
                public PrettyPrinter getPrettyPrinter() { return pp; }
            };

            StringWriter sw = new StringWriter();
            try (JsonGenerator gen = FAST_FACTORY.createGenerator(ctxt, sw)) {
                _writeValues(gen, values, count);
            }
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            try (JsonGenerator gen = FAST_FACTORY.createGenerator(ctxt, bytes)) {
                _writeValues(gen, values, count);
            }
            String desc = "indentLen="+indentLen;
            // Char- and byte-backed generators must agree...
            String doc = sw.toString();
            assertEquals(doc, bytes.toString(StandardCharsets.UTF_8), desc);
            // ... and every value must survive the round trip
            _verifyValues(doc, values, count, desc);
        }
    }

    private void _writeValues(JsonGenerator gen, double[] values, int count)
    {
        gen.writeStartArray();
        for (int i = 0; i < count; ++i) {
            gen.writeNumber(values[i % values.length]);
        }
        gen.writeEndArray();
    }

    private void _verifyValues(String doc, double[] values, int count, String desc) throws Exception
    {
        try (JsonParser p = FAST_FACTORY.createParser(ObjectReadContext.empty(), doc)) {
            assertToken(JsonToken.START_ARRAY, p.nextToken());
            for (int i = 0; i < count; ++i) {
                assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
                assertEquals(values[i % values.length], p.getDoubleValue(), desc+", index "+i);
            }
            assertToken(JsonToken.END_ARRAY, p.nextToken());
            assertNull(p.nextToken());
        }
    }

    private void _verify(String json, String expectedOne, int count)
    {
        assertTrue(json.startsWith("["));
        assertTrue(json.endsWith("]"));
        String[] parts = json.substring(1, json.length() - 1).split(",");
        assertEquals(count, parts.length);
        for (int i = 0; i < count; ++i) {
            assertEquals(expectedOne, parts[i], "at index " + i);
        }
    }
}