Skip to content

Commit d427d7f

Browse files
jasnellcodebytere
authored andcommitted
test: add common/udppair utility
Extracted from the QUIC PR. This adds a utility used to deterministically test UDP traffic. It is currently only used by the experimental QUIC implementation. Separated out on request to make review easier. PR-URL: #33380 Reviewed-By: Sam Roberts <[email protected]>
1 parent 5362fef commit d427d7f

8 files changed

+342
-0
lines changed

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@
571571
'src/js_native_api_v8_internals.h',
572572
'src/js_stream.cc',
573573
'src/json_utils.cc',
574+
'src/js_udp_wrap.cc',
574575
'src/module_wrap.cc',
575576
'src/node.cc',
576577
'src/node_api.cc',

src/async_wrap.h

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ namespace node {
5151
V(HTTPINCOMINGMESSAGE) \
5252
V(HTTPCLIENTREQUEST) \
5353
V(JSSTREAM) \
54+
V(JSUDPWRAP) \
5455
V(MESSAGEPORT) \
5556
V(PIPECONNECTWRAP) \
5657
V(PIPESERVERWRAP) \

src/js_udp_wrap.cc

+218
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
#include "udp_wrap.h"
2+
#include "async_wrap-inl.h"
3+
#include "node_errors.h"
4+
#include "node_sockaddr-inl.h"
5+
6+
#include <algorithm>
7+
8+
namespace node {
9+
10+
using errors::TryCatchScope;
11+
using v8::Array;
12+
using v8::Context;
13+
using v8::FunctionCallbackInfo;
14+
using v8::FunctionTemplate;
15+
using v8::HandleScope;
16+
using v8::Int32;
17+
using v8::Local;
18+
using v8::Object;
19+
using v8::String;
20+
using v8::Value;
21+
22+
// JSUDPWrap is a testing utility used by test/common/udppair.js
23+
// to simulate UDP traffic deterministically in Node.js tests.
24+
class JSUDPWrap final : public UDPWrapBase, public AsyncWrap {
25+
public:
26+
JSUDPWrap(Environment* env, Local<Object> obj);
27+
28+
int RecvStart() override;
29+
int RecvStop() override;
30+
ssize_t Send(uv_buf_t* bufs,
31+
size_t nbufs,
32+
const sockaddr* addr) override;
33+
SocketAddress GetPeerName() override;
34+
SocketAddress GetSockName() override;
35+
AsyncWrap* GetAsyncWrap() override { return this; }
36+
37+
static void New(const FunctionCallbackInfo<Value>& args);
38+
static void EmitReceived(const FunctionCallbackInfo<Value>& args);
39+
static void OnSendDone(const FunctionCallbackInfo<Value>& args);
40+
static void OnAfterBind(const FunctionCallbackInfo<Value>& args);
41+
42+
static void Initialize(Local<Object> target,
43+
Local<Value> unused,
44+
Local<Context> context,
45+
void* priv);
46+
SET_NO_MEMORY_INFO()
47+
SET_MEMORY_INFO_NAME(JSUDPWrap)
48+
SET_SELF_SIZE(JSUDPWrap)
49+
};
50+
51+
JSUDPWrap::JSUDPWrap(Environment* env, Local<Object> obj)
52+
: AsyncWrap(env, obj, PROVIDER_JSUDPWRAP) {
53+
MakeWeak();
54+
55+
obj->SetAlignedPointerInInternalField(
56+
kUDPWrapBaseField, static_cast<UDPWrapBase*>(this));
57+
}
58+
59+
int JSUDPWrap::RecvStart() {
60+
HandleScope scope(env()->isolate());
61+
Context::Scope context_scope(env()->context());
62+
TryCatchScope try_catch(env());
63+
Local<Value> value;
64+
int32_t value_int = UV_EPROTO;
65+
if (!MakeCallback(env()->onreadstart_string(), 0, nullptr).ToLocal(&value) ||
66+
!value->Int32Value(env()->context()).To(&value_int)) {
67+
if (try_catch.HasCaught() && !try_catch.HasTerminated())
68+
errors::TriggerUncaughtException(env()->isolate(), try_catch);
69+
}
70+
return value_int;
71+
}
72+
73+
int JSUDPWrap::RecvStop() {
74+
HandleScope scope(env()->isolate());
75+
Context::Scope context_scope(env()->context());
76+
TryCatchScope try_catch(env());
77+
Local<Value> value;
78+
int32_t value_int = UV_EPROTO;
79+
if (!MakeCallback(env()->onreadstop_string(), 0, nullptr).ToLocal(&value) ||
80+
!value->Int32Value(env()->context()).To(&value_int)) {
81+
if (try_catch.HasCaught() && !try_catch.HasTerminated())
82+
errors::TriggerUncaughtException(env()->isolate(), try_catch);
83+
}
84+
return value_int;
85+
}
86+
87+
ssize_t JSUDPWrap::Send(uv_buf_t* bufs,
88+
size_t nbufs,
89+
const sockaddr* addr) {
90+
HandleScope scope(env()->isolate());
91+
Context::Scope context_scope(env()->context());
92+
TryCatchScope try_catch(env());
93+
Local<Value> value;
94+
int64_t value_int = UV_EPROTO;
95+
size_t total_len = 0;
96+
97+
MaybeStackBuffer<Local<Value>, 16> buffers(nbufs);
98+
for (size_t i = 0; i < nbufs; i++) {
99+
buffers[i] = Buffer::Copy(env(), bufs[i].base, bufs[i].len)
100+
.ToLocalChecked();
101+
total_len += bufs[i].len;
102+
}
103+
104+
Local<Value> args[] = {
105+
listener()->CreateSendWrap(total_len)->object(),
106+
Array::New(env()->isolate(), buffers.out(), nbufs),
107+
AddressToJS(env(), addr)
108+
};
109+
110+
if (!MakeCallback(env()->onwrite_string(), arraysize(args), args)
111+
.ToLocal(&value) ||
112+
!value->IntegerValue(env()->context()).To(&value_int)) {
113+
if (try_catch.HasCaught() && !try_catch.HasTerminated())
114+
errors::TriggerUncaughtException(env()->isolate(), try_catch);
115+
}
116+
return value_int;
117+
}
118+
119+
SocketAddress JSUDPWrap::GetPeerName() {
120+
SocketAddress ret;
121+
CHECK(SocketAddress::New(AF_INET, "127.0.0.1", 1337, &ret));
122+
return ret;
123+
}
124+
125+
SocketAddress JSUDPWrap::GetSockName() {
126+
SocketAddress ret;
127+
CHECK(SocketAddress::New(AF_INET, "127.0.0.1", 1337, &ret));
128+
return ret;
129+
}
130+
131+
void JSUDPWrap::New(const FunctionCallbackInfo<Value>& args) {
132+
Environment* env = Environment::GetCurrent(args);
133+
CHECK(args.IsConstructCall());
134+
new JSUDPWrap(env, args.Holder());
135+
}
136+
137+
void JSUDPWrap::EmitReceived(const FunctionCallbackInfo<Value>& args) {
138+
JSUDPWrap* wrap;
139+
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
140+
Environment* env = wrap->env();
141+
142+
ArrayBufferViewContents<char> buffer(args[0]);
143+
const char* data = buffer.data();
144+
int len = buffer.length();
145+
146+
CHECK(args[1]->IsInt32()); // family
147+
CHECK(args[2]->IsString()); // address
148+
CHECK(args[3]->IsInt32()); // port
149+
CHECK(args[4]->IsInt32()); // flags
150+
int family = args[1].As<Int32>()->Value() == 4 ? AF_INET : AF_INET6;
151+
Utf8Value address(env->isolate(), args[2]);
152+
int port = args[3].As<Int32>()->Value();
153+
int flags = args[3].As<Int32>()->Value();
154+
155+
sockaddr_storage addr;
156+
CHECK_EQ(sockaddr_for_family(family, *address, port, &addr), 0);
157+
158+
// Repeatedly ask the stream's owner for memory, copy the data that we
159+
// just read from JS into those buffers and emit them as reads.
160+
while (len != 0) {
161+
uv_buf_t buf = wrap->listener()->OnAlloc(len);
162+
ssize_t avail = std::min<size_t>(buf.len, len);
163+
memcpy(buf.base, data, avail);
164+
data += avail;
165+
len -= avail;
166+
wrap->listener()->OnRecv(
167+
avail, buf, reinterpret_cast<sockaddr*>(&addr), flags);
168+
}
169+
}
170+
171+
void JSUDPWrap::OnSendDone(const FunctionCallbackInfo<Value>& args) {
172+
JSUDPWrap* wrap;
173+
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
174+
175+
CHECK(args[0]->IsObject());
176+
CHECK(args[1]->IsInt32());
177+
ReqWrap<uv_udp_send_t>* req_wrap;
178+
ASSIGN_OR_RETURN_UNWRAP(&req_wrap, args[0].As<Object>());
179+
int status = args[1].As<Int32>()->Value();
180+
181+
wrap->listener()->OnSendDone(req_wrap, status);
182+
}
183+
184+
void JSUDPWrap::OnAfterBind(const FunctionCallbackInfo<Value>& args) {
185+
JSUDPWrap* wrap;
186+
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
187+
188+
wrap->listener()->OnAfterBind();
189+
}
190+
191+
void JSUDPWrap::Initialize(Local<Object> target,
192+
Local<Value> unused,
193+
Local<Context> context,
194+
void* priv) {
195+
Environment* env = Environment::GetCurrent(context);
196+
197+
Local<FunctionTemplate> t = env->NewFunctionTemplate(New);
198+
Local<String> js_udp_wrap_string =
199+
FIXED_ONE_BYTE_STRING(env->isolate(), "JSUDPWrap");
200+
t->SetClassName(js_udp_wrap_string);
201+
t->InstanceTemplate()
202+
->SetInternalFieldCount(UDPWrapBase::kUDPWrapBaseField + 1);
203+
t->Inherit(AsyncWrap::GetConstructorTemplate(env));
204+
205+
UDPWrapBase::AddMethods(env, t);
206+
env->SetProtoMethod(t, "emitReceived", EmitReceived);
207+
env->SetProtoMethod(t, "onSendDone", OnSendDone);
208+
env->SetProtoMethod(t, "onAfterBind", OnAfterBind);
209+
210+
target->Set(env->context(),
211+
js_udp_wrap_string,
212+
t->GetFunction(context).ToLocalChecked()).Check();
213+
}
214+
215+
216+
} // namespace node
217+
218+
NODE_MODULE_CONTEXT_AWARE_INTERNAL(js_udp_wrap, node::JSUDPWrap::Initialize)

src/node_binding.cc

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
V(http_parser) \
5353
V(inspector) \
5454
V(js_stream) \
55+
V(js_udp_wrap) \
5556
V(messaging) \
5657
V(module_wrap) \
5758
V(native_module) \

src/udp_wrap.h

+5
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ class UDPWrap final : public HandleWrap,
215215
v8::Local<v8::Object> current_send_req_wrap_;
216216
};
217217

218+
int sockaddr_for_family(int address_family,
219+
const char* address,
220+
const unsigned short port,
221+
sockaddr_storage* addr);
222+
218223
} // namespace node
219224

220225
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

test/common/README.md

+13
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,19 @@ listener to process `'beforeExit'`. If a file needs to be left open until
943943
Node.js completes, use a child process and call `refresh()` only in the
944944
parent.
945945

946+
## UDP pair helper
947+
948+
The `common/udppair` module exports a function `makeUDPPair` and a class
949+
`FakeUDPWrap`.
950+
951+
`FakeUDPWrap` emits `'send'` events when data is to be sent on it, and provides
952+
an `emitReceived()` API for actin as if data has been received on it.
953+
954+
`makeUDPPair` returns an object `{ clientSide, serverSide }` where each side
955+
is an `FakeUDPWrap` connected to the other side.
956+
957+
There is no difference between cient or server side beyond their names.
958+
946959
## WPT Module
947960

948961
### `harness`

test/common/udppair.js

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/* eslint-disable node-core/require-common-first, node-core/required-modules */
2+
'use strict';
3+
const { internalBinding } = require('internal/test/binding');
4+
const { JSUDPWrap } = internalBinding('js_udp_wrap');
5+
const EventEmitter = require('events');
6+
7+
// FakeUDPWrap is a testing utility that emulates a UDP connection
8+
// for the sake of making UDP tests more deterministic.
9+
class FakeUDPWrap extends EventEmitter {
10+
constructor() {
11+
super();
12+
13+
this._handle = new JSUDPWrap();
14+
15+
this._handle.onreadstart = () => this._startReading();
16+
this._handle.onreadstop = () => this._stopReading();
17+
this._handle.onwrite =
18+
(wrap, buffers, addr) => this._write(wrap, buffers, addr);
19+
this._handle.getsockname = (obj) => {
20+
Object.assign(obj, { address: '127.0.0.1', family: 'IPv4', port: 1337 });
21+
return 0;
22+
};
23+
24+
this.reading = false;
25+
this.bufferedReceived = [];
26+
this.emitBufferedImmediate = null;
27+
}
28+
29+
_emitBuffered = () => {
30+
if (!this.reading) return;
31+
if (this.bufferedReceived.length > 0) {
32+
this.emitReceived(this.bufferedReceived.shift());
33+
this.emitBufferedImmediate = setImmediate(this._emitBuffered);
34+
} else {
35+
this.emit('wantRead');
36+
}
37+
};
38+
39+
_startReading() {
40+
this.reading = true;
41+
this.emitBufferedImmediate = setImmediate(this._emitBuffered);
42+
}
43+
44+
_stopReading() {
45+
this.reading = false;
46+
clearImmediate(this.emitBufferedImmediate);
47+
}
48+
49+
_write(wrap, buffers, addr) {
50+
this.emit('send', { buffers, addr });
51+
setImmediate(() => this._handle.onSendDone(wrap, 0));
52+
}
53+
54+
afterBind() {
55+
this._handle.onAfterBind();
56+
}
57+
58+
emitReceived(info) {
59+
if (!this.reading) {
60+
this.bufferedReceived.push(info);
61+
return;
62+
}
63+
64+
const {
65+
buffers,
66+
addr: {
67+
family = 4,
68+
address = '127.0.0.1',
69+
port = 1337,
70+
},
71+
flags = 0
72+
} = info;
73+
74+
let familyInt;
75+
switch (family) {
76+
case 'IPv4': familyInt = 4; break;
77+
case 'IPv6': familyInt = 6; break;
78+
default: throw new Error('bad family');
79+
}
80+
81+
for (const buffer of buffers) {
82+
this._handle.emitReceived(buffer, familyInt, address, port, flags);
83+
}
84+
}
85+
}
86+
87+
function makeUDPPair() {
88+
const serverSide = new FakeUDPWrap();
89+
const clientSide = new FakeUDPWrap();
90+
91+
serverSide.on('send',
92+
(chk) => setImmediate(() => clientSide.emitReceived(chk)));
93+
clientSide.on('send',
94+
(chk) => setImmediate(() => serverSide.emitReceived(chk)));
95+
96+
return { serverSide, clientSide };
97+
}
98+
99+
module.exports = {
100+
FakeUDPWrap,
101+
makeUDPPair
102+
};

test/sequential/test-async-wrap-getasyncid.js

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const { getSystemErrorName } = require('util');
4545
delete providers.STREAMPIPE;
4646
delete providers.MESSAGEPORT;
4747
delete providers.WORKER;
48+
delete providers.JSUDPWRAP;
4849
if (!common.isMainThread)
4950
delete providers.INSPECTORJSBINDING;
5051
delete providers.KEYPAIRGENREQUEST;

0 commit comments

Comments
 (0)