Coverage Report

Created: 2025-06-24 07:59

/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
}