Skip to main content

fuchsia_fuzzctl_test/
test.rs

1// Copyright 2022 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 crate::controller::FakeController;
6use crate::writer::BufferSink;
7use anyhow::{Context as _, Result, anyhow, bail};
8use flex_fuchsia_fuzzer as fuzz;
9use fuchsia_fuzzctl::{Writer, create_artifact_dir, create_corpus_dir};
10use serde_json::json;
11use std::cell::RefCell;
12use std::fmt::{Debug, Display};
13use std::path::{Path, PathBuf};
14use std::rc::Rc;
15use std::{env, fs};
16use tempfile::{TempDir, tempdir};
17
18#[cfg(feature = "fdomain")]
19use std::sync::Arc;
20
21pub const TEST_URL: &str = "fuchsia-pkg://fuchsia.com/fake#meta/foo-fuzzer.cm";
22
23/// General purpose test context for the `ffx fuzz` plugin unit tests.
24///
25/// This object groups several commonly used routines used to capture data produced as a part of
26/// unit tests, such as:
27///
28///   * It can create temporary files and directories, and ensure they persist for duration of
29///     the test.
30///   * It maintains a list of expected outputs.
31///   * It shares a list of actual outputs with a `BufferSink` and can verify that they match
32///     its expectations.
33///   * It can produce `Writer`s backed by its associated `BufferSink`.
34///
35#[derive(Clone, Debug)]
36pub struct Test {
37    // This temporary directory is used indirectly via `root_dir`, but must be kept in scope for
38    // the duration of the test to avoid it being deleted prematurely.
39    _tmp_dir: Rc<Option<TempDir>>,
40    root_dir: PathBuf,
41    url: Rc<RefCell<Option<String>>>,
42    controller: FakeController,
43    requests: Rc<RefCell<Vec<String>>>,
44    expected: Vec<Expectation>,
45    actual: Rc<RefCell<Vec<u8>>>,
46    writer: Writer<BufferSink>,
47    #[cfg(feature = "fdomain")]
48    client: Arc<flex_client::Client>,
49}
50
51// Output can be tested for exact matches or substring matches.
52#[derive(Clone, Debug)]
53pub enum Expectation {
54    Equals(String),
55    Contains(String),
56}
57
58impl Test {
59    /// Creates a new `Test`.
60    ///
61    /// When running tests, users may optionally set the FFX_FUZZ_ECHO_TEST_OUTPUT environment
62    /// variable, which will cause this object to use an existing directory rather than create a
63    /// temporary one.
64    pub fn try_new() -> Result<Self> {
65        let (tmp_dir, root_dir) = match env::var("FFX_FUZZ_TEST_ROOT_DIR") {
66            Ok(root_dir) => (None, PathBuf::from(root_dir)),
67            Err(_) => {
68                let tmp_dir = tempdir().context("failed to create test directory")?;
69                let root_dir = PathBuf::from(tmp_dir.path());
70                (Some(tmp_dir), root_dir)
71            }
72        };
73        let actual = Rc::new(RefCell::new(Vec::new()));
74        let mut writer = Writer::new(BufferSink::new(Rc::clone(&actual)));
75        writer.use_colors(false);
76
77        #[cfg(feature = "fdomain")]
78        let client = fdomain_local::local_client_empty();
79
80        Ok(Self {
81            _tmp_dir: Rc::new(tmp_dir),
82            root_dir,
83            url: Rc::new(RefCell::new(None)),
84            controller: FakeController::new(),
85            requests: Rc::new(RefCell::new(Vec::new())),
86            expected: Vec::new(),
87            actual,
88            writer,
89            #[cfg(feature = "fdomain")]
90            client,
91        })
92    }
93
94    /// Returns a "domain" provider for this test.
95    ///
96    /// This returns an object that can be used to create sockets, channels, etc. that are
97    /// compatible with the transport being tested.
98    pub fn domain(&self) -> flex_client::ClientArg {
99        #[cfg(feature = "fdomain")]
100        return Arc::clone(&self.client);
101        #[cfg(not(feature = "fdomain"))]
102        return flex_client::fidl::ZirconClient;
103    }
104
105    /// Creates a proxy and server end for a protocol.
106    pub fn create_proxy<P: flex_client::fidl::ProtocolMarker>(
107        &self,
108    ) -> (P::Proxy, flex_client::fidl::ServerEnd<P>) {
109        #[cfg(feature = "fdomain")]
110        return self.client.create_proxy::<P>();
111        #[cfg(not(feature = "fdomain"))]
112        return flex_client::fidl::create_proxy::<P>();
113    }
114
115    /// Returns the writable temporary directory for this test.
116    pub fn root_dir(&self) -> &Path {
117        self.root_dir.as_path()
118    }
119
120    /// Creates a directory under this object's `root_dir`.
121    ///
122    /// The given `path` may be relative or absolute, but if it is the latter it must be
123    /// prefixed with this object's `root_dir`.
124    ///
125    /// Returns a `PathBuf` on success and an error if the `path` is outside the `root_dir` or
126    /// the filesystem returns an error.
127    ///
128    pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
129        let path = path.as_ref();
130        let mut abspath = PathBuf::from(self.root_dir());
131        if path.is_relative() {
132            abspath.push(path);
133        } else if path.starts_with(self.root_dir()) {
134            abspath = PathBuf::from(path);
135        } else {
136            bail!(
137                "cannot create test directories outside the test root: {}",
138                path.to_string_lossy()
139            );
140        }
141        fs::create_dir_all(&abspath).with_context(|| {
142            format!("failed to create '{}' directory", abspath.to_string_lossy())
143        })?;
144        Ok(abspath)
145    }
146
147    /// Returns the path where the fuzzer will store artifacts.
148    pub fn artifact_dir(&self) -> PathBuf {
149        create_artifact_dir(&self.root_dir).unwrap()
150    }
151
152    /// Returns the path where the fuzzer will store its corpus of the given type.
153    pub fn corpus_dir(&self, corpus_type: fuzz::Corpus) -> PathBuf {
154        create_corpus_dir(&self.root_dir, corpus_type).unwrap()
155    }
156
157    /// Creates a fake ".fx-build-dir" file for testing.
158    ///
159    /// The ".fx-build-dir" file will be created under this object's `root_dir`, and will
160    /// contain the `relative_path` to the build directory. Except for some `util::tests`, unit
161    /// tests should prefer `create_tests_json`.
162    ///
163    /// Returns an error if any filesystem operations fail.
164    ///
165    pub fn write_fx_build_dir<P: AsRef<Path>>(&self, build_dir: P) -> Result<()> {
166        let build_dir = build_dir.as_ref();
167        let mut fx_build_dir = PathBuf::from(self.root_dir());
168        fx_build_dir.push(".fx-build-dir");
169        let build_dir = build_dir.to_string_lossy().to_string();
170        fs::write(&fx_build_dir, &build_dir)
171            .with_context(|| format!("failed to write to '{}'", fx_build_dir.to_string_lossy()))?;
172        Ok(())
173    }
174
175    /// Creates a fake "tests.json" file for testing.
176    ///
177    /// The "tests.json" will be created under the `relative_path` from this object's `root_dir`
178    /// and will contain the given `contents`. Except for some `util::tests`, unit tests should
179    /// prefer `create_tests_json`.
180    ///
181    /// Returns an error if any filesystem operations fail.
182    ///
183    pub fn write_tests_json<P: AsRef<Path>, S: AsRef<str>>(
184        &self,
185        build_dir: P,
186        contents: S,
187    ) -> Result<PathBuf> {
188        let build_dir = build_dir.as_ref();
189        let mut tests_json = PathBuf::from(build_dir);
190        tests_json.push("tests.json");
191        fs::write(&tests_json, contents.as_ref())
192            .with_context(|| format!("failed to write to '{}'", tests_json.to_string_lossy()))?;
193        Ok(tests_json)
194    }
195
196    /// Creates a fake "tests.json" file for testing.
197    ///
198    /// The "tests.json" will include an array of valid JSON objects for the given `urls`.
199    ///
200    /// Returns an error if any filesystem operations fail.
201    ///
202    pub fn create_tests_json<D: Display>(&self, urls: impl Iterator<Item = D>) -> Result<PathBuf> {
203        let build_dir = self
204            .create_dir("out/default")
205            .context("failed to create build directory for 'tests.json'")?;
206        self.write_fx_build_dir(&build_dir).with_context(|| {
207            format!("failed to set build directory to '{}'", build_dir.to_string_lossy())
208        })?;
209
210        let json_data: Vec<_> = urls
211            .map(|url| {
212                json!({
213                    "test": {
214                        "build_rule": "fuchsia_fuzzer_package",
215                        "package_url": url.to_string()
216                    }
217                })
218            })
219            .collect();
220        let json_data = json!(json_data);
221        self.write_tests_json(&build_dir, json_data.to_string()).with_context(|| {
222            format!("failed to create '{}/tests.json'", build_dir.to_string_lossy())
223        })
224    }
225
226    /// Creates several temporary `files` from the given iterator for testing.
227    ///
228    /// Each file's contents will simpl be its name, which is taken from the given `files`.
229    ///
230    /// Returns an error if writing to the filesystem fails.
231    ///
232    pub fn create_test_files<P: AsRef<Path>, D: Display>(
233        &self,
234        test_dir: P,
235        files: impl Iterator<Item = D>,
236    ) -> Result<()> {
237        let test_dir = self.create_dir(test_dir)?;
238        for filename in files {
239            let filename = filename.to_string();
240            fs::write(test_dir.join(&filename), filename.as_bytes())
241                .with_context(|| format!("failed to write to '{}'", filename))?;
242        }
243        Ok(())
244    }
245
246    /// Clones the `RefCell` holding the URL provided to the fake manager.
247    pub fn url(&self) -> Rc<RefCell<Option<String>>> {
248        self.url.clone()
249    }
250
251    /// Clones the fake fuzzer controller "connected" by the fake manager.
252    pub fn controller(&self) -> FakeController {
253        self.controller.clone()
254    }
255
256    /// Records a FIDL request made to a test fake.
257    pub fn record<S: AsRef<str>>(&mut self, request: S) {
258        let mut requests_mut = self.requests.borrow_mut();
259        requests_mut.push(request.as_ref().to_string());
260    }
261
262    /// Returns the recorded FIDL requests.
263    ///
264    /// As a side-effect, this resets the recorded requests.
265    ///
266    pub fn requests(&mut self) -> Vec<String> {
267        let mut requests_mut = self.requests.borrow_mut();
268        let requests = requests_mut.clone();
269        *requests_mut = Vec::new();
270        requests
271    }
272
273    /// Adds an expectation that an output written to the `BufferSink` will exactly match `msg`.
274    pub fn output_matches<T: AsRef<str> + Display>(&mut self, msg: T) {
275        let msg = msg.as_ref().trim().to_string();
276        if !msg.is_empty() {
277            self.expected.push(Expectation::Equals(msg));
278        }
279    }
280
281    /// Adds an expectation that an output written to the `BufferSink` will contain `msg`.
282    pub fn output_includes<T: AsRef<str> + Display>(&mut self, msg: T) {
283        let msg = msg.as_ref().trim().to_string();
284        if !msg.is_empty() {
285            self.expected.push(Expectation::Contains(msg));
286        }
287    }
288
289    /// Iterates over the expected and actual output and verifies expectations are met.
290    pub fn verify_output(&mut self) -> Result<()> {
291        let actual: Vec<u8> = {
292            let mut actual = self.actual.borrow_mut();
293            actual.drain(..).collect()
294        };
295        let actual = String::from_utf8_lossy(&actual);
296        let mut actual: Vec<String> = actual.split("\n").map(|s| s.trim().to_string()).collect();
297        actual.retain(|s| !s.is_empty());
298        let mut actual = actual.into_iter();
299
300        // `Contains` expectations may be surrounded by extra lines.
301        let mut extra = false;
302        for expectation in self.expected.drain(..) {
303            loop {
304                let line =
305                    actual.next().ok_or_else(|| anyhow!("unmet expectation: {:?}", expectation))?;
306                match &expectation {
307                    Expectation::Equals(msg) if line == *msg => {
308                        extra = false;
309                        break;
310                    }
311                    Expectation::Equals(_msg) if extra => continue,
312                    Expectation::Equals(msg) => {
313                        bail!("mismatch:\n  actual=`{}`\nexpected=`{}`", line, msg)
314                    }
315                    Expectation::Contains(msg) => {
316                        extra = true;
317                        if line.contains(msg) {
318                            break;
319                        }
320                    }
321                }
322            }
323        }
324        if !extra {
325            if let Some(line) = actual.next() {
326                bail!("unexpected line: {}", line);
327            }
328        }
329        Ok(())
330    }
331
332    /// Returns a `Writer` using the `BufferSink` associated with this object.
333    pub fn writer(&self) -> &Writer<BufferSink> {
334        &self.writer
335    }
336}