mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
Merge pull request #11544 from ethereum/standard-json-cli-common-input-file-processing
Common input file processing for CLI and Standard JSON
This commit is contained in:
commit
de2e72b868
@ -15,6 +15,8 @@ Bugfixes:
|
||||
* Code Generator: Fix crash when passing an empty string literal to ``bytes.concat()``.
|
||||
* Code Generator: Fix internal compiler error when calling functions bound to calldata structs and arrays.
|
||||
* Code Generator: Fix internal compiler error when passing a 32-byte hex literal or a zero literal to ``bytes.concat()`` by disallowing such literals.
|
||||
* Commandline Interface: Fix crash when a directory path is passed to ``--standard-json``.
|
||||
* Commandline Interface: Read JSON from standard input when ``--standard-json`` gets ``-`` as a file name.
|
||||
* Standard JSON: Include source location for errors in files with empty name.
|
||||
* Type Checker: Fix internal error and prevent static calls to unimplemented modifiers.
|
||||
* Yul Code Generator: Fix internal compiler error when using a long literal with bitwise negation.
|
||||
|
@ -377,8 +377,18 @@ void CommandLineInterface::handleGasEstimation(string const& _contract)
|
||||
}
|
||||
}
|
||||
|
||||
bool CommandLineInterface::readInputFilesAndConfigureFileReader()
|
||||
bool CommandLineInterface::readInputFiles()
|
||||
{
|
||||
solAssert(!m_standardJsonInput.has_value(), "");
|
||||
|
||||
m_fileReader.setBasePath(m_options.input.basePath);
|
||||
|
||||
if (m_fileReader.basePath() != "" && !boost::filesystem::is_directory(m_fileReader.basePath()))
|
||||
{
|
||||
serr() << "Base path must be a directory: " << m_fileReader.basePath() << endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (boost::filesystem::path const& allowedDirectory: m_options.input.allowedDirectories)
|
||||
m_fileReader.allowDirectory(allowedDirectory);
|
||||
|
||||
@ -411,16 +421,33 @@ bool CommandLineInterface::readInputFilesAndConfigureFileReader()
|
||||
}
|
||||
|
||||
// NOTE: we ignore the FileNotFound exception as we manually check above
|
||||
m_fileReader.setSource(infile, readFileAsString(infile.string()));
|
||||
m_fileReader.allowDirectory(boost::filesystem::path(boost::filesystem::canonical(infile).string()).remove_filename());
|
||||
string fileContent = readFileAsString(infile.string());
|
||||
if (m_options.input.mode == InputMode::StandardJson)
|
||||
{
|
||||
solAssert(!m_standardJsonInput.has_value(), "");
|
||||
m_standardJsonInput = move(fileContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_fileReader.setSource(infile, move(fileContent));
|
||||
m_fileReader.allowDirectory(boost::filesystem::canonical(infile).remove_filename());
|
||||
}
|
||||
}
|
||||
|
||||
if (m_options.input.addStdin)
|
||||
m_fileReader.setSource(g_stdinFileName, readUntilEnd(m_sin));
|
||||
|
||||
if (m_fileReader.sourceCodes().size() == 0)
|
||||
{
|
||||
serr() << "No input files given. If you wish to use the standard input please specify \"-\" explicitly." << endl;
|
||||
if (m_options.input.mode == InputMode::StandardJson)
|
||||
{
|
||||
solAssert(!m_standardJsonInput.has_value(), "");
|
||||
m_standardJsonInput = readUntilEnd(m_sin);
|
||||
}
|
||||
else
|
||||
m_fileReader.setSource(g_stdinFileName, readUntilEnd(m_sin));
|
||||
}
|
||||
|
||||
if (m_fileReader.sourceCodes().empty() && !m_standardJsonInput.has_value())
|
||||
{
|
||||
serr() << "All specified input files either do not exist or are not regular files." << endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -502,57 +529,35 @@ bool CommandLineInterface::parseArguments(int _argc, char const* const* _argv)
|
||||
|
||||
bool CommandLineInterface::processInput()
|
||||
{
|
||||
m_fileReader.setBasePath(m_options.input.basePath);
|
||||
|
||||
if (m_options.input.basePath != "" && !boost::filesystem::is_directory(m_options.input.basePath))
|
||||
switch (m_options.input.mode)
|
||||
{
|
||||
serr() << "Base path must be a directory: \"" << m_options.input.basePath << "\"\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_options.input.mode == InputMode::StandardJson)
|
||||
case InputMode::StandardJson:
|
||||
{
|
||||
string input;
|
||||
if (m_options.input.standardJsonFile.empty())
|
||||
input = readUntilEnd(m_sin);
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
input = readFileAsString(m_options.input.standardJsonFile);
|
||||
}
|
||||
catch (FileNotFound const&)
|
||||
{
|
||||
serr() << "File not found: " << m_options.input.standardJsonFile << endl;
|
||||
return false;
|
||||
}
|
||||
catch (NotAFile const&)
|
||||
{
|
||||
serr() << "Not a regular file: " << m_options.input.standardJsonFile << endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
solAssert(m_standardJsonInput.has_value(), "");
|
||||
|
||||
StandardCompiler compiler(m_fileReader.reader(), m_options.formatting.json);
|
||||
sout() << compiler.compile(std::move(input)) << endl;
|
||||
sout() << compiler.compile(move(m_standardJsonInput.value())) << endl;
|
||||
m_standardJsonInput.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!readInputFilesAndConfigureFileReader())
|
||||
return false;
|
||||
|
||||
if (m_options.input.mode == InputMode::Assembler)
|
||||
case InputMode::Assembler:
|
||||
{
|
||||
return assemble(
|
||||
m_options.assembly.inputLanguage,
|
||||
m_options.assembly.targetMachine,
|
||||
m_options.optimizer.enabled,
|
||||
m_options.optimizer.yulSteps
|
||||
);
|
||||
|
||||
if (m_options.input.mode == InputMode::Linker)
|
||||
}
|
||||
case InputMode::Linker:
|
||||
return link();
|
||||
case InputMode::Compiler:
|
||||
case InputMode::CompilerWithASTImport:
|
||||
return compile();
|
||||
}
|
||||
|
||||
solAssert(m_options.input.mode == InputMode::Compiler || m_options.input.mode == InputMode::CompilerWithASTImport, "");
|
||||
return compile();
|
||||
solAssert(false, "");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CommandLineInterface::compile()
|
||||
|
@ -53,6 +53,8 @@ public:
|
||||
|
||||
/// Parse command line arguments and return false if we should not continue
|
||||
bool parseArguments(int _argc, char const* const* _argv);
|
||||
/// Read the content of all input files and initialize the file reader.
|
||||
bool readInputFiles();
|
||||
/// Parse the files and create source code objects
|
||||
bool processInput();
|
||||
/// Perform actions on the input depending on provided compiler arguments
|
||||
@ -61,6 +63,7 @@ public:
|
||||
|
||||
CommandLineOptions const& options() const { return m_options; }
|
||||
FileReader const& fileReader() const { return m_fileReader; }
|
||||
std::optional<std::string> const& standardJsonInput() const { return m_standardJsonInput; }
|
||||
|
||||
private:
|
||||
bool compile();
|
||||
@ -95,10 +98,6 @@ private:
|
||||
void handleGasEstimation(std::string const& _contract);
|
||||
void handleStorageLayout(std::string const& _contract);
|
||||
|
||||
/// Reads the content of input files specified on the command line and passes them to FileReader.
|
||||
/// @return false if there are no input files or input files cannot be read.
|
||||
bool readInputFilesAndConfigureFileReader();
|
||||
|
||||
/// Tries to read @ m_sourceCodes as a JSONs holding ASTs
|
||||
/// such that they can be imported into the compiler (importASTs())
|
||||
/// (produced by --combined-json ast,compact-format <file.sol>
|
||||
@ -129,6 +128,7 @@ private:
|
||||
bool m_hasOutput = false;
|
||||
bool m_error = false; ///< If true, some error occurred.
|
||||
FileReader m_fileReader;
|
||||
std::optional<std::string> m_standardJsonInput;
|
||||
std::unique_ptr<frontend::CompilerStack> m_compiler;
|
||||
CommandLineOptions m_options;
|
||||
};
|
||||
|
@ -198,14 +198,14 @@ void CommandLineParser::printLicenseAndExit()
|
||||
}
|
||||
|
||||
|
||||
bool CommandLineParser::checkMutuallyExclusive(boost::program_options::variables_map const& args, string const& _optionA, string const& _optionB)
|
||||
bool CommandLineParser::checkMutuallyExclusive(vector<string> const& _optionNames)
|
||||
{
|
||||
if (args.count(_optionA) && args.count(_optionB))
|
||||
if (countEnabledOptions(_optionNames) > 1)
|
||||
{
|
||||
serr() << "Option " << _optionA << " and " << _optionB << " are mutually exclusive." << endl;
|
||||
serr() << "The following options are mutually exclusive: " << joinOptionNames(_optionNames) << ". ";
|
||||
serr() << "Select at most one." << endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -265,7 +265,6 @@ bool CommandLineOptions::operator==(CommandLineOptions const& _other) const noex
|
||||
{
|
||||
return
|
||||
input.paths == _other.input.paths &&
|
||||
input.standardJsonFile == _other.input.standardJsonFile &&
|
||||
input.remappings == _other.input.remappings &&
|
||||
input.addStdin == _other.input.addStdin &&
|
||||
input.basePath == _other.input.basePath &&
|
||||
@ -301,12 +300,20 @@ bool CommandLineOptions::operator==(CommandLineOptions const& _other) const noex
|
||||
bool CommandLineParser::parseInputPathsAndRemappings()
|
||||
{
|
||||
m_options.input.ignoreMissingFiles = (m_args.count(g_strIgnoreMissingFiles) > 0);
|
||||
|
||||
if (m_args.count(g_strInputFile))
|
||||
for (string path: m_args[g_strInputFile].as<vector<string>>())
|
||||
{
|
||||
auto eq = find(path.begin(), path.end(), '=');
|
||||
if (eq != path.end())
|
||||
{
|
||||
if (m_options.input.mode == InputMode::StandardJson)
|
||||
{
|
||||
serr() << "Import remappings are not accepted on the command line in Standard JSON mode." << endl;
|
||||
serr() << "Please put them under 'settings.remappings' in the JSON input." << endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (auto r = ImportRemapper::parseRemapping(path))
|
||||
m_options.input.remappings.emplace_back(std::move(*r));
|
||||
else
|
||||
@ -324,6 +331,25 @@ bool CommandLineParser::parseInputPathsAndRemappings()
|
||||
m_options.input.paths.insert(path);
|
||||
}
|
||||
|
||||
if (m_options.input.mode == InputMode::StandardJson)
|
||||
{
|
||||
if (m_options.input.paths.size() > 1 || (m_options.input.paths.size() == 1 && m_options.input.addStdin))
|
||||
{
|
||||
serr() << "Too many input files for --" << g_strStandardJSON << "." << endl;
|
||||
serr() << "Please either specify a single file name or provide its content on standard input." << endl;
|
||||
return false;
|
||||
}
|
||||
else if (m_options.input.paths.size() == 0)
|
||||
// Standard JSON mode input used to be handled separately and zero files meant "read from stdin".
|
||||
// Keep it working that way for backwards-compatibility.
|
||||
m_options.input.addStdin = true;
|
||||
}
|
||||
else if (m_options.input.paths.size() == 0 && !m_options.input.addStdin)
|
||||
{
|
||||
serr() << "No input files given. If you wish to use the standard input please specify \"-\" explicitly." << endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -735,7 +761,7 @@ General Information)").c_str(),
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!checkMutuallyExclusive(m_args, g_strColor, g_strNoColor))
|
||||
if (!checkMutuallyExclusive({g_strColor, g_strNoColor}))
|
||||
return false;
|
||||
|
||||
array<string, 8> const conflictingWithStopAfter{
|
||||
@ -750,7 +776,7 @@ General Information)").c_str(),
|
||||
};
|
||||
|
||||
for (auto& option: conflictingWithStopAfter)
|
||||
if (!checkMutuallyExclusive(m_args, g_strStopAfter, option))
|
||||
if (!checkMutuallyExclusive({g_strStopAfter, option}))
|
||||
return false;
|
||||
|
||||
if (m_args.count(g_strColor) > 0)
|
||||
@ -861,42 +887,33 @@ General Information)").c_str(),
|
||||
m_options.output.stopAfter = CompilerStack::State::Parsed;
|
||||
}
|
||||
|
||||
vector<string> const exclusiveModes = {
|
||||
if (!checkMutuallyExclusive({
|
||||
g_strStandardJSON,
|
||||
g_strLink,
|
||||
g_strAssemble,
|
||||
g_strStrictAssembly,
|
||||
g_strYul,
|
||||
g_strImportAst,
|
||||
};
|
||||
if (countEnabledOptions(exclusiveModes) > 1)
|
||||
{
|
||||
serr() << "The following options are mutually exclusive: " << joinOptionNames(exclusiveModes) << ". ";
|
||||
serr() << "Select at most one." << endl;
|
||||
}))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_args.count(g_strStandardJSON))
|
||||
{
|
||||
if (m_args.count(g_strStandardJSON) > 0)
|
||||
m_options.input.mode = InputMode::StandardJson;
|
||||
|
||||
vector<string> inputFiles;
|
||||
if (m_args.count(g_strInputFile))
|
||||
inputFiles = m_args[g_strInputFile].as<vector<string>>();
|
||||
if (inputFiles.size() == 1)
|
||||
m_options.input.standardJsonFile = inputFiles[0];
|
||||
else if (inputFiles.size() > 1)
|
||||
{
|
||||
serr() << "If --" << g_strStandardJSON << " is used, only zero or one input files are supported." << endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (m_args.count(g_strAssemble) > 0 || m_args.count(g_strStrictAssembly) > 0 || m_args.count(g_strYul) > 0)
|
||||
m_options.input.mode = InputMode::Assembler;
|
||||
else if (m_args.count(g_strLink) > 0)
|
||||
m_options.input.mode = InputMode::Linker;
|
||||
else if (m_args.count(g_strImportAst) > 0)
|
||||
m_options.input.mode = InputMode::CompilerWithASTImport;
|
||||
else
|
||||
m_options.input.mode = InputMode::Compiler;
|
||||
|
||||
if (!parseInputPathsAndRemappings())
|
||||
return false;
|
||||
|
||||
if (m_options.input.mode == InputMode::StandardJson)
|
||||
return true;
|
||||
|
||||
if (m_args.count(g_strLibraries))
|
||||
for (string const& library: m_args[g_strLibraries].as<vector<string>>())
|
||||
if (!parseLibraryOption(library))
|
||||
@ -914,10 +931,8 @@ General Information)").c_str(),
|
||||
m_options.output.evmVersion = *versionOption;
|
||||
}
|
||||
|
||||
if (m_args.count(g_strAssemble) || m_args.count(g_strStrictAssembly) || m_args.count(g_strYul))
|
||||
if (m_options.input.mode == InputMode::Assembler)
|
||||
{
|
||||
m_options.input.mode = InputMode::Assembler;
|
||||
|
||||
vector<string> const nonAssemblyModeOptions = {
|
||||
// TODO: The list is not complete. Add more.
|
||||
g_strOutputDir,
|
||||
@ -1031,11 +1046,8 @@ General Information)").c_str(),
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_args.count(g_strLink))
|
||||
{
|
||||
m_options.input.mode = InputMode::Linker;
|
||||
if (m_options.input.mode == InputMode::Linker)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_args.count(g_strMetadataHash))
|
||||
{
|
||||
@ -1128,9 +1140,7 @@ General Information)").c_str(),
|
||||
m_options.optimizer.yulSteps = m_args[g_strYulOptimizations].as<string>();
|
||||
}
|
||||
|
||||
if (m_args.count(g_strImportAst) > 0)
|
||||
m_options.input.mode = InputMode::CompilerWithASTImport;
|
||||
else
|
||||
if (m_options.input.mode == InputMode::Compiler)
|
||||
m_options.input.errorRecovery = (m_args.count(g_strErrorRecovery) > 0);
|
||||
|
||||
solAssert(m_options.input.mode == InputMode::Compiler || m_options.input.mode == InputMode::CompilerWithASTImport, "");
|
||||
|
@ -107,7 +107,6 @@ struct CommandLineOptions
|
||||
{
|
||||
InputMode mode = InputMode::Compiler;
|
||||
std::set<boost::filesystem::path> paths;
|
||||
std::string standardJsonFile;
|
||||
std::vector<ImportRemapper::Remapping> remappings;
|
||||
bool addStdin = false;
|
||||
boost::filesystem::path basePath = "";
|
||||
@ -218,11 +217,7 @@ private:
|
||||
/// @return false if there are any validation errors, true otherwise.
|
||||
bool parseLibraryOption(std::string const& _input);
|
||||
|
||||
bool checkMutuallyExclusive(
|
||||
boost::program_options::variables_map const& args,
|
||||
std::string const& _optionA,
|
||||
std::string const& _optionB
|
||||
);
|
||||
bool checkMutuallyExclusive(std::vector<std::string> const& _optionNames);
|
||||
[[noreturn]] void printVersionAndExit();
|
||||
[[noreturn]] void printLicenseAndExit();
|
||||
size_t countEnabledOptions(std::vector<std::string> const& _optionNames) const;
|
||||
|
@ -57,6 +57,8 @@ int main(int argc, char** argv)
|
||||
solidity::frontend::CommandLineInterface cli(cin, cout, cerr);
|
||||
if (!cli.parseArguments(argc, argv))
|
||||
return 1;
|
||||
if (!cli.readInputFiles())
|
||||
return 1;
|
||||
if (!cli.processInput())
|
||||
return 1;
|
||||
bool success = false;
|
||||
|
@ -152,6 +152,9 @@ set(libyul_sources
|
||||
detect_stray_source_files("${libyul_sources}" "libyul/")
|
||||
|
||||
set(solcli_sources
|
||||
solc/Common.cpp
|
||||
solc/Common.h
|
||||
solc/CommandLineInterface.cpp
|
||||
solc/CommandLineParser.cpp
|
||||
)
|
||||
detect_stray_source_files("${solcli_sources}" "solc/")
|
||||
|
@ -24,6 +24,21 @@ using namespace std;
|
||||
using namespace solidity;
|
||||
using namespace solidity::test;
|
||||
|
||||
void solidity::test::createFilesWithParentDirs(set<boost::filesystem::path> const& _paths, string const& _content)
|
||||
{
|
||||
for (boost::filesystem::path const& path: _paths)
|
||||
{
|
||||
if (!path.parent_path().empty())
|
||||
boost::filesystem::create_directories(path.parent_path());
|
||||
|
||||
ofstream newFile(path.string());
|
||||
newFile << _content;
|
||||
|
||||
if (newFile.fail() || !boost::filesystem::exists(path))
|
||||
BOOST_THROW_EXCEPTION(runtime_error("Failed to create an empty file: \"" + path.string() + "\"."));
|
||||
}
|
||||
}
|
||||
|
||||
void solidity::test::createFileWithContent(boost::filesystem::path const& _path, string const& content)
|
||||
{
|
||||
if (boost::filesystem::is_regular_file(_path))
|
||||
|
@ -23,11 +23,16 @@
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
namespace solidity::test
|
||||
{
|
||||
|
||||
/// Creates all the specified files and fills them with the specifiedcontent. Creates their parent
|
||||
/// directories if they do not exist. Throws an exception if any part of the operation does not succeed.
|
||||
void createFilesWithParentDirs(std::set<boost::filesystem::path> const& _paths, std::string const& _content = "");
|
||||
|
||||
/// Creates a file with the exact content specified in the second argument.
|
||||
/// Throws an exception if the file already exists or if the parent directory of the file does not.
|
||||
void createFileWithContent(boost::filesystem::path const& _path, std::string const& content);
|
||||
|
@ -1 +1 @@
|
||||
File not found: input.sol
|
||||
"input.sol" is not found.
|
||||
|
@ -215,7 +215,7 @@ ostream& operator<<(ostream& _out, map<string, set<string>> const& _map)
|
||||
namespace boost::test_tools::tt_detail
|
||||
{
|
||||
|
||||
// Boost won't find find the << operator unless we put it in the std namespace which is illegal.
|
||||
// Boost won't find the << operator unless we put it in the std namespace which is illegal.
|
||||
// The recommended solution is to overload print_log_value<> struct and make it use our operator.
|
||||
|
||||
template<>
|
||||
|
346
test/solc/CommandLineInterface.cpp
Normal file
346
test/solc/CommandLineInterface.cpp
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
|
||||
/// Unit tests for solc/CommandLineInterface.h
|
||||
|
||||
#include <solc/CommandLineInterface.h>
|
||||
|
||||
#include <test/solc/Common.h>
|
||||
|
||||
#include <test/Common.h>
|
||||
#include <test/libsolidity/util/SoltestErrors.h>
|
||||
#include <test/FilesystemUtils.h>
|
||||
#include <test/TemporaryDirectory.h>
|
||||
|
||||
#include <range/v3/view/transform.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <ostream>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
using namespace solidity::frontend;
|
||||
using namespace solidity::test;
|
||||
|
||||
using PathSet = set<boost::filesystem::path>;
|
||||
|
||||
#define TEST_CASE_NAME (boost::unit_test::framework::current_test_case().p_name)
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
ostream& operator<<(ostream& _out, vector<ImportRemapper::Remapping> const& _remappings)
|
||||
{
|
||||
static auto remappingToString = [](auto const& _remapping)
|
||||
{
|
||||
return _remapping.context + ":" + _remapping.prefix + "=" + _remapping.target;
|
||||
};
|
||||
|
||||
_out << "[" << joinHumanReadable(_remappings | ranges::views::transform(remappingToString)) << "]";
|
||||
return _out;
|
||||
}
|
||||
|
||||
ostream& operator<<(ostream& _out, map<string, string> const& _map)
|
||||
{
|
||||
_out << "{" << endl;
|
||||
for (auto const& [key, value]: _map)
|
||||
_out << "" << key << ": " << value << "," << endl;
|
||||
_out << "}";
|
||||
|
||||
return _out;
|
||||
}
|
||||
|
||||
ostream& operator<<(ostream& _out, PathSet const& _paths)
|
||||
{
|
||||
static auto pathString = [](auto const& _path) { return _path.string(); };
|
||||
|
||||
_out << "{" << joinHumanReadable(_paths | ranges::views::transform(pathString)) << "}";
|
||||
return _out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace boost::test_tools::tt_detail
|
||||
{
|
||||
|
||||
// Boost won't find the << operator unless we put it in the std namespace which is illegal.
|
||||
// The recommended solution is to overload print_log_value<> struct and make it use our operator.
|
||||
|
||||
template<>
|
||||
struct print_log_value<vector<ImportRemapper::Remapping>>
|
||||
{
|
||||
void operator()(std::ostream& _out, vector<ImportRemapper::Remapping> const& _value) { ::operator<<(_out, _value); }
|
||||
};
|
||||
|
||||
template<>
|
||||
struct print_log_value<map<string, string>>
|
||||
{
|
||||
void operator()(std::ostream& _out, map<string, string> const& _value) { ::operator<<(_out, _value); }
|
||||
};
|
||||
|
||||
template<>
|
||||
struct print_log_value<PathSet>
|
||||
{
|
||||
void operator()(std::ostream& _out, PathSet const& _value) { ::operator<<(_out, _value); }
|
||||
};
|
||||
|
||||
} // namespace boost::test_tools::tt_detail
|
||||
|
||||
namespace solidity::frontend::test
|
||||
{
|
||||
|
||||
BOOST_AUTO_TEST_SUITE(CommandLineInterfaceTest)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(multiple_input_modes)
|
||||
{
|
||||
array<string, 6> inputModeOptions = {
|
||||
"--standard-json",
|
||||
"--link",
|
||||
"--assemble",
|
||||
"--strict-assembly",
|
||||
"--yul",
|
||||
"--import-ast",
|
||||
};
|
||||
string expectedMessage =
|
||||
"The following options are mutually exclusive: "
|
||||
"--standard-json, --link, --assemble, --strict-assembly, --yul, --import-ast. "
|
||||
"Select at most one.\n";
|
||||
|
||||
for (string const& mode1: inputModeOptions)
|
||||
for (string const& mode2: inputModeOptions)
|
||||
if (mode1 != mode2)
|
||||
{
|
||||
vector<string> commandLine = {"solc", mode1, mode2};
|
||||
OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles(commandLine);
|
||||
BOOST_TEST(!result.success);
|
||||
BOOST_TEST(result.stderrContent == expectedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(cli_input)
|
||||
{
|
||||
TemporaryDirectory tempDir1(TEST_CASE_NAME);
|
||||
TemporaryDirectory tempDir2(TEST_CASE_NAME);
|
||||
createFilesWithParentDirs({tempDir1.path() / "input1.sol"});
|
||||
createFilesWithParentDirs({tempDir2.path() / "input2.sol"});
|
||||
|
||||
vector<ImportRemapper::Remapping> expectedRemappings = {
|
||||
{"", "a", "b/c/d"},
|
||||
{"a", "b", "c/d/e/"},
|
||||
};
|
||||
map<string, string> expectedSources = {
|
||||
{"<stdin>", "\n"},
|
||||
{(tempDir1.path() / "input1.sol").generic_string(), ""},
|
||||
{(tempDir2.path() / "input2.sol").generic_string(), ""},
|
||||
};
|
||||
PathSet expectedAllowedPaths = {
|
||||
boost::filesystem::canonical(tempDir1.path()),
|
||||
boost::filesystem::canonical(tempDir2.path()),
|
||||
"b/c",
|
||||
"c/d/e",
|
||||
};
|
||||
|
||||
OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles({
|
||||
"solc",
|
||||
"a=b/c/d",
|
||||
(tempDir1.path() / "input1.sol").string(),
|
||||
(tempDir2.path() / "input2.sol").string(),
|
||||
"a:b=c/d/e/",
|
||||
"-",
|
||||
});
|
||||
|
||||
BOOST_TEST(result.success);
|
||||
BOOST_TEST(result.stderrContent == "");
|
||||
BOOST_TEST(result.options.input.mode == InputMode::Compiler);
|
||||
BOOST_TEST(result.options.input.addStdin);
|
||||
BOOST_CHECK_EQUAL(result.options.input.remappings, expectedRemappings);
|
||||
BOOST_CHECK_EQUAL(result.reader.sourceCodes(), expectedSources);
|
||||
BOOST_CHECK_EQUAL(result.reader.allowedDirectories(), expectedAllowedPaths);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(cli_ignore_missing_some_files_exist)
|
||||
{
|
||||
TemporaryDirectory tempDir1(TEST_CASE_NAME);
|
||||
TemporaryDirectory tempDir2(TEST_CASE_NAME);
|
||||
createFilesWithParentDirs({tempDir1.path() / "input1.sol"});
|
||||
|
||||
// NOTE: Allowed paths should not be added for skipped files.
|
||||
map<string, string> expectedSources = {{(tempDir1.path() / "input1.sol").generic_string(), ""}};
|
||||
PathSet expectedAllowedPaths = {boost::filesystem::canonical(tempDir1.path())};
|
||||
|
||||
OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles({
|
||||
"solc",
|
||||
(tempDir1.path() / "input1.sol").string(),
|
||||
(tempDir2.path() / "input2.sol").string(),
|
||||
"--ignore-missing",
|
||||
});
|
||||
BOOST_TEST(result.success);
|
||||
BOOST_TEST(result.stderrContent == "\"" + (tempDir2.path() / "input2.sol").string() + "\" is not found. Skipping.\n");
|
||||
BOOST_TEST(result.options.input.mode == InputMode::Compiler);
|
||||
BOOST_TEST(!result.options.input.addStdin);
|
||||
BOOST_CHECK_EQUAL(result.reader.sourceCodes(), expectedSources);
|
||||
BOOST_CHECK_EQUAL(result.reader.allowedDirectories(), expectedAllowedPaths);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(cli_ignore_missing_no_files_exist)
|
||||
{
|
||||
TemporaryDirectory tempDir(TEST_CASE_NAME);
|
||||
|
||||
string expectedMessage =
|
||||
"\"" + (tempDir.path() / "input1.sol").string() + "\" is not found. Skipping.\n"
|
||||
"\"" + (tempDir.path() / "input2.sol").string() + "\" is not found. Skipping.\n"
|
||||
"All specified input files either do not exist or are not regular files.\n";
|
||||
|
||||
OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles({
|
||||
"solc",
|
||||
(tempDir.path() / "input1.sol").string(),
|
||||
(tempDir.path() / "input2.sol").string(),
|
||||
"--ignore-missing",
|
||||
});
|
||||
BOOST_TEST(!result.success);
|
||||
BOOST_TEST(result.stderrContent == expectedMessage);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(cli_not_a_file)
|
||||
{
|
||||
TemporaryDirectory tempDir(TEST_CASE_NAME);
|
||||
|
||||
string expectedMessage = "\"" + tempDir.path().string() + "\" is not a valid file.\n";
|
||||
|
||||
OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles({"solc", tempDir.path().string()});
|
||||
BOOST_TEST(!result.success);
|
||||
BOOST_TEST(result.stderrContent == expectedMessage);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(standard_json_base_path)
|
||||
{
|
||||
TemporaryDirectory tempDir(TEST_CASE_NAME);
|
||||
|
||||
OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles({
|
||||
"solc",
|
||||
"--standard-json",
|
||||
"--base-path=" + tempDir.path().string(),
|
||||
});
|
||||
BOOST_TEST(result.success);
|
||||
BOOST_TEST(result.stderrContent == "");
|
||||
BOOST_TEST(result.options.input.mode == InputMode::StandardJson);
|
||||
BOOST_TEST(result.options.input.addStdin);
|
||||
BOOST_TEST(result.options.input.paths.empty());
|
||||
BOOST_TEST(result.reader.sourceCodes().empty());
|
||||
BOOST_TEST(result.reader.allowedDirectories().empty());
|
||||
BOOST_TEST(result.reader.basePath() == tempDir.path());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(standard_json_no_input_file)
|
||||
{
|
||||
OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles({"solc", "--standard-json"});
|
||||
BOOST_TEST(result.success);
|
||||
BOOST_TEST(result.stderrContent == "");
|
||||
BOOST_TEST(result.options.input.mode == InputMode::StandardJson);
|
||||
BOOST_TEST(result.options.input.addStdin);
|
||||
BOOST_TEST(result.options.input.paths.empty());
|
||||
BOOST_TEST(result.reader.sourceCodes().empty());
|
||||
BOOST_TEST(result.reader.allowedDirectories().empty());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(standard_json_dash)
|
||||
{
|
||||
OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles({"solc", "--standard-json", "-"});
|
||||
BOOST_TEST(result.success);
|
||||
BOOST_TEST(result.stderrContent == "");
|
||||
BOOST_TEST(result.options.input.mode == InputMode::StandardJson);
|
||||
BOOST_TEST(result.options.input.addStdin);
|
||||
BOOST_TEST(result.reader.sourceCodes().empty());
|
||||
BOOST_TEST(result.reader.allowedDirectories().empty());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(standard_json_one_input_file)
|
||||
{
|
||||
TemporaryDirectory tempDir(TEST_CASE_NAME);
|
||||
createFilesWithParentDirs({tempDir.path() / "input.json"});
|
||||
|
||||
vector<string> commandLine = {"solc", "--standard-json", (tempDir.path() / "input.json").string()};
|
||||
OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles(commandLine);
|
||||
BOOST_TEST(result.success);
|
||||
BOOST_TEST(result.stderrContent == "");
|
||||
BOOST_TEST(result.options.input.mode == InputMode::StandardJson);
|
||||
BOOST_TEST(!result.options.input.addStdin);
|
||||
BOOST_TEST(result.options.input.paths == PathSet{tempDir.path() / "input.json"});
|
||||
BOOST_TEST(result.reader.allowedDirectories().empty());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(standard_json_two_input_files)
|
||||
{
|
||||
string expectedMessage =
|
||||
"Too many input files for --standard-json.\n"
|
||||
"Please either specify a single file name or provide its content on standard input.\n";
|
||||
|
||||
vector<string> commandLine = {"solc", "--standard-json", "input1.json", "input2.json"};
|
||||
OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles(commandLine);
|
||||
BOOST_TEST(!result.success);
|
||||
BOOST_TEST(result.stderrContent == expectedMessage);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(standard_json_one_input_file_and_stdin)
|
||||
{
|
||||
string expectedMessage =
|
||||
"Too many input files for --standard-json.\n"
|
||||
"Please either specify a single file name or provide its content on standard input.\n";
|
||||
|
||||
vector<string> commandLine = {"solc", "--standard-json", "input1.json", "-"};
|
||||
OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles(commandLine);
|
||||
BOOST_TEST(!result.success);
|
||||
BOOST_TEST(result.stderrContent == expectedMessage);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(standard_json_ignore_missing)
|
||||
{
|
||||
TemporaryDirectory tempDir(TEST_CASE_NAME);
|
||||
|
||||
// This option is pretty much useless Standard JSON mode.
|
||||
string expectedMessage =
|
||||
"\"" + (tempDir.path() / "input.json").string() + "\" is not found. Skipping.\n"
|
||||
"All specified input files either do not exist or are not regular files.\n";
|
||||
|
||||
OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles({
|
||||
"solc",
|
||||
"--standard-json",
|
||||
(tempDir.path() / "input.json").string(),
|
||||
"--ignore-missing",
|
||||
});
|
||||
BOOST_TEST(!result.success);
|
||||
BOOST_TEST(result.stderrContent == expectedMessage);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(standard_json_remapping)
|
||||
{
|
||||
string expectedMessage =
|
||||
"Import remappings are not accepted on the command line in Standard JSON mode.\n"
|
||||
"Please put them under 'settings.remappings' in the JSON input.\n";
|
||||
|
||||
vector<string> commandLine = {"solc", "--standard-json", "a=b"};
|
||||
OptionsReaderAndMessages result = parseCommandLineAndReadInputFiles(commandLine);
|
||||
BOOST_TEST(!result.success);
|
||||
BOOST_TEST(result.stderrContent == expectedMessage);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
} // namespace solidity::frontend::test
|
@ -20,6 +20,8 @@
|
||||
|
||||
#include <solc/CommandLineParser.h>
|
||||
|
||||
#include <test/solc/Common.h>
|
||||
|
||||
#include <test/Common.h>
|
||||
#include <test/libsolidity/util/SoltestErrors.h>
|
||||
|
||||
@ -28,7 +30,6 @@
|
||||
#include <libsolidity/interface/Version.h>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
@ -46,20 +47,13 @@ using namespace solidity::yul;
|
||||
namespace
|
||||
{
|
||||
|
||||
optional<CommandLineOptions> parseCommandLine(vector<string> const& commandLine, ostream& _stdout, ostream& _stderr)
|
||||
optional<CommandLineOptions> parseCommandLine(vector<string> const& _commandLine, ostream& _stdout, ostream& _stderr)
|
||||
{
|
||||
size_t argc = commandLine.size();
|
||||
vector<char const*> argv(argc + 1);
|
||||
|
||||
// argv[argc] typically contains NULL
|
||||
argv[argc] = nullptr;
|
||||
|
||||
for (size_t i = 0; i < argc; ++i)
|
||||
argv[i] = commandLine[i].c_str();
|
||||
vector<char const*> argv = test::makeArgv(_commandLine);
|
||||
|
||||
CommandLineParser cliParser(_stdout, _stderr);
|
||||
bool success = cliParser.parse(
|
||||
static_cast<int>(argc),
|
||||
static_cast<int>(_commandLine.size()),
|
||||
argv.data(),
|
||||
false // interactiveTerminal
|
||||
);
|
||||
@ -70,7 +64,6 @@ optional<CommandLineOptions> parseCommandLine(vector<string> const& commandLine,
|
||||
return cliParser.options();
|
||||
}
|
||||
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace solidity::frontend::test
|
||||
@ -99,7 +92,7 @@ BOOST_AUTO_TEST_CASE(no_options)
|
||||
BOOST_TEST(sout.str() == "");
|
||||
BOOST_TEST(serr.str() == "");
|
||||
BOOST_REQUIRE(parsedOptions.has_value());
|
||||
BOOST_TEST((parsedOptions.value() == expectedOptions));
|
||||
BOOST_TEST(parsedOptions.value() == expectedOptions);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(help)
|
||||
@ -224,7 +217,7 @@ BOOST_AUTO_TEST_CASE(cli_mode_options)
|
||||
BOOST_TEST(sout.str() == "");
|
||||
BOOST_TEST(serr.str() == "");
|
||||
BOOST_REQUIRE(parsedOptions.has_value());
|
||||
BOOST_TEST((parsedOptions.value() == expectedOptions));
|
||||
BOOST_TEST(parsedOptions.value() == expectedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,8 +335,7 @@ BOOST_AUTO_TEST_CASE(assembly_mode_options)
|
||||
BOOST_TEST(sout.str() == "");
|
||||
BOOST_TEST(serr.str() == "Warning: Yul is still experimental. Please use the output with care.\n");
|
||||
BOOST_REQUIRE(parsedOptions.has_value());
|
||||
|
||||
BOOST_TEST((parsedOptions.value() == expectedOptions));
|
||||
BOOST_TEST(parsedOptions.value() == expectedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -355,7 +347,7 @@ BOOST_AUTO_TEST_CASE(standard_json_mode_options)
|
||||
"--standard-json",
|
||||
"--base-path=/home/user/",
|
||||
"--allow-paths=/tmp,/home,project,../contracts",
|
||||
"--ignore-missing", // Ignored in Standard JSON mode
|
||||
"--ignore-missing",
|
||||
"--error-recovery", // Ignored in Standard JSON mode
|
||||
"--output-dir=/tmp/out", // Accepted but has no effect in Standard JSON mode
|
||||
"--overwrite", // Accepted but has no effect in Standard JSON mode
|
||||
@ -393,10 +385,10 @@ BOOST_AUTO_TEST_CASE(standard_json_mode_options)
|
||||
CommandLineOptions expectedOptions;
|
||||
|
||||
expectedOptions.input.mode = InputMode::StandardJson;
|
||||
expectedOptions.input.paths = {};
|
||||
expectedOptions.input.standardJsonFile = "input.json";
|
||||
expectedOptions.input.paths = {"input.json"};
|
||||
expectedOptions.input.basePath = "/home/user/";
|
||||
expectedOptions.input.allowedDirectories = {"/tmp", "/home", "project", "../contracts"};
|
||||
expectedOptions.input.ignoreMissingFiles = true;
|
||||
expectedOptions.output.dir = "/tmp/out";
|
||||
expectedOptions.output.overwriteFiles = true;
|
||||
expectedOptions.output.revertStrings = RevertStrings::Strip;
|
||||
@ -419,7 +411,7 @@ BOOST_AUTO_TEST_CASE(standard_json_mode_options)
|
||||
BOOST_TEST(sout.str() == "");
|
||||
BOOST_TEST(serr.str() == "");
|
||||
BOOST_REQUIRE(parsedOptions.has_value());
|
||||
BOOST_TEST((parsedOptions.value() == expectedOptions));
|
||||
BOOST_TEST(parsedOptions.value() == expectedOptions);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
58
test/solc/Common.cpp
Normal file
58
test/solc/Common.cpp
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
|
||||
#include <test/solc/Common.h>
|
||||
|
||||
#include <solc/CommandLineInterface.h>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
using namespace std;
|
||||
using namespace solidity::frontend;
|
||||
|
||||
vector<char const*> test::makeArgv(vector<string> const& _commandLine)
|
||||
{
|
||||
size_t argc = _commandLine.size();
|
||||
vector<char const*> argv(_commandLine.size() + 1);
|
||||
|
||||
// C++ standard mandates argv[argc] to be NULL
|
||||
argv[argc] = nullptr;
|
||||
|
||||
for (size_t i = 0; i < argc; ++i)
|
||||
argv[i] = _commandLine[i].c_str();
|
||||
|
||||
return argv;
|
||||
}
|
||||
|
||||
test::OptionsReaderAndMessages test::parseCommandLineAndReadInputFiles(
|
||||
vector<string> const& _commandLine,
|
||||
string const& _standardInputContent,
|
||||
bool _processInput
|
||||
)
|
||||
{
|
||||
vector<char const*> argv = makeArgv(_commandLine);
|
||||
stringstream sin(_standardInputContent), sout, serr;
|
||||
CommandLineInterface cli(sin, sout, serr);
|
||||
bool success = cli.parseArguments(static_cast<int>(_commandLine.size()), argv.data());
|
||||
if (success)
|
||||
success = cli.readInputFiles();
|
||||
if (success && _processInput)
|
||||
success = cli.processInput();
|
||||
|
||||
return {success, cli.options(), cli.fileReader(), cli.standardJsonInput(), sout.str(), serr.str()};
|
||||
}
|
53
test/solc/Common.h
Normal file
53
test/solc/Common.h
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
|
||||
/// Utilities shared by multiple tests for code in solc/.
|
||||
|
||||
#include <solc/CommandLineParser.h>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
BOOST_TEST_DONT_PRINT_LOG_VALUE(solidity::frontend::CommandLineOptions)
|
||||
BOOST_TEST_DONT_PRINT_LOG_VALUE(solidity::frontend::InputMode)
|
||||
|
||||
namespace solidity::frontend::test
|
||||
{
|
||||
|
||||
struct OptionsReaderAndMessages
|
||||
{
|
||||
bool success;
|
||||
CommandLineOptions options;
|
||||
FileReader reader;
|
||||
std::optional<std::string> standardJsonInput;
|
||||
std::string stdoutContent;
|
||||
std::string stderrContent;
|
||||
};
|
||||
|
||||
std::vector<char const*> makeArgv(std::vector<std::string> const& _commandLine);
|
||||
|
||||
OptionsReaderAndMessages parseCommandLineAndReadInputFiles(
|
||||
std::vector<std::string> const& _commandLine,
|
||||
std::string const& _standardInputContent = "",
|
||||
bool _processInput = false
|
||||
);
|
||||
|
||||
} // namespace solidity::frontend::test
|
@ -57,7 +57,7 @@ std::ostream& operator<<(std::ostream& _output, std::tuple<T1, T2> const& _tuple
|
||||
namespace boost::test_tools::tt_detail
|
||||
{
|
||||
|
||||
// Boost won't find find the << operator unless we put it in the std namespace which is illegal.
|
||||
// Boost won't find the << operator unless we put it in the std namespace which is illegal.
|
||||
// The recommended solution is to overload print_log_value<> struct and make it use our global operator.
|
||||
template<typename T1,typename T2>
|
||||
struct print_log_value<std::tuple<T1, T2>>
|
||||
|
Loading…
Reference in New Issue
Block a user