Skip to main content

sonda_core/
lib.rs

1//! sonda-core — the engine for synthetic telemetry generation.
2//!
3//! This crate owns all domain logic: telemetry models, value generators,
4//! schedulers, encoders, and sinks. The CLI and HTTP server are thin layers
5//! that call into this library.
6//!
7//! # Stability
8//!
9//! Public enums and structs on the library surface — including [`SondaError`]
10//! and its sub-enums ([`ConfigError`], [`GeneratorError`], [`EncoderError`],
11//! [`RuntimeError`]), the config enums ([`GeneratorConfig`](generator::GeneratorConfig),
12//! [`EncoderConfig`](encoder::EncoderConfig), [`SinkConfig`](sink::SinkConfig),
13//! [`DistributionConfig`], [`ScenarioEntry`]), the compile-phase error enums
14//! under [`compiler`], and [`ScenarioStats`] — are marked
15//! `#[non_exhaustive]`. Downstream consumers that `match` on these types must
16//! include a wildcard `_ =>` arm, and [`ScenarioStats`] must be constructed
17//! via `Default::default()` plus field updates rather than a struct literal.
18//! This lets sonda-core add new variants and fields in a minor release
19//! without a semver-major bump.
20
21pub mod compiler;
22pub mod config;
23pub mod emit;
24pub mod encoder;
25pub mod generator;
26pub mod model;
27pub mod packs;
28pub mod scenarios;
29pub mod schedule;
30pub mod sink;
31pub(crate) mod util;
32
33pub use config::aliases::{desugar_entry, desugar_scenario_config};
34pub use config::BaseScheduleConfig;
35pub use config::BurstConfig;
36pub use config::CardinalitySpikeConfig;
37pub use config::DistributionConfig;
38pub use config::DynamicLabelConfig;
39pub use config::DynamicLabelStrategy;
40pub use config::HistogramScenarioConfig;
41pub use config::LogScenarioConfig;
42pub use config::OnSinkError;
43pub use config::ScenarioEntry;
44pub use config::SpikeStrategy;
45pub use config::SummaryScenarioConfig;
46pub use config::{expand_entry, expand_scenario};
47pub use model::log::LogEvent;
48pub use model::log::Severity;
49pub use model::metric::Labels;
50pub use model::metric::MetricEvent;
51pub use model::metric::ValidatedMetricName;
52pub use scenarios::BuiltinScenario;
53pub use schedule::handle::ScenarioHandle;
54pub use schedule::launch::{launch_scenario, prepare_entries, validate_entry, PreparedEntry};
55pub use schedule::stats::{ScenarioState, ScenarioStats};
56
57#[cfg(feature = "config")]
58pub use compiler::prepare::PrepareError;
59
60#[cfg(feature = "config")]
61pub use compile::{compile_scenario_file, compile_scenario_file_compiled, CompileError};
62
63#[cfg(feature = "config")]
64mod compile;
65
66/// Top-level error type for sonda-core.
67///
68/// Each variant delegates to a typed sub-enum that preserves the original
69/// error source where possible. This enables callers to programmatically
70/// inspect error origins (e.g., distinguish `io::ErrorKind::NotFound` from
71/// `PermissionDenied` in a generator file-read error) via the standard
72/// [`std::error::Error::source`] chain.
73///
74/// The `Sink` variant wraps [`std::io::Error`] without a blanket `#[from]`
75/// conversion — all I/O errors must be explicitly mapped to the correct
76/// variant at the call site. This prevents generator or config I/O errors
77/// from being misclassified as sink errors.
78#[derive(Debug, thiserror::Error)]
79#[non_exhaustive]
80pub enum SondaError {
81    /// An error in scenario configuration (invalid values, missing fields).
82    #[error("configuration error: {0}")]
83    Config(#[from] ConfigError),
84
85    /// An error during event encoding (serialization, timestamp, protobuf).
86    #[error("encoder error: {0}")]
87    Encoder(#[from] EncoderError),
88
89    /// An I/O error originating from a sink (stdout, file, TCP, UDP, HTTP).
90    ///
91    /// This variant does **not** use `#[from] std::io::Error` because not all
92    /// I/O errors originate from sinks. Generator file reads, for example,
93    /// produce [`SondaError::Generator`] instead.
94    #[error("sink error: {0}")]
95    Sink(std::io::Error),
96
97    /// An error from a generator (file I/O, invalid data).
98    #[error("generator error: {0}")]
99    Generator(#[from] GeneratorError),
100
101    /// A runtime or system error (thread spawn failure, thread panic).
102    ///
103    /// These are environmental failures that are outside the user's control
104    /// and cannot be fixed by editing configuration. Separated from
105    /// [`ConfigError`] so that consumers matching on config errors to
106    /// surface YAML validation feedback are not confused by thread panics.
107    #[error("runtime error: {0}")]
108    Runtime(#[from] RuntimeError),
109}
110
111/// Errors related to scenario configuration validation.
112///
113/// Covers invalid field values, missing required fields, unparseable
114/// durations, and similar problems that the user can fix by editing their
115/// YAML scenario file or adjusting programmatic config construction.
116#[derive(Debug, thiserror::Error)]
117#[non_exhaustive]
118pub enum ConfigError {
119    /// A configuration field has an invalid value.
120    ///
121    /// The message includes the field name and a human-readable explanation
122    /// of the constraint that was violated.
123    #[error("{0}")]
124    InvalidValue(String),
125}
126
127impl ConfigError {
128    /// Create a new [`ConfigError::InvalidValue`] from any displayable message.
129    pub(crate) fn invalid(msg: impl Into<String>) -> Self {
130        ConfigError::InvalidValue(msg.into())
131    }
132}
133
134/// Errors originating from value or log generators.
135///
136/// Currently contains [`FileRead`](GeneratorError::FileRead) for I/O failures
137/// when loading generator data from disk. This enum is designed for
138/// extensibility — future variants may include `InvalidData` (malformed file
139/// contents), `ParseFailed` (unparseable numeric columns), or
140/// `UnsupportedFormat` as generator capabilities grow.
141#[derive(Debug, thiserror::Error)]
142#[non_exhaustive]
143pub enum GeneratorError {
144    /// Failed to read a generator input file (CSV replay, log replay).
145    ///
146    /// Preserves the original [`std::io::Error`] via the `#[source]` attribute
147    /// so callers can inspect the error kind (e.g., `ErrorKind::NotFound` vs
148    /// `ErrorKind::PermissionDenied`) programmatically.
149    #[error("cannot read file {path:?}")]
150    FileRead {
151        /// The path that could not be read.
152        path: String,
153        /// The underlying I/O error.
154        #[source]
155        source: std::io::Error,
156    },
157}
158
159impl GeneratorError {
160    /// Returns the [`std::io::ErrorKind`] of the underlying I/O error, if this
161    /// is a [`FileRead`](GeneratorError::FileRead) variant.
162    ///
163    /// Convenience method that lets callers inspect the error kind without
164    /// manually traversing the `source()` chain.
165    pub fn source_io_kind(&self) -> Option<std::io::ErrorKind> {
166        match self {
167            GeneratorError::FileRead { source, .. } => Some(source.kind()),
168        }
169    }
170}
171
172/// Errors during event encoding (serialization, timestamp conversion, protobuf).
173///
174/// Preserves original error sources where possible so callers can inspect
175/// the underlying failure without string parsing.
176#[derive(Debug, thiserror::Error)]
177#[non_exhaustive]
178pub enum EncoderError {
179    /// JSON serialization failed.
180    ///
181    /// Preserves the original [`serde_json::Error`] so callers can inspect
182    /// whether the failure was due to I/O, data, syntax, or EOF conditions.
183    #[error("JSON serialization failed")]
184    SerializationFailed(#[source] serde_json::Error),
185
186    /// The event timestamp predates the Unix epoch.
187    ///
188    /// Preserves the original [`std::time::SystemTimeError`] so callers can
189    /// inspect how far before the epoch the timestamp was.
190    #[error("timestamp before Unix epoch")]
191    TimestampBeforeEpoch(#[source] std::time::SystemTimeError),
192
193    /// The encoder does not support this event type (e.g., a metric-only
194    /// encoder receiving a log event).
195    #[error("{0}")]
196    NotSupported(String),
197
198    /// A catch-all for encoder errors that do not fit other variants.
199    ///
200    /// Used for feature-gated encoders (protobuf, snappy) where preserving
201    /// the concrete error type would require conditional compilation on the
202    /// enum definition itself.
203    #[error("{0}")]
204    Other(String),
205}
206
207/// Runtime and system errors outside the user's control.
208///
209/// These represent environmental failures (OS thread limits, thread panics)
210/// that cannot be resolved by changing configuration.
211#[derive(Debug, thiserror::Error)]
212#[non_exhaustive]
213pub enum RuntimeError {
214    /// The OS refused to spawn a new thread.
215    ///
216    /// Preserves the original [`std::io::Error`] via the `#[source]` attribute
217    /// so callers can inspect the error kind (e.g., resource exhaustion)
218    /// programmatically via the standard [`std::error::Error::source`] chain.
219    #[error("failed to spawn scenario thread")]
220    SpawnFailed(#[source] std::io::Error),
221
222    /// A scenario thread panicked during execution.
223    #[error("scenario thread panicked")]
224    ThreadPanicked,
225
226    /// One or more scenarios in a multi-scenario run failed.
227    ///
228    /// The error messages from all failed scenario threads are collected and
229    /// joined into a single string. This variant exists to prevent thread-level
230    /// sink, runtime, or generator errors from being misclassified as
231    /// [`ConfigError`] when collected at the multi-runner level.
232    #[error("{0}")]
233    ScenariosFailed(String),
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    // ---- SondaError variant discrimination ------------------------------------
241
242    #[test]
243    fn io_error_does_not_auto_convert_to_sonda_error() {
244        // Verify that there is no From<io::Error> for SondaError.
245        // SondaError::Sink must be constructed explicitly.
246        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
247        let sonda_err = SondaError::Sink(io_err);
248        assert!(
249            matches!(sonda_err, SondaError::Sink(_)),
250            "explicit Sink construction must produce Sink variant"
251        );
252    }
253
254    #[test]
255    fn missing_replay_file_produces_generator_error_not_sink() {
256        let path = std::path::Path::new("/nonexistent/path/for/replay.log");
257        let result = generator::log_replay::LogReplayGenerator::from_file(path);
258        match result {
259            Err(ref err) => {
260                assert!(
261                    matches!(err, SondaError::Generator(_)),
262                    "missing replay file must produce Generator variant, got: {err:?}"
263                );
264            }
265            Ok(_) => panic!("missing file must return Err"),
266        }
267    }
268
269    #[test]
270    fn missing_csv_file_produces_generator_error_not_sink() {
271        let result = generator::csv_replay::CsvReplayGenerator::new(
272            "/nonexistent/path/for/data.csv",
273            0,
274            true,
275        );
276        match result {
277            Err(SondaError::Generator(GeneratorError::FileRead {
278                ref path,
279                ref source,
280            })) => {
281                assert_eq!(path, "/nonexistent/path/for/data.csv");
282                assert_eq!(source.kind(), std::io::ErrorKind::NotFound);
283            }
284            Err(ref err) => {
285                panic!("missing CSV file must produce Generator(FileRead) variant, got: {err:?}");
286            }
287            Ok(_) => panic!("missing CSV file must return Err"),
288        }
289    }
290
291    #[test]
292    fn log_replay_factory_missing_file_produces_generator_error() {
293        let config = generator::LogGeneratorConfig::Replay {
294            file: "/nonexistent/path/for/replay.log".to_string(),
295        };
296        let result = generator::create_log_generator(&config);
297        match result {
298            Err(ref err) => {
299                assert!(
300                    matches!(err, SondaError::Generator(_)),
301                    "factory with missing replay file must produce Generator variant, got: {err:?}"
302                );
303            }
304            Ok(_) => panic!("missing replay file must return Err"),
305        }
306    }
307
308    #[test]
309    fn sink_file_error_produces_sink_variant() {
310        // Opening a file at an invalid path must produce SondaError::Sink.
311        let result = sink::file::FileSink::new(std::path::Path::new(
312            "/nonexistent/deeply/nested/path/output.txt",
313        ));
314        match result {
315            Err(ref err) => {
316                assert!(
317                    matches!(err, SondaError::Sink(_)),
318                    "file sink I/O error must produce Sink variant, got: {err:?}"
319                );
320            }
321            Ok(_) => panic!("invalid file path must return Err"),
322        }
323    }
324
325    #[test]
326    fn sonda_error_display_includes_context() {
327        let err = SondaError::Generator(GeneratorError::FileRead {
328            path: "/some/file".to_string(),
329            source: std::io::Error::new(std::io::ErrorKind::NotFound, "no such file"),
330        });
331        let msg = format!("{err}");
332        assert!(
333            msg.contains("generator error"),
334            "Generator variant display must include 'generator error', got: {msg}"
335        );
336        assert!(
337            msg.contains("/some/file"),
338            "Generator variant display must include the file path, got: {msg}"
339        );
340    }
341
342    // ---- Sub-enum From conversions ------------------------------------------
343
344    #[test]
345    fn config_error_converts_to_sonda_error_via_from() {
346        let config_err = ConfigError::invalid("rate must be positive");
347        let sonda_err: SondaError = config_err.into();
348        assert!(
349            matches!(sonda_err, SondaError::Config(_)),
350            "ConfigError must convert to SondaError::Config"
351        );
352    }
353
354    #[test]
355    fn generator_error_converts_to_sonda_error_via_from() {
356        let gen_err = GeneratorError::FileRead {
357            path: "/tmp/test.csv".to_string(),
358            source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
359        };
360        let sonda_err: SondaError = gen_err.into();
361        assert!(
362            matches!(sonda_err, SondaError::Generator(_)),
363            "GeneratorError must convert to SondaError::Generator"
364        );
365    }
366
367    #[test]
368    fn encoder_error_converts_to_sonda_error_via_from() {
369        let enc_err = EncoderError::NotSupported("log encoding not supported".into());
370        let sonda_err: SondaError = enc_err.into();
371        assert!(
372            matches!(sonda_err, SondaError::Encoder(_)),
373            "EncoderError must convert to SondaError::Encoder"
374        );
375    }
376
377    #[test]
378    fn runtime_error_converts_to_sonda_error_via_from() {
379        let rt_err = RuntimeError::ThreadPanicked;
380        let sonda_err: SondaError = rt_err.into();
381        assert!(
382            matches!(sonda_err, SondaError::Runtime(_)),
383            "RuntimeError must convert to SondaError::Runtime"
384        );
385    }
386
387    // ---- source() chain preservation ----------------------------------------
388
389    #[test]
390    fn generator_file_read_preserves_io_error_source() {
391        use std::error::Error;
392
393        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
394        let gen_err = GeneratorError::FileRead {
395            path: "/secret/file".to_string(),
396            source: io_err,
397        };
398
399        // The source() chain must be present and be an io::Error.
400        let source = gen_err.source().expect("source() must return Some");
401        let io_source = source
402            .downcast_ref::<std::io::Error>()
403            .expect("source must be std::io::Error");
404        assert_eq!(io_source.kind(), std::io::ErrorKind::PermissionDenied);
405    }
406
407    #[test]
408    fn generator_file_read_io_error_kind_is_inspectable() {
409        let gen_err = GeneratorError::FileRead {
410            path: "/missing/file".to_string(),
411            source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
412        };
413        // Callers can match on the io::Error kind programmatically.
414        assert_eq!(gen_err.source_io_kind(), Some(std::io::ErrorKind::NotFound));
415    }
416
417    #[test]
418    fn encoder_serialization_preserves_serde_json_source() {
419        use std::error::Error;
420
421        // Provoke a real serde_json error by deserializing invalid JSON.
422        let json_err: serde_json::Error = serde_json::from_str::<serde_json::Value>("{{invalid}}")
423            .expect_err("invalid JSON must fail");
424        let enc_err = EncoderError::SerializationFailed(json_err);
425
426        let source = enc_err.source().expect("source() must return Some");
427        assert!(
428            source.downcast_ref::<serde_json::Error>().is_some(),
429            "source must be serde_json::Error"
430        );
431    }
432
433    #[test]
434    fn encoder_timestamp_preserves_system_time_source() {
435        use std::error::Error;
436
437        let pre_epoch = std::time::UNIX_EPOCH - std::time::Duration::from_secs(1);
438        let sys_err = pre_epoch.duration_since(std::time::UNIX_EPOCH).unwrap_err();
439        let enc_err = EncoderError::TimestampBeforeEpoch(sys_err);
440
441        let source = enc_err.source().expect("source() must return Some");
442        assert!(
443            source
444                .downcast_ref::<std::time::SystemTimeError>()
445                .is_some(),
446            "source must be SystemTimeError"
447        );
448    }
449
450    // ---- Runtime error classification (WARNING 1) ---------------------------
451
452    #[test]
453    fn spawn_failed_is_runtime_not_config() {
454        let io_err = std::io::Error::new(std::io::ErrorKind::Other, "resource limit");
455        let rt_err = RuntimeError::SpawnFailed(io_err);
456        let sonda_err: SondaError = rt_err.into();
457        assert!(
458            matches!(sonda_err, SondaError::Runtime(RuntimeError::SpawnFailed(_))),
459            "thread spawn failure must be Runtime::SpawnFailed, not Config"
460        );
461    }
462
463    #[test]
464    fn thread_panicked_is_runtime_not_config() {
465        let rt_err = RuntimeError::ThreadPanicked;
466        let sonda_err: SondaError = rt_err.into();
467        assert!(
468            matches!(sonda_err, SondaError::Runtime(RuntimeError::ThreadPanicked)),
469            "thread panic must be Runtime::ThreadPanicked, not Config"
470        );
471    }
472
473    #[test]
474    fn runtime_error_display_is_descriptive() {
475        let spawn_err = RuntimeError::SpawnFailed(std::io::Error::new(
476            std::io::ErrorKind::Other,
477            "too many threads",
478        ));
479        let msg = format!("{spawn_err}");
480        assert!(
481            msg.contains("failed to spawn scenario thread"),
482            "SpawnFailed display must describe the spawn failure, got: {msg}"
483        );
484
485        let panic_err = RuntimeError::ThreadPanicked;
486        let msg = format!("{panic_err}");
487        assert!(
488            msg.contains("panicked"),
489            "ThreadPanicked display must mention panic, got: {msg}"
490        );
491
492        let scenarios_err =
493            RuntimeError::ScenariosFailed("sink error: broken pipe; sink error: timeout".into());
494        let msg = format!("{scenarios_err}");
495        assert!(
496            msg.contains("sink error"),
497            "ScenariosFailed display must include the collected messages, got: {msg}"
498        );
499    }
500
501    #[test]
502    fn spawn_failed_preserves_io_error_source() {
503        use std::error::Error;
504
505        let io_err = std::io::Error::new(
506            std::io::ErrorKind::WouldBlock,
507            "resource temporarily unavailable",
508        );
509        let rt_err = RuntimeError::SpawnFailed(io_err);
510
511        let source = rt_err
512            .source()
513            .expect("SpawnFailed source() must return Some");
514        let io_source = source
515            .downcast_ref::<std::io::Error>()
516            .expect("source must be std::io::Error");
517        assert_eq!(io_source.kind(), std::io::ErrorKind::WouldBlock);
518    }
519
520    #[test]
521    fn spawn_failed_source_chain_traverses_through_sonda_error() {
522        use std::error::Error;
523
524        let io_err =
525            std::io::Error::new(std::io::ErrorKind::PermissionDenied, "cannot create thread");
526        let sonda_err = SondaError::Runtime(RuntimeError::SpawnFailed(io_err));
527
528        // SondaError::Runtime.source() -> RuntimeError
529        let runtime_source = sonda_err
530            .source()
531            .expect("SondaError::Runtime source() must return Some");
532        let rt_err = runtime_source
533            .downcast_ref::<RuntimeError>()
534            .expect("first source must be RuntimeError");
535
536        // RuntimeError::SpawnFailed.source() -> io::Error
537        let io_source = rt_err
538            .source()
539            .expect("SpawnFailed source() must return Some");
540        let io_inner = io_source
541            .downcast_ref::<std::io::Error>()
542            .expect("second source must be std::io::Error");
543        assert_eq!(io_inner.kind(), std::io::ErrorKind::PermissionDenied);
544    }
545
546    #[test]
547    fn scenarios_failed_is_runtime_not_config() {
548        let rt_err = RuntimeError::ScenariosFailed("thread failed".into());
549        let sonda_err: SondaError = rt_err.into();
550        assert!(
551            matches!(
552                sonda_err,
553                SondaError::Runtime(RuntimeError::ScenariosFailed(_))
554            ),
555            "multi-scenario failures must be Runtime::ScenariosFailed, not Config"
556        );
557    }
558
559    #[test]
560    fn scenarios_failed_converts_to_sonda_error_via_from() {
561        let rt_err = RuntimeError::ScenariosFailed("sink error: broken pipe".into());
562        let sonda_err: SondaError = rt_err.into();
563        assert!(
564            matches!(sonda_err, SondaError::Runtime(_)),
565            "ScenariosFailed must convert to SondaError::Runtime"
566        );
567    }
568
569    // ---- config feature gate tests --------------------------------------------
570
571    /// Config types are constructible in code regardless of the `config` feature.
572    /// This test runs with or without the feature enabled.
573    #[test]
574    fn config_types_constructible_without_yaml_parsing() {
575        use crate::config::{BaseScheduleConfig, ScenarioConfig};
576        use crate::encoder::EncoderConfig;
577        use crate::generator::GeneratorConfig;
578        use crate::sink::SinkConfig;
579
580        let _config = ScenarioConfig {
581            base: BaseScheduleConfig {
582                name: "test".to_string(),
583                rate: 10.0,
584                duration: None,
585                gaps: None,
586                bursts: None,
587                cardinality_spikes: None,
588                dynamic_labels: None,
589                labels: None,
590                sink: SinkConfig::Stdout,
591                phase_offset: None,
592                clock_group: None,
593                clock_group_is_auto: None,
594                jitter: None,
595                jitter_seed: None,
596                on_sink_error: crate::OnSinkError::Warn,
597            },
598            generator: GeneratorConfig::Constant { value: 1.0 },
599            encoder: EncoderConfig::PrometheusText { precision: None },
600        };
601    }
602
603    /// YAML deserialization is available when the `config` feature is active.
604    #[cfg(feature = "config")]
605    #[test]
606    fn config_feature_enables_yaml_deserialization() {
607        use crate::config::ScenarioConfig;
608
609        let yaml = r#"
610name: test
611rate: 10
612generator:
613  type: constant
614  value: 1.0
615"#;
616        let config: ScenarioConfig = serde_yaml_ng::from_str(yaml)
617            .expect("YAML deserialization must work with config feature");
618        assert_eq!(config.name, "test");
619    }
620
621    /// EncoderConfig, SinkConfig, and GeneratorConfig are all constructible
622    /// without deserialization and can be passed to their respective factory functions.
623    #[test]
624    fn factory_functions_work_without_deserialization() {
625        use crate::encoder::{create_encoder, EncoderConfig};
626        use crate::generator::{create_generator, GeneratorConfig};
627        use crate::sink::{create_sink, SinkConfig};
628
629        let gen_config = GeneratorConfig::Constant { value: 42.0 };
630        let gen = create_generator(&gen_config, 1.0).expect("generator factory must succeed");
631        assert_eq!(gen.value(0), 42.0);
632
633        let enc_config = EncoderConfig::PrometheusText { precision: None };
634        let _enc = create_encoder(&enc_config).expect("encoder factory must succeed");
635
636        let sink_config = SinkConfig::Stdout;
637        let _sink = create_sink(&sink_config, None).expect("sink factory must succeed");
638    }
639
640    #[test]
641    fn sonda_error_sink_display_includes_io_context() {
642        let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe broke");
643        let err = SondaError::Sink(io_err);
644        let msg = format!("{err}");
645        assert!(
646            msg.contains("sink error"),
647            "Sink variant display must include 'sink error', got: {msg}"
648        );
649        assert!(
650            msg.contains("pipe broke"),
651            "Sink variant display must include the I/O error message, got: {msg}"
652        );
653    }
654
655    // ---- Contract: error types are Send + Sync --------------------------------
656
657    #[test]
658    fn error_types_are_send_and_sync() {
659        fn assert_send_sync<T: Send + Sync>() {}
660        assert_send_sync::<SondaError>();
661        assert_send_sync::<ConfigError>();
662        assert_send_sync::<GeneratorError>();
663        assert_send_sync::<EncoderError>();
664        assert_send_sync::<RuntimeError>();
665    }
666}