Skip to content

Commit cc7a14a

Browse files
authored
Merge pull request #14518 from ethereum/analysis-framework-refactor
`AnalysisFramework` refactor
2 parents dc44f8a + 3fb2f1d commit cc7a14a

12 files changed

+291
-135
lines changed

test/libsolidity/AnalysisFramework.cpp

Lines changed: 80 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <test/libsolidity/AnalysisFramework.h>
2323

2424
#include <test/libsolidity/util/Common.h>
25+
#include <test/libsolidity/util/SoltestErrors.h>
2526
#include <test/Common.h>
2627

2728
#include <libsolidity/interface/CompilerStack.h>
@@ -41,36 +42,84 @@ using namespace solidity::langutil;
4142
using namespace solidity::frontend;
4243
using namespace solidity::frontend::test;
4344

44-
std::pair<SourceUnit const*, ErrorList>
45-
AnalysisFramework::parseAnalyseAndReturnError(
45+
std::pair<SourceUnit const*, ErrorList> AnalysisFramework::runAnalysisAndExpectNoParsingErrors(
4646
std::string const& _source,
47-
bool _reportWarnings,
48-
bool _insertLicenseAndVersionPragma,
49-
bool _allowMultipleErrors
47+
bool _includeWarningsAndInfos,
48+
bool _addPreamble,
49+
bool _allowMultiple
5050
)
5151
{
52-
compiler().reset();
53-
compiler().setSources({{"", _insertLicenseAndVersionPragma ? withPreamble(_source) : _source}});
54-
compiler().setEVMVersion(solidity::test::CommonOptions::get().evmVersion());
55-
if (!compiler().parse())
56-
{
57-
BOOST_FAIL("Parsing contract failed in analysis test suite:" + formatErrors(compiler().errors()));
58-
}
52+
runFramework(_addPreamble ? withPreamble(_source) : _source, PipelineStage::Analysis);
5953

60-
compiler().analyze();
54+
if (!stageSuccessful(PipelineStage::Parsing))
55+
BOOST_FAIL("Parsing contract failed in analysis test suite:" + formatErrors(m_compiler->errors()));
6156

62-
ErrorList errors = filteredErrors(_reportWarnings);
63-
if (errors.size() > 1 && !_allowMultipleErrors)
57+
ErrorList errors = filteredErrors(_includeWarningsAndInfos);
58+
if (errors.size() > 1 && !_allowMultiple)
6459
BOOST_FAIL("Multiple errors found: " + formatErrors(errors));
6560

6661
return make_pair(&compiler().ast(""), std::move(errors));
6762
}
6863

64+
bool AnalysisFramework::runFramework(StringMap _sources, PipelineStage _targetStage)
65+
{
66+
resetFramework();
67+
m_targetStage = _targetStage;
68+
soltestAssert(m_compiler);
69+
70+
m_compiler->setSources(std::move(_sources));
71+
setupCompiler(*m_compiler);
72+
executeCompilationPipeline();
73+
return pipelineSuccessful();
74+
}
75+
76+
void AnalysisFramework::resetFramework()
77+
{
78+
compiler().reset();
79+
m_targetStage = PipelineStage::Compilation;
80+
}
81+
6982
std::unique_ptr<CompilerStack> AnalysisFramework::createStack() const
7083
{
7184
return std::make_unique<CompilerStack>();
7285
}
7386

87+
void AnalysisFramework::setupCompiler(CompilerStack& _compiler)
88+
{
89+
// These are just defaults based on the (global) CLI options.
90+
// Technically, every TestCase should override these with values passed to it in TestCase::Config.
91+
// In practice TestCase::Config always matches global config so most test cases don't care.
92+
_compiler.setEVMVersion(solidity::test::CommonOptions::get().evmVersion());
93+
_compiler.setEOFVersion(solidity::test::CommonOptions::get().eofVersion());
94+
_compiler.setOptimiserSettings(solidity::test::CommonOptions::get().optimize);
95+
}
96+
97+
void AnalysisFramework::executeCompilationPipeline()
98+
{
99+
soltestAssert(m_compiler);
100+
101+
// If you add a new stage, remember to handle it below.
102+
soltestAssert(
103+
m_targetStage == PipelineStage::Parsing ||
104+
m_targetStage == PipelineStage::Analysis ||
105+
m_targetStage == PipelineStage::Compilation
106+
);
107+
108+
bool parsingSuccessful = m_compiler->parse();
109+
soltestAssert(parsingSuccessful || !filteredErrors(false /* _includeWarningsAndInfos */).empty());
110+
if (!parsingSuccessful || stageSuccessful(m_targetStage))
111+
return;
112+
113+
bool analysisSuccessful = m_compiler->analyze();
114+
soltestAssert(analysisSuccessful || !filteredErrors(false /* _includeWarningsAndInfos */).empty());
115+
if (!analysisSuccessful || stageSuccessful(m_targetStage))
116+
return;
117+
118+
bool compilationSuccessful = m_compiler->compile();
119+
soltestAssert(compilationSuccessful || !filteredErrors(false /* _includeWarningsAndInfos */).empty());
120+
soltestAssert(stageSuccessful(m_targetStage) == compilationSuccessful);
121+
}
122+
74123
ErrorList AnalysisFramework::filterErrors(ErrorList const& _errorList, bool _includeWarningsAndInfos) const
75124
{
76125
ErrorList errors;
@@ -113,28 +162,26 @@ ErrorList AnalysisFramework::filterErrors(ErrorList const& _errorList, bool _inc
113162
return errors;
114163
}
115164

116-
SourceUnit const* AnalysisFramework::parseAndAnalyse(std::string const& _source)
165+
bool AnalysisFramework::stageSuccessful(PipelineStage _stage) const
117166
{
118-
auto sourceAndError = parseAnalyseAndReturnError(_source);
119-
BOOST_REQUIRE(!!sourceAndError.first);
120-
std::string message;
121-
if (!sourceAndError.second.empty())
122-
message = "Unexpected error: " + formatErrors(compiler().errors());
123-
BOOST_REQUIRE_MESSAGE(sourceAndError.second.empty(), message);
124-
return sourceAndError.first;
125-
}
126-
127-
bool AnalysisFramework::success(std::string const& _source)
128-
{
129-
return parseAnalyseAndReturnError(_source).second.empty();
167+
switch (_stage) {
168+
case PipelineStage::Parsing: return compiler().state() >= CompilerStack::Parsed;
169+
case PipelineStage::Analysis: return compiler().state() >= CompilerStack::AnalysisSuccessful;
170+
case PipelineStage::Compilation: return compiler().state() >= CompilerStack::CompilationSuccessful;
171+
}
172+
unreachable();
130173
}
131174

132-
ErrorList AnalysisFramework::expectError(std::string const& _source, bool _warning, bool _allowMultiple)
175+
ErrorList AnalysisFramework::runAnalysisAndExpectError(
176+
std::string const& _source,
177+
bool _includeWarningsAndInfos,
178+
bool _allowMultiple
179+
)
133180
{
134-
auto sourceAndErrors = parseAnalyseAndReturnError(_source, _warning, true, _allowMultiple);
135-
BOOST_REQUIRE(!sourceAndErrors.second.empty());
136-
BOOST_REQUIRE_MESSAGE(!!sourceAndErrors.first, "Expected error, but no error happened.");
137-
return sourceAndErrors.second;
181+
auto [ast, errors] = runAnalysisAndExpectNoParsingErrors(_source, _includeWarningsAndInfos, true, _allowMultiple);
182+
BOOST_REQUIRE(!errors.empty());
183+
BOOST_REQUIRE_MESSAGE(ast, "Expected error, but no error happened.");
184+
return errors;
138185
}
139186

140187
std::string AnalysisFramework::formatErrors(

test/libsolidity/AnalysisFramework.h

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,66 @@ using FunctionTypePointer = FunctionType const*;
3939
namespace solidity::frontend::test
4040
{
4141

42+
enum class PipelineStage {
43+
Parsing,
44+
Analysis,
45+
Compilation,
46+
};
47+
4248
class AnalysisFramework
4349
{
4450

4551
protected:
46-
virtual std::pair<SourceUnit const*, langutil::ErrorList>
47-
parseAnalyseAndReturnError(
52+
virtual ~AnalysisFramework() = default;
53+
54+
/// Runs analysis via runFramework() and returns either an AST or a filtered list of errors.
55+
/// Uses Boost test macros to fail if errors do not occur specifically at the analysis stage.
56+
///
57+
/// @deprecated This is a legacy helper. Use runFramework() directly in new tests.
58+
///
59+
/// @param _includeWarningsAndInfos Do not remove warning and info messages from the error list.
60+
/// @param _addPreamble Apply withPreamble() to @p _source.
61+
/// @param _allowMultiple When false, use Boost test macros to fail when there's more
62+
/// than one item on the error list.
63+
std::pair<SourceUnit const*, langutil::ErrorList> runAnalysisAndExpectNoParsingErrors(
4864
std::string const& _source,
49-
bool _reportWarnings = false,
50-
bool _insertLicenseAndVersionPragma = true,
51-
bool _allowMultipleErrors = false
65+
bool _includeWarningsAndInfos = false,
66+
bool _addPreamble = true,
67+
bool _allowMultiple = false
5268
);
53-
virtual ~AnalysisFramework() = default;
5469

55-
SourceUnit const* parseAndAnalyse(std::string const& _source);
56-
bool success(std::string const& _source);
57-
langutil::ErrorList expectError(std::string const& _source, bool _warning = false, bool _allowMultiple = false);
70+
/// Runs analysis via runAnalysisAndExpectNoParsingErrors() and returns the list of errors.
71+
/// Uses Boost test macros to fail if there are no errors.
72+
///
73+
/// @deprecated This is a legacy helper. Use runFramework() directly in new tests.
74+
///
75+
/// @param _includeWarningsAndInfos Do not remove warning and info messages from the error list.
76+
/// @param _allowMultiple When false, use Boost test macros to fail when there's more
77+
/// than one item on the error list.
78+
langutil::ErrorList runAnalysisAndExpectError(
79+
std::string const& _source,
80+
bool _includeWarningsAndInfos = false,
81+
bool _allowMultiple = false
82+
);
83+
84+
public:
85+
/// Runs the full compiler pipeline on specified sources. This is the main function of the
86+
/// framework. Resets the stack, configures it and runs either until the first failed stage or
87+
/// until the @p _targetStage is reached.
88+
/// Afterwards the caller can inspect the stack via @p compiler(). The framework provides a few
89+
/// convenience helpers to check the state and error list, in general the caller can freely
90+
/// access the stack, including generating outputs if the compilation succeeded.
91+
bool runFramework(StringMap _sources, PipelineStage _targetStage = PipelineStage::Compilation);
92+
bool runFramework(std::string _source, PipelineStage _targetStage = PipelineStage::Compilation)
93+
{
94+
return runFramework({{"", std::move(_source)}}, _targetStage);
95+
}
96+
97+
void resetFramework();
98+
99+
PipelineStage targetStage() const { return m_targetStage; }
100+
bool pipelineSuccessful() const { return stageSuccessful(m_targetStage); }
101+
bool stageSuccessful(PipelineStage _stage) const;
58102

59103
std::string formatErrors(
60104
langutil::ErrorList const& _errors,
@@ -80,9 +124,6 @@ class AnalysisFramework
80124
return filterErrors(compiler().errors(), _includeWarningsAndInfos);
81125
}
82126

83-
std::vector<std::string> m_warningsToFilter = {"This is a pre-release compiler version"};
84-
std::vector<std::string> m_messagesToCut = {"Source file requires different compiler version (current compiler is"};
85-
86127
/// @returns reference to lazy-instantiated CompilerStack.
87128
solidity::frontend::CompilerStack& compiler()
88129
{
@@ -99,28 +140,41 @@ class AnalysisFramework
99140
return *m_compiler;
100141
}
101142

143+
protected:
102144
/// Creates a new instance of @p CompilerStack. Override if your test case needs to pass in
103145
/// custom constructor arguments.
104146
virtual std::unique_ptr<CompilerStack> createStack() const;
105147

148+
/// Configures @p CompilerStack. The default implementation sets basic parameters based on
149+
/// CLI options. Override if your test case needs extra configuration.
150+
virtual void setupCompiler(CompilerStack& _compiler);
151+
152+
/// Executes the requested pipeline stages until @p m_targetStage is reached.
153+
/// Stops at the first failed stage.
154+
void executeCompilationPipeline();
155+
156+
std::vector<std::string> m_warningsToFilter = {"This is a pre-release compiler version"};
157+
std::vector<std::string> m_messagesToCut = {"Source file requires different compiler version (current compiler is"};
158+
106159
private:
107160
mutable std::unique_ptr<solidity::frontend::CompilerStack> m_compiler;
161+
PipelineStage m_targetStage = PipelineStage::Compilation;
108162
};
109163

110164
// Asserts that the compilation down to typechecking
111165
// emits multiple errors of different types and messages, provided in the second argument.
112166
#define CHECK_ALLOW_MULTI(text, expectations) \
113167
do \
114168
{ \
115-
ErrorList errors = expectError((text), true, true); \
169+
ErrorList errors = runAnalysisAndExpectError((text), true, true); \
116170
auto message = searchErrors(errors, (expectations)); \
117171
BOOST_CHECK_MESSAGE(message.empty(), message); \
118172
} while(0)
119173

120-
#define CHECK_ERROR_OR_WARNING(text, typ, substrings, warning, allowMulti) \
174+
#define CHECK_ERROR_OR_WARNING(text, typ, substrings, includeWarningsAndInfos, allowMulti) \
121175
do \
122176
{ \
123-
ErrorList errors = expectError((text), (warning), (allowMulti)); \
177+
ErrorList errors = runAnalysisAndExpectError((text), (includeWarningsAndInfos), (allowMulti)); \
124178
std::vector<std::pair<Error::Type, std::string>> expectations; \
125179
for (auto const& str: substrings) \
126180
expectations.emplace_back((Error::Type::typ), str); \
@@ -154,16 +208,19 @@ CHECK_ERROR_OR_WARNING(text, Warning, std::vector<std::string>{(substring)}, tru
154208
CHECK_ERROR_OR_WARNING(text, Warning, substrings, true, true)
155209

156210
// [checkSuccess(text)] asserts that the compilation down to typechecking succeeds.
157-
#define CHECK_SUCCESS(text) do { BOOST_CHECK(success((text))); } while(0)
211+
#define CHECK_SUCCESS(text) do { \
212+
auto [ast, errors] = runAnalysisAndExpectNoParsingErrors((text)); \
213+
BOOST_CHECK(errors.empty()); \
214+
} while(0)
158215

159216
#define CHECK_SUCCESS_NO_WARNINGS(text) \
160217
do \
161218
{ \
162-
auto sourceAndError = parseAnalyseAndReturnError((text), true); \
219+
auto [ast, errors] = runAnalysisAndExpectNoParsingErrors((text), true); \
163220
std::string message; \
164-
if (!sourceAndError.second.empty()) \
165-
message = formatErrors(compiler().errors());\
166-
BOOST_CHECK_MESSAGE(sourceAndError.second.empty(), message); \
221+
if (!errors.empty()) \
222+
message = formatErrors(errors);\
223+
BOOST_CHECK_MESSAGE(errors.empty(), message); \
167224
} \
168225
while(0)
169226

test/libsolidity/GasTest.cpp

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,24 +97,31 @@ void GasTest::printUpdatedExpectations(std::ostream& _stream, std::string const&
9797
}
9898
}
9999

100-
TestCase::TestResult GasTest::run(std::ostream& _stream, std::string const& _linePrefix, bool _formatted)
100+
void GasTest::setupCompiler(CompilerStack& _compiler)
101101
{
102-
compiler().reset();
102+
AnalysisFramework::setupCompiler(_compiler);
103+
103104
// Prerelease CBOR metadata varies in size due to changing version numbers and build dates.
104105
// This leads to volatile creation cost estimates. Therefore we force the compiler to
105106
// release mode for testing gas estimates.
106-
compiler().setMetadataFormat(CompilerStack::MetadataFormat::NoMetadata);
107+
_compiler.setMetadataFormat(CompilerStack::MetadataFormat::NoMetadata);
107108
OptimiserSettings settings = m_optimise ? OptimiserSettings::standard() : OptimiserSettings::minimal();
108109
if (m_optimiseYul)
109110
{
110111
settings.runYulOptimiser = m_optimise;
111112
settings.optimizeStackAllocation = m_optimise;
112113
}
113114
settings.expectedExecutionsPerDeployment = m_optimiseRuns;
114-
compiler().setOptimiserSettings(settings);
115-
compiler().setSources({{"", withPreamble(m_source)}});
115+
_compiler.setOptimiserSettings(settings);
116+
117+
// Intentionally ignoring EVM version specified on the command line.
118+
// Gas expectations are only valid for the default version.
119+
_compiler.setEVMVersion(EVMVersion{});
120+
}
116121

117-
if (!compiler().parseAndAnalyze() || !compiler().compile())
122+
TestCase::TestResult GasTest::run(std::ostream& _stream, std::string const& _linePrefix, bool _formatted)
123+
{
124+
if (!runFramework(withPreamble(m_source), PipelineStage::Compilation))
118125
{
119126
_stream << formatErrors(filteredErrors(), _formatted);
120127
return TestResult::FatalError;

test/libsolidity/GasTest.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ class GasTest: AnalysisFramework, public TestCase
4444
void printSource(std::ostream &_stream, std::string const &_linePrefix = "", bool _formatted = false) const override;
4545
void printUpdatedExpectations(std::ostream& _stream, std::string const& _linePrefix) const override;
4646

47+
protected:
48+
void setupCompiler(CompilerStack& _compiler) override;
49+
4750
private:
4851
void parseExpectations(std::istream& _stream);
4952

test/libsolidity/MemoryGuardTest.cpp

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,17 @@ using namespace solidity::frontend;
3636
using namespace solidity::frontend::test;
3737
using namespace yul;
3838

39+
void MemoryGuardTest::setupCompiler(CompilerStack& _compiler)
40+
{
41+
AnalysisFramework::setupCompiler(_compiler);
42+
43+
_compiler.setViaIR(true);
44+
_compiler.setOptimiserSettings(OptimiserSettings::none());
45+
}
46+
3947
TestCase::TestResult MemoryGuardTest::run(std::ostream& _stream, std::string const& _linePrefix, bool _formatted)
4048
{
41-
compiler().reset();
42-
compiler().setSources(StringMap{{"", m_source}});
43-
compiler().setViaIR(true);
44-
compiler().setOptimiserSettings(OptimiserSettings::none());
45-
if (!compiler().compile())
49+
if (!runFramework(m_source, PipelineStage::Compilation))
4650
{
4751
_stream << formatErrors(filteredErrors(), _formatted);
4852
return TestResult::FatalError;

test/libsolidity/MemoryGuardTest.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ class MemoryGuardTest: public AnalysisFramework, public TestCase
4848
}
4949

5050
TestResult run(std::ostream& _stream, std::string const& _linePrefix = "", bool _formatted = false) override;
51+
52+
protected:
53+
void setupCompiler(CompilerStack& _compiler) override;
5154
};
5255

5356
}

0 commit comments

Comments
 (0)