From 0c8a5ccb33b0b9da14a5f80c173119616fc07b18 Mon Sep 17 00:00:00 2001 From: Ian Voysey Date: Wed, 26 May 2021 15:33:26 -0400 Subject: [PATCH] update test script to check output (#336) * formatting the test, removing old code * new test case with less going on * updating primops test case to use local vars not fields * scala style, whitespace, a new util function, comments, variable declaration first cut * updating bool literal test to refer to a local rather than a field * no prizes for guessing where i copied this file from * assign instead of declare; removing type annotations from declarations output with a comment about why * whitespace * similar enough case for decls, so might as well * updating ifthenelse test case to use locals not fields, fixing a bug in the output code * adding case for if, since it\'s so similar to ifthenelse * intconst test to locals not fields * removing sstore for local variables * fixing scaladocs * updating simplecall test to not use fields * adding a top level return to simple call so its testable * scala style * adding a slightly hacky script to run all the ganache tests locally in one pass, emulating what will happen in Travis without quite as much overhead * scraps * better shell scripting practices * more shell tweaks * Adding some logic to make local testing of specific files easier * shell script hacking; this will likely break on travis beacuse of unresolved issues with jq * moving a comment around; it's valid scala to have it where it was, but the IDE doesn't know how to parse it * whitespace and comments in testing script * adding a call to eth_call and a check on the result * updating travis build to include sha3 utility * fixed a type in a bash variable name, updated the hash implemention i'm using, now computing the right hash at least * possibly better travis install commands * apt command change * adding rhash to the build, to see if that works better * more hashing attempts * code to pick between which hash implementation based on uname * cutting hash to first 8 bytes in travis, adding messages * updating call to use the contract address * stripping quotes off the contract address, adding a dire warning about missing JSON fields, moving latest out of the dictionary part of the params object * guarded hack for running something locally, but i still want to spin up travis for the end of the day * Adding more gas; updating check on testexp and expected so it warns (at least textually; return value is unchanged); updating hostname check so it works * found a duplicated 0x with michael * checking against expected * comments * Adding check on getTransactionRecipt status code * Adding check on getTransactionRecipt status code * updating comments on testing script * removing redundant braces, some binaries from solidity code for local testing, using an easier allocation function * commenting out the failure count incrementer so i can check travis for all the tests * off by one error * removing binaries for local testing and commented line from travis script * fixed a bug with not assigning back to the temp var in the function call case; the simplecall test now passes * fixing Return test * updating Travis travis script to only make a call to eth_call if the JSON describing a test includes an expression to call and to mark it as an error if eth_call gets an error back * adding test expected values to other simple tests * test script output * breaking primops test up into one test per operator * copy paste typos * typos, small fixes * splitting subtraction test into two cases * typos * adding a rough summary of what passed and failed to the output for readability in travis * quick summary * updating codegen to fix logical negation, which entailed fixing bugs in if-then-else and the call to translate expression from translate statement * actually fixing the bug in if-then-else; adding a new test case that showed it to me * removing the json file for a failing test per #337 and #335 * removing JSON per #338 * adding a simple script that tells me which tests are defined but without json so they aren't being run * adding a warning about skipped tests * Adding comments to convenience shell scripts --- .../src/main/yul_templates/object.mustache | 2 - bin/run_obs_yul_all.sh | 14 + bin/skipped_tests.sh | 6 + .../tests/GanacheTests/AssignLocalAdd.json | 4 +- .../tests/GanacheTests/AssignLocalAdd.obs | 5 + resources/tests/GanacheTests/BoolLiteral.json | 4 +- resources/tests/GanacheTests/BoolLiteral.obs | 4 + resources/tests/GanacheTests/If.json | 4 +- resources/tests/GanacheTests/If.obs | 12 +- resources/tests/GanacheTests/IfThenElse.json | 4 +- resources/tests/GanacheTests/IfThenElse.obs | 10 + resources/tests/GanacheTests/IntConst.json | 4 +- resources/tests/GanacheTests/IntConst.obs | 6 +- resources/tests/GanacheTests/PrimOps.obs | 28 -- resources/tests/GanacheTests/PrimOpsAdd.json | 8 + resources/tests/GanacheTests/PrimOpsAdd.obs | 5 + resources/tests/GanacheTests/PrimOpsAnd.json | 8 + resources/tests/GanacheTests/PrimOpsAnd.obs | 5 + resources/tests/GanacheTests/PrimOpsDiv.json | 8 + resources/tests/GanacheTests/PrimOpsDiv.obs | 5 + resources/tests/GanacheTests/PrimOpsEq.json | 8 + resources/tests/GanacheTests/PrimOpsEq.obs | 5 + .../tests/GanacheTests/PrimOpsGreater.json | 8 + .../tests/GanacheTests/PrimOpsGreater.obs | 5 + .../tests/GanacheTests/PrimOpsGreaterEq.json | 8 + .../tests/GanacheTests/PrimOpsGreaterEq.obs | 5 + resources/tests/GanacheTests/PrimOpsLess.json | 8 + resources/tests/GanacheTests/PrimOpsLess.obs | 5 + .../tests/GanacheTests/PrimOpsLessEq.json | 8 + .../tests/GanacheTests/PrimOpsLessEq.obs | 5 + resources/tests/GanacheTests/PrimOpsMod.json | 8 + resources/tests/GanacheTests/PrimOpsMod.obs | 5 + resources/tests/GanacheTests/PrimOpsMul.json | 8 + resources/tests/GanacheTests/PrimOpsMul.obs | 5 + resources/tests/GanacheTests/PrimOpsNEq.json | 8 + resources/tests/GanacheTests/PrimOpsNEq.obs | 5 + resources/tests/GanacheTests/PrimOpsNeg.obs | 5 + .../tests/GanacheTests/PrimOpsNotFalse.json | 8 + .../tests/GanacheTests/PrimOpsNotFalse.obs | 5 + .../tests/GanacheTests/PrimOpsNotTrue.json | 8 + .../tests/GanacheTests/PrimOpsNotTrue.obs | 5 + resources/tests/GanacheTests/PrimOpsOr.json | 8 + resources/tests/GanacheTests/PrimOpsOr.obs | 5 + .../tests/GanacheTests/PrimOpsSubNeg.obs | 5 + .../tests/GanacheTests/PrimOpsSubPos.json | 8 + .../tests/GanacheTests/PrimOpsSubPos.obs | 5 + resources/tests/GanacheTests/Return.json | 4 +- resources/tests/GanacheTests/Return.obs | 6 +- resources/tests/GanacheTests/SimpleCall.json | 8 +- .../cmu/cs/obsidian/codegen/CodeGenYul.scala | 110 +++++-- .../edu/cmu/cs/obsidian/codegen/yulAST.scala | 40 +-- travis_specific/ganache_tests.sh | 278 ++++++++++++++---- travis_specific/install_ganache.sh | 3 + 53 files changed, 621 insertions(+), 140 deletions(-) create mode 100755 bin/run_obs_yul_all.sh create mode 100755 bin/skipped_tests.sh delete mode 100644 resources/tests/GanacheTests/PrimOps.obs create mode 100644 resources/tests/GanacheTests/PrimOpsAdd.json create mode 100644 resources/tests/GanacheTests/PrimOpsAdd.obs create mode 100644 resources/tests/GanacheTests/PrimOpsAnd.json create mode 100644 resources/tests/GanacheTests/PrimOpsAnd.obs create mode 100644 resources/tests/GanacheTests/PrimOpsDiv.json create mode 100644 resources/tests/GanacheTests/PrimOpsDiv.obs create mode 100644 resources/tests/GanacheTests/PrimOpsEq.json create mode 100644 resources/tests/GanacheTests/PrimOpsEq.obs create mode 100644 resources/tests/GanacheTests/PrimOpsGreater.json create mode 100644 resources/tests/GanacheTests/PrimOpsGreater.obs create mode 100644 resources/tests/GanacheTests/PrimOpsGreaterEq.json create mode 100644 resources/tests/GanacheTests/PrimOpsGreaterEq.obs create mode 100644 resources/tests/GanacheTests/PrimOpsLess.json create mode 100644 resources/tests/GanacheTests/PrimOpsLess.obs create mode 100644 resources/tests/GanacheTests/PrimOpsLessEq.json create mode 100644 resources/tests/GanacheTests/PrimOpsLessEq.obs create mode 100644 resources/tests/GanacheTests/PrimOpsMod.json create mode 100644 resources/tests/GanacheTests/PrimOpsMod.obs create mode 100644 resources/tests/GanacheTests/PrimOpsMul.json create mode 100644 resources/tests/GanacheTests/PrimOpsMul.obs create mode 100644 resources/tests/GanacheTests/PrimOpsNEq.json create mode 100644 resources/tests/GanacheTests/PrimOpsNEq.obs create mode 100644 resources/tests/GanacheTests/PrimOpsNeg.obs create mode 100644 resources/tests/GanacheTests/PrimOpsNotFalse.json create mode 100644 resources/tests/GanacheTests/PrimOpsNotFalse.obs create mode 100644 resources/tests/GanacheTests/PrimOpsNotTrue.json create mode 100644 resources/tests/GanacheTests/PrimOpsNotTrue.obs create mode 100644 resources/tests/GanacheTests/PrimOpsOr.json create mode 100644 resources/tests/GanacheTests/PrimOpsOr.obs create mode 100644 resources/tests/GanacheTests/PrimOpsSubNeg.obs create mode 100644 resources/tests/GanacheTests/PrimOpsSubPos.json create mode 100644 resources/tests/GanacheTests/PrimOpsSubPos.obs diff --git a/Obsidian_Runtime/src/main/yul_templates/object.mustache b/Obsidian_Runtime/src/main/yul_templates/object.mustache index 3ccccc57..e457ff66 100644 --- a/Obsidian_Runtime/src/main/yul_templates/object.mustache +++ b/Obsidian_Runtime/src/main/yul_templates/object.mustache @@ -35,7 +35,6 @@ object "{{creationObject}}" { {{! todo: is 4 a magic number or should it be generated based on the object in question? check the ABI }} if iszero(lt(calldatasize(), 4)) { - { {{#dispatch}} {{! TODO 224 is a magic number offset to shift to follow the spec above; check that it's right }} let selector := shr(224, calldataload(0)) @@ -95,5 +94,4 @@ object "{{creationObject}}" { {{runtimeSubObjects}} } {{subObjects}} -} } \ No newline at end of file diff --git a/bin/run_obs_yul_all.sh b/bin/run_obs_yul_all.sh new file mode 100755 index 00000000..05417e7d --- /dev/null +++ b/bin/run_obs_yul_all.sh @@ -0,0 +1,14 @@ +#!/bin/bash + + +# this is a wrapper on the run_obs_yul script, which produces yul for the ganache tests programs +# without actually running them, that runs it on all of the test programs in a batch mode. it is +# kind of a hack but it works for now and saves a rewrite of the run_obs_yul script. + +for f in $(run_obs_yul.sh | tail -n +2) +do + echo "---------------------------" + echo $f + echo "---------------------------" + run_obs_yul.sh "$f" +done diff --git a/bin/skipped_tests.sh b/bin/skipped_tests.sh new file mode 100755 index 00000000..5b810f90 --- /dev/null +++ b/bin/skipped_tests.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# when run from the top level of the repo, this prints out the names of the obsidian files +# in the ganache tests that do not also have a json file paired with them -- that means that +# they won't be run by travis. this also appears in the output of the travis script at the very top. +comm -13 <(ls resources/tests/GanacheTests/*.json | xargs basename -s '.json') <(ls resources/tests/GanacheTests/*.obs | xargs basename -s '.obs') diff --git a/resources/tests/GanacheTests/AssignLocalAdd.json b/resources/tests/GanacheTests/AssignLocalAdd.json index 08bc3d96..42113063 100644 --- a/resources/tests/GanacheTests/AssignLocalAdd.json +++ b/resources/tests/GanacheTests/AssignLocalAdd.json @@ -2,5 +2,7 @@ "gas" : 30000000, "gasprice" : "0x9184e72a000", "startingeth" : 5000000, - "numaccts" : 1 + "numaccts" : 1, + "testexp" : "main()", + "expected" : "17" } diff --git a/resources/tests/GanacheTests/AssignLocalAdd.obs b/resources/tests/GanacheTests/AssignLocalAdd.obs index ef31ce79..de8650e5 100644 --- a/resources/tests/GanacheTests/AssignLocalAdd.obs +++ b/resources/tests/GanacheTests/AssignLocalAdd.obs @@ -5,4 +5,9 @@ main contract AssignLocalAdd{ x = 5 + 12; return; } + + transaction main() returns int { + int x = 5 + 12; + return x; + } } diff --git a/resources/tests/GanacheTests/BoolLiteral.json b/resources/tests/GanacheTests/BoolLiteral.json index 08bc3d96..3c30ec3a 100644 --- a/resources/tests/GanacheTests/BoolLiteral.json +++ b/resources/tests/GanacheTests/BoolLiteral.json @@ -2,5 +2,7 @@ "gas" : 30000000, "gasprice" : "0x9184e72a000", "startingeth" : 5000000, - "numaccts" : 1 + "numaccts" : 1, + "testexp" : "main()", + "expected" : "0" } diff --git a/resources/tests/GanacheTests/BoolLiteral.obs b/resources/tests/GanacheTests/BoolLiteral.obs index 4172730c..dccec2c7 100644 --- a/resources/tests/GanacheTests/BoolLiteral.obs +++ b/resources/tests/GanacheTests/BoolLiteral.obs @@ -4,4 +4,8 @@ main contract BoolLiteral{ x = true; x = false; } + + transaction main() returns bool { + return false; + } } diff --git a/resources/tests/GanacheTests/If.json b/resources/tests/GanacheTests/If.json index 08bc3d96..c8ffa434 100644 --- a/resources/tests/GanacheTests/If.json +++ b/resources/tests/GanacheTests/If.json @@ -2,5 +2,7 @@ "gas" : 30000000, "gasprice" : "0x9184e72a000", "startingeth" : 5000000, - "numaccts" : 1 + "numaccts" : 1, + "testexp" : "main()", + "expected" : "1" } diff --git a/resources/tests/GanacheTests/If.obs b/resources/tests/GanacheTests/If.obs index 88090896..38029c79 100644 --- a/resources/tests/GanacheTests/If.obs +++ b/resources/tests/GanacheTests/If.obs @@ -2,8 +2,16 @@ main contract If { transaction iftest() { int x; if (true) { - x = 1; - } + x = 1; + } return; } + + transaction main() returns int { + int x = 0; + if (true) { + x = 1; + } + return x; + } } diff --git a/resources/tests/GanacheTests/IfThenElse.json b/resources/tests/GanacheTests/IfThenElse.json index 08bc3d96..34279d33 100644 --- a/resources/tests/GanacheTests/IfThenElse.json +++ b/resources/tests/GanacheTests/IfThenElse.json @@ -2,5 +2,7 @@ "gas" : 30000000, "gasprice" : "0x9184e72a000", "startingeth" : 5000000, - "numaccts" : 1 + "numaccts" : 1, + "testexp" : "main()", + "expected" : "90" } diff --git a/resources/tests/GanacheTests/IfThenElse.obs b/resources/tests/GanacheTests/IfThenElse.obs index 55e183ea..2ef8be1f 100644 --- a/resources/tests/GanacheTests/IfThenElse.obs +++ b/resources/tests/GanacheTests/IfThenElse.obs @@ -8,4 +8,14 @@ main contract IfThenElse { } return; } + + transaction main() returns int { + int x = 9; + if (false) { + x = 50; + } else { + x = 90; + } + return x; + } } diff --git a/resources/tests/GanacheTests/IntConst.json b/resources/tests/GanacheTests/IntConst.json index 08bc3d96..7e5a461e 100644 --- a/resources/tests/GanacheTests/IntConst.json +++ b/resources/tests/GanacheTests/IntConst.json @@ -2,5 +2,7 @@ "gas" : 30000000, "gasprice" : "0x9184e72a000", "startingeth" : 5000000, - "numaccts" : 1 + "numaccts" : 1, + "testexp" : "main()", + "expected" : "12" } diff --git a/resources/tests/GanacheTests/IntConst.obs b/resources/tests/GanacheTests/IntConst.obs index 298d4f06..0ca805d9 100644 --- a/resources/tests/GanacheTests/IntConst.obs +++ b/resources/tests/GanacheTests/IntConst.obs @@ -1,7 +1,11 @@ main contract IntConst{ transaction intconst() { int x; - x = 4; + x = 12; return; } + + transaction main() returns int { + return 12; + } } diff --git a/resources/tests/GanacheTests/PrimOps.obs b/resources/tests/GanacheTests/PrimOps.obs deleted file mode 100644 index 23178b1d..00000000 --- a/resources/tests/GanacheTests/PrimOps.obs +++ /dev/null @@ -1,28 +0,0 @@ -main contract PrimOps{ - transaction primops() { - int x; - bool y; - - y = true && false; - y = true || false; - - x = 4 + 4; - x = 4 - 4; - x = 4 / 4; - x = 4 * 4; - x = 4 % 4; - - y = 1 == 2; - y = 1 != 2; - y = 3 > 4; - y = 3 >= 4; - y = 3 < 3; - y = 3 <= 3; - y = 3 != 3; - - y = !y; - x = - x; - - return; - } -} diff --git a/resources/tests/GanacheTests/PrimOpsAdd.json b/resources/tests/GanacheTests/PrimOpsAdd.json new file mode 100644 index 00000000..e42ad540 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsAdd.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopsadd()", + "expected" : "13" +} diff --git a/resources/tests/GanacheTests/PrimOpsAdd.obs b/resources/tests/GanacheTests/PrimOpsAdd.obs new file mode 100644 index 00000000..e669160d --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsAdd.obs @@ -0,0 +1,5 @@ +main contract PrimOpsAdd{ + transaction primopsadd() returns int { + return(5+8); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsAnd.json b/resources/tests/GanacheTests/PrimOpsAnd.json new file mode 100644 index 00000000..c711d1ab --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsAnd.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopsand()", + "expected" : "0" +} diff --git a/resources/tests/GanacheTests/PrimOpsAnd.obs b/resources/tests/GanacheTests/PrimOpsAnd.obs new file mode 100644 index 00000000..4429dbb1 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsAnd.obs @@ -0,0 +1,5 @@ +main contract PrimOpsAnd{ + transaction primopsand() returns bool { + return(true && false); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsDiv.json b/resources/tests/GanacheTests/PrimOpsDiv.json new file mode 100644 index 00000000..39a58b00 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsDiv.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopsdiv()", + "expected" : "2" +} diff --git a/resources/tests/GanacheTests/PrimOpsDiv.obs b/resources/tests/GanacheTests/PrimOpsDiv.obs new file mode 100644 index 00000000..755ae3b2 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsDiv.obs @@ -0,0 +1,5 @@ +main contract PrimOpsDiv{ + transaction primopsdiv() returns int { + return(4/2); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsEq.json b/resources/tests/GanacheTests/PrimOpsEq.json new file mode 100644 index 00000000..23341fb6 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsEq.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopseq()", + "expected" : "0" +} diff --git a/resources/tests/GanacheTests/PrimOpsEq.obs b/resources/tests/GanacheTests/PrimOpsEq.obs new file mode 100644 index 00000000..3ad9745b --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsEq.obs @@ -0,0 +1,5 @@ +main contract PrimOpsEq{ + transaction primopseq() returns bool { + return(5 == 9); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsGreater.json b/resources/tests/GanacheTests/PrimOpsGreater.json new file mode 100644 index 00000000..0c14db06 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsGreater.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopsgreater()", + "expected" : "0" +} diff --git a/resources/tests/GanacheTests/PrimOpsGreater.obs b/resources/tests/GanacheTests/PrimOpsGreater.obs new file mode 100644 index 00000000..d5ecb188 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsGreater.obs @@ -0,0 +1,5 @@ +main contract PrimOpsGreater{ + transaction primopsgreater() returns bool { + return(12 > 19); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsGreaterEq.json b/resources/tests/GanacheTests/PrimOpsGreaterEq.json new file mode 100644 index 00000000..ba3ab4ff --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsGreaterEq.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopsgreatereq()", + "expected" : "1" +} diff --git a/resources/tests/GanacheTests/PrimOpsGreaterEq.obs b/resources/tests/GanacheTests/PrimOpsGreaterEq.obs new file mode 100644 index 00000000..ed6cad35 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsGreaterEq.obs @@ -0,0 +1,5 @@ +main contract PrimOpsGreaterEq{ + transaction primopsgreatereq() returns bool { + return(12 >= 12); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsLess.json b/resources/tests/GanacheTests/PrimOpsLess.json new file mode 100644 index 00000000..4dfe14a5 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsLess.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopsless()", + "expected" : "1" +} diff --git a/resources/tests/GanacheTests/PrimOpsLess.obs b/resources/tests/GanacheTests/PrimOpsLess.obs new file mode 100644 index 00000000..0a0db0ff --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsLess.obs @@ -0,0 +1,5 @@ +main contract PrimOpsLess{ + transaction primopsless() returns bool { + return(9 < 10); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsLessEq.json b/resources/tests/GanacheTests/PrimOpsLessEq.json new file mode 100644 index 00000000..5daeec5d --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsLessEq.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopslesseq()", + "expected" : "1" +} diff --git a/resources/tests/GanacheTests/PrimOpsLessEq.obs b/resources/tests/GanacheTests/PrimOpsLessEq.obs new file mode 100644 index 00000000..f2667567 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsLessEq.obs @@ -0,0 +1,5 @@ +main contract PrimOpsLessEq{ + transaction primopslesseq() returns bool { + return(9 <= 19); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsMod.json b/resources/tests/GanacheTests/PrimOpsMod.json new file mode 100644 index 00000000..0b815d36 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsMod.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopsmod()", + "expected" : "5" +} diff --git a/resources/tests/GanacheTests/PrimOpsMod.obs b/resources/tests/GanacheTests/PrimOpsMod.obs new file mode 100644 index 00000000..b961f3ae --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsMod.obs @@ -0,0 +1,5 @@ +main contract PrimOpsMod{ + transaction primopsmod() returns int { + return(13 % 8); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsMul.json b/resources/tests/GanacheTests/PrimOpsMul.json new file mode 100644 index 00000000..b6889565 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsMul.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopsmul()", + "expected" : "20" +} diff --git a/resources/tests/GanacheTests/PrimOpsMul.obs b/resources/tests/GanacheTests/PrimOpsMul.obs new file mode 100644 index 00000000..f6215cf0 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsMul.obs @@ -0,0 +1,5 @@ +main contract PrimOpsMul{ + transaction primopsmul() returns int { + return(5*4); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsNEq.json b/resources/tests/GanacheTests/PrimOpsNEq.json new file mode 100644 index 00000000..c312ffc1 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsNEq.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopsneq()", + "expected" : "1" +} diff --git a/resources/tests/GanacheTests/PrimOpsNEq.obs b/resources/tests/GanacheTests/PrimOpsNEq.obs new file mode 100644 index 00000000..34b1a600 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsNEq.obs @@ -0,0 +1,5 @@ +main contract PrimOpsNEq{ + transaction primopsneq() returns bool { + return(5 != 9); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsNeg.obs b/resources/tests/GanacheTests/PrimOpsNeg.obs new file mode 100644 index 00000000..6ec43c42 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsNeg.obs @@ -0,0 +1,5 @@ +main contract PrimOpsNeg{ + transaction primopsneg() returns int { + return(-20); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsNotFalse.json b/resources/tests/GanacheTests/PrimOpsNotFalse.json new file mode 100644 index 00000000..994bc96a --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsNotFalse.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopsnotfalse()", + "expected" : "1" +} diff --git a/resources/tests/GanacheTests/PrimOpsNotFalse.obs b/resources/tests/GanacheTests/PrimOpsNotFalse.obs new file mode 100644 index 00000000..1c8f543c --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsNotFalse.obs @@ -0,0 +1,5 @@ +main contract PrimOpsNotFalse{ + transaction primopsnotfalse() returns bool { + return(! false); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsNotTrue.json b/resources/tests/GanacheTests/PrimOpsNotTrue.json new file mode 100644 index 00000000..cb076031 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsNotTrue.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopsnottrue()", + "expected" : "0" +} diff --git a/resources/tests/GanacheTests/PrimOpsNotTrue.obs b/resources/tests/GanacheTests/PrimOpsNotTrue.obs new file mode 100644 index 00000000..07e2f14b --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsNotTrue.obs @@ -0,0 +1,5 @@ +main contract PrimOpsNotTrue{ + transaction primopsnottrue() returns bool { + return(! true); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsOr.json b/resources/tests/GanacheTests/PrimOpsOr.json new file mode 100644 index 00000000..da0102d1 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsOr.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopsor()", + "expected" : "1" +} diff --git a/resources/tests/GanacheTests/PrimOpsOr.obs b/resources/tests/GanacheTests/PrimOpsOr.obs new file mode 100644 index 00000000..eb047811 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsOr.obs @@ -0,0 +1,5 @@ +main contract PrimOpsOr{ + transaction primopsor() returns bool { + return(true || false); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsSubNeg.obs b/resources/tests/GanacheTests/PrimOpsSubNeg.obs new file mode 100644 index 00000000..1e8d0cb7 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsSubNeg.obs @@ -0,0 +1,5 @@ +main contract PrimOpsSubNeg{ + transaction primopssubneg() returns int { + return(5-20); + } +} diff --git a/resources/tests/GanacheTests/PrimOpsSubPos.json b/resources/tests/GanacheTests/PrimOpsSubPos.json new file mode 100644 index 00000000..61a3949d --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsSubPos.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "primopssubpos()", + "expected" : "15" +} diff --git a/resources/tests/GanacheTests/PrimOpsSubPos.obs b/resources/tests/GanacheTests/PrimOpsSubPos.obs new file mode 100644 index 00000000..9257a050 --- /dev/null +++ b/resources/tests/GanacheTests/PrimOpsSubPos.obs @@ -0,0 +1,5 @@ +main contract PrimOpsSubPos{ + transaction primopssubpos() returns int { + return(20-5); + } +} diff --git a/resources/tests/GanacheTests/Return.json b/resources/tests/GanacheTests/Return.json index 08bc3d96..8618ba5a 100644 --- a/resources/tests/GanacheTests/Return.json +++ b/resources/tests/GanacheTests/Return.json @@ -2,5 +2,7 @@ "gas" : 30000000, "gasprice" : "0x9184e72a000", "startingeth" : 5000000, - "numaccts" : 1 + "numaccts" : 1, + "testexp" : "f()", + "expected" : "8" } diff --git a/resources/tests/GanacheTests/Return.obs b/resources/tests/GanacheTests/Return.obs index 65eec700..8f5dd5ed 100644 --- a/resources/tests/GanacheTests/Return.obs +++ b/resources/tests/GanacheTests/Return.obs @@ -3,7 +3,11 @@ main contract Return { return (4+4); } transaction g(){ - f(); + int x = f(); return; } + + transaction main() returns int{ + return f(); + } } diff --git a/resources/tests/GanacheTests/SimpleCall.json b/resources/tests/GanacheTests/SimpleCall.json index 08bc3d96..5b3c96f3 100644 --- a/resources/tests/GanacheTests/SimpleCall.json +++ b/resources/tests/GanacheTests/SimpleCall.json @@ -1,6 +1,8 @@ { - "gas" : 30000000, + "gas" : 30000000000, "gasprice" : "0x9184e72a000", - "startingeth" : 5000000, - "numaccts" : 1 + "startingeth" : 500000000, + "numaccts" : 1, + "testexp" : "main()", + "expected" : "4" } diff --git a/src/main/scala/edu/cmu/cs/obsidian/codegen/CodeGenYul.scala b/src/main/scala/edu/cmu/cs/obsidian/codegen/CodeGenYul.scala index 5226793f..cbc138f2 100644 --- a/src/main/scala/edu/cmu/cs/obsidian/codegen/CodeGenYul.scala +++ b/src/main/scala/edu/cmu/cs/obsidian/codegen/CodeGenYul.scala @@ -15,7 +15,6 @@ import scala.collection.immutable.Map object CodeGenYul extends CodeGenerator { - // TODO improve this temporary symbol table var tempTableIdx: Int = 0 // counter indicating the next available slot in the table var stateIdx: Int = -1 // whether or not there is a state var stateEnumMapping: Map[String, Int] = Map() // map from state name to an enum value @@ -210,6 +209,7 @@ object CodeGenYul extends CodeGenerator { * @return */ def translateStatement(s: Statement, retVar: Option[String], contractName: String, checkedTable: SymbolTable): Seq[YulStatement] = { + //todo: why is retVar an option and why is it a string not an identifier? s match { case Return() => Seq(Leave()) @@ -239,26 +239,59 @@ object CodeGenYul extends CodeGenerator { Seq() } case IfThenElse(scrutinee, pos, neg) => + // generate a temp to store the last assignment used in either block + val id_last = nextTemp() + // generate a temp for the scrutinee val id_scrutinee: Identifier = nextTemp() + + // translate the scrutinee val scrutinee_yul: Seq[YulStatement] = translateExpr(id_scrutinee, scrutinee, contractName, checkedTable) - val pos_yul: Seq[YulStatement] = - pos.flatMap(s => { - val id_s: Identifier = nextTemp() - decl_0exp(id_s) +: translateStatement(s, Some(id_s.name), contractName, checkedTable) - }) - val neg_yul: Seq[YulStatement] = - neg.flatMap(s => { - val id_s: Identifier = nextTemp() - decl_0exp(id_s) +: translateStatement(s, Some(id_s.name), contractName, checkedTable) - }) - decl_0exp(id_scrutinee) +: + // todo: this may be useful elsewhere, too + + /** + * translate a statement into a new temporary variable along with its declaration + * + * @param s the statement to be translated + * @return a pair of the new variable and the sequence of yulstatements resulting + * from the translation; inductively that sequence will end in an assignment + * to the declared variable. + */ + def trans_store(s: Statement): (Identifier, Seq[YulStatement]) = { + val id_s: Identifier = nextTemp() + (id_s, decl_0exp(id_s) +: translateStatement(s, Some(id_s.name), contractName, checkedTable)) + } + + // translate each block and generate an extra assignment for the last statement + val pos_yul: Seq[(Identifier, Seq[YulStatement])] = pos.map(trans_store) + val pos_assign = assign1(id_last, pos_yul.last._1) + + val neg_yul: Seq[(Identifier, Seq[YulStatement])] = neg.map(trans_store) + val neg_assign = assign1(id_last, neg_yul.last._1) + + // assign back from which ever last statement gets run, or not depending on the inductive requirements + val assign_back = retVar match { + case Some(value) => Seq(assign1(Identifier(value), id_last)) + case None => Seq() + } + + // put the pieces together into a switch statement, preceeded by the evaluation of the + // scrutinee + (decl_0exp(id_last) +: + decl_0exp(id_scrutinee) +: scrutinee_yul :+ edu.cmu.cs.obsidian.codegen.Switch(id_scrutinee, - Seq( - Case(boollit(true), Block(pos_yul)), - Case(boollit(false), Block(neg_yul)))) - case e: Expression => translateExpr(nextTemp(), e, contractName, checkedTable) + Seq(Case(boollit(true), Block(pos_yul.flatMap(x => x._2) :+ pos_assign)), + Case(boollit(false), Block(neg_yul.flatMap(x => x._2) :+ neg_assign))))) ++ + assign_back + case e: Expression => + // todo: tighten up this logic, there's repeated code here + retVar match { + case Some(value) => translateExpr(Identifier(value), e, contractName, checkedTable) + case None => + val id = nextTemp() + decl_0exp(id) +: translateExpr(id, e, contractName, checkedTable) + } case VariableDecl(typ, varName) => Seq(decl_0exp_t(Identifier(varName), typ)) case VariableDeclWithInit(typ, varName, e) => @@ -283,11 +316,13 @@ object CodeGenYul extends CodeGenerator { s.flatMap(s => { val id_s: Identifier = nextTemp() decl_0exp(id_s) +: translateStatement(s, Some(id_s.name), contractName, checkedTable) + //todo: this also does not assign afterwards; likely the same bug as fixed in IfThenElse }) decl_0exp(id_scrutinee) +: scrutinee_yul :+ edu.cmu.cs.obsidian.codegen.If(id_scrutinee, Block(s_yul)) + // todo: also no assignment here case IfInState(e, ePerm, typeState, s1, s2) => assert(assertion = false, s"TODO: translateStatement unimplemented for ${s.toString}") Seq() @@ -358,8 +393,9 @@ object CodeGenYul extends CodeGenerator { } case e: UnaryExpression => e match { - case LogicalNegation(e) => call("not", retvar, contractName, checkedTable, e) // todo "bitwise “not” of x (every bit of x is negated)", which may be wrong - case Negate(e) => translateExpr(retvar, Subtract(NumLiteral(0), e), contractName, checkedTable) + case LogicalNegation(e) => translateStatement(IfThenElse(e, Seq(FalseLiteral()), Seq(TrueLiteral())), Some(retvar.name), contractName, checkedTable) + case Negate(e) => + translateExpr(retvar, Subtract(NumLiteral(0), e), contractName, checkedTable) case Dereference(_, _) => assert(assertion = false, "TODO: translation of " + e.toString + " is not implemented") Seq() @@ -387,29 +423,47 @@ object CodeGenYul extends CodeGenerator { case NotEquals(e1, e2) => translateExpr(retvar, LogicalNegation(Equals(e1, e2)), contractName, checkedTable) } case e@LocalInvocation(name, genericParams, params, args) => // todo: why are the middle two args not used? + // look up the name of the function in the table, get its return type, and then compute + // how wide of a tuple that return type is. (currently this is always either 0 or 1) val width = checkedTable.contractLookup(contractName).lookupTransaction(name) match { - case Some(trans) => trans.retType match { - case Some(typ) => obsTypeToWidth(typ) - case None => 0 - } + case Some(trans) => + trans.retType match { + case Some(typ) => obsTypeToWidth(typ) + case None => 0 + } case None => assert(assertion = false, "encountered a function name without knowing how many things it returns") -1 } // todo: some of this logic may be repeated in the dispatch table + + // todo: the code here is set up to mostly work in the world in which obsidian has tuples, + // which it does not. i wrote it before i knew that. the assert below is one place that it breaks; + // to fix it, i need to refactor this object so that i pass around a vector of temporary variables + // to assign returns to rather than just one (i think). this is OK for now, but technical debt that + // we'll have to address if we ever add tuples to obsidian. + + // for each argument expression, produce a new temp variable and translate it to a + // sequence of yul statements ending in an assignment to that variable. val (seqs, ids) = { args.map(p => { val id: Identifier = nextTemp() (translateExpr(id, p, contractName, checkedTable), id) }).unzip } - seqs.flatten :+ - (if (width == 0) { - ExpressionStatement(FunctionCall(Identifier(name), ids)) - } else { - decl_nexp(Seq.tabulate(width)(_ => nextTemp()), FunctionCall(Identifier(name), ids)) - }) + + // the result is the recursive translation and the expression either using the temp + // here or not. + // + // todo: this does not work with non-void functions that are called without binding their results, ie "f()" if f returns an int + seqs.flatten ++ (width match { + case 0 => Seq(ExpressionStatement(FunctionCall(Identifier(name), ids))) + case 1 => + val id: Identifier = nextTemp() + Seq(decl_1exp(id, FunctionCall(Identifier(name), ids)), assign1(retvar, id)) + case _ => assert(assertion = false, "obsidian currently does not support tuples; this shouldn't happen."); Seq() + }) case Invocation(recipient, genericParams, params, name, args, isFFIInvocation) => assert(assertion = false, "TODO: translation of " + e.toString + " is not implemented") diff --git a/src/main/scala/edu/cmu/cs/obsidian/codegen/yulAST.scala b/src/main/scala/edu/cmu/cs/obsidian/codegen/yulAST.scala index 6f400fc4..180cc6c8 100644 --- a/src/main/scala/edu/cmu/cs/obsidian/codegen/yulAST.scala +++ b/src/main/scala/edu/cmu/cs/obsidian/codegen/yulAST.scala @@ -101,26 +101,28 @@ case class Assignment(variableNames: Seq[Identifier], value: Expression) extends */ case class VariableDeclaration(variables: Seq[(Identifier, Option[String])], value: Option[Expression]) extends YulStatement { override def toString: String = { + // todo: + // added below after ._1.name, this code correctly adds type annotations to the output Yul, + // but as of Version: 0.8.1+commit.df193b15.Linux.g++ of solc, you get errors like + // "Error: "bool" is not a valid type (user defined types are not yet supported)." + // when you run that code through solc even though the spec says otherwise. + /* + + (v._2 match { + case Some(t) => s" : $t" + case None => "" + }) + */ + s"let ${ - variables.map(v => v._1.name - // todo: - // this code correctly adds type annotations to the output Yul, but as of - // Version: 0.8.1+commit.df193b15.Linux.g++ of solc, you get errors like - // "Error: "bool" is not a valid type (user defined types are not yet supported)." - // when you run that code through solc even though the spec says otherwise. - /* - + (v._2 match { - case Some(t) => s" : $t" - case None => "" - }) - */ - ).mkString(", ") + variables.map(v => v._1.name).mkString(", ") }" + (value match { - case Some(e) => s" := ${e.toString}" - case None => "" - }) + case Some (e) => s" := ${e.toString}" + case None => "" } + + ) +} } case class FunctionDefinition(name: String, @@ -276,8 +278,8 @@ case class YulObject(name: String, code: Code, subobjects: Seq[YulObject], data: codegen.ExpressionStatement(apply("abi_decode_tuple", intlit(4), apply("calldatasize"))), // fun_retrieve_24() call_f_and_maybe_assign, - // let memPos := allocate_memory(0) - decl_1exp(mp_id, apply("allocate_memory", intlit(0))), + // let memPos := allocate_unbounded() + decl_1exp(mp_id, apply("allocate_unbounded")), // let memEnd := abi_encode_tuple__to__fromStack(memPos) // let memEnd := abi_encode_tuple_t_uint256__to_t_uint256__fromStack(memPos , ret_0), etc. // nb: the code for these is written dynamically below so we can assume that they exist before they do @@ -380,7 +382,7 @@ case class YulObject(name: String, code: Code, subobjects: Seq[YulObject], data: val encode_lines: Seq[YulStatement] = var_indices.map(i => ExpressionStatement(apply("abi_encode_t_uint256_to_t_uint256_fromStack", Identifier("value" + i.toString), - apply("add", Identifier("headStart"), intlit(n * 32))))) + apply("add", Identifier("headStart"), intlit((n-1) * 32))))) val bod: Seq[YulStatement] = assign1(Identifier("tail"), apply("add", Identifier("headStart"), intlit(32 * n))) +: encode_lines FunctionDefinition("abi_encode_tuple_to_fromStack" + n.toString, diff --git a/travis_specific/ganache_tests.sh b/travis_specific/ganache_tests.sh index c33a833f..10893832 100755 --- a/travis_specific/ganache_tests.sh +++ b/travis_specific/ganache_tests.sh @@ -13,57 +13,90 @@ then exit 1 fi -for test in resources/tests/GanacheTests/*.json +# either test only the directories named as arguments or test everything if nothing is specified. +# since the travis.yml file doesn't give any argument here, CI will test everything, but this makes +# local testing easier. +tests=() +if [ $# -gt 0 ] +then + for arg in "$@" + do + tests+=("resources/tests/GanacheTests/$arg.json") + done +else + tests=(resources/tests/GanacheTests/*.json) +fi + +missing_json=$(comm -13 <(ls resources/tests/GanacheTests/*.json | xargs basename -s '.json') <(ls resources/tests/GanacheTests/*.obs | xargs basename -s '.obs')) +if [ "$missing_json" ] +then + echo "******** warning: some tests are defined but do not have json files and will not be run: $missing_json" +fi +# keep track of which tests fail so that we can output that at the bottom of the log +failed=() + +for test in "${tests[@]}" do echo "---------------------------------------------------------------" echo "running Ganache Test $test" echo "---------------------------------------------------------------" - NAME=$(basename -s '.json' $test) - GAS=$(cat "$test" | jq '.gas') + # pull values out of the json file for the test; some of these are integers stored as hex strings + # because they're so large, which means we have to cut out the quotation marks. + NAME=$(basename -s '.json' "$test") + GAS=$(<"$test" jq '.gas') GAS_HEX=$(printf '%x' "$GAS") - # nb: we store gas price as a string because it's usually quite large so - # it's good to have it in hex notation, but that means we need to crop - # off the quotations. - GAS_PRICE=$(cat "$test" | jq '.gasprice' | tr -d '"') - START_ETH=$(cat "$test" | jq '.startingeth') - NUM_ACCT=$(cat "$test" | jq '.numaccts') - - # compile the contract to yul, also creating the directory to work in - sbt "runMain edu.cmu.cs.obsidian.Main --yul resources/tests/GanacheTests/$NAME.obs" - - # check to make sure that solc succeeded, failing otherwise - if [ $? -ne 0 ]; then + GAS_PRICE=$(<"$test" jq '.gasprice' | tr -d '"') + START_ETH=$(<"$test" jq '.startingeth') + NUM_ACCT=$(<"$test" jq '.numaccts') + TESTEXP=$(<"$test" jq '.testexp' | tr -d '"') + EXPECTED=$(<"$test" jq '.expected' | tr -d '"') + + CHECK_OUTPUT=true + + if [[ $TESTEXP == "null" ]] + then + echo "*****WARNING: no test expression supplied" + CHECK_OUTPUT=false + fi + + if [[ $EXPECTED == "null" ]] + then + echo "*****WARNING: no expected result supplied" + CHECK_OUTPUT=false + fi + + # compile the contract to yul, also creating the directory to work in, failing otherwise + if ! sbt "runMain edu.cmu.cs.obsidian.Main --yul resources/tests/GanacheTests/$NAME.obs" + then echo "$NAME test failed: sbt exited cannot compile obs to yul" + failed+=("$test [sbt]") exit 1 fi if [ ! -d "$NAME" ]; then echo "$NAME directory failed to get created" + failed+=("$test [directory]") exit 1 fi - cd "$NAME" + cd "$NAME" || exit 1 - # generate the evm from yul + # generate the evm from yul, failing if not echo "running solc to produce evm bytecode" - docker run -v "$( pwd -P )":/sources ethereum/solc:stable --abi --bin --strict-assembly /sources/"$NAME".yul > "$NAME".evm - - # check to make sure that solc succeeded, failing otherwise - if [ $? -ne 0 ]; then + if ! docker run -v "$( pwd -P )":/sources ethereum/solc:stable --abi --bin --strict-assembly /sources/"$NAME".yul > "$NAME".evm + then echo "$NAME test failed: solc cannot compile yul code" + failed+=("$test [solc]") exit 1 fi - # todo this is a bit of a hack. solc is supposed to output a json object - # and it just isn't. so this is grepping through to grab the right lines - # with the hex that represents the output. this likely fails if the binary - # is more than one line long. (issue #302) - TOP=`grep -n "Binary representation" $NAME.evm | cut -f1 -d:` - BOT=`grep -n "Text representation" $NAME.evm | cut -f1 -d:` + # grep through the format output by solc in yul producing mode for the binary representation + TOP=$(grep -n "Binary representation" "$NAME".evm | cut -f1 -d:) + BOT=$(grep -n "Text representation" "$NAME".evm | cut -f1 -d:) TOP=$((TOP+1)) # drop the line with the name BOT=$((BOT-1)) # drop the empty line after the binary - EVM_BIN=`sed -n $TOP','$BOT'p' $NAME.evm` + EVM_BIN=$(sed -n $TOP','$BOT'p' "$NAME".evm) echo "binary representation is: $EVM_BIN" # start up ganache @@ -97,60 +130,190 @@ do # job passes or fails based on the last command run RET=0 - # todo: i'm not sure what account to mark as the "to" account. i think i - # can use that later to test the output of running more complicated - # contracts. i'll need to make more than one account when i start up - # ganache. (issue #302) - ACCT=`echo $ACCTS | jq '.result[0]' | tr -d '"'` + ## step 1: get an account from ganache + # todo: i'm not sure what account to use for the "to" account. (issue #302) + ACCT=$(echo "$ACCTS" | jq '.result[0]' | tr -d '"') echo "ACCT is $ACCT" - # todo what's that 0x0 mean? + # todo: 0x0 is the value being sent with the transaction; right now that's nothing (issue #302) PARAMS=$( jq -ncM \ --arg "fn" "$ACCT" \ --arg "gn" "0x$GAS_HEX" \ - --arg "gpn" "$GAS_PRICE" \ - --arg "vn" "0x0" \ - --arg "dn" "0x$EVM_BIN" \ - '{"from":$fn,"gas":$gn,"gasPrice":$gpn,"value":$vn,"data":$dn}' - ) + --arg "gpn" "$GAS_PRICE" \ + --arg "vn" "0x0" \ + --arg "dn" "0x$EVM_BIN" \ + '{"from":$fn,"gas":$gn,"gasPrice":$gpn,"value":$vn,"data":$dn}') + ## step 2: send a transaction SEND_DATA=$( jq -ncM \ --arg "jn" "2.0" \ --arg "mn" "eth_sendTransaction" \ --argjson "pn" "$PARAMS" \ --arg "idn" "1" \ - '{"jsonrpc":$jn,"method":$mn,"params":$pn,"id":$idn}' - ) + '{"jsonrpc":$jn,"method":$mn,"params":$pn,"id":$idn}') echo "transaction being sent is given by" - echo "$SEND_DATA" # | jq -M #todo why doesn't this work on travis? also below. (issue #302) + echo "$SEND_DATA" | jq echo RESP=$(curl -s -X POST --data "$SEND_DATA" http://localhost:8545) - echo "response from ganache is: $RESP" - # ((echo "$RESP" | tr -d '\n') ; echo) # | jq -M # (issue #302) + echo "response from ganache is: " #$RESP + echo "$RESP" | jq - # todo: this is not an exhaustive or principled way to check the output of - # curling a post. (issue #302) + # todo: this is not an exhaustive check on the output from curl (issue #302) if [ "$RESP" == "400 Bad Request" ] then echo "got a 400 bad response from ganache-cli" - exit 1 + failed+=("$test [400]") + RET=$((RET+1)) + continue fi ERROR=$(echo "$RESP" | tr -d '\n' | jq '.error.message') if [ "$ERROR" != "null" ] then - RET=1 + RET=$((RET+1)) echo "transaction produced an error: $ERROR" + failed+=("$test [transaction]") + continue fi - # todo check the result of test somehow to indicate failure or not (issue #302) + if [ $CHECK_OUTPUT == "true" ] + then + ## step 3: get a transaction receipt to get the contract address + # todo: check the result of test somehow to indicate failure or not (issue #302) + # todo: this block is copied; make a function? + echo "querying ganache CLI for transaction receipt" + + trans_hash=$(echo "$RESP" | jq '.result') + + SEND_DATA=$( jq -ncM \ + --arg "jn" "2.0" \ + --arg "mn" "eth_getTransactionReceipt" \ + --argjson "pn" "$trans_hash" \ + --arg "idn" "1" \ + '{"jsonrpc":$jn,"method":$mn,"params":[$pn],"id":$idn}' + ) + echo "eth_getTransactionReceipt is being sent" + echo "$SEND_DATA" | jq + echo + + RESP=$(curl -s -X POST --data "$SEND_DATA" http://localhost:8545) + echo "response from ganache is: " + echo "$RESP" | jq + + # check that the status code is 0x1 or else fail + if [[ ! $(echo "$RESP" | jq '.result.status' | tr -d '"' ) == "0x1" ]] + then + echo "eth_getTransactionReceipt returned an error status; aborting" + RET=$((RET+1)) + failed+=("$test [eth_getTransactionReceipt]") + continue + fi + + + ## step 4: get the contract address from the transaction receipt, stripping quotes + CONTRACT_ADDRESS=$(echo "$RESP" | jq '.result.contractAddress' | tr -d '"' ) + + ## step 5: use call and the contract address to get the result of the function + HASH_TO_CALL="" + + # the keccak implementation we want to use depends on the operating system; as of + # May 2021 i couldn't find one that was available in both apt and homebrew and produced + # output that matches the ABI, so we have to be flexible. this is a little bit of a hack. + if [[ $(uname) == "Linux" ]] + then + # this should be what happens on Travis running Ubuntu + if ! perl -e 'use Crypt::Digest::Keccak256 qw( :all )' + then + echo "the perl module Crypt::Digest::Keccak256 is not installed, Install it via cpam or 'apt install libcryptx-perl'." + failed+=("$test [perl hash]") + exit 1 + fi + echo "assuming that we are on travis and getting the Keccak256 via perl" + HASH_TO_CALL=$(echo -n "$TESTEXP" | perl -e 'use Crypt::Digest::Keccak256 qw( :all ); print(keccak256_hex()."\n")' | cut -c1-8) + elif [[ $(uname) == "Darwin" ]] + then + # this should be what happens on OS X + if ! hash keccak-256sum + then + echo "keccak-256sum is not installed, Install it with 'brew install sha3sum'." + failed+=("$test [os x hash]") + exit 1 + fi + echo "assuming that we are on OS X and getting the Keccak256 via keccak-256sum" + HASH_TO_CALL=$(echo -n "$TESTEXP" | keccak-256sum | cut -d' ' -f1 | cut -c1-8) + else + # if you are neither on travis nor OS X, you are on your own. + echo "unable to determine OS type to pick a keccak256 implementation" + failed+=("$test [no hash]") + exit 1 + fi + echo "hash to call: $HASH_TO_CALL" + + # "The documentation then tells to take the parameter, encode it in hex and pad it left to 32 + # bytes." + PADDED_ARG=$(printf "%032g" 0) + + DATA="$HASH_TO_CALL""$PADDED_ARG" + + echo "padded arg: $PADDED_ARG" + echo "data: $DATA" + + # build a JSON object to post to eth_call with the contract address as the to account, and + # padded data + PARAMS=$( jq -ncM \ + --arg "fn" "$ACCT" \ + --arg "tn" "$CONTRACT_ADDRESS" \ + --arg "dn" "0x$DATA" \ + '{"from":$fn,"to":$tn,"data":$dn}') + + SEND_DATA=$( jq -ncM \ + --arg "jn" "2.0" \ + --arg "mn" "eth_call" \ + --argjson "pn" "$PARAMS" \ + --arg "idn" "1" \ + '{"jsonrpc":$jn,"method":$mn,"params":[$pn,"latest"],"id":$idn}' ) + echo "eth_call is being sent" + echo "$SEND_DATA" | jq + echo + + RESP=$(curl -s -X POST --data "$SEND_DATA" http://localhost:8545) + echo "response from ganache is: " + echo "$RESP" | jq + + ERROR=$(echo "$RESP" | tr -d '\n' | jq '.error.message') + if [ "$ERROR" != "null" ] + then + RET=$((RET+1)) + echo "eth_call returned an error: $ERROR" + failed+=("$test [eth_call]") + continue + fi + + # pull the result out of the JSON object, delete the quotes and leading 0x, and make it upper case + GOT=$(echo "$RESP" | jq '.result' | tr -d '"' | sed -e "s/^0x//" | tr '[:lower:]' '[:upper:]') + # use BC to convert it to decimal + GOT_DEC=$(echo "obase=10; ibase=16;$GOT" | bc) + + # todo: extend JSON object with a decode field so that we can have expected values that aren't integers more easily + if [ "$GOT_DEC" == "$EXPECTED" ] + then + echo "expected $EXPECTED (in decimal)" + echo "test passed!" + else + echo "test failed! got $GOT_DEC but expected $EXPECTED" + RET=$((RET+1)) + failed+=("$test [wrong answer]") + fi + else + echo "*****WARNING: not checking the output of running this code because the JSON describing the test didn't include it" + fi # clean up by killing ganache and the local files # todo: make this a subroutine that can get called at any of the exits (issue #302) echo "killing ganache-cli" - kill -9 $(lsof -t -i:8545) + kill -9 "$(lsof -t -i:8545)" # todo: for debugging it's nice to be able to look at these. maybe delete # them by default but take a flag to keep them around. (issue #302) @@ -165,4 +328,19 @@ do fi done +echo +echo "----------------------------" +echo "ganache test quick summary:" +echo "----------------------------" +if [ ${#failed[@]} -ne 0 ] +then + echo "failed tests:" + for failure in "${failed[@]}" + do + echo "$failure" + done +else + echo "no failed tests!" +fi + exit "$ANY_FAILURES" diff --git a/travis_specific/install_ganache.sh b/travis_specific/install_ganache.sh index 433644c5..1e1fd75b 100755 --- a/travis_specific/install_ganache.sh +++ b/travis_specific/install_ganache.sh @@ -6,3 +6,6 @@ npm install -g ganache-cli npm audit fix sudo snap install jq # a commandline JSON tool + +sudo apt update +sudo apt install libcryptx-perl # perl crypto library that implements Keccak256