1use 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
100pub trait Tasklet {
126 fn execute(&self, step_execution: &StepExecution) -> Result<RepeatStatus, BatchError>;
135}
136
137pub 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 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
208pub struct TaskletBuilder<'a> {
232 name: String,
233 tasklet: Option<&'a dyn Tasklet>,
234}
235
236impl<'a> TaskletBuilder<'a> {
237 pub fn new(name: &str) -> Self {
250 Self {
251 name: name.to_string(),
252 tasklet: None,
253 }
254 }
255
256 pub fn tasklet(mut self, tasklet: &'a dyn Tasklet) -> Self {
279 self.tasklet = Some(tasklet);
280 self
281 }
282
283 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#[derive(Clone)]
340#[non_exhaustive]
341pub struct StepExecution {
342 pub id: Uuid,
344 pub name: String,
346 pub status: StepStatus,
348 pub start_time: Option<Instant>,
350 pub end_time: Option<Instant>,
352 pub duration: Option<Duration>,
354 pub read_count: usize,
356 pub write_count: usize,
358 pub read_error_count: usize,
360 pub process_count: usize,
362 pub process_error_count: usize,
364 pub filter_count: usize,
366 pub write_error_count: usize,
368 pub read_duration: Duration,
372 pub process_duration: Duration,
376 pub write_duration: Duration,
379 pub flush_duration: Duration,
383}
384
385impl StepExecution {
386 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 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
479pub enum BatchStatus {
497 COMPLETED,
499 STARTING,
501 STARTED,
503 STOPPING,
505 STOPPED,
507 FAILED,
509 ABANDONED,
511 UNKNOWN,
513}
514
515pub trait Step {
543 fn execute(&self, step_execution: &mut StepExecution) -> Result<(), BatchError>;
576
577 fn get_name(&self) -> &str;
593}
594
595#[derive(Debug, PartialEq)]
612pub enum RepeatStatus {
613 Continuable,
618 Finished,
623}
624
625pub struct ChunkOrientedStep<'a, I, O> {
667 name: String,
668 reader: &'a dyn ItemReader<I>,
670 processor: &'a dyn ItemProcessor<I, O>,
672 writer: &'a dyn ItemWriter<O>,
674 chunk_size: u16,
676 skip_limit: u16,
678}
679
680impl<I, O> Step for ChunkOrientedStep<'_, I, O> {
681 fn execute(&self, step_execution: &mut StepExecution) -> Result<(), BatchError> {
682 let start_time = Instant::now();
684 info!(
685 "Start of step: {}, id: {}",
686 step_execution.name, step_execution.id
687 );
688
689 Self::manage_error(self.writer.open());
691
692 loop {
694 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 read_items.is_empty() {
705 step_execution.status = StepStatus::Success;
706 break;
707 }
708
709 if self
711 .process_and_write_chunk(step_execution, read_items)
712 .is_err()
713 {
714 break; }
716
717 if chunk_status == ChunkStatus::Finished {
719 step_execution.status = StepStatus::Success;
720 break;
721 }
722 }
723
724 Self::manage_error(self.writer.close());
726
727 info!(
729 "End of step: {}, id: {}",
730 step_execution.name, step_execution.id
731 );
732
733 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 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 fn process_and_write_chunk(
768 &self,
769 step_execution: &mut StepExecution,
770 read_items: Vec<I>,
771 ) -> Result<(), BatchError> {
772 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 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 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 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 step_execution.status = StepStatus::ReadError;
855 return Err(error);
856 }
857 }
858 }
859 }
860 }
861
862 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 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 step_execution.status = StepStatus::ProcessorError;
910 return Err(error);
911 }
912 }
913 }
914 }
915
916 Ok(result)
917 }
918
919 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 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 fn manage_error(result: Result<(), BatchError>) {
985 if let Err(error) = result {
986 warn!("Non-fatal error: {}", error);
987 }
988 }
989}
990
991pub struct NoProcessor;
1041
1042pub struct HasProcessor;
1045
1046pub struct ChunkOrientedStepBuilder<'a, I, O, P = NoProcessor> {
1047 name: String,
1049 reader: Option<&'a dyn ItemReader<I>>,
1051 processor: Option<&'a dyn ItemProcessor<I, O>>,
1053 writer: Option<&'a dyn ItemWriter<O>>,
1055 chunk_size: u16,
1057 skip_limit: u16,
1059 _processor_state: PhantomData<P>,
1061}
1062
1063impl<'a, I, O> ChunkOrientedStepBuilder<'a, I, O, NoProcessor> {
1064 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 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 pub fn reader(mut self, reader: &'a dyn ItemReader<I>) -> Self {
1162 self.reader = Some(reader);
1163 self
1164 }
1165
1166 pub fn writer(mut self, writer: &'a dyn ItemWriter<O>) -> Self {
1204 self.writer = Some(writer);
1205 self
1206 }
1207
1208 pub fn chunk_size(mut self, chunk_size: u16) -> Self {
1226 self.chunk_size = chunk_size;
1227 self
1228 }
1229
1230 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 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 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
1357pub struct StepBuilder {
1422 name: String,
1423}
1424
1425impl StepBuilder {
1426 pub fn new(name: &str) -> Self {
1439 Self {
1440 name: name.to_string(),
1441 }
1442 }
1443
1444 pub fn tasklet(self, tasklet: &dyn Tasklet) -> TaskletBuilder<'_> {
1469 TaskletBuilder::new(&self.name).tasklet(tasklet)
1470 }
1471
1472 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#[derive(Debug, PartialEq)]
1535pub enum ChunkStatus {
1536 Finished,
1542
1543 Full,
1549}
1550
1551#[derive(Debug, PartialEq, Clone, Copy)]
1578pub enum StepStatus {
1579 Success,
1585
1586 ReadError,
1592
1593 ProcessorError,
1599
1600 WriteError,
1606
1607 Starting,
1612
1613 Failed,
1617
1618 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); 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 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 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 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 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 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 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 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); 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])); 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) .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])); let mut writer = MockTestItemWriter::default();
2546 writer.expect_open().times(1).returning(|| Ok(()));
2547 writer.expect_write().never(); 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) .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])); let mut writer = MockTestItemWriter::default();
2584 writer.expect_open().times(1).returning(|| Ok(()));
2585 writer.expect_write().never(); 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) .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); 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); 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) .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])); 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) .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 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) }
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) .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) .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); 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) .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); 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)); 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); 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 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 let builder = TaskletBuilder::new("test").tasklet(&tasklet);
2991 let _step = builder.build(); }
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 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)); 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])); let mut writer = MockTestItemWriter::default();
3040 writer.expect_open().times(1).returning(|| Ok(()));
3041 writer.expect_write().never(); 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) .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 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 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)); 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 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])); let mut writer = MockTestItemWriter::default();
3289 writer.expect_open().times(1).returning(|| Ok(()));
3290 writer.expect_write().never(); 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) .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(); 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])); 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) .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 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; assert_eq!(status1, status2);
3455 assert_eq!(status1, StepStatus::Success); }
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 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 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); }
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 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)); let mut writer = MockTestItemWriter::default();
3554 writer.expect_open().times(1).returning(|| Ok(()));
3555 writer.expect_write().never(); 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}