| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #[deny(warnings)] |
| |
| use chrono::TimeZone; |
| use failure::{Error, ResultExt}; |
| use fuchsia_async as fasync; |
| use fuchsia_syslog_listener as syslog_listener; |
| use fuchsia_syslog_listener::LogProcessor; |
| use fuchsia_zircon as zx; |
| use std::collections::hash_set::HashSet; |
| use std::collections::HashMap; |
| use std::env; |
| use std::fs; |
| use std::io::{self, Write}; |
| use std::path::PathBuf; |
| |
| // Include the generated FIDL bindings for the `Logger` service. |
| use fidl_fuchsia_logger::{ |
| LogFilterOptions, LogLevelFilter, LogMessage, MAX_TAGS, MAX_TAG_LEN_BYTES, |
| }; |
| |
| const DEFAULT_FILE_CAPACITY: u64 = 64000; |
| |
| #[derive(Debug, PartialEq)] |
| struct LogListenerOptions { |
| filter: LogFilterOptions, |
| local: LocalOptions, |
| } |
| |
| impl Default for LogListenerOptions { |
| fn default() -> LogListenerOptions { |
| LogListenerOptions { |
| filter: LogFilterOptions { |
| filter_by_pid: false, |
| pid: 0, |
| min_severity: LogLevelFilter::Info, |
| verbosity: 0, |
| filter_by_tid: false, |
| tid: 0, |
| tags: vec![], |
| }, |
| local: LocalOptions::default(), |
| } |
| } |
| } |
| |
| #[derive(Debug, PartialEq, Clone)] |
| struct LocalOptions { |
| file: Option<String>, |
| file_capacity: u64, |
| ignore_tags: HashSet<String>, |
| clock: Clock, |
| time_format: String, |
| } |
| |
| impl Default for LocalOptions { |
| fn default() -> LocalOptions { |
| LocalOptions { |
| file: None, |
| file_capacity: DEFAULT_FILE_CAPACITY, |
| ignore_tags: HashSet::new(), |
| clock: Clock::Monotonic, |
| time_format: "%Y-%m-%d %H:%M:%S".to_string(), |
| } |
| } |
| } |
| |
| impl LocalOptions { |
| fn format_time(&self, timestamp: zx::sys::zx_time_t) -> String { |
| match self.clock { |
| Clock::Monotonic => format!( |
| "{:05}.{:06}", |
| timestamp / 1000000000, |
| (timestamp / 1000) % 1000000 |
| ), |
| Clock::UTC => self |
| ._monotonic_to_utc(timestamp) |
| .format(&self.time_format) |
| .to_string(), |
| Clock::Local => chrono::Local |
| .from_utc_datetime(&self._monotonic_to_utc(timestamp)) |
| .format(&self.time_format) |
| .to_string(), |
| } |
| } |
| |
| fn _monotonic_to_utc(&self, timestamp: zx::sys::zx_time_t) -> chrono::NaiveDateTime { |
| // Find UTC offset for Monotonic. |
| // Must compute this every time since UTC time can be adjusted. |
| // Note that when printing old messages from memory buffer then |
| // this may offset them from UTC time as set when logged in |
| // case of UTC time adjustments since. |
| let monotonic_zero_as_utc = |
| zx::Time::get(zx::ClockId::UTC).nanos() - zx::Time::get(zx::ClockId::Monotonic).nanos(); |
| let shifted_timestamp = monotonic_zero_as_utc + timestamp; |
| let seconds = (shifted_timestamp / 1000000000) as i64; |
| let nanos = (shifted_timestamp % 1000000000) as u32; |
| chrono::NaiveDateTime::from_timestamp(seconds, nanos) |
| } |
| } |
| |
| #[derive(Debug, PartialEq, Clone)] |
| enum Clock { |
| Monotonic, // Corresponds to ZX_CLOCK_MONOTONIC |
| UTC, // Corresponds to ZX_UTC_MONOTONIC |
| Local, // Localized wall time |
| } |
| |
| struct MaxCapacityFile { |
| file_path: PathBuf, |
| file: fs::File, |
| capacity: u64, |
| curr_size: u64, |
| } |
| |
| impl MaxCapacityFile { |
| fn new<P: Into<PathBuf>>(file_path: P, capacity: u64) -> Result<MaxCapacityFile, Error> { |
| let file_path = file_path.into(); |
| let file = fs::OpenOptions::new().append(true).create(true).open(&file_path)?; |
| let curr_size = file.metadata()?.len(); |
| Ok(MaxCapacityFile { |
| file, |
| file_path, |
| capacity, |
| curr_size, |
| }) |
| } |
| |
| // rotate will move the current file to ${file_path}.log.old and create a new file at ${file_path} |
| // to hold future messages. |
| fn rotate(&mut self) -> io::Result<()> { |
| let mut new_file_name = |
| self.file_path |
| .to_str() |
| .ok_or(io::Error::new(io::ErrorKind::Other, "invalid file name"))? |
| .to_string(); |
| new_file_name.push_str(".old"); |
| |
| fs::rename(&self.file_path, PathBuf::from(new_file_name))?; |
| self.file = fs::OpenOptions::new().append(true).create(true).open(&self.file_path)?; |
| self.curr_size = 0; |
| Ok(()) |
| } |
| } |
| |
| impl Write for MaxCapacityFile { |
| fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
| if self.capacity == 0 { |
| return Ok(buf.len()); |
| } |
| if buf.len() as u64 > self.capacity / 2 { |
| return Err(io::Error::new(io::ErrorKind::Other, "buffer size larger than file capacity")); |
| } |
| if self.capacity != 0 && self.curr_size + (buf.len() as u64) > self.capacity / 2 { |
| self.rotate()?; |
| } |
| self.curr_size += buf.len() as u64; |
| self.file.write(buf) |
| } |
| fn flush(&mut self) -> io::Result<()> { |
| self.file.flush() |
| } |
| } |
| |
| fn help(name: &str) -> String { |
| format!( |
| r#"Usage: {name} [flags] |
| Flags: |
| --tag <string>: |
| Tag to filter on. Multiple tags can be specified by using multiple --tag flags. |
| All the logs containing at least one of the passed tags would be printed. |
| |
| --ignore-tag <string>: |
| Tag to ignore. Any logs containing at least one of the passed tags will not be |
| printed. |
| |
| --pid <integer>: |
| pid for the program to filter on. |
| |
| --tid <integer>: |
| tid for the program to filter on. |
| |
| --severity <INFO|WARN|ERROR|FATAL>: |
| Minimum severity to filter on. |
| Defaults to INFO. |
| |
| --verbosity <integer>: |
| Verbosity to filter on. It should be positive integer greater than 0. |
| If this is passed, it overrides default severity. |
| Errors out if both this and --severity are passed. |
| Defaults to 0 which means don't filter on verbosity. |
| |
| --file <string>: |
| File to write logs to. If omitted, logs are written to stdout. |
| |
| --file_capacity <integer>: |
| The maximum allowed amount of disk space to consume. Once the file being written to |
| reaches half of the capacity, it is moved to FILE.old and a new log file is created. |
| Defaults to {default_capacity}. Does nothing if --file is not specified. Setting this |
| to 0 disables this functionality. |
| |
| --clock <Monotonic|UTC|Local>: |
| Select clock to use for timestamps. |
| Monotonic (default): same as ZX_CLOCK_MONOTONIC. |
| UTC: same as ZX_CLOCK_UTC. |
| Local: localized wall time. |
| |
| --time_format <format>: |
| If --clock is not MONOTONIC, specify timestamp format. |
| See chrono::format::strftime for format specifiers. |
| Defaults to "%Y-%m-%d %H:%M:%S". |
| |
| --help | -h: |
| Prints usage."#, |
| name=name, default_capacity=DEFAULT_FILE_CAPACITY |
| ) |
| } |
| |
| fn parse_flags(args: &[String]) -> Result<LogListenerOptions, String> { |
| if args.len() % 2 != 0 { |
| return Err(String::from("Invalid args.")); |
| } |
| let mut options = LogListenerOptions::default(); |
| |
| let mut i = 0; |
| let mut severity_passed = false; |
| while i < args.len() { |
| let argument = &args[i]; |
| if args[i + 1].starts_with("-") { |
| return Err(format!( |
| "Invalid args. Pass argument after flag '{}'", |
| argument |
| )); |
| } |
| match argument.as_ref() { |
| "--tag" => { |
| let tag = &args[i + 1]; |
| if tag.len() > MAX_TAG_LEN_BYTES as usize { |
| return Err(format!( |
| "'{}' should not be more than {} characters", |
| tag, MAX_TAG_LEN_BYTES |
| )); |
| } |
| options.filter.tags.push(String::from(tag.as_ref())); |
| if options.filter.tags.len() > MAX_TAGS as usize { |
| return Err(format!("Max tags allowed: {}", MAX_TAGS)); |
| } |
| } |
| "--ignore-tag" => { |
| let tag = &args[i + 1]; |
| if tag.len() > MAX_TAG_LEN_BYTES as usize { |
| return Err(format!( |
| "'{}' should not be more than {} characters", |
| tag, MAX_TAG_LEN_BYTES |
| )); |
| } |
| options.local.ignore_tags.insert(String::from(tag.as_ref())); |
| } |
| "--severity" => { |
| if options.filter.verbosity > 0 { |
| return Err( |
| "Invalid arguments: Cannot pass both severity and verbosity".to_string() |
| ); |
| } |
| severity_passed = true; |
| match args[i + 1].as_ref() { |
| "INFO" => options.filter.min_severity = LogLevelFilter::Info, |
| "WARN" => options.filter.min_severity = LogLevelFilter::Warn, |
| "ERROR" => options.filter.min_severity = LogLevelFilter::Error, |
| "FATAL" => options.filter.min_severity = LogLevelFilter::Fatal, |
| a => return Err(format!("Invalid severity: {}", a)), |
| } |
| } |
| "--verbosity" => if let Ok(v) = args[i + 1].parse::<u8>() { |
| if severity_passed { |
| return Err( |
| "Invalid arguments: Cannot pass both severity and verbosity".to_string() |
| ); |
| } |
| if v == 0 { |
| return Err(format!( |
| "Invalid verbosity: '{}', should be positive integer greater than 0.", |
| args[i + 1] |
| )); |
| } |
| options.filter.min_severity = LogLevelFilter::None; |
| options.filter.verbosity = v; |
| } else { |
| return Err(format!( |
| "Invalid verbosity: '{}', should be positive integer greater than 0.", |
| args[i + 1] |
| )); |
| }, |
| "--pid" => { |
| options.filter.filter_by_pid = true; |
| match args[i + 1].parse::<u64>() { |
| Ok(pid) => { |
| options.filter.pid = pid; |
| } |
| Err(_) => { |
| return Err(format!( |
| "Invalid pid: '{}', should be a positive integer.", |
| args[i + 1] |
| )); |
| } |
| } |
| } |
| "--tid" => { |
| options.filter.filter_by_tid = true; |
| match args[i + 1].parse::<u64>() { |
| Ok(tid) => { |
| options.filter.tid = tid; |
| } |
| Err(_) => { |
| return Err(format!( |
| "Invalid tid: '{}', should be a positive integer.", |
| args[i + 1] |
| )); |
| } |
| } |
| } |
| "--file" => { |
| options.local.file = Some((&args[i + 1]).clone()); |
| } |
| "--file_capacity" => { |
| match args[i + 1].parse::<u64>() { |
| Ok(cap) => { |
| options.local.file_capacity = cap; |
| } |
| Err(_) => { |
| return Err(format!( |
| "Invalid file capacity: '{}', should be a positive integer.", |
| args[i + 1] |
| )); |
| } |
| } |
| } |
| "--clock" => match args[i + 1].to_lowercase().as_ref() { |
| "monotonic" => options.local.clock = Clock::Monotonic, |
| "utc" => options.local.clock = Clock::UTC, |
| "local" => options.local.clock = Clock::Local, |
| a => return Err(format!("Invalid clock: {}", a)), |
| }, |
| "--time_format" => { |
| options.local.time_format = args[i + 1].clone(); |
| } |
| a => { |
| return Err(format!("Invalid option {}", a)); |
| } |
| } |
| i = i + 2; |
| } |
| return Ok(options); |
| } |
| |
| struct Listener<W: Write + Send> { |
| // stores pid, dropped_logs |
| dropped_logs: HashMap<u64, u32>, |
| local_options: LocalOptions, |
| writer: W, |
| } |
| |
| impl<W> LogProcessor for Listener<W> |
| where |
| W: Write + Send, |
| { |
| fn log(&mut self, message: LogMessage) { |
| if message |
| .tags |
| .iter() |
| .any(|tag| self.local_options.ignore_tags.contains(tag)) |
| { |
| return; |
| } |
| let tags = message.tags.join(", "); |
| writeln!( |
| self.writer, |
| "[{}][{}][{}][{}] {}: {}", |
| self.local_options.format_time(message.time), |
| message.pid, |
| message.tid, |
| tags, |
| get_log_level(message.severity), |
| message.msg |
| ).expect("should not fail"); |
| if message.dropped_logs > 0 |
| && self |
| .dropped_logs |
| .get(&message.pid) |
| .map(|d| d < &message.dropped_logs) |
| .unwrap_or(true) |
| { |
| writeln!( |
| self.writer, |
| "[{}][{}][{}][{}] WARNING: Dropped logs count: {}", |
| self.local_options.format_time(message.time), |
| message.pid, |
| message.tid, |
| tags, |
| message.dropped_logs |
| ).expect("should not fail"); |
| self.dropped_logs.insert(message.pid, message.dropped_logs); |
| } |
| } |
| |
| fn done(&mut self) { |
| // ignore as this is not called incase of listener. |
| } |
| } |
| |
| fn get_log_level(level: i32) -> String { |
| match level { |
| 0 => "INFO".to_string(), |
| 1 => "WARNING".to_string(), |
| 2 => "ERROR".to_string(), |
| 3 => "FATAL".to_string(), |
| l => { |
| if l > 3 { |
| "INVALID".to_string() |
| } else { |
| format!("VLOG({})", -l) |
| } |
| } |
| } |
| } |
| |
| fn new_listener(local_options: LocalOptions) -> Result<Listener<Box<dyn Write + Send>>, Error> { |
| let writer: Box<dyn Write + Send> = match local_options.file { |
| None => Box::new(io::stdout()), |
| Some(ref name) => Box::new(MaxCapacityFile::new(name, local_options.file_capacity)?), |
| }; |
| Ok(Listener { |
| dropped_logs: HashMap::new(), |
| writer: writer, |
| local_options: local_options, |
| }) |
| } |
| |
| fn run_log_listener(options: Option<&mut LogListenerOptions>) -> Result<(), Error> { |
| let mut executor = fasync::Executor::new().context("Error creating executor")?; |
| let (filter_options, local_options) = options.map_or_else( |
| || (None, LocalOptions::default()), |
| |o| (Some(&mut o.filter), o.local.clone()), |
| ); |
| let l = new_listener(local_options)?; |
| let listener_fut = syslog_listener::run_log_listener(l, filter_options, false)?; |
| executor |
| .run_singlethreaded(listener_fut) |
| .map_err(Into::into) |
| } |
| |
| fn main() { |
| let args: Vec<String> = env::args().collect(); |
| if args.len() > 1 && (args[1] == "--help" || args[1] == "-h") { |
| println!("{}\n", help(args[0].as_ref())); |
| return; |
| } |
| let mut options = match parse_flags(&args[1..]) { |
| Err(e) => { |
| eprintln!("{}\n{}\n", e, help(args[0].as_ref())); |
| return; |
| } |
| Ok(o) => o, |
| }; |
| |
| if let Err(e) = run_log_listener(Some(&mut options)) { |
| eprintln!("LogListener: Error: {:?}", e); |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| use std::fs::File; |
| use std::io::Read; |
| use tempfile::TempDir; |
| |
| fn copy_log_message(msg: &LogMessage) -> LogMessage { |
| LogMessage { |
| pid: msg.pid, |
| tid: msg.tid, |
| severity: msg.severity, |
| time: msg.time, |
| msg: msg.msg.clone(), |
| dropped_logs: msg.dropped_logs, |
| tags: msg.tags.clone(), |
| } |
| } |
| |
| #[test] |
| fn test_log_fn() { |
| let _executor = fasync::Executor::new().expect("unable to create executor"); |
| let tmp_dir = TempDir::new().expect("should have created tempdir"); |
| let file_path = tmp_dir.path().join("tmp_file"); |
| let tmp_file = File::create(&file_path).expect("should have created file"); |
| let mut l = Listener { |
| dropped_logs: HashMap::new(), |
| writer: tmp_file, |
| local_options: LocalOptions::default(), |
| }; |
| |
| // test log levels |
| let mut message = LogMessage { |
| pid: 123, |
| tid: 321, |
| severity: 0, |
| time: 76352234564, |
| msg: "hello".to_string(), |
| dropped_logs: 0, |
| tags: vec![], |
| }; |
| l.log(copy_log_message(&message)); |
| |
| for level in vec![1, 2, 3, 4, 11, -1, -3] { |
| message.severity = level; |
| l.log(copy_log_message(&message)); |
| } |
| let mut expected = "".to_string(); |
| for level in &[ |
| "INFO", "WARNING", "ERROR", "FATAL", "INVALID", "INVALID", "VLOG(1)", "VLOG(3)", |
| ] { |
| expected.push_str(&format!("[00076.352234][123][321][] {}: hello\n", level)); |
| } |
| |
| // test tags |
| message.severity = 0; |
| message.tags = vec!["tag1".to_string()]; |
| l.log(copy_log_message(&message)); |
| expected.push_str("[00076.352234][123][321][tag1] INFO: hello\n"); |
| |
| message.tags.push("tag2".to_string()); |
| l.log(copy_log_message(&message)); |
| expected.push_str("[00076.352234][123][321][tag1, tag2] INFO: hello\n"); |
| |
| // test Monotonic time |
| message.time = 636253000631621; |
| l.log(copy_log_message(&message)); |
| let s = "[636253.000631][123][321][tag1, tag2] INFO: hello\n"; |
| expected.push_str(s); |
| |
| // test dropped logs |
| message.dropped_logs = 1; |
| l.log(copy_log_message(&message)); |
| expected.push_str(s); |
| expected.push_str("[636253.000631][123][321][tag1, tag2] WARNING: Dropped logs count: 1\n"); |
| l.log(copy_log_message(&message)); |
| // will not print log count again |
| expected.push_str(s); |
| |
| // change pid and test |
| message.pid = 1234; |
| l.log(copy_log_message(&message)); |
| expected.push_str("[636253.000631][1234][321][tag1, tag2] INFO: hello\n"); |
| expected |
| .push_str("[636253.000631][1234][321][tag1, tag2] WARNING: Dropped logs count: 1\n"); |
| |
| // switch back pid and test |
| message.pid = 123; |
| l.log(copy_log_message(&message)); |
| expected.push_str(s); |
| message.dropped_logs = 2; |
| l.log(copy_log_message(&message)); |
| expected.push_str(s); |
| expected.push_str("[636253.000631][123][321][tag1, tag2] WARNING: Dropped logs count: 2\n"); |
| |
| let mut tmp_file = File::open(&file_path).expect("should have opened the file"); |
| let mut content = String::new(); |
| tmp_file |
| .read_to_string(&mut content) |
| .expect("something went wrong reading the file"); |
| |
| assert_eq!(content, expected); |
| } |
| |
| #[test] |
| fn test_max_capacity_file_write() { |
| |
| struct TestCase { |
| file_cap: u64, |
| file_1_initial_state: Vec<u8>, |
| file_2_initial_state: Vec<u8>, |
| write_to_perform: Vec<u8>, |
| file_1_expected_state: Vec<u8>, |
| file_2_expected_state: Vec<u8>, |
| } |
| |
| let test_cases = vec![ |
| TestCase { |
| file_cap: 10, |
| file_1_initial_state: vec![], |
| file_2_initial_state: vec![], |
| write_to_perform: vec![], |
| file_1_expected_state: vec![], |
| file_2_expected_state: vec![], |
| }, |
| TestCase { |
| file_cap: 10, |
| file_1_initial_state: vec![], |
| file_2_initial_state: vec![], |
| write_to_perform: vec![0], |
| file_1_expected_state: vec![0], |
| file_2_expected_state: vec![], |
| }, |
| TestCase { |
| file_cap: 10, |
| file_1_initial_state: vec![0], |
| file_2_initial_state: vec![0], |
| write_to_perform: vec![], |
| file_1_expected_state: vec![0], |
| file_2_expected_state: vec![0], |
| }, |
| TestCase { |
| file_cap: 10, |
| file_1_initial_state: vec![], |
| file_2_initial_state: vec![], |
| write_to_perform: vec![0,1,2,3,4], |
| file_1_expected_state: vec![0,1,2,3,4], |
| file_2_expected_state: vec![], |
| }, |
| TestCase { |
| file_cap: 10, |
| file_1_initial_state: vec![0,1,2,3,4], |
| file_2_initial_state: vec![], |
| write_to_perform: vec![5], |
| file_1_expected_state: vec![5], |
| file_2_expected_state: vec![0,1,2,3,4], |
| }, |
| TestCase { |
| file_cap: 10, |
| file_1_initial_state: vec![5,6,7,8,9], |
| file_2_initial_state: vec![0,1,2,3,4], |
| write_to_perform: vec![10,11,12,13,14], |
| file_1_expected_state: vec![10,11,12,13,14], |
| file_2_expected_state: vec![5,6,7,8,9], |
| }, |
| TestCase { |
| file_cap: 0, |
| file_1_initial_state: vec![], |
| file_2_initial_state: vec![], |
| write_to_perform: vec![1,2,3,4,5], |
| file_1_expected_state: vec![], |
| file_2_expected_state: vec![], |
| }, |
| ]; |
| |
| for tc in test_cases { |
| let tmp_dir = TempDir::new().unwrap(); |
| let tmp_file_path = tmp_dir.path().join("test.log"); |
| fs::OpenOptions::new().append(true).create(true) |
| .open(&tmp_file_path).unwrap() |
| .write(&tc.file_1_initial_state).unwrap(); |
| fs::OpenOptions::new().append(true).create(true) |
| .open(&tmp_file_path.with_extension("log.old")).unwrap() |
| .write(&tc.file_2_initial_state).unwrap(); |
| |
| MaxCapacityFile::new(tmp_file_path.clone(), tc.file_cap).unwrap() |
| .write(&tc.write_to_perform).unwrap(); |
| |
| let mut file1 = fs::OpenOptions::new().read(true) |
| .open(&tmp_file_path).unwrap(); |
| let file_size = file1.metadata().unwrap().len(); |
| let mut buf = vec![0; file_size as usize]; |
| file1.read(&mut buf).unwrap(); |
| assert_eq!(buf, tc.file_1_expected_state); |
| |
| let mut file2 = fs::OpenOptions::new().read(true) |
| .open(&tmp_file_path.with_extension("log.old")).unwrap(); |
| let file_size = file2.metadata().unwrap().len(); |
| let mut buf = vec![0; file_size as usize]; |
| file2.read(&mut buf).unwrap(); |
| assert_eq!(buf, tc.file_2_expected_state); |
| } |
| } |
| |
| #[test] |
| fn test_format_monotonic_time() { |
| let mut local_options = LocalOptions::default(); |
| let timestamp = 636253000631621; |
| |
| let formatted = local_options.format_time(timestamp); // Test default |
| assert_eq!(formatted, "636253.000631"); |
| local_options.clock = Clock::Monotonic; |
| let formatted = local_options.format_time(timestamp); |
| assert_eq!(formatted, "636253.000631"); |
| } |
| |
| #[test] |
| fn test_format_utc_time() { |
| let mut local_options = LocalOptions::default(); |
| let timestamp = 636253000631621; |
| local_options.clock = Clock::UTC; |
| local_options.time_format = "%H:%M:%S %d/%m/%Y".to_string(); |
| |
| let timestamp_utc_formatted = local_options.format_time(timestamp); |
| let timestamp_utc_struct = chrono::NaiveDateTime::parse_from_str( |
| ×tamp_utc_formatted, |
| &local_options.time_format, |
| ).unwrap(); |
| assert_eq!( |
| timestamp_utc_struct |
| .format(&local_options.time_format) |
| .to_string(), |
| timestamp_utc_formatted |
| ); |
| let zero_utc_formatted = local_options.format_time(0); |
| assert_ne!(zero_utc_formatted, timestamp_utc_formatted); |
| } |
| |
| mod parse_flags { |
| use super::*; |
| |
| fn parse_flag_test_helper(args: &[String], options: Option<&LogListenerOptions>) { |
| match parse_flags(args) { |
| Ok(l) => match options { |
| None => { |
| panic!("parse_flags should have returned error, got: {:?}", l); |
| } |
| Some(options) => { |
| assert_eq!(&l, options); |
| } |
| }, |
| Err(e) => { |
| if let Some(_) = options { |
| panic!("did not expect error: {}", e); |
| } |
| } |
| } |
| } |
| |
| #[test] |
| fn invalid_options() { |
| let args = vec!["--tag".to_string()]; |
| parse_flag_test_helper(&args, None); |
| } |
| |
| #[test] |
| fn invalid_options2() { |
| let args = vec!["--tag".to_string(), "--severity".to_string()]; |
| parse_flag_test_helper(&args, None); |
| } |
| |
| #[test] |
| fn invalid_flag() { |
| let args = vec![ |
| "--tag".to_string(), |
| "tag".to_string(), |
| "--invalid".to_string(), |
| ]; |
| parse_flag_test_helper(&args, None); |
| } |
| |
| #[test] |
| fn one_tag() { |
| let args = vec!["--tag".to_string(), "tag".to_string()]; |
| let mut expected = LogListenerOptions::default(); |
| expected.filter.tags.push("tag".to_string()); |
| parse_flag_test_helper(&args, Some(&expected)); |
| } |
| |
| #[test] |
| fn multiple_tags() { |
| let args = vec![ |
| "--tag".to_string(), |
| "tag".to_string(), |
| "--tag".to_string(), |
| "tag1".to_string(), |
| ]; |
| let mut expected = LogListenerOptions::default(); |
| expected.filter.tags.push("tag".to_string()); |
| expected.filter.tags.push("tag1".to_string()); |
| parse_flag_test_helper(&args, Some(&expected)); |
| } |
| |
| #[test] |
| fn one_ignore_tag() { |
| let args = vec!["--ignore-tag".to_string(), "tag".to_string()]; |
| let mut expected = LogListenerOptions::default(); |
| expected.local.ignore_tags.insert("tag".to_string()); |
| parse_flag_test_helper(&args, Some(&expected)); |
| } |
| |
| #[test] |
| fn multiple_ignore_tags() { |
| let args = vec![ |
| "--ignore-tag".to_string(), |
| "tag".to_string(), |
| "--ignore-tag".to_string(), |
| "tag1".to_string(), |
| ]; |
| let mut expected = LogListenerOptions::default(); |
| expected.local.ignore_tags.insert("tag".to_string()); |
| expected.local.ignore_tags.insert("tag1".to_string()); |
| parse_flag_test_helper(&args, Some(&expected)); |
| } |
| |
| #[test] |
| fn pid() { |
| let args = vec!["--pid".to_string(), "123".to_string()]; |
| let mut expected = LogListenerOptions::default(); |
| expected.filter.filter_by_pid = true; |
| expected.filter.pid = 123; |
| parse_flag_test_helper(&args, Some(&expected)); |
| } |
| |
| #[test] |
| fn pid_fail() { |
| let args = vec!["--pid".to_string(), "123a".to_string()]; |
| parse_flag_test_helper(&args, None); |
| } |
| |
| #[test] |
| fn tid() { |
| let args = vec!["--tid".to_string(), "123".to_string()]; |
| let mut expected = LogListenerOptions::default(); |
| expected.filter.filter_by_tid = true; |
| expected.filter.tid = 123; |
| parse_flag_test_helper(&args, Some(&expected)); |
| } |
| |
| #[test] |
| fn tid_fail() { |
| let args = vec!["--tid".to_string(), "123a".to_string()]; |
| parse_flag_test_helper(&args, None); |
| } |
| |
| #[test] |
| fn severity() { |
| let mut expected = LogListenerOptions::default(); |
| expected.filter.min_severity = LogLevelFilter::None; |
| for s in vec!["INFO", "WARN", "ERROR", "FATAL"] { |
| let args = vec!["--severity".to_string(), s.to_string()]; |
| expected.filter.min_severity = LogLevelFilter::from_primitive( |
| expected.filter.min_severity.into_primitive() + 1, |
| ).unwrap(); |
| parse_flag_test_helper(&args, Some(&expected)); |
| } |
| } |
| |
| #[test] |
| fn severity_fail() { |
| let args = vec!["--severity".to_string(), "DEBUG".to_string()]; |
| parse_flag_test_helper(&args, None); |
| } |
| |
| #[test] |
| fn verbosity() { |
| let args = vec!["--verbosity".to_string(), "2".to_string()]; |
| let mut expected = LogListenerOptions::default(); |
| expected.filter.verbosity = 2; |
| expected.filter.min_severity = LogLevelFilter::None; |
| parse_flag_test_helper(&args, Some(&expected)); |
| } |
| |
| #[test] |
| fn severity_verbosity_together() { |
| let args = vec![ |
| "--verbosity".to_string(), |
| "2".to_string(), |
| "--severity".to_string(), |
| "DEBUG".to_string(), |
| ]; |
| parse_flag_test_helper(&args, None); |
| |
| let args = vec![ |
| "--severity".to_string(), |
| "DEBUG".to_string(), |
| "--verbosity".to_string(), |
| "2".to_string(), |
| ]; |
| parse_flag_test_helper(&args, None); |
| } |
| |
| #[test] |
| fn verbosity_fail() { |
| let mut args = vec!["--verbosity".to_string(), "-2".to_string()]; |
| parse_flag_test_helper(&args, None); |
| |
| args[1] = "str".to_string(); |
| parse_flag_test_helper(&args, None); |
| |
| args[1] = "0".to_string(); |
| parse_flag_test_helper(&args, None); |
| } |
| |
| #[test] |
| fn file_test() { |
| let mut expected = LogListenerOptions::default(); |
| expected.local.file = Some("/data/test".to_string()); |
| let args = vec!["--file".to_string(), "/data/test".to_string()]; |
| parse_flag_test_helper(&args, Some(&expected)); |
| } |
| |
| #[test] |
| fn file_empty() { |
| let args = Vec::new(); |
| let expected = LogListenerOptions::default(); |
| parse_flag_test_helper(&args, Some(&expected)); |
| } |
| |
| #[test] |
| fn clock() { |
| let args = vec!["--clock".to_string(), "UTC".to_string()]; |
| let mut expected = LogListenerOptions::default(); |
| expected.local.clock = Clock::UTC; |
| parse_flag_test_helper(&args, Some(&expected)); |
| } |
| |
| #[test] |
| fn clock_fail() { |
| let args = vec!["--clock".to_string(), "CLUCK!!".to_string()]; |
| parse_flag_test_helper(&args, None); |
| } |
| |
| #[test] |
| fn time_format() { |
| let args = vec!["--time_format".to_string(), "%H:%M:%S".to_string()]; |
| let mut expected = LogListenerOptions::default(); |
| expected.local.time_format = "%H:%M:%S".to_string(); |
| parse_flag_test_helper(&args, Some(&expected)); |
| } |
| |
| #[test] |
| fn tag_edge_case() { |
| let mut args = vec!["--tag".to_string()]; |
| let mut tag = "a".to_string(); |
| for _ in 1..MAX_TAG_LEN_BYTES { |
| tag.push('a'); |
| } |
| args.push(tag.clone()); |
| let mut expected = LogListenerOptions::default(); |
| expected.filter.tags.push(tag); |
| parse_flag_test_helper(&args, Some(&expected)); |
| |
| args[1] = "tag1".to_string(); |
| expected.filter.tags[0] = args[1].clone(); |
| for i in 1..MAX_TAGS { |
| args.push("--tag".to_string()); |
| args.push(format!("tag{}", i)); |
| expected.filter.tags.push(format!("tag{}", i)); |
| } |
| parse_flag_test_helper(&args, Some(&expected)); |
| } |
| |
| #[test] |
| fn tag_fail() { |
| let mut args = vec!["--tag".to_string()]; |
| let mut tag = "a".to_string(); |
| for _ in 0..MAX_TAG_LEN_BYTES { |
| tag.push('a'); |
| } |
| args.push(tag); |
| parse_flag_test_helper(&args, None); |
| |
| args[1] = "tag1".to_string(); |
| for i in 0..MAX_TAGS + 5 { |
| args.push("--tag".to_string()); |
| args.push(format!("tag{}", i)); |
| } |
| parse_flag_test_helper(&args, None); |
| } |
| } |
| } |