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     @SuppressWarnings("squid:S2699") // Suppress "Add at least one assertion to this test case"
235     void testBufferBreak() throws Exception {
236         final long delay = 50;
237         final File file = new File(temporaryFolder, "testBufferBreak.txt");
238         createFile(file, 0);
239         writeStrings(file, "SBTOURIST\n");
240         final TestTailerListener listener = new TestTailerListener();
241         try (Tailer tailer = new Tailer(file, listener, delay, false, 1)) {
242             final Thread thread = new Thread(tailer);
243             thread.start();
244             List<String> lines = listener.getLines();
245             while (lines.isEmpty() || !lines.get(lines.size() - 1).equals("SBTOURIST")) {
246                 lines = listener.getLines();
247             }
248             listener.clear();
249         }
250     }
251 
252     @Test
253     void testBuilderWithNonStandardTailable() throws Exception {
254         final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen-and-buffersize-and-charset.txt");
255         createFile(file, 0);
256         final TestTailerListener listener = new TestTailerListener(1);
257         try (Tailer tailer = Tailer.builder()
258                 .setExecutorService(Executors.newSingleThreadExecutor())
259                 .setTailable(new NonStandardTailable(file))
260                 .setTailerListener(listener)
261                 .get()) {
262             assertTrue(tailer.getTailable() instanceof NonStandardTailable);
263             validateTailer(listener, file);
264         }
265     }
266 
267     @Test
268     void testCreate() throws Exception {
269         final File file = new File(temporaryFolder, "tailer-create.txt");
270         createFile(file, 0);
271         final TestTailerListener listener = new TestTailerListener(1);
272         try (Tailer tailer = Tailer.create(file, listener)) {
273             validateTailer(listener, file);
274         }
275     }
276 
277     @Test
278     void testCreateWithDelay() throws Exception {
279         final File file = new File(temporaryFolder, "tailer-create-with-delay.txt");
280         createFile(file, 0);
281         final TestTailerListener listener = new TestTailerListener(1);
282         try (Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS)) {
283             validateTailer(listener, file);
284         }
285     }
286 
287     @Test
288     void testCreateWithDelayAndFromStart() throws Exception {
289         final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start.txt");
290         createFile(file, 0);
291         final TestTailerListener listener = new TestTailerListener(1);
292         try (Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false)) {
293             validateTailer(listener, file);
294         }
295     }
296 
297     @Test
298     void testCreateWithDelayAndFromStartWithBufferSize() throws Exception {
299         final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-buffersize.txt");
300         createFile(file, 0);
301         final TestTailerListener listener = new TestTailerListener(1);
302         try (Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false, TEST_BUFFER_SIZE)) {
303             validateTailer(listener, file);
304         }
305     }
306 
307     @Test
308     void testCreateWithDelayAndFromStartWithReopenAndBufferSize() throws Exception {
309         final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen-and-buffersize.txt");
310         createFile(file, 0);
311         final TestTailerListener listener = new TestTailerListener(1);
312         try (Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false, true, TEST_BUFFER_SIZE)) {
313             validateTailer(listener, file);
314         }
315     }
316 
317     @Test
318     void testCreateWithDelayAndFromStartWithReopenAndBufferSizeAndCharset() throws Exception {
319         final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen-and-buffersize-and-charset.txt");
320         createFile(file, 0);
321         final TestTailerListener listener = new TestTailerListener(1);
322         try (Tailer tailer = Tailer.create(file, StandardCharsets.UTF_8, listener, TEST_DELAY_MILLIS, false, true, TEST_BUFFER_SIZE)) {
323             validateTailer(listener, file);
324         }
325     }
326 
327     @Test
328     void testCreatorWithDelayAndFromStartWithReopen() throws Exception {
329         final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen.txt");
330         createFile(file, 0);
331         final TestTailerListener listener = new TestTailerListener(1);
332         try (Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false, false)) {
333             validateTailer(listener, file);
334         }
335     }
336 
337     /*
338      * Tests [IO-357][Tailer] InterruptedException while the thread is sleeping is silently ignored.
339      */
340     @Test
341     void testInterrupt() throws Exception {
342         final File file = new File(temporaryFolder, "nosuchfile");
343         assertFalse(file.exists(), "nosuchfile should not exist");
344         final TestTailerListener listener = new TestTailerListener();
345         // Use a long delay to try to make sure the test thread calls interrupt() while the tailer thread is sleeping.
346         final int delay = 1000;
347         final int idle = 50; // allow time for thread to work
348         try (Tailer tailer = new Tailer(file, listener, delay, false, IOUtils.DEFAULT_BUFFER_SIZE)) {
349             final Thread thread = new Thread(tailer);
350             thread.setDaemon(true);
351             thread.start();
352             TestUtils.sleep(idle);
353             thread.interrupt();
354             TestUtils.sleep(delay + idle);
355             assertNotNull(listener.exception, "Missing InterruptedException");
356             assertTrue(listener.exception instanceof InterruptedException, "Unexpected Exception: " + listener.exception);
357             assertEquals(1, listener.initialized, "Expected init to be called");
358             assertTrue(listener.notFound > 0, "fileNotFound should be called");
359             assertEquals(0, listener.rotated, "fileRotated should be not be called");
360             assertEquals(0, listener.reachedEndOfFile, "end of file never reached");
361         }
362     }
363 
364     @Test
365     void testIO335() throws Exception { // test CR behavior
366         // Create & start the Tailer
367         final long delayMillis = 50;
368         final File file = new File(temporaryFolder, "tailer-testio334.txt");
369         createFile(file, 0);
370         final TestTailerListener listener = new TestTailerListener();
371         try (Tailer tailer = new Tailer(file, listener, delayMillis, false)) {
372             final Thread thread = new Thread(tailer);
373             thread.start();
374 
375             // Write some lines to the file
376             writeStrings(file, "CRLF\r\n", "LF\n", "CR\r", "CRCR\r\r", "trail");
377             final long testDelayMillis = delayMillis * 10;
378             TestUtils.sleep(testDelayMillis);
379             final List<String> lines = listener.getLines();
380             assertEquals(4, lines.size(), "line count");
381             assertEquals("CRLF", lines.get(0), "line 1");
382             assertEquals("LF", lines.get(1), "line 2");
383             assertEquals("CR", lines.get(2), "line 3");
384             assertEquals("CRCR\r", lines.get(3), "line 4");
385         }
386     }
387 
388     @Test
389     @SuppressWarnings("squid:S2699") // Suppress "Add at least one assertion to this test case"
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);
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             listener.clear();
411         }
412     }
413 
414     @Test
415     void testMultiByteBreak() throws Exception {
416         // System.out.println("testMultiByteBreak() Default charset: " + Charset.defaultCharset().displayName());
417         final long delay = 50;
418         final File origin = TestResources.getFile("test-file-utf8.bin");
419         final File file = new File(temporaryFolder, "testMultiByteBreak.txt");
420         createFile(file, 0);
421         final TestTailerListener listener = new TestTailerListener();
422         final String osname = SystemProperties.getOsName();
423         final boolean isWindows = osname.startsWith("Windows");
424         // Need to use UTF-8 to read & write the file otherwise it can be corrupted (depending on the default charset)
425         final Charset charsetUTF8 = StandardCharsets.UTF_8;
426         try (Tailer tailer = new Tailer(file, charsetUTF8, listener, delay, false, isWindows, IOUtils.DEFAULT_BUFFER_SIZE)) {
427             final Thread thread = new Thread(tailer);
428             thread.start();
429             try (Writer out = new OutputStreamWriter(Files.newOutputStream(file.toPath()), charsetUTF8);
430                     BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(origin.toPath()), charsetUTF8))) {
431                 final List<String> lines = new ArrayList<>();
432                 String line;
433                 while ((line = reader.readLine()) != null) {
434                     out.write(line);
435                     out.write("\n");
436                     lines.add(line);
437                 }
438                 out.close(); // ensure data is written
439                 final long testDelayMillis = delay * 10;
440                 TestUtils.sleep(testDelayMillis);
441                 final List<String> tailerlines = listener.getLines();
442                 assertEquals(lines.size(), tailerlines.size(), "line count");
443                 for (int i = 0, len = lines.size(); i < len; i++) {
444                     final String expected = lines.get(i);
445                     final String actual = tailerlines.get(i);
446                     if (!expected.equals(actual)) {
447                         fail("Line: " + i + "\nExp: (" + expected.length() + ") " + expected + "\nAct: (" + actual.length() + ") " + actual);
448                     }
449                 }
450             }
451         }
452     }
453 
454     @Test
455     void testSimpleConstructor() throws Exception {
456         final File file = new File(temporaryFolder, "tailer-simple-constructor.txt");
457         createFile(file, 0);
458         final TestTailerListener listener = new TestTailerListener(1);
459         try (Tailer tailer = new Tailer(file, listener)) {
460             final Thread thread = new Thread(tailer);
461             thread.start();
462             validateTailer(listener, file);
463         }
464     }
465 
466     @Test
467     void testSimpleConstructorWithDelay() throws Exception {
468         final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay.txt");
469         createFile(file, 0);
470         final TestTailerListener listener = new TestTailerListener(1);
471         try (Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS)) {
472             final Thread thread = new Thread(tailer);
473             thread.start();
474             validateTailer(listener, file);
475         }
476     }
477 
478     @Test
479     void testSimpleConstructorWithDelayAndFromStart() throws Exception {
480         final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start.txt");
481         createFile(file, 0);
482         final TestTailerListener listener = new TestTailerListener(1);
483         try (Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS, false)) {
484             final Thread thread = new Thread(tailer);
485             thread.start();
486             validateTailer(listener, file);
487         }
488     }
489 
490     @Test
491     void testSimpleConstructorWithDelayAndFromStartWithBufferSize() throws Exception {
492         final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start-with-buffersize.txt");
493         createFile(file, 0);
494         final TestTailerListener listener = new TestTailerListener(1);
495         try (Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS, false, TEST_BUFFER_SIZE)) {
496             final Thread thread = new Thread(tailer);
497             thread.start();
498             validateTailer(listener, file);
499         }
500     }
501 
502     @Test
503     void testSimpleConstructorWithDelayAndFromStartWithReopen() throws Exception {
504         final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start-with-reopen.txt");
505         createFile(file, 0);
506         final TestTailerListener listener = new TestTailerListener(1);
507         try (Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS, false, false)) {
508             final Thread thread = new Thread(tailer);
509             thread.start();
510             validateTailer(listener, file);
511         }
512     }
513 
514     @Test
515     void testSimpleConstructorWithDelayAndFromStartWithReopenAndBufferSize() throws Exception {
516         final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start-with-reopen-and-buffersize.txt");
517         createFile(file, 0);
518         final TestTailerListener listener = new TestTailerListener(1);
519         try (Tailer tailer = new Tailer(file, listener, TEST_DELAY_MILLIS, false, true, TEST_BUFFER_SIZE)) {
520             final Thread thread = new Thread(tailer);
521             thread.start();
522             validateTailer(listener, file);
523         }
524     }
525 
526     @Test
527     void testSimpleConstructorWithDelayAndFromStartWithReopenAndBufferSizeAndCharset() throws Exception {
528         final File file = new File(temporaryFolder, "tailer-simple-constructor-with-delay-and-from-start-with-reopen-and-buffersize-and-charset.txt");
529         createFile(file, 0);
530         final TestTailerListener listener = new TestTailerListener(1);
531         try (Tailer tailer = new Tailer(file, StandardCharsets.UTF_8, listener, TEST_DELAY_MILLIS, false, true, TEST_BUFFER_SIZE)) {
532             final Thread thread = new Thread(tailer);
533             thread.start();
534             validateTailer(listener, file);
535         }
536     }
537 
538     @Test
539     void testStopWithNoFile() throws Exception {
540         final File file = new File(temporaryFolder, "nosuchfile");
541         assertFalse(file.exists(), "nosuchfile should not exist");
542         final TestTailerListener listener = new TestTailerListener();
543         final int delay = 100;
544         final int idle = 50; // allow time for thread to work
545         try (Tailer tailer = Tailer.create(file, listener, delay, false)) {
546             TestUtils.sleep(idle);
547         }
548         TestUtils.sleep(delay + idle);
549         if (listener.exception != null) {
550             listener.exception.printStackTrace();
551         }
552         assertNull(listener.exception, "Should not generate Exception");
553         assertEquals(1, listener.initialized, "Expected init to be called");
554         assertTrue(listener.notFound > 0, "fileNotFound should be called");
555         assertEquals(0, listener.rotated, "fileRotated should be not be called");
556         assertEquals(0, listener.reachedEndOfFile, "end of file never reached");
557     }
558 
559     @Test
560     void testStopWithNoFileUsingExecutor() throws Exception {
561         final File file = new File(temporaryFolder, "nosuchfile");
562         assertFalse(file.exists(), "nosuchfile should not exist");
563         final TestTailerListener listener = new TestTailerListener();
564         final int delay = 100;
565         final int idle = 50; // allow time for thread to work
566         try (Tailer tailer = new Tailer(file, listener, delay, false)) {
567             final Executor exec = new ScheduledThreadPoolExecutor(1);
568             exec.execute(tailer);
569             TestUtils.sleep(idle);
570         }
571         TestUtils.sleep(delay + idle);
572         assertNull(listener.exception, "Should not generate Exception");
573         assertEquals(1, listener.initialized, "Expected init to be called");
574         assertTrue(listener.notFound > 0, "fileNotFound should be called");
575         assertEquals(0, listener.rotated, "fileRotated should be not be called");
576         assertEquals(0, listener.reachedEndOfFile, "end of file never reached");
577     }
578 
579     @Test
580     void testTailer() throws Exception {
581         // Create & start the Tailer
582         final long delayMillis = 50;
583         final File file = new File(temporaryFolder, "tailer1-test.txt");
584         createFile(file, 0);
585         final TestTailerListener listener = new TestTailerListener();
586         final String osname = SystemProperties.getOsName();
587         final boolean isWindows = osname.startsWith("Windows");
588         try (Tailer tailer = new Tailer(file, listener, delayMillis, false, isWindows)) {
589             final Thread thread = new Thread(tailer);
590             thread.start();
591             // Write some lines to the file
592             writeLines(file, "Line one", "Line two");
593             final long testDelayMillis = delayMillis * 10;
594             TestUtils.sleep(testDelayMillis);
595             List<String> lines = listener.getLines();
596             assertEquals(2, lines.size(), "1 line count");
597             assertEquals("Line one", lines.get(0), "1 line 1");
598             assertEquals("Line two", lines.get(1), "1 line 2");
599             listener.clear();
600             // Write another line to the file
601             writeLines(file, "Line three");
602             TestUtils.sleep(testDelayMillis);
603             lines = listener.getLines();
604             assertEquals(1, lines.size(), "2 line count");
605             assertEquals("Line three", lines.get(0), "2 line 3");
606             listener.clear();
607             // Check file does actually have all the lines
608             lines = FileUtils.readLines(file, StandardCharsets.UTF_8);
609             assertEquals(3, lines.size(), "3 line count");
610             assertEquals("Line one", lines.get(0), "3 line 1");
611             assertEquals("Line two", lines.get(1), "3 line 2");
612             assertEquals("Line three", lines.get(2), "3 line 3");
613             // Delete & re-create
614             file.delete();
615             assertFalse(file.exists(), "File should not exist");
616             createFile(file, 0);
617             assertTrue(file.exists(), "File should now exist");
618             TestUtils.sleep(testDelayMillis);
619             // Write another line
620             writeLines(file, "Line four");
621             TestUtils.sleep(testDelayMillis);
622             lines = listener.getLines();
623             assertEquals(1, lines.size(), "4 line count");
624             assertEquals("Line four", lines.get(0), "4 line 3");
625             listener.clear();
626             // Stop
627             thread.interrupt();
628             TestUtils.sleep(testDelayMillis * 4);
629             writeLines(file, "Line five");
630             assertEquals(0, listener.getLines().size(), "4 line count");
631             assertNotNull(listener.exception, "Missing InterruptedException");
632             assertTrue(listener.exception instanceof InterruptedException, "Unexpected Exception: " + listener.exception);
633             assertEquals(1, listener.initialized, "Expected init to be called");
634             // assertEquals(0 , listener.notFound, "fileNotFound should not be called"); // there is a window when it might be
635             // called
636             assertEquals(1, listener.rotated, "fileRotated should be called");
637         }
638     }
639 
640     @Test
641     void testTailerEndOfFileReached() throws Exception {
642         // Create & start the Tailer
643         final long delayMillis = 50;
644         final long testDelayMillis = delayMillis * 10;
645         final File file = new File(temporaryFolder, "tailer-eof-test.txt");
646         createFile(file, 0);
647         final TestTailerListener listener = new TestTailerListener();
648         final String osname = SystemProperties.getOsName();
649         final boolean isWindows = osname.startsWith("Windows");
650         try (Tailer tailer = new Tailer(file, listener, delayMillis, false, isWindows)) {
651             final Thread thread = new Thread(tailer);
652             thread.start();
653             // write a few lines
654             writeLines(file, "line1", "line2", "line3");
655             TestUtils.sleep(testDelayMillis);
656             // write a few lines
657             writeLines(file, "line4", "line5", "line6");
658             TestUtils.sleep(testDelayMillis);
659             // write a few lines
660             writeLines(file, "line7", "line8", "line9");
661             TestUtils.sleep(testDelayMillis);
662             // May be > 3 times due to underlying OS behavior wrt streams
663             assertTrue(listener.reachedEndOfFile >= 3, "end of file reached at least 3 times");
664         }
665     }
666 
667     @Test
668     void testTailerEof() throws Exception {
669         // Create & start the Tailer
670         final long delayMillis = 100;
671         final File file = new File(temporaryFolder, "tailer2-test.txt");
672         createFile(file, 0);
673         final TestTailerListener listener = new TestTailerListener();
674         try (Tailer tailer = new Tailer(file, listener, delayMillis, false)) {
675             final Thread thread = new Thread(tailer);
676             thread.start();
677             // Write some lines to the file
678             writeStrings(file, "Line");
679             TestUtils.sleep(delayMillis * 2);
680             List<String> lines = listener.getLines();
681             assertEquals(0, lines.size(), "1 line count");
682             writeStrings(file, " one\n");
683             TestUtils.sleep(delayMillis * 4);
684             lines = listener.getLines();
685             assertEquals(1, lines.size(), "1 line count");
686             assertEquals("Line one", lines.get(0), "1 line 1");
687             listener.clear();
688         }
689     }
690 
691     @Test
692     void testTailerIgnoreTouch() throws Exception {
693         // Create & start the Tailer
694         final long delayMillis = 50;
695         final File file = new File(temporaryFolder, "tailer1-testIgnoreTouch.txt");
696         createFile(file, 0);
697         final TestTailerListener listener = new TestTailerListener();
698         try (Tailer tailer = Tailer.builder().setFile(file).setTailerListener(listener).setDelayDuration(Duration.ofMillis(delayMillis)).setStartThread(false)
699                 .setIgnoreTouch(true).get()) {
700             final Thread thread = new Thread(tailer);
701             thread.start();
702             // Write some lines to the file
703             writeLines(file, "Line one");
704             List<String> lines = expectLinesWithLongTimeout(listener, delayMillis, 20);
705             assertEquals(1, lines.size(), "1 line count");
706             assertEquals("Line one", lines.get(0), "1 line 1");
707             listener.clear();
708             // touch the file
709             TestUtils.sleepToNextSecond(); // ensure to be within the next second because of posix fs limitation
710             file.setLastModified(System.currentTimeMillis());
711             TestUtils.sleep(delayMillis * 10);
712             lines = listener.getLines();
713             assertEquals(0, lines.size(), "nothing should have changed by touching");
714         }
715     }
716 
717     @Test
718     void testTailerReissueOnTouch() throws Exception {
719         // Create & start the Tailer
720         final long delayMillis = 50;
721         final File file = new File(temporaryFolder, "tailer1-testReissueOnTouch.txt");
722         createFile(file, 0);
723         final TestTailerListener listener = new TestTailerListener();
724         try (Tailer tailer = Tailer.builder().setFile(file).setTailerListener(listener).setDelayDuration(Duration.ofMillis(delayMillis)).setStartThread(false)
725                 .setIgnoreTouch(false).get()) {
726             final Thread thread = new Thread(tailer);
727             thread.start();
728             // Write some lines to the file
729             writeLines(file, "Line one");
730             List<String> lines = expectLinesWithLongTimeout(listener, delayMillis, 50);
731             assertEquals(1, lines.size(), "1 line count");
732             assertEquals("Line one", lines.get(0), "1 line 1");
733             listener.clear();
734             // touch the file
735             TestUtils.sleepToNextSecond(); // ensure to be within the next second because of posix fs limitation
736             file.setLastModified(System.currentTimeMillis());
737             lines = expectLinesWithLongTimeout(listener, delayMillis, 20);
738             assertEquals(1, lines.size(), "1 line count");
739             assertEquals("Line one", lines.get(0), "1 line 1");
740             listener.clear();
741         }
742     }
743 
744     private void validateTailer(final TestTailerListener listener, final File file) throws IOException, InterruptedException {
745         writeLines(file, "foo");
746         final int timeout = 30;
747         final TimeUnit timeoutUnit = TimeUnit.SECONDS;
748         assertTrue(listener.awaitExpectedLines(timeout, timeoutUnit), () -> String.format("await timed out after %s %s", timeout, timeoutUnit));
749         assertEquals(listener.getLines(), Arrays.asList("foo"), "lines");
750     }
751 
752     /** Appends lines to a file */
753     private void writeLines(final File file, final String... lines) throws IOException {
754         try (Writer writer = Files.newBufferedWriter(file.toPath(), StandardOpenOption.APPEND)) {
755             for (final String line : lines) {
756                 writer.write(line + "\n");
757             }
758         }
759     }
760 
761     /** Appends strings to a file */
762     private void writeStrings(final File file, final String... strings) throws IOException {
763         try (Writer writer = Files.newBufferedWriter(file.toPath(), StandardOpenOption.APPEND)) {
764             for (final String string : strings) {
765                 writer.write(string);
766             }
767         }
768     }
769 }