blob: 0bbbf827da5e2199d8e7c7153018e88f056a068d [file] [log] [blame]
// 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()
}