diff --git a/Changelog.md b/Changelog.md
index 1ec6faf1e7e7..2872799427c0 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -21,6 +21,7 @@ Bugfixes:
Build System:
* Add support for continuous fuzzing via Google oss-fuzz
* Ubuntu PPA Packages: Use CVC4 as SMT solver instead of Z3
+ * Soltest: Add parser that is used in the file-based unit test environment.
### 0.5.3 (2019-01-22)
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 10b78bdcd72b..e231a6f977e8 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -17,6 +17,8 @@ if (LLL)
endif()
file(GLOB libsolidity_sources "libsolidity/*.cpp")
file(GLOB libsolidity_headers "libsolidity/*.h")
+file(GLOB libsolidity_util_sources "libsolidity/util/*.cpp")
+file(GLOB libsolidity_util_headers "libsolidity/util/*.h")
add_executable(soltest ${sources} ${headers}
${contracts_sources} ${contracts_headers}
@@ -26,6 +28,7 @@ add_executable(soltest ${sources} ${headers}
${libyul_sources} ${libyul_headers}
${liblll_sources} ${liblll_headers}
${libsolidity_sources} ${libsolidity_headers}
+ ${libsolidity_util_sources} ${libsolidity_util_headers}
)
target_link_libraries(soltest PRIVATE libsolc yul solidity evmasm devcore ${Boost_UNIT_TEST_FRAMEWORK_LIBRARIES})
diff --git a/test/libsolidity/util/TestFileParser.cpp b/test/libsolidity/util/TestFileParser.cpp
new file mode 100644
index 000000000000..5f1a420e9a59
--- /dev/null
+++ b/test/libsolidity/util/TestFileParser.cpp
@@ -0,0 +1,401 @@
+/*
+ This file is part of solidity.
+
+ solidity 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.
+
+ solidity 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 solidity. If not, see .
+*/
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace dev;
+using namespace langutil;
+using namespace solidity;
+using namespace dev::solidity::test;
+using namespace std;
+using namespace soltest;
+
+namespace
+{
+ bool isIdentifierStart(char c)
+ {
+ return c == '_' || c == '$' || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z');
+ }
+ bool isIdentifierPart(char c)
+ {
+ return isIdentifierStart(c) || isdigit(c);
+ }
+}
+
+vector TestFileParser::parseFunctionCalls()
+{
+ vector calls;
+ if (!accept(Token::EOS))
+ {
+ assert(m_scanner.currentToken() == Token::Unknown);
+ m_scanner.scanNextToken();
+
+ while (!accept(Token::EOS))
+ {
+ if (!accept(Token::Whitespace))
+ {
+ FunctionCall call;
+
+ /// If this is not the first call in the test,
+ /// the last call to parseParameter could have eaten the
+ /// new line already. This could only be fixed with a one
+ /// token lookahead that checks parseParameter
+ /// if the next token is an identifier.
+ if (calls.empty())
+ expect(Token::Newline);
+ else
+ accept(Token::Newline, true);
+
+ call.signature = parseFunctionSignature();
+ if (accept(Token::Comma, true))
+ call.value = parseFunctionCallValue();
+ if (accept(Token::Colon, true))
+ call.arguments = parseFunctionCallArguments();
+
+ if (accept(Token::Newline, true))
+ call.displayMode = FunctionCall::DisplayMode::MultiLine;
+
+ call.arguments.comment = parseComment();
+
+ if (accept(Token::Newline, true))
+ call.displayMode = FunctionCall::DisplayMode::MultiLine;
+
+ expect(Token::Arrow);
+ call.expectations = parseFunctionCallExpectations();
+ call.expectations.comment = parseComment();
+
+ calls.emplace_back(std::move(call));
+ }
+ }
+ }
+ return calls;
+}
+
+bool TestFileParser::accept(soltest::Token _token, bool const _expect)
+{
+ if (m_scanner.currentToken() != _token)
+ return false;
+ if (_expect)
+ return expect(_token);
+ return true;
+}
+
+bool TestFileParser::expect(soltest::Token _token, bool const _advance)
+{
+ if (m_scanner.currentToken() != _token || m_scanner.currentToken() == Token::Invalid)
+ throw Error(
+ Error::Type::ParserError,
+ "Unexpected " + formatToken(m_scanner.currentToken()) + ": \"" +
+ m_scanner.currentLiteral() + "\". " +
+ "Expected \"" + formatToken(_token) + "\"."
+ );
+ if (_advance)
+ m_scanner.scanNextToken();
+ return true;
+}
+
+string TestFileParser::parseFunctionSignature()
+{
+ string signature = m_scanner.currentLiteral();
+ expect(Token::Identifier);
+
+ signature += formatToken(Token::LParen);
+ expect(Token::LParen);
+
+ string parameters;
+ if (!accept(Token::RParen, false))
+ parameters = parseIdentifierOrTuple();
+
+ while (accept(Token::Comma))
+ {
+ parameters += formatToken(Token::Comma);
+ expect(Token::Comma);
+ parameters += parseIdentifierOrTuple();
+ }
+ if (accept(Token::Arrow, true))
+ throw Error(Error::Type::ParserError, "Invalid signature detected: " + signature);
+
+ signature += parameters;
+
+ expect(Token::RParen);
+ signature += formatToken(Token::RParen);
+ return signature;
+}
+
+u256 TestFileParser::parseFunctionCallValue()
+{
+ u256 value = convertNumber(parseNumber());
+ expect(Token::Ether);
+ return value;
+}
+
+FunctionCallArgs TestFileParser::parseFunctionCallArguments()
+{
+ FunctionCallArgs arguments;
+
+ auto param = parseParameter();
+ if (param.abiType.type == ABIType::None)
+ throw Error(Error::Type::ParserError, "No argument provided.");
+ arguments.parameters.emplace_back(param);
+
+ while (accept(Token::Comma, true))
+ arguments.parameters.emplace_back(parseParameter());
+ return arguments;
+}
+
+FunctionCallExpectations TestFileParser::parseFunctionCallExpectations()
+{
+ FunctionCallExpectations expectations;
+
+ auto param = parseParameter();
+ if (param.abiType.type == ABIType::None)
+ {
+ expectations.failure = false;
+ return expectations;
+ }
+ expectations.result.emplace_back(param);
+
+ while (accept(Token::Comma, true))
+ expectations.result.emplace_back(parseParameter());
+
+ /// We have always one virtual parameter in the parameter list.
+ /// If its type is FAILURE, the expected result is also a REVERT etc.
+ if (expectations.result.at(0).abiType.type != ABIType::Failure)
+ expectations.failure = false;
+ return expectations;
+}
+
+Parameter TestFileParser::parseParameter()
+{
+ Parameter parameter;
+ if (accept(Token::Newline, true))
+ parameter.format.newline = true;
+ auto literal = parseABITypeLiteral();
+ parameter.rawBytes = literal.first;
+ parameter.abiType = literal.second;
+ return parameter;
+}
+
+pair TestFileParser::parseABITypeLiteral()
+{
+ try
+ {
+ u256 number{0};
+ ABIType abiType{ABIType::None, 0};
+
+ if (accept(Token::Sub))
+ {
+ abiType = ABIType{ABIType::SignedDec, 32};
+ expect(Token::Sub);
+ number = convertNumber(parseNumber()) * -1;
+ }
+ else
+ {
+ if (accept(Token::Number))
+ {
+ abiType = ABIType{ABIType::UnsignedDec, 32};
+ number = convertNumber(parseNumber());
+ }
+ else if (accept(Token::Failure, true))
+ {
+ abiType = ABIType{ABIType::Failure, 0};
+ return make_pair(bytes{}, abiType);
+ }
+ }
+ return make_pair(toBigEndian(number), abiType);
+ }
+ catch (std::exception const&)
+ {
+ throw Error(Error::Type::ParserError, "Number encoding invalid.");
+ }
+}
+
+string TestFileParser::parseIdentifierOrTuple()
+{
+ string identOrTuple;
+
+ if (accept(Token::Identifier))
+ {
+ identOrTuple = m_scanner.currentLiteral();
+ expect(Token::Identifier);
+ return identOrTuple;
+ }
+ expect(Token::LParen);
+ identOrTuple += formatToken(Token::LParen);
+ identOrTuple += parseIdentifierOrTuple();
+
+ while (accept(Token::Comma))
+ {
+ identOrTuple += formatToken(Token::Comma);
+ expect(Token::Comma);
+ identOrTuple += parseIdentifierOrTuple();
+ }
+ expect(Token::RParen);
+ identOrTuple += formatToken(Token::RParen);
+ return identOrTuple;
+}
+
+string TestFileParser::parseComment()
+{
+ string comment = m_scanner.currentLiteral();
+ if (accept(Token::Comment, true))
+ return comment;
+ return string{};
+}
+
+string TestFileParser::parseNumber()
+{
+ string literal = m_scanner.currentLiteral();
+ expect(Token::Number);
+ return literal;
+}
+
+u256 TestFileParser::convertNumber(string const& _literal)
+{
+ try {
+ return u256{_literal};
+ }
+ catch (std::exception const&)
+ {
+ throw Error(Error::Type::ParserError, "Number encoding invalid.");
+ }
+}
+
+void TestFileParser::Scanner::readStream(istream& _stream)
+{
+ std::string line;
+ while (std::getline(_stream, line))
+ m_line += line;
+ m_char = m_line.begin();
+}
+
+void TestFileParser::Scanner::scanNextToken()
+{
+ // Make code coverage happy.
+ assert(formatToken(Token::NUM_TOKENS) == "");
+
+ auto detectKeyword = [](std::string const& _literal = "") -> TokenDesc {
+ if (_literal == "ether") return TokenDesc{Token::Ether, _literal};
+ if (_literal == "FAILURE") return TokenDesc{Token::Failure, _literal};
+ return TokenDesc{Token::Identifier, _literal};
+ };
+
+ auto selectToken = [this](Token _token, std::string const& _literal = "") -> TokenDesc {
+ advance();
+ return make_pair(_token, !_literal.empty() ? _literal : formatToken(_token));
+ };
+
+ TokenDesc token = make_pair(Token::Unknown, "");
+ do
+ {
+ switch(current())
+ {
+ case '/':
+ advance();
+ if (current() == '/')
+ token = selectToken(Token::Newline);
+ else
+ token = selectToken(Token::Invalid);
+ break;
+ case '-':
+ if (peek() == '>')
+ {
+ advance();
+ token = selectToken(Token::Arrow);
+ }
+ else
+ token = selectToken(Token::Sub);
+ break;
+ case ':':
+ token = selectToken(Token::Colon);
+ break;
+ case '#':
+ token = selectToken(Token::Comment, scanComment());
+ break;
+ case ',':
+ token = selectToken(Token::Comma);
+ break;
+ case '(':
+ token = selectToken(Token::LParen);
+ break;
+ case ')':
+ token = selectToken(Token::RParen);
+ break;
+ default:
+ if (isIdentifierStart(current()))
+ {
+ TokenDesc detectedToken = detectKeyword(scanIdentifierOrKeyword());
+ token = selectToken(detectedToken.first, detectedToken.second);
+ }
+ else if (isdigit(current()))
+ token = selectToken(Token::Number, scanNumber());
+ else if (isspace(current()))
+ token = selectToken(Token::Whitespace);
+ else if (isEndOfLine())
+ token = selectToken(Token::EOS);
+ break;
+ }
+ }
+ while (token.first == Token::Whitespace);
+ m_currentToken = token;
+}
+
+string TestFileParser::Scanner::scanComment()
+{
+ string comment;
+ advance();
+
+ while (current() != '#')
+ {
+ comment += current();
+ advance();
+ }
+ return comment;
+}
+
+string TestFileParser::Scanner::scanIdentifierOrKeyword()
+{
+ string identifier;
+ identifier += current();
+ while (isIdentifierPart(peek()))
+ {
+ advance();
+ identifier += current();
+ }
+ return identifier;
+}
+
+string TestFileParser::Scanner::scanNumber()
+{
+ string number;
+ number += current();
+ while (isdigit(peek()))
+ {
+ advance();
+ number += current();
+ }
+ return number;
+}
diff --git a/test/libsolidity/util/TestFileParser.h b/test/libsolidity/util/TestFileParser.h
new file mode 100644
index 000000000000..6b79008833fd
--- /dev/null
+++ b/test/libsolidity/util/TestFileParser.h
@@ -0,0 +1,359 @@
+/*
+ This file is part of solidity.
+ solidity 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.
+ solidity 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 solidity. If not, see .
+*/
+
+#pragma once
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace dev
+{
+namespace solidity
+{
+namespace test
+{
+
+/**
+ * All soltest tokens.
+ */
+#define SOLT_TOKEN_LIST(T, K) \
+ T(Unknown, "unknown", 0) \
+ T(Invalid, "invalid", 0) \
+ T(EOS, "EOS", 0) \
+ T(Whitespace, "_", 0) \
+ /* punctuations */ \
+ T(LParen, "(", 0) \
+ T(RParen, ")", 0) \
+ T(LBrack, "[", 0) \
+ T(RBrack, "]", 0) \
+ T(LBrace, "{", 0) \
+ T(RBrace, "}", 0) \
+ T(Sub, "-", 0) \
+ T(Colon, ":", 0) \
+ T(Comma, ",", 0) \
+ T(Period, ".", 0) \
+ T(Arrow, "->", 0) \
+ T(Newline, "//", 0) \
+ /* Literals & identifier */ \
+ T(Comment, "#", 0) \
+ T(Number, "number", 0) \
+ T(Identifier, "identifier", 0) \
+ /* type keywords */ \
+ K(Ether, "ether", 0) \
+ /* special keywords */ \
+ K(Failure, "FAILURE", 0) \
+
+namespace soltest
+{
+ enum class Token : unsigned int {
+ #define T(name, string, precedence) name,
+ SOLT_TOKEN_LIST(T, T)
+ NUM_TOKENS
+ #undef T
+ };
+
+ /// Prints a friendly string representation of \param _token.
+ inline std::string formatToken(Token _token)
+ {
+ switch (_token)
+ {
+ #define T(name, string, precedence) case Token::name: return string;
+ SOLT_TOKEN_LIST(T, T)
+ #undef T
+ default: // Token::NUM_TOKENS:
+ return "";
+ }
+ }
+}
+
+
+
+/**
+ * The purpose of the ABI type is the storage of type information
+ * retrieved while parsing a test. This information is used
+ * for the conversion of human-readable function arguments and
+ * return values to `bytes` and vice-versa.
+ * Defaults to None, a 0-byte representation. 0-bytes
+ * can also be interpreted as Failure, which means
+ * either a REVERT or another EVM failure.
+ */
+struct ABIType
+{
+ enum Type {
+ UnsignedDec,
+ SignedDec,
+ Failure,
+ None
+ };
+ Type type = ABIType::None;
+ size_t size = 0;
+};
+
+/**
+ * Helper that can hold format information retrieved
+ * while scanning through a parameter list in soltest.
+ */
+struct FormatInfo
+{
+ bool newline;
+};
+
+/**
+ * Parameter abstraction used for the encoding and decoding of
+ * function parameter and expectation / return value lists.
+ * A parameter list is usually a comma-separated list of literals.
+ * It should not be possible to call create a parameter holding
+ * an identifier, but if so, the ABI type would be invalid.
+ */
+struct Parameter
+{
+ /// ABI encoded / decoded `bytes` of values.
+ /// These `bytes` are used to pass values to function calls
+ /// and also to store expected return vales. These are
+ /// compared to the actual result of a function call
+ /// and used for validating it.
+ bytes rawBytes;
+ /// Types that were used to encode `rawBytes`. Expectations
+ /// are usually comma separated literals. Their type is auto-
+ /// detected and retained in order to format them later on.
+ ABIType abiType;
+ /// Format info attached to the parameter. It handles newlines given
+ /// in the declaration of it.
+ FormatInfo format;
+};
+using ParameterList = std::vector;
+
+/**
+ * Represents the expected result of a function call after it has been executed. This may be a single
+ * return value or a comma-separated list of return values. It also contains the detected input
+ * formats used to convert the values to `bytes` needed for the comparison with the actual result
+ * of a call. In addition to that, it also stores the expected transaction status.
+ * An optional comment can be assigned.
+ */
+struct FunctionCallExpectations
+{
+ /// Representation of the comma-separated (or empty) list of expected result values
+ /// attached to the function call object. It is checked against the actual result of
+ /// a function call when used in test framework.
+ ParameterList result;
+ /// Expected status of the transaction. It can be either
+ /// a REVERT or a different EVM failure (e.g. out-of-gas).
+ bool failure = true;
+ /// A Comment that can be attached to the expectations,
+ /// that is retained and can be displayed.
+ std::string comment;
+ /// ABI encoded `bytes` of parsed expected return values. It is checked
+ /// against the actual result of a function call when used in test framework.
+ bytes rawBytes() const
+ {
+ bytes raw;
+ for (auto const& param: result)
+ raw += param.rawBytes;
+ return raw;
+ }
+};
+
+/**
+ * Represents the arguments passed to a function call. This can be a single
+ * argument or a comma-separated list of arguments. It also contains the detected input
+ * formats used to convert the arguments to `bytes` needed for the call.
+ * An optional comment can be assigned.
+ */
+struct FunctionCallArgs
+{
+ /// Types that were used to encode `rawBytes`. Parameters
+ /// are usually comma separated literals. Their type is auto-
+ /// detected and retained in order to format them later on.
+ ParameterList parameters;
+ /// A Comment that can be attached to the expectations,
+ /// that is retained and can be displayed.
+ std::string comment;
+ /// ABI encoded `bytes` of parsed parameters. These `bytes`
+ /// passed to the function call.
+ bytes rawBytes() const
+ {
+ bytes raw;
+ for (auto const& param: parameters)
+ raw += param.rawBytes;
+ return raw;
+ }
+};
+
+/**
+ * Represents a function call read from an input stream. It contains the signature, the
+ * arguments, an optional ether value and an expected execution result.
+ */
+struct FunctionCall
+{
+ /// Signature of the function call, e.g. `f(uint256, uint256)`.
+ std::string signature;
+ /// Optional `ether` value that can be send with the call.
+ u256 value;
+ /// Object that holds all function parameters in their `bytes`
+ /// representations given by the contract ABI.
+ FunctionCallArgs arguments;
+ /// Object that holds all function call expectation in
+ /// their `bytes` representations given by the contract ABI.
+ /// They are checked against the actual results and their
+ /// `bytes` representation, as well as the transaction status.
+ FunctionCallExpectations expectations;
+ /// single / multi-line mode will be detected as follows:
+ /// every newline (//) in source results in a function call
+ /// that has its display mode set to multi-mode. Function and
+ /// result parameter lists are an exception: a single parameter
+ /// stores a format information that contains a newline definition.
+ enum DisplayMode {
+ SingleLine,
+ MultiLine
+ };
+ DisplayMode displayMode = DisplayMode::SingleLine;
+};
+
+/**
+ * Class that is able to parse an additional and well-formed comment section in a Solidity
+ * source file used by the file-based unit test environment. For now, it parses function
+ * calls and their expected result after the call was made.
+ *
+ * - Function calls defined in blocks:
+ * // f(uint256, uint256): 1, 1 # Signature and comma-separated list of arguments #
+ * // -> 1, 1 # Expected result value #
+ * // g(), 2 ether # (Optional) Ether to be send with the call #
+ * // -> 2, 3
+ * // h(uint256), 1 ether: 42
+ * // -> FAILURE # If REVERT or other EVM failure was detected #
+ * ...
+ */
+class TestFileParser
+{
+public:
+ /// Constructor that takes an input stream \param _stream to operate on
+ /// and creates the internal scanner.
+ TestFileParser(std::istream& _stream): m_scanner(_stream) {}
+
+ /// Parses function calls blockwise and returns a list of function calls found.
+ /// Throws an exception if a function call cannot be parsed because of its
+ /// incorrect structure, an invalid or unsupported encoding
+ /// of its arguments or expected results.
+ std::vector parseFunctionCalls();
+
+private:
+ using Token = soltest::Token;
+ /**
+ * Token scanner that is used internally to abstract away character traversal.
+ */
+ class Scanner
+ {
+ public:
+ /// Constructor that takes an input stream \param _stream to operate on.
+ /// It reads all lines into one single line, keeping the newlines.
+ Scanner(std::istream& _stream) { readStream(_stream); }
+
+ /// Reads input stream into a single line and resets the current iterator.
+ void readStream(std::istream& _stream);
+
+ /// Reads character stream and creates token.
+ void scanNextToken();
+
+ soltest::Token currentToken() { return m_currentToken.first; }
+ std::string currentLiteral() { return m_currentToken.second; }
+
+ std::string scanComment();
+ std::string scanIdentifierOrKeyword();
+ std::string scanNumber();
+
+ private:
+ using TokenDesc = std::pair;
+
+ /// Advances current position in the input stream.
+ void advance() { ++m_char; }
+ /// Returns the current character.
+ char current() const { return *m_char; }
+ /// Peeks the next character.
+ char peek() const { auto it = m_char; return *(it + 1); }
+ /// Returns true if the end of a line is reached, false otherwise.
+ bool isEndOfLine() const { return m_char == m_line.end(); }
+
+ std::string m_line;
+ std::string::iterator m_char;
+
+ std::string m_currentLiteral;
+
+ TokenDesc m_currentToken;
+ };
+
+ bool accept(soltest::Token _token, bool const _expect = false);
+ bool expect(soltest::Token _token, bool const _advance = true);
+
+ /// Parses a function call signature in the form of f(uint256, ...).
+ std::string parseFunctionSignature();
+
+ /// Parses the optional ether value that can be passed alongside the
+ /// function call arguments. Throws an InvalidEtherValueEncoding exception
+ /// if given value cannot be converted to `u256`.
+ u256 parseFunctionCallValue();
+
+ /// Parses a comma-separated list of arguments passed with a function call.
+ /// Does not check for a potential mismatch between the signature and the number
+ /// or types of arguments.
+ FunctionCallArgs parseFunctionCallArguments();
+
+ /// Parses the expected result of a function call execution.
+ FunctionCallExpectations parseFunctionCallExpectations();
+
+ /// Parses the next parameter in a comma separated list.
+ /// Takes a newly parsed, and type-annotated `bytes` argument,
+ /// appends it to the internal `bytes` buffer of the parameter. It can also
+ /// store newlines found in the source, that are needed to
+ /// format input and output of the interactive update.
+ Parameter parseParameter();
+
+ /// Parses and converts the current literal to its byte representation and
+ /// preserves the chosen ABI type. Based on that type information, the driver of
+ /// this parser can format arguments, expectations and results. Supported types:
+ /// - unsigned and signed decimal number literals.
+ /// Returns invalid ABI type for empty literal. This is needed in order
+ /// to detect empty expectations. Throws a ParserError if data is encoded incorrectly or
+ /// if data type is not supported.
+ std::pair parseABITypeLiteral();
+
+ /// Recursively parses an identifier or a tuple definition that contains identifiers
+ /// and / or parentheses like `((uint, uint), (uint, (uint, uint)), uint)`.
+ std::string parseIdentifierOrTuple();
+
+ /// Parses a comment that is defined like this:
+ /// # A nice comment. #
+ std::string parseComment();
+
+ /// Parses the current number literal.
+ std::string parseNumber();
+
+ /// Tries to convert \param _literal to `uint256` and throws if
+ /// conversion fails.
+ u256 convertNumber(std::string const& _literal);
+
+ /// A scanner instance
+ Scanner m_scanner;
+};
+
+}
+}
+}
diff --git a/test/libsolidity/util/TestFileParserTests.cpp b/test/libsolidity/util/TestFileParserTests.cpp
new file mode 100644
index 000000000000..5b89341da4e8
--- /dev/null
+++ b/test/libsolidity/util/TestFileParserTests.cpp
@@ -0,0 +1,531 @@
+/*
+ This file is part of solidity.
+
+ solidity 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.
+
+ solidity 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 solidity. If not, see .
+*/
+/**
+ * Unit tests for Solidity's test expectation parser.
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+using namespace std;
+using namespace dev::test;
+
+namespace dev
+{
+namespace solidity
+{
+namespace test
+{
+
+using fmt = ExecutionFramework;
+using Mode = FunctionCall::DisplayMode;
+
+vector parse(string const& _source)
+{
+ istringstream stream{_source, ios_base::out};
+ TestFileParser parser{stream};
+ return parser.parseFunctionCalls();
+}
+
+void testFunctionCall(
+ FunctionCall const& _call,
+ FunctionCall::DisplayMode _mode,
+ string _signature = "",
+ bool _failure = true,
+ bytes _arguments = bytes{},
+ bytes _expectations = bytes{},
+ u256 _value = 0,
+ string _argumentComment = "",
+ string _expectationComment = ""
+)
+{
+ BOOST_REQUIRE_EQUAL(_call.expectations.failure, _failure);
+ BOOST_REQUIRE_EQUAL(_call.signature, _signature);
+ ABI_CHECK(_call.arguments.rawBytes(), _arguments);
+ ABI_CHECK(_call.expectations.rawBytes(), _expectations);
+ BOOST_REQUIRE_EQUAL(_call.displayMode, _mode);
+ BOOST_REQUIRE_EQUAL(_call.value, _value);
+ BOOST_REQUIRE_EQUAL(_call.arguments.comment, _argumentComment);
+ BOOST_REQUIRE_EQUAL(_call.expectations.comment, _expectationComment);
+}
+
+BOOST_AUTO_TEST_SUITE(TestFileParserTest)
+
+BOOST_AUTO_TEST_CASE(smoke_test)
+{
+ char const* source = R"()";
+ BOOST_REQUIRE_EQUAL(parse(source).size(), 0);
+}
+
+BOOST_AUTO_TEST_CASE(call_succees)
+{
+ char const* source = R"(
+ // success() ->
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 1);
+ testFunctionCall(calls.at(0), Mode::SingleLine, "success()", false);
+}
+
+BOOST_AUTO_TEST_CASE(non_existent_call_revert_single_line)
+{
+ char const* source = R"(
+ // i_am_not_there() -> FAILURE
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 1);
+ testFunctionCall(calls.at(0), Mode::SingleLine, "i_am_not_there()", true);
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments_success)
+{
+ char const* source = R"(
+ // f(uint256): 1
+ // ->
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 1);
+ testFunctionCall(calls.at(0), Mode::MultiLine, "f(uint256)", false, fmt::encodeArgs(u256{1}));
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments_comments_success)
+{
+ char const* source = R"(
+ // f(uint256, uint256): 1, 1
+ // ->
+ // # This call should not return a value, but still succeed. #
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 1);
+ testFunctionCall(
+ calls.at(0),
+ Mode::MultiLine,
+ "f(uint256,uint256)",
+ false,
+ fmt::encodeArgs(1, 1),
+ fmt::encodeArgs(),
+ 0,
+ "",
+ " This call should not return a value, but still succeed. "
+ );
+}
+
+BOOST_AUTO_TEST_CASE(simple_single_line_call_comment_success)
+{
+ char const* source = R"(
+ // f(uint256): 1 -> # f(uint256) does not return a value. #
+ // f(uint256): 1 -> 1
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 2);
+
+ testFunctionCall(
+ calls.at(0),
+ Mode::SingleLine,
+ "f(uint256)",
+ false,
+ fmt::encodeArgs(1),
+ fmt::encodeArgs(),
+ 0,
+ "",
+ " f(uint256) does not return a value. "
+ );
+ testFunctionCall(calls.at(1), Mode::SingleLine, "f(uint256)", false, fmt::encode(1), fmt::encode(1));
+}
+
+BOOST_AUTO_TEST_CASE(multiple_single_line)
+{
+ char const* source = R"(
+ // f(uint256): 1 -> 1
+ // g(uint256): 1 ->
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 2);
+
+ testFunctionCall(calls.at(0), Mode::SingleLine, "f(uint256)", false, fmt::encodeArgs(1), fmt::encodeArgs(1));
+ testFunctionCall(calls.at(1), Mode::SingleLine, "g(uint256)", false, fmt::encodeArgs(1));
+}
+
+BOOST_AUTO_TEST_CASE(multiple_single_line_swapped)
+{
+ char const* source = R"(
+ // f(uint256): 1 ->
+ // g(uint256): 1 -> 1
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 2);
+
+ testFunctionCall(calls.at(0), Mode::SingleLine, "f(uint256)", false, fmt::encodeArgs(1));
+ testFunctionCall(calls.at(1), Mode::SingleLine, "g(uint256)", false, fmt::encodeArgs(1), fmt::encodeArgs(1));
+
+}
+
+BOOST_AUTO_TEST_CASE(non_existent_call_revert)
+{
+ char const* source = R"(
+ // i_am_not_there()
+ // -> FAILURE
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 1);
+ testFunctionCall(calls.at(0), Mode::MultiLine, "i_am_not_there()", true);
+}
+
+BOOST_AUTO_TEST_CASE(call_expectations_empty_single_line)
+{
+ char const* source = R"(
+ // _exp_() ->
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 1);
+ testFunctionCall(calls.at(0), Mode::SingleLine, "_exp_()", false);
+}
+
+BOOST_AUTO_TEST_CASE(call_expectations_empty_multiline)
+{
+ char const* source = R"(
+ // _exp_()
+ // ->
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 1);
+ testFunctionCall(calls.at(0), Mode::MultiLine, "_exp_()", false);
+}
+
+BOOST_AUTO_TEST_CASE(call_comments)
+{
+ char const* source = R"(
+ // f() # Parameter comment # -> 1 # Expectation comment #
+ // f() # Parameter comment #
+ // -> 1 # Expectation comment #
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 2);
+ testFunctionCall(
+ calls.at(0),
+ Mode::SingleLine,
+ "f()",
+ false,
+ fmt::encodeArgs(),
+ fmt::encodeArgs(1),
+ 0,
+ " Parameter comment ",
+ " Expectation comment "
+ );
+ testFunctionCall(
+ calls.at(1),
+ Mode::MultiLine,
+ "f()",
+ false,
+ fmt::encodeArgs(),
+ fmt::encodeArgs(1),
+ 0,
+ " Parameter comment ",
+ " Expectation comment "
+ );
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments)
+{
+ char const* source = R"(
+ // f(uint256), 314 ether: 5 # optional ether value #
+ // -> 4
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 1);
+ testFunctionCall(
+ calls.at(0),
+ Mode::MultiLine,
+ "f(uint256)",
+ false,
+ fmt::encodeArgs(5),
+ fmt::encodeArgs(4),
+ 314,
+ " optional ether value "
+ );
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments_tuple)
+{
+ char const* source = R"(
+ // f((uint256, bytes32), uint256) ->
+ // f((uint8), uint8) ->
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 2);
+ testFunctionCall(calls.at(0), Mode::SingleLine, "f((uint256,bytes32),uint256)", false);
+ testFunctionCall(calls.at(1), Mode::SingleLine, "f((uint8),uint8)", false);
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments_tuple_of_tuples)
+{
+ char const* source = R"(
+ // f(((uint256, bytes32), bytes32), uint256)
+ // # f(S memory s, uint256 b) #
+ // ->
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 1);
+ testFunctionCall(
+ calls.at(0),
+ Mode::MultiLine,
+ "f(((uint256,bytes32),bytes32),uint256)",
+ false,
+ fmt::encodeArgs(),
+ fmt::encodeArgs(),
+ 0,
+ " f(S memory s, uint256 b) "
+ );
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments_recursive_tuples)
+{
+ char const* source = R"(
+ // f(((((bytes, bytes, bytes), bytes), bytes), bytes), bytes) ->
+ // f(((((bytes, bytes, (bytes)), bytes), bytes), (bytes, bytes)), (bytes, bytes)) ->
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 2);
+ testFunctionCall(
+ calls.at(0),
+ Mode::SingleLine,
+ "f(((((bytes,bytes,bytes),bytes),bytes),bytes),bytes)",
+ false
+ );
+ testFunctionCall(
+ calls.at(1),
+ Mode::SingleLine,
+ "f(((((bytes,bytes,(bytes)),bytes),bytes),(bytes,bytes)),(bytes,bytes))",
+ false
+ );
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments_mismatch)
+{
+ char const* source = R"(
+ // f(uint256):
+ // 1, 2
+ // # This only throws at runtime #
+ // -> 1
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 1);
+ testFunctionCall(
+ calls.at(0),
+ Mode::MultiLine,
+ "f(uint256)",
+ false,
+ fmt::encodeArgs(1, 2),
+ fmt::encodeArgs(1),
+ 0,
+ " This only throws at runtime "
+ );
+}
+
+BOOST_AUTO_TEST_CASE(call_multiple_arguments)
+{
+ char const* source = R"(
+ // test(uint256, uint256):
+ // 1,
+ // 2
+ // -> 1,
+ // 1
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 1);
+ testFunctionCall(
+ calls.at(0),
+ Mode::MultiLine,
+ "test(uint256,uint256)",
+ false,
+ fmt::encodeArgs(1, 2),
+ fmt::encodeArgs(1, 1)
+ );
+}
+
+BOOST_AUTO_TEST_CASE(call_multiple_arguments_mixed_format)
+{
+ char const* source = R"(
+ // test(uint256, uint256), 314 ether:
+ // 1, -2
+ // -> -1, 2
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 1);
+ testFunctionCall(
+ calls.at(0),
+ Mode::MultiLine,
+ "test(uint256,uint256)",
+ false,
+ fmt::encodeArgs(1, -2),
+ fmt::encodeArgs(-1, 2),
+ 314
+ );
+}
+
+BOOST_AUTO_TEST_CASE(call_signature)
+{
+ char const* source = R"(
+ // f(uint256, uint8, string) -> FAILURE
+ // f(invalid, xyz, foo) -> FAILURE
+ )";
+ auto const calls = parse(source);
+ BOOST_REQUIRE_EQUAL(calls.size(), 2);
+ testFunctionCall(calls.at(0), Mode::SingleLine, "f(uint256,uint8,string)", true);
+ testFunctionCall(calls.at(1), Mode::SingleLine, "f(invalid,xyz,foo)", true);
+}
+
+BOOST_AUTO_TEST_CASE(call_newline_invalid)
+{
+ char const* source = R"(
+ /
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_invalid)
+{
+ char const* source = R"(
+ / f() ->
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_signature_invalid)
+{
+ char const* source = R"(
+ // f(uint8,) -> FAILURE
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments_tuple_invalid)
+{
+ char const* source = R"(
+ // f((uint8,) -> FAILURE
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments_tuple_invalid_empty)
+{
+ char const* source = R"(
+ // f(uint8, ()) -> FAILURE
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments_tuple_invalid_parantheses)
+{
+ char const* source = R"(
+ // f((uint8,() -> FAILURE
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_expectations_missing)
+{
+ char const* source = R"(
+ // f())";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_ether_value_expectations_missing)
+{
+ char const* source = R"(
+ // f(), 0)";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments_invalid)
+{
+ char const* source = R"(
+ // f(uint256): abc -> 1
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments_invalid_decimal)
+{
+ char const* source = R"(
+ // sig(): 0.h3 ->
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_ether_value_invalid)
+{
+ char const* source = R"(
+ // f(uint256), abc : 1 -> 1
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_ether_value_invalid_decimal)
+{
+ char const* source = R"(
+ // sig(): 0.1hd ether ->
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_ether_type_invalid)
+{
+ char const* source = R"(
+ // f(uint256), 2 btc : 1 -> 1
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments_colon)
+{
+ char const* source = R"(
+ // h256():
+ // -> 1
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_arguments_newline_colon)
+{
+ char const* source = R"(
+ // h256()
+ // :
+ // -> 1
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_CASE(call_arrow_missing)
+{
+ char const* source = R"(
+ // h256()
+ )";
+ BOOST_REQUIRE_THROW(parse(source), langutil::Error);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+}
+}
+}