use serde::{Deserialize, Serialize};
pub const SCHEMA: &str = "synth-wcet-v1";
pub const HINTS_SCHEMA: &str = "synth-wcet-hints-v1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WcetDecline {
Loop,
Call,
Recursion,
IndirectCall,
CalleeUnbounded,
UnresolvedBranch,
LoopedExpansion,
UnsupportedCore,
UnmodeledOp,
}
impl WcetDecline {
pub fn note(&self) -> &'static str {
match self {
WcetDecline::Loop => {
"backward branch (loop) without a statically-proven trip count — \
canonical const-bound counted loops are proven automatically; \
equality-exit shapes need a verified --wcet-hints entry; \
data-dependent bounds are the scry loop-bound-inference follow-up"
}
WcetDecline::Call => {
"direct call to an external/imported/unresolvable callee with no \
per-function bound in this module — cannot compose an \
inter-procedural bound (local direct calls ARE composed, #778 phase 3)"
}
WcetDecline::Recursion => {
"cycle in the direct call graph (self- or mutual recursion) — an \
upper cycle bound cannot be composed from a recursive call graph"
}
WcetDecline::IndirectCall => {
"indirect call (Blx <reg> / call_indirect / function-pointer \
dispatch) — the callee is not statically known, cannot compose"
}
WcetDecline::CalleeUnbounded => {
"a directly-called callee is itself unbounded — the decline \
propagates up the call graph (a caller cannot be bounded while a \
callee it invokes is unbounded)"
}
WcetDecline::UnresolvedBranch => {
"residual external/unresolved label branch — direction not \
statically known, cannot prove loop-free"
}
WcetDecline::LoopedExpansion => {
"op expands to an internal runtime loop (i64 software div/rem, \
executed 64×) — straight sum would undercount"
}
WcetDecline::UnsupportedCore => {
"core class not soundly summable with a zero-wait per-op table \
(Cortex-M7 dual-issue + cache wait-states)"
}
WcetDecline::UnmodeledOp => "op not classified by the cycle model",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WcetLoopBoundSource {
Static,
HintVerified,
MaskCeiling,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WcetLoopBound {
pub head_offset: u64,
pub trip_count: u64,
pub region_instr_count: usize,
pub source: WcetLoopBoundSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hint: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WcetHintReject {
HintBelowDerivedTrip,
HintUnverifiableInduction,
HintUnknownLoop,
HintUnverifiableRecursion,
HintBelowDerivedDepth,
}
impl WcetHintReject {
pub fn note(&self) -> &'static str {
match self {
WcetHintReject::HintBelowDerivedTrip => {
"hint is below synth's derived trip count — a wrong hint; \
trusting it would emit an unsound bound"
}
WcetHintReject::HintUnverifiableInduction => {
"loop induction not verifiable against the hint (counter not \
provably monotonic toward a statically-known bound ≤ hint) — \
an unverifiable hint is never trusted into a bound"
}
WcetHintReject::HintUnknownLoop => {
"hint indexes a loop that does not exist in the final \
instruction stream"
}
WcetHintReject::HintUnverifiableRecursion => {
"recursion-depth hint not verifiable — the self-recursion is not a \
single-self-call chain whose controlling value is entry-independently \
bounded (masked-slot counter decreasing by a const step toward a base \
guard on the same masked quantity); depth is runtime-unbounded, so \
the hint is never trusted into a bound"
}
WcetHintReject::HintBelowDerivedDepth => {
"recursion-depth hint is below synth's derived maximum depth (the \
entry-independent ceiling proven from the masked-slot induction) — \
a wrong hint; trusting it would emit an unsound bound"
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WcetHintRejection {
pub loop_index: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub head_offset: Option<u64>,
pub hint: u64,
pub reason: WcetHintReject,
pub note: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WcetRecursionBound {
pub max_depth: u64,
pub frame_count: u64,
pub hint: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WcetHintKey {
pub key: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub build_local: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "kebab-case")]
pub enum WcetFunction {
Bounded {
name: String,
cycles: u64,
instr_count: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
loops: Vec<WcetLoopBound>,
#[serde(default, skip_serializing_if = "Option::is_none")]
recursion: Option<WcetRecursionBound>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
hint_rejections: Vec<WcetHintRejection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
hint_key: Option<WcetHintKey>,
},
Declined {
name: String,
reason: WcetDecline,
note: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
op: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
offset: Option<u64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
hint_rejections: Vec<WcetHintRejection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
hint_key: Option<WcetHintKey>,
},
}
impl WcetFunction {
pub fn declined(name: impl Into<String>, reason: WcetDecline) -> Self {
let note = reason.note().to_string();
WcetFunction::Declined {
name: name.into(),
reason,
note,
op: None,
offset: None,
hint_rejections: Vec::new(),
hint_key: None,
}
}
pub fn declined_at(
name: impl Into<String>,
reason: WcetDecline,
op: impl Into<String>,
offset: Option<u64>,
) -> Self {
let note = reason.note().to_string();
WcetFunction::Declined {
name: name.into(),
reason,
note,
op: Some(op.into()),
offset,
hint_rejections: Vec::new(),
hint_key: None,
}
}
pub fn declined_with_rejections(
name: impl Into<String>,
reason: WcetDecline,
hint_rejections: Vec<WcetHintRejection>,
) -> Self {
let note = reason.note().to_string();
WcetFunction::Declined {
name: name.into(),
reason,
note,
op: None,
offset: None,
hint_rejections,
hint_key: None,
}
}
pub fn name(&self) -> &str {
match self {
WcetFunction::Bounded { name, .. } | WcetFunction::Declined { name, .. } => name,
}
}
pub fn set_identity(&mut self, display_name: &str, key: &WcetHintKey) {
match self {
WcetFunction::Bounded { name, hint_key, .. }
| WcetFunction::Declined { name, hint_key, .. } => {
*name = display_name.to_string();
*hint_key = Some(key.clone());
}
}
}
}
pub fn stable_name_key(raw: &str) -> String {
let b = raw.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(b.len());
let mut i = 0;
while i < b.len() {
if b[i] == b'C' && i + 1 < b.len() && b[i + 1] == b's' {
let mut j = i + 2;
while j < b.len() && b[j].is_ascii_alphanumeric() {
j += 1;
}
if j > i + 2 && j < b.len() && b[j] == b'_' {
out.push(b'C');
i = j + 1;
continue;
}
}
out.push(b[i]);
i += 1;
}
let mut out = String::from_utf8(out).unwrap_or_else(|_| raw.to_string());
if !out.is_ascii() {
return out;
}
if out.len() >= 20 && out.ends_with('E') {
let tail = &out[out.len() - 20..out.len() - 1];
if let Some(hex) = tail.strip_prefix("17h")
&& hex.bytes().all(|c| c.is_ascii_hexdigit())
{
out.truncate(out.len() - 20);
out.push('E');
return out;
}
}
if out.len() >= 19 {
let tail = &out[out.len() - 19..];
if let Some(hex) = tail.strip_prefix("::h")
&& hex.bytes().all(|c| c.is_ascii_hexdigit())
{
out.truncate(out.len() - 19);
}
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WcetFnIdentity {
pub index: u32,
pub export_name: Option<String>,
pub debug_name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WcetKeyAssignment {
pub compile_name: String,
pub display_name: String,
pub hint_key: WcetHintKey,
pub accepted_keys: Vec<String>,
}
pub fn assign_hint_keys(fns: &[WcetFnIdentity]) -> Vec<WcetKeyAssignment> {
use std::collections::{HashMap, HashSet};
let exports: HashSet<&str> = fns
.iter()
.filter_map(|f| f.export_name.as_deref())
.collect();
let mut stripped_counts: HashMap<String, usize> = HashMap::new();
let mut raw_counts: HashMap<&str, usize> = HashMap::new();
for f in fns {
if let Some(d) = f.debug_name.as_deref() {
*stripped_counts.entry(stable_name_key(d)).or_default() += 1;
*raw_counts.entry(d).or_default() += 1;
}
}
fns.iter()
.map(|f| {
let fallback = format!("func_{}", f.index);
let raw_ok = |d: &str| raw_counts.get(d).copied() == Some(1) && !exports.contains(d);
let (compile_name, display_name, key, build_local) =
match (&f.export_name, &f.debug_name) {
(Some(e), _) => (e.clone(), e.clone(), e.clone(), false),
(None, Some(d)) => {
let stripped = stable_name_key(d);
if stripped_counts.get(&stripped).copied() == Some(1)
&& !exports.contains(stripped.as_str())
{
(fallback.clone(), d.clone(), stripped, false)
} else if raw_ok(d) {
(fallback.clone(), d.clone(), d.clone(), true)
} else {
(fallback.clone(), d.clone(), fallback.clone(), true)
}
}
(None, None) => (fallback.clone(), fallback.clone(), fallback.clone(), true),
};
let mut accepted = vec![key.clone()];
if let Some(d) = f.debug_name.as_deref()
&& raw_ok(d)
&& !accepted.iter().any(|k| k == d)
{
accepted.push(d.to_string());
}
WcetKeyAssignment {
compile_name,
display_name,
hint_key: WcetHintKey { key, build_local },
accepted_keys: accepted,
}
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WcetHintResolution {
pub hints: WcetHints,
pub resolved: Vec<(String, String)>,
pub diagnostics: Vec<WcetHintKeyDiagnostic>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WcetHintKeyReason {
#[serde(rename = "wcet-hint-key-duplicate")]
Duplicate,
#[serde(rename = "wcet-hint-key-ambiguous")]
Ambiguous,
#[serde(rename = "wcet-hint-key-index-refused")]
IndexRefused,
#[serde(rename = "wcet-hint-key-unknown")]
Unknown,
#[serde(rename = "wcet-hint-key-skipped-function")]
SkippedFunction,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WcetHintKeyDiagnostic {
pub key: String,
pub reason: WcetHintKeyReason,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub function: Option<String>,
pub detail: String,
}
impl std::fmt::Display for WcetHintKeyDiagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.detail)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WcetResolvedHint {
pub key: String,
pub function: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WcetHintsOutcome {
pub resolved: Vec<WcetResolvedHint>,
pub diagnostics: Vec<WcetHintKeyDiagnostic>,
}
pub fn resolve_hint_keys(
hints: WcetHints,
assignments: &[WcetKeyAssignment],
) -> WcetHintResolution {
use std::collections::HashMap;
let mut by_key: HashMap<&str, Vec<usize>> = HashMap::new();
for (i, a) in assignments.iter().enumerate() {
for k in &a.accepted_keys {
let v = by_key.entry(k.as_str()).or_default();
if !v.contains(&i) {
v.push(i);
}
}
}
let mut out = WcetHints {
schema: hints.schema,
functions: std::collections::BTreeMap::new(),
};
let mut resolved: Vec<(String, String)> = Vec::new();
let mut diagnostics: Vec<WcetHintKeyDiagnostic> = Vec::new();
for (k, entry) in hints.functions {
match by_key.get(k.as_str()).map(Vec::as_slice) {
Some([i]) => {
let a = &assignments[*i];
if out.functions.contains_key(&a.compile_name) {
diagnostics.push(WcetHintKeyDiagnostic {
reason: WcetHintKeyReason::Duplicate,
function: Some(a.display_name.clone()),
detail: format!(
"--wcet-hints key '{k}' duplicates an earlier entry for function \
'{}' — this entry was not consumed (wcet-hint-key-duplicate, #1063)",
a.display_name
),
key: k,
});
} else {
out.functions.insert(a.compile_name.clone(), entry);
resolved.push((k, a.compile_name.clone()));
}
}
Some(many) => diagnostics.push(WcetHintKeyDiagnostic {
reason: WcetHintKeyReason::Ambiguous,
function: None,
detail: format!(
"--wcet-hints key '{k}' is AMBIGUOUS in this module ({} functions accept \
it) — the hint was not consumed (wcet-hint-key-ambiguous, #1063)",
many.len()
),
key: k,
}),
None => {
if let Some(a) = assignments
.iter()
.find(|a| a.compile_name == k && !a.accepted_keys.contains(&k))
{
diagnostics.push(WcetHintKeyDiagnostic {
reason: WcetHintKeyReason::IndexRefused,
function: Some(a.display_name.clone()),
detail: format!(
"--wcet-hints key '{k}' is an INDEX key, but that function carries \
the name '{}' — an index is not an identity (it silently retargets \
when the index space shifts), so it is refused; key the hint on \
'{}' instead (wcet-hint-key-index-refused, #1063)",
a.display_name, a.hint_key.key
),
key: k,
});
} else {
diagnostics.push(WcetHintKeyDiagnostic {
reason: WcetHintKeyReason::Unknown,
function: None,
detail: format!(
"--wcet-hints names function '{k}' which is not in this module — \
the hint was not consumed (wcet-hint-key-unknown)"
),
key: k,
});
}
}
}
}
WcetHintResolution {
hints: out,
resolved,
diagnostics,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WcetRecursionCert {
pub self_label: String,
pub max_depth: u64,
pub hint: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WcetCallSite {
pub callee_label: String,
pub multiplier: u128,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WcetDeclineSite {
pub op: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub offset: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WcetIntermediate {
Declined {
site: Option<WcetDeclineSite>,
name: String,
reason: WcetDecline,
hint_rejections: Vec<WcetHintRejection>,
},
Composable {
name: String,
own_cycles: u64,
instr_count: usize,
call_sites: Vec<WcetCallSite>,
loops: Vec<WcetLoopBound>,
recursion_cert: Option<WcetRecursionCert>,
hint_rejections: Vec<WcetHintRejection>,
},
}
impl WcetIntermediate {
pub fn name(&self) -> &str {
match self {
WcetIntermediate::Declined { name, .. } | WcetIntermediate::Composable { name, .. } => {
name
}
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WcetHints {
pub schema: String,
#[serde(default)]
pub functions: std::collections::BTreeMap<String, WcetFunctionHints>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WcetFunctionHints {
#[serde(default)]
pub loop_bounds: Vec<Option<u64>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recursion_depth: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WcetReport {
pub schema: String,
pub module: String,
pub core_class: String,
pub wait_states: u32,
pub memory_assumption: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hints: Option<WcetHintsOutcome>,
pub functions: Vec<WcetFunction>,
}
impl WcetReport {
pub fn new(module: impl Into<String>, core_class: impl Into<String>) -> Self {
WcetReport {
schema: SCHEMA.to_string(),
module: module.into(),
core_class: core_class.into(),
wait_states: 0,
memory_assumption:
"zero-wait-state instruction memory (flash accelerator / I-cache hit); \
in-order single-issue pipeline; documented per-instruction worst-case cycles"
.to_string(),
hints: None,
functions: Vec::new(),
}
}
pub fn to_json(&self) -> serde_json::Result<String> {
serde_json::to_string_pretty(self)
}
pub fn sidecar_path(output: &std::path::Path) -> std::path::PathBuf {
let mut s = output.as_os_str().to_os_string();
s.push(".wcet.json");
std::path::PathBuf::from(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bounded_and_declined_roundtrip() {
let mut r = WcetReport::new("m", "cortex-m4");
r.functions.push(WcetFunction::Bounded {
name: "leaf".into(),
cycles: 42,
instr_count: 7,
loops: Vec::new(),
recursion: None,
hint_rejections: Vec::new(),
hint_key: None,
});
r.functions
.push(WcetFunction::declined("spins", WcetDecline::Loop));
let json = r.to_json().unwrap();
let back: WcetReport = serde_json::from_str(&json).unwrap();
assert_eq!(r, back);
assert!(json.contains("\"reason\": \"loop\""));
assert!(json.contains("synth-wcet-v1"));
}
#[test]
fn sidecar_path_appends_suffix() {
let p = WcetReport::sidecar_path(std::path::Path::new("out/app.elf"));
assert_eq!(p, std::path::PathBuf::from("out/app.elf.wcet.json"));
}
#[test]
fn stable_key_strips_v0_crate_disambiguator() {
assert_eq!(
stable_name_key("_RNvCs942N1ctoMYm_4fixt12inner_eqexit"),
"_RNvC4fixt12inner_eqexit"
);
assert_eq!(
stable_name_key("_RNvNtCs942N1ctoMYm_4core3fmt3num__Cs1AbCd_5other"),
"_RNvNtC4core3fmt3num__C5other"
);
assert_eq!(
stable_name_key("_RNCNvCs942N1ctoMYm_4main4mains_0"),
"_RNCNvC4main4mains_0"
);
}
#[test]
fn stable_key_strips_legacy_hashes_and_keeps_plain_names() {
assert_eq!(
stable_name_key("_ZN4core3fmt9Formatter3pad17h2b9e27d1f4d3ba32E"),
"_ZN4core3fmt9Formatter3padE"
);
assert_eq!(
stable_name_key("core::fmt::Formatter::pad::h2b9e27d1f4d3ba32"),
"core::fmt::Formatter::pad"
);
assert_eq!(stable_name_key("memcpy"), "memcpy");
assert_eq!(stable_name_key("entry"), "entry");
}
fn idents() -> Vec<WcetFnIdentity> {
vec![
WcetFnIdentity {
index: 0,
export_name: None,
debug_name: Some("_RNvCs942N1ctoMYm_4fixt12inner_eqexit".into()),
},
WcetFnIdentity {
index: 1,
export_name: Some("entry".into()),
debug_name: Some("_RNvCs942N1ctoMYm_4fixt5entry".into()),
},
WcetFnIdentity {
index: 2,
export_name: None,
debug_name: None,
},
]
}
#[test]
fn assign_priority_export_then_stripped_then_index() {
let a = assign_hint_keys(&idents());
assert_eq!(a[0].compile_name, "func_0");
assert_eq!(a[0].display_name, "_RNvCs942N1ctoMYm_4fixt12inner_eqexit");
assert_eq!(a[0].hint_key.key, "_RNvC4fixt12inner_eqexit");
assert!(!a[0].hint_key.build_local);
assert!(
a[0].accepted_keys
.iter()
.any(|k| k == "_RNvCs942N1ctoMYm_4fixt12inner_eqexit"),
"raw name-section name must be an accepted alias"
);
assert!(
!a[0].accepted_keys.iter().any(|k| k == "func_0"),
"an index key is refused once the function carries a name"
);
assert_eq!(a[1].hint_key.key, "entry");
assert!(!a[1].hint_key.build_local);
assert_eq!(a[2].hint_key.key, "func_2");
assert!(a[2].hint_key.build_local, "an index is not an identity");
}
#[test]
fn assign_demotes_stripped_collision_to_raw_build_local() {
let fns = vec![
WcetFnIdentity {
index: 0,
export_name: None,
debug_name: Some("_RNvCsAAAA_4c3f".into()),
},
WcetFnIdentity {
index: 1,
export_name: None,
debug_name: Some("_RNvCsBBBB_4c3f".into()),
},
];
let a = assign_hint_keys(&fns);
assert_eq!(a[0].hint_key.key, "_RNvCsAAAA_4c3f");
assert!(a[0].hint_key.build_local);
assert_eq!(a[1].hint_key.key, "_RNvCsBBBB_4c3f");
assert!(a[1].hint_key.build_local);
}
#[test]
fn resolve_rekeys_and_names_every_refusal() {
let a = assign_hint_keys(&idents());
let mut h = WcetHints {
schema: HINTS_SCHEMA.into(),
functions: std::collections::BTreeMap::new(),
};
let entry = WcetFunctionHints {
loop_bounds: vec![Some(8)],
recursion_depth: None,
};
h.functions
.insert("_RNvC4fixt12inner_eqexit".into(), entry.clone());
h.functions.insert(
"_RNvCs942N1ctoMYm_4fixt12inner_eqexit".into(),
entry.clone(),
);
h.functions.insert("func_0".into(), entry.clone());
h.functions.insert("nosuch".into(), entry);
let res = resolve_hint_keys(h, &a);
assert!(res.hints.functions.contains_key("func_0"));
assert_eq!(res.resolved.len(), 1);
assert_eq!(res.diagnostics.len(), 3);
assert!(
res.diagnostics
.iter()
.any(|d| d.reason == WcetHintKeyReason::Duplicate
&& d.function.as_deref() == Some("_RNvCs942N1ctoMYm_4fixt12inner_eqexit"))
);
assert!(
res.diagnostics
.iter()
.any(|d| d.reason == WcetHintKeyReason::IndexRefused
&& d.key == "func_0"
&& d.function.as_deref() == Some("_RNvCs942N1ctoMYm_4fixt12inner_eqexit")
&& d.detail.contains("_RNvC4fixt12inner_eqexit"))
);
assert!(
res.diagnostics
.iter()
.any(|d| d.reason == WcetHintKeyReason::Unknown
&& d.key == "nosuch"
&& d.function.is_none()
&& d.detail.contains("not in this module"))
);
}
#[test]
fn hint_key_diagnostics_serialize_with_stderr_tags() {
for (r, tag) in [
(WcetHintKeyReason::Duplicate, "wcet-hint-key-duplicate"),
(WcetHintKeyReason::Ambiguous, "wcet-hint-key-ambiguous"),
(
WcetHintKeyReason::IndexRefused,
"wcet-hint-key-index-refused",
),
(WcetHintKeyReason::Unknown, "wcet-hint-key-unknown"),
(
WcetHintKeyReason::SkippedFunction,
"wcet-hint-key-skipped-function",
),
] {
assert_eq!(
serde_json::to_value(r).unwrap(),
serde_json::Value::String(tag.into())
);
}
let mut rep = WcetReport::new("m", "cortex-m4");
assert!(!rep.to_json().unwrap().contains("\"hints\""));
rep.hints = Some(WcetHintsOutcome {
resolved: vec![],
diagnostics: vec![WcetHintKeyDiagnostic {
key: "func_0".into(),
reason: WcetHintKeyReason::IndexRefused,
function: Some("real_name".into()),
detail: "refused".into(),
}],
});
let json = rep.to_json().unwrap();
let back: WcetReport = serde_json::from_str(&json).unwrap();
assert_eq!(rep, back);
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
let d = &v["hints"]["diagnostics"][0];
assert_eq!(d["reason"], "wcet-hint-key-index-refused");
rep.hints.as_mut().unwrap().diagnostics[0].function = None;
assert!(!rep.to_json().unwrap().contains("\"function\""));
}
}