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