This is an automated email from the ASF dual-hosted git repository.

olabusayo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/daffodil.git


The following commit(s) were added to refs/heads/main by this push:
     new aee45959c Better Error Reporting for Library API
aee45959c is described below

commit aee45959c71168fb678b830fb6c286c27488aec4
Author: olabusayoT <[email protected]>
AuthorDate: Thu Jul 20 15:04:33 2023 -0400

    Better Error Reporting for Library API
    
    - replace abort in usageError/usage with UsageException
    - add checkNotError helper method
    - add/update tests
    
    Deprecation/Compatibility:
    Assert.usage() now throws a UsageException instead of an Abort. 
Compiler.forLanguage(), DataProcessor.parse(), DataProcessor.unparse() and 
ProcessorFactor.onPath(), when isError is true, throws a UsageError, which 
wraps an IllegalStateException, unlike before where it threw an Abort.
    
    DAFFODIL-2072
---
 .../apache/daffodil/core/compiler/Compiler.scala   |  2 +-
 .../core/runtime1/SchemaSetRuntime1Mixin.scala     |  3 +-
 .../io/TestInputSourceDataInputStream2.scala       |  4 +--
 .../org/apache/daffodil/example/TestJavaAPI.java   | 16 ++++++++++
 .../test/resources/test/japi/mySchema6.dfdl.xsd    | 34 ++++++++++++++++++++++
 .../org/apache/daffodil/lib/api/Diagnostic.scala   | 14 +++++++++
 .../apache/daffodil/lib/exceptions/Assert.scala    | 22 ++++++++++++--
 .../daffodil/lib/macros/TestAssertMacros.scala     | 13 +++++++--
 .../daffodil/lib/exceptions/AssertMacros.scala     | 15 ++++++++--
 .../runtime1/processors/DataProcessor.scala        |  5 ++--
 .../test/resources/test/sapi/mySchema6.dfdl.xsd    | 34 ++++++++++++++++++++++
 .../org/apache/daffodil/example/TestScalaAPI.scala | 26 +++++++++++++++--
 12 files changed, 170 insertions(+), 18 deletions(-)

diff --git 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/compiler/Compiler.scala 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/compiler/Compiler.scala
index bed74d926..d6a05556d 100644
--- 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/compiler/Compiler.scala
+++ 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/compiler/Compiler.scala
@@ -110,7 +110,7 @@ final class ProcessorFactory private (
   override def onPath(xpath: String): DFDL.DataProcessor = sset.onPath(xpath)
 
   override def forLanguage(language: String): DFDL.CodeGenerator = {
-    Assert.usage(!isError)
+    checkNotError()
 
     // Do a poor man's pluggable code generator implementation - we can replace
     // it after we observe how the validator SPI evolves and wait for our
diff --git 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/SchemaSetRuntime1Mixin.scala
 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/SchemaSetRuntime1Mixin.scala
index 534648bc9..2756d4f55 100644
--- 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/SchemaSetRuntime1Mixin.scala
+++ 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/SchemaSetRuntime1Mixin.scala
@@ -19,7 +19,6 @@ package org.apache.daffodil.core.runtime1
 
 import org.apache.daffodil.core.dsom.SchemaSet
 import org.apache.daffodil.core.grammar.VariableMapFactory
-import org.apache.daffodil.lib.exceptions.Assert
 import org.apache.daffodil.lib.util.Logger
 import org.apache.daffodil.runtime1.api.DFDL
 import org.apache.daffodil.runtime1.processors.DataProcessor
@@ -68,7 +67,7 @@ trait SchemaSetRuntime1Mixin {
   }.value
 
   def onPath(xpath: String): DFDL.DataProcessor = {
-    Assert.usage(!isError)
+    checkNotError()
     if (xpath != "/")
       root.notYetImplemented("""Path must be "/". Other path support is not 
yet implemented.""")
     val rootERD = root.elementRuntimeData
diff --git 
a/daffodil-io/src/test/scala/org/apache/daffodil/io/TestInputSourceDataInputStream2.scala
 
b/daffodil-io/src/test/scala/org/apache/daffodil/io/TestInputSourceDataInputStream2.scala
index f7b22cf59..73cab81b6 100644
--- 
a/daffodil-io/src/test/scala/org/apache/daffodil/io/TestInputSourceDataInputStream2.scala
+++ 
b/daffodil-io/src/test/scala/org/apache/daffodil/io/TestInputSourceDataInputStream2.scala
@@ -18,7 +18,7 @@
 package org.apache.daffodil.io
 
 import org.apache.daffodil.lib.Implicits._
-import org.apache.daffodil.lib.exceptions.Abort
+import org.apache.daffodil.lib.exceptions.UsageException
 import org.apache.daffodil.lib.util.MaybeULong
 
 import org.junit.Assert._
@@ -107,7 +107,7 @@ class TestInputSourceDataInputStream2 {
     assertEquals(81, dis.bitLimit1b.get)
     assertEquals(10, dis.bytePos0b)
     dis.reset(m1)
-    intercept[Abort] {
+    intercept[UsageException] {
       dis.reset(m2)
     }
   }
diff --git 
a/daffodil-japi/src/test/java/org/apache/daffodil/example/TestJavaAPI.java 
b/daffodil-japi/src/test/java/org/apache/daffodil/example/TestJavaAPI.java
index d975a59ed..9f4be4a7b 100644
--- a/daffodil-japi/src/test/java/org/apache/daffodil/example/TestJavaAPI.java
+++ b/daffodil-japi/src/test/java/org/apache/daffodil/example/TestJavaAPI.java
@@ -1216,6 +1216,22 @@ public class TestJavaAPI {
         assertTrue(DaffodilXMLEntityResolver.getLSResourceResolver() != null);
     }
 
+    @Test
+    public void testJavaAPI27() throws IOException {
+        org.apache.daffodil.japi.Compiler c = Daffodil.compiler();
+        java.io.File schemaFile = getResource("/test/japi/mySchema6.dfdl.xsd");
+        ProcessorFactory pf = c.compileFile(schemaFile);
+        assertTrue(pf.isError());
+        try {
+            pf.onPath("/");
+        } catch (Exception e) {
+            Throwable cause = e.getCause();
+            assertTrue(cause.toString().contains("Must call isError"));
+            assertTrue(cause.getCause().toString().contains("Schema Definition 
Error"));
+            assertTrue(cause.getCause().toString().contains("Cannot resolve 
the name 'tns:nonExistent'"));
+        }
+    }
+
     @Test
     public void testJavaAPINullXMLTextEscapeStyle() throws IOException, 
ClassNotFoundException {
         ByteArrayOutputStream xmlBos = new ByteArrayOutputStream();
diff --git a/daffodil-japi/src/test/resources/test/japi/mySchema6.dfdl.xsd 
b/daffodil-japi/src/test/resources/test/japi/mySchema6.dfdl.xsd
new file mode 100644
index 000000000..fc21dbb9e
--- /dev/null
+++ b/daffodil-japi/src/test/resources/test/japi/mySchema6.dfdl.xsd
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+
+<schema xmlns="http://www.w3.org/2001/XMLSchema";
+  targetNamespace="http://example.com"; 
xmlns:dfdl="http://www.ogf.org/dfdl/dfdl-1.0/";
+  xmlns:xsd="http://www.w3.org/2001/XMLSchema";
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xmlns:tns="http://example.com";>
+  
+  <annotation>
+    <appinfo source="http://www.ogf.org/dfdl/";>
+      <dfdl:format ref="tns:GeneralFormat" />
+    </appinfo>
+  </annotation>
+  
+  <include 
schemaLocation="org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd"/>
+  
+  <element name="e2" dfdl:lengthKind="delimited" type="tns:nonExistent"/>
+
+</schema>
diff --git 
a/daffodil-lib/src/main/scala/org/apache/daffodil/lib/api/Diagnostic.scala 
b/daffodil-lib/src/main/scala/org/apache/daffodil/lib/api/Diagnostic.scala
index 1cd2e2f69..0da0c86af 100644
--- a/daffodil-lib/src/main/scala/org/apache/daffodil/lib/api/Diagnostic.scala
+++ b/daffodil-lib/src/main/scala/org/apache/daffodil/lib/api/Diagnostic.scala
@@ -308,4 +308,18 @@ trait WithDiagnostics {
    * then one can proceed to run the compiled entity.
    */
   def isError: Boolean
+
+  /**
+   * Helper method to check that isError is false, if not it throws
+   * a usage error caused by illegal state caused by a compilation error
+   */
+  def checkNotError(): Unit = {
+    Assert.usage(
+      !isError,
+      new IllegalStateException(
+        "Must call isError() to ensure there are no errors",
+        getDiagnostics.find(_.isError).get,
+      ),
+    )
+  }
 }
diff --git 
a/daffodil-lib/src/main/scala/org/apache/daffodil/lib/exceptions/Assert.scala 
b/daffodil-lib/src/main/scala/org/apache/daffodil/lib/exceptions/Assert.scala
index afeea355d..5417d5f1d 100644
--- 
a/daffodil-lib/src/main/scala/org/apache/daffodil/lib/exceptions/Assert.scala
+++ 
b/daffodil-lib/src/main/scala/org/apache/daffodil/lib/exceptions/Assert.scala
@@ -51,7 +51,10 @@ abstract class UnsuppressableException(m: String, th: 
Throwable) extends Excepti
   def this(th: Throwable) = this(null, th)
 }
 
-class UsageException(m: String) extends UnsuppressableException(m)
+class UsageException(m: String, th: Throwable) extends 
UnsuppressableException(m, th) {
+  def this(th: Throwable) = this(null, th)
+  def this(m: String) = this(m, null)
+}
 class NotYetImplementedException(m: String)
   extends UnsuppressableException("Not yet implemented: " + m)
 class Abort(m: String, th: Throwable) extends UnsuppressableException(m, th) {
@@ -83,13 +86,18 @@ object Assert extends Assert {
    */
   def usageErrorUnless(testAbortsIfFalse: Boolean, message: String): Unit =
     macro AssertMacros.usageMacro2
+  def usageErrorUnless(testAbortsIfFalse: Boolean, cause: Throwable): Unit =
+    macro AssertMacros.usageMacro2Cause
   def usageErrorUnless(testAbortsIfFalse: Boolean): Unit = macro 
AssertMacros.usageMacro1
 
   /**
    * Brief form
    */
   def usage(testAbortsIfFalse: Boolean, message: String): Unit = macro 
AssertMacros.usageMacro2
-  def usage(testAbortsIfFalse: Boolean): Unit = macro AssertMacros.usageMacro1
+  def usage(testAbortsIfFalse: Boolean, cause: Throwable): Unit =
+    macro AssertMacros.usageMacro2Cause
+  def usage(testAbortsIfFalse: Boolean): Unit =
+    macro AssertMacros.usageMacro1
 
   /**
    * test for something that the program is supposed to be ensuring.
@@ -128,7 +136,15 @@ object Assert extends Assert {
   //
 
   def usageError(message: String = "Usage error."): Nothing = {
-    abort(message)
+    toss(new UsageException(message))
+  }
+
+  def usageError(cause: Throwable): Nothing = {
+    toss(new UsageException(cause))
+  }
+
+  def usageError2(message: String = "Usage error.", testAsString: String): 
Nothing = {
+    usageError(message + " (" + testAsString + ")")
   }
 
   def nyi(info: String): Nothing = {
diff --git 
a/daffodil-lib/src/test/scala/org/apache/daffodil/lib/macros/TestAssertMacros.scala
 
b/daffodil-lib/src/test/scala/org/apache/daffodil/lib/macros/TestAssertMacros.scala
index 58dc558c8..bfdd3b053 100644
--- 
a/daffodil-lib/src/test/scala/org/apache/daffodil/lib/macros/TestAssertMacros.scala
+++ 
b/daffodil-lib/src/test/scala/org/apache/daffodil/lib/macros/TestAssertMacros.scala
@@ -41,7 +41,7 @@ class TestAssertMacros {
   }
 
   @Test def testUsage1Arg(): Unit = {
-    val e = intercept[Abort] {
+    val e = intercept[UsageException] {
       Assert.usage(if (1 == x) true else false)
     }
     val msg = e.getMessage()
@@ -54,7 +54,7 @@ class TestAssertMacros {
   }
 
   @Test def testUsage2Arg(): Unit = {
-    val e = intercept[Abort] {
+    val e = intercept[UsageException] {
       Assert.usage(if (1 == x) true else false, "foobar")
     }
     val msg = e.getMessage()
@@ -110,4 +110,13 @@ class TestAssertMacros {
     assertTrue(msg.contains("false"))
     assertTrue(msg.contains("foobar"))
   }
+
+  @Test def testUsage2ArgCause(): Unit = {
+    val e = intercept[UsageException] {
+      Assert.usage(if (1 == x) true else false, new Exception("test"))
+    }
+    val cause = e.getCause.toString
+    assertTrue(cause.contains("Exception"))
+    assertTrue(cause.contains("test"))
+  }
 }
diff --git 
a/daffodil-macro-lib/src/main/scala/org/apache/daffodil/lib/exceptions/AssertMacros.scala
 
b/daffodil-macro-lib/src/main/scala/org/apache/daffodil/lib/exceptions/AssertMacros.scala
index 1131872f0..0feeeee3b 100644
--- 
a/daffodil-macro-lib/src/main/scala/org/apache/daffodil/lib/exceptions/AssertMacros.scala
+++ 
b/daffodil-macro-lib/src/main/scala/org/apache/daffodil/lib/exceptions/AssertMacros.scala
@@ -28,7 +28,17 @@ object AssertMacros {
 
     q"""
     if (!($testAbortsIfFalse)) {
-         Assert.abort2("Usage error: " + $message, $testAsString)
+         Assert.usageError2("Usage error: " + $message, $testAsString)
+    }
+    """
+  }
+
+  def usageMacro2Cause(c: Context)(testAbortsIfFalse: c.Tree, cause: c.Tree): 
c.Tree = {
+    import c.universe._
+
+    q"""
+    if (!($testAbortsIfFalse)) {
+         Assert.usageError($cause)
     }
     """
   }
@@ -40,7 +50,7 @@ object AssertMacros {
 
     q"""
     if (!($testAbortsIfFalse)) {
-         Assert.abort("Usage error: " + $testAsString)
+         Assert.usageError("Usage error: " + $testAsString)
     }
     """
   }
@@ -100,5 +110,4 @@ object AssertMacros {
     }
     """
   }
-
 }
diff --git 
a/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/DataProcessor.scala
 
b/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/DataProcessor.scala
index 015e614df..efedbdb8b 100644
--- 
a/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/DataProcessor.scala
+++ 
b/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/DataProcessor.scala
@@ -340,8 +340,7 @@ class DataProcessor(
    * runtime. Instead we deal with success and failure statuses.
    */
   def parse(input: InputSourceDataInputStream, output: InfosetOutputter): 
DFDL.ParseResult = {
-    Assert.usage(!this.isError)
-
+    checkNotError()
     // If full validation is enabled, tee all the infoset events to a second
     // infoset outputter that writes the infoset to a byte array, and then
     // we'll validate that byte array upon a successful parse.
@@ -501,7 +500,7 @@ class DataProcessor(
   }
 
   def unparse(inputter: InfosetInputter, output: DFDL.Output): 
DFDL.UnparseResult = {
-    Assert.usage(!this.isError)
+    checkNotError()
     val outStream = java.nio.channels.Channels.newOutputStream(output)
     unparse(inputter, outStream)
   }
diff --git a/daffodil-sapi/src/test/resources/test/sapi/mySchema6.dfdl.xsd 
b/daffodil-sapi/src/test/resources/test/sapi/mySchema6.dfdl.xsd
new file mode 100644
index 000000000..fc21dbb9e
--- /dev/null
+++ b/daffodil-sapi/src/test/resources/test/sapi/mySchema6.dfdl.xsd
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+
+<schema xmlns="http://www.w3.org/2001/XMLSchema";
+  targetNamespace="http://example.com"; 
xmlns:dfdl="http://www.ogf.org/dfdl/dfdl-1.0/";
+  xmlns:xsd="http://www.w3.org/2001/XMLSchema";
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xmlns:tns="http://example.com";>
+  
+  <annotation>
+    <appinfo source="http://www.ogf.org/dfdl/";>
+      <dfdl:format ref="tns:GeneralFormat" />
+    </appinfo>
+  </annotation>
+  
+  <include 
schemaLocation="org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd"/>
+  
+  <element name="e2" dfdl:lengthKind="delimited" type="tns:nonExistent"/>
+
+</schema>
diff --git 
a/daffodil-sapi/src/test/scala/org/apache/daffodil/example/TestScalaAPI.scala 
b/daffodil-sapi/src/test/scala/org/apache/daffodil/example/TestScalaAPI.scala
index 761e650b3..5e9a2092f 100644
--- 
a/daffodil-sapi/src/test/scala/org/apache/daffodil/example/TestScalaAPI.scala
+++ 
b/daffodil-sapi/src/test/scala/org/apache/daffodil/example/TestScalaAPI.scala
@@ -28,7 +28,7 @@ import java.nio.charset.StandardCharsets
 import java.nio.file.Paths
 import javax.xml.XMLConstants
 
-import org.apache.daffodil.lib.exceptions.Abort
+import org.apache.daffodil.lib.exceptions.UsageException
 import org.apache.daffodil.sapi.Daffodil
 import org.apache.daffodil.sapi.DaffodilParseXMLReader
 import org.apache.daffodil.sapi.DaffodilUnhandledSAXException
@@ -887,7 +887,7 @@ class TestScalaAPI {
     try {
       res = dp.parse(input, outputter)
     } catch {
-      case e: Abort => {
+      case e: UsageException => {
         assertTrue(e.getMessage().contains("Usage error"))
         assertTrue(e.getMessage().contains("invalid input source"))
       }
@@ -1207,6 +1207,28 @@ class TestScalaAPI {
     assertEquals(expectedData, bos.toString())
   }
 
+  @Test
+  def testScalaAPI26(): Unit = {
+    val c = Daffodil.compiler()
+    val schemaFile = getResource("/test/sapi/mySchema6.dfdl.xsd")
+    val pf = c.compileFile(schemaFile)
+    assertTrue(pf.isError())
+    try {
+      pf.onPath("/")
+    } catch {
+      case e: UsageException => {
+        val cause = e.getCause
+        assertTrue(cause.toString.contains("Must call isError"))
+        assertTrue(
+          cause.getCause.toString.contains("Schema Definition Error"),
+        )
+        assertTrue(
+          cause.getCause.toString.contains("Cannot resolve the name 
'tns:nonExistent'"),
+        )
+      }
+    }
+  }
+
   @Test
   def testScalaAPICDATA1(): Unit = {
     val expected = "NO_WHITESPACE_OR_SPECIAL_CHARS"

Reply via email to