use std::collections::{BTreeMap, HashMap};
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};
use consortium_cfg::Session;
use consortium_cfg::bridge::LoweredIr;
use consortium_cfg::components::dbg::ElfLocation;
use consortium_cfg_common::config::{Codec, VisibleCore};
use proc_macro2::TokenStream;
fn ty_crate() -> syn::Path {
syn::parse_str("crate::shared").unwrap()
}
fn render_items(tokens: TokenStream) -> String {
consortium_cfg::render(tokens).expect("generated code should parse as Rust")
}
fn cores(ir: &LoweredIr) -> Vec<VisibleCore> {
let mut seen: BTreeMap<String, VisibleCore> = BTreeMap::new();
let mut has_ap = false;
for t in ir.shuhan.transceivers() {
for (core, _) in [&t.channel_primary, &t.channel_secondary] {
if *core == VisibleCore::Ap {
has_ap = true;
} else {
seen.insert(core.to_string(), *core);
}
}
}
let mut out = Vec::new();
if has_ap {
out.push(VisibleCore::Ap);
}
out.extend(seen.into_values());
out
}
fn message_types(ir: &LoweredIr) -> BTreeMap<String, Codec> {
let mut types = BTreeMap::new();
for t in ir.shuhan.transceivers() {
types.insert(t.channel_primary.1.type_name.clone(), t.codec);
types.insert(t.channel_secondary.1.type_name.clone(), t.codec);
}
types
}
fn shared_module(types: &BTreeMap<String, Codec>) -> String {
let mut out = String::from("mod shared {\n");
for (name, codec) in types {
let derives = match codec {
Codec::Postcard => "#[derive(::serde::Serialize, ::serde::Deserialize)]",
Codec::Rkyv => "#[derive(::rkyv::Archive, ::rkyv::Serialize)]",
Codec::Prost => panic!(
"trybuild stub for the prost codec is not implemented (type `{name}`); \
add a `prost::Message` stub and the `prost` codec feature if a fixture needs it"
),
};
writeln!(out, " {derives}").unwrap();
writeln!(
out,
" pub struct {name} {{\n pub _v: u32,\n }}"
)
.unwrap();
}
out.push_str("}\n");
out
}
fn ui_file(ir: &LoweredIr, core: VisibleCore, types: &BTreeMap<String, Codec>) -> String {
let mut file = String::new();
file.push_str("#![allow(dead_code, unused, unused_unsafe, non_snake_case)]\n\n");
file.push_str(&shared_module(types));
file.push('\n');
file.push_str(&render_items(ir.initialize(core, &ty_crate())));
file.push_str("\nfn main() {}\n");
file
}
fn tests_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests")
}
fn reset_dir(dir: &Path) {
fs::create_dir_all(dir).expect("create ui dir");
for entry in fs::read_dir(dir).expect("read ui dir") {
let path = entry.expect("dir entry").path();
if path.extension().is_some_and(|e| e == "rs") {
fs::remove_file(&path).expect("remove stale ui file");
}
}
}
#[test]
fn generated_init_code_compiles() {
let tests = tests_dir();
let configs = tests.join("configs");
let portable = tests.join("ui").join("portable");
reset_dir(&portable);
let linux = tests.join("ui").join("linux");
reset_dir(&linux);
let mut entries: Vec<_> = fs::read_dir(&configs)
.expect("configs dir exists")
.map(|e| e.expect("dir entry").path())
.filter(|p| p.is_dir())
.collect();
entries.sort();
assert!(!entries.is_empty(), "no config fixtures under {configs:?}");
for config in entries {
let name = config
.file_name()
.expect("config dir name")
.to_str()
.expect("utf-8 config name")
.to_string();
let manifest = config.join("Consortium.toml");
let toml = fs::read_to_string(&manifest)
.unwrap_or_else(|e| panic!("read {}: {e}", manifest.display()));
let session = Session::from_toml(&toml)
.unwrap_or_else(|e| panic!("parse {}: {e}", manifest.display()));
let mut ir = session
.lower()
.unwrap_or_else(|e| panic!("lower {}: {e}", manifest.display()));
let fixture = PathBuf::from("../../fixtures/placeholder.elf");
let resolved: HashMap<VisibleCore, ElfLocation> = ir
.dbg
.rings()
.keys()
.map(|core| (*core, ElfLocation::verbatim(fixture.clone())))
.collect();
ir.dbg = std::mem::take(&mut ir.dbg).with_resolved_elf_paths(&resolved);
let types = message_types(&ir);
for core in cores(&ir) {
let source = ui_file(&ir, core, &types);
let file_name = format!("{name}__{core}.rs");
if core == VisibleCore::Ap {
fs::write(linux.join(&file_name), source).expect("write linux ui file");
} else {
fs::write(portable.join(&file_name), source).expect("write portable ui file");
}
}
}
let t = trybuild::TestCases::new();
t.pass("tests/ui/portable/*.rs");
#[cfg(target_os = "linux")]
t.pass("tests/ui/linux/*.rs");
}