Coverage Report

Created: 2026-08-08 07:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/adhd/audio_processor/src/config.rs
Line
Count
Source
1
// Copyright 2024 The ChromiumOS Authors
2
// Use of this source code is governed by a BSD-style license that can be
3
// found in the LICENSE file.
4
5
use std::fmt::Debug;
6
use std::path::PathBuf;
7
use std::rc::Rc;
8
use std::sync::mpsc::Sender;
9
10
use anyhow::bail;
11
use anyhow::Context;
12
use hound::WavSpec;
13
use hound::WavWriter;
14
use serde::Deserialize;
15
use serde::Serialize;
16
17
use crate::processors::peer::ManagedBlockingSeqPacketProcessor;
18
use crate::processors::peer::ThreadedWorkerFactory;
19
use crate::processors::peer::WorkerFactory;
20
use crate::processors::profile::Profile;
21
use crate::processors::profile::ProfileStats;
22
use crate::processors::ChunkWrapper;
23
use crate::processors::DynamicPluginProcessor;
24
use crate::processors::InPlaceNegateAudioProcessor;
25
use crate::processors::SpeexResampler;
26
use crate::AudioProcessor;
27
use crate::Format;
28
use crate::Pipeline;
29
30
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
31
pub enum Processor {
32
    Negate,
33
    WavSink {
34
        path: PathBuf,
35
    },
36
    Plugin {
37
        path: PathBuf,
38
        constructor: String,
39
    },
40
    WrapChunk {
41
        inner: Box<Processor>,
42
        inner_block_size: usize,
43
        /// Prevents merging with the outer pipeline even if they have the same block size.
44
        /// Used when the outer pipeline doesn't actually have a stable block size and the
45
        /// ChunkWrapper is used for regulating it.
46
        disallow_hoisting: bool,
47
    },
48
    Resample {
49
        output_frame_rate: usize,
50
    },
51
    Pipeline {
52
        processors: Vec<Processor>,
53
    },
54
    Preloaded(PreloadedProcessor),
55
    ShuffleChannels {
56
        channel_indexes: Vec<usize>,
57
    },
58
    /// Checks the current format of the pipeline.
59
    /// Does not actually builds a processor,
60
    /// so unlike `crate::processors::CheckShape`, does not perform checks
61
    /// when processing audio.
62
    CheckFormat {
63
        channels: Option<usize>,
64
        block_size: Option<usize>,
65
        frame_rate: Option<usize>,
66
    },
67
    Nothing,
68
    Peer {
69
        processor: Box<Processor>,
70
    },
71
}
72
73
/// PreloadedProcessor is a config that describes a processor that is already created
74
/// out of the config system.
75
pub struct PreloadedProcessor {
76
    pub description: &'static str,
77
    pub processor: Box<dyn AudioProcessor<I = f32, O = f32> + Send>,
78
}
79
80
impl Serialize for PreloadedProcessor {
81
0
    fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
82
0
    where
83
0
        S: serde::Serializer,
84
    {
85
0
        Err(serde::ser::Error::custom(
86
0
            "PreloadedProcessor cannot be serialized",
87
0
        ))
88
0
    }
89
}
90
91
impl<'de> Deserialize<'de> for PreloadedProcessor {
92
0
    fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
93
0
    where
94
0
        D: serde::Deserializer<'de>,
95
    {
96
0
        Err(serde::de::Error::custom(
97
0
            "PreloadedProcessor cannot be deserialized",
98
0
        ))
99
0
    }
100
}
101
102
impl Debug for PreloadedProcessor {
103
0
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104
0
        f.debug_struct("Preloaded")
105
0
            .field("description", &self.description)
106
0
            .finish()
107
0
    }
108
}
109
110
impl PartialEq for PreloadedProcessor {
111
0
    fn eq(&self, other: &Self) -> bool {
112
0
        std::ptr::eq(self, other)
113
0
    }
114
}
115
116
impl Eq for PreloadedProcessor {}
117
118
pub struct PipelineBuilder {
119
    pipeline: Pipeline,
120
    profile_sender: Option<Sender<ProfileStats>>,
121
    worker_factory: Rc<dyn WorkerFactory>,
122
}
123
124
impl PipelineBuilder {
125
0
    pub fn new(input_format: Format) -> Self {
126
0
        Self {
127
0
            pipeline: Pipeline::new(input_format),
128
0
            profile_sender: None,
129
0
            worker_factory: Rc::new(ThreadedWorkerFactory),
130
0
        }
131
0
    }
132
133
0
    pub fn build(mut self, config: Processor) -> anyhow::Result<Pipeline> {
134
0
        self.add(config)?;
135
0
        Ok(self.pipeline)
136
0
    }
137
138
    /// Enable profiling. The results are sent with sender when the pipeline
139
    /// is dropped.
140
    /// Currently only `Plugin`s are profiled.
141
0
    pub fn with_profile_sender(mut self, sender: Sender<ProfileStats>) -> Self {
142
0
        self.profile_sender = Some(sender);
143
0
        self
144
0
    }
145
146
    /// Use the factory to spawn peer workers.
147
0
    pub fn with_worker_factory(mut self, factory: impl WorkerFactory + 'static) -> Self {
148
0
        self.worker_factory = Rc::new(factory);
149
0
        self
150
0
    }
Unexecuted instantiation: <audio_processor::config::PipelineBuilder>::with_worker_factory::<_>
Unexecuted instantiation: <audio_processor::config::PipelineBuilder>::with_worker_factory::<audio_processor::processors::peer::managed::AudioWorkerSubprocessFactory>
151
152
0
    fn output_format(&self) -> Format {
153
0
        self.pipeline.get_output_format()
154
0
    }
155
156
0
    fn child_builder(&self, input_format: Format) -> Self {
157
0
        Self {
158
0
            pipeline: Pipeline::new(input_format),
159
0
            profile_sender: self.profile_sender.clone(),
160
0
            worker_factory: self.worker_factory.clone(),
161
0
        }
162
0
    }
163
164
0
    fn add(&mut self, config: Processor) -> anyhow::Result<()> {
165
        use Processor::*;
166
0
        match config {
167
0
            Negate => {
168
0
                self.pipeline
169
0
                    .add(InPlaceNegateAudioProcessor::new(self.output_format()));
170
0
            }
171
0
            WavSink { path } => {
172
0
                let output_format = self.output_format();
173
0
                self.pipeline.add(crate::processors::WavSink::new(
174
0
                    WavWriter::create(
175
0
                        &path,
176
                        WavSpec {
177
0
                            channels: output_format.channels.try_into().context("channels")?,
178
0
                            sample_format: hound::SampleFormat::Float,
179
0
                            sample_rate: output_format
180
0
                                .frame_rate
181
0
                                .try_into()
182
0
                                .context("sample_rate")?,
183
                            bits_per_sample: 32,
184
                        },
185
                    )
186
0
                    .context("WavWriter::create")?,
187
0
                    output_format.block_size,
188
                ));
189
            }
190
0
            Plugin { path, constructor } => {
191
0
                let plugin = DynamicPluginProcessor::new(
192
0
                    path.to_str().context("path.to_str")?,
193
0
                    &constructor,
194
0
                    self.output_format(),
195
                )
196
0
                .context("DynamicPluginProcessor::new")?;
197
0
                if let Some(sender) = &self.profile_sender {
198
0
                    let mut profile = Profile::new(plugin);
199
0
                    profile.set_key(format!(
200
0
                        "{}@{}",
201
                        constructor,
202
0
                        path.file_name()
203
0
                            .context("path.file_name() failed")?
204
0
                            .to_string_lossy(),
205
                    ));
206
0
                    profile.set_sender(sender.clone());
207
0
                    self.pipeline.add(profile);
208
0
                } else {
209
0
                    self.pipeline.add(plugin);
210
0
                }
211
            }
212
            WrapChunk {
213
0
                inner,
214
0
                inner_block_size,
215
0
                disallow_hoisting,
216
            } => {
217
0
                if self.output_format().block_size == inner_block_size && !disallow_hoisting {
218
0
                    self.add(*inner).context("inner")?;
219
                } else {
220
0
                    let inner_pipeline = self
221
0
                        .child_builder(Format {
222
0
                            block_size: inner_block_size,
223
0
                            ..self.output_format()
224
0
                        })
225
0
                        .build(*inner)
226
0
                        .context("inner")?;
227
228
0
                    let inner_channels = inner_pipeline.get_output_format().channels;
229
                    // TODO: When the inner_pipeline only has a single processor, wrap just that processor.
230
0
                    self.pipeline.add(ChunkWrapper::new(
231
0
                        inner_pipeline,
232
0
                        self.output_format().block_size,
233
0
                        inner_block_size,
234
0
                        self.output_format().channels,
235
0
                        inner_channels,
236
                    ));
237
                }
238
            }
239
0
            Resample { output_frame_rate } => {
240
0
                if self.output_format().frame_rate != output_frame_rate {
241
0
                    self.pipeline.add(
242
0
                        SpeexResampler::new(self.output_format(), output_frame_rate)
243
0
                            .context("SpeexResampler::new")?,
244
                    );
245
0
                }
246
            }
247
0
            Pipeline { processors } => {
248
0
                for (i, config) in processors.into_iter().enumerate() {
249
0
                    self.add(config)
250
0
                        .with_context(|| format!("pipeline processor#{i}"))?;
251
                }
252
            }
253
0
            Preloaded(PreloadedProcessor { processor, .. }) => {
254
0
                self.pipeline.vec.push(processor);
255
0
            }
256
0
            ShuffleChannels { channel_indexes } => {
257
                // Optimization: only shuffle channels if it would change the
258
                // channel layout.
259
0
                if !channel_indexes
260
0
                    .iter()
261
0
                    .cloned()
262
0
                    .eq(0..self.output_format().channels)
263
                {
264
0
                    self.pipeline.add(crate::processors::ShuffleChannels::new(
265
0
                        &channel_indexes,
266
0
                        self.output_format(),
267
                    ))
268
0
                }
269
            }
270
            CheckFormat {
271
0
                channels,
272
0
                block_size,
273
0
                frame_rate,
274
            } => {
275
0
                let format = self.output_format();
276
0
                if let Some(channels) = channels {
277
0
                    if channels != format.channels {
278
0
                        bail!("expected channels {channels:?} got {format:?}");
279
0
                    }
280
0
                }
281
0
                if let Some(block_size) = block_size {
282
0
                    if block_size != format.block_size {
283
0
                        bail!("expected block_size {block_size:?} got {format:?}");
284
0
                    }
285
0
                }
286
0
                if let Some(frame_rate) = frame_rate {
287
0
                    if frame_rate != format.frame_rate {
288
0
                        bail!("expected frame_rate {frame_rate:?} got {format:?}");
289
0
                    }
290
0
                }
291
            }
292
0
            Nothing => {
293
0
                // Do nothing.
294
0
            }
295
0
            Peer { processor } => {
296
0
                self.pipeline.add(
297
0
                    ManagedBlockingSeqPacketProcessor::new(
298
0
                        self.worker_factory.as_ref(),
299
0
                        self.output_format(),
300
0
                        *processor,
301
                    )
302
0
                    .context("ManagedBlockingSeqPacketProcessor::new")?,
303
                );
304
            }
305
        }
306
0
        Ok(())
307
0
    }
308
}
309
310
#[cfg(test)]
311
mod tests {
312
    use core::panic;
313
314
    use assert_matches::assert_matches;
315
    use hound::WavSpec;
316
317
    use crate::config::PipelineBuilder;
318
    use crate::config::PreloadedProcessor;
319
    use crate::config::Processor;
320
    use crate::processors::NegateAudioProcessor;
321
    use crate::util::read_wav;
322
    use crate::AudioProcessor;
323
    use crate::Format;
324
    use crate::MultiBuffer;
325
326
    #[test]
327
    fn simple_pipeline() {
328
        use Processor::*;
329
330
        let tempdir = tempfile::tempdir().unwrap();
331
332
        let config = Pipeline {
333
            processors: vec![
334
                WavSink {
335
                    path: tempdir.path().join("0.wav"),
336
                },
337
                Negate,
338
                WavSink {
339
                    path: tempdir.path().join("1.wav"),
340
                },
341
                WrapChunk {
342
                    inner: Box::new(Negate),
343
                    inner_block_size: 1,
344
                    disallow_hoisting: false,
345
                },
346
                WavSink {
347
                    path: tempdir.path().join("2.wav"),
348
                },
349
                WrapChunk {
350
                    inner: Box::new(Pipeline {
351
                        processors: vec![
352
                            WavSink {
353
                                path: tempdir.path().join("3.wav"),
354
                            },
355
                            Negate,
356
                            WavSink {
357
                                path: tempdir.path().join("4.wav"),
358
                            },
359
                        ],
360
                    }),
361
                    inner_block_size: 2,
362
                    disallow_hoisting: false,
363
                },
364
                WrapChunk {
365
                    inner_block_size: 5, // Same block size, should pass through.
366
                    inner: Box::new(WavSink {
367
                        path: tempdir.path().join("5.wav"),
368
                    }),
369
                    disallow_hoisting: false,
370
                },
371
                Resample {
372
                    output_frame_rate: 48000,
373
                },
374
                WavSink {
375
                    path: tempdir.path().join("6.wav"),
376
                },
377
            ],
378
        };
379
        let mut pipeline = PipelineBuilder::new(Format {
380
            channels: 2,
381
            block_size: 5,
382
            frame_rate: 24000,
383
        })
384
        .build(config)
385
        .unwrap();
386
387
        let mut input =
388
            MultiBuffer::from(vec![vec![1f32, 2., 3., 4., 5.], vec![6., 7., 8., 9., 10.]]);
389
        let output = MultiBuffer::from(pipeline.process(input.as_multi_slice()).unwrap());
390
391
        // Drop pipeline to flush WavSinks.
392
        drop(pipeline);
393
394
        let mut wavs = vec![];
395
        for i in 0..7 {
396
            let path = tempdir.path().join(format!("{i}.wav"));
397
            wavs.push(read_wav::<f32>(&path).unwrap());
398
        }
399
400
        assert_matches!(
401
            wavs[0].0,
402
            WavSpec {
403
                channels: 2,
404
                sample_rate: 24000,
405
                ..
406
            }
407
        );
408
        assert_eq!(
409
            wavs[0].1.to_vecs(),
410
            [[1.0, 2.0, 3.0, 4.0, 5.0], [6.0, 7.0, 8.0, 9.0, 10.0]]
411
        );
412
413
        assert_matches!(
414
            wavs[1].0,
415
            WavSpec {
416
                channels: 2,
417
                sample_rate: 24000,
418
                ..
419
            }
420
        );
421
        assert_eq!(
422
            wavs[1].1.to_vecs(),
423
            [
424
                [-1.0, -2.0, -3.0, -4.0, -5.0],
425
                [-6.0, -7.0, -8.0, -9.0, -10.0]
426
            ]
427
        );
428
429
        assert_matches!(
430
            wavs[2].0,
431
            WavSpec {
432
                channels: 2,
433
                sample_rate: 24000,
434
                ..
435
            }
436
        );
437
        assert_eq!(
438
            wavs[2].1.to_vecs(),
439
            [[0.0, 1.0, 2.0, 3.0, 4.0], [0.0, 6.0, 7.0, 8.0, 9.0]]
440
        );
441
442
        assert_matches!(
443
            wavs[3].0,
444
            WavSpec {
445
                channels: 2,
446
                sample_rate: 24000,
447
                ..
448
            }
449
        );
450
        assert_eq!(
451
            wavs[3].1.to_vecs(),
452
            [[0.0, 1.0, 2.0, 3.0], [0.0, 6.0, 7.0, 8.0]]
453
        );
454
455
        assert_matches!(
456
            wavs[4].0,
457
            WavSpec {
458
                channels: 2,
459
                sample_rate: 24000,
460
                ..
461
            }
462
        );
463
        assert_eq!(
464
            wavs[4].1.to_vecs(),
465
            [[-0.0, -1.0, -2.0, -3.0], [-0.0, -6.0, -7.0, -8.0]]
466
        );
467
468
        assert_matches!(
469
            wavs[5].0,
470
            WavSpec {
471
                channels: 2,
472
                sample_rate: 24000,
473
                ..
474
            }
475
        );
476
        assert_eq!(
477
            wavs[5].1.to_vecs(),
478
            [[0.0, 0.0, -0.0, -1.0, -2.0], [0.0, 0.0, -0.0, -6.0, -7.0]]
479
        );
480
481
        assert_matches!(
482
            wavs[6].0,
483
            WavSpec {
484
                channels: 2,
485
                sample_rate: 48000,
486
                ..
487
            }
488
        );
489
        assert_eq!(wavs[6].1.to_vecs(), output.to_vecs());
490
    }
491
492
    #[test]
493
    fn preloaded() {
494
        let mut input: MultiBuffer<f32> =
495
            MultiBuffer::from(vec![vec![1., 2., 3., 4.], vec![5., 6., 7., 8.]]);
496
497
        let input_format = Format {
498
            channels: 2,
499
            block_size: 4,
500
            frame_rate: 48000,
501
        };
502
503
        let mut pipeline = PipelineBuilder::new(input_format)
504
            .build(Processor::Preloaded(PreloadedProcessor {
505
                description: "preloaded negate",
506
                processor: Box::new(NegateAudioProcessor::new(input_format)),
507
            }))
508
            .unwrap();
509
510
        let output = pipeline.process(input.as_multi_slice()).unwrap();
511
512
        // output = abs(input)
513
        assert_eq!(
514
            output.into_raw(),
515
            [[-1., -2., -3., -4.], [-5., -6., -7., -8.]]
516
        );
517
    }
518
519
    #[test]
520
    fn shuffle_channels() {
521
        let mut pipeline = PipelineBuilder::new(Format {
522
            channels: 2,
523
            block_size: 2,
524
            frame_rate: 48000,
525
        })
526
        .build(Processor::ShuffleChannels {
527
            channel_indexes: vec![1, 0, 1],
528
        })
529
        .unwrap();
530
531
        let mut input = MultiBuffer::from(vec![vec![1., 2.], vec![3., 4.]]);
532
        let output = pipeline.process(input.as_multi_slice()).unwrap();
533
        assert_eq!(output.into_raw(), [[3., 4.], [1., 2.], [3., 4.]]);
534
    }
535
536
    #[test]
537
    fn shuffle_channels_opt() {
538
        let pipeline = PipelineBuilder::new(Format {
539
            channels: 2,
540
            block_size: 2,
541
            frame_rate: 48000,
542
        })
543
        .build(Processor::ShuffleChannels {
544
            channel_indexes: vec![1, 0],
545
        })
546
        .unwrap();
547
        assert_eq!(
548
            pipeline.vec.len(),
549
            1,
550
            "channel swap, should not be optimized away"
551
        );
552
553
        let pipeline = PipelineBuilder::new(Format {
554
            channels: 2,
555
            block_size: 2,
556
            frame_rate: 48000,
557
        })
558
        .build(Processor::ShuffleChannels {
559
            channel_indexes: vec![0, 1, 1],
560
        })
561
        .unwrap();
562
        assert_eq!(
563
            pipeline.vec.len(),
564
            1,
565
            "different length, should not be optimized away"
566
        );
567
568
        let pipeline = PipelineBuilder::new(Format {
569
            channels: 2,
570
            block_size: 2,
571
            frame_rate: 48000,
572
        })
573
        .build(Processor::ShuffleChannels {
574
            channel_indexes: vec![0],
575
        })
576
        .unwrap();
577
        assert_eq!(
578
            pipeline.vec.len(),
579
            1,
580
            "different length, should not be optimized away"
581
        );
582
583
        let pipeline = PipelineBuilder::new(Format {
584
            channels: 2,
585
            block_size: 2,
586
            frame_rate: 48000,
587
        })
588
        .build(Processor::ShuffleChannels {
589
            channel_indexes: vec![0, 1],
590
        })
591
        .unwrap();
592
        assert_eq!(pipeline.vec.len(), 0, "should optimize");
593
    }
594
595
    #[test]
596
    fn check_format() {
597
        let Err(err) = PipelineBuilder::new(Format {
598
            channels: 2,
599
            block_size: 2,
600
            frame_rate: 48000,
601
        })
602
        .build(Processor::CheckFormat {
603
            channels: Some(3),
604
            block_size: None,
605
            frame_rate: None,
606
        }) else {
607
            panic!("should fail");
608
        };
609
        assert!(err.to_string().contains("expected channels 3"), "{err}");
610
611
        let Err(err) = PipelineBuilder::new(Format {
612
            channels: 2,
613
            block_size: 2,
614
            frame_rate: 48000,
615
        })
616
        .build(Processor::CheckFormat {
617
            channels: None,
618
            block_size: Some(1),
619
            frame_rate: None,
620
        }) else {
621
            panic!("should fail");
622
        };
623
        assert!(err.to_string().contains("expected block_size 1"), "{err}");
624
625
        let Err(err) = PipelineBuilder::new(Format {
626
            channels: 2,
627
            block_size: 2,
628
            frame_rate: 48000,
629
        })
630
        .build(Processor::CheckFormat {
631
            channels: None,
632
            block_size: None,
633
            frame_rate: Some(99999),
634
        }) else {
635
            panic!("should fail");
636
        };
637
        assert!(
638
            err.to_string().contains("expected frame_rate 99999"),
639
            "{err}"
640
        );
641
    }
642
643
    #[test]
644
    fn peer() {
645
        let mut input: MultiBuffer<f32> =
646
            MultiBuffer::from(vec![vec![1., 2., 3., 4.], vec![5., 6., 7., 8.]]);
647
648
        let input_format = Format {
649
            channels: 2,
650
            block_size: 4,
651
            frame_rate: 48000,
652
        };
653
654
        let mut pipeline = PipelineBuilder::new(input_format)
655
            .build(Processor::Peer {
656
                processor: Box::new(Processor::Negate),
657
            })
658
            .unwrap();
659
660
        let output = pipeline.process(input.as_multi_slice()).unwrap();
661
662
        // output = abs(input)
663
        assert_eq!(
664
            output.into_raw(),
665
            [[-1., -2., -3., -4.], [-5., -6., -7., -8.]]
666
        );
667
    }
668
}
669
670
#[cfg(test)]
671
#[cfg(feature = "bazel")]
672
mod bazel_tests {
673
    use std::env;
674
    use std::sync::mpsc::channel;
675
    use std::time::Duration;
676
677
    use crate::config::PipelineBuilder;
678
    use crate::config::Processor;
679
    use crate::AudioProcessor;
680
    use crate::Format;
681
    use crate::MultiBuffer;
682
683
    #[test]
684
    fn abs_process() {
685
        let mut input: MultiBuffer<f32> =
686
            MultiBuffer::from(vec![vec![1., -2., 3., -4.], vec![5., -6., 7., -8.]]);
687
688
        let mut pipeline = PipelineBuilder::new(Format {
689
            channels: 2,
690
            block_size: 4,
691
            frame_rate: 48000,
692
        })
693
        .build(Processor::Plugin {
694
            path: std::env::var("LIBTEST_PLUGINS_SO").unwrap().into(),
695
            constructor: "abs_processor_create".into(),
696
        })
697
        .unwrap();
698
699
        let output = pipeline.process(input.as_multi_slice()).unwrap();
700
701
        // output = abs(input)
702
        assert_eq!(output.into_raw(), [[1., 2., 3., 4.], [5., 6., 7., 8.]]);
703
    }
704
705
    #[test]
706
    fn profile() {
707
        let test_plugin_path: std::path::PathBuf = env::var("LIBTEST_PLUGINS_SO").unwrap().into();
708
        let (sender, receiver) = channel();
709
710
        let pipeline = PipelineBuilder::new(Format {
711
            channels: 2,
712
            block_size: 2,
713
            frame_rate: 48000,
714
        })
715
        .with_profile_sender(sender)
716
        .build(Processor::Pipeline {
717
            processors: vec![
718
                Processor::Plugin {
719
                    path: test_plugin_path.clone(),
720
                    constructor: "abs_processor_create".into(),
721
                },
722
                Processor::WrapChunk {
723
                    inner: Box::new(Processor::Pipeline {
724
                        processors: vec![
725
                            Processor::Plugin {
726
                                path: test_plugin_path.clone(),
727
                                constructor: "negate_processor_create".into(),
728
                            },
729
                            Processor::Plugin {
730
                                path: test_plugin_path.clone(),
731
                                constructor: "echo_processor_create".into(),
732
                            },
733
                        ],
734
                    }),
735
                    inner_block_size: 4096,
736
                    disallow_hoisting: false,
737
                },
738
            ],
739
        })
740
        .unwrap();
741
742
        assert!(receiver.recv_timeout(Duration::ZERO).is_err());
743
744
        drop(pipeline);
745
        let mut keys = receiver
746
            .into_iter()
747
            .map(|stat| stat.key)
748
            .collect::<Vec<_>>();
749
        keys.sort();
750
        let basename = test_plugin_path.file_name().unwrap().to_str().unwrap();
751
        assert_eq!(
752
            keys,
753
            [
754
                format!("abs_processor_create@{basename}"),
755
                format!("echo_processor_create@{basename}"),
756
                format!("negate_processor_create@{basename}")
757
            ]
758
        );
759
    }
760
}