diff --git a/libraries/chain/include/eosio/chain/controller.hpp b/libraries/chain/include/eosio/chain/controller.hpp index 611f7fd52c9..96fdb648af7 100644 --- a/libraries/chain/include/eosio/chain/controller.hpp +++ b/libraries/chain/include/eosio/chain/controller.hpp @@ -12,6 +12,7 @@ #include #include +struct test_chain; namespace chainbase { class database; } @@ -398,6 +399,7 @@ namespace eosio { namespace chain { private: friend class apply_context; friend class transaction_context; + friend struct ::test_chain; friend std::unique_ptr db_util::create_kv_context(const controller&, name,const kv_resource_manager&, const kv_database_config&); chainbase::database& mutable_db()const; diff --git a/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/intrinsic_mapping.hpp b/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/intrinsic_mapping.hpp index be288c20942..68b5f734ab9 100644 --- a/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/intrinsic_mapping.hpp +++ b/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/intrinsic_mapping.hpp @@ -276,7 +276,9 @@ constexpr auto intrinsic_table = boost::hana::make_tuple( "env.push_data"_s, "env.print_time_us"_s, "env.get_input_data"_s, - "env.set_output_data"_s + "env.set_output_data"_s, + "env.coverage_getinc"_s, + "env.coverage_dump"_s ); }}} \ No newline at end of file diff --git a/libraries/chain/include/eosio/chain/webassembly/interface.hpp b/libraries/chain/include/eosio/chain/webassembly/interface.hpp index c6dc44c2976..f86f00cb973 100644 --- a/libraries/chain/include/eosio/chain/webassembly/interface.hpp +++ b/libraries/chain/include/eosio/chain/webassembly/interface.hpp @@ -1897,6 +1897,10 @@ namespace eosio { namespace chain { namespace webassembly { int32_t __lttf2(uint64_t, uint64_t, uint64_t, uint64_t) const; int32_t __unordtf2(uint64_t, uint64_t, uint64_t, uint64_t) const; + // code coverage support functions + uint32_t coverage_getinc(uint64_t code, uint32_t file_num, uint32_t func_or_line_num, uint32_t mode, bool inc); + uint64_t coverage_dump(uint64_t code, uint32_t file_num, span file_name, uint32_t max, bool append, uint32_t mode, bool reset); + private: apply_context& context; }; diff --git a/libraries/chain/include/eosio/chain/webassembly/preconditions.hpp b/libraries/chain/include/eosio/chain/webassembly/preconditions.hpp index 3416557904b..70f52ca3c10 100644 --- a/libraries/chain/include/eosio/chain/webassembly/preconditions.hpp +++ b/libraries/chain/include/eosio/chain/webassembly/preconditions.hpp @@ -3,6 +3,9 @@ #include #include #include +#ifdef EOSIO_EOS_VM_OC_RUNTIME_ENABLED +#include +#endif #include #include @@ -167,4 +170,16 @@ namespace eosio { namespace chain { namespace webassembly { static_assert( are_whitelisted_legacy_types_v...>, "legacy whitelisted type violation"); })); + template + struct host_function_registrator { + template + constexpr host_function_registrator(Mod mod_name, Name fn_name) { + using rhf_t = eos_vm_host_functions_t; + rhf_t::add(mod_name.c_str(), fn_name.c_str()); + #ifdef EOSIO_EOS_VM_OC_RUNTIME_ENABLED + eosvmoc::register_eosvm_oc>(mod_name + BOOST_HANA_STRING(".") + fn_name); + #endif + } + }; + }}} // ns eosio::chain::webassembly diff --git a/libraries/chain/webassembly/runtimes/eos-vm.cpp b/libraries/chain/webassembly/runtimes/eos-vm.cpp index 16b10c437cf..4c28b6791a3 100644 --- a/libraries/chain/webassembly/runtimes/eos-vm.cpp +++ b/libraries/chain/webassembly/runtimes/eos-vm.cpp @@ -7,11 +7,7 @@ //eos-vm includes #include #include -#ifdef EOSIO_EOS_VM_OC_RUNTIME_ENABLED -#include -#endif #include -#include namespace eosio { namespace chain { namespace webassembly { namespace eos_vm_runtime { @@ -289,18 +285,6 @@ std::unique_ptr eos_vm_profile_runtime::inst } -template -struct host_function_registrator { - template - constexpr host_function_registrator(Mod mod_name, Name fn_name) { - using rhf_t = eos_vm_host_functions_t; - rhf_t::add(mod_name.c_str(), fn_name.c_str()); -#ifdef EOSIO_EOS_VM_OC_RUNTIME_ENABLED - eosvmoc::register_eosvm_oc>(mod_name + BOOST_HANA_STRING(".") + fn_name); -#endif - } -}; - #define REGISTER_HOST_FUNCTION(NAME, ...) \ static host_function_registrator<&interface::NAME, core_precondition, context_aware_check, ##__VA_ARGS__> \ NAME##_registrator_impl() { \ diff --git a/libraries/rodeos/include/b1/rodeos/callbacks/coverage.hpp b/libraries/rodeos/include/b1/rodeos/callbacks/coverage.hpp new file mode 100644 index 00000000000..188ca0a9eec --- /dev/null +++ b/libraries/rodeos/include/b1/rodeos/callbacks/coverage.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include + +namespace b1::rodeos { + +using eosio::coverage::coverage_maps; +using eosio::coverage::coverage_mode; + +constexpr auto rodeos_n = eosio::name{"rodeos"}.value; + +struct coverage_state { +}; + +template +struct coverage_callbacks { + Derived& derived() { return static_cast(*this); } + + uint32_t coverage_getinc(uint64_t code, uint32_t file_num, uint32_t func_or_line_num, uint32_t mode, bool inc) { + auto cov_mode = static_cast(mode); + if(inc) { + if (cov_mode == coverage_mode::func) { + eosio::coverage::coverage_inc_cnt(code, file_num, func_or_line_num, coverage_maps::instance().funcnt_map); + } else if (cov_mode == coverage_mode::line) { + eosio::coverage::coverage_inc_cnt(code, file_num, func_or_line_num, coverage_maps::instance().linecnt_map); + } + } + else { + if (cov_mode == coverage_mode::func) { + return eosio::coverage::coverage_get_cnt(code, file_num, func_or_line_num, coverage_maps::instance().funcnt_map); + } + else if (cov_mode == coverage_mode::line) { + return eosio::coverage::coverage_get_cnt(code, file_num, func_or_line_num, coverage_maps::instance().linecnt_map); + } + } + return 0; + } + + uint64_t coverage_dump(uint64_t code, uint32_t file_num, eosio::vm::span file_name, uint32_t max, bool append, uint32_t mode, bool reset) { + auto cov_mode = static_cast(mode); + if (reset) { + coverage_maps::instance().funcnt_map.clear(); + coverage_maps::instance().linecnt_map.clear(); + } + else if (cov_mode == coverage_mode::func) { + return eosio::coverage::coverage_dump(code, file_num, file_name.data(), file_name.size(), max, append, coverage_maps::instance().funcnt_map); + } + else if (cov_mode == coverage_mode::line) { + return eosio::coverage::coverage_dump(code, file_num, file_name.data(), file_name.size(), max, append, coverage_maps::instance().linecnt_map); + } + return 0; + } + + template + static void register_callbacks() { + // todo: preconditions + RODEOS_REGISTER_CALLBACK(Rft, Derived, coverage_getinc); + RODEOS_REGISTER_CALLBACK(Rft, Derived, coverage_dump); + } +}; // coverage_callbacks + +} // namespace b1::rodeos \ No newline at end of file diff --git a/libraries/rodeos/include/b1/rodeos/filter.hpp b/libraries/rodeos/include/b1/rodeos/filter.hpp index b139ac7ee2f..2374704257f 100644 --- a/libraries/rodeos/include/b1/rodeos/filter.hpp +++ b/libraries/rodeos/include/b1/rodeos/filter.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #ifdef EOSIO_EOS_VM_OC_RUNTIME_ENABLED # include # include @@ -65,19 +66,23 @@ struct callbacks : b1::rodeos::chaindb_callbacks, b1::rodeos::filter_callbacks, b1::rodeos::memory_callbacks, b1::rodeos::unimplemented_callbacks, - b1::rodeos::system_callbacks { + b1::rodeos::system_callbacks, + b1::rodeos::coverage_callbacks { filter::filter_state& filter_state; b1::rodeos::chaindb_state& chaindb_state; b1::rodeos::db_view_state& db_view_state; + b1::rodeos::coverage_state& coverage_state; callbacks(filter::filter_state& filter_state, b1::rodeos::chaindb_state& chaindb_state, - b1::rodeos::db_view_state& db_view_state) - : filter_state{ filter_state }, chaindb_state{ chaindb_state }, db_view_state{ db_view_state } {} + b1::rodeos::db_view_state& db_view_state, b1::rodeos::coverage_state& coverage_state) + : filter_state{ filter_state }, chaindb_state{ chaindb_state }, db_view_state{ db_view_state } + , coverage_state{ coverage_state } {} auto& get_state() { return filter_state; } auto& get_filter_callback_state() { return filter_state; } auto& get_chaindb_state() { return chaindb_state; } auto& get_db_view_state() { return db_view_state; } + auto& get_coverage_state() { return coverage_state; } }; inline void register_callbacks() { @@ -92,6 +97,7 @@ inline void register_callbacks() { b1::rodeos::memory_callbacks::register_callbacks(); b1::rodeos::system_callbacks::register_callbacks(); b1::rodeos::unimplemented_callbacks::register_callbacks(); + b1::rodeos::coverage_callbacks::register_callbacks(); } } // namespace b1::rodeos::filter diff --git a/libraries/rodeos/include/eosio/coverage.hpp b/libraries/rodeos/include/eosio/coverage.hpp new file mode 100644 index 00000000000..3816dd897e7 --- /dev/null +++ b/libraries/rodeos/include/eosio/coverage.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include +#include + +using cov_map_t = std::unordered_map > >; + +// rodeos lib is not the ideal location for this as it is also used by eosio-tester but rodeos lib keeps it out of nodeos +namespace eosio { +namespace coverage { + +template +class coverage_maps { + public: + static coverage_maps& instance() { + static coverage_maps instance; + return instance; + } + coverage_maps(const coverage_maps&) = delete; + void operator=(const coverage_maps&) = delete; + + cov_map_t funcnt_map; + cov_map_t linecnt_map; + private: + coverage_maps() = default; +}; + +enum class coverage_mode : uint32_t { + func=0, + line=1 +}; + +inline void coverage_inc_cnt( uint64_t code, uint32_t file_num, uint32_t func_or_line_num, cov_map_t& cov_map) { + auto& code_map = cov_map[code]; + auto& cnt_map = code_map[file_num]; + cnt_map[func_or_line_num]++; +} + +inline uint32_t coverage_get_cnt( uint64_t code, uint32_t file_num, uint32_t func_or_line_num, cov_map_t& cov_map) { + auto& code_map = cov_map[code]; + auto& cnt_map = code_map[file_num]; + return cnt_map[func_or_line_num]; +} + +// dump coverage output (function or line) to binary file +// if code == 0, begin at first code in the map +// max is only checked for every code, so it is possible to exceed the number +// if max == 0, then only dump coverage for specific code and specific file_num +// in theis case code must be > 0 +// returns the next code, or 0 if at end +inline uint64_t coverage_dump(uint64_t code, uint32_t file_num, const char* file_name, uint32_t file_name_size, uint32_t max, bool append, cov_map_t& cov_map) { + std::ofstream out_bin_file; + auto flags = std::ofstream::out | std::ofstream::binary; + if (append) + flags = flags | std::ofstream::app; + else + flags = flags | std::ofstream::trunc; + + ilog("coverage_dump_funcnt: file_name= ${f} max= ${max} ${app}", ("f", file_name)("nax", max)("app", append)); + out_bin_file.open(file_name, flags ); + uint32_t i = 0; + auto code_itr = cov_map.begin(); + if (max == 0 && code == 0) { + elog("coverage_dump_funcnt: when max == 0, code must be > 0"); + return 0; + } + if (code > 0) { + code_itr = cov_map.find(code); + } + while (code_itr != cov_map.end() && (max == 0 || i < max)) { + auto codenum = code_itr->first; + auto& filenum_map = code_itr->second; + auto filenum_itr = filenum_map.begin(); + if (max == 0) { + filenum_itr = filenum_map.find(file_num); + } + while (filenum_itr != filenum_map.end()) { + auto filenum = filenum_itr->first; + auto& funcnum_map = filenum_itr->second; + for (const auto& funcnum_itr : funcnum_map) { + auto func_or_line_num = funcnum_itr.first; + auto calls = funcnum_itr.second; + out_bin_file.write(reinterpret_cast(&codenum), sizeof(code)); + out_bin_file.write(reinterpret_cast(&filenum), sizeof(filenum)); + out_bin_file.write(reinterpret_cast(&func_or_line_num), sizeof(func_or_line_num)); + out_bin_file.write(reinterpret_cast(&calls), sizeof(calls)); + ++i; + } + ++filenum_itr; + if (max == 0) + break; + } + ++code_itr; + if (max == 0) + break; + } + + out_bin_file.flush(); + out_bin_file.close(); + + uint64_t r = 0; + if(code_itr != cov_map.end()) + r = code_itr->first; + return r; +} + +} // namespace coverage +} // namespace eosio diff --git a/libraries/rodeos/rodeos.cpp b/libraries/rodeos/rodeos.cpp index e3be40cdcc8..2db5483aa7f 100644 --- a/libraries/rodeos/rodeos.cpp +++ b/libraries/rodeos/rodeos.cpp @@ -337,8 +337,9 @@ void rodeos_filter::process(rodeos_db_snapshot& snapshot, const ship_protocol::g snapshot.check_write(result); chaindb_state chaindb_state; db_view_state view_state{ name, *snapshot.db, *snapshot.write_session, snapshot.partition->contract_kv_prefix }; + coverage_state coverage_state; view_state.kv_state.enable_write = true; - filter::callbacks cb{ *filter_state, chaindb_state, view_state }; + filter::callbacks cb{ *filter_state, chaindb_state, view_state, coverage_state }; filter_state->max_console_size = 10000; filter_state->console.clear(); filter_state->input_data = bin; diff --git a/libraries/rodeos/wasm_ql.cpp b/libraries/rodeos/wasm_ql.cpp index f3f11303626..d72b3ad859f 100644 --- a/libraries/rodeos/wasm_ql.cpp +++ b/libraries/rodeos/wasm_ql.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -72,18 +73,22 @@ struct callbacks : action_callbacks, db_callbacks, memory_callbacks, query_callbacks, - unimplemented_callbacks { + unimplemented_callbacks, + coverage_callbacks { wasm_ql::thread_state& thread_state; rodeos::chaindb_state& chaindb_state; rodeos::db_view_state& db_view_state; + rodeos::coverage_state& coverage_state; callbacks(wasm_ql::thread_state& thread_state, rodeos::chaindb_state& chaindb_state, - rodeos::db_view_state& db_view_state) - : thread_state{ thread_state }, chaindb_state{ chaindb_state }, db_view_state{ db_view_state } {} + rodeos::db_view_state& db_view_state, rodeos::coverage_state& coverage_state) + : thread_state{ thread_state }, chaindb_state{ chaindb_state }, db_view_state{ db_view_state } + , coverage_state{ coverage_state } {} auto& get_state() { return thread_state; } auto& get_chaindb_state() { return chaindb_state; } auto& get_db_view_state() { return db_view_state; } + auto& get_coverage_state() { return coverage_state; } }; std::once_flag registered_callbacks; @@ -99,6 +104,7 @@ void register_callbacks() { memory_callbacks::register_callbacks(); query_callbacks::register_callbacks(); unimplemented_callbacks::register_callbacks(); + coverage_callbacks::register_callbacks(); } struct backend_entry { @@ -273,7 +279,8 @@ void run_action(wasm_ql::thread_state& thread_state, const std::vector& co thread_state.block_info.reset(); chaindb_state chaindb_state; - callbacks cb{ thread_state, chaindb_state, db_view_state }; + coverage_state coverage_state; + callbacks cb{ thread_state, chaindb_state, db_view_state, coverage_state }; entry->backend->set_wasm_allocator(&thread_state.wa); try { diff --git a/programs/eosio-tester/main.cpp b/programs/eosio-tester/main.cpp index a0b594eddd7..ee0f174bd7d 100644 --- a/programs/eosio-tester/main.cpp +++ b/programs/eosio-tester/main.cpp @@ -1,6 +1,11 @@ +#include #include #include #include +#include +#include +#include +#include #include #include #include @@ -10,6 +15,7 @@ #include #include #include +#include #undef N @@ -97,6 +103,65 @@ struct transaction_checktime_factory { }; }; // namespace +// Defined here to keep it out of nodeos +namespace eosio::chain::webassembly { + +#define REGISTER_HOST_FUNCTION(NAME, ...) \ + static host_function_registrator<&interface::NAME, core_precondition, context_aware_check, ##__VA_ARGS__> \ + NAME##_registrator_impl() { \ + return {BOOST_HANA_STRING("env"), BOOST_HANA_STRING(#NAME)}; \ + } \ + inline static auto NAME##_registrator = NAME##_registrator_impl(); + +// code coverage API +REGISTER_HOST_FUNCTION(coverage_getinc) +REGISTER_HOST_FUNCTION(coverage_dump) + +using eosio::coverage::coverage_maps; +using eosio::coverage::coverage_mode; + +constexpr auto testchain_n = eosio::name{"testchain"}.value; + +uint32_t interface::coverage_getinc(uint64_t code, uint32_t file_num, uint32_t func_or_line_num, uint32_t mode, bool inc) { + uint32_t r = 0; + auto cov_mode = static_cast(mode); + if(inc) { + if (cov_mode == coverage_mode::func) { + eosio::coverage::coverage_inc_cnt(code, file_num, func_or_line_num, coverage_maps::instance().funcnt_map); + } else if (cov_mode == coverage_mode::line) { + eosio::coverage::coverage_inc_cnt(code, file_num, func_or_line_num, coverage_maps::instance().linecnt_map); + } + } + else { + if (cov_mode == coverage_mode::func) { + r = eosio::coverage::coverage_get_cnt(code, file_num, func_or_line_num, coverage_maps::instance().funcnt_map); + } + else if (cov_mode == coverage_mode::line) { + r = eosio::coverage::coverage_get_cnt(code, file_num, func_or_line_num, coverage_maps::instance().linecnt_map); + } + } + return r; +} + +uint64_t interface::coverage_dump(uint64_t code, uint32_t file_num, eosio::vm::span file_name, uint32_t max, bool append, uint32_t mode, bool reset) { + auto cov_mode = static_cast(mode); + if (reset) { + coverage_maps::instance().funcnt_map.clear(); + coverage_maps::instance().linecnt_map.clear(); + return 0; + } + if (cov_mode == coverage_mode::func) { + return eosio::coverage::coverage_dump(code, file_num, file_name.data(), file_name.size(), max, append, coverage_maps::instance().funcnt_map); + } + else if (cov_mode == coverage_mode::line) { + return eosio::coverage::coverage_dump(code, file_num, file_name.data(), file_name.size(), max, append, coverage_maps::instance().linecnt_map); + } + return 0; +} + +} // namespace eosio::chain::webassembly + + struct intrinsic_context { eosio::chain::controller& control; eosio::chain::packed_transaction trx; @@ -179,6 +244,12 @@ struct test_chain { test_chain(const char* snapshot) { eosio::chain::genesis_state genesis; + // increase default limits to allow for code coverage + genesis.initial_configuration.max_transaction_net_usage *= 2; + genesis.initial_configuration.max_block_net_usage *= 2; + genesis.initial_configuration.max_block_cpu_usage *= 2; + genesis.initial_configuration.max_transaction_cpu_usage *= 2; + genesis.initial_timestamp = fc::time_point::from_iso_string("2020-01-01T00:00:00.000"); cfg = std::make_unique(); cfg->blog.log_dir = dir.path() / "blocks"; @@ -223,6 +294,10 @@ struct test_chain { control->start_block(control->head_block_time() + fc::microseconds(block_interval_us), 0, { *control->get_protocol_feature_manager().get_builtin_digest( eosio::chain::builtin_protocol_feature_t::preactivate_feature) }); + control->mutable_db().modify( control->mutable_db().get(), [&]( auto& ps ) { + eosio::chain::add_intrinsic_to_whitelist( ps.whitelisted_intrinsics, "coverage_getinc" ); + eosio::chain::add_intrinsic_to_whitelist( ps.whitelisted_intrinsics, "coverage_dump" ); + } ); } } @@ -1120,7 +1195,6 @@ struct callbacks { } } - }; // callbacks #define DB_REGISTER_SECONDARY(IDX) \