Skip to content

Commit

Permalink
added file processor and requests batch, added tests (#14)
Browse files Browse the repository at this point in the history
* added file processor and requests batch

refactored worker execution flow and logic
added file parser class
small refactoring and renaming
added test urls file
extra exit code

* added test and additional checks

added schema check for HTTP
added integraion test

* allow lines w/o methods, added some tests

URL with no method will be GET by default
added test for the file processor
some tweak for the worker's test
  • Loading branch information
artemoliynyk committed Mar 3, 2024
1 parent c501844 commit 15e37d5
Show file tree
Hide file tree
Showing 12 changed files with 363 additions and 91 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "oxiflow"
version = "0.9.2"
version = "1.0.0-pre-a1"
edition = "2021"
description = "Minimal HTTP loadtester with concurrency"
license = "GPL-3.0"
Expand Down
3 changes: 2 additions & 1 deletion src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ pub mod progressbar;
pub mod http;
pub mod worker;
pub mod cli;
pub mod report;
pub mod report;
pub mod file_processor;
33 changes: 10 additions & 23 deletions src/components/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ use env_logger::Builder as log_builder;

use crate::components::http;

const EXIT_ERROR_PARSING_ARGS: u8 = 3;
const EXIT_UNKNOWN_METHOD: u8 = 4;
const EXIT_NOT_SUPPORTED_YET: u8 = 5;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
/// Simple, fast, concurrent load tester with minimal reporting
Expand All @@ -22,14 +18,14 @@ pub struct Args {
required_unless_present("file"),
default_value = ""
)]
pub address: String,
pub url: String,

/// config file with URLs definition to call
#[arg(
long,
short('f'),
conflicts_with("address"),
required_unless_present("address"),
conflicts_with("url"),
required_unless_present("url"),
default_value = ""
)]
pub file: String,
Expand Down Expand Up @@ -70,7 +66,7 @@ impl Cli {
Ok(instance) => instance,
Err(err) => {
err.print().expect("Unable to format error details");
return Err(EXIT_ERROR_PARSING_ARGS);
return Err(crate::EXIT_ERROR_PARSING_ARGS);
}
};
cli.set_log_level();
Expand All @@ -79,7 +75,7 @@ impl Cli {
println!("Defined method is not supported '{}'", &cli.args.method);
println!("Supported methods: {}", http::list_methods());

return Err(EXIT_UNKNOWN_METHOD);
return Err(crate::EXIT_UNKNOWN_METHOD);
}

if cli.args.repeat > 0 && cli.args.delay >= 30 {
Expand All @@ -89,15 +85,6 @@ impl Cli {
);
}

if !cli.args.file.is_empty() {
println!(
"File option is not supported yet, ignoring '{}'",
cli.args.file
);

return Err(EXIT_NOT_SUPPORTED_YET);
}

Ok(cli)
}

Expand Down Expand Up @@ -143,7 +130,7 @@ mod tests {
}

#[test]
fn test_long_address() {
fn test_long_url() {
let test_args = self::create_iter_from_cmd(
"programm_name.exe -vvv --method TEST123 --concurrent 2 --repeat 3 --timeout 4 \
--delay 5 http://address.local/long-test",
Expand All @@ -158,11 +145,11 @@ mod tests {
assert_eq!(cli.args.timeout, 4);
assert_eq!(cli.args.delay, 5);

assert_eq!(&cli.args.address, "http://address.local/long-test");
assert_eq!(&cli.args.url, "http://address.local/long-test");
}

#[test]
fn test_short_address() {
fn test_short_url() {
let test_args = self::create_iter_from_cmd(
"programm_name.exe -vvvv -mTEST123 -c2 -r3 -t4 -d5 http://address.local/short-test",
);
Expand All @@ -177,7 +164,7 @@ mod tests {
assert_eq!(cli.args.timeout, 4);
assert_eq!(cli.args.delay, 5);

assert_eq!(&cli.args.address, "http://address.local/short-test");
assert_eq!(&cli.args.url, "http://address.local/short-test");
}

#[test]
Expand Down Expand Up @@ -207,7 +194,7 @@ mod tests {

#[test]
#[should_panic]
fn test_address_and_file_error() {
fn test_url_and_file_error() {
let test_args =
self::create_iter_from_cmd("programm_name.exe --file test.txt http://error.local/");

Expand Down
137 changes: 137 additions & 0 deletions src/components/file_processor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use std::{
error::Error,
fs::File,
io::{BufRead, BufReader, Lines},
path::Path,
};

use super::worker::request::WorkerRequest;

#[derive(Default)]
pub struct FileProcessor<'a> {
file_path: &'a str,
}

impl<'a> FileProcessor<'a> {
pub fn new(file_path: &str) -> FileProcessor<'_> {
FileProcessor { file_path }
}

#[allow(dead_code)]
const fn mock() -> FileProcessor<'static> {
FileProcessor { file_path: "" }
}

pub fn read_urls(&self) -> Result<Vec<WorkerRequest>, Box<dyn Error>> {
let lines = self.read_file()?;

let mut requests: Vec<WorkerRequest> = Vec::new();

for line in lines.flatten() {
if line.is_empty() {
continue;
}

if let Some(req) = self.parse_line(&line) {
requests.push(req);
}
}

if requests.is_empty() {
return Err("File found, but not URLs recognised".into());
}

Ok(requests)
}

fn read_file(&self) -> std::result::Result<Lines<BufReader<File>>, Box<dyn std::error::Error>> {
let path = Path::new(self.file_path.trim());

if !path.exists() || !path.is_file() {
return Err(format!("No file found: '{}'", self.file_path).into());
}

Ok(BufReader::new(File::open(path)?).lines())
}

fn parse_line(&self, line: &str) -> Option<WorkerRequest> {
if line.is_empty() {
return None;
}

let mut method = "GET";
let mut url = line.trim();

// any spaces may indicate method
if let Some(pos) = url.find('\u{20}') {
method = &url[0..pos];
url = &url[pos + 1..];
}
Some(WorkerRequest::new(method.to_string(), url.to_string()))
}
}

#[cfg(test)]
mod tests {
use super::FileProcessor;

const MOCK_URL: &str = "http://example.net/test-123";
const MOCK_PROCESSOR: FileProcessor<'_> = FileProcessor::mock();

#[test]
fn test_line_parsing_correct_url() {
let result = MOCK_PROCESSOR.parse_line("GET http://example.net/test-123");
assert!(result.is_some());

let req = result.unwrap();
assert_eq!(req.clone().method, "GET");
assert_eq!(req.clone().url, MOCK_URL);
}

#[test]
fn test_line_parsing_wrong_method() {
let result = MOCK_PROCESSOR.parse_line("OHNO http://example.net/test-123");
assert!(result.is_some());

let req = result.unwrap();
assert_eq!(req.clone().method, "OHNO");
assert_eq!(req.clone().url, MOCK_URL);
}

#[test]
fn test_line_parsing_empty_line() {
assert!(MOCK_PROCESSOR.parse_line("").is_none());
}

#[test]
fn test_line_parsing_space_padded_url() {
// with method
let result = MOCK_PROCESSOR.parse_line(" http://example.net/test-123");
assert!(result.is_some());

let req = result.unwrap();
assert_eq!(req.clone().method, "GET");
assert_eq!(req.clone().url, MOCK_URL);
}

#[test]
fn test_line_parsing_space_padded_method() {
// no method
let result = MOCK_PROCESSOR.parse_line(" POST http://example.net/test-123");
assert!(result.is_some());

let req = result.unwrap();
assert_eq!(req.clone().method, "POST");
assert_eq!(req.clone().url, MOCK_URL);
}

#[test]
fn test_line_parsing_no_method() {
let result = MOCK_PROCESSOR.parse_line("http://example.net/test-123");
assert!(result.is_some());

let req = result.unwrap();
assert_eq!(req.clone().method, "GET");
assert_eq!(req.clone().url, MOCK_URL);
}
}
2 changes: 1 addition & 1 deletion src/components/http/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ impl HttpClient {
return Err(format!("Unsupported method: '{}'", &req.method).into());
}

let url = req.address.clone();
let url = req.url.clone();
let req = match req.method.trim().to_uppercase().as_str() {
"GET" => self.get(url),
"POST" => self.post(url),
Expand Down

0 comments on commit 15e37d5

Please sign in to comment.