From e9ee571b354a95496dc17f1d41eef31d37bb11bd Mon Sep 17 00:00:00 2001 From: Alexander Arlt Date: Wed, 28 Apr 2021 16:35:59 -0500 Subject: [PATCH] [isoltest] Add support for call side-effects. --- test/libsolidity/SemanticTest.cpp | 40 +++++- test/libsolidity/SemanticTest.h | 2 + .../{smoke_test.sol => builtins.sol} | 3 +- .../semanticTests/isoltestTesting/effects.sol | 15 +++ test/libsolidity/util/SoltestTypes.h | 6 + test/libsolidity/util/TestFileParser.cpp | 50 +++++++- test/libsolidity/util/TestFileParser.h | 14 ++- test/libsolidity/util/TestFileParserTests.cpp | 115 +++++++++++++++++- test/libsolidity/util/TestFunctionCall.cpp | 17 +++ test/libsolidity/util/TestFunctionCall.h | 1 + 10 files changed, 242 insertions(+), 21 deletions(-) rename test/libsolidity/semanticTests/isoltestTesting/{smoke_test.sol => builtins.sol} (60%) create mode 100644 test/libsolidity/semanticTests/isoltestTesting/effects.sol diff --git a/test/libsolidity/SemanticTest.cpp b/test/libsolidity/SemanticTest.cpp index 3409614cb..1af3cac34 100644 --- a/test/libsolidity/SemanticTest.cpp +++ b/test/libsolidity/SemanticTest.cpp @@ -58,6 +58,7 @@ SemanticTest::SemanticTest( m_sources(m_reader.sources()), m_lineOffset(m_reader.lineNumber()), m_builtins(makeBuiltins()), + m_sideEffectHooks(makeSideEffectHooks()), m_enforceViaYul(_enforceViaYul), m_enforceCompileToEwasm(_enforceCompileToEwasm), m_enforceGasCost(_enforceGasCost), @@ -127,12 +128,22 @@ map SemanticTest::makeBuiltins() { return { { - "smokeTest", + "isoltest_builtin_test", [](FunctionCall const&) -> optional { return util::toBigEndian(u256(0x1234)); } }, + { + "isoltest_side_effects_test", + [](FunctionCall const& _call) -> optional + { + if (_call.arguments.parameters.empty()) + return util::toBigEndian(0); + else + return _call.arguments.rawBytes(); + } + }, { "balance", [this](FunctionCall const& _call) -> optional @@ -167,6 +178,23 @@ map SemanticTest::makeBuiltins() }; } + +vector SemanticTest::makeSideEffectHooks() const +{ + return { + [](FunctionCall const& _call) -> vector + { + if (_call.signature == "isoltest_side_effects_test") + { + vector result; + for (auto const& argument: _call.arguments.parameters) + result.emplace_back(toHex(argument.rawBytes)); + return result; + } + return {}; + }}; +} + TestCase::TestResult SemanticTest::run(ostream& _stream, string const& _linePrefix, bool _formatted) { TestResult result = TestResult::Success; @@ -322,6 +350,13 @@ TestCase::TestResult SemanticTest::runTest( test.setRawBytes(move(output)); test.setContractABI(m_compiler.contractABI(m_compiler.lastContractName(m_sources.mainSourceFile))); } + + vector effects; + for (SideEffectHook const& hook: m_sideEffectHooks) + effects += hook(test.call()); + test.setSideEffects(move(effects)); + + success &= test.call().expectedSideEffects == test.call().actualSideEffects; } if (!m_testCaseWantsYulRun && _isYulRun) @@ -538,8 +573,7 @@ void SemanticTest::printUpdatedSettings(ostream& _stream, string const& _linePre void SemanticTest::parseExpectations(istream& _stream) { - TestFileParser parser{_stream, m_builtins}; - m_tests += parser.parseFunctionCalls(m_lineOffset); + m_tests += TestFileParser{_stream, m_builtins}.parseFunctionCalls(m_lineOffset); } bool SemanticTest::deploy( diff --git a/test/libsolidity/SemanticTest.h b/test/libsolidity/SemanticTest.h index adbcd4f5d..97fdb2de6 100644 --- a/test/libsolidity/SemanticTest.h +++ b/test/libsolidity/SemanticTest.h @@ -81,10 +81,12 @@ private: TestResult runTest(std::ostream& _stream, std::string const& _linePrefix, bool _formatted, bool _isYulRun, bool _isEwasmRun); bool checkGasCostExpectation(TestFunctionCall& io_test, bool _compileViaYul) const; std::map makeBuiltins(); + std::vector makeSideEffectHooks() const; SourceMap m_sources; std::size_t m_lineOffset; std::vector m_tests; std::map const m_builtins; + std::vector const m_sideEffectHooks; bool m_testCaseWantsYulRun = false; bool m_testCaseWantsEwasmRun = false; bool m_testCaseWantsLegacyRun = true; diff --git a/test/libsolidity/semanticTests/isoltestTesting/smoke_test.sol b/test/libsolidity/semanticTests/isoltestTesting/builtins.sol similarity index 60% rename from test/libsolidity/semanticTests/isoltestTesting/smoke_test.sol rename to test/libsolidity/semanticTests/isoltestTesting/builtins.sol index d41b0e15e..c34cfa4e4 100644 --- a/test/libsolidity/semanticTests/isoltestTesting/smoke_test.sol +++ b/test/libsolidity/semanticTests/isoltestTesting/builtins.sol @@ -3,5 +3,4 @@ contract SmokeTest { // ==== // compileViaYul: also // ---- -// constructor() -// smokeTest -> 0x1234 +// isoltest_builtin_test -> 0x1234 diff --git a/test/libsolidity/semanticTests/isoltestTesting/effects.sol b/test/libsolidity/semanticTests/isoltestTesting/effects.sol new file mode 100644 index 000000000..0f00e5de8 --- /dev/null +++ b/test/libsolidity/semanticTests/isoltestTesting/effects.sol @@ -0,0 +1,15 @@ +contract SmokeTest { +} +// ==== +// compileViaYul: also +// ---- +// isoltest_side_effects_test -> 0 +// isoltest_side_effects_test: 0x1234 -> 0x1234 +// ~ 0000000000000000000000000000000000000000000000000000000000001234 +// isoltest_side_effects_test: 0x1234, 0x2345 # comment # -> 0x1234, 0x2345 +// ~ 0000000000000000000000000000000000000000000000000000000000001234 +// ~ 0000000000000000000000000000000000000000000000000000000000002345 +// isoltest_side_effects_test: 0x1234, 0x2345, 0x3456 -> 0x1234, 0x2345, 0x3456 # comment # +// ~ 0000000000000000000000000000000000000000000000000000000000001234 +// ~ 0000000000000000000000000000000000000000000000000000000000002345 +// ~ 0000000000000000000000000000000000000000000000000000000000003456 diff --git a/test/libsolidity/util/SoltestTypes.h b/test/libsolidity/util/SoltestTypes.h index b746fd80a..9a6fef1b5 100644 --- a/test/libsolidity/util/SoltestTypes.h +++ b/test/libsolidity/util/SoltestTypes.h @@ -38,6 +38,7 @@ namespace solidity::frontend::test T(LBrace, "{", 0) \ T(RBrace, "}", 0) \ T(Sub, "-", 0) \ + T(Tilde, "~", 0) \ T(Colon, ":", 0) \ T(Comma, ",", 0) \ T(Period, ".", 0) \ @@ -302,8 +303,13 @@ struct FunctionCall /// Marks this function call as "short-handed", meaning /// no `->` declared. bool omitsArrow = true; + /// A textual representation of the expected side-effect of the function call. + std::vector expectedSideEffects{}; + /// A textual representation of the actual side-effect of the function call. + std::vector actualSideEffects{}; }; using Builtin = std::function(FunctionCall const&)>; +using SideEffectHook = std::function(FunctionCall const&)>; } diff --git a/test/libsolidity/util/TestFileParser.cpp b/test/libsolidity/util/TestFileParser.cpp index ba472bdc4..11f59b5a6 100644 --- a/test/libsolidity/util/TestFileParser.cpp +++ b/test/libsolidity/util/TestFileParser.cpp @@ -42,7 +42,7 @@ using Token = soltest::Token; char TestFileParser::Scanner::peek() const noexcept { - if (std::distance(m_char, m_line.end()) < 2) + if (std::distance(m_char, m_source.end()) < 2) return '\0'; auto next = m_char; @@ -97,7 +97,6 @@ vector TestFileParser::parseFunctionCall else { FunctionCall call; - if (accept(Token::Library, true)) { expect(Token::Colon); @@ -154,7 +153,10 @@ vector TestFileParser::parseFunctionCall call.kind = FunctionCall::Kind::Constructor; } - calls.emplace_back(std::move(call)); + accept(Token::Newline, true); + call.expectedSideEffects = parseFunctionCallSideEffects(); + + calls.emplace_back(move(call)); } } catch (TestParserError const& _e) @@ -169,6 +171,22 @@ vector TestFileParser::parseFunctionCall return calls; } +vector TestFileParser::parseFunctionCallSideEffects() +{ + vector result; + while (accept(Token::Tilde, false)) + { + string effect = m_scanner.currentLiteral(); + result.emplace_back(effect); + soltestAssert(m_scanner.currentToken() == Token::Tilde, ""); + m_scanner.scanNextToken(); + if (m_scanner.currentToken() == Token::Newline) + m_scanner.scanNextToken(); + } + + return result; +} + bool TestFileParser::accept(Token _token, bool const _expect) { if (m_scanner.currentToken() != _token) @@ -492,9 +510,10 @@ string TestFileParser::parseString() void TestFileParser::Scanner::readStream(istream& _stream) { std::string line; + // TODO: std::getline(..) removes newlines '\n', if present. This could be improved. while (std::getline(_stream, line)) - m_line += line; - m_char = m_line.begin(); + m_source += line; + m_char = m_source.begin(); } void TestFileParser::Scanner::scanNextToken() @@ -545,6 +564,10 @@ void TestFileParser::Scanner::scanNextToken() else selectToken(Token::Sub); break; + case '~': + advance(); + selectToken(Token::Tilde, readLine()); + break; case ':': selectToken(Token::Colon); break; @@ -588,7 +611,7 @@ void TestFileParser::Scanner::scanNextToken() } else if (langutil::isWhiteSpace(current())) selectToken(Token::Whitespace); - else if (isEndOfLine()) + else if (isEndOfFile()) { m_currentToken = Token::EOS; m_currentLiteral = ""; @@ -601,6 +624,21 @@ void TestFileParser::Scanner::scanNextToken() while (m_currentToken == Token::Whitespace); } +string TestFileParser::Scanner::readLine() +{ + string line; + // Right now the scanner discards all (real) new-lines '\n' in TestFileParser::Scanner::readStream(..). + // Token::NewLine is defined as `//`, and NOT '\n'. We are just searching here for the next `/`. + // Note that `/` anywhere else than at the beginning of a line is currently forbidden (TODO: until we fix newline handling). + // Once the end of the file would be reached (or beyond), peek() will return '\0'. + while (peek() != '\0' && peek() != '/') + { + advance(); + line += current(); + } + return line; +} + string TestFileParser::Scanner::scanComment() { string comment; diff --git a/test/libsolidity/util/TestFileParser.h b/test/libsolidity/util/TestFileParser.h index 00658b75c..7e89ff09e 100644 --- a/test/libsolidity/util/TestFileParser.h +++ b/test/libsolidity/util/TestFileParser.h @@ -90,20 +90,21 @@ private: std::string scanDecimalNumber(); std::string scanHexNumber(); std::string scanString(); + std::string readLine(); char scanHexPart(); private: /// Advances current position in the input stream. void advance(unsigned n = 1) { - solAssert(m_char != m_line.end(), "Cannot advance beyond end."); + solAssert(m_char != m_source.end(), "Cannot advance beyond end."); m_char = std::next(m_char, n); } /// Returns the current character or '\0' if at end of input. char current() const noexcept { - if (m_char == m_line.end()) + if (m_char == m_source.end()) return '\0'; return *m_char; @@ -113,10 +114,10 @@ private: /// without advancing the input stream iterator. char peek() const noexcept; - /// Returns true if the end of a line is reached, false otherwise. - bool isEndOfLine() const { return m_char == m_line.end(); } + /// Returns true if the end of the file is reached, false otherwise. + bool isEndOfFile() const { return m_char == m_source.end(); } - std::string m_line; + std::string m_source; std::string::const_iterator m_char; std::string m_currentLiteral; @@ -180,6 +181,9 @@ private: /// Parses the current string literal. std::string parseString(); + /// Parses the expected side effects of a function call execution. + std::vector parseFunctionCallSideEffects(); + /// Checks whether a builtin function with the given signature exist. /// @returns true, if builtin found, false otherwise bool isBuiltinFunction(std::string const& _signature); diff --git a/test/libsolidity/util/TestFileParserTests.cpp b/test/libsolidity/util/TestFileParserTests.cpp index 0a412b303..faf9d26a7 100644 --- a/test/libsolidity/util/TestFileParserTests.cpp +++ b/test/libsolidity/util/TestFileParserTests.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -42,12 +43,10 @@ using Mode = FunctionCall::DisplayMode; namespace { -vector parse(string const& _source) +vector parse(string const& _source, std::map const& _builtins = {}) { - static std::map const builtins = {}; - istringstream stream{_source, ios_base::out}; - return TestFileParser{stream, builtins}.parseFunctionCalls(0); + return TestFileParser{stream, _builtins}.parseFunctionCalls(0); } void testFunctionCall( @@ -100,7 +99,7 @@ BOOST_AUTO_TEST_CASE(smoke_test) BOOST_REQUIRE_EQUAL(parse(source).size(), 0); } -BOOST_AUTO_TEST_CASE(call_success) +BOOST_AUTO_TEST_CASE(call_succees) { char const* source = R"( // success() -> @@ -959,6 +958,112 @@ BOOST_AUTO_TEST_CASE(library) ); } +BOOST_AUTO_TEST_CASE(call_effects) +{ + std::map builtins; + builtins["builtin_returning_call_effect"] = [](FunctionCall const&) -> std::optional + { + return util::toBigEndian(u256(0x1234)); + }; + builtins["builtin_returning_call_effect_no_ret"] = [](FunctionCall const&) -> std::optional + { + return {}; + }; + + char const* source = R"( + // builtin_returning_call_effect -> 1 + // ~ bla + // ~ bla bla + // ~ bla bla bla + )"; + vector calls = parse(source, builtins); + BOOST_REQUIRE_EQUAL(calls.size(), 1); + BOOST_REQUIRE_EQUAL(calls[0].expectedSideEffects.size(), 3); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[0]), "bla"); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[1]), "bla bla"); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[2]), "bla bla bla"); + source = R"( + // builtin_returning_call_effect -> 1 + // ~ bla + // ~ bla bla + // builtin_returning_call_effect -> 2 + // ~ bla bla bla + // builtin_returning_call_effect -> 3 + )"; + calls = parse(source, builtins); + BOOST_REQUIRE_EQUAL(calls.size(), 3); + BOOST_REQUIRE_EQUAL(calls[0].expectedSideEffects.size(), 2); + BOOST_REQUIRE_EQUAL(calls[1].expectedSideEffects.size(), 1); + BOOST_REQUIRE_EQUAL(calls[2].expectedSideEffects.size(), 0); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[0]), "bla"); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[1]), "bla bla"); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[1].expectedSideEffects[0]), "bla bla bla"); + source = R"( + // builtin_returning_call_effect -> 1 + // ~ bla + // ~ bla bla bla + // ~ abc ~ def ~ ghi + // ~ ~ ~ + )"; + calls = parse(source, builtins); + BOOST_REQUIRE_EQUAL(calls.size(), 1); + BOOST_REQUIRE_EQUAL(calls[0].expectedSideEffects.size(), 4); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[0]), "bla"); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[1]), "bla bla bla"); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[2]), "abc ~ def ~ ghi"); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[3]), "~ ~"); + source = R"( + // builtin_returning_call_effect_no_ret -> + // ~ hello world + )"; + calls = parse(source, builtins); + BOOST_REQUIRE_EQUAL(calls.size(), 1); + BOOST_REQUIRE_EQUAL(calls[0].expectedSideEffects.size(), 1); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[0]), "hello world"); + source = R"( + // builtin_returning_call_effect -> 1 + // ~ + )"; + calls = parse(source, builtins); + BOOST_REQUIRE_EQUAL(calls.size(), 1); + BOOST_REQUIRE_EQUAL(calls[0].expectedSideEffects.size(), 1); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[0]), ""); + source = R"( + // builtin_returning_call_effect -> 1 # a comment # + // ~ hello world + )"; + calls = parse(source, builtins); + BOOST_REQUIRE_EQUAL(calls.size(), 1); + BOOST_REQUIRE_EQUAL(calls[0].expectedSideEffects.size(), 1); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[0]), "hello world"); + source = R"( + // builtin_returning_call_effect_no_ret -> # another comment # + // ~ hello world + )"; + calls = parse(source, builtins); + BOOST_REQUIRE_EQUAL(calls.size(), 1); + BOOST_REQUIRE_EQUAL(calls[0].expectedSideEffects.size(), 1); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[0]), "hello world"); + source = R"( + // builtin_returning_call_effect_no_ret # another comment # + // ~ hello world + )"; + calls = parse(source, builtins); + BOOST_REQUIRE_EQUAL(calls.size(), 1); + BOOST_REQUIRE_EQUAL(calls[0].expectedSideEffects.size(), 1); + BOOST_REQUIRE_EQUAL(boost::trim_copy(calls[0].expectedSideEffects[0]), "hello world"); + source = R"( + // builtin_returning_call_effect_no_ret # another comment # + // ~ hello/world + )"; + BOOST_CHECK_THROW(parse(source, builtins), std::exception); + source = R"( + // builtin_returning_call_effect_no_ret # another comment # + // ~ hello//world + )"; + BOOST_CHECK_THROW(parse(source, builtins), std::exception); +} + BOOST_AUTO_TEST_SUITE_END() } diff --git a/test/libsolidity/util/TestFunctionCall.cpp b/test/libsolidity/util/TestFunctionCall.cpp index 65300512c..ad543b06b 100644 --- a/test/libsolidity/util/TestFunctionCall.cpp +++ b/test/libsolidity/util/TestFunctionCall.cpp @@ -197,6 +197,23 @@ string TestFunctionCall::format( } stream << formatGasExpectations(_linePrefix, _renderMode == RenderMode::ExpectedValuesActualGas, _interactivePrint); + + vector sideEffects; + if (_renderMode == RenderMode::ExpectedValuesExpectedGas || _renderMode == RenderMode::ExpectedValuesActualGas) + sideEffects = m_call.expectedSideEffects; + else + sideEffects = m_call.actualSideEffects; + + if (!sideEffects.empty()) + { + stream << std::endl; + for (string const& effect: sideEffects) + { + stream << _linePrefix << "// ~ " << effect; + if (effect != *sideEffects.rbegin()) + stream << std::endl; + } + } }; formatOutput(m_call.displayMode == FunctionCall::DisplayMode::SingleLine); diff --git a/test/libsolidity/util/TestFunctionCall.h b/test/libsolidity/util/TestFunctionCall.h index 35ae139ad..c54a440ac 100644 --- a/test/libsolidity/util/TestFunctionCall.h +++ b/test/libsolidity/util/TestFunctionCall.h @@ -93,6 +93,7 @@ public: void setRawBytes(const bytes _rawBytes) { m_rawBytes = _rawBytes; } void setGasCost(std::string const& _runType, u256 const& _gasCost) { m_gasCosts[_runType] = _gasCost; } void setContractABI(Json::Value _contractABI) { m_contractABI = std::move(_contractABI); } + void setSideEffects(std::vector _sideEffects) { m_call.actualSideEffects = _sideEffects; } private: /// Tries to format the given `bytes`, applying the detected ABI types that have be set for each parameter.