terrazzo-terminal 0.2.7

A simple web-based terminal emulator built on Terrazzo.
#![cfg(feature = "server")]

use std::path::Path;
use std::process::Stdio;

use nameth::NamedEnumValues as _;
use nameth::nameth;
use scopeguard::defer;
use tokio::io::AsyncBufReadExt as _;
use tokio::io::BufReader;
use tokio::process::Command;
use tonic::Code;
use tracing::Instrument as _;
use tracing::debug;
use tracing::debug_span;
use tracing::trace;

use super::synthetic::SyntheticDiagnostic;
use crate::backend::client_service::grpc_error::GrpcError;
use crate::backend::client_service::grpc_error::IsGrpcError;

#[nameth]
#[derive(Debug, thiserror::Error)]
pub enum CargoCheckError {
    #[error("[{n}] {0}", n = self.name())]
    SpawnProcess(std::io::Error),

    #[error("[{n}] Process doesn't have an stdout", n = self.name())]
    MissingStdout,

    #[error("[{n}] {0}", n = self.name())]
    Failure(std::io::Error),
}

impl IsGrpcError for CargoCheckError {
    fn code(&self) -> Code {
        Code::Internal
    }
}

pub fn cargo_check(
    base_path: impl AsRef<Path>,
    features: &[&str],
) -> impl Future<Output = Result<Vec<SyntheticDiagnostic>, GrpcError<CargoCheckError>>> {
    let base_path = base_path.as_ref();
    let _span = debug_span!("Cargo check", ?base_path).entered();
    debug!("Start");

    let mut command = {
        let mut command = Command::new("cargo");
        command
            .current_dir(base_path)
            .args(["check", "--message-format=json"])
            .stdout(Stdio::piped())
            .stderr(Stdio::null());
        if !features.is_empty() {
            command.arg("--features").arg(features.join(","));
        }
        command
    };

    debug!("Spawn {command:?}");
    async move {
        let mut child = command.spawn().map_err(CargoCheckError::SpawnProcess)?;
        let output = child.stdout.take().ok_or(CargoCheckError::MissingStdout)?;
        let mut reader = BufReader::new(output).lines();

        let mut results = vec![];
        defer!(debug!("End"));
        loop {
            let next_line = reader.next_line().await;
            let next_line = match next_line {
                Ok(Some(next_line)) => next_line,
                Ok(None) => break,
                Err(error) => {
                    if results.is_empty() {
                        return Err(CargoCheckError::Failure(error))?;
                    } else {
                        debug!("Bad line: {error}");
                        break;
                    }
                }
            };
            let next_line = next_line.trim();
            if next_line.is_empty() {
                continue;
            }

            let Ok(message) = serde_json::from_str::<super::messages::CargoCheckMessage>(next_line)
                .inspect_err(|error| trace!("Invalid cargo check JSON: {error}: {next_line}"))
            else {
                continue;
            };
            if message.reason != "compiler-message" {
                continue;
            }

            results.extend(SyntheticDiagnostic::new(&message));
        }
        Ok(results)
    }
    .in_current_span()
}

#[cfg(not(feature = "bazel"))]
#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use trz_gateway_common::tracing::test_utils::enable_tracing_for_tests;

    use super::super::synthetic::SyntheticDiagnosticCode;
    use super::super::synthetic::SyntheticDiagnosticSpan;

    const RUST_LANG_CHECKS: &str = "src/text_editor/rust_lang/tests/rust_lang_checks";

    #[tokio::test]
    async fn some_unused_method() {
        enable_tracing_for_tests();
        let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(RUST_LANG_CHECKS);
        let result = super::cargo_check(&base_path, &["some_unused_method"])
            .await
            .unwrap();
        assert_eq!(base_path, PathBuf::from(&result[0].base_path));
        assert_eq!("warning", &result[0].level);
        assert_eq!(
            &Some(SyntheticDiagnosticCode {
                code: "dead_code".into(),
                explanation: None
            }),
            &result[0].code
        );
        assert_eq!(
            &SyntheticDiagnosticSpan {
                file_name: "src/main.rs".into(),
                byte_start: 88,
                byte_end: 106,
                line_start: 6,
                line_end: 6,
                column_start: 4,
                column_end: 22,
                suggested_replacement: None,
                suggestion_applicability: None
            },
            &result[0].spans[0]
        )
    }

    #[tokio::test]
    async fn method_does_not_exist() {
        enable_tracing_for_tests();
        let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(RUST_LANG_CHECKS);
        let result = super::cargo_check(&base_path, &["method_does_not_exist"])
            .await
            .unwrap();
        assert_eq!(base_path, PathBuf::from(&result[0].base_path));
        assert_eq!("error", &result[0].level);
        assert_eq!("E0599", result[0].code.as_ref().unwrap().code);
        assert!(result[0].message.contains("no method named `unwrap2`"));
    }

    #[tokio::test]
    #[ignore = "Not a good idea to compile the current project"]
    async fn terminal() {
        enable_tracing_for_tests();
        let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        let result = super::cargo_check(&base_path, &[]).await.unwrap();
        assert_eq!(result, vec![]);
    }
}