tests/test_buffer.cpp

Wed, 27 Dec 2023 16:18:10 +0100

author
Mike Becker <universe@uap-core.de>
date
Wed, 27 Dec 2023 16:18:10 +0100
changeset 773
bbf2f18b3818
parent 761
61d5197d612b
child 789
9b2f5661bebd
permissions
-rw-r--r--

fix missing LDFLAGS when linking ucxtest

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "cx/buffer.h"

#include <gtest/gtest.h>
#include "util_allocator.h"

class BufferFixture : public ::testing::Test {
protected:
    void SetUp() override {
        cxBufferInit(&buf, nullptr, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
        buf.size = 6;
        buf.pos = 3;
    }

    void TearDown() override {
        cxBufferDestroy(&buf);
    }

    CxBuffer buf{};
};

static void expect_default_flush_config(CxBuffer *buf) {
    EXPECT_EQ(buf->flush_blkmax, 0);
    EXPECT_EQ(buf->flush_blksize, 4096);
    EXPECT_EQ(buf->flush_threshold, SIZE_MAX);
    EXPECT_EQ(buf->flush_func, nullptr);
    EXPECT_EQ(buf->flush_target, nullptr);
}

TEST(BufferInit, WrapSpace) {
    CxTestingAllocator alloc;
    CxBuffer buf;
    void *space = cxMalloc(&alloc, 16);
    cxBufferInit(&buf, space, 16, &alloc, CX_BUFFER_DEFAULT);
    expect_default_flush_config(&buf);
    EXPECT_EQ(buf.space, space);
    EXPECT_EQ(buf.flags & CX_BUFFER_AUTO_EXTEND, 0);
    EXPECT_EQ(buf.flags & CX_BUFFER_FREE_CONTENTS, 0);
    EXPECT_EQ(buf.pos, 0);
    EXPECT_EQ(buf.size, 0);
    EXPECT_EQ(buf.capacity, 16);
    EXPECT_EQ(buf.allocator, &alloc);
    cxBufferDestroy(&buf);
    EXPECT_FALSE(alloc.verify());
    cxFree(&alloc, space);
    EXPECT_TRUE(alloc.verify());
}

TEST(BufferInit, WrapSpaceAutoExtend) {
    CxTestingAllocator alloc;
    CxBuffer buf;
    void *space = cxMalloc(&alloc, 16);
    cxBufferInit(&buf, space, 16, &alloc, CX_BUFFER_AUTO_EXTEND);
    expect_default_flush_config(&buf);
    EXPECT_EQ(buf.space, space);
    EXPECT_EQ(buf.flags & CX_BUFFER_AUTO_EXTEND, CX_BUFFER_AUTO_EXTEND);
    EXPECT_EQ(buf.flags & CX_BUFFER_FREE_CONTENTS, 0);
    EXPECT_EQ(buf.pos, 0);
    EXPECT_EQ(buf.size, 0);
    EXPECT_EQ(buf.capacity, 16);
    EXPECT_EQ(buf.allocator, &alloc);
    cxBufferDestroy(&buf);
    EXPECT_FALSE(alloc.verify());
    cxFree(&alloc, space);
    EXPECT_TRUE(alloc.verify());
}

TEST(BufferInit, WrapSpaceAutoFree) {
    CxTestingAllocator alloc;
    CxBuffer buf;
    void *space = cxMalloc(&alloc, 16);
    cxBufferInit(&buf, space, 16, &alloc, CX_BUFFER_FREE_CONTENTS);
    expect_default_flush_config(&buf);
    EXPECT_EQ(buf.space, space);
    EXPECT_EQ(buf.flags & CX_BUFFER_AUTO_EXTEND, 0);
    EXPECT_EQ(buf.flags & CX_BUFFER_FREE_CONTENTS, CX_BUFFER_FREE_CONTENTS);
    EXPECT_EQ(buf.pos, 0);
    EXPECT_EQ(buf.size, 0);
    EXPECT_EQ(buf.capacity, 16);
    EXPECT_EQ(buf.allocator, &alloc);
    EXPECT_FALSE(alloc.verify());
    cxBufferDestroy(&buf);
    EXPECT_TRUE(alloc.verify());
}

TEST(BufferInit, FreshSpace) {
    CxTestingAllocator alloc;
    CxBuffer buf;
    cxBufferInit(&buf, nullptr, 8, &alloc, CX_BUFFER_DEFAULT);
    expect_default_flush_config(&buf);
    EXPECT_NE(buf.space, nullptr);
    EXPECT_EQ(buf.flags & CX_BUFFER_AUTO_EXTEND, 0);
    EXPECT_EQ(buf.flags & CX_BUFFER_FREE_CONTENTS, CX_BUFFER_FREE_CONTENTS);
    EXPECT_EQ(buf.pos, 0);
    EXPECT_EQ(buf.size, 0);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(buf.allocator, &alloc);
    EXPECT_FALSE(alloc.verify()); // space is still allocated
    cxBufferDestroy(&buf);
    EXPECT_TRUE(alloc.verify());
}

TEST(BufferInit, OnHeap) {
    CxTestingAllocator alloc;
    CxBuffer *buf;
    void *space = cxMalloc(&alloc, 16);
    buf = cxBufferCreate(space, 16, &alloc, CX_BUFFER_FREE_CONTENTS);
    EXPECT_NE(buf, nullptr);
    expect_default_flush_config(buf);
    EXPECT_EQ(buf->space, space);
    EXPECT_EQ(buf->flags & CX_BUFFER_AUTO_EXTEND, 0);
    EXPECT_EQ(buf->flags & CX_BUFFER_FREE_CONTENTS, CX_BUFFER_FREE_CONTENTS);
    EXPECT_EQ(buf->pos, 0);
    EXPECT_EQ(buf->size, 0);
    EXPECT_EQ(buf->capacity, 16);
    EXPECT_EQ(buf->allocator, &alloc);
    cxBufferFree(buf);
    EXPECT_TRUE(alloc.verify());
}

class BufferShiftFixture : public ::testing::Test {
protected:
    void SetUp() override {
        ASSERT_TRUE(alloc.verify());
        cxBufferInit(&buf, nullptr, 16, &alloc, CX_BUFFER_DEFAULT);
        memcpy(buf.space, "test____________", 16);
        buf.capacity = 8; // purposely pretend that the buffer has less capacity s.t. we can test beyond the range
        buf.pos = 4;
        buf.size = 4;
    }

    void TearDown() override {
        cxBufferDestroy(&buf);
        EXPECT_TRUE(alloc.verify());
    }

    CxTestingAllocator alloc;
    CxBuffer buf{};
};

class BufferShiftLeft : public BufferShiftFixture {
};

TEST_F(BufferShiftLeft, Zero) {
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    int ret = cxBufferShiftLeft(&buf, 0);
    EXPECT_EQ(ret, 0);
    EXPECT_EQ(buf.pos, 4);
    EXPECT_EQ(buf.size, 4);
    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
}

TEST_F(BufferShiftLeft, ZeroOffsetInterface) {
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    int ret = cxBufferShift(&buf, -0);
    EXPECT_EQ(ret, 0);
    EXPECT_EQ(buf.pos, 4);
    EXPECT_EQ(buf.size, 4);
    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
}

TEST_F(BufferShiftLeft, Standard) {
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    int ret = cxBufferShiftLeft(&buf, 2);
    EXPECT_EQ(ret, 0);
    EXPECT_EQ(buf.pos, 2);
    EXPECT_EQ(buf.size, 2);
    EXPECT_TRUE(memcmp(buf.space, "stst________", 8) == 0);
}

TEST_F(BufferShiftLeft, Overshift) {
    ASSERT_LT(buf.pos, 6);
    ASSERT_LT(buf.size, 6);
    int ret = cxBufferShiftLeft(&buf, 6);
    EXPECT_EQ(ret, 0);
    EXPECT_EQ(buf.pos, 0);
    EXPECT_EQ(buf.size, 0);
    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
}

TEST_F(BufferShiftLeft, OvershiftPosOnly) {
    buf.pos = 2;
    ASSERT_EQ(buf.size, 4);
    int ret = cxBufferShiftLeft(&buf, 3);
    EXPECT_EQ(ret, 0);
    EXPECT_EQ(buf.pos, 0);
    EXPECT_EQ(buf.size, 1);
    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
}

TEST_F(BufferShiftLeft, OffsetInterface) {
    buf.pos = 3;
    ASSERT_EQ(buf.size, 4);
    int ret = cxBufferShift(&buf, -2);
    EXPECT_EQ(ret, 0);
    EXPECT_EQ(buf.pos, 1);
    EXPECT_EQ(buf.size, 2);
    EXPECT_TRUE(memcmp(buf.space, "stst________", 8) == 0);
}

class BufferShiftRight : public BufferShiftFixture {
};

TEST_F(BufferShiftRight, Zero) {
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    int ret = cxBufferShiftRight(&buf, 0);
    EXPECT_EQ(ret, 0);
    EXPECT_EQ(buf.pos, 4);
    EXPECT_EQ(buf.size, 4);
    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
}

TEST_F(BufferShiftRight, ZeroOffsetInterface) {
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    int ret = cxBufferShift(&buf, +0);
    EXPECT_EQ(ret, 0);
    EXPECT_EQ(buf.pos, 4);
    EXPECT_EQ(buf.size, 4);
    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
}

TEST_F(BufferShiftRight, Standard) {
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    int ret = cxBufferShiftRight(&buf, 3);
    EXPECT_EQ(ret, 0);
    EXPECT_EQ(buf.pos, 7);
    EXPECT_EQ(buf.size, 7);
    EXPECT_TRUE(memcmp(buf.space, "testest_____", 8) == 0);
}

TEST_F(BufferShiftRight, OvershiftDiscard) {
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    ASSERT_EQ(buf.capacity, 8);
    int ret = cxBufferShiftRight(&buf, 6);
    EXPECT_EQ(ret, 0);
    EXPECT_EQ(buf.pos, 8);
    EXPECT_EQ(buf.size, 8);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_TRUE(memcmp(buf.space, "test__te____", 8) == 0);
}

TEST_F(BufferShiftRight, OvershiftExtend) {
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    ASSERT_EQ(buf.capacity, 8);
    buf.flags |= CX_BUFFER_AUTO_EXTEND;
    int ret = cxBufferShiftRight(&buf, 6);
    EXPECT_EQ(ret, 0);
    EXPECT_EQ(buf.pos, 10);
    EXPECT_EQ(buf.size, 10);
    EXPECT_GE(buf.capacity, 10);
    EXPECT_TRUE(memcmp(buf.space, "test__test__", 8) == 0);
}

TEST_F(BufferShiftRight, OffsetInterface) {
    buf.pos = 3;
    ASSERT_EQ(buf.size, 4);
    int ret = cxBufferShift(&buf, 2);
    EXPECT_EQ(ret, 0);
    EXPECT_EQ(buf.pos, 5);
    EXPECT_EQ(buf.size, 6);
    EXPECT_TRUE(memcmp(buf.space, "tetest______", 8) == 0);
}

TEST(BufferMinimumCapacity, Sufficient) {
    CxTestingAllocator alloc;
    auto space = cxMalloc(&alloc, 8);
    CxBuffer buf;
    cxBufferInit(&buf, space, 8, &alloc, CX_BUFFER_FREE_CONTENTS);
    memcpy(space, "Testing", 8);
    buf.size = 8;
    cxBufferMinimumCapacity(&buf, 6);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(buf.size, 8);
    EXPECT_TRUE(memcmp(buf.space, "Testing", 8) == 0);
    cxBufferDestroy(&buf);
    EXPECT_TRUE(alloc.verify());
}

TEST(BufferMinimumCapacity, Extend) {
    CxTestingAllocator alloc;
    auto space = cxMalloc(&alloc, 8);
    CxBuffer buf;
    cxBufferInit(&buf, space, 8, &alloc, CX_BUFFER_FREE_CONTENTS); // NO auto extend!
    memcpy(space, "Testing", 8);
    buf.size = 8;
    cxBufferMinimumCapacity(&buf, 16);
    EXPECT_EQ(buf.capacity, 16);
    EXPECT_EQ(buf.size, 8);
    EXPECT_TRUE(memcmp(buf.space, "Testing", 8) == 0);
    cxBufferDestroy(&buf);
    EXPECT_TRUE(alloc.verify());
}

TEST(BufferClear, Test) {
    char space[16];
    strcpy(space, "clear test");
    CxBuffer buf;
    cxBufferInit(&buf, space, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
    ASSERT_EQ(buf.size, 0);
    // only clear the used part of the buffer
    cxBufferClear(&buf);
    EXPECT_EQ(memcmp(space, "clear test", 10), 0);
    buf.size = 5;
    buf.pos = 3;
    cxBufferClear(&buf);
    EXPECT_EQ(memcmp(space, "\0\0\0\0\0 test", 10), 0);
    EXPECT_EQ(buf.size, 0);
    EXPECT_EQ(buf.pos, 0);
    cxBufferDestroy(&buf);
}

TEST(BufferReset, Test) {
    char space[16];
    strcpy(space, "reset test");
    CxBuffer buf;
    cxBufferInit(&buf, space, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
    buf.size = 5;
    buf.pos = 3;
    cxBufferReset(&buf);
    EXPECT_EQ(memcmp(space, "reset test", 10), 0);
    EXPECT_EQ(buf.size, 0);
    EXPECT_EQ(buf.pos, 0);
    cxBufferDestroy(&buf);
}

class BufferWrite : public ::testing::Test {
protected:
    CxBuffer buf{}, target{};

    void SetUp() override {
        cxBufferInit(&target, nullptr, 16, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
        cxBufferInit(&buf, nullptr, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
        buf.capacity = 8; // artificially reduce capacity to check OOB writes
        memset(buf.space, 0, 16);
        memcpy(buf.space, "prep", 4);
        buf.size = buf.pos = 4;
    }

    void TearDown() override {
        cxBufferDestroy(&buf);
        cxBufferDestroy(&target);
    }

    void enableFlushing() {
        buf.flush_target = &target;
        buf.flush_func = reinterpret_cast<cx_write_func>(cxBufferWrite);
        buf.flush_blkmax = 1;
    }
};

static size_t mock_write_limited_rate(
        void const *ptr,
        size_t size,
        __attribute__((unused)) size_t nitems,
        CxBuffer *buffer
) {
    // simulate limited target drain capacity
    static bool full = false;
    if (full) {
        full = false;
        return 0;
    } else {
        full = true;
        return cxBufferWrite(ptr, size, nitems > 2 ? 2 : nitems, buffer);
    }
}

TEST_F(BufferWrite, SizeOneFit) {
    const char *data = "test";
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    size_t written = cxBufferWrite(data, 1, 4, &buf);
    EXPECT_EQ(written, 4);
    EXPECT_EQ(buf.size, 8);
    EXPECT_EQ(buf.pos, 8);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(memcmp(buf.space, "preptest", 8), 0);
}

TEST_F(BufferWrite, SizeOneDiscard) {
    const char *data = "testing";
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    size_t written = cxBufferWrite(data, 1, 7, &buf);
    EXPECT_EQ(written, 4);
    EXPECT_EQ(buf.size, 8);
    EXPECT_EQ(buf.pos, 8);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(memcmp(buf.space, "preptest\0", 9), 0);
}

TEST_F(BufferWrite, SizeOneExtend) {
    buf.flags |= CX_BUFFER_AUTO_EXTEND;
    const char *data = "testing";
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    size_t written = cxBufferWrite(data, 1, 7, &buf);
    EXPECT_EQ(written, 7);
    EXPECT_EQ(buf.size, 11);
    EXPECT_EQ(buf.pos, 11);
    EXPECT_GE(buf.capacity, 11);
    EXPECT_EQ(memcmp(buf.space, "preptesting", 11), 0);
}

TEST_F(BufferWrite, MultibyteFit) {
    const char *data = "test";
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    size_t written = cxBufferWrite(data, 2, 2, &buf);
    EXPECT_EQ(written, 2);
    EXPECT_EQ(buf.size, 8);
    EXPECT_EQ(buf.pos, 8);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(memcmp(buf.space, "preptest", 8), 0);
}

TEST_F(BufferWrite, MultibyteDiscard) {
    const char *data = "testing";
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.size, 4);
    buf.pos = 3;
    size_t written = cxBufferWrite(data, 2, 4, &buf);
    // remember: whole elements are discarded if they do not fit
    EXPECT_EQ(written, 2);
    EXPECT_EQ(buf.size, 7);
    EXPECT_EQ(buf.pos, 7);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(memcmp(buf.space, "pretest\0", 8), 0);
}

TEST_F(BufferWrite, MultibyteExtend) {
    buf.flags |= CX_BUFFER_AUTO_EXTEND;
    const char *data = "tester";
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.size, 4);
    buf.pos = 3;
    size_t written = cxBufferWrite(data, 2, 3, &buf);
    // remember: whole elements are discarded if they do not fit
    EXPECT_EQ(written, 3);
    EXPECT_EQ(buf.size, 9);
    EXPECT_EQ(buf.pos, 9);
    EXPECT_GE(buf.capacity, 9);
    EXPECT_EQ(memcmp(buf.space, "pretester", 9), 0);
}

TEST_F(BufferWrite, PutcWrapperFit) {
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    int c = cxBufferPut(&buf, 0x200 | 'a');
    EXPECT_EQ(c, 'a');
    EXPECT_EQ(buf.size, 5);
    EXPECT_EQ(buf.pos, 5);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(memcmp(buf.space, "prepa\0", 6), 0);
}

TEST_F(BufferWrite, PutcWrapperDiscard) {
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.size, 4);
    buf.pos = 8;
    int c = cxBufferPut(&buf, 0x200 | 'a');
    EXPECT_EQ(c, EOF);
    EXPECT_EQ(buf.size, 4);
    EXPECT_EQ(buf.pos, 8);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(memcmp(buf.space, "prep\0\0\0\0\0", 9), 0);
}

TEST_F(BufferWrite, PutcWrapperExtend) {
    buf.flags |= CX_BUFFER_AUTO_EXTEND;
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.size, 4);
    buf.pos = 8;
    int c = cxBufferPut(&buf, 0x200 | 'a');
    EXPECT_EQ(c, 'a');
    EXPECT_EQ(buf.size, 9);
    EXPECT_EQ(buf.pos, 9);
    EXPECT_GE(buf.capacity, 9);
    EXPECT_EQ(memcmp(buf.space, "prep\0\0\0\0a", 9), 0);
}

TEST_F(BufferWrite, PutStringWrapperFit) {
    const char *data = "test";
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    size_t written = cxBufferPutString(&buf, data);
    EXPECT_EQ(written, 4);
    EXPECT_EQ(buf.size, 8);
    EXPECT_EQ(buf.pos, 8);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(memcmp(buf.space, "preptest", 8), 0);
}

TEST_F(BufferWrite, PutStringWrapperDiscard) {
    const char *data = "testing";
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    size_t written = cxBufferPutString(&buf, data);
    EXPECT_EQ(written, 4);
    EXPECT_EQ(buf.size, 8);
    EXPECT_EQ(buf.pos, 8);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(memcmp(buf.space, "preptest\0", 9), 0);
}

TEST_F(BufferWrite, PutStringWrapperExtend) {
    buf.flags |= CX_BUFFER_AUTO_EXTEND;
    const char *data = "testing";
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    size_t written = cxBufferPutString(&buf, data);
    EXPECT_EQ(written, 7);
    EXPECT_EQ(buf.size, 11);
    EXPECT_EQ(buf.pos, 11);
    EXPECT_GE(buf.capacity, 11);
    EXPECT_EQ(memcmp(buf.space, "preptesting", 11), 0);
}

TEST_F(BufferWrite, MultOverflow) {
    const char *data = "testing";
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    size_t written = cxBufferWrite(data, 8, SIZE_MAX / 4, &buf);
    EXPECT_EQ(written, 0);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(buf.pos, 4);
    EXPECT_EQ(buf.size, 4);
    EXPECT_EQ(memcmp(buf.space, "prep\0", 5), 0);
}

TEST_F(BufferWrite, MaxCapaOverflow) {
    buf.flags |= CX_BUFFER_AUTO_EXTEND;
    const char *data = "testing";
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.pos, 4);
    ASSERT_EQ(buf.size, 4);
    size_t written = cxBufferWrite(data, 1, SIZE_MAX - 2, &buf);
    EXPECT_EQ(written, 0);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(buf.pos, 4);
    EXPECT_EQ(buf.size, 4);
    EXPECT_EQ(memcmp(buf.space, "prep\0", 5), 0);
}

TEST_F(BufferWrite, OnlyOverwrite) {
    buf.flags |= CX_BUFFER_AUTO_EXTEND;
    ASSERT_EQ(buf.capacity, 8);
    memcpy(buf.space, "preptest", 8);
    buf.pos = 3;
    buf.size = 8;
    size_t written = cxBufferWrite("XXX", 2, 2, &buf);
    EXPECT_EQ(written, 2);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(buf.size, 8);
    EXPECT_EQ(buf.pos, 7);
    EXPECT_EQ(memcmp(buf.space, "preXXX\0t", 8), 0);
}

TEST_F(BufferWrite, FlushAtCapacity) {
    enableFlushing();
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.pos, 4);
    size_t written = cxBufferWrite("foo", 1, 3, &buf);
    EXPECT_EQ(written, 3);
    ASSERT_EQ(buf.pos, 7);
    ASSERT_EQ(buf.size, 7);
    ASSERT_EQ(target.pos, 0);
    ASSERT_EQ(target.size, 0);
    written = cxBufferWrite("hello", 1, 5, &buf);
    EXPECT_EQ(written, 5);
    EXPECT_EQ(buf.pos, 0);
    EXPECT_EQ(buf.size, 0);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(target.pos, 12);
    ASSERT_EQ(target.size, 12);
    EXPECT_EQ(memcmp(target.space, "prepfoohello", 12), 0);
}

TEST_F(BufferWrite, FlushAtThreshold) {
    enableFlushing();
    buf.flush_threshold = 12;
    buf.flags |= CX_BUFFER_AUTO_EXTEND;
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.pos, 4);
    size_t written = cxBufferWrite("foobar", 1, 6, &buf);
    EXPECT_EQ(written, 6);
    ASSERT_EQ(buf.pos, 10);
    ASSERT_EQ(buf.size, 10);
    ASSERT_GE(buf.capacity, 10);
    ASSERT_LE(buf.capacity, 12);
    ASSERT_EQ(target.pos, 0);
    ASSERT_EQ(target.size, 0);
    written = cxBufferWrite("hello", 1, 5, &buf);
    EXPECT_EQ(written, 5);
    EXPECT_EQ(buf.pos, 0);
    EXPECT_EQ(buf.size, 0);
    EXPECT_LE(buf.capacity, 12);
    EXPECT_EQ(target.pos, 15);
    ASSERT_EQ(target.size, 15);
    EXPECT_EQ(memcmp(target.space, "prepfoobarhello", 15), 0);
}

TEST_F(BufferWrite, FlushRateLimited) {
    enableFlushing();
    // limit the rate of the flush function and the capacity of the target
    target.capacity = 16;
    target.flags &= ~CX_BUFFER_AUTO_EXTEND;
    buf.flush_func = (cx_write_func) mock_write_limited_rate;
    ASSERT_EQ(buf.capacity, 8);
    ASSERT_EQ(buf.pos, 4);
    size_t written = cxBufferWrite("foo", 1, 3, &buf);
    EXPECT_EQ(written, 3);
    ASSERT_EQ(buf.pos, 7);
    ASSERT_EQ(buf.size, 7);
    ASSERT_EQ(target.pos, 0);
    ASSERT_EQ(target.size, 0);
    written = cxBufferWrite("hello, world!", 1, 13, &buf);
    // " world!" fits into this buffer, the remaining stuff is flushed out
    EXPECT_EQ(written, 13);
    EXPECT_EQ(buf.pos, 7);
    EXPECT_EQ(buf.size, 7);
    EXPECT_EQ(buf.capacity, 8);
    EXPECT_EQ(memcmp(buf.space, " world!", 7), 0);
    EXPECT_EQ(target.pos, 13);
    ASSERT_EQ(target.size, 13);
    EXPECT_EQ(target.capacity, 16);
    EXPECT_EQ(memcmp(target.space, "prepfoohello,", 13), 0);
}

class BufferSeek : public BufferFixture {
};

TEST_F(BufferSeek, SetZero) {
    int result = cxBufferSeek(&buf, 0, SEEK_SET);
    EXPECT_EQ(result, 0);
    EXPECT_EQ(buf.pos, 0);
}

TEST_F(BufferSeek, SetValid) {
    int result = cxBufferSeek(&buf, 5, SEEK_SET);
    EXPECT_EQ(result, 0);
    EXPECT_EQ(buf.pos, 5);
}

TEST_F(BufferSeek, SetInvalid) {
    ASSERT_EQ(buf.pos, 3);
    int result = cxBufferSeek(&buf, 6, SEEK_SET);
    EXPECT_NE(result, 0);
    EXPECT_EQ(buf.pos, 3);
}

TEST_F(BufferSeek, CurZero) {
    ASSERT_EQ(buf.pos, 3);
    int result = cxBufferSeek(&buf, 0, SEEK_CUR);
    EXPECT_EQ(result, 0);
    EXPECT_EQ(buf.pos, 3);
}

TEST_F(BufferSeek, CurValidPositive) {
    ASSERT_EQ(buf.pos, 3);
    int result = cxBufferSeek(&buf, 2, SEEK_CUR);
    EXPECT_EQ(result, 0);
    EXPECT_EQ(buf.pos, 5);
}

TEST_F(BufferSeek, CurValidNegative) {
    ASSERT_EQ(buf.pos, 3);
    int result = cxBufferSeek(&buf, -3, SEEK_CUR);
    EXPECT_EQ(result, 0);
    EXPECT_EQ(buf.pos, 0);
}

TEST_F(BufferSeek, CurInvalidPositive) {
    ASSERT_EQ(buf.pos, 3);
    int result = cxBufferSeek(&buf, 3, SEEK_CUR);
    EXPECT_NE(result, 0);
    EXPECT_EQ(buf.pos, 3);
}

TEST_F(BufferSeek, CurInvalidNegative) {
    ASSERT_EQ(buf.pos, 3);
    int result = cxBufferSeek(&buf, -4, SEEK_CUR);
    EXPECT_NE(result, 0);
    EXPECT_EQ(buf.pos, 3);
}

TEST_F(BufferSeek, EndZero) {
    ASSERT_EQ(buf.size, 6);
    int result = cxBufferSeek(&buf, 0, SEEK_END);
    // the (past-the-)end position is always invalid
    EXPECT_NE(result, 0);
    EXPECT_EQ(buf.pos, 3);
}

TEST_F(BufferSeek, EndValid) {
    ASSERT_EQ(buf.size, 6);
    int result = cxBufferSeek(&buf, -6, SEEK_END);
    EXPECT_EQ(result, 0);
    EXPECT_EQ(buf.pos, 0);
}

TEST_F(BufferSeek, EndInvalid) {
    ASSERT_EQ(buf.size, 6);
    int result = cxBufferSeek(&buf, 1, SEEK_END);
    EXPECT_NE(result, 0);
    EXPECT_EQ(buf.pos, 3);
}

TEST_F(BufferSeek, WhenceInvalid) {
    ASSERT_EQ(buf.size, 6);
    ASSERT_EQ(buf.pos, 3);
    int result = cxBufferSeek(&buf, 2, 9000);
    EXPECT_NE(result, 0);
    EXPECT_EQ(buf.size, 6);
    EXPECT_EQ(buf.pos, 3);
}

class BufferEof : public BufferFixture {
};

TEST_F(BufferEof, Reached) {
    buf.pos = buf.size;
    EXPECT_TRUE(cxBufferEof(&buf));
    buf.pos = buf.size - 1;
    ASSERT_FALSE(cxBufferEof(&buf));
    cxBufferPut(&buf, 'a');
    EXPECT_TRUE(cxBufferEof(&buf));
}

TEST_F(BufferEof, NotReached) {
    buf.pos = buf.size - 1;
    EXPECT_FALSE(cxBufferEof(&buf));
    buf.pos = 0;
    cxBufferWrite("test", 1, 5, &buf);
    EXPECT_FALSE(cxBufferEof(&buf));
}

class BufferRead : public ::testing::Test {
protected:
    CxBuffer buf{};

    void SetUp() override {
        cxBufferInit(&buf, nullptr, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
        buf.capacity = 8; // artificially reduce capacity to check OOB writes
        memset(buf.space, 0, 16);
        memcpy(buf.space, "some data", 9);
        buf.size = 9;
    }

    void TearDown() override {
        cxBufferDestroy(&buf);
    }
};

TEST_F(BufferRead, GetByte) {
    buf.pos = 2;
    EXPECT_EQ(cxBufferGet(&buf), 'm');
    EXPECT_EQ(cxBufferGet(&buf), 'e');
    EXPECT_EQ(cxBufferGet(&buf), ' ');
    EXPECT_EQ(cxBufferGet(&buf), 'd');
    EXPECT_EQ(buf.pos, 6);
}

TEST_F(BufferRead, GetEof) {
    buf.pos = buf.size;
    EXPECT_EQ(cxBufferGet(&buf), EOF);
}

TEST_F(BufferRead, ReadWithinBounds) {
    buf.pos = 2;
    char target[4];
    auto read = cxBufferRead(&target, 1, 4, &buf);
    ASSERT_EQ(read, 4);
    EXPECT_EQ(memcmp(&target, "me d", 4), 0);
    EXPECT_EQ(buf.pos, 6);
}

TEST_F(BufferRead, ReadOutOfBounds) {
    buf.pos = 6;
    char target[4];
    auto read = cxBufferRead(&target, 1, 4, &buf);
    ASSERT_EQ(read, 3);
    EXPECT_EQ(memcmp(&target, "ata", 3), 0);
    EXPECT_EQ(buf.pos, 9);
}

TEST_F(BufferRead, ReadOutOfBoundsMultibyte) {
    buf.pos = 6;
    char target[4];
    target[2] = '\0';
    auto read = cxBufferRead(&target, 2, 2, &buf);
    ASSERT_EQ(read, 1);
    EXPECT_EQ(memcmp(&target, "at\0", 3), 0);
    EXPECT_EQ(buf.pos, 8);
}

TEST_F(BufferRead, ReadEof) {
    buf.pos = 9;
    char target[4];
    auto read = cxBufferRead(&target, 1, 1, &buf);
    ASSERT_EQ(read, 0);
    EXPECT_EQ(buf.pos, 9);
}

mercurial