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.
45use 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};
1718pub const TEST_URL: &str = "fuchsia-pkg://fuchsia.com/fake#meta/foo-fuzzer.cm";
1920/// 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}
4546// Output can be tested for exact matches or substring matches.
47#[derive(Clone, Debug)]
48pub enum Expectation {
49 Equals(String),
50 Contains(String),
51}
5253impl 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.
59pub fn try_new() -> Result<Self> {
60let (tmp_dir, root_dir) = match env::var("FFX_FUZZ_TEST_ROOT_DIR") {
61Ok(root_dir) => (None, PathBuf::from(root_dir)),
62Err(_) => {
63let tmp_dir = tempdir().context("failed to create test directory")?;
64let root_dir = PathBuf::from(tmp_dir.path());
65 (Some(tmp_dir), root_dir)
66 }
67 };
68let actual = Rc::new(RefCell::new(Vec::new()));
69let mut writer = Writer::new(BufferSink::new(Rc::clone(&actual)));
70 writer.use_colors(false);
71Ok(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 }
8283/// Returns the writable temporary directory for this test.
84pub fn root_dir(&self) -> &Path {
85self.root_dir.as_path()
86 }
8788/// 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 ///
96pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
97let path = path.as_ref();
98let mut abspath = PathBuf::from(self.root_dir());
99if path.is_relative() {
100 abspath.push(path);
101 } else if path.starts_with(self.root_dir()) {
102 abspath = PathBuf::from(path);
103 } else {
104bail!(
105"cannot create test directories outside the test root: {}",
106 path.to_string_lossy()
107 );
108 }
109 fs::create_dir_all(&abspath).with_context(|| {
110format!("failed to create '{}' directory", abspath.to_string_lossy())
111 })?;
112Ok(abspath)
113 }
114115/// Returns the path where the fuzzer will store artifacts.
116pub fn artifact_dir(&self) -> PathBuf {
117 create_artifact_dir(&self.root_dir).unwrap()
118 }
119120/// Returns the path where the fuzzer will store its corpus of the given type.
121pub fn corpus_dir(&self, corpus_type: fuzz::Corpus) -> PathBuf {
122 create_corpus_dir(&self.root_dir, corpus_type).unwrap()
123 }
124125/// 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 ///
133pub fn write_fx_build_dir<P: AsRef<Path>>(&self, build_dir: P) -> Result<()> {
134let build_dir = build_dir.as_ref();
135let mut fx_build_dir = PathBuf::from(self.root_dir());
136 fx_build_dir.push(".fx-build-dir");
137let 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()))?;
140Ok(())
141 }
142143/// 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 ///
151pub fn write_tests_json<P: AsRef<Path>, S: AsRef<str>>(
152&self,
153 build_dir: P,
154 contents: S,
155 ) -> Result<PathBuf> {
156let build_dir = build_dir.as_ref();
157let 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()))?;
161Ok(tests_json)
162 }
163164/// 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 ///
170pub fn create_tests_json<D: Display>(&self, urls: impl Iterator<Item = D>) -> Result<PathBuf> {
171let build_dir = self
172.create_dir("out/default")
173 .context("failed to create build directory for 'tests.json'")?;
174self.write_fx_build_dir(&build_dir).with_context(|| {
175format!("failed to set build directory to '{}'", build_dir.to_string_lossy())
176 })?;
177178let json_data: Vec<_> = urls
179 .map(|url| {
180json!({
181"test": {
182"build_rule": "fuchsia_fuzzer_package",
183"package_url": url.to_string()
184 }
185 })
186 })
187 .collect();
188let json_data = json!(json_data);
189self.write_tests_json(&build_dir, json_data.to_string()).with_context(|| {
190format!("failed to create '{}/tests.json'", build_dir.to_string_lossy())
191 })
192 }
193194/// 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 ///
200pub fn create_test_files<P: AsRef<Path>, D: Display>(
201&self,
202 test_dir: P,
203 files: impl Iterator<Item = D>,
204 ) -> Result<()> {
205let test_dir = self.create_dir(test_dir)?;
206for filename in files {
207let filename = filename.to_string();
208 fs::write(test_dir.join(&filename), filename.as_bytes())
209 .with_context(|| format!("failed to write to '{}'", filename))?;
210 }
211Ok(())
212 }
213214/// Clones the `RefCell` holding the URL provided to the fake manager.
215pub fn url(&self) -> Rc<RefCell<Option<String>>> {
216self.url.clone()
217 }
218219/// Clones the fake fuzzer controller "connected" by the fake manager.
220pub fn controller(&self) -> FakeController {
221self.controller.clone()
222 }
223224/// Records a FIDL request made to a test fake.
225pub fn record<S: AsRef<str>>(&mut self, request: S) {
226let mut requests_mut = self.requests.borrow_mut();
227 requests_mut.push(request.as_ref().to_string());
228 }
229230/// Returns the recorded FIDL requests.
231 ///
232 /// As a side-effect, this resets the recorded requests.
233 ///
234pub fn requests(&mut self) -> Vec<String> {
235let mut requests_mut = self.requests.borrow_mut();
236let requests = requests_mut.clone();
237*requests_mut = Vec::new();
238 requests
239 }
240241/// Adds an expectation that an output written to the `BufferSink` will exactly match `msg`.
242pub fn output_matches<T: AsRef<str> + Display>(&mut self, msg: T) {
243let msg = msg.as_ref().trim().to_string();
244if !msg.is_empty() {
245self.expected.push(Expectation::Equals(msg));
246 }
247 }
248249/// Adds an expectation that an output written to the `BufferSink` will contain `msg`.
250pub fn output_includes<T: AsRef<str> + Display>(&mut self, msg: T) {
251let msg = msg.as_ref().trim().to_string();
252if !msg.is_empty() {
253self.expected.push(Expectation::Contains(msg));
254 }
255 }
256257/// Iterates over the expected and actual output and verifies expectations are met.
258pub fn verify_output(&mut self) -> Result<()> {
259let actual: Vec<u8> = {
260let mut actual = self.actual.borrow_mut();
261 actual.drain(..).collect()
262 };
263let actual = String::from_utf8_lossy(&actual);
264let mut actual: Vec<String> = actual.split("\n").map(|s| s.trim().to_string()).collect();
265 actual.retain(|s| !s.is_empty());
266let mut actual = actual.into_iter();
267268// `Contains` expectations may be surrounded by extra lines.
269let mut extra = false;
270for expectation in self.expected.drain(..) {
271loop {
272let line = actual.next().ok_or(anyhow!("unmet expectation: {:?}", expectation))?;
273match &expectation {
274 Expectation::Equals(msg) if line == *msg => {
275 extra = false;
276break;
277 }
278 Expectation::Equals(_msg) if extra => continue,
279 Expectation::Equals(msg) => {
280bail!("mismatch:\n actual=`{}`\nexpected=`{}`", line, msg)
281 }
282 Expectation::Contains(msg) => {
283 extra = true;
284if line.contains(msg) {
285break;
286 }
287 }
288 }
289 }
290 }
291if !extra {
292if let Some(line) = actual.next() {
293bail!("unexpected line: {}", line);
294 }
295 }
296Ok(())
297 }
298299/// Returns a `Writer` using the `BufferSink` associated with this object.
300pub fn writer(&self) -> &Writer<BufferSink> {
301&self.writer
302 }
303}