src/server/test/io.c

Mon, 12 Aug 2024 00:22:37 +0200

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Mon, 12 Aug 2024 00:22:37 +0200
changeset 543
3335f431a91b
parent 513
9a49c245a49c
child 548
40ecc0a6b280
permissions
-rw-r--r--

implement keep-alive timeout

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2023 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 "io.h"

#include "testutils.h"


UCX_TEST(test_io_http_stream_parse_chunk_header_hdronly_first) {
    char *str = strdup("100\r\n");
    size_t len = strlen(str);
    char *str2 = strdup("12345\r\n");
    size_t len2 = strlen(str2);
    char *str3 = strdup("FF\r\n");
    size_t len3 = strlen(str3);
    
    UCX_TEST_BEGIN;
    
    int64_t chunklen;
    int ret = http_stream_parse_chunk_header(str, len, TRUE, &chunklen);
    UCX_TEST_ASSERT(ret == 5, "ret != 5");
    UCX_TEST_ASSERT(chunklen == 0x100, "wrong chunk length");
    
    // test 2
    ret = http_stream_parse_chunk_header(str2, len2, TRUE, &chunklen);
    UCX_TEST_ASSERT(ret == 7, "ret != 7 (test 2)");
    UCX_TEST_ASSERT(chunklen == 0x12345, "wrong chunk length (test 2)");
    
    // test 3: hex test
    ret = http_stream_parse_chunk_header(str3, len3, TRUE, &chunklen);
    UCX_TEST_ASSERT(ret == 4, "ret != 7 (test 3)");
    UCX_TEST_ASSERT(chunklen == 0xFF, "wrong chunk length (test 3)");
            
    UCX_TEST_END;
    free(str);
    free(str2);
}

UCX_TEST(test_io_http_stream_parse_chunk_header_hdronly) {
    char *str = strdup("\r\n100\r\n");
    size_t len = strlen(str);
    char *str2 = strdup("\nab\n");
    size_t len2 = strlen(str2);
    
    UCX_TEST_BEGIN;
    
    int64_t chunklen;
    int ret = http_stream_parse_chunk_header(str, len, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == 7, "ret != 7");
    UCX_TEST_ASSERT(chunklen == 0x100, "wrong chunk length");
    
    // test 2 with just \n as line break
    ret = http_stream_parse_chunk_header(str2, len2, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == 4, "ret != 5 (test 2)");
    UCX_TEST_ASSERT(chunklen == 0xab, "wrong chunk length (test 2)");
            
    UCX_TEST_END;
    free(str);
    free(str2);
}

UCX_TEST(test_io_http_stream_parse_chunk_header_hdronly_seq_fail) {
    // test: after the first chunk header, \r\n is required before any new header
    char *str = strdup("ff\r\n");
    size_t len = strlen(str);
    
    UCX_TEST_BEGIN;
    
    int64_t chunklen;
    int ret = http_stream_parse_chunk_header(str, len, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == -1, "ret != -1");
            
    UCX_TEST_END;
    free(str);
}

UCX_TEST(test_io_http_stream_parse_chunk_header_hdr_data) {
    char *str = strdup("\r\nb\r\nhello world\r\n");
    size_t len = strlen(str);
    
    UCX_TEST_BEGIN;
    
    int64_t chunklen;
    int ret = http_stream_parse_chunk_header(str, len, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == 5, "ret != 5");
            
    UCX_TEST_END;
    free(str);
}

UCX_TEST(test_io_http_stream_parse_chunk_header_empty) {
    char *str = "";
    size_t len = strlen(str);
    
    UCX_TEST_BEGIN;
    
    int64_t chunklen;
    int ret = http_stream_parse_chunk_header(str, len, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == 0, "ret != 0");
    
    ret = http_stream_parse_chunk_header(str, len, TRUE, &chunklen);
    UCX_TEST_ASSERT(ret == 0, "ret != 0 (test 2)");
            
    UCX_TEST_END;
}

UCX_TEST(test_io_http_stream_parse_chunk_header_partial_first) {
    char *str = strdup("123");
    size_t len = strlen(str);
    
    UCX_TEST_BEGIN;
         
    int64_t chunklen;
    int ret = http_stream_parse_chunk_header(str, len, TRUE, &chunklen);
    UCX_TEST_ASSERT(ret == 0, "ret != 0");
    
    UCX_TEST_END;
    free(str);
}

UCX_TEST(test_io_http_stream_parse_chunk_header_partial) {
    char *str = strdup("123");
    size_t len = strlen(str);
    char *str2 = strdup("\r\n");
    size_t len2 = strlen(str2);
    char *str3 = strdup("\r");
    size_t len3 = strlen(str3);
    char *str4 = strdup("\r\n123");
    size_t len4 = strlen(str4);
    
    UCX_TEST_BEGIN;
         
    int64_t chunklen;
    int ret = http_stream_parse_chunk_header(str, len, TRUE, &chunklen);
    UCX_TEST_ASSERT(ret == 0, "ret != 0");
    ret = http_stream_parse_chunk_header(str2, len2, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == 0, "ret != 0");
    ret = http_stream_parse_chunk_header(str3, len3, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == 0, "ret != 0");
    ret = http_stream_parse_chunk_header(str4, len4, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == 0, "ret != 0");
    
    UCX_TEST_END;
    free(str);
    free(str2);
    free(str3);
    free(str4);
}

UCX_TEST(test_io_http_stream_parse_chunk_header_invalid) {
    char *str = strdup("hello\r\n");
    size_t len = strlen(str);
    char *str2 = strdup("x4\r\n\r\n123\r\n");
    size_t len2 = strlen(str2);
    char *str3 = strdup("\r\n\r\n123\r\n");
    size_t len3 = strlen(str3);
    
    UCX_TEST_BEGIN;
    
    int64_t chunklen;
    int ret;
    
    ret = http_stream_parse_chunk_header(str, len, TRUE, &chunklen);
    UCX_TEST_ASSERT(ret == -1, "ret != -1 (test 1a)");
    ret = http_stream_parse_chunk_header(str, len, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == -1, "ret != -1 (test 1b)");
    
    ret = http_stream_parse_chunk_header(str2, len2, TRUE, &chunklen);
    UCX_TEST_ASSERT(ret == -1, "ret != -1 (test 1a)");
    ret = http_stream_parse_chunk_header(str2, len2, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == -1, "ret != -1 (test 1b)");
    
    ret = http_stream_parse_chunk_header(str3, len3, TRUE, &chunklen);
    UCX_TEST_ASSERT(ret == -1, "ret != -1 (test 1a)");
    ret = http_stream_parse_chunk_header(str3, len3, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == -1, "ret != -1 (test 1b)");
            
    UCX_TEST_END;
    free(str);
    free(str2);
    free(str3);
}

UCX_TEST(test_io_http_stream_parse_chunk_header_zero) {
    char *str = strdup("\r\n0\r\n\r\n");
    size_t len = strlen(str);
    char *str2 = strdup("0\r\n\r\n");
    size_t len2 = strlen(str2);
    
    // incomplete
    char *str3 = strdup("\r\n0\r\n");
    size_t len3 = strlen(str3);
    char *str4 = strdup("\r\n0");
    size_t len4 = strlen(str4);
    
    
    UCX_TEST_BEGIN;
         
    int64_t chunklen = -1;
    int ret = http_stream_parse_chunk_header(str, len, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == 7, "ret != 7");
    UCX_TEST_ASSERT(chunklen == 0, "chunklen != 0");
    
    chunklen = -1;
    ret = http_stream_parse_chunk_header(str2, len2, TRUE, &chunklen);
    UCX_TEST_ASSERT(ret == 5, "ret != 5 (test 2)");
    UCX_TEST_ASSERT(chunklen == 0, "chunklen != 0 (test 2)");
    
    // expect 0 (incomplete)
    ret = http_stream_parse_chunk_header(str3, len3, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == 0, "ret != 3 (test 3)");
    
    ret = http_stream_parse_chunk_header(str4, len4, FALSE, &chunklen);
    UCX_TEST_ASSERT(ret == 0, "ret != 3 (test 4)");
    
    UCX_TEST_END;
    free(str);
    free(str2);
    free(str3);
    free(str4);
}


UCX_TEST(test_io_httpstream_write) {
    Session *sn = testutil_session();
    
    TestIOStream *st = testutil_iostream(2048, TRUE);
    IOStream *http = httpstream_new(sn->pool, (IOStream*)st);
    
    UCX_TEST_BEGIN;
    
    char *msg = "hello world!";
    size_t msglen = strlen(msg);
    
    ssize_t w = net_write(http, msg, msglen);
    
    UCX_TEST_ASSERT(w == msglen, "wrong size returned by net_write");
    UCX_TEST_ASSERT(st->buf->size == msglen, "wrong buffer size");
    UCX_TEST_ASSERT(!memcmp(st->buf->space, msg, msglen), "wrong buffer content");
    
    // test again, make sure the second message is written directly after the wirst one
    char *msg2 = "test";
    size_t msglen2 = strlen(msg2);
    
    w = net_write(http, msg2, msglen2);
    
    UCX_TEST_ASSERT(w == msglen2, "wrong size returned by net_write (2)");
    UCX_TEST_ASSERT(st->buf->size == msglen+msglen2, "wrong buffer size (2)");
    UCX_TEST_ASSERT(!memcmp(st->buf->space + msglen, msg2, msglen2), "wrong buffer content (2)");
    
    UCX_TEST_END;
    
    testutil_destroy_session(sn);
}

UCX_TEST(test_io_httpstream_chunked_write) {
    Session *sn = testutil_session();
    
    TestIOStream *st = testutil_iostream(2048, TRUE);
    IOStream *http = httpstream_new(sn->pool, (IOStream*)st);
    httpstream_enable_chunked_write(http);
    
    UCX_TEST_BEGIN;
    
    char *msg = "hello world!";
    size_t msglen = strlen(msg);
    
    char *bufmsg = "c\r\nhello world!\r\n";
    size_t bufmsglen = strlen(bufmsg);
    
    ssize_t w = net_write(http, msg, msglen);
    
    cxstring s1 = cx_strn(st->buf->space, st->buf->size);
    cxstring s2 = cx_strn(bufmsg, bufmsglen);
    
    UCX_TEST_ASSERT(w == msglen, "wrong size returned by net_write");
    UCX_TEST_ASSERT(st->buf->size == bufmsglen, "wrong buffer size");
    UCX_TEST_ASSERT(!cx_strcasecmp(s1, s2), "wrong buffer content");
    
    // write again
    w = net_write(http, msg, msglen);
    UCX_TEST_ASSERT(w == msglen, "write 2: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 2*bufmsglen, "write 2: wrong buf size");
    
    cxstring s3 = cx_strn(st->buf->space+bufmsglen, bufmsglen);
    UCX_TEST_ASSERT(!cx_strcasecmp(s2, s3), "write 2: wrong buf content");
    
    
    UCX_TEST_END;
}

UCX_TEST(test_io_httpstream_chunked_write_end) {
    Session *sn = testutil_session();
    
    TestIOStream *st = testutil_iostream(2048, TRUE);
    IOStream *http = httpstream_new(sn->pool, (IOStream*)st);
    httpstream_enable_chunked_write(http);
    
    UCX_TEST_BEGIN;
    
    char *msg = "hello world!";
    size_t msglen = strlen(msg);
    
    char *bufmsg = "c\r\nhello world!\r\n0\r\n\r\n";
    size_t bufmsglen = strlen(bufmsg);
    
    ssize_t w = net_write(http, msg, msglen);
    net_finish(http);
    
    cxstring s1 = cx_strn(st->buf->space, st->buf->size);
    cxstring s2 = cx_strn(bufmsg, bufmsglen);
    
    UCX_TEST_ASSERT(w == msglen, "wrong size returned by net_write");
    UCX_TEST_ASSERT(st->buf->size == bufmsglen, "wrong buffer size");
    UCX_TEST_ASSERT(!cx_strcasecmp(s1, s2), "wrong buffer content");
    
    
    UCX_TEST_END;
}

UCX_TEST(test_io_httpstream_chunked_write_xx) {
    // This test creates a giant buffer and writes it with
    // chunked transfer encoding to the http stream with varying chunk length
    
    Session *sn = testutil_session();
    
    TestIOStream *st = testutil_iostream(2048, TRUE);
    IOStream *http = httpstream_new(sn->pool, (IOStream*)st);
    httpstream_enable_chunked_write(http);
    
    UCX_TEST_BEGIN;
    
    // create test data
    CxBuffer *testdata = cxBufferCreate(NULL, 1024*1024*4, cxDefaultAllocator, 0);
    for(size_t i=0;i<testdata->capacity;i++) {
        cxBufferPut(testdata, 35+(i%91));
    }
    
    // write chunks, start with single diget chunk length and increase
    // chunk size with each step
    size_t pos = 0;
    int j = 0;
    ssize_t i=15;
    while(pos<testdata->size) {
        char *buf = testdata->space + pos;
        size_t remaining = testdata->size - pos;
        ssize_t len = pos + i < remaining ? i : remaining;
        pos += len;
        
        ssize_t w = net_write(http, buf, len);
        
        UCX_TEST_ASSERT(w == len, "wrong size returned by net_write");
        i+=100; // increase chunk size
        j++; // debug
    }
    
    // terminate chunk
    net_finish(http);
    
    // code below also used in test_io_httpstream_chunked_write_xx_limit
    
    // make sure the output is correctly encoded
    // extract chunks from st->buf by using http_stream_parse_chunk_header
    // (which should be well-tested)
    WSBool first_chunk = TRUE;
    int64_t chunklen = 0;
    
    char *buf = st->buf->space;
    size_t bufsize = st->buf->size;
    
    pos = 0; // st->buf position
    size_t srcpos = 0; // testdata position
    int debug_counter = 0;
    while(pos < bufsize) {
        ssize_t remaining =  bufsize - pos;
        ssize_t src_remaining = testdata->size - srcpos;
        
        int ret = http_stream_parse_chunk_header(buf+pos, remaining, first_chunk, &chunklen);
        first_chunk = FALSE;
        
        // ret must always be > 0 (0: incomplete chunk header, -1: invalid syntax)
        UCX_TEST_ASSERT(ret > 0, "http_stream_parse_chunk_header ret <= 0");
        if(chunklen == 0) {
            UCX_TEST_ASSERT(src_remaining == 0, "stream end reached but src_remaining > 0");
            break;
        }
        
        UCX_TEST_ASSERT(chunklen <= src_remaining, "chunklen > src_remaining");
        
        char *src_chunk = testdata->space+srcpos;
        char *buf_chunk = buf+pos+ret;
        
        UCX_TEST_ASSERT(!memcmp(buf_chunk, src_chunk, chunklen), "memcmp failed");
        
        pos += ret + chunklen;
        srcpos += chunklen;
        
        debug_counter++;
    }
    
    cxBufferFree(testdata);
    testutil_destroy_session(sn);
    testutil_iostream_destroy(st);
    
    UCX_TEST_END;
}

UCX_TEST(test_io_httpstream_chunked_write_partial_header) {
    Session *sn = testutil_session();
    
    TestIOStream *st = testutil_iostream(2048, TRUE);
    IOStream *http = httpstream_new(sn->pool, (IOStream*)st);
    httpstream_enable_chunked_write(http);
    
    UCX_TEST_BEGIN;
    
    memset(st->buf->space, 0, st->buf->capacity);
    
    char *msg = "hello world!";
    size_t msglen = strlen(msg);
    
    st->max_write = 1; // limit the test stream max write size
    io_set_max_writes(1);
    
    // only 1 byte of the header is written, 0 bytes of msg
    ssize_t w = net_write(http, msg, msglen);
    UCX_TEST_ASSERT(w == 0, "write 1: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 1, "write 1: wrong buf size");
    UCX_TEST_ASSERT(tolower(st->buf->space[0]) == 'c', "write 1: wrong buf content");
    
    // next header byte: '\r'
    w = net_write(http, msg, msglen);
    UCX_TEST_ASSERT(w == 0, "write 2: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 2, "write 2: wrong buf size");
    UCX_TEST_ASSERT(st->buf->space[1] == '\r', "write 2: wrong content");
    
    // next header byte: '\n'
    w = net_write(http, msg, msglen);
    UCX_TEST_ASSERT(w == 0, "write 3: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 3, "write 3: wrong buf size");
    UCX_TEST_ASSERT(st->buf->space[2] == '\n', "write 3: wrong content");
    
    // next: content
    w = net_write(http, msg, msglen);
    UCX_TEST_ASSERT(w == 1, "write 4: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 4, "write 3: wrong buf size");
    UCX_TEST_ASSERT(st->buf->space[3] == msg[0], "write 3: wrong content");
    
    testutil_destroy_session(sn);
    testutil_iostream_destroy(st);
    
    UCX_TEST_END;
}

UCX_TEST(test_io_httpstream_chunked_write_partial_data) {
    Session *sn = testutil_session();
    
    TestIOStream *st = testutil_iostream(2048, TRUE);
    IOStream *http = httpstream_new(sn->pool, (IOStream*)st);
    httpstream_enable_chunked_write(http);
    
    UCX_TEST_BEGIN;
    
    memset(st->buf->space, 0, st->buf->capacity);
    
    char *msg = "hello world!";
    size_t msglen = strlen(msg);
    size_t msglen_orig = msglen;
    
    // limit first write to 3 to only write the header
    st->max_write = 3;
    io_set_max_writes(1);
    
    ssize_t w = net_write(http, msg, msglen);
    
    UCX_TEST_ASSERT(w == 0, "write 1: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 3, "write 1: wrong buf size");
    UCX_TEST_ASSERT(st->buf->space[0] == 'c', "write 1: wrong buf content");
    UCX_TEST_ASSERT(st->buf->space[2] == '\n', "write 1: wrong buf content");
    
    w = net_write(http, msg, msglen);
    UCX_TEST_ASSERT(w == 3, "write 2: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 6, "write 2: wrong buf size");
    UCX_TEST_ASSERT(!memcmp(st->buf->space, "c\r\nhel\0", 7), "write 2: wrong buf content");
    
    msg += w;
    msglen -= w;
    
    w = net_write(http, msg, msglen);
    UCX_TEST_ASSERT(w == 3, "write 3: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 9, "write 3: wrong buf size");
    UCX_TEST_ASSERT(!memcmp(st->buf->space, "c\r\nhello \0", 10), "write 3: wrong buf content");
    
    st->max_write = 1024;
    msg += w;
    msglen -= w;
    
    w = net_write(http, msg, msglen);
    UCX_TEST_ASSERT(w == msglen, "write 4: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 3 + msglen_orig + 2, "write 4: wrong buf size");
    UCX_TEST_ASSERT(!memcmp(st->buf->space, "c\r\nhello world!\r\n", st->buf->size), "write 4: wrong buf content");
    
    testutil_destroy_session(sn);
    testutil_iostream_destroy(st);
    
    UCX_TEST_END;
}

UCX_TEST(test_io_httpstream_chunked_write_partial_trailer) {
    Session *sn = testutil_session();
    
    TestIOStream *st = testutil_iostream(2048, TRUE);
    IOStream *http = httpstream_new(sn->pool, (IOStream*)st);
    httpstream_enable_chunked_write(http);
    io_set_max_writes(1);
    
    UCX_TEST_BEGIN;
    
    memset(st->buf->space, 0, st->buf->capacity);
    
    char *msg = "hello world!";
    size_t msglen = strlen(msg);
    
    char *msg2 = "newmsg";
    size_t msglen2 = strlen(msg2);
    
    char *msg3 = "msg3";
    size_t msglen3 = strlen(msg3);
    
    st->max_write = 3 + msglen; // header + msg, but without trailer
    
    ssize_t w = net_write(http, msg, msglen);
    
    UCX_TEST_ASSERT(w == msglen, "write 1: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 3 + msglen, "write 1: wrong buf size");
    UCX_TEST_ASSERT(!memcmp(st->buf->space, "c\r\nhello world!\0", st->buf->size + 1), "write 1: wrong buf content");
    
    st->max_write = 2 + 3 + msglen2; // trailer + new header + new msg, without new trailer
    
    w = net_write(http, msg2, msglen2);
    UCX_TEST_ASSERT(w == msglen2, "write 2: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 3 + msglen + 2 + 3 + msglen2, "write 2: wrong buf size");
    UCX_TEST_ASSERT(!memcmp(st->buf->space, "c\r\nhello world!\r\n6\r\nnewmsg\0", st->buf->size + 1), "write 2: wrong buf content");
    
    // limit write to 1 byte: two writes required for trailer, net_write should return 0
    st->max_write = 1;
    
    w = net_write(http, "dummymsg", 8);
    UCX_TEST_ASSERT(w == 0, "write 3: wrong return value");
    
    w = net_write(http, "dummymsg", 8);
    UCX_TEST_ASSERT(w == 0, "write 4: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 3 + msglen + 2 + 3 + msglen2 + 2, "write 4: wrong buf size");
    UCX_TEST_ASSERT(!memcmp(st->buf->space, "c\r\nhello world!\r\n6\r\nnewmsg\r\n\0", st->buf->size + 1), "write 4: wrong buf content");
    
    st->max_write = 1024;
    w = net_write(http, msg3, msglen3);
    
    UCX_TEST_ASSERT(w == msglen3, "write 5: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 3 + msglen + 2 + 3 + msglen2 + 2 + 3 + msglen3 + 2, "write 5: wrong buf size");
    UCX_TEST_ASSERT(!memcmp(st->buf->space, "c\r\nhello world!\r\n6\r\nnewmsg\r\n4\r\nmsg3\r\n", st->buf->size + 1), "write 5: wrong buf content");
    
    
    testutil_destroy_session(sn);
    testutil_iostream_destroy(st);
    
    UCX_TEST_END;
}

UCX_TEST(test_io_httpstream_chunked_write_partial_trailer_partial_header) {
    Session *sn = testutil_session();
    
    TestIOStream *st = testutil_iostream(2048, TRUE);
    IOStream *http = httpstream_new(sn->pool, (IOStream*)st);
    httpstream_enable_chunked_write(http);
    io_set_max_writes(1);
    
    UCX_TEST_BEGIN;
    
    memset(st->buf->space, 0, st->buf->capacity);
    
    char *msg = "hello world!";
    size_t msglen = strlen(msg);
    
    char *msg2 = "newmsg";
    size_t msglen2 = strlen(msg2);
    
    // Test: write partial trailer followed by partial header write
    
    st->max_write = 3 + msglen + 1;
    
    ssize_t w = net_write(http, msg, msglen);
    
    UCX_TEST_ASSERT(w == msglen, "write 1: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 3 + msglen + 1, "write 1: wrong buf size");
    UCX_TEST_ASSERT(!memcmp(st->buf->space, "c\r\nhello world!\r\0", st->buf->size + 1), "write 1: wrong buf content");
    
    st->max_write = 2; // write 1 trailer byte and 1 header byte
    
    w = net_write(http, msg2, msglen2);
    
    UCX_TEST_ASSERT(w == 0, "write 2: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 3 + msglen + 2 + 1, "write 2: wrong buf size");
    UCX_TEST_ASSERT(!memcmp(st->buf->space, "c\r\nhello world!\r\n6\0", st->buf->size + 1), "write 2: wrong buf content");
    
    // force partial header write again
    st->max_write = 1;
    
    w = net_write(http, msg2, msglen2);
    
    UCX_TEST_ASSERT(w == 0, "write 3: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 3 + msglen + 2 + 2, "write 3: wrong buf size");
    UCX_TEST_ASSERT(!memcmp(st->buf->space, "c\r\nhello world!\r\n6\r\0", st->buf->size + 1), "write 3: wrong buf content");
    
    st->max_write = 1024;
    
    w = net_write(http, msg2, msglen2);
    
    UCX_TEST_ASSERT(w ==msglen2, "write 4: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 3 + msglen + 2 + 3 + msglen2 + 2, "write 4: wrong buf size");
    UCX_TEST_ASSERT(!memcmp(st->buf->space, "c\r\nhello world!\r\n6\r\nnewmsg\r\n", st->buf->size + 1), "write 4: wrong buf content");
    
    
    testutil_destroy_session(sn);
    testutil_iostream_destroy(st);
    
    UCX_TEST_END;
}

UCX_TEST(test_io_httpstream_chunked_write_data_2x) {
    Session *sn = testutil_session();
    
    TestIOStream *st = testutil_iostream(2048, TRUE);
    IOStream *http = httpstream_new(sn->pool, (IOStream*)st);
    httpstream_enable_chunked_write(http);
    io_set_max_writes(1);
    
    UCX_TEST_BEGIN;
    
    memset(st->buf->space, 0, st->buf->capacity);
    
    // Test: First write a partial header, which forces a chunk with a specific
    //       size. After that, write a message, that is bigger than the first
    //       chunk, forcing a start of a second chunk, in one big writev op.
    
    char *msg = "hello world!";
    size_t msglen = strlen(msg);
    
    char *msg2 = "newmsg";
    size_t msglen2 = strlen(msg2);
    
    char *msg_big = "hello world!newmsg";
    size_t msglen_big = strlen(msg_big);
    
    st->max_write = 1;
    
    ssize_t w = net_write(http, msg, msglen); // first chunk: msg
    
    UCX_TEST_ASSERT(w == 0, "write 1: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 1, "write 1: wrong buf size");
    
    st->max_write = 1024;
    
    w = net_write(http, msg_big, msglen_big); // first chunk + new chunk
    
    UCX_TEST_ASSERT(w == msglen_big, "write 2: wrong return value");
    UCX_TEST_ASSERT(st->buf->size == 3 + msglen + 2 + 3 + msglen2 + 2, "write 2: wrong buf size");
    UCX_TEST_ASSERT(!memcmp(st->buf->space, "c\r\nhello world!\r\n6\r\nnewmsg\r\n", st->buf->size + 1), "write 2: wrong buf content");
    
    
    testutil_destroy_session(sn);
    testutil_iostream_destroy(st);
    
    UCX_TEST_END;
}

UCX_TEST(test_io_httpstream_chunked_write_xx_limit) {
    Session *sn = testutil_session();
    
    TestIOStream *st = testutil_iostream(2048, TRUE);
    IOStream *http = httpstream_new(sn->pool, (IOStream*)st);
    httpstream_enable_chunked_write(http);
    io_set_max_writes(1);
    
    UCX_TEST_BEGIN;
    
    // Test: create testdata and write it in varying chunk sizes, but
    //       limit TestIOStream to 1 to 3 byte writes
    
    // create test data
    CxBuffer *testdata = cxBufferCreate(NULL, 1024*16, cxDefaultAllocator, 0);
    for(size_t i=0;i<testdata->capacity;i++) {
        cxBufferPut(testdata, 35+(i%91));
    }
    
    st->max_write = 1;
    
    size_t pos = 0;
    int chunksize = 1;
    while(pos < testdata->size) {
        size_t available = testdata->size - pos;
        
        char *chunk = testdata->space + pos;
        size_t chunklen = chunksize > available ? available : chunksize;
        
        // write chunk
        size_t chunkpos = 0;
        int max_writes = chunklen + 24; // max number of write attempts
        int writes = 0;
        while(chunkpos < chunklen) {
            ssize_t w = net_write(http, chunk+chunkpos, chunklen-chunkpos);
            UCX_TEST_ASSERT(w >= 0, "net_write failed");
            chunkpos += w;
            
            writes++;
            UCX_TEST_ASSERT(writes < max_writes, "max writes attempts reached");
        }
        
        pos += chunklen;
        chunksize += 5;
        
        // increase max write size at some point
        if(pos + chunksize >= testdata->size) {
            st->max_write = INT_MAX;
        } else if(pos > 1024*2) {
            if(pos < 1024*8) {
                st->max_write = 2;
            } else {
                st->max_write = 3;
            }
        }
    }
    
    // terminate chunk
    net_finish(http);
    
    
    // same code as test_io_httpstream_chunked_write_xx
    
    // make sure the output is correctly encoded
    // extract chunks from st->buf by using http_stream_parse_chunk_header
    // (which should be well-tested)
    
    WSBool first_chunk = TRUE;
    int64_t chunklen = 0;
    
    char *buf = st->buf->space;
    size_t bufsize = st->buf->size;
    
    pos = 0; // st->buf position
    size_t srcpos = 0; // testdata position
    int debug_counter = 0;
    while(pos < bufsize) {
        ssize_t remaining =  bufsize - pos;
        ssize_t src_remaining = testdata->size - srcpos;
        
        int ret = http_stream_parse_chunk_header(buf+pos, remaining, first_chunk, &chunklen);
        first_chunk = FALSE;
        
        // ret must always be > 0 (0: incomplete chunk header, -1: invalid syntax)
        UCX_TEST_ASSERT(ret > 0, "http_stream_parse_chunk_header ret <= 0");
        if(chunklen == 0) {
            UCX_TEST_ASSERT(src_remaining == 0, "stream end reached but src_remaining > 0");
            break;
        }
        
        UCX_TEST_ASSERT(chunklen <= src_remaining, "chunklen > src_remaining");
        
        char *src_chunk = testdata->space+srcpos;
        char *buf_chunk = buf+pos+ret;
        
        UCX_TEST_ASSERT(!memcmp(buf_chunk, src_chunk, chunklen), "memcmp failed");
        
        pos += ret + chunklen;
        srcpos += chunklen;
        
        debug_counter++;
    }
    
    
    testutil_destroy_session(sn);
    testutil_iostream_destroy(st);
    cxBufferFree(testdata);
    
    UCX_TEST_END;
}

mercurial