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