fuchsia_audio_dai/
test.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::format_err;
6use derivative::Derivative;
7use fidl_fuchsia_hardware_audio::*;
8use fuchsia_async as fasync;
9use futures::{Future, StreamExt};
10use log::warn;
11use std::sync::{Arc, Mutex};
12use vfs::directory::entry_container::Directory;
13use vfs::{pseudo_directory, service};
14
15use crate::driver::{ensure_dai_format_is_supported, ensure_pcm_format_is_supported};
16use crate::DigitalAudioInterface;
17
18/// The status of the current device.  Retrievable via `TestHandle::status`.
19#[derive(Derivative, Clone, Debug)]
20#[derivative(Default)]
21pub enum TestStatus {
22    #[derivative(Default)]
23    Idle,
24    Configured {
25        dai_format: DaiFormat,
26        pcm_format: PcmFormat,
27    },
28    Started {
29        dai_format: DaiFormat,
30        pcm_format: PcmFormat,
31    },
32}
33
34impl TestStatus {
35    fn start(&mut self) -> Result<(), ()> {
36        if let Self::Configured { dai_format, pcm_format } = self {
37            *self = Self::Started { dai_format: *dai_format, pcm_format: *pcm_format };
38            Ok(())
39        } else {
40            Err(())
41        }
42    }
43
44    fn stop(&mut self) {
45        if let Self::Started { dai_format, pcm_format } = *self {
46            *self = Self::Configured { dai_format, pcm_format };
47        }
48    }
49}
50
51#[derive(Clone)]
52pub struct TestHandle(Arc<Mutex<TestStatus>>);
53
54impl TestHandle {
55    pub fn new() -> Self {
56        Self(Arc::new(Mutex::new(TestStatus::default())))
57    }
58
59    pub fn status(&self) -> TestStatus {
60        self.0.lock().unwrap().clone()
61    }
62
63    pub fn is_started(&self) -> bool {
64        let lock = self.0.lock().unwrap();
65        match *lock {
66            TestStatus::Started { .. } => true,
67            _ => false,
68        }
69    }
70
71    fn set_configured(&self, dai_format: DaiFormat, pcm_format: PcmFormat) {
72        let mut lock = self.0.lock().unwrap();
73        *lock = TestStatus::Configured { dai_format, pcm_format };
74    }
75
76    fn start(&self) -> Result<(), ()> {
77        self.0.lock().unwrap().start()
78    }
79
80    fn stop(&self) {
81        self.0.lock().unwrap().stop()
82    }
83}
84
85/// Logs and breaks out of the loop if the result is an Error.
86macro_rules! log_error {
87    ($result:expr, $tag:expr) => {
88        if let Err(e) = $result {
89            warn!("Error sending {}: {:?}", $tag, e);
90            break;
91        }
92    };
93}
94
95async fn handle_ring_buffer(mut requests: RingBufferRequestStream, handle: TestHandle) {
96    while let Some(req) = requests.next().await {
97        if let Err(e) = req {
98            warn!("Error processing RingBuffer request stream: {:?}", e);
99            break;
100        }
101        match req.unwrap() {
102            RingBufferRequest::Start { responder } => match handle.start() {
103                Ok(()) => log_error!(responder.send(0), "ring buffer start response"),
104                Err(()) => {
105                    warn!("Started when we couldn't expect it, shutting down");
106                }
107            },
108            RingBufferRequest::Stop { responder } => {
109                handle.stop();
110                log_error!(responder.send(), "ring buffer stop response");
111            }
112            x => unimplemented!("RingBuffer Request not implemented: {:?}", x),
113        };
114    }
115}
116
117async fn test_handle_dai_requests(
118    dai_formats: DaiSupportedFormats,
119    pcm_formats: SupportedFormats,
120    as_input: bool,
121    mut requests: DaiRequestStream,
122    handle: TestHandle,
123) {
124    use std::slice::from_ref;
125    let properties = DaiProperties {
126        is_input: Some(as_input),
127        manufacturer: Some("Fuchsia".to_string()),
128        ..Default::default()
129    };
130
131    #[allow(clippy::collection_is_never_read)]
132    let mut _rb_task = None;
133    while let Some(req) = requests.next().await {
134        if let Err(e) = req {
135            warn!("Error processing DAI request stream: {:?}", e);
136            break;
137        }
138        match req.unwrap() {
139            DaiRequest::GetProperties { responder } => {
140                log_error!(responder.send(&properties), "properties response");
141            }
142            DaiRequest::GetDaiFormats { responder } => {
143                log_error!(responder.send(Ok(from_ref(&dai_formats))), "formats response");
144            }
145            DaiRequest::GetRingBufferFormats { responder } => {
146                log_error!(responder.send(Ok(from_ref(&pcm_formats))), "pcm response");
147            }
148            DaiRequest::CreateRingBuffer {
149                dai_format, ring_buffer_format, ring_buffer, ..
150            } => {
151                let shutdown_bad_args =
152                    |server: fidl::endpoints::ServerEnd<RingBufferMarker>, err: anyhow::Error| {
153                        warn!("CreateRingBuffer: {:?}", err);
154                        if let Err(e) = server.close_with_epitaph(zx::Status::INVALID_ARGS) {
155                            warn!("Couldn't send ring buffer epitaph: {:?}", e);
156                        }
157                    };
158                if let Err(e) = ensure_dai_format_is_supported(from_ref(&dai_formats), &dai_format)
159                {
160                    shutdown_bad_args(ring_buffer, e);
161                    continue;
162                }
163                let pcm_format = match ring_buffer_format.pcm_format {
164                    Some(f) => f,
165                    None => {
166                        shutdown_bad_args(ring_buffer, format_err!("Only PCM format supported"));
167                        continue;
168                    }
169                };
170                if let Err(e) = ensure_pcm_format_is_supported(from_ref(&pcm_formats), &pcm_format)
171                {
172                    shutdown_bad_args(ring_buffer, e);
173                    continue;
174                }
175                handle.set_configured(dai_format, pcm_format);
176                let requests = ring_buffer.into_stream();
177                _rb_task = Some(fasync::Task::spawn(handle_ring_buffer(requests, handle.clone())));
178            }
179            x => unimplemented!("DAI request not implemented: {:?}", x),
180        };
181    }
182}
183
184/// Represents a mock DAI device that processes `Dai` requests from the provided `requests`
185/// stream.
186/// Returns a Future representing the processing task and a `TestHandle` which can be used
187/// to validate behavior.
188fn mock_dai_device(
189    as_input: bool,
190    requests: DaiRequestStream,
191) -> (impl Future<Output = ()>, TestHandle) {
192    let supported_dai_formats = DaiSupportedFormats {
193        number_of_channels: vec![1],
194        sample_formats: vec![DaiSampleFormat::PcmSigned],
195        frame_formats: vec![DaiFrameFormat::FrameFormatStandard(DaiFrameFormatStandard::Tdm1)],
196        frame_rates: vec![8000, 16000, 32000, 48000, 96000],
197        bits_per_slot: vec![16],
198        bits_per_sample: vec![16],
199    };
200
201    let number_of_channels = 1usize;
202    let attributes = vec![ChannelAttributes::default(); number_of_channels];
203    let channel_set = ChannelSet { attributes: Some(attributes), ..Default::default() };
204    let supported_pcm_formats = SupportedFormats {
205        pcm_supported_formats: Some(PcmSupportedFormats {
206            channel_sets: Some(vec![channel_set]),
207            sample_formats: Some(vec![SampleFormat::PcmSigned]),
208            bytes_per_sample: Some(vec![2]),
209            valid_bits_per_sample: Some(vec![16]),
210            frame_rates: Some(vec![8000, 16000, 32000, 48000, 96000]),
211            ..Default::default()
212        }),
213        ..Default::default()
214    };
215
216    let handle = TestHandle::new();
217
218    let handler_fut = test_handle_dai_requests(
219        supported_dai_formats,
220        supported_pcm_formats,
221        as_input,
222        requests,
223        handle.clone(),
224    );
225    (handler_fut, handle)
226}
227
228/// Builds and returns a DigitalAudioInterface for testing scenarios. Returns the
229/// `TestHandle` associated with this device which can be used to validate behavior.
230pub fn test_digital_audio_interface(as_input: bool) -> (DigitalAudioInterface, TestHandle) {
231    let (proxy, requests) = fidl::endpoints::create_proxy_and_stream::<DaiMarker>();
232
233    let (handler, handle) = mock_dai_device(as_input, requests);
234    fasync::Task::spawn(handler).detach();
235
236    (DigitalAudioInterface::from_proxy(proxy), handle)
237}
238
239async fn handle_dai_connect_requests(as_input: bool, mut stream: DaiConnectorRequestStream) {
240    while let Some(request) = stream.next().await {
241        if let Ok(DaiConnectorRequest::Connect { dai_protocol, .. }) = request {
242            let (handler, _test_handle) = mock_dai_device(as_input, dai_protocol.into_stream());
243            fasync::Task::spawn(handler).detach();
244        }
245    }
246}
247
248/// Builds and returns a VFS with a mock input and output DAI device.
249pub fn mock_dai_dev_with_io_devices(input: String, output: String) -> Arc<dyn Directory> {
250    pseudo_directory! {
251        "class" => pseudo_directory! {
252            "dai" => pseudo_directory! {
253                &input => service::host(
254                    move |stream: DaiConnectorRequestStream| handle_dai_connect_requests(true,
255                                                                                         stream)
256                ),
257                &output => service::host(
258                    move |stream: DaiConnectorRequestStream| handle_dai_connect_requests(false,
259                                                                                         stream)
260                ),
261            }
262        }
263    }
264}