mirror of
https://github.com/uutils/diffutils.git
synced 2026-06-10 15:48:59 -07:00
Merge branch 'main' into context-diff-modification-time
This commit is contained in:
Generated
+7
@@ -132,6 +132,7 @@ dependencies = [
|
||||
"regex",
|
||||
"same-file",
|
||||
"tempfile",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -406,6 +407,12 @@ version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.0"
|
||||
|
||||
@@ -19,6 +19,7 @@ chrono = "0.4.35"
|
||||
diff = "0.1.10"
|
||||
regex = "1.10.4"
|
||||
same-file = "1.0.6"
|
||||
unicode-width = "0.1.11"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::io::Write;
|
||||
use std::process::Command;
|
||||
|
||||
fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result<Vec<u8>, DiffError> {
|
||||
let mut output = ed_diff::diff(expected, actual)?;
|
||||
let mut output = ed_diff::diff(expected, actual, false, false, 8)?;
|
||||
writeln!(&mut output, "w {filename}").unwrap();
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>)| {
|
||||
} else {
|
||||
return
|
||||
}*/
|
||||
let diff = normal_diff::diff(&from, &to);
|
||||
let diff = normal_diff::diff(&from, &to, false, false, 8);
|
||||
File::create("target/fuzz.file.original")
|
||||
.unwrap()
|
||||
.write_all(&from)
|
||||
|
||||
@@ -26,6 +26,9 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>, u8)| {
|
||||
&to,
|
||||
"target/fuzz.file",
|
||||
context as usize,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
File::create("target/fuzz.file.original")
|
||||
.unwrap()
|
||||
|
||||
+37
-10
@@ -6,6 +6,8 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Write;
|
||||
|
||||
use crate::utils::do_write_line;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DiffLine {
|
||||
Context(Vec<u8>),
|
||||
@@ -279,6 +281,7 @@ fn get_modification_time(file_path: &str) -> String {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn diff(
|
||||
expected: &[u8],
|
||||
expected_filename: &str,
|
||||
@@ -286,6 +289,8 @@ pub fn diff(
|
||||
actual_filename: &str,
|
||||
context_size: usize,
|
||||
stop_early: bool,
|
||||
expand_tabs: bool,
|
||||
tabsize: usize,
|
||||
) -> Vec<u8> {
|
||||
let expected_file_modified_time = get_modification_time(expected_filename);
|
||||
let actual_file_modified_time = get_modification_time(actual_filename);
|
||||
@@ -332,17 +337,20 @@ pub fn diff(
|
||||
match line {
|
||||
DiffLine::Context(e) => {
|
||||
write!(output, " ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &e, expand_tabs, tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Change(e) => {
|
||||
write!(output, "! ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &e, expand_tabs, tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Add(e) => {
|
||||
write!(output, "- ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &e, expand_tabs, tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -359,17 +367,20 @@ pub fn diff(
|
||||
match line {
|
||||
DiffLine::Context(e) => {
|
||||
write!(output, " ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &e, expand_tabs, tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Change(e) => {
|
||||
write!(output, "! ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &e, expand_tabs, tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Add(e) => {
|
||||
write!(output, "+ ").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &e, expand_tabs, tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -443,6 +454,8 @@ mod tests {
|
||||
&format!("{target}/alef"),
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
File::create(&format!("{target}/ab.diff"))
|
||||
.unwrap()
|
||||
@@ -523,6 +536,8 @@ mod tests {
|
||||
&format!("{target}/alef_"),
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
File::create(&format!("{target}/ab_.diff"))
|
||||
.unwrap()
|
||||
@@ -606,6 +621,8 @@ mod tests {
|
||||
&format!("{target}/alefx"),
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
File::create(&format!("{target}/abx.diff"))
|
||||
.unwrap()
|
||||
@@ -692,6 +709,8 @@ mod tests {
|
||||
&format!("{target}/alefr"),
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
File::create(&format!("{target}/abr.diff"))
|
||||
.unwrap()
|
||||
@@ -730,10 +749,10 @@ mod tests {
|
||||
|
||||
let from_filename = "foo";
|
||||
let _ = File::create(&format!("foo")).unwrap();
|
||||
let from = vec!["a", "b", "c", ""].join("\n");
|
||||
let from = ["a", "b", "c", ""].join("\n");
|
||||
let to_filename = "bar";
|
||||
let _ = File::create(&format!("bar")).unwrap();
|
||||
let to = vec!["a", "d", "c", ""].join("\n");
|
||||
let to = ["a", "d", "c", ""].join("\n");
|
||||
let context_size: usize = 3;
|
||||
|
||||
let diff_full = diff(
|
||||
@@ -743,13 +762,15 @@ mod tests {
|
||||
to_filename,
|
||||
context_size,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
|
||||
let diff_full_text = str::from_utf8(&diff_full).unwrap();
|
||||
let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap();
|
||||
let diff_full = re.replace_all(diff_full_text, "");
|
||||
|
||||
let expected_full = vec![
|
||||
let expected_full = [
|
||||
"*** foo\t",
|
||||
"--- bar\t",
|
||||
"***************",
|
||||
@@ -773,12 +794,14 @@ mod tests {
|
||||
to_filename,
|
||||
context_size,
|
||||
true,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
|
||||
let diff_brief_text = str::from_utf8(&diff_brief).unwrap();
|
||||
let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap();
|
||||
let diff_brief = re.replace_all(diff_brief_text, "");
|
||||
let expected_brief = vec!["*** foo\t", "--- bar\t", ""].join("\n");
|
||||
let expected_brief = ["*** foo\t", "--- bar\t", ""].join("\n");
|
||||
assert_eq!(diff_brief, expected_brief);
|
||||
|
||||
let nodiff_full = diff(
|
||||
@@ -788,6 +811,8 @@ mod tests {
|
||||
to_filename,
|
||||
context_size,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
assert!(nodiff_full.is_empty());
|
||||
|
||||
@@ -798,6 +823,8 @@ mod tests {
|
||||
to_filename,
|
||||
context_size,
|
||||
true,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
assert!(nodiff_brief.is_empty());
|
||||
}
|
||||
|
||||
+20
-12
@@ -5,6 +5,8 @@
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use crate::utils::do_write_line;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Mismatch {
|
||||
pub line_number_expected: usize,
|
||||
@@ -107,7 +109,13 @@ fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result<Vec<Mis
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result<Vec<u8>, DiffError> {
|
||||
pub fn diff(
|
||||
expected: &[u8],
|
||||
actual: &[u8],
|
||||
stop_early: bool,
|
||||
expand_tabs: bool,
|
||||
tabsize: usize,
|
||||
) -> Result<Vec<u8>, DiffError> {
|
||||
let mut output = Vec::new();
|
||||
let diff_results = make_diff(expected, actual, stop_early)?;
|
||||
if stop_early && !diff_results.is_empty() {
|
||||
@@ -145,7 +153,7 @@ pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result<Vec<u8>,
|
||||
if actual == b"." {
|
||||
writeln!(&mut output, "..\n.\ns/.//\na").unwrap();
|
||||
} else {
|
||||
output.write_all(actual).unwrap();
|
||||
do_write_line(&mut output, actual, expand_tabs, tabsize).unwrap();
|
||||
writeln!(&mut output).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -160,7 +168,7 @@ mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result<Vec<u8>, DiffError> {
|
||||
let mut output = diff(expected, actual, false)?;
|
||||
let mut output = diff(expected, actual, false, false, 8)?;
|
||||
writeln!(&mut output, "w {filename}").unwrap();
|
||||
Ok(output)
|
||||
}
|
||||
@@ -169,8 +177,8 @@ mod tests {
|
||||
fn test_basic() {
|
||||
let from = b"a\n";
|
||||
let to = b"b\n";
|
||||
let diff = diff(from, to, false).unwrap();
|
||||
let expected = vec!["1c", "b", ".", ""].join("\n");
|
||||
let diff = diff(from, to, false, false, 8).unwrap();
|
||||
let expected = ["1c", "b", ".", ""].join("\n");
|
||||
assert_eq!(diff, expected.as_bytes());
|
||||
}
|
||||
|
||||
@@ -401,21 +409,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_stop_early() {
|
||||
let from = vec!["a", "b", "c", ""].join("\n");
|
||||
let to = vec!["a", "d", "c", ""].join("\n");
|
||||
let from = ["a", "b", "c", ""].join("\n");
|
||||
let to = ["a", "d", "c", ""].join("\n");
|
||||
|
||||
let diff_full = diff(from.as_bytes(), to.as_bytes(), false).unwrap();
|
||||
let expected_full = vec!["2c", "d", ".", ""].join("\n");
|
||||
let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false, 8).unwrap();
|
||||
let expected_full = ["2c", "d", ".", ""].join("\n");
|
||||
assert_eq!(diff_full, expected_full.as_bytes());
|
||||
|
||||
let diff_brief = diff(from.as_bytes(), to.as_bytes(), true).unwrap();
|
||||
let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false, 8).unwrap();
|
||||
let expected_brief = "\0".as_bytes();
|
||||
assert_eq!(diff_brief, expected_brief);
|
||||
|
||||
let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false).unwrap();
|
||||
let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false, 8).unwrap();
|
||||
assert!(nodiff_full.is_empty());
|
||||
|
||||
let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true).unwrap();
|
||||
let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false, 8).unwrap();
|
||||
assert!(nodiff_brief.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod context_diff;
|
||||
pub mod ed_diff;
|
||||
pub mod normal_diff;
|
||||
pub mod unified_diff;
|
||||
pub mod utils;
|
||||
|
||||
// Re-export the public functions/types you need
|
||||
pub use context_diff::diff as context_diff;
|
||||
|
||||
+15
-5
@@ -15,6 +15,7 @@ mod ed_diff;
|
||||
mod normal_diff;
|
||||
mod params;
|
||||
mod unified_diff;
|
||||
mod utils;
|
||||
|
||||
// Exit codes are documented at
|
||||
// https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html.
|
||||
@@ -30,6 +31,8 @@ fn main() -> ExitCode {
|
||||
format,
|
||||
report_identical_files,
|
||||
brief,
|
||||
expand_tabs,
|
||||
tabsize,
|
||||
} = parse_params(opts).unwrap_or_else(|error| {
|
||||
eprintln!("{error}");
|
||||
exit(2);
|
||||
@@ -65,7 +68,9 @@ fn main() -> ExitCode {
|
||||
};
|
||||
// run diff
|
||||
let result: Vec<u8> = match format {
|
||||
Format::Normal => normal_diff::diff(&from_content, &to_content, brief),
|
||||
Format::Normal => {
|
||||
normal_diff::diff(&from_content, &to_content, brief, expand_tabs, tabsize)
|
||||
}
|
||||
Format::Unified => unified_diff::diff(
|
||||
&from_content,
|
||||
&from.to_string_lossy(),
|
||||
@@ -73,6 +78,8 @@ fn main() -> ExitCode {
|
||||
&to.to_string_lossy(),
|
||||
context_count,
|
||||
brief,
|
||||
expand_tabs,
|
||||
tabsize,
|
||||
),
|
||||
Format::Context => context_diff::diff(
|
||||
&from_content,
|
||||
@@ -81,11 +88,14 @@ fn main() -> ExitCode {
|
||||
&to.to_string_lossy(),
|
||||
context_count,
|
||||
brief,
|
||||
expand_tabs,
|
||||
tabsize,
|
||||
),
|
||||
Format::Ed => ed_diff::diff(&from_content, &to_content, brief).unwrap_or_else(|error| {
|
||||
eprintln!("{error}");
|
||||
exit(2);
|
||||
}),
|
||||
Format::Ed => ed_diff::diff(&from_content, &to_content, brief, expand_tabs, tabsize)
|
||||
.unwrap_or_else(|error| {
|
||||
eprintln!("{error}");
|
||||
exit(2);
|
||||
}),
|
||||
};
|
||||
if brief && !result.is_empty() {
|
||||
println!(
|
||||
|
||||
+23
-15
@@ -5,6 +5,8 @@
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use crate::utils::do_write_line;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Mismatch {
|
||||
pub line_number_expected: usize,
|
||||
@@ -114,7 +116,13 @@ fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec<Mismatch>
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec<u8> {
|
||||
pub fn diff(
|
||||
expected: &[u8],
|
||||
actual: &[u8],
|
||||
stop_early: bool,
|
||||
expand_tabs: bool,
|
||||
tabsize: usize,
|
||||
) -> Vec<u8> {
|
||||
// See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Normal.html
|
||||
// for details on the syntax of the normal format.
|
||||
let mut output = Vec::new();
|
||||
@@ -188,7 +196,7 @@ pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec<u8> {
|
||||
}
|
||||
for expected in &result.expected {
|
||||
write!(&mut output, "< ").unwrap();
|
||||
output.write_all(expected).unwrap();
|
||||
do_write_line(&mut output, expected, expand_tabs, tabsize).unwrap();
|
||||
writeln!(&mut output).unwrap();
|
||||
}
|
||||
if result.expected_missing_nl {
|
||||
@@ -199,7 +207,7 @@ pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec<u8> {
|
||||
}
|
||||
for actual in &result.actual {
|
||||
write!(&mut output, "> ").unwrap();
|
||||
output.write_all(actual).unwrap();
|
||||
do_write_line(&mut output, actual, expand_tabs, tabsize).unwrap();
|
||||
writeln!(&mut output).unwrap();
|
||||
}
|
||||
if result.actual_missing_nl {
|
||||
@@ -220,7 +228,7 @@ mod tests {
|
||||
a.write_all(b"a\n").unwrap();
|
||||
let mut b = Vec::new();
|
||||
b.write_all(b"b\n").unwrap();
|
||||
let diff = diff(&a, &b, false);
|
||||
let diff = diff(&a, &b, false, false, 8);
|
||||
let expected = b"1c1\n< a\n---\n> b\n".to_vec();
|
||||
assert_eq!(diff, expected);
|
||||
}
|
||||
@@ -273,7 +281,7 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet, false);
|
||||
let diff = diff(&alef, &bet, false, false, 8);
|
||||
File::create(&format!("{target}/ab.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
@@ -365,7 +373,7 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet, false);
|
||||
let diff = diff(&alef, &bet, false, false, 8);
|
||||
File::create(&format!("{target}/abn.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
@@ -439,7 +447,7 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet, false);
|
||||
let diff = diff(&alef, &bet, false, false, 8);
|
||||
File::create(&format!("{target}/ab_.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
@@ -517,7 +525,7 @@ mod tests {
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet, false);
|
||||
let diff = diff(&alef, &bet, false, false, 8);
|
||||
File::create(&format!("{target}/abr.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
@@ -549,21 +557,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_stop_early() {
|
||||
let from = vec!["a", "b", "c"].join("\n");
|
||||
let to = vec!["a", "d", "c"].join("\n");
|
||||
let from = ["a", "b", "c"].join("\n");
|
||||
let to = ["a", "d", "c"].join("\n");
|
||||
|
||||
let diff_full = diff(from.as_bytes(), to.as_bytes(), false);
|
||||
let expected_full = vec!["2c2", "< b", "---", "> d", ""].join("\n");
|
||||
let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false, 8);
|
||||
let expected_full = ["2c2", "< b", "---", "> d", ""].join("\n");
|
||||
assert_eq!(diff_full, expected_full.as_bytes());
|
||||
|
||||
let diff_brief = diff(from.as_bytes(), to.as_bytes(), true);
|
||||
let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false, 8);
|
||||
let expected_brief = "\0".as_bytes();
|
||||
assert_eq!(diff_brief, expected_brief);
|
||||
|
||||
let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false);
|
||||
let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false, 8);
|
||||
assert!(nodiff_full.is_empty());
|
||||
|
||||
let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true);
|
||||
let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false, 8);
|
||||
assert!(nodiff_brief.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
+170
-60
@@ -1,7 +1,10 @@
|
||||
use std::ffi::{OsStr, OsString};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
use regex::Regex;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum Format {
|
||||
#[default]
|
||||
Normal,
|
||||
Unified,
|
||||
Context,
|
||||
@@ -27,6 +30,23 @@ pub struct Params {
|
||||
pub context_count: usize,
|
||||
pub report_identical_files: bool,
|
||||
pub brief: bool,
|
||||
pub expand_tabs: bool,
|
||||
pub tabsize: usize,
|
||||
}
|
||||
|
||||
impl Default for Params {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
from: OsString::default(),
|
||||
to: OsString::default(),
|
||||
format: Format::default(),
|
||||
context_count: 3,
|
||||
report_identical_files: false,
|
||||
brief: false,
|
||||
expand_tabs: false,
|
||||
tabsize: 8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params, String> {
|
||||
@@ -36,12 +56,11 @@ pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params,
|
||||
let Some(exe) = opts.next() else {
|
||||
return Err("Usage: <exe> <from> <to>".to_string());
|
||||
};
|
||||
let mut params = Params::default();
|
||||
let mut from = None;
|
||||
let mut to = None;
|
||||
let mut format = None;
|
||||
let mut context_count = 3;
|
||||
let mut report_identical_files = false;
|
||||
let mut brief = false;
|
||||
let tabsize_re = Regex::new(r"^--tabsize=(?<num>\d+)$").unwrap();
|
||||
while let Some(param) = opts.next() {
|
||||
if param == "--" {
|
||||
break;
|
||||
@@ -57,11 +76,31 @@ pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params,
|
||||
continue;
|
||||
}
|
||||
if param == "-s" || param == "--report-identical-files" {
|
||||
report_identical_files = true;
|
||||
params.report_identical_files = true;
|
||||
continue;
|
||||
}
|
||||
if param == "-q" || param == "--brief" {
|
||||
brief = true;
|
||||
params.brief = true;
|
||||
continue;
|
||||
}
|
||||
if param == "-t" || param == "--expand-tabs" {
|
||||
params.expand_tabs = true;
|
||||
continue;
|
||||
}
|
||||
if tabsize_re.is_match(param.to_string_lossy().as_ref()) {
|
||||
// Because param matches the regular expression,
|
||||
// it is safe to assume it is valid UTF-8.
|
||||
let param = param.into_string().unwrap();
|
||||
let tabsize_str = tabsize_re
|
||||
.captures(param.as_str())
|
||||
.unwrap()
|
||||
.name("num")
|
||||
.unwrap()
|
||||
.as_str();
|
||||
params.tabsize = match tabsize_str.parse::<usize>() {
|
||||
Ok(num) => num,
|
||||
Err(_) => return Err(format!("invalid tabsize «{}»", tabsize_str)),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
let p = osstr_bytes(¶m);
|
||||
@@ -72,10 +111,10 @@ pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params,
|
||||
while let Some(b) = bit.next() {
|
||||
match b {
|
||||
b'0'..=b'9' => {
|
||||
context_count = (b - b'0') as usize;
|
||||
params.context_count = (b - b'0') as usize;
|
||||
while let Some(b'0'..=b'9') = bit.peek() {
|
||||
context_count *= 10;
|
||||
context_count += (bit.next().unwrap() - b'0') as usize;
|
||||
params.context_count *= 10;
|
||||
params.context_count += (bit.next().unwrap() - b'0') as usize;
|
||||
}
|
||||
}
|
||||
b'c' => {
|
||||
@@ -109,7 +148,7 @@ pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params,
|
||||
if let Some(context_count_maybe) =
|
||||
context_count_maybe.and_then(|x| x.parse().ok())
|
||||
{
|
||||
context_count = context_count_maybe;
|
||||
params.context_count = context_count_maybe;
|
||||
break;
|
||||
}
|
||||
return Err("Invalid context count".to_string());
|
||||
@@ -125,29 +164,22 @@ pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params,
|
||||
return Err(format!("Usage: {} <from> <to>", exe.to_string_lossy()));
|
||||
}
|
||||
}
|
||||
let from = if let Some(from) = from {
|
||||
params.from = if let Some(from) = from {
|
||||
from
|
||||
} else if let Some(param) = opts.next() {
|
||||
param
|
||||
} else {
|
||||
return Err(format!("Usage: {} <from> <to>", exe.to_string_lossy()));
|
||||
};
|
||||
let to = if let Some(to) = to {
|
||||
params.to = if let Some(to) = to {
|
||||
to
|
||||
} else if let Some(param) = opts.next() {
|
||||
param
|
||||
} else {
|
||||
return Err(format!("Usage: {} <from> <to>", exe.to_string_lossy()));
|
||||
};
|
||||
let format = format.unwrap_or(Format::Normal);
|
||||
Ok(Params {
|
||||
from,
|
||||
to,
|
||||
format,
|
||||
context_count,
|
||||
report_identical_files,
|
||||
brief,
|
||||
})
|
||||
params.format = format.unwrap_or(Format::default());
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -162,10 +194,7 @@ mod tests {
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Normal,
|
||||
context_count: 3,
|
||||
report_identical_files: false,
|
||||
brief: false,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
|
||||
);
|
||||
@@ -177,9 +206,7 @@ mod tests {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Ed,
|
||||
context_count: 3,
|
||||
report_identical_files: false,
|
||||
brief: false,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("-e"), os("foo"), os("bar")].iter().cloned())
|
||||
);
|
||||
@@ -192,8 +219,7 @@ mod tests {
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
context_count: 54,
|
||||
report_identical_files: false,
|
||||
brief: false,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-u54"), os("foo"), os("bar")]
|
||||
@@ -207,8 +233,7 @@ mod tests {
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
context_count: 54,
|
||||
report_identical_files: false,
|
||||
brief: false,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-U54"), os("foo"), os("bar")]
|
||||
@@ -222,8 +247,7 @@ mod tests {
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
context_count: 54,
|
||||
report_identical_files: false,
|
||||
brief: false,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-U"), os("54"), os("foo"), os("bar")]
|
||||
@@ -237,8 +261,7 @@ mod tests {
|
||||
to: os("bar"),
|
||||
format: Format::Context,
|
||||
context_count: 54,
|
||||
report_identical_files: false,
|
||||
brief: false,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-c54"), os("foo"), os("bar")]
|
||||
@@ -253,10 +276,7 @@ mod tests {
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Normal,
|
||||
context_count: 3,
|
||||
report_identical_files: false,
|
||||
brief: false,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
|
||||
);
|
||||
@@ -264,10 +284,8 @@ mod tests {
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Normal,
|
||||
context_count: 3,
|
||||
report_identical_files: true,
|
||||
brief: false,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("-s"), os("foo"), os("bar")].iter().cloned())
|
||||
);
|
||||
@@ -275,10 +293,8 @@ mod tests {
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Normal,
|
||||
context_count: 3,
|
||||
report_identical_files: true,
|
||||
brief: false,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[
|
||||
@@ -298,10 +314,7 @@ mod tests {
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Normal,
|
||||
context_count: 3,
|
||||
report_identical_files: false,
|
||||
brief: false,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
|
||||
);
|
||||
@@ -309,10 +322,8 @@ mod tests {
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Normal,
|
||||
context_count: 3,
|
||||
report_identical_files: false,
|
||||
brief: true,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("-q"), os("foo"), os("bar")].iter().cloned())
|
||||
);
|
||||
@@ -320,10 +331,8 @@ mod tests {
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Normal,
|
||||
context_count: 3,
|
||||
report_identical_files: false,
|
||||
brief: true,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("--brief"), os("foo"), os("bar"),]
|
||||
@@ -333,15 +342,116 @@ mod tests {
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn expand_tabs() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
|
||||
);
|
||||
for option in ["-t", "--expand-tabs"] {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
expand_tabs: true,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os(option), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn tabsize() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
tabsize: 0,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("--tabsize=0"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
tabsize: 42,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("--tabsize=42"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
);
|
||||
assert!(parse_params(
|
||||
[os("diff"), os("--tabsize"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_params(
|
||||
[os("diff"), os("--tabsize="), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_params(
|
||||
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_params(
|
||||
[os("diff"), os("--tabsize=-1"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_params(
|
||||
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_params(
|
||||
[
|
||||
os("diff"),
|
||||
os("--tabsize=92233720368547758088"),
|
||||
os("foo"),
|
||||
os("bar")
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn double_dash() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
from: os("-g"),
|
||||
to: os("-h"),
|
||||
format: Format::Normal,
|
||||
context_count: 3,
|
||||
report_identical_files: false,
|
||||
brief: false,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("--"), os("-g"), os("-h")].iter().cloned())
|
||||
);
|
||||
|
||||
+33
-7
@@ -6,6 +6,8 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Write;
|
||||
|
||||
use crate::utils::do_write_line;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DiffLine {
|
||||
Context(Vec<u8>),
|
||||
@@ -234,6 +236,7 @@ fn make_diff(
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn diff(
|
||||
expected: &[u8],
|
||||
expected_filename: &str,
|
||||
@@ -241,6 +244,8 @@ pub fn diff(
|
||||
actual_filename: &str,
|
||||
context_size: usize,
|
||||
stop_early: bool,
|
||||
expand_tabs: bool,
|
||||
tabsize: usize,
|
||||
) -> Vec<u8> {
|
||||
let mut output = format!("--- {expected_filename}\t\n+++ {actual_filename}\t\n").into_bytes();
|
||||
let diff_results = make_diff(expected, actual, context_size, stop_early);
|
||||
@@ -371,17 +376,20 @@ pub fn diff(
|
||||
match line {
|
||||
DiffLine::Expected(e) => {
|
||||
write!(output, "-").expect("write to Vec is infallible");
|
||||
output.write_all(&e).expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &e, expand_tabs, tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Context(c) => {
|
||||
write!(output, " ").expect("write to Vec is infallible");
|
||||
output.write_all(&c).expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &c, expand_tabs, tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Actual(r) => {
|
||||
write!(output, "+",).expect("write to Vec is infallible");
|
||||
output.write_all(&r).expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &r, expand_tabs, tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::MissingNL => {
|
||||
@@ -454,6 +462,8 @@ mod tests {
|
||||
&format!("{target}/alef"),
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
File::create(&format!("{target}/ab.diff"))
|
||||
.unwrap()
|
||||
@@ -568,6 +578,8 @@ mod tests {
|
||||
&format!("{target}/alefn"),
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
File::create(&format!("{target}/abn.diff"))
|
||||
.unwrap()
|
||||
@@ -662,6 +674,8 @@ mod tests {
|
||||
&format!("{target}/alef_"),
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
File::create(&format!("{target}/ab_.diff"))
|
||||
.unwrap()
|
||||
@@ -741,6 +755,8 @@ mod tests {
|
||||
&format!("{target}/alefx"),
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
File::create(&format!("{target}/abx.diff"))
|
||||
.unwrap()
|
||||
@@ -825,6 +841,8 @@ mod tests {
|
||||
&format!("{target}/alefr"),
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
File::create(&format!("{target}/abr.diff"))
|
||||
.unwrap()
|
||||
@@ -857,9 +875,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_stop_early() {
|
||||
let from_filename = "foo";
|
||||
let from = vec!["a", "b", "c", ""].join("\n");
|
||||
let from = ["a", "b", "c", ""].join("\n");
|
||||
let to_filename = "bar";
|
||||
let to = vec!["a", "d", "c", ""].join("\n");
|
||||
let to = ["a", "d", "c", ""].join("\n");
|
||||
let context_size: usize = 3;
|
||||
|
||||
let diff_full = diff(
|
||||
@@ -869,8 +887,10 @@ mod tests {
|
||||
to_filename,
|
||||
context_size,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
let expected_full = vec![
|
||||
let expected_full = [
|
||||
"--- foo\t",
|
||||
"+++ bar\t",
|
||||
"@@ -1,3 +1,3 @@",
|
||||
@@ -890,8 +910,10 @@ mod tests {
|
||||
to_filename,
|
||||
context_size,
|
||||
true,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
let expected_brief = vec!["--- foo\t", "+++ bar\t", ""].join("\n");
|
||||
let expected_brief = ["--- foo\t", "+++ bar\t", ""].join("\n");
|
||||
assert_eq!(diff_brief, expected_brief.as_bytes());
|
||||
|
||||
let nodiff_full = diff(
|
||||
@@ -901,6 +923,8 @@ mod tests {
|
||||
to_filename,
|
||||
context_size,
|
||||
false,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
assert!(nodiff_full.is_empty());
|
||||
|
||||
@@ -911,6 +935,8 @@ mod tests {
|
||||
to_filename,
|
||||
context_size,
|
||||
true,
|
||||
false,
|
||||
8,
|
||||
);
|
||||
assert!(nodiff_brief.is_empty());
|
||||
}
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// Replace tabs by spaces in the input line.
|
||||
/// Correctly handle multi-bytes characters.
|
||||
/// This assumes that line does not contain any line breaks (if it does, the result is undefined).
|
||||
pub fn do_expand_tabs(line: &[u8], tabsize: usize) -> Vec<u8> {
|
||||
let tab = b'\t';
|
||||
let ntabs = line.iter().filter(|c| **c == tab).count();
|
||||
if ntabs == 0 {
|
||||
return line.to_vec();
|
||||
}
|
||||
let mut result = Vec::with_capacity(line.len() + ntabs * (tabsize - 1));
|
||||
let mut offset = 0;
|
||||
|
||||
let mut iter = line.split(|c| *c == tab).peekable();
|
||||
while let Some(chunk) = iter.next() {
|
||||
match String::from_utf8(chunk.to_vec()) {
|
||||
Ok(s) => offset += UnicodeWidthStr::width(s.as_str()),
|
||||
Err(_) => offset += chunk.len(),
|
||||
}
|
||||
result.extend_from_slice(chunk);
|
||||
if iter.peek().is_some() {
|
||||
result.resize(result.len() + tabsize - offset % tabsize, b' ');
|
||||
offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Write a single line to an output stream, expanding tabs to space if necessary.
|
||||
/// This assumes that line does not contain any line breaks
|
||||
/// (if it does and tabs are to be expanded to spaces, the result is undefined).
|
||||
pub fn do_write_line(
|
||||
output: &mut Vec<u8>,
|
||||
line: &[u8],
|
||||
expand_tabs: bool,
|
||||
tabsize: usize,
|
||||
) -> std::io::Result<()> {
|
||||
if expand_tabs {
|
||||
output.write_all(do_expand_tabs(line, tabsize).as_slice())
|
||||
} else {
|
||||
output.write_all(line)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod expand_tabs {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn assert_tab_expansion(line: &str, tabsize: usize, expected: &str) {
|
||||
assert_eq!(
|
||||
do_expand_tabs(line.as_bytes(), tabsize),
|
||||
expected.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basics() {
|
||||
assert_tab_expansion("foo barr baz", 8, "foo barr baz");
|
||||
assert_tab_expansion("foo\tbarr\tbaz", 8, "foo barr baz");
|
||||
assert_tab_expansion("foo\tbarr\tbaz", 5, "foo barr baz");
|
||||
assert_tab_expansion("foo\tbarr\tbaz", 2, "foo barr baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multibyte_chars() {
|
||||
assert_tab_expansion("foo\tépée\tbaz", 8, "foo épée baz");
|
||||
assert_tab_expansion("foo\t😉\tbaz", 5, "foo 😉 baz");
|
||||
|
||||
// Note: The Woman Scientist emoji (👩🔬) is a ZWJ sequence combining
|
||||
// the Woman emoji (👩) and the Microscope emoji (🔬). On supported platforms
|
||||
// it is displayed as a single emoji and should have a print size of 2 columns,
|
||||
// but terminal emulators tend to not support this, and display the two emojis
|
||||
// side by side, thus accounting for a print size of 4 columns.
|
||||
assert_tab_expansion("foo\t👩🔬\tbaz", 6, "foo 👩🔬 baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_utf8() {
|
||||
// [240, 240, 152, 137] is an invalid UTF-8 sequence, so it is handled as 4 bytes
|
||||
assert_eq!(
|
||||
do_expand_tabs(&[240, 240, 152, 137, 9, 102, 111, 111], 8),
|
||||
&[240, 240, 152, 137, 32, 32, 32, 32, 102, 111, 111]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod write_line {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn assert_line_written(line: &str, expand_tabs: bool, tabsize: usize, expected: &str) {
|
||||
let mut output: Vec<u8> = Vec::new();
|
||||
assert!(do_write_line(&mut output, line.as_bytes(), expand_tabs, tabsize).is_ok());
|
||||
assert_eq!(output, expected.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basics() {
|
||||
assert_line_written("foo bar baz", false, 8, "foo bar baz");
|
||||
assert_line_written("foo bar\tbaz", false, 8, "foo bar\tbaz");
|
||||
assert_line_written("foo bar\tbaz", true, 8, "foo bar baz");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user