[isoltest] Add support for call side-effects.

This commit is contained in:
Alexander Arlt 2021-04-28 16:35:59 -05:00
parent 29c8f282e4
commit e9ee571b35
10 changed files with 242 additions and 21 deletions

View File

@ -58,6 +58,7 @@ SemanticTest::SemanticTest(
m_sources(m_reader.sources()), m_sources(m_reader.sources()),
m_lineOffset(m_reader.lineNumber()), m_lineOffset(m_reader.lineNumber()),
m_builtins(makeBuiltins()), m_builtins(makeBuiltins()),
m_sideEffectHooks(makeSideEffectHooks()),
m_enforceViaYul(_enforceViaYul), m_enforceViaYul(_enforceViaYul),
m_enforceCompileToEwasm(_enforceCompileToEwasm), m_enforceCompileToEwasm(_enforceCompileToEwasm),
m_enforceGasCost(_enforceGasCost), m_enforceGasCost(_enforceGasCost),
@ -127,12 +128,22 @@ map<string, Builtin> SemanticTest::makeBuiltins()
{ {
return { return {
{ {
"smokeTest", "isoltest_builtin_test",
[](FunctionCall const&) -> optional<bytes> [](FunctionCall const&) -> optional<bytes>
{ {
return util::toBigEndian(u256(0x1234)); return util::toBigEndian(u256(0x1234));
} }
}, },
{
"isoltest_side_effects_test",
[](FunctionCall const& _call) -> optional<bytes>
{
if (_call.arguments.parameters.empty())
return util::toBigEndian(0);
else
return _call.arguments.rawBytes();
}
},
{ {
"balance", "balance",
[this](FunctionCall const& _call) -> optional<bytes> [this](FunctionCall const& _call) -> optional<bytes>
@ -167,6 +178,23 @@ map<string, Builtin> SemanticTest::makeBuiltins()
}; };
} }
vector<SideEffectHook> SemanticTest::makeSideEffectHooks() const
{
return {
[](FunctionCall const& _call) -> vector<string>
{
if (_call.signature == "isoltest_side_effects_test")
{
vector<string> 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) TestCase::TestResult SemanticTest::run(ostream& _stream, string const& _linePrefix, bool _formatted)
{ {
TestResult result = TestResult::Success; TestResult result = TestResult::Success;
@ -322,6 +350,13 @@ TestCase::TestResult SemanticTest::runTest(
test.setRawBytes(move(output)); test.setRawBytes(move(output));
test.setContractABI(m_compiler.contractABI(m_compiler.lastContractName(m_sources.mainSourceFile))); test.setContractABI(m_compiler.contractABI(m_compiler.lastContractName(m_sources.mainSourceFile)));
} }
vector<string> 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) if (!m_testCaseWantsYulRun && _isYulRun)
@ -538,8 +573,7 @@ void SemanticTest::printUpdatedSettings(ostream& _stream, string const& _linePre
void SemanticTest::parseExpectations(istream& _stream) void SemanticTest::parseExpectations(istream& _stream)
{ {
TestFileParser parser{_stream, m_builtins}; m_tests += TestFileParser{_stream, m_builtins}.parseFunctionCalls(m_lineOffset);
m_tests += parser.parseFunctionCalls(m_lineOffset);
} }
bool SemanticTest::deploy( bool SemanticTest::deploy(

View File

@ -81,10 +81,12 @@ private:
TestResult runTest(std::ostream& _stream, std::string const& _linePrefix, bool _formatted, bool _isYulRun, bool _isEwasmRun); TestResult runTest(std::ostream& _stream, std::string const& _linePrefix, bool _formatted, bool _isYulRun, bool _isEwasmRun);
bool checkGasCostExpectation(TestFunctionCall& io_test, bool _compileViaYul) const; bool checkGasCostExpectation(TestFunctionCall& io_test, bool _compileViaYul) const;
std::map<std::string, Builtin> makeBuiltins(); std::map<std::string, Builtin> makeBuiltins();
std::vector<SideEffectHook> makeSideEffectHooks() const;
SourceMap m_sources; SourceMap m_sources;
std::size_t m_lineOffset; std::size_t m_lineOffset;
std::vector<TestFunctionCall> m_tests; std::vector<TestFunctionCall> m_tests;
std::map<std::string, Builtin> const m_builtins; std::map<std::string, Builtin> const m_builtins;
std::vector<SideEffectHook> const m_sideEffectHooks;
bool m_testCaseWantsYulRun = false; bool m_testCaseWantsYulRun = false;
bool m_testCaseWantsEwasmRun = false; bool m_testCaseWantsEwasmRun = false;
bool m_testCaseWantsLegacyRun = true; bool m_testCaseWantsLegacyRun = true;

View File

@ -3,5 +3,4 @@ contract SmokeTest {
// ==== // ====
// compileViaYul: also // compileViaYul: also
// ---- // ----
// constructor() // isoltest_builtin_test -> 0x1234
// smokeTest -> 0x1234

View File

@ -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

View File

@ -38,6 +38,7 @@ namespace solidity::frontend::test
T(LBrace, "{", 0) \ T(LBrace, "{", 0) \
T(RBrace, "}", 0) \ T(RBrace, "}", 0) \
T(Sub, "-", 0) \ T(Sub, "-", 0) \
T(Tilde, "~", 0) \
T(Colon, ":", 0) \ T(Colon, ":", 0) \
T(Comma, ",", 0) \ T(Comma, ",", 0) \
T(Period, ".", 0) \ T(Period, ".", 0) \
@ -302,8 +303,13 @@ struct FunctionCall
/// Marks this function call as "short-handed", meaning /// Marks this function call as "short-handed", meaning
/// no `->` declared. /// no `->` declared.
bool omitsArrow = true; bool omitsArrow = true;
/// A textual representation of the expected side-effect of the function call.
std::vector<std::string> expectedSideEffects{};
/// A textual representation of the actual side-effect of the function call.
std::vector<std::string> actualSideEffects{};
}; };
using Builtin = std::function<std::optional<bytes>(FunctionCall const&)>; using Builtin = std::function<std::optional<bytes>(FunctionCall const&)>;
using SideEffectHook = std::function<std::vector<std::string>(FunctionCall const&)>;
} }

View File

@ -42,7 +42,7 @@ using Token = soltest::Token;
char TestFileParser::Scanner::peek() const noexcept 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'; return '\0';
auto next = m_char; auto next = m_char;
@ -97,7 +97,6 @@ vector<solidity::frontend::test::FunctionCall> TestFileParser::parseFunctionCall
else else
{ {
FunctionCall call; FunctionCall call;
if (accept(Token::Library, true)) if (accept(Token::Library, true))
{ {
expect(Token::Colon); expect(Token::Colon);
@ -154,7 +153,10 @@ vector<solidity::frontend::test::FunctionCall> TestFileParser::parseFunctionCall
call.kind = FunctionCall::Kind::Constructor; 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) catch (TestParserError const& _e)
@ -169,6 +171,22 @@ vector<solidity::frontend::test::FunctionCall> TestFileParser::parseFunctionCall
return calls; return calls;
} }
vector<string> TestFileParser::parseFunctionCallSideEffects()
{
vector<string> 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) bool TestFileParser::accept(Token _token, bool const _expect)
{ {
if (m_scanner.currentToken() != _token) if (m_scanner.currentToken() != _token)
@ -492,9 +510,10 @@ string TestFileParser::parseString()
void TestFileParser::Scanner::readStream(istream& _stream) void TestFileParser::Scanner::readStream(istream& _stream)
{ {
std::string line; std::string line;
// TODO: std::getline(..) removes newlines '\n', if present. This could be improved.
while (std::getline(_stream, line)) while (std::getline(_stream, line))
m_line += line; m_source += line;
m_char = m_line.begin(); m_char = m_source.begin();
} }
void TestFileParser::Scanner::scanNextToken() void TestFileParser::Scanner::scanNextToken()
@ -545,6 +564,10 @@ void TestFileParser::Scanner::scanNextToken()
else else
selectToken(Token::Sub); selectToken(Token::Sub);
break; break;
case '~':
advance();
selectToken(Token::Tilde, readLine());
break;
case ':': case ':':
selectToken(Token::Colon); selectToken(Token::Colon);
break; break;
@ -588,7 +611,7 @@ void TestFileParser::Scanner::scanNextToken()
} }
else if (langutil::isWhiteSpace(current())) else if (langutil::isWhiteSpace(current()))
selectToken(Token::Whitespace); selectToken(Token::Whitespace);
else if (isEndOfLine()) else if (isEndOfFile())
{ {
m_currentToken = Token::EOS; m_currentToken = Token::EOS;
m_currentLiteral = ""; m_currentLiteral = "";
@ -601,6 +624,21 @@ void TestFileParser::Scanner::scanNextToken()
while (m_currentToken == Token::Whitespace); 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 TestFileParser::Scanner::scanComment()
{ {
string comment; string comment;

View File

@ -90,20 +90,21 @@ private:
std::string scanDecimalNumber(); std::string scanDecimalNumber();
std::string scanHexNumber(); std::string scanHexNumber();
std::string scanString(); std::string scanString();
std::string readLine();
char scanHexPart(); char scanHexPart();
private: private:
/// Advances current position in the input stream. /// Advances current position in the input stream.
void advance(unsigned n = 1) 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); m_char = std::next(m_char, n);
} }
/// Returns the current character or '\0' if at end of input. /// Returns the current character or '\0' if at end of input.
char current() const noexcept char current() const noexcept
{ {
if (m_char == m_line.end()) if (m_char == m_source.end())
return '\0'; return '\0';
return *m_char; return *m_char;
@ -113,10 +114,10 @@ private:
/// without advancing the input stream iterator. /// without advancing the input stream iterator.
char peek() const noexcept; char peek() const noexcept;
/// Returns true if the end of a line is reached, false otherwise. /// Returns true if the end of the file is reached, false otherwise.
bool isEndOfLine() const { return m_char == m_line.end(); } 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::const_iterator m_char;
std::string m_currentLiteral; std::string m_currentLiteral;
@ -180,6 +181,9 @@ private:
/// Parses the current string literal. /// Parses the current string literal.
std::string parseString(); std::string parseString();
/// Parses the expected side effects of a function call execution.
std::vector<std::string> parseFunctionCallSideEffects();
/// Checks whether a builtin function with the given signature exist. /// Checks whether a builtin function with the given signature exist.
/// @returns true, if builtin found, false otherwise /// @returns true, if builtin found, false otherwise
bool isBuiltinFunction(std::string const& _signature); bool isBuiltinFunction(std::string const& _signature);

View File

@ -23,6 +23,7 @@
#include <string> #include <string>
#include <tuple> #include <tuple>
#include <boost/test/unit_test.hpp> #include <boost/test/unit_test.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <liblangutil/Exceptions.h> #include <liblangutil/Exceptions.h>
#include <test/ExecutionFramework.h> #include <test/ExecutionFramework.h>
@ -42,12 +43,10 @@ using Mode = FunctionCall::DisplayMode;
namespace namespace
{ {
vector<FunctionCall> parse(string const& _source) vector<FunctionCall> parse(string const& _source, std::map<std::string, Builtin> const& _builtins = {})
{ {
static std::map<std::string, Builtin> const builtins = {};
istringstream stream{_source, ios_base::out}; istringstream stream{_source, ios_base::out};
return TestFileParser{stream, builtins}.parseFunctionCalls(0); return TestFileParser{stream, _builtins}.parseFunctionCalls(0);
} }
void testFunctionCall( void testFunctionCall(
@ -100,7 +99,7 @@ BOOST_AUTO_TEST_CASE(smoke_test)
BOOST_REQUIRE_EQUAL(parse(source).size(), 0); BOOST_REQUIRE_EQUAL(parse(source).size(), 0);
} }
BOOST_AUTO_TEST_CASE(call_success) BOOST_AUTO_TEST_CASE(call_succees)
{ {
char const* source = R"( char const* source = R"(
// success() -> // success() ->
@ -959,6 +958,112 @@ BOOST_AUTO_TEST_CASE(library)
); );
} }
BOOST_AUTO_TEST_CASE(call_effects)
{
std::map<std::string, Builtin> builtins;
builtins["builtin_returning_call_effect"] = [](FunctionCall const&) -> std::optional<bytes>
{
return util::toBigEndian(u256(0x1234));
};
builtins["builtin_returning_call_effect_no_ret"] = [](FunctionCall const&) -> std::optional<bytes>
{
return {};
};
char const* source = R"(
// builtin_returning_call_effect -> 1
// ~ bla
// ~ bla bla
// ~ bla bla bla
)";
vector<FunctionCall> 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() BOOST_AUTO_TEST_SUITE_END()
} }

View File

@ -197,6 +197,23 @@ string TestFunctionCall::format(
} }
stream << formatGasExpectations(_linePrefix, _renderMode == RenderMode::ExpectedValuesActualGas, _interactivePrint); stream << formatGasExpectations(_linePrefix, _renderMode == RenderMode::ExpectedValuesActualGas, _interactivePrint);
vector<string> 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); formatOutput(m_call.displayMode == FunctionCall::DisplayMode::SingleLine);

View File

@ -93,6 +93,7 @@ public:
void setRawBytes(const bytes _rawBytes) { m_rawBytes = _rawBytes; } void setRawBytes(const bytes _rawBytes) { m_rawBytes = _rawBytes; }
void setGasCost(std::string const& _runType, u256 const& _gasCost) { m_gasCosts[_runType] = _gasCost; } void setGasCost(std::string const& _runType, u256 const& _gasCost) { m_gasCosts[_runType] = _gasCost; }
void setContractABI(Json::Value _contractABI) { m_contractABI = std::move(_contractABI); } void setContractABI(Json::Value _contractABI) { m_contractABI = std::move(_contractABI); }
void setSideEffects(std::vector<std::string> _sideEffects) { m_call.actualSideEffects = _sideEffects; }
private: private:
/// Tries to format the given `bytes`, applying the detected ABI types that have be set for each parameter. /// Tries to format the given `bytes`, applying the detected ABI types that have be set for each parameter.