inspect_stubs/
stubs.rs

1// Copyright 2023 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//! Stubs for tracking unimplemented code paths.
6//!
7//! This crate provides macros and utilities to track stubbed implementations
8//! and surface them in Inspect for diagnostics.
9
10use fuchsia_inspect::Inspector;
11use fuchsia_sync::Mutex;
12use futures::future::BoxFuture;
13use std::collections::HashMap;
14use std::num::NonZeroU64;
15use std::panic::Location;
16use std::sync::LazyLock;
17
18static STUB_COUNTS: LazyLock<Mutex<HashMap<Invocation, Counts>>> =
19    LazyLock::new(|| Mutex::new(HashMap::new()));
20
21/// Tracks a stubbed implementation.
22///
23/// This macro records that a stub was encountered so that it may be surfaced in inspect.
24/// The first time a particular stub is encountered, a log message will be emitted.
25///
26/// Example:
27/// ```
28/// track_stub!(TODO("https://fxbug.dev/12345"), "my component is not implemented");
29/// ```
30#[macro_export]
31macro_rules! track_stub {
32    (TODO($bug_url:literal), $message:expr, $flags:expr $(,)?) => {{
33        $crate::__track_stub_inner(
34            $crate::bug_ref!($bug_url),
35            $message,
36            Some($flags.into()),
37            std::panic::Location::caller(),
38        );
39    }};
40    (TODO($bug_url:literal), $message:expr $(,)?) => {{
41        $crate::__track_stub_inner(
42            $crate::bug_ref!($bug_url),
43            $message,
44            None,
45            std::panic::Location::caller(),
46        );
47    }};
48}
49
50/// Tracks a stubbed implementation with a specified log level.
51///
52/// This macro records that a stub was encountered so that it may be surfaced in inspect.
53/// The first time a particular stub is encountered, a log message will be emitted at the
54/// specified level.
55///
56/// Example:
57/// ```
58/// track_stub_log!(log::Level::Warn, TODO("https://fxbug.dev/12345"), "my component is not implemented");
59/// ```
60#[macro_export]
61macro_rules! track_stub_log {
62    ($level:expr, TODO($bug_url:literal), $message:expr, $flags:expr $(,)?) => {{
63        $crate::__track_stub_inner_with_level(
64            $level,
65            $crate::bug_ref!($bug_url),
66            $message,
67            Some($flags.into()),
68            std::panic::Location::caller(),
69        );
70    }};
71    ($level:expr, TODO($bug_url:literal), $message:expr $(,)?) => {{
72        $crate::__track_stub_inner_with_level(
73            $level,
74            $crate::bug_ref!($bug_url),
75            $message,
76            None,
77            std::panic::Location::caller(),
78        );
79    }};
80}
81
82#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
83struct Invocation {
84    location: &'static Location<'static>,
85    message: &'static str,
86    bug: BugRef,
87}
88
89#[derive(Default)]
90struct Counts {
91    by_flags: HashMap<Option<u64>, u64>,
92}
93
94#[doc(hidden)]
95#[inline]
96pub fn __track_stub_inner(
97    bug: BugRef,
98    message: &'static str,
99    flags: Option<u64>,
100    location: &'static Location<'static>,
101) -> u64 {
102    __track_stub_inner_with_level(log::Level::Debug, bug, message, flags, location)
103}
104
105#[doc(hidden)]
106#[inline]
107pub fn __track_stub_inner_with_level(
108    level: log::Level,
109    bug: BugRef,
110    message: &'static str,
111    flags: Option<u64>,
112    location: &'static Location<'static>,
113) -> u64 {
114    let mut counts = STUB_COUNTS.lock();
115    let message_counts = counts.entry(Invocation { location, message, bug }).or_default();
116    let context_count = message_counts.by_flags.entry(flags).or_default();
117
118    // Log the first time we see a particular file/message/context tuple but don't risk spamming.
119    if *context_count == 0 {
120        match flags {
121            Some(flags) => {
122                log::log!(level, tag = "track_stub", location:%; "{bug} {message}: 0x{flags:x}");
123            }
124            None => {
125                log::log!(level, tag = "track_stub", location:%; "{bug} {message}");
126            }
127        }
128    }
129
130    *context_count += 1;
131    *context_count
132}
133
134/// Returns a future that resolves to an `Inspector` containing stub information.
135///
136/// This function can be used to create a lazy node in inspect that exposes the locations
137/// where stubs have been tracked.
138pub fn track_stub_lazy_node_callback() -> BoxFuture<'static, Result<Inspector, anyhow::Error>> {
139    Box::pin(async {
140        let inspector = Inspector::default();
141        for (Invocation { location, message, bug }, context_counts) in STUB_COUNTS.lock().iter() {
142            inspector.root().atomic_update(|root| {
143                root.record_child(*message, |message_node| {
144                    message_node.record_string("file", location.file());
145                    message_node.record_uint("line", location.line().into());
146                    message_node.record_string("bug", bug.to_string());
147
148                    // Make a copy of the map so we can mutate it while recording values.
149                    let mut context_counts = context_counts.by_flags.clone();
150
151                    if let Some(no_context_count) = context_counts.remove(&None) {
152                        // If the track_stub callsite doesn't provide any context,
153                        // record the count as a property on the node without an intermediate.
154                        message_node.record_uint("count", no_context_count);
155                    }
156
157                    if !context_counts.is_empty() {
158                        message_node.record_child("counts", |counts_node| {
159                            for (context, count) in context_counts {
160                                if let Some(c) = context {
161                                    counts_node.record_uint(format!("0x{c:x}"), count);
162                                }
163                            }
164                        });
165                    }
166                });
167            });
168        }
169        Ok(inspector)
170    })
171}
172
173/// Creates a `BugRef` from a URL literal.
174///
175/// This macro will cause a compilation error if the provided literal is not a valid Fuchsia bug URL.
176#[macro_export]
177macro_rules! bug_ref {
178    ($bug_url:literal) => {{
179        // Assign the value to a const to ensure we get compile-time validation of the URL.
180        const __REF: $crate::BugRef = match $crate::BugRef::from_str($bug_url) {
181            Some(b) => b,
182            None => panic!("bug references must have the form `https://fxbug.dev/123456789`"),
183        };
184        __REF
185    }};
186}
187
188/// Represents a reference to a Fuchsia bug.
189///
190/// This struct is used to ensure that stubs are tracked against a valid bug.
191#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
192pub struct BugRef {
193    number: u64,
194}
195
196impl BugRef {
197    #[doc(hidden)] // use bug_ref!() instead
198    pub const fn from_str(url: &'static str) -> Option<Self> {
199        let expected_prefix = b"https://fxbug.dev/";
200        let url = str::as_bytes(url);
201
202        if url.len() < expected_prefix.len() {
203            return None;
204        }
205        let (scheme_and_domain, number_str) = url.split_at(expected_prefix.len());
206        if number_str.is_empty() {
207            return None;
208        }
209
210        // The standard library doesn't seem to have a const string or slice equality function.
211        {
212            let mut i = 0;
213            while i < scheme_and_domain.len() {
214                if scheme_and_domain[i] != expected_prefix[i] {
215                    return None;
216                }
217                i += 1;
218            }
219        }
220
221        // The standard library doesn't seem to have a const base 10 string parser.
222        let mut number = 0;
223        {
224            let mut i = 0;
225            while i < number_str.len() {
226                number *= 10;
227                number += match number_str[i] {
228                    b'0' => 0,
229                    b'1' => 1,
230                    b'2' => 2,
231                    b'3' => 3,
232                    b'4' => 4,
233                    b'5' => 5,
234                    b'6' => 6,
235                    b'7' => 7,
236                    b'8' => 8,
237                    b'9' => 9,
238                    _ => return None,
239                };
240                i += 1;
241            }
242        }
243
244        assert!(number != 0, "Zero does not a valid bug number make.");
245
246        Some(Self { number })
247    }
248}
249
250impl From<NonZeroU64> for BugRef {
251    /// Converts a `NonZeroU64` into a `BugRef`.
252    fn from(value: NonZeroU64) -> Self {
253        Self { number: value.get() }
254    }
255}
256
257impl Into<NonZeroU64> for BugRef {
258    /// Converts a `BugRef` into a `NonZeroU64`.
259    fn into(self) -> NonZeroU64 {
260        NonZeroU64::new(self.number).unwrap()
261    }
262}
263
264impl std::fmt::Display for BugRef {
265    /// Formats the `BugRef` as a URL string.
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        write!(f, "https://fxbug.dev/{}", self.number)
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn valid_url_parses() {
277        assert_eq!(BugRef::from_str("https://fxbug.dev/1234567890").unwrap().number, 1234567890);
278    }
279
280    #[test]
281    fn missing_prefix_fails() {
282        assert_eq!(BugRef::from_str("1234567890"), None);
283    }
284
285    #[test]
286    fn missing_number_fails() {
287        assert_eq!(BugRef::from_str("https://fxbug.dev/"), None);
288    }
289
290    #[test]
291    fn short_prefixes_fail() {
292        assert_eq!(BugRef::from_str("b/1234567890"), None);
293        assert_eq!(BugRef::from_str("fxb/1234567890"), None);
294        assert_eq!(BugRef::from_str("fxbug.dev/1234567890"), None);
295    }
296}