View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.io.input;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertNotNull;
22  import static org.junit.jupiter.api.Assertions.assertNull;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  import static org.junit.jupiter.api.Assertions.fail;
25  
26  import java.io.BufferedOutputStream;
27  import java.io.BufferedReader;
28  import java.io.File;
29  import java.io.FileNotFoundException;
30  import java.io.IOException;
31  import java.io.InputStreamReader;
32  import java.io.OutputStreamWriter;
33  import java.io.RandomAccessFile;
34  import java.io.Writer;
35  import java.nio.charset.Charset;
36  import java.nio.charset.StandardCharsets;
37  import java.nio.file.Files;
38  import java.nio.file.StandardOpenOption;
39  import java.nio.file.attribute.FileTime;
40  import java.time.Duration;
41  import java.util.ArrayList;
42  import java.util.Arrays;
43  import java.util.Collections;
44  import java.util.List;
45  import java.util.concurrent.CountDownLatch;
46  import java.util.concurrent.Executor;
47  import java.util.concurrent.Executors;
48  import java.util.concurrent.ScheduledThreadPoolExecutor;
49  import java.util.concurrent.TimeUnit;
50  
51  import org.apache.commons.io.FileUtils;
52  import org.apache.commons.io.IOUtils;
53  import org.apache.commons.io.RandomAccessFileMode;
54  import org.apache.commons.io.TestResources;
55  import org.apache.commons.io.test.TestUtils;
56  import org.apache.commons.lang3.SystemProperties;
57  import org.junit.jupiter.api.Test;
58  import org.junit.jupiter.api.io.TempDir;
59  
60  /**
61   * Test for {@link Tailer}.
62   */
63  class TailerTest {
64  
65      private static final class NonStandardTailable implements Tailer.Tailable {
66  
67          private final File file;
68  
69          NonStandardTailable(final File file) {
70              this.file = file;
71          }
72  
73          @Override
74          public Tailer.RandomAccessResourceBridge getRandomAccess(final String mode) throws FileNotFoundException {
75              return new Tailer.RandomAccessResourceBridge() {
76  
77                  private final RandomAccessFile randomAccessFile = new RandomAccessFile(file, mode);
78  
79                  @Override
80                  public void close() throws IOException {
81                      randomAccessFile.close();
82                  }
83  
84                  @Override
85                  public long getPointer() throws IOException {
86                      return randomAccessFile.getFilePointer();
87                  }
88  
89                  @Override
90                  public int read(final byte[] b) throws IOException {
91                      return randomAccessFile.read(b);
92                  }
93  
94                  @Override
95                  public void seek(final long position) throws IOException {
96                      randomAccessFile.seek(position);
97                  }
98              };
99          }
100 
101         @Override
102         public boolean isNewer(final FileTime fileTime) throws IOException {
103             return FileUtils.isFileNewer(file, fileTime);
104         }
105 
106         @Override
107         public FileTime lastModifiedFileTime() throws IOException {
108             return FileUtils.lastModifiedFileTime(file);
109         }
110 
111         @Override
112         public long size() {
113             return file.length();
114         }
115     }
116 
117     /**
118      * Test {@link TailerListener} implementation.
119      */
120     private static final class TestTailerListener extends TailerListenerAdapter {
121 
122         // Must be synchronized because it is written by one thread and read by another
123         private final List<String> lines = Collections.synchronizedList(new ArrayList<>());
124 
125         private final CountDownLatch latch;
126 
127         volatile Exception exception;
128 
129         volatile int notFound;
130 
131         volatile int rotated;
132 
133         volatile int initialized;
134 
135         volatile int reachedEndOfFile;
136 
137         TestTailerListener() {
138             latch = new CountDownLatch(1);
139         }
140 
141         TestTailerListener(final int expectedLines) {
142             latch = new CountDownLatch(expectedLines);
143         }
144 
145         public boolean awaitExpectedLines(final long timeout, final TimeUnit timeUnit) throws InterruptedException {
146             return latch.await(timeout, timeUnit);
147         }
148 
149         public void clear() {
150             lines.clear();
151         }
152 
153         @Override
154         public void endOfFileReached() {
155             reachedEndOfFile++; // not atomic, but OK because only updated here.
156         }
157 
158         @Override
159         public void fileNotFound() {
160             notFound++; // not atomic, but OK because only updated here.
161         }
162 
163         @Override
164         public void fileRotated() {
165             rotated++; // not atomic, but OK because only updated here.
166         }
167 
168         public List<String> getLines() {
169             return lines;
170         }
171 
172         @Override
173         public void handle(final Exception e) {
174             exception = e;
175         }
176 
177         @Override
178         public void handle(final String line) {
179             lines.add(line);
180             latch.countDown();
181         }
182 
183         @Override
184         public void init(final Tailer tailer) {
185             initialized++; // not atomic, but OK because only updated here.
186         }
187     }
188 
189     private static final int TEST_BUFFER_SIZE = 1024;
190 
191     private static final int TEST_DELAY_MILLIS = 1500;
192 
193     @TempDir
194     public static File temporaryFolder;
195 
196     protected void createFile(final File file, final long size) throws IOException {
197         assertTrue(file.getParentFile().exists(), () -> "Cannot create file " + file + " as the parent directory does not exist");
198         try (BufferedOutputStream output = new BufferedOutputStream(Files.newOutputStream(file.toPath()))) {
199             TestUtils.generateTestData(output, size);
200         }
201         // try to make sure file is found
202         // (to stop continuum occasionally failing)
203         RandomAccessFile reader = null;
204         try {
205             while (reader == null) {
206                 try {
207                     reader = RandomAccessFileMode.READ_ONLY.create(file);
208                 } catch (final FileNotFoundException ignore) {
209                     // ignore
210                 }
211                 TestUtils.sleepQuietly(200L);
212             }
213         } finally {
214             IOUtils.closeQuietly(reader);
215         }
216         // sanity checks
217         assertTrue(file.exists());
218         assertEquals(size, file.length());
219     }
220 
221     private List<String> expectLinesWithLongTimeout(final TestTailerListener listener, final long minDelay, final int count) throws Exception {
222         for (int i = 0; i < count; i++) {
223             TestUtils.sleep(minDelay);
224             final List<String> lines = listener.getLines();
225             if (lines.size() > 0) {
226                 return lines;
227             }
228         }
229         fail("Waiting for TestTailerListener.getLines() timed out after " + count * minDelay + " ms");
230         return null;
231     }
232 
233     @Test
234     void testBufferBreak() throws Exception {
235         final long delay = 50;
236         final File file = new File(temporaryFolder, "testBufferBreak.txt");
237         createFile(file, 0);
238         final String data = "SBTOURIST\n";
239         writeStrings(file, data);
240         final TestTailerListener listener = new TestTailerListener();
241         try (Tailer tailer = new Tailer(file, listener, delay, false, 1)) {
242             final Thread thread = new Thread(tailer, "commons-io-tailer-testBufferBreak");
243             thread.start();
244             List<String> lines = listener.getLines();
245             assertEquals(data.length(), tailer.getTailable().size());
246             while (lines.isEmpty() || !lines.get(lines.size() - 1).equals("SBTOURIST")) {
247                 lines = listener.getLines();
248             }
249             listener.clear();
250         }
251     }
252 
253     @Test
254     void testBuilderWithNonStandardTailable() throws Exception {
255         final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen-and-buffersize-and-charset.txt");
256         createFile(file, 0);
257         final TestTailerListener listener = new TestTailerListener(1);
258         try (Tailer tailer = Tailer.builder()
259                 .setExecutorService(Executors.newSingleThreadExecutor())
260                 .setTailable(new NonStandardTailable(file))
261                 .setTailerListener(listener)
262                 .get()) {
263             assertTrue(tailer.getTailable() instanceof NonStandardTailable);
264             validateTailer(listener, file);
265         }
266     }
267 
268     @Test
269     void testCreate() throws Exception {
270         final File file = new File(temporaryFolder, "tailer-create.txt");
271         createFile(file, 0);
272         final TestTailerListener listener = new TestTailerListener(1);
273         try (Tailer tailer = Tailer.create(file, listener)) {
274             validateTailer(listener, file);
275         }
276     }
277 
278     @Test
279     void testCreateWithDelay() throws Exception {
280         final File file = new File(temporaryFolder, "tailer-create-with-delay.txt");
281         createFile(file, 0);
282         final TestTailerListener listener = new TestTailerListener(1);
283         try (Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS)) {
284             validateTailer(listener, file);
285         }
286     }
287 
288     @Test
289     void testCreateWithDelayAndFromStart() throws Exception {
290         final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start.txt");
291         createFile(file, 0);
292         final TestTailerListener listener = new TestTailerListener(1);
293         try (Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false)) {
294             validateTailer(listener, file);
295         }
296     }
297 
298     @Test
299     void testCreateWithDelayAndFromStartWithBufferSize() throws Exception {
300         final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-buffersize.txt");
301         createFile(file, 0);
302         final TestTailerListener listener = new TestTailerListener(1);
303         try (Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false, TEST_BUFFER_SIZE)) {
304             validateTailer(listener, file);
305         }
306     }
307 
308     @Test
309     void testCreateWithDelayAndFromStartWithReopenAndBufferSize() throws Exception {
310         final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen-and-buffersize.txt");
311         createFile(file, 0);
312         final TestTailerListener listener = new TestTailerListener(1);
313         try (Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false, true, TEST_BUFFER_SIZE)) {
314             validateTailer(listener, file);
315         }
316     }
317 
318     @Test
319     void testCreateWithDelayAndFromStartWithReopenAndBufferSizeAndCharset() throws Exception {
320         final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen-and-buffersize-and-charset.txt");
321         createFile(file, 0);
322         final TestTailerListener listener = new TestTailerListener(1);
323         try (Tailer tailer = Tailer.create(file, StandardCharsets.UTF_8, listener, TEST_DELAY_MILLIS, false, true, TEST_BUFFER_SIZE)) {
324             validateTailer(listener, file);
325         }
326     }
327 
328     @Test
329     void testCreatorWithDelayAndFromStartWithReopen() throws Exception {
330         final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen.txt");
331         createFile(file, 0);
332         final TestTailerListener listener = new TestTailerListener(1);
333         try (Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false, false)) {
334             validateTailer(listener, file);
335         }
336     }
337 
338     /*
339      * Tests [IO-357][Tailer] InterruptedException while the thread is sleeping is silently ignored.
340      */
341     @Test
342     void testInterrupt() throws Exception {
343         final File file = new File(temporaryFolder, "nosuchfile");
344         assertFalse(file.exists(), "nosuchfile should not exist");
345         final TestTailerListener listener = new TestTailerListener();
346         // Use a long delay to try to make sure the test thread calls interrupt() while the tailer thread is sleeping.
347         final int delay = 1000;
348         final int idle = 50; // allow time for thread to work
349         try (Tailer tailer = new Tailer(file, listener, delay, false, IOUtils.DEFAULT_BUFFER_SIZE)) {
350             final Thread thread = new Thread(tailer, "commons-io-tailer-testInterrupt");
351             thread.setDaemon(true);
352             thread.start();
353             TestUtils.sleep(idle);
354             thread.interrupt();
355             TestUtils.sleep(delay + idle);
356             assertNotNull(listener.exception, "Missing InterruptedException");
357             assertTrue(listener.exception instanceof InterruptedException, "Unexpected Exception: " + listener.exception);
358             assertEquals(1, listener.initialized, "Expected init to be called");
359             assertTrue(listener.notFound > 0, "fileNotFound should be called");
360             assertEquals(0, listener.rotated, "fileRotated should be not be called");
361             assertEquals(0, listener.reachedEndOfFile, "end of file never reached");
362         }
363     }
364 
365     @Test
366     void testIO335() throws Exception { // test CR behavior
367         // Create & start the Tailer
368         final long delayMillis = 50;
369         final File file = new File(temporaryFolder, "tailer-testio334.txt");
370         createFile(file, 0);
371         final TestTailerListener listener = new TestTailerListener();
372         try (Tailer tailer = new Tailer(file, listener, delayMillis, false)) {
373             final Thread thread = new Thread(tailer, "commons-io-tailer-testIO335");
374             thread.start();
375 
376             // Write some lines to the file
377             writeStrings(file, "CRLF\r\n", "LF\n", "CR\r", "CRCR\r\r", "trail");
378             final long testDelayMillis = delayMillis * 10;
379             TestUtils.sleep(testDelayMillis);
380             final List<String> lines = listener.getLines();
381             assertEquals(4, lines.size(), "line count");
382             assertEquals("CRLF", lines.get(0), "line 1");
383             assertEquals("LF", lines.get(1), "line 2");
384             assertEquals("CR", lines.get(2), "line 3");
385             assertEquals("CRCR\r", lines.get(3), "line 4");
386         }
387     }
388 
389     @Test
390     void testLongFile() throws Exception {
391         final long delay = 50;
392         final File file = new File(temporaryFolder, "testLongFile.txt");
393         createFile(file, 0);
394         try (Writer writer = Files.newBufferedWriter(file.toPath(), StandardOpenOption.APPEND)) {
395             for (int i = 0; i < 100000; i++) {
396                 writer.write("LineLineLineLineLineLineLineLineLineLine\n");
397             }
398             writer.write("SBTOURIST\n");
399         }
400         final TestTailerListener listener = new TestTailerListener();
401         try (Tailer tailer = new Tailer(file, listener, delay, false)) {
402             // final long start = System.currentTimeMillis();
403             final Thread thread = new Thread(tailer, "commons-io-tailer-testLongFile");
404             thread.start();
405             List<String> lines = listener.getLines();
406             while (lines.isEmpty() || !lines.get(lines.size() - 1).equals("SBTOURIST")) {
407                 lines = listener.getLines();
408             }
409             // System.out.println("Elapsed: " + (System.currentTimeMillis() - start));
410             assertFalse(lines.isEmpty());
411             listener.clear();
412         }
413     }
414 
415     @Test
416     void testMultiByteBreak() throws Exception {
417         // System.out.println("testMultiByteBreak() Default charset: " + Charset.defaultCharset().displayName());
418         final long delay = 50;
419         final File origin = TestResources.getFile("test-file-utf8.bin");
420         final File file = new File(temporaryFolder, "testMultiByteBreak.txt");
421         createFile(file, 0);
422         final TestTailerListener listener = new TestTailerListener();
423         final String osname = SystemProperties.getOsName();
424         final boolean isWindows = osname.startsWith("Windows");
425         // Need to use UTF-8 to read & write the file otherwise it can be corrupted (depending on the default charset)
426         final Charset charsetUTF8 = StandardCharsets.UTF_8;
427         try (Tailer tailer = new Tailer(file, charsetUTF8, listener, delay, false, isWindows, IOUtils.DEFAULT_BUFFER_SIZE)) {
428             final Thread thread = new Thread(tailer, "commons-io-tailer-testMultiByteBreak");
429             thread.start();
430             try (Writer out = new OutputStreamWriter(Files.newOutputStream(file.toPath()), charsetUTF8);
431                     BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(origin.toPath()), charsetUTF8))) {
432                 final List<String> lines = new ArrayList<>();
433                 String line;
434                 while ((line = reader.readLine()) != null) {
435                     out.write(line);
436                     out.write("\n");
437                     lines.add(line);
438                 }
439                 out.close(); // ensure data is written
440                 final long testDelayMillis = delay * 10;
441                 TestUtils.sleep(testDelayMillis);
442                 final List<String> tailerlines = listener.getLines();
443                 assertEquals(lines.size(), tailerlines.size(), "line count");
444                 for (int i = 0, len = lines.size(); i < len; i++) {
445                     final String expected = lines.get(i);
446                     final String actual = tailerlines.get(i);
447                     if (!expected.equals(actual)) {
448                         fail("Line: " + i + "\nExp: (" + expected.length() + ") " + expected + "\nAct: (" + actual.length() + ") " + actual);
449                     }
450                 }
451             }
452         }
453     }
454 
455     @Test
456     void testSimpleConstructor() throws Exception {
457         final File file = new File(temporaryFolder, "tailer-simple-constructor.txt");
458         createFile(file, 0);
459         final TestTailerListener listener = new TestTailerListener(1);
460         try (Tailer tailer = new Tailer(file, listener)) {
461             final Thread thread = new Thread(tailer, "commons-io-tailer-testSimpleConstructor");
462             thread.start();
463             validateTailer(listener, file);
464         }
465     }
466 
467     @Test
468     void testSimpleConstructorWithDelay() throws Exception {
469         final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay.txt");
470         createFile(file, 0);
471         final TestTailerListener listener = new TestTailerListener(1);
472         try (Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS)) {
473             final Thread thread = new Thread(tailer, "commons-io-tailer-testSimpleConstructorWithDelay");
474             thread.start();
475             validateTailer(listener, file);
476         }
477     }
478 
479     @Test
480     void testSimpleConstructorWithDelayAndFromStart() throws Exception {
481         final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start.txt");
482         createFile(file, 0);
483         final TestTailerListener listener = new TestTailerListener(1);
484         try (Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS, false)) {
485             final Thread thread = new Thread(tailer, "commons-io-tailer-testSimpleConstructorWithDelayAndFromStart");
486             thread.start();
487             validateTailer(listener, file);
488         }
489     }
490 
491     @Test
492     void testSimpleConstructorWithDelayAndFromStartWithBufferSize() throws Exception {
493         final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start-with-buffersize.txt");
494         createFile(file, 0);
495         final TestTailerListener listener = new TestTailerListener(1);
496         try (Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS, false, TEST_BUFFER_SIZE)) {
497             final Thread thread = new Thread(tailer, "commons-io-tailer-testSimpleConstructorWithDelayAndFromStartWithBufferSize");
498             thread.start();
499             validateTailer(listener, file);
500         }
501     }
502 
503     @Test
504     void testSimpleConstructorWithDelayAndFromStartWithReopen() throws Exception {
505         final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start-with-reopen.txt");
506         createFile(file, 0);
507         final TestTailerListener listener = new TestTailerListener(1);
508         try (Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS, false, false)) {
509             final Thread thread = new Thread(tailer, "commons-io-tailer-testSimpleConstructorWithDelayAndFromStartWithReopen");
510             thread.start();
511             validateTailer(listener, file);
512         }
513     }
514 
515     @Test
516     void testSimpleConstructorWithDelayAndFromStartWithReopenAndBufferSize() throws Exception {
517         final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start-with-reopen-and-buffersize.txt");
518         createFile(file, 0);
519         final TestTailerListener listener = new TestTailerListener(1);
520         try (Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS, false, true, TEST_BUFFER_SIZE)) {
521             final Thread thread = new Thread(tailer, "commons-io-tailer-testSimpleConstructorWithDelayAndFromStartWithReopenAndBufferSize");
522             thread.start();
523             validateTailer(listener, file);
524         }
525     }
526 
527     @Test
528     void testSimpleConstructorWithDelayAndFromStartWithReopenAndBufferSizeAndCharset() throws Exception {
529         final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start-with-reopen-and-buffersize-and-charset.txt");
530         createFile(file, 0);
531         final TestTailerListener listener = new TestTailerListener(1);
532         try (Tailer tailer = new Tailer(file, StandardCharsets.UTF_8, listener, TEST_DELAY_MILLIS, false, true, TEST_BUFFER_SIZE)) {
533             final Thread thread = new Thread(tailer, "commons-io-tailer-testSimpleConstructorWithDelayAndFromStartWithReopenAndBufferSizeAndCharset");
534             thread.start();
535             validateTailer(listener, file);
536         }
537     }
538 
539     @Test
540     void testStopWithNoFile() throws Exception {
541         final File file = new File(temporaryFolder, "nosuchfile");
542         assertFalse(file.exists(), "nosuchfile should not exist");
543         final TestTailerListener listener = new TestTailerListener();
544         final int delay = 100;
545         final int idle = 50; // allow time for thread to work
546         try (Tailer tailer = Tailer.create(file, listener, delay, false)) {
547             TestUtils.sleep(idle);
548         }
549         TestUtils.sleep(delay + idle);
550         if (listener.exception != null) {
551             listener.exception.printStackTrace();
552         }
553         assertNull(listener.exception, "Should not generate Exception");
554         assertEquals(1, listener.initialized, "Expected init to be called");
555         assertTrue(listener.notFound > 0, "fileNotFound should be called");
556         assertEquals(0, listener.rotated, "fileRotated should be not be called");
557         assertEquals(0, listener.reachedEndOfFile, "end of file never reached");
558     }
559 
560     @Test
561     void testStopWithNoFileUsingExecutor() throws Exception {
562         final File file = new File(temporaryFolder, "nosuchfile");
563         assertFalse(file.exists(), "nosuchfile should not exist");
564         final TestTailerListener listener = new TestTailerListener();
565         final int delay = 100;
566         final int idle = 50; // allow time for thread to work
567         try (Tailer tailer = new Tailer(file, listener, delay, false)) {
568             final Executor exec = new ScheduledThreadPoolExecutor(1);
569             exec.execute(tailer);
570             TestUtils.sleep(idle);
571         }
572         TestUtils.sleep(delay + idle);
573         assertNull(listener.exception, "Should not generate Exception");
574         assertEquals(1, listener.initialized, "Expected init to be called");
575         assertTrue(listener.notFound > 0, "fileNotFound should be called");
576         assertEquals(0, listener.rotated, "fileRotated should be not be called");
577         assertEquals(0, listener.reachedEndOfFile, "end of file never reached");
578     }
579 
580     @Test
581     void testTailer() throws Exception {
582         // Create & start the Tailer
583         final long delayMillis = 50;
584         final File file = new File(temporaryFolder, "tailer1-test.txt");
585         createFile(file, 0);
586         final TestTailerListener listener = new TestTailerListener();
587         final String osname = SystemProperties.getOsName();
588         final boolean isWindows = osname.startsWith("Windows");
589         try (Tailer tailer = new Tailer(file, listener, delayMillis, false, isWindows)) {
590             final Thread thread = new Thread(tailer, "commons-io-tailer-testTailer");
591             thread.start();
592             // Write some lines to the file
593             writeLines(file, "Line one", "Line two");
594             final long testDelayMillis = delayMillis * 10;
595             TestUtils.sleep(testDelayMillis);
596             List<String> lines = listener.getLines();
597             assertEquals(2, lines.size(), "1 line count");
598             assertEquals("Line one", lines.get(0), "1 line 1");
599             assertEquals("Line two", lines.get(1), "1 line 2");
600             listener.clear();
601             // Write another line to the file
602             writeLines(file, "Line three");
603             TestUtils.sleep(testDelayMillis);
604             lines = listener.getLines();
605             assertEquals(1, lines.size(), "2 line count");
606             assertEquals("Line three", lines.get(0), "2 line 3");
607             listener.clear();
608             // Check file does actually have all the lines
609             lines = FileUtils.readLines(file, StandardCharsets.UTF_8);
610             assertEquals(3, lines.size(), "3 line count");
611             assertEquals("Line one", lines.get(0), "3 line 1");
612             assertEquals("Line two", lines.get(1), "3 line 2");
613             assertEquals("Line three", lines.get(2), "3 line 3");
614             // Delete & re-create
615             file.delete();
616             assertFalse(file.exists(), "File should not exist");
617             createFile(file, 0);
618             assertTrue(file.exists(), "File should now exist");
619             TestUtils.sleep(testDelayMillis);
620             // Write another line
621             writeLines(file, "Line four");
622             TestUtils.sleep(testDelayMillis);
623             lines = listener.getLines();
624             assertEquals(1, lines.size(), "4 line count");
625             assertEquals("Line four", lines.get(0), "4 line 3");
626             listener.clear();
627             // Stop
628             thread.interrupt();
629             TestUtils.sleep(testDelayMillis * 4);
630             writeLines(file, "Line five");
631             assertEquals(0, listener.getLines().size(), "4 line count");
632             assertNotNull(listener.exception, "Missing InterruptedException");
633             assertTrue(listener.exception instanceof InterruptedException, "Unexpected Exception: " + listener.exception);
634             assertEquals(1, listener.initialized, "Expected init to be called");
635             // assertEquals(0 , listener.notFound, "fileNotFound should not be called"); // there is a window when it might be
636             // called
637             assertEquals(1, listener.rotated, "fileRotated should be called");
638         }
639     }
640 
641     @Test
642     void testTailerEndOfFileReached() throws Exception {
643         // Create & start the Tailer
644         final long delayMillis = 50;
645         final long testDelayMillis = delayMillis * 10;
646         final File file = new File(temporaryFolder, "tailer-eof-test.txt");
647         createFile(file, 0);
648         final TestTailerListener listener = new TestTailerListener();
649         final String osname = SystemProperties.getOsName();
650         final boolean isWindows = osname.startsWith("Windows");
651         try (Tailer tailer = new Tailer(file, listener, delayMillis, false, isWindows)) {
652             final Thread thread = new Thread(tailer, "commons-io-tailer-testTailerEndOfFileReached");
653             thread.start();
654             // write a few lines
655             writeLines(file, "line1", "line2", "line3");
656             TestUtils.sleep(testDelayMillis);
657             // write a few lines
658             writeLines(file, "line4", "line5", "line6");
659             TestUtils.sleep(testDelayMillis);
660             // write a few lines
661             writeLines(file, "line7", "line8", "line9");
662             TestUtils.sleep(testDelayMillis);
663             // May be > 3 times due to underlying OS behavior and streams.
664             assertTrue(listener.reachedEndOfFile >= 3, "end of file reached at least 3 times");
665         }
666     }
667 
668     @Test
669     void testTailerEof() throws Exception {
670         // Create & start the Tailer
671         final long delayMillis = 100;
672         final File file = new File(temporaryFolder, "tailer2-test.txt");
673         createFile(file, 0);
674         final TestTailerListener listener = new TestTailerListener();
675         try (Tailer tailer = new Tailer(file, listener, delayMillis, false)) {
676             final Thread thread = new Thread(tailer, "commons-io-tailer-testTailerEof");
677             thread.start();
678             // Write some lines to the file
679             writeStrings(file, "Line");
680             TestUtils.sleep(delayMillis * 2);
681             List<String> lines = listener.getLines();
682             assertEquals(0, lines.size(), "1 line count");
683             writeStrings(file, " one\n");
684             TestUtils.sleep(delayMillis * 4);
685             lines = listener.getLines();
686             assertEquals(1, lines.size(), "1 line count");
687             assertEquals("Line one", lines.get(0), "1 line 1");
688             listener.clear();
689         }
690     }
691 
692     @Test
693     void testTailerIgnoreTouch() throws Exception {
694         // Create & start the Tailer
695         final long delayMillis = 50;
696         final File file = new File(temporaryFolder, "tailer1-testIgnoreTouch.txt");
697         createFile(file, 0);
698         final TestTailerListener listener = new TestTailerListener();
699         try (Tailer tailer = Tailer.builder().setFile(file).setTailerListener(listener).setDelayDuration(Duration.ofMillis(delayMillis)).setStartThread(false)
700                 .setIgnoreTouch(true).get()) {
701             final Thread thread = new Thread(tailer, "commons-io-tailer-testTailerIgnoreTouch");
702             thread.start();
703             // Write some lines to the file
704             writeLines(file, "Line one");
705             List<String> lines = expectLinesWithLongTimeout(listener, delayMillis, 20);
706             assertEquals(1, lines.size(), "1 line count");
707             assertEquals("Line one", lines.get(0), "1 line 1");
708             listener.clear();
709             // touch the file
710             TestUtils.sleepToNextSecond(); // ensure to be within the next second because of posix fs limitation
711             file.setLastModified(System.currentTimeMillis());
712             TestUtils.sleep(delayMillis * 10);
713             lines = listener.getLines();
714             assertEquals(0, lines.size(), "nothing should have changed by touching");
715         }
716     }
717 
718     @Test
719     void testTailerReissueOnTouch() throws Exception {
720         // Create & start the Tailer
721         final long delayMillis = 50;
722         final File file = new File(temporaryFolder, "tailer1-testReissueOnTouch.txt");
723         createFile(file, 0);
724         final TestTailerListener listener = new TestTailerListener();
725         try (Tailer tailer = Tailer.builder().setFile(file).setTailerListener(listener).setDelayDuration(Duration.ofMillis(delayMillis)).setStartThread(false)
726                 .setIgnoreTouch(false).get()) {
727             final Thread thread = new Thread(tailer, "commons-io-tailer-testTailerReissueOnTouch");
728             thread.start();
729             // Write some lines to the file
730             writeLines(file, "Line one");
731             List<String> lines = expectLinesWithLongTimeout(listener, delayMillis, 50);
732             assertEquals(1, lines.size(), "1 line count");
733             assertEquals("Line one", lines.get(0), "1 line 1");
734             listener.clear();
735             // touch the file
736             TestUtils.sleepToNextSecond(); // ensure to be within the next second because of posix fs limitation
737             file.setLastModified(System.currentTimeMillis());
738             lines = expectLinesWithLongTimeout(listener, delayMillis, 20);
739             assertEquals(1, lines.size(), "1 line count");
740             assertEquals("Line one", lines.get(0), "1 line 1");
741             listener.clear();
742         }
743     }
744 
745     private void validateTailer(final TestTailerListener listener, final File file) throws IOException, InterruptedException {
746         writeLines(file, "foo");
747         final int timeout = 30;
748         final TimeUnit timeoutUnit = TimeUnit.SECONDS;
749         assertTrue(listener.awaitExpectedLines(timeout, timeoutUnit), () -> String.format("await timed out after %s %s", timeout, timeoutUnit));
750         assertEquals(listener.getLines(), Arrays.asList("foo"), "lines");
751     }
752 
753     /** Appends lines to a file */
754     private void writeLines(final File file, final String... lines) throws IOException {
755         try (Writer writer = Files.newBufferedWriter(file.toPath(), StandardOpenOption.APPEND)) {
756             for (final String line : lines) {
757                 writer.write(line + "\n");
758             }
759         }
760     }
761 
762     /** Appends strings to a file */
763     private void writeStrings(final File file, final String... strings) throws IOException {
764         try (Writer writer = Files.newBufferedWriter(file.toPath(), StandardOpenOption.APPEND)) {
765             for (final String string : strings) {
766                 writer.write(string);
767             }
768         }
769     }
770 }