toolproof 0.17.1

Standalone test suite for CLI tools and applications
use async_trait::async_trait;

use crate::civilization::Civilization;
use crate::errors::ToolproofStepError;

use super::{SegmentArgs, ToolproofInstruction, ToolproofRetriever};

mod env_var {
    use super::*;

    pub struct EnvVar;

    inventory::submit! {
        &EnvVar as &dyn ToolproofInstruction
    }

    #[async_trait]
    impl ToolproofInstruction for EnvVar {
        fn segments(&self) -> &'static str {
            "I have the environment variable {name} set to {value}"
        }

        async fn run(
            &self,
            args: &SegmentArgs<'_>,
            civ: &mut Civilization,
        ) -> Result<(), ToolproofStepError> {
            let env_name = args.get_string("name")?;
            let env_value = args.get_string("value")?;

            civ.set_env(env_name.to_string(), env_value.to_string());

            Ok(())
        }
    }
}

mod run {
    use crate::errors::ToolproofTestFailure;

    use super::*;

    pub struct Run;

    inventory::submit! {
        &Run as &dyn ToolproofInstruction
    }

    #[async_trait]
    impl ToolproofInstruction for Run {
        fn segments(&self) -> &'static str {
            "I run {command}"
        }

        async fn run(
            &self,
            args: &SegmentArgs<'_>,
            civ: &mut Civilization,
        ) -> Result<(), ToolproofStepError> {
            let command = args.get_string("command")?;

            let exit_status = civ.run_command(command.to_string()).await?;

            if !exit_status.success() {
                return Err(ToolproofTestFailure::Custom {
                    msg: format!("Failed to run command ({})\nCommand: {command}\nstdout:\n---\n{}\n---\nstderr:\n---\n{}\n---",
                    exit_status,
                    civ.last_command_output.as_ref().map(|o| o.stdout.as_str()).unwrap_or_else(|| "<empty>"),
                    civ.last_command_output.as_ref().map(|o| o.stderr.as_str()).unwrap_or_else(|| "<empty>"),
                ),
                }
                .into());
            }

            Ok(())
        }
    }

    pub struct FailingRun;

    inventory::submit! {
        &FailingRun as &dyn ToolproofInstruction
    }

    #[async_trait]
    impl ToolproofInstruction for FailingRun {
        fn segments(&self) -> &'static str {
            "I run {command} and expect it to fail"
        }

        async fn run(
            &self,
            args: &SegmentArgs<'_>,
            civ: &mut Civilization,
        ) -> Result<(), ToolproofStepError> {
            let command = args.get_string("command")?;

            let exit_status = civ.run_command(command.to_string()).await?;

            if exit_status.success() {
                return Err(ToolproofTestFailure::Custom {
                    msg: format!(
                        "Command ran successfully, but should not have ({})\nCommand: {command}\nstdout:\n---\n{}\n---\nstderr:\n---\n{}\n---",
                        exit_status,
                        civ.last_command_output.as_ref().map(|o| o.stdout.as_str()).unwrap_or_else(|| "<empty>"),
                        civ.last_command_output.as_ref().map(|o| o.stderr.as_str()).unwrap_or_else(|| "<empty>"),
                    ),
                }
                .into());
            }

            Ok(())
        }
    }
}

mod stdio {
    use crate::errors::ToolproofTestFailure;

    use super::*;

    pub struct StdOut;

    inventory::submit! {
        &StdOut as &dyn ToolproofRetriever
    }

    #[async_trait]
    impl ToolproofRetriever for StdOut {
        fn segments(&self) -> &'static str {
            "stdout"
        }

        async fn run(
            &self,
            _args: &SegmentArgs<'_>,
            civ: &mut Civilization,
        ) -> Result<serde_json::Value, ToolproofStepError> {
            let Some(output) = &civ.last_command_output else {
                return Err(ToolproofStepError::Assertion(
                    ToolproofTestFailure::Custom {
                        msg: "no stdout exists".into(),
                    },
                ));
            };

            Ok(output.stdout.clone().into())
        }
    }

    pub struct StdErr;

    inventory::submit! {
        &StdErr as &dyn ToolproofRetriever
    }

    #[async_trait]
    impl ToolproofRetriever for StdErr {
        fn segments(&self) -> &'static str {
            "stderr"
        }

        async fn run(
            &self,
            _args: &SegmentArgs<'_>,
            civ: &mut Civilization,
        ) -> Result<serde_json::Value, ToolproofStepError> {
            let Some(output) = &civ.last_command_output else {
                return Err(ToolproofStepError::Assertion(
                    ToolproofTestFailure::Custom {
                        msg: "no stderr exists".into(),
                    },
                ));
            };

            Ok(output.stderr.clone().into())
        }
    }
}