#![allow(clippy::expect_used)]
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
fn repo_root() -> PathBuf {
let manifest_dir: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest_dir
.parent()
.and_then(Path::parent)
.expect("crate manifest dir must have a grandparent (the repo root)")
.to_path_buf()
}
fn example_bundle_toml() -> PathBuf {
repo_root()
.join("examples")
.join("guests")
.join("rust")
.join("decoder")
.join("bundle.toml")
}
fn run_polyplugc(args: &[&std::ffi::OsStr]) -> std::process::Output {
let bin: &str = env!("CARGO_BIN_EXE_polyplugc");
Command::new(bin)
.args(args)
.output()
.expect("failed to spawn polyplugc binary")
}
fn canonicalize_for_toolchain(path: &Path) -> PathBuf {
let canonical: PathBuf = path.canonicalize().expect("canonicalize tempdir");
if cfg!(windows) {
let s: String = canonical.to_string_lossy().into_owned();
if let Some(rest) = s.strip_prefix(r"\\?\UNC\") {
PathBuf::from(format!(r"\\{rest}"))
} else if let Some(rest) = s.strip_prefix(r"\\?\") {
PathBuf::from(rest)
} else {
canonical
}
} else {
canonical
}
}
fn generate_into(out_dir: &Path, lang: &str) {
let output: std::process::Output = run_polyplugc(&[
"generate".as_ref(),
"--bundle".as_ref(),
example_bundle_toml().as_os_str(),
"--lang".as_ref(),
lang.as_ref(),
"--out".as_ref(),
out_dir.as_os_str(),
]);
assert!(
output.status.success(),
"polyplugc generate --lang {lang} failed (status {:?})\n--- stdout ---\n{}\n--- stderr ---\n{}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
#[test]
fn cpp_generated_glue_compiles() {
let tmp: tempfile::TempDir = tempfile::tempdir().expect("tempdir");
let project_dir: PathBuf = tmp.path().join("plugin");
let gen_dir: PathBuf = project_dir.join("gen");
std::fs::create_dir_all(&project_dir).expect("create project dir");
generate_into(&gen_dir, "cpp");
assert!(
gen_dir.join("guest/init.hpp").exists(),
"generated guest/init.hpp must exist at {}",
gen_dir.join("guest/init.hpp").display()
);
let plugin_cpp: &str = "#include \"guest/init.hpp\"\n\
#include <string>\n\
#include <algorithm>\n\
\n\
namespace polyplug_plugin {\n\
class DecoderImpl : public PipelineDecoderGuestContract {\n\
public:\n\
explicit DecoderImpl(const HostApi* host) : host_(host) {}\n\
StringView decode(StringView input) override {\n\
std::string s = polyplug::abi::to_string(input);\n\
std::replace(s.begin(), s.end(), ',', '|');\n\
return polyplug::alloc_string(host_, \"DECODED:\" + s);\n\
}\n\
private:\n\
const HostApi* host_;\n\
};\n\
PipelineDecoderGuestContract* polyplug_create_decoder(const HostApi* host) { return new DecoderImpl(host); }\n\
} // namespace polyplug_plugin\n";
let plugin_src: PathBuf = project_dir.join("plugin.cpp");
std::fs::write(&plugin_src, plugin_cpp).expect("write plugin.cpp");
let cpp_abi_include: PathBuf = repo_root().join("sdks").join("cpp").join("abi");
let cpp_guest_include: PathBuf = repo_root().join("sdks").join("cpp").join("guest");
let out_lib: PathBuf = project_dir.join("libplugin.so");
let build: std::process::Output = Command::new("c++")
.arg("-std=c++20")
.arg("-fPIC")
.arg("-shared")
.arg("-O0")
.arg("-I")
.arg(&gen_dir)
.arg("-I")
.arg(&cpp_abi_include)
.arg("-I")
.arg(&cpp_guest_include)
.arg(&plugin_src)
.arg("-o")
.arg(&out_lib)
.output()
.expect("failed to spawn c++ compiler");
assert!(
build.status.success(),
"c++ build of generated cpp glue failed (status {:?})\n--- stdout ---\n{}\n--- stderr ---\n{}",
build.status.code(),
String::from_utf8_lossy(&build.stdout),
String::from_utf8_lossy(&build.stderr),
);
assert!(
out_lib.exists(),
"c++ build reported success but no shared library was produced at {}",
out_lib.display(),
);
}
#[test]
fn csharp_generated_glue_compiles() {
let tmp: tempfile::TempDir = tempfile::tempdir().expect("tempdir");
let tmp_root: PathBuf = canonicalize_for_toolchain(tmp.path());
let project_dir: PathBuf = tmp_root.join("plugin");
let gen_dir: PathBuf = project_dir.join("generated");
std::fs::create_dir_all(&project_dir).expect("create project dir");
generate_into(&gen_dir, "csharp");
assert!(
gen_dir.join("guest/Interfaces.cs").exists(),
"generated guest/Interfaces.cs must exist at {}",
gen_dir.join("guest/Interfaces.cs").display()
);
let guest_csproj: PathBuf = repo_root()
.join("sdks")
.join("csharp")
.join("guest")
.join("Polyplug.Guest.csproj");
assert!(
guest_csproj.exists(),
"in-tree guest SDK csproj must exist at {}",
guest_csproj.display()
);
let csproj: String = format!(
"<Project Sdk=\"Microsoft.NET.Sdk\">\n\
<PropertyGroup>\n\
<TargetFramework>net10.0</TargetFramework>\n\
<Nullable>enable</Nullable>\n\
<ImplicitUsings>enable</ImplicitUsings>\n\
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>\n\
<AssemblyName>plugin</AssemblyName>\n\
</PropertyGroup>\n\
<ItemGroup>\n\
<ProjectReference Include=\"{}\" />\n\
</ItemGroup>\n\
</Project>\n",
guest_csproj.display(),
);
std::fs::write(project_dir.join("Plugin.csproj"), csproj).expect("write Plugin.csproj");
let plugin_cs: &str = "using System.Runtime.CompilerServices;\n\
using Polyplug.Guest;\n\
using Polyplug.Abi;\n\
\n\
public sealed class DecoderPlugin : IPipelineDecoderGuestContract\n\
{\n\
private readonly IntPtr _host;\n\
\n\
public DecoderPlugin(IntPtr host)\n\
{\n\
_host = host;\n\
}\n\
\n\
public StringView Decode(StringView input)\n\
{\n\
string s = StringViewHelper.ToString(input).Replace(',', '|');\n\
return PolyplugHost.AllocString(_host, $\"DECODED:{s}\");\n\
}\n\
}\n\
\n\
public static class Registration\n\
{\n\
[ModuleInitializer]\n\
public static void Register()\n\
{\n\
DecoderInterfaces.SetDecoderFactory(host => new DecoderPlugin(host));\n\
}\n\
}\n";
std::fs::write(project_dir.join("Plugin.cs"), plugin_cs).expect("write Plugin.cs");
let out_dir: PathBuf = project_dir.join("out");
let build: std::process::Output = Command::new("dotnet")
.arg("build")
.arg(project_dir.join("Plugin.csproj"))
.arg("-c")
.arg("Release")
.arg(format!("-p:OutputPath={}", out_dir.display()))
.output()
.expect("failed to spawn dotnet build");
assert!(
build.status.success(),
"dotnet build of generated csharp glue failed (status {:?})\n--- stdout ---\n{}\n--- stderr ---\n{}",
build.status.code(),
String::from_utf8_lossy(&build.stdout),
String::from_utf8_lossy(&build.stderr),
);
let produced_assembly: bool = std::fs::read_dir(&out_dir)
.expect("output dir must exist after a successful build")
.filter_map(Result::ok)
.any(|entry: std::fs::DirEntry| entry.file_name().to_string_lossy() == "plugin.dll");
assert!(
produced_assembly,
"dotnet build succeeded but no plugin.dll was produced in {}",
out_dir.display(),
);
}