Skip to main content

spring_batch_rs/core/
step.rs

1//! # Step Module
2//!
3//! This module provides the core step execution functionality for the Spring Batch framework.
4//! A step represents a single phase of a batch job that processes data in chunks or executes
5//! a single task (tasklet).
6//!
7//! ## Overview
8//!
9//! The step module supports two main execution patterns:
10//!
11//! ### Chunk-Oriented Processing
12//! Processes data in configurable chunks using the read-process-write pattern:
13//! - **Reader**: Reads items from a data source
14//! - **Processor**: Transforms items (optional)
15//! - **Writer**: Writes processed items to a destination
16//!
17//! ### Tasklet Processing
18//! Executes a single task or operation that doesn't follow the chunk pattern.
19//!
20//! ## Key Features
21//!
22//! - **Error Handling**: Configurable skip limits for fault tolerance
23//! - **Metrics Tracking**: Comprehensive execution statistics
24//! - **Lifecycle Management**: Proper resource management with open/close operations
25//! - **Builder Pattern**: Fluent API for step configuration
26//!
27//! ## Examples
28//!
29//! ### Basic Chunk-Oriented Step
30//!
31//! ```rust
32//! use spring_batch_rs::core::step::{StepBuilder, StepExecution, Step};
33//! use spring_batch_rs::core::item::{ItemReader, ItemProcessor, ItemWriter};
34//! use spring_batch_rs::BatchError;
35//!
36//! // Implement your reader, processor, and writer
37//! # struct MyReader;
38//! # impl ItemReader<String> for MyReader {
39//! #     fn read(&self) -> Result<Option<String>, BatchError> { Ok(None) }
40//! # }
41//! # struct MyProcessor;
42//! # impl ItemProcessor<String, String> for MyProcessor {
43//! #     fn process(&self, item: String) -> Result<Option<String>, BatchError> { Ok(Some(item)) }
44//! # }
45//! # struct MyWriter;
46//! # impl ItemWriter<String> for MyWriter {
47//! #     fn write(&self, items: &[String]) -> Result<(), BatchError> { Ok(()) }
48//! #     fn flush(&self) -> Result<(), BatchError> { Ok(()) }
49//! #     fn open(&self) -> Result<(), BatchError> { Ok(()) }
50//! #     fn close(&self) -> Result<(), BatchError> { Ok(()) }
51//! # }
52//!
53//! let reader = MyReader;
54//! let processor = MyProcessor;
55//! let writer = MyWriter;
56//!
57//! let step = StepBuilder::new("my-step")
58//!     .chunk(100)                    // Process 100 items per chunk
59//!     .reader(&reader)
60//!     .processor(&processor)
61//!     .writer(&writer)
62//!     .skip_limit(10)               // Allow up to 10 errors
63//!     .build();
64//!
65//! let mut step_execution = StepExecution::new(step.get_name());
66//! let result = step.execute(&mut step_execution);
67//! ```
68//!
69//! ### Tasklet Step
70//!
71//! ```rust
72//! use spring_batch_rs::core::step::{StepBuilder, StepExecution, RepeatStatus, Step, Tasklet};
73//! use spring_batch_rs::BatchError;
74//!
75//! # struct MyTasklet;
76//! # impl Tasklet for MyTasklet {
77//! #     fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
78//! #         Ok(RepeatStatus::Finished)
79//! #     }
80//! # }
81//!
82//! let tasklet = MyTasklet;
83//!
84//! let step = StepBuilder::new("my-tasklet-step")
85//!     .tasklet(&tasklet)
86//!     .build();
87//!
88//! let mut step_execution = StepExecution::new(step.get_name());
89//! let result = step.execute(&mut step_execution);
90//! ```
91
92use crate::BatchError;
93use log::{debug, error, info, warn};
94use std::marker::PhantomData;
95use std::time::{Duration, Instant};
96use uuid::Uuid;
97
98use super::item::{ItemProcessor, ItemReader, ItemWriter, PassThroughProcessor};
99
100/// A tasklet represents a single task or operation that can be executed as part of a step.
101///
102/// Tasklets are useful for operations that don't fit the chunk-oriented processing model,
103/// such as file operations, database maintenance, or custom business logic.
104///
105/// # Examples
106///
107/// ```rust
108/// use spring_batch_rs::core::step::{StepExecution, RepeatStatus};
109/// use spring_batch_rs::BatchError;
110///
111/// use spring_batch_rs::core::step::Tasklet;
112///
113/// struct FileCleanupTasklet {
114///     directory: String,
115/// }
116///
117/// impl Tasklet for FileCleanupTasklet {
118///     fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
119///         // Perform file cleanup logic here
120///         println!("Cleaning up directory: {}", self.directory);
121///         Ok(RepeatStatus::Finished)
122///     }
123/// }
124/// ```
125pub trait Tasklet {
126    /// Executes the tasklet operation.
127    ///
128    /// # Parameters
129    /// - `step_execution`: The current step execution context for accessing metrics and state
130    ///
131    /// # Returns
132    /// - `Ok(RepeatStatus)`: The tasklet completed successfully
133    /// - `Err(BatchError)`: An error occurred during execution
134    fn execute(&self, step_execution: &StepExecution) -> Result<RepeatStatus, BatchError>;
135}
136
137/// A step implementation that executes a single tasklet.
138///
139/// TaskletStep is used for operations that don't follow the chunk-oriented processing pattern.
140/// It executes a single tasklet and manages the step lifecycle.
141///
142/// # Examples
143///
144/// ```rust
145/// use spring_batch_rs::core::step::{StepBuilder, StepExecution, RepeatStatus, Tasklet};
146/// use spring_batch_rs::BatchError;
147///
148/// # struct MyTasklet;
149/// # impl Tasklet for MyTasklet {
150/// #     fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
151/// #         Ok(RepeatStatus::Finished)
152/// #     }
153/// # }
154/// let tasklet = MyTasklet;
155/// let step = StepBuilder::new("tasklet-step")
156///     .tasklet(&tasklet)
157///     .build();
158/// ```
159pub struct TaskletStep<'a> {
160    name: String,
161    tasklet: &'a dyn Tasklet,
162}
163
164impl Step for TaskletStep<'_> {
165    fn execute(&self, step_execution: &mut StepExecution) -> Result<(), BatchError> {
166        step_execution.status = StepStatus::Started;
167        let start_time = Instant::now();
168
169        info!(
170            "Start of step: {}, id: {}",
171            step_execution.name, step_execution.id
172        );
173
174        loop {
175            let result = self.tasklet.execute(step_execution);
176            match result {
177                Ok(RepeatStatus::Continuable) => {}
178                Ok(RepeatStatus::Finished) => {
179                    step_execution.status = StepStatus::Success;
180                    break;
181                }
182                Err(e) => {
183                    error!(
184                        "Error in step: {}, id: {}, error: {}",
185                        step_execution.name, step_execution.id, e
186                    );
187                    step_execution.status = StepStatus::Failed;
188                    step_execution.end_time = Some(Instant::now());
189                    step_execution.duration = Some(start_time.elapsed());
190                    return Err(e);
191                }
192            }
193        }
194
195        // Calculate the step execution details
196        step_execution.start_time = Some(start_time);
197        step_execution.end_time = Some(Instant::now());
198        step_execution.duration = Some(start_time.elapsed());
199
200        Ok(())
201    }
202
203    fn get_name(&self) -> &str {
204        &self.name
205    }
206}
207
208/// Builder for creating TaskletStep instances.
209///
210/// Provides a fluent API for configuring tasklet steps with validation
211/// to ensure all required components are provided.
212///
213/// # Examples
214///
215/// ```rust
216/// use spring_batch_rs::core::step::{TaskletBuilder, Tasklet, RepeatStatus, StepExecution};
217/// use spring_batch_rs::BatchError;
218///
219/// # struct MyTasklet;
220/// # impl Tasklet for MyTasklet {
221/// #     fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
222/// #         Ok(RepeatStatus::Finished)
223/// #     }
224/// # }
225///
226/// let tasklet = MyTasklet;
227/// let builder = TaskletBuilder::new("my-tasklet")
228///     .tasklet(&tasklet);
229/// let step = builder.build();
230/// ```
231pub struct TaskletBuilder<'a> {
232    name: String,
233    tasklet: Option<&'a dyn Tasklet>,
234}
235
236impl<'a> TaskletBuilder<'a> {
237    /// Creates a new TaskletBuilder with the specified name.
238    ///
239    /// # Parameters
240    /// - `name`: Human-readable name for the step
241    ///
242    /// # Examples
243    ///
244    /// ```rust
245    /// use spring_batch_rs::core::step::TaskletBuilder;
246    ///
247    /// let builder = TaskletBuilder::new("file-cleanup-step");
248    /// ```
249    pub fn new(name: &str) -> Self {
250        Self {
251            name: name.to_string(),
252            tasklet: None,
253        }
254    }
255
256    /// Sets the tasklet to be executed by this step.
257    ///
258    /// # Parameters
259    /// - `tasklet`: The tasklet implementation to execute
260    ///
261    /// # Examples
262    ///
263    /// ```rust
264    /// use spring_batch_rs::core::step::{TaskletBuilder, Tasklet, RepeatStatus, StepExecution};
265    /// use spring_batch_rs::BatchError;
266    ///
267    /// # struct MyTasklet;
268    /// # impl Tasklet for MyTasklet {
269    /// #     fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
270    /// #         Ok(RepeatStatus::Finished)
271    /// #     }
272    /// # }
273    ///
274    /// let tasklet = MyTasklet;
275    /// let builder = TaskletBuilder::new("my-step")
276    ///     .tasklet(&tasklet);
277    /// ```
278    pub fn tasklet(mut self, tasklet: &'a dyn Tasklet) -> Self {
279        self.tasklet = Some(tasklet);
280        self
281    }
282
283    /// Builds the TaskletStep instance.
284    ///
285    /// # Panics
286    /// Panics if no tasklet has been set using the `tasklet()` method.
287    ///
288    /// # Examples
289    ///
290    /// ```rust
291    /// use spring_batch_rs::core::step::{TaskletBuilder, Tasklet, RepeatStatus, StepExecution};
292    /// use spring_batch_rs::BatchError;
293    ///
294    /// # struct MyTasklet;
295    /// # impl Tasklet for MyTasklet {
296    /// #     fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
297    /// #         Ok(RepeatStatus::Finished)
298    /// #     }
299    /// # }
300    ///
301    /// let tasklet = MyTasklet;
302    /// let step = TaskletBuilder::new("my-step")
303    ///     .tasklet(&tasklet)
304    ///     .build();
305    /// ```
306    pub fn build(self) -> TaskletStep<'a> {
307        TaskletStep {
308            name: self.name,
309            tasklet: self
310                .tasklet
311                .expect("Tasklet is required for building a step"),
312        }
313    }
314}
315
316/// Represents the execution context and metrics for a step.
317///
318/// StepExecution tracks all relevant information about a step's execution,
319/// including timing, item counts, error counts, and current status.
320///
321/// # Construction
322///
323/// This type is `#[non_exhaustive]`, so it cannot be built with a struct literal from
324/// outside the crate — use [`StepExecution::new`]. Fields stay public and freely
325/// readable; the attribute exists so that new metrics can be added in a minor release
326/// rather than a breaking one.
327///
328/// # Examples
329///
330/// ```rust
331/// use spring_batch_rs::core::step::{StepExecution, StepStatus};
332///
333/// let mut step_execution = StepExecution::new("data-processing-step");
334/// assert_eq!(step_execution.status, StepStatus::Starting);
335/// assert_eq!(step_execution.read_count, 0);
336/// assert_eq!(step_execution.write_count, 0);
337/// assert_eq!(step_execution.filter_count, 0);
338/// ```
339#[derive(Clone)]
340#[non_exhaustive]
341pub struct StepExecution {
342    /// Unique identifier for this step instance
343    pub id: Uuid,
344    /// Human-readable name for the step
345    pub name: String,
346    /// Current status of the step execution
347    pub status: StepStatus,
348    /// Timestamp when the step started execution
349    pub start_time: Option<Instant>,
350    /// Timestamp when the step completed execution
351    pub end_time: Option<Instant>,
352    /// Total duration of step execution
353    pub duration: Option<Duration>,
354    /// Number of items successfully read from the source
355    pub read_count: usize,
356    /// Number of items successfully written to the destination
357    pub write_count: usize,
358    /// Number of errors encountered during reading
359    pub read_error_count: usize,
360    /// Number of items successfully processed and passed to the writer (excludes filtered items)
361    pub process_count: usize,
362    /// Number of errors encountered during processing
363    pub process_error_count: usize,
364    /// Number of items filtered by the processor (processor returned Ok(None))
365    pub filter_count: usize,
366    /// Number of errors encountered during writing
367    pub write_error_count: usize,
368    /// Cumulative time spent in the read phase across all chunks. Covers the whole
369    /// per-chunk read loop, including the framework's buffering, not just `ItemReader::read`.
370    /// Only chunk-oriented steps populate this; tasklet steps leave it at zero.
371    pub read_duration: Duration,
372    /// Cumulative time spent in the process phase across all chunks. Covers the whole
373    /// per-chunk process loop, including collecting the results, not just `ItemProcessor::process`.
374    /// Only chunk-oriented steps populate this; tasklet steps leave it at zero.
375    pub process_duration: Duration,
376    /// Cumulative time spent in `ItemWriter::write` across all chunks, excluding the flush
377    /// that follows it. Only chunk-oriented steps populate this; tasklet steps leave it at zero.
378    pub write_duration: Duration,
379    /// Cumulative time spent in `ItemWriter::flush` across all chunks, measured separately
380    /// from [`StepExecution::write_duration`] so the cost of flushing once per chunk can be
381    /// quantified. Only chunk-oriented steps populate this; tasklet steps leave it at zero.
382    pub flush_duration: Duration,
383}
384
385impl StepExecution {
386    /// Creates a new StepExecution with the specified name.
387    ///
388    /// Initializes all counters to zero and sets the status to `Starting`.
389    /// A unique UUID is generated for this execution instance.
390    ///
391    /// # Parameters
392    /// - `name`: Human-readable name for the step
393    ///
394    /// # Examples
395    ///
396    /// ```rust
397    /// use spring_batch_rs::core::step::{StepExecution, StepStatus};
398    ///
399    /// let step_execution = StepExecution::new("my-step");
400    /// assert_eq!(step_execution.name, "my-step");
401    /// assert_eq!(step_execution.status, StepStatus::Starting);
402    /// assert!(!step_execution.id.is_nil());
403    /// ```
404    pub fn new(name: &str) -> Self {
405        Self {
406            id: Uuid::new_v4(),
407            name: name.to_string(),
408            status: StepStatus::Starting,
409            start_time: None,
410            end_time: None,
411            duration: None,
412            read_count: 0,
413            write_count: 0,
414            read_error_count: 0,
415            process_count: 0,
416            process_error_count: 0,
417            filter_count: 0,
418            write_error_count: 0,
419            read_duration: Duration::ZERO,
420            process_duration: Duration::ZERO,
421            write_duration: Duration::ZERO,
422            flush_duration: Duration::ZERO,
423        }
424    }
425
426    /// Formats the per-phase timing breakdown as a single human-readable line.
427    ///
428    /// Percentages are relative to the step's total [`StepExecution::duration`].
429    /// When `duration` is `None` or zero — for instance before the step has run —
430    /// every percentage is reported as `0%` rather than `NaN`.
431    ///
432    /// The four phases will not sum to exactly 100%. The remainder holds the writer's
433    /// `open()` and `close()` calls — which is where the CSV, JSON and XML writers perform
434    /// their final flush — the teardown of each processed chunk, and the framework's own
435    /// bookkeeping. A large remainder is itself a useful signal.
436    ///
437    /// Only chunk-oriented steps populate the four phase fields. Calling this on a tasklet
438    /// step's execution yields a line whose phases all read `0.0s (0%)`.
439    ///
440    /// # Examples
441    ///
442    /// ```rust
443    /// use spring_batch_rs::core::step::StepExecution;
444    /// use std::time::Duration;
445    ///
446    /// let mut step_execution = StepExecution::new("load-postgres");
447    /// step_execution.duration = Some(Duration::from_secs(10));
448    /// step_execution.read_duration = Duration::from_secs(5);
449    ///
450    /// let summary = step_execution.phase_summary();
451    /// assert!(summary.contains("read 5.0s (50%)"));
452    /// ```
453    pub fn phase_summary(&self) -> String {
454        let total = self.duration.unwrap_or_default().as_secs_f64();
455        let pct = |d: Duration| -> f64 {
456            if total > 0.0 {
457                d.as_secs_f64() / total * 100.0
458            } else {
459                0.0
460            }
461        };
462
463        format!(
464            "Step '{}' {:.1}s — read {:.1}s ({:.0}%) | process {:.1}s ({:.0}%) | write {:.1}s ({:.0}%) | flush {:.1}s ({:.0}%)",
465            self.name,
466            total,
467            self.read_duration.as_secs_f64(),
468            pct(self.read_duration),
469            self.process_duration.as_secs_f64(),
470            pct(self.process_duration),
471            self.write_duration.as_secs_f64(),
472            pct(self.write_duration),
473            self.flush_duration.as_secs_f64(),
474            pct(self.flush_duration),
475        )
476    }
477}
478
479/// Represents the overall status of a batch job.
480///
481/// This enum defines all possible states that a batch job can be in
482/// during its lifecycle, from initialization to completion.
483///
484/// # Examples
485///
486/// ```rust
487/// use spring_batch_rs::core::step::BatchStatus;
488///
489/// let status = BatchStatus::COMPLETED;
490/// match status {
491///     BatchStatus::COMPLETED => println!("Job finished successfully"),
492///     BatchStatus::FAILED => println!("Job failed"),
493///     _ => println!("Job in progress or other state"),
494/// }
495/// ```
496pub enum BatchStatus {
497    /// The batch job has successfully completed its execution.
498    COMPLETED,
499    /// Status of a batch job prior to its execution.
500    STARTING,
501    /// Status of a batch job that is running.
502    STARTED,
503    /// Status of batch job waiting for a step to complete before stopping the batch job.
504    STOPPING,
505    /// Status of a batch job that has been stopped by request.
506    STOPPED,
507    /// Status of a batch job that has failed during its execution.
508    FAILED,
509    /// Status of a batch job that did not stop properly and can not be restarted.
510    ABANDONED,
511    /// Status of a batch job that is in an uncertain state.
512    UNKNOWN,
513}
514
515/// Core trait that defines the contract for step execution.
516///
517/// All step implementations must provide execution logic and a name.
518/// The step is responsible for coordinating the processing of data
519/// and managing its own lifecycle.
520///
521/// # Examples
522///
523/// ```rust
524/// use spring_batch_rs::core::step::{Step, StepExecution};
525/// use spring_batch_rs::BatchError;
526///
527/// struct CustomStep {
528///     name: String,
529/// }
530///
531/// impl Step for CustomStep {
532///     fn execute(&self, step_execution: &mut StepExecution) -> Result<(), BatchError> {
533///         // Custom step logic here
534///         Ok(())
535///     }
536///
537///     fn get_name(&self) -> &str {
538///         &self.name
539///     }
540/// }
541/// ```
542pub trait Step {
543    /// Executes the step.
544    ///
545    /// This method represents the main operation of the step. It coordinates
546    /// reading items, processing them, and writing them out for chunk-oriented
547    /// steps, or executes a single task for tasklet steps.
548    ///
549    /// # Parameters
550    /// - `step_execution`: Mutable reference to track execution state and metrics
551    ///
552    /// # Returns
553    /// - `Ok(())`: The step completed successfully
554    /// - `Err(BatchError)`: The step failed due to an error
555    ///
556    /// # Examples
557    ///
558    /// ```rust
559    /// use spring_batch_rs::core::step::{Step, StepExecution, StepStatus};
560    /// use spring_batch_rs::BatchError;
561    ///
562    /// # struct MyStep { name: String }
563    /// # impl Step for MyStep {
564    /// #     fn execute(&self, step_execution: &mut StepExecution) -> Result<(), BatchError> {
565    /// #         step_execution.status = StepStatus::Success;
566    /// #         Ok(())
567    /// #     }
568    /// #     fn get_name(&self) -> &str { &self.name }
569    /// # }
570    /// let step = MyStep { name: "test".to_string() };
571    /// let mut execution = StepExecution::new(step.get_name());
572    /// let result = step.execute(&mut execution);
573    /// assert!(result.is_ok());
574    /// ```
575    fn execute(&self, step_execution: &mut StepExecution) -> Result<(), BatchError>;
576
577    /// Returns the name of this step.
578    ///
579    /// # Examples
580    ///
581    /// ```rust
582    /// # use spring_batch_rs::core::step::{Step, StepExecution};
583    /// # use spring_batch_rs::BatchError;
584    /// # struct MyStep { name: String }
585    /// # impl Step for MyStep {
586    /// #     fn execute(&self, _step_execution: &mut StepExecution) -> Result<(), BatchError> { Ok(()) }
587    /// #     fn get_name(&self) -> &str { &self.name }
588    /// # }
589    /// let step = MyStep { name: "data-processing".to_string() };
590    /// assert_eq!(step.get_name(), "data-processing");
591    /// ```
592    fn get_name(&self) -> &str;
593}
594
595/// Indicates whether a tasklet should continue executing or has finished.
596///
597/// This enum is returned by tasklet implementations to control
598/// the execution flow and indicate completion status.
599///
600/// # Examples
601///
602/// ```rust
603/// use spring_batch_rs::core::step::RepeatStatus;
604///
605/// let status = RepeatStatus::Finished;
606/// match status {
607///     RepeatStatus::Continuable => println!("Tasklet can continue"),
608///     RepeatStatus::Finished => println!("Tasklet has completed"),
609/// }
610/// ```
611#[derive(Debug, PartialEq)]
612pub enum RepeatStatus {
613    /// The tasklet can continue to execute.
614    ///
615    /// This indicates that the tasklet has more work to do and should
616    /// be called again in the next execution cycle.
617    Continuable,
618    /// The tasklet has finished executing.
619    ///
620    /// This indicates that the tasklet has completed all its work
621    /// and should not be executed again.
622    Finished,
623}
624
625/// A step implementation that processes items in chunks.
626///
627/// ChunkOrientedStep reads items from a reader, processes them through a processor,
628/// and writes them using a writer. It handles errors gracefully with configurable
629/// skip limits and provides comprehensive metrics tracking.
630///
631/// # Examples
632///
633/// ## Chunk-Oriented Processing
634///
635/// ```rust
636/// use spring_batch_rs::core::step::{StepBuilder, StepExecution, Step};
637/// use spring_batch_rs::core::item::{ItemReader, ItemWriter};
638/// use spring_batch_rs::BatchError;
639///
640/// # struct MyReader;
641/// # impl ItemReader<String> for MyReader {
642/// #     fn read(&self) -> Result<Option<String>, BatchError> { Ok(None) }
643/// # }
644/// # struct MyWriter;
645/// # impl ItemWriter<String> for MyWriter {
646/// #     fn write(&self, items: &[String]) -> Result<(), BatchError> { Ok(()) }
647/// #     fn flush(&self) -> Result<(), BatchError> { Ok(()) }
648/// #     fn open(&self) -> Result<(), BatchError> { Ok(()) }
649/// #     fn close(&self) -> Result<(), BatchError> { Ok(()) }
650/// # }
651///
652/// let reader = MyReader;
653/// let writer = MyWriter;
654///
655/// // No processor needed: reader and writer both use `String`.
656/// let step = StepBuilder::new("my-step")
657///     .chunk::<String, String>(100)                    // Process 100 items per chunk
658///     .reader(&reader)
659///     .writer(&writer)
660///     .skip_limit(10)               // Allow up to 10 errors
661///     .build();
662///
663/// let mut step_execution = StepExecution::new(step.get_name());
664/// let result = step.execute(&mut step_execution);
665/// ```
666pub struct ChunkOrientedStep<'a, I, O> {
667    name: String,
668    /// Component responsible for reading items from the source
669    reader: &'a dyn ItemReader<I>,
670    /// Component responsible for processing items
671    processor: &'a dyn ItemProcessor<I, O>,
672    /// Component responsible for writing items to the destination
673    writer: &'a dyn ItemWriter<O>,
674    /// Number of items to process in each chunk
675    chunk_size: u16,
676    /// Maximum number of errors allowed before failing the step
677    skip_limit: u16,
678}
679
680impl<I, O> Step for ChunkOrientedStep<'_, I, O> {
681    fn execute(&self, step_execution: &mut StepExecution) -> Result<(), BatchError> {
682        // Start the timer and logging
683        let start_time = Instant::now();
684        info!(
685            "Start of step: {}, id: {}",
686            step_execution.name, step_execution.id
687        );
688
689        // Open the writer and handle any errors
690        Self::manage_error(self.writer.open());
691
692        // Main processing loop
693        loop {
694            // Read chunk
695            let (read_items, chunk_status) = match self.read_chunk(step_execution) {
696                Ok(chunk_data) => chunk_data,
697                Err(_) => {
698                    step_execution.status = StepStatus::ReadError;
699                    break;
700                }
701            };
702
703            // If no items to process, we're done
704            if read_items.is_empty() {
705                step_execution.status = StepStatus::Success;
706                break;
707            }
708
709            // Process and write the chunk
710            if self
711                .process_and_write_chunk(step_execution, read_items)
712                .is_err()
713            {
714                break; // Status already set in the method
715            }
716
717            // Check if we've reached the end
718            if chunk_status == ChunkStatus::Finished {
719                step_execution.status = StepStatus::Success;
720                break;
721            }
722        }
723
724        // Close the writer and handle any errors
725        Self::manage_error(self.writer.close());
726
727        // Log the end of the step
728        info!(
729            "End of step: {}, id: {}",
730            step_execution.name, step_execution.id
731        );
732
733        // Calculate the step execution details
734        step_execution.start_time = Some(start_time);
735        step_execution.end_time = Some(Instant::now());
736        step_execution.duration = Some(start_time.elapsed());
737
738        info!("{}", step_execution.phase_summary());
739
740        // Return the step execution details if the step is successful,
741        // or an error if the step failed
742        if StepStatus::Success == step_execution.status {
743            Ok(())
744        } else {
745            Err(BatchError::Step(step_execution.name.clone()))
746        }
747    }
748
749    fn get_name(&self) -> &str {
750        &self.name
751    }
752}
753
754impl<I, O> ChunkOrientedStep<'_, I, O> {
755    /// Processes a chunk of items and writes them.
756    ///
757    /// This method combines the processing and writing operations for a chunk,
758    /// handling errors appropriately and updating the step execution status.
759    ///
760    /// # Parameters
761    /// - `step_execution`: Mutable reference to track execution state
762    /// - `read_items`: Vector of items to process and write (consumed)
763    ///
764    /// # Returns
765    /// - `Ok(())`: The chunk was processed and written successfully
766    /// - `Err(BatchError)`: An error occurred during processing or writing
767    fn process_and_write_chunk(
768        &self,
769        step_execution: &mut StepExecution,
770        read_items: Vec<I>,
771    ) -> Result<(), BatchError> {
772        // Process the chunk
773        let processed_items = match self.process_chunk(step_execution, read_items) {
774            Ok(items) => items,
775            Err(error) => {
776                step_execution.status = StepStatus::ProcessorError;
777                return Err(error);
778            }
779        };
780
781        // Write the processed items
782        match self.write_chunk(step_execution, &processed_items) {
783            Ok(()) => Ok(()),
784            Err(error) => {
785                step_execution.status = StepStatus::WriteError;
786                Err(error)
787            }
788        }
789    }
790
791    /// Reads a chunk of items from the reader.
792    ///
793    /// This method attempts to read up to `chunk_size` items from the reader.
794    /// It stops when either:
795    /// - The chunk is full (reached `chunk_size` items)
796    /// - There are no more items to read
797    /// - The error skip limit is reached
798    ///
799    /// # Parameters
800    /// - `read_items`: Vector to store the read items
801    ///
802    /// # Returns
803    /// - `Ok(ChunkStatus::Full)`: The chunk is full with `chunk_size` items
804    /// - `Ok(ChunkStatus::Finished)`: There are no more items to read
805    /// - `Err(BatchError)`: An error occurred and skip limit was reached
806    fn read_chunk(
807        &self,
808        step_execution: &mut StepExecution,
809    ) -> Result<(Vec<I>, ChunkStatus), BatchError> {
810        let start = Instant::now();
811        let result = self.read_chunk_inner(step_execution);
812        step_execution.read_duration += start.elapsed();
813        result
814    }
815
816    // timed by the wrapper above
817    fn read_chunk_inner(
818        &self,
819        step_execution: &mut StepExecution,
820    ) -> Result<(Vec<I>, ChunkStatus), BatchError> {
821        debug!("Start reading chunk");
822
823        let mut read_items = Vec::with_capacity(self.chunk_size as usize);
824
825        loop {
826            let read_result = self.reader.read();
827
828            match read_result {
829                Ok(item) => {
830                    match item {
831                        Some(item) => {
832                            read_items.push(item);
833                            step_execution.read_count += 1;
834
835                            if read_items.len() >= self.chunk_size as usize {
836                                return Ok((read_items, ChunkStatus::Full));
837                            }
838                        }
839                        None => {
840                            if read_items.is_empty() {
841                                return Ok((read_items, ChunkStatus::Finished));
842                            } else {
843                                return Ok((read_items, ChunkStatus::Full));
844                            }
845                        }
846                    };
847                }
848                Err(error) => {
849                    warn!("Error reading item: {}", error);
850                    step_execution.read_error_count += 1;
851
852                    if self.is_skip_limit_reached(step_execution) {
853                        // Set the status to ReadError when we hit the limit
854                        step_execution.status = StepStatus::ReadError;
855                        return Err(error);
856                    }
857                }
858            }
859        }
860    }
861
862    /// Processes a chunk of items using the processor.
863    ///
864    /// This method applies the processor to each item in the input chunk.
865    /// It collects the successfully processed items and tracks any errors.
866    ///
867    /// # Parameters
868    /// - `read_items`: Vector of items to process
869    ///
870    /// # Returns
871    /// - `Ok(Vec<W>)`: Vector of successfully processed items
872    /// - `Err(BatchError)`: An error occurred and skip limit was reached
873    fn process_chunk(
874        &self,
875        step_execution: &mut StepExecution,
876        read_items: Vec<I>,
877    ) -> Result<Vec<O>, BatchError> {
878        let start = Instant::now();
879        let result = self.process_chunk_inner(step_execution, read_items);
880        step_execution.process_duration += start.elapsed();
881        result
882    }
883
884    // timed by the wrapper above
885    fn process_chunk_inner(
886        &self,
887        step_execution: &mut StepExecution,
888        read_items: Vec<I>,
889    ) -> Result<Vec<O>, BatchError> {
890        debug!("Processing chunk of {} items", read_items.len());
891        let mut result = Vec::with_capacity(read_items.len());
892
893        for item in read_items {
894            match self.processor.process(item) {
895                Ok(Some(processed_item)) => {
896                    result.push(processed_item);
897                    step_execution.process_count += 1;
898                }
899                Ok(None) => {
900                    step_execution.filter_count += 1;
901                    debug!("Item filtered by processor");
902                }
903                Err(error) => {
904                    warn!("Error processing item: {}", error);
905                    step_execution.process_error_count += 1;
906
907                    if self.is_skip_limit_reached(step_execution) {
908                        // Set the status to ProcessorError when we hit the limit
909                        step_execution.status = StepStatus::ProcessorError;
910                        return Err(error);
911                    }
912                }
913            }
914        }
915
916        Ok(result)
917    }
918
919    /// Writes a chunk of processed items using the writer.
920    ///
921    /// This method writes the processed items to the destination
922    /// and handles any errors that occur.
923    ///
924    /// # Parameters
925    /// - `processed_items`: Vector of items to write
926    ///
927    /// # Returns
928    /// - `Ok(())`: All items were written successfully
929    /// - `Err(BatchError)`: An error occurred and skip limit was reached
930    fn write_chunk(
931        &self,
932        step_execution: &mut StepExecution,
933        processed_items: &[O],
934    ) -> Result<(), BatchError> {
935        debug!("Writing chunk of {} items", processed_items.len());
936
937        if processed_items.is_empty() {
938            debug!("No items to write, skipping write call");
939            return Ok(());
940        }
941
942        let write_start = Instant::now();
943        let write_result = self.writer.write(processed_items);
944        step_execution.write_duration += write_start.elapsed();
945
946        match write_result {
947            Ok(()) => {
948                step_execution.write_count += processed_items.len();
949
950                let flush_start = Instant::now();
951                let flush_result = self.writer.flush();
952                step_execution.flush_duration += flush_start.elapsed();
953                Self::manage_error(flush_result);
954
955                Ok(())
956            }
957            Err(error) => {
958                warn!("Error writing items: {}", error);
959                step_execution.write_error_count += processed_items.len();
960
961                if self.is_skip_limit_reached(step_execution) {
962                    // Set the status to WriteError to indicate a write failure
963                    step_execution.status = StepStatus::WriteError;
964                    return Err(error);
965                }
966                Ok(())
967            }
968        }
969    }
970
971    fn is_skip_limit_reached(&self, step_execution: &StepExecution) -> bool {
972        step_execution.read_error_count
973            + step_execution.write_error_count
974            + step_execution.process_error_count
975            > self.skip_limit.into()
976    }
977    /// Helper method to handle errors gracefully.
978    ///
979    /// This method is used to handle errors from operations where we want
980    /// to log the error but not fail the step.
981    ///
982    /// # Parameters
983    /// - `result`: Result to check for errors
984    fn manage_error(result: Result<(), BatchError>) {
985        if let Err(error) = result {
986            warn!("Non-fatal error: {}", error);
987        }
988    }
989}
990
991/// Builder for creating ChunkOrientedStep instances.
992///
993/// Provides a fluent API for configuring chunk-oriented steps with validation
994/// to ensure all required components (reader, processor, writer) are provided.
995///
996/// # Type Parameters
997/// - `I`: The input item type (what the reader produces)
998/// - `O`: The output item type (what the processor produces and writer consumes)
999///
1000/// # Examples
1001///
1002/// ```rust
1003/// use spring_batch_rs::core::step::ChunkOrientedStepBuilder;
1004/// use spring_batch_rs::core::item::{ItemReader, ItemProcessor, ItemWriter};
1005/// use spring_batch_rs::BatchError;
1006///
1007/// # struct MyReader;
1008/// # impl ItemReader<i32> for MyReader {
1009/// #     fn read(&self) -> Result<Option<i32>, BatchError> { Ok(None) }
1010/// # }
1011/// # struct MyProcessor;
1012/// # impl ItemProcessor<i32, String> for MyProcessor {
1013/// #     fn process(&self, item: i32) -> Result<Option<String>, BatchError> { Ok(Some(item.to_string())) }
1014/// # }
1015/// # struct MyWriter;
1016/// # impl ItemWriter<String> for MyWriter {
1017/// #     fn write(&self, items: &[String]) -> Result<(), BatchError> { Ok(()) }
1018/// #     fn flush(&self) -> Result<(), BatchError> { Ok(()) }
1019/// #     fn open(&self) -> Result<(), BatchError> { Ok(()) }
1020/// #     fn close(&self) -> Result<(), BatchError> { Ok(()) }
1021/// # }
1022/// let reader = MyReader;
1023/// let processor = MyProcessor;
1024/// let writer = MyWriter;
1025///
1026/// let step = ChunkOrientedStepBuilder::new("number-to-string")
1027///     .reader(&reader)
1028///     .processor(&processor)
1029///     .writer(&writer)
1030///     .chunk_size(500)
1031///     .skip_limit(25)
1032///     .build();
1033/// ```
1034/// Type-state marker indicating that no processor has been set yet on a
1035/// [`ChunkOrientedStepBuilder`].
1036///
1037/// See the type-level documentation on [`ChunkOrientedStepBuilder`] for how
1038/// this is used to make `.processor(...)` optional only when it is safe to
1039/// do so (reader output type equals writer input type).
1040pub struct NoProcessor;
1041
1042/// Type-state marker indicating that a processor has been set on a
1043/// [`ChunkOrientedStepBuilder`] via [`ChunkOrientedStepBuilder::processor`].
1044pub struct HasProcessor;
1045
1046pub struct ChunkOrientedStepBuilder<'a, I, O, P = NoProcessor> {
1047    /// Name for the step
1048    name: String,
1049    /// Component responsible for reading items from the source
1050    reader: Option<&'a dyn ItemReader<I>>,
1051    /// Component responsible for processing items
1052    processor: Option<&'a dyn ItemProcessor<I, O>>,
1053    /// Component responsible for writing items to the destination
1054    writer: Option<&'a dyn ItemWriter<O>>,
1055    /// Number of items to process in each chunk
1056    chunk_size: u16,
1057    /// Maximum number of errors allowed before failing the step
1058    skip_limit: u16,
1059    /// Type-state marker tracking whether `.processor(...)` has been called
1060    _processor_state: PhantomData<P>,
1061}
1062
1063impl<'a, I, O> ChunkOrientedStepBuilder<'a, I, O, NoProcessor> {
1064    /// Creates a new ChunkOrientedStepBuilder with the specified name.
1065    ///
1066    /// Sets default values:
1067    /// - `chunk_size`: 10
1068    /// - `skip_limit`: 0 (no error tolerance)
1069    ///
1070    /// # Parameters
1071    /// - `name`: Human-readable name for the step
1072    ///
1073    /// # Examples
1074    ///
1075    /// ```rust
1076    /// use spring_batch_rs::core::step::ChunkOrientedStepBuilder;
1077    ///
1078    /// let builder = ChunkOrientedStepBuilder::<String, String>::new("data-migration");
1079    /// ```
1080    pub fn new(name: &str) -> Self {
1081        Self {
1082            name: name.to_string(),
1083            reader: None,
1084            processor: None,
1085            writer: None,
1086            chunk_size: 10,
1087            skip_limit: 0,
1088            _processor_state: PhantomData,
1089        }
1090    }
1091
1092    /// Sets the item processor for this step.
1093    ///
1094    /// The processor transforms items from type `I` to type `O`. Setting a
1095    /// processor is optional: when the reader's output type `I` and the
1096    /// writer's input type `O` are the same, omitting this call falls back to
1097    /// an internal identity processor in [`ChunkOrientedStepBuilder::build`].
1098    ///
1099    /// # Parameters
1100    /// - `processor`: Implementation of ItemProcessor that transforms items from `I` to `O`
1101    ///
1102    /// # Examples
1103    ///
1104    /// ```rust
1105    /// # use spring_batch_rs::core::step::ChunkOrientedStepBuilder;
1106    /// # use spring_batch_rs::core::item::{ItemReader, ItemProcessor};
1107    /// # use spring_batch_rs::BatchError;
1108    /// # struct FileReader;
1109    /// # impl ItemReader<String> for FileReader {
1110    /// #     fn read(&self) -> Result<Option<String>, BatchError> { Ok(None) }
1111    /// # }
1112    /// # struct UppercaseProcessor;
1113    /// # impl ItemProcessor<String, String> for UppercaseProcessor {
1114    /// #     fn process(&self, item: String) -> Result<Option<String>, BatchError> { Ok(Some(item.to_uppercase())) }
1115    /// # }
1116    /// let reader = FileReader;
1117    /// let processor = UppercaseProcessor;
1118    /// let builder = ChunkOrientedStepBuilder::new("text-processing")
1119    ///     .reader(&reader)
1120    ///     .processor(&processor);
1121    /// ```
1122    pub fn processor(
1123        self,
1124        processor: &'a dyn ItemProcessor<I, O>,
1125    ) -> ChunkOrientedStepBuilder<'a, I, O, HasProcessor> {
1126        ChunkOrientedStepBuilder {
1127            name: self.name,
1128            reader: self.reader,
1129            processor: Some(processor),
1130            writer: self.writer,
1131            chunk_size: self.chunk_size,
1132            skip_limit: self.skip_limit,
1133            _processor_state: PhantomData,
1134        }
1135    }
1136}
1137
1138impl<'a, I, O, P> ChunkOrientedStepBuilder<'a, I, O, P> {
1139    /// Sets the item reader for this step.
1140    ///
1141    /// The reader is responsible for providing items to be processed.
1142    /// This is a required component for chunk-oriented steps.
1143    ///
1144    /// # Parameters
1145    /// - `reader`: Implementation of ItemReader that produces items of type `I`
1146    ///
1147    /// # Examples
1148    ///
1149    /// ```rust
1150    /// # use spring_batch_rs::core::step::ChunkOrientedStepBuilder;
1151    /// # use spring_batch_rs::core::item::ItemReader;
1152    /// # use spring_batch_rs::BatchError;
1153    /// # struct FileReader;
1154    /// # impl ItemReader<String> for FileReader {
1155    /// #     fn read(&self) -> Result<Option<String>, BatchError> { Ok(None) }
1156    /// # }
1157    /// let reader = FileReader;
1158    /// let builder = ChunkOrientedStepBuilder::<String, String>::new("file-processing")
1159    ///     .reader(&reader);
1160    /// ```
1161    pub fn reader(mut self, reader: &'a dyn ItemReader<I>) -> Self {
1162        self.reader = Some(reader);
1163        self
1164    }
1165
1166    /// Sets the item writer for this step.
1167    ///
1168    /// The writer is responsible for persisting processed items.
1169    /// This is a required component for chunk-oriented steps.
1170    ///
1171    /// # Parameters
1172    /// - `writer`: Implementation of ItemWriter that consumes items of type `O`
1173    ///
1174    /// # Examples
1175    ///
1176    /// ```rust
1177    /// # use spring_batch_rs::core::step::ChunkOrientedStepBuilder;
1178    /// # use spring_batch_rs::core::item::{ItemReader, ItemProcessor, ItemWriter};
1179    /// # use spring_batch_rs::BatchError;
1180    /// # struct FileReader;
1181    /// # impl ItemReader<String> for FileReader {
1182    /// #     fn read(&self) -> Result<Option<String>, BatchError> { Ok(None) }
1183    /// # }
1184    /// # struct UppercaseProcessor;
1185    /// # impl ItemProcessor<String, String> for UppercaseProcessor {
1186    /// #     fn process(&self, item: String) -> Result<Option<String>, BatchError> { Ok(Some(item.to_uppercase())) }
1187    /// # }
1188    /// # struct FileWriter;
1189    /// # impl ItemWriter<String> for FileWriter {
1190    /// #     fn write(&self, items: &[String]) -> Result<(), BatchError> { Ok(()) }
1191    /// #     fn flush(&self) -> Result<(), BatchError> { Ok(()) }
1192    /// #     fn open(&self) -> Result<(), BatchError> { Ok(()) }
1193    /// #     fn close(&self) -> Result<(), BatchError> { Ok(()) }
1194    /// # }
1195    /// let reader = FileReader;
1196    /// let processor = UppercaseProcessor;
1197    /// let writer = FileWriter;
1198    /// let builder = ChunkOrientedStepBuilder::new("file-processing")
1199    ///     .reader(&reader)
1200    ///     .processor(&processor)
1201    ///     .writer(&writer);
1202    /// ```
1203    pub fn writer(mut self, writer: &'a dyn ItemWriter<O>) -> Self {
1204        self.writer = Some(writer);
1205        self
1206    }
1207
1208    /// Sets the chunk size for this step.
1209    ///
1210    /// The chunk size determines how many items are processed together
1211    /// in a single transaction. Larger chunks can improve performance
1212    /// but use more memory.
1213    ///
1214    /// # Parameters
1215    /// - `chunk_size`: Number of items to process per chunk (must be > 0)
1216    ///
1217    /// # Examples
1218    ///
1219    /// ```rust
1220    /// use spring_batch_rs::core::step::ChunkOrientedStepBuilder;
1221    ///
1222    /// let builder = ChunkOrientedStepBuilder::<String, String>::new("bulk-processing")
1223    ///     .chunk_size(1000); // Process 1000 items per chunk
1224    /// ```
1225    pub fn chunk_size(mut self, chunk_size: u16) -> Self {
1226        self.chunk_size = chunk_size;
1227        self
1228    }
1229
1230    /// Sets the skip limit for this step.
1231    ///
1232    /// The skip limit determines how many errors are tolerated before
1233    /// the step fails. A value of 0 means no errors are tolerated.
1234    ///
1235    /// # Parameters
1236    /// - `skip_limit`: Maximum number of errors allowed
1237    ///
1238    /// # Examples
1239    ///
1240    /// ```rust
1241    /// use spring_batch_rs::core::step::ChunkOrientedStepBuilder;
1242    ///
1243    /// let builder = ChunkOrientedStepBuilder::<String, String>::new("fault-tolerant-processing")
1244    ///     .skip_limit(100); // Allow up to 100 errors
1245    /// ```
1246    pub fn skip_limit(mut self, skip_limit: u16) -> Self {
1247        self.skip_limit = skip_limit;
1248        self
1249    }
1250}
1251
1252impl<'a, I, O> ChunkOrientedStepBuilder<'a, I, O, HasProcessor> {
1253    /// Builds the ChunkOrientedStep instance using the processor set via
1254    /// [`ChunkOrientedStepBuilder::processor`].
1255    ///
1256    /// # Panics
1257    /// Panics if the reader or the writer has not been set.
1258    ///
1259    /// # Examples
1260    ///
1261    /// ```rust
1262    /// # use spring_batch_rs::core::step::ChunkOrientedStepBuilder;
1263    /// # use spring_batch_rs::core::item::{ItemReader, ItemProcessor, ItemWriter};
1264    /// # use spring_batch_rs::BatchError;
1265    /// # struct MyReader;
1266    /// # impl ItemReader<String> for MyReader {
1267    /// #     fn read(&self) -> Result<Option<String>, BatchError> { Ok(None) }
1268    /// # }
1269    /// # struct MyProcessor;
1270    /// # impl ItemProcessor<String, String> for MyProcessor {
1271    /// #     fn process(&self, item: String) -> Result<Option<String>, BatchError> { Ok(Some(item)) }
1272    /// # }
1273    /// # struct MyWriter;
1274    /// # impl ItemWriter<String> for MyWriter {
1275    /// #     fn write(&self, items: &[String]) -> Result<(), BatchError> { Ok(()) }
1276    /// #     fn flush(&self) -> Result<(), BatchError> { Ok(()) }
1277    /// #     fn open(&self) -> Result<(), BatchError> { Ok(()) }
1278    /// #     fn close(&self) -> Result<(), BatchError> { Ok(()) }
1279    /// # }
1280    /// let reader = MyReader;
1281    /// let processor = MyProcessor;
1282    /// let writer = MyWriter;
1283    ///
1284    /// let step = ChunkOrientedStepBuilder::new("complete-step")
1285    ///     .reader(&reader)
1286    ///     .processor(&processor)
1287    ///     .writer(&writer)
1288    ///     .chunk_size(500)
1289    ///     .skip_limit(10)
1290    ///     .build();
1291    /// ```
1292    pub fn build(self) -> ChunkOrientedStep<'a, I, O> {
1293        ChunkOrientedStep {
1294            name: self.name,
1295            reader: self.reader.expect("Reader is required for building a step"),
1296            processor: self
1297                .processor
1298                .expect("Processor is required for building a step"),
1299            writer: self.writer.expect("Writer is required for building a step"),
1300            chunk_size: self.chunk_size,
1301            skip_limit: self.skip_limit,
1302        }
1303    }
1304}
1305
1306impl<'a, I> ChunkOrientedStepBuilder<'a, I, I, NoProcessor> {
1307    /// Builds the ChunkOrientedStep instance, falling back to an internal
1308    /// identity processor since no processor was set.
1309    ///
1310    /// This overload is only available when the reader's output type `I` and
1311    /// the writer's input type `O` are the same type: the fallback processor
1312    /// returns each item unchanged, so `I` and `O` must match. If they differ
1313    /// and no processor was set via [`ChunkOrientedStepBuilder::processor`],
1314    /// this method is not applicable and the call to `.build()` fails to
1315    /// compile.
1316    ///
1317    /// # Panics
1318    /// Panics if the reader or the writer has not been set.
1319    ///
1320    /// # Examples
1321    ///
1322    /// ```rust
1323    /// # use spring_batch_rs::core::step::ChunkOrientedStepBuilder;
1324    /// # use spring_batch_rs::core::item::{ItemReader, ItemWriter};
1325    /// # use spring_batch_rs::BatchError;
1326    /// # struct MyReader;
1327    /// # impl ItemReader<String> for MyReader {
1328    /// #     fn read(&self) -> Result<Option<String>, BatchError> { Ok(None) }
1329    /// # }
1330    /// # struct MyWriter;
1331    /// # impl ItemWriter<String> for MyWriter {
1332    /// #     fn write(&self, items: &[String]) -> Result<(), BatchError> { Ok(()) }
1333    /// #     fn flush(&self) -> Result<(), BatchError> { Ok(()) }
1334    /// #     fn open(&self) -> Result<(), BatchError> { Ok(()) }
1335    /// #     fn close(&self) -> Result<(), BatchError> { Ok(()) }
1336    /// # }
1337    /// let reader = MyReader;
1338    /// let writer = MyWriter;
1339    ///
1340    /// let step = ChunkOrientedStepBuilder::new("pass-through-step")
1341    ///     .reader(&reader)
1342    ///     .writer(&writer)
1343    ///     .build();
1344    /// ```
1345    pub fn build(self) -> ChunkOrientedStep<'a, I, I> {
1346        ChunkOrientedStep {
1347            name: self.name,
1348            reader: self.reader.expect("Reader is required for building a step"),
1349            processor: Box::leak(Box::new(PassThroughProcessor::new())),
1350            writer: self.writer.expect("Writer is required for building a step"),
1351            chunk_size: self.chunk_size,
1352            skip_limit: self.skip_limit,
1353        }
1354    }
1355}
1356
1357/// Main entry point for building steps of any type.
1358///
1359/// StepBuilder provides a unified interface for creating both chunk-oriented
1360/// and tasklet steps. It uses the builder pattern to provide a fluent API
1361/// for step configuration.
1362///
1363/// # Type Parameters
1364/// - `I`: The input item type for chunk-oriented steps
1365/// - `O`: The output item type for chunk-oriented steps
1366///
1367/// # Examples
1368///
1369/// ## Creating a Chunk-Oriented Step
1370///
1371/// ```rust
1372/// use spring_batch_rs::core::step::{StepBuilder, StepExecution, Step};
1373/// use spring_batch_rs::core::item::{ItemReader, ItemProcessor, ItemWriter};
1374/// use spring_batch_rs::BatchError;
1375///
1376/// # struct MyReader;
1377/// # impl ItemReader<String> for MyReader {
1378/// #     fn read(&self) -> Result<Option<String>, BatchError> { Ok(None) }
1379/// # }
1380/// # struct MyProcessor;
1381/// # impl ItemProcessor<String, String> for MyProcessor {
1382/// #     fn process(&self, item: String) -> Result<Option<String>, BatchError> { Ok(Some(item)) }
1383/// # }
1384/// # struct MyWriter;
1385/// # impl ItemWriter<String> for MyWriter {
1386/// #     fn write(&self, items: &[String]) -> Result<(), BatchError> { Ok(()) }
1387/// #     fn flush(&self) -> Result<(), BatchError> { Ok(()) }
1388/// #     fn open(&self) -> Result<(), BatchError> { Ok(()) }
1389/// #     fn close(&self) -> Result<(), BatchError> { Ok(()) }
1390/// # }
1391/// let reader = MyReader;
1392/// let processor = MyProcessor;
1393/// let writer = MyWriter;
1394///
1395/// let step = StepBuilder::new("data-processing")
1396///     .chunk::<String, String>(100)
1397///     .reader(&reader)
1398///     .processor(&processor)
1399///     .writer(&writer)
1400///     .build();
1401/// ```
1402///
1403/// ## Creating a Tasklet Step
1404///
1405/// ```rust
1406/// use spring_batch_rs::core::step::{StepBuilder, StepExecution, RepeatStatus, Tasklet};
1407/// use spring_batch_rs::BatchError;
1408///
1409/// # struct MyTasklet;
1410/// # impl Tasklet for MyTasklet {
1411/// #     fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
1412/// #         Ok(RepeatStatus::Finished)
1413/// #     }
1414/// # }
1415/// let tasklet = MyTasklet;
1416///
1417/// let step = StepBuilder::new("cleanup-task")
1418///     .tasklet(&tasklet)
1419///     .build();
1420/// ```
1421pub struct StepBuilder {
1422    name: String,
1423}
1424
1425impl StepBuilder {
1426    /// Creates a new StepBuilder with the specified name.
1427    ///
1428    /// # Parameters
1429    /// - `name`: Human-readable name for the step
1430    ///
1431    /// # Examples
1432    ///
1433    /// ```rust
1434    /// use spring_batch_rs::core::step::StepBuilder;
1435    ///
1436    /// let builder = StepBuilder::new("my-step");
1437    /// ```
1438    pub fn new(name: &str) -> Self {
1439        Self {
1440            name: name.to_string(),
1441        }
1442    }
1443
1444    /// Configures this step to use a tasklet for execution.
1445    ///
1446    /// Returns a TaskletBuilder for further configuration of the tasklet step.
1447    ///
1448    /// # Parameters
1449    /// - `tasklet`: The tasklet implementation to execute
1450    ///
1451    /// # Examples
1452    ///
1453    /// ```rust
1454    /// use spring_batch_rs::core::step::{StepBuilder, StepExecution, RepeatStatus, Tasklet};
1455    /// use spring_batch_rs::BatchError;
1456    ///
1457    /// # struct FileCleanupTasklet;
1458    /// # impl Tasklet for FileCleanupTasklet {
1459    /// #     fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
1460    /// #         Ok(RepeatStatus::Finished)
1461    /// #     }
1462    /// # }
1463    /// let tasklet = FileCleanupTasklet;
1464    /// let step = StepBuilder::new("cleanup")
1465    ///     .tasklet(&tasklet)
1466    ///     .build();
1467    /// ```
1468    pub fn tasklet(self, tasklet: &dyn Tasklet) -> TaskletBuilder<'_> {
1469        TaskletBuilder::new(&self.name).tasklet(tasklet)
1470    }
1471
1472    /// Configures this step for chunk-oriented processing.
1473    ///
1474    /// Returns a ChunkOrientedStepBuilder for further configuration of the chunk step.
1475    ///
1476    /// # Parameters
1477    /// - `chunk_size`: Number of items to process per chunk
1478    ///
1479    /// # Examples
1480    ///
1481    /// ```rust
1482    /// use spring_batch_rs::core::step::{StepBuilder, Step};
1483    /// use spring_batch_rs::core::item::{ItemReader, ItemProcessor, ItemWriter};
1484    /// use spring_batch_rs::BatchError;
1485    ///
1486    /// # struct MyReader;
1487    /// # impl ItemReader<String> for MyReader {
1488    /// #     fn read(&self) -> Result<Option<String>, BatchError> { Ok(None) }
1489    /// # }
1490    /// # struct MyProcessor;
1491    /// # impl ItemProcessor<String, String> for MyProcessor {
1492    /// #     fn process(&self, item: String) -> Result<Option<String>, BatchError> { Ok(Some(item)) }
1493    /// # }
1494    /// # struct MyWriter;
1495    /// # impl ItemWriter<String> for MyWriter {
1496    /// #     fn write(&self, items: &[String]) -> Result<(), BatchError> { Ok(()) }
1497    /// #     fn flush(&self) -> Result<(), BatchError> { Ok(()) }
1498    /// #     fn open(&self) -> Result<(), BatchError> { Ok(()) }
1499    /// #     fn close(&self) -> Result<(), BatchError> { Ok(()) }
1500    /// # }
1501    /// let reader = MyReader;
1502    /// let processor = MyProcessor;
1503    /// let writer = MyWriter;
1504    ///
1505    /// let step = StepBuilder::new("bulk-processing")
1506    ///     .chunk(1000)  // Process 1000 items per chunk
1507    ///     .reader(&reader)
1508    ///     .processor(&processor)
1509    ///     .writer(&writer)
1510    ///     .build();
1511    /// ```
1512    pub fn chunk<'a, I, O>(self, chunk_size: u16) -> ChunkOrientedStepBuilder<'a, I, O> {
1513        ChunkOrientedStepBuilder::new(&self.name).chunk_size(chunk_size)
1514    }
1515}
1516
1517/// Represents the status of a chunk during processing.
1518///
1519/// This enum indicates whether a chunk has been fully processed or if
1520/// there are more items to process. It's used internally by the step
1521/// execution logic to control the processing loop.
1522///
1523/// # Examples
1524///
1525/// ```rust
1526/// use spring_batch_rs::core::step::ChunkStatus;
1527///
1528/// let status = ChunkStatus::Full;
1529/// match status {
1530///     ChunkStatus::Full => println!("Chunk is ready for processing"),
1531///     ChunkStatus::Finished => println!("No more items to process"),
1532/// }
1533/// ```
1534#[derive(Debug, PartialEq)]
1535pub enum ChunkStatus {
1536    /// The chunk has been fully processed.
1537    ///
1538    /// This indicates that there are no more items to process in the current
1539    /// data source (typically because we've reached the end of the input).
1540    /// The step should complete after processing any remaining items.
1541    Finished,
1542
1543    /// The chunk is full and ready to be processed.
1544    ///
1545    /// This indicates that we've collected a full chunk of items (based on
1546    /// the configured chunk size) and they are ready to be processed.
1547    /// The step should continue reading more chunks after processing this one.
1548    Full,
1549}
1550
1551/// Represents the current status of a step execution.
1552///
1553/// This enum indicates the current state of a step execution, including
1554/// both success and various failure states. It helps track the step's
1555/// progress and identify the cause of any failures.
1556///
1557/// # Examples
1558///
1559/// ```rust
1560/// use spring_batch_rs::core::step::{StepExecution, StepStatus};
1561///
1562/// let mut step_execution = StepExecution::new("my-step");
1563/// assert_eq!(step_execution.status, StepStatus::Starting);
1564///
1565/// // After successful execution
1566/// step_execution.status = StepStatus::Success;
1567/// match step_execution.status {
1568///     StepStatus::Success => println!("Step completed successfully"),
1569///     StepStatus::ReadError => println!("Failed during reading"),
1570///     StepStatus::ProcessorError => println!("Failed during processing"),
1571///     StepStatus::WriteError => println!("Failed during writing"),
1572///     StepStatus::Starting => println!("Step is starting"),
1573///     StepStatus::Failed => println!("Step has failed"),
1574///     StepStatus::Started => println!("Step has started"),
1575/// }
1576/// ```
1577#[derive(Debug, PartialEq, Clone, Copy)]
1578pub enum StepStatus {
1579    /// The step executed successfully.
1580    ///
1581    /// All items were read, processed, and written without errors
1582    /// exceeding configured skip limits. This is the desired end state
1583    /// for a step execution.
1584    Success,
1585
1586    /// An error occurred during the read operation.
1587    ///
1588    /// This indicates that an error occurred while reading items from the
1589    /// source, and the error count exceeded the configured skip limit.
1590    /// The step was terminated due to too many read failures.
1591    ReadError,
1592
1593    /// An error occurred during the processing operation.
1594    ///
1595    /// This indicates that an error occurred while processing items, and
1596    /// the error count exceeded the configured skip limit.
1597    /// The step was terminated due to too many processing failures.
1598    ProcessorError,
1599
1600    /// An error occurred during the write operation.
1601    ///
1602    /// This indicates that an error occurred while writing items to the
1603    /// destination, and the error count exceeded the configured skip limit.
1604    /// The step was terminated due to too many write failures.
1605    WriteError,
1606
1607    /// The step is starting.
1608    ///
1609    /// This is the initial state of a step before execution begins.
1610    /// All steps start in this state when first created.
1611    Starting,
1612
1613    /// The step is failed.
1614    ///
1615    /// This is the final state of a step after execution has failed.
1616    Failed,
1617
1618    /// The step is started.
1619    ///
1620    /// This is the state of a step after execution has started.
1621    Started,
1622}
1623
1624#[cfg(test)]
1625mod tests {
1626    use anyhow::Result;
1627    use mockall::mock;
1628    use serde::{Deserialize, Serialize};
1629    use std::time::Duration;
1630
1631    use crate::{
1632        BatchError,
1633        core::{
1634            item::{
1635                ItemProcessor, ItemProcessorResult, ItemReader, ItemReaderResult, ItemWriter,
1636                ItemWriterResult, PassThroughProcessor,
1637            },
1638            step::{StepExecution, StepStatus},
1639        },
1640    };
1641
1642    use super::{
1643        BatchStatus, ChunkOrientedStepBuilder, ChunkStatus, RepeatStatus, Step, StepBuilder,
1644        Tasklet, TaskletBuilder,
1645    };
1646
1647    mock! {
1648        pub TestItemReader {}
1649        impl ItemReader<Car> for TestItemReader {
1650            fn read(&self) -> ItemReaderResult<Car>;
1651        }
1652    }
1653
1654    mock! {
1655        pub TestProcessor {}
1656        impl ItemProcessor<Car, Car> for TestProcessor {
1657            fn process(&self, item: Car) -> ItemProcessorResult<Car>;
1658        }
1659    }
1660
1661    mock! {
1662        pub TestItemWriter {}
1663        impl ItemWriter<Car> for TestItemWriter {
1664            fn write(&self, items: &[Car]) -> ItemWriterResult;
1665            fn flush(&self) -> ItemWriterResult;
1666            fn open(&self) -> ItemWriterResult;
1667            fn close(&self) -> ItemWriterResult;
1668        }
1669    }
1670
1671    mock! {
1672        pub TestTasklet {}
1673        impl Tasklet for TestTasklet {
1674            fn execute(&self, step_execution: &StepExecution) -> Result<RepeatStatus, BatchError>;
1675        }
1676    }
1677
1678    #[derive(Deserialize, Serialize, Debug, Clone)]
1679    struct Car {
1680        year: u16,
1681        make: String,
1682        model: String,
1683        description: String,
1684    }
1685
1686    fn mock_read(i: &mut u16, error_count: u16, end_count: u16) -> ItemReaderResult<Car> {
1687        if end_count > 0 && *i == end_count {
1688            return Ok(None);
1689        } else if error_count > 0 && *i == error_count {
1690            return Err(BatchError::ItemReader("mock read error".to_string()));
1691        }
1692
1693        let car = Car {
1694            year: 1979,
1695            make: "make".to_owned(),
1696            model: "model".to_owned(),
1697            description: "description".to_owned(),
1698        };
1699        *i += 1;
1700        Ok(Some(car))
1701    }
1702
1703    fn sample_car() -> Option<Car> {
1704        Some(Car {
1705            year: 2024,
1706            make: "Renault".to_string(),
1707            model: "Zoe".to_string(),
1708            description: "electric".to_string(),
1709        })
1710    }
1711
1712    fn mock_process(i: &mut u16, error_at: &[u16]) -> ItemProcessorResult<Car> {
1713        *i += 1;
1714        if error_at.contains(i) {
1715            return Err(BatchError::ItemProcessor("mock process error".to_string()));
1716        }
1717
1718        let car = Car {
1719            year: 1979,
1720            make: "make".to_owned(),
1721            model: "model".to_owned(),
1722            description: "description".to_owned(),
1723        };
1724        Ok(Some(car))
1725    }
1726
1727    #[test]
1728    fn step_should_succeded_with_empty_data() -> Result<()> {
1729        let mut reader = MockTestItemReader::default();
1730        let reader_result = Ok(None);
1731        reader.expect_read().return_once(move || reader_result);
1732
1733        let mut processor = MockTestProcessor::default();
1734        processor.expect_process().never();
1735
1736        let mut writer = MockTestItemWriter::default();
1737        writer.expect_open().times(1).returning(|| Ok(()));
1738        writer.expect_write().never();
1739        writer.expect_close().times(1).returning(|| Ok(()));
1740
1741        let step = StepBuilder::new("test")
1742            .chunk(3)
1743            .reader(&reader)
1744            .processor(&processor)
1745            .writer(&writer)
1746            .build();
1747
1748        let mut step_execution = StepExecution::new(&step.name);
1749
1750        let result = step.execute(&mut step_execution);
1751
1752        assert!(result.is_ok());
1753        assert_eq!(step.get_name(), "test");
1754        assert!(!step.get_name().is_empty());
1755        assert!(!step_execution.id.is_nil());
1756        assert_eq!(step_execution.status, StepStatus::Success);
1757
1758        Ok(())
1759    }
1760
1761    #[test]
1762    fn step_should_failed_with_processor_error() -> Result<()> {
1763        let mut i = 0;
1764        let mut reader = MockTestItemReader::default();
1765        reader
1766            .expect_read()
1767            .returning(move || mock_read(&mut i, 0, 4));
1768
1769        let mut processor = MockTestProcessor::default();
1770        let mut i = 0;
1771        processor
1772            .expect_process()
1773            .returning(move |_| mock_process(&mut i, &[2]));
1774
1775        let mut writer = MockTestItemWriter::default();
1776        writer.expect_open().times(1).returning(|| Ok(()));
1777        writer.expect_write().never();
1778        writer.expect_close().times(1).returning(|| Ok(()));
1779
1780        let step = StepBuilder::new("test")
1781            .chunk(3)
1782            .reader(&reader)
1783            .processor(&processor)
1784            .writer(&writer)
1785            .build();
1786
1787        let mut step_execution = StepExecution::new(&step.name);
1788
1789        let result = step.execute(&mut step_execution);
1790
1791        assert!(result.is_err());
1792        assert_eq!(step_execution.status, StepStatus::ProcessorError);
1793
1794        Ok(())
1795    }
1796
1797    #[test]
1798    fn step_should_failed_with_write_error() -> Result<()> {
1799        let mut i = 0;
1800        let mut reader = MockTestItemReader::default();
1801        reader
1802            .expect_read()
1803            .returning(move || mock_read(&mut i, 0, 4));
1804
1805        let mut processor = MockTestProcessor::default();
1806        let mut i = 0;
1807        processor
1808            .expect_process()
1809            .returning(move |_| mock_process(&mut i, &[]));
1810
1811        let mut writer = MockTestItemWriter::default();
1812        writer.expect_open().times(1).returning(|| Ok(()));
1813        let result = Err(BatchError::ItemWriter("mock write error".to_string()));
1814        writer.expect_write().return_once(move |_| result);
1815        writer.expect_close().times(1).returning(|| Ok(()));
1816
1817        let step = StepBuilder::new("test")
1818            .chunk(3)
1819            .reader(&reader)
1820            .processor(&processor)
1821            .writer(&writer)
1822            .build();
1823
1824        let mut step_execution = StepExecution::new(&step.name);
1825
1826        let result = step.execute(&mut step_execution);
1827
1828        assert!(result.is_err());
1829        assert_eq!(step_execution.status, StepStatus::WriteError);
1830
1831        Ok(())
1832    }
1833
1834    #[test]
1835    fn step_should_succeed_even_with_processor_error() -> Result<()> {
1836        let mut i = 0;
1837        let mut reader = MockTestItemReader::default();
1838        reader
1839            .expect_read()
1840            .returning(move || mock_read(&mut i, 0, 4));
1841
1842        let mut processor = MockTestProcessor::default();
1843        let mut i = 0;
1844        processor
1845            .expect_process()
1846            .returning(move |_| mock_process(&mut i, &[2]));
1847
1848        let mut writer = MockTestItemWriter::default();
1849        writer.expect_open().times(1).returning(|| Ok(()));
1850        writer.expect_write().times(2).returning(|_| Ok(()));
1851        writer.expect_flush().times(2).returning(|| Ok(()));
1852        writer.expect_close().times(1).returning(|| Ok(()));
1853
1854        let step = StepBuilder::new("test")
1855            .chunk(3)
1856            .reader(&reader)
1857            .processor(&processor)
1858            .writer(&writer)
1859            .skip_limit(1)
1860            .build();
1861
1862        let mut step_execution = StepExecution::new(step.get_name());
1863
1864        let result = step.execute(&mut step_execution);
1865
1866        assert!(result.is_ok());
1867        assert_eq!(step_execution.status, StepStatus::Success);
1868
1869        Ok(())
1870    }
1871
1872    #[test]
1873    fn step_should_fail_with_read_error() -> Result<()> {
1874        let mut i = 0;
1875        let mut reader = MockTestItemReader::default();
1876        reader
1877            .expect_read()
1878            .returning(move || mock_read(&mut i, 1, 4));
1879
1880        let mut processor = MockTestProcessor::default();
1881        let mut i = 0;
1882        processor
1883            .expect_process()
1884            .returning(move |_| mock_process(&mut i, &[]));
1885
1886        let mut writer = MockTestItemWriter::default();
1887        writer.expect_open().times(1).returning(|| Ok(()));
1888        writer.expect_write().never();
1889        writer.expect_close().times(1).returning(|| Ok(()));
1890
1891        let step = StepBuilder::new("test")
1892            .chunk(3)
1893            .reader(&reader)
1894            .processor(&processor)
1895            .writer(&writer)
1896            .build();
1897
1898        let mut step_execution = StepExecution::new(&step.name);
1899
1900        let result = step.execute(&mut step_execution);
1901
1902        assert!(result.is_err());
1903        assert_eq!(step_execution.status, StepStatus::ReadError);
1904        assert_eq!(step_execution.read_error_count, 1);
1905
1906        Ok(())
1907    }
1908
1909    #[test]
1910    fn step_should_respect_chunk_size() -> Result<()> {
1911        let mut i = 0;
1912        let mut reader = MockTestItemReader::default();
1913        reader
1914            .expect_read()
1915            .returning(move || mock_read(&mut i, 0, 6));
1916
1917        let mut processor = MockTestProcessor::default();
1918        let mut i = 0;
1919        processor
1920            .expect_process()
1921            .returning(move |_| mock_process(&mut i, &[]));
1922
1923        let mut writer = MockTestItemWriter::default();
1924        writer.expect_open().times(1).returning(|| Ok(()));
1925        writer.expect_write().times(2).returning(|_| Ok(()));
1926        writer.expect_flush().times(2).returning(|| Ok(()));
1927        writer.expect_close().times(1).returning(|| Ok(()));
1928
1929        let step = StepBuilder::new("test")
1930            .chunk(3)
1931            .reader(&reader)
1932            .processor(&processor)
1933            .writer(&writer)
1934            .build();
1935
1936        let mut step_execution = StepExecution::new(&step.name);
1937
1938        let result = step.execute(&mut step_execution);
1939
1940        assert!(result.is_ok());
1941        assert_eq!(step_execution.status, StepStatus::Success);
1942        assert_eq!(step_execution.read_count, 6);
1943        assert_eq!(step_execution.write_count, 6);
1944
1945        Ok(())
1946    }
1947
1948    #[test]
1949    fn step_should_track_error_counts() -> Result<()> {
1950        let mut i = 0;
1951        let mut reader = MockTestItemReader::default();
1952        reader
1953            .expect_read()
1954            .returning(move || mock_read(&mut i, 0, 4));
1955
1956        let mut processor = MockTestProcessor::default();
1957        let mut i = 0;
1958        processor
1959            .expect_process()
1960            .returning(move |_| mock_process(&mut i, &[1, 2]));
1961
1962        let mut writer = MockTestItemWriter::default();
1963        writer.expect_open().times(1).returning(|| Ok(()));
1964        writer.expect_write().times(2).returning(|_| Ok(()));
1965        writer.expect_flush().times(2).returning(|| Ok(()));
1966        writer.expect_close().times(1).returning(|| Ok(()));
1967
1968        let step = StepBuilder::new("test")
1969            .chunk(3)
1970            .reader(&reader)
1971            .processor(&processor)
1972            .writer(&writer)
1973            .skip_limit(2)
1974            .build();
1975
1976        let mut step_execution = StepExecution::new(&step.name);
1977
1978        let result = step.execute(&mut step_execution);
1979
1980        assert!(result.is_ok());
1981        assert_eq!(step_execution.status, StepStatus::Success);
1982        assert_eq!(step_execution.process_error_count, 2);
1983
1984        Ok(())
1985    }
1986
1987    #[test]
1988    fn step_should_measure_execution_time() -> Result<()> {
1989        let mut i = 0;
1990        let mut reader = MockTestItemReader::default();
1991        reader
1992            .expect_read()
1993            .returning(move || mock_read(&mut i, 0, 2));
1994
1995        let mut processor = MockTestProcessor::default();
1996        let mut i = 0;
1997        processor
1998            .expect_process()
1999            .returning(move |_| mock_process(&mut i, &[]));
2000
2001        let mut writer = MockTestItemWriter::default();
2002        writer.expect_open().times(1).returning(|| Ok(()));
2003        writer.expect_write().times(1).returning(|_| Ok(()));
2004        writer.expect_flush().times(1).returning(|| Ok(()));
2005        writer.expect_close().times(1).returning(|| Ok(()));
2006
2007        let step = StepBuilder::new("test")
2008            .chunk(3)
2009            .reader(&reader)
2010            .processor(&processor)
2011            .writer(&writer)
2012            .build();
2013
2014        let mut step_execution = StepExecution::new(&step.name);
2015
2016        let result = step.execute(&mut step_execution);
2017
2018        assert!(result.is_ok());
2019        assert!(step_execution.duration.unwrap().as_nanos() > 0);
2020        assert!(step_execution.start_time.unwrap() <= step_execution.end_time.unwrap());
2021
2022        Ok(())
2023    }
2024
2025    #[test]
2026    fn step_should_handle_empty_chunk_at_end() -> Result<()> {
2027        let mut i = 0;
2028        let mut reader = MockTestItemReader::default();
2029        reader
2030            .expect_read()
2031            .returning(move || mock_read(&mut i, 0, 1));
2032
2033        let mut processor = MockTestProcessor::default();
2034        let mut i = 0;
2035        processor
2036            .expect_process()
2037            .returning(move |_| mock_process(&mut i, &[]));
2038
2039        let mut writer = MockTestItemWriter::default();
2040        writer.expect_open().times(1).returning(|| Ok(()));
2041        writer.expect_write().times(1).returning(|items| {
2042            assert_eq!(items.len(), 1); // Partial chunk with 1 item
2043            Ok(())
2044        });
2045        writer.expect_flush().times(1).returning(|| Ok(()));
2046        writer.expect_close().times(1).returning(|| Ok(()));
2047
2048        let step = StepBuilder::new("test")
2049            .chunk(3)
2050            .reader(&reader)
2051            .processor(&processor)
2052            .writer(&writer)
2053            .build();
2054
2055        let mut step_execution = StepExecution::new(&step.name);
2056
2057        let result = step.execute(&mut step_execution);
2058
2059        assert!(result.is_ok());
2060        assert_eq!(step_execution.status, StepStatus::Success);
2061        assert_eq!(step_execution.read_count, 1);
2062        assert_eq!(step_execution.write_count, 1);
2063
2064        Ok(())
2065    }
2066
2067    #[test]
2068    fn step_execution_should_initialize_correctly() -> Result<()> {
2069        let step_execution = StepExecution::new("test_step");
2070
2071        assert_eq!(step_execution.name, "test_step");
2072        assert_eq!(step_execution.status, StepStatus::Starting);
2073        assert!(step_execution.start_time.is_none());
2074        assert!(step_execution.end_time.is_none());
2075        assert!(step_execution.duration.is_none());
2076        assert_eq!(step_execution.read_count, 0);
2077        assert_eq!(step_execution.write_count, 0);
2078        assert_eq!(step_execution.read_error_count, 0);
2079        assert_eq!(step_execution.process_count, 0);
2080        assert_eq!(step_execution.process_error_count, 0);
2081        assert_eq!(step_execution.write_error_count, 0);
2082        assert!(!step_execution.id.is_nil());
2083
2084        Ok(())
2085    }
2086
2087    #[test]
2088    fn should_format_phase_summary_with_percentages() {
2089        let mut step_execution = StepExecution::new("load-postgres");
2090        step_execution.duration = Some(Duration::from_secs(10));
2091        step_execution.read_duration = Duration::from_secs(5);
2092        step_execution.process_duration = Duration::from_secs(1);
2093        step_execution.write_duration = Duration::from_secs(3);
2094        step_execution.flush_duration = Duration::from_secs(1);
2095
2096        let summary = step_execution.phase_summary();
2097
2098        assert!(summary.contains("load-postgres"), "summary: {summary}");
2099        assert!(summary.contains("read 5.0s (50%)"), "summary: {summary}");
2100        assert!(summary.contains("process 1.0s (10%)"), "summary: {summary}");
2101        assert!(summary.contains("write 3.0s (30%)"), "summary: {summary}");
2102        assert!(summary.contains("flush 1.0s (10%)"), "summary: {summary}");
2103    }
2104
2105    #[test]
2106    fn should_report_zero_percentages_when_duration_is_unset() {
2107        let step_execution = StepExecution::new("never-ran");
2108
2109        let summary = step_execution.phase_summary();
2110
2111        assert!(summary.contains("(0%)"), "summary: {summary}");
2112        assert!(
2113            !summary.contains("NaN"),
2114            "division by zero leaked: {summary}"
2115        );
2116        assert!(
2117            !summary.contains("inf"),
2118            "division by zero leaked: {summary}"
2119        );
2120    }
2121
2122    #[test]
2123    fn tasklet_step_should_execute_successfully() -> Result<()> {
2124        let mut tasklet = MockTestTasklet::default();
2125        tasklet
2126            .expect_execute()
2127            .times(1)
2128            .returning(|_| Ok(RepeatStatus::Finished));
2129
2130        let step = StepBuilder::new("tasklet_test").tasklet(&tasklet).build();
2131
2132        let mut step_execution = StepExecution::new(&step.name);
2133
2134        let result = step.execute(&mut step_execution);
2135
2136        assert!(result.is_ok());
2137        assert_eq!(step.get_name(), "tasklet_test");
2138
2139        Ok(())
2140    }
2141
2142    #[test]
2143    fn tasklet_step_should_handle_tasklet_error() -> Result<()> {
2144        let mut tasklet = MockTestTasklet::default();
2145        tasklet
2146            .expect_execute()
2147            .times(1)
2148            .returning(|_| Err(BatchError::Step("tasklet error".to_string())));
2149
2150        let step = StepBuilder::new("tasklet_test").tasklet(&tasklet).build();
2151
2152        let mut step_execution = StepExecution::new(&step.name);
2153
2154        let result = step.execute(&mut step_execution);
2155
2156        // The tasklet step should now properly handle errors
2157        assert!(result.is_err());
2158        if let Err(BatchError::Step(msg)) = result {
2159            assert_eq!(msg, "tasklet error");
2160        } else {
2161            panic!("Expected Step error");
2162        }
2163
2164        Ok(())
2165    }
2166
2167    #[test]
2168    fn tasklet_step_should_handle_continuable_status() -> Result<()> {
2169        use std::cell::Cell;
2170
2171        let call_count = Cell::new(0);
2172        let mut tasklet = MockTestTasklet::default();
2173        tasklet.expect_execute().times(4).returning(move |_| {
2174            let count = call_count.get();
2175            call_count.set(count + 1);
2176            if count < 3 {
2177                Ok(RepeatStatus::Continuable)
2178            } else {
2179                Ok(RepeatStatus::Finished)
2180            }
2181        });
2182
2183        let step = StepBuilder::new("continuable_tasklet_test")
2184            .tasklet(&tasklet)
2185            .build();
2186
2187        let mut step_execution = StepExecution::new(&step.name);
2188
2189        let result = step.execute(&mut step_execution);
2190
2191        assert!(result.is_ok());
2192        assert_eq!(step.get_name(), "continuable_tasklet_test");
2193
2194        Ok(())
2195    }
2196
2197    #[test]
2198    fn tasklet_step_should_handle_multiple_continuable_cycles() -> Result<()> {
2199        use std::cell::Cell;
2200
2201        let call_count = Cell::new(0);
2202        let mut tasklet = MockTestTasklet::default();
2203
2204        // Set up a sequence: 5 Continuable calls -> 1 Finished call
2205        tasklet.expect_execute().times(6).returning(move |_| {
2206            let count = call_count.get();
2207            call_count.set(count + 1);
2208            if count < 5 {
2209                Ok(RepeatStatus::Continuable)
2210            } else {
2211                Ok(RepeatStatus::Finished)
2212            }
2213        });
2214
2215        let step = StepBuilder::new("multi_cycle_tasklet_test")
2216            .tasklet(&tasklet)
2217            .build();
2218
2219        let mut step_execution = StepExecution::new(&step.name);
2220
2221        let result = step.execute(&mut step_execution);
2222
2223        assert!(result.is_ok());
2224        assert_eq!(step.get_name(), "multi_cycle_tasklet_test");
2225
2226        Ok(())
2227    }
2228
2229    #[test]
2230    fn tasklet_step_should_handle_error_after_continuable() -> Result<()> {
2231        use std::cell::Cell;
2232
2233        let call_count = Cell::new(0);
2234        let mut tasklet = MockTestTasklet::default();
2235
2236        // Set up a sequence: 2 Continuable calls -> 1 Error
2237        tasklet.expect_execute().times(3).returning(move |_| {
2238            let count = call_count.get();
2239            call_count.set(count + 1);
2240            if count < 2 {
2241                Ok(RepeatStatus::Continuable)
2242            } else {
2243                Err(BatchError::Step("error after continuable".to_string()))
2244            }
2245        });
2246
2247        let step = StepBuilder::new("error_after_continuable_test")
2248            .tasklet(&tasklet)
2249            .build();
2250
2251        let mut step_execution = StepExecution::new(&step.name);
2252
2253        let result = step.execute(&mut step_execution);
2254
2255        assert!(result.is_err());
2256        if let Err(BatchError::Step(msg)) = result {
2257            assert_eq!(msg, "error after continuable");
2258        } else {
2259            panic!("Expected Step error");
2260        }
2261
2262        Ok(())
2263    }
2264
2265    #[test]
2266    fn tasklet_step_should_handle_immediate_finished_status() -> Result<()> {
2267        let mut tasklet = MockTestTasklet::default();
2268        tasklet
2269            .expect_execute()
2270            .times(1)
2271            .returning(|_| Ok(RepeatStatus::Finished));
2272
2273        let step = StepBuilder::new("immediate_finished_test")
2274            .tasklet(&tasklet)
2275            .build();
2276
2277        let mut step_execution = StepExecution::new(&step.name);
2278
2279        let result = step.execute(&mut step_execution);
2280
2281        assert!(result.is_ok());
2282        assert_eq!(step.get_name(), "immediate_finished_test");
2283
2284        Ok(())
2285    }
2286
2287    #[test]
2288    fn tasklet_step_should_access_step_execution_context() -> Result<()> {
2289        let mut tasklet = MockTestTasklet::default();
2290        tasklet
2291            .expect_execute()
2292            .times(1)
2293            .withf(|step_execution| {
2294                // Verify that the tasklet receives the correct step execution context
2295                step_execution.name == "context_test"
2296                    && step_execution.status == StepStatus::Started
2297            })
2298            .returning(|_| Ok(RepeatStatus::Finished));
2299
2300        let step = StepBuilder::new("context_test").tasklet(&tasklet).build();
2301
2302        let mut step_execution = StepExecution::new(&step.name);
2303
2304        let result = step.execute(&mut step_execution);
2305
2306        assert!(result.is_ok());
2307
2308        Ok(())
2309    }
2310
2311    #[test]
2312    fn tasklet_builder_should_create_valid_tasklet_step() -> Result<()> {
2313        let mut tasklet = MockTestTasklet::default();
2314        tasklet
2315            .expect_execute()
2316            .times(1)
2317            .returning(|_| Ok(RepeatStatus::Finished));
2318
2319        let step = TaskletBuilder::new("builder_test")
2320            .tasklet(&tasklet)
2321            .build();
2322
2323        let mut step_execution = StepExecution::new(&step.name);
2324
2325        let result = step.execute(&mut step_execution);
2326
2327        assert!(result.is_ok());
2328        assert_eq!(step.get_name(), "builder_test");
2329
2330        Ok(())
2331    }
2332
2333    #[test]
2334    fn tasklet_builder_should_panic_without_tasklet() {
2335        let result = std::panic::catch_unwind(|| TaskletBuilder::new("test").build());
2336
2337        assert!(result.is_err());
2338    }
2339
2340    #[test]
2341    fn step_should_handle_writer_open_error() -> Result<()> {
2342        let mut reader = MockTestItemReader::default();
2343        let reader_result = Ok(None);
2344        reader.expect_read().return_once(move || reader_result);
2345
2346        let mut processor = MockTestProcessor::default();
2347        processor.expect_process().never();
2348
2349        let mut writer = MockTestItemWriter::default();
2350        writer
2351            .expect_open()
2352            .times(1)
2353            .returning(|| Err(BatchError::ItemWriter("open error".to_string())));
2354        writer.expect_close().times(1).returning(|| Ok(()));
2355
2356        let step = StepBuilder::new("test")
2357            .chunk(3)
2358            .reader(&reader)
2359            .processor(&processor)
2360            .writer(&writer)
2361            .build();
2362
2363        let mut step_execution = StepExecution::new(&step.name);
2364
2365        let result = step.execute(&mut step_execution);
2366
2367        // The step should still succeed as open errors are managed
2368        assert!(result.is_ok());
2369        assert_eq!(step_execution.status, StepStatus::Success);
2370
2371        Ok(())
2372    }
2373
2374    #[test]
2375    fn step_should_handle_writer_close_error() -> Result<()> {
2376        let mut reader = MockTestItemReader::default();
2377        let reader_result = Ok(None);
2378        reader.expect_read().return_once(move || reader_result);
2379
2380        let mut processor = MockTestProcessor::default();
2381        processor.expect_process().never();
2382
2383        let mut writer = MockTestItemWriter::default();
2384        writer.expect_open().times(1).returning(|| Ok(()));
2385        writer.expect_write().never();
2386        writer
2387            .expect_close()
2388            .times(1)
2389            .returning(|| Err(BatchError::ItemWriter("close error".to_string())));
2390
2391        let step = StepBuilder::new("test")
2392            .chunk(3)
2393            .reader(&reader)
2394            .processor(&processor)
2395            .writer(&writer)
2396            .build();
2397
2398        let mut step_execution = StepExecution::new(&step.name);
2399
2400        let result = step.execute(&mut step_execution);
2401
2402        // The step should still succeed as close errors are managed
2403        assert!(result.is_ok());
2404        assert_eq!(step_execution.status, StepStatus::Success);
2405
2406        Ok(())
2407    }
2408
2409    #[test]
2410    fn step_should_handle_writer_flush_error() -> Result<()> {
2411        let mut i = 0;
2412        let mut reader = MockTestItemReader::default();
2413        reader
2414            .expect_read()
2415            .returning(move || mock_read(&mut i, 0, 2));
2416
2417        let mut processor = MockTestProcessor::default();
2418        let mut i = 0;
2419        processor
2420            .expect_process()
2421            .returning(move |_| mock_process(&mut i, &[]));
2422
2423        let mut writer = MockTestItemWriter::default();
2424        writer.expect_open().times(1).returning(|| Ok(()));
2425        writer.expect_write().times(1).returning(|_| Ok(()));
2426        writer
2427            .expect_flush()
2428            .times(1)
2429            .returning(|| Err(BatchError::ItemWriter("flush error".to_string())));
2430        writer.expect_close().times(1).returning(|| Ok(()));
2431
2432        let step = StepBuilder::new("test")
2433            .chunk(3)
2434            .reader(&reader)
2435            .processor(&processor)
2436            .writer(&writer)
2437            .build();
2438
2439        let mut step_execution = StepExecution::new(&step.name);
2440
2441        let result = step.execute(&mut step_execution);
2442
2443        // The step should still succeed as flush errors are managed
2444        assert!(result.is_ok());
2445        assert_eq!(step_execution.status, StepStatus::Success);
2446
2447        Ok(())
2448    }
2449
2450    #[test]
2451    fn step_should_handle_multiple_chunks_with_exact_chunk_size() -> Result<()> {
2452        let mut i = 0;
2453        let mut reader = MockTestItemReader::default();
2454        reader
2455            .expect_read()
2456            .returning(move || mock_read(&mut i, 0, 6));
2457
2458        let mut processor = MockTestProcessor::default();
2459        let mut i = 0;
2460        processor
2461            .expect_process()
2462            .returning(move |_| mock_process(&mut i, &[]));
2463
2464        let mut writer = MockTestItemWriter::default();
2465        writer.expect_open().times(1).returning(|| Ok(()));
2466        writer.expect_write().times(2).returning(|items| {
2467            assert_eq!(items.len(), 3); // Each chunk should have exactly 3 items
2468            Ok(())
2469        });
2470        writer.expect_flush().times(2).returning(|| Ok(()));
2471        writer.expect_close().times(1).returning(|| Ok(()));
2472
2473        let step = StepBuilder::new("test")
2474            .chunk(3)
2475            .reader(&reader)
2476            .processor(&processor)
2477            .writer(&writer)
2478            .build();
2479
2480        let mut step_execution = StepExecution::new(&step.name);
2481
2482        let result = step.execute(&mut step_execution);
2483
2484        assert!(result.is_ok());
2485        assert_eq!(step_execution.status, StepStatus::Success);
2486        assert_eq!(step_execution.read_count, 6);
2487        assert_eq!(step_execution.write_count, 6);
2488
2489        Ok(())
2490    }
2491
2492    #[test]
2493    fn step_should_handle_skip_limit_boundary() -> Result<()> {
2494        let mut i = 0;
2495        let mut reader = MockTestItemReader::default();
2496        reader
2497            .expect_read()
2498            .returning(move || mock_read(&mut i, 0, 4));
2499
2500        let mut processor = MockTestProcessor::default();
2501        let mut i = 0;
2502        processor
2503            .expect_process()
2504            .returning(move |_| mock_process(&mut i, &[1, 2])); // 2 errors
2505
2506        let mut writer = MockTestItemWriter::default();
2507        writer.expect_open().times(1).returning(|| Ok(()));
2508        writer.expect_write().times(2).returning(|_| Ok(()));
2509        writer.expect_flush().times(2).returning(|| Ok(()));
2510        writer.expect_close().times(1).returning(|| Ok(()));
2511
2512        let step = StepBuilder::new("test")
2513            .chunk(3)
2514            .reader(&reader)
2515            .processor(&processor)
2516            .writer(&writer)
2517            .skip_limit(2) // Exactly at the limit
2518            .build();
2519
2520        let mut step_execution = StepExecution::new(&step.name);
2521
2522        let result = step.execute(&mut step_execution);
2523
2524        assert!(result.is_ok());
2525        assert_eq!(step_execution.status, StepStatus::Success);
2526        assert_eq!(step_execution.process_error_count, 2);
2527
2528        Ok(())
2529    }
2530
2531    #[test]
2532    fn step_should_fail_when_skip_limit_exceeded() -> Result<()> {
2533        let mut i = 0;
2534        let mut reader = MockTestItemReader::default();
2535        reader
2536            .expect_read()
2537            .returning(move || mock_read(&mut i, 0, 4));
2538
2539        let mut processor = MockTestProcessor::default();
2540        let mut i = 0;
2541        processor
2542            .expect_process()
2543            .returning(move |_| mock_process(&mut i, &[1, 2, 3])); // 3 errors
2544
2545        let mut writer = MockTestItemWriter::default();
2546        writer.expect_open().times(1).returning(|| Ok(()));
2547        writer.expect_write().never(); // Should not reach write due to error
2548        writer.expect_close().times(1).returning(|| Ok(()));
2549
2550        let step = StepBuilder::new("test")
2551            .chunk(3)
2552            .reader(&reader)
2553            .processor(&processor)
2554            .writer(&writer)
2555            .skip_limit(2) // Exceeded by 1
2556            .build();
2557
2558        let mut step_execution = StepExecution::new(&step.name);
2559
2560        let result = step.execute(&mut step_execution);
2561
2562        assert!(result.is_err());
2563        assert_eq!(step_execution.status, StepStatus::ProcessorError);
2564        assert_eq!(step_execution.process_error_count, 3);
2565
2566        Ok(())
2567    }
2568
2569    #[test]
2570    fn step_should_handle_empty_processed_chunk() -> Result<()> {
2571        let mut i = 0;
2572        let mut reader = MockTestItemReader::default();
2573        reader
2574            .expect_read()
2575            .returning(move || mock_read(&mut i, 0, 3));
2576
2577        let mut processor = MockTestProcessor::default();
2578        let mut i = 0;
2579        processor
2580            .expect_process()
2581            .returning(move |_| mock_process(&mut i, &[1, 2, 3, 4])); // All items fail processing
2582
2583        let mut writer = MockTestItemWriter::default();
2584        writer.expect_open().times(1).returning(|| Ok(()));
2585        writer.expect_write().never(); // Empty chunks are not written
2586        writer.expect_close().times(1).returning(|| Ok(()));
2587
2588        let step = StepBuilder::new("test")
2589            .chunk(3)
2590            .reader(&reader)
2591            .processor(&processor)
2592            .writer(&writer)
2593            .skip_limit(3) // Allow all errors
2594            .build();
2595
2596        let mut step_execution = StepExecution::new(&step.name);
2597
2598        let result = step.execute(&mut step_execution);
2599
2600        assert!(result.is_ok());
2601        assert_eq!(step_execution.status, StepStatus::Success);
2602        assert_eq!(step_execution.process_error_count, 3);
2603        assert_eq!(step_execution.write_count, 0); // No items written
2604
2605        Ok(())
2606    }
2607
2608    #[test]
2609    fn chunk_status_should_be_comparable() {
2610        assert_eq!(ChunkStatus::Finished, ChunkStatus::Finished);
2611        assert_eq!(ChunkStatus::Full, ChunkStatus::Full);
2612        assert_ne!(ChunkStatus::Finished, ChunkStatus::Full);
2613    }
2614
2615    #[test]
2616    fn step_status_should_be_comparable() {
2617        assert_eq!(StepStatus::Success, StepStatus::Success);
2618        assert_eq!(StepStatus::ReadError, StepStatus::ReadError);
2619        assert_eq!(StepStatus::ProcessorError, StepStatus::ProcessorError);
2620        assert_eq!(StepStatus::WriteError, StepStatus::WriteError);
2621        assert_eq!(StepStatus::Starting, StepStatus::Starting);
2622
2623        assert_ne!(StepStatus::Success, StepStatus::ReadError);
2624        assert_ne!(StepStatus::ProcessorError, StepStatus::WriteError);
2625    }
2626
2627    #[test]
2628    fn repeat_status_should_be_comparable() {
2629        assert_eq!(RepeatStatus::Continuable, RepeatStatus::Continuable);
2630        assert_eq!(RepeatStatus::Finished, RepeatStatus::Finished);
2631        assert_ne!(RepeatStatus::Continuable, RepeatStatus::Finished);
2632    }
2633
2634    #[test]
2635    fn step_builder_should_create_chunk_oriented_step() -> Result<()> {
2636        let mut reader = MockTestItemReader::default();
2637        reader.expect_read().return_once(|| Ok(None));
2638
2639        let mut processor = MockTestProcessor::default();
2640        processor.expect_process().never();
2641
2642        let mut writer = MockTestItemWriter::default();
2643        writer.expect_open().times(1).returning(|| Ok(()));
2644        writer.expect_write().never();
2645        writer.expect_close().times(1).returning(|| Ok(()));
2646
2647        let step = StepBuilder::new("builder_test")
2648            .chunk(5)
2649            .reader(&reader)
2650            .processor(&processor)
2651            .writer(&writer)
2652            .skip_limit(10)
2653            .build();
2654
2655        let mut step_execution = StepExecution::new(&step.name);
2656        let result = step.execute(&mut step_execution);
2657
2658        assert!(result.is_ok());
2659        assert_eq!(step.get_name(), "builder_test");
2660
2661        Ok(())
2662    }
2663
2664    #[test]
2665    fn step_should_handle_large_chunk_size() -> Result<()> {
2666        let mut i = 0;
2667        let mut reader = MockTestItemReader::default();
2668        reader
2669            .expect_read()
2670            .returning(move || mock_read(&mut i, 0, 5));
2671
2672        let mut processor = MockTestProcessor::default();
2673        let mut i = 0;
2674        processor
2675            .expect_process()
2676            .returning(move |_| mock_process(&mut i, &[]));
2677
2678        let mut writer = MockTestItemWriter::default();
2679        writer.expect_open().times(1).returning(|| Ok(()));
2680        writer.expect_write().times(1).returning(|items| {
2681            assert_eq!(items.len(), 5); // All items in one chunk
2682            Ok(())
2683        });
2684        writer.expect_flush().times(1).returning(|| Ok(()));
2685        writer.expect_close().times(1).returning(|| Ok(()));
2686
2687        let step = StepBuilder::new("test")
2688            .chunk(100) // Chunk size larger than available items
2689            .reader(&reader)
2690            .processor(&processor)
2691            .writer(&writer)
2692            .build();
2693
2694        let mut step_execution = StepExecution::new(&step.name);
2695
2696        let result = step.execute(&mut step_execution);
2697
2698        assert!(result.is_ok());
2699        assert_eq!(step_execution.status, StepStatus::Success);
2700        assert_eq!(step_execution.read_count, 5);
2701        assert_eq!(step_execution.write_count, 5);
2702
2703        Ok(())
2704    }
2705
2706    #[test]
2707    fn step_should_handle_mixed_errors_within_skip_limit() -> Result<()> {
2708        use std::cell::Cell;
2709
2710        let read_counter = Cell::new(0u16);
2711        let mut reader = MockTestItemReader::default();
2712        reader.expect_read().returning(move || {
2713            let current = read_counter.get();
2714            if current == 2 {
2715                read_counter.set(current + 1);
2716                Err(BatchError::ItemReader("read error".to_string()))
2717            } else {
2718                let mut i = current;
2719                let result = mock_read(&mut i, 0, 6);
2720                read_counter.set(i);
2721                result
2722            }
2723        });
2724
2725        let mut processor = MockTestProcessor::default();
2726        let mut i = 0;
2727        processor
2728            .expect_process()
2729            .returning(move |_| mock_process(&mut i, &[2])); // 1 process error
2730
2731        let mut writer = MockTestItemWriter::default();
2732        writer.expect_open().times(1).returning(|| Ok(()));
2733        writer.expect_write().times(2).returning(|_| Ok(()));
2734        writer.expect_flush().times(2).returning(|| Ok(()));
2735        writer.expect_close().times(1).returning(|| Ok(()));
2736
2737        let step = StepBuilder::new("test")
2738            .chunk(3)
2739            .reader(&reader)
2740            .processor(&processor)
2741            .writer(&writer)
2742            .skip_limit(2) // Allow 1 read error + 1 process error
2743            .build();
2744
2745        let mut step_execution = StepExecution::new(&step.name);
2746
2747        let result = step.execute(&mut step_execution);
2748
2749        assert!(result.is_ok());
2750        assert_eq!(step_execution.status, StepStatus::Success);
2751        assert_eq!(step_execution.read_error_count, 1);
2752        assert_eq!(step_execution.process_error_count, 1);
2753
2754        Ok(())
2755    }
2756
2757    #[test]
2758    fn step_execution_should_be_cloneable() -> Result<()> {
2759        let step_execution = StepExecution::new("test_step");
2760        let cloned_execution = step_execution.clone();
2761
2762        assert_eq!(step_execution.id, cloned_execution.id);
2763        assert_eq!(step_execution.name, cloned_execution.name);
2764        assert_eq!(step_execution.status, cloned_execution.status);
2765        assert_eq!(step_execution.read_count, cloned_execution.read_count);
2766        assert_eq!(step_execution.write_count, cloned_execution.write_count);
2767
2768        Ok(())
2769    }
2770
2771    #[test]
2772    fn step_should_handle_zero_chunk_size() -> Result<()> {
2773        let mut reader = MockTestItemReader::default();
2774        reader.expect_read().return_once(|| Ok(None));
2775
2776        let mut processor = MockTestProcessor::default();
2777        processor.expect_process().never();
2778
2779        let mut writer = MockTestItemWriter::default();
2780        writer.expect_open().times(1).returning(|| Ok(()));
2781        writer.expect_write().never();
2782        writer.expect_close().times(1).returning(|| Ok(()));
2783
2784        // Test with chunk size of 1 (minimum practical value)
2785        let step = StepBuilder::new("test")
2786            .chunk(1)
2787            .reader(&reader)
2788            .processor(&processor)
2789            .writer(&writer)
2790            .build();
2791
2792        let mut step_execution = StepExecution::new(&step.name);
2793
2794        let result = step.execute(&mut step_execution);
2795
2796        assert!(result.is_ok());
2797        assert_eq!(step_execution.status, StepStatus::Success);
2798
2799        Ok(())
2800    }
2801
2802    #[test]
2803    fn step_should_handle_continuous_read_errors_until_skip_limit() -> Result<()> {
2804        use std::cell::Cell;
2805
2806        let counter = Cell::new(0u16);
2807        let mut reader = MockTestItemReader::default();
2808        reader.expect_read().returning(move || {
2809            let current = counter.get();
2810            counter.set(current + 1);
2811            if current < 3 {
2812                Err(BatchError::ItemReader("continuous read error".to_string()))
2813            } else {
2814                Ok(None) // End of data after errors
2815            }
2816        });
2817
2818        let mut processor = MockTestProcessor::default();
2819        processor.expect_process().never();
2820
2821        let mut writer = MockTestItemWriter::default();
2822        writer.expect_open().times(1).returning(|| Ok(()));
2823        writer.expect_write().never();
2824        writer.expect_close().times(1).returning(|| Ok(()));
2825
2826        let step = StepBuilder::new("test")
2827            .chunk(3)
2828            .reader(&reader)
2829            .processor(&processor)
2830            .writer(&writer)
2831            .skip_limit(2) // Should fail after 3 errors (exceeds limit of 2)
2832            .build();
2833
2834        let mut step_execution = StepExecution::new(&step.name);
2835
2836        let result = step.execute(&mut step_execution);
2837
2838        assert!(result.is_err());
2839        assert_eq!(step_execution.status, StepStatus::ReadError);
2840        assert_eq!(step_execution.read_error_count, 3);
2841
2842        Ok(())
2843    }
2844
2845    #[test]
2846    fn step_should_handle_write_error_with_skip_limit() -> Result<()> {
2847        let mut i = 0;
2848        let mut reader = MockTestItemReader::default();
2849        reader
2850            .expect_read()
2851            .returning(move || mock_read(&mut i, 0, 4));
2852
2853        let mut processor = MockTestProcessor::default();
2854        let mut i = 0;
2855        processor
2856            .expect_process()
2857            .returning(move |_| mock_process(&mut i, &[]));
2858
2859        let mut writer = MockTestItemWriter::default();
2860        writer.expect_open().times(1).returning(|| Ok(()));
2861        writer
2862            .expect_write()
2863            .times(1)
2864            .returning(|_| Err(BatchError::ItemWriter("write error".to_string())));
2865        writer.expect_close().times(1).returning(|| Ok(()));
2866
2867        let step = StepBuilder::new("test")
2868            .chunk(3)
2869            .reader(&reader)
2870            .processor(&processor)
2871            .writer(&writer)
2872            .skip_limit(0) // No tolerance for errors
2873            .build();
2874
2875        let mut step_execution = StepExecution::new(&step.name);
2876
2877        let result = step.execute(&mut step_execution);
2878
2879        assert!(result.is_err());
2880        assert_eq!(step_execution.status, StepStatus::WriteError);
2881        assert_eq!(step_execution.write_error_count, 3); // All items in chunk failed
2882
2883        Ok(())
2884    }
2885
2886    #[test]
2887    fn step_should_succeed_when_write_error_within_skip_limit() -> Result<()> {
2888        let mut i = 0;
2889        let mut reader = MockTestItemReader::default();
2890        reader
2891            .expect_read()
2892            .returning(move || mock_read(&mut i, 0, 3));
2893
2894        let mut processor = MockTestProcessor::default();
2895        let mut i = 0;
2896        processor
2897            .expect_process()
2898            .returning(move |_| mock_process(&mut i, &[]));
2899
2900        let mut writer = MockTestItemWriter::default();
2901        writer.expect_open().times(1).returning(|| Ok(()));
2902        writer
2903            .expect_write()
2904            .times(1)
2905            .returning(|_| Err(BatchError::ItemWriter("write error".to_string())));
2906        writer.expect_close().times(1).returning(|| Ok(()));
2907
2908        let step = StepBuilder::new("test")
2909            .chunk(3)
2910            .reader(&reader)
2911            .processor(&processor)
2912            .writer(&writer)
2913            .skip_limit(3) // Exactly at limit: 3 write errors <= skip_limit(3), step continues
2914            .build();
2915
2916        let mut step_execution = StepExecution::new(&step.name);
2917
2918        let result = step.execute(&mut step_execution);
2919
2920        assert!(result.is_ok());
2921        assert_eq!(step_execution.status, StepStatus::Success);
2922        assert_eq!(step_execution.write_error_count, 3);
2923        assert_eq!(step_execution.write_count, 0); // No successful writes
2924
2925        Ok(())
2926    }
2927
2928    #[test]
2929    fn step_should_handle_partial_chunk_at_end() -> Result<()> {
2930        let mut i = 0;
2931        let mut reader = MockTestItemReader::default();
2932        reader
2933            .expect_read()
2934            .returning(move || mock_read(&mut i, 0, 2)); // Only 2 items, chunk size is 3
2935
2936        let mut processor = MockTestProcessor::default();
2937        let mut i = 0;
2938        processor
2939            .expect_process()
2940            .returning(move |_| mock_process(&mut i, &[]));
2941
2942        let mut writer = MockTestItemWriter::default();
2943        writer.expect_open().times(1).returning(|| Ok(()));
2944        writer.expect_write().times(1).returning(|items| {
2945            assert_eq!(items.len(), 2); // Partial chunk with 2 items
2946            Ok(())
2947        });
2948        writer.expect_flush().times(1).returning(|| Ok(()));
2949        writer.expect_close().times(1).returning(|| Ok(()));
2950
2951        let step = StepBuilder::new("test")
2952            .chunk(3)
2953            .reader(&reader)
2954            .processor(&processor)
2955            .writer(&writer)
2956            .build();
2957
2958        let mut step_execution = StepExecution::new(&step.name);
2959
2960        let result = step.execute(&mut step_execution);
2961
2962        assert!(result.is_ok());
2963        assert_eq!(step_execution.status, StepStatus::Success);
2964        assert_eq!(step_execution.read_count, 2);
2965        assert_eq!(step_execution.write_count, 2);
2966
2967        Ok(())
2968    }
2969
2970    #[test]
2971    fn batch_status_should_have_all_variants() {
2972        // Test that all BatchStatus variants exist and can be created
2973        let _completed = BatchStatus::COMPLETED;
2974        let _starting = BatchStatus::STARTING;
2975        let _started = BatchStatus::STARTED;
2976        let _stopping = BatchStatus::STOPPING;
2977        let _stopped = BatchStatus::STOPPED;
2978        let _failed = BatchStatus::FAILED;
2979        let _abandoned = BatchStatus::ABANDONED;
2980        let _unknown = BatchStatus::UNKNOWN;
2981    }
2982
2983    #[test]
2984    fn tasklet_builder_should_require_tasklet() {
2985        let mut tasklet = MockTestTasklet::default();
2986        tasklet.expect_execute().never();
2987
2988        // This test documents that the builder panics if tasklet is not set
2989        // In a real scenario, this would be caught at compile time or runtime
2990        let builder = TaskletBuilder::new("test").tasklet(&tasklet);
2991        let _step = builder.build(); // Should not panic with tasklet set
2992    }
2993
2994    #[test]
2995    fn chunk_oriented_step_builder_should_require_all_components() -> Result<()> {
2996        let mut reader = MockTestItemReader::default();
2997        reader.expect_read().return_once(|| Ok(None));
2998
2999        let mut processor = MockTestProcessor::default();
3000        processor.expect_process().never();
3001
3002        let mut writer = MockTestItemWriter::default();
3003        writer.expect_open().times(1).returning(|| Ok(()));
3004        writer.expect_write().never();
3005        writer.expect_close().times(1).returning(|| Ok(()));
3006
3007        // Test that builder works with all required components
3008        let step = ChunkOrientedStepBuilder::new("test")
3009            .reader(&reader)
3010            .processor(&processor)
3011            .writer(&writer)
3012            .chunk_size(10)
3013            .skip_limit(5)
3014            .build();
3015
3016        let mut step_execution = StepExecution::new(&step.name);
3017        let result = step.execute(&mut step_execution);
3018
3019        assert!(result.is_ok());
3020        assert_eq!(step.get_name(), "test");
3021
3022        Ok(())
3023    }
3024
3025    #[test]
3026    fn step_should_handle_maximum_skip_limit() -> Result<()> {
3027        let mut i = 0;
3028        let mut reader = MockTestItemReader::default();
3029        reader
3030            .expect_read()
3031            .returning(move || mock_read(&mut i, 0, 3)); // Only 3 items to match chunk size
3032
3033        let mut processor = MockTestProcessor::default();
3034        let mut i = 0;
3035        processor
3036            .expect_process()
3037            .returning(move |_| mock_process(&mut i, &[1, 2, 3])); // All items fail
3038
3039        let mut writer = MockTestItemWriter::default();
3040        writer.expect_open().times(1).returning(|| Ok(()));
3041        writer.expect_write().never(); // No items to write since all fail processing
3042        writer.expect_close().times(1).returning(|| Ok(()));
3043
3044        let step = StepBuilder::new("test")
3045            .chunk(3)
3046            .reader(&reader)
3047            .processor(&processor)
3048            .writer(&writer)
3049            .skip_limit(u16::MAX) // Maximum skip limit
3050            .build();
3051
3052        let mut step_execution = StepExecution::new(&step.name);
3053
3054        let result = step.execute(&mut step_execution);
3055
3056        assert!(result.is_ok());
3057        assert_eq!(step_execution.status, StepStatus::Success);
3058        assert_eq!(step_execution.process_error_count, 3);
3059
3060        Ok(())
3061    }
3062
3063    #[test]
3064    fn step_should_handle_tasklet_step_timing() -> Result<()> {
3065        let mut tasklet = MockTestTasklet::default();
3066        tasklet
3067            .expect_execute()
3068            .times(1)
3069            .returning(|_| Ok(RepeatStatus::Finished));
3070
3071        let step = StepBuilder::new("timing_test").tasklet(&tasklet).build();
3072
3073        let mut step_execution = StepExecution::new(&step.name);
3074
3075        let result = step.execute(&mut step_execution);
3076
3077        assert!(result.is_ok());
3078        assert!(step_execution.start_time.is_some());
3079        assert!(step_execution.end_time.is_some());
3080        assert!(step_execution.duration.is_some());
3081        assert!(step_execution.duration.unwrap().as_nanos() > 0);
3082
3083        Ok(())
3084    }
3085
3086    #[test]
3087    fn step_should_handle_tasklet_step_status_transitions() -> Result<()> {
3088        let mut tasklet = MockTestTasklet::default();
3089        tasklet
3090            .expect_execute()
3091            .times(1)
3092            .returning(|step_execution| {
3093                // Verify the step status is Started when tasklet is called
3094                assert_eq!(step_execution.status, StepStatus::Started);
3095                Ok(RepeatStatus::Finished)
3096            });
3097
3098        let step = StepBuilder::new("status_test").tasklet(&tasklet).build();
3099
3100        let mut step_execution = StepExecution::new(&step.name);
3101        assert_eq!(step_execution.status, StepStatus::Starting);
3102
3103        let result = step.execute(&mut step_execution);
3104
3105        assert!(result.is_ok());
3106        assert_eq!(step_execution.status, StepStatus::Success);
3107
3108        Ok(())
3109    }
3110
3111    #[test]
3112    fn step_should_handle_tasklet_step_failed_status() -> Result<()> {
3113        let mut tasklet = MockTestTasklet::default();
3114        tasklet
3115            .expect_execute()
3116            .times(1)
3117            .returning(|_| Err(BatchError::Step("tasklet failure".to_string())));
3118
3119        let step = StepBuilder::new("failed_test").tasklet(&tasklet).build();
3120
3121        let mut step_execution = StepExecution::new(&step.name);
3122
3123        let result = step.execute(&mut step_execution);
3124
3125        assert!(result.is_err());
3126        assert_eq!(step_execution.status, StepStatus::Failed);
3127        assert!(step_execution.end_time.is_some());
3128        assert!(step_execution.duration.is_some());
3129
3130        Ok(())
3131    }
3132
3133    #[test]
3134    fn chunk_oriented_step_builder_should_panic_without_reader() {
3135        let mut processor = MockTestProcessor::default();
3136        processor.expect_process().never();
3137
3138        let mut writer = MockTestItemWriter::default();
3139        writer.expect_open().never();
3140
3141        let result = std::panic::catch_unwind(|| {
3142            ChunkOrientedStepBuilder::new("test")
3143                .processor(&processor)
3144                .writer(&writer)
3145                .build()
3146        });
3147
3148        assert!(result.is_err());
3149    }
3150
3151    #[test]
3152    fn chunk_oriented_step_builder_should_build_without_processor_when_types_match() {
3153        // Car -> Car: reader and writer item types are identical, so omitting
3154        // .processor(...) is allowed and falls back to PassThroughProcessor.
3155        let mut reader = MockTestItemReader::default();
3156        reader.expect_read().returning(|| Ok(None));
3157
3158        let mut writer = MockTestItemWriter::default();
3159        writer.expect_open().returning(|| Ok(()));
3160        writer.expect_close().returning(|| Ok(()));
3161
3162        let step = ChunkOrientedStepBuilder::new("test")
3163            .reader(&reader)
3164            .writer(&writer)
3165            .build();
3166
3167        let mut step_execution = StepExecution::new(step.get_name());
3168        let result = step.execute(&mut step_execution);
3169        assert!(
3170            result.is_ok(),
3171            "step with no processor should run fine when I == O"
3172        );
3173    }
3174
3175    #[test]
3176    fn chunk_oriented_step_builder_without_processor_passes_items_through_unchanged() {
3177        let mut reader = MockTestItemReader::default();
3178        let mut call = 0;
3179        reader.expect_read().returning(move || {
3180            call += 1;
3181            if call == 1 {
3182                Ok(Some(Car {
3183                    year: 2020,
3184                    make: "Toyota".to_string(),
3185                    model: "Corolla".to_string(),
3186                    description: "unchanged".to_string(),
3187                }))
3188            } else {
3189                Ok(None)
3190            }
3191        });
3192
3193        let mut writer = MockTestItemWriter::default();
3194        writer.expect_open().returning(|| Ok(()));
3195        writer.expect_close().returning(|| Ok(()));
3196        writer.expect_flush().returning(|| Ok(()));
3197        writer.expect_write().returning(|items: &[Car]| {
3198            assert_eq!(items.len(), 1);
3199            assert_eq!(items[0].description, "unchanged");
3200            Ok(())
3201        });
3202
3203        let step = ChunkOrientedStepBuilder::new("test")
3204            .reader(&reader)
3205            .writer(&writer)
3206            .build();
3207
3208        let mut step_execution = StepExecution::new(step.get_name());
3209        step.execute(&mut step_execution)
3210            .expect("step should complete successfully");
3211    }
3212
3213    #[test]
3214    fn chunk_oriented_step_builder_should_panic_without_writer() {
3215        let mut reader = MockTestItemReader::default();
3216        reader.expect_read().never();
3217
3218        let mut processor = MockTestProcessor::default();
3219        processor.expect_process().never();
3220
3221        let result = std::panic::catch_unwind(|| {
3222            ChunkOrientedStepBuilder::new("test")
3223                .reader(&reader)
3224                .processor(&processor)
3225                .build()
3226        });
3227
3228        assert!(result.is_err());
3229    }
3230
3231    #[test]
3232    fn step_should_handle_read_chunk_with_full_chunk() -> Result<()> {
3233        let mut i = 0;
3234        let mut reader = MockTestItemReader::default();
3235        reader
3236            .expect_read()
3237            .returning(move || mock_read(&mut i, 0, 4)); // 4 items total
3238
3239        let mut processor = MockTestProcessor::default();
3240        let mut i = 0;
3241        processor
3242            .expect_process()
3243            .returning(move |_| mock_process(&mut i, &[]));
3244
3245        let mut writer = MockTestItemWriter::default();
3246        writer.expect_open().times(1).returning(|| Ok(()));
3247        writer.expect_write().times(2).returning(|items| {
3248            // First chunk has 3 items, second chunk has 1 item
3249            assert!(items.len() <= 3);
3250            Ok(())
3251        });
3252        writer.expect_flush().times(2).returning(|| Ok(()));
3253        writer.expect_close().times(1).returning(|| Ok(()));
3254
3255        let step = StepBuilder::new("test")
3256            .chunk(3)
3257            .reader(&reader)
3258            .processor(&processor)
3259            .writer(&writer)
3260            .build();
3261
3262        let mut step_execution = StepExecution::new(&step.name);
3263
3264        let result = step.execute(&mut step_execution);
3265
3266        assert!(result.is_ok());
3267        assert_eq!(step_execution.status, StepStatus::Success);
3268        assert_eq!(step_execution.read_count, 4);
3269        assert_eq!(step_execution.write_count, 4);
3270
3271        Ok(())
3272    }
3273
3274    #[test]
3275    fn step_should_handle_process_chunk_with_all_errors() -> Result<()> {
3276        let mut i = 0;
3277        let mut reader = MockTestItemReader::default();
3278        reader
3279            .expect_read()
3280            .returning(move || mock_read(&mut i, 0, 3));
3281
3282        let mut processor = MockTestProcessor::default();
3283        let mut i = 0;
3284        processor
3285            .expect_process()
3286            .returning(move |_| mock_process(&mut i, &[1, 2, 3])); // All items fail
3287
3288        let mut writer = MockTestItemWriter::default();
3289        writer.expect_open().times(1).returning(|| Ok(()));
3290        writer.expect_write().never(); // No items to write since all fail processing
3291        writer.expect_close().times(1).returning(|| Ok(()));
3292
3293        let step = StepBuilder::new("test")
3294            .chunk(3)
3295            .reader(&reader)
3296            .processor(&processor)
3297            .writer(&writer)
3298            .skip_limit(5) // Allow all errors
3299            .build();
3300
3301        let mut step_execution = StepExecution::new(&step.name);
3302
3303        let result = step.execute(&mut step_execution);
3304
3305        assert!(result.is_ok());
3306        assert_eq!(step_execution.status, StepStatus::Success);
3307        assert_eq!(step_execution.process_error_count, 3);
3308        assert_eq!(step_execution.write_count, 0);
3309
3310        Ok(())
3311    }
3312
3313    #[test]
3314    fn step_should_handle_write_chunk_with_empty_items() -> Result<()> {
3315        let mut reader = MockTestItemReader::default();
3316        reader.expect_read().return_once(|| Ok(None));
3317
3318        let mut processor = MockTestProcessor::default();
3319        processor.expect_process().never();
3320
3321        let mut writer = MockTestItemWriter::default();
3322        writer.expect_open().times(1).returning(|| Ok(()));
3323        writer.expect_write().never(); // No items to write
3324        writer.expect_close().times(1).returning(|| Ok(()));
3325
3326        let step = StepBuilder::new("test")
3327            .chunk(3)
3328            .reader(&reader)
3329            .processor(&processor)
3330            .writer(&writer)
3331            .build();
3332
3333        let mut step_execution = StepExecution::new(&step.name);
3334
3335        let result = step.execute(&mut step_execution);
3336
3337        assert!(result.is_ok());
3338        assert_eq!(step_execution.status, StepStatus::Success);
3339        assert_eq!(step_execution.read_count, 0);
3340        assert_eq!(step_execution.write_count, 0);
3341
3342        Ok(())
3343    }
3344
3345    #[test]
3346    fn step_should_handle_is_skip_limit_reached_boundary_conditions() -> Result<()> {
3347        let mut i = 0;
3348        let mut reader = MockTestItemReader::default();
3349        reader
3350            .expect_read()
3351            .returning(move || mock_read(&mut i, 0, 4));
3352
3353        let mut processor = MockTestProcessor::default();
3354        let mut i = 0;
3355        processor
3356            .expect_process()
3357            .returning(move |_| mock_process(&mut i, &[1, 2])); // 2 errors
3358
3359        let mut writer = MockTestItemWriter::default();
3360        writer.expect_open().times(1).returning(|| Ok(()));
3361        writer.expect_write().times(2).returning(|_| Ok(()));
3362        writer.expect_flush().times(2).returning(|| Ok(()));
3363        writer.expect_close().times(1).returning(|| Ok(()));
3364
3365        let step = StepBuilder::new("test")
3366            .chunk(3)
3367            .reader(&reader)
3368            .processor(&processor)
3369            .writer(&writer)
3370            .skip_limit(2) // Exactly at the limit
3371            .build();
3372
3373        let mut step_execution = StepExecution::new(&step.name);
3374
3375        let result = step.execute(&mut step_execution);
3376
3377        assert!(result.is_ok());
3378        assert_eq!(step_execution.status, StepStatus::Success);
3379        assert_eq!(step_execution.process_error_count, 2);
3380
3381        Ok(())
3382    }
3383
3384    #[test]
3385    fn step_should_handle_manage_error_with_various_errors() -> Result<()> {
3386        let mut reader = MockTestItemReader::default();
3387        reader.expect_read().return_once(|| Ok(None));
3388
3389        let mut processor = MockTestProcessor::default();
3390        processor.expect_process().never();
3391
3392        let mut writer = MockTestItemWriter::default();
3393        writer
3394            .expect_open()
3395            .times(1)
3396            .returning(|| Err(BatchError::ItemWriter("open error".to_string())));
3397        writer.expect_write().never();
3398        writer
3399            .expect_close()
3400            .times(1)
3401            .returning(|| Err(BatchError::ItemWriter("close error".to_string())));
3402
3403        let step = StepBuilder::new("test")
3404            .chunk(3)
3405            .reader(&reader)
3406            .processor(&processor)
3407            .writer(&writer)
3408            .build();
3409
3410        let mut step_execution = StepExecution::new(&step.name);
3411
3412        let result = step.execute(&mut step_execution);
3413
3414        // Should still succeed as open/close errors are managed
3415        assert!(result.is_ok());
3416        assert_eq!(step_execution.status, StepStatus::Success);
3417
3418        Ok(())
3419    }
3420
3421    #[test]
3422    fn step_execution_should_have_unique_ids() -> Result<()> {
3423        let step_execution1 = StepExecution::new("test1");
3424        let step_execution2 = StepExecution::new("test2");
3425
3426        assert_ne!(step_execution1.id, step_execution2.id);
3427
3428        Ok(())
3429    }
3430
3431    #[test]
3432    fn step_execution_should_clone_with_same_values() -> Result<()> {
3433        let mut step_execution = StepExecution::new("test_step");
3434        step_execution.read_count = 10;
3435        step_execution.write_count = 8;
3436        step_execution.status = StepStatus::Success;
3437
3438        let cloned_execution = step_execution.clone();
3439
3440        assert_eq!(step_execution.id, cloned_execution.id);
3441        assert_eq!(step_execution.name, cloned_execution.name);
3442        assert_eq!(step_execution.status, cloned_execution.status);
3443        assert_eq!(step_execution.read_count, cloned_execution.read_count);
3444        assert_eq!(step_execution.write_count, cloned_execution.write_count);
3445
3446        Ok(())
3447    }
3448
3449    #[test]
3450    fn step_status_should_support_copy_trait() {
3451        let status1 = StepStatus::Success;
3452        let status2 = status1; // This should work because StepStatus implements Copy
3453
3454        assert_eq!(status1, status2);
3455        assert_eq!(status1, StepStatus::Success); // Original should still be usable
3456    }
3457
3458    #[test]
3459    fn step_status_should_support_debug_trait() {
3460        let status = StepStatus::ProcessorError;
3461        let debug_string = format!("{:?}", status);
3462
3463        assert!(debug_string.contains("ProcessorError"));
3464    }
3465
3466    #[test]
3467    fn chunk_status_should_support_debug_trait() {
3468        let status = ChunkStatus::Full;
3469        let debug_string = format!("{:?}", status);
3470
3471        assert!(debug_string.contains("Full"));
3472    }
3473
3474    #[test]
3475    fn repeat_status_should_support_debug_trait() {
3476        let status = RepeatStatus::Continuable;
3477        let debug_string = format!("{:?}", status);
3478
3479        assert!(debug_string.contains("Continuable"));
3480    }
3481
3482    #[test]
3483    fn step_should_count_filtered_items() -> Result<()> {
3484        // Reader returns 4 items (items 0,1,2,3), ends at 4
3485        let mut i = 0u16;
3486        let mut reader = MockTestItemReader::default();
3487        reader
3488            .expect_read()
3489            .returning(move || mock_read(&mut i, 0, 4));
3490
3491        // Processor filters item at position 2 (returns Ok(None))
3492        let mut j = 0u16;
3493        let mut processor = MockTestProcessor::default();
3494        processor.expect_process().returning(move |_| {
3495            j += 1;
3496            if j == 2 {
3497                return Ok(None); // filter this item
3498            }
3499            Ok(Some(Car {
3500                year: 1979,
3501                make: "make".to_owned(),
3502                model: "model".to_owned(),
3503                description: "description".to_owned(),
3504            }))
3505        });
3506
3507        let mut writer = MockTestItemWriter::default();
3508        writer.expect_open().times(1).returning(|| Ok(()));
3509        // 3 items pass through (4 read - 1 filtered), written in one chunk
3510        writer.expect_write().times(1).returning(|items| {
3511            assert_eq!(items.len(), 3, "expected 3 items written after filtering");
3512            Ok(())
3513        });
3514        writer.expect_flush().returning(|| Ok(()));
3515        writer.expect_close().times(1).returning(|| Ok(()));
3516
3517        let step = StepBuilder::new("test")
3518            .chunk(10)
3519            .reader(&reader)
3520            .processor(&processor)
3521            .writer(&writer)
3522            .build();
3523
3524        let mut step_execution = StepExecution::new(&step.name);
3525        let result = step.execute(&mut step_execution);
3526
3527        assert!(result.is_ok());
3528        assert_eq!(step_execution.read_count, 4, "should have read 4 items");
3529        assert_eq!(
3530            step_execution.filter_count, 1,
3531            "should have filtered 1 item"
3532        );
3533        assert_eq!(
3534            step_execution.process_count, 3,
3535            "should have processed 3 items"
3536        );
3537        assert_eq!(step_execution.write_count, 3, "should have written 3 items");
3538
3539        Ok(())
3540    }
3541
3542    #[test]
3543    fn step_should_not_call_writer_when_all_items_filtered() -> Result<()> {
3544        let mut i = 0u16;
3545        let mut reader = MockTestItemReader::default();
3546        reader
3547            .expect_read()
3548            .returning(move || mock_read(&mut i, 0, 3));
3549
3550        let mut processor = MockTestProcessor::default();
3551        processor.expect_process().returning(|_| Ok(None)); // filter every item
3552
3553        let mut writer = MockTestItemWriter::default();
3554        writer.expect_open().times(1).returning(|| Ok(()));
3555        writer.expect_write().never(); // must NOT be called
3556        writer.expect_close().times(1).returning(|| Ok(()));
3557
3558        let step = StepBuilder::new("test")
3559            .chunk(10)
3560            .reader(&reader)
3561            .processor(&processor)
3562            .writer(&writer)
3563            .build();
3564
3565        let mut step_execution = StepExecution::new(&step.name);
3566        let result = step.execute(&mut step_execution);
3567
3568        assert!(result.is_ok());
3569        assert_eq!(
3570            step_execution.filter_count, 3,
3571            "all 3 items should be filtered"
3572        );
3573        assert_eq!(
3574            step_execution.process_count, 0,
3575            "no items should reach process_count"
3576        );
3577        assert_eq!(step_execution.write_count, 0, "nothing should be written");
3578
3579        Ok(())
3580    }
3581
3582    #[test]
3583    fn should_initialize_phase_durations_to_zero() {
3584        let step_execution = StepExecution::new("phase-init");
3585
3586        assert_eq!(step_execution.read_duration, Duration::ZERO);
3587        assert_eq!(step_execution.process_duration, Duration::ZERO);
3588        assert_eq!(step_execution.write_duration, Duration::ZERO);
3589        assert_eq!(step_execution.flush_duration, Duration::ZERO);
3590    }
3591
3592    #[test]
3593    fn should_record_nonzero_read_duration_after_step() {
3594        let mut reader = MockTestItemReader::default();
3595        let mut counter = 0u16;
3596        reader.expect_read().returning(move || {
3597            std::thread::sleep(Duration::from_millis(20));
3598            counter += 1;
3599            if counter > 2 {
3600                Ok(None)
3601            } else {
3602                Ok(sample_car())
3603            }
3604        });
3605
3606        let processor = PassThroughProcessor::<Car>::new();
3607
3608        let mut writer = MockTestItemWriter::default();
3609        writer.expect_open().returning(|| Ok(()));
3610        writer.expect_write().returning(|_| Ok(()));
3611        writer.expect_flush().returning(|| Ok(()));
3612        writer.expect_close().returning(|| Ok(()));
3613
3614        let step = StepBuilder::new("read-timing")
3615            .chunk(10)
3616            .reader(&reader)
3617            .processor(&processor)
3618            .writer(&writer)
3619            .build();
3620
3621        let mut step_execution = StepExecution::new("read-timing");
3622        step.execute(&mut step_execution).unwrap();
3623
3624        assert!(
3625            step_execution.read_duration >= Duration::from_millis(40),
3626            "expected read_duration to cover 3 sleeping reads, got {:?}",
3627            step_execution.read_duration
3628        );
3629    }
3630
3631    #[test]
3632    fn should_attribute_slow_reads_to_read_duration_not_process() {
3633        let mut reader = MockTestItemReader::default();
3634        let mut counter = 0u16;
3635        reader.expect_read().returning(move || {
3636            std::thread::sleep(Duration::from_millis(30));
3637            counter += 1;
3638            if counter > 2 {
3639                Ok(None)
3640            } else {
3641                Ok(sample_car())
3642            }
3643        });
3644
3645        let processor = PassThroughProcessor::<Car>::new();
3646
3647        let mut writer = MockTestItemWriter::default();
3648        writer.expect_open().returning(|| Ok(()));
3649        writer.expect_write().returning(|_| Ok(()));
3650        writer.expect_flush().returning(|| Ok(()));
3651        writer.expect_close().returning(|| Ok(()));
3652
3653        let step = StepBuilder::new("read-attribution")
3654            .chunk(10)
3655            .reader(&reader)
3656            .processor(&processor)
3657            .writer(&writer)
3658            .build();
3659
3660        let mut step_execution = StepExecution::new("read-attribution");
3661        step.execute(&mut step_execution).unwrap();
3662
3663        assert!(
3664            step_execution.read_duration > step_execution.process_duration,
3665            "a sleeping reader must not have its time attributed to process: read={:?} process={:?}",
3666            step_execution.read_duration,
3667            step_execution.process_duration
3668        );
3669    }
3670
3671    #[test]
3672    fn should_record_flush_duration_separately_from_write() {
3673        let mut reader = MockTestItemReader::default();
3674        let mut counter = 0u16;
3675        reader.expect_read().returning(move || {
3676            counter += 1;
3677            if counter > 2 {
3678                Ok(None)
3679            } else {
3680                Ok(sample_car())
3681            }
3682        });
3683
3684        let processor = PassThroughProcessor::<Car>::new();
3685
3686        let mut writer = MockTestItemWriter::default();
3687        writer.expect_open().returning(|| Ok(()));
3688        writer.expect_write().returning(|_| Ok(()));
3689        writer.expect_flush().returning(|| {
3690            std::thread::sleep(Duration::from_millis(30));
3691            Ok(())
3692        });
3693        writer.expect_close().returning(|| Ok(()));
3694
3695        let step = StepBuilder::new("flush-timing")
3696            .chunk(10)
3697            .reader(&reader)
3698            .processor(&processor)
3699            .writer(&writer)
3700            .build();
3701
3702        let mut step_execution = StepExecution::new("flush-timing");
3703        step.execute(&mut step_execution).unwrap();
3704
3705        assert!(
3706            step_execution.flush_duration >= Duration::from_millis(30),
3707            "flush_duration should capture the sleeping flush, got {:?}",
3708            step_execution.flush_duration
3709        );
3710        assert!(
3711            step_execution.flush_duration > step_execution.write_duration,
3712            "a slow flush must not be attributed to write: flush={:?} write={:?}",
3713            step_execution.flush_duration,
3714            step_execution.write_duration
3715        );
3716    }
3717
3718    #[test]
3719    fn should_attribute_duration_to_the_correct_phase() {
3720        let mut reader = MockTestItemReader::default();
3721        let mut counter = 0u16;
3722        reader.expect_read().returning(move || {
3723            counter += 1;
3724            if counter > 3 {
3725                Ok(None)
3726            } else {
3727                Ok(sample_car())
3728            }
3729        });
3730
3731        let processor = PassThroughProcessor::<Car>::new();
3732
3733        let mut writer = MockTestItemWriter::default();
3734        writer.expect_open().returning(|| Ok(()));
3735        writer.expect_write().returning(|_| {
3736            std::thread::sleep(Duration::from_millis(50));
3737            Ok(())
3738        });
3739        writer.expect_flush().returning(|| Ok(()));
3740        writer.expect_close().returning(|| Ok(()));
3741
3742        let step = StepBuilder::new("phase-attribution")
3743            .chunk(10)
3744            .reader(&reader)
3745            .processor(&processor)
3746            .writer(&writer)
3747            .build();
3748
3749        let mut step_execution = StepExecution::new("phase-attribution");
3750        step.execute(&mut step_execution).unwrap();
3751
3752        assert!(
3753            step_execution.write_duration > step_execution.read_duration,
3754            "slow writer should dominate: write={:?} read={:?}",
3755            step_execution.write_duration,
3756            step_execution.read_duration
3757        );
3758        assert!(
3759            step_execution.write_duration > step_execution.process_duration,
3760            "slow writer should dominate: write={:?} process={:?}",
3761            step_execution.write_duration,
3762            step_execution.process_duration
3763        );
3764    }
3765
3766    #[test]
3767    fn should_leave_write_duration_at_zero_for_empty_chunk() {
3768        let mut reader = MockTestItemReader::default();
3769        reader.expect_read().returning(|| Ok(None));
3770
3771        let processor = PassThroughProcessor::<Car>::new();
3772
3773        let mut writer = MockTestItemWriter::default();
3774        writer.expect_open().returning(|| Ok(()));
3775        writer.expect_close().returning(|| Ok(()));
3776
3777        let step = StepBuilder::new("empty-chunk")
3778            .chunk(10)
3779            .reader(&reader)
3780            .processor(&processor)
3781            .writer(&writer)
3782            .build();
3783
3784        let mut step_execution = StepExecution::new("empty-chunk");
3785        step.execute(&mut step_execution).unwrap();
3786
3787        assert_eq!(
3788            step_execution.write_duration,
3789            Duration::ZERO,
3790            "the empty-chunk early return skips the writer entirely"
3791        );
3792        assert_eq!(step_execution.flush_duration, Duration::ZERO);
3793    }
3794}