← Commit list

23952f7f80

Max Dymond
Max Dymond
2026-09-21
SUCCESS Duration: 416s

Subject

mqtt: drain queued output before advancing the state machine

Binary

No binary (build failed)

Architecture & Compiler Flags

Architecture: x86_64-linux-gnu Compiler: gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0

—

Build Log

(log yok)

Commit Diff GitHub →

lib/mqtt.c+61 -28
@@ -67,8 +67,10 @@ enum mqttstate {
MQTT_SUBACK_COMING, /* 4 - the SUBACK remainder */
MQTT_PUBWAIT, /* 5 - wait for publish */
MQTT_PUB_REMAIN, /* 6 - wait for the remainder of the publish */
+ MQTT_POST_DRAIN, /* 7 - finish sending PUBLISH */
+ MQTT_DISCONNECT_DRAIN, /* 8 - finish sending DISCONNECT */
- MQTT_NOSTATE /* 7 - never used an actual state */
+ MQTT_NOSTATE /* 9 - never used an actual state */
};
struct mqtt_conn {
@@ -140,32 +142,48 @@ static CURLcode mqtt_send(struct Curl_easy *data,
if(!mq)
return CURLE_FAILED_INIT;
+ /* A new packet must not replace a previously queued packet tail. */
+ DEBUGASSERT(!curlx_dyn_len(&mq->sendbuf));
result = Curl_xfer_send(data, buf, len, FALSE, &n);
if(result)
return result;
mq->lastTime = *Curl_pgrs_now(data);
Curl_debug(data, CURLINFO_HEADER_OUT, buf, n);
- if(len != n) {
- size_t nsend = len - n;
- if(curlx_dyn_len(&mq->sendbuf)) {
- DEBUGASSERT(curlx_dyn_len(&mq->sendbuf) >= nsend);
- result = curlx_dyn_tail(&mq->sendbuf, nsend); /* keep this much */
- }
- else {
- result = curlx_dyn_addn(&mq->sendbuf, &buf[n], nsend);
- }
- }
- else
- curlx_dyn_reset(&mq->sendbuf);
+ if(len != n)
+ result = curlx_dyn_addn(&mq->sendbuf, &buf[n], len - n);
return result;
}
+/* Send the queued tail, preserving any bytes that still cannot be sent. */
+static CURLcode mqtt_flush(struct Curl_easy *data)
+{
+ struct MQTT *mq = Curl_meta_get(data, CURL_META_MQTT_EASY);
+ size_t len, n;
+ CURLcode result;
+
+ if(!mq)
+ return CURLE_FAILED_INIT;
+ len = curlx_dyn_len(&mq->sendbuf);
+ if(!len)
+ return CURLE_OK;
+
+ result = Curl_xfer_send(data, curlx_dyn_ptr(&mq->sendbuf), len, FALSE, &n);
+ if(result)
+ return result;
+ mq->lastTime = *Curl_pgrs_now(data);
+ Curl_debug(data, CURLINFO_HEADER_OUT, curlx_dyn_ptr(&mq->sendbuf), n);
+ return curlx_dyn_tail(&mq->sendbuf, len - n);
+}
+
/* Generic function called by the multi interface to figure out what socket(s)
to wait for and for what actions during the DOING and PROTOCONNECT
states */
static CURLcode mqtt_pollset(struct Curl_easy *data,
struct easy_pollset *ps)
{
+ struct MQTT *mq = Curl_meta_get(data, CURL_META_MQTT_EASY);
+ if(mq && curlx_dyn_len(&mq->sendbuf))
+ return Curl_pollset_add_out(data, ps, data->conn->sock[FIRSTSOCKET]);
return Curl_pollset_add_in(data, ps, data->conn->sock[FIRSTSOCKET]);
}
@@ -637,6 +655,8 @@ static const char * const statenames[] = {
"MQTT_SUBACK_COMING",
"MQTT_PUBWAIT",
"MQTT_PUB_REMAIN",
+ "MQTT_POST_DRAIN",
+ "MQTT_DISCONNECT_DRAIN",
"NOT A STATE"
};
@@ -843,14 +863,16 @@ static CURLcode mqtt_doing(struct Curl_easy *data, bool *done)
if(curlx_dyn_len(&mq->sendbuf)) {
/* send the remainder of an outgoing packet */
- result = mqtt_send(data, curlx_dyn_ptr(&mq->sendbuf),
- curlx_dyn_len(&mq->sendbuf));
- if(result)
+ result = mqtt_flush(data);
+ /* CURLE_OK can still mean a short write. Wait for writable progress
+ before sending another packet or processing a reply. */
+ if(result || curlx_dyn_len(&mq->sendbuf))
return result;
}
result = mqtt_ping(data);
- if(result)
+ /* Packet processing may send more output, so finish PINGREQ first. */
+ if(result || curlx_dyn_len(&mq->sendbuf))
return result;
infof(data, "mqtt_doing: state [%d]", (int)mqtt->state);
@@ -935,20 +957,31 @@ static CURLcode mqtt_doing(struct Curl_easy *data, bool *done)
if(result)
break;
- if(data->state.httpreq == HTTPREQ_POST) {
- result = mqtt_publish(data);
- if(!result) {
- result = mqtt_disconnect(data);
- *done = TRUE;
- }
- mqtt->nextstate = MQTT_FIRST;
- }
- else {
+ if(data->state.httpreq != HTTPREQ_POST) {
result = mqtt_subscribe(data);
- if(!result) {
+ if(!result)
mqstate(data, MQTT_FIRST, MQTT_SUBACK);
- }
+ break;
}
+
+ result = mqtt_publish(data);
+ if(result)
+ break;
+ mqstate(data, MQTT_POST_DRAIN, MQTT_NOSTATE);
+ FALLTHROUGH();
+ case MQTT_POST_DRAIN:
+ /* PUBLISH must be complete before DISCONNECT can use the send buffer. */
+ if(curlx_dyn_len(&mq->sendbuf))
+ break;
+ result = mqtt_disconnect(data);
+ if(result)
+ break;
+ mqstate(data, MQTT_DISCONNECT_DRAIN, MQTT_NOSTATE);
+ FALLTHROUGH();
+ case MQTT_DISCONNECT_DRAIN:
+ /* Completing now would discard any DISCONNECT bytes still queued. */
+ if(!curlx_dyn_len(&mq->sendbuf))
+ *done = TRUE;
break;
case MQTT_SUBACK:
tests/data/Makefile.am+5 -0
@@ -1711,6 +1711,7 @@ TESTCASES = \
test1690 \
test1691 \
test1692 \
+ test1693 \
\
test1700 \
test1702 \
@@ -1937,6 +1938,10 @@ TESTCASES = \
test2206 \
test2207 \
test2208 \
+ test2209 \
+ test2210 \
+ test2211 \
+ test2212 \
\
test2270 \
test2271 \
tests/data/test1693+19 -0
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="US-ASCII"?>
+<testcase>
+<info>
+<keywords>
+unittest
+MQTT
+</keywords>
+</info>
+
+<client>
+<features>
+unittest
+mqtt
+</features>
+<name>
+MQTT partial sends, would-block, polling and packet ordering
+</name>
+</client>
+</testcase>
tests/data/test2209+46 -0
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="US-ASCII"?>
+<testcase>
+<info>
+<keywords>
+MQTT
+MQTT PUBLISH
+</keywords>
+</info>
+
+<reply>
+<data>
+</data>
+</reply>
+
+<client>
+<features>
+mqtt
+Debug
+</features>
+<server>
+mqtt
+</server>
+<name>
+MQTT PUBLISH with partial writes
+</name>
+<setenv>
+CURL_DBG_SOCK_WPARTIAL=50
+</setenv>
+<command option="binary-trace">
+mqtt://%HOSTIP:%MQTTPORT/%TESTNUMBER -d something --max-time 10
+</command>
+</client>
+
+<verify>
+# Strip the random part of the client ID.
+<strippart>
+s/^(.* 00044d5154540402003c000c6375726c).*/$1/
+</strippart>
+<protocol>
+client CONNECT 18 00044d5154540402003c000c6375726c
+server CONNACK 2 20020000
+client PUBLISH f 000432323039736f6d657468696e67
+client DISCONNECT 0 e000
+</protocol>
+</verify>
+</testcase>
tests/data/test2210+49 -0
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="US-ASCII"?>
+<testcase>
+<info>
+<keywords>
+MQTT
+MQTT SUBSCRIBE
+</keywords>
+</info>
+
+<reply>
+<data nocheck="yes">
+hello
+</data>
+</reply>
+
+<client>
+<features>
+mqtt
+Debug
+</features>
+<server>
+mqtt
+</server>
+<name>
+MQTT SUBSCRIBE with partial writes
+</name>
+<setenv>
+CURL_DBG_SOCK_WPARTIAL=50
+</setenv>
+<command option="binary-trace">
+mqtt://%HOSTIP:%MQTTPORT/%TESTNUMBER --max-time 10
+</command>
+</client>
+
+<verify>
+# Strip the random part of the client ID.
+<strippart>
+s/^(.* 00044d5154540402003c000c6375726c).*/$1/
+</strippart>
+<protocol>
+client CONNECT 18 00044d5154540402003c000c6375726c
+server CONNACK 2 20020000
+client SUBSCRIBE 9 000100043232313000
+server SUBACK 3 9003000100
+server PUBLISH c 300c00043232313068656c6c6f0a
+server DISCONNECT 0 e000
+</protocol>
+</verify>
+</testcase>
tests/data/test2211+48 -0
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="US-ASCII"?>
+<testcase>
+<info>
+<keywords>
+MQTT
+MQTT PUBLISH
+MQTTS
+</keywords>
+</info>
+
+<reply>
+<data>
+</data>
+</reply>
+
+<client>
+<features>
+mqtts
+Debug
+</features>
+<server>
+mqtts
+</server>
+<name>
+MQTTS PUBLISH with partial writes
+</name>
+<setenv>
+# Split MQTT packets, including DISCONNECT, without fragmenting TLS handshakes.
+CURL_SMALLSENDS=1
+</setenv>
+<command option="binary-trace">
+mqtts://%HOSTIP:%MQTTSPORT/%TESTNUMBER -d something --insecure --max-time 10
+</command>
+</client>
+
+<verify>
+# Strip the random part of the client ID.
+<strippart>
+s/^(.* 00044d5154540402003c000c6375726c).*/$1/
+</strippart>
+<protocol>
+client CONNECT 18 00044d5154540402003c000c6375726c
+server CONNACK 2 20020000
+client PUBLISH f 000432323131736f6d657468696e67
+client DISCONNECT 0 e000
+</protocol>
+</verify>
+</testcase>
tests/data/test2212+51 -0
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="US-ASCII"?>
+<testcase>
+<info>
+<keywords>
+MQTT
+MQTT SUBSCRIBE
+MQTTS
+</keywords>
+</info>
+
+<reply>
+<data nocheck="yes">
+hello
+</data>
+</reply>
+
+<client>
+<features>
+mqtts
+Debug
+</features>
+<server>
+mqtts
+</server>
+<name>
+MQTTS SUBSCRIBE with partial writes
+</name>
+<setenv>
+# Split MQTT packets without fragmenting TLS handshakes.
+CURL_SMALLSENDS=1
+</setenv>
+<command option="binary-trace">
+mqtts://%HOSTIP:%MQTTSPORT/%TESTNUMBER --insecure --max-time 10
+</command>
+</client>
+
+<verify>
+# Strip the random part of the client ID.
+<strippart>
+s/^(.* 00044d5154540402003c000c6375726c).*/$1/
+</strippart>
+<protocol>
+client CONNECT 18 00044d5154540402003c000c6375726c
+server CONNACK 2 20020000
+client SUBSCRIBE 9 000100043232313200
+server SUBACK 3 9003000100
+server PUBLISH c 300c00043232313268656c6c6f0a
+server DISCONNECT 0 e000
+</protocol>
+</verify>
+</testcase>
tests/server/mqttd.c+30 -22
@@ -413,6 +413,25 @@ static int publish(FILE *dump,
static char topic[MAX_TOPIC_LENGTH + 1];
+static bool readall(curl_socket_t fd, unsigned char *buffer, size_t len)
+{
+ size_t nread = 0;
+
+ while(nread < len) {
+ ssize_t rc = sread(fd, buffer + nread, len - nread);
+ if(rc <= 0) {
+ if(rc < 0 && SOCKERRNO == SOCKEINTR && !got_exit_signal)
+ continue;
+ logmsg("READ %zd bytes [SHORT!]", rc);
+ return FALSE;
+ }
+ logmsg("READ %zd bytes", rc);
+ loghex(buffer + nread, rc);
+ nread += (size_t)rc;
+ }
+ return TRUE;
+}
+
static bool fixedheader(curl_socket_t fd,
unsigned char *bytep,
size_t *remaining_lengthp,
@@ -421,23 +440,18 @@ static bool fixedheader(curl_socket_t fd,
/* get the fixed header */
unsigned char buffer[10];
- /* get the first two bytes */
- ssize_t rc = sread(fd, buffer, 2);
size_t i;
- if(rc < 2) {
- logmsg("READ %zd bytes [SHORT!]", rc);
+
+ /* get the first two bytes */
+ if(!readall(fd, buffer, 2))
return FALSE; /* fail */
- }
- logmsg("READ %zd bytes", rc);
- loghex(buffer, rc);
*bytep = buffer[0];
/* if the length byte has the top bit set, get the next one too */
i = 1;
while(buffer[i] & 0x80) {
i++;
- rc = sread(fd, &buffer[i], 1);
- if(rc != 1) {
+ if(!readall(fd, &buffer[i], 1)) {
logmsg("Remaining Length broken");
return FALSE;
}
@@ -513,14 +527,10 @@ static curl_socket_t mqttit(curl_socket_t fd)
buffer = newbuffer;
}
- if(remaining_length) {
- /* reading variable header and payload into buffer */
- rc = sread(fd, buffer, remaining_length);
- if(rc > 0) {
- logmsg("READ %zd bytes", rc);
- loghex(buffer, rc);
- }
- }
+ /* Read the complete variable header and payload. */
+ if(!readall(fd, buffer, remaining_length))
+ goto end;
+ rc = (ssize_t)remaining_length;
if(byte == MQTT_MSG_CONNECT) {
logprotocol(FROM_CLIENT, "CONNECT", remaining_length, dump, buffer, rc);
@@ -658,11 +668,9 @@ static curl_socket_t mqttit(curl_socket_t fd)
#endif
/* expect a disconnect here */
/* get the request */
- rc = sread(fd, &buffer[0], 2);
-
- logmsg("READ %zd bytes [DISCONNECT]", rc);
- loghex(buffer, rc);
- logprotocol(FROM_CLIENT, "DISCONNECT", 0, dump, buffer, rc);
+ if(!readall(fd, buffer, 2))
+ goto end;
+ logprotocol(FROM_CLIENT, "DISCONNECT", 0, dump, buffer, 2);
goto end;
}
else {
tests/unit/Makefile.inc+1 -0
@@ -80,6 +80,7 @@ TESTS_C = \
unit1674.c \
unit1675.c \
unit1676.c \
+ unit1693.c \
unit1961.c \
unit1979.c \
unit1980.c \
tests/unit/unit1693.c+236 -0
@@ -0,0 +1,236 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <[email protected]>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "unitcheck.h"
+#include "urldata.h"
+#include "mqtt.h"
+#include "select.h"
+#include "url.h"
+
+#ifndef CURL_DISABLE_MQTT
+
+enum {
+ T1693_CONNECT_LEN = 26,
+ T1693_PUBLISH_LEN = 15,
+ T1693_DISCONNECT_LEN = 2,
+ T1693_PUBLISH_END = T1693_CONNECT_LEN + T1693_PUBLISH_LEN,
+ T1693_WIRE_LEN = T1693_PUBLISH_END + T1693_DISCONNECT_LEN
+};
+
+struct t1693_send {
+ size_t n;
+ CURLcode result;
+};
+
+struct t1693_ctx {
+ const struct t1693_send *sends;
+ size_t nsends;
+ size_t send_index;
+ size_t recv_index;
+ size_t written;
+ size_t pending;
+ unsigned char expected[T1693_WIRE_LEN];
+ unsigned char output[T1693_WIRE_LEN];
+};
+
+static CURLcode t1693_send(struct Curl_easy *data, int8_t sockindex,
+ const uint8_t *buf, size_t len, bool eos,
+ size_t *pnwritten)
+{
+ struct t1693_ctx *ctx = data->set.private_data;
+ const struct t1693_send *send;
+ size_t remaining;
+ (void)sockindex;
+ (void)eos;
+ *pnwritten = 0;
+
+ abort_unless(ctx->send_index < ctx->nsends, "unexpected send");
+ send = &ctx->sends[ctx->send_index++];
+ if(ctx->send_index == 1) {
+ /* The CONNECT client ID is random. Save the packet for retry checks. */
+ abort_unless(len == T1693_CONNECT_LEN, "unexpected CONNECT size");
+ memcpy(ctx->expected, buf, len);
+ fail_unless(buf[0] == 0x10 && buf[1] == 0x18,
+ "unexpected CONNECT header");
+ }
+
+ if(ctx->written < T1693_CONNECT_LEN)
+ remaining = T1693_CONNECT_LEN - ctx->written;
+ else if(ctx->written < T1693_PUBLISH_END)
+ remaining = T1693_PUBLISH_END - ctx->written;
+ else
+ remaining = sizeof(ctx->expected) - ctx->written;
+ abort_unless(len == remaining, "packet sent before previous packet drained");
+ fail_unless(!memcmp(buf, ctx->expected + ctx->written, len),
+ "queued packet bytes changed");
+ ctx->pending = len;
+ if(send->result)
+ return send->result;
+
+ abort_unless(send->n <= len, "send script exceeds packet size");
+ memcpy(ctx->output + ctx->written, buf, send->n);
+ ctx->written += send->n;
+ ctx->pending -= send->n;
+ *pnwritten = send->n;
+ return CURLE_OK;
+
+unit_test_abort:
+ return CURLE_SEND_ERROR;
+}
+
+static CURLcode t1693_recv(struct Curl_easy *data, int8_t sockindex,
+ char *buf, size_t len, size_t *pnread)
+{
+ static const unsigned char connack[] = { 0x20, 0x02, 0x00, 0x00 };
+ struct t1693_ctx *ctx = data->set.private_data;
+ size_t n = sizeof(connack) - ctx->recv_index;
+ (void)sockindex;
+ *pnread = 0;
+ abort_unless(ctx->written == T1693_CONNECT_LEN,
+ "read while an outgoing packet is queued");
+ abort_unless(n, "unexpected read after CONNACK");
+ if(n > len)
+ n = len;
+ memcpy(buf, connack + ctx->recv_index, n);
+ ctx->recv_index += n;
+ *pnread = n;
+ return CURLE_OK;
+
+unit_test_abort:
+ return CURLE_RECV_ERROR;
+}
+
+static void t1693_meta_dtor(void *p)
+{
+ /* MQTT metadata supplies its own destructor. */
+ (void)p;
+}
+
+static void t1693_run(const struct t1693_send *sends, size_t nsends,
+ CURLcode expected_result)
+{
+ static const unsigned char post[] = {
+ 0x30, 0x0d, 0x00, 0x04, 't', 'e', 's', 't',
+ 'p', 'a', 'y', 'l', 'o', 'a', 'd', 0xe0, 0x00
+ };
+ struct t1693_ctx ctx;
+ struct Curl_easy *data = NULL;
+ struct connectdata *conn = NULL;
+ struct easy_pollset ps;
+ CURLcode result;
+ bool done = FALSE;
+ size_t i;
+
+ Curl_pollset_init(&ps);
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.sends = sends;
+ ctx.nsends = nsends;
+ memcpy(ctx.expected + T1693_CONNECT_LEN, post, sizeof(post));
+
+ data = curl_easy_init();
+ abort_unless(data, "easy handle allocation failed");
+ conn = curlx_calloc(1, sizeof(*conn));
+ abort_unless(conn, "connection allocation failed");
+ Curl_hash_init(&conn->meta_hash, 23, CURL_HASH_TYPE_BYTES, t1693_meta_dtor);
+ data->conn = conn;
+ conn->sock[FIRSTSOCKET] = 42;
+ conn->sock[SECONDARYSOCKET] = CURL_SOCKET_BAD;
+ conn->send[FIRSTSOCKET] = t1693_send;
+ conn->recv[FIRSTSOCKET] = t1693_recv;
+ data->set.private_data = &ctx;
+ data->set.postfields = CURL_UNCONST("payload");
+ data->state.httpreq = HTTPREQ_POST;
+ data->state.up.path = curlx_strdup("/test");
+ abort_unless(data->state.up.path, "path allocation failed");
+ result = Curl_protocol_mqtt.setup_connection(data, conn);
+ abort_unless(!result, "MQTT setup failed");
+
+ result = Curl_protocol_mqtt.do_it(data, &done);
+ for(i = 0; !result && !done && i < nsends + 4; ++i) {
+ Curl_pollset_reset(&ps);
+ result = Curl_protocol_mqtt.doing_pollset(data, &ps);
+ abort_unless(!result, "MQTT pollset failed");
+ abort_unless(ps.n == 1 && ps.sockets[0] == 42,
+ "unexpected pollset socket");
+ fail_unless(ps.actions[0] == (ctx.pending ? CURL_POLL_OUT : CURL_POLL_IN),
+ "polling the wrong direction for queued output");
+ result = Curl_protocol_mqtt.doing(data, &done);
+ fail_unless(!done || ctx.written == sizeof(ctx.output),
+ "transfer completed with unsent packet bytes");
+ }
+ fail_unless(result == expected_result, "unexpected transfer result");
+ fail_unless(ctx.send_index == nsends, "not all scripted sends were used");
+ fail_unless(!memcmp(ctx.output, ctx.expected, ctx.written),
+ "packets were not sent in order");
+ if(!expected_result) {
+ fail_unless(done, "transfer did not complete");
+ fail_unless(ctx.written == sizeof(ctx.output), "missing packet bytes");
+ }
+ else
+ fail_unless(!done, "send failure marked the transfer complete");
+
+unit_test_abort:
+ Curl_pollset_cleanup(&ps);
+ if(conn)
+ Curl_conn_free(data, conn);
+ if(data) {
+ data->conn = NULL;
+ curl_easy_cleanup(data);
+ }
+}
+
+#endif /* CURL_DISABLE_MQTT */
+
+static CURLcode test_unit1693(const char *arg)
+{
+ UNITTEST_BEGIN_SIMPLE
+
+#ifndef CURL_DISABLE_MQTT
+ static const struct t1693_send full[] = {
+ { T1693_CONNECT_LEN, CURLE_OK }, { T1693_PUBLISH_LEN, CURLE_OK },
+ { T1693_DISCONNECT_LEN, CURLE_OK }
+ };
+ static const struct t1693_send partial[] = {
+ { 3, CURLE_OK }, { 0, CURLE_AGAIN }, { 4, CURLE_OK }, { 19, CURLE_OK },
+ { 3, CURLE_OK }, { 4, CURLE_OK }, { 0, CURLE_AGAIN }, { 8, CURLE_OK },
+ { 0, CURLE_AGAIN }, { 1, CURLE_OK }, { 0, CURLE_AGAIN }, { 1, CURLE_OK }
+ };
+ static const struct t1693_send zero[] = {
+ { 0, CURLE_OK }, { T1693_CONNECT_LEN, CURLE_OK },
+ { 0, CURLE_OK }, { T1693_PUBLISH_LEN, CURLE_OK },
+ { 0, CURLE_OK }, { T1693_DISCONNECT_LEN, CURLE_OK }
+ };
+ static const struct t1693_send send_error[] = {
+ { T1693_CONNECT_LEN, CURLE_OK }, { 4, CURLE_OK }, { 0, CURLE_SEND_ERROR }
+ };
+
+ abort_unless(!curl_global_init(CURL_GLOBAL_ALL), "global init failed");
+ t1693_run(full, CURL_ARRAYSIZE(full), CURLE_OK);
+ t1693_run(partial, CURL_ARRAYSIZE(partial), CURLE_OK);
+ t1693_run(zero, CURL_ARRAYSIZE(zero), CURLE_OK);
+ t1693_run(send_error, CURL_ARRAYSIZE(send_error), CURLE_SEND_ERROR);
+ curl_global_cleanup();
+#endif
+
+ UNITTEST_END_SIMPLE
+}