[llvm-branch-commits] [llvm] [Dexter] Add !step node for testing stepping behaviour (PR #203844)

Stephen Tozer via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Mon Jun 15 05:17:12 PDT 2026


https://github.com/SLTozer updated https://github.com/llvm/llvm-project/pull/203844

>From 38ec5922b52f994777959609af8d05cc602b5ee8 Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Fri, 12 Jun 2026 16:24:40 +0100
Subject: [PATCH] [Dexter] Add !step node for testing stepping behaviour

This patch adds a node for generating metrics based on lines stepped on. The
new node has 3 versions: !step exactly, !step order, and !step never, which
check an expected list of line numbers against the actual line numbers seen
while the expect is active.
---
 .../dexter/dex/evaluation/ExpectWriter.py     |   5 +-
 .../dexter/dex/evaluation/Metrics.py          |  81 +++++++++++-
 .../dexter/dex/evaluation/RunMatch.py         | 122 ++++++++++++++----
 .../dexter/dex/test_script/Nodes.py           |  33 ++++-
 .../dexter/dex/test_script/Script.py          |  12 ++
 .../evaluation/eval_steps_penalties.cpp       |  69 ++++++++++
 .../scripts/evaluation/eval_steps_perfect.cpp |  46 +++++++
 .../parser/step-node-expected-values.test     |  19 +++
 8 files changed, 354 insertions(+), 33 deletions(-)
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_steps_penalties.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_steps_perfect.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/step-node-expected-values.test

diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectWriter.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectWriter.py
index e49ad5c2f35a7..c6e32e2823435 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectWriter.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectWriter.py
@@ -220,9 +220,10 @@ def collect_expects_to_write(expect: Expect, expected_value: Any, scope: Scope):
                 assert expected_value is None
                 self.scope_expect_rewrites[expect] = []
                 return
+            if expected_value is not None:
+                return
             assert isinstance(expect, Value), "Non-Value expects currently unsupported"
-            if expected_value is None:
-                self.unknown_expect_rewrites[expect] = []
+            self.unknown_expect_rewrites[expect] = []
 
         script = dext_ir.script
         assert (
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/Metrics.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/Metrics.py
index 1cac20303e09f..27859beab81c9 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/Metrics.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/Metrics.py
@@ -7,14 +7,14 @@
 """Produce metric results from the results of a comparison of a DexterScript and debugger output.
 """
 
-from typing import Any, Dict, List, Union
+from typing import Any, Dict, List, Optional, Union
 
 from dex.evaluation.ExpectMatch import (
     DebuggerExpectMatch,
     MatchResult,
     get_expected_value_set,
 )
-from dex.test_script.Nodes import Expect, Value
+from dex.test_script.Nodes import Expect, Step, Value
 
 
 class Metric:
@@ -156,3 +156,80 @@ def get_variable_metrics(
         "missing_values": ScalarMetric(num_missing_values, improves_asc=False),
     }
     return metrics
+
+
+def lcs_len(a: List[int], b: List[int]) -> int:
+    """Returns the length of the longest common subsequence between a and b."""
+    lcs_table: List[List[int]] = [
+        [0 for _ in range(len(b) + 1)] for _ in range(len(a) + 1)
+    ]
+    for a_idx in range(len(a)):
+        for b_idx in range(len(b)):
+            if a[a_idx] == b[b_idx]:
+                lcs_table[a_idx + 1][b_idx + 1] = 1 + lcs_table[a_idx][b_idx]
+            else:
+                lcs_table[a_idx + 1][b_idx + 1] = max(
+                    lcs_table[a_idx + 1][b_idx], lcs_table[a_idx][b_idx + 1]
+                )
+    return lcs_table[-1][-1]
+
+
+def get_step_metrics(
+    expect: Step, expected_lines: List[int], step_lines: List[int]
+) -> Dict[str, Metric]:
+    """Given an Expect node with its expected values and a list of all matches for that Expect in a debugger session,
+    returns the computed metrics for that Expect node."""
+
+    expected_line_set = set(expected_lines)
+    actual_line_set = set(step_lines)
+
+    total_line_steps = len(step_lines)
+    if expect.kind == "exactly" or expect.kind == "order":
+        num_matching_steps = lcs_len(expected_lines, step_lines)
+        # Inefficient, but not to the point that we care!
+        num_matching_steps_ignoring_order = lcs_len(
+            sorted(expected_lines), sorted(step_lines)
+        )
+
+        max_possible_correct_line_steps = len(expected_lines)
+        correct_line_steps = num_matching_steps
+        misordered_line_steps = num_matching_steps_ignoring_order - num_matching_steps
+        missing_lines = sum(1 for e in expected_line_set if e not in actual_line_set)
+        if expect.kind == "exactly":
+            incorrect_line_steps = total_line_steps - correct_line_steps
+            unexpected_lines = sum(
+                1 for a in actual_line_set if a not in expected_line_set
+            )
+        else:
+            # For `!step order` there are no "incorrect" or "unexpected" lines, since we explicitly ignore seen lines
+            # outside of the expected lines.
+            incorrect_line_steps = 0
+            unexpected_lines = 0
+    else:
+        assert expect.kind == "never"
+        max_possible_correct_line_steps = total_line_steps
+        correct_line_steps = sum(
+            1 for line in step_lines if line not in expected_line_set
+        )
+        incorrect_line_steps = total_line_steps - correct_line_steps
+        unexpected_lines = sum(1 for a in actual_line_set if a in expected_line_set)
+        # For `!step never` there are no "missing" or "misordered" lines, since we only declare lines we *don't* want to
+        # see.
+        missing_lines = 0
+        misordered_line_steps = 0
+
+    metrics: Dict[str, Metric] = {
+        "total_line_steps": ScalarMetric(total_line_steps),
+        "correct_line_steps": ScalarMetric(correct_line_steps),
+        "correct_line_score": FractionMetric(
+            correct_line_steps, max_possible_correct_line_steps
+        ),
+        "misordered_line_steps": ScalarMetric(
+            misordered_line_steps, improves_asc=False
+        ),
+        "missing_lines": ScalarMetric(missing_lines, improves_asc=False),
+        "incorrect_line_steps": ScalarMetric(incorrect_line_steps, improves_asc=False),
+        "unexpected_lines": ScalarMetric(unexpected_lines, improves_asc=False),
+    }
+
+    return metrics
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py
index 75f6e6fa81a0b..e4cf117110e37 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py
@@ -11,7 +11,7 @@
 # aggregated, and each individual metric can be expressed in a scalar form that is considered "better" as it either
 # ascends or descends.
 from collections import defaultdict
-from typing import Any, Dict, List, Tuple
+from typing import Any, Dict, List, Optional, Tuple
 
 from dex.dextIR import DextIR, StepIR
 from dex.evaluation.ExpectMatch import (
@@ -22,12 +22,13 @@
 )
 from dex.evaluation.Metrics import (
     Metric,
+    get_step_metrics,
     get_variable_metrics,
     serialize_metric_to_json,
 )
 from dex.evaluation.StateMatch import StateMatchContext, get_active_where_matches
 from dex.test_script import DexterScript, Scope
-from dex.test_script.Nodes import Expect, Value
+from dex.test_script.Nodes import Expect, Line, Step, Value
 
 class DebuggerStepMatch:
     """Class used to record the match between a DexterScript and a StepIR, including the state match, determining which
@@ -50,18 +51,25 @@ def __init__(
             for where_match in self.state_match.values()
             for expect in where_match.active_expects
         }
-        self.expect_matches: Dict[Expect, DebuggerExpectMatch] = {}
+        self.var_expect_matches: Dict[Expect, DebuggerExpectMatch] = {}
+        self.step_expect_matches: Dict[Step, int] = {}
 
         def add_expected_values(expect: Expect, expected_value: Any, scope: Scope):
-            assert isinstance(expect, Value), "Non-Value expects currently unsupported"
-            if expect in expects_to_match:
-                expect_frame_idx = expects_to_match[expect]
-                self.expect_matches[expect] = get_expect_match(
-                    expect,
-                    expected_value,
-                    step.frames[expect_frame_idx].watches[expect.get_watched_expr()],
-                    self.match_context,
-                )
+            if expect not in expects_to_match:
+                return
+            expect_frame_idx = expects_to_match[expect]
+            if isinstance(expect, Step):
+                self.step_expect_matches[expect] = step.frames[
+                    expect_frame_idx
+                ].loc.lineno
+                return
+            assert isinstance(expect, Value), f"Unexpected expect node kind {expect}"
+            self.var_expect_matches[expect] = get_expect_match(
+                expect,
+                expected_value,
+                step.frames[expect_frame_idx].watches[expect.get_watched_expr()],
+                self.match_context,
+            )
 
         script.visit_script(visit_expect=add_expected_values)
 
@@ -79,20 +87,24 @@ def __init__(self, dex_context, dext_ir: DextIR):
         self.dext_ir = dext_ir
         self.metrics: Dict[str, Metric] = {}
         self.step_matches: List[DebuggerStepMatch] = []
-        self.per_expect_results: Dict[
+        self.per_var_expect_results: Dict[
             Expect, list[Tuple[int, DebuggerExpectMatch]]
         ] = {}
+        self.per_step_expect_results: Dict[Step, list[Tuple[int, int]]] = {}
 
         script = self.dext_ir.script
         assert script is not None, "Trying to evaluate DextIR without attached script?"
 
         # Gather the expected values for each Expect.
-        expected_values = {}
+        self.expected_values = {}
 
         def add_expected_values(expect: Expect, expected_value: Any, scope: Scope):
-            assert isinstance(expect, Value), "Non-Value expects currently unsupported"
-            expected_values[expect] = expected_value
-            self.per_expect_results[expect] = []
+            self.expected_values[expect] = expected_value
+            if isinstance(expect, Value):
+                self.per_var_expect_results[expect] = []
+                return
+            assert isinstance(expect, Step), f"Unexpected expect node kind {expect}"
+            self.per_step_expect_results[expect] = []
 
         script.visit_script(visit_expect=add_expected_values)
 
@@ -105,16 +117,39 @@ def add_expected_values(expect: Expect, expected_value: Any, scope: Scope):
 
         # Then, for each expect, produce the list of results for just that variable.
         for step_match in self.step_matches:
-            for expect, expect_match in step_match.expect_matches.items():
-                self.per_expect_results[expect].append(
+            for step_expect, line in step_match.step_expect_matches.items():
+                self.per_step_expect_results[step_expect].append(
+                    (step_match.step.step_index, line)
+                )
+            for expect, expect_match in step_match.var_expect_matches.items():
+                self.per_var_expect_results[expect].append(
                     (step_match.step.step_index, expect_match)
                 )
 
+        # For !steps, once we know the file that they are in, we apply any labels.
+        for step_expect, step_results in self.per_step_expect_results.items():
+            if not step_results:
+                # We may not be able to resolve any !labels in the expected value list if the expect was never active;
+                # as a workaround, just set any integers here - the result will be the same.
+                self.expected_values[step_expect] = [
+                    0 for l in self.expected_values[step_expect]
+                ]
+                continue
+            active_path = self.dext_ir.steps[0].current_location.path
+            assert all(
+                self.dext_ir.steps[step_index].current_location.path == active_path
+                for step_index, line in step_results[1:]
+            ), "!step node unexpectedly active over multiple files"
+            path_labels = script.get_labels(active_path)
+            self.expected_values[step_expect] = [
+                Line(l).to_line(path_labels) for l in self.expected_values[step_expect]
+            ]
+
         # Finally, compare the match results against the expected values to produce the metrics.
-        for expect, expect_results in self.per_expect_results.items():
+        for expect, expect_results in self.per_var_expect_results.items():
             expect_matches = [match for step, match in expect_results]
             expect_metrics = get_variable_metrics(
-                expect, expected_values[expect], expect_matches
+                expect, self.expected_values[expect], expect_matches
             )
             for metric_name, metric in expect_metrics.items():
                 if metric_name not in self.metrics:
@@ -123,6 +158,18 @@ def add_expected_values(expect: Expect, expected_value: Any, scope: Scope):
                     self.metrics[metric_name] = self.metrics[metric_name].aggregate(
                         metric
                     )
+        for expect, lines in self.per_step_expect_results.items():
+            actual_lines = [lines for step, lines in lines]
+            step_metrics = get_step_metrics(
+                expect, self.expected_values[expect], actual_lines
+            )
+            for metric_name, metric in step_metrics.items():
+                if metric_name not in self.metrics:
+                    self.metrics[metric_name] = metric
+                else:
+                    self.metrics[metric_name] = self.metrics[metric_name].aggregate(
+                        metric
+                    )
 
     def dump_step_results(self) -> str:
         result = ""
@@ -145,23 +192,42 @@ def dump_step_results(self) -> str:
             result += f"  Active !where nodes:\n"
             for frame_idx, wheres in frame_active_wheres_list:
                 result += f"    Frame {frame_idx}: [{', '.join(wheres)}]\n"
-            if not step_match.expect_matches:
+            if not step_match.var_expect_matches and not step_match.step_expect_matches:
                 continue
             result += f"  Active !expect nodes:\n"
             matching_expects = [
-                (expect, match)
-                for expect, match in step_match.expect_matches.items()
+                (expect, match.short_str())
+                for expect, match in step_match.var_expect_matches.items()
                 if match.match_result == MatchResult.TRUE
             ]
             non_matching_expects = [
-                (expect, match)
-                for expect, match in step_match.expect_matches.items()
+                (expect, match.short_str())
+                for expect, match in step_match.var_expect_matches.items()
                 if match.match_result != MatchResult.TRUE
             ]
+
+            def step_expect_matches(expect: Step, step_line: int) -> bool:
+                expected_lines = self.expected_values[expect]
+                assert isinstance(expected_lines, list) and all(
+                    isinstance(l, int) for l in expected_lines
+                )
+                step_line_in_expected_list = step_line in expected_lines
+                if expect.kind == "never":
+                    print(f"never line hit: {step_line_in_expected_list}")
+                    return not step_line_in_expected_list
+                return step_line_in_expected_list
+
+            for step_expect, step_line in step_match.step_expect_matches.items():
+                list_to_append = (
+                    matching_expects
+                    if step_expect_matches(step_expect, step_line)
+                    else non_matching_expects
+                )
+                list_to_append.append((step_expect, str(step_line)))
             if matching_expects:
-                result += f"    Matching nodes:     [{', '.join(f'{expect}={match.short_str()}' for expect, match in matching_expects)}]\n"
+                result += f"    Matching nodes:     [{', '.join(f'{expect}={match}' for expect, match in matching_expects)}]\n"
             if non_matching_expects:
-                result += f"    Non-matching nodes: [{', '.join(f'{expect}={match.short_str()}' for expect, match in non_matching_expects)}]\n"
+                result += f"    Non-matching nodes: [{', '.join(f'{expect}={match}' for expect, match in non_matching_expects)}]\n"
         return result
 
     def get_metric_output(self):
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Nodes.py b/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Nodes.py
index b883eda60dad3..e88b2053739c2 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Nodes.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Nodes.py
@@ -18,7 +18,7 @@
 
 
 def setup_yaml_parser(loader):
-    reg_classes = [Where, Value, DexRange, Label, Then, Address, ValueAll]
+    reg_classes = [Where, Value, DexRange, Label, Then, Address, ValueAll, Step]
     for c in reg_classes:
         c.register_yaml(loader)
 
@@ -223,6 +223,37 @@ def register_yaml(loader):
         yaml.add_representer(ValueAll, ValueAll.representer)
 
 
+class Step(Expect):
+    """Sets an expectation for stepping behaviour, with the expected value being a list of integer lines:
+    - !step exactly: while this !expect is active, we expect see exactly the expected lines in-order as many times as
+      they appear in the expected lines list.
+    - !step order: while this !expect is active, we expect to see each of the expected lines in-order at least as many
+      times as they appear in the expected list, ignoring excess lines and lines not in the expected lines list.
+    - !step never: while this !expect is active, we expect to not see any of the lines in the expected lines list.
+    """
+
+    def __init__(self, kind: str):
+        self.kind = kind
+        if kind not in ["exactly", "order", "never"]:
+            raise DexterNodeError(self, f'invalid !step kind "{self.kind}"')
+
+    def __repr__(self):
+        return f"Step({self.kind})"
+
+    @staticmethod
+    def constructor(loader: yaml.Loader, node):
+        return Step(loader.construct_scalar(node))
+
+    @staticmethod
+    def representer(dumper, data):
+        return dumper.represent_scalar("!step", data.kind)
+
+    @staticmethod
+    def register_yaml(loader):
+        yaml.add_constructor("!step", Step.constructor, loader)
+        yaml.add_representer(Step, Step.representer)
+
+
 ##############
 ## Execution Nodes: Can appear as leaf nodes directly under a state node to perform debugger actions when they become
 ## active, to advance the debugger state.
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Script.py b/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Script.py
index f1d430ea8cf43..5546d46527cb9 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Script.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Script.py
@@ -19,9 +19,11 @@
 from dex.test_script.Nodes import (
     Expect,
     FileLabels,
+    Label,
     Where,
     Then,
     ValueAll,
+    Step,
     setup_yaml_parser,
 )
 
@@ -186,6 +188,16 @@ def validate_expect(expect: Expect, expected_value, scope: Scope):
                 raise DexterScriptError(
                     f"!expect/all node {expect} should not have an expected value."
                 )
+            if isinstance(expect, Step):
+                if expected_value is None:
+                    raise DexterScriptError(f"rewriting !step nodes not yet supported.")
+                if not (
+                    isinstance(expected_value, list)
+                    and all(isinstance(l, (int, Label)) for l in expected_value)
+                ):
+                    raise DexterScriptError(
+                        f"Expected value for !step node {expect} must be list of integers"
+                    )
 
         def validate_where(where: Where, scope: Scope):
             if where.is_and and not scope.where:
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_steps_penalties.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_steps_penalties.cpp
new file mode 100644
index 0000000000000..233aa4bfc872b
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_steps_penalties.cpp
@@ -0,0 +1,69 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --use-script --binary %t -- %s \
+// RUN:   | FileCheck %s
+
+// Test evaluation of !step nodes in Dexter.
+
+// CHECK: total_line_steps: 15
+// CHECK: correct_line_steps: 11
+// CHECK: correct_line_score: 73.3% (11/15)
+// CHECK: misordered_line_steps: 2
+// CHECK: missing_lines: 1
+// CHECK: incorrect_line_steps: 2
+// CHECK: unexpected_lines: 2
+
+// We want some janky formatting for the sake of this test.
+// clang-format off
+int doublify(int N) { return N * 2; }
+
+int stepBackwards(int N) {
+  return doublify(            // !dex_label rbegin
+      doublify(
+        doublify(N))); // !dex_label rend
+}
+
+void reportError() {}
+
+void pleasePassTrue(bool ShouldDefinitelyBeTrue = true) {
+  if (!ShouldDefinitelyBeTrue) // !dex_label error_check
+    reportError();
+}
+
+int factorial(int N) {
+  int Result = 1; // !dex_label fac_start
+  for (int I = N; I-- > 0;) {
+    Result *= I;
+  }
+  if (Result > 0)
+    return Result;
+  return 0; // !dex_label fac_end
+}
+
+int main() {
+  stepBackwards(10);
+  pleasePassTrue(false);
+  factorial(3);
+  return 0;
+}
+// clang-format on
+
+/*
+---
+!where {lines: !range [!label rbegin, !label rend]}:
+  # Actual stepping order is reversed.
+  # 3 steps, 1/3 correct, 2 misordered.
+  !step order: [!label rbegin, !label rbegin + 1, !label rbegin + 2]
+!where {lines: !range [!label error_check, !label error_check + 1]}:
+  # "Never" line is stepped on.
+  # 2 steps, 1/2 correct, 1 incorrect, 1 unexpected
+  !step never: [!label error_check + 1]
+!where {lines: !range [!label fac_start, !label fac_end]}:
+  # Loop iterates 4 times instead of 3, and exits on line 39 instead of 38.
+  # 10 steps, 9/10 correct, 1 incorrect, 1 unexpected, 1 missing
+  !step exactly: [!label fac_start,
+    !label fac_start + 1, !label fac_start + 2,
+    !label fac_start + 1, !label fac_start + 2,
+    !label fac_start + 1, !label fac_start + 2,
+    !label fac_start + 1, !label fac_start + 4, !label fac_start + 5]
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_steps_perfect.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_steps_perfect.cpp
new file mode 100644
index 0000000000000..2848ae1635104
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_steps_perfect.cpp
@@ -0,0 +1,46 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --use-script --binary %t -- %s \
+// RUN:   | FileCheck %s
+
+// Test evaluation of !step nodes in Dexter.
+
+// CHECK: total_line_steps: 150
+// CHECK: correct_line_steps: 105
+// CHECK: correct_line_score: 100.0% (105/105)
+// CHECK: misordered_line_steps: 0
+// CHECK: missing_lines: 0
+// CHECK: incorrect_line_steps: 0
+// CHECK: unexpected_lines: 0
+
+void fizz() {}
+void buzz() {}
+void fizzbuzz() {}
+
+void doFizzbuzz(int N) {
+  for (int I = 1; I <= N; ++I) {
+    if (I % 3 == 0) {
+      if (I % 5 == 0)
+        fizzbuzz();
+      else
+        fizz();
+    } else if (I % 5 == 0) {
+      buzz();
+    }
+  }
+}
+
+int main() {
+  doFizzbuzz(10);
+  return 0;
+}
+
+/*
+---
+!where {function: doFizzbuzz}:
+  !step exactly: [20, 21, 26, 29, 20, 21, 26, 29, 20, 21, 22, 25, 26, 29, 20,
+    21, 26, 29, 20, 21, 26, 27, 29, 20, 21, 22, 25, 26, 29, 20, 21, 26, 29, 20,
+    21, 26, 29, 20, 21, 22, 25, 26, 29, 20, 21, 26, 27, 29, 20, 30]
+  !step order: [25, 27, 25, 25, 27]
+  !step never: [23]
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/step-node-expected-values.test b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/step-node-expected-values.test
new file mode 100644
index 0000000000000..b98d4f2767dcc
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/step-node-expected-values.test
@@ -0,0 +1,19 @@
+RUN: not %dexter_regression_test_run --binary %s --use-script --skip-run -- %s 2>&1 | FileCheck %s
+
+Tests that !step expected values must not be anything other than a list of integers.
+
+CHECK: No valid Dexter script found in file
+
+CHECK: Script starting line [[# @LINE + 2]]:
+CHECK: Expected value for !step node Step(order) must be list of integers
+---
+!where {function: foo}:
+    !step order: 12
+...
+
+CHECK: Script starting line [[# @LINE + 2]]:
+CHECK: Expected value for !step node Step(order) must be list of integers
+---
+!where {function: foo}:
+    !step order: ["0", "1"]
+...



More information about the llvm-branch-commits mailing list