Skip to content

Commit 81dde61

Browse files
committed
server: even more proptesting
1 parent 58cb957 commit 81dde61

File tree

2 files changed

+253
-0
lines changed

2 files changed

+253
-0
lines changed

bin/propolis-server/src/lib/vm/request_queue.rs

+252
Original file line numberDiff line numberDiff line change
@@ -1049,4 +1049,256 @@ mod test {
10491049
}
10501050
}
10511051
}
1052+
1053+
/// An operation that can be performed during a [`QueueDequeueTest`].
1054+
#[derive(Clone, Copy, Debug)]
1055+
enum QueueOp {
1056+
Enqueue(RequestKind),
1057+
Dequeue,
1058+
}
1059+
1060+
fn queue_op_strategy() -> impl Strategy<Value = QueueOp> {
1061+
prop_oneof![
1062+
request_strategy().prop_map(QueueOp::Enqueue),
1063+
Just(QueueOp::Dequeue)
1064+
]
1065+
}
1066+
1067+
/// A helper that queues and dequeues requests in a proptest-generated
1068+
/// order and that sends fake completion notifications back to the request
1069+
/// queue.
1070+
struct QueueDequeueTest {
1071+
/// The external request queue under test.
1072+
queue: ExternalRequestQueue,
1073+
1074+
/// The set of state change requests that the helper expects to see from
1075+
/// the external queue.
1076+
expected_state: VecDeque<RequestKind>,
1077+
1078+
/// The set of component change requests that the helper expects to see
1079+
/// from the external queue.
1080+
expected_component: VecDeque<RequestKind>,
1081+
1082+
/// True if the helper has queued a request to start its fake VM.
1083+
start_requested: bool,
1084+
1085+
/// True if the helper has successfully started its fake VM.
1086+
started: bool,
1087+
1088+
/// True if the helper has queued a request to stop its fake VM.
1089+
stop_requested: bool,
1090+
1091+
/// True if the helper has an outstanding request to reboot its fake VM.
1092+
reboot_requested: bool,
1093+
1094+
/// True if the helper has an outstanding request to migrate its fake
1095+
/// VM.
1096+
migrate_out_requested: bool,
1097+
1098+
/// True if the fake VM is halted (for any reason).
1099+
halted: bool,
1100+
}
1101+
1102+
impl QueueDequeueTest {
1103+
fn new() -> Self {
1104+
Self {
1105+
queue: ExternalRequestQueue::new(
1106+
test_logger(),
1107+
InstanceAutoStart::No,
1108+
),
1109+
expected_state: Default::default(),
1110+
expected_component: Default::default(),
1111+
start_requested: false,
1112+
started: false,
1113+
stop_requested: false,
1114+
reboot_requested: false,
1115+
migrate_out_requested: false,
1116+
halted: false,
1117+
}
1118+
}
1119+
1120+
fn run(&mut self, ops: Vec<QueueOp>) {
1121+
for op in ops {
1122+
match op {
1123+
QueueOp::Enqueue(request) => self.queue_request(request),
1124+
QueueOp::Dequeue => {
1125+
self.dequeue_request();
1126+
if self.halted {
1127+
return;
1128+
}
1129+
}
1130+
}
1131+
}
1132+
}
1133+
1134+
/// Submits the supplied `request` to the external request queue,
1135+
/// determines the expected result of that submission based on the
1136+
/// helper's current flags, and asserts that the result matches the
1137+
/// helper's expectation. If the helper expects the request to be
1138+
/// queued, it pushes an entry to its internal expected-change queues.
1139+
fn queue_request(&mut self, request: RequestKind) {
1140+
let result = self.queue.try_queue(request.into());
1141+
match request {
1142+
RequestKind::Start { .. } => {
1143+
if self.halted || self.stop_requested {
1144+
assert!(result.is_err());
1145+
return;
1146+
}
1147+
1148+
assert!(result.is_ok());
1149+
if !self.start_requested {
1150+
self.start_requested = true;
1151+
self.expected_state.push_back(request);
1152+
}
1153+
}
1154+
RequestKind::Stop => {
1155+
if self.halted || self.stop_requested {
1156+
assert!(result.is_ok());
1157+
return;
1158+
}
1159+
1160+
if self.migrate_out_requested {
1161+
assert!(result.is_err());
1162+
return;
1163+
}
1164+
1165+
self.stop_requested = true;
1166+
self.expected_state.push_back(request);
1167+
}
1168+
RequestKind::Reboot => {
1169+
if !self.started
1170+
|| self.halted
1171+
|| self.stop_requested
1172+
|| self.migrate_out_requested
1173+
{
1174+
assert!(result.is_err());
1175+
return;
1176+
}
1177+
1178+
assert!(result.is_ok());
1179+
if !self.reboot_requested {
1180+
self.reboot_requested = true;
1181+
self.expected_state.push_back(request);
1182+
}
1183+
}
1184+
RequestKind::Migrate { .. } => {
1185+
if (!self.started && !self.start_requested)
1186+
|| self.halted
1187+
|| self.stop_requested
1188+
|| self.migrate_out_requested
1189+
{
1190+
assert!(result.is_err());
1191+
return;
1192+
}
1193+
1194+
assert!(result.is_ok());
1195+
self.expected_state.push_back(request);
1196+
self.migrate_out_requested = true;
1197+
}
1198+
RequestKind::ReconfigureCrucible => {
1199+
if self.halted {
1200+
assert!(result.is_err());
1201+
return;
1202+
}
1203+
1204+
assert!(result.is_ok());
1205+
self.expected_component.push_back(request);
1206+
}
1207+
}
1208+
}
1209+
1210+
/// Pops a request from the helper's external queue and verifies that it
1211+
/// matches the first request on the helper's expected-change queue. If
1212+
/// the requests do match, sends a completion notification to the
1213+
/// external queue.
1214+
fn dequeue_request(&mut self) {
1215+
let (dequeued, expected) = match (
1216+
self.queue.pop_front(),
1217+
self.expected_state
1218+
.pop_front()
1219+
.or_else(|| self.expected_component.pop_front()),
1220+
) {
1221+
(None, None) => return,
1222+
(Some(d), None) => {
1223+
panic!("dequeued request {d:?} but expected nothing")
1224+
}
1225+
(None, Some(e)) => {
1226+
panic!("expected request {e:?} but dequeued nothing")
1227+
}
1228+
(Some(d), Some(e)) => (d, e),
1229+
};
1230+
1231+
match (dequeued, expected) {
1232+
(
1233+
ExternalRequest::State(StateChangeRequest::Start),
1234+
RequestKind::Start { will_succeed },
1235+
) => {
1236+
self.queue.notify_request_completed(
1237+
CompletedRequest::Start { succeeded: will_succeed },
1238+
);
1239+
if will_succeed {
1240+
self.started = true;
1241+
} else {
1242+
self.halted = true;
1243+
}
1244+
}
1245+
(
1246+
ExternalRequest::State(StateChangeRequest::Stop),
1247+
RequestKind::Stop,
1248+
) => {
1249+
self.queue.notify_request_completed(CompletedRequest::Stop);
1250+
self.halted = true;
1251+
}
1252+
(
1253+
ExternalRequest::State(StateChangeRequest::Reboot),
1254+
RequestKind::Reboot,
1255+
) => {
1256+
self.queue
1257+
.notify_request_completed(CompletedRequest::Reboot);
1258+
self.reboot_requested = false;
1259+
}
1260+
(
1261+
ExternalRequest::State(
1262+
StateChangeRequest::MigrateAsSource { .. },
1263+
),
1264+
RequestKind::Migrate { will_succeed },
1265+
) => {
1266+
self.queue.notify_request_completed(
1267+
CompletedRequest::MigrationOut {
1268+
succeeded: will_succeed,
1269+
},
1270+
);
1271+
self.migrate_out_requested = false;
1272+
if will_succeed {
1273+
self.halted = true;
1274+
}
1275+
}
1276+
(
1277+
ExternalRequest::Component(
1278+
ComponentChangeRequest::ReconfigureCrucibleVolume {
1279+
..
1280+
},
1281+
),
1282+
RequestKind::ReconfigureCrucible,
1283+
) => {}
1284+
(d, e) => panic!(
1285+
"dequeued request {d:?} but expected to dequeue {e:?}\n\
1286+
remaining queue: {:#?}\n\
1287+
remaining expected (state): {:#?}\n\
1288+
remaining expected (components): {:#?}",
1289+
self.queue, self.expected_state, self.expected_component
1290+
),
1291+
}
1292+
}
1293+
}
1294+
1295+
proptest! {
1296+
#[test]
1297+
fn request_queue_dequeue(
1298+
ops in prop::collection::vec(queue_op_strategy(), 0..100)
1299+
) {
1300+
let mut test = QueueDequeueTest::new();
1301+
test.run(ops);
1302+
}
1303+
}
10521304
}

bin/propolis-server/src/proptest-regressions/vm/request_queue.txt

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ cc 03ba07e9b5a99141bddd9b878bff86845a6da9eb1aa015b3afc7b3ebfed7a6d1 # shrinks to
99
cc 67a067444d475068e86b43528884319ff178d6d9038a3d9223c32789f871baa3 # shrinks to reqs = [Start, Migrate]
1010
cc b3df4b82bdb87e3533f4bd47f0a3ee8be21893c0afc15b472281b2a79006aadf # shrinks to reqs = [Migrate]
1111
cc 3430b43ba860946e5feb7b3b0246623708efb1465dd4fe7a604ddf479d4dc3ae # shrinks to reqs = [Start { will_succeed: true }, Migrate { will_succeed: false }, Reboot]
12+
cc 2e8b284223a88421aaed16749309839818c16efda4bc4d8d930a35cbdce018cd # shrinks to ops = [Enqueue(ReconfigureCrucible), Enqueue(Start { will_succeed: true }), Dequeue]

0 commit comments

Comments
 (0)