The tunnel branch's other half is served for real against published alktunnels 0.4.1 / alkcall 0.10.1, with POC-3 §C.4 as the template: - tcpip_forward/streamlocal_forward (+ both cancel forms) map the vanilla listen request family-ward through TunnelListener::open on the held ChannelClient (bounded inline dial — the global request's reply + the &mut u32 bound-port out-param are the hook's); the wire bind endpoint rides as the resource string verbatim (OpenSSH client-side normalization parity). A reply without the minted listen token/bound report declines and reaps its fabric listener. - the door serves the fabric's inbound tunnel/forwarded opens: the serve config gains TunnelListenSeam (the seam-config second course-correction — the shared ServingConfig registry the assembly wired at from_connection_with_serving); alkssh registers the forwarded openable (ChannelCore over a clone of the held client's manager) at SshAdapter::new — a failed registration is a failed construction (AdapterError::ListenRegistration). - the forwarded-tcpip data leg: token correlation through the shared ListenRouter → the in-crate per-session channel cap at the accept surface (the acceptor vocabulary) → Handle:: channel_open_forwarded_tcpip streaming the listen request's own args + the actually-bound port (sshd parity) with TunnelForwardedParams.peer as the originator → pump_bidi(ssh, fabric). Correlation-miss → teardown-at-birth, never a half-open SSH channel. Unix arrivals ride forwarded-streamlocal@openssh.com. - bounded bookkeeping: the per-session listener map packet-loop-only with a dedicated max_concurrent_channels-ceiling counter; the route map bounded by construction (mints at accepted listens, removed at cancels + session teardown — the handler's Drop). - the harness grew the client-side accept hook (server_channel_open_forwarded_tcpip/_streamlocal, acceptor-driven) + a hand-rolled tracing subscriber; 18 contract rows + both real-tool smokes (ssh -L, ssh -R + curl) green in tests/tunnel_mapping.rs; the wave-2 stub listen rows migrated per build (unfeatured / seam-unconfigured). Verified: cargo test across default/git/session/sftp/tunnel and all combos + --all-features; clippy -D warnings (default, all-features, combos); cargo fmt --check; cargo doc --no-deps clean; taskgraph validate (26 tasks).
477 lines
18 KiB
Rust
477 lines
18 KiB
Rust
//! alkssh's wave-1 test doubles — the in-memory seams every later
|
|
//! wave's contract tests run against (the alkgit `tests/common/`
|
|
//! convention, adapted): an in-memory `IdentityProvider`
|
|
//! ([`MemIdentityProvider`]), the in-memory vault seam ([`TestVault`]),
|
|
//! an in-memory `HostKeyPolicy` ([`MemHostKeyPolicy`]), and the
|
|
//! ed25519 test-key fixtures ([`FIXTURE_KEYS`]).
|
|
//!
|
|
//! **Location decision (recorded, the OQ-03 freeze-inventory
|
|
//! discipline):** the doubles live *only* here — a shared module under
|
|
//! `tests/` reached with `mod common;` — and never in the public API:
|
|
//! no re-export from `lib.rs`, no feature gate, nothing doubles-related
|
|
//! reachable from the crate root. The doubles' shapes are test
|
|
//! surface, test-versioned freely; the only `src/`-side consumers are
|
|
//! `#[cfg(test)]` modules reading the fixture *files* through
|
|
//! `include_str!`, never a public export.
|
|
//!
|
|
//! **Vault double decision (recorded, flagged for the wave-1 gate):**
|
|
//! alkvault 0.1 ships an in-memory construction —
|
|
//! `VaultServiceHandle::new()` is an in-process vault that
|
|
//! `unlock_new` arms in-memory. The double is therefore a *wrapper*
|
|
//! ([`TestVault`]) around that construction, not a local re-impl
|
|
//! behind the source trait: the same decrypt error taxonomy, GCM
|
|
//! integrity behavior, and rotation semantics the production source
|
|
//! seam sees are exercised verbatim, and the POC-2 §3 failure postures
|
|
//! (locked / absent / tampered / wrong seed) ride the real seams.
|
|
//!
|
|
//! **Fixture decision (recorded):** committed static OpenSSH-PEM test
|
|
//! keys, minted by `ssh-keygen` at vector-generation time (the
|
|
//! independent issuer), never generated at test time — determinism and
|
|
//! no tooling dependency. The committed private halves are throwaway
|
|
//! fixtures, not secrets: minted for this suite only, never registered
|
|
//! anywhere real, never deployed. Each key's fingerprint is recorded
|
|
//! byte-verbatim from `ssh-keygen -lf` in
|
|
//! `tests/vectors/fingerprints.txt`; [`FIXTURE_KEYS`] carries the
|
|
//! recorded values and every fingerprint comparison in the suite
|
|
//! asserts parity against them.
|
|
//!
|
|
//! Each test binary's `mod common;` compiles the whole shared module
|
|
//! and no single binary exercises every double — dead_code is allowed
|
|
//! module-wide so one binary's suite never fails the next's.
|
|
//!
|
|
//! The row/stamp discipline (adapted from the sibling crates): test
|
|
//! functions are *rows* — the name states the property, not the
|
|
//! caller — and each carries a doc-comment list item beginning
|
|
//! **`Contract stamp:`** citing the pinning spec §/ADR § (never a bare
|
|
//! wave or date). The marker is greppable:
|
|
//! `grep -rn "Contract stamp:" tests/` is the inventory.
|
|
|
|
#![allow(dead_code)]
|
|
|
|
/// The loopback harness — *the* contract-test vehicle (its own
|
|
/// location/transport decisions live in its module docs:
|
|
/// `tests/common/harness.rs`).
|
|
pub mod harness;
|
|
|
|
/// The git-seam doubles (`w3-git-mapping`, `git`-gated — alkssh's
|
|
/// hand-rolled minimal alkgit quartet per `alkgit/src/backend.rs`'s
|
|
/// noop shapes; the module's own doc records the decisions:
|
|
/// `tests/common/git_doubles.rs`).
|
|
#[cfg(feature = "git")]
|
|
pub mod git_doubles;
|
|
|
|
/// The session-seam doubles (`w3-tty-session`, `session`-gated — the
|
|
/// hand-rolled TtyBackend shim + ownership witness; the module's own
|
|
/// doc records the decisions: `tests/common/tty_doubles.rs`).
|
|
#[cfg(feature = "session")]
|
|
pub mod tty_doubles;
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use alkcall::core::auth::{AuthToken, Identity, IdentityProvider};
|
|
use alkvault::{EncryptedData, VaultServiceHandle};
|
|
use async_trait::async_trait;
|
|
use russh::keys::{PrivateKey, PublicKey, decode_secret_key, parse_public_key_base64};
|
|
|
|
use alkssh::{HostKeyPolicy, HostKeySource, KeySourceError, PresentedHostKey};
|
|
|
|
/// One committed ed25519 test-key fixture: the private OpenSSH-PEM
|
|
/// (`tests/vectors/<name>.key`), the public one-line blob
|
|
/// (`tests/vectors/<name>.pub`), and the fingerprint recorded
|
|
/// byte-verbatim from `ssh-keygen -lf` (`tests/vectors/fingerprints.txt`).
|
|
///
|
|
/// The `pem`/`blob`/`fingerprint` values are committed constants — throwaway
|
|
/// fixture material, never registered or deployed anywhere real.
|
|
#[derive(Debug)]
|
|
pub struct FixtureKey {
|
|
/// The fixture's name (`alpha` … `delta`).
|
|
pub name: &'static str,
|
|
/// The OpenSSH private-key PEM, included verbatim from the
|
|
/// committed fixture file.
|
|
pub pem: &'static str,
|
|
/// The public key's base64 blob — the OpenSSH one-line encoding,
|
|
/// `ssh-ed25519 <blob>` stripped of the key-type prefix and comment.
|
|
pub blob: &'static str,
|
|
/// The fingerprint recorded from `ssh-keygen -lf` — the
|
|
/// independent-issuer value every comparison in the suite asserts
|
|
/// parity against (ADR-004 decision 1's contract invariant).
|
|
pub fingerprint: &'static str,
|
|
}
|
|
|
|
impl FixtureKey {
|
|
/// The decoded public key (parity input; ADR-004 decision 1).
|
|
pub fn public_key(&self) -> PublicKey {
|
|
parse_public_key_base64(self.blob).expect("committed fixture blob parses")
|
|
}
|
|
|
|
/// The decoded private key — `decode_secret_key(pem, None)`, the
|
|
/// ADR-005 decision 2 conversion (the fixtures' inner format is
|
|
/// exactly the at-rest contract: unencrypted OpenSSH PEM).
|
|
pub fn private_key(&self) -> PrivateKey {
|
|
decode_secret_key(self.pem, None).expect("committed fixture PEM decodes")
|
|
}
|
|
}
|
|
|
|
/// The wave-1 fixture set: four minted ed25519 keys. `alpha` is the
|
|
/// canonical client-dial fixture; `bravo`/`charlie` are the host-key
|
|
/// pair (rotation/multi-key rows); `delta` is the independent extra
|
|
/// (unregistered-peer, mismatch, and wrong-key rows).
|
|
pub const FIXTURE_KEYS: &[FixtureKey] = &[
|
|
FixtureKey {
|
|
name: "alpha",
|
|
pem: include_str!("../vectors/alpha.key"),
|
|
blob: "AAAAC3NzaC1lZDI1NTE5AAAAIM28sP5UAl3npJJ8trZgPt2kE+Af6477/Rx6J2VbM14z",
|
|
fingerprint: "SHA256:YyaiL42Xl8tcRRUhNCiqQBsiiRWVHHDR9hpXC45EQ6A",
|
|
},
|
|
FixtureKey {
|
|
name: "bravo",
|
|
pem: include_str!("../vectors/bravo.key"),
|
|
blob: "AAAAC3NzaC1lZDI1NTE5AAAAIMvu9S+hH9Dug7fdiHLeW+B9kee+i495jPAdBH8YaOvQ",
|
|
fingerprint: "SHA256:yQf/w2wYBoZ9adDlRGbcNh7AHE3mbqWRCqoGHra1ib0",
|
|
},
|
|
FixtureKey {
|
|
name: "charlie",
|
|
pem: include_str!("../vectors/charlie.key"),
|
|
blob: "AAAAC3NzaC1lZDI1NTE5AAAAIKL8zAzLkR0jKFY2l+QrBjC3Cv4Q3OBUUCX7rQV1jXQe",
|
|
fingerprint: "SHA256:b5jdwosbZUx5nsY5zfr/ZCt+4sJYehVBq7Xl/wstJ9U",
|
|
},
|
|
FixtureKey {
|
|
name: "delta",
|
|
pem: include_str!("../vectors/delta.key"),
|
|
blob: "AAAAC3NzaC1lZDI1NTE5AAAAIIcMK+njeiGqh8THEBPU9MTYrSJ3WHvxv3ZH11JTS/SU",
|
|
fingerprint: "SHA256:4MiQ0KGoTEaklHBTlxPDuQkeu0FTyqEbne10K6Z8CKs",
|
|
},
|
|
];
|
|
|
|
/// Looks a fixture up by name — `fixture("alpha")` is the canonical
|
|
/// client-dial key.
|
|
pub fn fixture(name: &str) -> &'static FixtureKey {
|
|
FIXTURE_KEYS
|
|
.iter()
|
|
.find(|k| k.name == name)
|
|
.unwrap_or_else(|| panic!("no fixture named {name}"))
|
|
}
|
|
|
|
/// Builds an [`Identity`] for registration (`id` + scopes; the
|
|
/// `resources` map is empty — the wire layer only needs the resolved
|
|
/// identity, richer ACL shapes are the assembly's).
|
|
pub fn identity(id: &str, scopes: &[&str]) -> Identity {
|
|
Identity {
|
|
id: id.into(),
|
|
scopes: scopes.iter().map(|s| s.to_string()).collect(),
|
|
resources: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// The in-memory [`IdentityProvider`] — registered fingerprints →
|
|
/// [`Identity`] resolutions over alkcall's trait verbatim.
|
|
///
|
|
/// The double the wave-2 auth bridge and dispatch tests resolve
|
|
/// against, and the analog of serving `Arc<dyn IdentityProvider>` in
|
|
/// [`alkssh::ServeConfig`]: the fingerprint → identity path is ADR-004
|
|
/// decision 1's only resolution shape.
|
|
pub struct MemIdentityProvider {
|
|
identities: parking_lot::RwLock<HashMap<String, Identity>>,
|
|
}
|
|
|
|
impl MemIdentityProvider {
|
|
/// An empty registry — nothing resolves until registered
|
|
/// (fail-closed: unregistered fingerprints decline).
|
|
pub fn new() -> Self {
|
|
Self {
|
|
identities: parking_lot::RwLock::new(HashMap::new()),
|
|
}
|
|
}
|
|
|
|
/// Registers (or replaces) one fingerprint → identity mapping.
|
|
pub fn register(&self, fingerprint: &str, identity: Identity) {
|
|
self.identities
|
|
.write()
|
|
.insert(fingerprint.to_string(), identity);
|
|
}
|
|
|
|
/// Removes a registration — the post-resolution-revoke shape.
|
|
pub fn revoke(&self, fingerprint: &str) {
|
|
self.identities.write().remove(fingerprint);
|
|
}
|
|
|
|
/// The registered fingerprints — inspection for test assertions.
|
|
pub fn registered_fingerprints(&self) -> Vec<String> {
|
|
let mut keys = self
|
|
.identities
|
|
.read()
|
|
.keys()
|
|
.cloned()
|
|
.collect::<Vec<String>>();
|
|
keys.sort();
|
|
keys
|
|
}
|
|
}
|
|
|
|
impl Default for MemIdentityProvider {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl IdentityProvider for MemIdentityProvider {
|
|
/// The fingerprint path — ADR-004 decision 1's single resolution
|
|
/// shape: exact string equality against registered values.
|
|
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity> {
|
|
self.identities.read().get(fingerprint).cloned()
|
|
}
|
|
|
|
/// The token path resolves nothing in the SSH direction —
|
|
/// ADR-004's dispositions are fingerprint-only on this surface
|
|
/// (`auth-bridge.md`); the double never invents a token shape.
|
|
fn resolve_from_token(&self, _: &AuthToken) -> Option<Identity> {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// The in-memory vault seam — a wrapper over alkvault 0.1's own
|
|
/// in-memory construction ([`VaultServiceHandle`], unlocked at
|
|
/// construction via `unlock_new`).
|
|
///
|
|
/// The locked/absent/tampered/mismatch failure postures (POC-2 §3) are
|
|
/// exercised *through the real production seams* the source traits
|
|
/// call, against this handle — the double exists so tests never touch
|
|
/// vault lifecycle decisions themselves: [`TestVault::unlocked`] arms
|
|
/// it, `lock` closes it, and the sources keep refusing, never
|
|
/// retrying (the crate never unlocks — ADR-005's separation).
|
|
#[derive(Clone)]
|
|
pub struct TestVault {
|
|
handle: VaultServiceHandle,
|
|
}
|
|
|
|
impl TestVault {
|
|
/// A freshly armed in-memory vault — unlocked, ready to encrypt.
|
|
pub fn unlocked() -> Self {
|
|
let handle = VaultServiceHandle::new();
|
|
handle
|
|
.unlock_new(24)
|
|
.expect("in-memory vault arms unlocked");
|
|
Self { handle }
|
|
}
|
|
|
|
/// The vault handle the production sources consume — clones share
|
|
/// the same vault state (locking through one is visible to all).
|
|
pub fn handle(&self) -> VaultServiceHandle {
|
|
self.handle.clone()
|
|
}
|
|
|
|
/// Seals one fixture key's PEM into an at-rest record — the
|
|
/// ADR-005 inner-format contract (unencrypted OpenSSH PEM in an
|
|
/// `EncryptedData` record).
|
|
pub fn seal(&self, key: &FixtureKey) -> EncryptedData {
|
|
self.encrypt(key.pem)
|
|
}
|
|
|
|
/// Encrypts an arbitrary plaintext (the non-key and tamper rows
|
|
/// need it; ADR-005 decision 4's decode-refusal shape).
|
|
pub fn encrypt(&self, plaintext: &str) -> EncryptedData {
|
|
self.handle
|
|
.encrypt(plaintext, alkvault::CURRENT_KEY_VERSION)
|
|
.expect("in-memory vault encrypts the plaintext")
|
|
}
|
|
|
|
/// Locks the vault — the locked-vault posture (POC-2 §3, the
|
|
/// `KeySourceError::Locked` refusal class).
|
|
pub fn lock(&self) {
|
|
self.handle.lock();
|
|
}
|
|
|
|
/// The vault's lifecycle state, for posture assertions.
|
|
pub fn is_unlocked(&self) -> bool {
|
|
self.handle.is_unlocked()
|
|
}
|
|
}
|
|
|
|
/// The in-memory [`HostKeySource`] double — a static decrypted key set
|
|
/// (wave-2 addition): the wire-shaped rows construct a real
|
|
/// `SshAdapter`, whose construction wants a non-empty host-key set, and
|
|
/// the direct-hook rows want a source that is never loaded (the handler
|
|
/// construction loads nothing — host keys are the russh `Config`'s
|
|
/// business, built in [`alkssh::SshAdapter::new`]).
|
|
///
|
|
/// `absent()` is the empty-set shape — its load refuses with
|
|
/// [`KeySourceError::Absent`] (the fail-closed contract), so any row
|
|
/// that accidentally loads an absent double fails loudly.
|
|
pub struct MemHostKeySource {
|
|
keys: Vec<PrivateKey>,
|
|
}
|
|
|
|
impl MemHostKeySource {
|
|
/// The never-loaded / absent shape.
|
|
pub fn absent() -> Self {
|
|
Self { keys: Vec::new() }
|
|
}
|
|
|
|
/// A static decrypted set from the fixture keys — the wire rows'
|
|
/// host-key set (e.g. `["bravo"]`, the rotation-pair member).
|
|
pub fn from_fixtures(keys: &[&'static FixtureKey]) -> Self {
|
|
Self {
|
|
keys: keys.iter().map(|k| k.private_key()).collect(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl HostKeySource for MemHostKeySource {
|
|
async fn load(&self) -> Result<Vec<PrivateKey>, KeySourceError> {
|
|
if self.keys.is_empty() {
|
|
Err(KeySourceError::Absent)
|
|
} else {
|
|
Ok(self.keys.clone())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The in-memory [`HostKeyPolicy`] — a configurable verdict double with
|
|
/// an inspectable decision log, for the wave-4 dial tests (and the
|
|
/// case-1/case-2 rows of every host-key contract test).
|
|
///
|
|
/// Verdicts: a rules list of exact `(host, fingerprint)` matches —
|
|
/// last matching rule wins — falling back to a configurable
|
|
/// default verdict for unlisted pairs. The default of the
|
|
/// **default-constructed double is reject** (the crate's fail-closed
|
|
/// posture, `OQ-05`); permissive verdicts are the test's explicit
|
|
/// choice, mirroring "permissive policy is the assembly's to write".
|
|
/// Every decision is recorded with the full [`PresentedHostKey`]
|
|
/// context; the log is the inspectable record.
|
|
pub struct MemHostKeyPolicy {
|
|
default_verdict: bool,
|
|
rules: parking_lot::RwLock<Vec<Rule>>,
|
|
log: parking_lot::Mutex<Vec<Decision>>,
|
|
}
|
|
|
|
struct Rule {
|
|
host: String,
|
|
fingerprint: String,
|
|
accept: bool,
|
|
}
|
|
|
|
/// One recorded host-key decision — the presented key and the verdict
|
|
/// the policy returned.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Decision {
|
|
/// The presented key that was decided.
|
|
pub presented: PresentedHostKey,
|
|
/// The verdict: `true` = accept, `false` = decline.
|
|
pub accepted: bool,
|
|
}
|
|
|
|
impl MemHostKeyPolicy {
|
|
/// A decision double with the fail-closed default (reject
|
|
/// everything without a matching accept rule).
|
|
pub fn new() -> Self {
|
|
Self::with_default(false)
|
|
}
|
|
|
|
/// A decision double with an explicit default verdict — the
|
|
/// case-2 permissive posture (`accepting` doubles) is the test's
|
|
/// explicit choice, never a default.
|
|
pub fn with_default(default_verdict: bool) -> Self {
|
|
Self {
|
|
default_verdict,
|
|
rules: parking_lot::RwLock::new(Vec::new()),
|
|
log: parking_lot::Mutex::new(Vec::new()),
|
|
}
|
|
}
|
|
|
|
/// Adds a rule: exact `(host, fingerprint)` → verdict. Last match
|
|
/// wins, so later rules override earlier ones.
|
|
pub fn rule(&self, host: &str, fingerprint: &str, accept: bool) {
|
|
self.rules.write().push(Rule {
|
|
host: host.into(),
|
|
fingerprint: fingerprint.into(),
|
|
accept,
|
|
});
|
|
}
|
|
|
|
/// Pins an accept rule for one `(host, fingerprint)` — the case-1
|
|
/// trust-anchor shape (hard accept on exact match).
|
|
pub fn pin(&self, host: &str, fingerprint: &str) {
|
|
self.rule(host, fingerprint, true);
|
|
}
|
|
|
|
/// The decision log — every [`Decision`] in call order.
|
|
pub fn decisions(&self) -> Vec<Decision> {
|
|
self.log.lock().clone()
|
|
}
|
|
}
|
|
|
|
impl Default for MemHostKeyPolicy {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl HostKeyPolicy for MemHostKeyPolicy {
|
|
async fn decide(&self, presented: &PresentedHostKey) -> bool {
|
|
let rules = self.rules.read();
|
|
let verdict = rules
|
|
.iter()
|
|
.rev()
|
|
.find(|r| r.host == presented.host && r.fingerprint == presented.fingerprint)
|
|
.map(|r| r.accept)
|
|
.unwrap_or(self.default_verdict);
|
|
drop(rules);
|
|
self.log.lock().push(Decision {
|
|
presented: presented.clone(),
|
|
accepted: verdict,
|
|
});
|
|
verdict
|
|
}
|
|
}
|
|
|
|
/// The leg-2 rows' tracing visibility (`w3-tunnel-listen`): a minimal
|
|
/// hand-rolled global [`tracing::Subscriber`] printing every event's
|
|
/// field pairs to stderr — the same hand-rolled convention the
|
|
/// crate-side vocabulary rows use (no `tracing-subscriber`
|
|
/// dependency; events fire from spawned tasks on both sides of the
|
|
/// fabric, so the global subscriber is the only capture shape that
|
|
/// works). Rows opt in: `trace_global()` once per row.
|
|
pub fn trace_global() {
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
struct TraceSubscriber(AtomicU64);
|
|
struct FieldPrinter(u64);
|
|
|
|
impl tracing::field::Visit for FieldPrinter {
|
|
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
|
eprintln!("trace[{}]: {} = {value:?}", self.0, field.name());
|
|
}
|
|
|
|
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
|
|
eprintln!("trace[{}]: {} = {value:?}", self.0, field.name());
|
|
}
|
|
}
|
|
|
|
impl tracing::Subscriber for TraceSubscriber {
|
|
fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
|
|
true
|
|
}
|
|
|
|
fn new_span(&self, _span: &tracing::span::Attributes<'_>) -> tracing::Id {
|
|
tracing::Id::from_u64(1)
|
|
}
|
|
|
|
fn record(&self, _span: &tracing::Id, _values: &tracing::span::Record<'_>) {}
|
|
|
|
fn record_follows_from(&self, _span: &tracing::Id, _follows: &tracing::Id) {}
|
|
|
|
fn event(&self, event: &tracing::Event<'_>) {
|
|
event.record(&mut FieldPrinter(self.0.fetch_add(1, Ordering::Relaxed)));
|
|
}
|
|
|
|
fn enter(&self, _span: &tracing::Id) {}
|
|
fn exit(&self, _span: &tracing::Id) {}
|
|
fn clone_span(&self, _id: &tracing::Id) -> tracing::Id {
|
|
tracing::Id::from_u64(1)
|
|
}
|
|
}
|
|
|
|
let _ = tracing::subscriber::set_global_default(TraceSubscriber(AtomicU64::new(0)));
|
|
}
|