| // Copyright 2016 `multipart` Crate Developers | |
| // | |
| // Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or | |
| // http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or | |
| // http://opensource.org/licenses/MIT>, at your option. This file may not be | |
| // copied, modified, or distributed except according to those terms. | |
| use mock::{ClientRequest, HttpBuffer}; | |
| use server::{MultipartField, ReadEntry, FieldHeaders}; | |
| use mime::{self, Mime}; | |
| use rand::{self, Rng}; | |
| use std::collections::{HashMap, HashSet}; | |
| use std::collections::hash_map::{Entry, OccupiedEntry}; | |
| use std::fmt; | |
| use std::io::prelude::*; | |
| use std::io::Cursor; | |
| use std::iter::{self, FromIterator}; | |
| const MIN_FIELDS: usize = 1; | |
| const MAX_FIELDS: usize = 3; | |
| const MIN_LEN: usize = 2; | |
| const MAX_LEN: usize = 5; | |
| const MAX_DASHES: usize = 2; | |
| fn collect_rand<C: FromIterator<T>, T, F: FnMut() -> T>(mut gen: F) -> C { | |
| (0 .. rand::thread_rng().gen_range(MIN_FIELDS, MAX_FIELDS)) | |
| .map(|_| gen()).collect() | |
| } | |
| macro_rules! expect_fmt ( | |
| ($val:expr, $($args:tt)*) => ( | |
| match $val { | |
| Some(val) => val, | |
| None => panic!($($args)*), | |
| } | |
| ); | |
| ); | |
| /// The error is provided as the `err` format argument | |
| macro_rules! expect_ok_fmt ( | |
| ($val:expr, $($args:tt)*) => ( | |
| match $val { | |
| Ok(val) => val, | |
| Err(e) => panic!($($args)*, err=e), | |
| } | |
| ); | |
| ); | |
| fn get_field<'m, V>(field: &FieldHeaders, fields: &'m mut HashMap<String, V>) -> Option<OccupiedEntry<'m, String, V>> { | |
| match fields.entry(field.name.to_string()) { | |
| Entry::Occupied(occupied) => Some(occupied), | |
| Entry::Vacant(_) => None, | |
| } | |
| } | |
| #[derive(Debug)] | |
| struct TestFields { | |
| texts: HashMap<String, HashSet<String>>, | |
| files: HashMap<String, HashSet<FileEntry>>, | |
| } | |
| impl TestFields { | |
| fn gen() -> Self { | |
| TestFields { | |
| texts: collect_rand(|| (gen_string(), collect_rand(gen_string))), | |
| files: collect_rand(|| (gen_string(), FileEntry::gen_many())), | |
| } | |
| } | |
| fn check_field<M: ReadEntry>(&mut self, mut field: MultipartField<M>) -> M { | |
| // text/plain fields would be considered a file by `TestFields` | |
| if field.headers.content_type.is_none() { | |
| let mut text_entries = expect_fmt!(get_field(&field.headers, &mut self.texts), | |
| "Got text field that wasn't in original dataset: {:?}", | |
| field.headers); | |
| let mut text = String::new(); | |
| expect_ok_fmt!( | |
| field.data.read_to_string(&mut text), | |
| "error failed to read text data to string: {:?}\n{err}", field.headers | |
| ); | |
| assert!( | |
| text_entries.get_mut().remove(&text), | |
| "Got field text data that wasn't in original data set: {:?}\n{:?}\n{:?}", | |
| field.headers, | |
| text, | |
| text_entries.get(), | |
| ); | |
| if text_entries.get().is_empty() { | |
| text_entries.remove_entry(); | |
| } | |
| return field.data.into_inner(); | |
| } | |
| let mut file_entries = expect_fmt!(get_field(&field.headers, &mut self.files), | |
| "Got file field that wasn't in original dataset: {:?}", | |
| field.headers); | |
| let field_name = field.headers.name.clone(); | |
| let (test_entry, inner) = FileEntry::from_field(field); | |
| assert!( | |
| file_entries.get_mut().remove(&test_entry), | |
| "Got field entry that wasn't in original dataset: name: {:?}\n{:?}\nEntries: {:?}", | |
| field_name, | |
| test_entry, | |
| file_entries.get() | |
| ); | |
| if file_entries.get().is_empty() { | |
| file_entries.remove_entry(); | |
| } | |
| return inner; | |
| } | |
| fn assert_is_empty(&self) { | |
| assert!(self.texts.is_empty(), "Text Fields were not exhausted! {:?}", self.texts); | |
| assert!(self.files.is_empty(), "File Fields were not exhausted! {:?}", self.files); | |
| } | |
| } | |
| #[derive(Debug, Hash, PartialEq, Eq)] | |
| struct FileEntry { | |
| content_type: Mime, | |
| filename: Option<String>, | |
| data: PrintHex, | |
| } | |
| impl FileEntry { | |
| fn from_field<M: ReadEntry>(mut field: MultipartField<M>) -> (FileEntry, M) { | |
| let mut data = Vec::new(); | |
| expect_ok_fmt!( | |
| field.data.read_to_end(&mut data), | |
| "Error reading file field: {:?}\n{err}", field.headers | |
| ); | |
| ( | |
| FileEntry { | |
| content_type: field.headers.content_type.unwrap_or(mime!(Application/OctetStream)), | |
| filename: field.headers.filename, | |
| data: PrintHex(data), | |
| }, | |
| field.data.into_inner() | |
| ) | |
| } | |
| fn gen_many() -> HashSet<FileEntry> { | |
| collect_rand(Self::gen) | |
| } | |
| fn gen() -> Self { | |
| let filename = match gen_bool() { | |
| true => Some(gen_string()), | |
| false => None, | |
| }; | |
| let data = PrintHex(match gen_bool() { | |
| true => gen_string().into_bytes(), | |
| false => gen_bytes(), | |
| }); | |
| FileEntry { | |
| content_type: rand_mime(), | |
| filename, | |
| data, | |
| } | |
| } | |
| fn filename(&self) -> Option<&str> { | |
| self.filename.as_ref().map(|s| &**s) | |
| } | |
| } | |
| #[derive(PartialEq, Eq, Hash)] | |
| struct PrintHex(Vec<u8>); | |
| impl fmt::Debug for PrintHex { | |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
| write!(f, "[")?; | |
| let mut written = false; | |
| for byte in &self.0 { | |
| write!(f, "{:X}", byte)?; | |
| if written { | |
| write!(f, ", ")?; | |
| } | |
| written = true; | |
| } | |
| write!(f, "]") | |
| } | |
| } | |
| macro_rules! do_test ( | |
| ($client_test:ident, $server_test:ident) => ( | |
| ::init_log(); | |
| info!("Client Test: {:?} Server Test: {:?}", stringify!($client_test), | |
| stringify!($server_test)); | |
| let mut test_fields = TestFields::gen(); | |
| trace!("Fields for test: {:?}", test_fields); | |
| let buf = $client_test(&test_fields); | |
| trace!( | |
| "\n==Test Buffer Begin==\n{}\n==Test Buffer End==", | |
| String::from_utf8_lossy(&buf.buf) | |
| ); | |
| $server_test(buf, &mut test_fields); | |
| test_fields.assert_is_empty(); | |
| ); | |
| ); | |
| #[test] | |
| fn reg_client_reg_server() { | |
| do_test!(test_client, test_server); | |
| } | |
| #[test] | |
| fn reg_client_entry_server() { | |
| do_test!(test_client, test_server_entry_api); | |
| } | |
| #[test] | |
| fn lazy_client_reg_server() { | |
| do_test!(test_client_lazy, test_server); | |
| } | |
| #[test] | |
| fn lazy_client_entry_server() { | |
| do_test!(test_client_lazy, test_server_entry_api); | |
| } | |
| mod extended { | |
| use super::{test_client, test_server, test_server_entry_api, test_client_lazy, TestFields}; | |
| use std::time::Instant; | |
| const TIME_LIMIT_SECS: u64 = 600; | |
| #[test] | |
| #[ignore] | |
| fn reg_client_reg_server() { | |
| let started = Instant::now(); | |
| while started.elapsed().as_secs() < TIME_LIMIT_SECS { | |
| do_test!(test_client, test_server); | |
| } | |
| } | |
| #[test] | |
| #[ignore] | |
| fn reg_client_entry_server() { | |
| let started = Instant::now(); | |
| while started.elapsed().as_secs() < TIME_LIMIT_SECS { | |
| do_test!(test_client, test_server_entry_api); | |
| } | |
| } | |
| #[test] | |
| #[ignore] | |
| fn lazy_client_reg_server() { | |
| let started = Instant::now(); | |
| while started.elapsed().as_secs() < TIME_LIMIT_SECS { | |
| do_test!(test_client_lazy, test_server); | |
| } | |
| } | |
| #[test] | |
| #[ignore] | |
| fn lazy_client_entry_server() { | |
| let started = Instant::now(); | |
| while started.elapsed().as_secs() < TIME_LIMIT_SECS { | |
| do_test!(test_client_lazy, test_server_entry_api); | |
| } | |
| } | |
| } | |
| fn gen_bool() -> bool { | |
| rand::thread_rng().gen() | |
| } | |
| fn gen_string() -> String { | |
| let mut rng_1 = rand::thread_rng(); | |
| let mut rng_2 = rand::thread_rng(); | |
| let str_len_1 = rng_1.gen_range(MIN_LEN, MAX_LEN + 1); | |
| let str_len_2 = rng_2.gen_range(MIN_LEN, MAX_LEN + 1); | |
| let num_dashes = rng_1.gen_range(0, MAX_DASHES + 1); | |
| rng_1.gen_ascii_chars().take(str_len_1) | |
| .chain(iter::repeat('-').take(num_dashes)) | |
| .chain(rng_2.gen_ascii_chars().take(str_len_2)) | |
| .collect() | |
| } | |
| fn gen_bytes() -> Vec<u8> { | |
| gen_string().into_bytes() | |
| } | |
| fn test_client(test_fields: &TestFields) -> HttpBuffer { | |
| use client::Multipart; | |
| let request = ClientRequest::default(); | |
| let mut test_files = test_fields.files.iter().flat_map( | |
| |(name, files)| files.iter().map(move |file| (name, file)) | |
| ); | |
| let test_texts = test_fields.texts.iter().flat_map( | |
| |(name, texts)| texts.iter().map(move |text| (name, text)) | |
| ); | |
| let mut multipart = Multipart::from_request(request).unwrap(); | |
| // Intersperse file fields amongst text fields | |
| for (name, text) in test_texts { | |
| if let Some((file_name, file)) = test_files.next() { | |
| multipart.write_stream(file_name, &mut &*file.data.0, file.filename(), | |
| Some(file.content_type.clone())).unwrap(); | |
| } | |
| multipart.write_text(name, text).unwrap(); | |
| } | |
| // Write remaining files | |
| for (file_name, file) in test_files { | |
| multipart.write_stream(file_name, &mut &*file.data.0, file.filename(), | |
| Some(file.content_type.clone())).unwrap(); | |
| } | |
| multipart.send().unwrap() | |
| } | |
| fn test_client_lazy(test_fields: &TestFields) -> HttpBuffer { | |
| use client::lazy::Multipart; | |
| let mut multipart = Multipart::new(); | |
| let mut test_files = test_fields.files.iter().flat_map( | |
| |(name, files)| files.iter().map(move |file| (name, file)) | |
| ); | |
| let test_texts = test_fields.texts.iter().flat_map( | |
| |(name, texts)| texts.iter().map(move |text| (name, text)) | |
| ); | |
| for (name, text) in test_texts { | |
| if let Some((file_name, file)) = test_files.next() { | |
| multipart.add_stream(&**file_name, Cursor::new(&file.data.0), file.filename(), | |
| Some(file.content_type.clone())); | |
| } | |
| multipart.add_text(&**name, &**text); | |
| } | |
| for (file_name, file) in test_files { | |
| multipart.add_stream(&**file_name, Cursor::new(&file.data.0), file.filename(), | |
| Some(file.content_type.clone())); | |
| } | |
| let mut prepared = multipart.prepare().unwrap(); | |
| let mut buf = Vec::new(); | |
| let boundary = prepared.boundary().to_owned(); | |
| let content_len = prepared.content_len(); | |
| prepared.read_to_end(&mut buf).unwrap(); | |
| HttpBuffer::with_buf(buf, boundary, content_len) | |
| } | |
| fn test_server(buf: HttpBuffer, fields: &mut TestFields) { | |
| use server::Multipart; | |
| let server_buf = buf.for_server(); | |
| if let Some(content_len) = server_buf.content_len { | |
| assert!(content_len == server_buf.data.len() as u64, "Supplied content_len different from actual"); | |
| } | |
| let mut multipart = Multipart::from_request(server_buf) | |
| .unwrap_or_else(|_| panic!("Buffer should be multipart!")); | |
| while let Some(field) = multipart.read_entry_mut().unwrap_opt() { | |
| fields.check_field(field); | |
| } | |
| } | |
| fn test_server_entry_api(buf: HttpBuffer, fields: &mut TestFields) { | |
| use server::Multipart; | |
| let server_buf = buf.for_server(); | |
| if let Some(content_len) = server_buf.content_len { | |
| assert!(content_len == server_buf.data.len() as u64, "Supplied content_len different from actual"); | |
| } | |
| let mut multipart = Multipart::from_request(server_buf) | |
| .unwrap_or_else(|_| panic!("Buffer should be multipart!")); | |
| let entry = multipart.into_entry().expect_alt("Expected entry, got none", "Error reading entry"); | |
| multipart = fields.check_field(entry); | |
| while let Some(entry) = multipart.into_entry().unwrap_opt() { | |
| multipart = fields.check_field(entry); | |
| } | |
| } | |
| fn rand_mime() -> Mime { | |
| rand::thread_rng().choose(&[ | |
| // TODO: fill this out, preferably with variants that may be hard to parse | |
| // i.e. containing hyphens, mainly | |
| mime!(Application/OctetStream), | |
| mime!(Text/Plain), | |
| mime!(Image/Png), | |
| ]).unwrap().clone() | |
| } |