munin_plugin/
lib.rs

1//! munin-plugin - Simple writing of plugins for munin in Rust
2//!
3//! SPDX-License-Identifier: MIT AND Apache-2.0
4//! Copyright (C) 2022 Joerg Jaspert <joerg@ganneff.de>
5//!
6//! # About
7
8//! Simple way to write munin plugins. There are basically two types of plugins,
9//! - **Simple** or **standard** ones, those are called once every munin
10//! run and gather and output there data at that time. Usually every 5
11//! minutes.
12//! - **Streaming** ones, those daemonize themself and _continuously_
13//! gather data, usually caching it in a file, and when munin comes
14//! around after 5 minutes again, they output everything they gathered
15//! in the meantime.
16//!
17//! Those _streaming_ plugins are needed/useful, when graphs with
18//! resolutions down to the second, rather than the default 5 minutes,
19//! should be created.
20//!
21//! Both types of plugins have to follow all the usual rules for munin
22//! plugins, such as outputting their data to stdout and reacting to
23//! the `config` parameter to print their munin graph configuration.
24//!
25//! # Repositories / plugins using this code
26//! - [Simple munin plugin to graph load](https://github.com/Ganneff/munin-load)
27//! - [Munin CPU graph with 1second resolution](https://github.com/Ganneff/cpu1sec/)
28//! - [Munin Interface graph with 1second resolution](https://github.com/Ganneff/if1sec)
29//!
30//! # Usage
31
32//! This library tries to abstract as much of the details away, so you
33//! can concentrate on the actual task - defining how the graph should
34//! appear and gathering the data. For that, you need to implement the
35//! [MuninPlugin] trait and provide the two functions `config` and
36//! `acquire`, all the rest are provided with a (hopefully) useful
37//! default implementation.
38//!
39//! ## config()
40//! The _config_ function will be called whenever the plugin gets
41//! called with the config argument from munin. This happens on every
42//! munin run, which usually happens every 5 minutes. It is expected
43//! to print out a munin graph configuration and you can find details
44//! on possible values to print at [the Munin Plugin
45//! Guide](http://guide.munin-monitoring.org/en/latest/plugin/writing.html).
46//! For some basics you can also look into the examples throughout
47//! this lib.
48//!
49//! **Note**: Streaming plugins should take care of correctly setting
50//! munins `graph_data_size` and `update_rate` option. Those is the
51//! difference in their configuration compared to standard plugins!
52//!
53//! ## acquire()
54//!
55//! The _acquire_ function will be called whenever the plugin needs to
56//! gather data. For a _standard_ plugin that will be once every 5
57//! minutes. A _streaming_ plugin will call this function once every
58//! second.
59//!
60//! In both cases, _standard_ and _streaming_, you should do whatever
61//! is needed to gather the data and then write it to the provided
62//! handle, this library will take care of either handing it directly
63//! to munin on stdout (_standard_) or storing it in a cache file
64//! (_streaming_), to hand it out whenever munin comes around to fetch
65//! the data.
66//!
67//! The format to write the data in is the one munin expects,
68//! - _standard_: fieldname.value VALUE
69//! - _streaming_: fieldname.value EPOCH:VALUE
70//! where fieldname matches the config output, EPOCH is the
71//! unix epoch in seconds and VALUE is whatever value got
72//! calculated.
73//!
74//! # Example
75//! The following implements the **load** plugin from munin, graphing
76//! the load average of the system, using the 5-minute value. As
77//! implemented, it expects to be run by munin every 5 minutes,
78//! usually munin will first run it with the config parameter,
79//! followed by no parameter to fetch data. If munin-node supports it
80//! and the capability _dirtyconfig_ is set, config will also print
81//! out data (this library handles that for you).
82//!
83//! It is a shortened version of the plugin linked above (Simple munin
84//! plugin to graph load), with things like logging dropped.
85//!
86//! For more example code look into the actual [MuninPlugin] trait and
87//! its function definitions.
88//!
89//! ```rust
90//! use anyhow::Result;
91//! use munin_plugin::{Config, MuninPlugin};
92//! use procfs::LoadAverage;
93//! use std::io::{self, BufWriter, Write};
94//!
95//! // Our plugin struct
96//! #[derive(Debug)]
97//! struct LoadPlugin;
98//!
99//! // Implement the needed functions
100//! impl MuninPlugin for LoadPlugin {
101//!     // Write out munin config. handle is setup as a bufwriter to stdout.
102//!     fn config<W: Write>(&self, handle: &mut BufWriter<W>) -> Result<()> {
103//!        writeln!(handle, "graph_title Load average")?;
104//!        writeln!(handle, "graph_args --base 1000 -l 0")?;
105//!        writeln!(handle, "graph_vlabel load")?;
106//!        writeln!(handle, "graph_scale no")?;
107//!        writeln!(handle, "graph_category system")?;
108//!        writeln!(handle, "load.label load")?;
109//!        writeln!(handle, "load.warning 10")?;
110//!        writeln!(handle, "load.critical 120")?;
111//!        writeln!(handle, "graph_info The load average of the machine describes how many processes are in the run-queue (scheduled to run immediately.")?;
112//!        writeln!(handle, "load.info Average load for the five minutes.")?;
113//!        Ok(())
114//!     }
115//!
116//!     // Calculate data (we want the 5-minute load average) and write it to the handle.
117//!     fn acquire<W: Write>(&mut self, handle: &mut BufWriter<W>, _config: &Config, _epoch: u64) -> Result<()> {
118//!         let load = (LoadAverage::new().unwrap().five * 100.0) as isize;
119//!         writeln!(handle, "load.value {}", load)?;
120//!         Ok(())
121//!     }
122//! }
123//!
124//! // The actual program start point
125//! fn main() -> Result<()> {
126//!     // Get our Plugin
127//!     let mut load = LoadPlugin;
128//!     // And let it do the work.
129//!     load.simple_start(String::from("load"))?;
130//!     Ok(())
131//! }
132//! ```
133//!
134//! # Logging
135//! This crate uses the default [log] crate to output log messages of
136//! level trace. If you want to see them, select a log framework you
137//! like and ensure its level will display trace messages. See
138//! that frameworks documentation on how to setup/include it.
139//!
140//! If you do not want/need log output, just do nothing.
141
142// Tell us if we forget to document things
143#![warn(missing_docs)]
144// We do not want to write unsafe code
145#![forbid(unsafe_code)]
146
147pub mod config;
148pub use crate::config::Config;
149
150use anyhow::{anyhow, Result};
151// daemonize
152use fs2::FileExt;
153use log::{trace, warn};
154// daemonize
155use daemonize::Daemonize;
156use spin_sleep::LoopHelper;
157use std::{
158    env,
159    io::{self, BufWriter, Write},
160    path::Path,
161};
162// daemonize
163use std::{
164    fs::{rename, OpenOptions},
165    process::{Command, Stdio},
166    thread,
167    time::{Duration, SystemTime, UNIX_EPOCH},
168};
169// daemonize
170use tempfile::NamedTempFile;
171
172/// Defines a Munin Plugin and the needed functions
173pub trait MuninPlugin {
174    /// Write out a munin config, read the [Developing
175    /// plugins](http://guide.munin-monitoring.org/en/latest/develop/plugins/index.html)
176    /// guide from munin for everything you can print out here.
177    ///
178    /// Note that munin expects this to appear on stdout, so the
179    /// plugin gives you a handle to write to, which is setup as a
180    /// [std::io::BufWriter] to stdout. The [std::io::BufWriter]
181    /// capacity defaults to 8192 bytes, but if you need more, its
182    /// size can be set using [Config::config_size]. An example where this
183    /// may be useful is a munin multigraph plugin that outputs config
184    /// for many graphs.
185    ///
186    /// # Example
187    /// ```rust
188    /// # pub use munin_plugin::*;
189    /// # use anyhow::{anyhow, Result};
190    /// # use std::{
191    /// # env,
192    /// # io::{self, BufWriter, Write},
193    /// # };
194    /// # struct LoadPlugin;
195    /// # impl MuninPlugin for LoadPlugin {
196    /// # fn acquire<W: Write>(&mut self, handle: &mut BufWriter<W>, config: &Config, epoch: u64) -> Result<()> { todo!() }
197    /// # fn fetch<W: Write>(&mut self, handle: &mut BufWriter<W>, config: &Config) -> Result<()> { todo!() }
198    /// fn config<W: Write>(&self, handle: &mut BufWriter<W>) -> Result<()> {
199    ///     writeln!(handle, "graph_title Load average")?;
200    ///     writeln!(handle, "graph_args --base 1000 -l 0")?;
201    ///     writeln!(handle, "graph_vlabel load")?;
202    ///     writeln!(handle, "graph_scale no")?;
203    ///     writeln!(handle, "graph_category system")?;
204    ///     writeln!(handle, "load.label load")?;
205    ///     writeln!(handle, "load.warning 10")?;
206    ///     writeln!(handle, "load.critical 120")?;
207    ///     writeln!(handle, "graph_info The load average of the machine describes how many processes are in the run-queue (scheduled to run immediately.")?;
208    ///     writeln!(handle, "load.info Average load for the five minutes.")?;
209    ///     Ok(())
210    /// }
211    /// # }
212    /// ```
213    fn config<W: Write>(&self, handle: &mut BufWriter<W>) -> Result<()>;
214
215    /// Acquire data
216    ///
217    /// Acquire is called whenever data should be gathered. For a
218    /// _standard_ plugin this will be every 5 minutes, a _streaming_
219    /// plugin will call acquire once a second. Acquire is expected to
220    /// do whatever is neccessary to gather the data that the plugin
221    /// is supposed to gather. It should writeln!() it to the provided
222    /// handle, which - depending on the plugin type - will either be
223    /// connected to stdout or a cachefile. The data written out has
224    /// to be in munin compatible format:
225    /// - _standard_ plugin: fieldname.value VALUE
226    /// - _streaming_ plugin: fieldname.value EPOCH:VALUE
227    /// where fieldname matches the config output, EPOCH is the unix
228    /// epoch in seconds and VALUE is whatever value got calculated.
229    ///
230    /// # Example 1, _standard_ plugin
231    /// ```rust
232    /// # pub use munin_plugin::*;
233    /// # use procfs::LoadAverage;
234    /// # use anyhow::{anyhow, Result};
235    /// # use std::{
236    /// # env,
237    /// # fs::{rename, OpenOptions},
238    /// # io::{self, BufWriter, Write},
239    /// # path::{Path, PathBuf},
240    /// # time::{SystemTime, UNIX_EPOCH},
241    /// # };
242    /// # struct InterfacePlugin {
243    /// #   interface: String,
244    /// #   cache: PathBuf,
245    /// #   if_txbytes: PathBuf,
246    /// #   if_rxbytes: PathBuf,
247    /// # };
248    /// # impl MuninPlugin for InterfacePlugin {
249    /// # fn fetch<W: Write>(&mut self, handle: &mut BufWriter<W>, config: &Config) -> Result<()> { todo!() }
250    /// # fn config<W: Write>(&self, handle: &mut BufWriter<W>) -> Result<()> { todo!() }
251    /// fn acquire<W: Write>(&mut self, handle: &mut BufWriter<W>, config: &Config, epoch: u64) -> Result<()> {
252    ///     let load = (LoadAverage::new().unwrap().five * 100.0) as isize;
253    ///     writeln!(handle, "load.value {}", load)?;
254    ///     Ok(())
255    /// }
256    /// # }
257    /// ```
258    ///
259    /// # Example 2, _streaming_ plugin
260    /// ```rust
261    /// # pub use munin_plugin::*;
262    /// # use anyhow::{anyhow, Result};
263    /// # use std::{
264    /// # env,
265    /// # fs::{rename, OpenOptions},
266    /// # io::{self, BufWriter, Write},
267    /// # path::{Path, PathBuf},
268    /// # time::{SystemTime, UNIX_EPOCH},
269    /// # };
270    /// # struct InterfacePlugin {
271    /// #   interface: String,
272    /// #   cache: PathBuf,
273    /// #   if_txbytes: PathBuf,
274    /// #   if_rxbytes: PathBuf,
275    /// # };
276    /// # impl MuninPlugin for InterfacePlugin {
277    /// # fn fetch<W: Write>(&mut self, handle: &mut BufWriter<W>, config: &Config) -> Result<()> { todo!() }
278    /// # fn config<W: Write>(&self, handle: &mut BufWriter<W>) -> Result<()> { todo!() }
279    /// fn acquire<W: Write>(&mut self, handle: &mut BufWriter<W>, config: &Config, epoch: u64) -> Result<()> {
280    ///     // Read in the received and transferred bytes, store as u64
281    ///     let rx: u64 = std::fs::read_to_string(&self.if_rxbytes)?.trim().parse()?;
282    ///     let tx: u64 = std::fs::read_to_string(&self.if_txbytes)?.trim().parse()?;
283    ///
284    ///     // And now write out values
285    ///     writeln!(handle, "{0}_tx.value {1}:{2}", self.interface, epoch, tx)?;
286    ///     writeln!(handle, "{0}_rx.value {1}:{2}", self.interface, epoch, rx)?;
287    ///
288    ///     Ok(())
289    /// }
290    /// # }
291    /// ```
292    fn acquire<W: Write>(
293        &mut self,
294        handle: &mut BufWriter<W>,
295        config: &Config,
296        epoch: u64,
297    ) -> Result<()>;
298
299    /// Daemonize
300    ///
301    /// This function is called whenever the plugin gets run with the
302    /// acquire argument. That usually happens on fetch and acquire
303    /// gets run in the background. `daemon()` will lock its pidfile,
304    /// to show it is running, start a loop, run once a second,
305    /// calling [MuninPlugin::acquire].
306    #[cfg(not(tarpaulin_include))]
307    fn daemon(&mut self, config: &Config) -> Result<()> {
308        // Need to run as daemon/forked in backgreound, so prepare
309        let daemonize = Daemonize::new()
310            .pid_file(&config.pidfile)
311            .chown_pid_file(true)
312            .working_directory("/tmp");
313
314        daemonize.start()?;
315
316        // Repeat once per second
317        let mut loop_helper = LoopHelper::builder().build_with_target_rate(1);
318
319        // We run forever
320        loop {
321            // Let loop helper prepare
322            loop_helper.loop_start();
323
324            // Streaming plugins need the epoch, so provide it
325            let epoch = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); // without the nanosecond part
326
327            // Own scope, so file is closed before we sleep. Ensures
328            // we won't have a file open, that fetch just moved away
329            // to send out to munin.
330            {
331                // Open the munin cachefile to store our values, using
332                // a BufWriter to "collect" the writeln!() in acquire
333                // together
334                let mut handle = BufWriter::with_capacity(
335                    config.fetch_size,
336                    OpenOptions::new()
337                        .create(true) // If not there, create
338                        .write(true) // We want to write
339                        .append(true) // We want to append
340                        .open(&config.plugin_cache)?,
341                );
342
343                self.acquire(&mut handle, config, epoch)?;
344            }
345            // Sleep for the rest of the second
346            loop_helper.loop_sleep();
347        }
348    }
349
350    /// Fetch delivers actual data to munin. This is called whenever
351    /// the plugin is called without an argument. If the
352    /// [config::Config::dirtyconfig] setting is true (auto-detected from
353    /// environment set by munin), this will also be called right
354    /// after having called [MuninPlugin::config].
355    ///
356    /// The size of the BufWriter this function uses is configurable
357    /// from [Config::fetch_size].
358    ///
359    /// This function will adjust its behaviour based on the plugin
360    /// being a _standard_ or _streaming_ plugin. For _standard_ plugins
361    /// it will simply call acquire, so data is gathered and written
362    /// to the provided handle (and as such, to stdout where munin
363    /// expects it).
364    ///
365    /// For _streaming_ plugins it will create a temporary file beside
366    /// the [config::Config::plugin_cache], will rename the
367    /// [config::Config::plugin_cache] and then use [std::io::copy] to
368    /// "copy" the data to the provided handle.
369    ///
370    /// # Overriding this function
371    /// If you want to override this function, you should ensure that
372    /// (for _streaming_ plugins) you ensure that the cache file is
373    /// reset, whenever `fetch()` runs, or old data may be given to
374    /// munin needlessly. You also need to ensure to not accidently
375    /// deleting data when dealing with your cachefile. For example:
376    /// You read the whole cachefile, then output it to munin, then
377    /// delete it - and during the halfsecond this took, new data
378    /// appeared in the file, now lost.
379    fn fetch<W: Write>(&mut self, handle: &mut BufWriter<W>, config: &Config) -> Result<()> {
380        // Daemonize means plugin writes a cachefile, so lets output that
381        if config.daemonize {
382            // We need a temporary file
383            let fetchpath = NamedTempFile::new_in(&config.plugin_statedir)?;
384            // Rename the cache file, to ensure that acquire doesn't add data
385            // between us outputting data and deleting the file
386            rename(&config.plugin_cache, &fetchpath)?;
387            // Want to read the tempfile now
388            let mut fetchfile = std::fs::File::open(&fetchpath)?;
389            // And ask io::copy to just take it all and shove it into the handle
390            io::copy(&mut fetchfile, handle)?;
391        } else {
392            // Not daemonizing, plugin gathers data and wants to output it directly.
393            // So we just call acquire, which is expected to write its data to handle.
394            self.acquire(handle, config, 0)?;
395        }
396        Ok(())
397    }
398
399    /// Check whatever is neccessary to decide if the plugin can
400    /// auto-configure itself.
401    ///
402    /// For example a network load plugin may check if network
403    /// interfaces exists and then return true, something presenting
404    /// values of a daemon like apache or ntp may check if that is
405    /// installed - and possibly if fetching values is possible.
406    ///
407    /// If this function is not overwritten, it defaults to false.
408    fn check_autoconf(&self) -> bool {
409        false
410    }
411
412    /// Tell munin if the plugin supports autoconf.
413    ///
414    /// Munin expects a simple yes or no on stdout, so we just print
415    /// it, depending on the return value of
416    /// [MuninPlugin::check_autoconf]. The default of that is a plain
417    /// false. If it is possible for your plugin to detect, if it can
418    /// autoconfigure itself, then implement the logic in
419    /// [MuninPlugin::check_autoconf] and have it return true.
420    #[cfg(not(tarpaulin_include))]
421    fn autoconf(&self) {
422        if self.check_autoconf() {
423            println!("yes")
424        } else {
425            println!("no")
426        }
427    }
428
429    /// A simplified start, only need a name, for the rest, defaults are fine.
430    ///
431    /// This is just a tiny bit of "being lazy is good" and will
432    /// create the [MuninPlugin::config] with the given name, then
433    /// call the real start function. Only useful for plugins that do
434    /// not use daemonization or need other config changes to run
435    /// successfully..
436    #[cfg(not(tarpaulin_include))]
437    fn simple_start(&mut self, name: String) -> Result<bool> {
438        trace!("Simple Start, setting up config");
439        let config = Config::new(name);
440        trace!("Plugin: {:#?}", config);
441
442        self.start(config)?;
443        Ok(true)
444    }
445
446    /// The main plugin function, this will deal with parsing
447    /// commandline arguments and doing what is expected of the plugin
448    /// (present config, fetch values, whatever).
449    #[cfg(not(tarpaulin_include))]
450    fn start(&mut self, config: Config) -> Result<bool> {
451        trace!("Plugin start");
452        trace!("My plugin config: {config:#?}");
453
454        // Store arguments for (possible) later use
455        let args: Vec<String> = env::args().collect();
456
457        // Now go over the args and see what we are supposed to do
458        match args.len() {
459            // no arguments passed, print data
460            1 => {
461                trace!("No argument, assuming fetch");
462                if config.daemonize {
463                    // For daemonization we need to check if a copy of us
464                    // with the acquire arg already runs. We do this by
465                    // trying to lock our pidfile. If that works, nothing
466                    // is running, then we need to start us in the
467                    // background.
468                    let lockfile = !Path::exists(&config.pidfile) || {
469                        let lockedfile = OpenOptions::new()
470                            .create(true)
471                            .write(true)
472                            .open(&config.pidfile)?;
473                        lockedfile.try_lock_exclusive().is_ok()
474                    };
475                    // If we could lock, it appears that acquire isn't running. Start it.
476                    if lockfile {
477                        trace!("Could lock the pidfile, will spawn acquire now");
478                        Command::new(&args[0])
479                            .arg("acquire")
480                            .stdin(Stdio::null())
481                            .stdout(Stdio::null())
482                            .stderr(Stdio::null())
483                            .spawn()?;
484                        trace!("Spawned, sleep for 1s, then continue");
485                        // Now we wait one second before going on, so the
486                        // newly spawned process had a chance to generate us
487                        // some data
488                        thread::sleep(Duration::from_secs(1));
489                    }
490                }
491                // Daemonized or not, fetch means handing out data, so lets do this.
492                trace!("Calling fetch");
493                // We want to write a possibly large amount to stdout, take and lock it
494                let stdout = io::stdout();
495                // Buffered writer, to gather multiple small writes together
496                let mut handle = BufWriter::with_capacity(config.fetch_size, stdout.lock());
497                // And give us data, please
498                self.fetch(&mut handle, &config)?;
499                trace!("Done");
500                // And flush the handle, so it can also deal with possible errors
501                handle.flush()?;
502
503                return Ok(true);
504            }
505            // Argument passed, check which one and act accordingly
506            2 => match args[1].as_str() {
507                "config" => {
508                    // We want to write a possibly large amount to stdout, take and lock it
509                    let stdout = io::stdout();
510                    {
511                        // Buffered writer, to gather multiple small writes together
512                        let mut handle =
513                            BufWriter::with_capacity(config.config_size, stdout.lock());
514                        self.config(&mut handle)?;
515                        // And flush the handle, so it can also deal with possible errors
516                        handle.flush()?;
517                    }
518                    // If munin supports dirtyconfig, send the data now
519                    if config.dirtyconfig {
520                        trace!("Munin supports dirtyconfig, sending data now");
521                        let mut handle = BufWriter::with_capacity(config.fetch_size, stdout.lock());
522                        self.fetch(&mut handle, &config)?;
523                        // And flush the handle, so it can also deal with possible errors
524                        handle.flush()?;
525                    }
526                    return Ok(true);
527                }
528                "autoconf" => {
529                    self.autoconf();
530                    return Ok(true);
531                }
532                "acquire" => {
533                    trace!("Called acquire to gather data, will run loop forever");
534                    // Will only ever process anything after this line, if
535                    // one process has our pidfile already locked, ie. if
536                    // another acquire is running. (Or if we can not
537                    // daemonize for another reason).
538                    if let Err(e) = self.daemon(&config) {
539                        return Err(anyhow!(
540                            "Could not start plugin {} in daemon mode to gather data - already running? ({})",
541                            config.plugin_name,
542                            e
543                        ));
544                    };
545                }
546                &_ => trace!("Unsupported argument: {}", args[1]),
547            },
548            // Whatever else
549            _ => return Err(anyhow!("No argument given")),
550        }
551        Ok(true)
552    }
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558
559    // Our plugin struct
560    #[derive(Debug)]
561    struct TestPlugin;
562    impl MuninPlugin for TestPlugin {
563        fn config<W: Write>(&self, handle: &mut BufWriter<W>) -> Result<()> {
564            writeln!(handle, "This is a test plugin")?;
565            writeln!(handle, "There is no config")?;
566            Ok(())
567        }
568        fn acquire<W: Write>(
569            &mut self,
570            handle: &mut BufWriter<W>,
571            config: &Config,
572            epoch: u64,
573        ) -> Result<()> {
574            writeln!(handle, "This is a value for {}", config.plugin_name)?;
575            writeln!(handle, "And one more value with epoch {}", epoch)?;
576            Ok(())
577        }
578    }
579
580    #[test]
581    fn test_config() {
582        let test = TestPlugin;
583
584        // We want to check the output of config contains our test string
585        // above, so have it "write" it to a variable, then check if
586        // the variable contains what we want
587        let checktext = Vec::new();
588        let mut handle = BufWriter::new(checktext);
589        test.config(&mut handle).unwrap();
590        handle.flush().unwrap();
591
592        // And now check what got "written" into the variable
593        let (recovered_writer, _buffered_data) = handle.into_parts();
594        let output = String::from_utf8(recovered_writer).unwrap();
595        assert_eq!(
596            output,
597            String::from("This is a test plugin\nThere is no config\n")
598        );
599    }
600
601    #[test]
602    fn test_fetch_standard() {
603        let mut test = TestPlugin;
604
605        // We want to check the output of fetch contains our test string
606        // above, so have it "write" it to a variable, then check if
607        // the variable contains what we want
608        let checktext = Vec::new();
609        let mut handle = BufWriter::new(checktext);
610        test.fetch(&mut handle, &config::Config::new("test".to_string()))
611            .unwrap();
612        handle.flush().unwrap();
613
614        // And now check what got "written" into the variable
615        let (recovered_writer, _buffered_data) = handle.into_parts();
616        let output = String::from_utf8(recovered_writer).unwrap();
617        assert_eq!(
618            output,
619            String::from("This is a value for test\nAnd one more value with epoch 0\n")
620        );
621    }
622
623    #[test]
624    fn test_fetch_streaming() {
625        let mut config = Config::new(String::from("testplugin"));
626        config.daemonize = true;
627        config.fetch_size = 16384;
628
629        let mut test = TestPlugin {};
630
631        // We need a temporary file
632        let fetchpath = NamedTempFile::new_in(
633            config
634                .plugin_cache
635                .parent()
636                .expect("Could not find useful temp path"),
637        )
638        .unwrap();
639
640        {
641            // Setup a bufwriter as daemon() does.
642            let mut handle = BufWriter::with_capacity(
643                config.fetch_size,
644                OpenOptions::new()
645                    .create(true) // If not there, create
646                    .write(true) // We want to write
647                    .append(true) // We want to append
648                    .open(&fetchpath)
649                    .unwrap(),
650            );
651
652            // And have acquire write to it
653            test.acquire(&mut handle, &config, 42).unwrap();
654        }
655
656        // And we want to access the tempfile and read from it
657        (_, config.plugin_cache) = fetchpath.keep().unwrap();
658        let checktext = Vec::new();
659        let mut handle = BufWriter::new(checktext);
660
661        test.fetch(&mut handle, &config).unwrap();
662        handle.flush().unwrap();
663        let (recovered_writer, _buffered_data) = handle.into_parts();
664        let output = String::from_utf8(recovered_writer).unwrap();
665
666        assert_eq!(
667            output,
668            String::from("This is a value for testplugin\nAnd one more value with epoch 42\n")
669        );
670    }
671
672    #[test]
673    // Kind of silly, its always false
674    fn test_check_autoconf() {
675        let test = TestPlugin;
676        assert!(!test.check_autoconf());
677    }
678}