diff --git a/libcpychecker/absinterp.py b/libcpychecker/absinterp.py index 26252dca..959f385e 100644 --- a/libcpychecker/absinterp.py +++ b/libcpychecker/absinterp.py @@ -1111,6 +1111,15 @@ def describe_stmt(stmt): else: return str(stmt.loc) +def get_locations(fun): + # given a gcc.Function, get all Location instances within it + result = [] + for bb in fun.cfg.basic_blocks: + if bb.gimple: + for idx, stmt in enumerate(bb.gimple): + result.append(Location(bb, idx)) + return result + class Location(object): """A location within a CFG: a gcc.BasicBlock together with an index into the gimple list. (We don't support SSA passes)""" @@ -1122,6 +1131,9 @@ def __init__(self, bb, idx): self.bb = bb self.idx = idx + def __hash__(self): + return hash(self.bb) ^ hash(self.idx) + def __repr__(self): return ('Location(bb=%i, idx=%i)' % (self.bb.index, self.idx)) @@ -1131,6 +1143,16 @@ def __str__(self): return ('block %i stmt:%i : %20r : %s' % (self.bb.index, self.idx, stmt, stmt)) + def prev_locs(self): + if self.bb.gimple and self.idx > 0: + # Previous gimple statement within this BB: + return [(Location(self.bb, self.idx - 1), None)] + else: + # At start of gimple statements: prior BBs: + return [(Location.get_block_end(inedge.src), inedge) + for inedge in self.bb.preds + if inedge.src.gimple] + def next_locs(self): """Get a list of Location instances, for what can happen next""" if self.bb.gimple and len(self.bb.gimple) > self.idx + 1: @@ -1153,6 +1175,11 @@ def next_loc(self): def __eq__(self, other): return self.bb == other.bb and self.idx == other.idx + @classmethod + def get_block_end(cls, bb): + # Don't bother iterating through phi_nodes if there aren't any: + return Location(bb, len(bb.gimple) - 1) + @classmethod def get_block_start(cls, bb): # Don't bother iterating through phi_nodes if there aren't any: diff --git a/libcpychecker/constraints.py b/libcpychecker/constraints.py new file mode 100644 index 00000000..f6694135 --- /dev/null +++ b/libcpychecker/constraints.py @@ -0,0 +1,636 @@ +import gcc +from libcpychecker.absinterp import Location, get_locations + +############################################################################ +# Hierarchy of Constraint classes. Instances are immutable +############################################################################ + +class Constraint: + def __and__(self, other): + return And([self, other]) + + def __or__(self, other): + return Or([self, other]) + + def simplify(self, fubar): + return self + + def delete(self, term): + # Recursively delete the constraints on the given term + # For use when handling assignment, to remove the constraints from + # the old value of the LHS. + raise NotImplementedError() + + def as_html(self): + raise NotImplementedError() + +class Boolean(Constraint): + def __init__(self, terms): + assert isinstance(terms, (set, frozenset, list)) + self.terms = frozenset(terms) + + def __eq__(self, other): + if self.__class__ == other.__class__: + if self.terms == other.terms: + return True + + def __hash__(self): + return hash(self.terms) + + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, + ', ' .join(repr(term) for term in self.terms)) + + def __str__(self): + return '(' + (' %s ' % self.name).join(str(term) for term in self.terms) + ')' + + def simplify(self, fubar): + newterms = set() + for term in self.terms: + term = term.simplify(fubar) + + # promote + # And(..., And(a, b, c), ....) + # to: + # And(..., a, b, c, ...) + # and analogously for Or(..., Or(), ...) + if term.__class__ == self.__class__: + for innerterm in term.terms: + newterms.add(innerterm) + else: + # Eliminate redundant + # and Top() + # and + # or Bottom() + # terms: + if isinstance(term, Top): + if isinstance(self, Or): + # "or Top()" is always True: + return Top() + elif isinstance(term, Bottom): + if isinstance(self, And): + # "and Bottom()" is impossible: + return Bottom() + newterms.add(term) + + # Now that we've handled the "always True" and "impossible" case, strip + # remaining "Top()" and "Bottom()" terms, as long as there are + # other terms (in which case they are redundant): + if len(newterms) > 1: + newterms = {term for term in newterms + if not isinstance(term, Top)} + if len(newterms) > 1: + newterms = {term for term in newterms + if not isinstance(term, Bottom)} + + # If we have just a single term, eliminate this And() or Or() clause + # around it, just using the term itself: + if len(newterms) == 1: + return newterms.pop() + + # Verify that And() clauses are actually possible: + # This isn't a full solver, but will catch some cases that are + # impossible + if isinstance(self, And) and fubar: + # Gather predicates by LHS: + # dict of expr -> And() condition affecting that expr: + exprpreds = {} + satisfiableterms = set() + for term in newterms: + if isinstance(term, Predicate): + print(term) + # FIXME: only the lhs for now + if term.lhs in exprpreds: + exprpreds[term.lhs] = exprpreds[term.lhs] & term + if isinstance(exprpreds[term.lhs], Bottom): + return Bottom() + else: + exprpreds[term.lhs] = term + else: + satisfiableterms.add(term) + for expr in exprpreds: + satisfiableterms.add(exprpreds[expr]) + return And(satisfiableterms).simplify(False) + + return self.__class__(newterms) + + def delete(self, term): + newterms = set() + for t in self.terms: + t = t.delete(term) + newterms.add(t) + return self.__class__(newterms).simplify(True) + + def as_html(self): + return ('(\n' + + ('\n %s\n' % self.name).join( + ['\n'.join(' %s' % line + for line in term.as_html().splitlines()) + for term in self.terms]) + + '\n)') + +class And(Boolean): + name = 'and' + + def __and__(self, other): + return And(list(self.terms) + [other]) + + #def __or__(self, other): + # return And([term | other for term in self.terms]) + +class Or(Boolean): + name = 'or' + + def __and__(self, other): + return Or([term & other for term in self.terms]) + + def __or__(self, other): + return Or(list(self.terms) + [other]) + +class Predicate(Constraint): + def __init__(self, lhs, op, rhs): + self.lhs = lhs + self.op = op + self.rhs = rhs + + def __repr__(self): + return 'Predicate(%r, %r, %r)' % (self.lhs, self.op, self.rhs) + + def __str__(self): + return '%s %s %s' % (self.lhs, self.op, self.rhs) + + def __eq__(self, other): + if isinstance(other, Predicate): + if self.lhs == other.lhs: + if self.op == other.op: + if self.rhs == other.rhs: + return True + + def __hash__(self): + return hash(self.lhs) ^ hash(self.op) ^ hash(self.rhs) + + def __and__(self, other): + print('%s __and__ %s' % (self, other)) + if isinstance(other, Predicate): + if self.lhs == other.lhs: + if isinstance(self.rhs, (int, long)) and isinstance(other.rhs, (int, long)): + if self.op == '==' and other.op == '==': + # We have (EXPR == valA) AND (EXPR == valB) + if self.rhs == other.rhs: + # second clause has no effect: + raise 'foo' + return self + else: + # impossible: + return Bottom() + elif self.op == '==' and other.op == '!=': + if self.rhs == other.rhs: + # impossible: + return Bottom() + else: + # second clause has no effect: + raise 'foo' + return self + elif self.op == '!=' and other.op == '==': + if self.rhs == other.rhs: + # impossible: + raise 'foo' + return Bottom() + else: + # second clause is a better condition: + return other + elif self.op == '==' and other.op == '<=': + if self.rhs <= other.rhs: + # second clause is redundant: + return self + else: + # impossible: + return Bottom() + elif self.op == '==' and other.op == '<': + if self.rhs < other.rhs: + # second clause is redundant: + return self + else: + # impossible: + return Bottom() + elif self.op == '==' and other.op == '>=': + if self.rhs >= other.rhs: + # second clause is redundant: + return self + else: + # impossible: + return Bottom() + elif self.op == '==' and other.op == '>': + if self.rhs > other.rhs: + # second clause is redundant: + return self + else: + # impossible: + return Bottom() + + return Constraint.__and__(self, other) + + def delete(self, term): + if self.lhs == term or self.rhs == term: + return Top() + return self + + def as_html(self): + return '%s %s %s' % (self.lhs, self.op, self.rhs) + +class IsUnitialized(Constraint): + def __init__(self, var): + self.var = var + + def __repr__(self): + return 'IsUnitialized(%r)' % self.var + + def __str__(self): + return 'IsUnitialized(%r)' % self.var + +class Note(Constraint): + def __init__(self, msg): + self.msg = msg + + def __hash__(self): + return hash(self.msg) + + def __eq__(self, other): + if isinstance(other, Note): + if self.msg == other.msg: + return True + + def __repr__(self): + return 'Note(%r)' % self.msg + + def __str__(self): + return repr(self.msg) + + def delete(self, term): + return self + + def as_html(self): + return '%s' % self.msg + +class Top(Constraint): + # no constraints, and reachable + def __repr__(self): + return 'Top()' + + def __str__(self): + return 'Top()' + + def __eq__(self, other): + return isinstance(other, Top) + + def __hash__(self): + return 1 + + def delete(self, term): + return self + + def as_html(self): + return 'Top()' + +class Bottom(Constraint): + # no constraints, but not reachable + def __repr__(self): + return 'Bottom()' + + def __str__(self): + return 'Bottom()' + + def __eq__(self, other): + return isinstance(other, Bottom) + + def __hash__(self): + return 0 + + def delete(self, term): + return self + + def as_html(self): + return 'Bottom()' + +############################################################################ + +class DummyExpr: + def __init__(self, text): + self.text = text + def __repr__(self): + return 'DummyExpr(%r)' % self.text + def __str__(self): + return self.text + + def __hash__(self): + return hash(self.text) + + def __eq__(self, other): + if isinstance(other, DummyExpr): + return self.text == other.text + +class Solution: + def __init__(self, fun): + # a mapping from Location to Constraint + # i.e. a snapshot of what we know at each location within the function + self.fun = fun + self.locations = get_locations(fun) + self.loc_to_constraint = {loc:Bottom() for loc in self.locations} + + # The initial state is the first block after entry (which has no statements): + initbb = fun.cfg.entry.succs[0].dest + initloc = Location(initbb, 0) + self.loc_to_constraint[initloc] = Top() + + # FIXME: initial state of vars + + def __eq__(self, other): + if not isinstance(other, Solution): + return False + return self.loc_to_constraint == other.loc_to_constraint + + def as_html_tr(self, out, stage, oldsol): + out.write('') + out.write('%s' % stage) + for loc in self.locations: + if oldsol: + oldconstraint = oldsol.loc_to_constraint[loc] + else: + oldconstraint = None + constraint = self.loc_to_constraint[loc] + if not (constraint == oldconstraint): + out.write('
%s
' % constraint.as_html()) + else: + out.write('
%s
' % constraint.as_html()) + out.write('\n') + + def eval(self, expr): + print('expr: %r' % expr) + if isinstance(expr, gcc.VarDecl): + return expr + if isinstance(expr, gcc.Constant): + return expr.constant + if isinstance(expr, gcc.ArrayRef): + return expr + if isinstance(expr, DummyExpr): + return expr + if expr is None: + return None + raise foo + + + def get_constraint_for_edge(self, srcloc, dstloc, edge): + class NewObj: + def __repr__(self): + return 'NewObj()' + def __str__(self): + return 'NewObj()' + class DerefField: + def __init__(self, ptr, fieldname): + self.ptr = ptr + self.fieldname = fieldname + def __repr__(self): + return 'DerefField(%r, %r)' % (self.ptr, self.fieldname) + def __str__(self): + return '%s->%s' % (self.ptr, self.fieldname) + + stmt = srcloc.get_stmt() + print(' %s ' % stmt) + + srcconstraint = self.loc_to_constraint[srcloc] + + if isinstance(stmt, gcc.GimpleAssign): + print(' %r %r %r' % (stmt.lhs, stmt.rhs, stmt.exprcode)) + if stmt.exprcode == gcc.IntegerCst: + rhs = self.eval(stmt.rhs[0]) + elif stmt.exprcode == gcc.VarDecl: + rhs = DummyExpr(stmt.rhs[0].name) # FIXME + elif stmt.exprcode == gcc.PlusExpr: + rhs = DummyExpr(str(stmt)) # FIXME + elif stmt.exprcode == gcc.Constructor: + rhs = DummyExpr(str(stmt)) # FIXME + else: + raise UnhandledAssignment() + lhs = self.eval(stmt.lhs) + # Remove old value from srcconstraint: + return srcconstraint.delete(lhs) & Predicate(lhs, '==', rhs) + + elif isinstance(stmt, gcc.GimpleCall): + print('%r %r %r' % (stmt.lhs, stmt.fn, stmt.args)) + if isinstance(stmt.fn, gcc.AddrExpr): + if isinstance(stmt.fn.operand, gcc.FunctionDecl): + print('stmt.fn.operand.name: %r' % stmt.fn.operand.name) + fnname = stmt.fn.operand.name + def make_result(message, op, value): + note = Note(message) + if stmt.lhs: + return note & Predicate(self.eval(stmt.lhs), op, value) + else: + return note + + def make_success(op, value): + return make_result('%s() succeeded' % fnname, op, value) + + def make_failure(op, value): + return make_result('%s() failed' % fnname, op, value) + + if fnname == 'PyArg_ParseTuple': + success = make_success('==', 1) + # FIXME: also update the args ^^^ + failure = make_failure('==', 0) + return srcconstraint & (success | failure) + elif fnname == 'PyList_New': + + newobj = NewObj() # FIXME + success = make_success('!=', 0) + """ + success = (Note('%s() succeeded' % fnname) & + Predicate(self.eval(stmt.lhs), '!=', 0) + #Predicate(self.eval(stmt.lhs), '==', newobj) & + #Predicate(newobj, '!=', 0) #& + #Predicate(DerefField(newobj, 'ob_refcnt'), '==', 1) & # FIXME + #Predicate(DerefField(newobj, 'ob_type'), '==', 'PyList_Type') + ) + """ + failure = make_failure('==', 0) + return srcconstraint & (success | failure) + elif fnname == 'PyList_Append': + # etc + success = make_success('==', 0) + failure = make_failure('==', -1) + return srcconstraint & (success | failure) + elif fnname == 'PyLong_FromLong': + newobj = NewObj() # FIXME + success = make_success('!=', 0) + """ + success = (Note('%s() succeeded' % fnname) & + Predicate(self.eval(stmt.lhs), '!=', 0) + #Predicate(self.eval(stmt.lhs), '==', newobj) & + #Predicate(newobj, '!=', 0) #& + #Predicate(DerefField(newobj, 'ob_refcnt'), '==', 1) & # FIXME + #Predicate(DerefField(newobj, 'ob_type'), '==', 'PyLong_Type') + ) + """ + failure = make_failure('==', 0) + return srcconstraint & (success | failure) + + elif fnname == 'random': + # FIXME: only listing this one for completeness + return srcconstraint & Top() # FIXME: make lhs not be uninitialized + else: + # Unknown function: + + # FIXME: + raise UnknownFunction() + + return Top() # FIXME: make lhs not be uninitialized + raise CantHandlePointerToFunctionYet() + elif isinstance(stmt, gcc.GimpleCond): + print(' %r %r %r %r %r' % (stmt.lhs, stmt.rhs, stmt.exprcode, stmt.true_label, stmt.false_label)) + print('edge: %r' % edge) + + if stmt.exprcode == gcc.EqExpr: + op = '==' if edge.true_value else '!=' + elif stmt.exprcode == gcc.LtExpr: + op = '<' if edge.true_value else '>=' + elif stmt.exprcode == gcc.LeExpr: + op = '<=' if edge.true_value else '>' + else: + raise UnhandledConditional() # FIXME + + cond = Predicate(self.eval(stmt.lhs), op, self.eval(stmt.rhs)) + return srcconstraint & cond + elif isinstance(stmt, gcc.GimpleLabel): + return srcconstraint & Top() + else: + raise UnhandledStatementType() + raise ShouldntGetHere() + +class HtmlLog: + def __init__(self, out, solver): + self.out = out + out.write('\n') + + # Write headings: + out.write('') + out.write('') + for loc in solver.locations: + out.write('' + % (loc.bb.index, loc.idx)) + out.write('\n') + out.write('') + out.write('') + for loc in solver.locations: + out.write('' % loc.get_stmt()) + out.write('\n') + out.write('') + out.write('') + for loc in solver.locations: + out.write('' % loc.get_stmt()) + out.write('\n') + +class Solver: + def __init__(self, fun): + self.fun = fun + self.locations = get_locations(fun) + self.solutions = [] + + def solve(self): + # calculate least fixed point + with open('constraints.html', 'w') as out: + html = HtmlLog(out, self) + while True: + idx = len(self.solutions) + if self.solutions: + oldsol = self.solutions[-1] + else: + oldsol = None + newsol = Solution(self.fun) + if oldsol: + # FIXME: optimize using a worklist: + for loc in self.locations: + newval = oldsol.loc_to_constraint[loc] + for prevloc, edge in loc.prev_locs(): + print(' edge from: %s' % prevloc) + print(' to: %s' % loc) + value = oldsol.get_constraint_for_edge(prevloc, loc, edge) + print(' str(value): %s' % value) + print('repr(value): %r' % value) + newval = newval | value + newval = newval.simplify(True) + print(' new value: %s' % newval) + + newsol.loc_to_constraint[loc] = newval + # TODO: update based on transfer functions + self.solutions.append(newsol) + print(newsol.loc_to_constraint) + newsol.as_html_tr(out, idx, oldsol) + if oldsol == newsol: + # We've reached a fixed point + break + + if len(self.solutions) > 20: + # bail out: termination isn't working for some reason + raise BailOut() + + +class ConstraintPass(gcc.GimplePass): + def __init__(self): + gcc.GimplePass.__init__(self, 'constraint-pass-gimple') + + def execute(self, fun): + print(fun) + + if 0: + # Dump location information + for loc in get_locations(fun): + print(loc) + for prevloc in loc.prev_locs(): + print(' prev: %s' % prevloc) + for nextloc in loc.next_locs(): + print(' next: %s' % nextloc) + + solver = Solver(fun) + solver.solve() + #with open('solution.html', 'w') as out: + # constraint.dump_as_html(out) + +def main(): + gimple_ps = ConstraintPass() + + """ + c1 = IsUnitialized('count') + print('c1') + print(c1) + print(repr(c1)) + + c2 = Or([And([Predicate('D1', '!=', 0), + Predicate('count', '>=', -0x80000000), + Predicate('count', '<', 0x80000000), + Note('PyArg_ParseTuple() succeeded')]), + And([Predicate('D1', '==', 0), + IsUnitialized('count'), + Note('PyArg_ParseTuple() failed')]) + ]) + print('c2') + print(c2) + print(repr(c2)) + + c3 = ((Predicate('D1', '!=', 0) & + Predicate('count', '>=', -0x80000000) & + Predicate('count', '<', 0x80000000) & + Note('PyArg_ParseTuple() succeeded')) | + (Predicate('D1', '==', 0) & + IsUnitialized('count') & + Note('PyArg_ParseTuple() failed'))) + print('c3') + print(c3) + print(repr(c3)) + """ + + if 1: + # non-SSA version: + gimple_ps.register_before('*warn_function_return') + else: + # SSA version: + gimple_ps.register_after('ssa') diff --git a/tests/nextsteps/synthetic/buffer-overflow-in-loop/input.c b/tests/nextsteps/synthetic/buffer-overflow-in-loop/input.c new file mode 100644 index 00000000..7ae82965 --- /dev/null +++ b/tests/nextsteps/synthetic/buffer-overflow-in-loop/input.c @@ -0,0 +1,45 @@ +/* + Copyright 2012 David Malcolm + Copyright 2012 Red Hat, Inc. + + This is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see + . +*/ + +#include + +/* + Ensure that the checker complains about a buffer overflow in a loop + (due to an off-by-one) +*/ + +void test(void) +{ + char buf[4096]; + int i; + + /* BUG: condition should have been <, not <=, so it will + write one past the end of the array: */ + for (i = 0; i <= 4096; i++) { + buf[i] = 42; + } +} + +/* + PEP-7 +Local variables: +c-basic-offset: 4 +indent-tabs-mode: nil +End: +*/ diff --git a/tests/nextsteps/synthetic/buffer-overflow-in-loop/script.py b/tests/nextsteps/synthetic/buffer-overflow-in-loop/script.py new file mode 100644 index 00000000..fca3cbcd --- /dev/null +++ b/tests/nextsteps/synthetic/buffer-overflow-in-loop/script.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright 2012 David Malcolm +# Copyright 2012 Red Hat, Inc. +# +# This is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# . + +from libcpychecker.constraints import main + +main() diff --git a/tests/nextsteps/synthetic/buffer-overflow-in-loop/stderr.txt b/tests/nextsteps/synthetic/buffer-overflow-in-loop/stderr.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/nextsteps/synthetic/buffer-overflow-in-loop/stderr.txt @@ -0,0 +1 @@ + diff --git a/tests/nextsteps/synthetic/buffer-overflow-in-loop/stdout.txt b/tests/nextsteps/synthetic/buffer-overflow-in-loop/stdout.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/nextsteps/synthetic/buffer-overflow-in-loop/stdout.txt @@ -0,0 +1 @@ + diff --git a/tests/nextsteps/synthetic/list-of-random-ints/input.c b/tests/nextsteps/synthetic/list-of-random-ints/input.c new file mode 100644 index 00000000..a9830848 --- /dev/null +++ b/tests/nextsteps/synthetic/list-of-random-ints/input.c @@ -0,0 +1,22 @@ +#include + +PyObject * +make_a_list_of_random_ints_badly(PyObject *self, + PyObject *args) +{ + PyObject *list, *item; + long count, i; + + if (!PyArg_ParseTuple(args, "i", &count)) { + return NULL; + } + + list = PyList_New(0); + + for (i = 0; i < count; i++) { + item = PyLong_FromLong(random()); + PyList_Append(list, item); + } + + return list; +} diff --git a/tests/nextsteps/synthetic/list-of-random-ints/metadata.ini b/tests/nextsteps/synthetic/list-of-random-ints/metadata.ini new file mode 100644 index 00000000..0d5711cb --- /dev/null +++ b/tests/nextsteps/synthetic/list-of-random-ints/metadata.ini @@ -0,0 +1,3 @@ +[ExpectedBehavior] +# We expect only compilation *warnings*, so we expect a 0 exit code +exitcode = 0 diff --git a/tests/nextsteps/synthetic/list-of-random-ints/script.py b/tests/nextsteps/synthetic/list-of-random-ints/script.py new file mode 100644 index 00000000..fca3cbcd --- /dev/null +++ b/tests/nextsteps/synthetic/list-of-random-ints/script.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright 2012 David Malcolm +# Copyright 2012 Red Hat, Inc. +# +# This is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# . + +from libcpychecker.constraints import main + +main() diff --git a/tests/nextsteps/synthetic/list-of-random-ints/stderr.txt b/tests/nextsteps/synthetic/list-of-random-ints/stderr.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/nextsteps/synthetic/list-of-random-ints/stderr.txt @@ -0,0 +1 @@ + diff --git a/tests/nextsteps/synthetic/list-of-random-ints/stdout.txt b/tests/nextsteps/synthetic/list-of-random-ints/stdout.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/nextsteps/synthetic/list-of-random-ints/stdout.txt @@ -0,0 +1 @@ +
Stageblock %i stmt:%i
Stage%r
Stage%s