From 7fa892eca93cc1b3fd75eb9341c11f0a471970d9 Mon Sep 17 00:00:00 2001 From: Daniel Kirchner Date: Wed, 14 Mar 2018 19:15:48 +0100 Subject: [PATCH] Add interactive test tool isoltest. --- libdevcore/CommonIO.cpp | 49 ++++++ libdevcore/CommonIO.h | 3 + scripts/isoltest.sh | 6 + test/tools/CMakeLists.txt | 3 + test/tools/isoltest.cpp | 346 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 407 insertions(+) create mode 100755 scripts/isoltest.sh create mode 100644 test/tools/isoltest.cpp diff --git a/libdevcore/CommonIO.cpp b/libdevcore/CommonIO.cpp index 8c7e08f6b..6526baf97 100644 --- a/libdevcore/CommonIO.cpp +++ b/libdevcore/CommonIO.cpp @@ -27,6 +27,7 @@ #if defined(_WIN32) #include #else +#include #include #endif #include @@ -118,3 +119,51 @@ void dev::writeFile(std::string const& _file, bytesConstRef _data, bool _writeDe } } } + +#if defined(_WIN32) +class DisableConsoleBuffering +{ +public: + DisableConsoleBuffering() + { + m_stdin = GetStdHandle(STD_INPUT_HANDLE); + GetConsoleMode(m_stdin, &m_oldMode); + SetConsoleMode(m_stdin, m_oldMode & (~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT))); + } + ~DisableConsoleBuffering() + { + SetConsoleMode(m_stdin, m_oldMode); + } +private: + HANDLE m_stdin; + DWORD m_oldMode; +}; +#else +class DisableConsoleBuffering +{ +public: + DisableConsoleBuffering() + { + tcgetattr(0, &m_termios); + m_termios.c_lflag &= ~ICANON; + m_termios.c_lflag &= ~ECHO; + m_termios.c_cc[VMIN] = 1; + m_termios.c_cc[VTIME] = 0; + tcsetattr(0, TCSANOW, &m_termios); + } + ~DisableConsoleBuffering() + { + m_termios.c_lflag |= ICANON; + m_termios.c_lflag |= ECHO; + tcsetattr(0, TCSADRAIN, &m_termios); + } +private: + struct termios m_termios; +}; +#endif + +int dev::readStandardInputChar() +{ + DisableConsoleBuffering disableConsoleBuffering; + return cin.get(); +} diff --git a/libdevcore/CommonIO.h b/libdevcore/CommonIO.h index 33769ec3d..3ecdb4c38 100644 --- a/libdevcore/CommonIO.h +++ b/libdevcore/CommonIO.h @@ -37,6 +37,9 @@ std::string readFileAsString(std::string const& _file); /// Retrieve and returns the contents of standard input (until EOF). std::string readStandardInput(); +/// Retrieve and returns a character from standard input (without waiting for EOL). +int readStandardInputChar(); + /// Write the given binary data into the given file, replacing the file if it pre-exists. /// Throws exception on error. /// @param _writeDeleteRename useful not to lose any data: If set, first writes to another file in diff --git a/scripts/isoltest.sh b/scripts/isoltest.sh new file mode 100755 index 000000000..8aed0b3aa --- /dev/null +++ b/scripts/isoltest.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e + +REPO_ROOT="$(dirname "$0")"/.. +exec ${REPO_ROOT}/build/test/tools/isoltest --testpath ${REPO_ROOT}/test diff --git a/test/tools/CMakeLists.txt b/test/tools/CMakeLists.txt index a693ebab4..febb0c26b 100644 --- a/test/tools/CMakeLists.txt +++ b/test/tools/CMakeLists.txt @@ -1,2 +1,5 @@ add_executable(solfuzzer fuzzer.cpp) target_link_libraries(solfuzzer PRIVATE libsolc evmasm ${Boost_PROGRAM_OPTIONS_LIBRARIES}) + +add_executable(isoltest isoltest.cpp ../Options.cpp ../libsolidity/SyntaxTest.cpp ../libsolidity/AnalysisFramework.cpp) +target_link_libraries(isoltest PRIVATE libsolc solidity evmasm ${Boost_PROGRAM_OPTIONS_LIBRARIES} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARIES}) diff --git a/test/tools/isoltest.cpp b/test/tools/isoltest.cpp new file mode 100644 index 000000000..668481cf8 --- /dev/null +++ b/test/tools/isoltest.cpp @@ -0,0 +1,346 @@ +/* + 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 +#include + +using namespace dev; +using namespace dev::solidity; +using namespace dev::solidity::test; +using namespace dev::solidity::test::formatting; +using namespace std; +namespace po = boost::program_options; +namespace fs = boost::filesystem; + +struct SyntaxTestStats +{ + int successCount; + int runCount; + operator bool() const { return successCount == runCount; } +}; + +class SyntaxTestTool +{ +public: + SyntaxTestTool(string const& _name, fs::path const& _path, bool _formatted): + m_formatted(_formatted), m_name(_name), m_path(_path) + {} + + enum class Result + { + Success, + Failure, + ParserError, + InputOutputError + }; + + Result process(); + + static SyntaxTestStats processPath( + fs::path const& _basepath, + fs::path const& _path, + bool const _formatted + ); + + static string editor; +private: + enum class Request + { + Skip, + Rerun, + Quit + }; + + Request handleResponse(bool const _parserError); + + void printContract() const; + + bool const m_formatted; + string const m_name; + fs::path const m_path; + unique_ptr m_test; +}; + +string SyntaxTestTool::editor; + +void SyntaxTestTool::printContract() const +{ + stringstream stream(m_test->source()); + string line; + while (getline(stream, line)) + cout << " " << line << endl; + cout << endl; +} + +SyntaxTestTool::Result SyntaxTestTool::process() +{ + bool success; + bool parserError = false; + std::stringstream outputMessages; + + (FormattedScope(cout, m_formatted, {BOLD}) << m_name << ": ").flush(); + + try + { + m_test = unique_ptr(new SyntaxTest(m_path.string())); + } + catch (std::exception const& _e) + { + FormattedScope(cout, m_formatted, {BOLD, RED}) << "cannot read test: " << _e.what() << endl; + return Result::InputOutputError; + } + + try + { + success = m_test->run(outputMessages, " ", m_formatted); + } + catch (...) + { + success = false; + parserError = true; + } + + if (success) + { + FormattedScope(cout, m_formatted, {BOLD, GREEN}) << "OK" << endl; + return Result::Success; + } + else + { + FormattedScope(cout, m_formatted, {BOLD, RED}) << "FAIL" << endl; + + FormattedScope(cout, m_formatted, {BOLD, CYAN}) << " Contract:" << endl; + printContract(); + + if (parserError) + { + cout << " "; + FormattedScope(cout, m_formatted, {INVERSE, RED}) << "Parsing failed:" << endl; + m_test->printErrorList(cout, m_test->compilerErrors(), " ", true, true, m_formatted); + cout << endl; + return Result::ParserError; + } + else + { + cout << outputMessages.str() << endl; + return Result::Failure; + } + } +} + +SyntaxTestTool::Request SyntaxTestTool::handleResponse(bool const _parserError) +{ + if (_parserError) + cout << "(e)dit/(s)kip/(q)uit? "; + else + cout << "(e)dit/(u)pdate expectations/(s)kip/(q)uit? "; + cout.flush(); + + while (true) + { + switch(readStandardInputChar()) + { + case 's': + cout << endl; + return Request::Skip; + case 'u': + if (_parserError) + break; + else + { + cout << endl; + ofstream file(m_path.string(), ios::trunc); + file << m_test->source(); + file << "// ----" << endl; + if (!m_test->errorList().empty()) + m_test->printErrorList(file, m_test->errorList(), "// ", false, false, false); + return Request::Rerun; + } + case 'e': + cout << endl << endl; + if (system((editor + " \"" + m_path.string() + "\"").c_str())) + cerr << "Error running editor command." << endl << endl; + return Request::Rerun; + case 'q': + cout << endl; + return Request::Quit; + default: + break; + } + } +} + + +SyntaxTestStats SyntaxTestTool::processPath( + fs::path const& _basepath, + fs::path const& _path, + bool const _formatted +) +{ + std::queue paths; + paths.push(_path); + int successCount = 0; + int runCount = 0; + + while (!paths.empty()) + { + auto currentPath = paths.front(); + + fs::path fullpath = _basepath / currentPath; + if (fs::is_directory(fullpath)) + { + paths.pop(); + for (auto const& entry: boost::iterator_range( + fs::directory_iterator(fullpath), + fs::directory_iterator() + )) + paths.push(currentPath / entry.path().filename()); + } + else + { + SyntaxTestTool testTool(currentPath.string(), fullpath, _formatted); + ++runCount; + auto result = testTool.process(); + + switch(result) + { + case Result::Failure: + case Result::ParserError: + switch(testTool.handleResponse(result == Result::ParserError)) + { + case Request::Quit: + return { successCount, runCount }; + case Request::Rerun: + cout << "Re-running test case..." << endl; + --runCount; + break; + case Request::Skip: + paths.pop(); + break; + } + break; + case Result::Success: + paths.pop(); + ++successCount; + break; + default: + // non-recoverable error; continue with next test case + paths.pop(); + break; + } + } + } + + return { successCount, runCount }; + +} + +int main(int argc, char *argv[]) +{ + if (getenv("EDITOR")) + SyntaxTestTool::editor = getenv("EDITOR"); + + fs::path testPath; + bool formatted = true; + po::options_description options( + R"(isoltest, tool for interactively managing test contracts. +Usage: isoltest [Options] --testpath path +Interactively validates test contracts. + +Allowed options)", + po::options_description::m_default_line_length, + po::options_description::m_default_line_length - 23); + options.add_options() + ("help", "Show this help screen.") + ("testpath", po::value(&testPath), "path to test files") + ("no-color", "don't use colors") + ("editor", po::value(&SyntaxTestTool::editor), "editor for opening contracts"); + + po::variables_map arguments; + try + { + po::command_line_parser cmdLineParser(argc, argv); + cmdLineParser.options(options); + po::store(cmdLineParser.run(), arguments); + + if (arguments.count("help")) + { + cout << options << endl; + return 0; + } + + if (arguments.count("no-color")) + formatted = false; + + po::notify(arguments); + } + catch (po::error const& _exception) + { + cerr << _exception.what() << endl; + return 1; + } + + if (testPath.empty()) + { + auto const searchPath = + { + fs::current_path() / ".." / ".." / ".." / "test", + fs::current_path() / ".." / ".." / "test", + fs::current_path() / ".." / "test", + fs::current_path() / "test", + fs::current_path() + }; + for (auto const& basePath : searchPath) + { + fs::path syntaxTestPath = basePath / "libsolidity" / "syntaxTests"; + if (fs::exists(syntaxTestPath) && fs::is_directory(syntaxTestPath)) + { + testPath = basePath; + break; + } + } + } + + fs::path syntaxTestPath = testPath / "libsolidity" / "syntaxTests"; + + if (fs::exists(syntaxTestPath) && fs::is_directory(syntaxTestPath)) + { + auto stats = SyntaxTestTool::processPath(testPath / "libsolidity", "syntaxTests", formatted); + + cout << endl << "Summary: "; + FormattedScope(cout, formatted, {BOLD, stats ? GREEN : RED}) << + stats.successCount << "/" << stats.runCount; + cout << " tests successful." << endl; + + return stats ? 0 : 1; + } + else + { + cerr << "Test path not found. Use the --testpath argument." << endl; + return 1; + } +}