#![cfg(feature = "ctp-native")]
use ek_core::section::Configurable;
use std::path::PathBuf;
fn manifest() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn esc(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', " ")
}
struct Rec {
lines: Vec<String>,
fails: usize,
}
impl Rec {
fn new() -> Self {
Self {
lines: Vec::new(),
fails: 0,
}
}
fn add(&mut self, id: &str, ok: bool, detail: &str) {
let v = if ok { "PASS" } else { "FAIL" };
if !ok {
self.fails += 1;
}
println!("[{v}] {id} :: {detail}");
self.lines.push(format!(
r#"{{"step":"{}","observed":"{}","verdict":"{v}"}}"#,
esc(id),
esc(detail)
));
}
fn info(&mut self, id: &str, detail: &str) {
println!("[INFO] {id} :: {detail}");
self.lines.push(format!(
r#"{{"step":"{}","observed":"{}","verdict":"INFO"}}"#,
esc(id),
esc(detail)
));
}
fn flush(&self) {
let dir = manifest().join("target").join("b-mode");
let _ = std::fs::create_dir_all(&dir);
let p = dir.join("D1-cfg-paths.jsonl");
let _ = std::fs::write(&p, self.lines.join("\n") + "\n");
println!("[报表] {} 行 → {}", self.lines.len(), p.display());
}
}
fn form_like_restore(map: &ek_core::section::SectionMap) -> ek_ui::CtpForm {
let mut form = ek_ui::CtpForm {
broker_id: map.get_plain("broker_id").unwrap_or_default().to_owned(),
td_addr: map.get_plain("td_addr").unwrap_or_default().to_owned(),
td_user: map.get_plain("td_user").unwrap_or_default().to_owned(),
td_password: String::new(),
td_auth_code: map.get_plain("td_auth_code").unwrap_or_default().to_owned(),
td_app_id: map.get_plain("td_app_id").unwrap_or_default().to_owned(),
md_addr: map.get_plain("md_addr").unwrap_or_default().to_owned(),
md_user: String::new(),
md_password: String::new(),
symbols: String::new(),
accept_futures: false,
accept_options: false,
};
for c in map
.get_plain("product_classes")
.unwrap_or_default()
.split(',')
{
if c.trim() == "1" {
form.accept_futures = true;
}
}
form
}
fn classes(v: &str) -> Vec<String> {
let raw: Vec<String> = v
.split(',')
.map(str::trim)
.filter(|c| !c.is_empty())
.map(str::to_owned)
.collect();
ek_ctp::map::ProductClasses(raw).normalize()
}
#[test]
#[ignore = "需要现场档案根(EK_CFG_FALLBACK_ROOT)"]
fn d1_d2_archive_and_form_paths() {
let src_root = std::env::var("EK_CFG_FALLBACK_ROOT").expect("需要 EK_CFG_FALLBACK_ROOT");
let src_root = PathBuf::from(src_root);
let run = manifest().join("target").join("arch-d12");
let _ = std::fs::remove_dir_all(&run);
std::fs::create_dir_all(run.join("cfg")).expect("建副本目录");
std::fs::copy(
src_root
.parent()
.expect("档案根应有父目录")
.join("master.key"),
run.join("master.key"),
)
.expect("拷 master.key");
std::fs::copy(src_root.join("cfg.db"), run.join("cfg").join("cfg.db")).expect("拷 cfg.db");
unsafe {
std::env::set_var("EK_CFG_FALLBACK_ROOT", run.join("cfg"));
}
let store = ek_cfg::Store::eco().expect("档案根可用");
let mut rec = Rec::new();
let mut namespaces: Vec<String> = store
.list_namespaces()
.expect("档案清点")
.into_iter()
.filter(|ns| ns.starts_with("ctp."))
.collect();
namespaces.sort();
assert!(!namespaces.is_empty(), "档案里没有 ctp.* 段");
for ns in &namespaces {
let map = match store.load(ns) {
Ok(ek_cfg::LoadOutcome::Loaded { map, warnings }) => {
if !warnings.is_empty() {
rec.info(&format!("{ns} 载入告警"), &warnings.join(" | "));
}
map
}
Ok(ek_cfg::LoadOutcome::Fresh) => {
rec.info(&format!("{ns} 段"), "Fresh(空段,跳过)");
continue;
}
Err(e) => {
rec.add(&format!("{ns} 读段"), false, &format!("{e:?}"));
continue;
}
};
let pa = ek_ctp::CtpPlugin::new(ek_ctp::CtpConfig::default());
let rep = match pa.apply_config(&map) {
Ok(r) => r,
Err(e) => {
rec.add(&format!("{ns} apply_config"), false, &format!("{e}"));
continue;
}
};
let da = pa.dump_config();
let form = form_like_restore(&map);
let mut req = form.to_configure_req();
req.td_password = ek_cfg::SecretString::from("probe-not-a-real-password".to_string());
let pb = ek_ctp::CtpPlugin::new(ek_ctp::CtpConfig::default());
let reply = pb.configure(&req);
if !reply.ok {
rec.add(&format!("{ns} configure(表单)"), false, &reply.msg);
continue;
}
let db = pb.dump_config();
let keys = [
"broker_id",
"td_addr",
"td_user",
"td_auth_code",
"td_app_id",
"md_addr",
];
let mut diffs: Vec<String> = Vec::new();
let mut detail: Vec<String> = Vec::new();
for k in keys {
let a = da.get_plain(k).unwrap_or("");
let b = db.get_plain(k).unwrap_or("");
detail.push(format!("{k}={a:?}/{b:?}"));
if a != b {
diffs.push(format!("{k}: 档案={a:?} 表单={b:?}"));
}
}
let md_a = da.get_plain("md_user").unwrap_or("");
let md_b = db.get_plain("md_user").unwrap_or("");
let td_b = db.get_plain("td_user").unwrap_or("");
let md_eff_b = if md_b.is_empty() { td_b } else { md_b };
detail.push(format!("md_user={md_a:?}/{md_b:?}(回落 {md_eff_b:?})"));
if md_a != md_b {
if md_a == md_eff_b {
rec.info(
&format!("D1 {ns} md_user 投影差"),
&format!(
"档案={md_a:?} 表单=空 → 回落 td_user={td_b:?},**有效值相等**;\
表单不设 md 账号位是 BD-UI-62 的裁定,不是漏映射"
),
);
} else {
diffs.push(format!(
"md_user: 档案={md_a:?} 表单={md_b:?}(回落 {md_eff_b:?}) —— 有效值也不等"
));
}
}
let ca = classes(da.get_plain("product_classes").unwrap_or(""));
let cb = classes(db.get_plain("product_classes").unwrap_or(""));
detail.push(format!("product_classes={ca:?}/{cb:?}"));
if ca != cb {
diffs.push(format!(
"product_classes: 档案={ca:?}(原始 {:?}) 表单={cb:?}(原始 {:?})",
da.get_plain("product_classes"),
db.get_plain("product_classes")
));
}
rec.add(
&format!("D1 {ns} 两路字段一致"),
diffs.is_empty(),
&format!(
"{};{}",
detail.join(" "),
if diffs.is_empty() {
"无差异".to_string()
} else {
format!("差异 → {}", diffs.join(" ;; "))
}
),
);
let sa = da.get_secret_key("td_password");
let sb = db.get_secret_key("td_password");
let plain_a = da.get_plain("td_password");
let plain_b = db.get_plain("td_password");
rec.add(
&format!("D1 {ns} 密码只出引用"),
sa.is_some() && sb.is_some() && plain_a.is_none() && plain_b.is_none(),
&format!("档案引用={sa:?} 表单引用={sb:?};明文(档案/表单)=({plain_a:?},{plain_b:?})"),
);
let mut silent: Vec<String> = Vec::new();
for (k, _f) in map.iter() {
let adopted = rep.adopted.iter().any(|x| x == k);
let deferred = rep.deferred.iter().any(|x| x == k);
if !adopted && !deferred {
silent.push(k.to_string());
}
}
let mut extra: Vec<String> = Vec::new();
let original_plain = map
.iter()
.filter(|(_, f)| f.is_plain())
.map(|(k, _)| k.to_string())
.collect::<Vec<_>>();
for (k, f) in da.iter() {
if f.is_plain() && !original_plain.iter().any(|o| o == k) {
extra.push(k.to_string());
}
}
rec.add(
&format!("D2 {ns} 段键全覆盖"),
silent.is_empty() && extra.is_empty(),
&format!(
"段键 {} 个;adopted {} 个;deferred {:?};静默吞 {:?};dump 多出 {:?}",
map.len(),
rep.adopted.len(),
rep.deferred,
silent,
extra
),
);
}
rec.flush();
assert_eq!(rec.fails, 0, "D1/D2 有 {} 条 FAIL(见上)", rec.fails);
}