forge_test_harness/
lib.rs1#![forbid(unsafe_code)]
33
34use std::path::{Path, PathBuf};
35use std::process::Command;
36
37use forge_host::{Engine, GenerationOutput, Limits, Plugin, StageError, TransformOutput};
38use forge_ir::Ir;
39
40#[derive(Debug, thiserror::Error)]
49pub enum HarnessError {
50 #[error("failed to build plugin: {0}")]
51 Build(String),
52 #[error("plugin io: {0}")]
53 Io(#[from] std::io::Error),
54 #[error("could not locate built .wasm under {0:?}")]
55 NotFound(PathBuf),
56 #[error("engine init: {0}")]
57 Engine(String),
58 #[error("plugin load: {0}")]
59 Load(String),
60}
61
62#[derive(Debug)]
64pub struct PluginRunner {
65 engine: Engine,
66 plugin: Plugin,
67}
68
69impl PluginRunner {
70 pub fn build_and_load(manifest_dir: impl AsRef<Path>) -> Result<Self, HarnessError> {
73 let manifest_dir = manifest_dir.as_ref();
74 build(manifest_dir)?;
75 let wasm = locate_artifact(manifest_dir)?;
76 Self::load(wasm)
77 }
78
79 pub fn load(wasm_path: impl AsRef<Path>) -> Result<Self, HarnessError> {
82 let bytes = std::fs::read(wasm_path.as_ref())?;
83 let engine = Engine::new().map_err(|e| HarnessError::Engine(e.to_string()))?;
84 match Plugin::load_transformer(&engine, &bytes) {
85 Ok(p) => Ok(Self { engine, plugin: p }),
86 Err(_) => {
87 let p = Plugin::load_generator(&engine, &bytes)
88 .map_err(|e| HarnessError::Load(e.to_string()))?;
89 Ok(Self { engine, plugin: p })
90 }
91 }
92 }
93
94 pub fn info(&self) -> &forge_ir::PluginInfo {
95 self.plugin.info()
96 }
97
98 pub fn engine(&self) -> &Engine {
99 &self.engine
100 }
101
102 pub fn plugin(&self) -> &Plugin {
103 &self.plugin
104 }
105
106 pub fn transform(
107 &self,
108 ir: Ir,
109 config: serde_json::Value,
110 ) -> Result<TransformOutput, StageError> {
111 let s = config.to_string();
112 self.plugin.transform(ir, &s, Limits::transformer())
113 }
114
115 pub fn generate(
116 &self,
117 ir: Ir,
118 config: serde_json::Value,
119 ) -> Result<GenerationOutput, StageError> {
120 let s = config.to_string();
121 self.plugin.generate(ir, &s, Limits::generator())
122 }
123}
124
125fn build(manifest_dir: &Path) -> Result<(), HarnessError> {
126 let manifest = manifest_dir.join("Cargo.toml");
127 if !manifest.exists() {
128 return Err(HarnessError::Build(format!(
129 "no Cargo.toml at {}",
130 manifest.display()
131 )));
132 }
133 let status = Command::new(env!("CARGO"))
134 .args([
135 "build",
136 "--release",
137 "--target",
138 "wasm32-wasip2",
139 "--manifest-path",
140 ])
141 .arg(&manifest)
142 .status()
143 .map_err(|e| HarnessError::Build(format!("spawn cargo: {e}")))?;
144 if !status.success() {
145 return Err(HarnessError::Build(format!(
146 "cargo build exited with {status}"
147 )));
148 }
149 Ok(())
150}
151
152fn locate_artifact(manifest_dir: &Path) -> Result<PathBuf, HarnessError> {
153 let crate_name = read_crate_name(manifest_dir)?;
154 let underscore = crate_name.replace('-', "_");
155 let mut search = manifest_dir.to_path_buf();
156 loop {
157 let candidate = search
158 .join("target")
159 .join("wasm32-wasip2")
160 .join("release")
161 .join(format!("{underscore}.wasm"));
162 if candidate.exists() {
163 return Ok(candidate);
164 }
165 let Some(parent) = search.parent() else {
166 return Err(HarnessError::NotFound(
167 manifest_dir
168 .join("target/wasm32-wasip2/release")
169 .join(format!("{underscore}.wasm")),
170 ));
171 };
172 search = parent.to_path_buf();
173 }
174}
175
176fn read_crate_name(manifest_dir: &Path) -> Result<String, HarnessError> {
177 let manifest = std::fs::read_to_string(manifest_dir.join("Cargo.toml"))?;
178 let mut in_package = false;
182 for raw in manifest.lines() {
183 let line = raw.trim();
184 if line.starts_with('#') {
185 continue;
186 }
187 if line.starts_with('[') {
188 in_package = line == "[package]";
189 continue;
190 }
191 if in_package {
192 if let Some(rest) = line.strip_prefix("name") {
193 let rest = rest.trim_start_matches(|c: char| c.is_whitespace() || c == '=');
194 if let Some(name) = rest
195 .trim()
196 .strip_prefix('"')
197 .and_then(|s| s.strip_suffix('"'))
198 {
199 return Ok(name.to_string());
200 }
201 }
202 }
203 }
204 Err(HarnessError::Build(format!(
205 "no [package] name in {}",
206 manifest_dir.join("Cargo.toml").display()
207 )))
208}