#![cfg(feature = "prepare")]
use std::borrow::Cow;
use std::fs;
use std::path::{Path, PathBuf};
use blake3::Hasher;
use greentic_component::manifest::{ComponentManifest, parse_manifest};
use serde_json::{self, Value, json};
use tempfile::TempDir;
use wasm_encoder::{
CodeSection, CustomSection, ExportKind, ExportSection, Function, FunctionSection, Instruction,
Module, TypeSection,
};
use wit_component::{StringEncoding, embed_component_metadata};
use wit_parser::{Resolve, WorldId};
const WASI_MARKER: &str = "wasm32-wasip2";
fn default_input_schema() -> Value {
json!({
"type": "object",
"properties": {
"payload": { "type": "string" }
},
"required": ["payload"],
"additionalProperties": false
})
}
fn default_output_schema() -> Value {
json!({
"type": "object",
"properties": {
"message": { "type": "string" }
},
"required": ["message"],
"additionalProperties": false
})
}
#[allow(dead_code)]
pub struct TestComponent {
pub dir: TempDir,
pub wasm_path: PathBuf,
pub manifest_path: PathBuf,
pub manifest: ComponentManifest,
pub world: String,
}
impl TestComponent {
pub fn new(world_src: &str, funcs: &[&str]) -> Self {
let dir = TempDir::new().expect("tempdir");
let (wasm_bytes, world_ref) = build_module(world_src, funcs);
let wasm_path = dir.path().join("component.wasm");
fs::write(&wasm_path, &wasm_bytes).expect("write wasm");
let hash = blake3_hash(&wasm_bytes);
let world_value = world_ref.clone();
let default_operation = funcs.first().copied().unwrap_or("run");
let operations = funcs
.iter()
.map(|name| {
json!({
"name": name,
"input_schema": default_input_schema(),
"output_schema": default_output_schema()
})
})
.collect::<Vec<_>>();
let manifest_json = json!({
"id": "com.greentic.test.component",
"name": "Test Component",
"version": "0.1.0",
"world": world_value,
"describe_export": "describe",
"operations": operations,
"default_operation": default_operation,
"supports": ["messaging"],
"profiles": {
"default": "stateless",
"supported": ["stateless"]
},
"config_schema": {
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
},
"dev_flows": {
"default": {
"format": "flow-ir-json",
"graph": {
"nodes": [
{ "id": "start", "type": "start" },
{ "id": "end", "type": "end" }
],
"edges": [
{ "from": "start", "to": "end" }
]
}
}
},
"capabilities": {
"wasi": {
"filesystem": {
"mode": "none",
"mounts": []
},
"random": true,
"clocks": true
},
"host": {
"messaging": {
"inbound": true,
"outbound": true
},
"telemetry": {
"scope": "tenant"
}
}
},
"limits": {
"memory_mb": 64,
"wall_time_ms": 1000,
"fuel": 10,
"files": 2
},
"telemetry": {
"span_prefix": "test.component",
"attributes": {
"component": "test"
},
"emit_node_spans": true
},
"provenance": {
"builder": "greentic-dev",
"git_commit": "abcdef1",
"toolchain": "rustc",
"built_at_utc": "2024-01-01T00:00:00Z"
},
"artifacts": {
"component_wasm": "component.wasm"
},
"hashes": {
"component_wasm": hash
}
});
let manifest_path = dir.path().join("component.manifest.json");
fs::write(
&manifest_path,
serde_json::to_string_pretty(&manifest_json).unwrap(),
)
.expect("manifest");
let manifest = parse_manifest(&manifest_json.to_string()).expect("manifest parse");
TestComponent {
dir,
wasm_path,
manifest_path,
manifest,
world: world_ref,
}
}
}
fn build_module(world_src: &str, funcs: &[&str]) -> (Vec<u8>, String) {
let mut resolve = Resolve::default();
let pkg = resolve.push_str("test.wit", world_src).expect("push wit");
if resolve.packages[pkg].name.namespace == "root" && resolve.packages[pkg].name.name == "root" {
resolve.packages[pkg].name.namespace = "greentic".to_string();
resolve.packages[pkg].name.name = "component".to_string();
resolve.packages[pkg].name.version = None;
}
let world_name = resolve.packages[pkg]
.worlds
.keys()
.next()
.map(String::as_str)
.expect("world lookup");
let world = resolve
.select_world(&[pkg], Some(world_name))
.expect("world lookup");
let label = world_label(&resolve, world);
let mut module = Module::new();
let mut types = TypeSection::new();
types.ty().function([], []);
module.section(&types);
let mut funcs_section = FunctionSection::new();
for _ in funcs {
funcs_section.function(0);
}
module.section(&funcs_section);
let mut exports = ExportSection::new();
for (idx, name) in funcs.iter().enumerate() {
exports.export(name, ExportKind::Func, idx as u32);
}
module.section(&exports);
let mut code = CodeSection::new();
for _ in funcs {
let mut body = Function::new([]);
body.instruction(&Instruction::End);
code.function(&body);
}
module.section(&code);
module.section(&CustomSection {
name: "producers".into(),
data: Cow::Borrowed(WASI_MARKER.as_bytes()),
});
let mut wasm = module.finish();
embed_component_metadata(&mut wasm, &resolve, world, StringEncoding::UTF8)
.expect("metadata embed");
let observed = detect_world(&wasm).unwrap_or(label);
(wasm, observed)
}
fn blake3_hash(bytes: &[u8]) -> String {
let mut hasher = Hasher::new();
hasher.update(bytes);
format!("blake3:{}", hex::encode(hasher.finalize().as_bytes()))
}
#[allow(dead_code)]
pub fn write_embedded_payload(root: &Path, payload: &serde_json::Value) -> PathBuf {
let dir = root.join("schemas").join("v1");
fs::create_dir_all(&dir).expect("schema dir");
let path = dir.join("payload.json");
fs::write(&path, serde_json::to_string_pretty(payload).unwrap()).expect("payload");
path
}
fn world_label(resolve: &Resolve, world_id: WorldId) -> String {
let world = &resolve.worlds[world_id];
if let Some(pkg_id) = world.package {
let pkg = &resolve.packages[pkg_id];
if let Some(version) = &pkg.name.version {
format!(
"{}:{}/{}@{}",
pkg.name.namespace, pkg.name.name, world.name, version
)
} else {
format!("{}:{}/{}", pkg.name.namespace, pkg.name.name, world.name)
}
} else {
world.name.clone()
}
}
#[allow(dead_code)]
pub mod fixtures {
use super::*;
const FIXTURE_WIT: &str = r#"
package greentic:component@0.1.0;
world node {
export describe: func();
}
"#;
pub fn good_component() -> TestComponent {
TestComponent::new(FIXTURE_WIT, &["describe"])
}
pub fn bad_world_component() -> TestComponent {
let mut component = good_component();
let mut value: serde_json::Value = serde_json::from_str(
&fs::read_to_string(&component.manifest_path).expect("manifest read"),
)
.expect("manifest json");
value["world"] = serde_json::Value::String("greentic:component/bad@0.1.0".into());
fs::write(
&component.manifest_path,
serde_json::to_string_pretty(&value).unwrap(),
)
.expect("rewrite manifest");
component.manifest = parse_manifest(&value.to_string()).expect("parse mutated manifest");
component
}
}
fn detect_world(bytes: &[u8]) -> Option<String> {
let decoded = greentic_component::wasm::decode_world(bytes).ok()?;
Some(world_label(&decoded.resolve, decoded.world))
}