כתיבת בדיקות לפעולות ממושכות

Google Cloud ספריות הלקוח של Rust מספקות פונקציות עזר שמפשטות את האינטראקציה עם פעולות ממושכות (LRO). כדי לדמות את ההתנהגות של פעולות LRO בבדיקות יחידה, צריך להבין את הפרטים הבסיסיים שפונקציות העזר האלה מסתירות. במדריך הזה מוסבר איך לכתוב בדיקות יחידה לדגימה אוטומטית וידנית של LRO, כולל תרחישי שגיאה.

דרישות מוקדמות

במדריך הזה אנחנו יוצאים מנקודת הנחה שאתם מכירים את המדריכים הבאים:

בדיקת הסקרים האוטומטיים

נניח שקוד האפליקציה ממתין ל-lro::Poller::until_done() (סקר אוטומטי):

// An example application function that automatically polls.
//
// It starts an LRO, awaits the result, and processes it.
pub async fn my_automatic_poller(
    client: &speech::client::Speech,
    project_id: &str,
) -> Result<Option<wkt::Duration>> {
    use google_cloud_lro::Poller;
    client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .poller()
        .until_done()
        .await
        .map(|r| r.total_billed_duration)
}

מכיוון שהאפליקציה משתמשת רק בתוצאה הסופית של ה-LRO, אין צורך לבדוק איך היא מטפלת בתוצאות ביניים של דגימה. הבדיקה יכולה להחזיר את התוצאה הסופית של ה-LRO ישירות מה-mock.

יצירת longrunning::model::Operation

נניח שאתם רוצים שהקריאה תחזיר את התשובה הבאה:

fn expected_duration() -> Option<wkt::Duration> {
    Some(wkt::Duration::clamp(100, 0))
}

fn expected_response() -> BatchRecognizeResponse {
    BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}

ה-stub מחזיר longrunning::model::Operation במקום BatchRecognizeResponse. אורזים את התגובה הצפויה ב-Operation::result:

fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
    let any = wkt::Any::from_msg(response).expect("test message should succeed");
    let operation = Operation::new()
        .set_done(true)
        .set_result(OperationResult::Response(any.into()));
    Ok(Response::from(operation))
}

מגדירים את השדה done בתור true כדי לציין ל-Poller שהפעולה הושלמה, וכך מסיימים את לולאת הסקר:

.set_done(true)

כתיבת הבדיקה

  1. מגדירים את המבנה המדומה, שמטמיע את המאפיין speech::stub::Speech:

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

  2. בבדיקה, יוצרים את המוק ומגדירים את הציפיות ממנו:

    let mut mock = MockSpeech::new();
    mock.expect_batch_recognize()
        .return_once(|_, _| make_finished_operation(&expected_response()));

  3. יוצרים לקוח מהדמה, קוראים לפונקציה ומאמתים את התגובה:

    // Create a client, implemented by our mock.
    let client = speech::client::Speech::from_stub(mock);
    
    // Call our function which automatically polls.
    let billed_duration = my_automatic_poller(&client, "my-project").await?;
    
    // Verify the final result of the LRO.
    assert_eq!(billed_duration, expected_duration());

בדיקת סקר ידני עם מטא-נתונים זמניים

נניח שקוד האפליקציה שלכם מבצע סקר ידני ומעבד עדכונים חלקיים:

pub struct BatchRecognizeResult {
    pub progress_updates: Vec<i32>,
    pub billed_duration: Result<Option<wkt::Duration>>,
}

// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
    client: &speech::client::Speech,
    project_id: &str,
) -> BatchRecognizeResult {
    use google_cloud_lro::{Poller, PollingResult};
    let mut progress_updates = Vec::new();
    let mut poller = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .poller();
    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                let billed_duration = r.map(|r| r.total_billed_duration);
                return BatchRecognizeResult {
                    progress_updates,
                    billed_duration,
                };
            }
            PollingResult::InProgress(m) => {
                if let Some(metadata) = m {
                    // This is a silly application. Your application likely
                    // performs some task immediately with the partial
                    // update, instead of storing it for after the operation
                    // has completed.
                    progress_updates.push(metadata.progress_percent);
                }
            }
            PollingResult::PollingError(e) => {
                return BatchRecognizeResult {
                    progress_updates,
                    billed_duration: Err(e),
                };
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    // We can only get here if `poll()` returns `None`, but it only returns
    // `None` after it returned `PollingResult::Completed`. Therefore this
    // is never reached.
    unreachable!("loop should exit via the `Completed` branch.");
}

כדי לדמות את התנהגות האפליקציה כשהיא מקבלת מטא-נתונים זמניים, אפשר להחזיר פעולות שמתבצעות מהדמה.

יצירת longrunning::model::Operation

ה-RPC של BatchRecognize מחזיר תוצאות חלקיות בתור speech::model::OperationMetadata. אורזים את המטא-נתונים האלה בשדה Operation::metadata של ה-longrunning::model::Operation שמוחזר:

fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
    let metadata = OperationMetadata::new().set_progress_percent(progress);
    let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
    let operation = Operation::new().set_metadata(any);
    Ok(Response::from(operation))
}

כתיבת הבדיקה

  1. מגדירים את ה-struct המדומה, שמטמיע את המאפיין speech::stub::Speech ומחליף את get_operation():

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

  2. בבדיקה, יוצרים את המוק ומגדירים את הציפיות ממנו כך שיחזיר תוצאות חלקיות (25%, 50%, 75%) ואחריהן את התוצאה הסופית:

    let mut seq = mockall::Sequence::new();
    let mut mock = MockSpeech::new();
    mock.expect_batch_recognize()
        .once()
        .in_sequence(&mut seq)
        .returning(|_, _| make_partial_operation(25));
    mock.expect_get_operation()
        .once()
        .in_sequence(&mut seq)
        .returning(|_, _| make_partial_operation(50));
    mock.expect_get_operation()
        .once()
        .in_sequence(&mut seq)
        .returning(|_, _| make_partial_operation(75));
    mock.expect_get_operation()
        .once()
        .in_sequence(&mut seq)
        .returning(|_, _| make_finished_operation(&expected_response()));

    חשוב לשים לב לפרטים הבאים לגבי אופן ההגדרה של הציפיות:

    • קריאות RPC ראשוניות לעומת קריאות RPC עוקבות: הציפייה הראשונה מוגדרת ב-batch_recognize(), ואילו כל הציפיות העוקבות מוגדרות ב-get_operation(). ה-RPC הראשוני של BatchRecognize מפעיל את ה-LRO בשרת ומחזיר מזהה פעולה בשדה name (הושמט מקוד הבדיקה כדי לקצר). מאותו רגע, ספריית הלקוח דוגמת את הסטטוס של ה-LRO באמצעות ה-RPC‏ GetOperation.
    • סדר הקריאות: הציפיות נרשמות ב-Sequence. כך mockall יכול לאמת את סדר הקריאות ולקבוע איזו ציפייה של expect_get_operation תואמת לכל סקר.
  3. יוצרים לקוח מהדמה, קוראים לפונקציה ומאמתים את התגובה:

    // Create a client, implemented by our mock.
    let client = speech::client::Speech::from_stub(mock);
    
    // Call our function which manually polls.
    let result = my_manual_poller(&client, "my-project").await;
    
    // Verify the partial metadata updates, and the final result.
    assert_eq!(result.progress_updates, [25, 50, 75]);
    assert_eq!(result.billed_duration?, expected_duration());

הדמיית שגיאות

שגיאות יכולות להתרחש בכמה נקודות במהלך LRO.

אם האפליקציה משתמשת בסקרים אוטומטיים, כל מקורות השגיאה שווים: until_done() מחזיר את השגיאה ב-Result, בלי קשר למקור שלה. הדמיה של שגיאה בהפעלת LRO היא הבדיקה הפשוטה ביותר.

סימולציה של שגיאה בהפעלת LRO

כדי לדמות שגיאה בהתחלה של LRO, מגדירים את הבקשה הראשונית כך שתיכשל עם שגיאה:

mock.expect_batch_recognize().return_once(|_, _| {
    use gax::error::Error;
    use gax::error::rpc::{Code, Status};
    let status = Status::default()
        .set_code(Code::Aborted)
        .set_message("Resource exhausted");
    Err(Error::service(status))
});

בדגימה ידנית, הענף PollingResult::Completed מקבל את השגיאה מהפעלת ה-LRO, שמסיימת את לולאת הדגימה:

PollingResult::Completed(r) => {

סימולציה של LRO שגורמת לשגיאה

כדי לדמות פעולה ארוכה שנכשלת אחרי החזרת מטא-נתונים זמניים, מחזירים את השגיאה ב-longrunning::model::Operation הסופי:

fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
    let operation = Operation::new()
        .set_done(true)
        .set_result(OperationResult::Error(status.into()));
    Ok(Response::from(operation))
}

הגדרת ציפיות להחזרת Operation שנכשל מ-get_operation:

mock.expect_get_operation()
    .once()
    .in_sequence(&mut seq)
    .returning(|_, _| {
        // This is a common error for `Create*` RPCs, which are often
        // LROs. It is less applicable to `BatchRecognize` in practice.
        let status = rpc::model::Status::default()
            .set_code(gax::error::rpc::Code::AlreadyExists as i32)
            .set_message("resource already exists");
        make_failed_operation(status)
    });

בדגימה ידנית, הענף PollingResult::Completed מקבל את תוצאת השגיאה, ומסיים את לולאת הדגימה:

PollingResult::Completed(r) => {

הדמיה של שגיאת דגימה

לולאות דגימה יכולות להסתיים גם כשמדיניות הדגימה מוצתה. במצב כזה, ספריית הלקוח לא יכולה לקבוע באופן ודאי אם ה-LRO הושלם.

אם לאפליקציה יש לוגיקה מותאמת אישית לטיפול בשגיאות דגימה, אפשר לבדוק אותה על ידי החזרת שגיאה ישירות מהציפייה של get_operation:

mock.expect_get_operation()
    .once()
    .in_sequence(&mut seq)
    .returning(|_, _| {
        use gax::error::Error;
        use gax::error::rpc::{Code, Status};
        let status = Status::default()
            .set_code(Code::Aborted)
            .set_message("Operation was aborted");
        Err(Error::service(status))
    });

בסקר ידני, הענף PollingResult::PollingError מקבל את שגיאת הסקר המדומה:

PollingResult::PollingError(e) => {

קוד בדיקה מלא

בקטעים הבאים מוצג הקוד המלא של הבדיקות שמתוארות במדריך הזה.

סקר אוטומטי: בדיקה מלאה

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};

// Example application code that is under test
mod my_application {
    use super::*;

    // An example application function that automatically polls.
    //
    // It starts an LRO, awaits the result, and processes it.
    pub async fn my_automatic_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> Result<Option<wkt::Duration>> {
        use google_cloud_lro::Poller;
        client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller()
            .until_done()
            .await
            .map(|r| r.total_billed_duration)
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn automatic_polling() -> Result<()> {
        // Create a mock, and set expectations on it.
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .return_once(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which automatically polls.
        let billed_duration = my_automatic_poller(&client, "my-project").await?;

        // Verify the final result of the LRO.
        assert_eq!(billed_duration, expected_duration());

        Ok(())
    }
}

סקר ידני עם מטא-נתונים זמניים: בדיקה מלאה

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(
        response: &BatchRecognizeResponse,
    ) -> Result<gax::response::Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn manual_polling_with_metadata() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert_eq!(result.billed_duration?, expected_duration());

        Ok(())
    }
}

הדמיית שגיאות: בדיקות מלאות

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Error(status.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn error_starting_lro() -> Result<()> {
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize().return_once(|_, _| {
            use gax::error::Error;
            use gax::error::rpc::{Code, Status};
            let status = Status::default()
                .set_code(Code::Aborted)
                .set_message("Resource exhausted");
            Err(Error::service(status))
        });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the the final result.
        assert!(
            result.billed_duration.is_err(),
            "{:?}",
            result.billed_duration
        );

        Ok(())
    }

    #[tokio::test]
    async fn lro_ending_in_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                // This is a common error for `Create*` RPCs, which are often
                // LROs. It is less applicable to `BatchRecognize` in practice.
                let status = rpc::model::Status::default()
                    .set_code(gax::error::rpc::Code::AlreadyExists as i32)
                    .set_message("resource already exists");
                make_failed_operation(status)
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert!(
            result.billed_duration.is_err(),
            "{:?}",
            result.billed_duration
        );

        Ok(())
    }

    #[tokio::test]
    async fn polling_loop_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                use gax::error::Error;
                use gax::error::rpc::{Code, Status};
                let status = Status::default()
                    .set_code(Code::Aborted)
                    .set_message("Operation was aborted");
                Err(Error::service(status))
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25]);
        assert!(
            result.billed_duration.is_err(),
            "{:?}",
            result.billed_duration
        );

        Ok(())
    }
}