hutc 0.1.0

Simple af rest api testing client using lua
use clap::Parser;
use futures::future::join_all;
use std::{
    io::{Error, ErrorKind},
    time::Instant,
};

use crate::{
    cli::{Cli, Command},
    fs::load_lua,
    init::init,
    lua::{extract_lua_error, setup_lua},
    registry::TestRegistry,
    update::update_available_message,
};

mod cli;
mod expect;
mod fs;
mod http;
mod init;
mod lua;
mod registry;
mod update;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();

    match cli.command {
        Command::Init { path } => init(path)?,
        Command::Test { path } => run_tests(&path).await?,
    }

    Ok(())
}

async fn run_tests(path: &str) -> Result<(), Box<dyn std::error::Error>> {
    let suite_start = Instant::now();
    let registry = TestRegistry::new();
    let lua = setup_lua(registry.clone())?;
    let lua_files = load_lua(path)?;

    for lua_file in lua_files {
        let chunk_name = lua_file.path.to_string_lossy().to_string();
        let chunk = lua.load(&lua_file.content).set_name(&chunk_name);
        if let Err(err) = chunk.exec_async().await {
            let message = extract_lua_error(err);
            return Err(Error::other(format!(
                "failed to execute Lua file `{}`:\n{}",
                lua_file.path.display(),
                message
            ))
            .into());
        }
    }

    let loaded_tests = registry.get_tests();
    let total = loaded_tests.len();
    if total == 0 {
        return Err(Error::new(
            ErrorKind::NotFound,
            format!(
                "no tests were registered from `{path}`. make sure files call `test(\"name\", fn)`"
            ),
        )
        .into());
    }

    println!("running {total} test{}", if total == 1 { "" } else { "s" });

    let results = join_all(loaded_tests.into_iter().map(|test| async move {
        let started_at = Instant::now();
        let name = test.name;
        let result = test.func.call_async::<()>(()).await;
        let duration_ms = started_at.elapsed().as_millis();
        (name, result, duration_ms)
    }))
    .await;

    let mut passed = 0usize;
    let mut failed = 0usize;
    let mut failed_tests = Vec::new();

    for (index, (name, result, duration_ms)) in results.into_iter().enumerate() {
        match result {
            Ok(_) => {
                passed += 1;
                println!(
                    "[{}/{}] PASS {} ({} ms)",
                    index + 1,
                    total,
                    name,
                    duration_ms
                );
            }
            Err(e) => {
                failed += 1;
                failed_tests.push(name.clone());
                let err = extract_lua_error(e);
                println!(
                    "[{}/{}] FAIL {} ({} ms)",
                    index + 1,
                    total,
                    name,
                    duration_ms
                );
                println!("  error:");
                for line in err.lines() {
                    println!("    {line}");
                }
                println!("  end error");
            }
        }
    }

    let suite_duration_ms = suite_start.elapsed().as_millis();
    println!();
    println!(
        "summary: total={} passed={} failed={} duration={} ms",
        total, passed, failed, suite_duration_ms
    );
    let had_failures = !failed_tests.is_empty();
    if !failed_tests.is_empty() {
        println!("failed tests:");
        for name in failed_tests {
            println!("  - {name}");
        }
    }
    if let Some(message) = update_available_message().await {
        println!("{message}");
    }
    if had_failures {
        return Err(Error::other(format!("{failed}/{total} test(s) failed")).into());
    }

    Ok(())
}