file_rotation/
asynchronous.rs

1//! Write output to a file and rotate the files when limits have been exceeded.
2//!
3//! Defines a simple [tokio::io::Write] object that you can plug into your writers as middleware.
4//!
5//! # Rotating by Lines #
6//!
7//! We can rotate log files by using the amount of lines as a limit.
8//!
9//! ```
10//! use file_rotation::asynchronous::{FileRotate, RotationMode};
11//! use tokio::{fs, io::AsyncWriteExt};
12//! use tokio_test;
13//!
14//! tokio_test::block_on(async {
15//!   // Create a directory to store our logs, this is not strictly needed but shows how we can
16//!   // arbitrary paths.
17//!   fs::create_dir("target/async-my-log-directory-lines").await.unwrap();
18//!
19//!   // Create a new log writer. The first argument is anything resembling a path. The
20//!   // basename is used for naming the log files.
21//!   //
22//!   // Here we choose to limit logs by 10 lines, and have at most 2 rotated log files. This
23//!   // makes the total amount of log files 4, since the original file is present as well as
24//!   // file 0.
25//!   let mut log = FileRotate::new("target/async-my-log-directory-lines/my-log-file", RotationMode::Lines(3), 2).await.unwrap();
26//!
27//!   // Write a bunch of lines
28//!   log.write("Line 1: Hello World!\n".as_bytes()).await;
29//!   for idx in 2..11 {
30//!     log.write(format!("Line {}\n", idx).as_bytes()).await;
31//!   }
32//!
33//!   assert_eq!("Line 10\n", fs::read_to_string("target/async-my-log-directory-lines/my-log-file").await.unwrap());
34//!
35//!   assert_eq!("Line 1: Hello World!\nLine 2\nLine 3\n", fs::read_to_string("target/async-my-log-directory-lines/my-log-file.0").await.unwrap());
36//!   assert_eq!("Line 4\nLine 5\nLine 6\n", fs::read_to_string("target/async-my-log-directory-lines/my-log-file.1").await.unwrap());
37//!   //assert_eq!("Line 7\nLine 8\nLine 9\n", fs::read_to_string("target/async-my-log-directory-lines/my-log-file.2").await.unwrap());
38//!
39//!   fs::remove_dir_all("target/async-my-log-directory-lines").await;
40//! });
41//! ```
42//!
43//!
44//! # Rotating by Bytes surpassing a threshold, but without splitting a buffer mid-way#
45//!
46//! We can rotate log files but never splitting a buffer half-way. This means a single buffer may
47//! end up surpassing the number of expected bytes in a file, but that entire buffer will be
48//! written. When the file surpasses the number of bytes to rotate, it'll be rotated for the
49//! next buffer.
50//!
51//! When lines are written in a single buffer as demonstrated below, this ensures the logs
52//! contain complete lines which do not split across files.
53//!
54//! ```
55//! use file_rotation::asynchronous::{FileRotate, RotationMode};
56//! use tokio::{fs, io::AsyncWriteExt};
57//! use tokio_test;
58//!
59//! tokio_test::block_on(async {
60//!
61//!   // Create a directory to store our logs, this is not strictly needed but shows how we can
62//!   // arbitrary paths.
63//!   fs::create_dir("target/async-my-log-directory-lines").await.unwrap();
64//!
65//!   // Create a new log writer. The first argument is anything resembling a path. The
66//!   // basename is used for naming the log files.
67//!   //
68//!   // Here we choose to limit logs by 10 lines, and have at most 2 rotated log files. This
69//!   // makes the total amount of log files 4, since the original file is present as well as
70//!   // file 0.
71//!   let mut log = FileRotate::new("target/async-my-log-directory-lines/my-log-file", RotationMode::BytesSurpassed(2), 2).await.unwrap();
72//!
73//!   // Write a bunch of lines
74//!   log.write("Line 1: Hello World!\n".as_bytes()).await;
75//!   for idx in 2..11 {
76//!     log.write(format!("Line {}", idx).as_bytes()).await;
77//!   }
78//!
79//!   // the latest file is empty - since the previous file surpassed bytes and was rotated out
80//!   assert_eq!("", fs::read_to_string("target/async-my-log-directory-lines/my-log-file").await.unwrap());
81//!
82//!   assert_eq!("Line 10", fs::read_to_string("target/async-my-log-directory-lines/my-log-file.0").await.unwrap());
83//!   assert_eq!("Line 8", fs::read_to_string("target/async-my-log-directory-lines/my-log-file.1").await.unwrap());
84//!   assert_eq!("Line 9", fs::read_to_string("target/async-my-log-directory-lines/my-log-file.2").await.unwrap());
85//!
86//!   fs::remove_dir_all("target/async-my-log-directory-lines").await;
87//! });
88//! ```
89//!
90//!
91//! # Rotating by Bytes #
92//!
93//! Another method of rotation is by bytes instead of lines.
94//!
95//! ```
96//! use file_rotation::asynchronous::{FileRotate, RotationMode};
97//! use tokio::{fs, io::AsyncWriteExt};
98//! use tokio_test;
99//!
100//! tokio_test::block_on(async {
101//!   fs::create_dir("target/async-my-log-directory-bytes").await;
102//!
103//!   let mut log = FileRotate::new("target/async-my-log-directory-bytes/my-log-file", RotationMode::Bytes(5), 2).await.unwrap();
104//!
105//!   log.write("Test file\n".as_bytes()).await;
106//!
107//!   assert_eq!("Test ", fs::read_to_string("target/async-my-log-directory-bytes/my-log-file.0").await.unwrap());
108//!   assert_eq!("file\n", fs::read_to_string("target/async-my-log-directory-bytes/my-log-file").await.unwrap());
109//!
110//!   fs::remove_dir_all("target/async-my-log-directory-bytes").await;
111//! });
112//! ```
113//!
114//! # Rotation Method #
115//!
116//! The rotation method used is to always write to the base path, and then move the file to a new
117//! location when the limit is exceeded. The moving occurs in the sequence 0, 1, 2, n, 0, 1, 2...
118//!
119//! Here's an example with 1 byte limits:
120//!
121//! ```
122//! use file_rotation::asynchronous::{FileRotate, RotationMode};
123//! use tokio::{fs, io::AsyncWriteExt};
124//! use tokio_test;
125//!
126//! tokio_test::block_on(async {
127//!   fs::create_dir("target/async-my-log-directory-small").await;
128//!
129//!   let mut log = FileRotate::new("target/async-my-log-directory-small/my-log-file", RotationMode::Bytes(1), 3).await.unwrap();
130//!
131//!   log.write("A".as_bytes()).await;
132//!   assert_eq!("A", fs::read_to_string("target/async-my-log-directory-small/my-log-file").await.unwrap());
133//!
134//!   log.write("B".as_bytes()).await;
135//!   assert_eq!("A", fs::read_to_string("target/async-my-log-directory-small/my-log-file.0").await.unwrap());
136//!   assert_eq!("B", fs::read_to_string("target/async-my-log-directory-small/my-log-file").await.unwrap());
137//!
138//!   log.write("C".as_bytes()).await;
139//!   assert_eq!("A", fs::read_to_string("target/async-my-log-directory-small/my-log-file.0").await.unwrap());
140//!   assert_eq!("B", fs::read_to_string("target/async-my-log-directory-small/my-log-file.1").await.unwrap());
141//!   assert_eq!("C", fs::read_to_string("target/async-my-log-directory-small/my-log-file").await.unwrap());
142//!
143//!   log.write("D".as_bytes()).await;
144//!   assert_eq!("A", fs::read_to_string("target/async-my-log-directory-small/my-log-file.0").await.unwrap());
145//!   assert_eq!("B", fs::read_to_string("target/async-my-log-directory-small/my-log-file.1").await.unwrap());
146//!   assert_eq!("C", fs::read_to_string("target/async-my-log-directory-small/my-log-file.2").await.unwrap());
147//!   assert_eq!("D", fs::read_to_string("target/async-my-log-directory-small/my-log-file").await.unwrap());
148//!
149//!   log.write("E".as_bytes()).await;
150//!   assert_eq!("A", fs::read_to_string("target/async-my-log-directory-small/my-log-file.0").await.unwrap());
151//!   assert_eq!("B", fs::read_to_string("target/async-my-log-directory-small/my-log-file.1").await.unwrap());
152//!   assert_eq!("C", fs::read_to_string("target/async-my-log-directory-small/my-log-file.2").await.unwrap());
153//!   assert_eq!("D", fs::read_to_string("target/async-my-log-directory-small/my-log-file.3").await.unwrap());
154//!   assert_eq!("E", fs::read_to_string("target/async-my-log-directory-small/my-log-file").await.unwrap());
155//!
156//!
157//!   // Here we overwrite the 0 file since we're out of log files, restarting the sequencing
158//!   log.write("F".as_bytes()).await;
159//!   assert_eq!("E", fs::read_to_string("target/async-my-log-directory-small/my-log-file.0").await.unwrap());
160//!   assert_eq!("B", fs::read_to_string("target/async-my-log-directory-small/my-log-file.1").await.unwrap());
161//!   assert_eq!("C", fs::read_to_string("target/async-my-log-directory-small/my-log-file.2").await.unwrap());
162//!   assert_eq!("D", fs::read_to_string("target/async-my-log-directory-small/my-log-file.3").await.unwrap());
163//!   assert_eq!("F", fs::read_to_string("target/async-my-log-directory-small/my-log-file").await.unwrap());
164//!
165//!   fs::remove_dir_all("target/async-my-log-directory-small").await;
166//! });
167//! ```
168//!
169//! # Filesystem Errors #
170//!
171//! If the directory containing the logs is deleted or somehow made inaccessible then the rotator
172//! will simply continue operating without fault. When a rotation occurs, it attempts to open a
173//! file in the directory. If it can, it will just continue logging. If it can't then the written
174//! date is sent to the void.
175//!
176//! This logger never panics.
177use crate::error;
178use core::future::Future;
179use core::pin::Pin;
180use futures::task::{Context, Poll};
181use std::path::{Path, PathBuf};
182use tokio::{
183    fs::{self, File},
184    io::{self, AsyncWrite},
185};
186
187// ---
188
189type Result<T> = std::result::Result<T, error::Error>;
190
191pub enum RotateState {
192    PendingFlush,
193    PendingRename(Pin<Box<dyn Future<Output = io::Result<()>>>>),
194    PendingCreate(Pin<Box<dyn Future<Output = io::Result<fs::File>>>>),
195    Done,
196}
197
198/// Condition on which a file is rotated.
199pub enum RotationMode {
200    /// Cut the log at the exact size in bytes.
201    Bytes(usize),
202    /// Cut the log file at line breaks.
203    Lines(usize),
204    /// Cut the log file after surpassing size in bytes (but having written a complete buffer from a write call.)
205    BytesSurpassed(usize),
206}
207
208/// The main writer used for rotating logs.
209pub struct FileRotate {
210    basename: PathBuf,
211    count: usize,
212    file: Option<Pin<Box<File>>>,
213    file_number: usize,
214    max_file_number: usize,
215    mode: RotationMode,
216
217    //transient stuff used by poll
218    written: usize,
219    rotate_state: RotateState,
220}
221
222impl FileRotate {
223    /// Create a new [FileRotate].
224    ///
225    /// The basename of the `path` is used to create new log files by appending an extension of the
226    /// form `.N`, where N is `0..=max_file_number`.
227    ///
228    /// `rotation_mode` specifies the limits for rotating a file.
229    ///
230    /// # Panics
231    ///
232    /// Panics if `bytes == 0` or `lines == 0`.
233    pub async fn new<P: AsRef<Path>>(
234        path: P,
235        rotation_mode: RotationMode,
236        max_file_number: usize,
237    ) -> Result<Self> {
238        match rotation_mode {
239            RotationMode::Bytes(bytes) if bytes == 0 => {
240                return Err(error::Error::ZeroBytes);
241            }
242            RotationMode::Lines(lines) if lines == 0 => {
243                return Err(error::Error::ZeroLines);
244            }
245            RotationMode::BytesSurpassed(bytes) if bytes == 0 => {
246                return Err(error::Error::ZeroBytes);
247            }
248            _ => {}
249        };
250
251        Ok(Self {
252            basename: path.as_ref().to_path_buf(),
253            count: 0,
254            file: Some(Box::pin(File::create(&path).await?)),
255            file_number: 0,
256            max_file_number,
257            mode: rotation_mode,
258
259            written: 0,
260            rotate_state: RotateState::Done,
261        })
262    }
263
264    fn usable_file(&mut self) -> io::Result<Pin<&mut File>> {
265        if let Some(f) = &mut self.file {
266            Ok(f.as_mut())
267        } else {
268            Err(io::Error::from(io::ErrorKind::NotConnected))
269        }
270    }
271
272    fn reset(self: &mut Pin<&mut Self>) {
273        self.written = 0;
274        self.rotate_state = RotateState::Done;
275    }
276
277    fn poll_rotate(self: &mut Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
278        macro_rules! rename {
279            () => {
280                // drop old file... (we may have to recreate it again)
281                self.file = None;
282                let basename = self.basename.clone();
283                let mut path = self.basename.clone();
284                path.set_extension(self.file_number.to_string());
285                self.rotate_state =
286                    RotateState::PendingRename(Box::pin(fs::rename(basename, path)));
287                return self.poll_rotate(cx);
288            };
289        }
290
291        match self.rotate_state {
292            RotateState::Done => {
293                // if called when done, start rotation...
294                // don't pin-box-store future that captures self - let the next state call
295                // poll directly
296                self.rotate_state = RotateState::PendingFlush;
297                self.poll_rotate(cx)
298            }
299            RotateState::PendingFlush => match self.file {
300                None => {
301                    rename!();
302                }
303                Some(_) => match self.usable_file()?.poll_flush(cx) {
304                    Poll::Pending => Poll::Pending,
305                    Poll::Ready(Err(e)) => {
306                        self.rotate_state = RotateState::Done;
307                        Poll::Ready(Err(e))
308                    }
309                    Poll::Ready(Ok(())) => {
310                        rename!();
311                    }
312                },
313            },
314            RotateState::PendingRename(ref mut rename_future) => {
315                match rename_future.as_mut().poll(cx) {
316                    Poll::Pending => Poll::Pending,
317
318                    // Logic from synchronous side - ignore rename errors
319                    // so long as create succeeds, logging continues...
320                    Poll::Ready(Err(_)) | Poll::Ready(Ok(())) => {
321                        let basename = self.basename.clone();
322                        self.rotate_state =
323                            RotateState::PendingCreate(Box::pin(File::create(basename)));
324                        self.poll_rotate(cx)
325                    }
326                }
327            }
328            RotateState::PendingCreate(ref mut create_future) => {
329                match create_future.as_mut().poll(cx) {
330                    Poll::Pending => Poll::Pending,
331                    Poll::Ready(Err(e)) => {
332                        self.rotate_state = RotateState::Done;
333                        Poll::Ready(Err(e))
334                    }
335                    Poll::Ready(Ok(file)) => {
336                        self.file = Some(Box::pin(file));
337                        self.file_number = (self.file_number + 1) % (self.max_file_number + 1);
338                        self.count = 0;
339                        self.rotate_state = RotateState::Done;
340                        Poll::Ready(Ok(()))
341                    }
342                }
343            }
344        }
345    }
346
347    fn poll_write_bytes(
348        self: &mut Pin<&mut Self>,
349        cx: &mut Context<'_>,
350        complete_buf: &[u8],
351        bytes: usize,
352    ) -> Poll<io::Result<bool>> {
353        let buf_to_write = &complete_buf[self.written..];
354        let (subbuf, should_rotate) = if self.count + buf_to_write.len() > bytes {
355            // got more to write than allowed?
356            let bytes_left = bytes - self.count;
357            (&buf_to_write[..bytes_left], true)
358        } else {
359            (buf_to_write, false)
360        };
361
362        match self.usable_file() {
363            Err(e) => Poll::Ready(Err(e)),
364            Ok(file) => match file.poll_write(cx, subbuf) {
365                Poll::Pending => Poll::Pending,
366                Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
367                Poll::Ready(Ok(w)) => {
368                    self.written += w;
369                    self.count += w;
370                    Poll::Ready(Ok(should_rotate))
371                }
372            },
373        }
374    }
375
376    fn poll_write_bytes_surpassed(
377        self: &mut Pin<&mut Self>,
378        cx: &mut Context<'_>,
379        complete_buf: &[u8],
380        bytes: usize,
381    ) -> Poll<io::Result<bool>> {
382        let buf_to_write = &complete_buf[self.written..];
383
384        match self.usable_file() {
385            Err(e) => Poll::Ready(Err(e)),
386            Ok(file) => match file.poll_write(cx, buf_to_write) {
387                Poll::Pending => Poll::Pending,
388                Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
389                Poll::Ready(Ok(w)) => {
390                    self.written += w;
391                    self.count += w;
392                    Poll::Ready(Ok(self.count > bytes))
393                }
394            },
395        }
396    }
397
398    fn poll_write_lines(
399        self: &mut Pin<&mut Self>,
400        cx: &mut Context<'_>,
401        complete_buf: &[u8],
402        lines: usize,
403    ) -> Poll<io::Result<bool>> {
404        let buf_to_write = &complete_buf[self.written..];
405        let subbuf = if let Some((idx, _)) = buf_to_write
406            .iter()
407            .enumerate()
408            .find(|(_, byte)| *byte == &b'\n')
409        {
410            &buf_to_write[..idx + 1]
411        } else {
412            &buf_to_write
413        };
414
415        match self.usable_file() {
416            Err(e) => Poll::Ready(Err(e)),
417            Ok(file) => match file.poll_write(cx, subbuf) {
418                Poll::Pending => Poll::Pending,
419                Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
420                Poll::Ready(Ok(w)) => {
421                    self.written += w;
422                    self.count += 1;
423                    Poll::Ready(Ok(self.count >= lines))
424                }
425            },
426        }
427    }
428}
429
430impl AsyncWrite for FileRotate {
431    fn poll_write(
432        mut self: Pin<&mut Self>,
433        cx: &mut Context<'_>,
434        buf: &[u8],
435    ) -> Poll<io::Result<usize>> {
436        /// This generates a block to call poll_rotate and handle the immediate response
437        /// Unfortunately I don't know of any way to abstract this block out as a function
438        /// at each call site. I don't yet know how to rewrite the state machine to collapse
439        /// them all into fewer (or a single) callsite.
440        macro_rules! rotate {
441            () => {
442                match self.poll_rotate(cx) {
443                    Poll::Pending => return Poll::Pending,
444                    Poll::Ready(Err(e)) => {
445                        self.reset();
446                        return Poll::Ready(Err(e));
447                    }
448                    Poll::Ready(Ok(())) => return self.poll_write(cx, buf),
449                }
450            };
451        }
452
453        // are we waiting on a rotation future? Handle it
454        match self.rotate_state {
455            RotateState::Done => {}
456            _ => rotate!(),
457        }
458
459        // do we not have a file? If so, rotate
460        if self.file.is_none() {
461            rotate!();
462        }
463
464        // Is it done? Finish everything up.
465        if buf.len() == self.written {
466            let w = self.written;
467            self.reset();
468            return Poll::Ready(Ok(w));
469        }
470
471        let poll_write_result = match self.mode {
472            RotationMode::Bytes(bytes) => self.poll_write_bytes(cx, buf, bytes),
473            RotationMode::Lines(lines) => self.poll_write_lines(cx, buf, lines),
474            RotationMode::BytesSurpassed(bytes) => self.poll_write_bytes_surpassed(cx, buf, bytes),
475        };
476
477        match poll_write_result {
478            Poll::Pending => Poll::Pending,
479            Poll::Ready(Err(e)) => {
480                self.reset();
481                Poll::Ready(Err(e))
482            }
483            Poll::Ready(Ok(false)) => self.poll_write(cx, buf),
484            Poll::Ready(Ok(true)) => {
485                rotate!()
486            }
487        }
488    }
489
490    // pass flush down to the current file
491    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
492        match self.usable_file() {
493            Err(e) => Poll::Ready(Err(e)),
494            Ok(file) => file.poll_flush(cx),
495        }
496    }
497
498    // pass shutdown down to the current file
499    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
500        match self.usable_file() {
501            Err(e) => Poll::Ready(Err(e)),
502            Ok(file) => file.poll_shutdown(cx),
503        }
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use tokio::io::AsyncWriteExt;
511
512    #[tokio::test]
513    async fn zero_bytes() {
514        let zerobyteserr =
515            FileRotate::new("target/async_zero_bytes", RotationMode::Bytes(0), 0).await;
516        if let Err(error::Error::ZeroBytes) = zerobyteserr {
517        } else {
518            panic!("Expected Error::ZeroBytes");
519        };
520    }
521
522    #[tokio::test]
523    async fn zero_bytes_surpassed() {
524        let zerobyteserr = FileRotate::new(
525            "target/async_zero_bytes",
526            RotationMode::BytesSurpassed(0),
527            0,
528        )
529        .await;
530        if let Err(error::Error::ZeroBytes) = zerobyteserr {
531        } else {
532            panic!("Expected Error::ZeroBytes");
533        };
534    }
535
536    #[tokio::test]
537    async fn zero_lines() {
538        let zerolineserr =
539            FileRotate::new("target/async_zero_lines", RotationMode::Lines(0), 0).await;
540        if let Err(error::Error::ZeroLines) = zerolineserr {
541        } else {
542            panic!("Expected Error::ZeroLines");
543        };
544    }
545
546    #[tokio::test]
547    async fn rotate_to_deleted_directory() {
548        let _ = fs::remove_dir_all("target/async_rotate").await;
549        fs::create_dir("target/async_rotate").await.unwrap();
550
551        let mut rot = FileRotate::new("target/async_rotate/log", RotationMode::Lines(1), 0)
552            .await
553            .unwrap();
554        rot.write(b"a\n").await.unwrap();
555        assert_eq!(
556            "",
557            fs::read_to_string("target/async_rotate/log").await.unwrap()
558        );
559        assert_eq!(
560            "a\n",
561            fs::read_to_string("target/async_rotate/log.0")
562                .await
563                .unwrap()
564        );
565
566        fs::remove_dir_all("target/async_rotate").await.unwrap();
567
568        assert!(rot.write(b"b\n").await.is_err());
569
570        assert!(rot.flush().await.is_err());
571        assert!(fs::read_dir("target/async_rotate").await.is_err());
572
573        fs::create_dir("target/async_rotate").await.unwrap();
574
575        rot.write(b"c\n").await.unwrap();
576        assert_eq!(
577            "",
578            fs::read_to_string("target/async_rotate/log").await.unwrap()
579        );
580
581        rot.write(b"d\n").await.unwrap();
582        assert_eq!(
583            "",
584            fs::read_to_string("target/async_rotate/log").await.unwrap()
585        );
586        assert_eq!(
587            "d\n",
588            fs::read_to_string("target/async_rotate/log.0")
589                .await
590                .unwrap()
591        );
592    }
593
594    #[tokio::test]
595    async fn write_complete_record_until_bytes_surpassed() {
596        let _ = fs::remove_dir_all("target/async_surpassed_bytes").await;
597        fs::create_dir("target/async_surpassed_bytes")
598            .await
599            .unwrap();
600
601        let mut rot = FileRotate::new(
602            "target/async_surpassed_bytes/log",
603            RotationMode::BytesSurpassed(1),
604            1,
605        )
606        .await
607        .unwrap();
608
609        rot.write(b"0123456789").await.unwrap();
610        rot.flush().await.unwrap();
611        assert!(Path::new("target/async_surpassed_bytes/log.0").exists());
612        // shouldn't exist yet - because entire record was written in one shot
613        assert!(!Path::new("target/async_surpassed_bytes/log.1").exists());
614
615        // This should create the second file
616        rot.write(b"0123456789").await.unwrap();
617        rot.flush().await.unwrap();
618        assert!(Path::new("target/async_surpassed_bytes/log.1").exists());
619
620        fs::remove_dir_all("target/async_surpassed_bytes")
621            .await
622            .unwrap();
623    }
624
625    #[quickcheck_async::tokio]
626    async fn arbitrary_lines(count: usize) {
627        println!("Testing arbitary lines: {}\n", count);
628        let _ = fs::remove_dir_all("target/async_arbitrary_lines").await;
629        fs::create_dir("target/async_arbitrary_lines")
630            .await
631            .unwrap();
632
633        // we don't need an idiotically large number of lines
634        let count = count.max(1).min(100);
635        let mut rot = FileRotate::new(
636            "target/async_arbitrary_lines/log",
637            RotationMode::Lines(count),
638            0,
639        )
640        .await
641        .unwrap();
642
643        println!("Writing lines...: {}\n", count);
644        for _ in 0..count - 1 {
645            rot.write(b"\n").await.unwrap();
646        }
647
648        rot.flush().await.unwrap();
649        assert!(!Path::new("target/async_arbitrary_lines/log.0").exists());
650        rot.write(b"\n").await.unwrap();
651        assert!(Path::new("target/async_arbitrary_lines/log.0").exists());
652
653        fs::remove_dir_all("target/async_arbitrary_lines")
654            .await
655            .unwrap();
656    }
657
658    #[quickcheck_async::tokio]
659    async fn arbitrary_bytes() {
660        let _ = fs::remove_dir_all("target/async_arbitrary_bytes").await;
661        fs::create_dir("target/async_arbitrary_bytes")
662            .await
663            .unwrap();
664
665        let count = 0.max(1);
666        let mut rot = FileRotate::new(
667            "target/async_arbitrary_bytes/log",
668            RotationMode::Bytes(count),
669            0,
670        )
671        .await
672        .unwrap();
673
674        for _ in 0..count {
675            rot.write(b"0").await.unwrap();
676        }
677
678        rot.flush().await.unwrap();
679        assert!(!Path::new("target/async_arbitrary_bytes/log.0").exists());
680        rot.write(b"1").await.unwrap();
681        assert!(Path::new("target/async_arbitrary_bytes/log.0").exists());
682
683        fs::remove_dir_all("target/async_arbitrary_bytes")
684            .await
685            .unwrap();
686    }
687}