Skip to content

Commit fab8bf5

Browse files
committed
New Layer API & Improved Implementation
This commit fixes the remaining issues open about Daffodil's Layers extension/plugins. Partial progress was in commit ff9ba4. This finishes that work. - DAFFODIL-2845 - Layer Improvements (umbrella ticket) - DAFFODIL-2844 - LayerNotEnoughDataException ... - DAFFODIL-2825 - Define Supported Layering API - DAFFODIL-2841 - Layers do not support created SDE or PE/UE Errors - DAFFODIL-2843 - How to create PE/UE from a layer The layer API is all defined in Java clases and interfaces now. One of the built-in layers (Gzip) is written in Java now. There is only one layer property now: dfdlx:layer which takes a QName of the layer class. Layer classes are derived from org.apache.daffodil.runtime1.layers.api.Layer A layer a name and target namespace, and hence a QName is required to identify it. This namespace is OWNED by the layer. All DFDL variables defined in that namespace are either used to pass parameters to the layer code, or receive results (such as a checksum) back from the layer code. This is enforced. A layer that has no DFDL variables does not have to define a DFDL schema that defines the layer's target namespace, but any layer that uses DFDL variables *must* define a schema with the layer's namespace as its target namespace, with the variables declared in it (using dfdl:defineVariable). There is also an abstract base class for defining checksum layers called, org.apache.daffodil.runtime1.layers.api.ChecksumLayer All the layer properties about layer length kinds etc. are gone. dfdlx:layerLengthKind='boundaryMark' and dfdlx:boundaryMark properties are replaced by a layer named 'boundaryMark'. dfdlx:layerLengthKind='explicit' and dfdlx:layerLength properties are replaced by a layer named 'fixedLength'. dfdlx:layerLengthKind='implicit' is replaced by just not using one of the two above layers. dfdlx:layerTransform is replaced by just dfdlx:layer and is now a QName. Layers no longer have both transformers and length-limiters mixed together. There are two ways to do explicit layer limiting, that differ in an important way. If you use 'implicit' layer length (no restriction layer), and constrain the layer length by a surrounding element of specified length, then the layer length is controlled when parsing, but NOT controlled when unparsing. If you use the layer "fixedLength", that dictates the parse and unparse length to be the value of the layerLength variable. Similarly if you use a checksum layer built with the ChecksumLayer base class, the length is controlled for both parsing and unparsing as it works like the fixedLength layer. Layer Variables: Each variable is either a parameter passed to a special setter named setLayerVariableParameters, or is a return variable which is populated from a special getter. The special setter args and getters taken together must use up all the DFDL variables defined in the layer's namespace. (See the javadoc for the Layer base class.) A special getter example might be: int getLayerVariableResult_checksum() { .... } This corresponds to, and returns the value that populates, the DFDL variable named 'checksum' in the layer's namespace. That DFDL variable must have a DFDL type corresponding to the int type (xs:unsignedShort or xs:int). Layers could have multiple result variables, but we have no actual examples of more than one value being returned. (Returning multiple variables is tested. We just have no real use cases for it at this time.) Layer Exception Handling: Added setProcessingErrorException(...) method to Layer This allows the layer to specify that if the layer throws specific exceptions or runtime exceptions that they are converted into processing errors. This eliminates most need for layers to contain try-catches. Additional Fixes in this Commit: Fixed gzip layer which was not constructing on Java 17 or 21 due to static initializer differences of some sort. Removed restriction that a layer can only have a single term inside it. Added close() to InputSourceDataInputStream, as we weren't closing these. They now implement java.io.Closeable so can use try-with-resources for auto closing in Java. There are a large number of simplifications in the "layering" of the various layer-related implementation classes. LayerRegistry is gone, merged into LayerFactory, which is gone, merged into LayerDriver. In the examples/tests for layering: Changed check digit bad digit style to construct an always-invalid element instead of an assertion failure. About Testing: The testing is quite extensive now. There are tests for every way that a user can goof up the definition of a layer class, and there are tests for processingError, runtimeSchemaDefinitionError, and throwing an Exception from every place a user-defined Layer could cause these. Parse errors cause backtracking in all sensible cases. Nothing aborts. Protecting every place that layer code is called from any sort of error/throw requires quite a number of try/catch blocks. This may have performance implications. Add some fuzz testing around gzip layer GZip layer converts IOExceptions to PE explicitly. Fuzz tests show that gzip does a good job converting various bad data issues into IOException, so really that's the only one the GzipLayer has to deal with. Changed boundaryMarkLayer and gzip layer and AIS (test) layer to use setProcessingErrorException to handle IOExceptions. (and the test SimpleBombOutLayer.scala used to drive negative tests.) Added bug-related comments to other files about bugs discovered while fuzz-testing of other non-daffodil layer schemas. DEPRECATION/COMPATABILITY This new Layer API is 100% incompatible with schemas or layer code from Daffodil 3.6.0 and all prior versions of Daffodil. The layer feature was just an experimental feature in Daffodil, so we reserved the right to change it. However, it is our hope that the new Layer API introduced by this commit will prove to be stable and supportable indefinitely. DAFFODIL-2845, DAFFODIL-2844, DAFFODIL-2825, DAFFODIL-2841, DAFFODIL-2843
1 parent a853a2f commit fab8bf5

File tree

163 files changed

+10345
-4536
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

163 files changed

+10345
-4536
lines changed

DEVELOP.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ daffodil/
234234
├── daffodil-macro-lib/ - Defines Daffodil's Scala 2.x macros
235235
├── daffodil-propgen/ - Contains generators to generate more source code
236236
├── daffodil-runtime1/ - Contains Daffodil's Scala parser
237-
├── daffodil-runtime1-layers/ - Contains Daffodil's Scala layer transformers
237+
├── daffodil-runtime1-layers/ - Contains Daffodil's Scala layers
238238
├── daffodil-runtime1-unparser/ - Contains Daffodil's Scala unparser
239239
├── daffodil-sapi/ - Contains Daffodil's Scala API
240240
├── daffodil-schematron/

daffodil-cli/src/main/scala/org/apache/daffodil/cli/Main.scala

+126-124
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import org.apache.daffodil.core.dsom.ExpressionCompilers
4545
import org.apache.daffodil.io.DataDumper
4646
import org.apache.daffodil.io.FormatInfo
4747
import org.apache.daffodil.io.InputSourceDataInputStream
48+
import org.apache.daffodil.lib.Implicits.using
4849
import org.apache.daffodil.lib.api.DaffodilConfig
4950
import org.apache.daffodil.lib.api.DaffodilConfigException
5051
import org.apache.daffodil.lib.api.DaffodilTunables
@@ -70,7 +71,7 @@ import org.apache.daffodil.runtime1.debugger.DebuggerExitException
7071
import org.apache.daffodil.runtime1.debugger.InteractiveDebugger
7172
import org.apache.daffodil.runtime1.debugger.TraceDebuggerRunner
7273
import org.apache.daffodil.runtime1.externalvars.ExternalVariablesLoader
73-
import org.apache.daffodil.runtime1.layers.LayerExecutionException
74+
import org.apache.daffodil.runtime1.layers.LayerFatalException
7475
import org.apache.daffodil.runtime1.processors.DataLoc
7576
import org.apache.daffodil.runtime1.processors.DataProcessor
7677
import org.apache.daffodil.runtime1.processors.ExternalVariableException
@@ -1170,141 +1171,141 @@ class Main(
11701171
new FileInputStream(f)
11711172
}
11721173
}
1173-
val inStream = InputSourceDataInputStream(input)
1174+
using(InputSourceDataInputStream(input)) { inStream =>
1175+
val output = parseOpts.output.toOption match {
1176+
case Some("-") | None => STDOUT
1177+
case Some(file) => new FileOutputStream(file)
1178+
}
11741179

1175-
val output = parseOpts.output.toOption match {
1176-
case Some("-") | None => STDOUT
1177-
case Some(file) => new FileOutputStream(file)
1178-
}
1180+
val infosetHandler = InfosetType.getInfosetHandler(
1181+
parseOpts.infosetType(),
1182+
processor,
1183+
parseOpts.schema.map(_.uri).toOption,
1184+
forPerformance = false,
1185+
)
11791186

1180-
val infosetHandler = InfosetType.getInfosetHandler(
1181-
parseOpts.infosetType(),
1182-
processor,
1183-
parseOpts.schema.map(_.uri).toOption,
1184-
forPerformance = false,
1185-
)
1187+
var lastParseBitPosition = 0L
1188+
var keepParsing = true
1189+
var exitCode = ExitCode.Success
11861190

1187-
var lastParseBitPosition = 0L
1188-
var keepParsing = true
1189-
var exitCode = ExitCode.Success
1191+
while (keepParsing) {
1192+
val infosetResult =
1193+
Timer.getResult("parsing", infosetHandler.parse(inStream, output))
1194+
val parseResult = infosetResult.parseResult
11901195

1191-
while (keepParsing) {
1192-
val infosetResult =
1193-
Timer.getResult("parsing", infosetHandler.parse(inStream, output))
1194-
val parseResult = infosetResult.parseResult
1196+
val finfo = parseResult.resultState.asInstanceOf[FormatInfo]
1197+
val loc = parseResult.resultState.currentLocation.asInstanceOf[DataLoc]
1198+
displayDiagnostics(parseResult)
11951199

1196-
val finfo = parseResult.resultState.asInstanceOf[FormatInfo]
1197-
val loc = parseResult.resultState.currentLocation.asInstanceOf[DataLoc]
1198-
displayDiagnostics(parseResult)
1199-
1200-
if (parseResult.isProcessingError || parseResult.isValidationError) {
1201-
keepParsing = false
1202-
exitCode = ExitCode.ParseError
1203-
} else {
1204-
// Success. Some InfosetHandlers do not write the result to the output
1205-
// stream when parsing (e.g. they just create Scala objects). Since we are
1206-
// parsing, ask the InfosetHandler to serialize the result if it has an
1207-
// implementation so that the user can see an XML represenation regardless
1208-
// of the infoset type.
1209-
infosetResult.write(output)
1210-
output.flush()
1211-
1212-
if (!inStream.hasData()) {
1213-
// not even 1 more bit is available.
1214-
// do not try to keep parsing, nothing left to parse
1200+
if (parseResult.isProcessingError || parseResult.isValidationError) {
12151201
keepParsing = false
1202+
exitCode = ExitCode.ParseError
12161203
} else {
1217-
// There is more data available.
1218-
if (parseOpts.stream.toOption.get) {
1219-
// Streaming mode
1220-
if (lastParseBitPosition == loc.bitPos0b) {
1221-
// this parse consumed no data, that means this would get
1222-
// stuck in an infinite loop if we kept trying to stream,
1223-
// so we need to quit
1204+
// Success. Some InfosetHandlers do not write the result to the output
1205+
// stream when parsing (e.g. they just create Scala objects). Since we are
1206+
// parsing, ask the InfosetHandler to serialize the result if it has an
1207+
// implementation so that the user can see an XML represenation regardless
1208+
// of the infoset type.
1209+
infosetResult.write(output)
1210+
output.flush()
1211+
1212+
if (!inStream.hasData()) {
1213+
// not even 1 more bit is available.
1214+
// do not try to keep parsing, nothing left to parse
1215+
keepParsing = false
1216+
} else {
1217+
// There is more data available.
1218+
if (parseOpts.stream.toOption.get) {
1219+
// Streaming mode
1220+
if (lastParseBitPosition == loc.bitPos0b) {
1221+
// this parse consumed no data, that means this would get
1222+
// stuck in an infinite loop if we kept trying to stream,
1223+
// so we need to quit
1224+
val remainingBits =
1225+
if (loc.bitLimit0b.isDefined) {
1226+
(loc.bitLimit0b.get - loc.bitPos0b).toString
1227+
} else {
1228+
"at least " + (inStream.inputSource.knownBytesAvailable * 8)
1229+
}
1230+
Logger.log.error(
1231+
s"Left over data after consuming 0 bits while streaming. Stopped after consuming ${loc.bitPos0b} bit(s) with ${remainingBits} bit(s) remaining.",
1232+
)
1233+
keepParsing = false
1234+
exitCode = ExitCode.LeftOverData
1235+
} else {
1236+
// last parse did consume data, and we know there is more
1237+
// data to come, so try to parse again.
1238+
lastParseBitPosition = loc.bitPos0b
1239+
keepParsing = true
1240+
output.write(0) // NUL-byte separates streams
1241+
}
1242+
} else {
1243+
// not streaming mode, and there is more data available,
1244+
// so show left over data warning
1245+
val Dump = new DataDumper
1246+
val bitsAlreadyConsumed = (loc.bitPos0b % 8).toInt
1247+
val firstByteString = if (bitsAlreadyConsumed != 0) {
1248+
val bitsToDisplay = 8 - bitsAlreadyConsumed
1249+
val pbp = inStream.inputSource.position + 1
1250+
val firstByteBitArray = inStream.getByteArray(bitsToDisplay, finfo)
1251+
val fbs = firstByteBitArray(0).toBinaryString
1252+
.takeRight(8)
1253+
.reverse
1254+
.padTo(8, '0')
1255+
.reverse
1256+
val bits = if (finfo.bitOrder == BitOrder.MostSignificantBitFirst) {
1257+
"x" * bitsAlreadyConsumed + fbs.dropRight(bitsAlreadyConsumed)
1258+
} else {
1259+
fbs.takeRight(bitsToDisplay) + "x" * bitsAlreadyConsumed
1260+
}
1261+
val dumpString =
1262+
f"\nLeft over data starts with partial byte. Left over data (Binary) at byte $pbp is: (0b$bits)"
1263+
dumpString
1264+
} else ""
1265+
val curBytePosition1b = inStream.inputSource.position + 1
1266+
val bytesAvailable = inStream.inputSource.knownBytesAvailable
1267+
val bytesLimit = math.min(8, bytesAvailable).toInt
1268+
val destArray = new Array[Byte](bytesLimit)
1269+
val destArrayFilled = inStream.inputSource.get(destArray, 0, bytesLimit)
1270+
val dumpString =
1271+
if (destArrayFilled)
1272+
Dump
1273+
.dump(
1274+
Dump.TextOnly(Some("utf-8")),
1275+
0,
1276+
destArray.length * 8,
1277+
ByteBuffer.wrap(destArray),
1278+
includeHeadingLine = false,
1279+
)
1280+
.mkString("\n")
1281+
else ""
1282+
val dataText =
1283+
if (destArrayFilled)
1284+
s"\nLeft over data (UTF-8) starting at byte ${curBytePosition1b} is: (${dumpString}...)"
1285+
else ""
1286+
val dataHex =
1287+
if (destArrayFilled)
1288+
s"\nLeft over data (Hex) starting at byte ${curBytePosition1b} is: (0x${destArray.map { a =>
1289+
f"$a%02x"
1290+
}.mkString}...)"
1291+
else ""
12241292
val remainingBits =
12251293
if (loc.bitLimit0b.isDefined) {
12261294
(loc.bitLimit0b.get - loc.bitPos0b).toString
12271295
} else {
1228-
"at least " + (inStream.inputSource.knownBytesAvailable * 8)
1296+
"at least " + (bytesAvailable * 8)
12291297
}
1230-
Logger.log.error(
1231-
s"Left over data after consuming 0 bits while streaming. Stopped after consuming ${loc.bitPos0b} bit(s) with ${remainingBits} bit(s) remaining.",
1232-
)
1298+
val leftOverDataMessage =
1299+
s"Left over data. Consumed ${loc.bitPos0b} bit(s) with ${remainingBits} bit(s) remaining." + firstByteString + dataHex + dataText
1300+
Logger.log.error(leftOverDataMessage)
12331301
keepParsing = false
12341302
exitCode = ExitCode.LeftOverData
1235-
} else {
1236-
// last parse did consume data, and we know there is more
1237-
// data to come, so try to parse again.
1238-
lastParseBitPosition = loc.bitPos0b
1239-
keepParsing = true
1240-
output.write(0) // NUL-byte separates streams
12411303
}
1242-
} else {
1243-
// not streaming mode, and there is more data available,
1244-
// so show left over data warning
1245-
val Dump = new DataDumper
1246-
val bitsAlreadyConsumed = (loc.bitPos0b % 8).toInt
1247-
val firstByteString = if (bitsAlreadyConsumed != 0) {
1248-
val bitsToDisplay = 8 - bitsAlreadyConsumed
1249-
val pbp = inStream.inputSource.position + 1
1250-
val firstByteBitArray = inStream.getByteArray(bitsToDisplay, finfo)
1251-
val fbs = firstByteBitArray(0).toBinaryString
1252-
.takeRight(8)
1253-
.reverse
1254-
.padTo(8, '0')
1255-
.reverse
1256-
val bits = if (finfo.bitOrder == BitOrder.MostSignificantBitFirst) {
1257-
"x" * bitsAlreadyConsumed + fbs.dropRight(bitsAlreadyConsumed)
1258-
} else {
1259-
fbs.takeRight(bitsToDisplay) + "x" * bitsAlreadyConsumed
1260-
}
1261-
val dumpString =
1262-
f"\nLeft over data starts with partial byte. Left over data (Binary) at byte $pbp is: (0b$bits)"
1263-
dumpString
1264-
} else ""
1265-
val curBytePosition1b = inStream.inputSource.position + 1
1266-
val bytesAvailable = inStream.inputSource.knownBytesAvailable
1267-
val bytesLimit = math.min(8, bytesAvailable).toInt
1268-
val destArray = new Array[Byte](bytesLimit)
1269-
val destArrayFilled = inStream.inputSource.get(destArray, 0, bytesLimit)
1270-
val dumpString =
1271-
if (destArrayFilled)
1272-
Dump
1273-
.dump(
1274-
Dump.TextOnly(Some("utf-8")),
1275-
0,
1276-
destArray.length * 8,
1277-
ByteBuffer.wrap(destArray),
1278-
includeHeadingLine = false,
1279-
)
1280-
.mkString("\n")
1281-
else ""
1282-
val dataText =
1283-
if (destArrayFilled)
1284-
s"\nLeft over data (UTF-8) starting at byte ${curBytePosition1b} is: (${dumpString}...)"
1285-
else ""
1286-
val dataHex =
1287-
if (destArrayFilled)
1288-
s"\nLeft over data (Hex) starting at byte ${curBytePosition1b} is: (0x${destArray.map { a =>
1289-
f"$a%02x"
1290-
}.mkString}...)"
1291-
else ""
1292-
val remainingBits =
1293-
if (loc.bitLimit0b.isDefined) {
1294-
(loc.bitLimit0b.get - loc.bitPos0b).toString
1295-
} else {
1296-
"at least " + (bytesAvailable * 8)
1297-
}
1298-
val leftOverDataMessage =
1299-
s"Left over data. Consumed ${loc.bitPos0b} bit(s) with ${remainingBits} bit(s) remaining." + firstByteString + dataHex + dataText
1300-
Logger.log.error(leftOverDataMessage)
1301-
keepParsing = false
1302-
exitCode = ExitCode.LeftOverData
13031304
}
13041305
}
13051306
}
1307+
exitCode
13061308
}
1307-
exitCode
13081309
}
13091310
}
13101311
rc
@@ -1411,11 +1412,12 @@ class Main(
14111412
})
14121413
case Right(bytes) =>
14131414
Timer.getTimeResult({
1414-
val input = InputSourceDataInputStream(bytes)
1415-
val infosetResult =
1416-
infosetHandler.parse(input, nullOutputStreamForParse)
1417-
val parseResult = infosetResult.parseResult
1418-
parseResult
1415+
using(InputSourceDataInputStream(bytes)) { input =>
1416+
val infosetResult =
1417+
infosetHandler.parse(input, nullOutputStreamForParse)
1418+
val parseResult = infosetResult.parseResult
1419+
parseResult
1420+
}
14191421
})
14201422
}
14211423

@@ -1993,8 +1995,8 @@ class Main(
19931995
Logger.log.error(e.message)
19941996
ExitCode.ConfigError
19951997
}
1996-
case e: LayerExecutionException => {
1997-
Logger.log.error(e.message, e)
1998+
case e: LayerFatalException => {
1999+
Logger.log.error(e.getMessage, e)
19982000
ExitCode.LayerExecutionError
19992001
}
20002002
case e: Exception => {

daffodil-cli/src/test/resources/META-INF/services/org.apache.daffodil.runtime1.layers.LayerCompiler daffodil-cli/src/test/resources/META-INF/services/org.apache.daffodil.runtime1.layers.api.Layer

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
org.apache.daffodil.layers.BuggyLayerCompiler
16+
org.apache.daffodil.layers.BuggyLayer

daffodil-cli/src/test/resources/org/apache/daffodil/layers/buggy.dfdl.xsd

+2-5
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,8 @@
2121
xmlns="http://www.w3.org/2001/XMLSchema"
2222
xmlns:dfdl="http://www.ogf.org/dfdl/dfdl-1.0/"
2323
xmlns:dfdlx="http://www.ogf.org/dfdl/dfdl-1.0/extensions"
24-
xmlns:fn="http://www.w3.org/2005/xpath-functions"
25-
xmlns:daf="urn:ogf:dfdl:2013:imp:daffodil.apache.org:2018:ext"
2624
xmlns:buggy="urn:org.apache.daffodil.layers.buggy"
2725
xmlns:ex="http://example.com"
28-
xmlns:tns="http://example.com"
2926
targetNamespace="http://example.com">
3027

3128
<include schemaLocation="/org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd" />
@@ -36,13 +33,13 @@
3633

3734
<annotation>
3835
<appinfo source="http://www.ogf.org/dfdl/">
39-
<dfdl:format ref="tns:GeneralFormat" encoding="ascii" />
36+
<dfdl:format ref="ex:GeneralFormat" encoding="ascii" />
4037
</appinfo>
4138
</annotation>
4239

4340
<element name="r">
4441
<complexType>
45-
<sequence dfdl:ref="buggy:buggyFormat" dfdlx:layerLength="10">
42+
<sequence dfdlx:layer="buggy:buggy">
4643
<element name="value" type="xs:string" dfdl:lengthKind="explicit" dfdl:length="1"/>
4744
</sequence>
4845
</complexType>

daffodil-cli/src/test/resources/org/apache/daffodil/layers/xsd/buggyLayer.dfdl.xsd

+1-12
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,6 @@
2828

2929
<include schemaLocation="/org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd" />
3030

31-
<annotation>
32-
<appinfo source="http://www.ogf.org/dfdl/">
33-
34-
<dfdl:format ref="buggy:GeneralFormat" />
35-
36-
<dfdl:defineFormat name="buggyFormat">
37-
<dfdl:format dfdlx:layerTransform="buggy" dfdlx:layerLengthKind="explicit" dfdlx:layerLengthUnits="bytes"
38-
dfdlx:layerEncoding="ascii" />
39-
</dfdl:defineFormat>
40-
41-
</appinfo>
42-
</annotation>
31+
<!-- This layer has no parameters nor return variables -->
4332

4433
</schema>

daffodil-cli/src/test/resources/test/cli/fixedLength.dfdl.xsd

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@
2121
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
2222
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2323
xmlns:dfdl="http://www.ogf.org/dfdl/dfdl-1.0/"
24-
xmlns:tns="http://www.example.org/example1/"
24+
xmlns:ex="http://www.example.org/example1/"
2525
targetNamespace="http://www.example.org/example1/">
2626

2727
<xs:include schemaLocation="/org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd"/>
2828

2929
<xs:annotation>
3030
<xs:appinfo source="http://www.ogf.org/dfdl/">
31-
<dfdl:format ref="tns:GeneralFormat"/>
31+
<dfdl:format ref="ex:GeneralFormat"/>
3232
</xs:appinfo>
3333
</xs:annotation>
3434

daffodil-cli/src/test/scala/org/apache/daffodil/cli/cliTest/TestCLIParsing.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -939,7 +939,7 @@ class TestCLIParsing {
939939
)
940940
runCLI(args"parse -s $schema") { cli =>
941941
cli.sendLine("0", inputDone = true)
942-
cli.expectErr("Unexpected exception in layer transformer 'buggy'")
942+
cli.expectErr("bad input stream")
943943
}(ExitCode.LayerExecutionError)
944944
}
945945

0 commit comments

Comments
 (0)