From 16dc61e151e7463314a2ffbafb68f25ca6da0ad0 Mon Sep 17 00:00:00 2001 From: Tamas Palfy Date: Thu, 22 Apr 2021 16:40:59 +0200 Subject: [PATCH] NIFI-8458 TailFile - Fix some bugs. Add more tests. - During cleanup keep "tailingPostRollover" in the updated state. - Skipping tests that can't run on Windows. Signed-off-by: Mark Payne --- .../nifi/processors/standard/TailFile.java | 49 ++-- .../AbstractTestTailFileScenario.java | 270 ++++++++++++++++++ .../TestTailFileGeneratedScenarios.java | 146 ++++++++++ .../standard/TestTailFileSimpleScenarios.java | 114 ++++++++ 4 files changed, 556 insertions(+), 23 deletions(-) create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/AbstractTestTailFileScenario.java create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestTailFileGeneratedScenarios.java create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestTailFileSimpleScenarios.java diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/TailFile.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/TailFile.java index 5cb502ccc8..2b1bfcd2b1 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/TailFile.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/TailFile.java @@ -585,7 +585,8 @@ public class TailFile extends AbstractProcessor { for (TailFileObject tfo : states.values()) { cleanReader(tfo); final TailFileState state = tfo.getState(); - tfo.setState(new TailFileState(state.getFilename(), state.getFile(), null, state.getPosition(), state.getTimestamp(), state.getLength(), state.getChecksum(), state.getBuffer())); + tfo.setState(new TailFileState(state.getFilename(), state.getFile(), null, state.getPosition(), + state.getTimestamp(), state.getLength(), state.getChecksum(), state.getBuffer(), state.isTailingPostRollover())); } } @@ -824,24 +825,6 @@ public class TailFile extends AbstractProcessor { } }); - if (abort.get() != null) { - session.remove(flowFile); - final long newPosition = positionHolder.get(); - try { - reader.position(newPosition); - } catch (IOException ex) { - getLogger().warn("Couldn't reposition the reader for {} due to {}", new Object[]{ file, ex }, ex); - try { - reader.close(); - } catch (IOException ex2) { - getLogger().warn("Failed to close reader for {} due to {}", new Object[]{ file, ex2 }, ex2); - } - reader = null; - } - tfo.setState(new TailFileState(tailFile, file, reader, newPosition, timestamp, length, checksum, state.getBuffer())); - throw abort.get(); - } - // If there ended up being no data, just remove the FlowFile if (flowFile.getSize() == 0) { session.remove(flowFile); @@ -882,6 +865,22 @@ public class TailFile extends AbstractProcessor { tfo.setState(new TailFileState(tailFile, file, reader, position, timestamp, length, checksum, state.getBuffer())); persistState(tfo, session, context); + + if (abort.get() != null) { + final long newPosition = positionHolder.get(); + try { + reader.position(newPosition); + } catch (IOException ex) { + getLogger().warn("Couldn't reposition the reader for {} due to {}", new Object[]{ file, ex }, ex); + try { + reader.close(); + } catch (IOException ex2) { + getLogger().warn("Failed to close reader for {} due to {}", new Object[]{ file, ex2 }, ex2); + } + } + + throw abort.get(); + } } private long readLines(final FileChannel reader, final ByteBuffer buffer, final OutputStream out, final Checksum checksum, Boolean reReadOnNul) throws IOException { @@ -1208,8 +1207,8 @@ public class TailFile extends AbstractProcessor { final boolean tailFirstFile; if (rolloverOccurred) { final File firstFile = rolledOffFiles.get(0); - final long millisSinceModified = System.currentTimeMillis() - firstFile.lastModified(); - final boolean fileGrew = firstFile.length() >= position && position > 0; + final long millisSinceModified = getCurrentTimeMs() - firstFile.lastModified(); + final boolean fileGrew = firstFile.length() >= position; final boolean tailRolledFile = postRolloverTailMillis == 0 || millisSinceModified < postRolloverTailMillis; tailFirstFile = fileGrew && tailRolledFile && expectedChecksum != null; } else { @@ -1244,7 +1243,7 @@ public class TailFile extends AbstractProcessor { // If we don't notice that the file has been modified, per the checks above, then we want to keep checking until the last modified // date has eclipsed the configured value for the Post-Rollover Tail Period. Until then, return false. Once that occurs, we will // consume the rest of the data, including the last line, even if it doesn't have a line ending. - final long millisSinceModified = System.currentTimeMillis() - newestFile.lastModified(); + final long millisSinceModified = getCurrentTimeMs() - newestFile.lastModified(); if (millisSinceModified < postRolloverTailMillis) { getLogger().debug("Rolled over file {} (size={}, lastModified={}) was modified {} millis ago, which isn't long enough to consume file fully without taking line endings into " + "account. Will do nothing will file for now.", newestFile, newestFile.length(), newestFile.lastModified(), millisSinceModified); @@ -1343,7 +1342,7 @@ public class TailFile extends AbstractProcessor { // updated values. // But if we are not going to tail the rolled over file for any period of time, we can essentially reset the state. final long postRolloverTailMillis = context.getProperty(POST_ROLLOVER_TAIL_PERIOD).asTimePeriod(TimeUnit.MILLISECONDS); - final long millisSinceUpdate = System.currentTimeMillis() - timestamp; + final long millisSinceUpdate = getCurrentTimeMs() - timestamp; if (tailingPostRollover && postRolloverTailMillis > 0) { getLogger().debug("File {} has been rolled over, but it was updated {} millis ago, which is less than the configured {} ({} ms), so will continue tailing", fileToTail, millisSinceUpdate, POST_ROLLOVER_TAIL_PERIOD.getDisplayName(), postRolloverTailMillis); @@ -1410,6 +1409,10 @@ public class TailFile extends AbstractProcessor { return tfo.getState(); } + public long getCurrentTimeMs() { + return System.currentTimeMillis(); + } + static class TailFileObject { private TailFileState state; diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/AbstractTestTailFileScenario.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/AbstractTestTailFileScenario.java new file mode 100644 index 0000000000..2c98a6fef5 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/AbstractTestTailFileScenario.java @@ -0,0 +1,270 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.processors.standard; + +import org.apache.commons.lang3.SystemUtils; +import org.apache.nifi.util.MockFlowFile; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class AbstractTestTailFileScenario { + public static final String TEST_DIRECTORY = "testTailFileScenario"; + public static final String TARGET_FILE_PATH = "target/" + TEST_DIRECTORY + "/in.txt"; + public static final String NUL_SUBSTITUTE = "X"; + public static final Long POST_ROLLOVER_WAIT_PERSIOD_SECONDS = 100L; + + protected File file; + protected RandomAccessFile randomAccessFile; + + private TailFile processor; + protected TestRunner runner; + + private AtomicBoolean stopAfterEachTrigger; + + protected AtomicLong wordIndex; + protected AtomicLong rolloverIndex; + protected AtomicLong timeAdjustment; + protected AtomicBoolean rolloverSwitchPending; + + protected LinkedList nulPositions; + + protected List expected; + private Random random; + + @Before + public void setUp() throws IOException { + System.setProperty("org.slf4j.simpleLogger.log.org.apache.nifi.processors.standard", "TRACE"); + + clean(); + + File directory = new File("target/" + TEST_DIRECTORY); + if (!directory.exists()) { + assertTrue(directory.mkdirs()); + } + + createTargetFile(); + randomAccessFile = new RandomAccessFile(file, "rw"); + + processor = new TailFile() { + @Override + public long getCurrentTimeMs() { + return super.getCurrentTimeMs() + timeAdjustment.get(); + } + }; + runner = TestRunners.newTestRunner(processor); + runner.setProperty(TailFile.FILENAME, TARGET_FILE_PATH); + runner.setProperty(TailFile.ROLLING_FILENAME_PATTERN, "in.txt*"); + runner.setProperty(TailFile.REREAD_ON_NUL, "true"); + runner.setProperty(TailFile.POST_ROLLOVER_TAIL_PERIOD, POST_ROLLOVER_WAIT_PERSIOD_SECONDS + " sec"); + runner.assertValid(); + + runner.run(1, false, true); + + stopAfterEachTrigger = new AtomicBoolean(false); + + nulPositions = new LinkedList<>(); + wordIndex = new AtomicLong(1); + rolloverIndex = new AtomicLong(1); + timeAdjustment = new AtomicLong(0); + rolloverSwitchPending = new AtomicBoolean(false); + + expected = new ArrayList<>(); + + random = new Random(); + } + + @After + public void tearDown() throws IOException { + if (randomAccessFile != null) { + randomAccessFile.close(); + } + + processor.cleanup(); + } + + public void testScenario(List actions) throws Exception { + testScenario(actions, false); + + tearDown(); + setUp(); + + testScenario(actions, true); + } + + public void testScenario(List actions, boolean stopAfterEachTrigger) throws Exception { + if (actions.contains(Action.ROLLOVER)) { + Assume.assumeTrue("Test wants to rename an open file which is not allowed on Windows", !SystemUtils.IS_OS_WINDOWS); + } + + // GIVEN + this.stopAfterEachTrigger.set(stopAfterEachTrigger); + + // WHEN + for (Action action : actions) { + action.run(this); + } + overwriteRemainingNuls(); + Action.WRITE_NEW_LINE.run(this); + Action.TRIGGER.run(this); + Action.EXPIRE_ROLLOVER_WAIT_PERIOD.run(this); + Action.TRIGGER.run(this); + Action.TRIGGER.run(this); + + // THEN + List flowFiles = runner.getFlowFilesForRelationship(TailFile.REL_SUCCESS); + List actual = flowFiles.stream() + .map(MockFlowFile::toByteArray) + .map(String::new) + .collect(Collectors.toList()); + + assertEquals( + stopAfterEachTrigger + " " + actions.toString(), + expected.stream().collect(Collectors.joining()), + actual.stream().collect(Collectors.joining()) + ); + } + + private void clean() { + cleanFiles("target/" + TEST_DIRECTORY); + } + + private void cleanFiles(String directory) { + final File targetDir = new File(directory); + + if (targetDir.exists()) { + for (final File file : targetDir.listFiles()) { + file.delete(); + } + } + } + + private void createTargetFile() throws IOException { + file = new File(TARGET_FILE_PATH); + file.delete(); + assertTrue(file.createNewFile()); + } + + private void overwriteRemainingNuls() throws Exception { + while (!nulPositions.isEmpty()) { + Action.OVERWRITE_NUL.run(this); + } + } + + private void writeWord() throws IOException { + String word = "-word_" + wordIndex.getAndIncrement() + "-"; + + randomAccessFile.write(word.getBytes()); + + expected.add(word); + } + + private void writeNewLine() throws IOException { + randomAccessFile.write("\n".getBytes()); + + expected.add("\n"); + } + + private void writeNul() throws IOException { + nulPositions.add(randomAccessFile.getFilePointer()); + + randomAccessFile.write("\0".getBytes()); + + expected.add(NUL_SUBSTITUTE); + } + + private void overwriteNul() throws IOException { + if (!nulPositions.isEmpty()) { + Long nulPosition = nulPositions.remove(random.nextInt(nulPositions.size())); + + long currentPosition = randomAccessFile.getFilePointer(); + randomAccessFile.seek(nulPosition); + randomAccessFile.write(NUL_SUBSTITUTE.getBytes()); + randomAccessFile.seek(currentPosition); + } + } + + private void trigger() { + runner.run(1, stopAfterEachTrigger.get(), false); + } + + private void rollover() throws IOException { + File rolledOverFile = new File(file.getParentFile(), file.getName() + "." + rolloverIndex.getAndIncrement()); + file.renameTo(rolledOverFile); + + createTargetFile(); + + rolloverSwitchPending.set(true); + } + + private void switchFile() throws Exception { + if (rolloverSwitchPending.get()) { + overwriteRemainingNuls(); + + randomAccessFile.close(); + randomAccessFile = new RandomAccessFile(file, "rw"); + + rolloverSwitchPending.set(false); + } + } + + private void expireRolloverWaitPeriod() throws Exception { + long waitPeriod = POST_ROLLOVER_WAIT_PERSIOD_SECONDS * 1000 + 100; + timeAdjustment.set(timeAdjustment.get() + waitPeriod); + } + + protected enum Action { + WRITE_WORD(AbstractTestTailFileScenario::writeWord), + WRITE_NEW_LINE(AbstractTestTailFileScenario::writeNewLine), + WRITE_NUL(AbstractTestTailFileScenario::writeNul), + OVERWRITE_NUL(AbstractTestTailFileScenario::overwriteNul), + TRIGGER(AbstractTestTailFileScenario::trigger), + ROLLOVER(AbstractTestTailFileScenario::rollover), + SWITCH_FILE(AbstractTestTailFileScenario::switchFile), + EXPIRE_ROLLOVER_WAIT_PERIOD(AbstractTestTailFileScenario::expireRolloverWaitPeriod); + + private final ActionRunner actionRunner; + + Action(ActionRunner actionRunner) { + this.actionRunner = actionRunner; + } + + void run(AbstractTestTailFileScenario currentTest) throws Exception { + actionRunner.runAction(currentTest); + } + } + + private interface ActionRunner { + void runAction(AbstractTestTailFileScenario currentTest) throws Exception; + } +} diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestTailFileGeneratedScenarios.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestTailFileGeneratedScenarios.java new file mode 100644 index 0000000000..6622169f98 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestTailFileGeneratedScenarios.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.processors.standard; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +@Ignore("Stress test - longrunning. For manual testing.") +@RunWith(Parameterized.class) +public class TestTailFileGeneratedScenarios extends AbstractTestTailFileScenario { + private final List actions; + + public TestTailFileGeneratedScenarios(List actions) { + this.actions = actions; + } + + @Parameterized.Parameters + public static Collection parameters() { + Collection parameters = new ArrayList(); + + // Uncomment the portion for which to run the scenarios. + // They cannot be added to a single large batch because it opens too many files. +// List baseActions = Arrays.asList( +// Action.WRITE_WORD, +// Action.WRITE_NUL, +// Action.WRITE_NEW_LINE, +// Action.WRITE_NUL, +// Action.OVERWRITE_NUL, +// Action.WRITE_WORD, +// Action.OVERWRITE_NUL, +// Action.WRITE_NUL, +// Action.WRITE_NEW_LINE, +// Action.OVERWRITE_NUL, +// Action.WRITE_NUL, +// Action.OVERWRITE_NUL +// ); +// addAction(parameters, Action.TRIGGER, baseActions, 0, 0); +// new ArrayList<>(parameters).forEach(anActionList -> addAction(parameters, Action.ROLLOVER, (List)anActionList[0], 0, 0)); +// new ArrayList<>(parameters).forEach(anActionList -> addAction(parameters, Action.SWITCH_FILE, (List)anActionList[0], 0, 0)); + +// List baseActions = Arrays.asList( +// Action.WRITE_WORD, +// Action.WRITE_NEW_LINE, +// Action.WRITE_NUL, +// Action.OVERWRITE_NUL, +// Action.WRITE_WORD, +// Action.WRITE_NUL, +// Action.WRITE_NEW_LINE, +// Action.OVERWRITE_NUL, +// Action.ROLLOVER, +// Action.WRITE_WORD, +// Action.WRITE_NEW_LINE, +// Action.WRITE_NUL, +// Action.OVERWRITE_NUL, +// Action.WRITE_WORD, +// Action.WRITE_NUL, +// Action.WRITE_NEW_LINE, +// Action.OVERWRITE_NUL, +// Action.SWITCH_FILE, +// Action.WRITE_WORD, +// Action.WRITE_NEW_LINE, +// Action.WRITE_NUL, +// Action.OVERWRITE_NUL, +// Action.WRITE_WORD, +// Action.WRITE_NUL, +// Action.WRITE_NEW_LINE, +// Action.OVERWRITE_NUL +// ); +// addAction(parameters, Action.TRIGGER, baseActions, 0, 1); + + List baseActions = Arrays.asList( + Action.WRITE_WORD, Action.WRITE_WORD, + Action.WRITE_NEW_LINE, Action.WRITE_NEW_LINE, + Action.WRITE_NUL, Action.WRITE_NUL, + Action.WRITE_WORD, Action.WRITE_WORD, + Action.WRITE_NEW_LINE, Action.WRITE_NEW_LINE, + Action.OVERWRITE_NUL, Action.OVERWRITE_NUL, + Action.WRITE_NEW_LINE, Action.WRITE_NEW_LINE, + Action.WRITE_WORD, Action.WRITE_WORD, + Action.WRITE_NEW_LINE, Action.WRITE_NEW_LINE, + Action.ROLLOVER, + Action.WRITE_WORD, Action.WRITE_WORD, + Action.WRITE_NEW_LINE, Action.WRITE_NEW_LINE, + Action.WRITE_NUL, Action.WRITE_NUL, + Action.WRITE_WORD, Action.WRITE_WORD, + Action.WRITE_NEW_LINE, Action.WRITE_NEW_LINE, + Action.OVERWRITE_NUL, Action.OVERWRITE_NUL, + Action.WRITE_NEW_LINE, Action.WRITE_NEW_LINE, + Action.WRITE_WORD, Action.WRITE_WORD, + Action.WRITE_NEW_LINE, Action.WRITE_NEW_LINE, + Action.SWITCH_FILE, + Action.WRITE_WORD, Action.WRITE_WORD, + Action.WRITE_NEW_LINE, Action.WRITE_NEW_LINE, + Action.WRITE_NUL, Action.WRITE_NUL, + Action.WRITE_WORD, Action.WRITE_WORD, + Action.WRITE_NEW_LINE, Action.WRITE_NEW_LINE, + Action.OVERWRITE_NUL, Action.OVERWRITE_NUL, + Action.WRITE_NEW_LINE, Action.WRITE_NEW_LINE, + Action.WRITE_WORD, Action.WRITE_WORD, + Action.WRITE_NEW_LINE, Action.WRITE_NEW_LINE + ); + addAction(parameters, Action.TRIGGER, baseActions, 0, 1); + + return parameters; + } + + private static void addAction(Collection parameters, Action action, List baseActions, int currentDepth, int recursiveDepth) { + for (int triggerIndex = 0; triggerIndex < baseActions.size(); triggerIndex++) { + List actions = new LinkedList<>(baseActions); + actions.add(triggerIndex, action); + + parameters.add(new Object[]{ actions}); + + if (currentDepth < recursiveDepth) { + addAction(parameters, action, actions, currentDepth+1, recursiveDepth); + } + } + } + + @Test + public void testScenario() throws Exception { + testScenario(actions); + } +} diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestTailFileSimpleScenarios.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestTailFileSimpleScenarios.java new file mode 100644 index 0000000000..83da792f36 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestTailFileSimpleScenarios.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.processors.standard; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +public class TestTailFileSimpleScenarios extends AbstractTestTailFileScenario { + @Test + public void testSimpleScenario() throws Exception { + // GIVEN + List actions = Arrays.asList( + Action.WRITE_WORD, Action.WRITE_NEW_LINE, + Action.TRIGGER, + Action.ROLLOVER, + Action.TRIGGER, + Action.WRITE_WORD, Action.WRITE_NEW_LINE, + Action.WRITE_WORD, Action.WRITE_NUL, + Action.OVERWRITE_NUL, Action.WRITE_NEW_LINE, + Action.WRITE_WORD, Action.WRITE_NEW_LINE, + Action.SWITCH_FILE, + Action.WRITE_WORD, Action.WRITE_NEW_LINE + ); + + // WHEN + // THEN + testScenario(actions); + } + + @Test + public void testSimpleScenario2() throws Exception { + // GIVEN + List actions = Arrays.asList( + Action.WRITE_WORD, Action.WRITE_NEW_LINE, + Action.WRITE_WORD, + Action.WRITE_NUL, + Action.TRIGGER, + Action.WRITE_WORD, Action.WRITE_NEW_LINE, + Action.OVERWRITE_NUL + ); + + // WHEN + // THEN + testScenario(actions); + } + + @Test + public void testSimpleScenario3() throws Exception { + // GIVEN + List actions = Arrays.asList( + Action.WRITE_WORD, + Action.WRITE_NUL, + Action.TRIGGER, + Action.WRITE_WORD, Action.WRITE_NEW_LINE, + Action.OVERWRITE_NUL + ); + + // WHEN + // THEN + testScenario(actions); + } + + @Test + public void testSimpleScenario4() throws Exception { + // GIVEN + List actions = Arrays.asList( + Action.WRITE_WORD, Action.WRITE_NEW_LINE, + Action.ROLLOVER, + Action.TRIGGER, + Action.WRITE_WORD, Action.WRITE_NEW_LINE, + Action.SWITCH_FILE, + Action.WRITE_WORD, Action.WRITE_NEW_LINE + ); + + // WHEN + // THEN + testScenario(actions); + } + + @Test + public void testSimpleScenario5() throws Exception { + // GIVEN + List actions = Arrays.asList( + Action.WRITE_WORD, Action.WRITE_NEW_LINE, + Action.TRIGGER, + Action.WRITE_WORD, Action.WRITE_NEW_LINE, + Action.WRITE_NUL, + Action.TRIGGER, + Action.OVERWRITE_NUL, + Action.ROLLOVER, + Action.WRITE_WORD, Action.WRITE_NEW_LINE + ); + + // WHEN + // THEN + testScenario(actions); + } +}