From 4607118d2e9bbf59707b48f177aff54ac99163b8 Mon Sep 17 00:00:00 2001 From: chriseth Date: Tue, 9 Oct 2018 15:43:55 +0200 Subject: [PATCH 1/4] Add Yul optimizer test framework. --- test/boostTest.cpp | 7 ++ test/libjulia/YulOptimizerTest.cpp | 167 +++++++++++++++++++++++++++++ test/libjulia/YulOptimizerTest.h | 75 +++++++++++++ test/libsolidity/TestCase.cpp | 3 +- test/tools/CMakeLists.txt | 2 +- test/tools/isoltest.cpp | 14 +++ 6 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 test/libjulia/YulOptimizerTest.cpp create mode 100644 test/libjulia/YulOptimizerTest.h diff --git a/test/boostTest.cpp b/test/boostTest.cpp index d9e939ebd..cbbf586f0 100644 --- a/test/boostTest.cpp +++ b/test/boostTest.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -138,6 +139,12 @@ test_suite* init_unit_test_suite( int /*argc*/, char* /*argv*/[] ) "ASTJSON", ASTJSONTest::create ) > 0, "no JSON AST tests found"); + solAssert(registerTests( + master, + dev::test::Options::get().testPath / "libjulia", + "yulOptimizerTests", + dev::julia::test::YulOptimizerTest::create + ) > 0, "no Yul Optimizer tests found"); if (dev::test::Options::get().disableIPC) { for (auto suite: { diff --git a/test/libjulia/YulOptimizerTest.cpp b/test/libjulia/YulOptimizerTest.cpp new file mode 100644 index 000000000..fd15623c4 --- /dev/null +++ b/test/libjulia/YulOptimizerTest.cpp @@ -0,0 +1,167 @@ +/* + 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 + +#include +#include + +#include + +using namespace dev; +using namespace dev::julia; +using namespace dev::julia::test; +using namespace dev::solidity; +using namespace dev::solidity::test; +using namespace std; + +YulOptimizerTest::YulOptimizerTest(string const& _filename) +{ + boost::filesystem::path path(_filename); + + if (path.empty() || std::next(path.begin()) == path.end() || std::next(std::next(path.begin())) == path.end()) + BOOST_THROW_EXCEPTION(runtime_error("Filename path has to contain a directory: \"" + _filename + "\".")); + m_optimizerStep = std::prev(std::prev(path.end()))->string(); + + ifstream file(_filename); + if (!file) + BOOST_THROW_EXCEPTION(runtime_error("Cannot open test case: \"" + _filename + "\".")); + file.exceptions(ios::badbit); + + string line; + while (getline(file, line)) + { + if (boost::algorithm::starts_with(line, "// ----")) + break; + if (m_source.empty() && boost::algorithm::starts_with(line, "// yul")) + m_yul = true; + m_source += line + "\n"; + } + while (getline(file, line)) + if (boost::algorithm::starts_with(line, "// ")) + m_expectation += line.substr(3) + "\n"; + else + m_expectation += line + "\n"; +} + +bool YulOptimizerTest::run(ostream& _stream, string const& _linePrefix, bool const _formatted) +{ + assembly::AsmPrinter printer{m_yul}; + shared_ptr ast; + shared_ptr analysisInfo; + if (!parse(_stream, _linePrefix, _formatted)) + return false; + + if (m_optimizerStep == "disambiguator") + disambiguate(); + else + { + FormattedScope(_stream, _formatted, {formatting::BOLD, formatting::RED}) << _linePrefix << "Invalid optimizer step: " << m_optimizerStep << endl; + return false; + } + + m_obtainedResult = m_optimizerStep + "\n" + printer(*m_ast) + "\n"; + + if (m_expectation != m_obtainedResult) + { + string nextIndentLevel = _linePrefix + " "; + FormattedScope(_stream, _formatted, {formatting::BOLD, formatting::CYAN}) << _linePrefix << "Expected result:" << endl; + // TODO could compute a simple diff with highlighted lines + printIndented(_stream, m_expectation, nextIndentLevel); + FormattedScope(_stream, _formatted, {formatting::BOLD, formatting::CYAN}) << _linePrefix << "Obtained result:" << endl; + printIndented(_stream, m_obtainedResult, nextIndentLevel); + return false; + } + return true; +} + +void YulOptimizerTest::printSource(ostream& _stream, string const& _linePrefix, bool const) const +{ + printIndented(_stream, m_source, _linePrefix); +} + +void YulOptimizerTest::printUpdatedExpectations(ostream& _stream, string const& _linePrefix) const +{ + printIndented(_stream, m_obtainedResult, _linePrefix); +} + +void YulOptimizerTest::printIndented(ostream& _stream, string const& _output, string const& _linePrefix) const +{ + stringstream output(_output); + string line; + while (getline(output, line)) + _stream << _linePrefix << line << endl; +} + +bool YulOptimizerTest::parse(ostream& _stream, string const& _linePrefix, bool const _formatted) +{ + assembly::AsmFlavour flavour = m_yul ? assembly::AsmFlavour::Yul : assembly::AsmFlavour::Strict; + ErrorList errors; + ErrorReporter errorReporter(errors); + shared_ptr scanner = make_shared(CharStream(m_source), ""); + m_ast = assembly::Parser(errorReporter, flavour).parse(scanner, false); + if (!m_ast || !errorReporter.errors().empty()) + { + FormattedScope(_stream, _formatted, {formatting::BOLD, formatting::RED}) << _linePrefix << "Error parsing source." << endl; + printErrors(_stream, errorReporter.errors(), *scanner); + return false; + } + m_analysisInfo = make_shared(); + assembly::AsmAnalyzer analyzer( + *m_analysisInfo, + errorReporter, + dev::test::Options::get().evmVersion(), + boost::none, + flavour + ); + if (!analyzer.analyze(*m_ast) || !errorReporter.errors().empty()) + { + FormattedScope(_stream, _formatted, {formatting::BOLD, formatting::RED}) << _linePrefix << "Error analyzing source." << endl; + printErrors(_stream, errorReporter.errors(), *scanner); + return false; + } + return true; +} + +void YulOptimizerTest::disambiguate() +{ + *m_ast = boost::get(Disambiguator(*m_analysisInfo)(*m_ast)); + m_analysisInfo.reset(); +} + +void YulOptimizerTest::printErrors(ostream& _stream, ErrorList const& _errors, Scanner const& _scanner) +{ + SourceReferenceFormatter formatter(_stream, [&](string const&) -> Scanner const& { return _scanner; }); + + for (auto const& error: _errors) + formatter.printExceptionInformation( + *error, + (error->type() == Error::Type::Warning) ? "Warning" : "Error" + ); +} diff --git a/test/libjulia/YulOptimizerTest.h b/test/libjulia/YulOptimizerTest.h new file mode 100644 index 000000000..8f9a81f7a --- /dev/null +++ b/test/libjulia/YulOptimizerTest.h @@ -0,0 +1,75 @@ +/* + 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 . +*/ + +#pragma once + +#include + + +namespace dev +{ +namespace solidity +{ +class Scanner; +class Error; +using ErrorList = std::vector>; +namespace assembly +{ +struct AsmAnalysisInfo; +struct Block; +} +} +namespace julia +{ +namespace test +{ + +class YulOptimizerTest: public solidity::test::TestCase +{ +public: + static std::unique_ptr create(std::string const& _filename) + { + return std::unique_ptr(new YulOptimizerTest(_filename)); + } + + explicit YulOptimizerTest(std::string const& _filename); + + bool run(std::ostream& _stream, std::string const& _linePrefix = "", bool const _formatted = false) override; + + void printSource(std::ostream& _stream, std::string const &_linePrefix = "", bool const _formatted = false) const override; + void printUpdatedExpectations(std::ostream& _stream, std::string const& _linePrefix) const override; + +private: + void printIndented(std::ostream& _stream, std::string const& _output, std::string const& _linePrefix = "") const; + bool parse(std::ostream& _stream, std::string const& _linePrefix, bool const _formatted); + void disambiguate(); + + static void printErrors(std::ostream& _stream, solidity::ErrorList const& _errors, solidity::Scanner const& _scanner); + + std::string m_source; + bool m_yul = false; + std::string m_optimizerStep; + std::string m_expectation; + + std::shared_ptr m_ast; + std::shared_ptr m_analysisInfo; + std::string m_obtainedResult; +}; + +} +} +} diff --git a/test/libsolidity/TestCase.cpp b/test/libsolidity/TestCase.cpp index 6c4e0aeac..179722698 100644 --- a/test/libsolidity/TestCase.cpp +++ b/test/libsolidity/TestCase.cpp @@ -29,7 +29,8 @@ using namespace std; bool TestCase::isTestFilename(boost::filesystem::path const& _filename) { - return _filename.extension().string() == ".sol" && + string extension = _filename.extension().string(); + return (extension == ".sol" || extension == ".yul") && !boost::starts_with(_filename.string(), "~") && !boost::starts_with(_filename.string(), "."); } diff --git a/test/tools/CMakeLists.txt b/test/tools/CMakeLists.txt index 2f107d39c..bb7adc137 100644 --- a/test/tools/CMakeLists.txt +++ b/test/tools/CMakeLists.txt @@ -3,5 +3,5 @@ target_link_libraries(solfuzzer PRIVATE libsolc evmasm ${Boost_PROGRAM_OPTIONS_L add_executable(isoltest isoltest.cpp ../Options.cpp ../Common.cpp ../libsolidity/TestCase.cpp ../libsolidity/SyntaxTest.cpp ../libsolidity/AnalysisFramework.cpp ../libsolidity/SolidityExecutionFramework.cpp ../ExecutionFramework.cpp - ../RPCSession.cpp ../libsolidity/ASTJSONTest.cpp) + ../RPCSession.cpp ../libsolidity/ASTJSONTest.cpp ../libjulia/YulOptimizerTest.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 index dac87d1c0..5134fe4fd 100644 --- a/test/tools/isoltest.cpp +++ b/test/tools/isoltest.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -371,6 +372,8 @@ Allowed options)", TestStats global_stats{0, 0}; + // Actually run the tests. + // If you add new tests here, you also have to add them in boostTest.cpp if (auto stats = runTestSuite("Syntax", testPath / "libsolidity", "syntaxTests", SyntaxTest::create, formatted)) global_stats += *stats; else @@ -381,6 +384,17 @@ Allowed options)", else return 1; + if (auto stats = runTestSuite( + "Yul Optimizer", + testPath / "libjulia", + "yulOptimizerTests", + julia::test::YulOptimizerTest::create, + formatted + )) + global_stats += *stats; + else + return 1; + cout << endl << "Summary: "; FormattedScope(cout, formatted, {BOLD, global_stats ? GREEN : RED}) << global_stats.successCount << "/" << global_stats.testCount; From 6e32a1becb90728f3a3bcbbc83a8e6c94f5e000c Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 10 Oct 2018 15:15:38 +0200 Subject: [PATCH 2/4] Disambiguator tests. --- .../disambiguator/for_statement.yul | 28 +++++++++++++++++++ .../disambiguator/funtion_call.yul | 22 +++++++++++++++ .../disambiguator/if_statement.yul | 22 +++++++++++++++ .../yulOptimizerTests/disambiguator/smoke.yul | 5 ++++ .../disambiguator/smoke_yul.yul | 6 ++++ .../disambiguator/switch_statement.yul | 27 ++++++++++++++++++ .../disambiguator/variables.yul | 12 ++++++++ .../disambiguator/variables_clash.yul | 13 +++++++++ .../variables_inside_functions.yul | 24 ++++++++++++++++ 9 files changed, 159 insertions(+) create mode 100644 test/libjulia/yulOptimizerTests/disambiguator/for_statement.yul create mode 100644 test/libjulia/yulOptimizerTests/disambiguator/funtion_call.yul create mode 100644 test/libjulia/yulOptimizerTests/disambiguator/if_statement.yul create mode 100644 test/libjulia/yulOptimizerTests/disambiguator/smoke.yul create mode 100644 test/libjulia/yulOptimizerTests/disambiguator/smoke_yul.yul create mode 100644 test/libjulia/yulOptimizerTests/disambiguator/switch_statement.yul create mode 100644 test/libjulia/yulOptimizerTests/disambiguator/variables.yul create mode 100644 test/libjulia/yulOptimizerTests/disambiguator/variables_clash.yul create mode 100644 test/libjulia/yulOptimizerTests/disambiguator/variables_inside_functions.yul diff --git a/test/libjulia/yulOptimizerTests/disambiguator/for_statement.yul b/test/libjulia/yulOptimizerTests/disambiguator/for_statement.yul new file mode 100644 index 000000000..0d2a38c58 --- /dev/null +++ b/test/libjulia/yulOptimizerTests/disambiguator/for_statement.yul @@ -0,0 +1,28 @@ +// yul +{ + { let a:u256, b:u256 } + { + for { let a:u256 } a { a := a } { + let b:u256 := a + } + } +} +// ---- +// disambiguator +// { +// { +// let a:u256, b:u256 +// } +// { +// for { +// let a_1:u256 +// } +// a_1 +// { +// a_1 := a_1 +// } +// { +// let b_1:u256 := a_1 +// } +// } +// } diff --git a/test/libjulia/yulOptimizerTests/disambiguator/funtion_call.yul b/test/libjulia/yulOptimizerTests/disambiguator/funtion_call.yul new file mode 100644 index 000000000..f917bb68e --- /dev/null +++ b/test/libjulia/yulOptimizerTests/disambiguator/funtion_call.yul @@ -0,0 +1,22 @@ +// yul +{ + { let a:u256, b:u256, c:u256, d:u256, f:u256 } + { + function f(a:u256) -> c:u256, d:u256 { + let b:u256, c_1:u256 := f(a) + } + } +} +// ---- +// disambiguator +// { +// { +// let a:u256, b:u256, c:u256, d:u256, f:u256 +// } +// { +// function f_1(a_1:u256) -> c_1:u256, d_1:u256 +// { +// let b_1:u256, c_1_1:u256 := f_1(a_1) +// } +// } +// } diff --git a/test/libjulia/yulOptimizerTests/disambiguator/if_statement.yul b/test/libjulia/yulOptimizerTests/disambiguator/if_statement.yul new file mode 100644 index 000000000..14f537579 --- /dev/null +++ b/test/libjulia/yulOptimizerTests/disambiguator/if_statement.yul @@ -0,0 +1,22 @@ +// yul +{ + { let a:u256, b:u256, c:u256 } + { + let a:bool + if a { let b:bool := a } + } +} +// ---- +// disambiguator +// { +// { +// let a:u256, b:u256, c:u256 +// } +// { +// let a_1:bool +// if a_1 +// { +// let b_1:bool := a_1 +// } +// } +// } diff --git a/test/libjulia/yulOptimizerTests/disambiguator/smoke.yul b/test/libjulia/yulOptimizerTests/disambiguator/smoke.yul new file mode 100644 index 000000000..d6cd8a619 --- /dev/null +++ b/test/libjulia/yulOptimizerTests/disambiguator/smoke.yul @@ -0,0 +1,5 @@ +{ } +// ---- +// disambiguator +// { +// } diff --git a/test/libjulia/yulOptimizerTests/disambiguator/smoke_yul.yul b/test/libjulia/yulOptimizerTests/disambiguator/smoke_yul.yul new file mode 100644 index 000000000..e55f4cd3e --- /dev/null +++ b/test/libjulia/yulOptimizerTests/disambiguator/smoke_yul.yul @@ -0,0 +1,6 @@ +// yul +{ } +// ---- +// disambiguator +// { +// } diff --git a/test/libjulia/yulOptimizerTests/disambiguator/switch_statement.yul b/test/libjulia/yulOptimizerTests/disambiguator/switch_statement.yul new file mode 100644 index 000000000..340ecccf8 --- /dev/null +++ b/test/libjulia/yulOptimizerTests/disambiguator/switch_statement.yul @@ -0,0 +1,27 @@ +// yul +{ + { let a:u256, b:u256, c:u256 } + { + let a:u256 + switch a + case 0:u256 { let b:u256 := a } + default { let c:u256 := a } + } +} +// ---- +// disambiguator +// { +// { +// let a:u256, b:u256, c:u256 +// } +// { +// let a_1:u256 +// switch a_1 +// case 0:u256 { +// let b_1:u256 := a_1 +// } +// default { +// let c_1:u256 := a_1 +// } +// } +// } diff --git a/test/libjulia/yulOptimizerTests/disambiguator/variables.yul b/test/libjulia/yulOptimizerTests/disambiguator/variables.yul new file mode 100644 index 000000000..65bd4c8f8 --- /dev/null +++ b/test/libjulia/yulOptimizerTests/disambiguator/variables.yul @@ -0,0 +1,12 @@ +// yul +{ { let a:u256 } { let a:u256 } } +// ---- +// disambiguator +// { +// { +// let a:u256 +// } +// { +// let a_1:u256 +// } +// } diff --git a/test/libjulia/yulOptimizerTests/disambiguator/variables_clash.yul b/test/libjulia/yulOptimizerTests/disambiguator/variables_clash.yul new file mode 100644 index 000000000..e462442a9 --- /dev/null +++ b/test/libjulia/yulOptimizerTests/disambiguator/variables_clash.yul @@ -0,0 +1,13 @@ +// yul +{ { let a:u256 let a_1:u256 } { let a:u256 } } +// ---- +// disambiguator +// { +// { +// let a:u256 +// let a_1:u256 +// } +// { +// let a_2:u256 +// } +// } diff --git a/test/libjulia/yulOptimizerTests/disambiguator/variables_inside_functions.yul b/test/libjulia/yulOptimizerTests/disambiguator/variables_inside_functions.yul new file mode 100644 index 000000000..e80959f69 --- /dev/null +++ b/test/libjulia/yulOptimizerTests/disambiguator/variables_inside_functions.yul @@ -0,0 +1,24 @@ +// yul +{ + { let c:u256 let b:u256 } + function f(a:u256, c:u256) -> b:u256 { let x:u256 } + { + let a:u256 let x:u256 + } +} +// ---- +// disambiguator +// { +// { +// let c:u256 +// let b:u256 +// } +// function f(a:u256, c_1:u256) -> b_1:u256 +// { +// let x:u256 +// } +// { +// let a_1:u256 +// let x_1:u256 +// } +// } From a53d942da5074f237f561ef52eff20fe3da41d0e Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 10 Oct 2018 15:15:58 +0200 Subject: [PATCH 3/4] Remove old disambiguator test. --- test/libjulia/Disambiguator.cpp | 105 -------------------------------- 1 file changed, 105 deletions(-) delete mode 100644 test/libjulia/Disambiguator.cpp diff --git a/test/libjulia/Disambiguator.cpp b/test/libjulia/Disambiguator.cpp deleted file mode 100644 index 48e02c7e0..000000000 --- a/test/libjulia/Disambiguator.cpp +++ /dev/null @@ -1,105 +0,0 @@ -/* - 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 . -*/ -/** - * @date 2017 - * Unit tests for the Yul name disambiguator. - */ - -#include - -#include - -#include - -using namespace std; -using namespace dev::julia::test; -using namespace dev::solidity; - -#define CHECK(_original, _expectation)\ -do\ -{\ - assembly::AsmPrinter p(true);\ - string result = p(disambiguate(_original));\ - BOOST_CHECK_EQUAL(result, format(_expectation));\ - BOOST_CHECK_EQUAL(result, p(disambiguate(result)));\ -}\ -while(false) - -BOOST_AUTO_TEST_SUITE(YulDisambiguator) - -BOOST_AUTO_TEST_CASE(smoke_test) -{ - CHECK("{ }", "{ }"); -} - -BOOST_AUTO_TEST_CASE(variables) -{ - CHECK( - "{ { let a:u256 } { let a:u256 } }", - "{ { let a:u256 } { let a_1:u256 } }" - ); -} - -BOOST_AUTO_TEST_CASE(variables_clash) -{ - CHECK( - "{ { let a:u256 let a_1:u256 } { let a:u256 } }", - "{ { let a:u256 let a_1:u256 } { let a_2:u256 } }" - ); -} - -BOOST_AUTO_TEST_CASE(variables_inside_functions) -{ - CHECK( - "{ { let c:u256 let b:u256 } function f(a:u256, c:u256) -> b:u256 { let x:u256 } { let a:u256 let x:u256 } }", - "{ { let c:u256 let b:u256 } function f(a:u256, c_1:u256) -> b_1:u256 { let x:u256 } { let a_1:u256 let x_1:u256 } }" - ); -} - -BOOST_AUTO_TEST_CASE(function_call) -{ - CHECK( - "{ { let a:u256, b:u256, c:u256, d:u256, f:u256 } { function f(a:u256) -> c:u256, d:u256 { let b:u256, c_1:u256 := f(a) } } }", - "{ { let a:u256, b:u256, c:u256, d:u256, f:u256 } { function f_1(a_1:u256) -> c_1:u256, d_1:u256 { let b_1:u256, c_1_1:u256 := f_1(a_1) } } }" - ); -} - -BOOST_AUTO_TEST_CASE(for_statement) -{ - CHECK( - "{ { let a:u256, b:u256 } { for { let a:u256 } a { a := a } { let b:u256 := a } } }", - "{ { let a:u256, b:u256 } { for { let a_1:u256 } a_1 { a_1 := a_1 } { let b_1:u256 := a_1 } } }" - ); -} - -BOOST_AUTO_TEST_CASE(switch_statement) -{ - CHECK( - "{ { let a:u256, b:u256, c:u256 } { let a:u256 switch a case 0:u256 { let b:u256 := a } default { let c:u256 := a } } }", - "{ { let a:u256, b:u256, c:u256 } { let a_1:u256 switch a_1 case 0:u256 { let b_1:u256 := a_1 } default { let c_1:u256 := a_1 } } }" - ); -} - -BOOST_AUTO_TEST_CASE(if_statement) -{ - CHECK( - "{ { let a:u256, b:u256, c:u256 } { let a:bool if a { let b:bool := a } } }", - "{ { let a:u256, b:u256, c:u256 } { let a_1:bool if a_1 { let b_1:bool := a_1 } } }" - ); -} - -BOOST_AUTO_TEST_SUITE_END() From e62343c60bcd4dcf8fc2c43355542126b2c7f343 Mon Sep 17 00:00:00 2001 From: chriseth Date: Thu, 11 Oct 2018 17:01:06 +0200 Subject: [PATCH 4/4] Store test results as artifacts. --- .circleci/config.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index f8e380d9c..c975740d8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -233,6 +233,9 @@ jobs: command: codecov --flags all --gcov-root build - store_test_results: path: test_results/ + - store_artifacts: + path: test_results/ + destination: test_results/ test_x86_mac: macos: @@ -254,6 +257,9 @@ jobs: - run: *run_tests - store_test_results: path: test_results/ + - store_artifacts: + path: test_results/ + destination: test_results/ docs: docker: