/src/solidity/test/TestCaseReader.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | This file is part of solidity. |
3 | | |
4 | | solidity is free software: you can redistribute it and/or modify |
5 | | it under the terms of the GNU General Public License as published by |
6 | | the Free Software Foundation, either version 3 of the License, or |
7 | | (at your option) any later version. |
8 | | |
9 | | solidity is distributed in the hope that it will be useful, |
10 | | but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | | GNU General Public License for more details. |
13 | | |
14 | | You should have received a copy of the GNU General Public License |
15 | | along with solidity. If not, see <http://www.gnu.org/licenses/>. |
16 | | */ |
17 | | // SPDX-License-Identifier: GPL-3.0 |
18 | | |
19 | | #include <test/TestCaseReader.h> |
20 | | |
21 | | #include <libsolutil/CommonIO.h> |
22 | | |
23 | | #include <boost/algorithm/string.hpp> |
24 | | #include <boost/filesystem.hpp> |
25 | | |
26 | | using namespace solidity::frontend::test; |
27 | | |
28 | | namespace fs = boost::filesystem; |
29 | | |
30 | | TestCaseReader::TestCaseReader(std::string const& _filename): m_fileStream(_filename), m_fileName(_filename) |
31 | 0 | { |
32 | 0 | if (!m_fileStream) |
33 | 0 | BOOST_THROW_EXCEPTION(std::runtime_error("Cannot open file: \"" + _filename + "\".")); |
34 | 0 | m_fileStream.exceptions(std::ios::badbit); |
35 | |
|
36 | 0 | std::tie(m_sources, m_lineNumber) = parseSourcesAndSettingsWithLineNumber(m_fileStream); |
37 | 0 | m_unreadSettings = m_settings; |
38 | 0 | } |
39 | | |
40 | | TestCaseReader::TestCaseReader(std::istringstream const& _str) |
41 | 30.4k | { |
42 | 30.4k | std::tie(m_sources, m_lineNumber) = parseSourcesAndSettingsWithLineNumber( |
43 | 30.4k | static_cast<std::istream&>(const_cast<std::istringstream&>(_str)) |
44 | 30.4k | ); |
45 | 30.4k | } |
46 | | |
47 | | std::string const& TestCaseReader::source() const |
48 | 0 | { |
49 | 0 | if (m_sources.sources.size() != 1) |
50 | 0 | BOOST_THROW_EXCEPTION(std::runtime_error("Expected single source definition, but got multiple sources.")); |
51 | 0 | return m_sources.sources.at(m_sources.mainSourceFile); |
52 | 0 | } |
53 | | |
54 | | std::string TestCaseReader::simpleExpectations() |
55 | 0 | { |
56 | 0 | return parseSimpleExpectations(m_fileStream); |
57 | 0 | } |
58 | | |
59 | | bool TestCaseReader::boolSetting(std::string const& _name, bool _defaultValue) |
60 | 0 | { |
61 | 0 | if (m_settings.count(_name) == 0) |
62 | 0 | return _defaultValue; |
63 | | |
64 | 0 | m_unreadSettings.erase(_name); |
65 | 0 | std::string value = m_settings.at(_name); |
66 | 0 | if (value == "false") |
67 | 0 | return false; |
68 | 0 | if (value == "true") |
69 | 0 | return true; |
70 | | |
71 | 0 | BOOST_THROW_EXCEPTION(std::runtime_error("Invalid Boolean value: " + value + ".")); |
72 | 0 | } |
73 | | |
74 | | size_t TestCaseReader::sizetSetting(std::string const& _name, size_t _defaultValue) |
75 | 0 | { |
76 | 0 | if (m_settings.count(_name) == 0) |
77 | 0 | return _defaultValue; |
78 | | |
79 | 0 | m_unreadSettings.erase(_name); |
80 | |
|
81 | 0 | static_assert(sizeof(unsigned long) <= sizeof(size_t)); |
82 | 0 | return stoul(m_settings.at(_name)); |
83 | 0 | } |
84 | | |
85 | | std::string TestCaseReader::stringSetting(std::string const& _name, std::string const& _defaultValue) |
86 | 0 | { |
87 | 0 | if (m_settings.count(_name) == 0) |
88 | 0 | return _defaultValue; |
89 | | |
90 | 0 | m_unreadSettings.erase(_name); |
91 | 0 | return m_settings.at(_name); |
92 | 0 | } |
93 | | |
94 | | void TestCaseReader::ensureAllSettingsRead() const |
95 | 0 | { |
96 | 0 | if (!m_unreadSettings.empty()) |
97 | 0 | BOOST_THROW_EXCEPTION(std::runtime_error( |
98 | 0 | "Unknown setting(s): " + |
99 | 0 | util::joinHumanReadable(m_unreadSettings | ranges::views::keys) |
100 | 0 | )); |
101 | 0 | } |
102 | | |
103 | | std::pair<SourceMap, size_t> TestCaseReader::parseSourcesAndSettingsWithLineNumber(std::istream& _stream) |
104 | 30.4k | { |
105 | 30.4k | std::map<std::string, std::string> sources; |
106 | 30.4k | std::map<std::string, boost::filesystem::path> externalSources; |
107 | 30.4k | std::string currentSourceName; |
108 | 30.4k | std::string currentSource; |
109 | 30.4k | std::string line; |
110 | 30.4k | size_t lineNumber = 1; |
111 | 30.4k | static std::string const externalSourceDelimiterStart("==== ExternalSource:"); |
112 | 30.4k | static std::string const sourceDelimiterStart("==== Source:"); |
113 | 30.4k | static std::string const sourceDelimiterEnd("===="); |
114 | 30.4k | static std::string const comment("// "); |
115 | 30.4k | static std::string const settingsDelimiter("// ===="); |
116 | 30.4k | static std::string const expectationsDelimiter("// ----"); |
117 | 30.4k | bool sourcePart = true; |
118 | 279k | while (getline(_stream, line)) |
119 | 257k | { |
120 | 257k | lineNumber++; |
121 | | |
122 | | // Anything below the delimiter is left up to the test case to process in a custom way. |
123 | 257k | if (boost::algorithm::starts_with(line, expectationsDelimiter)) |
124 | 8.08k | break; |
125 | | |
126 | 249k | if (boost::algorithm::starts_with(line, settingsDelimiter)) |
127 | 8.99k | sourcePart = false; |
128 | 240k | else if (sourcePart) |
129 | 228k | { |
130 | 228k | if (boost::algorithm::starts_with(line, sourceDelimiterStart) && boost::algorithm::ends_with(line, sourceDelimiterEnd)) |
131 | 3.45k | { |
132 | 3.45k | if (!(currentSourceName.empty() && currentSource.empty())) |
133 | 2.84k | sources[currentSourceName] = std::move(currentSource); |
134 | 3.45k | currentSource = {}; |
135 | 3.45k | currentSourceName = boost::trim_copy(line.substr( |
136 | 3.45k | sourceDelimiterStart.size(), |
137 | 3.45k | line.size() - sourceDelimiterEnd.size() - sourceDelimiterStart.size() |
138 | 3.45k | )); |
139 | 3.45k | if (sources.count(currentSourceName)) |
140 | 12 | BOOST_THROW_EXCEPTION(std::runtime_error("Multiple definitions of test source \"" + currentSourceName + "\".")); |
141 | 3.45k | } |
142 | 225k | else if (boost::algorithm::starts_with(line, externalSourceDelimiterStart) && boost::algorithm::ends_with(line, sourceDelimiterEnd)) |
143 | 12 | { |
144 | 12 | std::string externalSourceString = boost::trim_copy(line.substr( |
145 | 12 | externalSourceDelimiterStart.size(), |
146 | 12 | line.size() - sourceDelimiterEnd.size() - externalSourceDelimiterStart.size() |
147 | 12 | )); |
148 | | |
149 | 12 | std::string externalSourceName; |
150 | 12 | size_t remappingPos = externalSourceString.find('='); |
151 | | // Does the external source define a remapping? |
152 | 12 | if (remappingPos != std::string::npos) |
153 | 11 | { |
154 | 11 | externalSourceName = boost::trim_copy(externalSourceString.substr(0, remappingPos)); |
155 | 11 | externalSourceString = boost::trim_copy(externalSourceString.substr(remappingPos + 1)); |
156 | 11 | } |
157 | 1 | else |
158 | 1 | externalSourceName = externalSourceString; |
159 | | |
160 | 12 | soltestAssert(!externalSourceName.empty(), ""); |
161 | 12 | fs::path externalSourceTarget(externalSourceString); |
162 | 12 | fs::path testCaseParentDir = m_fileName.parent_path(); |
163 | 12 | if (!externalSourceTarget.is_relative() || !externalSourceTarget.root_path().empty()) |
164 | | // NOTE: UNC paths (ones starting with // or \\) are considered relative by Boost |
165 | | // since they have an empty root directory (but non-empty root name). |
166 | 0 | BOOST_THROW_EXCEPTION(std::runtime_error("External Source paths need to be relative to the location of the test case.")); |
167 | 12 | fs::path externalSourceFullPath = testCaseParentDir / externalSourceTarget; |
168 | 12 | std::string externalSourceContent; |
169 | 12 | if (!fs::exists(externalSourceFullPath)) |
170 | 12 | BOOST_THROW_EXCEPTION(std::runtime_error("External Source '" + externalSourceTarget.string() + "' not found.")); |
171 | 0 | else |
172 | 0 | externalSourceContent = util::readFileAsString(externalSourceFullPath); |
173 | | |
174 | 0 | if (sources.count(externalSourceName)) |
175 | 0 | BOOST_THROW_EXCEPTION(std::runtime_error("Multiple definitions of test source \"" + externalSourceName + "\".")); |
176 | 0 | sources[externalSourceName] = externalSourceContent; |
177 | 0 | externalSources[externalSourceName] = externalSourceTarget; |
178 | 0 | } |
179 | 225k | else |
180 | 225k | currentSource += line + "\n"; |
181 | 228k | } |
182 | 11.2k | else if (boost::algorithm::starts_with(line, comment)) |
183 | 11.2k | { |
184 | 11.2k | size_t colon = line.find(':'); |
185 | 11.2k | if (colon == std::string::npos) |
186 | 6 | BOOST_THROW_EXCEPTION(std::runtime_error(std::string("Expected \":\" inside setting."))); |
187 | 11.2k | std::string key = line.substr(comment.size(), colon - comment.size()); |
188 | 11.2k | std::string value = line.substr(colon + 1); |
189 | 11.2k | boost::algorithm::trim(key); |
190 | 11.2k | boost::algorithm::trim(value); |
191 | 11.2k | m_settings[key] = value; |
192 | 11.2k | } |
193 | 19 | else |
194 | 19 | BOOST_THROW_EXCEPTION(std::runtime_error(std::string("Expected \"//\" or \"// ---\" to terminate settings and source."))); |
195 | 249k | } |
196 | | // Register the last source as the main one |
197 | 30.4k | sources[currentSourceName] = currentSource; |
198 | 30.4k | return {{std::move(sources), std::move(externalSources), std::move(currentSourceName)}, lineNumber}; |
199 | 30.4k | } |
200 | | |
201 | | std::string TestCaseReader::parseSimpleExpectations(std::istream& _file) |
202 | 0 | { |
203 | 0 | std::string result; |
204 | 0 | std::string line; |
205 | 0 | while (std::getline(_file, line)) |
206 | 0 | if (boost::algorithm::starts_with(line, "// ")) |
207 | 0 | result += line.substr(3) + "\n"; |
208 | 0 | else if (line == "//") |
209 | 0 | result += "\n"; |
210 | 0 | else |
211 | 0 | BOOST_THROW_EXCEPTION(std::runtime_error("Test expectations must start with \"// \".")); |
212 | 0 | return result; |
213 | 0 | } |