You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@daffodil.apache.org by sl...@apache.org on 2021/01/14 12:41:06 UTC

[incubator-daffodil] branch master updated: Add support for displaying variables in the debugger

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

slawrence pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-daffodil.git


The following commit(s) were added to refs/heads/master by this push:
     new 9da6568  Add support for displaying variables in the debugger
9da6568 is described below

commit 9da656836727c2e2eb049a5ea4411082d51cce25
Author: Steve Lawrence <sl...@apache.org>
AuthorDate: Wed Jan 13 09:08:00 2021 -0500

    Add support for displaying variables in the debugger
    
    Adds a new info subcommand called "variables" to show the current
    in-scope state and value of variables. For example:
    
        (debug) info variables
          variables:
            ex:var1:  (undefined)
            {http://www.ogf.org/dfdl/dfdl-1.0/}binaryFloatRep: ieee (default)
            {http://www.ogf.org/dfdl/dfdl-1.0/}byteOrder: bigEndian (default)
            {http://www.ogf.org/dfdl/dfdl-1.0/}encoding: UTF-8 (default)
            {http://www.ogf.org/dfdl/dfdl-1.0/}outputNewLine: %LF; (default)
    
    Also allows specifcying one or more variable names to only output those
    if you don't want to see all variables, for example:
    
        (debug) info varibles var1 byteOrder
          variables:
            ex:var1:  (undefined)
            {http://www.ogf.org/dfdl/dfdl-1.0/}byteOrder: bigEndian (default)
    
    This also modifies the info command so that you can provide parameters
    to subcommands, which is needed to allow variable names after the "info
    variables".
    
    Fixes the "info data" command which at one point
    had optional parameters to control how data should be output (either
    text or binary). This was broken at some point, but is now available.
    
    Allows the "clear" command to be used in the "display" command, e.g.
    
      (debug) display clear
    
    This can be useful to clear the screen every time the debugger pauses,
    sometimes making it easier to see what changed.
    
    Also adds variable support to the "info diff" command variable changes
    during trace mode are shown.
    
    DAFFODIL-2453
---
 .../apache/daffodil/debugger/TestCLIDebugger.scala |  91 ++++++++++-
 .../daffodil/debugger/InteractiveDebugger.scala    | 169 ++++++++++++++++++---
 .../daffodil/processors/ProcessorStateBases.scala  |   8 +-
 .../apache/daffodil/processors/VariableMap1.scala  |   4 +
 4 files changed, 250 insertions(+), 22 deletions(-)

diff --git a/daffodil-cli/src/it/scala/org/apache/daffodil/debugger/TestCLIDebugger.scala b/daffodil-cli/src/it/scala/org/apache/daffodil/debugger/TestCLIDebugger.scala
index b1c8da0..d6df06f 100644
--- a/daffodil-cli/src/it/scala/org/apache/daffodil/debugger/TestCLIDebugger.scala
+++ b/daffodil-cli/src/it/scala/org/apache/daffodil/debugger/TestCLIDebugger.scala
@@ -750,7 +750,7 @@ class TestCLIdebugger {
       shell.sendLine("display info dne1")
       shell.expect(contains("error: undefined info command: dne1"))
       shell.sendLine("display info bitLimit dne2")
-      shell.expect(contains("error: undefined info command: dne2"))
+      shell.expect(contains("error: bitLimit command requires zero arguments"))
       shell.sendLine("display break")
       shell.expect(contains("error: undefined command: break"))
       shell.sendLine("quit")
@@ -1156,5 +1156,94 @@ class TestCLIdebugger {
     }
   }
 
+  @Test def test_CLI_Debugger_info_variables(): Unit = {
+    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
+    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
+
+    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+
+    try {
+      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
+      shell.sendLine(cmd)
+      shell.expect(contains("(debug)"))
+      shell.sendLine("info variables byteOrder")
+      shell.expect(contains("byteOrder: bigEndian (default)"))
+      shell.sendLine("quit")
+    } finally {
+      shell.close()
+    }
+  }
+
+  @Test def test_CLI_Debugger_info_data_text(): Unit = {
+    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
+    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
+
+    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+
+    try {
+      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
+      shell.sendLine(cmd)
+      shell.expect(contains("(debug)"))
+      shell.sendLine("display info data text")
+      shell.expect(contains("(debug)"))
+      shell.sendLine("step")
+      shell.expect(contains("0~,~1~,~2~"))
+      shell.sendLine("quit")
+    } finally {
+      shell.close()
+    }
+  }
+
+  @Test def test_CLI_Debugger_info_data_binary(): Unit = {
+    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
+    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
+
+    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+
+    try {
+      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
+      shell.sendLine(cmd)
+      shell.expect(contains("(debug)"))
+      shell.sendLine("display info data binary")
+      shell.expect(contains("(debug)"))
+      shell.sendLine("step")
+      shell.expect(contains("302c 312c 32"))
+      shell.sendLine("quit")
+    } finally {
+      shell.close()
+    }
+  }
+
+  @Test def test_CLI_Debugger_info_diff(): Unit = {
+    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section07/variables/variables_01.dfdl.xsd")
+    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
+    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
+
+    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+
+    try {
+      val cmd = String.format("%s -d parse -s %s -r c %s", Util.binPath, testSchemaFile, testInputFile)
+      shell.sendLine(cmd)
+      shell.expect(contains("(debug)"))
+      shell.sendLine("display info diff")
+      shell.expect(contains("(debug)"))
+      shell.sendLine("step")
+      shell.expect(contains("No differences"))
+      shell.sendLine("step")
+      shell.expect(contains("No differences"))
+      shell.sendLine("step")
+      shell.expect(contains("variable: tns:v_with_default: 42 (default) -> 42 (read)"))
+      shell.sendLine("step")
+      shell.expect(contains("variable: tns:v_no_default: (undefined) -> 42 (set)"))
+      shell.sendLine("step")
+      shell.expect(contains("No differences"))
+      shell.sendLine("quit")
+    } finally {
+      shell.close()
+    }
+  }
 
 }
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/debugger/InteractiveDebugger.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/debugger/InteractiveDebugger.scala
index 4588eca..35f8e31 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/debugger/InteractiveDebugger.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/debugger/InteractiveDebugger.scala
@@ -940,12 +940,12 @@ class InteractiveDebugger(runner: InteractiveDebuggerRunner, eCompilers: Express
       val desc = "show value of expression each time program stops"
       val longDesc = """|Usage: di[splay] <debugger_command>
                         |
-                        |Execute a debugger command (limited to eval or info) every time a debugger
-                        |console is displayed to the user.
+                        |Execute a debugger command (limited to eval, info, and clear) every time a
+                        |there is a pause in the debugger.
                         |
                         |Example: display info infoset""".stripMargin
       override lazy val short = "di"
-      override val subcommands = Seq(Eval, Info)
+      override val subcommands = Seq(Eval, Info, Clear)
 
       def act(args: Seq[String], prestate: StateForDebugger, state: ParseOrUnparseState, processor: Processor): DebugState.Type = {
         DebuggerConfig.displays += new Display(DebuggerConfig.displayIndex, args)
@@ -1189,7 +1189,9 @@ class InteractiveDebugger(runner: InteractiveDebuggerRunner, eCompilers: Express
       val longDesc = """|Usage: i[nfo] <item>...
                         |
                         |Print internal information to the console. <item> can be specified
-                        |multiple times to display multiple pieces of information.
+                        |multiple times to display multiple pieces of information. <items>
+                        |that are not recognized as info commands are assumed to be arguments
+                        |to the previous <item>
                         |
                         |Example: info data infoset""".stripMargin
       override val subcommands =
@@ -1210,25 +1212,80 @@ class InteractiveDebugger(runner: InteractiveDebuggerRunner, eCompilers: Express
           InfoPath,
           InfoParser,
           InfoPointsOfUncertainty,
-          InfoUnparser)
+          InfoUnparser,
+          InfoVariables)
+
+      /**
+      * The info command allows printing multiple different kinds of
+      * information at once. For example "info foo bar" is equivalent to
+      * running the two commands "info foo" and "info bar". However, some info
+      * commands might take one or more parameters. In this case, we allow
+      * include the parameters after the subcommand, for example "info foo
+      * fooParam bar". To determine where one subcommand end and another
+      * subcommand begins, we assume unknown strings (e.g. fooParam) are parameters
+      * to the previous known subcommand. This function constructs a sequence
+      * that represents the different info commands and their parameters. For
+      * example, the following command:
+      *
+      *   info foo bar barParam1 barParam2 baz
+      *
+      * Is parsed to the following:
+      *
+      *   Seq(
+      *     Seq("foo"),
+      *     Seq("bar", "barParam1", "barParam2"),
+      *     Seq("baz"),
+      *   )
+      *
+      * This sequence of sequences can then be used to determine how to execute
+      * individual info commands and provide the appropriate arguments.
+      */
+      private def buildInfoCommands(args: Seq[String]): Seq[Seq[String]] = {
+        val backwardsInfoCommands = args.foldLeft(Seq.empty[Seq[String]]) { case (infoCmds, arg) =>
+          val cmd = subcommands.find(_ == arg)
+          if (cmd.isDefined || infoCmds.isEmpty) {
+            // Found a new info subcommand, or we don't have an info commands
+            // yet. Create a new Seq to hold the subcommand + args and
+            // prepend this Weq to our list of info subcommands. Note that if
+            // this isn't actually an info subcommand, we'll detect that later
+            // when we validate this list.
+            val newCommand = Seq(arg)
+            newCommand +: infoCmds
+          } else {
+            // Not a recognized info subcommand. Assume it is an arg to the
+            // most recent command we've seen and prepend it to that list.
+            val head :: tail = infoCmds
+            (arg +: head) +: tail
+          }
+        }
+
+        // We've built up a list of info commands with args, but the info
+        // commands and the args are all reversed because we prepended
+        // everything. So reverse that all to get the order correct
+        backwardsInfoCommands.map { _.reverse }.reverse
+      }
 
       override def validate(args: Seq[String]): Unit = {
         if (args.size == 0) {
           throw new DebugException("one or more commands are required")
         }
-        args.foreach(arg => {
-          subcommands.find(_ == arg) match {
-            case Some(c) => c.validate(Seq())
-            case None => throw new DebugException("undefined info command: %s".format(arg))
+        val infocmds = buildInfoCommands(args)
+        infocmds.foreach { cmds =>
+          val cmd :: args = cmds
+          subcommands.find(_ == cmd) match {
+            case Some(c) => c.validate(args)
+            case None => throw new DebugException("undefined info command: %s".format(cmd))
           }
-        })
+        }
       }
 
       def act(args: Seq[String], prestate: StateForDebugger, state: ParseOrUnparseState, processor: Processor): DebugState.Type = {
-        args.foreach(arg => {
-          val action = subcommands.find(_ == arg).get
-          action.act(Seq(), prestate, state, processor)
-        })
+        val infocmds = buildInfoCommands(args)
+        infocmds.foreach { cmds =>
+          val cmd :: args = cmds
+          val action = subcommands.find(_ == cmd).get
+          action.act(args, prestate, state, processor)
+        }
         DebugState.Pause
       }
 
@@ -1315,7 +1372,7 @@ class InteractiveDebugger(runner: InteractiveDebuggerRunner, eCompilers: Express
         }
       }
 
-      object InfoData extends DebugCommand with DebugCommandValidateZeroArgs {
+      object InfoData extends DebugCommand with DebugCommandValidateOptionalArg {
         val name = "data"
         val desc = "display the input/output data"
         val longDesc = desc
@@ -1326,14 +1383,22 @@ class InteractiveDebugger(runner: InteractiveDebuggerRunner, eCompilers: Express
           debugPrintln(lines, "  ")
         }
 
+        override def validate(args: Seq[String]): Unit = {
+          super.validate(args)
+          args.headOption.map(_.toLowerCase) match {
+            case None =>
+            case Some("text") =>
+            case Some("binary") =>
+            case _ => throw new DebugException("unknown data representation: %s. Must be one of 'text' or 'binary'".format(args(0)))
+          }
+        }
+
         def act(args: Seq[String], prestate: StateForDebugger, state: ParseOrUnparseState, processor: Processor): DebugState.Type = {
           debugPrintln("%s:".format(name))
           val rep = if (args.size > 0) {
             args(0).toLowerCase match {
-              case "t" => Some(Representation.Text)
-              case "b" => Some(Representation.Binary)
-              case _ =>
-                throw new DebugException("uknown representation: %s. Must be one of 't' for text or 'b' for binary".format(args(0)))
+              case "text" => Some(Representation.Text)
+              case "binary" => Some(Representation.Binary)
             }
           } else {
             if (state.hasInfoset) {
@@ -1404,6 +1469,24 @@ class InteractiveDebugger(runner: InteractiveDebuggerRunner, eCompilers: Express
               if (prestate.arrayPos != state.arrayPos) { debugPrintln("occursIndex: %d -> %d".format(prestate.arrayPos, state.arrayPos), "  "); diff = true }
               if (prestate.groupPos != state.groupPos) { debugPrintln("groupIndex: %d -> %d".format(prestate.groupPos, state.groupPos), "  "); diff = true }
               if (prestate.childPos != state.childPos) { debugPrintln("childIndex: %d -> %d".format(prestate.childPos, state.childPos), "  "); diff = true }
+              prestate.variableMap.qnames.foreach { qname =>
+                val pre_instance = prestate.variableMap.find(qname).get
+                val pre_value = pre_instance.value
+                val pre_state = pre_instance.state
+
+                val cur_instance = state.variableMap.find(qname).get
+                val cur_value = cur_instance.value
+                val cur_state = cur_instance.state
+
+                if (pre_value != cur_value || pre_state != cur_state) {
+                  debugPrintln("variable: %s: %s -> %s".format(
+                    qname,
+                    InfoVariables.variableInstanceToDebugString(pre_instance),
+                    InfoVariables.variableInstanceToDebugString(cur_instance),
+                  ), "  ")
+                  diff = true
+                }
+              }
             }
             case _ => // ok
           }
@@ -1608,6 +1691,54 @@ class InteractiveDebugger(runner: InteractiveDebuggerRunner, eCompilers: Express
       object InfoUnparser extends {
         override val name = "unparser"
       } with InfoProcessorBase
+
+      object InfoVariables extends DebugCommand {
+        val name = "variables"
+        override lazy val short = "v"
+        val desc = "display in-scope state of variables"
+        val longDesc = """|Usage: v[ariables] [<name>...]
+                          |
+                          |Display the in-scope state of variables matching <name>'s. If no
+                          |names are given, displays the in-scope state of all variabes.""".stripMargin
+
+        override def validate(args: Seq[String]): Unit = {
+          // no validation
+        }
+
+        def variableInstanceToDebugString(vinst: VariableInstance): String = {
+          val state = vinst.state match {
+            case VariableDefined => "default"
+            case VariableRead => "read"
+            case VariableSet => "set"
+            case VariableUndefined => "undefined"
+            case VariableInProcess => "in process"
+          }
+
+          if (vinst.value.isEmpty) "(%s)".format(state)
+          else "%s (%s)".format(vinst.value.value, state)
+        }
+
+        def act(args: Seq[String], prestate: StateForDebugger, state: ParseOrUnparseState, processor: Processor): DebugState.Type = {
+          val vmap = state.variableMap
+          val allQNames = vmap.qnames
+          val qnamesToPrint =
+            if (args.size == 0) allQNames
+            else {
+              allQNames.filter { qname =>
+                args.contains(qname.local) || args.contains(qname.toPrettyString)
+              }
+            }
+
+          debugPrintln("%s:".format(name))
+          qnamesToPrint.sortBy { _.toPrettyString }.foreach { qname =>
+            val instance = vmap.find(qname).get
+            val debugVal = variableInstanceToDebugString(instance)
+            debugPrintln("  %s: %s".format(qname.toPrettyString, debugVal))
+          }
+
+          DebugState.Pause
+        }
+      }
     }
 
     object Quit extends DebugCommand with DebugCommandValidateZeroArgs {
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/ProcessorStateBases.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/ProcessorStateBases.scala
index 956588c..2b0b99f 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/ProcessorStateBases.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/ProcessorStateBases.scala
@@ -77,6 +77,7 @@ trait StateForDebugger {
   def currentLocation: DataLocation
   def arrayPos: Long
   def bitLimit0b: MaybeULong
+  def variableMap: VariableMap
 }
 
 case class TupleForDebugger(
@@ -85,7 +86,8 @@ case class TupleForDebugger(
   val groupPos: Long,
   val currentLocation: DataLocation,
   val arrayPos: Long,
-  val bitLimit0b: MaybeULong)
+  val bitLimit0b: MaybeULong,
+  val variableMap: VariableMap)
   extends StateForDebugger
 
 trait SetProcessorMixin {
@@ -430,7 +432,9 @@ abstract class ParseOrUnparseState protected (
       groupPos,
       currentLocation,
       arrayPos,
-      bitLimit0b)
+      bitLimit0b,
+      variableMap.copy(), // deep copy since variableMap is mutable
+    )
   }
 
   final override def schemaFileLocation = getContext().schemaFileLocation
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/VariableMap1.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/VariableMap1.scala
index 35c2f73..0674cf5 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/VariableMap1.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/VariableMap1.scala
@@ -320,6 +320,10 @@ class VariableMap private(vTable: Map[GlobalQName, ArrayBuffer[VariableInstance]
     variab
   }
 
+  def qnames(): Seq[GlobalQName] = {
+    vTable.toSeq.map(_._1)
+  }
+
   def getVariableRuntimeData(qName: GlobalQName): Option[VariableRuntimeData] = {
     val optVariable = find(qName)
     if (optVariable.isDefined) Some(optVariable.get.rd) else None