feat(cli): support --single-thread in the Rust frontend

The Rust CLI frontend rejected --single-thread, but tests/playTests.sh
uses it 28 times, so the flag is required before the original CLI test
suite can gate the migration.

Mirror the C zstdcli.c semantics: --single-thread pins zero workers and
sets a latch that suppresses the automatic core-count resolution, so
fileio receives nbWorkers == 0 and runs its single-thread streaming
mode (slightly different from -T1, which uses one worker thread).  A
bare zero from -T0 or the zstdmt program name still auto-detects the
core count, and a later -T# overrides the pinned worker count while the
latch stays set, exactly as the C variable pair behaved.

Test plan:
- cd rust/cli && cargo clippy --all-targets -- -D warnings && cargo
  test --all-targets (plus the compression-only and decompression-only
  feature matrices); new parser tests cover the flag, the -T override
  order, and rejection of an attached value.
- make -C programs zstd, then: --single-thread -3/-19 round-trips
  against COPYING via cmp; --single-thread=1 fails with "does not take
  an argument".
This commit is contained in:
2026-07-11 09:39:26 +02:00
parent 529cd297e2
commit 25f2aa9502
+47 -4
View File
@@ -210,6 +210,7 @@ struct Cli {
mmap_dict: i32,
progress: i32,
workers: Option<i32>,
single_thread: bool,
block_size: Option<usize>,
mem_limit: Option<u32>,
ldm: bool,
@@ -254,6 +255,7 @@ impl Cli {
mmap_dict: ZSTD_PS_AUTO,
progress: FIO_PS_AUTO,
workers: None,
single_thread: false,
block_size: None,
mem_limit: None,
ldm: false,
@@ -332,8 +334,11 @@ unsafe fn default_worker_count() -> i32 {
}
#[cfg(feature = "compression")]
unsafe fn resolved_worker_count(workers: Option<i32>) -> i32 {
unsafe fn resolved_worker_count(workers: Option<i32>, single_thread: bool) -> i32 {
match workers {
/* --single-thread pins zero workers; a bare zero (-T0 or the zstdmt
* program name) auto-detects the core count as in the C CLI. */
Some(0) if single_thread => 0,
Some(0) => unsafe { UTIL_countPhysicalCores() }.max(1),
Some(workers) => workers,
None => unsafe { default_worker_count() },
@@ -385,7 +390,7 @@ fn usage(advanced: bool) {
let _ = writeln!(out, "\nImplemented advanced compression controls:");
let _ = writeln!(
out,
" --fast[=#], --ultra, --long[=#], --threads=#, --block-size=#"
" --fast[=#], --ultra, --long[=#], --threads=#, --single-thread, --block-size=#"
);
let _ = writeln!(
out,
@@ -638,6 +643,7 @@ fn parse_long_option(
| "--no-row-match-finder"
| "--row-match-finder"
| "--rsyncable"
| "--single-thread"
| "--compress-literals"
| "--no-compress-literals"
| "--exclude-compressed"
@@ -809,6 +815,14 @@ fn parse_long_option(
cli.workers = Some(parse_worker_count(&value)?);
Ok(None)
}
"--single-thread" => {
/* As in the C CLI: zero workers plus a latch that suppresses the
* automatic core-count resolution, so fileio runs its
* single-thread streaming mode (slightly different from -T1). */
cli.workers = Some(0);
cli.single_thread = true;
Ok(None)
}
"--memlimit" | "--memory" | "--memlimit-decompress" => {
let value = next_value(attached, args, index, name)?;
cli.mem_limit = Some(parse_u32(&value, "memory limit")?);
@@ -854,7 +868,6 @@ fn parse_long_option(
| "--trace"
| "--format"
| "--priority"
| "--single-thread"
| "--auto-threads"
| "--fake-stdin-is-console"
| "--fake-stdout-is-console"
@@ -1012,7 +1025,7 @@ unsafe fn apply_preferences(cli: &Cli, prefs: *mut FIO_prefs_t, ctx: *mut FIO_ct
}),
);
#[cfg(feature = "compression")]
FIO_setNbWorkers(prefs, resolved_worker_count(cli.workers));
FIO_setNbWorkers(prefs, resolved_worker_count(cli.workers, cli.single_thread));
FIO_setLdmFlag(prefs, u32::from(cli.ldm));
FIO_setAdaptiveMode(prefs, i32::from(cli.adapt));
FIO_setRsyncable(prefs, i32::from(cli.rsyncable));
@@ -1502,6 +1515,36 @@ mod tests {
let cli = parse(&["zstdmt", "input"]);
assert_eq!(cli.workers, Some(0));
assert!(!cli.single_thread);
}
#[test]
fn single_thread_pins_zero_workers() {
let cli = parse(&["zstd", "--single-thread", "input"]);
assert_eq!(cli.workers, Some(0));
assert!(cli.single_thread);
}
#[test]
fn a_later_thread_count_overrides_single_thread_workers() {
/* Mirrors the C CLI: -T after --single-thread wins the worker count,
* while the single-thread latch stays set. */
let cli = parse(&["zstd", "--single-thread", "-T2", "input"]);
assert_eq!(cli.workers, Some(2));
assert!(cli.single_thread);
}
#[test]
fn single_thread_rejects_attached_values() {
let error = parse_args(vec![
OsString::from("zstd"),
OsString::from("--single-thread=1"),
])
.expect_err("an attached value must not activate --single-thread");
assert!(error.contains("does not take an argument"));
}
#[test]