quanttide-work 0.1.0-beta.3

知识工作领域 Rust 语言工具包
Documentation
//! 契约:同一批用例向量(`../contract/*.json`),两侧各跑一遍,结论必须一样。
//!
//! 向量是两侧**共用**的同一批文件——契约写在向量里,不写在各自的测试代码里。

use quanttide_work::executor::AGENT;
use quanttide_work::workflow::Step;
use quanttide_work::{criterion, outcome, task, workflow};
use serde_json::{Value, json};
use serde_yaml::{Mapping, Value as Yaml};
use std::fs;

/// 向量文件是 JSON(夹具),工具箱的模型吃 YAML 值——JSON 本来就是合法的 YAML。
fn as_yaml(value: &Value) -> Yaml {
    serde_yaml::to_value(value).unwrap_or(Yaml::Null)
}

fn vectors() -> Vec<(String, Value)> {
    let dir = std::path::Path::new("../../tests/contract");
    let mut found: Vec<(String, Value)> = fs::read_dir(dir)
        .expect("找不到 contract 目录")
        .flatten()
        .map(|entry| entry.path())
        .filter(|path| path.extension().map(|e| e == "json").unwrap_or(false))
        .map(|path| {
            let name = path.file_name().unwrap().to_string_lossy().to_string();
            let text = fs::read_to_string(&path).expect("向量读不了");
            (
                name,
                serde_json::from_str(&text).expect("向量不是合法 JSON"),
            )
        })
        .collect();
    found.sort_by(|a, b| a.0.cmp(&b.0));
    found
}

/// 向量里的步骤名串成一条工作流(判出「走过哪几步」够用)。
fn workflow_of(steps: &Value) -> workflow::Workflow {
    workflow::Workflow {
        name: "v".to_string(),
        description: String::new(),
        steps: steps
            .as_array()
            .cloned()
            .unwrap_or_default()
            .iter()
            .filter_map(|value| value.as_str())
            .map(|name| Step {
                name: name.to_string(),
                description: String::new(),
                executor: AGENT.to_string(),
                criteria: Vec::new(),
            })
            .collect(),
    }
}

/// 向量里的流水装成一件任务。
fn task_of(events: &Value) -> task::Task {
    let mut payload = Mapping::new();
    payload.insert(
        Yaml::String("log".into()),
        Yaml::Sequence(
            events
                .as_array()
                .cloned()
                .unwrap_or_default()
                .iter()
                .map(as_yaml)
                .collect(),
        ),
    );
    task::Task::of("v", &Yaml::Mapping(payload))
}

#[test]
fn contract() {
    let vectors = vectors();
    for (name, vector) in vectors.iter() {
        let kind = vector["kind"].as_str().unwrap_or("");
        match kind {
            "validate" => {
                let file = vector["file"].as_str().unwrap_or("");
                let got = workflow::validate(&as_yaml(&vector["input"]), file);
                match vector["expect"].get("error") {
                    Some(Value::String(wanted)) => match got {
                        Ok(()) => panic!("{name}:期望报错,却通过了"),
                        Err(error) => assert_eq!(&error.0, wanted, "{name}:报错文字不一样"),
                    },
                    _ => {
                        if let Err(error) = got {
                            panic!("{name}:期望通过,却报错:{}", error.0);
                        }
                    }
                }
            }
            "items" => {
                let criteria: Vec<Value> = vector["input"].as_array().cloned().unwrap_or_default();
                let criteria: Vec<criterion::Criterion> = criteria
                    .iter()
                    .map(|value| workflow::criterion_of(&as_yaml(value)))
                    .collect();
                let got: Vec<Value> = criterion::items_of(&criteria)
                    .into_iter()
                    .map(|item| {
                        json!({
                            "description": item.description,
                            "kind": item.kind.map(|kind| format!("{kind:?}").to_lowercase()),
                            "args": item.args,
                        })
                    })
                    .collect();
                assert_eq!(
                    Value::Array(got),
                    vector["expect"],
                    "{name}:判据翻出来的不一样"
                );
            }
            "done" => {
                let got = task_of(&vector["events"]).done_steps(&workflow_of(&vector["steps"]));
                assert_eq!(json!(got), vector["expect"], "{name}:走过哪几步不一样");
            }
            "section" => {
                for case in vector["cases"].as_array().cloned().unwrap_or_default() {
                    let input = case["input"].as_str().unwrap_or("");
                    let got = workflow::looks_like_section(input);
                    assert_eq!(json!(got), case["expect"], "{name}:{input} 算不算小节名");
                }
            }
            "outcome" => {
                for case in vector["cases"].as_array().cloned().unwrap_or_default() {
                    let got = outcome::Outcome::from_json(&case["input"]).to_json();
                    let note = case["note"].as_str().unwrap_or("");
                    assert_eq!(got, case["expect"], "{name}:{note} 编解码不一样");
                }
            }
            "expand" => {
                let data = vector["data"].as_str().unwrap_or("");
                for case in vector["cases"].as_array().cloned().unwrap_or_default() {
                    let input = case["input"].as_str().unwrap_or("");
                    let got = workflow::expand_placeholders(input, data);
                    assert_eq!(json!(got), case["expect"], "{name}:{input} 展开得不对");
                }
            }
            other => panic!("{name}:不认得的向量类型 {other}"),
        }
    }
    assert!(vectors.len() >= 10, "向量太少:{}", vectors.len());
    println!("契约:{} 份向量,两侧一致", vectors.len());
}