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}