diff --git a/.github/workflows/GnuComment.yml b/.github/workflows/GnuComment.yml new file mode 100644 index 00000000000..f22abdf4223 --- /dev/null +++ b/.github/workflows/GnuComment.yml @@ -0,0 +1,52 @@ +name: GnuComment + +on: + workflow_run: + workflows: ["GnuTests"] + types: + - completed + +jobs: + post-comment: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.event == 'pull_request' + steps: + - name: 'Download artifact' + uses: actions/github-script@v3.1.0 + with: + script: | + // List all artifacts from GnuTests + var artifacts = await github.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{ github.event.workflow_run.id }}, + }); + + // Download the "comment" artifact, which contains a PR number (NR) and result.txt + var matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "comment" + })[0]; + var download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{ github.workspace }}/comment.zip', Buffer.from(download.data)); + - run: unzip comment.zip + + - name: 'Comment on PR' + uses: actions/github-script@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + var fs = require('fs'); + var issue_number = Number(fs.readFileSync('./NR')); + await github.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue_number, + body: String(fs.readFileSync('./result.txt')), + }); diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index b36a97cacfe..0861746548e 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -14,7 +14,8 @@ jobs: permissions: actions: read # for dawidd6/action-download-artifact to query and download artifacts contents: read # for actions/checkout to fetch code - pull-requests: read # for dawidd6/action-download-artifact to query commit hash + issues: write # to publish comment + pull-requests: write # for dawidd6/action-download-artifact to query commit hash & publish comment name: Run GNU tests runs-on: ubuntu-latest steps: @@ -186,6 +187,15 @@ jobs: REF_LOG_FILE='${{ steps.vars.outputs.path_reference }}/test-logs/test-suite.log' REF_SUMMARY_FILE='${{ steps.vars.outputs.path_reference }}/test-summary/gnu-result.json' REPO_DEFAULT_BRANCH='${{ steps.vars.outputs.repo_default_branch }}' + + mkdir -p ${{ steps.vars.outputs.path_reference }} + + COMMENT_DIR="${{ steps.vars.outputs.path_reference }}/comment" + mkdir -p ${COMMENT_DIR} + echo ${{ github.event.number }} > ${COMMENT_DIR}/NR + COMMENT_LOG="${COMMENT_DIR}/result.txt" + touch ${COMMENT_LOG} + if test -f "${REF_LOG_FILE}"; then echo "Reference SHA1/ID: $(sha1sum -- "${REF_SUMMARY_FILE}")" REF_ERROR=$(sed -n "s/^ERROR: \([[:print:]]\+\).*/\1/p" "${REF_LOG_FILE}" | sort) @@ -195,28 +205,36 @@ jobs: for LINE in ${REF_FAILING} do if ! grep -Fxq ${LINE}<<<"${NEW_FAILING}"; then - echo "::warning ::Congrats! The gnu test ${LINE} is no longer failing!" + MSG="Congrats! The gnu test ${LINE} is no longer failing!" + echo "::warning ::$MSG" + echo $MSG >> ${COMMENT_LOG} fi done for LINE in ${NEW_FAILING} do if ! grep -Fxq ${LINE}<<<"${REF_FAILING}" then - echo "::error ::GNU test failed: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?" + MSG="GNU test failed: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?" + echo "::error ::$MSG" + echo $MSG >> ${COMMENT_LOG} have_new_failures="true" fi done for LINE in ${REF_ERROR} do if ! grep -Fxq ${LINE}<<<"${NEW_ERROR}"; then - echo "::warning ::Congrats! The gnu test ${LINE} is no longer ERROR!" + MSG="Congrats! The gnu test ${LINE} is no longer ERROR!" + echo "::warning ::$MSG" + echo $MSG >> ${COMMENT_LOG} fi done for LINE in ${NEW_ERROR} do if ! grep -Fxq ${LINE}<<<"${REF_ERROR}" then - echo "::error ::GNU test error: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?" + MSG="GNU test error: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?" + echo "::error ::$MSG" + echo $MSG >> ${COMMENT_LOG} have_new_failures="true" fi done @@ -225,6 +243,12 @@ jobs: echo "::warning ::Skipping test failure comparison; no prior reference test logs are available." fi if test -n "${have_new_failures}" ; then exit -1 ; fi + - name: Upload comparison log (for GnuComment workflow) + if: success() || failure() # run regardless of prior step success/failure + uses: actions/upload-artifact@v3 + with: + name: comment + path: ${{ steps.vars.outputs.path_reference }}/comment/ - name: Compare test summary VS reference if: success() || failure() # run regardless of prior step success/failure shell: bash diff --git a/Cargo.lock b/Cargo.lock index cf806cd7877..1134a1c5988 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2834,6 +2834,7 @@ version = "0.0.15" dependencies = [ "clap", "libc", + "nix", "uucore", "winapi", ] diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 96164827244..79743385a93 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -22,15 +22,11 @@ use std::env; #[cfg(not(windows))] use std::ffi::CString; use std::fs::{self, File, OpenOptions}; -#[cfg(any(target_os = "linux", target_os = "android"))] -use std::io::Read; use std::io::{self, stderr, stdin, Write}; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, PermissionsExt}; -#[cfg(any(target_os = "linux", target_os = "android"))] -use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf, StripPrefixError}; use std::str::FromStr; use std::string::ToString; @@ -49,6 +45,9 @@ use uucore::fs::{ }; use walkdir::WalkDir; +mod platform; +use platform::copy_on_write; + quick_error! { #[derive(Debug)] pub enum Error { @@ -224,16 +223,6 @@ pub struct Options { verbose: bool, } -// From /usr/include/linux/fs.h: -// #define FICLONE _IOW(0x94, 9, int) -#[cfg(any(target_os = "linux", target_os = "android"))] -// Use a macro as libc::ioctl expects u32 or u64 depending on the arch -macro_rules! FICLONE { - () => { - 0x40049409 - }; -} - static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."; static LONG_HELP: &str = ""; static EXIT_ERR: i32 = 1; @@ -1598,26 +1587,7 @@ fn copy_helper( } else if source_is_symlink { copy_link(source, dest, symlinked_files)?; } else { - #[cfg(target_os = "macos")] - copy_on_write_macos( - source, - dest, - options.reflink_mode, - options.sparse_mode, - context, - )?; - - #[cfg(any(target_os = "linux", target_os = "android"))] - copy_on_write_linux( - source, - dest, - options.reflink_mode, - options.sparse_mode, - context, - )?; - - #[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] - copy_no_cow_fallback( + copy_on_write( source, dest, options.reflink_mode, @@ -1673,180 +1643,6 @@ fn copy_link( symlink_file(&link, &dest, &context_for(&link, &dest), symlinked_files) } -/// Copies `source` to `dest` for systems without copy-on-write -#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] -fn copy_no_cow_fallback( - source: &Path, - dest: &Path, - reflink_mode: ReflinkMode, - sparse_mode: SparseMode, - context: &str, -) -> CopyResult<()> { - if reflink_mode != ReflinkMode::Never { - return Err("--reflink is only supported on linux and macOS" - .to_string() - .into()); - } - if sparse_mode != SparseMode::Auto { - return Err("--sparse is only supported on linux".to_string().into()); - } - - fs::copy(source, dest).context(context)?; - - Ok(()) -} - -/// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone. -/// -/// If `fallback` is true and there is a failure performing the clone, -/// then this function performs a standard [`std::fs::copy`]. Otherwise, -/// this function returns an error. -#[cfg(any(target_os = "linux", target_os = "android"))] -fn clone

(source: P, dest: P, fallback: bool) -> std::io::Result<()> -where - P: AsRef, -{ - let src_file = File::open(&source)?; - let dst_file = File::create(&dest)?; - let src_fd = src_file.as_raw_fd(); - let dst_fd = dst_file.as_raw_fd(); - let result = unsafe { libc::ioctl(dst_fd, FICLONE!(), src_fd) }; - if result != 0 { - if fallback { - std::fs::copy(source, dest).map(|_| ()) - } else { - Err(std::io::Error::last_os_error()) - } - } else { - Ok(()) - } -} - -/// Perform a sparse copy from one file to another. -#[cfg(any(target_os = "linux", target_os = "android"))] -fn sparse_copy

(source: P, dest: P) -> std::io::Result<()> -where - P: AsRef, -{ - use std::os::unix::prelude::MetadataExt; - - let mut src_file = File::open(source)?; - let dst_file = File::create(dest)?; - let dst_fd = dst_file.as_raw_fd(); - - let size: usize = src_file.metadata()?.size().try_into().unwrap(); - if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 { - return Err(std::io::Error::last_os_error()); - } - - let blksize = dst_file.metadata()?.blksize(); - let mut buf: Vec = vec![0; blksize.try_into().unwrap()]; - let mut current_offset: usize = 0; - - // TODO Perhaps we can employ the "fiemap ioctl" API to get the - // file extent mappings: - // https://www.kernel.org/doc/html/latest/filesystems/fiemap.html - while current_offset < size { - let this_read = src_file.read(&mut buf)?; - if buf.iter().any(|&x| x != 0) { - unsafe { - libc::pwrite( - dst_fd, - buf.as_ptr() as *const libc::c_void, - this_read, - current_offset.try_into().unwrap(), - ) - }; - } - current_offset += this_read; - } - Ok(()) -} - -/// Copies `source` to `dest` using copy-on-write if possible. -#[cfg(any(target_os = "linux", target_os = "android"))] -fn copy_on_write_linux( - source: &Path, - dest: &Path, - reflink_mode: ReflinkMode, - sparse_mode: SparseMode, - context: &str, -) -> CopyResult<()> { - let result = match (reflink_mode, sparse_mode) { - (ReflinkMode::Never, _) => std::fs::copy(source, dest).map(|_| ()), - (ReflinkMode::Auto, SparseMode::Always) => sparse_copy(source, dest), - (ReflinkMode::Auto, _) => clone(source, dest, true), - (ReflinkMode::Always, SparseMode::Auto) => clone(source, dest, false), - (ReflinkMode::Always, _) => { - return Err("`--reflink=always` can be used only with --sparse=auto".into()) - } - }; - result.context(context)?; - Ok(()) -} - -/// Copies `source` to `dest` using copy-on-write if possible. -#[cfg(target_os = "macos")] -fn copy_on_write_macos( - source: &Path, - dest: &Path, - reflink_mode: ReflinkMode, - sparse_mode: SparseMode, - context: &str, -) -> CopyResult<()> { - if sparse_mode != SparseMode::Auto { - return Err("--sparse is only supported on linux".to_string().into()); - } - - // Extract paths in a form suitable to be passed to a syscall. - // The unwrap() is safe because they come from the command-line and so contain non nul - // character. - let src = CString::new(source.as_os_str().as_bytes()).unwrap(); - let dst = CString::new(dest.as_os_str().as_bytes()).unwrap(); - - // clonefile(2) was introduced in macOS 10.12 so we cannot statically link against it - // for backward compatibility. - let clonefile = CString::new("clonefile").unwrap(); - let raw_pfn = unsafe { libc::dlsym(libc::RTLD_NEXT, clonefile.as_ptr()) }; - - let mut error = 0; - if !raw_pfn.is_null() { - // Call clonefile(2). - // Safety: Casting a C function pointer to a rust function value is one of the few - // blessed uses of `transmute()`. - unsafe { - let pfn: extern "C" fn( - src: *const libc::c_char, - dst: *const libc::c_char, - flags: u32, - ) -> libc::c_int = std::mem::transmute(raw_pfn); - error = pfn(src.as_ptr(), dst.as_ptr(), 0); - if std::io::Error::last_os_error().kind() == std::io::ErrorKind::AlreadyExists { - // clonefile(2) fails if the destination exists. Remove it and try again. Do not - // bother to check if removal worked because we're going to try to clone again. - let _ = fs::remove_file(dest); - error = pfn(src.as_ptr(), dst.as_ptr(), 0); - } - } - } - - if raw_pfn.is_null() || error != 0 { - // clonefile(2) is either not supported or it errored out (possibly because the FS does not - // support COW). - match reflink_mode { - ReflinkMode::Always => { - return Err( - format!("failed to clone {:?} from {:?}: {}", source, dest, error).into(), - ) - } - ReflinkMode::Auto => fs::copy(source, dest).context(context)?, - ReflinkMode::Never => fs::copy(source, dest).context(context)?, - }; - } - - Ok(()) -} - /// Generate an error message if `target` is not the correct `target_type` pub fn verify_target_type(target: &Path, target_type: &TargetType) -> CopyResult<()> { match (target_type, target.is_dir()) { diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs new file mode 100644 index 00000000000..7f47b884a64 --- /dev/null +++ b/src/uu/cp/src/platform/linux.rs @@ -0,0 +1,110 @@ +// * 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. +// spell-checker:ignore ficlone reflink ftruncate pwrite fiemap +use std::fs::File; +use std::io::Read; +use std::os::unix::io::AsRawFd; +use std::path::Path; + +use quick_error::ResultExt; + +use crate::{CopyResult, ReflinkMode, SparseMode}; + +// From /usr/include/linux/fs.h: +// #define FICLONE _IOW(0x94, 9, int) +// Use a macro as libc::ioctl expects u32 or u64 depending on the arch +macro_rules! FICLONE { + () => { + 0x40049409 + }; +} + +/// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone. +/// +/// If `fallback` is true and there is a failure performing the clone, +/// then this function performs a standard [`std::fs::copy`]. Otherwise, +/// this function returns an error. +#[cfg(any(target_os = "linux", target_os = "android"))] +fn clone

(source: P, dest: P, fallback: bool) -> std::io::Result<()> +where + P: AsRef, +{ + let src_file = File::open(&source)?; + let dst_file = File::create(&dest)?; + let src_fd = src_file.as_raw_fd(); + let dst_fd = dst_file.as_raw_fd(); + let result = unsafe { libc::ioctl(dst_fd, FICLONE!(), src_fd) }; + if result != 0 { + if fallback { + std::fs::copy(source, dest).map(|_| ()) + } else { + Err(std::io::Error::last_os_error()) + } + } else { + Ok(()) + } +} + +/// Perform a sparse copy from one file to another. +#[cfg(any(target_os = "linux", target_os = "android"))] +fn sparse_copy

(source: P, dest: P) -> std::io::Result<()> +where + P: AsRef, +{ + use std::os::unix::prelude::MetadataExt; + + let mut src_file = File::open(source)?; + let dst_file = File::create(dest)?; + let dst_fd = dst_file.as_raw_fd(); + + let size: usize = src_file.metadata()?.size().try_into().unwrap(); + if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 { + return Err(std::io::Error::last_os_error()); + } + + let blksize = dst_file.metadata()?.blksize(); + let mut buf: Vec = vec![0; blksize.try_into().unwrap()]; + let mut current_offset: usize = 0; + + // TODO Perhaps we can employ the "fiemap ioctl" API to get the + // file extent mappings: + // https://www.kernel.org/doc/html/latest/filesystems/fiemap.html + while current_offset < size { + let this_read = src_file.read(&mut buf)?; + if buf.iter().any(|&x| x != 0) { + unsafe { + libc::pwrite( + dst_fd, + buf.as_ptr() as *const libc::c_void, + this_read, + current_offset.try_into().unwrap(), + ) + }; + } + current_offset += this_read; + } + Ok(()) +} + +/// Copies `source` to `dest` using copy-on-write if possible. +pub(crate) fn copy_on_write( + source: &Path, + dest: &Path, + reflink_mode: ReflinkMode, + sparse_mode: SparseMode, + context: &str, +) -> CopyResult<()> { + let result = match (reflink_mode, sparse_mode) { + (ReflinkMode::Never, _) => std::fs::copy(source, dest).map(|_| ()), + (ReflinkMode::Auto, SparseMode::Always) => sparse_copy(source, dest), + (ReflinkMode::Auto, _) => clone(source, dest, true), + (ReflinkMode::Always, SparseMode::Auto) => clone(source, dest, false), + (ReflinkMode::Always, _) => { + return Err("`--reflink=always` can be used only with --sparse=auto".into()) + } + }; + result.context(context)?; + Ok(()) +} diff --git a/src/uu/cp/src/platform/macos.rs b/src/uu/cp/src/platform/macos.rs new file mode 100644 index 00000000000..7f152415412 --- /dev/null +++ b/src/uu/cp/src/platform/macos.rs @@ -0,0 +1,74 @@ +// * 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. +// spell-checker:ignore reflink +use std::ffi::CString; +use std::fs; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; + +use quick_error::ResultExt; + +use crate::{CopyResult, ReflinkMode, SparseMode}; + +/// Copies `source` to `dest` using copy-on-write if possible. +pub(crate) fn copy_on_write( + source: &Path, + dest: &Path, + reflink_mode: ReflinkMode, + sparse_mode: SparseMode, + context: &str, +) -> CopyResult<()> { + if sparse_mode != SparseMode::Auto { + return Err("--sparse is only supported on linux".to_string().into()); + } + + // Extract paths in a form suitable to be passed to a syscall. + // The unwrap() is safe because they come from the command-line and so contain non nul + // character. + let src = CString::new(source.as_os_str().as_bytes()).unwrap(); + let dst = CString::new(dest.as_os_str().as_bytes()).unwrap(); + + // clonefile(2) was introduced in macOS 10.12 so we cannot statically link against it + // for backward compatibility. + let clonefile = CString::new("clonefile").unwrap(); + let raw_pfn = unsafe { libc::dlsym(libc::RTLD_NEXT, clonefile.as_ptr()) }; + + let mut error = 0; + if !raw_pfn.is_null() { + // Call clonefile(2). + // Safety: Casting a C function pointer to a rust function value is one of the few + // blessed uses of `transmute()`. + unsafe { + let pfn: extern "C" fn( + src: *const libc::c_char, + dst: *const libc::c_char, + flags: u32, + ) -> libc::c_int = std::mem::transmute(raw_pfn); + error = pfn(src.as_ptr(), dst.as_ptr(), 0); + if std::io::Error::last_os_error().kind() == std::io::ErrorKind::AlreadyExists { + // clonefile(2) fails if the destination exists. Remove it and try again. Do not + // bother to check if removal worked because we're going to try to clone again. + let _ = fs::remove_file(dest); + error = pfn(src.as_ptr(), dst.as_ptr(), 0); + } + } + } + + if raw_pfn.is_null() || error != 0 { + // clonefile(2) is either not supported or it errored out (possibly because the FS does not + // support COW). + match reflink_mode { + ReflinkMode::Always => { + return Err( + format!("failed to clone {:?} from {:?}: {}", source, dest, error).into(), + ) + } + ReflinkMode::Auto => fs::copy(source, dest).context(context)?, + ReflinkMode::Never => fs::copy(source, dest).context(context)?, + }; + } + + Ok(()) +} diff --git a/src/uu/cp/src/platform/mod.rs b/src/uu/cp/src/platform/mod.rs new file mode 100644 index 00000000000..9dbcefa806f --- /dev/null +++ b/src/uu/cp/src/platform/mod.rs @@ -0,0 +1,18 @@ +// * 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. +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "macos")] +pub(crate) use self::macos::copy_on_write; + +#[cfg(any(target_os = "linux", target_os = "android"))] +mod linux; +#[cfg(any(target_os = "linux", target_os = "android"))] +pub(crate) use self::linux::copy_on_write; + +#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] +mod other; +#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] +pub(crate) use self::other::copy_on_write; diff --git a/src/uu/cp/src/platform/other.rs b/src/uu/cp/src/platform/other.rs new file mode 100644 index 00000000000..b70da2f2341 --- /dev/null +++ b/src/uu/cp/src/platform/other.rs @@ -0,0 +1,33 @@ +// * 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. +// spell-checker:ignore reflink +use std::fs; +use std::path::Path; + +use quick_error::ResultExt; + +use crate::{CopyResult, ReflinkMode, SparseMode}; + +/// Copies `source` to `dest` for systems without copy-on-write +pub(crate) fn copy_on_write( + source: &Path, + dest: &Path, + reflink_mode: ReflinkMode, + sparse_mode: SparseMode, + context: &str, +) -> CopyResult<()> { + if reflink_mode != ReflinkMode::Never { + return Err("--reflink is only supported on linux and macOS" + .to_string() + .into()); + } + if sparse_mode != SparseMode::Auto { + return Err("--sparse is only supported on linux".to_string().into()); + } + + fs::copy(source, dest).context(context)?; + + Ok(()) +} diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 1be54adcea9..962ce164bbe 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -14,7 +14,7 @@ use clap::{ builder::{NonEmptyStringValueParser, ValueParser}, crate_version, Arg, Command, }; -use glob::Pattern; +use glob::{MatchOptions, Pattern}; use lscolors::LsColors; use number_prefix::NumberPrefix; use once_cell::unsync::OnceCell; @@ -41,6 +41,7 @@ use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; use unicode_width::UnicodeWidthStr; #[cfg(unix)] use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; +use uucore::parse_glob; use uucore::quoting_style::{escape_name, QuotingStyle}; use uucore::{ display::Quotable, @@ -765,7 +766,7 @@ impl Config { .into_iter() .flatten() { - match Pattern::new(pattern) { + match parse_glob::from_str(pattern) { Ok(p) => { ignore_patterns.push(p); } @@ -779,7 +780,7 @@ impl Config { .into_iter() .flatten() { - match Pattern::new(pattern) { + match parse_glob::from_str(pattern) { Ok(p) => { ignore_patterns.push(p); } @@ -1877,16 +1878,18 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool { return false; } - // check if explicitly ignored - for pattern in &config.ignore_patterns { - if pattern.matches(entry.file_name().to_str().unwrap()) { - return false; - }; - continue; - } - - // else default to display - true + // check if it is among ignore_patterns + let options = MatchOptions { + // setting require_literal_leading_dot to match behavior in GNU ls + require_literal_leading_dot: true, + require_literal_separator: false, + case_sensitive: true, + }; + let file_name = entry.file_name().into_string().unwrap(); + !config + .ignore_patterns + .iter() + .any(|p| p.matches_with(&file_name, options)) } fn enter_directory( diff --git a/src/uu/split/src/filenames.rs b/src/uu/split/src/filenames.rs index 6f68caeb41f..95ae5d40f33 100644 --- a/src/uu/split/src/filenames.rs +++ b/src/uu/split/src/filenames.rs @@ -28,6 +28,7 @@ use crate::number::DynamicWidthNumber; use crate::number::FixedWidthNumber; use crate::number::Number; +use uucore::error::{UResult, USimpleError}; /// The format to use for suffixes in the filename for each output chunk. #[derive(Clone, Copy)] @@ -119,19 +120,28 @@ impl<'a> FilenameIterator<'a> { additional_suffix: &'a str, suffix_length: usize, suffix_type: SuffixType, - ) -> FilenameIterator<'a> { + suffix_start: usize, + ) -> UResult> { let radix = suffix_type.radix(); let number = if suffix_length == 0 { - Number::DynamicWidth(DynamicWidthNumber::new(radix)) + Number::DynamicWidth(DynamicWidthNumber::new(radix, suffix_start)) } else { - Number::FixedWidth(FixedWidthNumber::new(radix, suffix_length)) + Number::FixedWidth( + FixedWidthNumber::new(radix, suffix_length, suffix_start).map_err(|_| { + USimpleError::new( + 1, + "numerical suffix start value is too large for the suffix length", + ) + })?, + ) }; - FilenameIterator { + + Ok(FilenameIterator { prefix, additional_suffix, number, first_iteration: true, - } + }) } } @@ -161,36 +171,36 @@ mod tests { #[test] fn test_filename_iterator_alphabetic_fixed_width() { - let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Alphabetic); + let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Alphabetic, 0).unwrap(); assert_eq!(it.next().unwrap(), "chunk_aa.txt"); assert_eq!(it.next().unwrap(), "chunk_ab.txt"); assert_eq!(it.next().unwrap(), "chunk_ac.txt"); - let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Alphabetic); + let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Alphabetic, 0).unwrap(); assert_eq!(it.nth(26 * 26 - 1).unwrap(), "chunk_zz.txt"); assert_eq!(it.next(), None); } #[test] fn test_filename_iterator_numeric_fixed_width() { - let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Decimal); + let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Decimal, 0).unwrap(); assert_eq!(it.next().unwrap(), "chunk_00.txt"); assert_eq!(it.next().unwrap(), "chunk_01.txt"); assert_eq!(it.next().unwrap(), "chunk_02.txt"); - let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Decimal); + let mut it = FilenameIterator::new("chunk_", ".txt", 2, SuffixType::Decimal, 0).unwrap(); assert_eq!(it.nth(10 * 10 - 1).unwrap(), "chunk_99.txt"); assert_eq!(it.next(), None); } #[test] fn test_filename_iterator_alphabetic_dynamic_width() { - let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Alphabetic); + let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Alphabetic, 0).unwrap(); assert_eq!(it.next().unwrap(), "chunk_aa.txt"); assert_eq!(it.next().unwrap(), "chunk_ab.txt"); assert_eq!(it.next().unwrap(), "chunk_ac.txt"); - let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Alphabetic); + let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Alphabetic, 0).unwrap(); assert_eq!(it.nth(26 * 25 - 1).unwrap(), "chunk_yz.txt"); assert_eq!(it.next().unwrap(), "chunk_zaaa.txt"); assert_eq!(it.next().unwrap(), "chunk_zaab.txt"); @@ -198,14 +208,49 @@ mod tests { #[test] fn test_filename_iterator_numeric_dynamic_width() { - let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Decimal); + let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Decimal, 0).unwrap(); assert_eq!(it.next().unwrap(), "chunk_00.txt"); assert_eq!(it.next().unwrap(), "chunk_01.txt"); assert_eq!(it.next().unwrap(), "chunk_02.txt"); - let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Decimal); + let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Decimal, 0).unwrap(); assert_eq!(it.nth(10 * 9 - 1).unwrap(), "chunk_89.txt"); assert_eq!(it.next().unwrap(), "chunk_9000.txt"); assert_eq!(it.next().unwrap(), "chunk_9001.txt"); } + + #[test] + fn test_filename_iterator_numeric_suffix_decimal() { + let mut it = FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Decimal, 5).unwrap(); + assert_eq!(it.next().unwrap(), "chunk_05.txt"); + assert_eq!(it.next().unwrap(), "chunk_06.txt"); + assert_eq!(it.next().unwrap(), "chunk_07.txt"); + } + + #[test] + fn test_filename_iterator_numeric_suffix_hex() { + let mut it = + FilenameIterator::new("chunk_", ".txt", 0, SuffixType::Hexadecimal, 9).unwrap(); + assert_eq!(it.next().unwrap(), "chunk_09.txt"); + assert_eq!(it.next().unwrap(), "chunk_0a.txt"); + assert_eq!(it.next().unwrap(), "chunk_0b.txt"); + } + + #[test] + fn test_filename_iterator_numeric_suffix_err() { + let mut it = FilenameIterator::new("chunk_", ".txt", 3, SuffixType::Decimal, 999).unwrap(); + assert_eq!(it.next().unwrap(), "chunk_999.txt"); + assert!(it.next().is_none()); + + let it = FilenameIterator::new("chunk_", ".txt", 3, SuffixType::Decimal, 1000); + assert!(it.is_err()); + + let mut it = + FilenameIterator::new("chunk_", ".txt", 3, SuffixType::Hexadecimal, 0xfff).unwrap(); + assert_eq!(it.next().unwrap(), "chunk_fff.txt"); + assert!(it.next().is_none()); + + let it = FilenameIterator::new("chunk_", ".txt", 3, SuffixType::Hexadecimal, 0x1000); + assert!(it.is_err()); + } } diff --git a/src/uu/split/src/number.rs b/src/uu/split/src/number.rs index c7557271db6..4605d6fb0d7 100644 --- a/src/uu/split/src/number.rs +++ b/src/uu/split/src/number.rs @@ -100,10 +100,10 @@ impl Number { /// differently and we only intend to use these numbers for display /// purposes and not for mathematical purposes. #[allow(dead_code)] - fn digits(&self) -> &Vec { + fn digits(&self) -> Vec { match self { - Self::FixedWidth(number) => &number.digits, - Self::DynamicWidth(number) => &number.digits, + Self::FixedWidth(number) => number.digits.clone(), + Self::DynamicWidth(number) => number.digits(), } } @@ -175,7 +175,7 @@ impl Display for Number { /// /// # Displaying /// -/// This number is only displayable if `radix` is 10, 26, or 26. If +/// This number is only displayable if `radix` is 10, 16, or 26. If /// `radix` is 10 or 16, then the digits are concatenated and /// displayed as a fixed-width decimal or hexadecimal number, /// respectively. If `radix` is 26, then each digit is translated to @@ -189,10 +189,21 @@ pub struct FixedWidthNumber { impl FixedWidthNumber { /// Instantiate a number of the given radix and width. - pub fn new(radix: u8, width: usize) -> Self { - Self { - radix, - digits: vec![0; width], + pub fn new(radix: u8, width: usize, mut suffix_start: usize) -> Result { + let mut digits = vec![0_u8; width]; + + for i in (0..digits.len()).rev() { + let remainder = (suffix_start % (radix as usize)) as u8; + suffix_start /= radix as usize; + digits[i] = remainder; + if suffix_start == 0 { + break; + } + } + if suffix_start != 0 { + Err(Overflow) + } else { + Ok(Self { radix, digits }) } } @@ -229,25 +240,12 @@ impl FixedWidthNumber { impl Display for FixedWidthNumber { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self.radix { - 10 => { - let digits: String = self.digits.iter().map(|d| (b'0' + d) as char).collect(); - write!(f, "{}", digits) - } - 16 => { - let digits: String = self - .digits - .iter() - .map(|d| (if *d < 10 { b'0' + d } else { b'a' + (d - 10) }) as char) - .collect(); - write!(f, "{}", digits) - } - 26 => { - let digits: String = self.digits.iter().map(|d| (b'a' + d) as char).collect(); - write!(f, "{}", digits) - } - _ => Err(fmt::Error), - } + let digits: String = self + .digits + .iter() + .map(|d| map_digit(self.radix, *d)) + .collect(); + write!(f, "{}", digits) } } @@ -293,105 +291,74 @@ impl Display for FixedWidthNumber { #[derive(Clone)] pub struct DynamicWidthNumber { radix: u8, - digits: Vec, + current: usize, } impl DynamicWidthNumber { - /// Instantiate a number of the given radix, starting with width 2. - /// - /// This associated function returns a new instance of the struct - /// with the given radix and a width of two digits, both 0. - pub fn new(radix: u8) -> Self { + pub fn new(radix: u8, suffix_start: usize) -> Self { Self { radix, - digits: vec![0, 0], - } - } - - /// Set all digits to zero. - fn reset(&mut self) { - for i in 0..self.digits.len() { - self.digits[i] = 0; + current: suffix_start, } } - /// Increment this number. - /// - /// This method adds one to this number. The first time that the - /// most significant digit would achieve its highest possible - /// value (that is, `radix - 1`), then all the digits get reset to - /// 0 and the number of digits increases by one. - /// - /// This method never returns an error. fn increment(&mut self) -> Result<(), Overflow> { - for i in (0..self.digits.len()).rev() { - // Increment the current digit. - self.digits[i] += 1; + self.current += 1; + Ok(()) + } - // If the digit overflows, then set it to 0 and continue - // to the next iteration to increment the next most - // significant digit. Otherwise, terminate the loop, since - // there will be no further changes to any higher order - // digits. - if self.digits[i] == self.radix { - self.digits[i] = 0; - } else { - break; - } + fn digits(&self) -> Vec { + let radix = self.radix as usize; + let mut remaining = self.current; + let mut sub_value = (radix - 1) * radix; + let mut num_fill_chars = 2; + + // Convert the number into "num_fill_chars" and "remaining" + while remaining >= sub_value { + remaining -= sub_value; + sub_value *= radix; + num_fill_chars += 1; } - // If the most significant digit is at its maximum value, then - // add another digit and reset all digits zero. - if self.digits[0] == self.radix - 1 { - self.digits.push(0); - self.reset(); + // Convert the "remainder" to digits + let mut digits = Vec::new(); + while remaining > 0 { + digits.push((remaining % radix) as u8); + remaining /= radix; } - Ok(()) + // Left pad the vec + digits.resize(num_fill_chars, 0); + digits.reverse(); + digits } } -impl Display for DynamicWidthNumber { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self.radix { - 10 => { - let num_fill_chars = self.digits.len() - 2; - let digits: String = self.digits.iter().map(|d| (b'0' + d) as char).collect(); - write!( - f, - "{empty:9 { - let num_fill_chars = self.digits.len() - 2; - let digits: String = self - .digits - .iter() - .map(|d| (if *d < 10 { b'0' + d } else { b'a' + (d - 10) }) as char) - .collect(); - write!( - f, - "{empty:f { - let num_fill_chars = self.digits.len() - 2; - let digits: String = self.digits.iter().map(|d| (b'a' + d) as char).collect(); - write!( - f, - "{empty:z char { + (match radix { + 10 => b'0' + d, + 16 => { + if d < 10 { + b'0' + d + } else { + b'a' + (d - 10) } - _ => Err(fmt::Error), } + 26 => b'a' + d, + _ => 0, + }) as char +} + +impl Display for DynamicWidthNumber { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let digits: String = self + .digits() + .iter() + .map(|d| map_digit(self.radix, *d)) + .collect(); + let fill: String = (0..digits.len() - 2) + .map(|_| map_digit(self.radix, self.radix - 1)) + .collect(); + write!(f, "{fill}{digits}") } } @@ -404,35 +371,36 @@ mod tests { #[test] fn test_dynamic_width_number_increment() { - let mut n = Number::DynamicWidth(DynamicWidthNumber::new(3)); - assert_eq!(n.digits(), &vec![0, 0]); + println!("Here"); + let mut n = Number::DynamicWidth(DynamicWidthNumber::new(3, 0)); + assert_eq!(n.digits(), vec![0, 0]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![0, 1]); + assert_eq!(n.digits(), vec![0, 1]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![0, 2]); + assert_eq!(n.digits(), vec![0, 2]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![1, 0]); + assert_eq!(n.digits(), vec![1, 0]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![1, 1]); + assert_eq!(n.digits(), vec![1, 1]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![1, 2]); + assert_eq!(n.digits(), vec![1, 2]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![0, 0, 0]); + assert_eq!(n.digits(), vec![0, 0, 0]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![0, 0, 1]); + assert_eq!(n.digits(), vec![0, 0, 1]); } #[test] fn test_dynamic_width_number_display_alphabetic() { fn num(n: usize) -> Number { - let mut number = Number::DynamicWidth(DynamicWidthNumber::new(26)); + let mut number = Number::DynamicWidth(DynamicWidthNumber::new(26, 0)); for _ in 0..n { number.increment().unwrap(); } @@ -456,7 +424,7 @@ mod tests { #[test] fn test_dynamic_width_number_display_numeric_decimal() { fn num(n: usize) -> Number { - let mut number = Number::DynamicWidth(DynamicWidthNumber::new(10)); + let mut number = Number::DynamicWidth(DynamicWidthNumber::new(10, 0)); for _ in 0..n { number.increment().unwrap(); } @@ -477,7 +445,7 @@ mod tests { #[test] fn test_dynamic_width_number_display_numeric_hexadecimal() { fn num(n: usize) -> Number { - let mut number = Number::DynamicWidth(DynamicWidthNumber::new(16)); + let mut number = Number::DynamicWidth(DynamicWidthNumber::new(16, 0)); for _ in 0..n { number.increment().unwrap(); } @@ -500,31 +468,31 @@ mod tests { #[test] fn test_fixed_width_number_increment() { - let mut n = Number::FixedWidth(FixedWidthNumber::new(3, 2)); - assert_eq!(n.digits(), &vec![0, 0]); + let mut n = Number::FixedWidth(FixedWidthNumber::new(3, 2, 0).unwrap()); + assert_eq!(n.digits(), vec![0, 0]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![0, 1]); + assert_eq!(n.digits(), vec![0, 1]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![0, 2]); + assert_eq!(n.digits(), vec![0, 2]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![1, 0]); + assert_eq!(n.digits(), vec![1, 0]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![1, 1]); + assert_eq!(n.digits(), vec![1, 1]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![1, 2]); + assert_eq!(n.digits(), vec![1, 2]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![2, 0]); + assert_eq!(n.digits(), vec![2, 0]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![2, 1]); + assert_eq!(n.digits(), vec![2, 1]); n.increment().unwrap(); - assert_eq!(n.digits(), &vec![2, 2]); + assert_eq!(n.digits(), vec![2, 2]); assert!(n.increment().is_err()); } #[test] fn test_fixed_width_number_display_alphabetic() { fn num(n: usize) -> Result { - let mut number = Number::FixedWidth(FixedWidthNumber::new(26, 2)); + let mut number = Number::FixedWidth(FixedWidthNumber::new(26, 2, 0).unwrap()); for _ in 0..n { number.increment()?; } @@ -549,7 +517,7 @@ mod tests { #[test] fn test_fixed_width_number_display_numeric_decimal() { fn num(n: usize) -> Result { - let mut number = Number::FixedWidth(FixedWidthNumber::new(10, 2)); + let mut number = Number::FixedWidth(FixedWidthNumber::new(10, 2, 0).unwrap()); for _ in 0..n { number.increment()?; } @@ -568,7 +536,7 @@ mod tests { #[test] fn test_fixed_width_number_display_numeric_hexadecimal() { fn num(n: usize) -> Result { - let mut number = Number::FixedWidth(FixedWidthNumber::new(16, 2)); + let mut number = Number::FixedWidth(FixedWidthNumber::new(16, 2, 0).unwrap()); for _ in 0..n { number.increment()?; } @@ -583,4 +551,32 @@ mod tests { assert_eq!(format!("{}", num(16 * 16 - 1).unwrap()), "ff"); assert!(num(16 * 16).is_err()); } + + #[test] + fn test_fixed_width_number_start_suffix() { + fn num(n: usize) -> Result { + let mut number = Number::FixedWidth(FixedWidthNumber::new(16, 2, 0x14)?); + for _ in 0..n { + number.increment()?; + } + Ok(number) + } + + assert_eq!(format!("{}", num(0).unwrap()), "14"); + assert_eq!(format!("{}", num(0xf).unwrap()), "23"); + } + + #[test] + fn test_dynamic_width_number_start_suffix() { + fn num(n: usize) -> Result { + let mut number = Number::DynamicWidth(DynamicWidthNumber::new(10, 8)); + for _ in 0..n { + number.increment()?; + } + Ok(number) + } + + assert_eq!(format!("{}", num(0).unwrap()), "08"); + assert_eq!(format!("{}", num(8).unwrap()), "16"); + } } diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 1224c83ec9d..2406ba8b718 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -144,7 +144,7 @@ pub fn uu_app<'a>() -> Command<'a> { .takes_value(true) .value_name("N") .default_value(OPT_DEFAULT_SUFFIX_LENGTH) - .help("use suffixes of length N (default 2)"), + .help("use suffixes of fixed length N. 0 implies dynamic length."), ) .arg( Arg::new(OPT_HEX_SUFFIXES) @@ -400,13 +400,23 @@ impl Strategy { } /// Parse the suffix type from the command-line arguments. -fn suffix_type_from(matches: &ArgMatches) -> SuffixType { +fn suffix_type_from(matches: &ArgMatches) -> Result<(SuffixType, usize), SettingsError> { if matches.value_source(OPT_NUMERIC_SUFFIXES) == Some(ValueSource::CommandLine) { - SuffixType::Decimal + let suffix_start = matches.value_of(OPT_NUMERIC_SUFFIXES); + let suffix_start = suffix_start.ok_or(SettingsError::SuffixNotParsable(String::new()))?; + let suffix_start = suffix_start + .parse() + .map_err(|_| SettingsError::SuffixNotParsable(suffix_start.to_string()))?; + Ok((SuffixType::Decimal, suffix_start)) } else if matches.value_source(OPT_HEX_SUFFIXES) == Some(ValueSource::CommandLine) { - SuffixType::Hexadecimal + let suffix_start = matches.value_of(OPT_HEX_SUFFIXES); + let suffix_start = suffix_start.ok_or(SettingsError::SuffixNotParsable(String::new()))?; + let suffix_start = usize::from_str_radix(suffix_start, 16) + .map_err(|_| SettingsError::SuffixNotParsable(suffix_start.to_string()))?; + Ok((SuffixType::Hexadecimal, suffix_start)) } else { - SuffixType::Alphabetic + // no numeric/hex suffix + Ok((SuffixType::Alphabetic, 0)) } } @@ -418,6 +428,7 @@ struct Settings { prefix: String, suffix_type: SuffixType, suffix_length: usize, + suffix_start: usize, additional_suffix: String, input: String, /// When supplied, a shell command to output to instead of xaa, xab … @@ -497,7 +508,7 @@ impl Settings { return Err(SettingsError::SuffixContainsSeparator(additional_suffix)); } let strategy = Strategy::from(matches).map_err(SettingsError::Strategy)?; - let suffix_type = suffix_type_from(matches); + let (suffix_type, suffix_start) = suffix_type_from(matches)?; let suffix_length_str = matches.get_one::(OPT_SUFFIX_LENGTH).unwrap(); let suffix_length: usize = suffix_length_str .parse() @@ -517,6 +528,7 @@ impl Settings { .parse() .map_err(|_| SettingsError::SuffixNotParsable(suffix_length_str.to_string()))?, suffix_type, + suffix_start, additional_suffix, verbose: matches.value_source("verbose") == Some(ValueSource::CommandLine), strategy, @@ -589,7 +601,8 @@ impl<'a> ByteChunkWriter<'a> { &settings.additional_suffix, settings.suffix_length, settings.suffix_type, - ); + settings.suffix_start, + )?; let filename = filename_iterator .next() .ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?; @@ -717,7 +730,8 @@ impl<'a> LineChunkWriter<'a> { &settings.additional_suffix, settings.suffix_length, settings.suffix_type, - ); + settings.suffix_start, + )?; let filename = filename_iterator .next() .ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?; @@ -825,7 +839,8 @@ impl<'a> LineBytesChunkWriter<'a> { &settings.additional_suffix, settings.suffix_length, settings.suffix_type, - ); + settings.suffix_start, + )?; let filename = filename_iterator .next() .ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?; @@ -1022,7 +1037,8 @@ where &settings.additional_suffix, settings.suffix_length, settings.suffix_type, - ); + settings.suffix_start, + )?; // Create one writer for each chunk. This will create each // of the underlying files (if not in `--filter` mode). @@ -1098,7 +1114,8 @@ where &settings.additional_suffix, settings.suffix_length, settings.suffix_type, - ); + settings.suffix_start, + )?; // Create one writer for each chunk. This will create each // of the underlying files (if not in `--filter` mode). diff --git a/src/uu/sync/Cargo.toml b/src/uu/sync/Cargo.toml index b5a9e14be5a..0d804e85bc1 100644 --- a/src/uu/sync/Cargo.toml +++ b/src/uu/sync/Cargo.toml @@ -19,6 +19,9 @@ clap = { version = "3.2", features = ["wrap_help", "cargo"] } libc = "0.2.132" uucore = { version=">=0.0.15", package="uucore", path="../../uucore", features=["wide"] } +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +nix = "0.25" + [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["errhandlingapi", "fileapi", "handleapi", "std", "winbase", "winerror"] } diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index 5bd21bb1b2c..5b08f45cbab 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -10,6 +10,12 @@ extern crate libc; use clap::{crate_version, Arg, Command}; +#[cfg(any(target_os = "linux", target_os = "android"))] +use nix::errno::Errno; +#[cfg(any(target_os = "linux", target_os = "android"))] +use nix::fcntl::{open, OFlag}; +#[cfg(any(target_os = "linux", target_os = "android"))] +use nix::sys::stat::Mode; use std::path::Path; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError}; @@ -169,12 +175,45 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); + if matches.is_present(options::DATA) && files.is_empty() { + return Err(USimpleError::new(1, "--data needs at least one argument")); + } + for f in &files { - if !Path::new(&f).exists() { - return Err(USimpleError::new( - 1, - format!("cannot stat {}: No such file or directory", f.quote()), - )); + // Use the Nix open to be able to set the NONBLOCK flags for fifo files + #[cfg(any(target_os = "linux", target_os = "android"))] + { + match open(Path::new(&f), OFlag::O_NONBLOCK, Mode::empty()) { + Ok(_) => {} + Err(e) => { + if e == Errno::ENOENT { + return Err(USimpleError::new( + 1, + format!("cannot stat {}: No such file or directory", f.quote()), + )); + } + if e == Errno::EACCES { + if Path::new(&f).is_dir() { + return Err(USimpleError::new( + 1, + format!("error opening {}: Permission denied", f.quote()), + )); + } else { + // ignore the issue + // ./target/debug/coreutils sync --data file + } + } + } + } + } + #[cfg(not(any(target_os = "linux", target_os = "android")))] + { + if !Path::new(&f).exists() { + return Err(USimpleError::new( + 1, + format!("cannot stat {}: No such file or directory", f.quote()), + )); + } } } diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index e083464d64e..a2a88a20ac3 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -2681,6 +2681,73 @@ fn test_ls_ignore_backups() { .stdout_does_not_contain(".somehiddenbackup~"); } +// This test fails on windows, see details at #3985 +#[cfg(not(windows))] +#[test] +fn test_ls_ignore_explicit_period() { + // In ls ignore patterns, leading periods must be explicitly specified + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + at.touch(".hidden.yml"); + at.touch("regular.yml"); + + scene + .ucmd() + .arg("-a") + .arg("--ignore") + .arg("?hidden.yml") + .succeeds() + .stdout_contains(".hidden.yml") + .stdout_contains("regular.yml"); + + scene + .ucmd() + .arg("-a") + .arg("--ignore") + .arg("*.yml") + .succeeds() + .stdout_contains(".hidden.yml") + .stdout_does_not_contain("regular.yml"); + + // Leading period is explicitly specified + scene + .ucmd() + .arg("-a") + .arg("--ignore") + .arg(".*.yml") + .succeeds() + .stdout_does_not_contain(".hidden.yml") + .stdout_contains("regular.yml"); +} + +// This test fails on windows, see details at #3985 +#[cfg(not(windows))] +#[test] +fn test_ls_ignore_negation() { + let scene = TestScenario::new(util_name!()); + + let at = &scene.fixtures; + at.touch("apple"); + at.touch("boy"); + + scene + .ucmd() + .arg("--ignore") + .arg("[!a]*") + .succeeds() + .stdout_contains("apple") + .stdout_does_not_contain("boy"); + + scene + .ucmd() + .arg("--ignore") + .arg("[^a]*") + .succeeds() + .stdout_contains("apple") + .stdout_does_not_contain("boy"); +} + #[test] fn test_ls_directory() { let scene = TestScenario::new(util_name!()); diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index c57e6bd6ec7..2527f4562db 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -701,3 +701,29 @@ fn test_multiple_of_input_chunk() { } assert_eq!(glob.collate(), at.read_bytes(name)); } + +#[test] +fn test_numeric_suffix() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["-n", "4", "--numeric-suffixes", "9", "threebytes.txt"]) + .succeeds() + .no_stdout() + .no_stderr(); + assert_eq!(at.read("x09"), "a"); + assert_eq!(at.read("x10"), "b"); + assert_eq!(at.read("x11"), "c"); + assert_eq!(at.read("x12"), ""); +} + +#[test] +fn test_hex_suffix() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["-n", "4", "--hex-suffixes", "9", "threebytes.txt"]) + .succeeds() + .no_stdout() + .no_stderr(); + assert_eq!(at.read("x09"), "a"); + assert_eq!(at.read("x0a"), "b"); + assert_eq!(at.read("x0b"), "c"); + assert_eq!(at.read("x0c"), ""); +} diff --git a/tests/by-util/test_sync.rs b/tests/by-util/test_sync.rs index 97259cddaef..7f2cd4b661e 100644 --- a/tests/by-util/test_sync.rs +++ b/tests/by-util/test_sync.rs @@ -44,3 +44,40 @@ fn test_sync_no_existing_files() { .fails() .stderr_contains("cannot stat"); } + +#[test] +fn test_sync_data_but_not_file() { + new_ucmd!() + .arg("--data") + .fails() + .stderr_contains("sync: --data needs at least one argument"); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(feature = "chmod")] +#[test] +fn test_sync_no_permission_dir() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + let dir = "foo"; + at.mkdir_all(dir); + + ts.ccmd("chmod").arg("0").arg(dir).succeeds(); + let result = ts.ucmd().arg("--data").arg(dir).fails(); + result.stderr_contains("sync: error opening 'foo': Permission denied"); + let result = ts.ucmd().arg(dir).fails(); + result.stderr_contains("sync: error opening 'foo': Permission denied"); +} + +#[cfg(not(target_os = "windows"))] +#[cfg(feature = "chmod")] +#[test] +fn test_sync_no_permission_file() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + let f = "file"; + at.touch(f); + + ts.ccmd("chmod").arg("0200").arg(f).succeeds(); + ts.ucmd().arg(f).succeeds(); +} diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 5460df554a1..98089f906fa 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -11,24 +11,23 @@ extern crate tail; use crate::common::random::*; use crate::common::util::*; +use rand::distributions::Alphanumeric; use std::char::from_digit; -#[cfg(unix)] use std::io::Read; use std::io::Write; use std::process::Stdio; -#[cfg(unix)] use std::thread::sleep; -#[cfg(unix)] use std::time::Duration; +use tail::chunks::BUFFER_SIZE as CHUNK_BUFFER_SIZE; static FOOBAR_TXT: &str = "foobar.txt"; static FOOBAR_2_TXT: &str = "foobar2.txt"; static FOOBAR_WITH_NULL_TXT: &str = "foobar_with_null.txt"; -#[cfg(unix)] +#[allow(dead_code)] static FOLLOW_NAME_TXT: &str = "follow_name.txt"; -#[cfg(unix)] +#[allow(dead_code)] static FOLLOW_NAME_SHORT_EXP: &str = "follow_name_short.expected"; -#[cfg(target_os = "linux")] +#[allow(dead_code)] static FOLLOW_NAME_EXP: &str = "follow_name.expected"; #[test] @@ -37,7 +36,6 @@ fn test_invalid_arg() { } #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_stdin_default() { new_ucmd!() .pipe_in_fixture(FOOBAR_TXT) @@ -47,7 +45,6 @@ fn test_stdin_default() { } #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_stdin_explicit() { new_ucmd!() .pipe_in_fixture(FOOBAR_TXT) @@ -58,7 +55,7 @@ fn test_stdin_explicit() { } #[test] -#[cfg(all(unix, not(any(target_os = "android", target_vendor = "apple"))))] // FIXME: make this work not just on Linux +#[cfg(not(target_vendor = "apple"))] // FIXME: for currently not working platforms fn test_stdin_redirect_file() { // $ echo foo > f @@ -93,7 +90,12 @@ fn test_stdin_redirect_file() { .run_no_wait(); sleep(Duration::from_millis(500)); - p.kill().unwrap(); + + // Cleanup the process if it is still running. The result isn't important + // for the test, so it is ignored. + // NOTE: The result may be Error on windows with an Os error `Permission + // Denied` if the process already terminated: + let _ = p.kill(); let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p); assert!(buf_stdout.eq("foo")); @@ -101,7 +103,7 @@ fn test_stdin_redirect_file() { } #[test] -#[cfg(all(unix, not(any(target_os = "android", target_vendor = "apple"))))] // FIXME: make this work not just on Linux +#[cfg(not(target_vendor = "apple"))] // FIXME: for currently not working platforms fn test_stdin_redirect_offset() { // inspired by: "gnu/tests/tail-2/start-middle.sh" use std::io::{Seek, SeekFrom}; @@ -122,8 +124,12 @@ fn test_stdin_redirect_offset() { } #[test] -#[cfg(all(unix, not(any(target_os = "android", target_vendor = "apple"))))] // FIXME: make this work not just on Linux +#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] // FIXME: for currently not working platforms fn test_stdin_redirect_offset2() { + // FIXME: windows: Failed because of difference in printed header. See below. + // actual : ==> - <== + // expected: ==> standard input <== + // like test_stdin_redirect_offset but with multiple files use std::io::{Seek, SeekFrom}; @@ -197,8 +203,9 @@ fn test_nc_0_wo_follow2() { .succeeded(); } +// TODO: Add similar test for windows #[test] -#[cfg(all(unix, not(target_os = "freebsd")))] +#[cfg(unix)] fn test_permission_denied() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; @@ -217,8 +224,9 @@ fn test_permission_denied() { .code_is(1); } +// TODO: Add similar test for windows #[test] -#[cfg(all(unix, not(target_os = "freebsd")))] +#[cfg(unix)] fn test_permission_denied_multiple() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; @@ -241,7 +249,6 @@ fn test_permission_denied_multiple() { } #[test] -#[cfg(target_os = "linux")] fn test_follow_redirect_stdin_name_retry() { // $ touch f && tail -F - < f // tail: cannot follow '-' by name @@ -265,8 +272,12 @@ fn test_follow_redirect_stdin_name_retry() { } #[test] -#[cfg(not(target_os = "macos"))] // See test_stdin_redirect_dir_when_target_os_is_macos -#[cfg(all(unix, not(any(target_os = "android", target_os = "freebsd"))))] // FIXME: fix this test for Android/FreeBSD +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_stdin_redirect_dir() { // $ mkdir dir // $ tail < dir, $ tail - < dir @@ -299,7 +310,7 @@ fn test_stdin_redirect_dir() { // redirected directories like linux show the correct message like in // `test_stdin_redirect_dir` #[test] -#[cfg(target_os = "macos")] +#[cfg(target_vendor = "apple")] fn test_stdin_redirect_dir_when_target_os_is_macos() { // $ mkdir dir // $ tail < dir, $ tail - < dir @@ -325,7 +336,6 @@ fn test_stdin_redirect_dir_when_target_os_is_macos() { } #[test] -#[cfg(target_os = "linux")] fn test_follow_stdin_descriptor() { let ts = TestScenario::new(util_name!()); @@ -345,7 +355,6 @@ fn test_follow_stdin_descriptor() { } #[test] -#[cfg(target_os = "linux")] fn test_follow_stdin_name_retry() { // $ tail -F - // tail: cannot follow '-' by name @@ -417,8 +426,6 @@ fn test_follow_stdin_explicit_indefinitely() { } #[test] -#[cfg(target_os = "linux")] -#[cfg(disable_until_fixed)] fn test_follow_bad_fd() { // Provoke a "bad file descriptor" error by closing the fd // inspired by: "gnu/tests/tail-2/follow-stdin.sh" @@ -465,7 +472,7 @@ fn test_null_default() { } #[test] -#[cfg(unix)] +#[cfg(not(target_os = "windows"))] // FIXME: test times out fn test_follow_single() { let (at, mut ucmd) = at_and_ucmd!(); @@ -489,7 +496,7 @@ fn test_follow_single() { /// Test for following when bytes are written that are not valid UTF-8. #[test] -#[cfg(unix)] +#[cfg(not(target_os = "windows"))] // FIXME: test times out fn test_follow_non_utf8_bytes() { // Tail the test file and start following it. let (at, mut ucmd) = at_and_ucmd!(); @@ -521,7 +528,7 @@ fn test_follow_non_utf8_bytes() { } #[test] -#[cfg(unix)] +#[cfg(not(target_os = "windows"))] // FIXME: test times out fn test_follow_multiple() { let (at, mut ucmd) = at_and_ucmd!(); let mut child = ucmd @@ -547,7 +554,7 @@ fn test_follow_multiple() { } #[test] -#[cfg(unix)] +#[cfg(not(target_os = "windows"))] // FIXME: test times out fn test_follow_name_multiple() { let (at, mut ucmd) = at_and_ucmd!(); let mut child = ucmd @@ -573,7 +580,6 @@ fn test_follow_name_multiple() { } #[test] -#[cfg(unix)] fn test_follow_multiple_untailable() { // $ tail -f DIR1 DIR2 // ==> DIR1 <== @@ -606,7 +612,6 @@ fn test_follow_multiple_untailable() { } #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_follow_stdin_pipe() { new_ucmd!() .arg("-f") @@ -617,7 +622,7 @@ fn test_follow_stdin_pipe() { } #[test] -#[cfg(unix)] +#[cfg(not(target_os = "windows"))] // FIXME: for currently not working platforms fn test_follow_invalid_pid() { new_ucmd!() .args(&["-f", "--pid=-1234"]) @@ -641,8 +646,12 @@ fn test_follow_invalid_pid() { } // FixME: test PASSES for usual windows builds, but fails for coverage testing builds (likely related to the specific RUSTFLAGS '-Zpanic_abort_tests -Cpanic=abort') This test also breaks tty settings under bash requiring a 'stty sane' or reset. // spell-checker:disable-line -#[cfg(disable_until_fixed)] #[test] +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "android") +))] // FIXME: for currently not working platforms fn test_follow_with_pid() { use std::process::{Command, Stdio}; use std::thread::sleep; @@ -652,6 +661,7 @@ fn test_follow_with_pid() { #[cfg(unix)] let dummy_cmd = "sh"; + #[cfg(windows)] let dummy_cmd = "cmd"; @@ -687,13 +697,18 @@ fn test_follow_with_pid() { let third_append = "should\nbe\nignored\n"; at.append(FOOBAR_TXT, third_append); - assert_eq!(read_size(&mut child, 1), "\u{0}"); + let mut buffer: [u8; 1] = [0; 1]; + let result = child.stdout.as_mut().unwrap().read(&mut buffer); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 0); // On Unix, trying to kill a process that's already dead is fine; on Windows it's an error. - #[cfg(unix)] - child.kill().unwrap(); - #[cfg(windows)] - assert_eq!(child.kill().is_err(), true); + let result = child.kill(); + if cfg!(windows) { + assert!(result.is_err()); + } else { + assert!(result.is_ok()); + } } #[test] @@ -735,7 +750,6 @@ fn test_bytes_single() { } #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_bytes_stdin() { new_ucmd!() .pipe_in_fixture(FOOBAR_TXT) @@ -984,7 +998,6 @@ fn test_sleep_interval() { /// Test for reading all but the first NUM bytes: `tail -c +3`. #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_positive_bytes() { new_ucmd!() .args(&["-c", "+3"]) @@ -995,7 +1008,6 @@ fn test_positive_bytes() { /// Test for reading all bytes, specified by `tail -c +0`. #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_positive_zero_bytes() { let ts = TestScenario::new(util_name!()); ts.ucmd() @@ -1013,7 +1025,6 @@ fn test_positive_zero_bytes() { /// Test for reading all but the first NUM lines: `tail -n +3`. #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_positive_lines() { new_ucmd!() .args(&["-n", "+3"]) @@ -1055,7 +1066,6 @@ once /// Test for reading all but the first NUM lines: `tail -3`. #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_obsolete_syntax_positive_lines() { new_ucmd!() .args(&["-3"]) @@ -1066,7 +1076,6 @@ fn test_obsolete_syntax_positive_lines() { /// Test for reading all but the first NUM lines: `tail -n -10`. #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_small_file() { new_ucmd!() .args(&["-n -10"]) @@ -1077,7 +1086,6 @@ fn test_small_file() { /// Test for reading all but the first NUM lines: `tail -10`. #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_obsolete_syntax_small_file() { new_ucmd!() .args(&["-10"]) @@ -1088,7 +1096,6 @@ fn test_obsolete_syntax_small_file() { /// Test for reading all lines, specified by `tail -n +0`. #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_positive_zero_lines() { let ts = TestScenario::new(util_name!()); ts.ucmd() @@ -1106,7 +1113,6 @@ fn test_positive_zero_lines() { } #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_invalid_num() { new_ucmd!() .args(&["-c", "1024R", "emptyfile.txt"]) @@ -1138,7 +1144,6 @@ fn test_invalid_num() { } #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_num_with_undocumented_sign_bytes() { // tail: '-' is not documented (8.32 man pages) // head: '+' is not documented (8.32 man pages) @@ -1162,7 +1167,7 @@ fn test_num_with_undocumented_sign_bytes() { #[test] #[cfg(unix)] -fn test_bytes_for_funny_files() { +fn test_bytes_for_funny_unix_files() { // inspired by: gnu/tests/tail-2/tail-c.sh let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; @@ -1181,7 +1186,6 @@ fn test_bytes_for_funny_files() { } #[test] -#[cfg(unix)] fn test_retry1() { // inspired by: gnu/tests/tail-2/retry.sh // Ensure --retry without --follow results in a warning. @@ -1198,7 +1202,6 @@ fn test_retry1() { } #[test] -#[cfg(unix)] fn test_retry2() { // inspired by: gnu/tests/tail-2/retry.sh // The same as test_retry2 with a missing file: expect error message and exit 1. @@ -1221,7 +1224,12 @@ fn test_retry2() { } #[test] -#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_retry3() { // inspired by: gnu/tests/tail-2/retry.sh // Ensure that `tail --retry --follow=name` waits for the file to appear. @@ -1259,7 +1267,12 @@ fn test_retry3() { } #[test] -#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_retry4() { // inspired by: gnu/tests/tail-2/retry.sh // Ensure that `tail --retry --follow=descriptor` waits for the file to appear. @@ -1309,7 +1322,12 @@ fn test_retry4() { } #[test] -#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_retry5() { // inspired by: gnu/tests/tail-2/retry.sh // Ensure that `tail --follow=descriptor --retry` exits when the file appears untailable. @@ -1345,7 +1363,7 @@ fn test_retry5() { } #[test] -#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS +#[cfg(not(target_os = "windows"))] // FIXME: for currently not working platforms fn test_retry6() { // inspired by: gnu/tests/tail-2/retry.sh // Ensure that --follow=descriptor (without --retry) does *not* try @@ -1383,7 +1401,12 @@ fn test_retry6() { } #[test] -#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_retry7() { // inspired by: gnu/tests/tail-2/retry.sh // Ensure that `tail -F` retries when the file is initially untailable. @@ -1450,7 +1473,12 @@ fn test_retry7() { } #[test] -#[cfg(all(unix, not(any(target_os = "android", target_vendor = "apple"))))] // FIXME: make this work not just on Linux +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_retry8() { // Ensure that inotify will switch to polling mode if directory // of the watched file was initially missing and later created. @@ -1512,7 +1540,11 @@ fn test_retry8() { } #[test] -#[cfg(all(unix, not(any(target_os = "android", target_vendor = "apple"))))] // FIXME: make this work not just on Linux +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_retry9() { // inspired by: gnu/tests/tail-2/inotify-dir-recreate.sh // Ensure that inotify will switch to polling mode if directory @@ -1589,7 +1621,11 @@ fn test_retry9() { } #[test] -#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_follow_descriptor_vs_rename1() { // inspired by: gnu/tests/tail-2/descriptor-vs-rename.sh // $ ((rm -f A && touch A && sleep 1 && echo -n "A\n" >> A && sleep 1 && \ @@ -1646,7 +1682,11 @@ fn test_follow_descriptor_vs_rename1() { } #[test] -#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_follow_descriptor_vs_rename2() { // Ensure the headers are correct for --verbose. // NOTE: GNU's tail does not update the header from FILE_A to FILE_C after `mv FILE_A FILE_C` @@ -1695,7 +1735,12 @@ fn test_follow_descriptor_vs_rename2() { } #[test] -#[cfg(target_os = "linux")] +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_follow_name_retry_headers() { // inspired by: "gnu/tests/tail-2/F-headers.sh" // Ensure tail -F distinguishes output with the @@ -1756,7 +1801,7 @@ fn test_follow_name_retry_headers() { } #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: make this work not just on Linux +#[cfg(all(not(target_os = "windows"), not(target_os = "android")))] // FIXME: for currently not working platforms fn test_follow_name_remove() { // This test triggers a remove event while `tail --follow=name file` is running. // ((sleep 2 && rm file &)>/dev/null 2>&1 &) ; tail --follow=name file @@ -1805,7 +1850,11 @@ fn test_follow_name_remove() { } #[test] -#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS +#[cfg(all( + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_follow_name_truncate1() { // This test triggers a truncate event while `tail --follow=name file` is running. // $ cp file backup && head file > file && sleep 1 && cp backup file @@ -1839,7 +1888,11 @@ fn test_follow_name_truncate1() { } #[test] -#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS +#[cfg(all( + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_follow_name_truncate2() { // This test triggers a truncate event while `tail --follow=name file` is running. // $ ((sleep 1 && echo -n "x\nx\nx\n" >> file && sleep 1 && \ @@ -1876,7 +1929,7 @@ fn test_follow_name_truncate2() { } #[test] -#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS +#[cfg(not(target_os = "windows"))] // FIXME: for currently not working platforms fn test_follow_name_truncate3() { // Opening an empty file in truncate mode should not trigger a truncate event while // `tail --follow=name file` is running. @@ -1907,7 +1960,7 @@ fn test_follow_name_truncate3() { } #[test] -#[cfg(unix)] +#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] // FIXME: for currently not working platforms fn test_follow_name_truncate4() { // Truncating a file with the same content it already has should not trigger a truncate event @@ -1939,7 +1992,7 @@ fn test_follow_name_truncate4() { } #[test] -#[cfg(all(unix, not(target_os = "android")))] +#[cfg(not(target_os = "windows"))] // FIXME: for currently not working platforms fn test_follow_truncate_fast() { // inspired by: "gnu/tests/tail-2/truncate.sh" // Ensure all logs are output upon file truncation @@ -1989,7 +2042,12 @@ fn test_follow_truncate_fast() { } #[test] -#[cfg(target_os = "linux")] // FIXME: make this work not just on Linux +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_follow_name_move_create1() { // This test triggers a move/create event while `tail --follow=name file` is running. // ((sleep 2 && mv file backup && sleep 2 && cp backup file &)>/dev/null 2>&1 &) ; tail --follow=name file @@ -2002,6 +2060,7 @@ fn test_follow_name_move_create1() { #[cfg(target_os = "linux")] let expected_stdout = at.read(FOLLOW_NAME_EXP); + #[cfg(target_os = "linux")] let expected_stderr = format!( "{}: {}: No such file or directory\n{0}: '{1}' has appeared; following new file\n", @@ -2009,8 +2068,10 @@ fn test_follow_name_move_create1() { ); // NOTE: We are less strict if not on Linux (inotify backend). + #[cfg(not(target_os = "linux"))] let expected_stdout = at.read(FOLLOW_NAME_SHORT_EXP); + #[cfg(not(target_os = "linux"))] let expected_stderr = format!("{}: {}: No such file or directory\n", ts.util_name, source); @@ -2034,7 +2095,11 @@ fn test_follow_name_move_create1() { } #[test] -#[cfg(target_os = "linux")] // FIXME: make this work not just on Linux +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_follow_name_move_create2() { // inspired by: "gnu/tests/tail-2/inotify-hash-abuse.sh" // Exercise an abort-inducing flaw in inotify-enabled tail -F @@ -2105,7 +2170,12 @@ fn test_follow_name_move_create2() { } #[test] -#[cfg(target_os = "linux")] // FIXME: make this work not just on Linux +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_follow_name_move1() { // This test triggers a move event while `tail --follow=name file` is running. // ((sleep 2 && mv file backup &)>/dev/null 2>&1 &) ; tail --follow=name file @@ -2150,7 +2220,12 @@ fn test_follow_name_move1() { } #[test] -#[cfg(target_os = "linux")] // FIXME: make this work not just on Linux +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_follow_name_move2() { // Like test_follow_name_move1, but move to a name that's already monitored. @@ -2231,7 +2306,12 @@ fn test_follow_name_move2() { } #[test] -#[cfg(target_os = "linux")] // FIXME: make this work not just on Linux +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_follow_name_move_retry1() { // Similar to test_follow_name_move1 but with `--retry` (`-F`) // This test triggers two move/rename events while `tail --follow=name --retry file` is running. @@ -2281,8 +2361,14 @@ fn test_follow_name_move_retry1() { delay /= 3; } } + #[test] -#[cfg(target_os = "linux")] // FIXME: make this work not just on Linux +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "android"), + not(target_os = "freebsd") +))] // FIXME: for currently not working platforms fn test_follow_name_move_retry2() { // inspired by: "gnu/tests/tail-2/F-vs-rename.sh" // Similar to test_follow_name_move2 (move to a name that's already monitored) @@ -2380,7 +2466,7 @@ fn test_follow_name_move_retry2() { } #[test] -#[cfg(all(unix, not(target_os = "freebsd")))] // FIXME: fix this test for FreeBSD +#[cfg(not(target_os = "windows"))] // FIXME: for currently not working platforms fn test_follow_inotify_only_regular() { // The GNU test inotify-only-regular.sh uses strace to ensure that `tail -f` // doesn't make inotify syscalls and only uses inotify for regular files or fifos. @@ -2403,7 +2489,6 @@ fn test_follow_inotify_only_regular() { assert_eq!(buf_stderr, "".to_string()); } -#[cfg(unix)] fn take_stdout_stderr(p: &mut std::process::Child) -> (String, String) { let mut buf_stdout = String::new(); let mut p_stdout = p.stdout.take().unwrap(); @@ -2426,13 +2511,11 @@ fn test_no_such_file() { } #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_no_trailing_newline() { new_ucmd!().pipe_in("x").succeeds().stdout_only("x"); } #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_lines_zero_terminated() { new_ucmd!() .args(&["-z", "-n", "2"]) @@ -2447,7 +2530,6 @@ fn test_lines_zero_terminated() { } #[test] -#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android fn test_presume_input_pipe_default() { new_ucmd!() .arg("---presume-input-pipe") @@ -2458,7 +2540,7 @@ fn test_presume_input_pipe_default() { } #[test] -#[cfg(unix)] +#[cfg(not(windows))] fn test_fifo() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; @@ -2512,722 +2594,721 @@ 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 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); +#[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", "999"]) - .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", "+3"]) - .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() + .stdout_only(test_string); - new_ucmd!() - .args(&["-n", "+4"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .no_stdout() - .no_stderr(); + new_ucmd!() + .args(&["-n", "+3"]) + .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(); - } + new_ucmd!() + .args(&["-n", "+4"]) + .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", "+999"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .no_stdout() + .no_stderr(); +} - new_ucmd!() - .args(&["-n", "0"]) - .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", "1"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only("b"); + new_ucmd!() + .args(&["-n", "0"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .no_stdout() + .no_stderr(); - new_ucmd!() - .args(&["-n", "2"]) - .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("b"); - #[test] - fn test_pipe_when_positive_lines_option_given_no_newline_at_eof() { - let test_string = "a\nb"; + new_ucmd!() + .args(&["-n", "2"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only("a\nb"); +} - new_ucmd!() - .args(&["-n", "+0"]) - .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", "+1"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only("a\nb"); + new_ucmd!() + .args(&["-n", "+0"]) + .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"); - } + new_ucmd!() + .args(&["-n", "+1"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only("a\nb"); - #[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", "+2"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only("b"); +} - new_ucmd!() - .args(&["-n", "+0"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only(test_string); +#[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", "+2"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only("⏻\nƒ\na"); + new_ucmd!() + .args(&["-n", "+0"]) + .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("ƒ\na"); + new_ucmd!() + .args(&["-n", "+2"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only("⏻\nƒ\na"); - new_ucmd!() - .args(&["-n", "+4"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only("a"); + new_ucmd!() + .args(&["-n", "+3"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only("ƒ\na"); - 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("a"); - new_ucmd!() - .args(&["-n", "-4"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only(test_string); + new_ucmd!() + .args(&["-n", "+5"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .no_stdout() + .no_stderr(); - new_ucmd!() - .args(&["-n", "-3"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only("⏻\nƒ\na"); + new_ucmd!() + .args(&["-n", "-4"]) + .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("ƒ\na"); + new_ucmd!() + .args(&["-n", "-3"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only("⏻\nƒ\na"); - new_ucmd!() - .args(&["-n", "-1"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only("a"); + new_ucmd!() + .args(&["-n", "-2"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only("ƒ\na"); - 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("a"); - #[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'); + new_ucmd!() + .args(&["-n", "-0"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .no_stdout() + .no_stderr(); +} - let expected = lines.clone().skip(1).collect::(); - new_ucmd!() - .args(&["-n", "+2"]) - .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_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::(); - new_ucmd!() - .args(&["-n", "-1"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only(expected); - } + let expected = lines.clone().skip(1).collect::(); + new_ucmd!() + .args(&["-n", "+2"]) + .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'); + let expected = lines.clone().skip(1).collect::(); + new_ucmd!() + .args(&["-n", "-1"]) + .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() - .stdout_only(random_string); +#[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'); - let expected = lines.clone().skip(1).collect::(); - 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() + .stdout_only(random_string); - new_ucmd!() - .args(&["-n", "-0"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .no_stdout() - .no_stderr(); + let expected = lines.clone().skip(1).collect::(); + new_ucmd!() + .args(&["-n", "+2"]) + .pipe_in(random_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only(expected); - let expected = lines.clone().skip(total_lines - 1).collect::(); - new_ucmd!() - .args(&["-n", "-1"]) - .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(1).collect::(); - new_ucmd!() - .args(&["-n", "-99"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only(expected); + let expected = lines.clone().skip(total_lines - 1).collect::(); + new_ucmd!() + .args(&["-n", "-1"]) + .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); - } + let expected = lines.clone().skip(1).collect::(); + 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_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", "-100"]) + .pipe_in(random_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only(random_string); +} - new_ucmd!() - .args(&["-n", "+0"]) - .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'); - let expected = lines.clone().skip(total_lines - 1).collect::(); - new_ucmd!() - .args(&["-n", "-1"]) - .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() + .stdout_only(random_string); - let expected = lines.clone().skip(1).collect::(); - new_ucmd!() - .args(&["-n", "+2"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only(expected); + let expected = lines.clone().skip(total_lines - 1).collect::(); + new_ucmd!() + .args(&["-n", "-1"]) + .pipe_in(random_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only(expected); - let expected = lines.clone().skip(1).collect::(); - new_ucmd!() - .args(&["-n", "-99"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only(expected); - } + let expected = lines.clone().skip(1).collect::(); + new_ucmd!() + .args(&["-n", "+2"]) + .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'); + let expected = lines.clone().skip(1).collect::(); + new_ucmd!() + .args(&["-n", "-99"]) + .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() - .stdout_only(random_string); +// FIXME: windows: this test failed with timeout in the CI. Running this test in +// a Windows VirtualBox image produces no errors. +#[test] +#[cfg(not(target_os = "windows"))] +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'); - let expected = lines.clone().skip(1).collect::(); - 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() + .stdout_only(random_string); - new_ucmd!() - .args(&["-n", "-0"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .no_stdout() - .no_stderr(); + let expected = lines.clone().skip(1).collect::(); + new_ucmd!() + .args(&["-n", "+2"]) + .pipe_in(random_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only(expected); - let expected = lines.clone().skip(total_lines - 1).collect::(); - new_ucmd!() - .args(&["-n", "-1"]) - .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(1).collect::(); - new_ucmd!() - .args(&["-n", "-99"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only(expected); + let expected = lines.clone().skip(total_lines - 1).collect::(); + new_ucmd!() + .args(&["-n", "-1"]) + .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); - } + let expected = lines.clone().skip(1).collect::(); + new_ucmd!() + .args(&["-n", "-99"]) + .pipe_in(random_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only(expected); - #[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(&["-n", "-100"]) + .pipe_in(random_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only(random_string); +} - new_ucmd!() - .args(&["-c", "5"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only(test_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", "999"]) - .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", "+4"]) - .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() + .stdout_only(test_string); - new_ucmd!() - .args(&["-c", "+5"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .no_stdout() - .no_stderr(); + new_ucmd!() + .args(&["-c", "+4"]) + .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(); - } + new_ucmd!() + .args(&["-c", "+5"]) + .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", "+999"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .no_stdout() + .no_stderr(); +} - new_ucmd!() - .args(&["-c", "+0"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only(test_string); +#[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", "+2"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only_bytes(&test_string.as_bytes()[1..]); + new_ucmd!() + .args(&["-c", "+0"]) + .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("⏻ƒa"); + 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", "+8"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only("ƒa"); + new_ucmd!() + .args(&["-c", "+5"]) + .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", "+8"]) + .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", "+10"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only("a"); - new_ucmd!() - .args(&["-c", "-1"]) - .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", "-2"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only_bytes(&"ƒa".as_bytes()[1..]); + new_ucmd!() + .args(&["-c", "-1"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only("a"); - new_ucmd!() - .args(&["-c", "-3"]) - .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", "-6"]) - .pipe_in(test_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only("⏻ƒa"); + new_ucmd!() + .args(&["-c", "-3"]) + .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); - } + new_ucmd!() + .args(&["-c", "-6"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only("⏻ƒa"); - #[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", "-10"]) + .pipe_in(test_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only(test_string); +} - new_ucmd!() - .args(&["-c", "+0"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only(random_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(); - 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() + .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()[1..]; + new_ucmd!() + .args(&["-c", "+2"]) + .pipe_in(random_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only_bytes(expected); - 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", "-0"]) + .pipe_in(random_string) + .ignore_stdin_write_error() + .succeeds() + .no_stdout() + .no_stderr(); - new_ucmd!() - .args(&["-c", "-8192"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only_bytes(random_string); + 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", "-8193"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only_bytes(random_string); + new_ucmd!() + .args(&["-c", "-8192"]) + .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); - } + new_ucmd!() + .args(&["-c", "-8193"]) + .pipe_in(random_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only_bytes(random_string); - #[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(); + 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); +} - new_ucmd!() - .args(&["-c", "+0"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only(random_string); +#[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(); - 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() + .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()[1..]; + new_ucmd!() + .args(&["-c", "+2"]) + .pipe_in(random_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only_bytes(expected); - 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); + 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", "-8192"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only_bytes(expected); + 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); - new_ucmd!() - .args(&["-c", "-8193"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .stdout_only(random_string); - } + 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(); +// FIXME: windows: this test failed with timeout in the CI. Running this test in +// a Windows VirtualBox image produces no errors. +#[test] +#[cfg(not(target_os = "windows"))] +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() + .stdout_only(random_string); - new_ucmd!() - .args(&["-c", "-0"]) - .pipe_in(random_string) - .ignore_stdin_write_error() - .succeeds() - .no_stdout() - .no_stderr(); + 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()[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()[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()[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()[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()[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()[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()[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); + 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); - } + new_ucmd!() + .args(&["-c", "-24576"]) + .pipe_in(random_string) + .ignore_stdin_write_error() + .succeeds() + .stdout_only(random_string); } #[test] diff --git a/tests/common/random.rs b/tests/common/random.rs index 338aeab5070..36ca3ab4f3a 100644 --- a/tests/common/random.rs +++ b/tests/common/random.rs @@ -311,4 +311,30 @@ mod tests { ); assert!(random_string.as_bytes().ends_with(&[0])); } + + /// Originally used to exclude an error within the `random` module. The two + /// affected tests timed out on windows, but only in the ci. These tests are + /// also the source for the concrete numbers. The timed out tests are + /// `test_tail.rs::test_pipe_when_lines_option_given_input_size_has_multiple_size_of_buffer_size` + /// `test_tail.rs::test_pipe_when_bytes_option_given_input_size_has_multiple_size_of_buffer_size`. + #[test] + fn test_generate_random_strings_when_length_is_around_critical_buffer_sizes() { + let length = 8192 * 3; + let random_string = RandomString::generate(AlphanumericNewline, length); + assert_eq!(length, random_string.len()); + + let length = 8192 * 3 + 1; + let random_string = + RandomString::generate_with_delimiter(&Alphanumeric, b'\n', 100, true, length); + assert_eq!(length, random_string.len()); + assert_eq!( + 100, + random_string + .as_bytes() + .iter() + .filter(|p| **p == b'\n') + .count() + ); + assert!(!random_string.as_bytes().ends_with(&[0])); + } }