use camino::Utf8PathBuf;
use chrono::DateTime;
use chrono::Utc;
use http::method::Method;
use hyper::body::to_bytes;
use hyper::client::HttpConnector;
use hyper::Body;
use hyper::Client;
use hyper::Request;
use hyper::Response;
use hyper::StatusCode;
use hyper::Uri;
use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde::Serialize;
use slog::Logger;
use std::convert::TryFrom;
use std::fmt::Debug;
use std::fs;
use std::iter::Iterator;
use std::net::SocketAddr;
use std::path::Path;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use crate::api_description::ApiDescription;
use crate::config::ConfigDropshot;
use crate::error::HttpErrorResponseBody;
use crate::http_util::CONTENT_TYPE_URL_ENCODED;
use crate::logging::ConfigLogging;
use crate::pagination::ResultsPage;
use crate::server::{HttpServer, HttpServerStarter, ServerContext};
enum AllowedValue<'a> {
Any,
OneOf(&'a [&'a str]),
}
struct AllowedHeader<'a> {
name: &'a str,
value: AllowedValue<'a>,
}
impl<'a> AllowedHeader<'a> {
const fn new(name: &'a str) -> Self {
Self { name, value: AllowedValue::Any }
}
}
pub const TEST_HEADER_1: &str = "x-dropshot-test-header-1";
pub const TEST_HEADER_2: &str = "x-dropshot-test-header-2";
const ALLOWED_HEADERS: [AllowedHeader<'static>; 8] = [
AllowedHeader::new("content-length"),
AllowedHeader::new("content-type"),
AllowedHeader::new("date"),
AllowedHeader::new("location"),
AllowedHeader::new("x-request-id"),
AllowedHeader {
name: "transfer-encoding",
value: AllowedValue::OneOf(&["chunked"]),
},
AllowedHeader::new(TEST_HEADER_1),
AllowedHeader::new(TEST_HEADER_2),
];
pub struct ClientTestContext {
pub bind_address: SocketAddr,
pub client: Client<HttpConnector>,
pub client_log: Logger,
}
impl ClientTestContext {
pub fn new(server_addr: SocketAddr, log: Logger) -> ClientTestContext {
ClientTestContext {
bind_address: server_addr,
client: Client::new(),
client_log: log,
}
}
pub fn url(&self, path: &str) -> Uri {
Uri::builder()
.scheme("http")
.authority(format!("{}", self.bind_address).as_str())
.path_and_query(path)
.build()
.expect("attempted to construct invalid URI")
}
pub async fn make_request<RequestBodyType: Serialize + Debug>(
&self,
method: Method,
path: &str,
request_body: Option<RequestBodyType>,
expected_status: StatusCode,
) -> Result<Response<Body>, HttpErrorResponseBody> {
let body: Body = match request_body {
None => Body::empty(),
Some(input) => serde_json::to_string(&input).unwrap().into(),
};
self.make_request_with_body(method, path, body, expected_status).await
}
pub async fn make_request_url_encoded<
RequestBodyType: Serialize + Debug,
>(
&self,
method: Method,
path: &str,
request_body: Option<RequestBodyType>,
expected_status: StatusCode,
) -> Result<Response<Body>, HttpErrorResponseBody> {
let body: Body = match request_body {
None => Body::empty(),
Some(input) => serde_urlencoded::to_string(&input).unwrap().into(),
};
self.make_request_with_body_url_encoded(
method,
path,
body,
expected_status,
)
.await
}
pub async fn make_request_no_body(
&self,
method: Method,
path: &str,
expected_status: StatusCode,
) -> Result<Response<Body>, HttpErrorResponseBody> {
self.make_request_with_body(
method,
path,
Body::empty(),
expected_status,
)
.await
}
pub async fn make_request_error(
&self,
method: Method,
path: &str,
expected_status: StatusCode,
) -> HttpErrorResponseBody {
self.make_request_with_body(method, path, "".into(), expected_status)
.await
.unwrap_err()
}
pub async fn make_request_error_body<T: Serialize + Debug>(
&self,
method: Method,
path: &str,
body: T,
expected_status: StatusCode,
) -> HttpErrorResponseBody {
self.make_request(method, path, Some(body), expected_status)
.await
.unwrap_err()
}
pub async fn make_request_with_body(
&self,
method: Method,
path: &str,
body: Body,
expected_status: StatusCode,
) -> Result<Response<Body>, HttpErrorResponseBody> {
let uri = self.url(path);
let request = Request::builder()
.method(method)
.uri(uri)
.body(body)
.expect("attempted to construct invalid request");
self.make_request_with_request(request, expected_status).await
}
pub async fn make_request_with_body_url_encoded(
&self,
method: Method,
path: &str,
body: Body,
expected_status: StatusCode,
) -> Result<Response<Body>, HttpErrorResponseBody> {
let uri = self.url(path);
let request = Request::builder()
.method(method)
.header(http::header::CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED)
.uri(uri)
.body(body)
.expect("attempted to construct invalid request");
self.make_request_with_request(request, expected_status).await
}
pub async fn make_request_with_request(
&self,
request: Request<Body>,
expected_status: StatusCode,
) -> Result<Response<Body>, HttpErrorResponseBody> {
let time_before = chrono::offset::Utc::now().timestamp();
info!(self.client_log, "client request";
"method" => %request.method(),
"uri" => %request.uri(),
"body" => ?&request.body(),
);
let mut response = self
.client
.request(request)
.await
.expect("failed to make request to server");
let status = response.status();
info!(self.client_log, "client received response"; "status" => ?status);
assert_eq!(expected_status, status);
let headers = response.headers();
for (header_name, header_value) in headers {
let mut okay = false;
for allowed_header in ALLOWED_HEADERS.iter() {
if header_name == allowed_header.name {
match allowed_header.value {
AllowedValue::Any => {
okay = true;
}
AllowedValue::OneOf(allowed_values) => {
let header = header_value
.to_str()
.expect("Cannot turn header value to string");
okay = allowed_values.contains(&header);
}
}
break;
}
}
if !okay {
panic!("header name not in allowed list: \"{}\"", header_name);
}
}
let time_after = chrono::offset::Utc::now().timestamp();
let date_header = headers
.get(http::header::DATE)
.expect("missing Date header")
.to_str()
.expect("non-ASCII characters in Date header");
let time_request = chrono::DateTime::parse_from_rfc2822(date_header)
.expect("unable to parse server's Date header");
assert!(
time_before <= time_after,
"time obviously went backwards during the test"
);
assert!(time_request.timestamp() >= time_before - 1);
assert!(time_request.timestamp() <= time_after + 1);
let request_id_header = headers
.get(crate::HEADER_REQUEST_ID)
.expect("missing request id header")
.to_str()
.expect("non-ASCII characters in request id")
.to_string();
if status == StatusCode::NO_CONTENT {
let body_bytes = to_bytes(response.body_mut())
.await
.expect("error reading body");
assert_eq!(0, body_bytes.len());
}
if !status.is_client_error() && !status.is_server_error() {
return Ok(response);
}
let error_body: HttpErrorResponseBody = read_json(&mut response).await;
info!(self.client_log, "client error"; "error_body" => ?error_body);
assert_eq!(error_body.request_id, request_id_header);
Err(error_body)
}
}
pub struct LogContext {
pub log: Logger,
log_path: Option<Utf8PathBuf>,
}
impl LogContext {
pub fn new(
test_name: &str,
initial_config_logging: &ConfigLogging,
) -> LogContext {
let (log_path, log_config) = match initial_config_logging {
ConfigLogging::File { level, path: dummy_path, if_exists } => {
assert_eq!(
dummy_path, "UNUSED",
"for test suite logging configuration, when mode = \
\"file\" is used, the path MUST be the sentinel string \
\"UNUSED\". It will be replaced with a unique path for \
each test."
);
let new_path = log_file_for_test(test_name);
eprintln!("log file: {}", new_path);
(
Some(new_path.clone()),
ConfigLogging::File {
level: level.clone(),
path: new_path,
if_exists: if_exists.clone(),
},
)
}
other_config => (None, other_config.clone()),
};
let log = log_config.to_logger(test_name).unwrap();
LogContext { log, log_path }
}
pub fn cleanup_successful(self) {
if let Some(ref log_path) = self.log_path {
fs::remove_file(log_path).unwrap();
}
}
}
pub struct TestContext<Context: ServerContext> {
pub client_testctx: ClientTestContext,
pub server: HttpServer<Context>,
pub log: Logger,
log_context: Option<LogContext>,
}
impl<Context: ServerContext> TestContext<Context> {
pub fn new(
api: ApiDescription<Context>,
private: Context,
config_dropshot: &ConfigDropshot,
log_context: Option<LogContext>,
log: Logger,
) -> TestContext<Context> {
assert_eq!(
0,
config_dropshot.bind_address.port(),
"test suite only supports binding on port 0 (any available port)"
);
let server =
HttpServerStarter::new(&config_dropshot, api, private, &log)
.unwrap()
.start();
let server_addr = server.local_addr();
let client_log = log.new(o!("http_client" => "dropshot test suite"));
let client_testctx = ClientTestContext::new(server_addr, client_log);
TestContext { client_testctx, server, log, log_context }
}
pub async fn teardown(self) {
self.server.close().await.expect("server stopped with an error");
if let Some(log_context) = self.log_context {
log_context.cleanup_successful();
}
}
}
pub async fn read_ndjson<T: DeserializeOwned>(
response: &mut Response<Body>,
) -> Vec<T> {
let headers = response.headers();
assert_eq!(
crate::CONTENT_TYPE_NDJSON,
headers.get(http::header::CONTENT_TYPE).expect("missing content-type")
);
let body_bytes =
to_bytes(response.body_mut()).await.expect("error reading body");
let body_string = String::from_utf8(body_bytes.as_ref().into())
.expect("response contained non-UTF-8 bytes");
body_string
.split('\n')
.filter(|line| !line.is_empty())
.map(|line| {
serde_json::from_str(line)
.expect("failed to parse server body as expected type")
})
.collect::<Vec<T>>()
}
pub async fn read_json<T: DeserializeOwned>(
response: &mut Response<Body>,
) -> T {
let headers = response.headers();
assert_eq!(
crate::CONTENT_TYPE_JSON,
headers.get(http::header::CONTENT_TYPE).expect("missing content-type")
);
let body_bytes =
to_bytes(response.body_mut()).await.expect("error reading body");
serde_json::from_slice(body_bytes.as_ref())
.expect("failed to parse server body as expected type")
}
pub async fn read_string(response: &mut Response<Body>) -> String {
let body_bytes =
to_bytes(response.body_mut()).await.expect("error reading body");
String::from_utf8(body_bytes.as_ref().into())
.expect("response contained non-UTF-8 bytes")
}
pub async fn object_get<T: DeserializeOwned>(
client: &ClientTestContext,
object_url: &str,
) -> T {
let mut response = client
.make_request_with_body(
Method::GET,
&object_url,
"".into(),
StatusCode::OK,
)
.await
.unwrap();
read_json::<T>(&mut response).await
}
pub async fn objects_list<T: DeserializeOwned>(
client: &ClientTestContext,
list_url: &str,
) -> Vec<T> {
let mut response = client
.make_request_with_body(
Method::GET,
&list_url,
"".into(),
StatusCode::OK,
)
.await
.unwrap();
read_ndjson::<T>(&mut response).await
}
pub async fn objects_list_page<ItemType>(
client: &ClientTestContext,
list_url: &str,
) -> ResultsPage<ItemType>
where
ItemType: DeserializeOwned,
{
let mut response = client
.make_request_with_body(
Method::GET,
&list_url,
"".into(),
StatusCode::OK,
)
.await
.unwrap();
read_json::<ResultsPage<ItemType>>(&mut response).await
}
pub async fn objects_post<S: Serialize + Debug, T: DeserializeOwned>(
client: &ClientTestContext,
collection_url: &str,
input: S,
) -> T {
let mut response = client
.make_request(
Method::POST,
&collection_url,
Some(input),
StatusCode::CREATED,
)
.await
.unwrap();
read_json::<T>(&mut response).await
}
pub async fn object_put<S: Serialize + Debug, T: DeserializeOwned>(
client: &ClientTestContext,
object_url: &str,
input: S,
status: StatusCode,
) {
client
.make_request(Method::PUT, &object_url, Some(input), status)
.await
.unwrap();
}
pub async fn object_delete(client: &ClientTestContext, object_url: &str) {
client
.make_request_no_body(
Method::DELETE,
&object_url,
StatusCode::NO_CONTENT,
)
.await
.unwrap();
}
pub async fn iter_collection<T: Clone + DeserializeOwned>(
client: &ClientTestContext,
collection_url: &str,
initial_params: &str,
limit: usize,
) -> (Vec<T>, usize) {
let mut page = objects_list_page::<T>(
&client,
&format!("{}?limit={}&{}", collection_url, limit, initial_params),
)
.await;
assert!(page.items.len() <= limit);
let mut rv = page.items.clone();
let mut npages = 1;
while let Some(token) = page.next_page {
page = objects_list_page::<T>(
&client,
&format!("{}?limit={}&page_token={}", collection_url, limit, token),
)
.await;
assert!(page.items.len() <= limit);
rv.extend_from_slice(&page.items);
npages += 1
}
(rv, npages)
}
static TEST_SUITE_LOGGER_ID: AtomicU32 = AtomicU32::new(0);
pub fn log_file_for_test(test_name: &str) -> Utf8PathBuf {
let arg0 = {
let arg0path = Utf8PathBuf::from(std::env::args().next().unwrap());
arg0path.file_name().unwrap().to_owned()
};
let mut pathbuf = Utf8PathBuf::try_from(std::env::temp_dir())
.expect("temp dir is valid UTF-8");
let id = TEST_SUITE_LOGGER_ID.fetch_add(1, Ordering::SeqCst);
let pid = std::process::id();
pathbuf.push(format!("{}-{}.{}.{}.log", arg0, test_name, pid, id));
pathbuf
}
pub fn read_config<T: DeserializeOwned + Debug>(
label: &str,
contents: &str,
) -> Result<T, toml::de::Error> {
let result = toml::from_str(contents);
eprintln!("config \"{}\": {:?}", label, result);
result
}
#[derive(Deserialize)]
pub struct BunyanLogRecord {
pub time: DateTime<Utc>,
pub name: String,
pub hostname: String,
pub pid: u32,
pub msg: String,
pub v: usize,
}
pub fn read_bunyan_log(logpath: &Path) -> Vec<BunyanLogRecord> {
let log_contents = fs::read_to_string(logpath).unwrap();
log_contents
.split('\n')
.filter(|line| !line.is_empty())
.map(|line| serde_json::from_str::<BunyanLogRecord>(line).unwrap())
.collect::<Vec<BunyanLogRecord>>()
}
pub struct BunyanLogRecordSpec {
pub name: Option<String>,
pub hostname: Option<String>,
pub pid: Option<u32>,
pub v: Option<usize>,
}
pub fn verify_bunyan_records<'a, 'b, I>(
iter: I,
expected: &'a BunyanLogRecordSpec,
) where
I: Iterator<Item = &'b BunyanLogRecord>,
{
for record in iter {
if let Some(ref expected_name) = expected.name {
assert_eq!(expected_name, &record.name);
}
if let Some(ref expected_hostname) = expected.hostname {
assert_eq!(expected_hostname, &record.hostname);
}
if let Some(expected_pid) = expected.pid {
assert_eq!(expected_pid, record.pid);
}
if let Some(expected_v) = expected.v {
assert_eq!(expected_v, record.v);
}
}
}
pub fn verify_bunyan_records_sequential<'a, 'b, I>(
iter: I,
maybe_time_before: Option<&'a DateTime<Utc>>,
maybe_time_after: Option<&'a DateTime<Utc>>,
) where
I: Iterator<Item = &'a BunyanLogRecord>,
{
let mut maybe_should_be_before = maybe_time_before;
for record in iter {
if let Some(should_be_before) = maybe_should_be_before {
assert!(should_be_before.timestamp() <= record.time.timestamp());
}
maybe_should_be_before = Some(&record.time);
}
if let Some(should_be_before) = maybe_should_be_before {
if let Some(time_after) = maybe_time_after {
assert!(should_be_before.timestamp() <= time_after.timestamp());
}
}
}
#[cfg(test)]
mod test {
const T1_STR: &str = "2020-03-24T00:00:00Z";
const T2_STR: &str = "2020-03-25T00:00:00Z";
use super::verify_bunyan_records;
use super::verify_bunyan_records_sequential;
use super::BunyanLogRecord;
use super::BunyanLogRecordSpec;
use chrono::DateTime;
use chrono::Utc;
fn make_dummy_record() -> BunyanLogRecord {
let t1: DateTime<Utc> =
DateTime::parse_from_rfc3339(T1_STR).unwrap().into();
BunyanLogRecord {
time: t1,
name: "n1".to_string(),
hostname: "h1".to_string(),
pid: 1,
msg: "msg1".to_string(),
v: 0,
}
}
#[test]
fn test_bunyan_easy_cases() {
let t1: DateTime<Utc> =
DateTime::parse_from_rfc3339(T1_STR).unwrap().into();
let r1 = make_dummy_record();
let r2 = BunyanLogRecord {
time: t1,
name: "n1".to_string(),
hostname: "h2".to_string(),
pid: 1,
msg: "msg2".to_string(),
v: 1,
};
let records: Vec<&BunyanLogRecord> = vec![&r1];
let iter = records.iter().map(|x| *x);
verify_bunyan_records(
iter,
&BunyanLogRecordSpec {
name: None,
hostname: None,
pid: None,
v: None,
},
);
let records: Vec<&BunyanLogRecord> = vec![&r1];
let iter = records.iter().map(|x| *x);
verify_bunyan_records(
iter,
&BunyanLogRecordSpec {
name: Some("n1".to_string()),
hostname: None,
pid: None,
v: None,
},
);
let records: Vec<&BunyanLogRecord> = vec![&r1];
let iter = records.iter().map(|x| *x);
verify_bunyan_records(
iter,
&BunyanLogRecordSpec {
name: None,
hostname: Some("h1".to_string()),
pid: None,
v: None,
},
);
let records: Vec<&BunyanLogRecord> = vec![&r1];
let iter = records.iter().map(|x| *x);
verify_bunyan_records(
iter,
&BunyanLogRecordSpec {
name: None,
hostname: None,
pid: Some(1),
v: None,
},
);
let records: Vec<&BunyanLogRecord> = vec![&r1];
let iter = records.iter().map(|x| *x);
verify_bunyan_records(
iter,
&BunyanLogRecordSpec {
name: None,
hostname: None,
pid: None,
v: Some(0),
},
);
let records: Vec<&BunyanLogRecord> = vec![&r1];
let iter = records.iter().map(|x| *x);
verify_bunyan_records(
iter,
&BunyanLogRecordSpec {
name: Some("n1".to_string()),
hostname: Some("h1".to_string()),
pid: Some(1),
v: Some(0),
},
);
let records: Vec<&BunyanLogRecord> = vec![&r1, &r2];
let iter = records.iter().map(|x| *x);
verify_bunyan_records(
iter,
&BunyanLogRecordSpec {
name: Some("n1".to_string()),
hostname: None,
pid: Some(1),
v: None,
},
);
}
#[test]
#[should_panic(expected = "assertion failed")]
fn test_bunyan_bad_name() {
let r1 = make_dummy_record();
let records: Vec<&BunyanLogRecord> = vec![&r1];
let iter = records.iter().map(|x| *x);
verify_bunyan_records(
iter,
&BunyanLogRecordSpec {
name: Some("n2".to_string()),
hostname: None,
pid: None,
v: None,
},
);
}
#[test]
#[should_panic(expected = "assertion failed")]
fn test_bunyan_bad_hostname() {
let r1 = make_dummy_record();
let records: Vec<&BunyanLogRecord> = vec![&r1];
let iter = records.iter().map(|x| *x);
verify_bunyan_records(
iter,
&BunyanLogRecordSpec {
name: None,
hostname: Some("h2".to_string()),
pid: None,
v: None,
},
);
}
#[test]
#[should_panic(expected = "assertion failed")]
fn test_bunyan_bad_pid() {
let r1 = make_dummy_record();
let records: Vec<&BunyanLogRecord> = vec![&r1];
let iter = records.iter().map(|x| *x);
verify_bunyan_records(
iter,
&BunyanLogRecordSpec {
name: None,
hostname: None,
pid: Some(2),
v: None,
},
);
}
#[test]
#[should_panic(expected = "assertion failed")]
fn test_bunyan_bad_v() {
let r1 = make_dummy_record();
let records: Vec<&BunyanLogRecord> = vec![&r1];
let iter = records.iter().map(|x| *x);
verify_bunyan_records(
iter,
&BunyanLogRecordSpec {
name: None,
hostname: None,
pid: None,
v: Some(1),
},
);
}
#[test]
fn test_bunyan_seq_easy_cases() {
let t1: DateTime<Utc> =
DateTime::parse_from_rfc3339(T1_STR).unwrap().into();
let t2: DateTime<Utc> =
DateTime::parse_from_rfc3339(T2_STR).unwrap().into();
let v0: Vec<BunyanLogRecord> = vec![];
let v1: Vec<BunyanLogRecord> = vec![BunyanLogRecord {
time: t1,
name: "dummy_name".to_string(),
hostname: "dummy_hostname".to_string(),
pid: 123,
msg: "dummy_msg".to_string(),
v: 0,
}];
let v2: Vec<BunyanLogRecord> = vec![
BunyanLogRecord {
time: t1,
name: "dummy_name".to_string(),
hostname: "dummy_hostname".to_string(),
pid: 123,
msg: "dummy_msg".to_string(),
v: 0,
},
BunyanLogRecord {
time: t2,
name: "dummy_name".to_string(),
hostname: "dummy_hostname".to_string(),
pid: 123,
msg: "dummy_msg".to_string(),
v: 0,
},
];
verify_bunyan_records_sequential(v0.iter(), None, None);
verify_bunyan_records_sequential(v0.iter(), Some(&t1), None);
verify_bunyan_records_sequential(v0.iter(), None, Some(&t1));
verify_bunyan_records_sequential(v0.iter(), Some(&t1), Some(&t2));
verify_bunyan_records_sequential(v1.iter(), None, None);
verify_bunyan_records_sequential(v1.iter(), Some(&t1), None);
verify_bunyan_records_sequential(v1.iter(), None, Some(&t2));
verify_bunyan_records_sequential(v1.iter(), Some(&t1), Some(&t2));
verify_bunyan_records_sequential(v2.iter(), None, None);
verify_bunyan_records_sequential(v2.iter(), Some(&t1), None);
verify_bunyan_records_sequential(v2.iter(), None, Some(&t2));
verify_bunyan_records_sequential(v2.iter(), Some(&t1), Some(&t2));
}
#[test]
#[should_panic(expected = "assertion failed: should_be_before")]
fn test_bunyan_seq_bounds_bad() {
let t1: DateTime<Utc> =
DateTime::parse_from_rfc3339(T1_STR).unwrap().into();
let t2: DateTime<Utc> =
DateTime::parse_from_rfc3339(T2_STR).unwrap().into();
let v0: Vec<BunyanLogRecord> = vec![];
verify_bunyan_records_sequential(v0.iter(), Some(&t2), Some(&t1));
}
#[test]
#[should_panic(expected = "assertion failed: should_be_before")]
fn test_bunyan_seq_lower_violated() {
let t1: DateTime<Utc> =
DateTime::parse_from_rfc3339(T1_STR).unwrap().into();
let t2: DateTime<Utc> =
DateTime::parse_from_rfc3339(T2_STR).unwrap().into();
let v1: Vec<BunyanLogRecord> = vec![BunyanLogRecord {
time: t1,
name: "dummy_name".to_string(),
hostname: "dummy_hostname".to_string(),
pid: 123,
msg: "dummy_msg".to_string(),
v: 0,
}];
verify_bunyan_records_sequential(v1.iter(), Some(&t2), None);
}
#[test]
#[should_panic(expected = "assertion failed: should_be_before")]
fn test_bunyan_seq_upper_violated() {
let t1: DateTime<Utc> =
DateTime::parse_from_rfc3339(T1_STR).unwrap().into();
let t2: DateTime<Utc> =
DateTime::parse_from_rfc3339(T2_STR).unwrap().into();
let v1: Vec<BunyanLogRecord> = vec![BunyanLogRecord {
time: t2,
name: "dummy_name".to_string(),
hostname: "dummy_hostname".to_string(),
pid: 123,
msg: "dummy_msg".to_string(),
v: 0,
}];
verify_bunyan_records_sequential(v1.iter(), None, Some(&t1));
}
#[test]
#[should_panic(expected = "assertion failed: should_be_before")]
fn test_bunyan_seq_bad_order() {
let t1: DateTime<Utc> =
DateTime::parse_from_rfc3339(T1_STR).unwrap().into();
let t2: DateTime<Utc> =
DateTime::parse_from_rfc3339(T2_STR).unwrap().into();
let v2: Vec<BunyanLogRecord> = vec![
BunyanLogRecord {
time: t2,
name: "dummy_name".to_string(),
hostname: "dummy_hostname".to_string(),
pid: 123,
msg: "dummy_msg".to_string(),
v: 0,
},
BunyanLogRecord {
time: t1,
name: "dummy_name".to_string(),
hostname: "dummy_hostname".to_string(),
pid: 123,
msg: "dummy_msg".to_string(),
v: 0,
},
];
verify_bunyan_records_sequential(v2.iter(), None, None);
}
}