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