fuchsia_fuzzctl_test_fdomain/
test.rs1use 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#[derive(Clone, Debug)]
36pub struct Test {
37 _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#[derive(Clone, Debug)]
53pub enum Expectation {
54 Equals(String),
55 Contains(String),
56}
57
58impl Test {
59 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 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 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 pub fn root_dir(&self) -> &Path {
117 self.root_dir.as_path()
118 }
119
120 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 pub fn artifact_dir(&self) -> PathBuf {
149 create_artifact_dir(&self.root_dir).unwrap()
150 }
151
152 pub fn corpus_dir(&self, corpus_type: fuzz::Corpus) -> PathBuf {
154 create_corpus_dir(&self.root_dir, corpus_type).unwrap()
155 }
156
157 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 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 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 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 pub fn url(&self) -> Rc<RefCell<Option<String>>> {
248 self.url.clone()
249 }
250
251 pub fn controller(&self) -> FakeController {
253 self.controller.clone()
254 }
255
256 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 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 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 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 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 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 pub fn writer(&self) -> &Writer<BufferSink> {
334 &self.writer
335 }
336}