BUG-7267: catch RuntimeExceptions when processing sources 51/49351/6
authorRobert Varga <rovarga@cisco.com>
Wed, 14 Dec 2016 13:30:40 +0000 (14:30 +0100)
committerRobert Varga <nite@hq.sk>
Thu, 15 Dec 2016 14:00:25 +0000 (14:00 +0000)
This adds wrapping of all RuntimeExceptions when building
EffectiveSchemaContext. This will allow users to identify
the offending source.

Since raw RuntimeExceptions should not be happening, but
rather should be specialized to SourceException and its
subclasses, also emit a warning guiding users to file
issues to fix codepaths which do not do so.

Also fixes up some instances where we use checkArgument()
and we ca actually use SourceException.throwIf().

Change-Id: Ie1c3d05b39b251996b746c38c5b7a51946b19ff5
Signed-off-by: Robert Varga <rovarga@cisco.com>
yang/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/stmt/reactor/BuildGlobalContext.java
yang/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/stmt/rfc6020/AugmentStatementImpl.java
yang/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/stmt/rfc6020/ImportStatementDefinition.java
yang/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/stmt/rfc6020/KeyStatementImpl.java
yang/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/stmt/rfc6020/Utils.java
yang/yang-parser-impl/src/test/java/org/opendaylight/yangtools/yang/stmt/AugmentArgumentParsingTest.java
yang/yang-parser-impl/src/test/java/org/opendaylight/yangtools/yang/stmt/Bug4933Test.java
yang/yang-parser-impl/src/test/java/org/opendaylight/yangtools/yang/stmt/KeyTest.java
yang/yang-parser-impl/src/test/java/org/opendaylight/yangtools/yang/stmt/SubstatementValidatorTest.java
yang/yang-parser-impl/src/test/java/org/opendaylight/yangtools/yang/stmt/yin/YinFileStmtTest.java

index 8781a3a442a6a4041b7748c36bd9d818a00d6192..6e67cd340b8720531478b9614d1007fc7ac0142f 100644 (file)
@@ -205,21 +205,37 @@ class BuildGlobalContext extends NamespaceStorageSupport implements NamespaceBeh
         return transformEffective();
     }
 
+    private SomeModifiersUnresolvedException propagateException(final SourceSpecificContext source,
+            final RuntimeException cause) throws SomeModifiersUnresolvedException {
+        final SourceIdentifier sourceId = Utils.createSourceIdentifier(source.getRoot());
+        if (!(cause instanceof SourceException)) {
+            /*
+             * This should not be happening as all our processing should provide SourceExceptions.
+             * We will wrap the exception to provide enough information to identify the problematic model,
+             * but also emit a warning so the offending codepath will get fixed.
+             */
+            LOG.warn("Unexpected error processing source {}. Please file an issue with this model attached.",
+                sourceId, cause);
+        }
+
+        throw new SomeModifiersUnresolvedException(currentPhase, sourceId, cause);
+    }
+
     private EffectiveSchemaContext transformEffective() throws ReactorException {
         Preconditions.checkState(finishedPhase == ModelProcessingPhase.EFFECTIVE_MODEL);
         final List<DeclaredStatement<?>> rootStatements = new ArrayList<>(sources.size());
         final List<EffectiveStatement<?, ?>> rootEffectiveStatements = new ArrayList<>(sources.size());
-        SourceIdentifier sourceId = null;
 
         try {
             for (final SourceSpecificContext source : sources) {
                 final RootStatementContext<?, ?, ?> root = source.getRoot();
-                sourceId = Utils.createSourceIdentifier(root);
-                rootStatements.add(root.buildDeclared());
-                rootEffectiveStatements.add(root.buildEffective());
+                try {
+                    rootStatements.add(root.buildDeclared());
+                    rootEffectiveStatements.add(root.buildEffective());
+                } catch (final RuntimeException ex) {
+                    throw propagateException(source, ex);
+                }
             }
-        } catch (final SourceException ex) {
-            throw new SomeModifiersUnresolvedException(currentPhase, sourceId, ex);
         } finally {
             RecursiveObjectLeaker.cleanup();
         }
@@ -241,9 +257,8 @@ class BuildGlobalContext extends NamespaceStorageSupport implements NamespaceBeh
         for (final SourceSpecificContext source : sources) {
             try {
                 source.loadStatements();
-            } catch (final SourceException ex) {
-                final SourceIdentifier sourceId = Utils.createSourceIdentifier(source.getRoot());
-                throw new SomeModifiersUnresolvedException(currentPhase, sourceId, ex);
+            } catch (final RuntimeException ex) {
+                throw propagateException(source, ex);
             }
         }
     }
@@ -298,35 +313,33 @@ class BuildGlobalContext extends NamespaceStorageSupport implements NamespaceBeh
     private void completePhaseActions() throws ReactorException {
         Preconditions.checkState(currentPhase != null);
         final List<SourceSpecificContext> sourcesToProgress = Lists.newArrayList(sources);
-        SourceIdentifier sourceId = null;
-        try {
-            boolean progressing = true;
-            while (progressing) {
-                // We reset progressing to false.
-                progressing = false;
-                final Iterator<SourceSpecificContext> currentSource = sourcesToProgress.iterator();
-                while (currentSource.hasNext()) {
-                    final SourceSpecificContext nextSourceCtx = currentSource.next();
-                    sourceId = Utils.createSourceIdentifier(nextSourceCtx.getRoot());
+        boolean progressing = true;
+        while (progressing) {
+            // We reset progressing to false.
+            progressing = false;
+            final Iterator<SourceSpecificContext> currentSource = sourcesToProgress.iterator();
+            while (currentSource.hasNext()) {
+                final SourceSpecificContext nextSourceCtx = currentSource.next();
+                try {
                     final PhaseCompletionProgress sourceProgress = nextSourceCtx.tryToCompletePhase(currentPhase);
                     switch (sourceProgress) {
-                    case FINISHED:
-                        currentSource.remove();
-                        // Fallback to progress, since we were able to make
-                        // progress in computation
-                    case PROGRESS:
-                        progressing = true;
-                        break;
-                    case NO_PROGRESS:
-                        // Noop
-                        break;
-                    default:
-                        throw new IllegalStateException("Unsupported phase progress " + sourceProgress);
+                        case FINISHED:
+                            currentSource.remove();
+                            // Fallback to progress, since we were able to make
+                            // progress in computation
+                        case PROGRESS:
+                            progressing = true;
+                            break;
+                        case NO_PROGRESS:
+                            // Noop
+                            break;
+                        default:
+                            throw new IllegalStateException("Unsupported phase progress " + sourceProgress);
                     }
+                } catch (RuntimeException ex) {
+                    throw propagateException(nextSourceCtx, ex);
                 }
             }
-        } catch (final SourceException e) {
-            throw new SomeModifiersUnresolvedException(currentPhase, sourceId, e);
         }
         if (!sourcesToProgress.isEmpty()) {
             final SomeModifiersUnresolvedException buildFailure = addSourceExceptions(sourcesToProgress);
index 19c644ed39855d444e6bc9feea1a12cd9af8afec..7651bc0615ea3eb59e6e16128d84b808c81377d8 100644 (file)
@@ -7,7 +7,6 @@
  */
 package org.opendaylight.yangtools.yang.parser.stmt.rfc6020;
 
-import com.google.common.base.Preconditions;
 import com.google.common.base.Verify;
 import com.google.common.collect.ImmutableList.Builder;
 import com.google.common.collect.ImmutableSet;
@@ -83,9 +82,10 @@ public class AugmentStatementImpl extends AbstractDeclaredStatement<SchemaNodeId
 
         @Override
         public SchemaNodeIdentifier parseArgumentValue(final StmtContext<?, ?, ?> ctx, final String value) {
-            Preconditions.checkArgument(!PATH_REL_PATTERN1.matcher(value).matches()
-                && !PATH_REL_PATTERN2.matcher(value).matches(),
-                "An argument for augment can be only absolute path; or descendant if used in uses");
+            SourceException.throwIf(PATH_REL_PATTERN1.matcher(value).matches()
+                || PATH_REL_PATTERN2.matcher(value).matches(), ctx.getStatementSourceReference(),
+                "Augment argument \'%s\' is not valid, it can be only absolute path; or descendant if used in uses",
+                value);
 
             return Utils.nodeIdentifierFromPath(ctx, value);
         }
index f4b9b4c440b5737ba499cdd262ebbcd785b3412b..37843bbfcfa51bc582a57658ce69b8873840a999 100644 (file)
@@ -49,6 +49,7 @@ import org.opendaylight.yangtools.yang.parser.spi.source.ImpPrefixToSemVerModule
 import org.opendaylight.yangtools.yang.parser.spi.source.ImportedModuleContext;
 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleCtxToModuleIdentifier;
 import org.opendaylight.yangtools.yang.parser.spi.source.ModuleNameToNamespace;
+import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
 import org.opendaylight.yangtools.yang.parser.stmt.rfc6020.effective.ImportEffectiveStatementImpl;
 
 public class ImportStatementDefinition extends
@@ -104,8 +105,10 @@ public class ImportStatementDefinition extends
                 final URI importedModuleNamespace = importedModuleContext.getFromNamespace(ModuleNameToNamespace.class,
                         moduleName);
                 Verify.verifyNotNull(importedModuleNamespace);
-                final String impPrefix = firstAttributeOf(stmt.declaredSubstatements(), PrefixStatement.class);
-                Verify.verifyNotNull(impPrefix);
+                final String impPrefix = SourceException.throwIfNull(
+                    firstAttributeOf(stmt.declaredSubstatements(), PrefixStatement.class),
+                    stmt.getStatementSourceReference(), "Missing prefix statement");
+
                 stmt.addToNs(ImpPrefixToNamespace.class, impPrefix, importedModuleNamespace);
             }
 
index 451e6130b23468df3a28386eb5033fa02262a182..74f968f312482e2864d5475744acccbca11705f1 100644 (file)
@@ -7,7 +7,6 @@
  */
 package org.opendaylight.yangtools.yang.parser.stmt.rfc6020;
 
-import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSet.Builder;
 import java.util.Collection;
@@ -20,6 +19,7 @@ import org.opendaylight.yangtools.yang.parser.spi.meta.AbstractDeclaredStatement
 import org.opendaylight.yangtools.yang.parser.spi.meta.AbstractStatementSupport;
 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContext;
 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils;
+import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
 import org.opendaylight.yangtools.yang.parser.stmt.rfc6020.effective.KeyEffectiveStatementImpl;
 
 public class KeyStatementImpl extends AbstractDeclaredStatement<Collection<SchemaNodeIdentifier>> implements
@@ -52,9 +52,8 @@ public class KeyStatementImpl extends AbstractDeclaredStatement<Collection<Schem
 
             // Throws NPE on nulls, retains first inserted value, cannot be modified
             final Collection<SchemaNodeIdentifier> ret = builder.build();
-
-            Preconditions.checkArgument(ret.size() == tokens, "Key argument '%s' contains duplicates. At %s", value,
-                    ctx.getStatementSourceReference());
+            SourceException.throwIf(ret.size() != tokens, ctx.getStatementSourceReference(),
+                    "Key argument '%s' contains duplicates", value);
             return ret;
         }
 
@@ -71,7 +70,7 @@ public class KeyStatementImpl extends AbstractDeclaredStatement<Collection<Schem
         }
 
         @Override
-        public void onFullDefinitionDeclared(StmtContext.Mutable<Collection<SchemaNodeIdentifier>, KeyStatement,
+        public void onFullDefinitionDeclared(final StmtContext.Mutable<Collection<SchemaNodeIdentifier>, KeyStatement,
                 EffectiveStatement<Collection<SchemaNodeIdentifier>, KeyStatement>> stmt) {
             super.onFullDefinitionDeclared(stmt);
             SUBSTATEMENT_VALIDATOR.validate(stmt);
index 716a48fa9a391ecad6cb3e51f4dfd60147900e59..0a5bfbc8c02262373e7b10a9cf6ba41e4aea12dd 100644 (file)
@@ -52,6 +52,7 @@ import org.opendaylight.yangtools.yang.model.repo.api.RevisionSourceIdentifier;
 import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
 import org.opendaylight.yangtools.yang.model.util.RevisionAwareXPathImpl;
 import org.opendaylight.yangtools.yang.parser.spi.meta.CopyType;
+import org.opendaylight.yangtools.yang.parser.spi.meta.InferenceException;
 import org.opendaylight.yangtools.yang.parser.spi.meta.QNameCacheNamespace;
 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContext;
 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils;
@@ -445,9 +446,9 @@ public final class Utils {
             try {
                 final QName qName = Utils.qNameFromArgument(ctx, nodeName);
                 qNames.add(qName);
-            } catch (final Exception e) {
-                throw new IllegalArgumentException(
-                    String.format("Failed to parse node '%s' in path '%s'", nodeName, path), e);
+            } catch (final RuntimeException e) {
+                throw new SourceException(ctx.getStatementSourceReference(), e,
+                        "Failed to parse node '%s' in path '%s'", nodeName, path);
             }
         }
 
@@ -519,9 +520,9 @@ public final class Utils {
             break;
         }
 
-        Preconditions.checkArgument(qNameModule != null,
-                "Error in module '%s': can not resolve QNameModule for '%s'. Statement source at %s",
-                ctx.getRoot().rawStatementArgument(), value, ctx.getStatementSourceReference());
+        qNameModule = InferenceException.throwIfNull(qNameModule, ctx.getStatementSourceReference(),
+            "Cannot resolve QNameModule for '%s'", value);
+
         final QNameModule resultQNameModule;
         if (qNameModule.getRevision() == null) {
             resultQNameModule = QNameModule.create(qNameModule.getNamespace(), SimpleDateFormatUtil.DEFAULT_DATE_REV)
@@ -582,9 +583,8 @@ public final class Utils {
     }
 
     public static DeviateKind parseDeviateFromString(final StmtContext<?, ?, ?> ctx, final String deviateKeyword) {
-        return Preconditions.checkNotNull(KEYWORD_TO_DEVIATE_MAP.get(deviateKeyword),
-                "String '%s' is not valid deviate argument. Statement source at %s", deviateKeyword,
-                ctx.getStatementSourceReference());
+        return SourceException.throwIfNull(KEYWORD_TO_DEVIATE_MAP.get(deviateKeyword),
+            ctx.getStatementSourceReference(), "String '%s' is not valid deviate argument", deviateKeyword);
     }
 
     public static Status parseStatus(final String value) {
index 0f4c8fd22d4a124e45ee26d65c4e9c653d9c0c01..d72b1ed9c6d670cde4cc7a496fd8ca21a1501851 100644 (file)
@@ -10,6 +10,7 @@ package org.opendaylight.yangtools.yang.stmt;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import org.junit.Ignore;
@@ -41,7 +42,7 @@ public class AugmentArgumentParsingTest {
             "/semantic-statement-parser/augment-arg-parsing/root-invalid-xpath.yang", false);
 
     @Test
-    public void validAugAbsTest() throws SourceException, ReactorException {
+    public void validAugAbsTest() throws ReactorException {
 
         BuildAction reactor = YangInferencePipeline.RFC6020_REACTOR.newBuild();
         addSources(reactor, IMPORTED, VALID_ARGS);
@@ -51,7 +52,7 @@ public class AugmentArgumentParsingTest {
     }
 
     @Test
-    public void invalidAugRel1Test() throws SourceException, ReactorException {
+    public void invalidAugRel1Test() {
 
         BuildAction reactor = YangInferencePipeline.RFC6020_REACTOR.newBuild();
         addSources(reactor, INVALID_REL1);
@@ -59,13 +60,13 @@ public class AugmentArgumentParsingTest {
         try {
             reactor.build();
             fail("reactor.process should fail due to invalid relative path");
-        } catch (Exception e) {
-            assertEquals(IllegalArgumentException.class, e.getClass());
+        } catch (ReactorException e) {
+            assertSourceExceptionCause(e, "Augment argument './aug1/aug11' is not valid");
         }
     }
 
     @Test
-    public void invalidAugRel2Test() throws SourceException, ReactorException {
+    public void invalidAugRel2Test() {
 
         BuildAction reactor = YangInferencePipeline.RFC6020_REACTOR.newBuild();
         addSources(reactor, INVALID_REL2);
@@ -73,13 +74,13 @@ public class AugmentArgumentParsingTest {
         try {
             reactor.build();
             fail("reactor.process should fail due to invalid relative path");
-        } catch (Exception e) {
-            assertEquals(IllegalArgumentException.class, e.getClass());
+        } catch (ReactorException e) {
+            assertSourceExceptionCause(e, "Augment argument '../aug1/aug11' is not valid");
         }
     }
 
     @Test
-    public void invalidAugAbs() throws SourceException, ReactorException {
+    public void invalidAugAbs() {
 
         BuildAction reactor = YangInferencePipeline.RFC6020_REACTOR.newBuild();
         addSources(reactor, INVALID_ABS);
@@ -87,13 +88,13 @@ public class AugmentArgumentParsingTest {
         try {
             reactor.build();
             fail("reactor.process should fail due to invalid absolute path");
-        } catch (Exception e) {
-            assertEquals(IllegalArgumentException.class, e.getClass());
+        } catch (ReactorException e) {
+            assertSourceExceptionCause(e, "Augment argument '//aug1/aug11/aug111' is not valid");
         }
     }
 
     @Test
-    public void invalidAugAbsPrefixedNoImp() throws SourceException, ReactorException {
+    public void invalidAugAbsPrefixedNoImp() {
 
         BuildAction reactor = YangInferencePipeline.RFC6020_REACTOR.newBuild();
         addSources(reactor, INVALID_ABS_PREFIXED_NO_IMP);
@@ -101,8 +102,8 @@ public class AugmentArgumentParsingTest {
         try {
             reactor.build();
             fail("reactor.process should fail due to missing import from augment path");
-        } catch (Exception e) {
-            assertEquals(IllegalArgumentException.class, e.getClass());
+        } catch (ReactorException e) {
+            assertSourceExceptionCause(e, "Failed to parse node 'imp:aug1'");
         }
     }
 
@@ -136,6 +137,12 @@ public class AugmentArgumentParsingTest {
         }
     }
 
+    private static void assertSourceExceptionCause(final Throwable e, final String start) {
+        final Throwable cause = e.getCause();
+        assertTrue(cause instanceof SourceException);
+        assertTrue(cause.getMessage().startsWith(start));
+    }
+
     private static void addSources(final BuildAction reactor, final YangStatementSourceImpl... sources) {
         for (YangStatementSourceImpl source : sources) {
             reactor.addSource(source);
index b927b18325d2174a913bd273ba86ea290acde4d2..948f4e3ed19a5fde2f5fa20aa18b9c7a443d9e68 100644 (file)
@@ -24,7 +24,7 @@ import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
 public class Bug4933Test {
 
     @Test
-    public void test() throws SourceException, ReactorException, FileNotFoundException, URISyntaxException {
+    public void test() throws ReactorException, FileNotFoundException, URISyntaxException {
         SchemaContext context = StmtTestUtils.parseYangSources("/bugs/bug4933/correct");
         assertNotNull(context);
 
@@ -33,13 +33,14 @@ public class Bug4933Test {
     }
 
     @Test
-    public void incorrectKeywordTest() throws SourceException, ReactorException, FileNotFoundException,
-            URISyntaxException {
+    public void incorrectKeywordTest() throws FileNotFoundException, URISyntaxException {
         try {
             StmtTestUtils.parseYangSources("/bugs/bug4933/incorrect");
-            fail("NullPointerException should be thrown.");
-        } catch (NullPointerException e) {
-            assertTrue(e.getMessage().startsWith("String 'not_supported' is not valid deviate argument. Statement source at"));
+            fail("ReactorException should be thrown.");
+        } catch (ReactorException e) {
+            final Throwable cause = e.getCause();
+            assertTrue(cause instanceof SourceException);
+            assertTrue(cause.getMessage().startsWith("String 'not_supported' is not valid deviate argument"));
         }
     }
 }
index 5c3eb5f16c3c2a2e82e016a78c67e625ce947df2..3d2a772ab135c5beb7e34720a80c950d71fdfbde 100644 (file)
@@ -8,9 +8,10 @@
 
 package org.opendaylight.yangtools.yang.stmt;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+
 import org.junit.Test;
 import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException;
 import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
@@ -27,7 +28,7 @@ public class KeyTest {
             "/semantic-statement-parser/key-arg-parsing/key-comp-duplicate.yang", false);
 
     @Test
-    public void keySimpleTest() throws SourceException, ReactorException {
+    public void keySimpleTest() throws ReactorException {
 
         BuildAction reactor = YangInferencePipeline.RFC6020_REACTOR.newBuild();
         addSources(reactor, KEY_SIMPLE_AND_COMP);
@@ -37,7 +38,7 @@ public class KeyTest {
     }
 
     @Test
-    public void keyCompositeInvalid() throws SourceException, ReactorException {
+    public void keyCompositeInvalid() {
 
         BuildAction reactor = YangInferencePipeline.RFC6020_REACTOR.newBuild();
         addSources(reactor, KEY_COMP_DUPLICATE);
@@ -45,8 +46,10 @@ public class KeyTest {
         try {
             reactor.build();
             fail("reactor.process should fail due to duplicate name in key");
-        } catch (Exception e) {
-            assertEquals(IllegalArgumentException.class, e.getClass());
+        } catch (ReactorException e) {
+            final Throwable cause = e.getCause();
+            assertTrue(cause instanceof SourceException);
+            assertTrue(cause.getMessage().startsWith("Key argument 'key1 key2 key2' contains duplicates"));
         }
     }
 
index f57a2a6a426b181b1ee7185b1d4c71f229fd5aa2..b4a2417718506f2e5e94ee56d88a63ba342d9810 100644 (file)
@@ -13,7 +13,6 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import com.google.common.base.VerifyException;
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
 import java.io.UnsupportedEncodingException;
@@ -78,7 +77,7 @@ public class SubstatementValidatorTest {
 
     @Test
     public void missingElementException() throws URISyntaxException, ReactorException {
-        expectedEx.expect(VerifyException.class);
+        expectedEx.expect(SomeModifiersUnresolvedException.class);
 
         TestUtils.loadModules(getClass().getResource("/substatement-validator/missing-element").toURI());
     }
index ed4d952184e4fb47619a472049a9c35ce261b5c6..69e4a6e6060f3423dee9e63b650a7d190b0c2dd5 100644 (file)
@@ -9,6 +9,8 @@ package org.opendaylight.yangtools.yang.stmt.yin;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.net.URISyntaxException;
 import java.util.Set;
@@ -63,12 +65,19 @@ public class YinFileStmtTest {
     }
 
     // parsing yin file with duplicate key name in a list statement
-    @Test(expected = IllegalArgumentException.class)
-    public void readAndParseInvalidYinFileTest2() throws ReactorException {
+    public void readAndParseInvalidYinFileTest2() {
         CrossSourceStatementReactor.BuildAction reactor = YangInferencePipeline.RFC6020_REACTOR.newBuild();
         addSources(reactor, INVALID_YIN_FILE_2);
-        EffectiveSchemaContext result = reactor.buildEffective();
-        assertNotNull(result);
+
+        try {
+            reactor.buildEffective();
+            fail("Reactor exception should have been thrown");
+        } catch (ReactorException e) {
+            final Throwable cause = e.getCause();
+            assertTrue(cause instanceof SourceException);
+            assertTrue(cause.getMessage().startsWith(
+                "Key argument 'testing-string testing-string' contains duplicates"));
+        }
     }
 
     @Test