tekhsi_rs 0.1.1

High-performance client for Tektronix TekHSI enabled oscilloscopes
Documentation
use super::{BatchDownloadResult, SymbolDownloadResult};
use crate::data::{Acquisition, ChannelData};
use crate::decoder::decode_waveform_chunks;
use crate::errors::TekHsiError;
use tracing::warn;

pub(super) fn decode_batch(batch: &mut BatchDownloadResult) -> Result<Acquisition, TekHsiError> {
    let mut has_success = false;
    let mut channel_data = Vec::with_capacity(batch.results.len());

    for result in batch.results.iter_mut() {
        match &result {
            SymbolDownloadResult::Success {
                symbol,
                header,
                data_chunks,
            } => {
                has_success = true;

                // Extract and validate data
                match decode_waveform_chunks(header, data_chunks) {
                    Ok(wf) => {
                        channel_data.push(ChannelData::Waveform {
                            acq_id: header.dataid,
                            symbol: symbol.clone(),
                            header: header.clone(),
                            waveform: wf,
                        });
                    }
                    Err(de) => {
                        channel_data.push(ChannelData::DecodeError {
                            symbol: symbol.clone(),
                            header: header.clone(),
                            error: de,
                        });
                    }
                };
            }
            SymbolDownloadResult::Failure { symbol, error } => {
                warn!("Failed to download symbol {}: {}", symbol, error);
                channel_data.push(ChannelData::AcquisitionError {
                    symbol: symbol.clone(),
                    error: error.clone(),
                });
            }
        }
    }

    if !has_success {
        // If we get here, then there was not even a single good acquisition
        return Err(TekHsiError::NoValidData);
    }

    Ok(Acquisition::new(
        channel_data,
        batch.wait_time,
        batch.download_time,
    ))
}

#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod tests {
    use super::super::{BatchDownloadResult, SymbolDownloadResult};
    use super::decode_batch;
    use crate::data::{ChannelData, Waveform};
    use crate::errors::AcquisitionError;
    use crate::errors::TekHsiError;
    use crate::tekscope::WaveformHeader;
    use crate::validation::is_header_valid;
    use smol_str::SmolStr;

    fn base_header() -> WaveformHeader {
        WaveformHeader {
            sourcename: "ch1".to_string(),
            verticalunits: "V".to_string(),
            horizontal_units: "s".to_string(),
            hasdata: true,
            ..Default::default()
        }
    }

    #[test]
    fn decode_batch_returns_no_valid_data() {
        let mut result = BatchDownloadResult {
            results: vec![SymbolDownloadResult::Failure {
                symbol: SmolStr::from("ch1"),
                error: AcquisitionError::DownloadFailed {
                    message: "test".to_string(),
                },
            }],
            ..Default::default()
        };

        let err = decode_batch(&mut result).expect_err("expected error");
        assert!(matches!(err, TekHsiError::NoValidData));
    }

    #[test]
    fn decode_batch_handles_partial_failures() {
        let header = WaveformHeader {
            wfmtype: 1,
            sourcewidth: 1,
            noofsamples: 2,
            dataid: 7,
            ..base_header()
        };
        assert!(is_header_valid(&header));

        let mut result = BatchDownloadResult {
            results: vec![
                SymbolDownloadResult::Success {
                    symbol: SmolStr::from("ch1"),
                    header,
                    data_chunks: vec![vec![1u8, 2]],
                },
                SymbolDownloadResult::Failure {
                    symbol: SmolStr::from("ch2"),
                    error: AcquisitionError::HeaderRequestFailed {
                        status: "Server did not provide header payload".to_string(),
                    },
                },
            ],
            ..Default::default()
        };

        let acquisition = decode_batch(&mut result).expect("expected acquisition");
        assert_eq!(acquisition.data.len(), 2);

        let ch1 = acquisition
            .data
            .iter()
            .find(|cd| cd.symbol() == "ch1")
            .unwrap();
        match ch1 {
            ChannelData::Waveform {
                acq_id,
                waveform: Waveform::Analog(_),
                ..
            } => assert_eq!(*acq_id, 7),
            _ => panic!("expected analog waveform for ch1"),
        }

        let ch2 = acquisition
            .data
            .iter()
            .find(|cd| cd.symbol() == "ch2")
            .unwrap();
        assert!(matches!(ch2, ChannelData::AcquisitionError { .. }));
    }

    #[test]
    fn decode_batch_successful_acquisition() {
        let header = WaveformHeader {
            wfmtype: 1,
            sourcewidth: 1,
            noofsamples: 2,
            dataid: 42,
            ..base_header()
        };
        assert!(is_header_valid(&header));

        let mut result = BatchDownloadResult {
            results: vec![SymbolDownloadResult::Success {
                symbol: SmolStr::from("ch1"),
                header,
                data_chunks: vec![vec![1u8, 2]],
            }],
            ..Default::default()
        };

        let acquisition = decode_batch(&mut result).expect("expected acquisition");
        assert_eq!(acquisition.data.len(), 1);

        let ch1 = acquisition
            .data
            .iter()
            .find(|cd| cd.symbol() == "ch1")
            .unwrap();
        match ch1 {
            ChannelData::Waveform {
                acq_id,
                waveform: Waveform::Analog(_),
                ..
            } => assert_eq!(*acq_id, 42),
            _ => panic!("expected analog waveform for ch1"),
        }
    }

    #[test]
    fn decode_batch_propagates_timing() {
        let header = WaveformHeader {
            wfmtype: 1,
            sourcewidth: 1,
            noofsamples: 2,
            dataid: 9,
            ..base_header()
        };
        assert!(is_header_valid(&header));

        let mut result = BatchDownloadResult {
            results: vec![SymbolDownloadResult::Success {
                symbol: SmolStr::from("ch1"),
                header,
                data_chunks: vec![vec![1u8, 2]],
            }],
            wait_time: std::time::Duration::from_millis(12),
            download_time: std::time::Duration::from_millis(34),
        };

        let acquisition = decode_batch(&mut result).expect("expected acquisition");
        assert_eq!(acquisition.wait_time, std::time::Duration::from_millis(12));
        assert_eq!(
            acquisition.download_time,
            std::time::Duration::from_millis(34)
        );
    }

    #[test]
    fn decode_batch_records_decode_error() {
        let header = WaveformHeader {
            wfmtype: 1,
            sourcewidth: 3,
            noofsamples: 1,
            dataid: 3,
            ..base_header()
        };
        assert!(is_header_valid(&header));

        let mut result = BatchDownloadResult {
            results: vec![SymbolDownloadResult::Success {
                symbol: SmolStr::from("ch1"),
                header,
                data_chunks: vec![vec![1u8]],
            }],
            ..Default::default()
        };

        let acquisition = decode_batch(&mut result).expect("expected acquisition");
        assert_eq!(acquisition.data.len(), 1);
        assert!(matches!(
            acquisition.data[0],
            ChannelData::DecodeError { .. }
        ));
    }
}