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"