Merge branch 'main' into cp-lb

This commit is contained in:
Sylvestre Ledru
2022-03-05 10:33:43 +01:00
committed by GitHub
10 changed files with 939 additions and 355 deletions
Generated
-10
View File
@@ -890,15 +890,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.2.4"
@@ -2905,7 +2896,6 @@ version = "0.0.12"
dependencies = [
"chrono",
"clap 3.0.10",
"getopts",
"itertools",
"quick-error",
"regex",
+336 -2
View File
@@ -5,7 +5,7 @@
//
// For the full copyright and license information, please view the LICENSE file
// that was distributed with this source code.
// spell-checker:ignore itotal iused iavail ipcent pcent
// spell-checker:ignore itotal iused iavail ipcent pcent tmpfs squashfs
mod table;
#[cfg(unix)]
@@ -106,6 +106,11 @@ impl From<&ArgMatches> for BlockSize {
}
}
/// Parameters that control the behavior of `df`.
///
/// Most of these parameters control which rows and which columns are
/// displayed. The `block_size` determines the units to use when
/// displaying numbers of bytes or inodes.
#[derive(Default)]
struct Options {
show_local_fs: bool,
@@ -115,6 +120,9 @@ struct Options {
show_inode_instead: bool,
block_size: BlockSize,
fs_selector: FsSelector,
/// Whether to show a final row comprising the totals for each column.
show_total: bool,
}
impl Options {
@@ -128,6 +136,7 @@ impl Options {
show_inode_instead: matches.is_present(OPT_INODES),
block_size: BlockSize::from(matches),
fs_selector: FsSelector::from(matches),
show_total: matches.is_present(OPT_TOTAL),
}
}
}
@@ -307,9 +316,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
.filter(|fs| fs.usage.blocks != 0 || opt.show_all_fs || opt.show_listed_fs)
.map(Into::into)
.collect();
println!("{}", Header::new(&opt));
let mut total = Row::new("total");
for row in data {
println!("{}", DisplayRow::new(row, &opt));
println!("{}", DisplayRow::new(&row, &opt));
total += row;
}
if opt.show_total {
println!("{}", DisplayRow::new(&total, &opt));
}
Ok(())
@@ -431,3 +446,322 @@ pub fn uu_app<'a>() -> App<'a> {
)
.arg(Arg::new(OPT_PATHS).multiple_occurrences(true))
}
#[cfg(test)]
mod tests {
mod mount_info_lt {
use crate::mount_info_lt;
use uucore::fsext::MountInfo;
/// Instantiate a [`MountInfo`] with the given fields.
fn mount_info(dev_name: &str, mount_root: &str, mount_dir: &str) -> MountInfo {
MountInfo {
dev_id: String::new(),
dev_name: String::from(dev_name),
fs_type: String::new(),
mount_dir: String::from(mount_dir),
mount_option: String::new(),
mount_root: String::from(mount_root),
remote: false,
dummy: false,
}
}
#[test]
fn test_absolute() {
// Prefer device name "/dev/foo" over "dev_foo".
let m1 = mount_info("/dev/foo", "/", "/mnt/bar");
let m2 = mount_info("dev_foo", "/", "/mnt/bar");
assert!(!mount_info_lt(&m1, &m2));
}
#[test]
fn test_shorter() {
// Prefer mount directory "/mnt/bar" over "/mnt/bar/baz"...
let m1 = mount_info("/dev/foo", "/", "/mnt/bar");
let m2 = mount_info("/dev/foo", "/", "/mnt/bar/baz");
assert!(!mount_info_lt(&m1, &m2));
// ..but prefer mount root "/root" over "/".
let m1 = mount_info("/dev/foo", "/root", "/mnt/bar");
let m2 = mount_info("/dev/foo", "/", "/mnt/bar/baz");
assert!(mount_info_lt(&m1, &m2));
}
#[test]
fn test_over_mounted() {
// Prefer the earlier entry if the devices are different but
// the mount directory is the same.
let m1 = mount_info("/dev/foo", "/", "/mnt/baz");
let m2 = mount_info("/dev/bar", "/", "/mnt/baz");
assert!(!mount_info_lt(&m1, &m2));
}
}
mod is_best {
use crate::is_best;
use uucore::fsext::MountInfo;
/// Instantiate a [`MountInfo`] with the given fields.
fn mount_info(dev_id: &str, mount_dir: &str) -> MountInfo {
MountInfo {
dev_id: String::from(dev_id),
dev_name: String::new(),
fs_type: String::new(),
mount_dir: String::from(mount_dir),
mount_option: String::new(),
mount_root: String::new(),
remote: false,
dummy: false,
}
}
#[test]
fn test_empty() {
let m = mount_info("0", "/mnt/bar");
assert!(is_best(&[], &m));
}
#[test]
fn test_different_dev_id() {
let m1 = mount_info("0", "/mnt/bar");
let m2 = mount_info("1", "/mnt/bar");
assert!(is_best(&[m1.clone()], &m2));
assert!(is_best(&[m2], &m1));
}
#[test]
fn test_same_dev_id() {
// There are several conditions under which a `MountInfo` is
// considered "better" than the others, we're just checking
// one condition in this test.
let m1 = mount_info("0", "/mnt/bar");
let m2 = mount_info("0", "/mnt/bar/baz");
assert!(!is_best(&[m1.clone()], &m2));
assert!(is_best(&[m2], &m1));
}
}
mod is_included {
use crate::{is_included, FsSelector, Options};
use std::collections::HashSet;
use uucore::fsext::MountInfo;
/// Instantiate a [`MountInfo`] with the given fields.
fn mount_info(fs_type: &str, mount_dir: &str, remote: bool, dummy: bool) -> MountInfo {
MountInfo {
dev_id: String::new(),
dev_name: String::new(),
fs_type: String::from(fs_type),
mount_dir: String::from(mount_dir),
mount_option: String::new(),
mount_root: String::new(),
remote,
dummy,
}
}
#[test]
fn test_remote_included() {
let opt = Default::default();
let paths = [];
let m = mount_info("ext4", "/mnt/foo", true, false);
assert!(is_included(&m, &paths, &opt));
}
#[test]
fn test_remote_excluded() {
let opt = Options {
show_local_fs: true,
..Default::default()
};
let paths = [];
let m = mount_info("ext4", "/mnt/foo", true, false);
assert!(!is_included(&m, &paths, &opt));
}
#[test]
fn test_dummy_included() {
let opt = Options {
show_all_fs: true,
show_listed_fs: true,
..Default::default()
};
let paths = [];
let m = mount_info("ext4", "/mnt/foo", false, true);
assert!(is_included(&m, &paths, &opt));
}
#[test]
fn test_dummy_excluded() {
let opt = Default::default();
let paths = [];
let m = mount_info("ext4", "/mnt/foo", false, true);
assert!(!is_included(&m, &paths, &opt));
}
#[test]
fn test_exclude_match() {
let exclude = HashSet::from([String::from("ext4")]);
let fs_selector = FsSelector {
exclude,
..Default::default()
};
let opt = Options {
fs_selector,
..Default::default()
};
let paths = [];
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(!is_included(&m, &paths, &opt));
}
#[test]
fn test_exclude_no_match() {
let exclude = HashSet::from([String::from("tmpfs")]);
let fs_selector = FsSelector {
exclude,
..Default::default()
};
let opt = Options {
fs_selector,
..Default::default()
};
let paths = [];
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(is_included(&m, &paths, &opt));
}
#[test]
fn test_include_match() {
let include = HashSet::from([String::from("ext4")]);
let fs_selector = FsSelector {
include,
..Default::default()
};
let opt = Options {
fs_selector,
..Default::default()
};
let paths = [];
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(is_included(&m, &paths, &opt));
}
#[test]
fn test_include_no_match() {
let include = HashSet::from([String::from("tmpfs")]);
let fs_selector = FsSelector {
include,
..Default::default()
};
let opt = Options {
fs_selector,
..Default::default()
};
let paths = [];
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(!is_included(&m, &paths, &opt));
}
#[test]
fn test_include_and_exclude_match_neither() {
let include = HashSet::from([String::from("tmpfs")]);
let exclude = HashSet::from([String::from("squashfs")]);
let fs_selector = FsSelector { include, exclude };
let opt = Options {
fs_selector,
..Default::default()
};
let paths = [];
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(!is_included(&m, &paths, &opt));
}
#[test]
fn test_include_and_exclude_match_exclude() {
let include = HashSet::from([String::from("tmpfs")]);
let exclude = HashSet::from([String::from("ext4")]);
let fs_selector = FsSelector { include, exclude };
let opt = Options {
fs_selector,
..Default::default()
};
let paths = [];
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(!is_included(&m, &paths, &opt));
}
#[test]
fn test_include_and_exclude_match_include() {
let include = HashSet::from([String::from("ext4")]);
let exclude = HashSet::from([String::from("squashfs")]);
let fs_selector = FsSelector { include, exclude };
let opt = Options {
fs_selector,
..Default::default()
};
let paths = [];
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(is_included(&m, &paths, &opt));
}
#[test]
fn test_include_and_exclude_match_both() {
// TODO The same filesystem type in both `include` and
// `exclude` should cause an error, but currently does
// not.
let include = HashSet::from([String::from("ext4")]);
let exclude = HashSet::from([String::from("ext4")]);
let fs_selector = FsSelector { include, exclude };
let opt = Options {
fs_selector,
..Default::default()
};
let paths = [];
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(!is_included(&m, &paths, &opt));
}
#[test]
fn test_paths_empty() {
let opt = Default::default();
let paths = [];
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(is_included(&m, &paths, &opt));
}
#[test]
fn test_not_in_paths() {
let opt = Default::default();
let paths = [String::from("/mnt/foo")];
let m = mount_info("ext4", "/mnt/bar", false, false);
assert!(!is_included(&m, &paths, &opt));
}
#[test]
fn test_in_paths() {
let opt = Default::default();
let paths = [String::from("/mnt/foo")];
let m = mount_info("ext4", "/mnt/foo", false, false);
assert!(is_included(&m, &paths, &opt));
}
}
mod filter_mount_list {
use crate::filter_mount_list;
#[test]
fn test_empty() {
let opt = Default::default();
let paths = [];
let mount_infos = vec![];
assert!(filter_mount_list(mount_infos, &paths, &opt).is_empty());
}
}
}
+65 -7
View File
@@ -15,6 +15,7 @@ use crate::{BlockSize, Filesystem, Options};
use uucore::fsext::{FsUsage, MountInfo};
use std::fmt;
use std::ops::AddAssign;
/// A row in the filesystem usage data table.
///
@@ -67,6 +68,63 @@ pub(crate) struct Row {
inodes_usage: Option<f64>,
}
impl Row {
pub(crate) fn new(source: &str) -> Self {
Self {
fs_device: source.into(),
fs_type: "-".into(),
fs_mount: "-".into(),
bytes: 0,
bytes_used: 0,
bytes_free: 0,
bytes_usage: None,
#[cfg(target_os = "macos")]
bytes_capacity: None,
inodes: 0,
inodes_used: 0,
inodes_free: 0,
inodes_usage: None,
}
}
}
impl AddAssign for Row {
/// Sum the numeric values of two rows.
///
/// The `Row::fs_device` field is set to `"total"` and the
/// remaining `String` fields are set to `"-"`.
fn add_assign(&mut self, rhs: Self) {
let bytes = self.bytes + rhs.bytes;
let bytes_used = self.bytes_used + rhs.bytes_used;
let inodes = self.inodes + rhs.inodes;
let inodes_used = self.inodes_used + rhs.inodes_used;
*self = Self {
fs_device: "total".into(),
fs_type: "-".into(),
fs_mount: "-".into(),
bytes,
bytes_used,
bytes_free: self.bytes_free + rhs.bytes_free,
bytes_usage: if bytes == 0 {
None
} else {
Some(bytes_used as f64 / bytes as f64)
},
// TODO Figure out how to compute this.
#[cfg(target_os = "macos")]
bytes_capacity: None,
inodes,
inodes_used,
inodes_free: self.inodes_free + rhs.inodes_free,
inodes_usage: if inodes == 0 {
None
} else {
Some(inodes_used as f64 / inodes as f64)
},
}
}
}
impl From<Filesystem> for Row {
fn from(fs: Filesystem) -> Self {
let MountInfo {
@@ -120,7 +178,7 @@ impl From<Filesystem> for Row {
/// The `options` control how the information in the row gets displayed.
pub(crate) struct DisplayRow<'a> {
/// The data in this row.
row: Row,
row: &'a Row,
/// Options that control how to display the data.
options: &'a Options,
@@ -135,7 +193,7 @@ pub(crate) struct DisplayRow<'a> {
impl<'a> DisplayRow<'a> {
/// Instantiate this struct.
pub(crate) fn new(row: Row, options: &'a Options) -> Self {
pub(crate) fn new(row: &'a Row, options: &'a Options) -> Self {
Self { row, options }
}
@@ -354,7 +412,7 @@ mod tests {
inodes_usage: Some(0.2),
};
assert_eq!(
DisplayRow::new(row, &options).to_string(),
DisplayRow::new(&row, &options).to_string(),
"my_device 100 25 75 25% my_mount "
);
}
@@ -385,7 +443,7 @@ mod tests {
inodes_usage: Some(0.2),
};
assert_eq!(
DisplayRow::new(row, &options).to_string(),
DisplayRow::new(&row, &options).to_string(),
"my_device my_type 100 25 75 25% my_mount "
);
}
@@ -416,7 +474,7 @@ mod tests {
inodes_usage: Some(0.2),
};
assert_eq!(
DisplayRow::new(row, &options).to_string(),
DisplayRow::new(&row, &options).to_string(),
"my_device 10 2 8 20% my_mount "
);
}
@@ -447,7 +505,7 @@ mod tests {
inodes_usage: Some(0.2),
};
assert_eq!(
DisplayRow::new(row, &options).to_string(),
DisplayRow::new(&row, &options).to_string(),
"my_device my_type 4.0k 1.0k 3.0k 25% my_mount "
);
}
@@ -478,7 +536,7 @@ mod tests {
inodes_usage: Some(0.2),
};
assert_eq!(
DisplayRow::new(row, &options).to_string(),
DisplayRow::new(&row, &options).to_string(),
"my_device my_type 4.0Ki 1.0Ki 3.0Ki 25% my_mount "
);
}
-1
View File
@@ -17,7 +17,6 @@ path = "src/pr.rs"
[dependencies]
clap = { version = "3.0", features = ["wrap_help", "cargo"] }
uucore = { version=">=0.0.7", package="uucore", path="../../uucore", features=["entries"] }
getopts = "0.2.21"
chrono = "0.4.19"
quick-error = "2.0.1"
itertools = "0.10.0"
+300 -334
View File
File diff suppressed because it is too large Load Diff
+72
View File
@@ -859,6 +859,11 @@ where
///
/// This function returns an error if there is a problem reading from
/// `reader` or writing to one of the output files.
///
/// # See also
///
/// * [`kth_chunk_by_line`], which splits its input in the same way,
/// but writes only one specified chunk to stdout.
fn split_into_n_chunks_by_line<R>(
settings: &Settings,
reader: &mut R,
@@ -915,6 +920,67 @@ where
Ok(())
}
/// Print the k-th chunk of a file, splitting by line.
///
/// This function is like [`split_into_n_chunks_by_line`], but instead
/// of writing each chunk to its own file, it only writes to stdout
/// the contents of the chunk identified by `chunk_number`.
///
/// # Errors
///
/// This function returns an error if there is a problem reading from
/// `reader` or writing to one of the output files.
///
/// # See also
///
/// * [`split_into_n_chunks_by_line`], which splits its input in the
/// same way, but writes each chunk to its own file.
fn kth_chunk_by_line<R>(
settings: &Settings,
reader: &mut R,
chunk_number: u64,
num_chunks: u64,
) -> UResult<()>
where
R: BufRead,
{
// Get the size of the input file in bytes and compute the number
// of bytes per chunk.
let metadata = metadata(&settings.input).unwrap();
let num_bytes = metadata.len();
let chunk_size = (num_bytes / (num_chunks as u64)) as usize;
// Write to stdout instead of to a file.
let stdout = std::io::stdout();
let mut writer = stdout.lock();
let mut num_bytes_remaining_in_current_chunk = chunk_size;
let mut i = 0;
for line_result in reader.lines() {
let line = line_result?;
let bytes = line.as_bytes();
if i == chunk_number {
writer.write_all(bytes)?;
writer.write_all(b"\n")?;
}
// Add one byte for the newline character.
let num_bytes = bytes.len() + 1;
if num_bytes >= num_bytes_remaining_in_current_chunk {
num_bytes_remaining_in_current_chunk = chunk_size;
i += 1;
} else {
num_bytes_remaining_in_current_chunk -= num_bytes;
}
if i > chunk_number {
break;
}
}
Ok(())
}
fn split(settings: &Settings) -> UResult<()> {
let mut reader = BufReader::new(if settings.input == "-" {
Box::new(stdin()) as Box<dyn Read>
@@ -935,6 +1001,12 @@ fn split(settings: &Settings) -> UResult<()> {
Strategy::Number(NumberType::Lines(num_chunks)) => {
split_into_n_chunks_by_line(settings, &mut reader, num_chunks)
}
Strategy::Number(NumberType::KthLines(chunk_number, num_chunks)) => {
// The chunk number is given as a 1-indexed number, but it
// is a little easier to deal with a 0-indexed number.
let chunk_number = chunk_number - 1;
kth_chunk_by_line(settings, &mut reader, chunk_number, num_chunks)
}
Strategy::Number(_) => Err(USimpleError::new(1, "-n mode not yet fully implemented")),
Strategy::Lines(chunk_size) => {
let mut writer = LineChunkWriter::new(chunk_size, settings)
+44
View File
@@ -1,3 +1,4 @@
// spell-checker:ignore udev
use crate::common::util::*;
#[test]
@@ -77,4 +78,47 @@ fn test_type_option() {
new_ucmd!().args(&["-t", "ext4", "-t", "ext3"]).succeeds();
}
#[test]
fn test_total() {
// Example output:
//
// Filesystem 1K-blocks Used Available Use% Mounted on
// udev 3858016 0 3858016 0% /dev
// ...
// /dev/loop14 63488 63488 0 100% /snap/core20/1361
// total 258775268 98099712 148220200 40% -
let output = new_ucmd!().arg("--total").succeeds().stdout_move_str();
// Skip the header line.
let lines: Vec<&str> = output.lines().skip(1).collect();
// Parse the values from the last row.
let last_line = lines.last().unwrap();
let mut iter = last_line.split_whitespace();
assert_eq!(iter.next().unwrap(), "total");
let reported_total_size = iter.next().unwrap().parse().unwrap();
let reported_total_used = iter.next().unwrap().parse().unwrap();
let reported_total_avail = iter.next().unwrap().parse().unwrap();
// Loop over each row except the last, computing the sum of each column.
let mut computed_total_size = 0;
let mut computed_total_used = 0;
let mut computed_total_avail = 0;
let n = lines.len();
for line in &lines[..n - 1] {
let mut iter = line.split_whitespace();
iter.next().unwrap();
computed_total_size += iter.next().unwrap().parse::<u64>().unwrap();
computed_total_used += iter.next().unwrap().parse::<u64>().unwrap();
computed_total_avail += iter.next().unwrap().parse::<u64>().unwrap();
}
// Check that the sum of each column matches the reported value in
// the last row.
assert_eq!(computed_total_size, reported_total_size);
assert_eq!(computed_total_used, reported_total_used);
assert_eq!(computed_total_avail, reported_total_avail);
// TODO We could also check here that the use percentage matches.
}
// ToDO: more tests...
+13
View File
@@ -448,3 +448,16 @@ fn test_with_join_lines_option() {
&valid_last_modified_template_vars(start),
);
}
#[test]
fn test_value_for_number_lines() {
// *5 is of the form [SEP[NUMBER]] so is accepted and succeeds
new_ucmd!().args(&["-n", "*5", "test.log"]).succeeds();
// a is of the form [SEP[NUMBER]] so is accepted and succeeds
new_ucmd!().args(&["-n", "a", "test.log"]).succeeds();
// foo5.txt is of not the form [SEP[NUMBER]] so is not used as value.
// Therefore, pr tries to access the file, which does not exist.
new_ucmd!().args(&["-n", "foo5.txt", "test.log"]).fails();
}
+9 -1
View File
@@ -2,7 +2,7 @@
// *
// * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code.
// spell-checker:ignore xzaaa sixhundredfiftyonebytes ninetyonebytes threebytes asciilowercase fghij klmno pqrst uvwxyz fivelines twohundredfortyonebytes
// spell-checker:ignore xzaaa sixhundredfiftyonebytes ninetyonebytes threebytes asciilowercase fghij klmno pqrst uvwxyz fivelines twohundredfortyonebytes onehundredlines
extern crate rand;
extern crate regex;
@@ -587,3 +587,11 @@ fn test_lines() {
assert_eq!(file_read("xaa"), "1\n2\n3\n");
assert_eq!(file_read("xab"), "4\n5\n");
}
#[test]
fn test_lines_kth() {
new_ucmd!()
.args(&["-n", "l/3/10", "onehundredlines.txt"])
.succeeds()
.stdout_only("20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n");
}
+100
View File
@@ -0,0 +1,100 @@
00
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99