diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 119104aca..b3b678983 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -84,6 +84,8 @@ set(libsolidity_sources libsolidity/Metadata.cpp libsolidity/MemoryGuardTest.cpp libsolidity/MemoryGuardTest.h + libsolidity/NatspecJSONTest.cpp + libsolidity/NatspecJSONTest.h libsolidity/SemanticTest.cpp libsolidity/SemanticTest.h libsolidity/SemVerMatcher.cpp diff --git a/test/InteractiveTests.h b/test/InteractiveTests.h index 01e47afe0..5021d801b 100644 --- a/test/InteractiveTests.h +++ b/test/InteractiveTests.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -74,6 +75,7 @@ Testsuite const g_interactiveTestsuites[] = { {"Semantic", "libsolidity", "semanticTests", false, true, &SemanticTest::create}, {"JSON AST", "libsolidity", "ASTJSON", false, false, &ASTJSONTest::create}, {"JSON ABI", "libsolidity", "ABIJson", false, false, &ABIJsonTest::create}, + {"JSON Natspec", "libsolidity", "natspecJSON", false, false, &NatspecJSONTest::create}, {"SMT Checker", "libsolidity", "smtCheckerTests", true, false, &SMTCheckerTest::create}, {"Gas Estimates", "libsolidity", "gasTests", false, false, &GasTest::create}, {"Memory Guard", "libsolidity", "memoryGuardTests", false, false, &MemoryGuardTest::create}, diff --git a/test/libsolidity/NatspecJSONTest.cpp b/test/libsolidity/NatspecJSONTest.cpp new file mode 100644 index 000000000..434d5b392 --- /dev/null +++ b/test/libsolidity/NatspecJSONTest.cpp @@ -0,0 +1,210 @@ +/* + 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 the solidity compiler ABI JSON Interface output. + */ + +#include + +#include +#include + +#include + +#include + +#include + +using namespace std; +using namespace solidity::frontend::test; +using namespace solidity::util; + +ostream& solidity::frontend::test::operator<<(ostream& _output, NatspecJSONKind _kind) +{ + switch (_kind) { + case NatspecJSONKind::Devdoc: _output << "devdoc"; break; + case NatspecJSONKind::Userdoc: _output << "userdoc"; break; + } + return _output; +} + +unique_ptr NatspecJSONTest::create(Config const& _config) +{ + return make_unique(_config.filename, _config.evmVersion); +} + +void NatspecJSONTest::parseCustomExpectations(istream& _stream) +{ + soltestAssert(m_expectedNatspecJSON.empty()); + + // We expect a series of expectations in the following format: + // + // // + // // + + string line; + while (getline(_stream, line)) + { + string_view strippedLine = expectLinePrefix(line); + if (strippedLine.empty()) + continue; + + auto [contractName, kind] = parseExpectationHeader(strippedLine); + + string rawJSON = extractExpectationJSON(_stream); + string jsonErrors; + Json::Value parsedJSON; + bool jsonParsingSuccessful = jsonParseStrict(rawJSON, parsedJSON, &jsonErrors); + if (!jsonParsingSuccessful) + BOOST_THROW_EXCEPTION(runtime_error(fmt::format( + "Malformed JSON in {} expectation for contract {}.\n" + "Note that JSON expectations must be pretty-printed to be split correctly. " + "The object is assumed to and at the first unindented closing brace.\n" + "{}", + toString(kind), + contractName, + rawJSON + ))); + + m_expectedNatspecJSON[string(contractName)][kind] = parsedJSON; + } +} + +bool NatspecJSONTest::expectationsMatch() +{ + // NOTE: Comparing pretty printed Json::Values to avoid using its operator==, which fails to + // compare equal numbers as equal. For example, for 'version' field the value is sometimes int, + // sometimes uint and they compare as different even when both are 1. + return + SyntaxTest::expectationsMatch() && + prettyPrinted(obtainedNatspec()) == prettyPrinted(m_expectedNatspecJSON); +} + +void NatspecJSONTest::printExpectedResult(ostream& _stream, string const& _linePrefix, bool _formatted) const +{ + SyntaxTest::printExpectedResult(_stream, _linePrefix, _formatted); + if (!m_expectedNatspecJSON.empty()) + { + _stream << _linePrefix << "----" << endl; + printIndented(_stream, formatNatspecExpectations(m_expectedNatspecJSON), _linePrefix); + } +} + +void NatspecJSONTest::printObtainedResult(ostream& _stream, string const& _linePrefix, bool _formatted) const +{ + SyntaxTest::printObtainedResult(_stream, _linePrefix, _formatted); + + NatspecMap natspecJSON = obtainedNatspec(); + if (!natspecJSON.empty()) + { + _stream << _linePrefix << "----" << endl; + // TODO: Diff both versions and highlight differences. + // We should have a helper for doing that in newly defined test cases without much effort. + printIndented(_stream, formatNatspecExpectations(natspecJSON), _linePrefix); + } +} + +tuple NatspecJSONTest::parseExpectationHeader(string_view _line) +{ + for (NatspecJSONKind kind: {NatspecJSONKind::Devdoc, NatspecJSONKind::Userdoc}) + { + string kindSuffix = " " + toString(kind); + if (boost::algorithm::ends_with(_line, kindSuffix)) + return {_line.substr(0, _line.size() - kindSuffix.size()), kind}; + } + + BOOST_THROW_EXCEPTION(runtime_error( + "Natspec kind (devdoc/userdoc) not present in the expectation: "s.append(_line) + )); +} + +string NatspecJSONTest::extractExpectationJSON(istream& _stream) +{ + string rawJSON; + string line; + while (getline(_stream, line)) + { + string_view strippedLine = expectLinePrefix(line); + rawJSON += strippedLine; + rawJSON += "\n"; + + if (boost::algorithm::starts_with(strippedLine, "}")) + break; + } + + return rawJSON; +} + +string_view NatspecJSONTest::expectLinePrefix(string_view _line) +{ + size_t startPosition = 0; + if (!boost::algorithm::starts_with(_line, "//")) + BOOST_THROW_EXCEPTION(runtime_error( + "Expectation line is not a comment: "s.append(_line) + )); + + startPosition += 2; + if (startPosition < _line.size() && _line[startPosition] == ' ') + ++startPosition; + + return _line.substr(startPosition, _line.size() - startPosition); +} + +string NatspecJSONTest::formatNatspecExpectations(NatspecMap const& _expectations) const +{ + string output; + bool first = true; + // NOTE: Not sorting explicitly because CompilerStack seems to put contracts roughly in the + // order in which they appear in the source, which is much better than alphabetical order. + for (auto const& [contractName, expectationsForAllKinds]: _expectations) + for (auto const& [jsonKind, natspecJSON]: expectationsForAllKinds) + { + if (!first) + output += "\n\n"; + first = false; + + output += contractName + " " + toString(jsonKind) + "\n"; + output += jsonPrint(natspecJSON, {JsonFormat::Pretty, 4}); + } + + return output; +} + +NatspecMap NatspecJSONTest::obtainedNatspec() const +{ + if (compiler().state() < CompilerStack::AnalysisSuccessful) + return {}; + + NatspecMap result; + for (string contractName: compiler().contractNames()) + { + result[contractName][NatspecJSONKind::Devdoc] = compiler().natspecDev(contractName); + result[contractName][NatspecJSONKind::Userdoc] = compiler().natspecUser(contractName); + } + + return result; +} + +SerializedNatspecMap NatspecJSONTest::prettyPrinted(NatspecMap const& _expectations) const +{ + SerializedNatspecMap result; + for (auto const& [contractName, expectationsForAllKinds]: _expectations) + for (auto const& [jsonKind, natspecJSON]: expectationsForAllKinds) + result[contractName][jsonKind] = jsonPrint(natspecJSON, {JsonFormat::Pretty, 4}); + + return result; +} diff --git a/test/libsolidity/NatspecJSONTest.h b/test/libsolidity/NatspecJSONTest.h new file mode 100644 index 000000000..762b88d10 --- /dev/null +++ b/test/libsolidity/NatspecJSONTest.h @@ -0,0 +1,82 @@ +/* + 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 . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * Unit tests for the Natspec userdoc and devdoc JSON output. + */ + +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace solidity::frontend::test +{ + +enum class NatspecJSONKind +{ + Devdoc, + Userdoc, +}; + +std::ostream& operator<<(std::ostream& _output, NatspecJSONKind _kind); + +using NatspecMap = std::map>; +using SerializedNatspecMap = std::map>; + +class NatspecJSONTest: public SyntaxTest +{ +public: + + static std::unique_ptr create(Config const& _config); + + NatspecJSONTest(std::string const& _filename, langutil::EVMVersion _evmVersion): + SyntaxTest( + _filename, + _evmVersion, + langutil::Error::Severity::Error // _minSeverity + ) + {} + +protected: + void parseCustomExpectations(std::istream& _stream) override; + bool expectationsMatch() override; + void printExpectedResult(std::ostream& _stream, std::string const& _linePrefix, bool _formatted) const override; + void printObtainedResult(std::ostream& _stream, std::string const& _linePrefix, bool _formatted) const override; + + NatspecMap m_expectedNatspecJSON; + +private: + static std::tuple parseExpectationHeader(std::string_view _line); + static std::string extractExpectationJSON(std::istream& _stream); + static std::string_view expectLinePrefix(std::string_view _line); + + std::string formatNatspecExpectations(NatspecMap const& _expectations) const; + SerializedNatspecMap prettyPrinted(NatspecMap const& _expectations) const; + NatspecMap obtainedNatspec() const; +}; + +} diff --git a/test/tools/CMakeLists.txt b/test/tools/CMakeLists.txt index 2e57718bd..a86e6382d 100644 --- a/test/tools/CMakeLists.txt +++ b/test/tools/CMakeLists.txt @@ -25,6 +25,7 @@ add_executable(isoltest ../libsolidity/util/TestFunctionCall.cpp ../libsolidity/GasTest.cpp ../libsolidity/MemoryGuardTest.cpp + ../libsolidity/NatspecJSONTest.cpp ../libsolidity/SyntaxTest.cpp ../libsolidity/SemanticTest.cpp ../libsolidity/AnalysisFramework.cpp