use az_device_contract_codegen::{
generate_bundle, ApiKind, ContractMethod, ContractRenderRequest, ContractService,
ContractTypedItem, GeneratedArtifact, GeneratedBundle, ReadReturnKind, RegisterArea,
TransportKind, ValueType,
};
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn should_compile_generated_c_and_rust_artifacts() -> Result<(), Box<dyn Error>> {
let request = build_sample_request();
let bundle = generate_bundle(&request)?;
let temp_root = create_temp_root("generated-artifacts-compile");
write_generated_bundle(&bundle, &temp_root)?;
compile_generated_c_sources(&temp_root)?;
compile_generated_rust_artifact(&temp_root, &bundle, "rtu")?;
compile_generated_rust_artifact(&temp_root, &bundle, "tcp")?;
Ok(())
}
fn build_sample_request() -> ContractRenderRequest {
ContractRenderRequest {
transports: vec![TransportKind::Rtu, TransportKind::Tcp],
services: vec![
ContractService {
api_kind: ApiKind::StaticRead,
interface_name: "DeviceStaticReadApi".to_string(),
interface_summary: Some("静态信息读取".to_string()),
methods: vec![ContractMethod {
summary: Some("读取设备身份".to_string()),
method_name: "getDeviceIdentity".to_string(),
read_return_kind: Some(ReadReturnKind::Dto),
read_return_type_name: Some("DeviceIdentityRegisters".to_string()),
read_area: Some(RegisterArea::HoldingRegister),
write_area: None,
read_fields: vec![
ContractTypedItem {
name: "macAddress".to_string(),
summary: Some("MAC 地址".to_string()),
value_type: ValueType::String,
},
ContractTypedItem {
name: "flashCapacityKb".to_string(),
summary: Some("Flash 容量".to_string()),
value_type: ValueType::Int,
},
],
parameters: vec![],
}],
},
ContractService {
api_kind: ApiKind::RuntimeRead,
interface_name: "DeviceReadApi".to_string(),
interface_summary: Some("运行态读取".to_string()),
methods: vec![
ContractMethod {
summary: Some("读取信号灯".to_string()),
method_name: "getSignalLights".to_string(),
read_return_kind: Some(ReadReturnKind::Dto),
read_return_type_name: Some("SignalLightsRegisters".to_string()),
read_area: Some(RegisterArea::DiscreteInput),
write_area: None,
read_fields: vec![
ContractTypedItem {
name: "ch1".to_string(),
summary: Some("第一路".to_string()),
value_type: ValueType::Boolean,
},
ContractTypedItem {
name: "ch2".to_string(),
summary: Some("第二路".to_string()),
value_type: ValueType::Boolean,
},
],
parameters: vec![],
},
ContractMethod {
summary: Some("读取输入寄存器里的布尔配置".to_string()),
method_name: "getInputRegisterFlags".to_string(),
read_return_kind: Some(ReadReturnKind::Dto),
read_return_type_name: Some("InputRegisterFlags".to_string()),
read_area: Some(RegisterArea::HoldingRegister),
write_area: None,
read_fields: vec![
ContractTypedItem {
name: "enabled".to_string(),
summary: Some("启用状态".to_string()),
value_type: ValueType::Boolean,
},
ContractTypedItem {
name: "ready".to_string(),
summary: Some("就绪状态".to_string()),
value_type: ValueType::Boolean,
},
],
parameters: vec![],
},
],
},
ContractService {
api_kind: ApiKind::Write,
interface_name: "DeviceWriteApi".to_string(),
interface_summary: Some("写操作".to_string()),
methods: vec![
ContractMethod {
summary: Some("设置故障灯".to_string()),
method_name: "writeIndicatorLights".to_string(),
read_return_kind: None,
read_return_type_name: None,
read_area: None,
write_area: Some(RegisterArea::Coil),
read_fields: vec![],
parameters: vec![
ContractTypedItem {
name: "faultLightOn".to_string(),
summary: None,
value_type: ValueType::Boolean,
},
ContractTypedItem {
name: "runLightOn".to_string(),
summary: None,
value_type: ValueType::Boolean,
},
],
},
ContractMethod {
summary: Some("写入寄存器配置".to_string()),
method_name: "writeRuntimeProfile".to_string(),
read_return_kind: None,
read_return_type_name: None,
read_area: None,
write_area: Some(RegisterArea::HoldingRegister),
read_fields: vec![],
parameters: vec![
ContractTypedItem {
name: "sampleIntervalMs".to_string(),
summary: Some("采样间隔".to_string()),
value_type: ValueType::Int,
},
ContractTypedItem {
name: "profileName".to_string(),
summary: Some("档位名称".to_string()),
value_type: ValueType::String,
},
],
},
],
},
],
}
}
fn create_temp_root(prefix: &str) -> PathBuf {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let process_id = std::process::id();
let directory = std::env::temp_dir().join(format!("{prefix}-{process_id}-{timestamp}"));
fs::create_dir_all(&directory).expect("创建临时目录失败");
directory
}
fn write_generated_bundle(bundle: &GeneratedBundle, root: &Path) -> Result<(), Box<dyn Error>> {
for artifact in &bundle.artifacts {
write_artifact(root, artifact)?;
}
Ok(())
}
fn write_artifact(root: &Path, artifact: &GeneratedArtifact) -> Result<(), Box<dyn Error>> {
let path = root.join(&artifact.relative_path);
let parent = path.parent().ok_or("生成产物缺少父目录")?;
fs::create_dir_all(parent)?;
fs::write(path, &artifact.content)?;
Ok(())
}
fn compile_generated_c_sources(root: &Path) -> Result<(), Box<dyn Error>> {
let include_root = root.join("Core/Inc/generated/modbus");
let object_root = root.join("c-objects");
fs::create_dir_all(&object_root)?;
let sources = [
root.join("Core/Src/generated/modbus/okm_modbus_dispatch.c"),
root.join("Core/Src/generated/modbus/rtu/okm_modbus_rtu.c"),
root.join("Core/Src/generated/modbus/tcp/okm_modbus_tcp.c"),
];
for source in sources {
let object_path = object_root.join(
source
.file_name()
.ok_or("C 源文件名不存在")?
.to_string_lossy()
.replace(".c", ".o"),
);
run_command(
Command::new("cc")
.arg("-std=c99")
.arg("-Wall")
.arg("-Wextra")
.arg("-I")
.arg(&include_root)
.arg("-c")
.arg(&source)
.arg("-o")
.arg(object_path),
"编译生成的 C 源文件失败",
)?;
}
Ok(())
}
fn compile_generated_rust_artifact(
root: &Path,
bundle: &GeneratedBundle,
segment: &str,
) -> Result<(), Box<dyn Error>> {
let artifact = bundle
.artifacts
.iter()
.find(|item| item.file_name == format!("okm_modbus_{segment}_tokio_client"))
.ok_or("缺少目标 Rust 产物")?;
let crate_root = root.join(format!("rust-check-{segment}"));
let source_root = crate_root.join("src");
fs::create_dir_all(&source_root)?;
fs::write(
crate_root.join("Cargo.toml"),
r#"[package]
name = "generated-rust-check"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/lib.rs"
[dependencies]
serialport = "4.8"
tokio-modbus = { version = "0.17", default-features = false, features = ["rtu-sync", "tcp-sync"] }
"#,
)?;
fs::write(source_root.join("lib.rs"), &artifact.content)?;
let target_dir = root.join("cargo-target");
run_command(
Command::new("cargo")
.arg("check")
.arg("--quiet")
.env("CARGO_TARGET_DIR", &target_dir)
.current_dir(&crate_root),
"编译生成的 Rust 产物失败",
)?;
Ok(())
}
fn run_command(command: &mut Command, error_context: &str) -> Result<(), Box<dyn Error>> {
let output = command.output()?;
if output.status.success() {
return Ok(());
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
Err(format!(
"{error_context}\nstatus: {}\nstdout:\n{}\nstderr:\n{}",
output.status, stdout, stderr
)
.into())
}