1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-09-16 03:36:18 +00:00

tail: improve performance of piped stdin

Rewrite handling of stdin when it is piped and read input in chunks.

Fixes https://github.com/uutils/coreutils/issues/3842
This commit is contained in:
Joining7943 2022-09-09 13:50:59 +02:00 committed by Sylvestre Ledru
parent b39f5239e7
commit 2658f8ae5b
7 changed files with 1704 additions and 83 deletions

View file

@ -3,7 +3,7 @@
// * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code.
// spell-checker:ignore (ToDO) abcdefghijklmnopqrstuvwxyz efghijklmnopqrstuvwxyz vwxyz emptyfile file siette ocho nueve diez
// spell-checker:ignore (ToDO) abcdefghijklmnopqrstuvwxyz efghijklmnopqrstuvwxyz vwxyz emptyfile file siette ocho nueve diez MULT
// spell-checker:ignore (libs) kqueue
// spell-checker:ignore (jargon) tailable untailable
@ -1090,18 +1090,6 @@ fn test_invalid_num() {
.fails()
.stderr_str()
.starts_with("tail: invalid number of lines: '1Y': Value too large for defined data type");
#[cfg(target_pointer_width = "32")]
{
let sizes = ["1000G", "10T"];
for size in &sizes {
new_ucmd!()
.args(&["-c", size])
.fails()
.code_is(1)
.stderr_str()
.starts_with("tail: Insufficient addressable memory");
}
}
new_ucmd!()
.args(&["-c", ""])
.fails()
@ -2484,6 +2472,725 @@ fn test_illegal_seek() {
assert_eq!(p.wait().unwrap().code().unwrap(), 1);
}
#[cfg(all(not(target_os = "android"), not(target_os = "windows")))] // FIXME: See https://github.com/uutils/coreutils/issues/3881
mod pipe_tests {
use super::*;
use crate::common::random::*;
use rand::distributions::Alphanumeric;
use tail::chunks::BUFFER_SIZE as CHUNK_BUFFER_SIZE;
#[test]
fn test_pipe_when_lines_option_value_is_higher_than_contained_lines() {
let test_string = "a\nb\n";
new_ucmd!()
.args(&["-n", "3"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(test_string);
new_ucmd!()
.args(&["-n", "4"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(test_string);
new_ucmd!()
.args(&["-n", "999"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(test_string);
new_ucmd!()
.args(&["-n", "+3"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
new_ucmd!()
.args(&["-n", "+4"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
new_ucmd!()
.args(&["-n", "+999"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
}
#[test]
fn test_pipe_when_negative_lines_option_given_no_newline_at_eof() {
let test_string = "a\nb";
new_ucmd!()
.args(&["-n", "0"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
new_ucmd!()
.args(&["-n", "1"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("b");
new_ucmd!()
.args(&["-n", "2"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("a\nb");
}
#[test]
fn test_pipe_when_positive_lines_option_given_no_newline_at_eof() {
let test_string = "a\nb";
new_ucmd!()
.args(&["-n", "+0"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("a\nb");
new_ucmd!()
.args(&["-n", "+1"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("a\nb");
new_ucmd!()
.args(&["-n", "+2"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("b");
}
#[test]
fn test_pipe_when_lines_option_given_multibyte_utf8_characters() {
// the test string consists of from left to right a 4-byte,3-byte,2-byte,1-byte utf-8 character
let test_string = "𝅘𝅥𝅮\n\nƒ\na";
new_ucmd!()
.args(&["-n", "+0"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(test_string);
new_ucmd!()
.args(&["-n", "+2"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("\nƒ\na");
new_ucmd!()
.args(&["-n", "+3"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("ƒ\na");
new_ucmd!()
.args(&["-n", "+4"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("a");
new_ucmd!()
.args(&["-n", "+5"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
new_ucmd!()
.args(&["-n", "-4"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(test_string);
new_ucmd!()
.args(&["-n", "-3"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("\nƒ\na");
new_ucmd!()
.args(&["-n", "-2"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("ƒ\na");
new_ucmd!()
.args(&["-n", "-1"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("a");
new_ucmd!()
.args(&["-n", "-0"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
}
#[test]
fn test_pipe_when_lines_option_given_input_size_is_equal_to_buffer_size_no_newline_at_eof() {
let total_lines = 1;
let random_string = RandomString::generate_with_delimiter(
Alphanumeric,
b'\n',
total_lines,
false,
CHUNK_BUFFER_SIZE,
);
let random_string = random_string.as_str();
let lines = random_string.split_inclusive('\n');
let expected = lines.clone().skip(1).collect::<String>();
new_ucmd!()
.args(&["-n", "+2"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(expected);
let expected = lines.clone().skip(1).collect::<String>();
new_ucmd!()
.args(&["-n", "-1"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(expected);
}
#[test]
fn test_pipe_when_lines_option_given_input_size_is_equal_to_buffer_size() {
let total_lines = 100;
let random_string = RandomString::generate_with_delimiter(
Alphanumeric,
b'\n',
total_lines,
true,
CHUNK_BUFFER_SIZE,
);
let random_string = random_string.as_str();
let lines = random_string.split_inclusive('\n');
new_ucmd!()
.args(&["-n", "+0"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(random_string);
let expected = lines.clone().skip(1).collect::<String>();
new_ucmd!()
.args(&["-n", "+2"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(expected);
new_ucmd!()
.args(&["-n", "-0"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
let expected = lines.clone().skip(total_lines - 1).collect::<String>();
new_ucmd!()
.args(&["-n", "-1"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(expected);
let expected = lines.clone().skip(1).collect::<String>();
new_ucmd!()
.args(&["-n", "-99"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(expected);
new_ucmd!()
.args(&["-n", "-100"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(random_string);
}
#[test]
fn test_pipe_when_lines_option_given_input_size_is_one_byte_greater_than_buffer_size() {
let total_lines = 100;
let random_string = RandomString::generate_with_delimiter(
Alphanumeric,
b'\n',
total_lines,
true,
CHUNK_BUFFER_SIZE + 1,
);
let random_string = random_string.as_str();
let lines = random_string.split_inclusive('\n');
new_ucmd!()
.args(&["-n", "+0"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(random_string);
let expected = lines.clone().skip(total_lines - 1).collect::<String>();
new_ucmd!()
.args(&["-n", "-1"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(expected);
let expected = lines.clone().skip(1).collect::<String>();
new_ucmd!()
.args(&["-n", "+2"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(expected);
let expected = lines.clone().skip(1).collect::<String>();
new_ucmd!()
.args(&["-n", "-99"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(expected);
}
#[test]
fn test_pipe_when_lines_option_given_input_size_has_multiple_size_of_buffer_size() {
let total_lines = 100;
let random_string = RandomString::generate_with_delimiter(
Alphanumeric,
b'\n',
total_lines,
true,
CHUNK_BUFFER_SIZE * 3 + 1,
);
let random_string = random_string.as_str();
let lines = random_string.split_inclusive('\n');
new_ucmd!()
.args(&["-n", "+0"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(random_string);
let expected = lines.clone().skip(1).collect::<String>();
new_ucmd!()
.args(&["-n", "+2"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(expected);
new_ucmd!()
.args(&["-n", "-0"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
let expected = lines.clone().skip(total_lines - 1).collect::<String>();
new_ucmd!()
.args(&["-n", "-1"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(expected);
let expected = lines.clone().skip(1).collect::<String>();
new_ucmd!()
.args(&["-n", "-99"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(expected);
new_ucmd!()
.args(&["-n", "-100"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(random_string);
}
#[test]
fn test_pipe_when_bytes_option_value_is_higher_than_contained_bytes() {
let test_string = "a\nb";
new_ucmd!()
.args(&["-c", "4"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(test_string);
new_ucmd!()
.args(&["-c", "5"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(test_string);
new_ucmd!()
.args(&["-c", "999"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(test_string);
new_ucmd!()
.args(&["-c", "+4"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
new_ucmd!()
.args(&["-c", "+5"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
new_ucmd!()
.args(&["-c", "+999"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
}
#[test]
fn test_pipe_when_bytes_option_given_multibyte_utf8_characters() {
// the test string consists of from left to right a 4-byte,3-byte,2-byte,1-byte utf-8 character
let test_string = "𝅘𝅥𝅮⏻ƒa";
new_ucmd!()
.args(&["-c", "+0"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(test_string);
new_ucmd!()
.args(&["-c", "+2"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(&test_string.as_bytes()[1..]);
new_ucmd!()
.args(&["-c", "+5"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("⏻ƒa");
new_ucmd!()
.args(&["-c", "+8"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("ƒa");
new_ucmd!()
.args(&["-c", "+10"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("a");
new_ucmd!()
.args(&["-c", "+11"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
new_ucmd!()
.args(&["-c", "-1"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("a");
new_ucmd!()
.args(&["-c", "-2"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(&"ƒa".as_bytes()[1..]);
new_ucmd!()
.args(&["-c", "-3"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("ƒa");
new_ucmd!()
.args(&["-c", "-6"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only("⏻ƒa");
new_ucmd!()
.args(&["-c", "-10"])
.pipe_in(test_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(test_string);
}
#[test]
fn test_pipe_when_bytes_option_given_input_size_is_equal_to_buffer_size() {
let random_string = RandomString::generate(AlphanumericNewline, CHUNK_BUFFER_SIZE);
let random_string = random_string.as_str();
new_ucmd!()
.args(&["-c", "+0"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(random_string);
let expected = &random_string.as_bytes()[1..];
new_ucmd!()
.args(&["-c", "+2"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
new_ucmd!()
.args(&["-c", "-0"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
let expected = &random_string.as_bytes()[1..];
new_ucmd!()
.args(&["-c", "-8191"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
new_ucmd!()
.args(&["-c", "-8192"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(random_string);
new_ucmd!()
.args(&["-c", "-8193"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(random_string);
let expected = &random_string.as_bytes()[CHUNK_BUFFER_SIZE - 1..];
new_ucmd!()
.args(&["-c", "-1"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
}
#[test]
fn test_pipe_when_bytes_option_given_input_size_is_one_byte_greater_than_buffer_size() {
let random_string = RandomString::generate(AlphanumericNewline, CHUNK_BUFFER_SIZE + 1);
let random_string = random_string.as_str();
new_ucmd!()
.args(&["-c", "+0"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(random_string);
let expected = &random_string.as_bytes()[1..];
new_ucmd!()
.args(&["-c", "+2"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
new_ucmd!()
.args(&["-c", "-0"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
let expected = &random_string.as_bytes()[CHUNK_BUFFER_SIZE..];
new_ucmd!()
.args(&["-c", "-1"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
let expected = &random_string.as_bytes()[1..];
new_ucmd!()
.args(&["-c", "-8192"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
new_ucmd!()
.args(&["-c", "-8193"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(random_string);
}
#[test]
fn test_pipe_when_bytes_option_given_input_size_has_multiple_size_of_buffer_size() {
let random_string = RandomString::generate(AlphanumericNewline, CHUNK_BUFFER_SIZE * 3);
let random_string = random_string.as_str();
new_ucmd!()
.args(&["-c", "+0"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(random_string);
new_ucmd!()
.args(&["-c", "-0"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.no_stdout()
.no_stderr();
let expected = &random_string.as_bytes()[8192..];
new_ucmd!()
.args(&["-c", "+8193"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
let expected = &random_string.as_bytes()[8193..];
new_ucmd!()
.args(&["-c", "+8194"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
let expected = &random_string.as_bytes()[16384..];
new_ucmd!()
.args(&["-c", "+16385"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
let expected = &random_string.as_bytes()[16385..];
new_ucmd!()
.args(&["-c", "+16386"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
let expected = &random_string.as_bytes()[16384..];
new_ucmd!()
.args(&["-c", "-8192"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
let expected = &random_string.as_bytes()[16383..];
new_ucmd!()
.args(&["-c", "-8193"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
let expected = &random_string.as_bytes()[8192..];
new_ucmd!()
.args(&["-c", "-16384"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
let expected = &random_string.as_bytes()[8191..];
new_ucmd!()
.args(&["-c", "-16385"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only_bytes(expected);
new_ucmd!()
.args(&["-c", "-24576"])
.pipe_in(random_string)
.ignore_stdin_write_error()
.succeeds()
.stdout_only(random_string);
}
}
#[test]
fn test_seek_bytes_backward_outside_file() {
new_ucmd!()

View file

@ -1,3 +1,4 @@
#[macro_use]
pub mod macros;
pub mod random;
pub mod util;

314
tests/common/random.rs Normal file
View file

@ -0,0 +1,314 @@
// * This file is part of the uutils coreutils package.
// *
// * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code.
use rand::distributions::{Distribution, Uniform};
use rand::{thread_rng, Rng};
/// Samples alphanumeric characters `[A-Za-z0-9]` including newline `\n`
///
/// # Examples
///
/// ```rust,ignore
/// use rand::{Rng, thread_rng};
///
/// let vec = thread_rng()
/// .sample_iter(AlphanumericNewline)
/// .take(10)
/// .collect::<Vec<u8>>();
/// println!("Random chars: {}", String::from_utf8(vec).unwrap());
/// ```
#[derive(Clone, Copy, Debug)]
pub struct AlphanumericNewline;
impl AlphanumericNewline {
/// The charset to act upon
const CHARSET: &'static [u8] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\n";
/// Generate a random byte from [`Self::CHARSET`] and return it as `u8`.
///
/// # Arguments
///
/// * `rng`: A [`rand::Rng`]
///
/// returns: u8
fn random<R>(rng: &mut R) -> u8
where
R: Rng + ?Sized,
{
let idx = rng.gen_range(0..Self::CHARSET.len());
Self::CHARSET[idx]
}
}
impl Distribution<u8> for AlphanumericNewline {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> u8 {
Self::random(rng)
}
}
/// Generate a random string from a [`Distribution`]
///
/// # Examples
///
/// ```rust,ignore
/// use crate::common::random::{AlphanumericNewline, RandomString};
/// use rand::distributions::Alphanumeric;
///
/// // generates a 100 byte string with characters from AlphanumericNewline
/// let random_string = RandomString::generate(&AlphanumericNewline, 100);
/// assert_eq!(100, random_string.len());
///
/// // generates a 100 byte string with 10 newline characters not ending with a newline
/// let string = RandomString::generate_with_delimiter(&Alphanumeric, b'\n', 10, false, 100);
/// assert_eq!(100, random_string.len());
/// ```
pub struct RandomString;
impl RandomString {
/// Generate a random string from the given [`Distribution`] with the given `length` in bytes.
///
/// # Arguments
///
/// * `dist`: A u8 [`Distribution`]
/// * `length`: the length of the resulting string in bytes
///
/// returns: String
pub fn generate<D>(dist: D, length: usize) -> String
where
D: Distribution<u8>,
{
thread_rng()
.sample_iter(dist)
.take(length)
.map(|b| b as char)
.collect()
}
/// Generate a random string from the [`Distribution`] with the given `length` in bytes. The
/// function takes a `delimiter`, which is randomly distributed in the string, such that exactly
/// `num_delimiter` amount of `delimiter`s occur. If `end_with_delimiter` is set, then the
/// string ends with the delimiter, else the string does not end with the delimiter.
///
/// # Arguments
///
/// * `dist`: A `u8` [`Distribution`]
/// * `delimiter`: A `u8` delimiter, which does not need to be included in the `Distribution`
/// * `num_delimiter`: The number of `delimiter`s contained in the resulting string
/// * `end_with_delimiter`: If the string shall end with the given delimiter
/// * `length`: the length of the resulting string in bytes
///
/// returns: String
///
/// # Examples
///
/// ```rust,ignore
/// use crate::common::random::{AlphanumericNewline, RandomString};
///
/// // generates a 100 byte string with 10 '\0' byte characters not ending with a '\0' byte
/// let string = RandomString::generate_with_delimiter(&AlphanumericNewline, 0, 10, false, 100);
/// assert_eq!(100, random_string.len());
/// assert_eq!(
/// 10,
/// random_string.as_bytes().iter().filter(|p| **p == 0).count()
/// );
/// assert!(!random_string.as_bytes().ends_with(&[0]));
/// ```
pub fn generate_with_delimiter<D>(
dist: D,
delimiter: u8,
num_delimiter: usize,
end_with_delimiter: bool,
length: usize,
) -> String
where
D: Distribution<u8>,
{
if length == 0 {
return String::from("");
} else if length == 1 {
return if num_delimiter > 0 {
String::from(delimiter as char)
} else {
String::from(thread_rng().sample(&dist) as char)
};
}
let samples = length - 1;
let mut result: Vec<u8> = thread_rng().sample_iter(&dist).take(samples).collect();
if num_delimiter == 0 {
result.push(thread_rng().sample(&dist));
return String::from_utf8(result).unwrap();
}
let num_delimiter = if end_with_delimiter {
num_delimiter - 1
} else {
num_delimiter
};
let between = Uniform::new(0, samples);
for _ in 0..num_delimiter {
let mut pos = between.sample(&mut thread_rng());
let turn = pos;
while result[pos] == delimiter {
pos += 1;
if pos >= samples {
pos = 0;
}
if pos == turn {
break;
}
}
result[pos] = delimiter;
}
if end_with_delimiter {
result.push(delimiter);
} else {
result.push(thread_rng().sample(&dist));
}
String::from_utf8(result).unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::distributions::Alphanumeric;
#[test]
fn test_random_string_generate() {
let random_string = RandomString::generate(&AlphanumericNewline, 0);
assert_eq!(0, random_string.len());
let random_string = RandomString::generate(&AlphanumericNewline, 1);
assert_eq!(1, random_string.len());
let random_string = RandomString::generate(&AlphanumericNewline, 100);
assert_eq!(100, random_string.len());
}
#[test]
fn test_random_string_generate_with_delimiter_when_length_is_zero() {
let random_string = RandomString::generate_with_delimiter(&Alphanumeric, 0, 0, false, 0);
assert_eq!(0, random_string.len());
}
#[test]
fn test_random_string_generate_with_delimiter_when_num_delimiter_is_greater_than_length() {
let random_string = RandomString::generate_with_delimiter(&Alphanumeric, 0, 2, false, 1);
assert_eq!(1, random_string.len());
assert!(random_string.as_bytes().contains(&0));
assert!(random_string.as_bytes().ends_with(&[0]));
}
#[test]
fn test_random_string_generate_with_delimiter_should_end_with_delimiter() {
let random_string = RandomString::generate_with_delimiter(&Alphanumeric, 0, 1, true, 1);
assert_eq!(1, random_string.len());
assert_eq!(
1,
random_string.as_bytes().iter().filter(|p| **p == 0).count()
);
assert!(random_string.as_bytes().ends_with(&[0]));
let random_string = RandomString::generate_with_delimiter(&Alphanumeric, 0, 1, false, 1);
assert_eq!(1, random_string.len());
assert_eq!(
1,
random_string.as_bytes().iter().filter(|p| **p == 0).count()
);
assert!(random_string.as_bytes().ends_with(&[0]));
let random_string = RandomString::generate_with_delimiter(&Alphanumeric, 0, 1, true, 2);
assert_eq!(2, random_string.len());
assert_eq!(
1,
random_string.as_bytes().iter().filter(|p| **p == 0).count()
);
assert!(random_string.as_bytes().ends_with(&[0]));
let random_string = RandomString::generate_with_delimiter(&Alphanumeric, 0, 2, true, 2);
assert_eq!(2, random_string.len());
assert_eq!(
2,
random_string.as_bytes().iter().filter(|p| **p == 0).count()
);
assert!(random_string.as_bytes().ends_with(&[0]));
let random_string = RandomString::generate_with_delimiter(&Alphanumeric, 0, 1, true, 3);
assert_eq!(3, random_string.len());
assert_eq!(
1,
random_string.as_bytes().iter().filter(|p| **p == 0).count()
);
assert!(random_string.as_bytes().ends_with(&[0]));
}
#[test]
fn test_random_string_generate_with_delimiter_should_not_end_with_delimiter() {
let random_string = RandomString::generate_with_delimiter(&Alphanumeric, 0, 0, false, 1);
assert_eq!(1, random_string.len());
assert_eq!(
0,
random_string.as_bytes().iter().filter(|p| **p == 0).count()
);
let random_string = RandomString::generate_with_delimiter(&Alphanumeric, 0, 0, true, 1);
assert_eq!(1, random_string.len());
assert_eq!(
0,
random_string.as_bytes().iter().filter(|p| **p == 0).count()
);
let random_string = RandomString::generate_with_delimiter(&Alphanumeric, 0, 1, false, 2);
assert_eq!(2, random_string.len());
assert_eq!(
1,
random_string.as_bytes().iter().filter(|p| **p == 0).count()
);
assert!(!random_string.as_bytes().ends_with(&[0]));
let random_string = RandomString::generate_with_delimiter(&Alphanumeric, 0, 1, false, 3);
assert_eq!(3, random_string.len());
assert_eq!(
1,
random_string.as_bytes().iter().filter(|p| **p == 0).count()
);
assert!(!random_string.as_bytes().ends_with(&[0]));
let random_string = RandomString::generate_with_delimiter(&Alphanumeric, 0, 2, false, 3);
assert_eq!(3, random_string.len());
assert_eq!(
2,
random_string.as_bytes().iter().filter(|p| **p == 0).count()
);
assert!(!random_string.as_bytes().ends_with(&[0]));
}
#[test]
fn test_generate_with_delimiter_with_greater_length() {
let random_string =
RandomString::generate_with_delimiter(&Alphanumeric, 0, 100, false, 1000);
assert_eq!(1000, random_string.len());
assert_eq!(
100,
random_string.as_bytes().iter().filter(|p| **p == 0).count()
);
assert!(!random_string.as_bytes().ends_with(&[0]));
let random_string =
RandomString::generate_with_delimiter(&Alphanumeric, 0, 100, true, 1000);
assert_eq!(1000, random_string.len());
assert_eq!(
100,
random_string.as_bytes().iter().filter(|p| **p == 0).count()
);
assert!(random_string.as_bytes().ends_with(&[0]));
}
}