Repository: xlate/staedi (java)

Issue:
Implementation loop cursor not properly reset for multiple occurrences
When reading an input stream containing multiple occurrences of an implementation loop not in the exact order specified in the implementation schema, occurrences fail to match earlier discriminator values once they are passed the first time.

E.g. 837 implementation loops received:
- 2000A
- 2000B
- 2000C
- 2000A
- 2000B
- 2000C

Would be incorrectly processed as:
- 2000A
- 2000B
- 2000C
- 2000C
- 2000C
- 2000C


Pull request description:
Allow implementation nodes to occur/reoccur in any order in the input
Fixes #223 

Patch:
diff --git a/src/main/java/io/xlate/edi/internal/stream/tokenization/ProxyEventHandler.java b/src/main/java/io/xlate/edi/internal/stream/tokenization/ProxyEventHandler.java
index da16e57..4d47b4e 100644
--- a/src/main/java/io/xlate/edi/internal/stream/tokenization/ProxyEventHandler.java
+++ b/src/main/java/io/xlate/edi/internal/stream/tokenization/ProxyEventHandler.java
@@ -564,72 +564,14 @@ public class ProxyEventHandler implements EventHandler {
                               Location location) {
 
         StreamEvent target = getPooledEvent();
-        EDIStreamEvent associatedEvent = eventQueue.isEmpty() ? null : getAssociatedEvent(error);
-
-        if (eventExists(associatedEvent)) {
-            /*
-             * Ensure segment errors occur before other event types
-             * when the array has other events already present.
-             */
-            int offset = eventQueue.size();
-            boolean complete = false;
-
-            while (!complete) {
-                StreamEvent enqueuedEvent = eventQueue.get(offset - 1);
-
-                if (enqueuedEvent.type == associatedEvent) {
-                    complete = true;
-                } else {
-                    if (eventQueue.size() == offset) {
-                        eventQueue.add(offset, enqueuedEvent);
-                    } else {
-                        eventQueue.set(offset, enqueuedEvent);
-                    }
-                    offset--;
-                }
-            }
-
-            eventQueue.set(offset, target);
-        } else {
-            eventQueue.add(target);
-        }
 
         target.type = event;
         target.errorType = error;
         target.setData(data);
         target.setTypeReference(typeReference);
         target.setLocation(location);
-    }
 
-    private boolean eventExists(EDIStreamEvent associatedEvent) {
-        int offset = eventQueue.size();
-
-        while (associatedEvent != null && offset > 0) {
-            if (eventQueue.get(offset - 1).type == associatedEvent) {
-                return true;
-            }
-            offset--;
-        }
-
-        return false;
+        eventQueue.add(target);
     }
 
-    private static EDIStreamEvent getAssociatedEvent(EDIStreamValidationError error) {
-        final EDIStreamEvent event;
-
-        switch (error) {
-        case IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES:
-            event = EDIStreamEvent.END_LOOP;
-            break;
-        case MANDATORY_SEGMENT_MISSING:
-        case IMPLEMENTATION_SEGMENT_BELOW_MINIMUM_USE:
-            event = null;
-            break;
-        default:
-            event = null;
-            break;
-        }
-
-        return event;
-    }
 }
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/UsageNode.java b/src/main/java/io/xlate/edi/internal/stream/validation/UsageNode.java
index 46a3eb7..0b34b54 100644
--- a/src/main/java/io/xlate/edi/internal/stream/validation/UsageNode.java
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/UsageNode.java
@@ -18,6 +18,7 @@ package io.xlate.edi.internal.stream.validation;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
 import io.xlate.edi.internal.stream.tokenization.Dialect;
@@ -43,10 +44,7 @@ class UsageNode {
     private int usageCount;
 
     UsageNode(UsageNode parent, int depth, EDIReference link, int siblingIndex) {
-        if (link == null) {
-            throw new NullPointerException();
-        }
-
+        Objects.requireNonNull(link, "link");
         this.parent = parent;
         this.depth = depth;
         this.link = link;
@@ -67,14 +65,21 @@ class UsageNode {
         return node == null || node.hasMinimumUsage(version);
     }
 
-    public static UsageNode getParent(UsageNode node) {
-        return node != null ? node.getParent() : null;
-    }
-
     public static UsageNode getFirstChild(UsageNode node) {
         return node != null ? node.getFirstChild() : null;
     }
 
+    public UsageNode getFirstSiblingSameType() {
+        EDIType type = getReferencedType();
+        UsageNode sibling = getFirstSibling();
+
+        while (!type.equals(sibling.getReferencedType())) {
+            sibling = sibling.getNextSibling();
+        }
+
+        return sibling;
+    }
+
     public static void resetChildren(UsageNode... nodes) {
         for (UsageNode node : nodes) {
             if (node != null) {
@@ -228,14 +233,17 @@ class UsageNode {
         return getChild(0);
     }
 
-    UsageNode getChildById(CharSequence id) {
-        return children.stream()
-                       .filter(c -> c != null && c.getId().contentEquals(id))
-                       .findFirst()
-                       .orElse(null);
+    private UsageNode getChildById(CharSequence id) {
+        for (UsageNode child : children) {
+            if (child.getId().contentEquals(id)) {
+                return child;
+            }
+        }
+        return null;
     }
 
     UsageNode getSiblingById(CharSequence id) {
         return parent != null ? parent.getChildById(id) : null;
     }
+
 }
diff --git a/src/main/java/io/xlate/edi/internal/stream/validation/Validator.java b/src/main/java/io/xlate/edi/internal/stream/validation/Validator.java
index 281b36e..b05a424 100644
--- a/src/main/java/io/xlate/edi/internal/stream/validation/Validator.java
+++ b/src/main/java/io/xlate/edi/internal/stream/validation/Validator.java
@@ -120,10 +120,10 @@ public class Validator {
         }
 
         void nagivateUp(int limit) {
-            standard = UsageNode.getParent(standard);
+            standard = standard.getParent();
 
             if (impl != null && impl.getDepth() > limit) {
-                impl = UsageNode.getParent(impl);
+                impl = impl.getParent().getFirstSiblingSameType();
             }
         }
 
@@ -439,7 +439,7 @@ public class Validator {
                  */
                 checkMinimumUsage(cursor.standard);
 
-                UsageNode nextImpl = checkMinimumImplUsage(cursor.impl, cursor.standard);
+                UsageNode nextImpl = getNextImplementationNode(cursor.impl, cursor.standard.getReferencedType());
 
                 if (cursor.hasNextSibling()) {
                     // Advance to the next segment in the loop
@@ -461,16 +461,6 @@ public class Validator {
         handleMissingMandatory(handler);
     }
 
-    UsageNode checkMinimumImplUsage(UsageNode nextImpl, UsageNode current) {
-        while (nextImpl != null && nextImpl.getReferencedType().equals(current.getReferencedType())) {
-            // Advance past multiple implementations of the 'current' standard node
-            checkMinimumUsage(nextImpl);
-            nextImpl = nextImpl.getNextSibling();
-        }
-
-        return nextImpl;
-    }
-
     boolean handleNode(CharSequence tag, UsageNode current, UsageNode currentImpl, int startDepth, ValidationEventHandler handler) {
         final boolean handled;
 
@@ -589,6 +579,17 @@ public class Validator {
         }
     }
 
+    UsageNode getNextImplementationNode(UsageNode implNode, EDIType type) {
+        while (implNode != null && implNode.getReferencedType().equals(type)) {
+            // Advance past multiple implementations of the 'current' standard node
+            checkMinimumUsage(implNode);
+            implNode = implNode.getNextSibling();
+        }
+
+        // `implNode` will be an implementation of the type following `type`
+        return implNode;
+    }
+
     boolean handleLoop(CharSequence tag, UsageNode current, UsageNode currentImpl, int startDepth, ValidationEventHandler handler) {
         if (!current.getFirstChild().getId().contentEquals(tag)) {
             return false;
@@ -781,7 +782,6 @@ public class Validator {
     }
 
     void handleImplementationSelected(UsageNode candidate, UsageNode implSeg, ValidationEventHandler handler) {
-        checkMinimumImplUsage(implNode, candidate, handler);
         implSegmentCandidates.clear();
         implNode = implSeg;
         implSegmentSelected = true;
@@ -820,14 +820,6 @@ public class Validator {
         }
     }
 
-    void checkMinimumImplUsage(UsageNode sibling, UsageNode selected, ValidationEventHandler handler) {
-        while (sibling != null && sibling != selected) {
-            checkMinimumUsage(sibling);
-            sibling = sibling.getNextSibling();
-        }
-        handleMissingMandatory(handler);
-    }
-
     /**
      * Validate any implementation elements previously skipped while searching
      * for the loop discriminator element. Validation of enumerated values specified


Test patch:
diff --git a/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java b/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java
index 74c2251..587f836 100644
--- a/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java
+++ b/src/test/java/io/xlate/edi/internal/stream/SegmentValidationTest.java
@@ -23,6 +23,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 
 import org.junit.jupiter.api.Test;
 
@@ -37,6 +40,8 @@ import io.xlate.edi.stream.EDIStreamException;
 import io.xlate.edi.stream.EDIStreamFilter;
 import io.xlate.edi.stream.EDIStreamReader;
 import io.xlate.edi.stream.EDIStreamValidationError;
+import io.xlate.edi.test.StaEDITestEvent;
+import io.xlate.edi.test.StaEDITestUtil;
 
 @SuppressWarnings("resource")
 class SegmentValidationTest {
@@ -420,20 +425,13 @@ class SegmentValidationTest {
         Schema schema = schemaFactory.createSchema(schemaLocation);
 
         EDIStreamReader unfiltered = factory.createEDIStreamReader(stream, schema);
-        EDIStreamReader reader = factory.createFilteredReader(unfiltered, new EDIStreamFilter() {
-            @Override
-            public boolean accept(EDIStreamReader reader) {
-                switch (reader.getEventType()) {
-                case START_TRANSACTION:
-                case SEGMENT_ERROR:
-                case START_LOOP:
-                case END_LOOP:
-                    return true;
-                default:
-                    return false;
-                }
-            }
-        });
+        EDIStreamReader reader = StaEDITestUtil.filterEvents(
+            factory,
+            unfiltered,
+            EDIStreamEvent.START_TRANSACTION,
+            EDIStreamEvent.SEGMENT_ERROR,
+            EDIStreamEvent.START_LOOP,
+            EDIStreamEvent.END_LOOP);
 
         assertEvent(reader, EDIStreamEvent.START_TRANSACTION);
         reader.setTransactionSchema(schemaFactory.createSchema(getClass().getResource("/x12/EDISchemaSegmentValidationImpl.xml")));
@@ -444,7 +442,6 @@ class SegmentValidationTest {
         assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_SEGMENT_BELOW_MINIMUM_USE, "S13");
         assertEvent(reader, EDIStreamEvent.END_LOOP, "0000A");
 
-
         // Loop B - Occurrence 1
         assertEvent(reader, EDIStreamEvent.START_LOOP, "0000B");
         assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.SEGMENT_EXCEEDS_MAXIMUM_USE, "S12");
@@ -463,8 +460,6 @@ class SegmentValidationTest {
         assertEvent(reader, EDIStreamEvent.START_LOOP, "0000C");
         assertEvent(reader, EDIStreamEvent.END_LOOP, "0000C");
 
-        assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, "S11");
-
         // Loop D - Occurrence 1
         assertEvent(reader, EDIStreamEvent.START_LOOP, "0000D");
 
@@ -472,12 +467,78 @@ class SegmentValidationTest {
         assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.LOOP_OCCURS_OVER_MAXIMUM_TIMES, "S11", "L0000");
         assertEvent(reader, EDIStreamEvent.END_LOOP, "0000D");
 
+        assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_LOOP_OCCURS_UNDER_MINIMUM_TIMES, "S11");
+
         // Loop L0001 has minOccurs=1 in standard (not used in implementation, invalid configuration)
         assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.MANDATORY_SEGMENT_MISSING, "S20");
 
         assertTrue(!reader.hasNext(), "Unexpected segment errors exist");
     }
 
+    @Test
+    void testImplementationValidAlternateSequence() throws EDISchemaException, EDIStreamException {
+        EDIInputFactory factory = EDIInputFactory.newFactory();
+        InputStream stream = new ByteArrayInputStream((""
+                + "ISA*00*          *00*          *ZZ*ReceiverID     *ZZ*Sender         *050812*1953*^*00501*508121953*0*P*:~"
+                + "S01*X~"
+                + "S0A*X~"
+                + "S11*A~"
+                + "S13*X~"
+                + "S11*C~"
+                + "S11*B~"
+                + "S12*X~"
+                + "S12*X~"
+                + "S11*D~"
+                + "S11*C~"
+                + "S09*X~"
+                + "IEA*1*508121953~").getBytes());
+
+        SchemaFactory schemaFactory = SchemaFactory.newFactory();
+        URL schemaLocation = getClass().getResource("/x12/EDISchemaSegmentValidation.xml");
+        Schema schema = schemaFactory.createSchema(schemaLocation);
+
+        EDIStreamReader unfiltered = factory.createEDIStreamReader(stream, schema);
+        EDIStreamReader reader = StaEDITestUtil.filterEvents(
+            factory,
+            unfiltered,
+            EDIStreamEvent.START_TRANSACTION,
+            EDIStreamEvent.SEGMENT_ERROR,
+            EDIStreamEvent.START_LOOP,
+            EDIStreamEvent.END_LOOP);
+
+        assertEvent(reader, EDIStreamEvent.START_TRANSACTION);
+        reader.setTransactionSchema(schemaFactory.createSchema(getClass().getResource("/x12/EDISchemaSegmentValidationImpl.xml")));
+
+        List<StaEDITestEvent> expected = Arrays.asList(
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, "TRANSACTION", "TRANSACTION"),
+            // Loop A
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0000", "0000A"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0000", "0000A"),
+            // Loop C - Occurrence 1
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0000", "0000C"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0000", "0000C"),
+            // Loop B
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0000", "0000B"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0000", "0000B"),
+            // Loop D
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0000", "0000D"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0000", "0000D"),
+            // Loop C - Occurrence 2
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0000", "0000C"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0000", "0000C"),
+
+            StaEDITestEvent.forError(EDIStreamValidationError.MANDATORY_SEGMENT_MISSING, "S20", "S20"));
+
+        List<StaEDITestEvent> events = new ArrayList<>();
+        events.add(StaEDITestEvent.from(reader, false));
+
+        while (reader.hasNext()) {
+            events.add(StaEDITestEvent.from(reader, false));
+        }
+
+        assertEquals(expected, events);
+    }
+
     @Test
     void testImplementationValidSequenceAllMissing() throws EDISchemaException, EDIStreamException {
         EDIInputFactory factory = EDIInputFactory.newFactory();
@@ -492,20 +553,13 @@ class SegmentValidationTest {
         Schema schema = schemaFactory.createSchema(schemaLocation);
 
         EDIStreamReader reader = factory.createEDIStreamReader(stream, schema);
-        reader = factory.createFilteredReader(reader, new EDIStreamFilter() {
-            @Override
-            public boolean accept(EDIStreamReader reader) {
-                switch (reader.getEventType()) {
-                case START_TRANSACTION:
-                case SEGMENT_ERROR:
-                case START_LOOP:
-                case END_LOOP:
-                    return true;
-                default:
-                    return false;
-                }
-            }
-        });
+        reader = StaEDITestUtil.filterEvents(
+            factory,
+            reader,
+            EDIStreamEvent.START_TRANSACTION,
+            EDIStreamEvent.SEGMENT_ERROR,
+            EDIStreamEvent.START_LOOP,
+            EDIStreamEvent.END_LOOP);
 
         assertEquals(EDIStreamEvent.START_TRANSACTION, reader.next(), "Expecting start of transaction");
         reader.setTransactionSchema(schemaFactory.createSchema(getClass().getResource("/x12/EDISchemaSegmentValidationImpl.xml")));
@@ -537,6 +591,7 @@ class SegmentValidationTest {
     @Test
     void testImplementationValidSequenceWithCompositeDiscr() throws EDISchemaException, EDIStreamException {
         EDIInputFactory factory = EDIInputFactory.newFactory();
+        SchemaFactory schemaFactory = SchemaFactory.newFactory();
         InputStream stream = new ByteArrayInputStream((""
                 + "ISA*00*          *00*          *ZZ*ReceiverID     *ZZ*Sender         *050812*1953*^*00501*508121953*0*P*:~"
                 + "S01*X~"
@@ -551,109 +606,90 @@ class SegmentValidationTest {
                 + "S31*B~"
                 + "S09*X~IEA*1*508121953~").getBytes());
 
-        SchemaFactory schemaFactory = SchemaFactory.newFactory();
         URL schemaLocation = getClass().getResource("/x12/EDISchemaSegmentValidation.xml");
         Schema schema = schemaFactory.createSchema(schemaLocation);
 
         EDIStreamReader reader = factory.createEDIStreamReader(stream, schema);
-        reader = factory.createFilteredReader(reader, new EDIStreamFilter() {
-            @Override
-            public boolean accept(EDIStreamReader reader) {
-                switch (reader.getEventType()) {
-                case START_TRANSACTION:
-                case SEGMENT_ERROR:
-                case START_LOOP:
-                case END_LOOP:
-                    return true;
-                default:
-                    return false;
-                }
-            }
-        });
+        reader = StaEDITestUtil.filterEvents(
+            factory,
+            reader,
+            EDIStreamEvent.START_TRANSACTION,
+            EDIStreamEvent.SEGMENT_ERROR,
+            EDIStreamEvent.START_LOOP,
+            EDIStreamEvent.END_LOOP);
 
-        assertEquals(EDIStreamEvent.START_TRANSACTION, reader.next(), "Expecting start of transaction");
+        assertEvent(reader, EDIStreamEvent.START_TRANSACTION);
         reader.setTransactionSchema(schemaFactory.createSchema(getClass().getResource("/x12/EDISchemaSegmentValidationImpl2.xml")));
 
-        // Loop A
-        assertEquals(EDIStreamEvent.START_LOOP, reader.next());
-        assertEquals("0000A", reader.getReferenceCode());
-
-        // Loop AXX
-        assertEquals(EDIStreamEvent.START_LOOP, reader.next());
-        assertEquals("0002AXX", reader.getReferenceCode());
-        assertEquals(EDIStreamEvent.END_LOOP, reader.next());
-        assertEquals("0002AXX", reader.getReferenceCode());
-
-        // Loop AYY
-        assertEquals(EDIStreamEvent.START_LOOP, reader.next());
-        assertEquals("0002AYY", reader.getReferenceCode());
-
-        assertEquals(EDIStreamEvent.SEGMENT_ERROR, reader.next());
-        assertEquals(EDIStreamValidationError.SEGMENT_EXCEEDS_MAXIMUM_USE, reader.getErrorType());
-        assertEquals("S31", reader.getReferenceCode());
-
-        assertEquals(EDIStreamEvent.SEGMENT_ERROR, reader.next());
-        assertEquals(EDIStreamValidationError.IMPLEMENTATION_SEGMENT_BELOW_MINIMUM_USE, reader.getErrorType());
-        assertEquals("S31", reader.getText());
-        assertEquals("S31B", reader.getReferenceCode());
-
-        assertEquals(EDIStreamEvent.END_LOOP, reader.next());
-        assertEquals("0002AYY", reader.getReferenceCode());
-
-        assertEquals(EDIStreamEvent.END_LOOP, reader.next());
-        assertEquals("0000A", reader.getReferenceCode());
+        List<StaEDITestEvent> expected = Arrays.asList(
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, "TRANSACTION", "TRANSACTION"),
+            // Loop A
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0000", "0000A"),
+            // Loop AXX
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0002", "0002AXX"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0002", "0002AXX"),
+            // Loop AYY
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0002", "0002AYY"),
+            StaEDITestEvent.forError(EDIStreamValidationError.SEGMENT_EXCEEDS_MAXIMUM_USE, "S31", "S31"),
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_SEGMENT_BELOW_MINIMUM_USE, "S31", "S31B"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0002", "0002AYY"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0000", "0000A"));
+
+        List<StaEDITestEvent> events = new ArrayList<>();
+        events.add(StaEDITestEvent.from(reader, false));
+
+        while (reader.hasNext()) {
+            events.add(StaEDITestEvent.from(reader, false));
+        }
 
-        assertTrue(!reader.hasNext(), "Unexpected segment errors exist");
+        assertEquals(expected, events);
     }
 
     @Test
     void testImplementation_Only_BHT_HL_Valid() throws EDISchemaException, EDIStreamException {
         EDIInputFactory factory = EDIInputFactory.newFactory();
+        SchemaFactory schemaFactory = SchemaFactory.newFactory();
         InputStream stream = getClass().getResourceAsStream("/x12/sample837-small.edi");
 
         EDIStreamReader reader = factory.createEDIStreamReader(stream);
-        reader = factory.createFilteredReader(reader, new EDIStreamFilter() {
-            @Override
-            public boolean accept(EDIStreamReader reader) {
-                switch (reader.getEventType()) {
-                case START_TRANSACTION:
-                case SEGMENT_ERROR:
-                case START_LOOP:
-                case END_LOOP:
-                    return true;
-                default:
-                    return false;
-                }
-            }
-        });
+        reader = StaEDITestUtil.filterEvents(
+            factory,
+            reader,
+            EDIStreamEvent.START_TRANSACTION,
+            EDIStreamEvent.SEGMENT_ERROR,
+            EDIStreamEvent.START_LOOP,
+            EDIStreamEvent.END_LOOP);
 
         assertEvent(reader, EDIStreamEvent.START_TRANSACTION);
-        SchemaFactory schemaFactory = SchemaFactory.newFactory();
-        URL schemaLocation = getClass().getResource("/x12/005010X222/837.xml");
-        Schema schema = schemaFactory.createSchema(schemaLocation);
-        reader.setTransactionSchema(schema);
-
-        // Occurrence 1
-        assertEvent(reader, EDIStreamEvent.START_LOOP, "L0001");
-        assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "NM1");
-        assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "PER");
-        assertEvent(reader, EDIStreamEvent.END_LOOP, "L0001");
-
-        // Occurrence 2
-        assertEvent(reader, EDIStreamEvent.START_LOOP, "L0001");
-        assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "NM1");
-        assertEvent(reader, EDIStreamEvent.END_LOOP, "L0001");
-
-        // Loop 2010A
-        assertEvent(reader, EDIStreamEvent.START_LOOP, "2010A");
-        assertEvent(reader, EDIStreamEvent.SEGMENT_ERROR, EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "PRV");
-        assertEvent(reader, EDIStreamEvent.END_LOOP, "2010A");
-
-        assertEvent(reader, EDIStreamEvent.START_TRANSACTION);
-        // Loop 2010A
-        assertEvent(reader, EDIStreamEvent.START_LOOP, "2010A");
-        assertEvent(reader, EDIStreamEvent.END_LOOP, "2010A");
+        reader.setTransactionSchema(schemaFactory.createSchema(getClass().getResource("/x12/005010X222/837.xml")));
+
+        List<StaEDITestEvent> expected = Arrays.asList(
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, "TRANSACTION", "TRANSACTION"),
+            // Occurrence 1
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0001", "L0001"),
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "NM1", "NM1"),
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "PER", "PER"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0001", "L0001"),
+            // Occurrence 2
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0001", "L0001"),
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "NM1", "NM1"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0001", "L0001"),
+            // Loop 2010A
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0002", "2010A"),
+            StaEDITestEvent.forError(EDIStreamValidationError.IMPLEMENTATION_UNUSED_SEGMENT_PRESENT, "PRV", "PRV"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0002", "2010A"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_TRANSACTION, "TRANSACTION", "TRANSACTION"),
+            // Loop 2010A
+            StaEDITestEvent.forEvent(EDIStreamEvent.START_LOOP, "L0002", "2010A"),
+            StaEDITestEvent.forEvent(EDIStreamEvent.END_LOOP, "L0002", "2010A"));
+
+        List<StaEDITestEvent> events = new ArrayList<>();
+        events.add(StaEDITestEvent.from(reader, false));
+
+        while (reader.hasNext()) {
+            events.add(StaEDITestEvent.from(reader, false));
+        }
 
-        assertTrue(!reader.hasNext(), "Unexpected events exist");
+        assertEquals(expected, events);
     }
 }
diff --git a/src/test/java/io/xlate/edi/test/StaEDITestEvent.java b/src/test/java/io/xlate/edi/test/StaEDITestEvent.java
new file mode 100644
index 0000000..ab920de
--- /dev/null
+++ b/src/test/java/io/xlate/edi/test/StaEDITestEvent.java
@@ -0,0 +1,85 @@
+package io.xlate.edi.test;
+
+import java.util.Objects;
+
+import io.xlate.edi.stream.EDIStreamEvent;
+import io.xlate.edi.stream.EDIStreamReader;
+import io.xlate.edi.stream.EDIStreamValidationError;
+import io.xlate.edi.stream.Location;
+
+public class StaEDITestEvent {
+
+    public final EDIStreamEvent event;
+    public final EDIStreamValidationError error;
+    public final String text;
+    public final String referenceCode;
+    public final Location location;
+
+    public StaEDITestEvent(EDIStreamEvent event, EDIStreamValidationError error, String text, String referenceCode, Location location) {
+        super();
+        this.event = event;
+        this.error = error;
+        this.text = text;
+        this.referenceCode = referenceCode;
+        this.location = location;
+    }
+
+    public static StaEDITestEvent from(EDIStreamReader reader, boolean includeLocation) {
+        EDIStreamEvent event = reader.getEventType();
+        boolean error;
+
+        switch (event) {
+        case SEGMENT_ERROR:
+        case ELEMENT_OCCURRENCE_ERROR:
+        case ELEMENT_DATA_ERROR:
+            error = true;
+            break;
+        default:
+            error = false;
+            break;
+        }
+
+        return new StaEDITestEvent(reader.getEventType(),
+                                   error ? reader.getErrorType() : null,
+                                   reader.getText(),
+                                   reader.getReferenceCode(),
+                                   includeLocation ? reader.getLocation().copy() : null);
+    }
+
+    public static StaEDITestEvent forError(EDIStreamValidationError error, String text, String referenceCode) {
+        return new StaEDITestEvent(error.getCategory(), error, text, referenceCode, null);
+    }
+
+    public static StaEDITestEvent forEvent(EDIStreamEvent event, String text, String referenceCode) {
+        return new StaEDITestEvent(event, null, text, referenceCode, null);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof StaEDITestEvent) {
+            StaEDITestEvent other = (StaEDITestEvent) obj;
+
+            return event == other.event
+                    && error == other.error
+                    && Objects.equals(text, other.text)
+                    && Objects.equals(referenceCode, other.referenceCode)
+                    && Objects.equals(location, other.location);
+        }
+
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder buffer = new StringBuilder("StaEDITestEvent");
+        buffer.append('(');
+
+        buffer.append("event=").append(event);
+        buffer.append(", ").append("error=").append(error);
+        buffer.append(", ").append("referenceCode=").append(referenceCode);
+        buffer.append(", ").append("text=").append(text);
+        buffer.append(", ").append("location=").append(location);
+
+        return buffer.append(')').append('\n').toString();
+    }
+}
diff --git a/src/test/java/io/xlate/edi/test/StaEDITestUtil.java b/src/test/java/io/xlate/edi/test/StaEDITestUtil.java
index a142487..f476224 100644
--- a/src/test/java/io/xlate/edi/test/StaEDITestUtil.java
+++ b/src/test/java/io/xlate/edi/test/StaEDITestUtil.java
@@ -87,4 +87,30 @@ public class StaEDITestUtil {
         String versionString = System.getProperty("java.version");
         return versionString.split("[\\._]");
     }
+
+    public static String toString(EDIStreamReader reader) {
+        StringBuilder buffer = new StringBuilder(reader.getClass().getName());
+        buffer.append('(');
+
+        EDIStreamEvent event = reader.getEventType();
+        buffer.append("event=").append(event);
+
+        switch (event) {
+        case SEGMENT_ERROR:
+        case ELEMENT_DATA_ERROR:
+        case ELEMENT_OCCURRENCE_ERROR:
+            buffer.append(", ").append("error=").append(reader.getErrorType());
+            buffer.append(", ").append("referenceCode=").append(reader.getReferenceCode());
+            break;
+        case ELEMENT_DATA:
+            buffer.append(", ").append("text=").append(reader.getText());
+            break;
+        default:
+            break;
+        }
+
+        buffer.append(", ").append("location=").append(reader.getLocation());
+
+        return buffer.append(')').toString();
+    }
 }