### Question:
TailableFile implements Closeable { static int readLines(TailableFile file, ByteBuffer buffer, int maxLines, FileChangeListener listener) throws IOException { int lines = 0; while (buffer.hasRemaining() && lines < maxLines) { int startPos = buffer.position(); boolean hasNewLine = skipUntil(buffer, NEW_LINE); int length = buffer.position() - startPos; if (hasNewLine) { length--; if (length > 0 && buffer.get(buffer.position() - 2) == '\r') { length--; } lines++; } if (length > 0) { while (!listener.onLineAvailable(file, buffer.array(), startPos, length, hasNewLine)) ; } } return lines; } TailableFile(File file); void deleteStateFileOnExit(); void ack(); void nak(); int tail(ByteBuffer buffer, FileChangeListener listener, int maxLines); @Override void close(); File getFile(); @Override String toString();  }### Answer:
@Test void testReadOneLine() throws IOException { ByteBuffer bytes = ByteBuffer.wrap(new byte[]{'c', 'a', 'f', 'e', '\n', 'b', 'a', 'b', 'e', '\r', '\n'}); TailableFile.readLines(tailableFile, bytes, 1, logListener); assertThat(logListener.lines).containsExactly("cafe"); }
@Test void testRetry() throws IOException { ByteBuffer bytes = ByteBuffer.wrap(new byte[]{'c', 'a', 'f', 'e', '\n'}); List<String> readBytes = new ArrayList<>(); AtomicBoolean processingSuccessful = new AtomicBoolean(false); TailableFile.readLines(tailableFile, bytes, 1, new FileChangeListenerAdapter() { @Override public boolean onLineAvailable(TailableFile file, byte[] line, int offset, int length, boolean eol) { readBytes.add(new String(line, offset, length)); return processingSuccessful.getAndSet(true); } }); assertThat(readBytes).containsExactly("cafe", "cafe"); }
@Test void testReadTwoLines() throws IOException { ByteBuffer bytes = ByteBuffer.wrap(new byte[]{'c', 'a', 'f', 'e', '\n', 'b', 'a', 'b', 'e', '\r', '\n'}); TailableFile.readLines(tailableFile, bytes, 4, logListener); assertThat(logListener.lines).containsExactly("cafe", "babe"); }