tests/test_buffer.cpp

changeset 654
c9d008861178
parent 653
e081643aae2a
child 683
aa0d09f2d81c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_buffer.cpp	Wed Feb 08 20:26:26 2023 +0100
@@ -0,0 +1,815 @@
+/*
+ * 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());
+}
+
+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);
+}
+
+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