|
| 1 | +// Copyright The OpenTelemetry Authors |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +//! End-to-end tests for the [`extension_capabilities!`] macro. |
| 5 | +//! |
| 6 | +//! Each test defines a hand-rolled test capability (the `#[capability]` |
| 7 | +//! proc macro cannot expand inside the engine crate itself — proc |
| 8 | +//! macros can't target paths in their host crate), invokes one arm of |
| 9 | +//! the macro, exercises the resulting `register_*` fn pointers against |
| 10 | +//! a real `CapabilityRegistry`, then resolves bindings and consumes the |
| 11 | +//! capability. |
| 12 | +
|
| 13 | +use super::registry::{ |
| 14 | + Capabilities, CapabilityRegistry, ConsumedTracker, LocalCapabilityEntry, |
| 15 | + SharedCapabilityEntry, resolve_bindings, |
| 16 | +}; |
| 17 | +use super::{ExtensionCapability, KNOWN_CAPABILITIES, KnownCapability}; |
| 18 | +use crate::extension::{LocalInstanceFactory, SharedInstanceFactory}; |
| 19 | +use otap_df_config::{CapabilityId, ExtensionId}; |
| 20 | +use std::any::{Any, TypeId}; |
| 21 | +use std::collections::{HashMap, HashSet}; |
| 22 | +use std::rc::Rc; |
| 23 | + |
| 24 | +// ── Test capability (mirrors what `#[capability]` generates) ───────── |
| 25 | + |
| 26 | +trait MacroTestCapLocal { |
| 27 | + fn value(&self) -> &str; |
| 28 | +} |
| 29 | +trait MacroTestCapShared: Send { |
| 30 | + fn value(&self) -> &str; |
| 31 | +} |
| 32 | + |
| 33 | +struct MacroTestCap; |
| 34 | +impl super::private::Sealed for MacroTestCap {} |
| 35 | +impl ExtensionCapability for MacroTestCap { |
| 36 | + const NAME: &'static str = "macro_test_cap"; |
| 37 | + type Local = dyn MacroTestCapLocal; |
| 38 | + type Shared = dyn MacroTestCapShared; |
| 39 | + fn wrap_shared_as_local(shared: Box<Self::Shared>) -> Rc<Self::Local> { |
| 40 | + struct Adapter(Box<dyn MacroTestCapShared>); |
| 41 | + impl MacroTestCapLocal for Adapter { |
| 42 | + fn value(&self) -> &str { |
| 43 | + self.0.value() |
| 44 | + } |
| 45 | + } |
| 46 | + Rc::new(Adapter(shared)) |
| 47 | + } |
| 48 | +} |
| 49 | + |
| 50 | +// Register the test capability so resolve_bindings sees it as known. |
| 51 | +#[allow(unsafe_code)] |
| 52 | +#[linkme::distributed_slice(KNOWN_CAPABILITIES)] |
| 53 | +#[linkme(crate = linkme)] |
| 54 | +static _MACRO_TEST_CAP: KnownCapability = KnownCapability { |
| 55 | + name: "macro_test_cap", |
| 56 | + description: "Test capability for extension_capabilities! macro tests", |
| 57 | + type_id: || TypeId::of::<MacroTestCap>(), |
| 58 | +}; |
| 59 | + |
| 60 | +// Casters mirror what `#[capability]` emits. The macro calls |
| 61 | +// `<MacroTestCap>::shared_entry::<E>` and `local_entry::<E>`. |
| 62 | +impl MacroTestCap { |
| 63 | + fn shared_entry<E>( |
| 64 | + ext_id: ExtensionId, |
| 65 | + factory: SharedInstanceFactory, |
| 66 | + ) -> SharedCapabilityEntry |
| 67 | + where |
| 68 | + E: MacroTestCapShared + 'static, |
| 69 | + { |
| 70 | + let produce = move || -> Box<dyn Any + Send> { |
| 71 | + let erased = factory.produce(); |
| 72 | + let concrete: Box<E> = erased.downcast().expect("instance factory"); |
| 73 | + let shared: Box<dyn MacroTestCapShared> = concrete; |
| 74 | + Box::new(shared) as Box<dyn Any + Send> |
| 75 | + }; |
| 76 | + let adapt_as_local: fn(Box<dyn Any + Send>) -> Rc<dyn Any> = |erased| { |
| 77 | + let shared: Box<Box<dyn MacroTestCapShared>> = |
| 78 | + erased.downcast().expect("envelope"); |
| 79 | + let rc_local = <MacroTestCap as ExtensionCapability>::wrap_shared_as_local(*shared); |
| 80 | + Rc::new(rc_local) as Rc<dyn Any> |
| 81 | + }; |
| 82 | + SharedCapabilityEntry::new(ext_id, produce, adapt_as_local) |
| 83 | + } |
| 84 | + |
| 85 | + fn local_entry<E>( |
| 86 | + ext_id: ExtensionId, |
| 87 | + factory: LocalInstanceFactory, |
| 88 | + ) -> LocalCapabilityEntry |
| 89 | + where |
| 90 | + E: MacroTestCapLocal + 'static, |
| 91 | + { |
| 92 | + let produce = move || -> Rc<dyn Any> { |
| 93 | + let erased = factory.produce(); |
| 94 | + let concrete: Rc<E> = erased.downcast().expect("instance factory"); |
| 95 | + let local: Rc<dyn MacroTestCapLocal> = concrete; |
| 96 | + Rc::new(local) as Rc<dyn Any> |
| 97 | + }; |
| 98 | + LocalCapabilityEntry::new(ext_id, produce) |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +// ── Test extension impls ───────────────────────────────────────────── |
| 103 | + |
| 104 | +#[derive(Clone)] |
| 105 | +struct Shared(&'static str); |
| 106 | +impl MacroTestCapShared for Shared { |
| 107 | + fn value(&self) -> &str { |
| 108 | + self.0 |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +struct Local(&'static str); |
| 113 | +impl MacroTestCapLocal for Local { |
| 114 | + fn value(&self) -> &str { |
| 115 | + self.0 |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +fn shared_factory(val: &'static str) -> SharedInstanceFactory { |
| 120 | + SharedInstanceFactory::new(move || Box::new(Shared(val)) as Box<dyn Any + Send>) |
| 121 | +} |
| 122 | +fn local_factory(val: &'static str) -> LocalInstanceFactory { |
| 123 | + let shared: Rc<Local> = Rc::new(Local(val)); |
| 124 | + LocalInstanceFactory::new(move || Rc::clone(&shared) as Rc<dyn Any>) |
| 125 | +} |
| 126 | + |
| 127 | +fn bindings() -> HashMap<CapabilityId, ExtensionId> { |
| 128 | + let mut m = HashMap::new(); |
| 129 | + let _ = m.insert("macro_test_cap".into(), "ext".into()); |
| 130 | + m |
| 131 | +} |
| 132 | +fn known_exts() -> HashSet<ExtensionId> { |
| 133 | + let mut s = HashSet::new(); |
| 134 | + let _ = s.insert("ext".into()); |
| 135 | + s |
| 136 | +} |
| 137 | + |
| 138 | +fn resolve(registry: &CapabilityRegistry) -> Capabilities { |
| 139 | + let mut tracker = ConsumedTracker::new(); |
| 140 | + resolve_bindings(&bindings(), registry, &known_exts(), &mut tracker) |
| 141 | + .expect("resolve_bindings") |
| 142 | +} |
| 143 | + |
| 144 | +// ── Tests ──────────────────────────────────────────────────────────── |
| 145 | + |
| 146 | +#[test] |
| 147 | +fn macro_shared_only_form() { |
| 148 | + let ec = extension_capabilities!(shared: Shared => [MacroTestCap]); |
| 149 | + assert_eq!(ec.shared, &["macro_test_cap"]); |
| 150 | + assert!(ec.local.is_empty()); |
| 151 | + |
| 152 | + let mut registry = CapabilityRegistry::new(); |
| 153 | + (ec.register_shared)("ext".into(), shared_factory("s-only"), &mut registry) |
| 154 | + .expect("register_shared"); |
| 155 | + // register_local must be a no-op even when called with a local factory. |
| 156 | + (ec.register_local)("ext".into(), local_factory("unused"), &mut registry) |
| 157 | + .expect("register_local no-op"); |
| 158 | + |
| 159 | + let caps = resolve(®istry); |
| 160 | + assert_eq!(caps.require_shared::<MacroTestCap>().unwrap().value(), "s-only"); |
| 161 | + // Local consumers are served by the SharedAsLocal fallback. |
| 162 | + assert_eq!(caps.require_local::<MacroTestCap>().unwrap().value(), "s-only"); |
| 163 | +} |
| 164 | + |
| 165 | +#[test] |
| 166 | +fn macro_local_only_form() { |
| 167 | + let ec = extension_capabilities!(local: Local => [MacroTestCap]); |
| 168 | + assert!(ec.shared.is_empty()); |
| 169 | + assert_eq!(ec.local, &["macro_test_cap"]); |
| 170 | + |
| 171 | + let mut registry = CapabilityRegistry::new(); |
| 172 | + (ec.register_local)("ext".into(), local_factory("l-only"), &mut registry) |
| 173 | + .expect("register_local"); |
| 174 | + |
| 175 | + let caps = resolve(®istry); |
| 176 | + assert_eq!(caps.require_local::<MacroTestCap>().unwrap().value(), "l-only"); |
| 177 | + assert!(caps.require_shared::<MacroTestCap>().is_err()); |
| 178 | +} |
| 179 | + |
| 180 | +#[test] |
| 181 | +fn macro_dual_form() { |
| 182 | + let ec = extension_capabilities!( |
| 183 | + (shared: Shared, local: Local) => [MacroTestCap] |
| 184 | + ); |
| 185 | + assert_eq!(ec.shared, &["macro_test_cap"]); |
| 186 | + assert_eq!(ec.local, &["macro_test_cap"]); |
| 187 | + |
| 188 | + let mut registry = CapabilityRegistry::new(); |
| 189 | + (ec.register_shared)("ext".into(), shared_factory("s-dual"), &mut registry) |
| 190 | + .expect("register_shared"); |
| 191 | + (ec.register_local)("ext".into(), local_factory("l-dual"), &mut registry) |
| 192 | + .expect("register_local"); |
| 193 | + |
| 194 | + let caps = resolve(®istry); |
| 195 | + // Each side returns its own type's value — not a SharedAsLocal |
| 196 | + // adapter over the shared instance. |
| 197 | + assert_eq!(caps.require_shared::<MacroTestCap>().unwrap().value(), "s-dual"); |
| 198 | + assert_eq!(caps.require_local::<MacroTestCap>().unwrap().value(), "l-dual"); |
| 199 | +} |
0 commit comments