1pub mod results;
8pub mod workflow;
9pub mod xctestrun;
10
11use crate::proto::nskeyedarchiver_encode::{
12 archive_uuid, archive_xct_capabilities, archive_xctest_configuration, XcTestConfiguration,
13 XctCapabilities,
14};
15use bytes::Bytes;
16use tokio::io::{AsyncRead, AsyncWrite};
17use uuid::Uuid;
18
19use results::TestExecutionEvent;
20
21use crate::services::dtx::{
22 archived_object, encode_dtx, DtxConnection, DtxError, DtxMessage, DtxPayload, NSObject, PrimArg,
23};
24
25pub const SERVICE_IOS17: &str = "com.apple.dt.testmanagerd.remote";
26pub const SERVICE_IOS14: &str = "com.apple.testmanagerd.lockdown.secure";
27pub const SERVICE_LEGACY: &str = "com.apple.testmanagerd.lockdown";
28pub const SERVICE_NAME: &str = SERVICE_IOS17;
29pub const DAEMON_CONNECTION_INTERFACE: &str =
30 "dtxproxy:XCTestManager_IDEInterface:XCTestManager_DaemonConnectionInterface";
31pub const DRIVER_INTERFACE: &str = "dtxproxy:XCTestDriverInterface:XCTestManager_IDEInterface";
32pub const START_EXECUTING_SELECTOR: &str = "_IDE_startExecutingTestPlanWithProtocolVersion:";
33pub const INITIATE_SESSION_SELECTOR: &str = "_IDE_initiateSessionWithIdentifier:capabilities:";
34pub const INITIATE_CONTROL_SESSION_SELECTOR: &str = "_IDE_initiateControlSessionWithCapabilities:";
35pub const AUTHORIZE_TEST_SESSION_SELECTOR: &str = "_IDE_authorizeTestSessionWithProcessID:";
36pub const TEST_RUNNER_READY_SELECTOR: &str = "_XCT_testRunnerReadyWithCapabilities:";
37pub const TEST_BUNDLE_READY_SELECTOR: &str =
38 "_XCT_testBundleReadyWithProtocolVersion:minimumVersion:";
39pub const REQUEST_CHANNEL_SELECTOR: &str = "_requestChannelWithCode:identifier:";
40pub const PROTOCOL_VERSION: u32 = 36;
41const MSG_RESPONSE: u32 = 3;
42
43#[derive(Debug, Clone)]
44pub enum StartupEvent {
45 TestRunnerReady {
46 message: DtxMessage,
47 },
48 TestBundleReady {
49 message: DtxMessage,
50 protocol_version: u64,
51 minimum_version: u64,
52 },
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct StartupSummary {
57 pub protocol_version: u64,
58 pub minimum_version: u64,
59}
60
61pub struct TestmanagerClient<S1, S2 = S1> {
62 session: DtxConnection<S1>,
63 session_channel: i32,
64 driver_channel: Option<i32>,
65 control: Option<(DtxConnection<S2>, i32)>,
66}
67
68impl<S1, S2> TestmanagerClient<S1, S2>
69where
70 S1: AsyncRead + AsyncWrite + Unpin + Send,
71 S2: AsyncRead + AsyncWrite + Unpin + Send,
72{
73 pub async fn connect(session_stream: S1, control_stream: S2) -> Result<Self, DtxError> {
74 let mut session = DtxConnection::new(session_stream);
75 let mut control = DtxConnection::new(control_stream);
76
77 let session_channel = session.request_channel(DAEMON_CONNECTION_INTERFACE).await?;
78 let control_channel = control.request_channel(DAEMON_CONNECTION_INTERFACE).await?;
79
80 Ok(Self {
81 session,
82 session_channel,
83 driver_channel: None,
84 control: Some((control, control_channel)),
85 })
86 }
87
88 pub async fn initiate_session(
89 &mut self,
90 session_identifier: Bytes,
91 capabilities: Bytes,
92 ) -> Result<DtxMessage, DtxError> {
93 self.session
94 .method_call(
95 self.session_channel,
96 INITIATE_SESSION_SELECTOR,
97 &[
98 archived_object(session_identifier),
99 archived_object(capabilities),
100 ],
101 )
102 .await
103 }
104
105 pub async fn initiate_control_session(
106 &mut self,
107 capabilities: Bytes,
108 ) -> Result<DtxMessage, DtxError> {
109 let (control, channel) = self.control_mut()?;
110 control
111 .method_call(
112 channel,
113 INITIATE_CONTROL_SESSION_SELECTOR,
114 &[archived_object(capabilities)],
115 )
116 .await
117 }
118
119 pub async fn initiate_session_with_capabilities(
120 &mut self,
121 session_identifier: Uuid,
122 capabilities: XctCapabilities,
123 ) -> Result<DtxMessage, DtxError> {
124 self.initiate_session(
125 Bytes::from(archive_uuid(session_identifier)),
126 Bytes::from(archive_xct_capabilities(capabilities)),
127 )
128 .await
129 }
130
131 pub async fn initiate_control_session_with_capabilities(
132 &mut self,
133 capabilities: XctCapabilities,
134 ) -> Result<DtxMessage, DtxError> {
135 self.initiate_control_session(Bytes::from(archive_xct_capabilities(capabilities)))
136 .await
137 }
138
139 pub async fn authorize_test_session_with_process_id(
140 &mut self,
141 pid: u64,
142 ) -> Result<bool, DtxError> {
143 let (control, channel) = self.control_mut()?;
144 let response = control
145 .method_call(
146 channel,
147 AUTHORIZE_TEST_SESSION_SELECTOR,
148 &[PrimArg::Int64(pid as i64)],
149 )
150 .await?;
151
152 match response.payload {
153 DtxPayload::Response(NSObject::Bool(authorized)) => Ok(authorized),
154 other => Err(DtxError::Protocol(format!(
155 "unexpected authorize test session response: {other:?}"
156 ))),
157 }
158 }
159
160 pub async fn request_driver_channel(&mut self) -> Result<i32, DtxError> {
161 let channel = self.session.request_channel(DRIVER_INTERFACE).await?;
162 self.driver_channel = Some(channel);
163 Ok(channel)
164 }
165
166 pub async fn await_driver_channel_request(&mut self) -> Result<i32, DtxError> {
167 loop {
168 let msg = self.session.recv().await?;
169 if let DtxPayload::MethodInvocation { selector, .. } = &msg.payload {
170 if selector == REQUEST_CHANNEL_SELECTOR {
171 let (_requested_code, identifier) = decode_channel_request(&msg)?;
172 if msg.expects_reply {
173 self.session.send_ack(&msg).await?;
174 }
175
176 if identifier == DRIVER_INTERFACE {
177 self.driver_channel = Some(-1);
180 return Ok(-1);
181 }
182 continue;
183 }
184 }
185
186 if msg.expects_reply {
187 self.session.send_ack(&msg).await?;
188 }
189 }
190 }
191
192 pub async fn start_executing_test_plan(&mut self) -> Result<(), DtxError> {
193 let channel = self.driver_channel.unwrap_or(-1);
194 self.session
195 .method_call_async(
196 channel,
197 START_EXECUTING_SELECTOR,
198 &[PrimArg::Int64(PROTOCOL_VERSION as i64)],
199 )
200 .await
201 }
202
203 pub async fn recv_startup_event(&mut self) -> Result<StartupEvent, DtxError> {
204 loop {
205 let msg = self.session.recv().await?;
206 if let DtxPayload::MethodInvocation { selector, .. } = &msg.payload {
207 match selector.as_str() {
208 TEST_RUNNER_READY_SELECTOR => {
209 return Ok(StartupEvent::TestRunnerReady { message: msg });
210 }
211 TEST_BUNDLE_READY_SELECTOR => {
212 let (protocol_version, minimum_version) =
213 decode_test_bundle_ready_versions(&msg)?;
214 return Ok(StartupEvent::TestBundleReady {
215 message: msg,
216 protocol_version,
217 minimum_version,
218 });
219 }
220 _ => {}
221 }
222 }
223
224 if msg.expects_reply {
225 self.session.send_ack(&msg).await?;
226 }
227 }
228 }
229
230 pub async fn recv_execution_event(&mut self) -> Result<TestExecutionEvent, DtxError> {
231 loop {
232 let msg = self.session.recv().await?;
233 let event = TestExecutionEvent::from_dtx_message(&msg);
234
235 if msg.expects_reply {
236 self.session.send_ack(&msg).await?;
237 }
238
239 if let Some(event) = event {
240 return Ok(event);
241 }
242 }
243 }
244
245 pub async fn respond_test_runner_ready(
246 &mut self,
247 msg: &DtxMessage,
248 configuration: Bytes,
249 ) -> Result<(), DtxError> {
250 let frame = encode_dtx(
251 msg.identifier,
252 msg.conversation_idx + 1,
253 msg.channel_code,
254 false,
255 MSG_RESPONSE,
256 &configuration,
257 &[],
258 );
259 self.session.send_raw(&frame).await
260 }
261
262 pub async fn respond_test_runner_ready_with_configuration(
263 &mut self,
264 msg: &DtxMessage,
265 configuration: XcTestConfiguration,
266 ) -> Result<(), DtxError> {
267 self.respond_test_runner_ready(
268 msg,
269 Bytes::from(archive_xctest_configuration(configuration)),
270 )
271 .await
272 }
273
274 pub async fn complete_startup_with_configuration(
275 &mut self,
276 configuration: XcTestConfiguration,
277 ) -> Result<StartupSummary, DtxError> {
278 let mut bundle_ready = None;
279 let mut pending_configuration = Some(configuration);
280
281 loop {
282 match self.recv_startup_event().await? {
283 StartupEvent::TestBundleReady {
284 protocol_version,
285 minimum_version,
286 ..
287 } => {
288 bundle_ready = Some(StartupSummary {
289 protocol_version,
290 minimum_version,
291 });
292 }
293 StartupEvent::TestRunnerReady { message } => {
294 let configuration = pending_configuration.take().ok_or_else(|| {
295 DtxError::Protocol("test runner ready received more than once".into())
296 })?;
297 self.respond_test_runner_ready_with_configuration(&message, configuration)
298 .await?;
299
300 if let Some(summary) = bundle_ready {
301 return Ok(summary);
302 }
303 }
304 }
305 }
306 }
307
308 pub async fn authorize_and_start_test_plan_with_configuration(
309 &mut self,
310 pid: u64,
311 configuration: XcTestConfiguration,
312 ) -> Result<StartupSummary, DtxError> {
313 if !self.authorize_test_session_with_process_id(pid).await? {
314 return Err(DtxError::Protocol(
315 "testmanagerd rejected test session authorization".into(),
316 ));
317 }
318
319 let mut bundle_ready = None;
320 let mut pending_configuration = Some(configuration);
321 let mut driver_ready = self.driver_channel.is_some();
322
323 loop {
324 let msg = self.session.recv().await?;
325 if let DtxPayload::MethodInvocation { selector, .. } = &msg.payload {
326 match selector.as_str() {
327 TEST_BUNDLE_READY_SELECTOR => {
328 let (protocol_version, minimum_version) =
329 decode_test_bundle_ready_versions(&msg)?;
330 bundle_ready = Some(StartupSummary {
331 protocol_version,
332 minimum_version,
333 });
334 }
335 TEST_RUNNER_READY_SELECTOR => {
336 let configuration = pending_configuration.take().ok_or_else(|| {
337 DtxError::Protocol(
338 "test runner ready received more than once during startup".into(),
339 )
340 })?;
341 self.respond_test_runner_ready_with_configuration(&msg, configuration)
342 .await?;
343 }
344 REQUEST_CHANNEL_SELECTOR => {
345 let (_requested_code, identifier) = decode_channel_request(&msg)?;
346 if msg.expects_reply {
347 self.session.send_ack(&msg).await?;
348 }
349 if identifier == DRIVER_INTERFACE {
350 self.driver_channel = Some(-1);
351 driver_ready = true;
352 }
353 if let Some(summary) = bundle_ready.filter(|_| driver_ready) {
354 if pending_configuration.is_none() {
355 self.start_executing_test_plan().await?;
357 return Ok(summary);
358 }
359 }
360 continue;
361 }
362 _ => {}
363 }
364 }
365
366 if msg.expects_reply
367 && !matches!(
368 &msg.payload,
369 DtxPayload::MethodInvocation {
370 selector,
371 ..
372 } if selector == TEST_RUNNER_READY_SELECTOR
373 )
374 {
375 self.session.send_ack(&msg).await?;
376 }
377
378 if let Some(summary) = bundle_ready.filter(|_| driver_ready) {
381 if pending_configuration.is_none() {
382 self.start_executing_test_plan().await?;
383 return Ok(summary);
384 }
385 }
386 }
387 }
388
389 fn control_mut(&mut self) -> Result<(&mut DtxConnection<S2>, i32), DtxError> {
390 self.control
391 .as_mut()
392 .map(|(control, channel)| (control, *channel))
393 .ok_or_else(|| DtxError::Protocol("control connection is not configured".into()))
394 }
395}
396
397fn decode_test_bundle_ready_versions(msg: &DtxMessage) -> Result<(u64, u64), DtxError> {
398 let DtxPayload::MethodInvocation { args, .. } = &msg.payload else {
399 return Err(DtxError::Protocol(
400 "test bundle ready event did not contain a method invocation".into(),
401 ));
402 };
403
404 let protocol_version = args
405 .first()
406 .and_then(|value| value.as_int())
407 .ok_or_else(|| DtxError::Protocol("missing test bundle protocol version".into()))?;
408 let minimum_version = args
409 .get(1)
410 .and_then(|value| value.as_int())
411 .ok_or_else(|| DtxError::Protocol("missing test bundle minimum version".into()))?;
412
413 if protocol_version < 0 || minimum_version < 0 {
414 return Err(DtxError::Protocol(
415 "test bundle versions must be non-negative".into(),
416 ));
417 }
418
419 Ok((protocol_version as u64, minimum_version as u64))
420}
421
422fn decode_channel_request(msg: &DtxMessage) -> Result<(i32, String), DtxError> {
423 let DtxPayload::MethodInvocation { args, .. } = &msg.payload else {
424 return Err(DtxError::Protocol(
425 "channel request did not contain a method invocation".into(),
426 ));
427 };
428
429 let requested_code = args
430 .first()
431 .and_then(|value| value.as_int())
432 .ok_or_else(|| DtxError::Protocol("missing requested channel code".into()))?;
433 let identifier = args
434 .get(1)
435 .and_then(|value| value.as_str())
436 .ok_or_else(|| DtxError::Protocol("missing requested channel identifier".into()))?;
437
438 if requested_code < i32::MIN as i64 || requested_code > i32::MAX as i64 {
439 return Err(DtxError::Protocol(
440 "requested channel code out of range".into(),
441 ));
442 }
443
444 Ok((requested_code as i32, identifier.to_string()))
445}
446
447impl<S> TestmanagerClient<S, S>
448where
449 S: AsyncRead + AsyncWrite + Unpin + Send,
450{
451 #[cfg(feature = "testmanager")]
453 pub fn from_session_connection_for_test(session_stream: S, session_channel: i32) -> Self {
454 Self {
455 session: DtxConnection::new(session_stream),
456 session_channel,
457 driver_channel: None,
458 control: None,
459 }
460 }
461
462 #[cfg(feature = "testmanager")]
464 pub fn from_connections_for_test(
465 session_stream: S,
466 session_channel: i32,
467 control_stream: S,
468 control_channel: i32,
469 ) -> Self {
470 Self {
471 session: DtxConnection::new(session_stream),
472 session_channel,
473 driver_channel: None,
474 control: Some((DtxConnection::new(control_stream), control_channel)),
475 }
476 }
477}