use crate::env;
use crate::step_job::StepJob;
use crate::tera;
use std::path::PathBuf;
use std::sync::Arc;
use super::{ShellType, types::Step};
const CMD_COMMAND_LINE_LIMIT: usize = 8191;
const CMD_COMMAND_LINE_SAFE_LIMIT: usize = CMD_COMMAND_LINE_LIMIT / 2;
impl Step {
fn auto_batch_safe_limit(&self) -> usize {
match self.shell_type() {
ShellType::Cmd => CMD_COMMAND_LINE_SAFE_LIMIT,
_ => *env::ARG_MAX / 2,
}
}
pub(crate) fn estimate_files_string_size(&self, files: &[PathBuf]) -> usize {
files
.iter()
.map(|f| {
let path_str = f.to_str().unwrap_or("");
path_str.len() * 2 + 2 + 1
})
.sum()
}
fn render_run_command_size(
&self,
original_job: &StepJob,
files: &[PathBuf],
base_tctx: &tera::Context,
) -> Option<usize> {
let run_cmd = if original_job.check_first {
self.check_first_cmd()
} else {
self.run_cmd(original_job.run_type)
}?;
let run = run_cmd.to_string();
if run.trim().is_empty() {
return None;
}
let run = if let Some(prefix) = &self.prefix {
format!("{prefix} {run}")
} else {
run
};
let mut temp = StepJob::new(
Arc::clone(&original_job.step),
files.to_vec(),
original_job.run_type,
);
temp.check_first = original_job.check_first;
if let Some(wi) = original_job.workspace_indicator() {
temp = temp.with_workspace_indicator(wi.clone());
}
let tctx = temp.tctx(base_tctx);
tera::render(&run, &tctx).ok().map(|s| s.len())
}
pub(crate) fn auto_batch_jobs(
&self,
jobs: Vec<StepJob>,
base_tctx: &tera::Context,
) -> Vec<StepJob> {
if self.stdin.is_some() {
return jobs;
}
let safe_limit = self.auto_batch_safe_limit();
let mut batched_jobs = Vec::with_capacity(jobs.len());
for job in jobs {
if job.skip_reason.is_some() || job.files.len() <= 1 {
batched_jobs.push(job);
continue;
}
let full_size = self
.render_run_command_size(&job, &job.files, base_tctx)
.unwrap_or_else(|| self.estimate_files_string_size(&job.files));
if full_size <= safe_limit {
batched_jobs.push(job);
continue;
}
debug!(
"{}: auto-batching {} files (rendered size: {} bytes, limit: {} bytes)",
self.name,
job.files.len(),
full_size,
safe_limit
);
let mut low = 1;
let mut high = job.files.len();
while low < high {
let mid = (low + high).div_ceil(2);
let test_size = self
.render_run_command_size(&job, &job.files[..mid], base_tctx)
.unwrap_or_else(|| self.estimate_files_string_size(&job.files[..mid]));
if test_size <= safe_limit {
low = mid;
} else {
high = mid - 1;
}
}
let batch_size = low.max(1);
debug!(
"{}: using batch size of {} files per batch",
self.name, batch_size
);
for chunk in job.files.chunks(batch_size) {
let mut new_job = StepJob::new(Arc::clone(&job.step), chunk.to_vec(), job.run_type);
new_job.check_first = job.check_first;
new_job.skip_reason = job.skip_reason.clone();
if let Some(wi) = job.workspace_indicator() {
new_job = new_job.with_workspace_indicator(wi.clone());
}
batched_jobs.push(new_job);
}
}
batched_jobs
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::step::RunType;
use crate::step_job::StepJob;
#[test]
fn cmd_shell_uses_cmd_command_line_limit() {
let step = Step {
shell: Some("cmd.exe".parse().unwrap()),
..Default::default()
};
assert_eq!(step.auto_batch_safe_limit(), CMD_COMMAND_LINE_SAFE_LIMIT);
}
#[test]
fn non_cmd_shell_uses_arg_max_limit() {
let step = Step {
shell: Some("sh".parse().unwrap()),
..Default::default()
};
assert_eq!(step.auto_batch_safe_limit(), *env::ARG_MAX / 2);
}
#[test]
fn cmd_shell_auto_batches_below_unix_arg_max() {
let step = Step {
name: "test".to_string(),
shell: Some("cmd.exe".parse().unwrap()),
check: Some("echo {{files}}".parse().unwrap()),
..Default::default()
};
let files = (0..400)
.map(|i| {
PathBuf::from(format!(
"directory/with/a/long/path/file_with_a_long_name_{i}.txt"
))
})
.collect();
let job = StepJob::new(Arc::new(step.clone()), files, RunType::Check);
let jobs = step.auto_batch_jobs(vec![job], &tera::Context::default());
assert!(jobs.len() > 1);
}
}
impl Step {
pub(crate) fn has_filters(&self) -> bool {
self.glob.is_some()
|| self.dir.is_some()
|| self
.exclude
.as_ref()
.is_some_and(|pattern| !pattern.is_empty())
|| self.types.is_some()
}
}