1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
use super::*;
use crate::arithmetic::push_int;
use crate::scheduler::scheduler_init;
#[test]
fn test_tcp_listen() {
unsafe {
scheduler_init();
let stack = crate::stack::alloc_test_stack();
let stack = push_int(stack, 0); // Port 0 = OS assigns random port
let stack = tcp_listen(stack);
// Now returns (Int, Bool) - Bool on top
let (stack, success) = pop(stack);
assert!(
matches!(success, Value::Bool(true)),
"tcp_listen should succeed"
);
let (_stack, result) = pop(stack);
match result {
Value::Int(listener_id) => {
assert!(listener_id >= 0, "Listener ID should be non-negative");
}
_ => panic!("Expected Int (listener_id), got {:?}", result),
}
}
}
#[test]
fn test_tcp_listen_invalid_port_negative() {
unsafe {
scheduler_init();
let stack = crate::stack::alloc_test_stack();
let stack = push_int(stack, -1);
let stack = tcp_listen(stack);
// Invalid port returns (0, false)
let (stack, success) = pop(stack);
assert!(
matches!(success, Value::Bool(false)),
"Invalid port should return false"
);
let (_stack, result) = pop(stack);
assert!(
matches!(result, Value::Int(0)),
"Invalid port should return 0"
);
}
}
#[test]
fn test_tcp_listen_invalid_port_too_high() {
unsafe {
scheduler_init();
let stack = crate::stack::alloc_test_stack();
let stack = push_int(stack, 65536);
let stack = tcp_listen(stack);
// Invalid port returns (0, false)
let (stack, success) = pop(stack);
assert!(
matches!(success, Value::Bool(false)),
"Invalid port should return false"
);
let (_stack, result) = pop(stack);
assert!(
matches!(result, Value::Int(0)),
"Invalid port should return 0"
);
}
}
#[test]
fn test_tcp_port_range_valid() {
unsafe {
scheduler_init();
// Test port 0 (OS-assigned)
let stack = push_int(crate::stack::alloc_test_stack(), 0);
let stack = tcp_listen(stack);
let (stack, success) = pop(stack);
assert!(matches!(success, Value::Bool(true)));
let (_, result) = pop(stack);
assert!(matches!(result, Value::Int(_)));
// Test a non-privileged port (ports 1-1023 require root on Unix)
// Use port 9999 which should be available and doesn't require privileges
let stack = push_int(crate::stack::alloc_test_stack(), 9999);
let stack = tcp_listen(stack);
let (stack, success) = pop(stack);
assert!(matches!(success, Value::Bool(true)));
let (_, result) = pop(stack);
assert!(matches!(result, Value::Int(_)));
// Note: Can't easily test all edge cases (port 1, 65535) as they
// may require privileges or be in use. Port validation logic is
// tested separately in the invalid port tests.
}
}
#[test]
fn test_socket_id_reuse_after_close() {
unsafe {
scheduler_init();
// Create a listener and accept a hypothetical connection
let stack = push_int(crate::stack::alloc_test_stack(), 0);
let stack = tcp_listen(stack);
let (stack, success) = pop(stack);
assert!(matches!(success, Value::Bool(true)));
let (_stack, listener_result) = pop(stack);
let listener_id = match listener_result {
Value::Int(id) => id,
_ => panic!("Expected listener ID"),
};
// Verify listener ID is valid
assert!(listener_id >= 0);
// Note: We can't easily test connection acceptance without
// actually making a connection, but we can test the registry behavior
// Clean up
}
}
#[test]
fn test_tcp_read_invalid_socket_id() {
unsafe {
scheduler_init();
// Invalid socket ID now returns ("", false) instead of panicking
let stack = push_int(crate::stack::alloc_test_stack(), 9999);
let stack = tcp_read(stack);
let (stack, success) = pop(stack);
assert!(
matches!(success, Value::Bool(false)),
"Invalid socket should return false"
);
let (_stack, result) = pop(stack);
match result {
Value::String(s) => assert_eq!(s.as_str_or_empty(), ""),
_ => panic!("Expected empty string"),
}
}
}
#[test]
fn test_tcp_write_invalid_socket_id() {
unsafe {
scheduler_init();
// Invalid socket ID now returns false instead of panicking
let stack = push(
crate::stack::alloc_test_stack(),
Value::String("test".into()),
);
let stack = push_int(stack, 9999);
let stack = tcp_write(stack);
let (_stack, success) = pop(stack);
assert!(
matches!(success, Value::Bool(false)),
"Invalid socket should return false"
);
}
}
#[test]
fn test_tcp_close_idempotent() {
unsafe {
scheduler_init();
// Create a socket to close
let stack = push_int(crate::stack::alloc_test_stack(), 0);
let stack = tcp_listen(stack);
let (stack, success) = pop(stack);
assert!(matches!(success, Value::Bool(true)));
let (stack, _listener_result) = pop(stack);
// Close an invalid socket - now returns false instead of crashing
let stack = push_int(stack, 9999);
let stack = tcp_close(stack);
let (_stack, success) = pop(stack);
assert!(
matches!(success, Value::Bool(false)),
"Invalid socket close should return false"
);
}
}
#[test]
fn test_socket_registry_capacity() {
// Test that MAX_SOCKETS limit is enforced
// Note: We can't easily allocate 10,000 real sockets in a unit test,
// but the limit check is in the code at lines 38-41
// This test documents the expected behavior
// If we could allocate that many:
// - First 10,000 allocations should succeed
// - 10,001st allocation should panic with "Maximum socket limit reached"
// For now, just verify the constant exists
assert_eq!(MAX_SOCKETS, 10_000);
}
#[test]
fn test_max_read_size_limit() {
// Test that MAX_READ_SIZE limit exists and is reasonable
assert_eq!(MAX_READ_SIZE, 1_048_576); // 1 MB
// In practice, if tcp_read receives more than 1 MB, it should panic
// with "read size limit exceeded". Testing this requires a real socket
// with more than 1 MB of data, which is impractical for unit tests.
}
#[test]
fn test_socket_registry_release_reserved_round_trip() {
// Pins the round-trip semantics that the TLS-handshake-failure
// path silently broke until #486 review: after a successful
// `take_tcp`, the slot's inner Option is None, which makes
// plain `free()` a no-op. Without `release_reserved`, the id
// would leak — the Vec grows but the free list never sees the
// id again. This test allocates, takes, releases-reserved, and
// allocates a second time, asserting we get the same id back.
let mut reg: SocketRegistry<i32> = SocketRegistry::new();
// Allocate, get id 0.
let id = reg.allocate(42).expect("allocate") as usize;
assert_eq!(id, 0, "first allocation should give id 0");
// Simulate take_tcp: pull the value out, leaving the slot
// Some(None). This is the shape upgrade_tcp_in_place sees on
// entry before it hands the stream to its callback.
let slot = reg.get_mut(id).expect("slot exists");
let _value = slot.take().expect("slot was occupied");
assert!(slot.is_none(), "slot should be reserved after take");
// Release the reserved id and confirm the round-trip.
reg.release_reserved(id);
let id2 = reg.allocate(99).expect("second allocate") as usize;
assert_eq!(
id2, id,
"release_reserved must put the id back on the free list \
so the next allocate reuses it (otherwise the id leaks)"
);
// Double-release of the same id must not double-push onto the
// free list — if it did, the next two allocations would get
// the same id and we'd have a use-after-free in disguise.
let slot = reg.get_mut(id2).expect("slot exists");
let _value = slot.take().expect("slot was occupied");
reg.release_reserved(id2);
reg.release_reserved(id2); // duplicate
let id3 = reg.allocate(7).expect("third allocate") as usize;
let id4 = reg.allocate(8).expect("fourth allocate") as usize;
assert_ne!(
id3, id4,
"double release_reserved must not cause two allocations \
to receive the same id"
);
}