Skip to main content

driver_tools/subcommands/doctor/
mod.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5pub mod args;
6
7use anyhow::{Context, Result, anyhow};
8use bind::bytecode_constants::{FALSE_VAL, RawOp, RawValueType, TRUE_VAL};
9use bind::compiler::Symbol;
10use bind::compiler::symbol_table::get_deprecated_key_identifier;
11use bind::interpreter::common::{BytecodeIter, next_u8, next_u32};
12use bind::interpreter::decode_bind_rules::{DecodedCompositeBindRules, DecodedRules};
13use bind::interpreter::match_bind::{DeviceProperties, MatchBindData, PropertyKey, match_bind};
14use fidl_fuchsia_driver_development as fdd;
15use fidl_fuchsia_driver_framework as fdf;
16use fuchsia_driver_dev as fdev;
17use itertools::Itertools;
18use std::collections::{HashMap, HashSet};
19use std::io;
20
21trait DiagnosableParent {
22    fn to_properties(&self) -> DeviceProperties;
23    fn evaluate_bind_rules(&self, properties: &DeviceProperties) -> Vec<Diagnostic>;
24    fn is_fuzzy_match(&self, properties: &DeviceProperties) -> bool;
25}
26
27impl DiagnosableParent for fdf::ParentSpec {
28    fn to_properties(&self) -> DeviceProperties {
29        node_to_bind_properties(Some(&self.properties))
30    }
31    fn evaluate_bind_rules(&self, properties: &DeviceProperties) -> Vec<Diagnostic> {
32        evaluate_bind_rules(&self.bind_rules, properties)
33    }
34    fn is_fuzzy_match(&self, properties: &DeviceProperties) -> bool {
35        is_spec_fuzzy_match(&self.bind_rules, properties)
36    }
37}
38
39impl DiagnosableParent for fdf::ParentSpec2 {
40    fn to_properties(&self) -> DeviceProperties {
41        node_to_bind_properties2(Some(&self.properties))
42    }
43    fn evaluate_bind_rules(&self, properties: &DeviceProperties) -> Vec<Diagnostic> {
44        evaluate_bind_rules2(&self.bind_rules, properties)
45    }
46    fn is_fuzzy_match(&self, properties: &DeviceProperties) -> bool {
47        is_spec_fuzzy_match2(&self.bind_rules, properties)
48    }
49}
50
51pub async fn doctor(
52    cmd: args::DoctorCommand,
53    driver_dev_proxy: fdd::ManagerProxy,
54    writer: &mut dyn io::Write,
55) -> Result<()> {
56    match (cmd.driver.as_ref(), cmd.node.as_ref(), cmd.composite_node_spec.as_ref()) {
57        (Some(driver_filter), Some(node_moniker), _) => {
58            let driver = resolve_driver(driver_filter, &driver_dev_proxy, writer).await?;
59            if let Some(driver) = driver {
60                diagnose_driver_and_node(&driver, node_moniker, &driver_dev_proxy, writer).await?;
61            }
62        }
63        (None, Some(node_moniker), Some(spec_name)) => {
64            diagnose_spec_and_node(spec_name, node_moniker, &driver_dev_proxy, writer).await?;
65        }
66        (Some(driver_filter), None, Some(spec_name)) => {
67            let driver = resolve_driver(driver_filter, &driver_dev_proxy, writer).await?;
68            if let Some(driver) = driver {
69                diagnose_driver_and_spec(&driver, spec_name, &driver_dev_proxy, writer).await?;
70            }
71        }
72        (Some(driver_filter), None, None) => {
73            let driver = resolve_driver(driver_filter, &driver_dev_proxy, writer).await?;
74            if let Some(driver) = driver {
75                diagnose_driver(&driver, &driver_dev_proxy, writer).await?;
76            }
77        }
78        (None, Some(node_moniker), None) => {
79            diagnose_node(node_moniker, &driver_dev_proxy, writer).await?;
80        }
81        (None, None, Some(spec_name)) => {
82            diagnose_spec(spec_name, &driver_dev_proxy, writer).await?;
83        }
84        (None, None, None) => {
85            diagnose_all(&driver_dev_proxy, writer).await?;
86        }
87    }
88
89    Ok(())
90}
91
92async fn resolve_driver(
93    filter: &str,
94    driver_dev_proxy: &fdd::ManagerProxy,
95    writer: &mut dyn io::Write,
96) -> Result<Option<fdf::DriverInfo>> {
97    let matched_drivers = fdev::get_drivers_from_query(filter, driver_dev_proxy).await?;
98
99    if matched_drivers.is_empty() {
100        writeln!(writer, "ERROR: No drivers matched the filter '{}'.", filter)?;
101        return Ok(None);
102    }
103
104    if matched_drivers.len() > 1 {
105        if let Some(exact) = matched_drivers.iter().find(|d| d.url.as_deref() == Some(filter)) {
106            return Ok(Some(exact.clone()));
107        }
108
109        writeln!(writer, "ERROR: Multiple drivers matched the filter '{}':", filter)?;
110        for driver in &matched_drivers {
111            writeln!(writer, "  {}", driver.url.as_deref().unwrap_or("unknown"))?;
112        }
113        writeln!(writer, "Please provide a more specific filter.")?;
114        return Ok(None);
115    }
116
117    Ok(Some(matched_drivers[0].clone()))
118}
119
120async fn diagnose_driver_and_node(
121    driver: &fdf::DriverInfo,
122    node_moniker: &str,
123    driver_dev_proxy: &fdd::ManagerProxy,
124    writer: &mut dyn io::Write,
125) -> Result<()> {
126    let driver_url = driver.url.as_deref().unwrap_or("unknown");
127    writeln!(writer, "Diagnosing driver {} against node {}", driver_url, node_moniker)?;
128    let nodes = fdev::get_device_info(driver_dev_proxy, &[node_moniker.to_string()], true)
129        .await
130        .context("Failed to find node")?;
131    if nodes.is_empty() {
132        return Err(anyhow!("Node not found: {}", node_moniker));
133    }
134    let node = &nodes[0];
135
136    if let Some(bytecode) = &driver.bind_rules_bytecode {
137        match DecodedRules::new(bytecode.clone())? {
138            DecodedRules::Normal(rules) => {
139                let properties = node_to_bind_properties(node.node_property_list.as_deref());
140                if match_bind(
141                    MatchBindData {
142                        symbol_table: &rules.symbol_table,
143                        instructions: &rules.instructions,
144                    },
145                    &properties,
146                )
147                .unwrap_or(false)
148                {
149                    writeln!(writer, "  Matches!")?;
150                } else {
151                    let diagnostics = evaluate_rules_bytecode(
152                        &rules.symbol_table,
153                        &rules.instructions,
154                        &properties,
155                    );
156                    report_diagnostics(&diagnostics, writer, 2)?;
157                }
158            }
159            DecodedRules::Composite(_) => {
160                writeln!(
161                    writer,
162                    "Driver is a composite driver. Use --composite-node-spec to diagnose if you know which spec it should match."
163                )?;
164            }
165        }
166    } else {
167        writeln!(writer, "Driver has no bind rules bytecode.")?;
168    }
169
170    Ok(())
171}
172
173async fn diagnose_spec_and_node(
174    spec_name: &str,
175    node_moniker: &str,
176    driver_dev_proxy: &fdd::ManagerProxy,
177    writer: &mut dyn io::Write,
178) -> Result<()> {
179    writeln!(writer, "Diagnosing composite node spec {} against node {}", spec_name, node_moniker)?;
180    let all_specs = fdev::get_composite_node_specs(driver_dev_proxy, None)
181        .await
182        .context("Failed to get composite node specs")?;
183    let spec = all_specs
184        .iter()
185        .find(|s| {
186            s.spec
187                .as_ref()
188                .and_then(|spec| spec.name.as_ref())
189                .map(|name| name == spec_name)
190                .unwrap_or(false)
191        })
192        .ok_or_else(|| anyhow!("Spec not found: {}", spec_name))?;
193
194    let nodes = fdev::get_device_info(driver_dev_proxy, &[node_moniker.to_string()], true)
195        .await
196        .context("Failed to find node")?;
197    if nodes.is_empty() {
198        return Err(anyhow!("Node not found: {}", node_moniker));
199    }
200    let node = &nodes[0];
201    let node_properties = node_to_bind_properties(node.node_property_list.as_deref());
202
203    if let Some(spec) = &spec.spec {
204        if let Some(parents) = &spec.parents {
205            for (i, parent) in parents.iter().enumerate() {
206                writeln!(writer, "\nComparing against spec parent {}:", i)?;
207                let diagnostics = parent.evaluate_bind_rules(&node_properties);
208                report_diagnostics(&diagnostics, writer, 2)?;
209            }
210        } else if let Some(parents2) = &spec.parents2 {
211            for (i, parent) in parents2.iter().enumerate() {
212                writeln!(writer, "\nComparing against spec parent {}:", i)?;
213                let diagnostics = parent.evaluate_bind_rules(&node_properties);
214                report_diagnostics(&diagnostics, writer, 2)?;
215            }
216        }
217    }
218
219    Ok(())
220}
221
222async fn diagnose_driver_and_spec(
223    driver: &fdf::DriverInfo,
224    spec_name: &str,
225    driver_dev_proxy: &fdd::ManagerProxy,
226    writer: &mut dyn io::Write,
227) -> Result<()> {
228    let driver_url = driver.url.as_deref().unwrap_or("unknown");
229    writeln!(writer, "Diagnosing driver {} against composite node spec {}", driver_url, spec_name)?;
230
231    let all_specs = fdev::get_composite_node_specs(driver_dev_proxy, None)
232        .await
233        .context("Failed to get composite node specs")?;
234
235    let spec = all_specs
236        .iter()
237        .find(|s| {
238            s.spec
239                .as_ref()
240                .and_then(|spec| spec.name.as_ref())
241                .map(|name| name == spec_name)
242                .unwrap_or(false)
243        })
244        .ok_or_else(|| anyhow!("Spec not found: {}", spec_name))?;
245
246    if let Some(bytecode) = &driver.bind_rules_bytecode {
247        match DecodedRules::new(bytecode.clone())? {
248            DecodedRules::Normal(_) => {
249                writeln!(
250                    writer,
251                    "Driver is NOT a composite driver, but it is being compared against a composite node spec."
252                )?;
253            }
254            DecodedRules::Composite(rules) => {
255                if let Some(spec) = &spec.spec {
256                    if let Some(parents) = &spec.parents {
257                        diagnose_composite_match(&rules, parents, writer)?;
258                    } else if let Some(parents2) = &spec.parents2 {
259                        diagnose_composite_match(&rules, parents2, writer)?;
260                    } else {
261                        writeln!(writer, "  Spec '{}' has no parents.", spec_name)?;
262                    }
263                } else {
264                    writeln!(writer, "  Spec '{}' is missing detailed information.", spec_name)?;
265                }
266            }
267        }
268    } else {
269        writeln!(writer, "  Driver '{}' has no bind rules bytecode.", driver_url)?;
270    }
271
272    Ok(())
273}
274
275fn diagnose_composite_match<P: DiagnosableParent>(
276    rules: &DecodedCompositeBindRules,
277    parents: &[P],
278    writer: &mut dyn io::Write,
279) -> Result<()> {
280    if parents.is_empty() {
281        writeln!(writer, "  Spec has no parents.")?;
282        return Ok(());
283    }
284
285    let mut matched_parents = HashSet::new();
286
287    writeln!(writer, "\nMatching primary node...")?;
288
289    let mut primary_matched = false;
290    for (p_idx, parent) in parents.iter().enumerate() {
291        let props = parent.to_properties();
292        if match_bind(
293            MatchBindData {
294                symbol_table: &rules.symbol_table,
295                instructions: &rules.primary_node.instructions,
296            },
297            &props,
298        )
299        .unwrap_or(false)
300        {
301            writeln!(writer, "  Primary node matches parent {}.", p_idx)?;
302            primary_matched = true;
303            matched_parents.insert(p_idx);
304            break;
305        }
306    }
307
308    if !primary_matched {
309        writeln!(
310            writer,
311            "  ERROR: Primary node matches no parent in spec. Mismatches against parent 0:"
312        )?;
313        let primary_props = parents[0].to_properties();
314        let diagnostics = evaluate_rules_bytecode(
315            &rules.symbol_table,
316            &rules.primary_node.instructions,
317            &primary_props,
318        );
319        report_diagnostics(&diagnostics, writer, 4)?;
320    }
321
322    for (i, node) in rules.additional_nodes.iter().enumerate() {
323        let node_name = rules
324            .symbol_table
325            .get(&node.name_id)
326            .cloned()
327            .unwrap_or_else(|| format!("additional_{}", i));
328        writeln!(writer, "\nMatching additional node {} ({})...", i, node_name)?;
329
330        let mut matched = false;
331        for (p_idx, parent) in parents.iter().enumerate() {
332            if matched_parents.contains(&p_idx) {
333                continue;
334            }
335            let props = parent.to_properties();
336            if match_bind(
337                MatchBindData {
338                    symbol_table: &rules.symbol_table,
339                    instructions: &node.instructions,
340                },
341                &props,
342            )
343            .unwrap_or(false)
344            {
345                writeln!(writer, "  Matches parent {}.", p_idx)?;
346                matched = true;
347                matched_parents.insert(p_idx);
348                break;
349            }
350        }
351
352        if !matched {
353            writeln!(writer, "  ERROR: No parent in spec matches additional node {}.", node_name)?;
354
355            let mut best_parent = None;
356            let mut best_score = 0;
357            let driver_keys = get_keys_from_instructions(&node.decoded_instructions);
358
359            for (p_idx, parent) in parents.iter().enumerate() {
360                if matched_parents.contains(&p_idx) {
361                    continue;
362                }
363                let props = parent.to_properties();
364                let score = driver_keys.iter().filter(|k| props.contains_key(k)).count();
365                if score >= best_score {
366                    best_score = score;
367                    best_parent = Some(p_idx);
368                }
369            }
370
371            if let Some(p_idx) = best_parent {
372                writeln!(writer, "  Mismatches against parent {}:", p_idx)?;
373                let props = parents[p_idx].to_properties();
374                let diags =
375                    evaluate_rules_bytecode(&rules.symbol_table, &node.instructions, &props);
376                report_diagnostics(&diags, writer, 4)?;
377            } else {
378                writeln!(
379                    writer,
380                    "  No available parent in the spec to compare against. This can happen if the driver has more nodes than the spec has parents."
381                )?;
382            }
383        }
384    }
385    Ok(())
386}
387
388async fn diagnose_driver(
389    driver: &fdf::DriverInfo,
390    driver_dev_proxy: &fdd::ManagerProxy,
391    writer: &mut dyn io::Write,
392) -> Result<()> {
393    let driver_url = driver.url.as_deref().unwrap_or("unknown");
394    writeln!(writer, "Diagnosing driver {}", driver_url)?;
395    if let Some(bytecode) = &driver.bind_rules_bytecode {
396        let rules = DecodedRules::new(bytecode.clone())?;
397
398        match &rules {
399            DecodedRules::Normal(r) => {
400                writeln!(writer, "\nFuzzy matching against all unbound nodes...")?;
401                let nodes = fdev::get_device_info(driver_dev_proxy, &[], false).await?;
402                let unbound_nodes = nodes.into_iter().filter(|n| is_node_unbound(n)).collect_vec();
403                for node in unbound_nodes {
404                    let props = node_to_bind_properties(node.node_property_list.as_deref());
405                    if is_fuzzy_match(&rules, &props) {
406                        writeln!(
407                            writer,
408                            "\n------------------------------------------------------------\nPotential match found: node {}",
409                            node.moniker.as_deref().unwrap_or("unknown")
410                        )?;
411                        if match_bind(
412                            MatchBindData {
413                                symbol_table: &r.symbol_table,
414                                instructions: &r.instructions,
415                            },
416                            &props,
417                        )
418                        .unwrap_or(false)
419                        {
420                            writeln!(writer, "  Matches!")?;
421                        } else {
422                            let diags =
423                                evaluate_rules_bytecode(&r.symbol_table, &r.instructions, &props);
424                            report_diagnostics(&diags, writer, 2)?;
425                        }
426                    }
427                }
428            }
429            DecodedRules::Composite(r) => {
430                writeln!(writer, "\nFuzzy matching against all unmatched composite node specs...")?;
431                let specs = fdev::get_composite_node_specs(driver_dev_proxy, None).await?;
432                let unmatched_specs =
433                    specs.into_iter().filter(|s| s.matched_driver.is_none()).collect_vec();
434                for spec_info in unmatched_specs {
435                    if let Some(spec) = &spec_info.spec {
436                        let mut potential_match = false;
437                        if let Some(parents) = &spec.parents {
438                            if parents.iter().any(|p| is_fuzzy_match(&rules, &p.to_properties())) {
439                                potential_match = true;
440                            }
441                        } else if let Some(parents2) = &spec.parents2 {
442                            if parents2.iter().any(|p| is_fuzzy_match(&rules, &p.to_properties())) {
443                                potential_match = true;
444                            }
445                        }
446
447                        if potential_match {
448                            writeln!(
449                                writer,
450                                "\n------------------------------------------------------------\nPotential match found: spec {}",
451                                spec.name.as_deref().unwrap_or("unknown")
452                            )?;
453                            if let Some(parents) = &spec.parents {
454                                diagnose_composite_match(r, parents, writer)?;
455                            } else if let Some(parents2) = &spec.parents2 {
456                                diagnose_composite_match(r, parents2, writer)?;
457                            }
458                        }
459                    }
460                }
461            }
462        }
463    }
464    Ok(())
465}
466
467fn is_fuzzy_match(rules: &DecodedRules, properties: &DeviceProperties) -> bool {
468    let keys = match rules {
469        DecodedRules::Normal(r) => get_keys_from_instructions(&r.decoded_instructions),
470        DecodedRules::Composite(r) => {
471            let mut k = get_keys_from_instructions(&r.primary_node.decoded_instructions);
472            for n in &r.additional_nodes {
473                k.extend(get_keys_from_instructions(&n.decoded_instructions));
474            }
475            k
476        }
477    };
478    keys.iter().any(|k| properties.contains_key(k))
479}
480
481fn get_keys_from_instructions(
482    insts: &[bind::interpreter::instruction_decoder::DecodedInstruction],
483) -> Vec<PropertyKey> {
484    let mut keys = Vec::new();
485    for inst in insts {
486        if let bind::interpreter::instruction_decoder::DecodedInstruction::Condition(cond) = inst {
487            match &cond.lhs {
488                Symbol::NumberValue(v) => keys.push(PropertyKey::NumberKey(*v)),
489                Symbol::StringValue(v) => keys.push(PropertyKey::StringKey(v.clone())),
490                Symbol::Key(v, _) => keys.push(PropertyKey::StringKey(v.clone())),
491                _ => {}
492            }
493        }
494    }
495    keys
496}
497
498fn is_node_unbound(node: &fdd::NodeInfo) -> bool {
499    match &node.bound_driver_url {
500        None => true,
501        Some(url) => url == "unbound",
502    }
503}
504
505async fn diagnose_node_info(
506    node: &fdd::NodeInfo,
507    driver_dev_proxy: &fdd::ManagerProxy,
508    writer: &mut dyn io::Write,
509) -> Result<()> {
510    if !is_node_unbound(node) {
511        writeln!(
512            writer,
513            "  Node is bound to {}",
514            node.bound_driver_url.as_deref().unwrap_or("unknown")
515        )?;
516    } else {
517        writeln!(writer, "  Node is UNBOUND.")?;
518        if node.quarantined == Some(true) {
519            writeln!(writer, "  Node is QUARANTINED (driver failed to start). Check driver logs.")?;
520        }
521
522        let properties = node_to_bind_properties(node.node_property_list.as_deref());
523
524        writeln!(writer, "\nFuzzy matching against all drivers...")?;
525        let drivers = fdev::get_driver_info(driver_dev_proxy, &[]).await?;
526        for driver in drivers {
527            if let Some(bytecode) = &driver.bind_rules_bytecode {
528                if let Ok(rules) = DecodedRules::new(bytecode.clone()) {
529                    if is_fuzzy_match(&rules, &properties) {
530                        writeln!(
531                            writer,
532                            "\n------------------------------------------------------------\nPotential match: driver {}",
533                            driver.url.as_deref().unwrap_or("unknown")
534                        )?;
535                        match rules {
536                            DecodedRules::Normal(r) => {
537                                if match_bind(
538                                    MatchBindData {
539                                        symbol_table: &r.symbol_table,
540                                        instructions: &r.instructions,
541                                    },
542                                    &properties,
543                                )
544                                .unwrap_or(false)
545                                {
546                                    writeln!(writer, "  Matches!")?;
547                                } else {
548                                    let diags = evaluate_rules_bytecode(
549                                        &r.symbol_table,
550                                        &r.instructions,
551                                        &properties,
552                                    );
553                                    report_diagnostics(&diags, writer, 2)?;
554                                }
555                            }
556                            DecodedRules::Composite(_) => {
557                                writeln!(
558                                    writer,
559                                    "  This is a composite driver. For a detailed analysis, run `ffx driver doctor --driver {}` or use --composite-node-spec if you know which spec it should match.",
560                                    driver.url.as_deref().unwrap_or("unknown")
561                                )?;
562                            }
563                        }
564                    }
565                }
566            }
567        }
568
569        writeln!(writer, "\nFuzzy matching against all composite node specs...")?;
570        let specs = fdev::get_composite_node_specs(driver_dev_proxy, None).await?;
571        for spec_info in specs {
572            if let Some(spec) = &spec_info.spec {
573                if let Some(parents) = &spec.parents {
574                    for (i, parent) in parents.iter().enumerate() {
575                        if parent.is_fuzzy_match(&properties) {
576                            let spec_name = spec.name.as_deref().unwrap_or("unknown");
577                            writeln!(
578                                writer,
579                                "\n------------------------------------------------------------\nPotential match: spec {} parent {}",
580                                spec_name, i
581                            )?;
582                            let diags = parent.evaluate_bind_rules(&properties);
583                            report_diagnostics(&diags, writer, 2)?;
584                        }
585                    }
586                } else if let Some(parents2) = &spec.parents2 {
587                    for (i, parent) in parents2.iter().enumerate() {
588                        if parent.is_fuzzy_match(&properties) {
589                            let spec_name = spec.name.as_deref().unwrap_or("unknown");
590                            writeln!(
591                                writer,
592                                "\n------------------------------------------------------------\nPotential match: spec {} parent {}",
593                                spec_name, i
594                            )?;
595                            let diags = parent.evaluate_bind_rules(&properties);
596                            report_diagnostics(&diags, writer, 2)?;
597                        }
598                    }
599                }
600            }
601        }
602    }
603    Ok(())
604}
605
606async fn diagnose_node(
607    node_moniker: &str,
608    driver_dev_proxy: &fdd::ManagerProxy,
609    writer: &mut dyn io::Write,
610) -> Result<()> {
611    writeln!(writer, "Diagnosing node {}", node_moniker)?;
612    let nodes = fdev::get_device_info(driver_dev_proxy, &[node_moniker.to_string()], true).await?;
613    if nodes.is_empty() {
614        writeln!(writer, "  ERROR: Node not found.")?;
615        return Ok(());
616    }
617    diagnose_node_info(&nodes[0], driver_dev_proxy, writer).await
618}
619
620fn is_spec_fuzzy_match(rules: &[fdf::BindRule], properties: &DeviceProperties) -> bool {
621    rules.iter().any(|rule| {
622        let key = match &rule.key {
623            fdf::NodePropertyKey::IntValue(v) => PropertyKey::NumberKey(*v as u64),
624            fdf::NodePropertyKey::StringValue(v) => PropertyKey::StringKey(v.clone()),
625        };
626        properties.contains_key(&key)
627    })
628}
629
630fn is_spec_fuzzy_match2(rules: &[fdf::BindRule2], properties: &DeviceProperties) -> bool {
631    rules.iter().any(|rule| {
632        let key = PropertyKey::StringKey(rule.key.clone());
633        properties.contains_key(&key)
634    })
635}
636
637async fn diagnose_spec_info(
638    spec_info: &fdf::CompositeInfo,
639    driver_dev_proxy: &fdd::ManagerProxy,
640    writer: &mut dyn io::Write,
641) -> Result<()> {
642    if let Some(driver) = spec_info
643        .matched_driver
644        .as_ref()
645        .and_then(|m| m.composite_driver.as_ref())
646        .and_then(|cd| cd.driver_info.as_ref())
647        .and_then(|di| di.url.as_deref())
648    {
649        writeln!(writer, "  Spec matched to driver: {}", driver)?;
650    } else {
651        writeln!(writer, "  Spec did NOT match any driver.")?;
652        writeln!(writer, "\nFuzzy matching against all composite drivers...")?;
653        if let Some(spec) = &spec_info.spec {
654            let drivers = fdev::get_driver_info(driver_dev_proxy, &[]).await?;
655            for driver in drivers {
656                if let Some(bytecode) = &driver.bind_rules_bytecode {
657                    if let Ok(DecodedRules::Composite(rules)) = DecodedRules::new(bytecode.clone())
658                    {
659                        let mut potential_match = false;
660                        if let Some(parents) = &spec.parents {
661                            if parents.iter().any(|p| {
662                                is_fuzzy_match(
663                                    &DecodedRules::Composite(rules.clone()),
664                                    &p.to_properties(),
665                                )
666                            }) {
667                                potential_match = true;
668                            }
669                        } else if let Some(parents2) = &spec.parents2 {
670                            if parents2.iter().any(|p| {
671                                is_fuzzy_match(
672                                    &DecodedRules::Composite(rules.clone()),
673                                    &p.to_properties(),
674                                )
675                            }) {
676                                potential_match = true;
677                            }
678                        }
679
680                        if potential_match {
681                            writeln!(
682                                writer,
683                                "\n------------------------------------------------------------\nPotential match: driver {}",
684                                driver.url.as_deref().unwrap_or("unknown")
685                            )?;
686                            if let Some(parents) = &spec.parents {
687                                diagnose_composite_match(&rules, parents, writer)?;
688                            } else if let Some(parents2) = &spec.parents2 {
689                                diagnose_composite_match(&rules, parents2, writer)?;
690                            }
691                        }
692                    }
693                }
694            }
695        }
696    }
697    Ok(())
698}
699
700async fn diagnose_spec(
701    spec_name: &str,
702    driver_dev_proxy: &fdd::ManagerProxy,
703    writer: &mut dyn io::Write,
704) -> Result<()> {
705    writeln!(writer, "Diagnosing composite node spec {}", spec_name)?;
706    let specs = fdev::get_composite_node_specs(driver_dev_proxy, None).await?;
707    let spec = specs.iter().find(|s| {
708        s.spec
709            .as_ref()
710            .and_then(|spec| spec.name.as_ref())
711            .map(|name| name == spec_name)
712            .unwrap_or(false)
713    });
714
715    match spec {
716        None => writeln!(writer, "  ERROR: Composite node spec not found.")?,
717        Some(s) => {
718            diagnose_spec_info(s, driver_dev_proxy, writer).await?;
719        }
720    }
721
722    Ok(())
723}
724
725async fn diagnose_all(
726    driver_dev_proxy: &fdd::ManagerProxy,
727    writer: &mut dyn io::Write,
728) -> Result<()> {
729    writeln!(writer, "Diagnosing all unbound nodes")?;
730    let nodes = fdev::get_device_info(driver_dev_proxy, &[], false).await?;
731    let unbound_nodes = nodes.into_iter().filter(|n| is_node_unbound(n)).collect_vec();
732
733    if unbound_nodes.is_empty() {
734        writeln!(writer, "No unbound nodes found.")?;
735    } else {
736        for node in unbound_nodes {
737            writeln!(writer, "Diagnosing node {}", node.moniker.as_deref().unwrap_or("unknown"))?;
738            diagnose_node_info(&node, driver_dev_proxy, writer).await?;
739        }
740    }
741
742    writeln!(writer, "\n\nDiagnosing all unmatched composite node specs")?;
743    let specs = fdev::get_composite_node_specs(driver_dev_proxy, None).await?;
744    let unmatched_specs = specs.into_iter().filter(|s| s.matched_driver.is_none()).collect_vec();
745
746    if unmatched_specs.is_empty() {
747        writeln!(writer, "No unmatched composite node specs found.")?;
748    } else {
749        for spec in unmatched_specs {
750            let name = spec.spec.as_ref().and_then(|s| s.name.as_deref()).unwrap_or("unknown");
751            writeln!(writer, "Diagnosing composite node spec {}", name)?;
752            diagnose_spec_info(&spec, driver_dev_proxy, writer).await?;
753        }
754    }
755
756    Ok(())
757}
758
759fn node_to_bind_properties(node_props: Option<&[fdf::NodeProperty]>) -> DeviceProperties {
760    let mut props = HashMap::new();
761    if let Some(node_props) = node_props {
762        for prop in node_props {
763            let key = match &prop.key {
764                fdf::NodePropertyKey::IntValue(v) => PropertyKey::NumberKey(*v as u64),
765                fdf::NodePropertyKey::StringValue(v) => PropertyKey::StringKey(v.clone()),
766            };
767            let value = match &prop.value {
768                fdf::NodePropertyValue::IntValue(v) => Symbol::NumberValue(*v as u64),
769                fdf::NodePropertyValue::StringValue(v) => Symbol::StringValue(v.clone()),
770                fdf::NodePropertyValue::BoolValue(v) => Symbol::BoolValue(*v),
771                fdf::NodePropertyValue::EnumValue(v) => Symbol::EnumValue(v.clone()),
772                _ => continue,
773            };
774            props.insert(key, value);
775        }
776    }
777    props
778}
779
780fn node_to_bind_properties2(node_props: Option<&[fdf::NodeProperty2]>) -> DeviceProperties {
781    let mut props = HashMap::new();
782    if let Some(node_props) = node_props {
783        for prop in node_props {
784            let key = PropertyKey::StringKey(prop.key.clone());
785            let value = match &prop.value {
786                fdf::NodePropertyValue::IntValue(v) => Symbol::NumberValue(*v as u64),
787                fdf::NodePropertyValue::StringValue(v) => Symbol::StringValue(v.clone()),
788                fdf::NodePropertyValue::BoolValue(v) => Symbol::BoolValue(*v),
789                fdf::NodePropertyValue::EnumValue(v) => Symbol::EnumValue(v.clone()),
790                _ => continue,
791            };
792            props.insert(key, value);
793        }
794    }
795    props
796}
797
798#[derive(Debug)]
799enum Diagnostic {
800    Mismatch { key: PropertyKey, expected: Symbol, actual: Option<Symbol>, is_equal: bool },
801    AbortReached,
802}
803
804fn evaluate_rules_bytecode(
805    symbol_table: &HashMap<u32, String>,
806    instructions: &[u8],
807    properties: &DeviceProperties,
808) -> Vec<Diagnostic> {
809    let mut diagnostics = Vec::new();
810    let mut iter = instructions.iter();
811
812    while let Some(byte) = iter.next() {
813        let op_byte_val = *byte;
814        if op_byte_val == RawOp::EqualCondition as u8
815            || op_byte_val == RawOp::InequalCondition as u8
816        {
817            let is_equal = op_byte_val == RawOp::EqualCondition as u8;
818            let (res, diag) =
819                read_and_evaluate_values(&mut iter, symbol_table, properties, is_equal);
820            if !res {
821                if let Some(d) = diag {
822                    diagnostics.push(d);
823                }
824                break;
825            }
826        } else if op_byte_val == RawOp::Abort as u8 {
827            diagnostics.push(Diagnostic::AbortReached);
828            break;
829        } else if op_byte_val == RawOp::UnconditionalJump as u8 {
830            let offset = match next_u32(&mut iter) {
831                Ok(o) => o,
832                Err(_) => break,
833            };
834            for _ in 0..offset {
835                if iter.next().is_none() {
836                    break;
837                }
838            }
839            if iter.next() != Some(&(RawOp::JumpLandPad as u8)) {
840                break;
841            }
842        } else if op_byte_val == RawOp::JumpIfEqual as u8
843            || op_byte_val == RawOp::JumpIfNotEqual as u8
844        {
845            let is_equal = op_byte_val == RawOp::JumpIfEqual as u8;
846            let offset = match next_u32(&mut iter) {
847                Ok(o) => o,
848                Err(_) => break,
849            };
850            let (res, _) = read_and_evaluate_values(&mut iter, symbol_table, properties, is_equal);
851            if res {
852                for _ in 0..offset {
853                    if iter.next().is_none() {
854                        break;
855                    }
856                }
857                if iter.next() != Some(&(RawOp::JumpLandPad as u8)) {
858                    break;
859                }
860            }
861        } else if op_byte_val == RawOp::JumpLandPad as u8 {
862            // Nothing to do
863        } else {
864            // Unknown opcode, stop
865            break;
866        }
867    }
868    diagnostics
869}
870
871fn read_and_evaluate_values(
872    iter: &mut BytecodeIter<'_>,
873    symbol_table: &HashMap<u32, String>,
874    properties: &DeviceProperties,
875    is_equal: bool,
876) -> (bool, Option<Diagnostic>) {
877    let lhs = match read_next_value(iter, symbol_table) {
878        Ok(v) => v,
879        Err(_) => return (false, None),
880    };
881    let rhs = match read_next_value(iter, symbol_table) {
882        Ok(v) => v,
883        Err(_) => return (false, None),
884    };
885
886    let key = match lhs {
887        Symbol::NumberValue(v) => PropertyKey::NumberKey(v),
888        Symbol::StringValue(v) => PropertyKey::StringKey(v),
889        Symbol::Key(v, _) => PropertyKey::StringKey(v),
890        _ => return (false, None),
891    };
892
893    let actual = properties.get(&key).cloned();
894    let mut effective_actual = actual;
895    if effective_actual.is_none() {
896        if let PropertyKey::NumberKey(int_key) = &key {
897            if let Some(str_key) = get_deprecated_key_identifier(*int_key as u32) {
898                effective_actual = properties.get(&PropertyKey::StringKey(str_key)).cloned();
899            }
900        }
901    }
902
903    let matches = match &effective_actual {
904        Some(val) => {
905            let mut val_for_compare = val.clone();
906            if let Symbol::EnumValue(v) = val {
907                val_for_compare = Symbol::StringValue(v.clone());
908            }
909            let mut rhs_for_compare = rhs.clone();
910            if let Symbol::EnumValue(v) = &rhs {
911                rhs_for_compare = Symbol::StringValue(v.clone());
912            }
913
914            val_for_compare == rhs_for_compare
915        }
916        None => &bind::compiler::Symbol::NumberValue(0) == &rhs,
917    };
918
919    if matches == is_equal {
920        (true, None)
921    } else {
922        (
923            false,
924            Some(Diagnostic::Mismatch { key, expected: rhs, actual: effective_actual, is_equal }),
925        )
926    }
927}
928
929fn read_next_value(
930    iter: &mut BytecodeIter<'_>,
931    symbol_table: &HashMap<u32, String>,
932) -> Result<Symbol, ()> {
933    let value_type_byte = next_u8(iter).map_err(|_| ())?;
934    let value_type_val = *value_type_byte;
935    let value = next_u32(iter).map_err(|_| ())?;
936
937    if value_type_val == RawValueType::NumberValue as u8 {
938        Ok(Symbol::NumberValue(value as u64))
939    } else if value_type_val == RawValueType::Key as u8 {
940        let name = symbol_table.get(&value).ok_or(())?.clone();
941        Ok(Symbol::Key(name, bind::parser::bind_library::ValueType::Str))
942    } else if value_type_val == RawValueType::StringValue as u8 {
943        let val = symbol_table.get(&value).ok_or(())?.clone();
944        Ok(Symbol::StringValue(val))
945    } else if value_type_val == RawValueType::BoolValue as u8 {
946        match value {
947            FALSE_VAL => Ok(Symbol::BoolValue(false)),
948            TRUE_VAL => Ok(Symbol::BoolValue(true)),
949            _ => Err(()),
950        }
951    } else if value_type_val == RawValueType::EnumValue as u8 {
952        let val = symbol_table.get(&value).ok_or(())?.clone();
953        Ok(Symbol::EnumValue(val))
954    } else {
955        Err(())
956    }
957}
958
959fn evaluate_bind_rules(rules: &[fdf::BindRule], properties: &DeviceProperties) -> Vec<Diagnostic> {
960    let mut diagnostics = Vec::new();
961    for rule in rules {
962        let key = match &rule.key {
963            fdf::NodePropertyKey::IntValue(v) => PropertyKey::NumberKey(*v as u64),
964            fdf::NodePropertyKey::StringValue(v) => PropertyKey::StringKey(v.clone()),
965        };
966        let actual = properties.get(&key).cloned();
967
968        let mut matched = false;
969        for val in &rule.values {
970            let symbol_val = match val {
971                fdf::NodePropertyValue::IntValue(v) => Symbol::NumberValue(*v as u64),
972                fdf::NodePropertyValue::StringValue(v) => Symbol::StringValue(v.clone()),
973                fdf::NodePropertyValue::BoolValue(v) => Symbol::BoolValue(*v),
974                fdf::NodePropertyValue::EnumValue(v) => Symbol::EnumValue(v.clone()),
975                _ => continue,
976            };
977            if actual.as_ref() == Some(&symbol_val) {
978                matched = true;
979                break;
980            }
981        }
982
983        match rule.condition {
984            fdf::Condition::Accept => {
985                if !matched {
986                    diagnostics.push(Diagnostic::Mismatch {
987                        key,
988                        expected: Symbol::StringValue("one of requested values".to_string()),
989                        actual,
990                        is_equal: true,
991                    });
992                }
993            }
994            fdf::Condition::Reject => {
995                if matched {
996                    diagnostics.push(Diagnostic::Mismatch {
997                        key,
998                        expected: Symbol::StringValue("none of requested values".to_string()),
999                        actual,
1000                        is_equal: false,
1001                    });
1002                }
1003            }
1004            _ => {}
1005        }
1006    }
1007    diagnostics
1008}
1009
1010fn evaluate_bind_rules2(
1011    rules: &[fdf::BindRule2],
1012    properties: &DeviceProperties,
1013) -> Vec<Diagnostic> {
1014    let mut diagnostics = Vec::new();
1015    for rule in rules {
1016        let key = PropertyKey::StringKey(rule.key.clone());
1017        let actual = properties.get(&key).cloned();
1018
1019        let mut matched = false;
1020        for val in &rule.values {
1021            let symbol_val = match val {
1022                fdf::NodePropertyValue::IntValue(v) => Symbol::NumberValue(*v as u64),
1023                fdf::NodePropertyValue::StringValue(v) => Symbol::StringValue(v.clone()),
1024                fdf::NodePropertyValue::BoolValue(v) => Symbol::BoolValue(*v),
1025                fdf::NodePropertyValue::EnumValue(v) => Symbol::EnumValue(v.clone()),
1026                _ => continue,
1027            };
1028            if actual.as_ref() == Some(&symbol_val) {
1029                matched = true;
1030                break;
1031            }
1032        }
1033
1034        match rule.condition {
1035            fdf::Condition::Accept => {
1036                if !matched {
1037                    diagnostics.push(Diagnostic::Mismatch {
1038                        key,
1039                        expected: Symbol::StringValue("one of requested values".to_string()),
1040                        actual,
1041                        is_equal: true,
1042                    });
1043                }
1044            }
1045            fdf::Condition::Reject => {
1046                if matched {
1047                    diagnostics.push(Diagnostic::Mismatch {
1048                        key,
1049                        expected: Symbol::StringValue("none of requested values".to_string()),
1050                        actual,
1051                        is_equal: false,
1052                    });
1053                }
1054            }
1055            _ => {}
1056        }
1057    }
1058    diagnostics
1059}
1060
1061fn report_diagnostics(
1062    diagnostics: &[Diagnostic],
1063    writer: &mut dyn io::Write,
1064    indent: usize,
1065) -> Result<()> {
1066    let indent_str = " ".repeat(indent);
1067    if diagnostics.is_empty() {
1068        writeln!(writer, "{}No issues found with direct property matching.", indent_str)?;
1069    } else {
1070        for diag in diagnostics {
1071            match diag {
1072                Diagnostic::Mismatch { key, expected, actual, is_equal } => {
1073                    let key_str = match key {
1074                        PropertyKey::NumberKey(v) => format!("{:#x}", v),
1075                        PropertyKey::StringKey(v) => v.clone(),
1076                    };
1077                    let actual_str = match actual {
1078                        Some(v) => v.to_string(),
1079                        None => "missing".to_string(),
1080                    };
1081                    if *is_equal {
1082                        writeln!(
1083                            writer,
1084                            "{}Mismatch: key {} expected {} but found {}",
1085                            indent_str, key_str, expected, actual_str
1086                        )?;
1087                    } else {
1088                        writeln!(
1089                            writer,
1090                            "{}Mismatch: key {} expected NOT {} but found {}",
1091                            indent_str, key_str, expected, actual_str
1092                        )?;
1093                    }
1094                }
1095                Diagnostic::AbortReached => {
1096                    writeln!(writer, "{}Unconditional Abort reached in bind rules.", indent_str)?;
1097                }
1098            }
1099        }
1100    }
1101    Ok(())
1102}
1103
1104#[cfg(test)]
1105mod tests {
1106    use super::*;
1107    use bind::interpreter::instruction_decoder::{DecodedCondition, DecodedInstruction};
1108    use bind::parser::bind_library::ValueType;
1109
1110    #[test]
1111    fn test_evaluate_rules_mismatch() {
1112        let symbol_table = HashMap::new();
1113        let mut instructions = vec![RawOp::EqualCondition as u8];
1114        instructions.push(RawValueType::NumberValue as u8);
1115        instructions.extend_from_slice(&1u32.to_le_bytes());
1116        instructions.push(RawValueType::NumberValue as u8);
1117        instructions.extend_from_slice(&100u32.to_le_bytes());
1118
1119        let mut properties = HashMap::new();
1120        properties.insert(PropertyKey::NumberKey(1), Symbol::NumberValue(200));
1121
1122        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1123        assert_eq!(diagnostics.len(), 1);
1124        match &diagnostics[0] {
1125            Diagnostic::Mismatch { key, expected, actual, is_equal } => {
1126                assert_eq!(key, &PropertyKey::NumberKey(1));
1127                assert_eq!(expected, &Symbol::NumberValue(100));
1128                assert_eq!(actual, &Some(Symbol::NumberValue(200)));
1129                assert!(is_equal);
1130            }
1131            _ => panic!("Expected Mismatch"),
1132        }
1133    }
1134
1135    #[test]
1136    fn test_evaluate_rules_match() {
1137        let symbol_table = HashMap::new();
1138        let mut instructions = vec![RawOp::EqualCondition as u8];
1139        instructions.push(RawValueType::NumberValue as u8);
1140        instructions.extend_from_slice(&1u32.to_le_bytes());
1141        instructions.push(RawValueType::NumberValue as u8);
1142        instructions.extend_from_slice(&100u32.to_le_bytes());
1143
1144        let mut properties = HashMap::new();
1145        properties.insert(PropertyKey::NumberKey(1), Symbol::NumberValue(100));
1146
1147        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1148        assert_eq!(diagnostics.len(), 0);
1149    }
1150
1151    #[test]
1152    fn test_evaluate_rules_missing_property() {
1153        let mut symbol_table = HashMap::new();
1154        symbol_table.insert(1, "key1".to_string());
1155
1156        // IF key1 == 0 (should match if missing)
1157        let mut instructions = vec![RawOp::EqualCondition as u8];
1158        instructions.push(RawValueType::StringValue as u8);
1159        instructions.extend_from_slice(&1u32.to_le_bytes());
1160        instructions.push(RawValueType::NumberValue as u8);
1161        instructions.extend_from_slice(&0u32.to_le_bytes());
1162
1163        let properties = HashMap::new();
1164
1165        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1166        assert_eq!(diagnostics.len(), 0);
1167
1168        // IF key1 != 0 (should mismatch if missing)
1169        let mut instructions = vec![RawOp::InequalCondition as u8];
1170        instructions.push(RawValueType::StringValue as u8);
1171        instructions.extend_from_slice(&1u32.to_le_bytes());
1172        instructions.push(RawValueType::NumberValue as u8);
1173        instructions.extend_from_slice(&0u32.to_le_bytes());
1174
1175        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1176        assert_eq!(diagnostics.len(), 1);
1177        match &diagnostics[0] {
1178            Diagnostic::Mismatch { key, expected, actual, is_equal } => {
1179                assert_eq!(key, &PropertyKey::StringKey("key1".to_string()));
1180                assert_eq!(expected, &Symbol::NumberValue(0));
1181                assert_eq!(actual, &None);
1182                assert!(!is_equal);
1183            }
1184            _ => panic!("Expected Mismatch"),
1185        }
1186    }
1187
1188    #[test]
1189    fn test_evaluate_rules_control_flow() {
1190        let mut symbol_table = HashMap::new();
1191        symbol_table.insert(1, "protocol".to_string());
1192        symbol_table.insert(2, "vendor_id".to_string());
1193
1194        // if protocol == PCI { vendor_id == GOOGLE }
1195        // PCI = 10, GOOGLE = 0x1234
1196        // Bytecode:
1197        // JumpIfNotEqual(protocol, 10, target)
1198        // EqualCondition(vendor_id, 0x1234)
1199        // JumpLandPad(target)
1200
1201        let mut instructions = vec![RawOp::JumpIfNotEqual as u8];
1202        instructions.extend_from_slice(&11u32.to_le_bytes()); // Offset
1203        instructions.push(RawValueType::StringValue as u8);
1204        instructions.extend_from_slice(&1u32.to_le_bytes());
1205        instructions.push(RawValueType::NumberValue as u8);
1206        instructions.extend_from_slice(&10u32.to_le_bytes());
1207
1208        instructions.push(RawOp::EqualCondition as u8);
1209        instructions.push(RawValueType::StringValue as u8);
1210        instructions.extend_from_slice(&2u32.to_le_bytes());
1211        instructions.push(RawValueType::NumberValue as u8);
1212        instructions.extend_from_slice(&0x1234u32.to_le_bytes());
1213
1214        instructions.push(RawOp::JumpLandPad as u8);
1215
1216        // Case 1: protocol is USB (20). Should jump over vendor_id check.
1217        let mut properties = HashMap::new();
1218        properties.insert(PropertyKey::StringKey("protocol".to_string()), Symbol::NumberValue(20));
1219        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1220        assert_eq!(diagnostics.len(), 0);
1221
1222        // Case 2: protocol is PCI (10). Should NOT jump, and report vendor_id mismatch if it's wrong.
1223        let mut properties = HashMap::new();
1224        properties.insert(PropertyKey::StringKey("protocol".to_string()), Symbol::NumberValue(10));
1225        properties
1226            .insert(PropertyKey::StringKey("vendor_id".to_string()), Symbol::NumberValue(0x5678));
1227        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1228        assert_eq!(diagnostics.len(), 1);
1229        match &diagnostics[0] {
1230            Diagnostic::Mismatch { key, .. } => {
1231                assert_eq!(key, &PropertyKey::StringKey("vendor_id".to_string()));
1232            }
1233            _ => panic!("Expected vendor_id mismatch"),
1234        }
1235    }
1236
1237    #[test]
1238    fn test_evaluate_rules_abort() {
1239        let symbol_table = HashMap::new();
1240        let instructions = vec![RawOp::Abort as u8];
1241        let properties = HashMap::new();
1242
1243        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1244        assert_eq!(diagnostics.len(), 1);
1245        match &diagnostics[0] {
1246            Diagnostic::AbortReached => {}
1247            _ => panic!("Expected AbortReached"),
1248        }
1249    }
1250
1251    #[test]
1252    fn test_evaluate_rules_deprecated_key() {
1253        let symbol_table = HashMap::new();
1254        // fuchsia.BIND_PROTOCOL is 0x0001
1255        let mut instructions = vec![RawOp::EqualCondition as u8];
1256        instructions.push(RawValueType::NumberValue as u8);
1257        instructions.extend_from_slice(&1u32.to_le_bytes());
1258        instructions.push(RawValueType::NumberValue as u8);
1259        instructions.extend_from_slice(&85u32.to_le_bytes());
1260
1261        let mut properties = HashMap::new();
1262        properties.insert(
1263            PropertyKey::StringKey("fuchsia.BIND_PROTOCOL".to_string()),
1264            Symbol::NumberValue(85),
1265        );
1266
1267        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1268        assert_eq!(diagnostics.len(), 0);
1269    }
1270
1271    #[test]
1272    fn test_evaluate_bind_rules_mismatch() {
1273        let rules = vec![fdf::BindRule {
1274            key: fdf::NodePropertyKey::IntValue(1),
1275            condition: fdf::Condition::Accept,
1276            values: vec![fdf::NodePropertyValue::IntValue(100)],
1277        }];
1278        let mut properties = HashMap::new();
1279        properties.insert(PropertyKey::NumberKey(1), Symbol::NumberValue(200));
1280
1281        let diagnostics = evaluate_bind_rules(&rules, &properties);
1282        assert_eq!(diagnostics.len(), 1);
1283    }
1284
1285    #[test]
1286    fn test_evaluate_bind_rules2_mismatch() {
1287        let rules = vec![fdf::BindRule2 {
1288            key: "key1".to_string(),
1289            condition: fdf::Condition::Accept,
1290            values: vec![fdf::NodePropertyValue::IntValue(100)],
1291        }];
1292        let mut properties = HashMap::new();
1293        properties.insert(PropertyKey::StringKey("key1".to_string()), Symbol::NumberValue(200));
1294
1295        let diagnostics = evaluate_bind_rules2(&rules, &properties);
1296        assert_eq!(diagnostics.len(), 1);
1297        match &diagnostics[0] {
1298            Diagnostic::Mismatch { key, expected: _, actual, is_equal: _ } => {
1299                assert_eq!(key, &PropertyKey::StringKey("key1".to_string()));
1300                assert_eq!(actual, &Some(Symbol::NumberValue(200)));
1301            }
1302            _ => panic!("Expected Mismatch"),
1303        }
1304    }
1305
1306    #[test]
1307    fn test_is_fuzzy_match() {
1308        let mut symbol_table = HashMap::new();
1309        symbol_table.insert(1, "key1".to_string());
1310        let rules = DecodedRules::Normal(bind::interpreter::decode_bind_rules::DecodedBindRules {
1311            symbol_table,
1312            instructions: vec![],
1313            decoded_instructions: vec![DecodedInstruction::Condition(DecodedCondition {
1314                lhs: Symbol::Key("key1".to_string(), ValueType::Str),
1315                rhs: Symbol::NumberValue(100),
1316                is_equal: true,
1317            })],
1318            debug_info: None,
1319        });
1320        let mut properties = HashMap::new();
1321        properties.insert(PropertyKey::StringKey("key1".to_string()), Symbol::NumberValue(100));
1322        assert!(is_fuzzy_match(&rules, &properties));
1323    }
1324
1325    #[test]
1326    fn test_get_keys_from_instructions() {
1327        let instructions = vec![
1328            DecodedInstruction::Condition(DecodedCondition {
1329                lhs: Symbol::NumberValue(1),
1330                rhs: Symbol::NumberValue(100),
1331                is_equal: true,
1332            }),
1333            DecodedInstruction::Condition(DecodedCondition {
1334                lhs: Symbol::StringValue("key_str".to_string()),
1335                rhs: Symbol::NumberValue(200),
1336                is_equal: true,
1337            }),
1338        ];
1339        let keys = get_keys_from_instructions(&instructions);
1340        assert_eq!(keys.len(), 2);
1341        assert!(keys.contains(&PropertyKey::NumberKey(1)));
1342        assert!(keys.contains(&PropertyKey::StringKey("key_str".to_string())));
1343    }
1344}