use questdb_confstr::{parse_conf_str, ErrorKind, ParsingError};
use std::collections::HashMap;
#[test]
fn empty() -> Result<(), ParsingError> {
let input = "";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind().clone(), ErrorKind::ExpectedIdentifierNotEmpty);
assert_eq!(err.position(), 0);
assert_eq!(
err.to_string(),
"expected identifier, not an empty string at position 0"
);
Ok(())
}
#[test]
fn basic() -> Result<(), ParsingError> {
let input = "http::host=127.0.0.1;port=9000;";
let config = parse_conf_str(input)?;
assert_eq!(config.service(), "http");
assert_eq!(config.get("host"), Some("127.0.0.1"));
assert_eq!(config.get("port"), Some("9000"));
assert_eq!(format!("{:?}", config), "ConfStr { service: \"http\", .. }");
Ok(())
}
#[test]
fn case_sensitivity() -> Result<(), ParsingError> {
let input = "TcP::Host=LoCaLhOsT;Port=9000;";
let config = parse_conf_str(input)?;
assert_eq!(config.service(), "TcP");
assert_eq!(config.get("Host"), Some("LoCaLhOsT"));
assert_eq!(config.get("host"), None);
assert_eq!(config.get("Port"), Some("9000"));
assert_eq!(config.get("port"), None);
Ok(())
}
#[test]
fn duplicate_key() {
let input = "http::host=127.0.0.1;host=localhost;port=9000;";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(
err.kind().clone(),
ErrorKind::DuplicateKey("host".to_string())
);
assert_eq!(err.position(), 21);
assert_eq!(err.to_string(), "duplicate key \"host\" at position 21");
}
#[test]
fn key_can_start_with_number() -> Result<(), ParsingError> {
let input = "https::123=456;";
let config = parse_conf_str(input)?;
assert_eq!(config.service(), "https");
let mut expected = HashMap::new();
expected.insert("123".to_string(), "456".to_string());
assert_eq!(config.params(), &expected);
Ok(())
}
#[test]
fn identifiers_can_contain_underscores() -> Result<(), ParsingError> {
let input = "_A_::__x_Y__=42;";
let config = parse_conf_str(input)?;
assert_eq!(config.service(), "_A_");
let mut expected = HashMap::new();
expected.insert("__x_Y__".to_string(), "42".to_string());
Ok(())
}
#[test]
fn key_must_be_alphanumeric() {
let input = "https::ho st=localhost;";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind().clone(), ErrorKind::MustBeAlphanumeric(' '));
assert_eq!(err.position(), 9);
assert_eq!(
err.to_string(),
"must be alphanumeric, not ' ' at position 9"
);
}
#[test]
fn key_cannot_be_empty() {
let input = "https::=localhost;";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind().clone(), ErrorKind::ExpectedIdentifierNot('='));
assert_eq!(err.position(), 7);
assert_eq!(
err.to_string(),
"expected identifier to start with ascii letter, not '=' at position 7"
);
}
#[test]
fn no_params_with_colons() -> Result<(), ParsingError> {
let input = "https::";
let config = parse_conf_str(input)?;
assert_eq!(config.service(), "https");
assert!(config.params().is_empty());
Ok(())
}
#[test]
fn no_params_no_colons() -> Result<(), ParsingError> {
let input = "https";
let config = parse_conf_str(input)?;
assert_eq!(config.service(), "https");
assert!(config.params().is_empty());
Ok(())
}
#[test]
fn bad_service_name_separator1() {
let input = "x;/host=localhost;";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind(), ErrorKind::BadSeparator((':', ';')));
assert_eq!(err.position(), 1);
assert_eq!(
err.to_string(),
"bad separator, expected ':' got ';' at position 1"
);
}
#[test]
fn bad_service_name_separator2() {
let input = "x:;host=localhost;";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind(), ErrorKind::BadSeparator((':', ';')));
assert_eq!(err.position(), 2);
assert_eq!(
err.to_string(),
"bad separator, expected ':' got ';' at position 2"
);
}
#[test]
fn url_as_service_separator() {
let input = "http://localhost:9000;host=localhost;";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind(), ErrorKind::BadSeparator((':', '/')));
assert_eq!(err.position(), 5);
assert_eq!(
err.to_string(),
"bad separator, expected ':' got '/' at position 5"
);
}
#[test]
fn no_service_name_with_colons() {
let input = "::host=localhost;";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind(), ErrorKind::ExpectedIdentifierNot(':'));
assert_eq!(err.position(), 0);
assert_eq!(
err.to_string(),
"expected identifier to start with ascii letter, not ':' at position 0"
);
}
#[test]
fn no_service_name_no_colons() {
let input = "host=localhost;";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind(), ErrorKind::BadSeparator((':', '=')));
assert_eq!(err.position(), 4);
assert_eq!(
err.to_string(),
"bad separator, expected ':' got '=' at position 4"
);
}
#[test]
fn bad_key_value_separator_semicolon() {
let input = "http::host=localhost;port9000;";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind(), ErrorKind::BadSeparator(('=', ';')));
assert_eq!(err.position(), 29);
assert_eq!(
err.to_string(),
"bad separator, expected '=' got ';' at position 29"
);
}
#[test]
fn bad_key_value_separator_colon() {
let input = "s3::host:localhost;port=9000;";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind(), ErrorKind::BadSeparator(('=', ':')));
assert_eq!(err.position(), 8);
assert_eq!(
err.to_string(),
"bad separator, expected '=' got ':' at position 8"
);
}
#[test]
fn test_incomplete_key_no_value() {
let input = "http::host";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind(), ErrorKind::IncompleteKeyValue);
assert_eq!(err.position(), 10);
assert_eq!(
err.to_string(),
"incomplete key-value pair before end of input at position 10"
);
}
#[test]
fn missing_trailing_semicolon() {
let input = "http::host=localhost;port=9000";
let config = parse_conf_str(input).unwrap();
assert_eq!(config.service(), "http");
assert_eq!(config.get("host"), Some("localhost"));
assert_eq!(config.get("port"), Some("9000"));
}
#[test]
fn escaped_semicolon_missing_trailing() {
let input = "http::host=localhost;port=9000;;";
let config = parse_conf_str(input).unwrap();
assert_eq!(config.service(), "http");
assert_eq!(config.get("host"), Some("localhost"));
assert_eq!(config.get("port"), Some("9000;"));
}
#[test]
fn escaped_semicolon() -> Result<(), ParsingError> {
let input = "FTP::HOSTS=abc.com;;def.com;;ghi.net;PORTS=9000;;8000;;7000;;;";
let config = parse_conf_str(input)?;
assert_eq!(config.service(), "FTP");
assert_eq!(config.get("HOSTS"), Some("abc.com;def.com;ghi.net"));
assert_eq!(config.get("PORTS"), Some("9000;8000;7000;"));
Ok(())
}
#[test]
fn byte_not_char_position() {
let input = "p::n=静;:=42;";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind(), ErrorKind::ExpectedIdentifierNot(':'));
assert_eq!(err.position(), 9);
assert_eq!(
err.to_string(),
"expected identifier to start with ascii letter, not ':' at position 9"
);
}
#[test]
fn unicode_service_name() {
let input = "協定";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind(), ErrorKind::ExpectedIdentifierNot('協'));
assert_eq!(err.position(), 0);
assert_eq!(
err.to_string(),
"expected identifier to start with ascii letter, not '協' at position 0"
);
}
#[test]
fn unicode_second_key_letter() {
let input = "http::x協定=42;";
let config = parse_conf_str(input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind(), ErrorKind::MustBeAlphanumeric('協'));
assert_eq!(err.position(), 7);
assert_eq!(
err.to_string(),
"must be alphanumeric, not '協' at position 7"
);
}
#[test]
fn unicode_value() -> Result<(), ParsingError> {
let input = "http::x=協定;";
let config = parse_conf_str(input)?;
assert_eq!(config.service(), "http");
assert_eq!(config.get("x"), Some("協定"));
Ok(())
}
#[test]
fn invalid_ctrl_chars_in_value() {
let bad_chars = [
'\x00', '\x01', '\x02', '\x03', '\x04', '\x1f', '\x7f', '\u{80}', '\u{8a}', '\u{9f}',
];
for bad in bad_chars {
let input = format!("http::x={};", bad);
let config = parse_conf_str(&input);
assert!(config.is_err());
let err = config.unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidCharInValue(bad));
assert_eq!(err.position(), 8);
assert_eq!(
err.to_string(),
format!("invalid char {:?} in value at position 8", bad)
);
}
}
#[test]
fn empty_value() -> Result<(), ParsingError> {
let input = "http::x=;";
let config = parse_conf_str(input)?;
assert_eq!(config.service(), "http");
assert_eq!(config.get("x"), Some(""));
Ok(())
}