Release 2.19.9

- [FEATURE] Add lsquic_stream_pwritev().  This function allows one to
  reduce the number of system calls required to read a file from disk
  by using lsquic_stream_pwritev() together with preadv(2).
- [BUGFIX] When stream is reset, it is writeable -- let user collect
  the error.
- [BUGFIX] Calculate correct conn flow control if reading ends early.
- [BUGFIX] Remove stream from read and write queues on internal
  shutdown.  This is a regression introduced in 2.19.7.
- [BUGFIX] Swapped arguments in IETF RESET_FRAME generation.
- Turn off mini conn history when compiling with Visual Studio; this
  allows the project to compile on Windows again.
- http_client: Add -3 flag to stop reading from streams early; code
  cleanup.
- Don't use -Werror.
This commit is contained in:
Dmitri Tikhonov 2020-09-08 11:43:03 -04:00
parent 49f1f4f620
commit 2f2f436324
25 changed files with 1545 additions and 85 deletions

View file

@ -59,6 +59,9 @@
#include "lsquic_hq.h"
#include "lsquic_data_in_if.h"
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
static const struct parse_funcs *g_pf = select_pf_by_ver(LSQVER_ID27);
struct test_ctl_settings
@ -270,11 +273,24 @@ struct test_objs {
struct lsquic_hset_if hsi_if;
};
static int s_ack_written;
static void
write_ack (struct lsquic_conn *conn, struct lsquic_packet_out *packet_out)
{
/* We don't need to generate full-blown ACK, as logic in
* lsquic_send_ctl_rollback() only looks at po_frame_types.
*/
packet_out->po_frame_types |= QUIC_FRAME_ACK;
s_ack_written = 1;
}
static int s_can_write_ack;
static int
unit_test_doesnt_write_ack (struct lsquic_conn *lconn)
can_write_ack (struct lsquic_conn *lconn)
{
return 0;
return s_can_write_ack;
}
@ -286,11 +302,31 @@ get_network_path (struct lsquic_conn *lconn, const struct sockaddr *sa)
return &network_path;
}
static enum {
SNAPSHOT_STATE_NONE = 0,
SNAPSHOT_STATE_TAKEN = 1 << 0,
SNAPSHOT_STATE_ROLLED_BACK = 1 << 1,
} s_snapshot_state;
static void
ack_snapshot (struct lsquic_conn *lconn, struct ack_state *ack_state)
{
s_snapshot_state |= SNAPSHOT_STATE_TAKEN;
}
static void
ack_rollback (struct lsquic_conn *lconn, struct ack_state *ack_state)
{
s_snapshot_state |= SNAPSHOT_STATE_ROLLED_BACK;
}
static const struct conn_iface our_conn_if =
{
.ci_can_write_ack = unit_test_doesnt_write_ack,
.ci_can_write_ack = can_write_ack,
.ci_write_ack = write_ack,
.ci_get_path = get_network_path,
.ci_ack_snapshot = ack_snapshot,
.ci_ack_rollback = ack_rollback,
};
@ -662,7 +698,7 @@ main_test_hq_framing (void)
* file. This allows afl-fuzz explore the code paths.
*/
void
fuzz_guided_testing (const char *input)
fuzz_guided_hq_framing_testing (const char *input)
{
/* Range */ /* Bytes from file */
unsigned short packet_sz; /* [200, 0x3FFF] */ /* 2 */
@ -728,6 +764,391 @@ fuzz_guided_testing (const char *input)
}
struct pwritev_stream_ctx
{
int limit; /* Test limit */
size_t avail;
const unsigned char *input;
size_t input_sz;
ssize_t nw; /* Number of bytes written */
};
static ssize_t
my_preadv (void *user_data, const struct iovec *iov, int iovcnt)
{
struct pwritev_stream_ctx *const pw_ctx = user_data;
const unsigned char *p;
size_t ntoread, tocopy;
int i;
ntoread = 0;
for (i = 0; i < iovcnt; ++i)
ntoread += iov[i].iov_len;
if (pw_ctx->limit < 0)
{
if ((size_t) -pw_ctx->limit < ntoread)
ntoread -= (size_t) -pw_ctx->limit;
}
else if ((size_t) pw_ctx->limit < ntoread)
ntoread = (size_t) pw_ctx->limit;
assert(ntoread <= pw_ctx->input_sz); /* Self-check */
p = pw_ctx->input;
for (i = 0; i < iovcnt; ++i)
{
tocopy = MIN(iov[i].iov_len, ntoread - (p - pw_ctx->input));
memcpy(iov[i].iov_base, p, tocopy);
p += tocopy;
if (ntoread == (size_t) (p - pw_ctx->input))
break;
}
assert(ntoread == (size_t) (p - pw_ctx->input));
return (ssize_t) (p - pw_ctx->input);
}
static void
pwritev_on_write (lsquic_stream_t *stream, lsquic_stream_ctx_t *ctx)
{
struct pwritev_stream_ctx *const pw_ctx = (void *) ctx;
ssize_t nw;
nw = lsquic_stream_pwritev(stream, my_preadv, pw_ctx, pw_ctx->input_sz);
pw_ctx->nw = nw;
lsquic_stream_wantwrite(stream, 0);
}
static const struct lsquic_stream_if pwritev_stream_if = {
.on_new_stream = packetization_on_new_stream,
.on_close = packetization_on_close,
.on_write = pwritev_on_write,
};
static void
test_pwritev (enum lsquic_version version, int http, int sched_immed,
int limit, unsigned short packet_sz, size_t prologue_sz,
unsigned n_packets)
{
struct test_objs tobjs;
struct lsquic_stream *stream;
size_t nw;
int fin, s;
unsigned char *buf_in, *buf_out;
/* Some values that are large enough: */
const size_t buf_in_sz = MAX(n_packets * packet_sz, 0x1000),
buf_out_sz = (float) buf_in_sz * 1.1;
const int ietf = (1 << version) & LSQUIC_IETF_VERSIONS;
const enum stream_ctor_flags ietf_flags = ietf ? SCF_IETF : 0;
s_snapshot_state = 0;
s_ack_written = 0;
/* We'll write headers first after which stream will switch to using
* data-framing writer. This is simply so that we don't have to
* expose more stream things only for testing.
*/
struct lsxpack_header header = { XHDR(":method", "GET") };
struct lsquic_http_headers headers = { 1, &header, };
buf_in = malloc(buf_in_sz);
buf_out = malloc(buf_out_sz);
assert(buf_in && buf_out);
struct pwritev_stream_ctx pwritev_stream_ctx =
{
.input = buf_in + prologue_sz,
.input_sz = buf_in_sz - prologue_sz,
.limit = limit,
};
init_buf(buf_in, buf_in_sz);
init_test_ctl_settings(&g_ctl_settings);
g_ctl_settings.tcs_schedule_stream_packets_immediately = sched_immed;
stream_ctor_flags |= ietf_flags;
init_test_objs(&tobjs, buf_out_sz, buf_out_sz, packet_sz);
tobjs.lconn.cn_version = version;
tobjs.lconn.cn_esf_c = select_esf_common_by_ver(version);
tobjs.stream_if_ctx = &pwritev_stream_ctx;
tobjs.ctor_flags |= (http ? SCF_HTTP : 0)|ietf_flags;
if (sched_immed)
{
g_ctl_settings.tcs_can_send = n_packets;
tobjs.stream_if = &pwritev_stream_if;
}
else
{
lsquic_send_ctl_set_max_bpq_count(n_packets);
g_ctl_settings.tcs_can_send = INT_MAX;
g_ctl_settings.tcs_bp_type = BPT_OTHER_PRIO;
/* Need this for on_new_stream() callback not to mess with
* the context, otherwise this is not used.
*/
tobjs.stream_if = &pwritev_stream_if;
}
stream = new_stream(&tobjs, 0, buf_out_sz);
if (http)
{
if (ietf)
{
s = lsquic_stream_send_headers(stream, &headers, 0);
assert(0 == s);
}
else
/* Here we fake it in order not to have to set up frame writer. */
stream->stream_flags |= STREAM_HEADERS_SENT;
}
if (prologue_sz)
{
ssize_t written = lsquic_stream_write(stream, buf_in, prologue_sz);
assert(written > 0 && (size_t) written == prologue_sz);
}
if (sched_immed)
{
lsquic_stream_dispatch_write_events(stream);
assert(!(s_snapshot_state & SNAPSHOT_STATE_TAKEN));
// lsquic_stream_flush(stream);
}
else
{
pwritev_on_write(stream, (void *) &pwritev_stream_ctx);
assert(s_snapshot_state & SNAPSHOT_STATE_TAKEN);
if (n_packets > 0
&& s_ack_written
&& tobjs.send_ctl.sc_buffered_packets[BPT_OTHER_PRIO].bpq_count == 0)
assert(s_snapshot_state & SNAPSHOT_STATE_ROLLED_BACK);
g_ctl_settings.tcs_schedule_stream_packets_immediately = 1;
lsquic_send_ctl_schedule_buffered(&tobjs.send_ctl, BPT_OTHER_PRIO);
g_ctl_settings.tcs_schedule_stream_packets_immediately = 0;
lsquic_send_ctl_set_max_bpq_count(10);
}
assert(pwritev_stream_ctx.nw >= 0);
/* Verify written data: */
nw = read_from_scheduled_packets(&tobjs.send_ctl, 0, buf_out, buf_out_sz,
0, &fin, 1);
assert(nw <= buf_in_sz);
if (ietf && http)
{ /* Remove framing and verify contents */
const unsigned char *src;
unsigned char *dst;
uint64_t sz;
unsigned frame_type;
int s;
src = buf_out;
dst = buf_out;
while (src < buf_out + nw)
{
frame_type = *src++;
s = vint_read(src, buf_out + buf_out_sz, &sz);
assert(s > 0);
/* In some rare circumstances it is possible to produce zero-length
* DATA frames:
*
* assert(sz > 0);
*/
assert(sz < (1 << 14));
src += s;
if (src == buf_out + s + 1)
{
/* Ignore headers */
assert(frame_type == HQFT_HEADERS);
src += sz;
}
else
{
assert(frame_type == HQFT_DATA);
if (src + sz > buf_out + nw) /* Chopped DATA frame (last) */
sz = buf_out + nw - src;
memmove(dst, src, sz);
dst += sz;
src += sz;
}
}
assert(nw <= buf_in_sz);
if (n_packets && pwritev_stream_ctx.nw)
{
assert((size_t) pwritev_stream_ctx.nw + prologue_sz == (uintptr_t) dst - (uintptr_t) buf_out);
assert(0 == memcmp(buf_in, buf_out, (uintptr_t) dst - (uintptr_t) buf_out));
}
else
assert((uintptr_t) dst - (uintptr_t) buf_out == 0
|| (uintptr_t) dst - (uintptr_t) buf_out == prologue_sz);
}
else
{
assert(nw <= buf_in_sz);
assert(nw <= buf_out_sz);
if (n_packets && pwritev_stream_ctx.nw)
{
assert((size_t) pwritev_stream_ctx.nw + prologue_sz == nw);
assert(0 == memcmp(buf_in, buf_out, (size_t) nw));
}
else
assert(nw == 0 || nw == prologue_sz);
}
lsquic_stream_destroy(stream);
deinit_test_objs(&tobjs);
free(buf_in);
free(buf_out);
stream_ctor_flags &= ~ietf_flags;
}
static void
main_test_pwritev (void)
{
const int limits[] = { INT_MAX, -1, -2, -3, -7, -10, -50, -100, -201, -211,
-1000, -2003, -3000, -4000, -17803, -20000, 16 * 1024, 16 * 1024 - 1,
16 * 1024 - 2, 8000, 273, 65, 63, 10, 5, 1, 0, };
unsigned n_packets;
const unsigned short packet_sz[] = { 1252, 1370, 0x1000, 0xFF00, };
const size_t prologues[] = { 0, 17, 238, };
unsigned i, j, k;
enum lsquic_version version;
int http, sched_immed;
const struct { unsigned iovecs, frames; } combos[] =
{
{ 32, 16, },
{ 16, 16, },
{ 16, 8, },
{ 3, 7, },
{ 7, 3, },
{ 100, 100, },
}, *combo = combos;
s_can_write_ack = 1;
run_test:
for (version = 0; version < N_LSQVER; ++version)
if ((1 << version) & LSQUIC_SUPPORTED_VERSIONS)
for (http = 0; http < 2; ++http)
for (sched_immed = 0; sched_immed <= 1; ++sched_immed)
for (i = 0; i < sizeof(limits) / sizeof(limits[i]); ++i)
for (j = 0; j < sizeof(packet_sz) / sizeof(packet_sz[0]);
++j)
for (k = 0; k < sizeof(prologues) / sizeof(prologues[0]); ++k)
for (n_packets = 1; n_packets < 21; ++n_packets)
test_pwritev(version, http, sched_immed,
limits[i], packet_sz[j], prologues[k], n_packets);
if (combo < combos + sizeof(combos) / sizeof(combos[0]))
{
lsquic_stream_set_pwritev_params(combo->iovecs, combo->frames);
++combo;
goto run_test;
}
s_can_write_ack = 0;
}
/* Instead of the not-very-random testing done in main_test_pwritev(),
* the fuzz-guided testing initializes parameters based on the fuzz input
* file. This allows afl-fuzz explore the code paths.
*/
void
fuzz_guided_pwritev_testing (const char *input)
{
/* Range */ /* Bytes from file */
unsigned short packet_sz; /* [1200, 0xFF00] */ /* 2 */
int limit; /* [INT_MIN, INT_MAX] */ /* 2 */
unsigned n_packets; /* [0, 255] */ /* 1 */
unsigned n_iovecs; /* [0, 255] */ /* 1 */
unsigned n_frames; /* [0, 255] */ /* 1 */
size_t prologue_sz; /* [0, 170] */ /* 1 */
enum lsquic_version version;/* [0,7] */ /* 1 */
int sched_immed; /* 0 or 1 */ /* 1 (same byte) */
int http; /* 0 or 1 */ /* 1 (same byte) */
/* TOTAL: 9 bytes */
FILE *f;
size_t nread;
union {
uint16_t tmp;
int16_t itmp;
} u;
unsigned char buf[10];
f = fopen(input, "rb");
if (!f)
{
assert(0);
return;
}
nread = fread(buf, 1, sizeof(buf), f);
if (nread != 9)
goto cleanup;
memcpy(&u.tmp, &buf[0], 2);
if (u.tmp < 1200)
u.tmp = 1200;
else if (u.tmp > 0xFF00)
u.tmp = 0xFF00;
packet_sz = u.tmp;
memcpy(&u.itmp, &buf[2], 2);
if (u.itmp < SHRT_MIN / 2)
limit = INT_MIN;
else if (u.itmp < SHRT_MIN / 4)
limit = 0;
else if (u.itmp > SHRT_MAX / 2)
limit = INT_MAX;
else if (u.itmp > SHRT_MAX / 2)
limit = 0;
else
limit = u.itmp;
n_packets = buf[4];
n_iovecs = buf[5];
n_frames = buf[6];
prologue_sz = buf[7];
if (prologue_sz > 170)
prologue_sz = 170;
switch (buf[8] & 7)
{
case 0: version = LSQVER_043; break;
case 1: version = LSQVER_046; break;
case 2: version = LSQVER_050; break;
case 3: version = LSQVER_ID27; break;
case 4: version = LSQVER_ID28; break;
default:
case 5: version = LSQVER_ID29; break;
}
sched_immed = !!(buf[8] & 0x08);
http = !!(buf[8] & 0x10);
lsquic_stream_set_pwritev_params(n_iovecs, n_frames);
test_pwritev(version, http, sched_immed, limit, packet_sz, prologue_sz,
n_packets);
cleanup:
(void) fclose(f);
}
static void
test_frame_header_split (unsigned n_packets, unsigned extra_sz,
int add_one_more)
@ -1170,18 +1591,22 @@ test_reading_zero_size_data_frame_scenario3 (void)
int
main (int argc, char **argv)
{
const char *fuzz_input = NULL;
const char *fuzz_hq_framing_input = NULL;
const char *fuzz_pwritev_input = NULL;
int opt, add_one_more;
unsigned n_packets, extra_sz;
lsquic_global_init(LSQUIC_GLOBAL_SERVER);
while (-1 != (opt = getopt(argc, argv, "f:l:")))
while (-1 != (opt = getopt(argc, argv, "f:p:l:")))
{
switch (opt)
{
case 'f':
fuzz_input = optarg;
fuzz_hq_framing_input = optarg;
break;
case 'p':
fuzz_pwritev_input = optarg;
break;
case 'l':
lsquic_log_to_fstream(stderr, 0);
@ -1194,10 +1619,14 @@ main (int argc, char **argv)
init_test_ctl_settings(&g_ctl_settings);
if (fuzz_input)
fuzz_guided_testing(fuzz_input);
if (fuzz_hq_framing_input)
fuzz_guided_hq_framing_testing(fuzz_hq_framing_input);
else if (fuzz_pwritev_input)
fuzz_guided_pwritev_testing(fuzz_pwritev_input);
else
{
main_test_pwritev();
return 0;
main_test_hq_framing();
for (n_packets = 1; n_packets <= 2; ++n_packets)
for (extra_sz = 0; extra_sz <= 2; ++extra_sz)

View file

@ -340,6 +340,9 @@ test_flushes_and_closes (void)
assert(s == 0);
s = lsquic_stream_close(stream);
assert(s == 0);
/* OK, we did not read FIN, expect these flags: */
assert((stream->sm_qflags & (SMQF_SEND_STOP_SENDING|SMQF_WAIT_FIN_OFF)) == (SMQF_SEND_STOP_SENDING|SMQF_WAIT_FIN_OFF));
lsquic_stream_ss_frame_sent(stream);
assert(stream->sm_hblock_sz == test_vals.prefix_sz + test_vals.headers_sz);
assert(0 == stream->sm_n_buffered);
assert(stream->sm_qflags & SMQF_WANT_WRITE); /* Still set */
@ -348,6 +351,9 @@ test_flushes_and_closes (void)
assert(stream->sm_qflags & SMQF_CALL_ONCLOSE);
lsquic_stream_acked(stream, QUIC_FRAME_STREAM);
lsquic_stream_call_on_close(stream);
assert(!(stream->sm_qflags & SMQF_FREE_STREAM)); /* Not yet */
lsquic_stream_rst_in(stream, 0, 0);
assert(!(stream->sm_qflags & (SMQF_SEND_STOP_SENDING|SMQF_WAIT_FIN_OFF)));
assert(stream->sm_qflags & SMQF_FREE_STREAM);
lsquic_stream_destroy(stream);
@ -386,6 +392,7 @@ test_headers_wantwrite_restoration (const int want_write)
assert(s == 0);
hset = lsquic_stream_get_hset(stream);
assert(hset == (void *) 12345);
stream->stream_flags |= STREAM_FIN_RECVD; /* Pretend we received FIN */
s = lsquic_stream_shutdown(stream, 0);
assert(0 == s);
test_vals.status = QWH_PARTIAL;

View file

@ -745,7 +745,7 @@ test_rem_FIN_loc_FIN (struct test_objs *tobjs)
/* Server: we read data and close the read side before reading FIN, which
* DOES NOT result in stream being reset.
* results in stream being reset.
*/
static void
test_rem_data_loc_close_and_rst_in (struct test_objs *tobjs)
@ -765,6 +765,13 @@ test_rem_data_loc_close_and_rst_in (struct test_objs *tobjs)
s = lsquic_stream_shutdown(stream, 0);
assert(0 == s);
/* Early read shutdown results in different frames on different QUIC
* transports:
*/
if (stream->sm_bflags & SMBF_IETF)
assert(stream->sm_qflags & SMQF_SEND_STOP_SENDING);
else
assert(stream->sm_qflags & SMQF_SEND_RST);
assert(TAILQ_EMPTY(&tobjs->conn_pub.service_streams));
assert(!((stream->sm_qflags & (SMQF_SERVICE_FLAGS)) == SMQF_CALL_ONCLOSE));
@ -776,6 +783,7 @@ test_rem_data_loc_close_and_rst_in (struct test_objs *tobjs)
assert(0 == s);
assert(1 == lsquic_send_ctl_n_scheduled(&tobjs->send_ctl)); /* Shutdown performs a flush */
assert(stream->n_unacked == 1);
assert(!TAILQ_EMPTY(&tobjs->conn_pub.service_streams));
assert((stream->sm_qflags & (SMQF_SERVICE_FLAGS)) == SMQF_CALL_ONCLOSE);
@ -783,7 +791,17 @@ test_rem_data_loc_close_and_rst_in (struct test_objs *tobjs)
s = lsquic_stream_rst_in(stream, 100, 1);
assert(0 == s);
assert(stream->sm_qflags & SMQF_FREE_STREAM);
assert(!(stream->sm_qflags & SMQF_FREE_STREAM)); /* Not yet */
assert(stream->sm_qflags & SMQF_CALL_ONCLOSE);
lsquic_stream_rst_frame_sent(stream);
stream->n_unacked++; /* RESET frame take a reference */
assert(!(stream->sm_qflags & SMQF_FREE_STREAM)); /* Not yet,
because: */ assert(stream->n_unacked == 2);
lsquic_stream_acked(stream, QUIC_FRAME_STREAM);
lsquic_stream_acked(stream, QUIC_FRAME_RST_STREAM);
assert(stream->sm_qflags & SMQF_FREE_STREAM); /* OK, now */
lsquic_stream_destroy(stream);
/* This simply checks that the stream got removed from the queue: */
@ -795,8 +813,8 @@ test_rem_data_loc_close_and_rst_in (struct test_objs *tobjs)
/* Server: we read data and close the read side before reading FIN. No
* FIN or RST arrive from peer. This should still place the stream on
* the "streams to be freed" list.
* FIN or RST arrive from peer. This should schedule RST_STREAM to be
* sent (this is gQUIC) and add "wait for known FIN" flag.
*/
static void
test_rem_data_loc_close (struct test_objs *tobjs)
@ -833,7 +851,16 @@ test_rem_data_loc_close (struct test_objs *tobjs)
assert(!(stream->sm_qflags & SMQF_FREE_STREAM));
lsquic_stream_acked(stream, QUIC_FRAME_STREAM);
assert(stream->sm_qflags & SMQF_FREE_STREAM);
lsquic_stream_rst_frame_sent(stream);
stream->n_unacked++; /* RESET frame take a reference */
assert(!(stream->sm_qflags & SMQF_FREE_STREAM)); /* No */
lsquic_stream_acked(stream, QUIC_FRAME_RST_STREAM);
assert(!(stream->sm_qflags & SMQF_FREE_STREAM)); /* Still no */
/* Stream will linger until we have the offset: */
assert(stream->sm_qflags & SMQF_WAIT_FIN_OFF);
lsquic_stream_destroy(stream);
/* This simply checks that the stream got removed from the queue: */
@ -918,6 +945,13 @@ test_loc_FIN_rem_RST (struct test_objs *tobjs)
ack_packet(&tobjs->send_ctl, 1);
ack_packet(&tobjs->send_ctl, 2);
#if 0
/* OK, here we pretend that we sent a RESET and it was acked */
assert(stream->sm_qflags & SMQF_SEND_RST);
stream->sm_qflags |= SMQF_SEND_RST;
stream->stream_flags
#endif
assert(!TAILQ_EMPTY(&tobjs->conn_pub.service_streams));
assert((stream->sm_qflags & (SMQF_SERVICE_FLAGS)) == (SMQF_CALL_ONCLOSE|SMQF_FREE_STREAM));
@ -1044,7 +1078,7 @@ test_loc_RST_rem_FIN (struct test_objs *tobjs)
assert(!TAILQ_EMPTY(&tobjs->conn_pub.sending_streams));
assert((stream->sm_qflags & SMQF_SENDING_FLAGS) == SMQF_SEND_RST);
sss = lsquic_stream_sending_state(stream);
assert(SSS_SEND == sss); /* Reset hasn't been packetized yet */
assert(SSS_DATA_SENT == sss); /* FIN was packetized */
s = lsquic_stream_frame_in(stream, new_frame_in(tobjs, 0, 90, 1));
assert(s == 0);
@ -1061,9 +1095,12 @@ test_loc_RST_rem_FIN (struct test_objs *tobjs)
sss = lsquic_stream_sending_state(stream);
assert(SSS_RESET_RECVD == sss);
lsquic_stream_call_on_close(stream);
assert(TAILQ_EMPTY(&tobjs->conn_pub.sending_streams));
lsquic_stream_call_on_close(stream);
assert(TAILQ_EMPTY(&tobjs->conn_pub.service_streams)); /* Not acked yet */
lsquic_stream_acked(stream, QUIC_FRAME_STREAM);
assert(!TAILQ_EMPTY(&tobjs->conn_pub.service_streams));
assert((stream->sm_qflags & SMQF_SERVICE_FLAGS) == SMQF_FREE_STREAM);
@ -1317,7 +1354,12 @@ test_data_flush_on_close (struct test_objs *tobjs)
assert(0 == lsquic_send_ctl_n_scheduled(&tobjs->send_ctl));
lsquic_stream_close(stream);
assert(1 == lsquic_send_ctl_n_scheduled(&tobjs->send_ctl));
/* Nothing is scheduled because STREAM frames are elided */
assert(0 == lsquic_send_ctl_n_scheduled(&tobjs->send_ctl));
assert(stream->sm_qflags & SMQF_SEND_RST);
assert(!(stream->sm_qflags & SMQF_FREE_STREAM));
assert(stream->sm_qflags & SMQF_WAIT_FIN_OFF);
/* We take connection cap hit after stream is flushed: */
assert(0x4000 - 100 == lsquic_conn_cap_avail(cap)); /* Conn cap hit */