display_mocks/
lib.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
5#![deny(missing_docs)]
6
7//! Unit test utilities for clients of the `fuchsia.hardware.display` FIDL API.
8
9use display_types::INVALID_DISP_ID;
10use fidl::endpoints::{ClientEnd, ServerEnd};
11use fidl_fuchsia_hardware_display::{
12    self as display, CoordinatorListenerMarker, CoordinatorListenerProxy, CoordinatorMarker,
13    CoordinatorRequestStream, VsyncAckCookie,
14};
15use fidl_fuchsia_hardware_display_types as display_types;
16use itertools::Itertools;
17use std::collections::HashMap;
18use thiserror::Error;
19
20/// Errors that can be returned by `MockCoordinator`.
21#[derive(Error, Debug)]
22pub enum MockCoordinatorError {
23    /// Duplicate IDs were given to a function that expects objects with unique IDs. For example,
24    /// MockCoordinator has been assigned multiple displays with clashing IDs.
25    #[error("duplicate IDs provided")]
26    DuplicateIds,
27
28    /// Error from the underlying FIDL bindings or channel transport.
29    #[error("FIDL error: {0}")]
30    FidlError(#[from] fidl::Error),
31}
32
33/// MockCoordinatorError Result type alias.
34pub type Result<T> = std::result::Result<T, MockCoordinatorError>;
35
36/// `MockCoordinator` implements the server-end of the `fuchsia.hardware.display.Coordinator`
37/// protocol. It minimally reproduces the display coordinator driver state machine to respond to
38/// FIDL messages in a predictable and configurable manner.
39pub struct MockCoordinator {
40    #[allow(unused)]
41    coordinator_stream: CoordinatorRequestStream,
42    listener_proxy: CoordinatorListenerProxy,
43
44    displays: HashMap<DisplayId, display::Info>,
45}
46
47// TODO(https://fxbug.dev/42080268): Instead of defining a separate DisplayId, we should
48// use the same DisplayId from display_utils instead.
49#[derive(Eq, Hash, Ord, PartialOrd, PartialEq)]
50struct DisplayId(u64);
51
52impl MockCoordinator {
53    /// Bind a new `MockCoordinator` to the server end of a FIDL channel.
54    pub fn new(
55        coordinator_server_end: ServerEnd<CoordinatorMarker>,
56        listener_client_end: ClientEnd<CoordinatorListenerMarker>,
57    ) -> Result<MockCoordinator> {
58        let coordinator_stream = coordinator_server_end.into_stream();
59        let listener_proxy = listener_client_end.into_proxy();
60        Ok(MockCoordinator { coordinator_stream, listener_proxy, displays: HashMap::new() })
61    }
62
63    /// Replace the list of available display devices with the given collection and send a
64    /// `fuchsia.hardware.display.Coordinator.OnDisplaysChanged` event reflecting the changes.
65    ///
66    /// All the new displays will be reported as added while previously present displays will
67    /// be reported as removed, regardless of their content.
68    ///
69    /// Returns an error if `displays` contains entries with repeated display IDs.
70    pub fn assign_displays(&mut self, displays: Vec<display::Info>) -> Result<()> {
71        let mut map = HashMap::new();
72        if !displays.into_iter().all(|info| map.insert(DisplayId(info.id.value), info).is_none()) {
73            return Err(MockCoordinatorError::DuplicateIds);
74        }
75
76        let added: Vec<_> = map
77            .iter()
78            .sorted_by(|a, b| Ord::cmp(&a.0, &b.0))
79            .map(|(_, info)| info.clone())
80            .collect();
81        let removed: Vec<display_types::DisplayId> =
82            self.displays.iter().map(|(_, info)| info.id).collect();
83        self.displays = map;
84        self.listener_proxy.on_displays_changed(&added, &removed)?;
85        Ok(())
86    }
87
88    /// Sends a single OnVsync event to the client. The vsync event will appear to be sent from the
89    /// given `display_id` even if a corresponding fake display has not been assigned by a call to
90    /// `assign_displays`.
91    // TODO(https://fxbug.dev/42080268): Currently we cannot use display_utils::DisplayId
92    // here due to circular dependency. Instead of passing a raw u64 value, we
93    // should use a generic and strong-typed DisplayId.
94    pub fn emit_vsync_event(
95        &self,
96        display_id_value: u64,
97        stamp: display::ConfigStamp,
98    ) -> Result<()> {
99        self.listener_proxy
100            .on_vsync(
101                &display_types::DisplayId { value: display_id_value },
102                zx::MonotonicInstant::get().into_nanos(),
103                &stamp,
104                &VsyncAckCookie { value: INVALID_DISP_ID },
105            )
106            .map_err(MockCoordinatorError::from)
107    }
108}
109
110/// Create a Zircon channel and return both endpoints with the server end bound to a
111/// `MockCoordinator`.
112///
113/// NOTE: This function instantiates FIDL bindings and thus requires a fuchsia-async executor to
114/// have been created beforehand.
115pub fn create_proxy_and_mock(
116) -> Result<(display::CoordinatorProxy, display::CoordinatorListenerRequestStream, MockCoordinator)>
117{
118    let (coordinator_proxy, coordinator_server) =
119        fidl::endpoints::create_proxy::<CoordinatorMarker>();
120    let (listener_client, listener_requests) =
121        fidl::endpoints::create_request_stream::<CoordinatorListenerMarker>();
122    Ok((
123        coordinator_proxy,
124        listener_requests,
125        MockCoordinator::new(coordinator_server, listener_client)?,
126    ))
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use anyhow::{Context, Result};
133    use fidl_fuchsia_hardware_display as display;
134    use futures::{future, TryStreamExt};
135
136    async fn wait_for_displays_changed_event(
137        listener_requests: &mut display::CoordinatorListenerRequestStream,
138    ) -> Result<(Vec<display::Info>, Vec<display_types::DisplayId>)> {
139        let mut stream = listener_requests.try_filter_map(|event| match event {
140            display::CoordinatorListenerRequest::OnDisplaysChanged {
141                added,
142                removed,
143                control_handle: _,
144            } => future::ok(Some((added, removed))),
145            _ => future::ok(None),
146        });
147        stream.try_next().await?.context("failed to listen to coordinator listener requests")
148    }
149
150    #[fuchsia::test]
151    async fn assign_displays_fails_with_duplicate_display_ids() {
152        let displays = vec![
153            display::Info {
154                id: display_types::DisplayId { value: 1 },
155                modes: Vec::new(),
156                pixel_format: Vec::new(),
157                manufacturer_name: "Foo".to_string(),
158                monitor_name: "what".to_string(),
159                monitor_serial: "".to_string(),
160                horizontal_size_mm: 0,
161                vertical_size_mm: 0,
162                using_fallback_size: false,
163            },
164            display::Info {
165                id: display_types::DisplayId { value: 1 },
166                modes: Vec::new(),
167                pixel_format: Vec::new(),
168                manufacturer_name: "Bar".to_string(),
169                monitor_name: "who".to_string(),
170                monitor_serial: "".to_string(),
171                horizontal_size_mm: 0,
172                vertical_size_mm: 0,
173                using_fallback_size: false,
174            },
175        ];
176
177        let (_proxy, _listener_requests, mut mock) =
178            create_proxy_and_mock().expect("failed to create MockCoordinator");
179        let result = mock.assign_displays(displays);
180        assert!(result.is_err());
181    }
182
183    #[fuchsia::test]
184    async fn assign_displays_displays_added() -> Result<()> {
185        let displays = vec![
186            display::Info {
187                id: display_types::DisplayId { value: 1 },
188                modes: Vec::new(),
189                pixel_format: Vec::new(),
190                manufacturer_name: "Foo".to_string(),
191                monitor_name: "what".to_string(),
192                monitor_serial: "".to_string(),
193                horizontal_size_mm: 0,
194                vertical_size_mm: 0,
195                using_fallback_size: false,
196            },
197            display::Info {
198                id: display_types::DisplayId { value: 2 },
199                modes: Vec::new(),
200                pixel_format: Vec::new(),
201                manufacturer_name: "Bar".to_string(),
202                monitor_name: "who".to_string(),
203                monitor_serial: "".to_string(),
204                horizontal_size_mm: 0,
205                vertical_size_mm: 0,
206                using_fallback_size: false,
207            },
208        ];
209
210        let (_proxy, mut listener_requests, mut mock) =
211            create_proxy_and_mock().expect("failed to create MockCoordinator");
212        mock.assign_displays(displays.clone())?;
213
214        let (added, removed) = wait_for_displays_changed_event(&mut listener_requests).await?;
215        assert_eq!(added, displays);
216        assert_eq!(removed, vec![]);
217
218        Ok(())
219    }
220
221    #[fuchsia::test]
222    async fn assign_displays_displays_removed() -> Result<()> {
223        let displays = vec![display::Info {
224            id: display_types::DisplayId { value: 1 },
225            modes: Vec::new(),
226            pixel_format: Vec::new(),
227            manufacturer_name: "Foo".to_string(),
228            monitor_name: "what".to_string(),
229            monitor_serial: "".to_string(),
230            horizontal_size_mm: 0,
231            vertical_size_mm: 0,
232            using_fallback_size: false,
233        }];
234
235        let (_proxy, mut listener_requests, mut mock) =
236            create_proxy_and_mock().expect("failed to create MockCoordinator");
237        mock.assign_displays(displays)?;
238
239        let _ = wait_for_displays_changed_event(&mut listener_requests).await?;
240
241        // Remove all displays.
242        mock.assign_displays(vec![])?;
243        let (added, removed) = wait_for_displays_changed_event(&mut listener_requests).await?;
244        assert_eq!(added, vec![]);
245        assert_eq!(removed, vec![display_types::DisplayId { value: 1 }]);
246
247        Ok(())
248    }
249}