From a3f5ed036016ab8828584395a23e87bcdf686cda Mon Sep 17 00:00:00 2001 From: cryptozoidberg Date: Wed, 24 Dec 2025 10:44:56 -0400 Subject: [PATCH 01/28] Add proposals for new Confidential Assets Added proposals for adding Wrapped Solana, Wrapped TON, and Wrapped Bitcoin Cash to the Zano asset whitelist. --- governance/governance.md | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/governance/governance.md b/governance/governance.md index 12118e588..71a83e678 100644 --- a/governance/governance.md +++ b/governance/governance.md @@ -52,6 +52,47 @@ Asset id: 93da681503353509367e241cda3234299dedbbad9ec851de31e900490807bf0c **End** 3412904 +--- + +### ZAP5: SOLX (Wrapped Solana) + +**Description** This proposal asks the Zano community to vote on whether to officially add the following Confidential Asset (CAs) to the Zano asset whitelist: + +SOLX (Wrapped Solana) +Asset id: 65b3bc549c8bc2c773781d5436f25f7af84644e61baaabd675d9867b007d17b4 + + +**Start** 3492190 + +**End** 3512350 + +--- + +### ZAP6: TONX (Wrapped TON) + +**Description** This proposal asks the Zano community to vote on whether to officially add the following Confidential Asset (CAs) to the Zano asset whitelist: + +TONX (Wrapped TON) +Asset id: bfa6609a94e39f418d9adb000f89edc7bd180fd120f1cd272201220e3070fb4f + + +**Start** 3492190 + +**End** 3512350 + +--- + +### ZAP7: BCHX (Wrapped Bitcoin Cash) + +**Description** This proposal asks the Zano community to vote on whether to officially add the following Confidential Asset (CAs) to the Zano asset whitelist: + +BCHX (Wrapped Bitcoin Cash) +Asset id: 3de9ad7243afa49e0ade6839e97a9f10a527c4958ece2fc9cb1b87a44032167d + + +**Start** 3492190 + +**End** 3512350 From b34563bba3837dcbf1ea22692e4a84baf5dddee1 Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Sat, 10 Jan 2026 15:00:37 +0300 Subject: [PATCH 02/28] parallel launch of core tests --- tests/core_tests/chaingen_main.cpp | 1835 ++++++++++++++++++++-------- 1 file changed, 1342 insertions(+), 493 deletions(-) diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index 474817437..ce9f4beb7 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -14,10 +14,20 @@ #include "random_helper.h" #include "core_state_helper.h" #include "common/db_backend_selector.h" +#include +#include +#include +#include +#include +#include +#include +#include #define TX_BLOBSIZE_CHECKER_LOG_FILENAME "get_object_blobsize(tx).log" namespace po = boost::program_options; +namespace bp = boost::process; +namespace pt = boost::property_tree; namespace { @@ -30,6 +40,39 @@ namespace const command_line::arg_descriptor arg_run_multiple_tests ("run-multiple-tests", "comma-separated list of tests to run, OR text file <@filename> containing list of tests"); const command_line::arg_descriptor arg_enable_debug_asserts ("enable-debug-asserts", "" ); const command_line::arg_descriptor arg_stop_on_fail ("stop-on-fail", ""); + const command_line::arg_descriptor arg_processes ("processes", "Number of worker processes", 1); + const command_line::arg_descriptor arg_worker_id ("worker-id", "Internal: worker index", -1); + const command_line::arg_descriptor arg_run_root ("run-root", "Internal: run root dir", ""); + + const char* JSON_WORKER_ID = "worker_id"; + const char* JSON_PROCESSES = "processes"; + const char* JSON_TESTS_COUNT = "tests_count"; + const char* JSON_UNIQUE_TESTS_COUNT = "unique_tests_count"; + const char* JSON_TOTAL_TIME_MS = "total_time_ms"; + const char* JSON_SKIP_ALL_TILL_END = "skip_all_till_the_end"; + const char* JSON_EXIT_CODE = "exit_code"; + + const char* JSON_FAILED_TESTS = "failed_tests"; + const char* JSON_TESTS_RUNNING_TIME = "tests_running_time"; + + const char* JSON_NAME = "name"; + const char* JSON_MS = "ms"; + + const char* WORKER_STDOUT_FILENAME = "worker_stdout.log"; + const char* WORKER_STDERR_FILENAME = "worker_stderr.log"; + const char* WORKER_REPORT_FILENAME = "coretests_report.json"; + + const char* ARG_WORKER_ID = "--worker-id"; + const char* ARG_WORKER_ID_EQ = "--worker-id="; + + const char* ARG_DATA_DIR = "--data-dir"; + const char* ARG_DATA_DIR_EQ = "--data-dir="; + const char* WORKER_DIR_PREFIX = "w"; + const char* TAKEN_TESTS_LOG_FILENAME = "taken_tests.log"; + + // Console output locking to avoid broken lines + static std::mutex cout_mx; + static std::mutex cerr_mx; boost::program_options::variables_map g_vm; } @@ -56,6 +99,57 @@ namespace return 1; \ } +#define REGISTER_TEST(genclass) \ + do { \ + const std::string __test_name = #genclass; \ + g_test_jobs.push_back(test_job{ \ + __test_name, \ + [&, __test_name]() -> bool { \ + if (!is_test_eligible_to_run(__test_name)) \ + return true; \ + return run_one_test_job( \ + __test_name, \ + [&]() -> bool { return generate_and_play(__test_name.c_str()); }, \ + stop_on_first_fail, \ + skip_all_till_the_end, \ + tests_count, \ + unique_tests_count, \ + failed_tests, \ + tests_running_time); \ + } \ + }); \ + } while (0) + +#define REGISTER_TEST_HF(genclass, hardfork_str_mask) \ + do { \ + const std::string __gen_name = #genclass; \ + std::vector __hardforks = parse_hardfork_str_mask(hardfork_str_mask); \ + if (__hardforks.empty()) { \ + LOG_ERROR("invalid hardforks mask: " << hardfork_str_mask << " for test " << __gen_name); \ + break; \ + } \ + for (size_t __i = 0; __i < __hardforks.size(); ++__i) \ + { \ + const size_t __hf_id = __hardforks[__i]; \ + const std::string __test_name = __gen_name + " @ HF " + epee::string_tools::num_to_string_fast(__hf_id); \ + g_test_jobs.push_back(test_job{ \ + __test_name, \ + [&, __test_name, __gen_name, __hf_id]() -> bool { \ + if (!is_hf_test_eligible_to_run(__gen_name, __hf_id)) \ + return true; \ + return run_one_test_job( \ + __test_name, \ + [&]() -> bool { return generate_and_play(__test_name.c_str(), __hf_id); }, \ + stop_on_first_fail, \ + skip_all_till_the_end, \ + tests_count, \ + unique_tests_count, \ + failed_tests, \ + tests_running_time); \ + } \ + }); \ + } \ + } while (0) std::vector parse_hardfork_str_mask(std::string s /* intentionally passing by value */) { @@ -386,24 +480,6 @@ bool gen_and_play_intermitted_by_blockchain_saveload(const char* const genclass_ return r; } - -#define GENERATE_AND_PLAY(genclass) \ - if (is_test_eligible_to_run(#genclass)) \ - { \ - TIME_MEASURE_START_MS(t); \ - ++tests_count; \ - ++unique_tests_count; \ - if (!generate_and_play(#genclass)) \ - { \ - failed_tests.insert(#genclass); \ - LOCAL_ASSERT(false); \ - if (stop_on_first_fail) \ - skip_all_till_the_end = true; \ - } \ - TIME_MEASURE_FINISH_MS(t); \ - tests_running_time.push_back(std::make_pair(#genclass, t)); \ - } - #define GENERATE_AND_PLAY_INTERMITTED_BY_BLOCKCHAIN_SAVELOAD(genclass) \ if (is_test_eligible_to_run(#genclass)) \ { \ @@ -422,37 +498,8 @@ bool gen_and_play_intermitted_by_blockchain_saveload(const char* const genclass_ tests_running_time.push_back(std::make_pair(testname, t)); \ } -#define GENERATE_AND_PLAY_HF(genclass, hardfork_str_mask) \ - if (!skip_all_till_the_end) \ - { \ - std::vector hardforks = parse_hardfork_str_mask(hardfork_str_mask); \ - CHECK_AND_ASSERT_MES(!hardforks.empty(), false, "invalid hardforks mask: " << hardfork_str_mask); \ - for(size_t i = 0; i < hardforks.size() && !skip_all_till_the_end; ++i) \ - { \ - if (!is_hf_test_eligible_to_run(#genclass, hardforks[i])) \ - continue; \ - std::string tns = std::string(#genclass) + " @ HF " + epee::string_tools::num_to_string_fast(hardforks[i]); \ - const char* testname = tns.c_str(); \ - TIME_MEASURE_START_MS(t); \ - ++tests_count; \ - if (!generate_and_play(testname, hardforks[i])) \ - { \ - failed_tests.insert(testname); \ - LOCAL_ASSERT(false); \ - if (stop_on_first_fail) \ - skip_all_till_the_end = true; \ - } \ - TIME_MEASURE_FINISH_MS(t); \ - tests_running_time.push_back(std::make_pair(testname, t)); \ - } \ - ++unique_tests_count; \ - } - - - //#define GENERATE_AND_PLAY(genclass) GENERATE_AND_PLAY_INTERMITTED_BY_BLOCKCHAIN_SAVELOAD(genclass) - #define CALL_TEST(test_name, function) \ { \ if(!function()) \ @@ -703,6 +750,31 @@ struct push_core_event_visitor : public boost::static_visitor } }; //-------------------------------------------------------------------------- +struct test_job +{ + std::string name; + std::function run; +}; + +static std::vector g_test_jobs; + +struct worker_report +{ + uint32_t worker_id = 0; + uint32_t processes = 1; + + size_t tests_count = 0; + size_t unique_tests_count = 0; + + uint64_t total_time_ms = 0; + + std::vector> tests_running_time; + std::set failed_tests; + bool skip_all_till_the_end = false; + + int exit_code = 0; +}; +//-------------------------------------------------------------------------- template inline bool replay_events_through_core(currency::core& cr, const std::vector& events, t_test_class& validator, test_core_listener* core_listener, size_t event_index_from = 0, size_t event_index_to = SIZE_MAX) { @@ -729,25 +801,6 @@ inline bool replay_events_through_core(currency::core& cr, const std::vector prepare_variables_map() -{ - boost::program_options::options_description desc("Allowed options"); - currency::core::init_options(desc); - command_line::add_arg(desc, command_line::arg_data_dir); - std::shared_ptr vm(new boost::program_options::variables_map); - bool r = command_line::handle_error_helper(desc, [&]() - { - boost::program_options::store(boost::program_options::basic_parsed_options(&desc), *vm.get()); - boost::program_options::notify(*vm.get()); - return true; - }); - - if (!r) - return nullptr; - - return vm; -} -//-------------------------------------------------------------------------- template inline bool do_replay_events(const std::vector& events, t_test_class& validator, size_t event_index_from = 0, size_t event_index_to = SIZE_MAX, bool deinit_core = true) @@ -898,25 +951,1095 @@ bool parse_cmd_specific_tests_to_run(std::unordered_multimap= prefix.size() && arg.compare(0, prefix.size(), prefix) == 0; +} + +static std::filesystem::path get_run_root_path() +{ + std::string run_root = command_line::get_arg(g_vm, arg_run_root); + if (run_root.empty()) + run_root = "chaingen_runs"; + + std::filesystem::path run_root_path(run_root); + + // Make it absolute to avoid nesting when workers use start_dir. + if (run_root_path.is_relative()) + run_root_path = std::filesystem::absolute(run_root_path); + + return run_root_path; +} + +static std::filesystem::path get_worker_dir_path(uint32_t worker_id) +{ + return get_run_root_path() / (WORKER_DIR_PREFIX+ std::to_string(worker_id)); +} + +static std::filesystem::path get_worker_taken_tests_log_path(uint32_t worker_id) +{ + return get_worker_dir_path(worker_id) / TAKEN_TESTS_LOG_FILENAME; +} + +static std::filesystem::path get_single_process_taken_tests_log_path() +{ + std::string data_dir = command_line::get_arg(g_vm, command_line::arg_data_dir); + return std::filesystem::path(data_dir) / TAKEN_TESTS_LOG_FILENAME; +} + +static void log_test_taken_by_this_process(const std::string& test_name) +{ + // Append-only log: one line per test, written when the test is about to start. + try + { + const uint32_t processes = command_line::get_arg(g_vm, arg_processes); + const int32_t worker_id = command_line::get_arg(g_vm, arg_worker_id); + + std::filesystem::path p; + + if (processes > 1 && worker_id >= 0) + { + const uint32_t wid = static_cast(worker_id); + std::filesystem::create_directories(get_worker_dir_path(wid)); + p = get_worker_taken_tests_log_path(wid); + } + else + { + std::filesystem::create_directories(command_line::get_arg(g_vm, command_line::arg_data_dir)); + p = get_single_process_taken_tests_log_path(); + } + + std::ofstream f(p, std::ios::out | std::ios::binary | std::ios::app); + if (!f.is_open()) + return; + + f << test_name << "\n"; + f.flush(); + } + catch (...) {} +} +//-------------------------------------------------------------------------- +static bool run_one_test_job( + const std::string& test_name, + const std::function& fn, + bool& stop_on_first_fail, + bool& skip_all_till_the_end, + size_t& tests_count, + size_t& unique_tests_count, + std::set& failed_tests, + std::vector>& tests_running_time) +{ + if (skip_all_till_the_end) + return true; + + TIME_MEASURE_START_MS(t); + + ++tests_count; + ++unique_tests_count; + + log_test_taken_by_this_process(test_name); + bool ok = fn(); + + if (!ok) + { + failed_tests.insert(test_name); + LOCAL_ASSERT(false); + if (stop_on_first_fail) + skip_all_till_the_end = true; + } + + TIME_MEASURE_FINISH_MS(t); + tests_running_time.push_back(std::make_pair(test_name, t)); + + return ok; +} + +static bool run_registered_tests( + bool& stop_on_first_fail, + bool& skip_all_till_the_end, + size_t& tests_count, + size_t& unique_tests_count, + std::set& failed_tests, + std::vector>& tests_running_time, + const std::function& /*is_test_eligible_to_run*/, + const std::function& /*is_hf_test_eligible_to_run*/) +{ + bool all_ok = true; + + const uint32_t processes = command_line::get_arg(g_vm, arg_processes); + const int32_t worker_id = command_line::get_arg(g_vm, arg_worker_id); + + size_t job_index = 0; + + for (auto& j : g_test_jobs) + { + if (skip_all_till_the_end) + break; + + if (processes > 1 && worker_id >= 0) + { + if ((job_index % processes) != static_cast(worker_id)) + { + ++job_index; + continue; + } + } + + bool ok = j.run(); + if (!ok) + all_ok = false; + + ++job_index; + } + + return all_ok; +} +//-------------------------------------------------------------------------- +static void register_all_tests( + bool& stop_on_first_fail, + bool& skip_all_till_the_end, + size_t& tests_count, + size_t& unique_tests_count, + std::set& failed_tests, + std::vector>& tests_running_time, + const std::function& is_test_eligible_to_run, + const std::function& is_hf_test_eligible_to_run) +{ + + g_test_jobs.clear(); + + // TODO // REGISTER_TEST(wallet_spend_form_auditable_and_track); + REGISTER_TEST(gen_block_big_major_version); + + REGISTER_TEST(pos_minting_tx_packing); + + REGISTER_TEST(multisig_wallet_test); + REGISTER_TEST(multisig_wallet_test_many_dst); + REGISTER_TEST(multisig_wallet_heterogenous_dst); + REGISTER_TEST(multisig_wallet_same_dst_addr); + REGISTER_TEST(multisig_wallet_ms_to_ms); + REGISTER_TEST(multisig_minimum_sigs); + REGISTER_TEST(multisig_and_fake_outputs); + REGISTER_TEST(multisig_and_unlock_time); + REGISTER_TEST(multisig_and_coinbase); + REGISTER_TEST(multisig_with_same_id_in_pool); + REGISTER_TEST_HF(multisig_and_checkpoints, "0"); // TODO: fix for HF 1-3 (checkpoint hash check) + REGISTER_TEST(multisig_and_checkpoints_bad_txs); + REGISTER_TEST(multisig_and_altchains); + REGISTER_TEST(multisig_out_make_and_spent_in_altchain); + REGISTER_TEST(multisig_unconfirmed_transfer_and_multiple_scan_pool_calls); + REGISTER_TEST(multisig_out_spent_in_altchain_case_b4); + REGISTER_TEST(multisig_n_participants_seq_signing); + + REGISTER_TEST(ref_by_id_basics); + REGISTER_TEST(ref_by_id_mixed_inputs_types); + + REGISTER_TEST(escrow_wallet_test); + REGISTER_TEST(escrow_w_and_fake_outputs); + REGISTER_TEST(escrow_incorrect_proposal); + REGISTER_TEST(escrow_proposal_expiration); + REGISTER_TEST(escrow_proposal_and_accept_expiration); + REGISTER_TEST(escrow_incorrect_proposal_acceptance); + REGISTER_TEST(escrow_custom_test); + REGISTER_TEST(escrow_incorrect_cancel_proposal); + REGISTER_TEST(escrow_proposal_not_enough_money); + REGISTER_TEST(escrow_cancellation_and_tx_order); + REGISTER_TEST(escrow_cancellation_proposal_expiration); + REGISTER_TEST(escrow_cancellation_acceptance_expiration); + // REGISTER_TEST(escrow_proposal_acceptance_in_alt_chain); -- work in progress + REGISTER_TEST(escrow_zero_amounts); + REGISTER_TEST(escrow_balance); + + REGISTER_TEST(escrow_altchain_meta_test<0>); + REGISTER_TEST(escrow_altchain_meta_test<1>); + REGISTER_TEST(escrow_altchain_meta_test<2>); + REGISTER_TEST(escrow_altchain_meta_test<3>); + REGISTER_TEST(escrow_altchain_meta_test<4>); + REGISTER_TEST(escrow_altchain_meta_test<5>); + REGISTER_TEST(escrow_altchain_meta_test<6>); + REGISTER_TEST(escrow_altchain_meta_test<7>); + REGISTER_TEST(escrow_altchain_meta_test<8>); + static_assert(escrow_altchain_meta_test_data<9>::empty_marker, ""); // make sure there are no sub-tests left + + REGISTER_TEST(offers_expiration_test); + REGISTER_TEST(offers_tests); + REGISTER_TEST(offers_filtering_1); + //REGISTER_TEST(offers_handling_on_chain_switching); + REGISTER_TEST(offer_removing_and_selected_output); + REGISTER_TEST(offers_multiple_update); + REGISTER_TEST(offer_sig_validity_in_update_and_cancel); + REGISTER_TEST(offer_lifecycle_via_tx_pool); + REGISTER_TEST(offers_updating_hack); + REGISTER_TEST(offer_cancellation_with_zero_fee); + + REGISTER_TEST(gen_crypted_attachments); + REGISTER_TEST(gen_checkpoints_attachments_basic); + REGISTER_TEST(gen_checkpoints_invalid_keyimage); + REGISTER_TEST(gen_checkpoints_altblock_before_and_after_cp); + REGISTER_TEST(gen_checkpoints_block_in_future); + REGISTER_TEST(gen_checkpoints_altchain_far_before_cp); + REGISTER_TEST(gen_checkpoints_block_in_future_after_cp); + REGISTER_TEST(gen_checkpoints_prun_txs_after_blockchain_load); + REGISTER_TEST(gen_checkpoints_reorganize); + REGISTER_TEST(gen_checkpoints_pos_validation_on_altchain); + REGISTER_TEST(gen_checkpoints_and_invalid_tx_to_pool); + REGISTER_TEST(gen_checkpoints_set_after_switching_to_altchain); + REGISTER_TEST_HF(gen_no_attchments_in_coinbase, "3"); + REGISTER_TEST(gen_no_attchments_in_coinbase_gentime); + + REGISTER_TEST_HF(gen_alias_tests, "3-*"); + REGISTER_TEST_HF(gen_alias_strange_data, "3-*"); + REGISTER_TEST_HF(gen_alias_concurrency_with_switch, "3-*"); + REGISTER_TEST_HF(gen_alias_same_alias_in_tx_pool, "3-*"); + REGISTER_TEST_HF(gen_alias_switch_and_tx_pool, "3-*"); + REGISTER_TEST_HF(gen_alias_update_after_addr_changed, "3-*"); + REGISTER_TEST_HF(gen_alias_blocking_reg_by_invalid_tx, "3-*"); + REGISTER_TEST_HF(gen_alias_blocking_update_by_invalid_tx, "3-*"); + REGISTER_TEST_HF(gen_alias_reg_with_locked_money, "*"); + REGISTER_TEST_HF(gen_alias_too_small_reward, "3-*"); + REGISTER_TEST_HF(gen_alias_too_much_reward, "3-*"); + REGISTER_TEST_HF(gen_alias_tx_no_outs, "*"); + REGISTER_TEST_HF(gen_alias_switch_and_check_block_template, "3-*"); + REGISTER_TEST_HF(gen_alias_too_many_regs_in_block_template, "3"); // disabled in HF4 due to tx outputs count limitation + REGISTER_TEST_HF(gen_alias_update_for_free, "3-*"); + REGISTER_TEST_HF(gen_alias_in_coinbase, "3-*"); + + REGISTER_TEST(gen_wallet_basic_transfer); + REGISTER_TEST(gen_wallet_refreshing_on_chain_switch); + REGISTER_TEST(gen_wallet_refreshing_on_chain_switch_2); + REGISTER_TEST(gen_wallet_unconfirmed_tx_from_tx_pool); + REGISTER_TEST_HF(gen_wallet_save_load_and_balance, "*"); + REGISTER_TEST_HF(gen_wallet_mine_pos_block, "3-*"); + REGISTER_TEST(gen_wallet_unconfirmed_outdated_tx); + REGISTER_TEST(gen_wallet_unlock_by_block_and_by_time); + REGISTER_TEST(gen_wallet_payment_id); + REGISTER_TEST(gen_wallet_oversized_payment_id); + REGISTER_TEST(gen_wallet_transfers_and_outdated_unconfirmed_txs); + REGISTER_TEST(gen_wallet_transfers_and_chain_switch); + REGISTER_TEST(gen_wallet_decrypted_payload_items); + REGISTER_TEST_HF(gen_wallet_alias_and_unconfirmed_txs, "3-*"); + REGISTER_TEST_HF(gen_wallet_alias_via_special_wallet_funcs, "3-*"); + REGISTER_TEST(gen_wallet_fake_outputs_randomness); + REGISTER_TEST(gen_wallet_fake_outputs_not_enough); + REGISTER_TEST(gen_wallet_offers_basic); + REGISTER_TEST(gen_wallet_offers_size_limit); + REGISTER_TEST(gen_wallet_dust_to_account); + REGISTER_TEST(gen_wallet_selecting_pos_entries); + REGISTER_TEST(gen_wallet_spending_coinstake_after_minting); + REGISTER_TEST(gen_wallet_fake_outs_while_having_too_little_own_outs); + // REGISTER_TEST(premine_wallet_test); // tests premine wallets; wallet files nedded; by demand only + REGISTER_TEST(mined_balance_wallet_test); + REGISTER_TEST(wallet_outputs_with_same_key_image); + REGISTER_TEST(wallet_unconfirmed_tx_expiration); + REGISTER_TEST(wallet_unconfimed_tx_balance); + REGISTER_TEST_HF(packing_outputs_on_pos_minting_wallet, "3"); + REGISTER_TEST_HF(wallet_watch_only_and_chain_switch, "3"); + REGISTER_TEST_HF(wallet_and_sweep_below, "3-*"); + + REGISTER_TEST(wallet_rpc_integrated_address); + REGISTER_TEST(wallet_rpc_integrated_address_transfer); + REGISTER_TEST(wallet_rpc_transfer); + REGISTER_TEST(wallet_rpc_alias_tests); + REGISTER_TEST_HF(wallet_rpc_exchange_suite, "3,4"); + REGISTER_TEST_HF(wallet_true_rpc_pos_mining, "4-*"); + REGISTER_TEST_HF(wallet_rpc_cold_signing, "3,5-*"); + // REGISTER_TEST_HF(wallet_rpc_multiple_receivers, "5-*"); work in progress -- sowle + REGISTER_TEST_HF(wallet_chain_switch_with_spending_the_same_ki, "3"); + REGISTER_TEST(wallet_sending_to_integrated_address); + REGISTER_TEST_HF(block_template_blacklist_test, "4-*"); + REGISTER_TEST_HF(wallet_rpc_hardfork_verification, "5"); + + // REGISTER_TEST(emission_test); // simulate 1 year of blockchain, too long run (1 y ~= 1 hr), by demand only + // LOG_ERROR2("print_reward_change_first_blocks.log", currency::print_reward_change_first_blocks(525601).str()); // outputs first 1 year of blocks' rewards (simplier) + + // pos tests + GENERATE_AND_PLAY_INTERMITTED_BY_BLOCKCHAIN_SAVELOAD(gen_pos_basic_tests); + // REGISTER_TEST(gen_pos_basic_tests); -- commented as this test is run intermittedly by previous line; uncomment if necessary (ex. for debugging) + REGISTER_TEST(gen_pos_coinstake_already_spent); + REGISTER_TEST(gen_pos_incorrect_timestamp); + REGISTER_TEST(gen_pos_too_early_pos_block); + REGISTER_TEST_HF(gen_pos_extra_nonce, "3-*"); + REGISTER_TEST(gen_pos_min_allowed_height); + REGISTER_TEST(gen_pos_invalid_coinbase); + // REGISTER_TEST(pos_wallet_minting_same_amount_diff_outs); // Long test! Takes ~10 hours to simulate 6000 blocks on 2015 middle-end computer + //REGISTER_TEST(pos_emission_test); // Long test! by demand only + REGISTER_TEST(pos_wallet_big_block_test); + //REGISTER_TEST(block_template_against_txs_size); // Long test! by demand only + REGISTER_TEST_HF(pos_altblocks_validation, "3-*"); + REGISTER_TEST_HF(pos_mining_with_decoys, "3"); + REGISTER_TEST_HF(pos_and_no_pow_blocks_between_output_and_stake, "4-*"); + + // alternative blocks and generic chain-switching tests + REGISTER_TEST(gen_chain_switch_pow_pos); + REGISTER_TEST(pow_pos_reorganize_specific_case); + REGISTER_TEST(gen_chain_switch_1); + REGISTER_TEST(bad_chain_switching_with_rollback); + REGISTER_TEST(chain_switching_and_tx_with_attachment_blobsize); + REGISTER_TEST_HF(chain_switching_when_gindex_spent_in_both_chains, "3-*"); + REGISTER_TEST(alt_chain_coins_pow_mined_then_spent); + REGISTER_TEST(gen_simple_chain_split_1); + REGISTER_TEST_HF(alt_blocks_validation_and_same_new_amount_in_two_txs, "3-*"); + REGISTER_TEST_HF(alt_blocks_with_the_same_txs, "3-*"); + REGISTER_TEST_HF(chain_switching_when_out_spent_in_alt_chain_mixin, "3-*"); + REGISTER_TEST_HF(chain_switching_when_out_spent_in_alt_chain_ref_id, "3-*"); + REGISTER_TEST_HF(alt_chain_and_block_tx_fee_median, "3-*"); + + // miscellaneous tests + REGISTER_TEST(test_blockchain_vs_spent_keyimges); + REGISTER_TEST(test_blockchain_vs_spent_multisig_outs); + REGISTER_TEST(block_template_vs_invalid_txs_from_pool); + REGISTER_TEST(cumulative_difficulty_adjustment_test); + REGISTER_TEST(cumulative_difficulty_adjustment_test_alt); + REGISTER_TEST(prun_ring_signatures); + REGISTER_TEST(gen_simple_chain_001); + REGISTER_TEST(one_block); + REGISTER_TEST(gen_ring_signature_1); + REGISTER_TEST(gen_ring_signature_2); + REGISTER_TEST(fill_tx_rpc_inputs); + //REGISTER_TEST(gen_ring_signature_big); // Takes up to XXX hours (if CURRENCY_MINED_MONEY_UNLOCK_WINDOW == 10) + + // tests for outputs mixing in + REGISTER_TEST(get_random_outs_test); + REGISTER_TEST(mix_attr_tests); + REGISTER_TEST(mix_in_spent_outs); + REGISTER_TEST(random_outs_and_burnt_coins); + + // Block verification tests + REGISTER_TEST_HF(gen_block_big_major_version, "0,3"); + REGISTER_TEST_HF(gen_block_big_minor_version, "0,3"); + REGISTER_TEST_HF(gen_block_ts_not_checked, "0,3"); + REGISTER_TEST_HF(gen_block_ts_in_past, "0,3"); + REGISTER_TEST_HF(gen_block_ts_in_future, "0,3"); + //REGISTER_TEST(gen_block_invalid_prev_id); disabled because impossible to generate text chain with wrong prev_id - pow hash not works without chaining + REGISTER_TEST_HF(gen_block_invalid_nonce, "0,3"); + REGISTER_TEST_HF(gen_block_no_miner_tx, "0,3"); + REGISTER_TEST_HF(gen_block_unlock_time_is_low, "0,3"); + REGISTER_TEST_HF(gen_block_unlock_time_is_high, "0,3"); + REGISTER_TEST_HF(gen_block_unlock_time_is_timestamp_in_past, "0,3"); + REGISTER_TEST_HF(gen_block_unlock_time_is_timestamp_in_future, "0,3"); + REGISTER_TEST_HF(gen_block_height_is_low, "0,3"); + REGISTER_TEST_HF(gen_block_height_is_high, "0,3"); + REGISTER_TEST_HF(block_with_correct_prev_id_on_wrong_height, "3-*"); + REGISTER_TEST_HF(block_reward_in_main_chain_basic, "3-*"); + REGISTER_TEST_HF(block_reward_in_alt_chain_basic, "3-*"); + REGISTER_TEST_HF(gen_block_miner_tx_has_2_tx_gen_in, "0,3"); + REGISTER_TEST_HF(gen_block_miner_tx_has_2_in, "0,3"); + REGISTER_TEST_HF(gen_block_miner_tx_with_txin_to_key, "0,3"); + REGISTER_TEST_HF(gen_block_miner_tx_out_is_small, "0,3"); + REGISTER_TEST_HF(gen_block_miner_tx_out_is_big, "0,3"); + REGISTER_TEST_HF(gen_block_miner_tx_has_no_out, "0,3"); + REGISTER_TEST_HF(gen_block_miner_tx_has_out_to_initiator, "0,3"); + REGISTER_TEST_HF(gen_block_has_invalid_tx, "0,3"); + REGISTER_TEST_HF(gen_block_is_too_big, "0,3"); + REGISTER_TEST_HF(gen_block_wrong_version_agains_hardfork, "0,3"); + REGISTER_TEST_HF(block_choice_rule_bigger_fee, "4-*"); + //REGISTER_TEST(gen_block_invalid_binary_format); // Takes up to 3 hours, if CURRENCY_MINED_MONEY_UNLOCK_WINDOW == 500, up to 30 minutes, if CURRENCY_MINED_MONEY_UNLOCK_WINDOW == 10 + + // Transaction verification tests + REGISTER_TEST(gen_broken_attachments); + REGISTER_TEST(gen_tx_big_version); + REGISTER_TEST(gen_tx_unlock_time); + REGISTER_TEST(gen_tx_input_is_not_txin_to_key); + REGISTER_TEST(gen_tx_no_inputs_no_outputs); + REGISTER_TEST(gen_tx_no_inputs_has_outputs); + REGISTER_TEST(gen_tx_has_inputs_no_outputs); + REGISTER_TEST(gen_tx_invalid_input_amount); + REGISTER_TEST(gen_tx_input_wo_key_offsets); + REGISTER_TEST(gen_tx_sender_key_offest_not_exist); + REGISTER_TEST(gen_tx_key_offest_points_to_foreign_key); + REGISTER_TEST(gen_tx_mixed_key_offest_not_exist); + REGISTER_TEST(gen_tx_key_image_not_derive_from_tx_key); + REGISTER_TEST(gen_tx_key_image_is_invalid); + REGISTER_TEST(gen_tx_check_input_unlock_time); + REGISTER_TEST(gen_tx_txout_to_key_has_invalid_key); + REGISTER_TEST(gen_tx_output_with_zero_amount); + REGISTER_TEST(gen_tx_output_is_not_txout_to_key); + REGISTER_TEST(gen_tx_signatures_are_invalid); + REGISTER_TEST(gen_tx_extra_double_entry); + REGISTER_TEST(gen_tx_double_key_image); + REGISTER_TEST(tx_expiration_time); + REGISTER_TEST(tx_expiration_time_and_block_template); + REGISTER_TEST(tx_expiration_time_and_chain_switching); + REGISTER_TEST(tx_key_image_pool_conflict); + //REGISTER_TEST_HF(tx_version_against_hardfork, "4-*"); + /* To execute the check of bare balance (function "check_tx_bare_balance") we need to run the test "tx_pool_semantic_validation" on the HF 3. By default behaviour bare outputs are disallowed on + the heights >= 10. */ + REGISTER_TEST_HF(tx_pool_semantic_validation, "3"); + REGISTER_TEST(input_refers_to_incompatible_by_type_output); + REGISTER_TEST_HF(tx_pool_validation_and_chain_switch, "4-5"); + REGISTER_TEST_HF(tx_coinbase_separate_sig_flag, "4-*"); + REGISTER_TEST(tx_input_mixins); + + // Double spend + REGISTER_TEST(gen_double_spend_in_tx); + REGISTER_TEST(gen_double_spend_in_tx); + REGISTER_TEST(gen_double_spend_in_the_same_block); + REGISTER_TEST(gen_double_spend_in_the_same_block); + REGISTER_TEST(gen_double_spend_in_different_blocks); + REGISTER_TEST(gen_double_spend_in_different_blocks); + REGISTER_TEST(gen_double_spend_in_different_chains); + REGISTER_TEST(gen_double_spend_in_alt_chain_in_the_same_block); + REGISTER_TEST(gen_double_spend_in_alt_chain_in_the_same_block); + REGISTER_TEST(gen_double_spend_in_alt_chain_in_different_blocks); + REGISTER_TEST(gen_double_spend_in_alt_chain_in_different_blocks); + + REGISTER_TEST(gen_uint_overflow_1); + REGISTER_TEST(gen_uint_overflow_2); + + // Hardfok 1 tests + REGISTER_TEST(before_hard_fork_1_cumulative_difficulty); + REGISTER_TEST(inthe_middle_hard_fork_1_cumulative_difficulty); + REGISTER_TEST(after_hard_fork_1_cumulative_difficulty); + REGISTER_TEST(hard_fork_1_locked_mining_test); + REGISTER_TEST(hard_fork_1_bad_pos_source); + REGISTER_TEST(hard_fork_1_unlock_time_2_in_normal_tx); + REGISTER_TEST(hard_fork_1_unlock_time_2_in_coinbase); + REGISTER_TEST(hard_fork_1_chain_switch_pow_only); + REGISTER_TEST(hard_fork_1_checkpoint_basic_test); + REGISTER_TEST(hard_fork_1_pos_locked_height_vs_time); + REGISTER_TEST(hard_fork_1_pos_and_locked_coins); + + // Hardfork 2 tests + //REGISTER_TEST(hard_fork_2_tx_payer_in_wallet); + //REGISTER_TEST(hard_fork_2_tx_receiver_in_wallet); + REGISTER_TEST(hard_fork_2_tx_extra_alias_entry_in_wallet); + REGISTER_TEST_HF(hard_fork_2_auditable_addresses_basics, "2-*"); + REGISTER_TEST(hard_fork_2_no_new_structures_before_hf); + REGISTER_TEST(hard_fork_2_awo_wallets_basic_test); + REGISTER_TEST(hard_fork_2_awo_wallets_basic_test); + REGISTER_TEST(hard_fork_2_alias_update_using_old_tx); + REGISTER_TEST(hard_fork_2_alias_update_using_old_tx); + REGISTER_TEST(hard_fork_2_incorrect_alias_update); + REGISTER_TEST(hard_fork_2_incorrect_alias_update); + + // HF4 + REGISTER_TEST_HF(hard_fork_4_consolidated_txs, "3-*"); + REGISTER_TEST_HF(hardfork_4_wallet_transfer_with_mandatory_mixins, "3-*"); + REGISTER_TEST(hardfork_4_wallet_sweep_bare_outs); + REGISTER_TEST_HF(hardfork_4_pop_tx_from_global_index, "4-*"); + + // HF5 + REGISTER_TEST_HF(hard_fork_5_tx_version, "5-*"); + + // HF6 + REGISTER_TEST(hard_fork_6_intrinsic_payment_id_basic_test); + REGISTER_TEST(hard_fork_6_intrinsic_payment_id_rpc_test); + + REGISTER_TEST_HF(isolate_auditable_and_proof, "2-*"); + + REGISTER_TEST(zarcanum_basic_test); + + REGISTER_TEST_HF(multiassets_basic_test, "4-*"); + REGISTER_TEST_HF(ionic_swap_basic_test, "4-*"); + REGISTER_TEST_HF(ionic_swap_exact_amounts_test, "4-*"); + REGISTER_TEST(zarcanum_test_n_inputs_validation); + REGISTER_TEST(zarcanum_gen_time_balance); + REGISTER_TEST(zarcanum_txs_with_big_shuffled_decoy_set_shuffled); + REGISTER_TEST(zarcanum_pos_block_math); + REGISTER_TEST(zarcanum_in_alt_chain); + REGISTER_TEST_HF(zarcanum_in_alt_chain_2, "4-*"); + REGISTER_TEST(assets_and_explicit_native_coins_in_outs); + REGISTER_TEST(zarcanum_block_with_txs); + REGISTER_TEST(asset_depoyment_and_few_zc_utxos); + REGISTER_TEST_HF(assets_and_pos_mining, "4-*"); + REGISTER_TEST_HF(asset_emission_and_unconfirmed_balance, "4-*"); + REGISTER_TEST_HF(asset_operation_in_consolidated_tx, "4-*"); + REGISTER_TEST_HF(asset_operation_and_hardfork_checks, "4-*"); + REGISTER_TEST_HF(eth_signed_asset_basics, "5-*"); // TODO: make HF4 version + REGISTER_TEST_HF(eth_signed_asset_via_rpc, "5-*"); // TODO: make HF4 version + //REGISTER_TEST_HF(asset_current_and_total_supplies_comparative_constraints, "4-*"); <-- temporary disabled, waiting for Stepan's fix -- sowle + REGISTER_TEST_HF(several_asset_emit_burn_txs_in_pool, "5-*"); + REGISTER_TEST_HF(assets_transfer_with_smallest_amount, "4-*"); + REGISTER_TEST_HF(asset_operations_and_chain_switching, "4-*"); + + REGISTER_TEST_HF(pos_fuse_test, "4-*"); + REGISTER_TEST_HF(wallet_reorganize_and_trim_test, "4-*"); + REGISTER_TEST_HF(wallet_rpc_thirdparty_custody, "5-*"); + + REGISTER_TEST_HF(attachment_isolation_test, "4-*"); + + // REGISTER_TEST(gen_block_reward); + // END OF TESTS */ +} + +static void fill_postponed_tests_set(std::set& postponed_tests) +{ + postponed_tests.clear(); + +#define MARK_TEST_AS_POSTPONED(genclass) postponed_tests.insert(#genclass) + MARK_TEST_AS_POSTPONED(gen_checkpoints_reorganize); + MARK_TEST_AS_POSTPONED(gen_alias_update_after_addr_changed); + MARK_TEST_AS_POSTPONED(gen_alias_blocking_reg_by_invalid_tx); + MARK_TEST_AS_POSTPONED(gen_alias_blocking_update_by_invalid_tx); + MARK_TEST_AS_POSTPONED(gen_wallet_fake_outputs_randomness); + MARK_TEST_AS_POSTPONED(gen_wallet_fake_outputs_not_enough); + MARK_TEST_AS_POSTPONED(gen_wallet_spending_coinstake_after_minting); + MARK_TEST_AS_POSTPONED(gen_wallet_fake_outs_while_having_too_little_own_outs); + MARK_TEST_AS_POSTPONED(gen_uint_overflow_1); + + MARK_TEST_AS_POSTPONED(after_hard_fork_1_cumulative_difficulty); + MARK_TEST_AS_POSTPONED(before_hard_fork_1_cumulative_difficulty); + MARK_TEST_AS_POSTPONED(inthe_middle_hard_fork_1_cumulative_difficulty); +#undef MARK_TEST_AS_POSTPONED +} +//-------------------------------------------------------------------------- +static std::vector build_base_args_without_worker_specific(int argc, char* argv[]) +{ + std::vector args; + args.reserve(static_cast(argc)); + + for (int i = 1; i < argc; ++i) + { + std::string current_arg = argv[i]; + + // Strip worker-specific args to avoid duplicates + if (current_arg == ARG_WORKER_ID || arg_has_prefix(current_arg, ARG_WORKER_ID_EQ)) + { + if (current_arg == ARG_WORKER_ID && i + 1 < argc) ++i; + continue; + } + + if (current_arg == ARG_DATA_DIR || arg_has_prefix(current_arg, ARG_DATA_DIR_EQ)) + { + if (current_arg == ARG_DATA_DIR && i + 1 < argc) ++i; + continue; + } + + args.emplace_back(std::move(current_arg)); + } + + return args; +} +//-------------------------------------------------------------------------- +static std::string make_worker_data_dir(const std::filesystem::path& run_root_abs, int worker_id) +{ + std::filesystem::path worker_dir = run_root_abs / (std::string(WORKER_DIR_PREFIX) + std::to_string(worker_id)); + std::filesystem::create_directories(worker_dir); + return worker_dir.string(); +} + +static std::filesystem::path get_worker_stdout_path(uint32_t worker_id) +{ + return get_worker_dir_path(worker_id) / WORKER_STDOUT_FILENAME; +} + +static std::filesystem::path get_worker_stderr_path(uint32_t worker_id) +{ + return get_worker_dir_path(worker_id) / WORKER_STDERR_FILENAME; +} + +static std::filesystem::path get_worker_report_path(uint32_t worker_id) +{ + return get_worker_dir_path(worker_id) / WORKER_REPORT_FILENAME; +} +//-------------------------------------------------------------------------- +static bool write_worker_report_file(const worker_report& rep) +{ + try + { + std::filesystem::create_directories(get_worker_dir_path(rep.worker_id)); + + pt::ptree root; + root.put("worker_id", rep.worker_id); + root.put("processes", rep.processes); + root.put("tests_count", static_cast(rep.tests_count)); + root.put("unique_tests_count", static_cast(rep.unique_tests_count)); + root.put("total_time_ms", rep.total_time_ms); + root.put("skip_all_till_the_end", rep.skip_all_till_the_end); + root.put("exit_code", rep.exit_code); + + pt::ptree failed_arr; + for (const auto& s : rep.failed_tests) + { + pt::ptree v; + v.put("", s); + failed_arr.push_back(std::make_pair("", v)); + } + root.add_child("failed_tests", failed_arr); + + pt::ptree times_arr; + for (const auto& it : rep.tests_running_time) + { + pt::ptree node; + node.put("name", it.first); + node.put("ms", it.second); + times_arr.push_back(std::make_pair("", node)); + } + root.add_child("tests_running_time", times_arr); + + std::ofstream out(get_worker_report_path(rep.worker_id)); + if (!out.is_open()) + return false; + + pt::write_json(out, root, /*pretty*/true); + return true; + } + catch (...) + { + return false; + } +} + +static bool read_worker_report_file(uint32_t worker_id, worker_report& rep) +{ + try + { + const auto path = get_worker_report_path(worker_id); + if (!std::filesystem::exists(path)) + return false; + + pt::ptree root; + pt::read_json(path.string(), root); + + rep.worker_id = root.get(JSON_WORKER_ID, worker_id); + rep.processes = root.get(JSON_PROCESSES, 1); + + rep.tests_count = static_cast(root.get(JSON_TESTS_COUNT, 0)); + rep.unique_tests_count = static_cast(root.get(JSON_UNIQUE_TESTS_COUNT, 0)); + rep.total_time_ms = root.get(JSON_TOTAL_TIME_MS, 0); + + rep.skip_all_till_the_end = root.get(JSON_SKIP_ALL_TILL_END, false); + rep.exit_code = root.get(JSON_EXIT_CODE, 1); + + for (const auto& v : root.get_child(JSON_FAILED_TESTS, pt::ptree())) + { + const std::string name = v.second.get_value(); + if (!name.empty()) + rep.failed_tests.insert(name); + } + + for (const auto& v : root.get_child(JSON_TESTS_RUNNING_TIME, pt::ptree())) + { + const auto& node = v.second; + const std::string name = node.get(JSON_NAME, ""); + const uint64_t ms = node.get(JSON_MS, 0); + if (!name.empty()) + rep.tests_running_time.emplace_back(name, ms); + } + + return true; + } + catch (...) + { + return false; + } +} + +static int print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wall_time_ms) +{ + std::vector reps; + reps.reserve(processes); + + bool any_missing = false; + int failed_workers = 0; + + for (uint32_t i = 0; i < processes; ++i) + { + worker_report r; + if (!read_worker_report_file(i, r)) + { + any_missing = true; + continue; + } + reps.push_back(std::move(r)); + } + + for (const auto& r : reps) + if (r.exit_code != 0) + ++failed_workers; + + std::set postponed_tests; + fill_postponed_tests_set(postponed_tests); + + size_t total_tests_count = 0; + size_t total_unique_tests_count = 0; + + std::set failed_tests_union; + + for (const auto& r : reps) + { + total_tests_count += r.tests_count; + total_unique_tests_count += r.unique_tests_count; + + failed_tests_union.insert(r.failed_tests.begin(), r.failed_tests.end()); + } + + size_t failed_postponed_tests_count = 0; + for (const auto& t : failed_tests_union) + if (postponed_tests.count(t) != 0) + ++failed_postponed_tests_count; + + const size_t serious_failures_count = failed_tests_union.size() - failed_postponed_tests_count; + + std::cout << (serious_failures_count == 0 && failed_workers == 0 && !any_missing ? concolor::green : concolor::magenta); + + std::cout << "\nREPORT (aggregated):\n"; + std::cout << " Workers: " << processes << "\n"; + if (any_missing) + std::cout << " Warning: some worker reports are missing\n"; + std::cout << " Worker failures: " << failed_workers << "\n"; + + std::cout << " Unique tests run: " << total_unique_tests_count << "\n"; + std::cout << " Total tests run: " << total_tests_count << "\n"; + std::cout << " Failures: " << serious_failures_count + << " (postponed failures: " << failed_postponed_tests_count << ")\n"; + std::cout << " Postponed: " << postponed_tests.size() << "\n"; + std::cout << " Total time: " << (wall_time_ms / 1000) << " s. (" + << (total_tests_count > 0 ? (wall_time_ms / total_tests_count) : 0) + << " ms per test in average)\n"; + if (!failed_tests_union.empty()) + { + std::cout << "FAILED/POSTPONED TESTS:\n"; + for (const auto& name : failed_tests_union) + { + const bool postponed = postponed_tests.count(name) != 0; + std::cout << " " << (postponed ? "POSTPONED: " : "FAILED: ") << name << "\n"; + } + } + + std::cout << concolor::normal << std::endl; + + // If any report missing or worker failed, treat as failure + if (any_missing || failed_workers != 0) + return 1; + + return serious_failures_count == 0 ? 0 : 1; +} +//-------------------------------------------------------------------------- +static std::string tail_file_lines(const std::filesystem::path& p, size_t max_lines) +{ + std::ifstream f(p, std::ios::in | std::ios::binary); + if (!f.is_open()) + return std::string(); + + f.seekg(0, std::ios::end); + std::streamoff size = f.tellg(); + if (size <= 0) + return std::string(); + + const std::streamoff chunk = 64 * 1024; // 64 KB + std::streamoff from = size > chunk ? (size - chunk) : 0; + f.seekg(from, std::ios::beg); + + std::string buf; + buf.resize(static_cast(size - from)); + f.read(&buf[0], buf.size()); + + std::vector lines; + lines.reserve(max_lines + 10); + std::string cur; + + for (char c : buf) + { + if (c == '\r') + continue; + if (c == '\n') + { + lines.emplace_back(std::move(cur)); + cur.clear(); + } + else + { + cur.push_back(c); + } + } + if (!cur.empty()) + lines.emplace_back(std::move(cur)); + + if (lines.empty()) + return std::string(); + + size_t start = lines.size() > max_lines ? (lines.size() - max_lines) : 0; + std::ostringstream out; + for (size_t i = start; i < lines.size(); ++i) + out << lines[i] << "\n"; + + return out.str(); +} + +static void print_worker_failure_reasons(uint32_t processes, const std::vector& worker_exit_codes) +{ + bool any = false; + + for (uint32_t i = 0; i < processes; ++i) + { + const bool report_exists = std::filesystem::exists(get_worker_report_path(i)); + const int ec = (i < worker_exit_codes.size() ? worker_exit_codes[i] : -999); + + if (ec == 0 && report_exists) + continue; + + if (!any) + { + std::cout << "\nFAILURE REASONS (workers):\n"; + any = true; + } + + std::cout << " Worker " << i << ":\n"; + std::cout << " exit_code: " << ec << "\n"; + if (!report_exists) + std::cout << " report: missing (" << get_worker_report_path(i).string() << ")\n"; + else + std::cout << " report: " << get_worker_report_path(i).string() << "\n"; + + const auto stderr_path = get_worker_stderr_path(i); + const auto stdout_path = get_worker_stdout_path(i); + + std::cout << " stderr: " << stderr_path.string() << "\n"; + std::string err_tail = tail_file_lines(stderr_path, 40); + if (!err_tail.empty()) + { + std::cout << " --- stderr tail ---\n"; + std::cout << err_tail; + std::cout << " --- end stderr tail ---\n"; + } + + std::cout << " stdout: " << stdout_path.string() << "\n"; + std::string out_tail = tail_file_lines(stdout_path, 20); + if (!out_tail.empty()) + { + std::cout << " --- stdout tail ---\n"; + std::cout << out_tail; + std::cout << " --- end stdout tail ---\n"; + } + } +} +//-------------------------------------------------------------------------- +static int run_workers_and_wait(int argc, char* argv[]) +{ + const uint32_t processes = command_line::get_arg(g_vm, arg_processes); + const int32_t worker_id = command_line::get_arg(g_vm, arg_worker_id); + + // Only parent (multi-process mode, worker_id not set) spawns workers. + if (processes <= 1 || worker_id >= 0) + return -1; + + // Do not spawn workers for these modes. + if (command_line::get_arg(g_vm, command_line::arg_help)) return -1; + if (command_line::get_arg(g_vm, arg_list_tests)) return -1; + if (command_line::get_arg(g_vm, arg_generate_test_data)) return -1; + if (command_line::get_arg(g_vm, arg_play_test_data)) return -1; + if (command_line::get_arg(g_vm, arg_generate_and_play_test_data)) return -1; + if (command_line::get_arg(g_vm, arg_test_transactions)) return -1; + + const std::string run_root = command_line::get_arg(g_vm, arg_run_root); + std::vector base_args = build_base_args_without_worker_specific(argc, argv); + + auto has_flag = [&](const char* flag, const std::string& prefix) -> bool + { + for (const auto& a : base_args) + if (a == flag || arg_has_prefix(a, prefix)) + return true; + return false; + }; + + if (!has_flag("--processes", "--processes=")) + { + base_args.emplace_back("--processes"); + base_args.emplace_back(std::to_string(processes)); + } + + std::filesystem::path run_root_abs = run_root.empty() + ? std::filesystem::path("chaingen_runs") + : std::filesystem::path(run_root); + + if (run_root_abs.is_relative()) + run_root_abs = std::filesystem::absolute(run_root_abs); + + if (!has_flag("--run-root", "--run-root=")) + { + base_args.emplace_back("--run-root"); + base_args.emplace_back(run_root_abs.string()); + } + + // Resolve executable path and make it absolute. + std::string exe = (argv && argv[0]) ? std::string(argv[0]) : std::string(); + if (exe.empty()) + { + std::cout << concolor::magenta + << "Cannot spawn workers: empty executable path (argv[0])" + << concolor::normal << std::endl; + return 1; + } + + try + { + std::filesystem::path exe_path(exe); + + // If no directory component is present, try PATH lookup first. + if (exe.find('/') == std::string::npos && exe.find('\\') == std::string::npos) + { + boost::filesystem::path resolved = bp::search_path(exe); + if (!resolved.empty()) + exe_path = std::filesystem::path(resolved.string()); + } + + // Make absolute because workers will run with start_dir = worker directory. + if (exe_path.is_relative()) + exe_path = std::filesystem::absolute(exe_path); + + exe = exe_path.string(); + } + catch (...) + { + // Keep exe as-is; spawning will fail with a clear message if it's invalid. + } + + if (exe.empty() || !std::filesystem::exists(std::filesystem::path(exe))) + { + std::cout << concolor::magenta + << "Cannot spawn workers: failed to resolve executable: " << exe + << concolor::normal << std::endl; + return 1; + } + + std::vector kids; + kids.reserve(processes); + + std::vector worker_exit_codes(processes, -1); + + // Streams for tee + std::vector> kids_out(processes); + std::vector> kids_err(processes); + + // Tee threads + std::vector tee_threads; + tee_threads.reserve(static_cast(processes) * 2); + + const auto wall_t0 = std::chrono::steady_clock::now(); + + for (uint32_t i = 0; i < processes; ++i) + { + std::vector args = base_args; + + args.emplace_back("--worker-id"); + args.emplace_back(std::to_string(i)); + + const std::string worker_data_dir = make_worker_data_dir(run_root_abs, static_cast(i)); + args.emplace_back("--data-dir"); + args.emplace_back(worker_data_dir); + + try + { + const auto worker_dir = get_worker_dir_path(i); + std::filesystem::create_directories(worker_dir); + + const auto stdout_path = get_worker_stdout_path(i); + const auto stderr_path = get_worker_stderr_path(i); + + kids_out[i] = std::make_unique(); + kids_err[i] = std::make_unique(); + + kids.emplace_back(bp::child( + exe, + bp::args(args), + bp::start_dir = worker_dir.string(), + bp::std_out > *kids_out[i], + bp::std_err > *kids_err[i] + )); + + // Tee stdout: worker -> console + file + tee_threads.emplace_back([p = stdout_path, s = kids_out[i].get()]() + { + std::ofstream f(p, std::ios::out | std::ios::binary | std::ios::app); + std::string line; + while (std::getline(*s, line)) + { + f << line << "\n"; + f.flush(); + + std::lock_guard lk(cout_mx); + std::cout << line << std::endl; + } + }); + + // Tee stderr: worker -> console(stderr) + file + tee_threads.emplace_back([p = stderr_path, s = kids_err[i].get()]() + { + std::ofstream f(p, std::ios::out | std::ios::binary | std::ios::app); + std::string line; + while (std::getline(*s, line)) + { + f << line << "\n"; + f.flush(); + + std::lock_guard lk(cerr_mx); + std::cerr << line << std::endl; + } + }); + } + catch (const std::exception& e) + { + std::cout << concolor::magenta + << "Failed to spawn worker " << i << ": " << e.what() + << concolor::normal << std::endl; + + for (auto& c : kids) + if (c.running()) + c.terminate(); + for (auto& c : kids) + c.wait(); + + for (auto& t : tee_threads) + if (t.joinable()) + t.join(); + + return 1; + } + } + + int failed_workers = 0; + for (uint32_t i = 0; i < kids.size(); ++i) + { + kids[i].wait(); + worker_exit_codes[i] = kids[i].exit_code(); + if (worker_exit_codes[i] != 0) + ++failed_workers; + } + + // Wait tee threads after children exit (streams will close). + for (auto& t : tee_threads) + if (t.joinable()) + t.join(); + + const auto wall_t1 = std::chrono::steady_clock::now(); + const uint64_t wall_ms = + static_cast(std::chrono::duration_cast(wall_t1 - wall_t0).count()); + + // Aggregated report first. + const int aggregated_rc = print_aggregated_report_and_return_rc(processes, wall_ms); + + // Then print diagnostics. + if (failed_workers != 0) + print_worker_failure_reasons(processes, worker_exit_codes); + + // Any worker non-zero => fail. + if (failed_workers != 0) + return 1; + + return aggregated_rc; +} //-------------------------------------------------------------------------- int main(int argc, char* argv[]) { TRY_ENTRY(); string_tools::set_module_name_and_folder(argv[0]); - //set up logging options + // set up logging options log_space::get_set_log_detalisation_level(true, LOG_LEVEL_2); log_space::log_singletone::add_logger(LOGGER_CONSOLE, NULL, NULL, LOG_LEVEL_4); - - log_space::log_singletone::add_logger(LOGGER_FILE, - log_space::log_singletone::get_default_log_file().c_str(), + + log_space::log_singletone::add_logger( + LOGGER_FILE, + log_space::log_singletone::get_default_log_file().c_str(), log_space::log_singletone::get_default_log_folder().c_str()); log_space::log_singletone::enable_channels("core,currency_protocol,tx_pool,p2p,wallet", false); tools::signal_handler::install_fatal([](int sig_number, void* address) { LOG_ERROR("\n\nFATAL ERROR\nsig: " << sig_number << ", address: " << address); - std::fflush(nullptr); // all open output streams are flushed + std::fflush(nullptr); // flush all open output streams }); po::options_description desc_options("Allowed options"); @@ -934,9 +2057,14 @@ int main(int argc, char* argv[]) command_line::add_arg(desc_options, command_line::arg_data_dir, std::string(".")); command_line::add_arg(desc_options, command_line::arg_stop_after_height); command_line::add_arg(desc_options, command_line::arg_disable_ntp); + command_line::add_arg(desc_options, arg_processes); + command_line::add_arg(desc_options, arg_worker_id); + command_line::add_arg(desc_options, arg_run_root); + command_line::add_arg(desc_options, arg_list_tests); currency::core::init_options(desc_options); tools::db::db_backend_selector::init_options(desc_options); + bool stop_on_first_fail = false; bool r = command_line::handle_error_helper(desc_options, [&]() { @@ -947,6 +2075,14 @@ int main(int argc, char* argv[]) if (!r) return 1; + // Parent spawns workers and must print aggregated report inside run_workers_and_wait(). + // Workers will return -1 here and continue into normal execution path. + { + int parent_rc = run_workers_and_wait(argc, argv); + if (parent_rc != -1) + return parent_rc; + } + if (command_line::get_arg(g_vm, command_line::arg_help)) { std::cout << desc_options << std::endl; @@ -956,7 +2092,8 @@ int main(int argc, char* argv[]) if (command_line::has_arg(g_vm, command_line::arg_log_level)) { int new_log_level = command_line::get_arg(g_vm, command_line::arg_log_level); - if (new_log_level >= LOG_LEVEL_MIN && new_log_level <= LOG_LEVEL_MAX && log_space::get_set_log_detalisation_level(false) != new_log_level) + if (new_log_level >= LOG_LEVEL_MIN && new_log_level <= LOG_LEVEL_MAX && + log_space::get_set_log_detalisation_level(false) != new_log_level) { log_space::get_set_log_detalisation_level(true, new_log_level); LOG_PRINT_L0("LOG_LEVEL set to " << new_log_level); @@ -968,15 +2105,19 @@ int main(int argc, char* argv[]) stop_on_first_fail = command_line::get_arg(g_vm, arg_stop_on_fail); } - + const uint32_t processes = command_line::get_arg(g_vm, arg_processes); + const int32_t worker_id = command_line::get_arg(g_vm, arg_worker_id); + const bool is_worker = (processes > 1 && worker_id >= 0); + bool skip_all_till_the_end = false; size_t tests_count = 0; size_t unique_tests_count = 0; size_t serious_failures_count = 0; std::set failed_tests; - std::string tests_folder = command_line::get_arg(g_vm, arg_test_data_path); + typedef std::vector> tests_running_time_t; tests_running_time_t tests_running_time; + if (command_line::get_arg(g_vm, arg_generate_test_data)) { GENERATE("chain001.dat", gen_simple_chain_001); @@ -991,7 +2132,8 @@ int main(int argc, char* argv[]) } else { - epee::debug::get_set_enable_assert(true, command_line::get_arg(g_vm, arg_enable_debug_asserts)); // don't comment out this: many tests have normal-negative checks (i.e. tx with invalid amount shouldn't be created), so be ready for MANY assertion breaks + // Do not comment this out: many tests have normal-negative checks. + epee::debug::get_set_enable_assert(true, command_line::get_arg(g_vm, arg_enable_debug_asserts)); std::unordered_multimap specific_tests_to_run; // test_name -> hf; if empty, all tests should be run CHECK_AND_ASSERT_MES(parse_cmd_specific_tests_to_run(specific_tests_to_run), 1, "Error while parsing specific tests to run"); @@ -1001,6 +2143,7 @@ int main(int argc, char* argv[]) auto is_hf_test_eligible_to_run = [&](const std::string& genclass_str, size_t hardfork) -> bool { if (skip_all_till_the_end) return false; + if (specific_tests_to_run.empty()) { if (postponed_tests.count(genclass_str) != 0) @@ -1011,18 +2154,23 @@ int main(int argc, char* argv[]) auto it_pair = specific_tests_to_run.equal_range(genclass_str); if (it_pair.first == it_pair.second) return false; + bool match = false; - for(auto it = it_pair.first; it != it_pair.second; ++it) + for (auto it = it_pair.first; it != it_pair.second; ++it) if (it->second == SIZE_MAX || hardfork == SIZE_MAX || it->second == hardfork) match = true; + if (!match) return false; } + LOG_PRINT_L0("good: " << genclass_str); return true; }; - auto is_test_eligible_to_run = [&](const std::string& genclass_str) -> bool { return is_hf_test_eligible_to_run(genclass_str, SIZE_MAX); }; + auto is_test_eligible_to_run = [&](const std::string& genclass_str) -> bool { + return is_hf_test_eligible_to_run(genclass_str, SIZE_MAX); + }; if (specific_tests_to_run.empty()) { @@ -1036,442 +2184,143 @@ int main(int argc, char* argv[]) CALL_TEST("test_parse_hardfork_str_mask", test_parse_hardfork_str_mask); } - //CALL_TEST("check_hash_and_difficulty_monte_carlo_test", check_hash_and_difficulty_monte_carlo_test); // it's rather an experiment with unclean results than a solid test, for further research... - - // Postponed tests - tests that may fail for the time being (believed that it's a serious issue and should be fixed later for some reason). - // In a perfect world this list is empty. -#define MARK_TEST_AS_POSTPONED(genclass) postponed_tests.insert(#genclass) - MARK_TEST_AS_POSTPONED(gen_checkpoints_reorganize); - MARK_TEST_AS_POSTPONED(gen_alias_update_after_addr_changed); - MARK_TEST_AS_POSTPONED(gen_alias_blocking_reg_by_invalid_tx); - MARK_TEST_AS_POSTPONED(gen_alias_blocking_update_by_invalid_tx); - MARK_TEST_AS_POSTPONED(gen_wallet_fake_outputs_randomness); - MARK_TEST_AS_POSTPONED(gen_wallet_fake_outputs_not_enough); - MARK_TEST_AS_POSTPONED(gen_wallet_spending_coinstake_after_minting); - MARK_TEST_AS_POSTPONED(gen_wallet_fake_outs_while_having_too_little_own_outs); - MARK_TEST_AS_POSTPONED(gen_uint_overflow_1); - - MARK_TEST_AS_POSTPONED(after_hard_fork_1_cumulative_difficulty); // reason: set_pos_to_low_timestamp is not supported anymore - MARK_TEST_AS_POSTPONED(before_hard_fork_1_cumulative_difficulty); - MARK_TEST_AS_POSTPONED(inthe_middle_hard_fork_1_cumulative_difficulty); - -#undef MARK_TEST_AS_POSTPONED - - - // TODO // GENERATE_AND_PLAY(wallet_spend_form_auditable_and_track); - GENERATE_AND_PLAY(gen_block_big_major_version); - - GENERATE_AND_PLAY(pos_minting_tx_packing); - - GENERATE_AND_PLAY(multisig_wallet_test); - GENERATE_AND_PLAY(multisig_wallet_test_many_dst); - GENERATE_AND_PLAY(multisig_wallet_heterogenous_dst); - GENERATE_AND_PLAY(multisig_wallet_same_dst_addr); - GENERATE_AND_PLAY(multisig_wallet_ms_to_ms); - GENERATE_AND_PLAY(multisig_minimum_sigs); - GENERATE_AND_PLAY(multisig_and_fake_outputs); - GENERATE_AND_PLAY(multisig_and_unlock_time); - GENERATE_AND_PLAY(multisig_and_coinbase); - GENERATE_AND_PLAY(multisig_with_same_id_in_pool); - GENERATE_AND_PLAY_HF(multisig_and_checkpoints, "0"); // TODO: fix for HF 1-3 (checkpoint hash check) - GENERATE_AND_PLAY(multisig_and_checkpoints_bad_txs); - GENERATE_AND_PLAY(multisig_and_altchains); - GENERATE_AND_PLAY(multisig_out_make_and_spent_in_altchain); - GENERATE_AND_PLAY(multisig_unconfirmed_transfer_and_multiple_scan_pool_calls); - GENERATE_AND_PLAY(multisig_out_spent_in_altchain_case_b4); - GENERATE_AND_PLAY(multisig_n_participants_seq_signing); - - GENERATE_AND_PLAY(ref_by_id_basics); - GENERATE_AND_PLAY(ref_by_id_mixed_inputs_types); - - GENERATE_AND_PLAY(escrow_wallet_test); - GENERATE_AND_PLAY(escrow_w_and_fake_outputs); - GENERATE_AND_PLAY(escrow_incorrect_proposal); - GENERATE_AND_PLAY(escrow_proposal_expiration); - GENERATE_AND_PLAY(escrow_proposal_and_accept_expiration); - GENERATE_AND_PLAY(escrow_incorrect_proposal_acceptance); - GENERATE_AND_PLAY(escrow_custom_test); - GENERATE_AND_PLAY(escrow_incorrect_cancel_proposal); - GENERATE_AND_PLAY(escrow_proposal_not_enough_money); - GENERATE_AND_PLAY(escrow_cancellation_and_tx_order); - GENERATE_AND_PLAY(escrow_cancellation_proposal_expiration); - GENERATE_AND_PLAY(escrow_cancellation_acceptance_expiration); - // GENERATE_AND_PLAY(escrow_proposal_acceptance_in_alt_chain); -- work in progress - GENERATE_AND_PLAY(escrow_zero_amounts); - GENERATE_AND_PLAY(escrow_balance); - - GENERATE_AND_PLAY(escrow_altchain_meta_test<0>); - GENERATE_AND_PLAY(escrow_altchain_meta_test<1>); - GENERATE_AND_PLAY(escrow_altchain_meta_test<2>); - GENERATE_AND_PLAY(escrow_altchain_meta_test<3>); - GENERATE_AND_PLAY(escrow_altchain_meta_test<4>); - GENERATE_AND_PLAY(escrow_altchain_meta_test<5>); - GENERATE_AND_PLAY(escrow_altchain_meta_test<6>); - GENERATE_AND_PLAY(escrow_altchain_meta_test<7>); - GENERATE_AND_PLAY(escrow_altchain_meta_test<8>); - static_assert(escrow_altchain_meta_test_data<9>::empty_marker, ""); // make sure there are no sub-tests left - - GENERATE_AND_PLAY(offers_expiration_test); - GENERATE_AND_PLAY(offers_tests); - GENERATE_AND_PLAY(offers_filtering_1); - //GENERATE_AND_PLAY(offers_handling_on_chain_switching); - GENERATE_AND_PLAY(offer_removing_and_selected_output); - GENERATE_AND_PLAY(offers_multiple_update); - GENERATE_AND_PLAY(offer_sig_validity_in_update_and_cancel); - GENERATE_AND_PLAY(offer_lifecycle_via_tx_pool); - GENERATE_AND_PLAY(offers_updating_hack); - GENERATE_AND_PLAY(offer_cancellation_with_zero_fee); - - GENERATE_AND_PLAY(gen_crypted_attachments); - GENERATE_AND_PLAY(gen_checkpoints_attachments_basic); - GENERATE_AND_PLAY(gen_checkpoints_invalid_keyimage); - GENERATE_AND_PLAY(gen_checkpoints_altblock_before_and_after_cp); - GENERATE_AND_PLAY(gen_checkpoints_block_in_future); - GENERATE_AND_PLAY(gen_checkpoints_altchain_far_before_cp); - GENERATE_AND_PLAY(gen_checkpoints_block_in_future_after_cp); - GENERATE_AND_PLAY(gen_checkpoints_prun_txs_after_blockchain_load); - GENERATE_AND_PLAY(gen_checkpoints_reorganize); - GENERATE_AND_PLAY(gen_checkpoints_pos_validation_on_altchain); - GENERATE_AND_PLAY(gen_checkpoints_and_invalid_tx_to_pool); - GENERATE_AND_PLAY(gen_checkpoints_set_after_switching_to_altchain); - GENERATE_AND_PLAY_HF(gen_no_attchments_in_coinbase, "3"); - GENERATE_AND_PLAY(gen_no_attchments_in_coinbase_gentime); - - GENERATE_AND_PLAY_HF(gen_alias_tests, "3-*"); - GENERATE_AND_PLAY_HF(gen_alias_strange_data, "3-*"); - GENERATE_AND_PLAY_HF(gen_alias_concurrency_with_switch, "3-*"); - GENERATE_AND_PLAY_HF(gen_alias_same_alias_in_tx_pool, "3-*"); - GENERATE_AND_PLAY_HF(gen_alias_switch_and_tx_pool, "3-*"); - GENERATE_AND_PLAY_HF(gen_alias_update_after_addr_changed, "3-*"); - GENERATE_AND_PLAY_HF(gen_alias_blocking_reg_by_invalid_tx, "3-*"); - GENERATE_AND_PLAY_HF(gen_alias_blocking_update_by_invalid_tx, "3-*"); - GENERATE_AND_PLAY_HF(gen_alias_reg_with_locked_money, "*"); - GENERATE_AND_PLAY_HF(gen_alias_too_small_reward, "3-*"); - GENERATE_AND_PLAY_HF(gen_alias_too_much_reward, "3-*"); - GENERATE_AND_PLAY_HF(gen_alias_tx_no_outs, "*"); - GENERATE_AND_PLAY_HF(gen_alias_switch_and_check_block_template, "3-*"); - GENERATE_AND_PLAY_HF(gen_alias_too_many_regs_in_block_template, "3"); // disabled in HF4 due to tx outputs count limitation - GENERATE_AND_PLAY_HF(gen_alias_update_for_free, "3-*"); - GENERATE_AND_PLAY_HF(gen_alias_in_coinbase, "3-*"); - - GENERATE_AND_PLAY(gen_wallet_basic_transfer); - GENERATE_AND_PLAY(gen_wallet_refreshing_on_chain_switch); - GENERATE_AND_PLAY(gen_wallet_refreshing_on_chain_switch_2); - GENERATE_AND_PLAY(gen_wallet_unconfirmed_tx_from_tx_pool); - GENERATE_AND_PLAY_HF(gen_wallet_save_load_and_balance, "*"); - GENERATE_AND_PLAY_HF(gen_wallet_mine_pos_block, "3-*"); - GENERATE_AND_PLAY(gen_wallet_unconfirmed_outdated_tx); - GENERATE_AND_PLAY(gen_wallet_unlock_by_block_and_by_time); - GENERATE_AND_PLAY(gen_wallet_payment_id); - GENERATE_AND_PLAY(gen_wallet_oversized_payment_id); - GENERATE_AND_PLAY(gen_wallet_transfers_and_outdated_unconfirmed_txs); - GENERATE_AND_PLAY(gen_wallet_transfers_and_chain_switch); - GENERATE_AND_PLAY(gen_wallet_decrypted_payload_items); - GENERATE_AND_PLAY_HF(gen_wallet_alias_and_unconfirmed_txs, "3-*"); - GENERATE_AND_PLAY_HF(gen_wallet_alias_via_special_wallet_funcs, "3-*"); - GENERATE_AND_PLAY(gen_wallet_fake_outputs_randomness); - GENERATE_AND_PLAY(gen_wallet_fake_outputs_not_enough); - GENERATE_AND_PLAY(gen_wallet_offers_basic); - GENERATE_AND_PLAY(gen_wallet_offers_size_limit); - GENERATE_AND_PLAY(gen_wallet_dust_to_account); - GENERATE_AND_PLAY(gen_wallet_selecting_pos_entries); - GENERATE_AND_PLAY(gen_wallet_spending_coinstake_after_minting); - GENERATE_AND_PLAY(gen_wallet_fake_outs_while_having_too_little_own_outs); - // GENERATE_AND_PLAY(premine_wallet_test); // tests premine wallets; wallet files nedded; by demand only - GENERATE_AND_PLAY(mined_balance_wallet_test); - GENERATE_AND_PLAY(wallet_outputs_with_same_key_image); - GENERATE_AND_PLAY(wallet_unconfirmed_tx_expiration); - GENERATE_AND_PLAY(wallet_unconfimed_tx_balance); - GENERATE_AND_PLAY_HF(packing_outputs_on_pos_minting_wallet, "3"); - GENERATE_AND_PLAY_HF(wallet_watch_only_and_chain_switch, "3"); - GENERATE_AND_PLAY_HF(wallet_and_sweep_below, "3-*"); - - GENERATE_AND_PLAY(wallet_rpc_integrated_address); - GENERATE_AND_PLAY(wallet_rpc_integrated_address_transfer); - GENERATE_AND_PLAY(wallet_rpc_transfer); - GENERATE_AND_PLAY(wallet_rpc_alias_tests); - GENERATE_AND_PLAY_HF(wallet_rpc_exchange_suite, "3,4"); - GENERATE_AND_PLAY_HF(wallet_true_rpc_pos_mining, "4-*"); - GENERATE_AND_PLAY_HF(wallet_rpc_cold_signing, "3,5-*"); - // GENERATE_AND_PLAY_HF(wallet_rpc_multiple_receivers, "5-*"); work in progress -- sowle - GENERATE_AND_PLAY_HF(wallet_chain_switch_with_spending_the_same_ki, "3"); - GENERATE_AND_PLAY(wallet_sending_to_integrated_address); - GENERATE_AND_PLAY_HF(block_template_blacklist_test, "4-*"); - GENERATE_AND_PLAY_HF(wallet_rpc_hardfork_verification, "5"); - - - // GENERATE_AND_PLAY(emission_test); // simulate 1 year of blockchain, too long run (1 y ~= 1 hr), by demand only - // LOG_ERROR2("print_reward_change_first_blocks.log", currency::print_reward_change_first_blocks(525601).str()); // outputs first 1 year of blocks' rewards (simplier) - - // pos tests - GENERATE_AND_PLAY_INTERMITTED_BY_BLOCKCHAIN_SAVELOAD(gen_pos_basic_tests); - // GENERATE_AND_PLAY(gen_pos_basic_tests); -- commented as this test is run intermittedly by previous line; uncomment if necessary (ex. for debugging) - GENERATE_AND_PLAY(gen_pos_coinstake_already_spent); - GENERATE_AND_PLAY(gen_pos_incorrect_timestamp); - GENERATE_AND_PLAY(gen_pos_too_early_pos_block); - GENERATE_AND_PLAY_HF(gen_pos_extra_nonce, "3-*"); - GENERATE_AND_PLAY(gen_pos_min_allowed_height); - GENERATE_AND_PLAY(gen_pos_invalid_coinbase); - // GENERATE_AND_PLAY(pos_wallet_minting_same_amount_diff_outs); // Long test! Takes ~10 hours to simulate 6000 blocks on 2015 middle-end computer - //GENERATE_AND_PLAY(pos_emission_test); // Long test! by demand only - GENERATE_AND_PLAY(pos_wallet_big_block_test); - //GENERATE_AND_PLAY(block_template_against_txs_size); // Long test! by demand only - GENERATE_AND_PLAY_HF(pos_altblocks_validation, "3-*"); - GENERATE_AND_PLAY_HF(pos_mining_with_decoys, "3"); - GENERATE_AND_PLAY_HF(pos_and_no_pow_blocks_between_output_and_stake, "4-*"); - - // alternative blocks and generic chain-switching tests - GENERATE_AND_PLAY(gen_chain_switch_pow_pos); - GENERATE_AND_PLAY(pow_pos_reorganize_specific_case); - GENERATE_AND_PLAY(gen_chain_switch_1); - GENERATE_AND_PLAY(bad_chain_switching_with_rollback); - GENERATE_AND_PLAY(chain_switching_and_tx_with_attachment_blobsize); - GENERATE_AND_PLAY_HF(chain_switching_when_gindex_spent_in_both_chains, "3-*"); - GENERATE_AND_PLAY(alt_chain_coins_pow_mined_then_spent); - GENERATE_AND_PLAY(gen_simple_chain_split_1); - GENERATE_AND_PLAY_HF(alt_blocks_validation_and_same_new_amount_in_two_txs, "3-*"); - GENERATE_AND_PLAY_HF(alt_blocks_with_the_same_txs, "3-*"); - GENERATE_AND_PLAY_HF(chain_switching_when_out_spent_in_alt_chain_mixin, "3-*"); - GENERATE_AND_PLAY_HF(chain_switching_when_out_spent_in_alt_chain_ref_id, "3-*"); - GENERATE_AND_PLAY_HF(alt_chain_and_block_tx_fee_median, "3-*"); - - // miscellaneous tests - GENERATE_AND_PLAY(test_blockchain_vs_spent_keyimges); - GENERATE_AND_PLAY(test_blockchain_vs_spent_multisig_outs); - GENERATE_AND_PLAY(block_template_vs_invalid_txs_from_pool); - GENERATE_AND_PLAY(cumulative_difficulty_adjustment_test); - GENERATE_AND_PLAY(cumulative_difficulty_adjustment_test); - GENERATE_AND_PLAY(cumulative_difficulty_adjustment_test_alt); - GENERATE_AND_PLAY(prun_ring_signatures); - GENERATE_AND_PLAY(gen_simple_chain_001); - GENERATE_AND_PLAY(one_block); - GENERATE_AND_PLAY(gen_ring_signature_1); - GENERATE_AND_PLAY(gen_ring_signature_2); - GENERATE_AND_PLAY(fill_tx_rpc_inputs); - //GENERATE_AND_PLAY(gen_ring_signature_big); // Takes up to XXX hours (if CURRENCY_MINED_MONEY_UNLOCK_WINDOW == 10) - - // tests for outputs mixing in - GENERATE_AND_PLAY(get_random_outs_test); - GENERATE_AND_PLAY(mix_attr_tests); - GENERATE_AND_PLAY(mix_in_spent_outs); - GENERATE_AND_PLAY(random_outs_and_burnt_coins); - - // Block verification tests - GENERATE_AND_PLAY_HF(gen_block_big_major_version, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_big_minor_version, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_ts_not_checked, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_ts_in_past, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_ts_in_future, "0,3"); - //GENERATE_AND_PLAY(gen_block_invalid_prev_id); disabled because impossible to generate text chain with wrong prev_id - pow hash not works without chaining - GENERATE_AND_PLAY_HF(gen_block_invalid_nonce, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_no_miner_tx, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_unlock_time_is_low, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_unlock_time_is_high, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_unlock_time_is_timestamp_in_past, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_unlock_time_is_timestamp_in_future, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_height_is_low, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_height_is_high, "0,3"); - GENERATE_AND_PLAY_HF(block_with_correct_prev_id_on_wrong_height, "3-*"); - GENERATE_AND_PLAY_HF(block_reward_in_main_chain_basic, "3-*"); - GENERATE_AND_PLAY_HF(block_reward_in_alt_chain_basic, "3-*"); - GENERATE_AND_PLAY_HF(gen_block_miner_tx_has_2_tx_gen_in, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_miner_tx_has_2_in, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_miner_tx_with_txin_to_key, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_miner_tx_out_is_small, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_miner_tx_out_is_big, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_miner_tx_has_no_out, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_miner_tx_has_out_to_initiator, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_has_invalid_tx, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_is_too_big, "0,3"); - GENERATE_AND_PLAY_HF(gen_block_wrong_version_agains_hardfork, "0,3"); - GENERATE_AND_PLAY_HF(block_choice_rule_bigger_fee, "4-*"); - //GENERATE_AND_PLAY(gen_block_invalid_binary_format); // Takes up to 3 hours, if CURRENCY_MINED_MONEY_UNLOCK_WINDOW == 500, up to 30 minutes, if CURRENCY_MINED_MONEY_UNLOCK_WINDOW == 10 - - - - // Transaction verification tests - GENERATE_AND_PLAY(gen_broken_attachments); - GENERATE_AND_PLAY(gen_tx_big_version); - GENERATE_AND_PLAY(gen_tx_unlock_time); - GENERATE_AND_PLAY(gen_tx_input_is_not_txin_to_key); - GENERATE_AND_PLAY(gen_tx_no_inputs_no_outputs); - GENERATE_AND_PLAY(gen_tx_no_inputs_has_outputs); - GENERATE_AND_PLAY(gen_tx_has_inputs_no_outputs); - GENERATE_AND_PLAY(gen_tx_invalid_input_amount); - GENERATE_AND_PLAY(gen_tx_input_wo_key_offsets); - GENERATE_AND_PLAY(gen_tx_sender_key_offest_not_exist); - GENERATE_AND_PLAY(gen_tx_key_offest_points_to_foreign_key); - GENERATE_AND_PLAY(gen_tx_mixed_key_offest_not_exist); - GENERATE_AND_PLAY(gen_tx_key_image_not_derive_from_tx_key); - GENERATE_AND_PLAY(gen_tx_key_image_is_invalid); - GENERATE_AND_PLAY(gen_tx_check_input_unlock_time); - GENERATE_AND_PLAY(gen_tx_txout_to_key_has_invalid_key); - GENERATE_AND_PLAY(gen_tx_output_with_zero_amount); - GENERATE_AND_PLAY(gen_tx_output_is_not_txout_to_key); - GENERATE_AND_PLAY(gen_tx_signatures_are_invalid); - GENERATE_AND_PLAY(gen_tx_extra_double_entry); - GENERATE_AND_PLAY(gen_tx_double_key_image); - GENERATE_AND_PLAY(tx_expiration_time); - GENERATE_AND_PLAY(tx_expiration_time_and_block_template); - GENERATE_AND_PLAY(tx_expiration_time_and_chain_switching); - GENERATE_AND_PLAY(tx_key_image_pool_conflict); - //GENERATE_AND_PLAY_HF(tx_version_against_hardfork, "4-*"); - /* To execute the check of bare balance (function "check_tx_bare_balance") we need to run the test "tx_pool_semantic_validation" on the HF 3. By default behaviour bare outputs are disallowed on - the heights >= 10. */ - GENERATE_AND_PLAY_HF(tx_pool_semantic_validation, "3"); - GENERATE_AND_PLAY(input_refers_to_incompatible_by_type_output); - GENERATE_AND_PLAY_HF(tx_pool_validation_and_chain_switch, "4-5"); - GENERATE_AND_PLAY_HF(tx_coinbase_separate_sig_flag, "4-*"); - GENERATE_AND_PLAY(tx_input_mixins); - - // Double spend - GENERATE_AND_PLAY(gen_double_spend_in_tx); - GENERATE_AND_PLAY(gen_double_spend_in_tx); - GENERATE_AND_PLAY(gen_double_spend_in_the_same_block); - GENERATE_AND_PLAY(gen_double_spend_in_the_same_block); - GENERATE_AND_PLAY(gen_double_spend_in_different_blocks); - GENERATE_AND_PLAY(gen_double_spend_in_different_blocks); - GENERATE_AND_PLAY(gen_double_spend_in_different_chains); - GENERATE_AND_PLAY(gen_double_spend_in_alt_chain_in_the_same_block); - GENERATE_AND_PLAY(gen_double_spend_in_alt_chain_in_the_same_block); - GENERATE_AND_PLAY(gen_double_spend_in_alt_chain_in_different_blocks); - GENERATE_AND_PLAY(gen_double_spend_in_alt_chain_in_different_blocks); - - GENERATE_AND_PLAY(gen_uint_overflow_1); - GENERATE_AND_PLAY(gen_uint_overflow_2); - - - // Hardfok 1 tests - GENERATE_AND_PLAY(before_hard_fork_1_cumulative_difficulty); - GENERATE_AND_PLAY(inthe_middle_hard_fork_1_cumulative_difficulty); - GENERATE_AND_PLAY(after_hard_fork_1_cumulative_difficulty); - GENERATE_AND_PLAY(hard_fork_1_locked_mining_test); - GENERATE_AND_PLAY(hard_fork_1_bad_pos_source); - GENERATE_AND_PLAY(hard_fork_1_unlock_time_2_in_normal_tx); - GENERATE_AND_PLAY(hard_fork_1_unlock_time_2_in_coinbase); - GENERATE_AND_PLAY(hard_fork_1_chain_switch_pow_only); - GENERATE_AND_PLAY(hard_fork_1_checkpoint_basic_test); - GENERATE_AND_PLAY(hard_fork_1_pos_locked_height_vs_time); - GENERATE_AND_PLAY(hard_fork_1_pos_and_locked_coins); - - // Hardfork 2 tests - //GENERATE_AND_PLAY(hard_fork_2_tx_payer_in_wallet); - //GENERATE_AND_PLAY(hard_fork_2_tx_receiver_in_wallet); - GENERATE_AND_PLAY(hard_fork_2_tx_extra_alias_entry_in_wallet); - GENERATE_AND_PLAY_HF(hard_fork_2_auditable_addresses_basics, "2-*"); - GENERATE_AND_PLAY(hard_fork_2_no_new_structures_before_hf); - GENERATE_AND_PLAY(hard_fork_2_awo_wallets_basic_test); - GENERATE_AND_PLAY(hard_fork_2_awo_wallets_basic_test); - GENERATE_AND_PLAY(hard_fork_2_alias_update_using_old_tx); - GENERATE_AND_PLAY(hard_fork_2_alias_update_using_old_tx); - GENERATE_AND_PLAY(hard_fork_2_incorrect_alias_update); - GENERATE_AND_PLAY(hard_fork_2_incorrect_alias_update); - - // HF4 - GENERATE_AND_PLAY_HF(hard_fork_4_consolidated_txs, "3-*"); - GENERATE_AND_PLAY_HF(hardfork_4_wallet_transfer_with_mandatory_mixins, "3-*"); - GENERATE_AND_PLAY(hardfork_4_wallet_sweep_bare_outs); - GENERATE_AND_PLAY_HF(hardfork_4_pop_tx_from_global_index, "4-*"); - - // HF5 - GENERATE_AND_PLAY_HF(hard_fork_5_tx_version, "5-*"); - - // HF6 - GENERATE_AND_PLAY(hard_fork_6_intrinsic_payment_id_basic_test); - GENERATE_AND_PLAY(hard_fork_6_intrinsic_payment_id_rpc_test); - - GENERATE_AND_PLAY_HF(isolate_auditable_and_proof, "2-*"); - - GENERATE_AND_PLAY(zarcanum_basic_test); - - GENERATE_AND_PLAY_HF(multiassets_basic_test, "4-*"); - GENERATE_AND_PLAY_HF(ionic_swap_basic_test, "4-*"); - GENERATE_AND_PLAY_HF(ionic_swap_exact_amounts_test, "4-*"); - GENERATE_AND_PLAY(zarcanum_test_n_inputs_validation); - GENERATE_AND_PLAY(zarcanum_gen_time_balance); - GENERATE_AND_PLAY(zarcanum_txs_with_big_shuffled_decoy_set_shuffled); - GENERATE_AND_PLAY(zarcanum_pos_block_math); - GENERATE_AND_PLAY(zarcanum_in_alt_chain); - GENERATE_AND_PLAY_HF(zarcanum_in_alt_chain_2, "4-*"); - GENERATE_AND_PLAY(assets_and_explicit_native_coins_in_outs); - GENERATE_AND_PLAY(zarcanum_block_with_txs); - GENERATE_AND_PLAY(asset_depoyment_and_few_zc_utxos); - GENERATE_AND_PLAY_HF(assets_and_pos_mining, "4-*"); - GENERATE_AND_PLAY_HF(asset_emission_and_unconfirmed_balance, "4-*"); - GENERATE_AND_PLAY_HF(asset_operation_in_consolidated_tx, "4-*"); - GENERATE_AND_PLAY_HF(asset_operation_and_hardfork_checks, "4-*"); - GENERATE_AND_PLAY_HF(eth_signed_asset_basics, "5-*"); // TODO: make HF4 version - GENERATE_AND_PLAY_HF(eth_signed_asset_via_rpc, "5-*"); // TODO: make HF4 version - //GENERATE_AND_PLAY_HF(asset_current_and_total_supplies_comparative_constraints, "4-*"); <-- temporary disabled, waiting for Stepan's fix -- sowle - GENERATE_AND_PLAY_HF(several_asset_emit_burn_txs_in_pool, "5-*"); - GENERATE_AND_PLAY_HF(assets_transfer_with_smallest_amount, "4-*"); - GENERATE_AND_PLAY_HF(asset_operations_and_chain_switching, "4-*"); - - GENERATE_AND_PLAY_HF(pos_fuse_test, "4-*"); - GENERATE_AND_PLAY_HF(wallet_reorganize_and_trim_test, "4-*"); - GENERATE_AND_PLAY_HF(wallet_rpc_thirdparty_custody, "5-*"); - - GENERATE_AND_PLAY_HF(attachment_isolation_test, "4-*"); - - // GENERATE_AND_PLAY(gen_block_reward); - // END OF TESTS */ + // Postponed tests list. + fill_postponed_tests_set(postponed_tests); + + register_all_tests( + stop_on_first_fail, + skip_all_till_the_end, + tests_count, + unique_tests_count, + failed_tests, + tests_running_time, + is_test_eligible_to_run, + is_hf_test_eligible_to_run); + + // run + run_registered_tests( + stop_on_first_fail, + skip_all_till_the_end, + tests_count, + unique_tests_count, + failed_tests, + tests_running_time, + is_test_eligible_to_run, + is_hf_test_eligible_to_run); size_t failed_postponed_tests_count = 0; uint64_t total_time = 0; + if (!tests_running_time.empty()) { - uint64_t max_time = std::max_element(tests_running_time.begin(), tests_running_time.end(), [](tests_running_time_t::value_type& lhs, tests_running_time_t::value_type& rhs)->bool { return lhs.second < rhs.second; })->second; - uint64_t max_test_name_len = std::max_element(tests_running_time.begin(), tests_running_time.end(), [](tests_running_time_t::value_type& lhs, tests_running_time_t::value_type& rhs)->bool { return lhs.first.size() < rhs.first.size(); })->first.size(); - size_t bar_width = 70; - for (auto i : tests_running_time) + uint64_t max_time = std::max_element( + tests_running_time.begin(), tests_running_time.end(), + [](tests_running_time_t::value_type& lhs, tests_running_time_t::value_type& rhs) -> bool { + return lhs.second < rhs.second; + })->second; + + uint64_t max_test_name_len = std::max_element( + tests_running_time.begin(), tests_running_time.end(), + [](tests_running_time_t::value_type& lhs, tests_running_time_t::value_type& rhs) -> bool { + return lhs.first.size() < rhs.first.size(); + })->first.size(); + + const size_t bar_width = 70; + + // Optional: keep this only in single-process mode to reduce console noise. + if (!is_worker) + { + for (const auto& i : tests_running_time) + { + const bool failed = failed_tests.count(i.first) != 0; + const bool postponed = postponed_tests.count(i.first) != 0; + if (failed && postponed) + ++failed_postponed_tests_count; + + std::string bar(bar_width * i.second / max_time, '#'); + std::cout << (failed ? (postponed ? concolor::yellow : concolor::magenta) : concolor::green) + << std::left << std::setw(max_test_name_len + 1) << i.first + << "\t" << std::setw(10) << i.second << " ms \t" << bar << std::endl; + + total_time += i.second; + } + } + else { - bool failed = failed_tests.count(i.first) != 0; - bool postponed = postponed_tests.count(i.first) != 0; - if (failed && postponed) - ++failed_postponed_tests_count; - std::string bar(bar_width * i.second / max_time, '#'); - std::cout << (failed ? (postponed ? concolor::yellow : concolor::magenta) : concolor::green) << std::left << std::setw(max_test_name_len + 1) << i.first << "\t" << std::setw(10) << i.second << " ms \t" << bar << std::endl; - total_time += i.second; + // Workers still must compute totals. + for (const auto& i : tests_running_time) + total_time += i.second; + + for (const auto& name : failed_tests) + if (postponed_tests.count(name) != 0) + ++failed_postponed_tests_count; } } - if (skip_all_till_the_end) - std::cout << ENDL << concolor::yellow << "(execution interrupted at the first failure; not all tests were run)" << ENDL; + if (skip_all_till_the_end && !is_worker) + std::cout << ENDL << concolor::yellow + << "(execution interrupted at the first failure; not all tests were run)" + << ENDL; serious_failures_count = failed_tests.size() - failed_postponed_tests_count; - if (!postponed_tests.empty()) + // Workers write per-worker report for parent aggregation. + if (is_worker) { - std::cout << concolor::yellow << std::endl << postponed_tests.size() << " POSTPONED TESTS:" << std::endl; - for(auto& el : postponed_tests) - std::cout << " " << el << std::endl; + worker_report rep; + rep.worker_id = static_cast(worker_id); + rep.processes = processes; + rep.tests_count = tests_count; + rep.unique_tests_count = unique_tests_count; + rep.total_time_ms = total_time; + rep.tests_running_time = tests_running_time; + rep.failed_tests = failed_tests; + rep.skip_all_till_the_end = skip_all_till_the_end; + rep.exit_code = (serious_failures_count == 0 ? 0 : 1); + + (void)write_worker_report_file(rep); } - - std::cout << (serious_failures_count == 0 ? concolor::green : concolor::magenta); - std::cout << "\nREPORT:\n"; - std::cout << " Unique tests run: " << unique_tests_count << std::endl; - std::cout << " Total tests run: " << tests_count << std::endl; - - std::cout << " Failures: " << serious_failures_count << " (postponed failures: " << failed_postponed_tests_count << ")" << std::endl; - std::cout << " Postponed: " << postponed_tests.size() << std::endl; - std::cout << " Total time: " << total_time / 1000 << " s. (" << (tests_count > 0 ? total_time / tests_count : 0) << " ms per test in average)" << std::endl; - if (!failed_tests.empty()) + + // Single-process run: keep local report (parent aggregation is only for multi-process mode). + if (!is_worker) { - std::cout << "FAILED/POSTPONED TESTS:\n"; - for (auto test_name : failed_tests) + if (!postponed_tests.empty()) + { + std::cout << concolor::yellow << std::endl << postponed_tests.size() << " POSTPONED TESTS:" << std::endl; + for (const auto& el : postponed_tests) + std::cout << " " << el << std::endl; + } + + std::cout << (serious_failures_count == 0 ? concolor::green : concolor::magenta); + std::cout << "\nREPORT:\n"; + std::cout << " Unique tests run: " << unique_tests_count << std::endl; + std::cout << " Total tests run: " << tests_count << std::endl; + + std::cout << " Failures: " << serious_failures_count + << " (postponed failures: " << failed_postponed_tests_count << ")" + << std::endl; + + std::cout << " Postponed: " << postponed_tests.size() << std::endl; + + std::cout << " Total time: " << total_time / 1000 + << " s. (" << (tests_count > 0 ? total_time / tests_count : 0) + << " ms per test in average)" + << std::endl; + + if (!failed_tests.empty()) { - bool postponed = postponed_tests.count(test_name); - std::cout << " " << (postponed ? "POSTPONED: " : "FAILED: ") << test_name << '\n'; + std::cout << "FAILED/POSTPONED TESTS:\n"; + for (const auto& test_name : failed_tests) + { + const bool postponed = postponed_tests.count(test_name) != 0; + std::cout << " " << (postponed ? "POSTPONED: " : "FAILED: ") << test_name << '\n'; + } } + + std::cout << concolor::normal << std::endl; } - std::cout << concolor::normal << std::endl; } - /*{ - std::cout << concolor::magenta << "Wrong arguments" << concolor::normal << std::endl; - std::cout << desc_options << std::endl; - return 2; - }*/ #ifdef _WIN32 ::Beep(1050, 1000); #endif From 59df5f241c4db9404210e3c5b855d339bcfb0f6b Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Mon, 12 Jan 2026 16:00:46 +0300 Subject: [PATCH 03/28] fix generating log files --- tests/core_tests/chaingen_main.cpp | 321 ++++++++++++++++++++--------- 1 file changed, 224 insertions(+), 97 deletions(-) diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index ce9f4beb7..633b04b1a 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -58,8 +58,6 @@ namespace const char* JSON_NAME = "name"; const char* JSON_MS = "ms"; - const char* WORKER_STDOUT_FILENAME = "worker_stdout.log"; - const char* WORKER_STDERR_FILENAME = "worker_stderr.log"; const char* WORKER_REPORT_FILENAME = "coretests_report.json"; const char* ARG_WORKER_ID = "--worker-id"; @@ -965,7 +963,6 @@ static std::filesystem::path get_run_root_path() std::filesystem::path run_root_path(run_root); - // Make it absolute to avoid nesting when workers use start_dir. if (run_root_path.is_relative()) run_root_path = std::filesystem::absolute(run_root_path); @@ -990,7 +987,6 @@ static std::filesystem::path get_single_process_taken_tests_log_path() static void log_test_taken_by_this_process(const std::string& test_name) { - // Append-only log: one line per test, written when the test is about to start. try { const uint32_t processes = command_line::get_arg(g_vm, arg_processes); @@ -1519,16 +1515,6 @@ static std::string make_worker_data_dir(const std::filesystem::path& run_root_ab return worker_dir.string(); } -static std::filesystem::path get_worker_stdout_path(uint32_t worker_id) -{ - return get_worker_dir_path(worker_id) / WORKER_STDOUT_FILENAME; -} - -static std::filesystem::path get_worker_stderr_path(uint32_t worker_id) -{ - return get_worker_dir_path(worker_id) / WORKER_STDERR_FILENAME; -} - static std::filesystem::path get_worker_report_path(uint32_t worker_id) { return get_worker_dir_path(worker_id) / WORKER_REPORT_FILENAME; @@ -1707,55 +1693,114 @@ static int print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wa return serious_failures_count == 0 ? 0 : 1; } //-------------------------------------------------------------------------- -static std::string tail_file_lines(const std::filesystem::path& p, size_t max_lines) +static std::string sanitize_filename(std::string s) { - std::ifstream f(p, std::ios::in | std::ios::binary); - if (!f.is_open()) - return std::string(); + for (char& c : s) + { + const bool ok = + (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '.' || c == '_' || c == '-'; + if (!ok) + c = '_'; + } - f.seekg(0, std::ios::end); - std::streamoff size = f.tellg(); - if (size <= 0) - return std::string(); + // Avoid empty name + if (s.empty()) + s = "unnamed_test"; - const std::streamoff chunk = 64 * 1024; // 64 KB - std::streamoff from = size > chunk ? (size - chunk) : 0; - f.seekg(from, std::ios::beg); + // Keep filenames reasonably short + constexpr size_t kMax = 180; + if (s.size() > kMax) + s.resize(kMax); - std::string buf; - buf.resize(static_cast(size - from)); - f.read(&buf[0], buf.size()); + return s; +} - std::vector lines; - lines.reserve(max_lines + 10); - std::string cur; +static std::filesystem::path make_unique_log_path(const std::filesystem::path& dir, const std::string& base_name_no_ext) +{ + std::filesystem::path p = dir / (base_name_no_ext + ".log"); + if (!std::filesystem::exists(p)) + return p; - for (char c : buf) + for (size_t i = 2; i < 10000; ++i) { - if (c == '\r') - continue; - if (c == '\n') - { - lines.emplace_back(std::move(cur)); - cur.clear(); - } - else + std::filesystem::path alt = dir / (base_name_no_ext + "_" + std::to_string(i) + ".log"); + if (!std::filesystem::exists(alt)) + return alt; + } + + return p; // fallback +} + +static bool parse_test_name_from_header(const std::string& line, std::string& out_name) +{ + constexpr const char* kPrefix = "#TEST# >>>>"; + auto pos = line.find(kPrefix); + if (pos != 0) + return false; + + std::string rest = line.substr(std::strlen(kPrefix)); + // trim left + while (!rest.empty() && std::isspace(static_cast(rest.front()))) + rest.erase(rest.begin()); + + auto end = rest.find("<<<<"); + if (end == std::string::npos) + return false; + + std::string name = rest.substr(0, end); + // trim right + while (!name.empty() && std::isspace(static_cast(name.back()))) + name.pop_back(); + + if (name.empty()) + return false; + + out_name = name; + return true; +} + +static std::vector find_logs_for_test( + const std::filesystem::path& worker_dir, + const std::string& test_name) +{ + std::vector result; + + const std::string base = sanitize_filename(test_name); + const std::string base_prefix = base + "_"; + + try + { + for (auto& de : std::filesystem::directory_iterator(worker_dir)) { - cur.push_back(c); + if (!de.is_regular_file()) + continue; + + const auto p = de.path(); + if (p.extension() != ".log") + continue; + + const std::string stem = p.stem().string(); + if (stem == base || stem.rfind(base_prefix, 0) == 0) + result.push_back(p); } } - if (!cur.empty()) - lines.emplace_back(std::move(cur)); - - if (lines.empty()) - return std::string(); + catch (...) {} - size_t start = lines.size() > max_lines ? (lines.size() - max_lines) : 0; - std::ostringstream out; - for (size_t i = start; i < lines.size(); ++i) - out << lines[i] << "\n"; + std::sort(result.begin(), result.end(), + [](const std::filesystem::path& a, const std::filesystem::path& b) + { + std::error_code ec1, ec2; + const auto ta = std::filesystem::last_write_time(a, ec1); + const auto tb = std::filesystem::last_write_time(b, ec2); + if (!ec1 && !ec2 && ta != tb) + return ta > tb; + return a.string() < b.string(); + }); - return out.str(); + return result; } static void print_worker_failure_reasons(uint32_t processes, const std::vector& worker_exit_codes) @@ -1764,10 +1809,13 @@ static void print_worker_failure_reasons(uint32_t processes, const std::vector(); kids_err[i] = std::make_unique(); @@ -1939,31 +1987,113 @@ static int run_workers_and_wait(int argc, char* argv[]) bp::std_err > *kids_err[i] )); - // Tee stdout: worker -> console + file - tee_threads.emplace_back([p = stdout_path, s = kids_out[i].get()]() + // Tee stdout: worker -> console; capture per-test log ONLY if failed + tee_threads.emplace_back([worker_dir, s = kids_out[i].get()]() { - std::ofstream f(p, std::ios::out | std::ios::binary | std::ios::app); + std::filesystem::create_directories(worker_dir); + + bool capturing = false; + std::string current_test_header; + std::filesystem::path current_log_path; + std::ofstream f_log; + + auto close_log = [&]() + { + if (f_log.is_open()) + f_log.close(); + }; + + auto start_capture = [&](const std::string& header_line) + { + close_log(); + capturing = true; + current_test_header = header_line; + + std::string test_name; + if (!parse_test_name_from_header(header_line, test_name)) + test_name = "unknown_test"; + + const std::string safe = sanitize_filename(test_name); + current_log_path = make_unique_log_path(worker_dir, safe); + + f_log.open(current_log_path, std::ios::out | std::ios::binary | std::ios::trunc); + if (f_log.is_open()) + { + f_log << "===== TEST OUTPUT BEGIN: " << header_line << " =====\n"; + f_log.flush(); + } + }; + + auto discard_capture = [&]() + { + close_log(); + capturing = false; + current_test_header.clear(); + + if (!current_log_path.empty()) + { + std::error_code ec; + std::filesystem::remove(current_log_path, ec); + } + current_log_path.clear(); + }; + + auto commit_capture = [&]() + { + if (f_log.is_open()) + { + f_log << "===== TEST OUTPUT END (FAILED): " << current_test_header << " =====\n"; + f_log.flush(); + } + close_log(); + + capturing = false; + current_test_header.clear(); + current_log_path.clear(); + }; + std::string line; while (std::getline(*s, line)) { - f << line << "\n"; - f.flush(); - - std::lock_guard lk(cout_mx); - std::cout << line << std::endl; + // Start marker + if (line.rfind("#TEST# >>>>", 0) == 0) + start_capture(line); + + // If capturing, write everything into per-test log + if (capturing && f_log.is_open()) + { + f_log << line << "\n"; + f_log.flush(); + } + + // End markers + if (capturing && line.find("<<<< Succeeded") != std::string::npos) + { + discard_capture(); + } + else if (capturing && line.find("<<<< FAILED") != std::string::npos) + { + commit_capture(); + } + + // Console output + { + std::lock_guard lk(cout_mx); + std::cout << line << std::endl; + } } + + // Worker died mid-test without FAILED marker: keep log as failed + if (capturing) + commit_capture(); }); - // Tee stderr: worker -> console(stderr) + file - tee_threads.emplace_back([p = stderr_path, s = kids_err[i].get()]() + // Tee stderr: worker -> console(stderr) ONLY (no worker_stderr.log) + tee_threads.emplace_back([s = kids_err[i].get()]() { - std::ofstream f(p, std::ios::out | std::ios::binary | std::ios::app); std::string line; while (std::getline(*s, line)) { - f << line << "\n"; - f.flush(); - std::lock_guard lk(cerr_mx); std::cerr << line << std::endl; } @@ -2060,7 +2190,6 @@ int main(int argc, char* argv[]) command_line::add_arg(desc_options, arg_processes); command_line::add_arg(desc_options, arg_worker_id); command_line::add_arg(desc_options, arg_run_root); - command_line::add_arg(desc_options, arg_list_tests); currency::core::init_options(desc_options); tools::db::db_backend_selector::init_options(desc_options); @@ -2132,10 +2261,9 @@ int main(int argc, char* argv[]) } else { - // Do not comment this out: many tests have normal-negative checks. epee::debug::get_set_enable_assert(true, command_line::get_arg(g_vm, arg_enable_debug_asserts)); - std::unordered_multimap specific_tests_to_run; // test_name -> hf; if empty, all tests should be run + std::unordered_multimap specific_tests_to_run; CHECK_AND_ASSERT_MES(parse_cmd_specific_tests_to_run(specific_tests_to_run), 1, "Error while parsing specific tests to run"); std::set postponed_tests; @@ -2227,7 +2355,6 @@ int main(int argc, char* argv[]) const size_t bar_width = 70; - // Optional: keep this only in single-process mode to reduce console noise. if (!is_worker) { for (const auto& i : tests_running_time) From a99d06efd20dbe7eeab78670d612c92e42f82f8a Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Mon, 12 Jan 2026 16:15:58 +0300 Subject: [PATCH 04/28] renamed macros --- tests/core_tests/chaingen_main.cpp | 642 ++++++++++++++--------------- 1 file changed, 321 insertions(+), 321 deletions(-) diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index 633b04b1a..ae9cfafdd 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -97,7 +97,7 @@ namespace return 1; \ } -#define REGISTER_TEST(genclass) \ +#define GENERATE_AND_PLAY(genclass) \ do { \ const std::string __test_name = #genclass; \ g_test_jobs.push_back(test_job{ \ @@ -118,7 +118,7 @@ namespace }); \ } while (0) -#define REGISTER_TEST_HF(genclass, hardfork_str_mask) \ +#define GENERATE_AND_PLAY_HF(genclass, hardfork_str_mask) \ do { \ const std::string __gen_name = #genclass; \ std::vector __hardforks = parse_hardfork_str_mask(hardfork_str_mask); \ @@ -1105,357 +1105,357 @@ static void register_all_tests( g_test_jobs.clear(); - // TODO // REGISTER_TEST(wallet_spend_form_auditable_and_track); - REGISTER_TEST(gen_block_big_major_version); - - REGISTER_TEST(pos_minting_tx_packing); - - REGISTER_TEST(multisig_wallet_test); - REGISTER_TEST(multisig_wallet_test_many_dst); - REGISTER_TEST(multisig_wallet_heterogenous_dst); - REGISTER_TEST(multisig_wallet_same_dst_addr); - REGISTER_TEST(multisig_wallet_ms_to_ms); - REGISTER_TEST(multisig_minimum_sigs); - REGISTER_TEST(multisig_and_fake_outputs); - REGISTER_TEST(multisig_and_unlock_time); - REGISTER_TEST(multisig_and_coinbase); - REGISTER_TEST(multisig_with_same_id_in_pool); - REGISTER_TEST_HF(multisig_and_checkpoints, "0"); // TODO: fix for HF 1-3 (checkpoint hash check) - REGISTER_TEST(multisig_and_checkpoints_bad_txs); - REGISTER_TEST(multisig_and_altchains); - REGISTER_TEST(multisig_out_make_and_spent_in_altchain); - REGISTER_TEST(multisig_unconfirmed_transfer_and_multiple_scan_pool_calls); - REGISTER_TEST(multisig_out_spent_in_altchain_case_b4); - REGISTER_TEST(multisig_n_participants_seq_signing); - - REGISTER_TEST(ref_by_id_basics); - REGISTER_TEST(ref_by_id_mixed_inputs_types); - - REGISTER_TEST(escrow_wallet_test); - REGISTER_TEST(escrow_w_and_fake_outputs); - REGISTER_TEST(escrow_incorrect_proposal); - REGISTER_TEST(escrow_proposal_expiration); - REGISTER_TEST(escrow_proposal_and_accept_expiration); - REGISTER_TEST(escrow_incorrect_proposal_acceptance); - REGISTER_TEST(escrow_custom_test); - REGISTER_TEST(escrow_incorrect_cancel_proposal); - REGISTER_TEST(escrow_proposal_not_enough_money); - REGISTER_TEST(escrow_cancellation_and_tx_order); - REGISTER_TEST(escrow_cancellation_proposal_expiration); - REGISTER_TEST(escrow_cancellation_acceptance_expiration); - // REGISTER_TEST(escrow_proposal_acceptance_in_alt_chain); -- work in progress - REGISTER_TEST(escrow_zero_amounts); - REGISTER_TEST(escrow_balance); - - REGISTER_TEST(escrow_altchain_meta_test<0>); - REGISTER_TEST(escrow_altchain_meta_test<1>); - REGISTER_TEST(escrow_altchain_meta_test<2>); - REGISTER_TEST(escrow_altchain_meta_test<3>); - REGISTER_TEST(escrow_altchain_meta_test<4>); - REGISTER_TEST(escrow_altchain_meta_test<5>); - REGISTER_TEST(escrow_altchain_meta_test<6>); - REGISTER_TEST(escrow_altchain_meta_test<7>); - REGISTER_TEST(escrow_altchain_meta_test<8>); + // TODO // GENERATE_AND_PLAY(wallet_spend_form_auditable_and_track); + GENERATE_AND_PLAY(gen_block_big_major_version); + + GENERATE_AND_PLAY(pos_minting_tx_packing); + + GENERATE_AND_PLAY(multisig_wallet_test); + GENERATE_AND_PLAY(multisig_wallet_test_many_dst); + GENERATE_AND_PLAY(multisig_wallet_heterogenous_dst); + GENERATE_AND_PLAY(multisig_wallet_same_dst_addr); + GENERATE_AND_PLAY(multisig_wallet_ms_to_ms); + GENERATE_AND_PLAY(multisig_minimum_sigs); + GENERATE_AND_PLAY(multisig_and_fake_outputs); + GENERATE_AND_PLAY(multisig_and_unlock_time); + GENERATE_AND_PLAY(multisig_and_coinbase); + GENERATE_AND_PLAY(multisig_with_same_id_in_pool); + GENERATE_AND_PLAY_HF(multisig_and_checkpoints, "0"); // TODO: fix for HF 1-3 (checkpoint hash check) + GENERATE_AND_PLAY(multisig_and_checkpoints_bad_txs); + GENERATE_AND_PLAY(multisig_and_altchains); + GENERATE_AND_PLAY(multisig_out_make_and_spent_in_altchain); + GENERATE_AND_PLAY(multisig_unconfirmed_transfer_and_multiple_scan_pool_calls); + GENERATE_AND_PLAY(multisig_out_spent_in_altchain_case_b4); + GENERATE_AND_PLAY(multisig_n_participants_seq_signing); + + GENERATE_AND_PLAY(ref_by_id_basics); + GENERATE_AND_PLAY(ref_by_id_mixed_inputs_types); + + GENERATE_AND_PLAY(escrow_wallet_test); + GENERATE_AND_PLAY(escrow_w_and_fake_outputs); + GENERATE_AND_PLAY(escrow_incorrect_proposal); + GENERATE_AND_PLAY(escrow_proposal_expiration); + GENERATE_AND_PLAY(escrow_proposal_and_accept_expiration); + GENERATE_AND_PLAY(escrow_incorrect_proposal_acceptance); + GENERATE_AND_PLAY(escrow_custom_test); + GENERATE_AND_PLAY(escrow_incorrect_cancel_proposal); + GENERATE_AND_PLAY(escrow_proposal_not_enough_money); + GENERATE_AND_PLAY(escrow_cancellation_and_tx_order); + GENERATE_AND_PLAY(escrow_cancellation_proposal_expiration); + GENERATE_AND_PLAY(escrow_cancellation_acceptance_expiration); + // GENERATE_AND_PLAY(escrow_proposal_acceptance_in_alt_chain); -- work in progress + GENERATE_AND_PLAY(escrow_zero_amounts); + GENERATE_AND_PLAY(escrow_balance); + + GENERATE_AND_PLAY(escrow_altchain_meta_test<0>); + GENERATE_AND_PLAY(escrow_altchain_meta_test<1>); + GENERATE_AND_PLAY(escrow_altchain_meta_test<2>); + GENERATE_AND_PLAY(escrow_altchain_meta_test<3>); + GENERATE_AND_PLAY(escrow_altchain_meta_test<4>); + GENERATE_AND_PLAY(escrow_altchain_meta_test<5>); + GENERATE_AND_PLAY(escrow_altchain_meta_test<6>); + GENERATE_AND_PLAY(escrow_altchain_meta_test<7>); + GENERATE_AND_PLAY(escrow_altchain_meta_test<8>); static_assert(escrow_altchain_meta_test_data<9>::empty_marker, ""); // make sure there are no sub-tests left - REGISTER_TEST(offers_expiration_test); - REGISTER_TEST(offers_tests); - REGISTER_TEST(offers_filtering_1); - //REGISTER_TEST(offers_handling_on_chain_switching); - REGISTER_TEST(offer_removing_and_selected_output); - REGISTER_TEST(offers_multiple_update); - REGISTER_TEST(offer_sig_validity_in_update_and_cancel); - REGISTER_TEST(offer_lifecycle_via_tx_pool); - REGISTER_TEST(offers_updating_hack); - REGISTER_TEST(offer_cancellation_with_zero_fee); - - REGISTER_TEST(gen_crypted_attachments); - REGISTER_TEST(gen_checkpoints_attachments_basic); - REGISTER_TEST(gen_checkpoints_invalid_keyimage); - REGISTER_TEST(gen_checkpoints_altblock_before_and_after_cp); - REGISTER_TEST(gen_checkpoints_block_in_future); - REGISTER_TEST(gen_checkpoints_altchain_far_before_cp); - REGISTER_TEST(gen_checkpoints_block_in_future_after_cp); - REGISTER_TEST(gen_checkpoints_prun_txs_after_blockchain_load); - REGISTER_TEST(gen_checkpoints_reorganize); - REGISTER_TEST(gen_checkpoints_pos_validation_on_altchain); - REGISTER_TEST(gen_checkpoints_and_invalid_tx_to_pool); - REGISTER_TEST(gen_checkpoints_set_after_switching_to_altchain); - REGISTER_TEST_HF(gen_no_attchments_in_coinbase, "3"); - REGISTER_TEST(gen_no_attchments_in_coinbase_gentime); - - REGISTER_TEST_HF(gen_alias_tests, "3-*"); - REGISTER_TEST_HF(gen_alias_strange_data, "3-*"); - REGISTER_TEST_HF(gen_alias_concurrency_with_switch, "3-*"); - REGISTER_TEST_HF(gen_alias_same_alias_in_tx_pool, "3-*"); - REGISTER_TEST_HF(gen_alias_switch_and_tx_pool, "3-*"); - REGISTER_TEST_HF(gen_alias_update_after_addr_changed, "3-*"); - REGISTER_TEST_HF(gen_alias_blocking_reg_by_invalid_tx, "3-*"); - REGISTER_TEST_HF(gen_alias_blocking_update_by_invalid_tx, "3-*"); - REGISTER_TEST_HF(gen_alias_reg_with_locked_money, "*"); - REGISTER_TEST_HF(gen_alias_too_small_reward, "3-*"); - REGISTER_TEST_HF(gen_alias_too_much_reward, "3-*"); - REGISTER_TEST_HF(gen_alias_tx_no_outs, "*"); - REGISTER_TEST_HF(gen_alias_switch_and_check_block_template, "3-*"); - REGISTER_TEST_HF(gen_alias_too_many_regs_in_block_template, "3"); // disabled in HF4 due to tx outputs count limitation - REGISTER_TEST_HF(gen_alias_update_for_free, "3-*"); - REGISTER_TEST_HF(gen_alias_in_coinbase, "3-*"); - - REGISTER_TEST(gen_wallet_basic_transfer); - REGISTER_TEST(gen_wallet_refreshing_on_chain_switch); - REGISTER_TEST(gen_wallet_refreshing_on_chain_switch_2); - REGISTER_TEST(gen_wallet_unconfirmed_tx_from_tx_pool); - REGISTER_TEST_HF(gen_wallet_save_load_and_balance, "*"); - REGISTER_TEST_HF(gen_wallet_mine_pos_block, "3-*"); - REGISTER_TEST(gen_wallet_unconfirmed_outdated_tx); - REGISTER_TEST(gen_wallet_unlock_by_block_and_by_time); - REGISTER_TEST(gen_wallet_payment_id); - REGISTER_TEST(gen_wallet_oversized_payment_id); - REGISTER_TEST(gen_wallet_transfers_and_outdated_unconfirmed_txs); - REGISTER_TEST(gen_wallet_transfers_and_chain_switch); - REGISTER_TEST(gen_wallet_decrypted_payload_items); - REGISTER_TEST_HF(gen_wallet_alias_and_unconfirmed_txs, "3-*"); - REGISTER_TEST_HF(gen_wallet_alias_via_special_wallet_funcs, "3-*"); - REGISTER_TEST(gen_wallet_fake_outputs_randomness); - REGISTER_TEST(gen_wallet_fake_outputs_not_enough); - REGISTER_TEST(gen_wallet_offers_basic); - REGISTER_TEST(gen_wallet_offers_size_limit); - REGISTER_TEST(gen_wallet_dust_to_account); - REGISTER_TEST(gen_wallet_selecting_pos_entries); - REGISTER_TEST(gen_wallet_spending_coinstake_after_minting); - REGISTER_TEST(gen_wallet_fake_outs_while_having_too_little_own_outs); - // REGISTER_TEST(premine_wallet_test); // tests premine wallets; wallet files nedded; by demand only - REGISTER_TEST(mined_balance_wallet_test); - REGISTER_TEST(wallet_outputs_with_same_key_image); - REGISTER_TEST(wallet_unconfirmed_tx_expiration); - REGISTER_TEST(wallet_unconfimed_tx_balance); - REGISTER_TEST_HF(packing_outputs_on_pos_minting_wallet, "3"); - REGISTER_TEST_HF(wallet_watch_only_and_chain_switch, "3"); - REGISTER_TEST_HF(wallet_and_sweep_below, "3-*"); - - REGISTER_TEST(wallet_rpc_integrated_address); - REGISTER_TEST(wallet_rpc_integrated_address_transfer); - REGISTER_TEST(wallet_rpc_transfer); - REGISTER_TEST(wallet_rpc_alias_tests); - REGISTER_TEST_HF(wallet_rpc_exchange_suite, "3,4"); - REGISTER_TEST_HF(wallet_true_rpc_pos_mining, "4-*"); - REGISTER_TEST_HF(wallet_rpc_cold_signing, "3,5-*"); - // REGISTER_TEST_HF(wallet_rpc_multiple_receivers, "5-*"); work in progress -- sowle - REGISTER_TEST_HF(wallet_chain_switch_with_spending_the_same_ki, "3"); - REGISTER_TEST(wallet_sending_to_integrated_address); - REGISTER_TEST_HF(block_template_blacklist_test, "4-*"); - REGISTER_TEST_HF(wallet_rpc_hardfork_verification, "5"); - - // REGISTER_TEST(emission_test); // simulate 1 year of blockchain, too long run (1 y ~= 1 hr), by demand only + GENERATE_AND_PLAY(offers_expiration_test); + GENERATE_AND_PLAY(offers_tests); + GENERATE_AND_PLAY(offers_filtering_1); + //GENERATE_AND_PLAY(offers_handling_on_chain_switching); + GENERATE_AND_PLAY(offer_removing_and_selected_output); + GENERATE_AND_PLAY(offers_multiple_update); + GENERATE_AND_PLAY(offer_sig_validity_in_update_and_cancel); + GENERATE_AND_PLAY(offer_lifecycle_via_tx_pool); + GENERATE_AND_PLAY(offers_updating_hack); + GENERATE_AND_PLAY(offer_cancellation_with_zero_fee); + + GENERATE_AND_PLAY(gen_crypted_attachments); + GENERATE_AND_PLAY(gen_checkpoints_attachments_basic); + GENERATE_AND_PLAY(gen_checkpoints_invalid_keyimage); + GENERATE_AND_PLAY(gen_checkpoints_altblock_before_and_after_cp); + GENERATE_AND_PLAY(gen_checkpoints_block_in_future); + GENERATE_AND_PLAY(gen_checkpoints_altchain_far_before_cp); + GENERATE_AND_PLAY(gen_checkpoints_block_in_future_after_cp); + GENERATE_AND_PLAY(gen_checkpoints_prun_txs_after_blockchain_load); + GENERATE_AND_PLAY(gen_checkpoints_reorganize); + GENERATE_AND_PLAY(gen_checkpoints_pos_validation_on_altchain); + GENERATE_AND_PLAY(gen_checkpoints_and_invalid_tx_to_pool); + GENERATE_AND_PLAY(gen_checkpoints_set_after_switching_to_altchain); + GENERATE_AND_PLAY_HF(gen_no_attchments_in_coinbase, "3"); + GENERATE_AND_PLAY(gen_no_attchments_in_coinbase_gentime); + + GENERATE_AND_PLAY_HF(gen_alias_tests, "3-*"); + GENERATE_AND_PLAY_HF(gen_alias_strange_data, "3-*"); + GENERATE_AND_PLAY_HF(gen_alias_concurrency_with_switch, "3-*"); + GENERATE_AND_PLAY_HF(gen_alias_same_alias_in_tx_pool, "3-*"); + GENERATE_AND_PLAY_HF(gen_alias_switch_and_tx_pool, "3-*"); + GENERATE_AND_PLAY_HF(gen_alias_update_after_addr_changed, "3-*"); + GENERATE_AND_PLAY_HF(gen_alias_blocking_reg_by_invalid_tx, "3-*"); + GENERATE_AND_PLAY_HF(gen_alias_blocking_update_by_invalid_tx, "3-*"); + GENERATE_AND_PLAY_HF(gen_alias_reg_with_locked_money, "*"); + GENERATE_AND_PLAY_HF(gen_alias_too_small_reward, "3-*"); + GENERATE_AND_PLAY_HF(gen_alias_too_much_reward, "3-*"); + GENERATE_AND_PLAY_HF(gen_alias_tx_no_outs, "*"); + GENERATE_AND_PLAY_HF(gen_alias_switch_and_check_block_template, "3-*"); + GENERATE_AND_PLAY_HF(gen_alias_too_many_regs_in_block_template, "3"); // disabled in HF4 due to tx outputs count limitation + GENERATE_AND_PLAY_HF(gen_alias_update_for_free, "3-*"); + GENERATE_AND_PLAY_HF(gen_alias_in_coinbase, "3-*"); + + GENERATE_AND_PLAY(gen_wallet_basic_transfer); + GENERATE_AND_PLAY(gen_wallet_refreshing_on_chain_switch); + GENERATE_AND_PLAY(gen_wallet_refreshing_on_chain_switch_2); + GENERATE_AND_PLAY(gen_wallet_unconfirmed_tx_from_tx_pool); + GENERATE_AND_PLAY_HF(gen_wallet_save_load_and_balance, "*"); + GENERATE_AND_PLAY_HF(gen_wallet_mine_pos_block, "3-*"); + GENERATE_AND_PLAY(gen_wallet_unconfirmed_outdated_tx); + GENERATE_AND_PLAY(gen_wallet_unlock_by_block_and_by_time); + GENERATE_AND_PLAY(gen_wallet_payment_id); + GENERATE_AND_PLAY(gen_wallet_oversized_payment_id); + GENERATE_AND_PLAY(gen_wallet_transfers_and_outdated_unconfirmed_txs); + GENERATE_AND_PLAY(gen_wallet_transfers_and_chain_switch); + GENERATE_AND_PLAY(gen_wallet_decrypted_payload_items); + GENERATE_AND_PLAY_HF(gen_wallet_alias_and_unconfirmed_txs, "3-*"); + GENERATE_AND_PLAY_HF(gen_wallet_alias_via_special_wallet_funcs, "3-*"); + GENERATE_AND_PLAY(gen_wallet_fake_outputs_randomness); + GENERATE_AND_PLAY(gen_wallet_fake_outputs_not_enough); + GENERATE_AND_PLAY(gen_wallet_offers_basic); + GENERATE_AND_PLAY(gen_wallet_offers_size_limit); + GENERATE_AND_PLAY(gen_wallet_dust_to_account); + GENERATE_AND_PLAY(gen_wallet_selecting_pos_entries); + GENERATE_AND_PLAY(gen_wallet_spending_coinstake_after_minting); + GENERATE_AND_PLAY(gen_wallet_fake_outs_while_having_too_little_own_outs); + // GENERATE_AND_PLAY(premine_wallet_test); // tests premine wallets; wallet files nedded; by demand only + GENERATE_AND_PLAY(mined_balance_wallet_test); + GENERATE_AND_PLAY(wallet_outputs_with_same_key_image); + GENERATE_AND_PLAY(wallet_unconfirmed_tx_expiration); + GENERATE_AND_PLAY(wallet_unconfimed_tx_balance); + GENERATE_AND_PLAY_HF(packing_outputs_on_pos_minting_wallet, "3"); + GENERATE_AND_PLAY_HF(wallet_watch_only_and_chain_switch, "3"); + GENERATE_AND_PLAY_HF(wallet_and_sweep_below, "3-*"); + + GENERATE_AND_PLAY(wallet_rpc_integrated_address); + GENERATE_AND_PLAY(wallet_rpc_integrated_address_transfer); + GENERATE_AND_PLAY(wallet_rpc_transfer); + GENERATE_AND_PLAY(wallet_rpc_alias_tests); + GENERATE_AND_PLAY_HF(wallet_rpc_exchange_suite, "3,4"); + GENERATE_AND_PLAY_HF(wallet_true_rpc_pos_mining, "4-*"); + GENERATE_AND_PLAY_HF(wallet_rpc_cold_signing, "3,5-*"); + // GENERATE_AND_PLAY_HF(wallet_rpc_multiple_receivers, "5-*"); work in progress -- sowle + GENERATE_AND_PLAY_HF(wallet_chain_switch_with_spending_the_same_ki, "3"); + GENERATE_AND_PLAY(wallet_sending_to_integrated_address); + GENERATE_AND_PLAY_HF(block_template_blacklist_test, "4-*"); + GENERATE_AND_PLAY_HF(wallet_rpc_hardfork_verification, "5"); + + // GENERATE_AND_PLAY(emission_test); // simulate 1 year of blockchain, too long run (1 y ~= 1 hr), by demand only // LOG_ERROR2("print_reward_change_first_blocks.log", currency::print_reward_change_first_blocks(525601).str()); // outputs first 1 year of blocks' rewards (simplier) // pos tests GENERATE_AND_PLAY_INTERMITTED_BY_BLOCKCHAIN_SAVELOAD(gen_pos_basic_tests); - // REGISTER_TEST(gen_pos_basic_tests); -- commented as this test is run intermittedly by previous line; uncomment if necessary (ex. for debugging) - REGISTER_TEST(gen_pos_coinstake_already_spent); - REGISTER_TEST(gen_pos_incorrect_timestamp); - REGISTER_TEST(gen_pos_too_early_pos_block); - REGISTER_TEST_HF(gen_pos_extra_nonce, "3-*"); - REGISTER_TEST(gen_pos_min_allowed_height); - REGISTER_TEST(gen_pos_invalid_coinbase); - // REGISTER_TEST(pos_wallet_minting_same_amount_diff_outs); // Long test! Takes ~10 hours to simulate 6000 blocks on 2015 middle-end computer - //REGISTER_TEST(pos_emission_test); // Long test! by demand only - REGISTER_TEST(pos_wallet_big_block_test); - //REGISTER_TEST(block_template_against_txs_size); // Long test! by demand only - REGISTER_TEST_HF(pos_altblocks_validation, "3-*"); - REGISTER_TEST_HF(pos_mining_with_decoys, "3"); - REGISTER_TEST_HF(pos_and_no_pow_blocks_between_output_and_stake, "4-*"); + // GENERATE_AND_PLAY(gen_pos_basic_tests); -- commented as this test is run intermittedly by previous line; uncomment if necessary (ex. for debugging) + GENERATE_AND_PLAY(gen_pos_coinstake_already_spent); + GENERATE_AND_PLAY(gen_pos_incorrect_timestamp); + GENERATE_AND_PLAY(gen_pos_too_early_pos_block); + GENERATE_AND_PLAY_HF(gen_pos_extra_nonce, "3-*"); + GENERATE_AND_PLAY(gen_pos_min_allowed_height); + GENERATE_AND_PLAY(gen_pos_invalid_coinbase); + // GENERATE_AND_PLAY(pos_wallet_minting_same_amount_diff_outs); // Long test! Takes ~10 hours to simulate 6000 blocks on 2015 middle-end computer + //GENERATE_AND_PLAY(pos_emission_test); // Long test! by demand only + GENERATE_AND_PLAY(pos_wallet_big_block_test); + //GENERATE_AND_PLAY(block_template_against_txs_size); // Long test! by demand only + GENERATE_AND_PLAY_HF(pos_altblocks_validation, "3-*"); + GENERATE_AND_PLAY_HF(pos_mining_with_decoys, "3"); + GENERATE_AND_PLAY_HF(pos_and_no_pow_blocks_between_output_and_stake, "4-*"); // alternative blocks and generic chain-switching tests - REGISTER_TEST(gen_chain_switch_pow_pos); - REGISTER_TEST(pow_pos_reorganize_specific_case); - REGISTER_TEST(gen_chain_switch_1); - REGISTER_TEST(bad_chain_switching_with_rollback); - REGISTER_TEST(chain_switching_and_tx_with_attachment_blobsize); - REGISTER_TEST_HF(chain_switching_when_gindex_spent_in_both_chains, "3-*"); - REGISTER_TEST(alt_chain_coins_pow_mined_then_spent); - REGISTER_TEST(gen_simple_chain_split_1); - REGISTER_TEST_HF(alt_blocks_validation_and_same_new_amount_in_two_txs, "3-*"); - REGISTER_TEST_HF(alt_blocks_with_the_same_txs, "3-*"); - REGISTER_TEST_HF(chain_switching_when_out_spent_in_alt_chain_mixin, "3-*"); - REGISTER_TEST_HF(chain_switching_when_out_spent_in_alt_chain_ref_id, "3-*"); - REGISTER_TEST_HF(alt_chain_and_block_tx_fee_median, "3-*"); + GENERATE_AND_PLAY(gen_chain_switch_pow_pos); + GENERATE_AND_PLAY(pow_pos_reorganize_specific_case); + GENERATE_AND_PLAY(gen_chain_switch_1); + GENERATE_AND_PLAY(bad_chain_switching_with_rollback); + GENERATE_AND_PLAY(chain_switching_and_tx_with_attachment_blobsize); + GENERATE_AND_PLAY_HF(chain_switching_when_gindex_spent_in_both_chains, "3-*"); + GENERATE_AND_PLAY(alt_chain_coins_pow_mined_then_spent); + GENERATE_AND_PLAY(gen_simple_chain_split_1); + GENERATE_AND_PLAY_HF(alt_blocks_validation_and_same_new_amount_in_two_txs, "3-*"); + GENERATE_AND_PLAY_HF(alt_blocks_with_the_same_txs, "3-*"); + GENERATE_AND_PLAY_HF(chain_switching_when_out_spent_in_alt_chain_mixin, "3-*"); + GENERATE_AND_PLAY_HF(chain_switching_when_out_spent_in_alt_chain_ref_id, "3-*"); + GENERATE_AND_PLAY_HF(alt_chain_and_block_tx_fee_median, "3-*"); // miscellaneous tests - REGISTER_TEST(test_blockchain_vs_spent_keyimges); - REGISTER_TEST(test_blockchain_vs_spent_multisig_outs); - REGISTER_TEST(block_template_vs_invalid_txs_from_pool); - REGISTER_TEST(cumulative_difficulty_adjustment_test); - REGISTER_TEST(cumulative_difficulty_adjustment_test_alt); - REGISTER_TEST(prun_ring_signatures); - REGISTER_TEST(gen_simple_chain_001); - REGISTER_TEST(one_block); - REGISTER_TEST(gen_ring_signature_1); - REGISTER_TEST(gen_ring_signature_2); - REGISTER_TEST(fill_tx_rpc_inputs); - //REGISTER_TEST(gen_ring_signature_big); // Takes up to XXX hours (if CURRENCY_MINED_MONEY_UNLOCK_WINDOW == 10) + GENERATE_AND_PLAY(test_blockchain_vs_spent_keyimges); + GENERATE_AND_PLAY(test_blockchain_vs_spent_multisig_outs); + GENERATE_AND_PLAY(block_template_vs_invalid_txs_from_pool); + GENERATE_AND_PLAY(cumulative_difficulty_adjustment_test); + GENERATE_AND_PLAY(cumulative_difficulty_adjustment_test_alt); + GENERATE_AND_PLAY(prun_ring_signatures); + GENERATE_AND_PLAY(gen_simple_chain_001); + GENERATE_AND_PLAY(one_block); + GENERATE_AND_PLAY(gen_ring_signature_1); + GENERATE_AND_PLAY(gen_ring_signature_2); + GENERATE_AND_PLAY(fill_tx_rpc_inputs); + //GENERATE_AND_PLAY(gen_ring_signature_big); // Takes up to XXX hours (if CURRENCY_MINED_MONEY_UNLOCK_WINDOW == 10) // tests for outputs mixing in - REGISTER_TEST(get_random_outs_test); - REGISTER_TEST(mix_attr_tests); - REGISTER_TEST(mix_in_spent_outs); - REGISTER_TEST(random_outs_and_burnt_coins); + GENERATE_AND_PLAY(get_random_outs_test); + GENERATE_AND_PLAY(mix_attr_tests); + GENERATE_AND_PLAY(mix_in_spent_outs); + GENERATE_AND_PLAY(random_outs_and_burnt_coins); // Block verification tests - REGISTER_TEST_HF(gen_block_big_major_version, "0,3"); - REGISTER_TEST_HF(gen_block_big_minor_version, "0,3"); - REGISTER_TEST_HF(gen_block_ts_not_checked, "0,3"); - REGISTER_TEST_HF(gen_block_ts_in_past, "0,3"); - REGISTER_TEST_HF(gen_block_ts_in_future, "0,3"); - //REGISTER_TEST(gen_block_invalid_prev_id); disabled because impossible to generate text chain with wrong prev_id - pow hash not works without chaining - REGISTER_TEST_HF(gen_block_invalid_nonce, "0,3"); - REGISTER_TEST_HF(gen_block_no_miner_tx, "0,3"); - REGISTER_TEST_HF(gen_block_unlock_time_is_low, "0,3"); - REGISTER_TEST_HF(gen_block_unlock_time_is_high, "0,3"); - REGISTER_TEST_HF(gen_block_unlock_time_is_timestamp_in_past, "0,3"); - REGISTER_TEST_HF(gen_block_unlock_time_is_timestamp_in_future, "0,3"); - REGISTER_TEST_HF(gen_block_height_is_low, "0,3"); - REGISTER_TEST_HF(gen_block_height_is_high, "0,3"); - REGISTER_TEST_HF(block_with_correct_prev_id_on_wrong_height, "3-*"); - REGISTER_TEST_HF(block_reward_in_main_chain_basic, "3-*"); - REGISTER_TEST_HF(block_reward_in_alt_chain_basic, "3-*"); - REGISTER_TEST_HF(gen_block_miner_tx_has_2_tx_gen_in, "0,3"); - REGISTER_TEST_HF(gen_block_miner_tx_has_2_in, "0,3"); - REGISTER_TEST_HF(gen_block_miner_tx_with_txin_to_key, "0,3"); - REGISTER_TEST_HF(gen_block_miner_tx_out_is_small, "0,3"); - REGISTER_TEST_HF(gen_block_miner_tx_out_is_big, "0,3"); - REGISTER_TEST_HF(gen_block_miner_tx_has_no_out, "0,3"); - REGISTER_TEST_HF(gen_block_miner_tx_has_out_to_initiator, "0,3"); - REGISTER_TEST_HF(gen_block_has_invalid_tx, "0,3"); - REGISTER_TEST_HF(gen_block_is_too_big, "0,3"); - REGISTER_TEST_HF(gen_block_wrong_version_agains_hardfork, "0,3"); - REGISTER_TEST_HF(block_choice_rule_bigger_fee, "4-*"); - //REGISTER_TEST(gen_block_invalid_binary_format); // Takes up to 3 hours, if CURRENCY_MINED_MONEY_UNLOCK_WINDOW == 500, up to 30 minutes, if CURRENCY_MINED_MONEY_UNLOCK_WINDOW == 10 + GENERATE_AND_PLAY_HF(gen_block_big_major_version, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_big_minor_version, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_ts_not_checked, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_ts_in_past, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_ts_in_future, "0,3"); + //GENERATE_AND_PLAY(gen_block_invalid_prev_id); disabled because impossible to generate text chain with wrong prev_id - pow hash not works without chaining + GENERATE_AND_PLAY_HF(gen_block_invalid_nonce, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_no_miner_tx, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_unlock_time_is_low, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_unlock_time_is_high, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_unlock_time_is_timestamp_in_past, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_unlock_time_is_timestamp_in_future, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_height_is_low, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_height_is_high, "0,3"); + GENERATE_AND_PLAY_HF(block_with_correct_prev_id_on_wrong_height, "3-*"); + GENERATE_AND_PLAY_HF(block_reward_in_main_chain_basic, "3-*"); + GENERATE_AND_PLAY_HF(block_reward_in_alt_chain_basic, "3-*"); + GENERATE_AND_PLAY_HF(gen_block_miner_tx_has_2_tx_gen_in, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_miner_tx_has_2_in, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_miner_tx_with_txin_to_key, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_miner_tx_out_is_small, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_miner_tx_out_is_big, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_miner_tx_has_no_out, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_miner_tx_has_out_to_initiator, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_has_invalid_tx, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_is_too_big, "0,3"); + GENERATE_AND_PLAY_HF(gen_block_wrong_version_agains_hardfork, "0,3"); + GENERATE_AND_PLAY_HF(block_choice_rule_bigger_fee, "4-*"); + //GENERATE_AND_PLAY(gen_block_invalid_binary_format); // Takes up to 3 hours, if CURRENCY_MINED_MONEY_UNLOCK_WINDOW == 500, up to 30 minutes, if CURRENCY_MINED_MONEY_UNLOCK_WINDOW == 10 // Transaction verification tests - REGISTER_TEST(gen_broken_attachments); - REGISTER_TEST(gen_tx_big_version); - REGISTER_TEST(gen_tx_unlock_time); - REGISTER_TEST(gen_tx_input_is_not_txin_to_key); - REGISTER_TEST(gen_tx_no_inputs_no_outputs); - REGISTER_TEST(gen_tx_no_inputs_has_outputs); - REGISTER_TEST(gen_tx_has_inputs_no_outputs); - REGISTER_TEST(gen_tx_invalid_input_amount); - REGISTER_TEST(gen_tx_input_wo_key_offsets); - REGISTER_TEST(gen_tx_sender_key_offest_not_exist); - REGISTER_TEST(gen_tx_key_offest_points_to_foreign_key); - REGISTER_TEST(gen_tx_mixed_key_offest_not_exist); - REGISTER_TEST(gen_tx_key_image_not_derive_from_tx_key); - REGISTER_TEST(gen_tx_key_image_is_invalid); - REGISTER_TEST(gen_tx_check_input_unlock_time); - REGISTER_TEST(gen_tx_txout_to_key_has_invalid_key); - REGISTER_TEST(gen_tx_output_with_zero_amount); - REGISTER_TEST(gen_tx_output_is_not_txout_to_key); - REGISTER_TEST(gen_tx_signatures_are_invalid); - REGISTER_TEST(gen_tx_extra_double_entry); - REGISTER_TEST(gen_tx_double_key_image); - REGISTER_TEST(tx_expiration_time); - REGISTER_TEST(tx_expiration_time_and_block_template); - REGISTER_TEST(tx_expiration_time_and_chain_switching); - REGISTER_TEST(tx_key_image_pool_conflict); - //REGISTER_TEST_HF(tx_version_against_hardfork, "4-*"); + GENERATE_AND_PLAY(gen_broken_attachments); + GENERATE_AND_PLAY(gen_tx_big_version); + GENERATE_AND_PLAY(gen_tx_unlock_time); + GENERATE_AND_PLAY(gen_tx_input_is_not_txin_to_key); + GENERATE_AND_PLAY(gen_tx_no_inputs_no_outputs); + GENERATE_AND_PLAY(gen_tx_no_inputs_has_outputs); + GENERATE_AND_PLAY(gen_tx_has_inputs_no_outputs); + GENERATE_AND_PLAY(gen_tx_invalid_input_amount); + GENERATE_AND_PLAY(gen_tx_input_wo_key_offsets); + GENERATE_AND_PLAY(gen_tx_sender_key_offest_not_exist); + GENERATE_AND_PLAY(gen_tx_key_offest_points_to_foreign_key); + GENERATE_AND_PLAY(gen_tx_mixed_key_offest_not_exist); + GENERATE_AND_PLAY(gen_tx_key_image_not_derive_from_tx_key); + GENERATE_AND_PLAY(gen_tx_key_image_is_invalid); + GENERATE_AND_PLAY(gen_tx_check_input_unlock_time); + GENERATE_AND_PLAY(gen_tx_txout_to_key_has_invalid_key); + GENERATE_AND_PLAY(gen_tx_output_with_zero_amount); + GENERATE_AND_PLAY(gen_tx_output_is_not_txout_to_key); + GENERATE_AND_PLAY(gen_tx_signatures_are_invalid); + GENERATE_AND_PLAY(gen_tx_extra_double_entry); + GENERATE_AND_PLAY(gen_tx_double_key_image); + GENERATE_AND_PLAY(tx_expiration_time); + GENERATE_AND_PLAY(tx_expiration_time_and_block_template); + GENERATE_AND_PLAY(tx_expiration_time_and_chain_switching); + GENERATE_AND_PLAY(tx_key_image_pool_conflict); + //GENERATE_AND_PLAY_HF(tx_version_against_hardfork, "4-*"); /* To execute the check of bare balance (function "check_tx_bare_balance") we need to run the test "tx_pool_semantic_validation" on the HF 3. By default behaviour bare outputs are disallowed on the heights >= 10. */ - REGISTER_TEST_HF(tx_pool_semantic_validation, "3"); - REGISTER_TEST(input_refers_to_incompatible_by_type_output); - REGISTER_TEST_HF(tx_pool_validation_and_chain_switch, "4-5"); - REGISTER_TEST_HF(tx_coinbase_separate_sig_flag, "4-*"); - REGISTER_TEST(tx_input_mixins); + GENERATE_AND_PLAY_HF(tx_pool_semantic_validation, "3"); + GENERATE_AND_PLAY(input_refers_to_incompatible_by_type_output); + GENERATE_AND_PLAY_HF(tx_pool_validation_and_chain_switch, "4-5"); + GENERATE_AND_PLAY_HF(tx_coinbase_separate_sig_flag, "4-*"); + GENERATE_AND_PLAY(tx_input_mixins); // Double spend - REGISTER_TEST(gen_double_spend_in_tx); - REGISTER_TEST(gen_double_spend_in_tx); - REGISTER_TEST(gen_double_spend_in_the_same_block); - REGISTER_TEST(gen_double_spend_in_the_same_block); - REGISTER_TEST(gen_double_spend_in_different_blocks); - REGISTER_TEST(gen_double_spend_in_different_blocks); - REGISTER_TEST(gen_double_spend_in_different_chains); - REGISTER_TEST(gen_double_spend_in_alt_chain_in_the_same_block); - REGISTER_TEST(gen_double_spend_in_alt_chain_in_the_same_block); - REGISTER_TEST(gen_double_spend_in_alt_chain_in_different_blocks); - REGISTER_TEST(gen_double_spend_in_alt_chain_in_different_blocks); - - REGISTER_TEST(gen_uint_overflow_1); - REGISTER_TEST(gen_uint_overflow_2); + GENERATE_AND_PLAY(gen_double_spend_in_tx); + GENERATE_AND_PLAY(gen_double_spend_in_tx); + GENERATE_AND_PLAY(gen_double_spend_in_the_same_block); + GENERATE_AND_PLAY(gen_double_spend_in_the_same_block); + GENERATE_AND_PLAY(gen_double_spend_in_different_blocks); + GENERATE_AND_PLAY(gen_double_spend_in_different_blocks); + GENERATE_AND_PLAY(gen_double_spend_in_different_chains); + GENERATE_AND_PLAY(gen_double_spend_in_alt_chain_in_the_same_block); + GENERATE_AND_PLAY(gen_double_spend_in_alt_chain_in_the_same_block); + GENERATE_AND_PLAY(gen_double_spend_in_alt_chain_in_different_blocks); + GENERATE_AND_PLAY(gen_double_spend_in_alt_chain_in_different_blocks); + + GENERATE_AND_PLAY(gen_uint_overflow_1); + GENERATE_AND_PLAY(gen_uint_overflow_2); // Hardfok 1 tests - REGISTER_TEST(before_hard_fork_1_cumulative_difficulty); - REGISTER_TEST(inthe_middle_hard_fork_1_cumulative_difficulty); - REGISTER_TEST(after_hard_fork_1_cumulative_difficulty); - REGISTER_TEST(hard_fork_1_locked_mining_test); - REGISTER_TEST(hard_fork_1_bad_pos_source); - REGISTER_TEST(hard_fork_1_unlock_time_2_in_normal_tx); - REGISTER_TEST(hard_fork_1_unlock_time_2_in_coinbase); - REGISTER_TEST(hard_fork_1_chain_switch_pow_only); - REGISTER_TEST(hard_fork_1_checkpoint_basic_test); - REGISTER_TEST(hard_fork_1_pos_locked_height_vs_time); - REGISTER_TEST(hard_fork_1_pos_and_locked_coins); + GENERATE_AND_PLAY(before_hard_fork_1_cumulative_difficulty); + GENERATE_AND_PLAY(inthe_middle_hard_fork_1_cumulative_difficulty); + GENERATE_AND_PLAY(after_hard_fork_1_cumulative_difficulty); + GENERATE_AND_PLAY(hard_fork_1_locked_mining_test); + GENERATE_AND_PLAY(hard_fork_1_bad_pos_source); + GENERATE_AND_PLAY(hard_fork_1_unlock_time_2_in_normal_tx); + GENERATE_AND_PLAY(hard_fork_1_unlock_time_2_in_coinbase); + GENERATE_AND_PLAY(hard_fork_1_chain_switch_pow_only); + GENERATE_AND_PLAY(hard_fork_1_checkpoint_basic_test); + GENERATE_AND_PLAY(hard_fork_1_pos_locked_height_vs_time); + GENERATE_AND_PLAY(hard_fork_1_pos_and_locked_coins); // Hardfork 2 tests - //REGISTER_TEST(hard_fork_2_tx_payer_in_wallet); - //REGISTER_TEST(hard_fork_2_tx_receiver_in_wallet); - REGISTER_TEST(hard_fork_2_tx_extra_alias_entry_in_wallet); - REGISTER_TEST_HF(hard_fork_2_auditable_addresses_basics, "2-*"); - REGISTER_TEST(hard_fork_2_no_new_structures_before_hf); - REGISTER_TEST(hard_fork_2_awo_wallets_basic_test); - REGISTER_TEST(hard_fork_2_awo_wallets_basic_test); - REGISTER_TEST(hard_fork_2_alias_update_using_old_tx); - REGISTER_TEST(hard_fork_2_alias_update_using_old_tx); - REGISTER_TEST(hard_fork_2_incorrect_alias_update); - REGISTER_TEST(hard_fork_2_incorrect_alias_update); + //GENERATE_AND_PLAY(hard_fork_2_tx_payer_in_wallet); + //GENERATE_AND_PLAY(hard_fork_2_tx_receiver_in_wallet); + GENERATE_AND_PLAY(hard_fork_2_tx_extra_alias_entry_in_wallet); + GENERATE_AND_PLAY_HF(hard_fork_2_auditable_addresses_basics, "2-*"); + GENERATE_AND_PLAY(hard_fork_2_no_new_structures_before_hf); + GENERATE_AND_PLAY(hard_fork_2_awo_wallets_basic_test); + GENERATE_AND_PLAY(hard_fork_2_awo_wallets_basic_test); + GENERATE_AND_PLAY(hard_fork_2_alias_update_using_old_tx); + GENERATE_AND_PLAY(hard_fork_2_alias_update_using_old_tx); + GENERATE_AND_PLAY(hard_fork_2_incorrect_alias_update); + GENERATE_AND_PLAY(hard_fork_2_incorrect_alias_update); // HF4 - REGISTER_TEST_HF(hard_fork_4_consolidated_txs, "3-*"); - REGISTER_TEST_HF(hardfork_4_wallet_transfer_with_mandatory_mixins, "3-*"); - REGISTER_TEST(hardfork_4_wallet_sweep_bare_outs); - REGISTER_TEST_HF(hardfork_4_pop_tx_from_global_index, "4-*"); + GENERATE_AND_PLAY_HF(hard_fork_4_consolidated_txs, "3-*"); + GENERATE_AND_PLAY_HF(hardfork_4_wallet_transfer_with_mandatory_mixins, "3-*"); + GENERATE_AND_PLAY(hardfork_4_wallet_sweep_bare_outs); + GENERATE_AND_PLAY_HF(hardfork_4_pop_tx_from_global_index, "4-*"); // HF5 - REGISTER_TEST_HF(hard_fork_5_tx_version, "5-*"); + GENERATE_AND_PLAY_HF(hard_fork_5_tx_version, "5-*"); // HF6 - REGISTER_TEST(hard_fork_6_intrinsic_payment_id_basic_test); - REGISTER_TEST(hard_fork_6_intrinsic_payment_id_rpc_test); + GENERATE_AND_PLAY(hard_fork_6_intrinsic_payment_id_basic_test); + GENERATE_AND_PLAY(hard_fork_6_intrinsic_payment_id_rpc_test); - REGISTER_TEST_HF(isolate_auditable_and_proof, "2-*"); + GENERATE_AND_PLAY_HF(isolate_auditable_and_proof, "2-*"); - REGISTER_TEST(zarcanum_basic_test); - - REGISTER_TEST_HF(multiassets_basic_test, "4-*"); - REGISTER_TEST_HF(ionic_swap_basic_test, "4-*"); - REGISTER_TEST_HF(ionic_swap_exact_amounts_test, "4-*"); - REGISTER_TEST(zarcanum_test_n_inputs_validation); - REGISTER_TEST(zarcanum_gen_time_balance); - REGISTER_TEST(zarcanum_txs_with_big_shuffled_decoy_set_shuffled); - REGISTER_TEST(zarcanum_pos_block_math); - REGISTER_TEST(zarcanum_in_alt_chain); - REGISTER_TEST_HF(zarcanum_in_alt_chain_2, "4-*"); - REGISTER_TEST(assets_and_explicit_native_coins_in_outs); - REGISTER_TEST(zarcanum_block_with_txs); - REGISTER_TEST(asset_depoyment_and_few_zc_utxos); - REGISTER_TEST_HF(assets_and_pos_mining, "4-*"); - REGISTER_TEST_HF(asset_emission_and_unconfirmed_balance, "4-*"); - REGISTER_TEST_HF(asset_operation_in_consolidated_tx, "4-*"); - REGISTER_TEST_HF(asset_operation_and_hardfork_checks, "4-*"); - REGISTER_TEST_HF(eth_signed_asset_basics, "5-*"); // TODO: make HF4 version - REGISTER_TEST_HF(eth_signed_asset_via_rpc, "5-*"); // TODO: make HF4 version - //REGISTER_TEST_HF(asset_current_and_total_supplies_comparative_constraints, "4-*"); <-- temporary disabled, waiting for Stepan's fix -- sowle - REGISTER_TEST_HF(several_asset_emit_burn_txs_in_pool, "5-*"); - REGISTER_TEST_HF(assets_transfer_with_smallest_amount, "4-*"); - REGISTER_TEST_HF(asset_operations_and_chain_switching, "4-*"); - - REGISTER_TEST_HF(pos_fuse_test, "4-*"); - REGISTER_TEST_HF(wallet_reorganize_and_trim_test, "4-*"); - REGISTER_TEST_HF(wallet_rpc_thirdparty_custody, "5-*"); - - REGISTER_TEST_HF(attachment_isolation_test, "4-*"); - - // REGISTER_TEST(gen_block_reward); + GENERATE_AND_PLAY(zarcanum_basic_test); + + GENERATE_AND_PLAY_HF(multiassets_basic_test, "4-*"); + GENERATE_AND_PLAY_HF(ionic_swap_basic_test, "4-*"); + GENERATE_AND_PLAY_HF(ionic_swap_exact_amounts_test, "4-*"); + GENERATE_AND_PLAY(zarcanum_test_n_inputs_validation); + GENERATE_AND_PLAY(zarcanum_gen_time_balance); + GENERATE_AND_PLAY(zarcanum_txs_with_big_shuffled_decoy_set_shuffled); + GENERATE_AND_PLAY(zarcanum_pos_block_math); + GENERATE_AND_PLAY(zarcanum_in_alt_chain); + GENERATE_AND_PLAY_HF(zarcanum_in_alt_chain_2, "4-*"); + GENERATE_AND_PLAY(assets_and_explicit_native_coins_in_outs); + GENERATE_AND_PLAY(zarcanum_block_with_txs); + GENERATE_AND_PLAY(asset_depoyment_and_few_zc_utxos); + GENERATE_AND_PLAY_HF(assets_and_pos_mining, "4-*"); + GENERATE_AND_PLAY_HF(asset_emission_and_unconfirmed_balance, "4-*"); + GENERATE_AND_PLAY_HF(asset_operation_in_consolidated_tx, "4-*"); + GENERATE_AND_PLAY_HF(asset_operation_and_hardfork_checks, "4-*"); + GENERATE_AND_PLAY_HF(eth_signed_asset_basics, "5-*"); // TODO: make HF4 version + GENERATE_AND_PLAY_HF(eth_signed_asset_via_rpc, "5-*"); // TODO: make HF4 version + //GENERATE_AND_PLAY_HF(asset_current_and_total_supplies_comparative_constraints, "4-*"); <-- temporary disabled, waiting for Stepan's fix -- sowle + GENERATE_AND_PLAY_HF(several_asset_emit_burn_txs_in_pool, "5-*"); + GENERATE_AND_PLAY_HF(assets_transfer_with_smallest_amount, "4-*"); + GENERATE_AND_PLAY_HF(asset_operations_and_chain_switching, "4-*"); + + GENERATE_AND_PLAY_HF(pos_fuse_test, "4-*"); + GENERATE_AND_PLAY_HF(wallet_reorganize_and_trim_test, "4-*"); + GENERATE_AND_PLAY_HF(wallet_rpc_thirdparty_custody, "5-*"); + + GENERATE_AND_PLAY_HF(attachment_isolation_test, "4-*"); + + // GENERATE_AND_PLAY(gen_block_reward); // END OF TESTS */ } From 6e93a01238735dcbc443fced44cab028ad6c3abf Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Mon, 12 Jan 2026 23:01:38 +0300 Subject: [PATCH 05/28] separated the logic of maine and parallelism --- tests/core_tests/chaingen_main.cpp | 790 +----------------- .../parallel/parallel_test_runner.cpp | 765 +++++++++++++++++ .../parallel/parallel_test_runner.h | 57 ++ 3 files changed, 848 insertions(+), 764 deletions(-) create mode 100644 tests/core_tests/parallel/parallel_test_runner.cpp create mode 100644 tests/core_tests/parallel/parallel_test_runner.h diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index ae9cfafdd..73a341293 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -14,6 +14,7 @@ #include "random_helper.h" #include "core_state_helper.h" #include "common/db_backend_selector.h" +#include "parallel/parallel_test_runner.h" #include #include #include @@ -29,50 +30,25 @@ namespace po = boost::program_options; namespace bp = boost::process; namespace pt = boost::property_tree; +extern const char* TAKEN_TESTS_LOG_FILENAME; + +command_line::arg_descriptor arg_test_data_path ("test-data-path", "", ""); +command_line::arg_descriptor arg_generate_test_data ("generate-test-data", ""); +command_line::arg_descriptor arg_play_test_data ("play-test-data", ""); +command_line::arg_descriptor arg_generate_and_play_test_data ("generate-and-play-test-data", ""); +command_line::arg_descriptor arg_test_transactions ("test-transactions", ""); +command_line::arg_descriptor arg_run_single_test ("run-single-test", " TEST_NAME -- name of a single test to run, HF -- specific hardfork id to run the test for" ); +command_line::arg_descriptor arg_run_multiple_tests ("run-multiple-tests", "comma-separated list of tests to run, OR text file <@filename> containing list of tests"); +command_line::arg_descriptor arg_enable_debug_asserts ("enable-debug-asserts", "" ); +command_line::arg_descriptor arg_stop_on_fail ("stop-on-fail", ""); +command_line::arg_descriptor arg_processes ("processes", "Number of worker processes", 1); +command_line::arg_descriptor arg_worker_id ("worker-id", "Internal: worker index", -1); +command_line::arg_descriptor arg_run_root ("run-root", "Internal: run root dir", ""); + namespace { - const command_line::arg_descriptor arg_test_data_path ("test-data-path", "", ""); - const command_line::arg_descriptor arg_generate_test_data ("generate-test-data", ""); - const command_line::arg_descriptor arg_play_test_data ("play-test-data", ""); - const command_line::arg_descriptor arg_generate_and_play_test_data ("generate-and-play-test-data", ""); - const command_line::arg_descriptor arg_test_transactions ("test-transactions", ""); - const command_line::arg_descriptor arg_run_single_test ("run-single-test", " TEST_NAME -- name of a single test to run, HF -- specific hardfork id to run the test for" ); - const command_line::arg_descriptor arg_run_multiple_tests ("run-multiple-tests", "comma-separated list of tests to run, OR text file <@filename> containing list of tests"); - const command_line::arg_descriptor arg_enable_debug_asserts ("enable-debug-asserts", "" ); - const command_line::arg_descriptor arg_stop_on_fail ("stop-on-fail", ""); - const command_line::arg_descriptor arg_processes ("processes", "Number of worker processes", 1); - const command_line::arg_descriptor arg_worker_id ("worker-id", "Internal: worker index", -1); - const command_line::arg_descriptor arg_run_root ("run-root", "Internal: run root dir", ""); - - const char* JSON_WORKER_ID = "worker_id"; - const char* JSON_PROCESSES = "processes"; - const char* JSON_TESTS_COUNT = "tests_count"; - const char* JSON_UNIQUE_TESTS_COUNT = "unique_tests_count"; - const char* JSON_TOTAL_TIME_MS = "total_time_ms"; - const char* JSON_SKIP_ALL_TILL_END = "skip_all_till_the_end"; - const char* JSON_EXIT_CODE = "exit_code"; - - const char* JSON_FAILED_TESTS = "failed_tests"; - const char* JSON_TESTS_RUNNING_TIME = "tests_running_time"; - - const char* JSON_NAME = "name"; - const char* JSON_MS = "ms"; - - const char* WORKER_REPORT_FILENAME = "coretests_report.json"; - - const char* ARG_WORKER_ID = "--worker-id"; - const char* ARG_WORKER_ID_EQ = "--worker-id="; - - const char* ARG_DATA_DIR = "--data-dir"; - const char* ARG_DATA_DIR_EQ = "--data-dir="; - const char* WORKER_DIR_PREFIX = "w"; - const char* TAKEN_TESTS_LOG_FILENAME = "taken_tests.log"; - - // Console output locking to avoid broken lines - static std::mutex cout_mx; - static std::mutex cerr_mx; - boost::program_options::variables_map g_vm; + std::unique_ptr g_runner; } #define GENERATE(filename, genclass) \ @@ -756,22 +732,6 @@ struct test_job static std::vector g_test_jobs; -struct worker_report -{ - uint32_t worker_id = 0; - uint32_t processes = 1; - - size_t tests_count = 0; - size_t unique_tests_count = 0; - - uint64_t total_time_ms = 0; - - std::vector> tests_running_time; - std::set failed_tests; - bool skip_all_till_the_end = false; - - int exit_code = 0; -}; //-------------------------------------------------------------------------- template inline bool replay_events_through_core(currency::core& cr, const std::vector& events, t_test_class& validator, test_core_listener* core_listener, size_t event_index_from = 0, size_t event_index_to = SIZE_MAX) @@ -950,33 +910,9 @@ bool parse_cmd_specific_tests_to_run(std::unordered_multimap= prefix.size() && arg.compare(0, prefix.size(), prefix) == 0; -} - -static std::filesystem::path get_run_root_path() -{ - std::string run_root = command_line::get_arg(g_vm, arg_run_root); - if (run_root.empty()) - run_root = "chaingen_runs"; - - std::filesystem::path run_root_path(run_root); - - if (run_root_path.is_relative()) - run_root_path = std::filesystem::absolute(run_root_path); - - return run_root_path; -} - -static std::filesystem::path get_worker_dir_path(uint32_t worker_id) -{ - return get_run_root_path() / (WORKER_DIR_PREFIX+ std::to_string(worker_id)); -} - static std::filesystem::path get_worker_taken_tests_log_path(uint32_t worker_id) { - return get_worker_dir_path(worker_id) / TAKEN_TESTS_LOG_FILENAME; + return g_runner->get_worker_dir_path(worker_id) / TAKEN_TESTS_LOG_FILENAME; } static std::filesystem::path get_single_process_taken_tests_log_path() @@ -997,7 +933,7 @@ static void log_test_taken_by_this_process(const std::string& test_name) if (processes > 1 && worker_id >= 0) { const uint32_t wid = static_cast(worker_id); - std::filesystem::create_directories(get_worker_dir_path(wid)); + std::filesystem::create_directories(g_runner->get_worker_dir_path(wid)); p = get_worker_taken_tests_log_path(wid); } else @@ -1459,7 +1395,7 @@ static void register_all_tests( // END OF TESTS */ } -static void fill_postponed_tests_set(std::set& postponed_tests) +void fill_postponed_tests_set(std::set& postponed_tests) { postponed_tests.clear(); @@ -1480,677 +1416,6 @@ static void fill_postponed_tests_set(std::set& postponed_tests) #undef MARK_TEST_AS_POSTPONED } //-------------------------------------------------------------------------- -static std::vector build_base_args_without_worker_specific(int argc, char* argv[]) -{ - std::vector args; - args.reserve(static_cast(argc)); - - for (int i = 1; i < argc; ++i) - { - std::string current_arg = argv[i]; - - // Strip worker-specific args to avoid duplicates - if (current_arg == ARG_WORKER_ID || arg_has_prefix(current_arg, ARG_WORKER_ID_EQ)) - { - if (current_arg == ARG_WORKER_ID && i + 1 < argc) ++i; - continue; - } - - if (current_arg == ARG_DATA_DIR || arg_has_prefix(current_arg, ARG_DATA_DIR_EQ)) - { - if (current_arg == ARG_DATA_DIR && i + 1 < argc) ++i; - continue; - } - - args.emplace_back(std::move(current_arg)); - } - - return args; -} -//-------------------------------------------------------------------------- -static std::string make_worker_data_dir(const std::filesystem::path& run_root_abs, int worker_id) -{ - std::filesystem::path worker_dir = run_root_abs / (std::string(WORKER_DIR_PREFIX) + std::to_string(worker_id)); - std::filesystem::create_directories(worker_dir); - return worker_dir.string(); -} - -static std::filesystem::path get_worker_report_path(uint32_t worker_id) -{ - return get_worker_dir_path(worker_id) / WORKER_REPORT_FILENAME; -} -//-------------------------------------------------------------------------- -static bool write_worker_report_file(const worker_report& rep) -{ - try - { - std::filesystem::create_directories(get_worker_dir_path(rep.worker_id)); - - pt::ptree root; - root.put("worker_id", rep.worker_id); - root.put("processes", rep.processes); - root.put("tests_count", static_cast(rep.tests_count)); - root.put("unique_tests_count", static_cast(rep.unique_tests_count)); - root.put("total_time_ms", rep.total_time_ms); - root.put("skip_all_till_the_end", rep.skip_all_till_the_end); - root.put("exit_code", rep.exit_code); - - pt::ptree failed_arr; - for (const auto& s : rep.failed_tests) - { - pt::ptree v; - v.put("", s); - failed_arr.push_back(std::make_pair("", v)); - } - root.add_child("failed_tests", failed_arr); - - pt::ptree times_arr; - for (const auto& it : rep.tests_running_time) - { - pt::ptree node; - node.put("name", it.first); - node.put("ms", it.second); - times_arr.push_back(std::make_pair("", node)); - } - root.add_child("tests_running_time", times_arr); - - std::ofstream out(get_worker_report_path(rep.worker_id)); - if (!out.is_open()) - return false; - - pt::write_json(out, root, /*pretty*/true); - return true; - } - catch (...) - { - return false; - } -} - -static bool read_worker_report_file(uint32_t worker_id, worker_report& rep) -{ - try - { - const auto path = get_worker_report_path(worker_id); - if (!std::filesystem::exists(path)) - return false; - - pt::ptree root; - pt::read_json(path.string(), root); - - rep.worker_id = root.get(JSON_WORKER_ID, worker_id); - rep.processes = root.get(JSON_PROCESSES, 1); - - rep.tests_count = static_cast(root.get(JSON_TESTS_COUNT, 0)); - rep.unique_tests_count = static_cast(root.get(JSON_UNIQUE_TESTS_COUNT, 0)); - rep.total_time_ms = root.get(JSON_TOTAL_TIME_MS, 0); - - rep.skip_all_till_the_end = root.get(JSON_SKIP_ALL_TILL_END, false); - rep.exit_code = root.get(JSON_EXIT_CODE, 1); - - for (const auto& v : root.get_child(JSON_FAILED_TESTS, pt::ptree())) - { - const std::string name = v.second.get_value(); - if (!name.empty()) - rep.failed_tests.insert(name); - } - - for (const auto& v : root.get_child(JSON_TESTS_RUNNING_TIME, pt::ptree())) - { - const auto& node = v.second; - const std::string name = node.get(JSON_NAME, ""); - const uint64_t ms = node.get(JSON_MS, 0); - if (!name.empty()) - rep.tests_running_time.emplace_back(name, ms); - } - - return true; - } - catch (...) - { - return false; - } -} - -static int print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wall_time_ms) -{ - std::vector reps; - reps.reserve(processes); - - bool any_missing = false; - int failed_workers = 0; - - for (uint32_t i = 0; i < processes; ++i) - { - worker_report r; - if (!read_worker_report_file(i, r)) - { - any_missing = true; - continue; - } - reps.push_back(std::move(r)); - } - - for (const auto& r : reps) - if (r.exit_code != 0) - ++failed_workers; - - std::set postponed_tests; - fill_postponed_tests_set(postponed_tests); - - size_t total_tests_count = 0; - size_t total_unique_tests_count = 0; - - std::set failed_tests_union; - - for (const auto& r : reps) - { - total_tests_count += r.tests_count; - total_unique_tests_count += r.unique_tests_count; - - failed_tests_union.insert(r.failed_tests.begin(), r.failed_tests.end()); - } - - size_t failed_postponed_tests_count = 0; - for (const auto& t : failed_tests_union) - if (postponed_tests.count(t) != 0) - ++failed_postponed_tests_count; - - const size_t serious_failures_count = failed_tests_union.size() - failed_postponed_tests_count; - - std::cout << (serious_failures_count == 0 && failed_workers == 0 && !any_missing ? concolor::green : concolor::magenta); - - std::cout << "\nREPORT (aggregated):\n"; - std::cout << " Workers: " << processes << "\n"; - if (any_missing) - std::cout << " Warning: some worker reports are missing\n"; - std::cout << " Worker failures: " << failed_workers << "\n"; - - std::cout << " Unique tests run: " << total_unique_tests_count << "\n"; - std::cout << " Total tests run: " << total_tests_count << "\n"; - std::cout << " Failures: " << serious_failures_count - << " (postponed failures: " << failed_postponed_tests_count << ")\n"; - std::cout << " Postponed: " << postponed_tests.size() << "\n"; - std::cout << " Total time: " << (wall_time_ms / 1000) << " s. (" - << (total_tests_count > 0 ? (wall_time_ms / total_tests_count) : 0) - << " ms per test in average)\n"; - if (!failed_tests_union.empty()) - { - std::cout << "FAILED/POSTPONED TESTS:\n"; - for (const auto& name : failed_tests_union) - { - const bool postponed = postponed_tests.count(name) != 0; - std::cout << " " << (postponed ? "POSTPONED: " : "FAILED: ") << name << "\n"; - } - } - - std::cout << concolor::normal << std::endl; - - // If any report missing or worker failed, treat as failure - if (any_missing || failed_workers != 0) - return 1; - - return serious_failures_count == 0 ? 0 : 1; -} -//-------------------------------------------------------------------------- -static std::string sanitize_filename(std::string s) -{ - for (char& c : s) - { - const bool ok = - (c >= 'a' && c <= 'z') || - (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9') || - c == '.' || c == '_' || c == '-'; - if (!ok) - c = '_'; - } - - // Avoid empty name - if (s.empty()) - s = "unnamed_test"; - - // Keep filenames reasonably short - constexpr size_t kMax = 180; - if (s.size() > kMax) - s.resize(kMax); - - return s; -} - -static std::filesystem::path make_unique_log_path(const std::filesystem::path& dir, const std::string& base_name_no_ext) -{ - std::filesystem::path p = dir / (base_name_no_ext + ".log"); - if (!std::filesystem::exists(p)) - return p; - - for (size_t i = 2; i < 10000; ++i) - { - std::filesystem::path alt = dir / (base_name_no_ext + "_" + std::to_string(i) + ".log"); - if (!std::filesystem::exists(alt)) - return alt; - } - - return p; // fallback -} - -static bool parse_test_name_from_header(const std::string& line, std::string& out_name) -{ - constexpr const char* kPrefix = "#TEST# >>>>"; - auto pos = line.find(kPrefix); - if (pos != 0) - return false; - - std::string rest = line.substr(std::strlen(kPrefix)); - // trim left - while (!rest.empty() && std::isspace(static_cast(rest.front()))) - rest.erase(rest.begin()); - - auto end = rest.find("<<<<"); - if (end == std::string::npos) - return false; - - std::string name = rest.substr(0, end); - // trim right - while (!name.empty() && std::isspace(static_cast(name.back()))) - name.pop_back(); - - if (name.empty()) - return false; - - out_name = name; - return true; -} - -static std::vector find_logs_for_test( - const std::filesystem::path& worker_dir, - const std::string& test_name) -{ - std::vector result; - - const std::string base = sanitize_filename(test_name); - const std::string base_prefix = base + "_"; - - try - { - for (auto& de : std::filesystem::directory_iterator(worker_dir)) - { - if (!de.is_regular_file()) - continue; - - const auto p = de.path(); - if (p.extension() != ".log") - continue; - - const std::string stem = p.stem().string(); - if (stem == base || stem.rfind(base_prefix, 0) == 0) - result.push_back(p); - } - } - catch (...) {} - - std::sort(result.begin(), result.end(), - [](const std::filesystem::path& a, const std::filesystem::path& b) - { - std::error_code ec1, ec2; - const auto ta = std::filesystem::last_write_time(a, ec1); - const auto tb = std::filesystem::last_write_time(b, ec2); - if (!ec1 && !ec2 && ta != tb) - return ta > tb; - return a.string() < b.string(); - }); - - return result; -} - -static void print_worker_failure_reasons(uint32_t processes, const std::vector& worker_exit_codes) -{ - bool any = false; - - for (uint32_t i = 0; i < processes; ++i) - { - const int ec = (i < worker_exit_codes.size() ? worker_exit_codes[i] : -999); - - worker_report rep; - const bool report_ok = read_worker_report_file(i, rep); - - // Skip successful workers that produced a report with exit_code 0. - if (report_ok && ec == 0 && rep.exit_code == 0) - continue; - - if (!any) - { - std::cout << "\nFAILURE REASONS (workers):\n"; - any = true; - } - - const auto worker_dir = get_worker_dir_path(i); - const auto report_path = get_worker_report_path(i); - - std::cout << " Worker " << i << ":\n"; - std::cout << " exit_code: " << ec << "\n"; - std::cout << " report: " << (std::filesystem::exists(report_path) ? report_path.string() : ("missing (" + report_path.string() + ")")) << "\n"; - std::cout << " logs dir: " << worker_dir.string() << "\n"; - - if (!report_ok) - { - std::cout << " failed tests: (cannot read report)\n"; - continue; - } - - if (rep.failed_tests.empty()) - { - std::cout << " failed tests: (none in report)\n"; - continue; - } - - std::cout << " failed tests:\n"; - for (const auto& test_name : rep.failed_tests) - { - std::cout << " - " << test_name << "\n"; - - const auto logs = find_logs_for_test(worker_dir, test_name); - if (logs.empty()) - { - std::cout << " log: (not found, expected prefix: " << sanitize_filename(test_name) << ")\n"; - continue; - } - - std::cout << " log: " << logs.front().string() << "\n"; - } - } -} -//-------------------------------------------------------------------------- -static int run_workers_and_wait(int argc, char* argv[]) -{ - const uint32_t processes = command_line::get_arg(g_vm, arg_processes); - const int32_t worker_id = command_line::get_arg(g_vm, arg_worker_id); - - // Only parent (multi-process mode, worker_id not set) spawns workers. - if (processes <= 1 || worker_id >= 0) - return -1; - - // Do not spawn workers for these modes. - if (command_line::get_arg(g_vm, command_line::arg_help)) return -1; - if (command_line::get_arg(g_vm, arg_generate_test_data)) return -1; - if (command_line::get_arg(g_vm, arg_play_test_data)) return -1; - if (command_line::get_arg(g_vm, arg_generate_and_play_test_data)) return -1; - if (command_line::get_arg(g_vm, arg_test_transactions)) return -1; - - const std::string run_root = command_line::get_arg(g_vm, arg_run_root); - std::vector base_args = build_base_args_without_worker_specific(argc, argv); - - auto has_flag = [&](const char* flag, const std::string& prefix) -> bool - { - for (const auto& a : base_args) - if (a == flag || arg_has_prefix(a, prefix)) - return true; - return false; - }; - - if (!has_flag("--processes", "--processes=")) - { - base_args.emplace_back("--processes"); - base_args.emplace_back(std::to_string(processes)); - } - - std::filesystem::path run_root_abs = run_root.empty() - ? std::filesystem::path("chaingen_runs") - : std::filesystem::path(run_root); - - if (run_root_abs.is_relative()) - run_root_abs = std::filesystem::absolute(run_root_abs); - - if (!has_flag("--run-root", "--run-root=")) - { - base_args.emplace_back("--run-root"); - base_args.emplace_back(run_root_abs.string()); - } - - // Resolve executable path and make it absolute. - std::string exe = (argv && argv[0]) ? std::string(argv[0]) : std::string(); - if (exe.empty()) - { - std::cout << concolor::magenta - << "Cannot spawn workers: empty executable path (argv[0])" - << concolor::normal << std::endl; - return 1; - } - - try - { - std::filesystem::path exe_path(exe); - - // If no directory component is present, try PATH lookup first. - if (exe.find('/') == std::string::npos && exe.find('\\') == std::string::npos) - { - boost::filesystem::path resolved = bp::search_path(exe); - if (!resolved.empty()) - exe_path = std::filesystem::path(resolved.string()); - } - - // Make absolute because workers will run with start_dir = worker directory. - if (exe_path.is_relative()) - exe_path = std::filesystem::absolute(exe_path); - - exe = exe_path.string(); - } - catch (...) {} - - if (exe.empty() || !std::filesystem::exists(std::filesystem::path(exe))) - { - std::cout << concolor::magenta - << "Cannot spawn workers: failed to resolve executable: " << exe - << concolor::normal << std::endl; - return 1; - } - - std::vector kids; - kids.reserve(processes); - - std::vector worker_exit_codes(processes, -1); - - // Streams for tee - std::vector> kids_out(processes); - std::vector> kids_err(processes); - - // Tee threads - std::vector tee_threads; - tee_threads.reserve(static_cast(processes) * 2); - - const auto wall_t0 = std::chrono::steady_clock::now(); - - for (uint32_t i = 0; i < processes; ++i) - { - std::vector args = base_args; - - args.emplace_back("--worker-id"); - args.emplace_back(std::to_string(i)); - - const std::string worker_data_dir = make_worker_data_dir(run_root_abs, static_cast(i)); - args.emplace_back("--data-dir"); - args.emplace_back(worker_data_dir); - - try - { - const auto worker_dir = get_worker_dir_path(i); - std::filesystem::create_directories(worker_dir); - - kids_out[i] = std::make_unique(); - kids_err[i] = std::make_unique(); - - kids.emplace_back(bp::child( - exe, - bp::args(args), - bp::start_dir = worker_dir.string(), - bp::std_out > *kids_out[i], - bp::std_err > *kids_err[i] - )); - - // Tee stdout: worker -> console; capture per-test log ONLY if failed - tee_threads.emplace_back([worker_dir, s = kids_out[i].get()]() - { - std::filesystem::create_directories(worker_dir); - - bool capturing = false; - std::string current_test_header; - std::filesystem::path current_log_path; - std::ofstream f_log; - - auto close_log = [&]() - { - if (f_log.is_open()) - f_log.close(); - }; - - auto start_capture = [&](const std::string& header_line) - { - close_log(); - capturing = true; - current_test_header = header_line; - - std::string test_name; - if (!parse_test_name_from_header(header_line, test_name)) - test_name = "unknown_test"; - - const std::string safe = sanitize_filename(test_name); - current_log_path = make_unique_log_path(worker_dir, safe); - - f_log.open(current_log_path, std::ios::out | std::ios::binary | std::ios::trunc); - if (f_log.is_open()) - { - f_log << "===== TEST OUTPUT BEGIN: " << header_line << " =====\n"; - f_log.flush(); - } - }; - - auto discard_capture = [&]() - { - close_log(); - capturing = false; - current_test_header.clear(); - - if (!current_log_path.empty()) - { - std::error_code ec; - std::filesystem::remove(current_log_path, ec); - } - current_log_path.clear(); - }; - - auto commit_capture = [&]() - { - if (f_log.is_open()) - { - f_log << "===== TEST OUTPUT END (FAILED): " << current_test_header << " =====\n"; - f_log.flush(); - } - close_log(); - - capturing = false; - current_test_header.clear(); - current_log_path.clear(); - }; - - std::string line; - while (std::getline(*s, line)) - { - // Start marker - if (line.rfind("#TEST# >>>>", 0) == 0) - start_capture(line); - - // If capturing, write everything into per-test log - if (capturing && f_log.is_open()) - { - f_log << line << "\n"; - f_log.flush(); - } - - // End markers - if (capturing && line.find("<<<< Succeeded") != std::string::npos) - { - discard_capture(); - } - else if (capturing && line.find("<<<< FAILED") != std::string::npos) - { - commit_capture(); - } - - // Console output - { - std::lock_guard lk(cout_mx); - std::cout << line << std::endl; - } - } - - // Worker died mid-test without FAILED marker: keep log as failed - if (capturing) - commit_capture(); - }); - - // Tee stderr: worker -> console(stderr) ONLY (no worker_stderr.log) - tee_threads.emplace_back([s = kids_err[i].get()]() - { - std::string line; - while (std::getline(*s, line)) - { - std::lock_guard lk(cerr_mx); - std::cerr << line << std::endl; - } - }); - } - catch (const std::exception& e) - { - std::cout << concolor::magenta - << "Failed to spawn worker " << i << ": " << e.what() - << concolor::normal << std::endl; - - for (auto& c : kids) - if (c.running()) - c.terminate(); - for (auto& c : kids) - c.wait(); - - for (auto& t : tee_threads) - if (t.joinable()) - t.join(); - - return 1; - } - } - - int failed_workers = 0; - for (uint32_t i = 0; i < kids.size(); ++i) - { - kids[i].wait(); - worker_exit_codes[i] = kids[i].exit_code(); - if (worker_exit_codes[i] != 0) - ++failed_workers; - } - - // Wait tee threads after children exit (streams will close). - for (auto& t : tee_threads) - if (t.joinable()) - t.join(); - - const auto wall_t1 = std::chrono::steady_clock::now(); - const uint64_t wall_ms = - static_cast(std::chrono::duration_cast(wall_t1 - wall_t0).count()); - - // Aggregated report first. - const int aggregated_rc = print_aggregated_report_and_return_rc(processes, wall_ms); - - // Then print diagnostics. - if (failed_workers != 0) - print_worker_failure_reasons(processes, worker_exit_codes); - - // Any worker non-zero => fail. - if (failed_workers != 0) - return 1; - - return aggregated_rc; -} -//-------------------------------------------------------------------------- int main(int argc, char* argv[]) { TRY_ENTRY(); @@ -2204,13 +1469,10 @@ int main(int argc, char* argv[]) if (!r) return 1; - // Parent spawns workers and must print aggregated report inside run_workers_and_wait(). - // Workers will return -1 here and continue into normal execution path. - { - int parent_rc = run_workers_and_wait(argc, argv); - if (parent_rc != -1) - return parent_rc; - } + g_runner = std::make_unique(g_vm); + const int parent_rc = g_runner->run_parent_if_needed(argc, argv); + if (parent_rc != parallel_test_runner::kNotParent) + return parent_rc; if (command_line::get_arg(g_vm, command_line::arg_help)) { @@ -2394,7 +1656,7 @@ int main(int argc, char* argv[]) // Workers write per-worker report for parent aggregation. if (is_worker) { - worker_report rep; + parallel_test_runner::worker_report rep; rep.worker_id = static_cast(worker_id); rep.processes = processes; rep.tests_count = tests_count; @@ -2405,7 +1667,7 @@ int main(int argc, char* argv[]) rep.skip_all_till_the_end = skip_all_till_the_end; rep.exit_code = (serious_failures_count == 0 ? 0 : 1); - (void)write_worker_report_file(rep); + (void)g_runner->write_worker_report(rep); } // Single-process run: keep local report (parent aggregation is only for multi-process mode). diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp new file mode 100644 index 000000000..eb043e978 --- /dev/null +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -0,0 +1,765 @@ +#include +#include +#include +#include +#include +#include + +#include "parallel_test_runner.h" +#include "../../src/common/command_line.h" +#include "../chaingen.h" + +namespace bp = boost::process; +namespace pt = boost::property_tree; + +extern command_line::arg_descriptor arg_run_root; +extern command_line::arg_descriptor arg_processes; +extern command_line::arg_descriptor arg_worker_id; +extern command_line::arg_descriptor arg_generate_test_data; +extern command_line::arg_descriptor arg_play_test_data; +extern command_line::arg_descriptor arg_generate_and_play_test_data; +extern command_line::arg_descriptor arg_test_transactions; + +std::string TAKEN_TESTS_LOG_FILENAME = "taken_tests.log"; + +namespace +{ + const char* JSON_WORKER_ID = "worker_id"; + const char* JSON_PROCESSES = "processes"; + const char* JSON_TESTS_COUNT = "tests_count"; + const char* JSON_UNIQUE_TESTS_COUNT = "unique_tests_count"; + const char* JSON_TOTAL_TIME_MS = "total_time_ms"; + const char* JSON_SKIP_ALL_TILL_END = "skip_all_till_the_end"; + const char* JSON_EXIT_CODE = "exit_code"; + + const char* JSON_FAILED_TESTS = "failed_tests"; + const char* JSON_TESTS_RUNNING_TIME = "tests_running_time"; + + const char* JSON_NAME = "name"; + const char* JSON_MS = "ms"; + + const char* WORKER_REPORT_FILENAME = "coretests_report.json"; + + const char* ARG_WORKER_ID = "--worker-id"; + const char* ARG_WORKER_ID_EQ = "--worker-id="; + + const char* ARG_DATA_DIR = "--data-dir"; + const char* ARG_DATA_DIR_EQ = "--data-dir="; + const char* WORKER_DIR_PREFIX = "w"; + + std::mutex cout_mx; + std::mutex cerr_mx; + + bool arg_has_prefix(const std::string& arg, const std::string& prefix) + { + return arg.size() >= prefix.size() && arg.compare(0, prefix.size(), prefix) == 0; + } +} + +parallel_test_runner::parallel_test_runner( + const boost::program_options::variables_map& vm) + : m_vm(vm) +{} + +int parallel_test_runner::run_parent_if_needed(int argc, char* argv[]) const +{ + const int rc = run_workers_and_wait(argc, argv); + return rc; +} + +bool parallel_test_runner::write_worker_report(const worker_report& rep) const +{ + return write_worker_report_file(rep); +} + +std::filesystem::path parallel_test_runner::get_run_root_path() const +{ + std::string run_root = command_line::get_arg(m_vm, arg_run_root); + if (run_root.empty()) + run_root = "chaingen_runs"; + + std::filesystem::path run_root_path(run_root); + + if (run_root_path.is_relative()) + run_root_path = std::filesystem::absolute(run_root_path); + + return run_root_path; +} + +std::filesystem::path parallel_test_runner::get_worker_dir_path(uint32_t worker_id) const +{ + return get_run_root_path() / (WORKER_DIR_PREFIX+ std::to_string(worker_id)); +} + +std::filesystem::path parallel_test_runner::get_worker_report_path(uint32_t worker_id) const +{ + return get_worker_dir_path(worker_id) / WORKER_REPORT_FILENAME; +} + +std::string parallel_test_runner::make_worker_data_dir(const std::filesystem::path& run_root_abs, int worker_id) const +{ + std::filesystem::path worker_dir = run_root_abs / (std::string(WORKER_DIR_PREFIX) + std::to_string(worker_id)); + std::filesystem::create_directories(worker_dir); + return worker_dir.string(); +} + +std::vector parallel_test_runner::build_base_args_without_worker_specific(int argc, char* argv[]) const +{ + std::vector args; + args.reserve(static_cast(argc)); + + for (int i = 1; i < argc; ++i) + { + std::string current_arg = argv[i]; + + // Strip worker-specific args to avoid duplicates + if (current_arg == ARG_WORKER_ID || arg_has_prefix(current_arg, ARG_WORKER_ID_EQ)) + { + if (current_arg == ARG_WORKER_ID && i + 1 < argc) ++i; + continue; + } + + if (current_arg == ARG_DATA_DIR || arg_has_prefix(current_arg, ARG_DATA_DIR_EQ)) + { + if (current_arg == ARG_DATA_DIR && i + 1 < argc) ++i; + continue; + } + + args.emplace_back(std::move(current_arg)); + } + + return args; +} + +void fill_postponed_tests_set(std::set& postponed_tests); + +int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wall_time_ms) const +{ + std::vector reps; + reps.reserve(processes); + + bool any_missing = false; + int failed_workers = 0; + + for (uint32_t i = 0; i < processes; ++i) + { + worker_report r; + if (!read_worker_report_file(i, r)) + { + any_missing = true; + continue; + } + reps.push_back(std::move(r)); + } + + for (const auto& r : reps) + if (r.exit_code != 0) + ++failed_workers; + + std::set postponed_tests; + fill_postponed_tests_set(postponed_tests); + + size_t total_tests_count = 0; + size_t total_unique_tests_count = 0; + + std::set failed_tests_union; + + for (const auto& r : reps) + { + total_tests_count += r.tests_count; + total_unique_tests_count += r.unique_tests_count; + + failed_tests_union.insert(r.failed_tests.begin(), r.failed_tests.end()); + } + + size_t failed_postponed_tests_count = 0; + for (const auto& t : failed_tests_union) + if (postponed_tests.count(t) != 0) + ++failed_postponed_tests_count; + + const size_t serious_failures_count = failed_tests_union.size() - failed_postponed_tests_count; + + std::cout << (serious_failures_count == 0 && failed_workers == 0 && !any_missing ? concolor::green : concolor::magenta); + + std::cout << "\nREPORT (aggregated):\n"; + std::cout << " Workers: " << processes << "\n"; + if (any_missing) + std::cout << " Warning: some worker reports are missing\n"; + std::cout << " Worker failures: " << failed_workers << "\n"; + + std::cout << " Unique tests run: " << total_unique_tests_count << "\n"; + std::cout << " Total tests run: " << total_tests_count << "\n"; + std::cout << " Failures: " << serious_failures_count + << " (postponed failures: " << failed_postponed_tests_count << ")\n"; + std::cout << " Postponed: " << postponed_tests.size() << "\n"; + std::cout << " Total time: " << (wall_time_ms / 1000) << " s. (" + << (total_tests_count > 0 ? (wall_time_ms / total_tests_count) : 0) + << " ms per test in average)\n"; + if (!failed_tests_union.empty()) + { + std::cout << "FAILED/POSTPONED TESTS:\n"; + for (const auto& name : failed_tests_union) + { + const bool postponed = postponed_tests.count(name) != 0; + std::cout << " " << (postponed ? "POSTPONED: " : "FAILED: ") << name << "\n"; + } + } + + std::cout << concolor::normal << std::endl; + + // If any report missing or worker failed, treat as failure + if (any_missing || failed_workers != 0) + return 1; + + return serious_failures_count == 0 ? 0 : 1; +} + +void parallel_test_runner::print_worker_failure_reasons(uint32_t processes, const std::vector& worker_exit_codes) const +{ + bool any = false; + + for (uint32_t i = 0; i < processes; ++i) + { + const int ec = (i < worker_exit_codes.size() ? worker_exit_codes[i] : -999); + + worker_report rep; + const bool report_ok = read_worker_report_file(i, rep); + + // Skip successful workers that produced a report with exit_code 0. + if (report_ok && ec == 0 && rep.exit_code == 0) + continue; + + if (!any) + { + std::cout << "\nFAILURE REASONS (workers):\n"; + any = true; + } + + const auto worker_dir = get_worker_dir_path(i); + const auto report_path = get_worker_report_path(i); + + std::cout << " Worker " << i << ":\n"; + std::cout << " exit_code: " << ec << "\n"; + std::cout << " report: " << (std::filesystem::exists(report_path) ? report_path.string() : ("missing (" + report_path.string() + ")")) << "\n"; + std::cout << " logs dir: " << worker_dir.string() << "\n"; + + if (!report_ok) + { + std::cout << " failed tests: (cannot read report)\n"; + continue; + } + + if (rep.failed_tests.empty()) + { + std::cout << " failed tests: (none in report)\n"; + continue; + } + + std::cout << " failed tests:\n"; + for (const auto& test_name : rep.failed_tests) + { + std::cout << " - " << test_name << "\n"; + + const auto logs = find_logs_for_test(worker_dir, test_name); + if (logs.empty()) + { + std::cout << " log: (not found, expected prefix: " << sanitize_filename(test_name) << ")\n"; + continue; + } + + std::cout << " log: " << logs.front().string() << "\n"; + } + } +} + +int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const +{ + const uint32_t processes = command_line::get_arg(m_vm, arg_processes); + const int32_t worker_id = command_line::get_arg(m_vm, arg_worker_id); + + // Only parent (multi-process mode, worker_id not set) spawns workers. + if (processes <= 1 || worker_id >= 0) + return -1; + + // Do not spawn workers for these modes. + if (command_line::get_arg(m_vm, command_line::arg_help)) return -1; + if (command_line::get_arg(m_vm, arg_generate_test_data)) return -1; + if (command_line::get_arg(m_vm, arg_play_test_data)) return -1; + if (command_line::get_arg(m_vm, arg_generate_and_play_test_data)) return -1; + if (command_line::get_arg(m_vm, arg_test_transactions)) return -1; + + const std::string run_root = command_line::get_arg(m_vm, arg_run_root); + std::vector base_args = build_base_args_without_worker_specific(argc, argv); + + auto has_flag = [&](const char* flag, const std::string& prefix) -> bool + { + for (const auto& a : base_args) + if (a == flag || arg_has_prefix(a, prefix)) + return true; + return false; + }; + + if (!has_flag("--processes", "--processes=")) + { + base_args.emplace_back("--processes"); + base_args.emplace_back(std::to_string(processes)); + } + + std::filesystem::path run_root_abs = run_root.empty() + ? std::filesystem::path("chaingen_runs") + : std::filesystem::path(run_root); + + if (run_root_abs.is_relative()) + run_root_abs = std::filesystem::absolute(run_root_abs); + + if (!has_flag("--run-root", "--run-root=")) + { + base_args.emplace_back("--run-root"); + base_args.emplace_back(run_root_abs.string()); + } + + // Resolve executable path and make it absolute. + std::string exe = (argv && argv[0]) ? std::string(argv[0]) : std::string(); + if (exe.empty()) + { + std::cout << concolor::magenta + << "Cannot spawn workers: empty executable path (argv[0])" + << concolor::normal << std::endl; + return 1; + } + + try + { + std::filesystem::path exe_path(exe); + + // If no directory component is present, try PATH lookup first. + if (exe.find('/') == std::string::npos && exe.find('\\') == std::string::npos) + { + boost::filesystem::path resolved = bp::search_path(exe); + if (!resolved.empty()) + exe_path = std::filesystem::path(resolved.string()); + } + + // Make absolute because workers will run with start_dir = worker directory. + if (exe_path.is_relative()) + exe_path = std::filesystem::absolute(exe_path); + + exe = exe_path.string(); + } + catch (...) {} + + if (exe.empty() || !std::filesystem::exists(std::filesystem::path(exe))) + { + std::cout << concolor::magenta + << "Cannot spawn workers: failed to resolve executable: " << exe + << concolor::normal << std::endl; + return 1; + } + + std::vector kids; + kids.reserve(processes); + + std::vector worker_exit_codes(processes, -1); + + // Streams for tee + std::vector> kids_out(processes); + std::vector> kids_err(processes); + + // Tee threads + std::vector tee_threads; + tee_threads.reserve(static_cast(processes) * 2); + + const auto wall_t0 = std::chrono::steady_clock::now(); + + for (uint32_t i = 0; i < processes; ++i) + { + std::vector args = base_args; + + args.emplace_back("--worker-id"); + args.emplace_back(std::to_string(i)); + + const std::string worker_data_dir = make_worker_data_dir(run_root_abs, static_cast(i)); + args.emplace_back("--data-dir"); + args.emplace_back(worker_data_dir); + + try + { + const auto worker_dir = get_worker_dir_path(i); + std::filesystem::create_directories(worker_dir); + + kids_out[i] = std::make_unique(); + kids_err[i] = std::make_unique(); + + kids.emplace_back(bp::child( + exe, + bp::args(args), + bp::start_dir = worker_dir.string(), + bp::std_out > *kids_out[i], + bp::std_err > *kids_err[i] + )); + + // Tee stdout: worker -> console; capture per-test log ONLY if failed + tee_threads.emplace_back([worker_dir, s = kids_out[i].get()]() + { + std::filesystem::create_directories(worker_dir); + + bool capturing = false; + std::string current_test_header; + std::filesystem::path current_log_path; + std::ofstream f_log; + + auto close_log = [&]() + { + if (f_log.is_open()) + f_log.close(); + }; + + auto start_capture = [&](const std::string& header_line) + { + close_log(); + capturing = true; + current_test_header = header_line; + + std::string test_name; + if (!parse_test_name_from_header(header_line, test_name)) + test_name = "unknown_test"; + + const std::string safe = sanitize_filename(test_name); + current_log_path = make_unique_log_path(worker_dir, safe); + + f_log.open(current_log_path, std::ios::out | std::ios::binary | std::ios::trunc); + if (f_log.is_open()) + { + f_log << "===== TEST OUTPUT BEGIN: " << header_line << " =====\n"; + f_log.flush(); + } + }; + + auto discard_capture = [&]() + { + close_log(); + capturing = false; + current_test_header.clear(); + + if (!current_log_path.empty()) + { + std::error_code ec; + std::filesystem::remove(current_log_path, ec); + } + current_log_path.clear(); + }; + + auto commit_capture = [&]() + { + if (f_log.is_open()) + { + f_log << "===== TEST OUTPUT END (FAILED): " << current_test_header << " =====\n"; + f_log.flush(); + } + close_log(); + + capturing = false; + current_test_header.clear(); + current_log_path.clear(); + }; + + std::string line; + while (std::getline(*s, line)) + { + // Start marker + if (line.rfind("#TEST# >>>>", 0) == 0) + start_capture(line); + + // If capturing, write everything into per-test log + if (capturing && f_log.is_open()) + { + f_log << line << "\n"; + f_log.flush(); + } + + // End markers + if (capturing && line.find("<<<< Succeeded") != std::string::npos) + { + discard_capture(); + } + else if (capturing && line.find("<<<< FAILED") != std::string::npos) + { + commit_capture(); + } + + // Console output + { + std::lock_guard lk(cout_mx); + std::cout << line << std::endl; + } + } + + // Worker died mid-test without FAILED marker: keep log as failed + if (capturing) + commit_capture(); + }); + + // Tee stderr: worker -> console(stderr) ONLY (no worker_stderr.log) + tee_threads.emplace_back([s = kids_err[i].get()]() + { + std::string line; + while (std::getline(*s, line)) + { + std::lock_guard lk(cerr_mx); + std::cerr << line << std::endl; + } + }); + } + catch (const std::exception& e) + { + std::cout << concolor::magenta + << "Failed to spawn worker " << i << ": " << e.what() + << concolor::normal << std::endl; + + for (auto& c : kids) + if (c.running()) + c.terminate(); + for (auto& c : kids) + c.wait(); + + for (auto& t : tee_threads) + if (t.joinable()) + t.join(); + + return 1; + } + } + + int failed_workers = 0; + for (uint32_t i = 0; i < kids.size(); ++i) + { + kids[i].wait(); + worker_exit_codes[i] = kids[i].exit_code(); + if (worker_exit_codes[i] != 0) + ++failed_workers; + } + + // Wait tee threads after children exit (streams will close). + for (auto& t : tee_threads) + if (t.joinable()) + t.join(); + + const auto wall_t1 = std::chrono::steady_clock::now(); + const uint64_t wall_ms = + static_cast(std::chrono::duration_cast(wall_t1 - wall_t0).count()); + + // Aggregated report first. + const int aggregated_rc = print_aggregated_report_and_return_rc(processes, wall_ms); + + // Then print diagnostics. + if (failed_workers != 0) + print_worker_failure_reasons(processes, worker_exit_codes); + + // Any worker non-zero => fail. + if (failed_workers != 0) + return 1; + + return aggregated_rc; +} + +std::string parallel_test_runner::sanitize_filename(std::string s) +{ + for (char& c : s) + { + const bool ok = + (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '.' || c == '_' || c == '-'; + if (!ok) + c = '_'; + } + + // Avoid empty name + if (s.empty()) + s = "unnamed_test"; + + // Keep filenames reasonably short + constexpr size_t kMax = 180; + if (s.size() > kMax) + s.resize(kMax); + + return s; +} + +bool parallel_test_runner::parse_test_name_from_header(const std::string& line, std::string& out_name) +{ + constexpr const char* kPrefix = "#TEST# >>>>"; + auto pos = line.find(kPrefix); + if (pos != 0) + return false; + + std::string rest = line.substr(std::strlen(kPrefix)); + // trim left + while (!rest.empty() && std::isspace(static_cast(rest.front()))) + rest.erase(rest.begin()); + + auto end = rest.find("<<<<"); + if (end == std::string::npos) + return false; + + std::string name = rest.substr(0, end); + // trim right + while (!name.empty() && std::isspace(static_cast(name.back()))) + name.pop_back(); + + if (name.empty()) + return false; + + out_name = name; + return true; +} + +std::filesystem::path parallel_test_runner::make_unique_log_path(const std::filesystem::path& dir, const std::string& base_name_no_ext) +{ + std::filesystem::path p = dir / (base_name_no_ext + ".log"); + if (!std::filesystem::exists(p)) + return p; + + for (size_t i = 2; i < 10000; ++i) + { + std::filesystem::path alt = dir / (base_name_no_ext + "_" + std::to_string(i) + ".log"); + if (!std::filesystem::exists(alt)) + return alt; + } + + return p; // fallback +} + +std::vector parallel_test_runner::find_logs_for_test( + const std::filesystem::path& worker_dir, + const std::string& test_name) +{ + std::vector result; + + const std::string base = sanitize_filename(test_name); + const std::string base_prefix = base + "_"; + + try + { + for (auto& de : std::filesystem::directory_iterator(worker_dir)) + { + if (!de.is_regular_file()) + continue; + + const auto p = de.path(); + if (p.extension() != ".log") + continue; + + const std::string stem = p.stem().string(); + if (stem == base || stem.rfind(base_prefix, 0) == 0) + result.push_back(p); + } + } + catch (...) {} + + std::sort(result.begin(), result.end(), + [](const std::filesystem::path& a, const std::filesystem::path& b) + { + std::error_code ec1, ec2; + const auto ta = std::filesystem::last_write_time(a, ec1); + const auto tb = std::filesystem::last_write_time(b, ec2); + if (!ec1 && !ec2 && ta != tb) + return ta > tb; + return a.string() < b.string(); + }); + + return result; +} + +bool parallel_test_runner::write_worker_report_file(const worker_report& rep) const +{ + try + { + std::filesystem::create_directories(get_worker_dir_path(rep.worker_id)); + + pt::ptree root; + root.put("worker_id", rep.worker_id); + root.put("processes", rep.processes); + root.put("tests_count", static_cast(rep.tests_count)); + root.put("unique_tests_count", static_cast(rep.unique_tests_count)); + root.put("total_time_ms", rep.total_time_ms); + root.put("skip_all_till_the_end", rep.skip_all_till_the_end); + root.put("exit_code", rep.exit_code); + + pt::ptree failed_arr; + for (const auto& s : rep.failed_tests) + { + pt::ptree v; + v.put("", s); + failed_arr.push_back(std::make_pair("", v)); + } + root.add_child("failed_tests", failed_arr); + + pt::ptree times_arr; + for (const auto& it : rep.tests_running_time) + { + pt::ptree node; + node.put("name", it.first); + node.put("ms", it.second); + times_arr.push_back(std::make_pair("", node)); + } + root.add_child("tests_running_time", times_arr); + + std::ofstream out(get_worker_report_path(rep.worker_id)); + if (!out.is_open()) + return false; + + pt::write_json(out, root, /*pretty*/true); + return true; + } + catch (...) + { + return false; + } +} + +bool parallel_test_runner::read_worker_report_file(uint32_t worker_id, worker_report& rep) const +{ + try + { + const auto path = get_worker_report_path(worker_id); + if (!std::filesystem::exists(path)) + return false; + + pt::ptree root; + pt::read_json(path.string(), root); + + rep.worker_id = root.get(JSON_WORKER_ID, worker_id); + rep.processes = root.get(JSON_PROCESSES, 1); + + rep.tests_count = static_cast(root.get(JSON_TESTS_COUNT, 0)); + rep.unique_tests_count = static_cast(root.get(JSON_UNIQUE_TESTS_COUNT, 0)); + rep.total_time_ms = root.get(JSON_TOTAL_TIME_MS, 0); + + rep.skip_all_till_the_end = root.get(JSON_SKIP_ALL_TILL_END, false); + rep.exit_code = root.get(JSON_EXIT_CODE, 1); + + for (const auto& v : root.get_child(JSON_FAILED_TESTS, pt::ptree())) + { + const std::string name = v.second.get_value(); + if (!name.empty()) + rep.failed_tests.insert(name); + } + + for (const auto& v : root.get_child(JSON_TESTS_RUNNING_TIME, pt::ptree())) + { + const auto& node = v.second; + const std::string name = node.get(JSON_NAME, ""); + const uint64_t ms = node.get(JSON_MS, 0); + if (!name.empty()) + rep.tests_running_time.emplace_back(name, ms); + } + + return true; + } + catch (...) + { + return false; + } +} diff --git a/tests/core_tests/parallel/parallel_test_runner.h b/tests/core_tests/parallel/parallel_test_runner.h new file mode 100644 index 000000000..70cd7cc96 --- /dev/null +++ b/tests/core_tests/parallel/parallel_test_runner.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include + +class parallel_test_runner +{ +public: + static constexpr int kNotParent = -1; + + explicit parallel_test_runner(const boost::program_options::variables_map& vm); + + struct worker_report + { + uint32_t worker_id = 0; + uint32_t processes = 1; + + size_t tests_count = 0; + size_t unique_tests_count = 0; + + uint64_t total_time_ms = 0; + + std::vector> tests_running_time; + std::set failed_tests; + bool skip_all_till_the_end = false; + + int exit_code = 0; + }; + + int run_parent_if_needed(int argc, char* argv[]) const; + std::filesystem::path get_worker_dir_path(uint32_t worker_id) const; + bool write_worker_report(const worker_report& rep) const; + +private: + const boost::program_options::variables_map& m_vm; + + std::filesystem::path get_run_root_path() const; + std::filesystem::path get_worker_report_path(uint32_t worker_id) const; + std::string make_worker_data_dir(const std::filesystem::path& run_root_abs, int worker_id) const; + + std::vector build_base_args_without_worker_specific(int argc, char* argv[]) const; + + int run_workers_and_wait(int argc, char* argv[]) const; + + int print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wall_time_ms) const; + void print_worker_failure_reasons(uint32_t processes, const std::vector& worker_exit_codes) const; + + static std::string sanitize_filename(std::string s); + static bool parse_test_name_from_header(const std::string& line, std::string& out_name); + static std::filesystem::path make_unique_log_path(const std::filesystem::path& dir, const std::string& base_name_no_ext); + static std::vector find_logs_for_test(const std::filesystem::path& worker_dir, const std::string& test_name); + + bool write_worker_report_file(const worker_report& rep) const; + bool read_worker_report_file(uint32_t worker_id, worker_report& rep) const; +}; From 9f9e47f76d5d1585e2700d368774b4a223491393 Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Tue, 13 Jan 2026 00:09:16 +0300 Subject: [PATCH 06/28] fix formating and variable --- tests/core_tests/chaingen_main.cpp | 47 ++++++++++-------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index 73a341293..19d64c4bf 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -30,7 +30,7 @@ namespace po = boost::program_options; namespace bp = boost::process; namespace pt = boost::property_tree; -extern const char* TAKEN_TESTS_LOG_FILENAME; +extern std::string TAKEN_TESTS_LOG_FILENAME; command_line::arg_descriptor arg_test_data_path ("test-data-path", "", ""); command_line::arg_descriptor arg_generate_test_data ("generate-test-data", ""); @@ -76,10 +76,11 @@ namespace #define GENERATE_AND_PLAY(genclass) \ do { \ const std::string __test_name = #genclass; \ + auto __is_test_eligible_to_run = is_test_eligible_to_run; \ g_test_jobs.push_back(test_job{ \ __test_name, \ - [&, __test_name]() -> bool { \ - if (!is_test_eligible_to_run(__test_name)) \ + [&, __test_name, __is_test_eligible_to_run]() -> bool { \ + if (!__is_test_eligible_to_run(__test_name)) \ return true; \ return run_one_test_job( \ __test_name, \ @@ -102,14 +103,15 @@ namespace LOG_ERROR("invalid hardforks mask: " << hardfork_str_mask << " for test " << __gen_name); \ break; \ } \ + auto __hf_filter = is_hf_test_eligible_to_run; \ for (size_t __i = 0; __i < __hardforks.size(); ++__i) \ { \ const size_t __hf_id = __hardforks[__i]; \ const std::string __test_name = __gen_name + " @ HF " + epee::string_tools::num_to_string_fast(__hf_id); \ g_test_jobs.push_back(test_job{ \ __test_name, \ - [&, __test_name, __gen_name, __hf_id]() -> bool { \ - if (!is_hf_test_eligible_to_run(__gen_name, __hf_id)) \ + [&, __test_name, __gen_name, __hf_id, __hf_filter]() -> bool { \ + if (!__hf_filter(__gen_name, __hf_id)) \ return true; \ return run_one_test_job( \ __test_name, \ @@ -952,15 +954,8 @@ static void log_test_taken_by_this_process(const std::string& test_name) catch (...) {} } //-------------------------------------------------------------------------- -static bool run_one_test_job( - const std::string& test_name, - const std::function& fn, - bool& stop_on_first_fail, - bool& skip_all_till_the_end, - size_t& tests_count, - size_t& unique_tests_count, - std::set& failed_tests, - std::vector>& tests_running_time) +static bool run_one_test_job(const std::string& test_name, const std::function& fn, bool& stop_on_first_fail, bool& skip_all_till_the_end, + size_t& tests_count, size_t& unique_tests_count, std::set& failed_tests, std::vector>& tests_running_time) { if (skip_all_till_the_end) return true; @@ -987,15 +982,9 @@ static bool run_one_test_job( return ok; } -static bool run_registered_tests( - bool& stop_on_first_fail, - bool& skip_all_till_the_end, - size_t& tests_count, - size_t& unique_tests_count, - std::set& failed_tests, - std::vector>& tests_running_time, - const std::function& /*is_test_eligible_to_run*/, - const std::function& /*is_hf_test_eligible_to_run*/) +static bool run_registered_tests(bool& stop_on_first_fail, bool& skip_all_till_the_end, size_t& tests_count, size_t& unique_tests_count, + std::set& failed_tests, std::vector>& tests_running_time, + const std::function& /*is_test_eligible_to_run*/, const std::function& /*is_hf_test_eligible_to_run*/) { bool all_ok = true; @@ -1028,15 +1017,9 @@ static bool run_registered_tests( return all_ok; } //-------------------------------------------------------------------------- -static void register_all_tests( - bool& stop_on_first_fail, - bool& skip_all_till_the_end, - size_t& tests_count, - size_t& unique_tests_count, - std::set& failed_tests, - std::vector>& tests_running_time, - const std::function& is_test_eligible_to_run, - const std::function& is_hf_test_eligible_to_run) +static void register_all_tests(bool& stop_on_first_fail, bool& skip_all_till_the_end, size_t& tests_count, size_t& unique_tests_count, + std::set& failed_tests, std::vector>& tests_running_time, + std::function is_test_eligible_to_run, std::function is_hf_test_eligible_to_run) { g_test_jobs.clear(); From da477202119de03ecdcccb524a3702b60a2350d1 Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Tue, 13 Jan 2026 13:30:54 +0300 Subject: [PATCH 07/28] update logs --- tests/core_tests/chaingen_main.cpp | 2 +- .../parallel/parallel_test_runner.cpp | 112 ++++-------------- .../parallel/parallel_test_runner.h | 25 +++- 3 files changed, 46 insertions(+), 93 deletions(-) diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index 19d64c4bf..d561d0432 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -1454,7 +1454,7 @@ int main(int argc, char* argv[]) g_runner = std::make_unique(g_vm); const int parent_rc = g_runner->run_parent_if_needed(argc, argv); - if (parent_rc != parallel_test_runner::kNotParent) + if (parent_rc != parallel_test_runner::k_not_parent) return parent_rc; if (command_line::get_arg(g_vm, command_line::arg_help)) diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index eb043e978..5429c8f37 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -1,3 +1,9 @@ +#if defined(_WIN32) + #include "chaingen.h" +#else + #include "../chaingen.h" +#endif + #include #include #include @@ -7,7 +13,6 @@ #include "parallel_test_runner.h" #include "../../src/common/command_line.h" -#include "../chaingen.h" namespace bp = boost::process; namespace pt = boost::property_tree; @@ -39,6 +44,7 @@ namespace const char* JSON_MS = "ms"; const char* WORKER_REPORT_FILENAME = "coretests_report.json"; + const char* WORKER_LOG_FILENAME = "worker.log"; const char* ARG_WORKER_ID = "--worker-id"; const char* ARG_WORKER_ID_EQ = "--worker-id="; @@ -237,11 +243,11 @@ void parallel_test_runner::print_worker_failure_reasons(uint32_t processes, cons const auto worker_dir = get_worker_dir_path(i); const auto report_path = get_worker_report_path(i); + const auto log_path = worker_dir / WORKER_LOG_FILENAME; - std::cout << " Worker " << i << ":\n"; - std::cout << " exit_code: " << ec << "\n"; std::cout << " report: " << (std::filesystem::exists(report_path) ? report_path.string() : ("missing (" + report_path.string() + ")")) << "\n"; std::cout << " logs dir: " << worker_dir.string() << "\n"; + std::cout << " log file: " << (std::filesystem::exists(log_path) ? log_path.string() : ("missing (" + log_path.string() + ")")) << "\n"; if (!report_ok) { @@ -398,113 +404,32 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const bp::std_err > *kids_err[i] )); + std::shared_ptr sink(new worker_log_sink()); + sink->open(get_worker_dir_path(i) / WORKER_LOG_FILENAME); + // Tee stdout: worker -> console; capture per-test log ONLY if failed - tee_threads.emplace_back([worker_dir, s = kids_out[i].get()]() + tee_threads.emplace_back([worker_dir, s = kids_out[i].get(), sink]() { std::filesystem::create_directories(worker_dir); - bool capturing = false; - std::string current_test_header; - std::filesystem::path current_log_path; - std::ofstream f_log; - - auto close_log = [&]() - { - if (f_log.is_open()) - f_log.close(); - }; - - auto start_capture = [&](const std::string& header_line) - { - close_log(); - capturing = true; - current_test_header = header_line; - - std::string test_name; - if (!parse_test_name_from_header(header_line, test_name)) - test_name = "unknown_test"; - - const std::string safe = sanitize_filename(test_name); - current_log_path = make_unique_log_path(worker_dir, safe); - - f_log.open(current_log_path, std::ios::out | std::ios::binary | std::ios::trunc); - if (f_log.is_open()) - { - f_log << "===== TEST OUTPUT BEGIN: " << header_line << " =====\n"; - f_log.flush(); - } - }; - - auto discard_capture = [&]() - { - close_log(); - capturing = false; - current_test_header.clear(); - - if (!current_log_path.empty()) - { - std::error_code ec; - std::filesystem::remove(current_log_path, ec); - } - current_log_path.clear(); - }; - - auto commit_capture = [&]() - { - if (f_log.is_open()) - { - f_log << "===== TEST OUTPUT END (FAILED): " << current_test_header << " =====\n"; - f_log.flush(); - } - close_log(); - - capturing = false; - current_test_header.clear(); - current_log_path.clear(); - }; - std::string line; while (std::getline(*s, line)) { - // Start marker - if (line.rfind("#TEST# >>>>", 0) == 0) - start_capture(line); - - // If capturing, write everything into per-test log - if (capturing && f_log.is_open()) - { - f_log << line << "\n"; - f_log.flush(); - } - - // End markers - if (capturing && line.find("<<<< Succeeded") != std::string::npos) - { - discard_capture(); - } - else if (capturing && line.find("<<<< FAILED") != std::string::npos) - { - commit_capture(); - } - - // Console output + sink->write_line(line); { std::lock_guard lk(cout_mx); std::cout << line << std::endl; } } - - // Worker died mid-test without FAILED marker: keep log as failed - if (capturing) - commit_capture(); }); // Tee stderr: worker -> console(stderr) ONLY (no worker_stderr.log) - tee_threads.emplace_back([s = kids_err[i].get()]() + tee_threads.emplace_back([s = kids_err[i].get(), sink]() { std::string line; while (std::getline(*s, line)) { + sink->write_line(std::string("[stderr] ") + line); std::lock_guard lk(cerr_mx); std::cerr << line << std::endl; } @@ -763,3 +688,8 @@ bool parallel_test_runner::read_worker_report_file(uint32_t worker_id, worker_re return false; } } + +std::filesystem::path parallel_test_runner::get_worker_log_path(uint32_t worker_id) const +{ + return get_worker_dir_path(worker_id) / WORKER_LOG_FILENAME; +} diff --git a/tests/core_tests/parallel/parallel_test_runner.h b/tests/core_tests/parallel/parallel_test_runner.h index 70cd7cc96..a853addf4 100644 --- a/tests/core_tests/parallel/parallel_test_runner.h +++ b/tests/core_tests/parallel/parallel_test_runner.h @@ -8,7 +8,7 @@ class parallel_test_runner { public: - static constexpr int kNotParent = -1; + static constexpr int k_not_parent = -1; explicit parallel_test_runner(const boost::program_options::variables_map& vm); @@ -54,4 +54,27 @@ class parallel_test_runner bool write_worker_report_file(const worker_report& rep) const; bool read_worker_report_file(uint32_t worker_id, worker_report& rep) const; + + std::filesystem::path get_worker_log_path(uint32_t worker_id) const; +}; + +struct worker_log_sink +{ + std::mutex mx; + std::ofstream f; + + void open(const std::filesystem::path& path) + { + std::filesystem::create_directories(path.parent_path()); + f.open(path, std::ios::out | std::ios::binary | std::ios::trunc); + } + + void write_line(const std::string& line) + { + std::lock_guard lk(mx); + if (!f.is_open()) + return; + f << line << "\n"; + f.flush(); + } }; From 32dc206a98bb0420dcb6a44c176cdc90892b2db5 Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Tue, 13 Jan 2026 14:38:50 +0300 Subject: [PATCH 08/28] refactoring --- tests/core_tests/chaingen_args.cpp | 17 ++ tests/core_tests/chaingen_args.h | 20 ++ tests/core_tests/chaingen_main.cpp | 234 ++++++------------ .../parallel/parallel_test_runner.cpp | 90 +++++-- .../parallel/parallel_test_runner.h | 9 + 5 files changed, 188 insertions(+), 182 deletions(-) create mode 100644 tests/core_tests/chaingen_args.cpp create mode 100644 tests/core_tests/chaingen_args.h diff --git a/tests/core_tests/chaingen_args.cpp b/tests/core_tests/chaingen_args.cpp new file mode 100644 index 000000000..8171921fe --- /dev/null +++ b/tests/core_tests/chaingen_args.cpp @@ -0,0 +1,17 @@ +#include "chaingen_args.h" + +namespace chaingen_args +{ + command_line::arg_descriptor arg_test_data_path ("test-data-path", "", ""); + command_line::arg_descriptor arg_generate_test_data ("generate-test-data", ""); + command_line::arg_descriptor arg_play_test_data ("play-test-data", ""); + command_line::arg_descriptor arg_generate_and_play_test_data ("generate-and-play-test-data", ""); + command_line::arg_descriptor arg_test_transactions ("test-transactions", ""); + command_line::arg_descriptor arg_run_single_test ("run-single-test", " TEST_NAME -- name of a single test to run, HF -- specific hardfork id to run the test for" ); + command_line::arg_descriptor arg_run_multiple_tests ("run-multiple-tests", "comma-separated list of tests to run, OR text file <@filename> containing list of tests"); + command_line::arg_descriptor arg_enable_debug_asserts ("enable-debug-asserts", "" ); + command_line::arg_descriptor arg_stop_on_fail ("stop-on-fail", ""); + command_line::arg_descriptor arg_processes ("processes", "Number of worker processes", 1); + command_line::arg_descriptor arg_worker_id ("worker-id", "Internal: worker index", -1); + command_line::arg_descriptor arg_run_root ("run-root", "Internal: run root dir", ""); +} diff --git a/tests/core_tests/chaingen_args.h b/tests/core_tests/chaingen_args.h new file mode 100644 index 000000000..34d81af2e --- /dev/null +++ b/tests/core_tests/chaingen_args.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include +#include "common/command_line.h" + +namespace chaingen_args +{ + extern command_line::arg_descriptor arg_test_data_path; + extern command_line::arg_descriptor arg_generate_test_data; + extern command_line::arg_descriptor arg_play_test_data; + extern command_line::arg_descriptor arg_generate_and_play_test_data; + extern command_line::arg_descriptor arg_test_transactions; + extern command_line::arg_descriptor arg_run_single_test; + extern command_line::arg_descriptor arg_run_multiple_tests; + extern command_line::arg_descriptor arg_enable_debug_asserts; + extern command_line::arg_descriptor arg_stop_on_fail; + extern command_line::arg_descriptor arg_processes; + extern command_line::arg_descriptor arg_worker_id; + extern command_line::arg_descriptor arg_run_root; +} diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index d561d0432..e5ff8aa84 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -6,7 +6,6 @@ #include "chaingen.h" #include "chaingen_tests_list.h" -#include "common/command_line.h" #include "transaction_tests.h" #include "../../src/wallet/core_fast_rpc_proxy.h" #include "test_core_proxy.h" @@ -15,6 +14,7 @@ #include "core_state_helper.h" #include "common/db_backend_selector.h" #include "parallel/parallel_test_runner.h" +#include "chaingen_args.h" #include #include #include @@ -26,29 +26,17 @@ #define TX_BLOBSIZE_CHECKER_LOG_FILENAME "get_object_blobsize(tx).log" +using namespace chaingen_args; + namespace po = boost::program_options; namespace bp = boost::process; namespace pt = boost::property_tree; -extern std::string TAKEN_TESTS_LOG_FILENAME; - -command_line::arg_descriptor arg_test_data_path ("test-data-path", "", ""); -command_line::arg_descriptor arg_generate_test_data ("generate-test-data", ""); -command_line::arg_descriptor arg_play_test_data ("play-test-data", ""); -command_line::arg_descriptor arg_generate_and_play_test_data ("generate-and-play-test-data", ""); -command_line::arg_descriptor arg_test_transactions ("test-transactions", ""); -command_line::arg_descriptor arg_run_single_test ("run-single-test", " TEST_NAME -- name of a single test to run, HF -- specific hardfork id to run the test for" ); -command_line::arg_descriptor arg_run_multiple_tests ("run-multiple-tests", "comma-separated list of tests to run, OR text file <@filename> containing list of tests"); -command_line::arg_descriptor arg_enable_debug_asserts ("enable-debug-asserts", "" ); -command_line::arg_descriptor arg_stop_on_fail ("stop-on-fail", ""); -command_line::arg_descriptor arg_processes ("processes", "Number of worker processes", 1); -command_line::arg_descriptor arg_worker_id ("worker-id", "Internal: worker index", -1); -command_line::arg_descriptor arg_run_root ("run-root", "Internal: run root dir", ""); - namespace { boost::program_options::variables_map g_vm; std::unique_ptr g_runner; + std::vector g_test_jobs; } #define GENERATE(filename, genclass) \ @@ -73,60 +61,6 @@ namespace return 1; \ } -#define GENERATE_AND_PLAY(genclass) \ - do { \ - const std::string __test_name = #genclass; \ - auto __is_test_eligible_to_run = is_test_eligible_to_run; \ - g_test_jobs.push_back(test_job{ \ - __test_name, \ - [&, __test_name, __is_test_eligible_to_run]() -> bool { \ - if (!__is_test_eligible_to_run(__test_name)) \ - return true; \ - return run_one_test_job( \ - __test_name, \ - [&]() -> bool { return generate_and_play(__test_name.c_str()); }, \ - stop_on_first_fail, \ - skip_all_till_the_end, \ - tests_count, \ - unique_tests_count, \ - failed_tests, \ - tests_running_time); \ - } \ - }); \ - } while (0) - -#define GENERATE_AND_PLAY_HF(genclass, hardfork_str_mask) \ - do { \ - const std::string __gen_name = #genclass; \ - std::vector __hardforks = parse_hardfork_str_mask(hardfork_str_mask); \ - if (__hardforks.empty()) { \ - LOG_ERROR("invalid hardforks mask: " << hardfork_str_mask << " for test " << __gen_name); \ - break; \ - } \ - auto __hf_filter = is_hf_test_eligible_to_run; \ - for (size_t __i = 0; __i < __hardforks.size(); ++__i) \ - { \ - const size_t __hf_id = __hardforks[__i]; \ - const std::string __test_name = __gen_name + " @ HF " + epee::string_tools::num_to_string_fast(__hf_id); \ - g_test_jobs.push_back(test_job{ \ - __test_name, \ - [&, __test_name, __gen_name, __hf_id, __hf_filter]() -> bool { \ - if (!__hf_filter(__gen_name, __hf_id)) \ - return true; \ - return run_one_test_job( \ - __test_name, \ - [&]() -> bool { return generate_and_play(__test_name.c_str(), __hf_id); }, \ - stop_on_first_fail, \ - skip_all_till_the_end, \ - tests_count, \ - unique_tests_count, \ - failed_tests, \ - tests_running_time); \ - } \ - }); \ - } \ - } while (0) - std::vector parse_hardfork_str_mask(std::string s /* intentionally passing by value */) { // "*" -> 0, 1, 2, ..., ZANO_HARDFORKS_TOTAL-1 @@ -456,6 +390,60 @@ bool gen_and_play_intermitted_by_blockchain_saveload(const char* const genclass_ return r; } +#define GENERATE_AND_PLAY(genclass) \ + do { \ + const std::string __test_name = #genclass; \ + auto __is_test_eligible_to_run = is_test_eligible_to_run; \ + g_test_jobs.push_back(test_job{ \ + __test_name, \ + [&, __test_name, __is_test_eligible_to_run]() -> bool { \ + if (!__is_test_eligible_to_run(__test_name)) \ + return true; \ + return run_one_test_job( \ + __test_name, \ + [&]() -> bool { return generate_and_play(__test_name.c_str()); }, \ + stop_on_first_fail, \ + skip_all_till_the_end, \ + tests_count, \ + unique_tests_count, \ + failed_tests, \ + tests_running_time); \ + } \ + }); \ + } while (0) + +#define GENERATE_AND_PLAY_HF(genclass, hardfork_str_mask) \ + do { \ + const std::string __gen_name = #genclass; \ + std::vector __hardforks = parse_hardfork_str_mask(hardfork_str_mask); \ + if (__hardforks.empty()) { \ + LOG_ERROR("invalid hardforks mask: " << hardfork_str_mask << " for test " << __gen_name); \ + break; \ + } \ + auto __hf_filter = is_hf_test_eligible_to_run; \ + for (size_t __i = 0; __i < __hardforks.size(); ++__i) \ + { \ + const size_t __hf_id = __hardforks[__i]; \ + const std::string __test_name = __gen_name + " @ HF " + epee::string_tools::num_to_string_fast(__hf_id); \ + g_test_jobs.push_back(test_job{ \ + __test_name, \ + [&, __test_name, __gen_name, __hf_id, __hf_filter]() -> bool { \ + if (!__hf_filter(__gen_name, __hf_id)) \ + return true; \ + return run_one_test_job( \ + __test_name, \ + [&]() -> bool { return generate_and_play(__test_name.c_str(), __hf_id); }, \ + stop_on_first_fail, \ + skip_all_till_the_end, \ + tests_count, \ + unique_tests_count, \ + failed_tests, \ + tests_running_time); \ + } \ + }); \ + } \ + } while (0) + #define GENERATE_AND_PLAY_INTERMITTED_BY_BLOCKCHAIN_SAVELOAD(genclass) \ if (is_test_eligible_to_run(#genclass)) \ { \ @@ -725,15 +713,6 @@ struct push_core_event_visitor : public boost::static_visitor LOG_PRINT_COLOR_NO_PREFIX("=== EVENT # " << m_ev_index << ": " << event_type, LOG_LEVEL_0, LOG_COLOR_YELLOW); } }; -//-------------------------------------------------------------------------- -struct test_job -{ - std::string name; - std::function run; -}; - -static std::vector g_test_jobs; - //-------------------------------------------------------------------------- template inline bool replay_events_through_core(currency::core& cr, const std::vector& events, t_test_class& validator, test_core_listener* core_listener, size_t event_index_from = 0, size_t event_index_to = SIZE_MAX) @@ -911,47 +890,11 @@ bool parse_cmd_specific_tests_to_run(std::unordered_multimapget_worker_dir_path(worker_id) / TAKEN_TESTS_LOG_FILENAME; -} - -static std::filesystem::path get_single_process_taken_tests_log_path() -{ - std::string data_dir = command_line::get_arg(g_vm, command_line::arg_data_dir); - return std::filesystem::path(data_dir) / TAKEN_TESTS_LOG_FILENAME; -} - +//-------------------------------------------------------------------------- static void log_test_taken_by_this_process(const std::string& test_name) { - try - { - const uint32_t processes = command_line::get_arg(g_vm, arg_processes); - const int32_t worker_id = command_line::get_arg(g_vm, arg_worker_id); - - std::filesystem::path p; - - if (processes > 1 && worker_id >= 0) - { - const uint32_t wid = static_cast(worker_id); - std::filesystem::create_directories(g_runner->get_worker_dir_path(wid)); - p = get_worker_taken_tests_log_path(wid); - } - else - { - std::filesystem::create_directories(command_line::get_arg(g_vm, command_line::arg_data_dir)); - p = get_single_process_taken_tests_log_path(); - } - - std::ofstream f(p, std::ios::out | std::ios::binary | std::ios::app); - if (!f.is_open()) - return; - - f << test_name << "\n"; - f.flush(); - } - catch (...) {} + if (g_runner) + g_runner->log_test_taken_by_this_process(test_name); } //-------------------------------------------------------------------------- static bool run_one_test_job(const std::string& test_name, const std::function& fn, bool& stop_on_first_fail, bool& skip_all_till_the_end, @@ -1404,20 +1347,18 @@ int main(int argc, char* argv[]) TRY_ENTRY(); string_tools::set_module_name_and_folder(argv[0]); - // set up logging options + //set up logging options log_space::get_set_log_detalisation_level(true, LOG_LEVEL_2); log_space::log_singletone::add_logger(LOGGER_CONSOLE, NULL, NULL, LOG_LEVEL_4); - log_space::log_singletone::add_logger( - LOGGER_FILE, - log_space::log_singletone::get_default_log_file().c_str(), - log_space::log_singletone::get_default_log_folder().c_str()); + log_space::log_singletone::add_logger(LOGGER_FILE, + log_space::log_singletone::get_default_log_file().c_str(), log_space::log_singletone::get_default_log_folder().c_str()); log_space::log_singletone::enable_channels("core,currency_protocol,tx_pool,p2p,wallet", false); tools::signal_handler::install_fatal([](int sig_number, void* address) { LOG_ERROR("\n\nFATAL ERROR\nsig: " << sig_number << ", address: " << address); - std::fflush(nullptr); // flush all open output streams + std::fflush(nullptr); // all open output streams are flushed }); po::options_description desc_options("Allowed options"); @@ -1441,7 +1382,6 @@ int main(int argc, char* argv[]) currency::core::init_options(desc_options); tools::db::db_backend_selector::init_options(desc_options); - bool stop_on_first_fail = false; bool r = command_line::handle_error_helper(desc_options, [&]() { @@ -1491,7 +1431,6 @@ int main(int argc, char* argv[]) typedef std::vector> tests_running_time_t; tests_running_time_t tests_running_time; - if (command_line::get_arg(g_vm, arg_generate_test_data)) { GENERATE("chain001.dat", gen_simple_chain_001); @@ -1506,7 +1445,7 @@ int main(int argc, char* argv[]) } else { - epee::debug::get_set_enable_assert(true, command_line::get_arg(g_vm, arg_enable_debug_asserts)); + epee::debug::get_set_enable_assert(true, command_line::get_arg(g_vm, arg_enable_debug_asserts)); // don't comment out this: many tests have normal-negative checks (i.e. tx with invalid amount shouldn't be created), so be ready for MANY assertion breaks std::unordered_multimap specific_tests_to_run; CHECK_AND_ASSERT_MES(parse_cmd_specific_tests_to_run(specific_tests_to_run), 1, "Error while parsing specific tests to run"); @@ -1516,7 +1455,6 @@ int main(int argc, char* argv[]) auto is_hf_test_eligible_to_run = [&](const std::string& genclass_str, size_t hardfork) -> bool { if (skip_all_till_the_end) return false; - if (specific_tests_to_run.empty()) { if (postponed_tests.count(genclass_str) != 0) @@ -1527,12 +1465,10 @@ int main(int argc, char* argv[]) auto it_pair = specific_tests_to_run.equal_range(genclass_str); if (it_pair.first == it_pair.second) return false; - bool match = false; - for (auto it = it_pair.first; it != it_pair.second; ++it) + for(auto it = it_pair.first; it != it_pair.second; ++it) if (it->second == SIZE_MAX || hardfork == SIZE_MAX || it->second == hardfork) match = true; - if (!match) return false; } @@ -1541,9 +1477,7 @@ int main(int argc, char* argv[]) return true; }; - auto is_test_eligible_to_run = [&](const std::string& genclass_str) -> bool { - return is_hf_test_eligible_to_run(genclass_str, SIZE_MAX); - }; + auto is_test_eligible_to_run = [&](const std::string& genclass_str) -> bool { return is_hf_test_eligible_to_run(genclass_str, SIZE_MAX); }; if (specific_tests_to_run.empty()) { @@ -1583,7 +1517,6 @@ int main(int argc, char* argv[]) size_t failed_postponed_tests_count = 0; uint64_t total_time = 0; - if (!tests_running_time.empty()) { uint64_t max_time = std::max_element( @@ -1610,16 +1543,13 @@ int main(int argc, char* argv[]) ++failed_postponed_tests_count; std::string bar(bar_width * i.second / max_time, '#'); - std::cout << (failed ? (postponed ? concolor::yellow : concolor::magenta) : concolor::green) - << std::left << std::setw(max_test_name_len + 1) << i.first - << "\t" << std::setw(10) << i.second << " ms \t" << bar << std::endl; + std::cout << (failed ? (postponed ? concolor::yellow : concolor::magenta) : concolor::green) << std::left << std::setw(max_test_name_len + 1) << i.first << "\t" << std::setw(10) << i.second << " ms \t" << bar << std::endl; total_time += i.second; } } else { - // Workers still must compute totals. for (const auto& i : tests_running_time) total_time += i.second; @@ -1630,13 +1560,10 @@ int main(int argc, char* argv[]) } if (skip_all_till_the_end && !is_worker) - std::cout << ENDL << concolor::yellow - << "(execution interrupted at the first failure; not all tests were run)" - << ENDL; + std::cout << ENDL << concolor::yellow << "(execution interrupted at the first failure; not all tests were run)" << ENDL; serious_failures_count = failed_tests.size() - failed_postponed_tests_count; - // Workers write per-worker report for parent aggregation. if (is_worker) { parallel_test_runner::worker_report rep; @@ -1653,7 +1580,6 @@ int main(int argc, char* argv[]) (void)g_runner->write_worker_report(rep); } - // Single-process run: keep local report (parent aggregation is only for multi-process mode). if (!is_worker) { if (!postponed_tests.empty()) @@ -1668,23 +1594,16 @@ int main(int argc, char* argv[]) std::cout << " Unique tests run: " << unique_tests_count << std::endl; std::cout << " Total tests run: " << tests_count << std::endl; - std::cout << " Failures: " << serious_failures_count - << " (postponed failures: " << failed_postponed_tests_count << ")" - << std::endl; - + std::cout << " Failures: " << serious_failures_count << " (postponed failures: " << failed_postponed_tests_count << ")" << std::endl; std::cout << " Postponed: " << postponed_tests.size() << std::endl; - - std::cout << " Total time: " << total_time / 1000 - << " s. (" << (tests_count > 0 ? total_time / tests_count : 0) - << " ms per test in average)" - << std::endl; + std::cout << " Total time: " << total_time / 1000 << " s. (" << (tests_count > 0 ? total_time / tests_count : 0) << " ms per test in average)" << std::endl; if (!failed_tests.empty()) { std::cout << "FAILED/POSTPONED TESTS:\n"; - for (const auto& test_name : failed_tests) + for (auto& test_name : failed_tests) { - const bool postponed = postponed_tests.count(test_name) != 0; + bool postponed = postponed_tests.count(test_name) != 0; std::cout << " " << (postponed ? "POSTPONED: " : "FAILED: ") << test_name << '\n'; } } @@ -1693,6 +1612,11 @@ int main(int argc, char* argv[]) } } + /*{ + std::cout << concolor::magenta << "Wrong arguments" << concolor::normal << std::endl; + std::cout << desc_options << std::endl; + return 2; + }*/ #ifdef _WIN32 ::Beep(1050, 1000); #endif diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index 5429c8f37..5afd49857 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -12,21 +12,14 @@ #include #include "parallel_test_runner.h" +#include "../chaingen_args.h" #include "../../src/common/command_line.h" +using namespace chaingen_args; + namespace bp = boost::process; namespace pt = boost::property_tree; -extern command_line::arg_descriptor arg_run_root; -extern command_line::arg_descriptor arg_processes; -extern command_line::arg_descriptor arg_worker_id; -extern command_line::arg_descriptor arg_generate_test_data; -extern command_line::arg_descriptor arg_play_test_data; -extern command_line::arg_descriptor arg_generate_and_play_test_data; -extern command_line::arg_descriptor arg_test_transactions; - -std::string TAKEN_TESTS_LOG_FILENAME = "taken_tests.log"; - namespace { const char* JSON_WORKER_ID = "worker_id"; @@ -43,6 +36,7 @@ namespace const char* JSON_NAME = "name"; const char* JSON_MS = "ms"; + const char* TAKEN_TESTS_LOG_FILENAME = "taken_tests.log"; const char* WORKER_REPORT_FILENAME = "coretests_report.json"; const char* WORKER_LOG_FILENAME = "worker.log"; @@ -213,7 +207,6 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process std::cout << concolor::normal << std::endl; - // If any report missing or worker failed, treat as failure if (any_missing || failed_workers != 0) return 1; @@ -231,7 +224,6 @@ void parallel_test_runner::print_worker_failure_reasons(uint32_t processes, cons worker_report rep; const bool report_ok = read_worker_report_file(i, rep); - // Skip successful workers that produced a report with exit_code 0. if (report_ok && ec == 0 && rep.exit_code == 0) continue; @@ -283,11 +275,9 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const const uint32_t processes = command_line::get_arg(m_vm, arg_processes); const int32_t worker_id = command_line::get_arg(m_vm, arg_worker_id); - // Only parent (multi-process mode, worker_id not set) spawns workers. if (processes <= 1 || worker_id >= 0) return -1; - // Do not spawn workers for these modes. if (command_line::get_arg(m_vm, command_line::arg_help)) return -1; if (command_line::get_arg(m_vm, arg_generate_test_data)) return -1; if (command_line::get_arg(m_vm, arg_play_test_data)) return -1; @@ -324,7 +314,6 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const base_args.emplace_back(run_root_abs.string()); } - // Resolve executable path and make it absolute. std::string exe = (argv && argv[0]) ? std::string(argv[0]) : std::string(); if (exe.empty()) { @@ -338,7 +327,6 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const { std::filesystem::path exe_path(exe); - // If no directory component is present, try PATH lookup first. if (exe.find('/') == std::string::npos && exe.find('\\') == std::string::npos) { boost::filesystem::path resolved = bp::search_path(exe); @@ -346,7 +334,6 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const exe_path = std::filesystem::path(resolved.string()); } - // Make absolute because workers will run with start_dir = worker directory. if (exe_path.is_relative()) exe_path = std::filesystem::absolute(exe_path); @@ -367,11 +354,9 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const std::vector worker_exit_codes(processes, -1); - // Streams for tee std::vector> kids_out(processes); std::vector> kids_err(processes); - // Tee threads std::vector tee_threads; tee_threads.reserve(static_cast(processes) * 2); @@ -407,7 +392,6 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const std::shared_ptr sink(new worker_log_sink()); sink->open(get_worker_dir_path(i) / WORKER_LOG_FILENAME); - // Tee stdout: worker -> console; capture per-test log ONLY if failed tee_threads.emplace_back([worker_dir, s = kids_out[i].get(), sink]() { std::filesystem::create_directories(worker_dir); @@ -423,7 +407,6 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const } }); - // Tee stderr: worker -> console(stderr) ONLY (no worker_stderr.log) tee_threads.emplace_back([s = kids_err[i].get(), sink]() { std::string line; @@ -464,7 +447,6 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const ++failed_workers; } - // Wait tee threads after children exit (streams will close). for (auto& t : tee_threads) if (t.joinable()) t.join(); @@ -473,14 +455,11 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const const uint64_t wall_ms = static_cast(std::chrono::duration_cast(wall_t1 - wall_t0).count()); - // Aggregated report first. const int aggregated_rc = print_aggregated_report_and_return_rc(processes, wall_ms); - // Then print diagnostics. if (failed_workers != 0) print_worker_failure_reasons(processes, worker_exit_codes); - // Any worker non-zero => fail. if (failed_workers != 0) return 1; @@ -500,11 +479,9 @@ std::string parallel_test_runner::sanitize_filename(std::string s) c = '_'; } - // Avoid empty name if (s.empty()) s = "unnamed_test"; - // Keep filenames reasonably short constexpr size_t kMax = 180; if (s.size() > kMax) s.resize(kMax); @@ -638,8 +615,16 @@ bool parallel_test_runner::write_worker_report_file(const worker_report& rep) co pt::write_json(out, root, /*pretty*/true); return true; } + catch (const std::exception& e) + { + std::lock_guard lk(cerr_mx); + std::cerr << "write_worker_report_file: exception: " << e.what() << ", worker_id=" << rep.worker_id << std::endl; + return false; + } catch (...) { + std::lock_guard lk(cerr_mx); + std::cerr << "write_worker_report_file: unknown exception, worker_id=" << rep.worker_id << std::endl; return false; } } @@ -683,8 +668,16 @@ bool parallel_test_runner::read_worker_report_file(uint32_t worker_id, worker_re return true; } + catch (const std::exception& e) + { + std::lock_guard lk(cerr_mx); + std::cerr << "read_worker_report_file: exception: " << e.what() << ", worker_id=" << worker_id << ", path=" << get_worker_report_path(worker_id).string() << std::endl; + return false; + } catch (...) { + std::lock_guard lk(cerr_mx); + std::cerr << "read_worker_report_file: unknown exception" << ", worker_id=" << worker_id << ", path=" << get_worker_report_path(worker_id).string() << std::endl; return false; } } @@ -693,3 +686,46 @@ std::filesystem::path parallel_test_runner::get_worker_log_path(uint32_t worker_ { return get_worker_dir_path(worker_id) / WORKER_LOG_FILENAME; } + +std::filesystem::path parallel_test_runner::get_taken_tests_log_path_for_this_process() const +{ + const uint32_t processes = command_line::get_arg(m_vm, arg_processes); + const int32_t worker_id = command_line::get_arg(m_vm, arg_worker_id); + + if (processes > 1 && worker_id >= 0) + { + const uint32_t wid = static_cast(worker_id); + std::filesystem::create_directories(get_worker_dir_path(wid)); + return get_worker_dir_path(wid) / TAKEN_TESTS_LOG_FILENAME; + } + + const std::string data_dir = command_line::get_arg(m_vm, command_line::arg_data_dir); + std::filesystem::create_directories(data_dir); + return std::filesystem::path(data_dir) / TAKEN_TESTS_LOG_FILENAME; +} + +void parallel_test_runner::log_test_taken_by_this_process(const std::string& test_name) const +{ + std::filesystem::path p; + + try + { + p = get_taken_tests_log_path_for_this_process(); + + std::ofstream f(p, std::ios::out | std::ios::binary | std::ios::app); + if (!f.is_open()) + return; + + f << test_name << '\n'; + } + catch (const std::exception& e) + { + std::lock_guard lk(cerr_mx); + std::cerr << "parallel_test_runner::log_test_taken_by_this_process: exception: " << e.what() << ", test=" << test_name << ", path=" << p.string() << std::endl; + } + catch (...) + { + std::lock_guard lk(cerr_mx); + std::cerr << "parallel_test_runner::log_test_taken_by_this_process: unknown exception" << ", test=" << test_name << ", path=" << p.string() << std::endl; + } +} diff --git a/tests/core_tests/parallel/parallel_test_runner.h b/tests/core_tests/parallel/parallel_test_runner.h index a853addf4..185f2249d 100644 --- a/tests/core_tests/parallel/parallel_test_runner.h +++ b/tests/core_tests/parallel/parallel_test_runner.h @@ -33,6 +33,9 @@ class parallel_test_runner std::filesystem::path get_worker_dir_path(uint32_t worker_id) const; bool write_worker_report(const worker_report& rep) const; + std::filesystem::path get_taken_tests_log_path_for_this_process() const; + void log_test_taken_by_this_process(const std::string& test_name) const; + private: const boost::program_options::variables_map& m_vm; @@ -78,3 +81,9 @@ struct worker_log_sink f.flush(); } }; + +struct test_job +{ + std::string name; + std::function run; +}; From a2e40535dfae165d6a0ae44e9d05226e755bdde2 Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Tue, 13 Jan 2026 15:57:31 +0300 Subject: [PATCH 09/28] improving the distribution of tests between processes --- tests/core_tests/chaingen_args.cpp | 2 + tests/core_tests/chaingen_args.h | 2 + tests/core_tests/chaingen_main.cpp | 193 +++++++++++++----- .../parallel/parallel_test_runner.cpp | 80 ++++++++ .../parallel/parallel_test_runner.h | 3 + 5 files changed, 227 insertions(+), 53 deletions(-) diff --git a/tests/core_tests/chaingen_args.cpp b/tests/core_tests/chaingen_args.cpp index 8171921fe..653e479ab 100644 --- a/tests/core_tests/chaingen_args.cpp +++ b/tests/core_tests/chaingen_args.cpp @@ -2,6 +2,8 @@ namespace chaingen_args { + const char* WORKERS_REPORT_FILENAME = "workers_report.json"; + command_line::arg_descriptor arg_test_data_path ("test-data-path", "", ""); command_line::arg_descriptor arg_generate_test_data ("generate-test-data", ""); command_line::arg_descriptor arg_play_test_data ("play-test-data", ""); diff --git a/tests/core_tests/chaingen_args.h b/tests/core_tests/chaingen_args.h index 34d81af2e..4cafb2413 100644 --- a/tests/core_tests/chaingen_args.h +++ b/tests/core_tests/chaingen_args.h @@ -5,6 +5,8 @@ namespace chaingen_args { + extern const char* WORKERS_REPORT_FILENAME; + extern command_line::arg_descriptor arg_test_data_path; extern command_line::arg_descriptor arg_generate_test_data; extern command_line::arg_descriptor arg_play_test_data; diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index e5ff8aa84..ecd4497c4 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -20,9 +20,6 @@ #include #include #include -#include -#include -#include #define TX_BLOBSIZE_CHECKER_LOG_FILENAME "get_object_blobsize(tx).log" @@ -412,6 +409,24 @@ bool gen_and_play_intermitted_by_blockchain_saveload(const char* const genclass_ }); \ } while (0) +#define GENERATE_AND_PLAY_INTERMITTED_BY_BLOCKCHAIN_SAVELOAD(genclass) \ + if (is_test_eligible_to_run(#genclass)) \ + { \ + const char* testname = #genclass " (BC saveload)"; \ + TIME_MEASURE_START_MS(t); \ + ++tests_count; \ + ++unique_tests_count; \ + if (!gen_and_play_intermitted_by_blockchain_saveload(testname)) \ + { \ + failed_tests.insert(testname); \ + LOCAL_ASSERT(false); \ + if (stop_on_first_fail) \ + skip_all_till_the_end = true; \ + } \ + TIME_MEASURE_FINISH_MS(t); \ + tests_running_time.push_back(std::make_pair(testname, t)); \ + } + #define GENERATE_AND_PLAY_HF(genclass, hardfork_str_mask) \ do { \ const std::string __gen_name = #genclass; \ @@ -444,24 +459,6 @@ bool gen_and_play_intermitted_by_blockchain_saveload(const char* const genclass_ } \ } while (0) -#define GENERATE_AND_PLAY_INTERMITTED_BY_BLOCKCHAIN_SAVELOAD(genclass) \ - if (is_test_eligible_to_run(#genclass)) \ - { \ - const char* testname = #genclass " (BC saveload)"; \ - TIME_MEASURE_START_MS(t); \ - ++tests_count; \ - ++unique_tests_count; \ - if (!gen_and_play_intermitted_by_blockchain_saveload(testname)) \ - { \ - failed_tests.insert(testname); \ - LOCAL_ASSERT(false); \ - if (stop_on_first_fail) \ - skip_all_till_the_end = true; \ - } \ - TIME_MEASURE_FINISH_MS(t); \ - tests_running_time.push_back(std::make_pair(testname, t)); \ - } - //#define GENERATE_AND_PLAY(genclass) GENERATE_AND_PLAY_INTERMITTED_BY_BLOCKCHAIN_SAVELOAD(genclass) #define CALL_TEST(test_name, function) \ @@ -897,6 +894,90 @@ static void log_test_taken_by_this_process(const std::string& test_name) g_runner->log_test_taken_by_this_process(test_name); } //-------------------------------------------------------------------------- +static bool read_workers_report_ms_map( + const std::filesystem::path& path, + std::unordered_map& out_ms_by_test) +{ + out_ms_by_test.clear(); + + try + { + if (!std::filesystem::exists(path)) + return false; + + pt::ptree root; + pt::read_json(path.string(), root); + + for (const auto& v : root.get_child("tests", pt::ptree())) + { + const auto& node = v.second; + const std::string name = node.get("name", ""); + const uint64_t ms = node.get("ms", 0); + if (!name.empty()) + out_ms_by_test[name] = ms; + } + + return !out_ms_by_test.empty(); + } + catch (...) + { + return false; + } +} +//-------------------------------------------------------------------------- +static std::vector build_lpt_owner_plan( + const std::vector& jobs, + uint32_t processes, + const std::unordered_map& ms_by_test) +{ + const size_t n = jobs.size(); + std::vector owner(n, 0); + + struct item_t { uint64_t ms; std::string name; size_t idx; }; + std::vector items; + items.reserve(n); + + std::vector known; + known.reserve(ms_by_test.size()); + for (const auto& kv : ms_by_test) + known.push_back(kv.second); + std::sort(known.begin(), known.end()); + const uint64_t fallback = + known.empty() ? 1 : known[known.size() / 2]; + + for (size_t i = 0; i < n; ++i) + { + const std::string& name = jobs[i].name; + auto it = ms_by_test.find(name); + const uint64_t ms = (it != ms_by_test.end() ? it->second : fallback); + items.push_back(item_t{ms, name, i}); + } + + std::sort(items.begin(), items.end(), + [](const item_t& a, const item_t& b) + { + if (a.ms != b.ms) return a.ms > b.ms; + return a.name < b.name; + }); + + std::vector load(processes, 0); + + for (const auto& it : items) + { + uint32_t best_w = 0; + for (uint32_t w = 1; w < processes; ++w) + { + if (load[w] < load[best_w]) + best_w = w; + } + + owner[it.idx] = static_cast(best_w); + load[best_w] += it.ms; + } + + return owner; +} +//-------------------------------------------------------------------------- static bool run_one_test_job(const std::string& test_name, const std::function& fn, bool& stop_on_first_fail, bool& skip_all_till_the_end, size_t& tests_count, size_t& unique_tests_count, std::set& failed_tests, std::vector>& tests_running_time) { @@ -925,36 +1006,53 @@ static bool run_one_test_job(const std::string& test_name, const std::function& failed_tests, std::vector>& tests_running_time, - const std::function& /*is_test_eligible_to_run*/, const std::function& /*is_hf_test_eligible_to_run*/) +static bool run_registered_tests(bool& stop_on_first_fail, bool& skip_all_till_the_end, size_t& tests_count, size_t& unique_tests_count, std::set& failed_tests, + std::vector>& tests_running_time, const std::function& /*is_test_eligible_to_run*/,const std::function& /*is_hf_test_eligible_to_run*/) { bool all_ok = true; const uint32_t processes = command_line::get_arg(g_vm, arg_processes); const int32_t worker_id = command_line::get_arg(g_vm, arg_worker_id); - size_t job_index = 0; + std::vector owner_plan; + bool has_plan = false; - for (auto& j : g_test_jobs) + if (processes > 1 && worker_id >= 0) + { + std::filesystem::path run_root_abs = command_line::get_arg(g_vm, arg_run_root); + if (run_root_abs.empty()) + run_root_abs = std::filesystem::path("chaingen_runs"); + if (run_root_abs.is_relative()) + run_root_abs = std::filesystem::absolute(run_root_abs); + + const auto report_path = run_root_abs / WORKERS_REPORT_FILENAME; + + std::unordered_map ms_by_test; + if (read_workers_report_ms_map(report_path, ms_by_test)) + { + owner_plan = build_lpt_owner_plan(g_test_jobs, processes, ms_by_test); + has_plan = (owner_plan.size() == g_test_jobs.size()); + } + } + + for (size_t i = 0; i < g_test_jobs.size(); ++i) { if (skip_all_till_the_end) break; if (processes > 1 && worker_id >= 0) { - if ((job_index % processes) != static_cast(worker_id)) - { - ++job_index; + const int expected_owner = has_plan + ? owner_plan[i] + : static_cast(i % processes); + + if (expected_owner != worker_id) continue; - } } - bool ok = j.run(); + bool ok = g_test_jobs[i].run(); if (!ok) all_ok = false; - - ++job_index; } return all_ok; @@ -1351,8 +1449,9 @@ int main(int argc, char* argv[]) log_space::get_set_log_detalisation_level(true, LOG_LEVEL_2); log_space::log_singletone::add_logger(LOGGER_CONSOLE, NULL, NULL, LOG_LEVEL_4); - log_space::log_singletone::add_logger(LOGGER_FILE, - log_space::log_singletone::get_default_log_file().c_str(), log_space::log_singletone::get_default_log_folder().c_str()); + log_space::log_singletone::add_logger(LOGGER_FILE, + log_space::log_singletone::get_default_log_file().c_str(), + log_space::log_singletone::get_default_log_folder().c_str()); log_space::log_singletone::enable_channels("core,currency_protocol,tx_pool,p2p,wallet", false); @@ -1519,32 +1618,20 @@ int main(int argc, char* argv[]) uint64_t total_time = 0; if (!tests_running_time.empty()) { - uint64_t max_time = std::max_element( - tests_running_time.begin(), tests_running_time.end(), - [](tests_running_time_t::value_type& lhs, tests_running_time_t::value_type& rhs) -> bool { - return lhs.second < rhs.second; - })->second; - - uint64_t max_test_name_len = std::max_element( - tests_running_time.begin(), tests_running_time.end(), - [](tests_running_time_t::value_type& lhs, tests_running_time_t::value_type& rhs) -> bool { - return lhs.first.size() < rhs.first.size(); - })->first.size(); - - const size_t bar_width = 70; + uint64_t max_time = std::max_element(tests_running_time.begin(), tests_running_time.end(), [](tests_running_time_t::value_type& lhs, tests_running_time_t::value_type& rhs)->bool { return lhs.second < rhs.second; })->second; + uint64_t max_test_name_len = std::max_element(tests_running_time.begin(), tests_running_time.end(), [](tests_running_time_t::value_type& lhs, tests_running_time_t::value_type& rhs)->bool { return lhs.first.size() < rhs.first.size(); })->first.size(); + size_t bar_width = 70; if (!is_worker) { for (const auto& i : tests_running_time) { - const bool failed = failed_tests.count(i.first) != 0; - const bool postponed = postponed_tests.count(i.first) != 0; + bool failed = failed_tests.count(i.first) != 0; + bool postponed = postponed_tests.count(i.first) != 0; if (failed && postponed) ++failed_postponed_tests_count; - std::string bar(bar_width * i.second / max_time, '#'); std::cout << (failed ? (postponed ? concolor::yellow : concolor::magenta) : concolor::green) << std::left << std::setw(max_test_name_len + 1) << i.first << "\t" << std::setw(10) << i.second << " ms \t" << bar << std::endl; - total_time += i.second; } } @@ -1560,7 +1647,7 @@ int main(int argc, char* argv[]) } if (skip_all_till_the_end && !is_worker) - std::cout << ENDL << concolor::yellow << "(execution interrupted at the first failure; not all tests were run)" << ENDL; + std::cout << ENDL << concolor::yellow << "(execution interrupted at the first failure; not all tests were run)" << ENDL; serious_failures_count = failed_tests.size() - failed_postponed_tests_count; diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index 5afd49857..22b53118e 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -207,6 +207,8 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process std::cout << concolor::normal << std::endl; + (void)write_workers_report_file(processes, reps); + if (any_missing || failed_workers != 0) return 1; @@ -729,3 +731,81 @@ void parallel_test_runner::log_test_taken_by_this_process(const std::string& tes std::cerr << "parallel_test_runner::log_test_taken_by_this_process: unknown exception" << ", test=" << test_name << ", path=" << p.string() << std::endl; } } + +std::filesystem::path parallel_test_runner::get_workers_report_path() const +{ + return get_run_root_path() / WORKERS_REPORT_FILENAME; +} + +bool parallel_test_runner::write_workers_report_file( + uint32_t processes, + const std::vector& reps) const +{ + try + { + std::unordered_map ms_by_test; + ms_by_test.reserve(8192); + + for (const auto& r : reps) + { + for (const auto& it : r.tests_running_time) + { + const std::string& name = it.first; + const uint64_t ms = it.second; + auto ins = ms_by_test.emplace(name, ms); + if (!ins.second) + ins.first->second = std::max(ins.first->second, ms); + } + } + + std::vector> tests; + tests.reserve(ms_by_test.size()); + for (auto& kv : ms_by_test) + tests.emplace_back(std::move(kv.first), kv.second); + + std::sort(tests.begin(), tests.end(), [](const auto& a, const auto& b) + { + if (a.second != b.second) return a.second > b.second; + return a.first < b.first; + } + ); + + pt::ptree root; + root.put("format", 1); + root.put("processes", processes); + + pt::ptree arr; + for (const auto& t : tests) + { + pt::ptree node; + node.put("name", t.first); + node.put("ms", t.second); + arr.push_back(std::make_pair("", node)); + } + root.add_child("tests", arr); + + std::filesystem::path path = get_workers_report_path(); + std::ofstream out(path); + if (!out.is_open()) + { + std::lock_guard lk(cerr_mx); + std::cerr << "write_workers_report_file: cannot open file: " << path.string() << std::endl; + return false; + } + + pt::write_json(out, root, /*pretty*/true); + return true; + } + catch (const std::exception& e) + { + std::lock_guard lk(cerr_mx); + std::cerr << "write_workers_report_file: exception: " << e.what() << ", path=" << get_workers_report_path().string() << std::endl; + return false; + } + catch (...) + { + std::lock_guard lk(cerr_mx); + std::cerr << "write_workers_report_file: unknown exception, path=" << get_workers_report_path().string() << std::endl; + return false; + } +} diff --git a/tests/core_tests/parallel/parallel_test_runner.h b/tests/core_tests/parallel/parallel_test_runner.h index 185f2249d..ee5b6d5e2 100644 --- a/tests/core_tests/parallel/parallel_test_runner.h +++ b/tests/core_tests/parallel/parallel_test_runner.h @@ -36,6 +36,9 @@ class parallel_test_runner std::filesystem::path get_taken_tests_log_path_for_this_process() const; void log_test_taken_by_this_process(const std::string& test_name) const; + std::filesystem::path get_workers_report_path() const; + bool write_workers_report_file(uint32_t processes, const std::vector& reps) const; + private: const boost::program_options::variables_map& m_vm; From 0e917f3cfe64a9a11b4cd1effaf838b487296afc Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Tue, 13 Jan 2026 16:04:09 +0300 Subject: [PATCH 10/28] minor fixs --- tests/core_tests/chaingen_main.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index ecd4497c4..63df2afe8 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -1448,7 +1448,7 @@ int main(int argc, char* argv[]) //set up logging options log_space::get_set_log_detalisation_level(true, LOG_LEVEL_2); log_space::log_singletone::add_logger(LOGGER_CONSOLE, NULL, NULL, LOG_LEVEL_4); - + log_space::log_singletone::add_logger(LOGGER_FILE, log_space::log_singletone::get_default_log_file().c_str(), log_space::log_singletone::get_default_log_folder().c_str()); @@ -1505,8 +1505,7 @@ int main(int argc, char* argv[]) if (command_line::has_arg(g_vm, command_line::arg_log_level)) { int new_log_level = command_line::get_arg(g_vm, command_line::arg_log_level); - if (new_log_level >= LOG_LEVEL_MIN && new_log_level <= LOG_LEVEL_MAX && - log_space::get_set_log_detalisation_level(false) != new_log_level) + if (new_log_level >= LOG_LEVEL_MIN && new_log_level <= LOG_LEVEL_MAX && log_space::get_set_log_detalisation_level(false) != new_log_level) { log_space::get_set_log_detalisation_level(true, new_log_level); LOG_PRINT_L0("LOG_LEVEL set to " << new_log_level); @@ -1571,7 +1570,6 @@ int main(int argc, char* argv[]) if (!match) return false; } - LOG_PRINT_L0("good: " << genclass_str); return true; }; @@ -1624,7 +1622,7 @@ int main(int argc, char* argv[]) if (!is_worker) { - for (const auto& i : tests_running_time) + for (auto& i : tests_running_time) { bool failed = failed_tests.count(i.first) != 0; bool postponed = postponed_tests.count(i.first) != 0; From e904a9cc8dad4668b5d08f99899a99f559acb15c Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Tue, 13 Jan 2026 22:23:40 +0300 Subject: [PATCH 11/28] improve output and write logs --- tests/core_tests/chaingen_args.cpp | 7 +- tests/core_tests/chaingen_args.h | 1 + tests/core_tests/chaingen_main.cpp | 182 ++++---- .../parallel/parallel_test_runner.cpp | 421 ++++++++---------- .../parallel/parallel_test_runner.h | 62 +-- 5 files changed, 338 insertions(+), 335 deletions(-) diff --git a/tests/core_tests/chaingen_args.cpp b/tests/core_tests/chaingen_args.cpp index 653e479ab..2455933e9 100644 --- a/tests/core_tests/chaingen_args.cpp +++ b/tests/core_tests/chaingen_args.cpp @@ -13,7 +13,8 @@ namespace chaingen_args command_line::arg_descriptor arg_run_multiple_tests ("run-multiple-tests", "comma-separated list of tests to run, OR text file <@filename> containing list of tests"); command_line::arg_descriptor arg_enable_debug_asserts ("enable-debug-asserts", "" ); command_line::arg_descriptor arg_stop_on_fail ("stop-on-fail", ""); - command_line::arg_descriptor arg_processes ("processes", "Number of worker processes", 1); - command_line::arg_descriptor arg_worker_id ("worker-id", "Internal: worker index", -1); - command_line::arg_descriptor arg_run_root ("run-root", "Internal: run root dir", ""); + command_line::arg_descriptor arg_processes ("multiprocess-run", "Run tests in parallel using the specified number of worker processes", 1); + command_line::arg_descriptor arg_worker_id ("multiprocess-worker-id", "Internal: index of the worker process (assigned automatically by parent)", -1); + command_line::arg_descriptor arg_run_root ("multiprocess-run-root", "Internal: directory used by parent and workers to store run artifacts and reports", ""); + command_line::arg_descriptor arg_shm_name ("multiprocess-shm-name", "Internal: shared memory name used to report failed tests", ""); } diff --git a/tests/core_tests/chaingen_args.h b/tests/core_tests/chaingen_args.h index 4cafb2413..d6a1c6947 100644 --- a/tests/core_tests/chaingen_args.h +++ b/tests/core_tests/chaingen_args.h @@ -19,4 +19,5 @@ namespace chaingen_args extern command_line::arg_descriptor arg_processes; extern command_line::arg_descriptor arg_worker_id; extern command_line::arg_descriptor arg_run_root; + extern command_line::arg_descriptor arg_shm_name; } diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index 63df2afe8..d15154287 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -15,18 +15,13 @@ #include "common/db_backend_selector.h" #include "parallel/parallel_test_runner.h" #include "chaingen_args.h" -#include -#include -#include #include -#include #define TX_BLOBSIZE_CHECKER_LOG_FILENAME "get_object_blobsize(tx).log" using namespace chaingen_args; namespace po = boost::program_options; -namespace bp = boost::process; namespace pt = boost::property_tree; namespace @@ -34,6 +29,8 @@ namespace boost::program_options::variables_map g_vm; std::unique_ptr g_runner; std::vector g_test_jobs; + coretests_shm::shared_state* g_shm_state = nullptr; + std::unique_ptr g_shm_region; } #define GENERATE(filename, genclass) \ @@ -387,76 +384,82 @@ bool gen_and_play_intermitted_by_blockchain_saveload(const char* const genclass_ return r; } -#define GENERATE_AND_PLAY(genclass) \ - do { \ - const std::string __test_name = #genclass; \ - auto __is_test_eligible_to_run = is_test_eligible_to_run; \ - g_test_jobs.push_back(test_job{ \ - __test_name, \ - [&, __test_name, __is_test_eligible_to_run]() -> bool { \ - if (!__is_test_eligible_to_run(__test_name)) \ - return true; \ - return run_one_test_job( \ - __test_name, \ - [&]() -> bool { return generate_and_play(__test_name.c_str()); }, \ - stop_on_first_fail, \ - skip_all_till_the_end, \ - tests_count, \ - unique_tests_count, \ - failed_tests, \ - tests_running_time); \ - } \ - }); \ +#define GENERATE_AND_PLAY(genclass) \ + do { \ + const std::string __test_name = #genclass; \ + auto __is_test_eligible_to_run = is_test_eligible_to_run; \ + g_test_jobs.push_back(test_job{ \ + __test_name, \ + [&, __test_name, __is_test_eligible_to_run]() -> bool { \ + if (!__is_test_eligible_to_run(__test_name)) \ + return true; \ + return run_one_test_job( \ + __test_name, \ + [&]() -> bool { return generate_and_play(__test_name.c_str()); }, \ + stop_on_first_fail, \ + skip_all_till_the_end, \ + tests_count, \ + unique_tests_count, \ + failed_tests, \ + tests_running_time); \ + } \ + }); \ } while (0) #define GENERATE_AND_PLAY_INTERMITTED_BY_BLOCKCHAIN_SAVELOAD(genclass) \ - if (is_test_eligible_to_run(#genclass)) \ - { \ - const char* testname = #genclass " (BC saveload)"; \ - TIME_MEASURE_START_MS(t); \ - ++tests_count; \ - ++unique_tests_count; \ - if (!gen_and_play_intermitted_by_blockchain_saveload(testname)) \ + do { \ + const std::string __test_name = std::string(#genclass) + " (BC saveload)"; \ + auto __is_test_eligible_to_run = is_test_eligible_to_run; \ + g_test_jobs.push_back(test_job{ \ + __test_name, \ + [&, __test_name, __is_test_eligible_to_run]() -> bool { \ + if (!__is_test_eligible_to_run(#genclass)) \ + return true; \ + return run_one_test_job( \ + __test_name, \ + [&]() -> bool { return gen_and_play_intermitted_by_blockchain_saveload(__test_name.c_str()); }, \ + stop_on_first_fail, \ + skip_all_till_the_end, \ + tests_count, \ + unique_tests_count, \ + failed_tests, \ + tests_running_time); \ + } \ + }); \ + } while (0) + +#define GENERATE_AND_PLAY_HF(genclass, hardfork_str_mask) \ + do { \ + const std::string __gen_name = #genclass; \ + std::vector __hardforks = parse_hardfork_str_mask(hardfork_str_mask); \ + if (__hardforks.empty()) \ { \ - failed_tests.insert(testname); \ + LOG_ERROR("invalid hardforks mask: " << hardfork_str_mask << " for test " << __gen_name); \ LOCAL_ASSERT(false); \ - if (stop_on_first_fail) \ - skip_all_till_the_end = true; \ + break; \ + } \ + auto __hf_filter = is_hf_test_eligible_to_run; \ + for (size_t __i = 0; __i < __hardforks.size(); ++__i) \ + { \ + const size_t __hf_id = __hardforks[__i]; \ + const std::string __test_name = __gen_name + " @ HF " + epee::string_tools::num_to_string_fast(__hf_id); \ + g_test_jobs.push_back(test_job{ \ + __test_name, \ + [&, __test_name, __gen_name, __hf_id, __hf_filter]() -> bool { \ + if (!__hf_filter(__gen_name, __hf_id)) \ + return true; \ + return run_one_test_job( \ + __test_name, \ + [&]() -> bool { return generate_and_play(__test_name.c_str(), __hf_id); }, \ + stop_on_first_fail, \ + skip_all_till_the_end, \ + tests_count, \ + unique_tests_count, \ + failed_tests, \ + tests_running_time); \ + } \ + }); \ } \ - TIME_MEASURE_FINISH_MS(t); \ - tests_running_time.push_back(std::make_pair(testname, t)); \ - } - -#define GENERATE_AND_PLAY_HF(genclass, hardfork_str_mask) \ - do { \ - const std::string __gen_name = #genclass; \ - std::vector __hardforks = parse_hardfork_str_mask(hardfork_str_mask); \ - if (__hardforks.empty()) { \ - LOG_ERROR("invalid hardforks mask: " << hardfork_str_mask << " for test " << __gen_name); \ - break; \ - } \ - auto __hf_filter = is_hf_test_eligible_to_run; \ - for (size_t __i = 0; __i < __hardforks.size(); ++__i) \ - { \ - const size_t __hf_id = __hardforks[__i]; \ - const std::string __test_name = __gen_name + " @ HF " + epee::string_tools::num_to_string_fast(__hf_id); \ - g_test_jobs.push_back(test_job{ \ - __test_name, \ - [&, __test_name, __gen_name, __hf_id, __hf_filter]() -> bool { \ - if (!__hf_filter(__gen_name, __hf_id)) \ - return true; \ - return run_one_test_job( \ - __test_name, \ - [&]() -> bool { return generate_and_play(__test_name.c_str(), __hf_id); }, \ - stop_on_first_fail, \ - skip_all_till_the_end, \ - tests_count, \ - unique_tests_count, \ - failed_tests, \ - tests_running_time); \ - } \ - }); \ - } \ } while (0) //#define GENERATE_AND_PLAY(genclass) GENERATE_AND_PLAY_INTERMITTED_BY_BLOCKCHAIN_SAVELOAD(genclass) @@ -894,9 +897,7 @@ static void log_test_taken_by_this_process(const std::string& test_name) g_runner->log_test_taken_by_this_process(test_name); } //-------------------------------------------------------------------------- -static bool read_workers_report_ms_map( - const std::filesystem::path& path, - std::unordered_map& out_ms_by_test) +static bool read_workers_report_ms_map(const std::filesystem::path& path, std::unordered_map& out_ms_by_test) { out_ms_by_test.clear(); @@ -925,10 +926,7 @@ static bool read_workers_report_ms_map( } } //-------------------------------------------------------------------------- -static std::vector build_lpt_owner_plan( - const std::vector& jobs, - uint32_t processes, - const std::unordered_map& ms_by_test) +static std::vector build_lpt_owner_plan(const std::vector& jobs, uint32_t processes, const std::unordered_map& ms_by_test) { const size_t n = jobs.size(); std::vector owner(n, 0); @@ -996,6 +994,12 @@ static bool run_one_test_job(const std::string& test_name, const std::function 1 && wid >= 0 && g_shm_state) + g_shm_state->record_fail(static_cast(wid), test_name.c_str()); + if (stop_on_first_fail) skip_all_till_the_end = true; } @@ -1440,6 +1444,26 @@ void fill_postponed_tests_set(std::set& postponed_tests) #undef MARK_TEST_AS_POSTPONED } //-------------------------------------------------------------------------- +static void init_shared_fail_report_if_needed() +{ + const std::string shm_name = command_line::get_arg(g_vm, chaingen_args::arg_shm_name); + if (shm_name.empty()) + return; + + namespace bip = boost::interprocess; + try + { + bip::shared_memory_object shm(bip::open_only, shm_name.c_str(), bip::read_write); + g_shm_region.reset(new bip::mapped_region(shm, bip::read_write)); + g_shm_state = static_cast(g_shm_region->get_address()); + } + catch (...) + { + g_shm_state = nullptr; + g_shm_region.reset(); + } +} +//-------------------------------------------------------------------------- int main(int argc, char* argv[]) { TRY_ENTRY(); @@ -1478,6 +1502,7 @@ int main(int argc, char* argv[]) command_line::add_arg(desc_options, arg_processes); command_line::add_arg(desc_options, arg_worker_id); command_line::add_arg(desc_options, arg_run_root); + command_line::add_arg(desc_options, arg_shm_name); currency::core::init_options(desc_options); tools::db::db_backend_selector::init_options(desc_options); @@ -1521,6 +1546,9 @@ int main(int argc, char* argv[]) const int32_t worker_id = command_line::get_arg(g_vm, arg_worker_id); const bool is_worker = (processes > 1 && worker_id >= 0); + if (is_worker) + init_shared_fail_report_if_needed(); + bool skip_all_till_the_end = false; size_t tests_count = 0; size_t unique_tests_count = 0; @@ -1545,7 +1573,7 @@ int main(int argc, char* argv[]) { epee::debug::get_set_enable_assert(true, command_line::get_arg(g_vm, arg_enable_debug_asserts)); // don't comment out this: many tests have normal-negative checks (i.e. tx with invalid amount shouldn't be created), so be ready for MANY assertion breaks - std::unordered_multimap specific_tests_to_run; + std::unordered_multimap specific_tests_to_run; // test_name -> hf; if empty, all tests should be run CHECK_AND_ASSERT_MES(parse_cmd_specific_tests_to_run(specific_tests_to_run), 1, "Error while parsing specific tests to run"); std::set postponed_tests; diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index 22b53118e..779a59f6e 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -4,7 +4,7 @@ #include "../chaingen.h" #endif -#include +#include #include #include #include @@ -39,13 +39,14 @@ namespace const char* TAKEN_TESTS_LOG_FILENAME = "taken_tests.log"; const char* WORKER_REPORT_FILENAME = "coretests_report.json"; const char* WORKER_LOG_FILENAME = "worker.log"; + const char* ARG_WORKER_ID = "--multiprocess-worker-id"; + const char* ARG_WORKER_ID_EQ = "--multiprocess-worker-id="; - const char* ARG_WORKER_ID = "--worker-id"; - const char* ARG_WORKER_ID_EQ = "--worker-id="; + const char* ARG_RUN_ROOT = "--multiprocess-run-root"; + const char* ARG_RUN_ROOT_EQ = "--multiprocess-run-root="; - const char* ARG_DATA_DIR = "--data-dir"; - const char* ARG_DATA_DIR_EQ = "--data-dir="; - const char* WORKER_DIR_PREFIX = "w"; + const char* ARG_PROCESSES = "--multiprocess-run"; + const char* ARG_PROCESSES_EQ = "--multiprocess-run="; const char* WORKER_DIR_PREFIX = "w"; std::mutex cout_mx; std::mutex cerr_mx; @@ -64,7 +65,7 @@ parallel_test_runner::parallel_test_runner( int parallel_test_runner::run_parent_if_needed(int argc, char* argv[]) const { const int rc = run_workers_and_wait(argc, argv); - return rc; + return (rc < 0) ? k_not_parent : rc; } bool parallel_test_runner::write_worker_report(const worker_report& rep) const @@ -119,9 +120,15 @@ std::vector parallel_test_runner::build_base_args_without_worker_sp continue; } - if (current_arg == ARG_DATA_DIR || arg_has_prefix(current_arg, ARG_DATA_DIR_EQ)) + if (current_arg == ARG_PROCESSES || arg_has_prefix(current_arg, ARG_PROCESSES_EQ)) + { + if (current_arg == ARG_PROCESSES && i + 1 < argc) ++i; + continue; + } + + if (current_arg == ARG_RUN_ROOT || arg_has_prefix(current_arg, ARG_RUN_ROOT_EQ)) { - if (current_arg == ARG_DATA_DIR && i + 1 < argc) ++i; + if (current_arg == ARG_RUN_ROOT && i + 1 < argc) ++i; continue; } @@ -133,7 +140,7 @@ std::vector parallel_test_runner::build_base_args_without_worker_sp void fill_postponed_tests_set(std::set& postponed_tests); -int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wall_time_ms) const +int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wall_time_ms, const coretests_shm::shared_state* shm_state) const { std::vector reps; reps.reserve(processes); @@ -168,19 +175,32 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process { total_tests_count += r.tests_count; total_unique_tests_count += r.unique_tests_count; - failed_tests_union.insert(r.failed_tests.begin(), r.failed_tests.end()); } + if (shm_state) + { + uint32_t n_fails = shm_state->write_idx.load(std::memory_order_relaxed); + if (n_fails > coretests_shm::k_max_fails) + n_fails = coretests_shm::k_max_fails; + + for (uint32_t j = 0; j < n_fails; ++j) + { + const auto& e = shm_state->entries[j]; + if (e.test_name[0] != '\0') + failed_tests_union.insert(std::string(e.test_name)); + } + } + size_t failed_postponed_tests_count = 0; for (const auto& t : failed_tests_union) if (postponed_tests.count(t) != 0) ++failed_postponed_tests_count; - const size_t serious_failures_count = failed_tests_union.size() - failed_postponed_tests_count; - - std::cout << (serious_failures_count == 0 && failed_workers == 0 && !any_missing ? concolor::green : concolor::magenta); + const size_t serious_failures_count = failed_tests_union.size() >= failed_postponed_tests_count ? (failed_tests_union.size() - failed_postponed_tests_count) : 0; + const bool ok = (!any_missing && failed_workers == 0 && serious_failures_count == 0); + std::cout << (ok ? concolor::green : concolor::magenta); std::cout << "\nREPORT (aggregated):\n"; std::cout << " Workers: " << processes << "\n"; if (any_missing) @@ -189,12 +209,10 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process std::cout << " Unique tests run: " << total_unique_tests_count << "\n"; std::cout << " Total tests run: " << total_tests_count << "\n"; - std::cout << " Failures: " << serious_failures_count - << " (postponed failures: " << failed_postponed_tests_count << ")\n"; + std::cout << " Failures: " << serious_failures_count << " (postponed failures: " << failed_postponed_tests_count << ")\n"; std::cout << " Postponed: " << postponed_tests.size() << "\n"; - std::cout << " Total time: " << (wall_time_ms / 1000) << " s. (" - << (total_tests_count > 0 ? (wall_time_ms / total_tests_count) : 0) - << " ms per test in average)\n"; + std::cout << " Total time: " << (wall_time_ms / 1000) << " s. (" << (total_tests_count > 0 ? (wall_time_ms / total_tests_count) : 0) << " ms per test in average)\n"; + if (!failed_tests_union.empty()) { std::cout << "FAILED/POSTPONED TESTS:\n"; @@ -205,10 +223,11 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process } } - std::cout << concolor::normal << std::endl; + std::cout << concolor::normal << '\n'; + + if (!reps.empty()) + (void)write_workers_report_file(processes, reps); - (void)write_workers_report_file(processes, reps); - if (any_missing || failed_workers != 0) return 1; @@ -257,18 +276,7 @@ void parallel_test_runner::print_worker_failure_reasons(uint32_t processes, cons std::cout << " failed tests:\n"; for (const auto& test_name : rep.failed_tests) - { std::cout << " - " << test_name << "\n"; - - const auto logs = find_logs_for_test(worker_dir, test_name); - if (logs.empty()) - { - std::cout << " log: (not found, expected prefix: " << sanitize_filename(test_name) << ")\n"; - continue; - } - - std::cout << " log: " << logs.front().string() << "\n"; - } } } @@ -286,6 +294,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const if (command_line::get_arg(m_vm, arg_generate_and_play_test_data)) return -1; if (command_line::get_arg(m_vm, arg_test_transactions)) return -1; + const auto t0 = std::chrono::steady_clock::now(); const std::string run_root = command_line::get_arg(m_vm, arg_run_root); std::vector base_args = build_base_args_without_worker_specific(argc, argv); @@ -297,9 +306,9 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const return false; }; - if (!has_flag("--processes", "--processes=")) + if (!has_flag(ARG_PROCESSES, ARG_PROCESSES_EQ)) { - base_args.emplace_back("--processes"); + base_args.emplace_back(ARG_PROCESSES); base_args.emplace_back(std::to_string(processes)); } @@ -310,18 +319,16 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const if (run_root_abs.is_relative()) run_root_abs = std::filesystem::absolute(run_root_abs); - if (!has_flag("--run-root", "--run-root=")) + if (!has_flag(ARG_RUN_ROOT, ARG_RUN_ROOT_EQ)) { - base_args.emplace_back("--run-root"); + base_args.emplace_back(ARG_RUN_ROOT); base_args.emplace_back(run_root_abs.string()); } std::string exe = (argv && argv[0]) ? std::string(argv[0]) : std::string(); if (exe.empty()) { - std::cout << concolor::magenta - << "Cannot spawn workers: empty executable path (argv[0])" - << concolor::normal << std::endl; + std::cout << concolor::magenta << "Cannot spawn workers: empty executable path (argv[0])" << concolor::normal << '\n'; return 1; } @@ -341,239 +348,199 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const exe = exe_path.string(); } - catch (...) {} + catch (const std::exception& e) + { + std::cout << concolor::magenta << "Failed to resolve executable path: " << e.what() << concolor::normal << '\n'; + } + catch (...) + { + std::cout << concolor::magenta << "Failed to resolve executable path: unknown error" << concolor::normal << '\n'; + } if (exe.empty() || !std::filesystem::exists(std::filesystem::path(exe))) { - std::cout << concolor::magenta - << "Cannot spawn workers: failed to resolve executable: " << exe - << concolor::normal << std::endl; + std::cout << concolor::magenta << "Cannot spawn workers: failed to resolve executable: " << exe << concolor::normal << '\n'; return 1; } - std::vector kids; - kids.reserve(processes); + const uint32_t parent_pid = +#ifdef _WIN32 + static_cast(::GetCurrentProcessId()); +#else + static_cast(::getpid()); +#endif - std::vector worker_exit_codes(processes, -1); + const std::string shm_name = "zano_coretests_fail_" + std::to_string(parent_pid); - std::vector> kids_out(processes); - std::vector> kids_err(processes); + namespace bip = boost::interprocess; - std::vector tee_threads; - tee_threads.reserve(static_cast(processes) * 2); + bip::shared_memory_object::remove(shm_name.c_str()); - const auto wall_t0 = std::chrono::steady_clock::now(); + int failed_workers = 0; + int rc = 1; - for (uint32_t i = 0; i < processes; ++i) + try { - std::vector args = base_args; - - args.emplace_back("--worker-id"); - args.emplace_back(std::to_string(i)); + bip::shared_memory_object shm(bip::create_only, shm_name.c_str(), bip::read_write); + shm.truncate(sizeof(coretests_shm::shared_state)); + bip::mapped_region region(shm, bip::read_write); - const std::string worker_data_dir = make_worker_data_dir(run_root_abs, static_cast(i)); - args.emplace_back("--data-dir"); - args.emplace_back(worker_data_dir); + auto* st = new (region.get_address()) coretests_shm::shared_state(); + st->init(); - try + if (!has_flag("--multiprocess-shm-name", std::string("--multiprocess-shm-name="))) { - const auto worker_dir = get_worker_dir_path(i); - std::filesystem::create_directories(worker_dir); + base_args.emplace_back("--multiprocess-shm-name"); + base_args.emplace_back(shm_name); + } - kids_out[i] = std::make_unique(); - kids_err[i] = std::make_unique(); + struct worker_proc + { + bp::child child; + std::unique_ptr out; + std::unique_ptr file; + std::thread reader; + uint32_t wid = 0; + }; - kids.emplace_back(bp::child( - exe, - bp::args(args), - bp::start_dir = worker_dir.string(), - bp::std_out > *kids_out[i], - bp::std_err > *kids_err[i] - )); + std::vector kids; + kids.reserve(processes); - std::shared_ptr sink(new worker_log_sink()); - sink->open(get_worker_dir_path(i) / WORKER_LOG_FILENAME); + std::mutex cout_mutex; - tee_threads.emplace_back([worker_dir, s = kids_out[i].get(), sink]() - { - std::filesystem::create_directories(worker_dir); + for (uint32_t i = 0; i < processes; ++i) + { + std::vector args = base_args; - std::string line; - while (std::getline(*s, line)) - { - sink->write_line(line); - { - std::lock_guard lk(cout_mx); - std::cout << line << std::endl; - } - } - }); + args.emplace_back(ARG_WORKER_ID); + args.emplace_back(std::to_string(i)); - tee_threads.emplace_back([s = kids_err[i].get(), sink]() - { - std::string line; - while (std::getline(*s, line)) - { - sink->write_line(std::string("[stderr] ") + line); - std::lock_guard lk(cerr_mx); - std::cerr << line << std::endl; - } - }); - } - catch (const std::exception& e) - { - std::cout << concolor::magenta - << "Failed to spawn worker " << i << ": " << e.what() - << concolor::normal << std::endl; + const std::string worker_data_dir = make_worker_data_dir(run_root_abs, static_cast(i)); + args.emplace_back("--data-dir"); + args.emplace_back(worker_data_dir); - for (auto& c : kids) - if (c.running()) - c.terminate(); - for (auto& c : kids) - c.wait(); + const auto worker_dir = get_worker_dir_path(i); + std::filesystem::create_directories(worker_dir); - for (auto& t : tee_threads) - if (t.joinable()) - t.join(); + try + { + const auto log_path = (worker_dir / WORKER_LOG_FILENAME).string(); - return 1; - } - } + worker_proc wp; + wp.wid = i; + wp.out = std::make_unique(); + wp.file = std::make_unique(log_path, std::ios::out | std::ios::app); - int failed_workers = 0; - for (uint32_t i = 0; i < kids.size(); ++i) - { - kids[i].wait(); - worker_exit_codes[i] = kids[i].exit_code(); - if (worker_exit_codes[i] != 0) - ++failed_workers; - } + if (!wp.file->is_open()) + { + std::cout << concolor::magenta << "Failed to open log file for worker " << i << ": " << log_path << concolor::normal << '\n'; + failed_workers = 1; + break; + } - for (auto& t : tee_threads) - if (t.joinable()) - t.join(); + wp.child = bp::child( + exe, + bp::args(args), + bp::start_dir = worker_dir.string(), + bp::std_out > *wp.out + ); - const auto wall_t1 = std::chrono::steady_clock::now(); - const uint64_t wall_ms = - static_cast(std::chrono::duration_cast(wall_t1 - wall_t0).count()); + wp.reader = std::thread([&, out = wp.out.get(), f = wp.file.get()]() + { + std::string line; + std::string buffer; + buffer.reserve(64 * 1024); - const int aggregated_rc = print_aggregated_report_and_return_rc(processes, wall_ms); + std::function flush = [&]() + { + if (!buffer.empty()) + { + (*f) << buffer; - if (failed_workers != 0) - print_worker_failure_reasons(processes, worker_exit_codes); + { + std::lock_guard lk(cout_mutex); + std::cout << buffer; + } - if (failed_workers != 0) - return 1; + buffer.clear(); + } + }; - return aggregated_rc; -} + while (std::getline(*out, line)) + { + buffer.append(line); + buffer.push_back('\n'); -std::string parallel_test_runner::sanitize_filename(std::string s) -{ - for (char& c : s) - { - const bool ok = - (c >= 'a' && c <= 'z') || - (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9') || - c == '.' || c == '_' || c == '-'; - if (!ok) - c = '_'; - } + if (buffer.size() >= 64 * 1024) + flush(); + } - if (s.empty()) - s = "unnamed_test"; + flush(); + }); - constexpr size_t kMax = 180; - if (s.size() > kMax) - s.resize(kMax); + kids.emplace_back(std::move(wp)); + } + catch (const std::exception& e) + { + std::cout << concolor::magenta << "Failed to spawn worker " << i << ": " << e.what() << concolor::normal << '\n'; + + for (auto& p : kids) + if (p.child.running()) + p.child.terminate(); + for (auto& p : kids) + p.child.wait(); + for (auto& p : kids) + if (p.reader.joinable()) + p.reader.join(); + + failed_workers = 1; + break; + } + } - return s; -} + for (auto& p : kids) + p.child.wait(); -bool parallel_test_runner::parse_test_name_from_header(const std::string& line, std::string& out_name) -{ - constexpr const char* kPrefix = "#TEST# >>>>"; - auto pos = line.find(kPrefix); - if (pos != 0) - return false; + for (auto& p : kids) + if (p.reader.joinable()) + p.reader.join(); - std::string rest = line.substr(std::strlen(kPrefix)); - // trim left - while (!rest.empty() && std::isspace(static_cast(rest.front()))) - rest.erase(rest.begin()); + for (const auto& p : kids) + if (p.child.exit_code() != 0) + ++failed_workers; - auto end = rest.find("<<<<"); - if (end == std::string::npos) - return false; + const auto t1 = std::chrono::steady_clock::now(); + const uint64_t wall_time_ms = + static_cast(std::chrono::duration_cast(t1 - t0).count()); - std::string name = rest.substr(0, end); - // trim right - while (!name.empty() && std::isspace(static_cast(name.back()))) - name.pop_back(); + rc = print_aggregated_report_and_return_rc(processes, wall_time_ms, st); - if (name.empty()) - return false; + if (failed_workers != 0 && rc == 0) + rc = 1; - out_name = name; - return true; -} + std::vector exit_codes; + exit_codes.reserve(kids.size()); + for (const auto& p : kids) + exit_codes.push_back(p.child.exit_code()); -std::filesystem::path parallel_test_runner::make_unique_log_path(const std::filesystem::path& dir, const std::string& base_name_no_ext) -{ - std::filesystem::path p = dir / (base_name_no_ext + ".log"); - if (!std::filesystem::exists(p)) - return p; + const uint32_t spawned = static_cast(kids.size()); - for (size_t i = 2; i < 10000; ++i) + if (failed_workers != 0 || rc != 0) + print_worker_failure_reasons(spawned, exit_codes); + } + catch (const std::exception& e) { - std::filesystem::path alt = dir / (base_name_no_ext + "_" + std::to_string(i) + ".log"); - if (!std::filesystem::exists(alt)) - return alt; + std::cout << concolor::magenta << "Cannot spawn workers: " << e.what() << concolor::normal << std::endl; + rc = 1; } - - return p; // fallback -} - -std::vector parallel_test_runner::find_logs_for_test( - const std::filesystem::path& worker_dir, - const std::string& test_name) -{ - std::vector result; - - const std::string base = sanitize_filename(test_name); - const std::string base_prefix = base + "_"; - - try + catch (...) { - for (auto& de : std::filesystem::directory_iterator(worker_dir)) - { - if (!de.is_regular_file()) - continue; - - const auto p = de.path(); - if (p.extension() != ".log") - continue; - - const std::string stem = p.stem().string(); - if (stem == base || stem.rfind(base_prefix, 0) == 0) - result.push_back(p); - } + std::cout << concolor::magenta << "Cannot spawn workers: unknown error" << concolor::normal << std::endl; + rc = 1; } - catch (...) {} - std::sort(result.begin(), result.end(), - [](const std::filesystem::path& a, const std::filesystem::path& b) - { - std::error_code ec1, ec2; - const auto ta = std::filesystem::last_write_time(a, ec1); - const auto tb = std::filesystem::last_write_time(b, ec2); - if (!ec1 && !ec2 && ta != tb) - return ta > tb; - return a.string() < b.string(); - }); - - return result; + bip::shared_memory_object::remove(shm_name.c_str()); + return rc; } bool parallel_test_runner::write_worker_report_file(const worker_report& rep) const @@ -737,9 +704,7 @@ std::filesystem::path parallel_test_runner::get_workers_report_path() const return get_run_root_path() / WORKERS_REPORT_FILENAME; } -bool parallel_test_runner::write_workers_report_file( - uint32_t processes, - const std::vector& reps) const +bool parallel_test_runner::write_workers_report_file( uint32_t processes, const std::vector& reps) const { try { diff --git a/tests/core_tests/parallel/parallel_test_runner.h b/tests/core_tests/parallel/parallel_test_runner.h index ee5b6d5e2..af102bfec 100644 --- a/tests/core_tests/parallel/parallel_test_runner.h +++ b/tests/core_tests/parallel/parallel_test_runner.h @@ -1,10 +1,44 @@ #pragma once #include +#include +#include #include #include #include +namespace coretests_shm +{ + constexpr uint32_t k_max_fails = 16384; + constexpr uint32_t k_name_max = 256; + + struct fail_entry + { + uint32_t worker_id; + char test_name[k_name_max]; + }; + + struct shared_state + { + std::atomic write_idx; + fail_entry entries[k_max_fails]; + + void init() { write_idx.store(0, std::memory_order_relaxed); } + + void record_fail(uint32_t worker_id, const char* name) + { + uint32_t idx = write_idx.fetch_add(1, std::memory_order_relaxed); + if (idx >= k_max_fails) + return; + + entries[idx].worker_id = worker_id; + + std::strncpy(entries[idx].test_name, name ? name : "", k_name_max - 1); + entries[idx].test_name[k_name_max - 1] = '\0'; + } + }; +} + class parallel_test_runner { public: @@ -50,41 +84,15 @@ class parallel_test_runner int run_workers_and_wait(int argc, char* argv[]) const; - int print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wall_time_ms) const; + int print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wall_time_ms, const coretests_shm::shared_state* shm_state) const; void print_worker_failure_reasons(uint32_t processes, const std::vector& worker_exit_codes) const; - static std::string sanitize_filename(std::string s); - static bool parse_test_name_from_header(const std::string& line, std::string& out_name); - static std::filesystem::path make_unique_log_path(const std::filesystem::path& dir, const std::string& base_name_no_ext); - static std::vector find_logs_for_test(const std::filesystem::path& worker_dir, const std::string& test_name); - bool write_worker_report_file(const worker_report& rep) const; bool read_worker_report_file(uint32_t worker_id, worker_report& rep) const; std::filesystem::path get_worker_log_path(uint32_t worker_id) const; }; -struct worker_log_sink -{ - std::mutex mx; - std::ofstream f; - - void open(const std::filesystem::path& path) - { - std::filesystem::create_directories(path.parent_path()); - f.open(path, std::ios::out | std::ios::binary | std::ios::trunc); - } - - void write_line(const std::string& line) - { - std::lock_guard lk(mx); - if (!f.is_open()) - return; - f << line << "\n"; - f.flush(); - } -}; - struct test_job { std::string name; From f8a0821906a0c423421e9f778fdf077a07ff5a3c Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Wed, 14 Jan 2026 16:20:23 +0300 Subject: [PATCH 12/28] minor fixes --- tests/core_tests/parallel/parallel_test_runner.cpp | 6 ++---- tests/core_tests/parallel/parallel_test_runner.h | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index 779a59f6e..a775aa2cd 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -46,10 +46,8 @@ namespace const char* ARG_RUN_ROOT_EQ = "--multiprocess-run-root="; const char* ARG_PROCESSES = "--multiprocess-run"; - const char* ARG_PROCESSES_EQ = "--multiprocess-run="; const char* WORKER_DIR_PREFIX = "w"; - - std::mutex cout_mx; - std::mutex cerr_mx; + const char* ARG_PROCESSES_EQ = "--multiprocess-run="; + const char* WORKER_DIR_PREFIX = "w"; bool arg_has_prefix(const std::string& arg, const std::string& prefix) { diff --git a/tests/core_tests/parallel/parallel_test_runner.h b/tests/core_tests/parallel/parallel_test_runner.h index af102bfec..0e1730148 100644 --- a/tests/core_tests/parallel/parallel_test_runner.h +++ b/tests/core_tests/parallel/parallel_test_runner.h @@ -75,6 +75,8 @@ class parallel_test_runner private: const boost::program_options::variables_map& m_vm; + mutable std::mutex cout_mx; + mutable std::mutex cerr_mx; std::filesystem::path get_run_root_path() const; std::filesystem::path get_worker_report_path(uint32_t worker_id) const; From 4d4da47773d162a24bd218261033aa3389910cd6 Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Wed, 14 Jan 2026 18:41:32 +0300 Subject: [PATCH 13/28] update worker-specific arguments --- .../parallel/parallel_test_runner.cpp | 67 ++++++++----------- 1 file changed, 27 insertions(+), 40 deletions(-) diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index a775aa2cd..730e6c9cd 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -39,14 +39,6 @@ namespace const char* TAKEN_TESTS_LOG_FILENAME = "taken_tests.log"; const char* WORKER_REPORT_FILENAME = "coretests_report.json"; const char* WORKER_LOG_FILENAME = "worker.log"; - const char* ARG_WORKER_ID = "--multiprocess-worker-id"; - const char* ARG_WORKER_ID_EQ = "--multiprocess-worker-id="; - - const char* ARG_RUN_ROOT = "--multiprocess-run-root"; - const char* ARG_RUN_ROOT_EQ = "--multiprocess-run-root="; - - const char* ARG_PROCESSES = "--multiprocess-run"; - const char* ARG_PROCESSES_EQ = "--multiprocess-run="; const char* WORKER_DIR_PREFIX = "w"; bool arg_has_prefix(const std::string& arg, const std::string& prefix) @@ -102,38 +94,33 @@ std::string parallel_test_runner::make_worker_data_dir(const std::filesystem::pa return worker_dir.string(); } +// Builds a base argument list by removing all worker-specific command-line options. +// This is needed to avoid duplicating multiprocess-related arguments when spawning +// child worker processes, while preserving all common arguments unchanged. std::vector parallel_test_runner::build_base_args_without_worker_specific(int argc, char* argv[]) const { - std::vector args; - args.reserve(static_cast(argc)); - - for (int i = 1; i < argc; ++i) - { - std::string current_arg = argv[i]; - - // Strip worker-specific args to avoid duplicates - if (current_arg == ARG_WORKER_ID || arg_has_prefix(current_arg, ARG_WORKER_ID_EQ)) - { - if (current_arg == ARG_WORKER_ID && i + 1 < argc) ++i; - continue; - } - - if (current_arg == ARG_PROCESSES || arg_has_prefix(current_arg, ARG_PROCESSES_EQ)) - { - if (current_arg == ARG_PROCESSES && i + 1 < argc) ++i; - continue; - } + static const std::set worker_specific_args = { + "--multiprocess-worker-id", "--multiprocess-run", "--multiprocess-run-root" + }; - if (current_arg == ARG_RUN_ROOT || arg_has_prefix(current_arg, ARG_RUN_ROOT_EQ)) + std::vector args; + for (int i = 1; i < argc; ++i) { - if (current_arg == ARG_RUN_ROOT && i + 1 < argc) ++i; - continue; + std::string current_arg = argv[i]; + bool skip = false; + for (const auto& wa : worker_specific_args) + { + if (current_arg == wa || current_arg.find(wa + "=") == 0) + { + if (current_arg == wa && i + 1 < argc) ++i; + skip = true; + break; + } + } + if (!skip) + args.emplace_back(std::move(current_arg)); } - - args.emplace_back(std::move(current_arg)); - } - - return args; + return args; } void fill_postponed_tests_set(std::set& postponed_tests); @@ -304,9 +291,9 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const return false; }; - if (!has_flag(ARG_PROCESSES, ARG_PROCESSES_EQ)) + if (!has_flag("--multiprocess-run", "--multiprocess-run=")) { - base_args.emplace_back(ARG_PROCESSES); + base_args.emplace_back("--multiprocess-run"); base_args.emplace_back(std::to_string(processes)); } @@ -317,9 +304,9 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const if (run_root_abs.is_relative()) run_root_abs = std::filesystem::absolute(run_root_abs); - if (!has_flag(ARG_RUN_ROOT, ARG_RUN_ROOT_EQ)) + if (!has_flag("--multiprocess-run-root", "--multiprocess-run-root=")) { - base_args.emplace_back(ARG_RUN_ROOT); + base_args.emplace_back("--multiprocess-run-root"); base_args.emplace_back(run_root_abs.string()); } @@ -410,7 +397,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const { std::vector args = base_args; - args.emplace_back(ARG_WORKER_ID); + args.emplace_back("--multiprocess-worker-id"); args.emplace_back(std::to_string(i)); const std::string worker_data_dir = make_worker_data_dir(run_root_abs, static_cast(i)); From 5fd4c8930361848d1f3b5a977538b353ab9be3ed Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Wed, 14 Jan 2026 20:52:47 +0300 Subject: [PATCH 14/28] fix comments --- tests/core_tests/parallel/parallel_test_runner.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index 730e6c9cd..42fc9e6f5 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -94,9 +94,7 @@ std::string parallel_test_runner::make_worker_data_dir(const std::filesystem::pa return worker_dir.string(); } -// Builds a base argument list by removing all worker-specific command-line options. -// This is needed to avoid duplicating multiprocess-related arguments when spawning -// child worker processes, while preserving all common arguments unchanged. +// Removes worker-specific arguments from the command line std::vector parallel_test_runner::build_base_args_without_worker_specific(int argc, char* argv[]) const { static const std::set worker_specific_args = { From 1da61b4ac656af4f0000e53c006cfe1021609b2a Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Wed, 14 Jan 2026 21:27:33 +0300 Subject: [PATCH 15/28] update check into generate and play hf --- tests/core_tests/chaingen_main.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index d15154287..04837bc44 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -432,12 +432,8 @@ bool gen_and_play_intermitted_by_blockchain_saveload(const char* const genclass_ do { \ const std::string __gen_name = #genclass; \ std::vector __hardforks = parse_hardfork_str_mask(hardfork_str_mask); \ - if (__hardforks.empty()) \ - { \ - LOG_ERROR("invalid hardforks mask: " << hardfork_str_mask << " for test " << __gen_name); \ - LOCAL_ASSERT(false); \ - break; \ - } \ + CHECK_AND_ASSERT_MES_CUSTOM(!__hardforks.empty(), /*fail_ret_val=*/, \ + /*custom_code=*/skip_all_till_the_end = true, "invalid hardforks mask: " << hardfork_str_mask << " for test " << __gen_name); \ auto __hf_filter = is_hf_test_eligible_to_run; \ for (size_t __i = 0; __i < __hardforks.size(); ++__i) \ { \ From 29a614c5674ee29d1e686253614974858f7a228d Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Thu, 15 Jan 2026 04:33:03 +0300 Subject: [PATCH 16/28] improving the distribution of tests --- tests/core_tests/chaingen_main.cpp | 162 +++------ .../parallel/parallel_test_runner.cpp | 317 ++++++++++-------- .../parallel/parallel_test_runner.h | 121 +++++-- 3 files changed, 311 insertions(+), 289 deletions(-) diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index 04837bc44..34d9fa930 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -887,91 +887,6 @@ bool parse_cmd_specific_tests_to_run(std::unordered_multimaplog_test_taken_by_this_process(test_name); -} -//-------------------------------------------------------------------------- -static bool read_workers_report_ms_map(const std::filesystem::path& path, std::unordered_map& out_ms_by_test) -{ - out_ms_by_test.clear(); - - try - { - if (!std::filesystem::exists(path)) - return false; - - pt::ptree root; - pt::read_json(path.string(), root); - - for (const auto& v : root.get_child("tests", pt::ptree())) - { - const auto& node = v.second; - const std::string name = node.get("name", ""); - const uint64_t ms = node.get("ms", 0); - if (!name.empty()) - out_ms_by_test[name] = ms; - } - - return !out_ms_by_test.empty(); - } - catch (...) - { - return false; - } -} -//-------------------------------------------------------------------------- -static std::vector build_lpt_owner_plan(const std::vector& jobs, uint32_t processes, const std::unordered_map& ms_by_test) -{ - const size_t n = jobs.size(); - std::vector owner(n, 0); - - struct item_t { uint64_t ms; std::string name; size_t idx; }; - std::vector items; - items.reserve(n); - - std::vector known; - known.reserve(ms_by_test.size()); - for (const auto& kv : ms_by_test) - known.push_back(kv.second); - std::sort(known.begin(), known.end()); - const uint64_t fallback = - known.empty() ? 1 : known[known.size() / 2]; - - for (size_t i = 0; i < n; ++i) - { - const std::string& name = jobs[i].name; - auto it = ms_by_test.find(name); - const uint64_t ms = (it != ms_by_test.end() ? it->second : fallback); - items.push_back(item_t{ms, name, i}); - } - - std::sort(items.begin(), items.end(), - [](const item_t& a, const item_t& b) - { - if (a.ms != b.ms) return a.ms > b.ms; - return a.name < b.name; - }); - - std::vector load(processes, 0); - - for (const auto& it : items) - { - uint32_t best_w = 0; - for (uint32_t w = 1; w < processes; ++w) - { - if (load[w] < load[best_w]) - best_w = w; - } - - owner[it.idx] = static_cast(best_w); - load[best_w] += it.ms; - } - - return owner; -} -//-------------------------------------------------------------------------- static bool run_one_test_job(const std::string& test_name, const std::function& fn, bool& stop_on_first_fail, bool& skip_all_till_the_end, size_t& tests_count, size_t& unique_tests_count, std::set& failed_tests, std::vector>& tests_running_time) { @@ -983,9 +898,10 @@ static bool run_one_test_job(const std::string& test_name, const std::functionlog_test_taken_by_this_process(test_name); + bool ok = fn(); if (!ok) { failed_tests.insert(test_name); @@ -1011,58 +927,56 @@ static bool run_registered_tests(bool& stop_on_first_fail, bool& skip_all_till_t { bool all_ok = true; - const uint32_t processes = command_line::get_arg(g_vm, arg_processes); const int32_t worker_id = command_line::get_arg(g_vm, arg_worker_id); + const bool is_worker = (worker_id >= 0); - std::vector owner_plan; - bool has_plan = false; - - if (processes > 1 && worker_id >= 0) + if (!is_worker) { - std::filesystem::path run_root_abs = command_line::get_arg(g_vm, arg_run_root); - if (run_root_abs.empty()) - run_root_abs = std::filesystem::path("chaingen_runs"); - if (run_root_abs.is_relative()) - run_root_abs = std::filesystem::absolute(run_root_abs); - - const auto report_path = run_root_abs / WORKERS_REPORT_FILENAME; - - std::unordered_map ms_by_test; - if (read_workers_report_ms_map(report_path, ms_by_test)) + for (size_t i = 0; i < g_test_jobs.size() && !skip_all_till_the_end; ++i) { - owner_plan = build_lpt_owner_plan(g_test_jobs, processes, ms_by_test); - has_plan = (owner_plan.size() == g_test_jobs.size()); + const bool ok = g_test_jobs[i].run(); + all_ok = all_ok && ok; } + return all_ok; } - for (size_t i = 0; i < g_test_jobs.size(); ++i) + if (!g_shm_state) + return all_ok; + + while (!skip_all_till_the_end) { - if (skip_all_till_the_end) + if (g_shm_state->stop_all.load(std::memory_order_acquire)) break; - if (processes > 1 && worker_id >= 0) - { - const int expected_owner = has_plan - ? owner_plan[i] - : static_cast(i % processes); + uint32_t job_idx = 0; + if (!g_shm_state->try_take_next(job_idx)) + break; - if (expected_owner != worker_id) - continue; - } + if (job_idx >= g_test_jobs.size()) + continue; + + const bool ok = g_test_jobs[job_idx].run(); + if (ok) + continue; + + all_ok = false; - bool ok = g_test_jobs[i].run(); - if (!ok) - all_ok = false; + if (stop_on_first_fail) + { + skip_all_till_the_end = true; + g_shm_state->stop_all.store(true, std::memory_order_release); + break; + } } return all_ok; } //-------------------------------------------------------------------------- +// parameters are used inside macros static void register_all_tests(bool& stop_on_first_fail, bool& skip_all_till_the_end, size_t& tests_count, size_t& unique_tests_count, std::set& failed_tests, std::vector>& tests_running_time, std::function is_test_eligible_to_run, std::function is_hf_test_eligible_to_run) { - g_test_jobs.clear(); // TODO // GENERATE_AND_PLAY(wallet_spend_form_auditable_and_track); @@ -1513,9 +1427,6 @@ int main(int argc, char* argv[]) return 1; g_runner = std::make_unique(g_vm); - const int parent_rc = g_runner->run_parent_if_needed(argc, argv); - if (parent_rc != parallel_test_runner::k_not_parent) - return parent_rc; if (command_line::get_arg(g_vm, command_line::arg_help)) { @@ -1538,9 +1449,8 @@ int main(int argc, char* argv[]) stop_on_first_fail = command_line::get_arg(g_vm, arg_stop_on_fail); } - const uint32_t processes = command_line::get_arg(g_vm, arg_processes); const int32_t worker_id = command_line::get_arg(g_vm, arg_worker_id); - const bool is_worker = (processes > 1 && worker_id >= 0); + const bool is_worker = (worker_id >= 0); if (is_worker) init_shared_fail_report_if_needed(); @@ -1625,6 +1535,10 @@ int main(int argc, char* argv[]) is_test_eligible_to_run, is_hf_test_eligible_to_run); + const int parent_rc = g_runner->run_parent_if_needed(argc, argv, g_test_jobs); + if (parent_rc != parallel_test_runner::k_not_parent) + return parent_rc; + // run run_registered_tests( stop_on_first_fail, @@ -1677,7 +1591,7 @@ int main(int argc, char* argv[]) { parallel_test_runner::worker_report rep; rep.worker_id = static_cast(worker_id); - rep.processes = processes; + rep.processes = command_line::get_arg(g_vm, chaingen_args::arg_processes); rep.tests_count = tests_count; rep.unique_tests_count = unique_tests_count; rep.total_time_ms = total_time; diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index 42fc9e6f5..ce28a2112 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include @@ -16,45 +15,16 @@ #include "../../src/common/command_line.h" using namespace chaingen_args; - namespace bp = boost::process; -namespace pt = boost::property_tree; - -namespace -{ - const char* JSON_WORKER_ID = "worker_id"; - const char* JSON_PROCESSES = "processes"; - const char* JSON_TESTS_COUNT = "tests_count"; - const char* JSON_UNIQUE_TESTS_COUNT = "unique_tests_count"; - const char* JSON_TOTAL_TIME_MS = "total_time_ms"; - const char* JSON_SKIP_ALL_TILL_END = "skip_all_till_the_end"; - const char* JSON_EXIT_CODE = "exit_code"; - - const char* JSON_FAILED_TESTS = "failed_tests"; - const char* JSON_TESTS_RUNNING_TIME = "tests_running_time"; - - const char* JSON_NAME = "name"; - const char* JSON_MS = "ms"; - - const char* TAKEN_TESTS_LOG_FILENAME = "taken_tests.log"; - const char* WORKER_REPORT_FILENAME = "coretests_report.json"; - const char* WORKER_LOG_FILENAME = "worker.log"; - const char* WORKER_DIR_PREFIX = "w"; - - bool arg_has_prefix(const std::string& arg, const std::string& prefix) - { - return arg.size() >= prefix.size() && arg.compare(0, prefix.size(), prefix) == 0; - } -} parallel_test_runner::parallel_test_runner( const boost::program_options::variables_map& vm) : m_vm(vm) {} -int parallel_test_runner::run_parent_if_needed(int argc, char* argv[]) const +int parallel_test_runner::run_parent_if_needed(int argc, char* argv[], const std::vector& g_test_jobs) const { - const int rc = run_workers_and_wait(argc, argv); + const int rc = run_workers_and_wait(argc, argv, g_test_jobs); return (rc < 0) ? k_not_parent : rc; } @@ -67,7 +37,7 @@ std::filesystem::path parallel_test_runner::get_run_root_path() const { std::string run_root = command_line::get_arg(m_vm, arg_run_root); if (run_root.empty()) - run_root = "chaingen_runs"; + run_root = paths::default_run_root; std::filesystem::path run_root_path(run_root); @@ -79,17 +49,17 @@ std::filesystem::path parallel_test_runner::get_run_root_path() const std::filesystem::path parallel_test_runner::get_worker_dir_path(uint32_t worker_id) const { - return get_run_root_path() / (WORKER_DIR_PREFIX+ std::to_string(worker_id)); + return get_run_root_path() / (paths::worker_dir_prefix + std::to_string(worker_id)); } std::filesystem::path parallel_test_runner::get_worker_report_path(uint32_t worker_id) const { - return get_worker_dir_path(worker_id) / WORKER_REPORT_FILENAME; + return get_worker_dir_path(worker_id) / files::worker_report; } std::string parallel_test_runner::make_worker_data_dir(const std::filesystem::path& run_root_abs, int worker_id) const { - std::filesystem::path worker_dir = run_root_abs / (std::string(WORKER_DIR_PREFIX) + std::to_string(worker_id)); + std::filesystem::path worker_dir = run_root_abs / (std::string(paths::worker_dir_prefix) + std::to_string(worker_id)); std::filesystem::create_directories(worker_dir); return worker_dir.string(); } @@ -97,28 +67,29 @@ std::string parallel_test_runner::make_worker_data_dir(const std::filesystem::pa // Removes worker-specific arguments from the command line std::vector parallel_test_runner::build_base_args_without_worker_specific(int argc, char* argv[]) const { - static const std::set worker_specific_args = { - "--multiprocess-worker-id", "--multiprocess-run", "--multiprocess-run-root" - }; + static const std::set worker_specific_args = { + cli_args::multiprocess_worker_id, cli_args::multiprocess_run, cli_args::multiprocess_run_root, + cli_args::multiprocess_shm_name, cli_args::data_dir, + }; - std::vector args; - for (int i = 1; i < argc; ++i) + std::vector args; + for (int i = 1; i < argc; ++i) + { + std::string current_arg = argv[i]; + bool skip = false; + for (const auto& wa : worker_specific_args) { - std::string current_arg = argv[i]; - bool skip = false; - for (const auto& wa : worker_specific_args) - { - if (current_arg == wa || current_arg.find(wa + "=") == 0) - { - if (current_arg == wa && i + 1 < argc) ++i; - skip = true; - break; - } - } - if (!skip) - args.emplace_back(std::move(current_arg)); + if (current_arg == wa || current_arg.find(wa + "=") == 0) + { + if (current_arg == wa && i + 1 < argc) ++i; + skip = true; + break; + } } - return args; + if (!skip) + args.emplace_back(std::move(current_arg)); + } + return args; } void fill_postponed_tests_set(std::set& postponed_tests); @@ -142,13 +113,6 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process reps.push_back(std::move(r)); } - for (const auto& r : reps) - if (r.exit_code != 0) - ++failed_workers; - - std::set postponed_tests; - fill_postponed_tests_set(postponed_tests); - size_t total_tests_count = 0; size_t total_unique_tests_count = 0; @@ -156,11 +120,17 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process for (const auto& r : reps) { + if (r.exit_code != 0) + ++failed_workers; + total_tests_count += r.tests_count; total_unique_tests_count += r.unique_tests_count; failed_tests_union.insert(r.failed_tests.begin(), r.failed_tests.end()); } + std::set postponed_tests; + fill_postponed_tests_set(postponed_tests); + if (shm_state) { uint32_t n_fails = shm_state->write_idx.load(std::memory_order_relaxed); @@ -169,7 +139,7 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process for (uint32_t j = 0; j < n_fails; ++j) { - const auto& e = shm_state->entries[j]; + const coretests_shm::fail_entry& e = shm_state->entries[j]; if (e.test_name[0] != '\0') failed_tests_union.insert(std::string(e.test_name)); } @@ -237,9 +207,9 @@ void parallel_test_runner::print_worker_failure_reasons(uint32_t processes, cons any = true; } - const auto worker_dir = get_worker_dir_path(i); - const auto report_path = get_worker_report_path(i); - const auto log_path = worker_dir / WORKER_LOG_FILENAME; + const std::filesystem::path worker_dir = get_worker_dir_path(i); + const std::filesystem::path report_path = get_worker_report_path(i); + const std::filesystem::path log_path = worker_dir / files::worker_log; std::cout << " report: " << (std::filesystem::exists(report_path) ? report_path.string() : ("missing (" + report_path.string() + ")")) << "\n"; std::cout << " logs dir: " << worker_dir.string() << "\n"; @@ -263,12 +233,12 @@ void parallel_test_runner::print_worker_failure_reasons(uint32_t processes, cons } } -int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const +int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std::vector& g_test_jobs) const { const uint32_t processes = command_line::get_arg(m_vm, arg_processes); const int32_t worker_id = command_line::get_arg(m_vm, arg_worker_id); - if (processes <= 1 || worker_id >= 0) + if (worker_id >= 0) return -1; if (command_line::get_arg(m_vm, command_line::arg_help)) return -1; @@ -277,36 +247,19 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const if (command_line::get_arg(m_vm, arg_generate_and_play_test_data)) return -1; if (command_line::get_arg(m_vm, arg_test_transactions)) return -1; - const auto t0 = std::chrono::steady_clock::now(); + const auto wall_start = std::chrono::steady_clock::now(); const std::string run_root = command_line::get_arg(m_vm, arg_run_root); std::vector base_args = build_base_args_without_worker_specific(argc, argv); - auto has_flag = [&](const char* flag, const std::string& prefix) -> bool - { - for (const auto& a : base_args) - if (a == flag || arg_has_prefix(a, prefix)) - return true; - return false; - }; - - if (!has_flag("--multiprocess-run", "--multiprocess-run=")) - { - base_args.emplace_back("--multiprocess-run"); - base_args.emplace_back(std::to_string(processes)); - } - std::filesystem::path run_root_abs = run_root.empty() - ? std::filesystem::path("chaingen_runs") + ? std::filesystem::path(paths::default_run_root) : std::filesystem::path(run_root); if (run_root_abs.is_relative()) run_root_abs = std::filesystem::absolute(run_root_abs); - if (!has_flag("--multiprocess-run-root", "--multiprocess-run-root=")) - { - base_args.emplace_back("--multiprocess-run-root"); - base_args.emplace_back(run_root_abs.string()); - } + base_args.emplace_back(cli_args::multiprocess_run_root); + base_args.emplace_back(run_root_abs.string()); std::string exe = (argv && argv[0]) ? std::string(argv[0]) : std::string(); if (exe.empty()) @@ -368,14 +321,11 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const shm.truncate(sizeof(coretests_shm::shared_state)); bip::mapped_region region(shm, bip::read_write); - auto* st = new (region.get_address()) coretests_shm::shared_state(); + coretests_shm::shared_state* st = new (region.get_address()) coretests_shm::shared_state(); st->init(); - if (!has_flag("--multiprocess-shm-name", std::string("--multiprocess-shm-name="))) - { - base_args.emplace_back("--multiprocess-shm-name"); - base_args.emplace_back(shm_name); - } + base_args.emplace_back(cli_args::multiprocess_shm_name); + base_args.emplace_back(shm_name); struct worker_proc { @@ -388,26 +338,26 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const std::vector kids; kids.reserve(processes); - std::mutex cout_mutex; + fill_shm_work_order(st, g_test_jobs); for (uint32_t i = 0; i < processes; ++i) { std::vector args = base_args; - args.emplace_back("--multiprocess-worker-id"); + args.emplace_back(cli_args::multiprocess_worker_id); args.emplace_back(std::to_string(i)); const std::string worker_data_dir = make_worker_data_dir(run_root_abs, static_cast(i)); - args.emplace_back("--data-dir"); + args.emplace_back(cli_args::data_dir); args.emplace_back(worker_data_dir); - const auto worker_dir = get_worker_dir_path(i); + const std::filesystem::path worker_dir = get_worker_dir_path(i); std::filesystem::create_directories(worker_dir); try { - const auto log_path = (worker_dir / WORKER_LOG_FILENAME).string(); + const std::filesystem::path log_path = (worker_dir / files::worker_log).string(); worker_proc wp; wp.wid = i; @@ -492,9 +442,8 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const if (p.child.exit_code() != 0) ++failed_workers; - const auto t1 = std::chrono::steady_clock::now(); - const uint64_t wall_time_ms = - static_cast(std::chrono::duration_cast(t1 - t0).count()); + const auto wall_end = std::chrono::steady_clock::now(); + const uint64_t wall_time_ms = static_cast(std::chrono::duration_cast(wall_end - wall_start).count()); rc = print_aggregated_report_and_return_rc(processes, wall_time_ms, st); @@ -526,20 +475,34 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[]) const return rc; } +void parallel_test_runner::fill_worker_report_header(pt::ptree& root, const worker_report& rep) const { + root.put(worker_report_json::worker_id, rep.worker_id); + root.put(worker_report_json::processes, rep.processes); + root.put(worker_report_json::tests_count, static_cast(rep.tests_count)); + root.put(worker_report_json::unique_tests_count, static_cast(rep.unique_tests_count)); + root.put(worker_report_json::total_time_ms, rep.total_time_ms); + root.put(worker_report_json::skip_all_till_end, rep.skip_all_till_the_end); + root.put(worker_report_json::exit_code, rep.exit_code); +} + +void parallel_test_runner::read_worker_report_header(const pt::ptree& root, uint32_t worker_id_fallback, worker_report& rep) const +{ + rep.worker_id = root.get(worker_report_json::worker_id, worker_id_fallback); + rep.processes = root.get(worker_report_json::processes, 1); + rep.tests_count = static_cast(root.get(worker_report_json::tests_count, 0)); + rep.unique_tests_count = static_cast(root.get(worker_report_json::unique_tests_count, 0)); + rep.total_time_ms = root.get(worker_report_json::total_time_ms, 0); + rep.skip_all_till_the_end = root.get(worker_report_json::skip_all_till_end, false); + rep.exit_code = root.get(worker_report_json::exit_code, 1); +} + bool parallel_test_runner::write_worker_report_file(const worker_report& rep) const { try { - std::filesystem::create_directories(get_worker_dir_path(rep.worker_id)); - pt::ptree root; - root.put("worker_id", rep.worker_id); - root.put("processes", rep.processes); - root.put("tests_count", static_cast(rep.tests_count)); - root.put("unique_tests_count", static_cast(rep.unique_tests_count)); - root.put("total_time_ms", rep.total_time_ms); - root.put("skip_all_till_the_end", rep.skip_all_till_the_end); - root.put("exit_code", rep.exit_code); + std::filesystem::create_directories(get_worker_dir_path(rep.worker_id)); + fill_worker_report_header(root, rep); pt::ptree failed_arr; for (const auto& s : rep.failed_tests) @@ -548,17 +511,17 @@ bool parallel_test_runner::write_worker_report_file(const worker_report& rep) co v.put("", s); failed_arr.push_back(std::make_pair("", v)); } - root.add_child("failed_tests", failed_arr); + root.add_child(worker_report_json::failed_tests, failed_arr); pt::ptree times_arr; for (const auto& it : rep.tests_running_time) { pt::ptree node; - node.put("name", it.first); - node.put("ms", it.second); + node.put(worker_report_json::name, it.first); + node.put(worker_report_json::ms, it.second); times_arr.push_back(std::make_pair("", node)); } - root.add_child("tests_running_time", times_arr); + root.add_child(worker_report_json::tests_running_time, times_arr); std::ofstream out(get_worker_report_path(rep.worker_id)); if (!out.is_open()) @@ -585,35 +548,26 @@ bool parallel_test_runner::read_worker_report_file(uint32_t worker_id, worker_re { try { - const auto path = get_worker_report_path(worker_id); + const std::filesystem::path path = get_worker_report_path(worker_id); if (!std::filesystem::exists(path)) return false; pt::ptree root; pt::read_json(path.string(), root); + read_worker_report_header(root, worker_id, rep); - rep.worker_id = root.get(JSON_WORKER_ID, worker_id); - rep.processes = root.get(JSON_PROCESSES, 1); - - rep.tests_count = static_cast(root.get(JSON_TESTS_COUNT, 0)); - rep.unique_tests_count = static_cast(root.get(JSON_UNIQUE_TESTS_COUNT, 0)); - rep.total_time_ms = root.get(JSON_TOTAL_TIME_MS, 0); - - rep.skip_all_till_the_end = root.get(JSON_SKIP_ALL_TILL_END, false); - rep.exit_code = root.get(JSON_EXIT_CODE, 1); - - for (const auto& v : root.get_child(JSON_FAILED_TESTS, pt::ptree())) + for (const auto& v : root.get_child(worker_report_json::failed_tests, pt::ptree())) { const std::string name = v.second.get_value(); if (!name.empty()) rep.failed_tests.insert(name); } - for (const auto& v : root.get_child(JSON_TESTS_RUNNING_TIME, pt::ptree())) + for (const auto& v : root.get_child(worker_report_json::tests_running_time, pt::ptree())) { const auto& node = v.second; - const std::string name = node.get(JSON_NAME, ""); - const uint64_t ms = node.get(JSON_MS, 0); + const std::string name = node.get(worker_report_json::name, ""); + const uint64_t ms = node.get(worker_report_json::ms, 0); if (!name.empty()) rep.tests_running_time.emplace_back(name, ms); } @@ -636,7 +590,7 @@ bool parallel_test_runner::read_worker_report_file(uint32_t worker_id, worker_re std::filesystem::path parallel_test_runner::get_worker_log_path(uint32_t worker_id) const { - return get_worker_dir_path(worker_id) / WORKER_LOG_FILENAME; + return get_worker_dir_path(worker_id) / files::worker_log; } std::filesystem::path parallel_test_runner::get_taken_tests_log_path_for_this_process() const @@ -648,12 +602,12 @@ std::filesystem::path parallel_test_runner::get_taken_tests_log_path_for_this_pr { const uint32_t wid = static_cast(worker_id); std::filesystem::create_directories(get_worker_dir_path(wid)); - return get_worker_dir_path(wid) / TAKEN_TESTS_LOG_FILENAME; + return get_worker_dir_path(wid) / files::taken_tests_log; } const std::string data_dir = command_line::get_arg(m_vm, command_line::arg_data_dir); std::filesystem::create_directories(data_dir); - return std::filesystem::path(data_dir) / TAKEN_TESTS_LOG_FILENAME; + return std::filesystem::path(data_dir) / files::taken_tests_log; } void parallel_test_runner::log_test_taken_by_this_process(const std::string& test_name) const @@ -719,18 +673,18 @@ bool parallel_test_runner::write_workers_report_file( uint32_t processes, const ); pt::ptree root; - root.put("format", 1); - root.put("processes", processes); + root.put(worker_report_json::format, 1); + root.put(worker_report_json::processes, processes); pt::ptree arr; for (const auto& t : tests) { pt::ptree node; - node.put("name", t.first); - node.put("ms", t.second); + node.put(worker_report_json::name, t.first); + node.put(worker_report_json::ms, t.second); arr.push_back(std::make_pair("", node)); } - root.add_child("tests", arr); + root.add_child(worker_report_json::tests, arr); std::filesystem::path path = get_workers_report_path(); std::ofstream out(path); @@ -757,3 +711,88 @@ bool parallel_test_runner::write_workers_report_file( uint32_t processes, const return false; } } + +bool parallel_test_runner::read_workers_report_ms_map(const std::filesystem::path& path, std::unordered_map& out_ms_by_test) const +{ + out_ms_by_test.clear(); + + try + { + if (!std::filesystem::exists(path)) + return false; + + pt::ptree root; + pt::read_json(path.string(), root); + + for (const auto& v : root.get_child("tests", pt::ptree())) + { + const auto& node = v.second; + const std::string name = node.get("name", ""); + const uint64_t ms = node.get("ms", 0); + if (!name.empty()) + out_ms_by_test[name] = ms; + } + + return !out_ms_by_test.empty(); + } + catch (...) + { + return false; + } +} + +bool parallel_test_runner::fill_shm_work_order(coretests_shm::shared_state* st, const std::vector& jobs) const +{ + if (!st) + return false; + + std::unordered_map ms_by_test; + const std::filesystem::path report_path = get_workers_report_path(); + (void)read_workers_report_ms_map(report_path, ms_by_test); + + struct item_t { uint64_t ms; uint32_t idx; }; + std::vector items; + items.reserve(jobs.size()); + + uint64_t fallback = 1; + if (!ms_by_test.empty()) + { + std::vector known; + known.reserve(ms_by_test.size()); + for (const auto& kv : ms_by_test) known.push_back(kv.second); + std::sort(known.begin(), known.end()); + fallback = known[known.size() / 2]; + } + + for (uint32_t i = 0; i < jobs.size(); ++i) + { + const auto it = ms_by_test.find(jobs[i].name); + const uint64_t ms = (it != ms_by_test.end() ? it->second : fallback); + items.push_back(item_t{ms, i}); + } + + std::sort(items.begin(), items.end(), [](const item_t& a, const item_t& b) { + if (a.ms != b.ms) return a.ms > b.ms; + return a.idx < b.idx; + }); + + const uint32_t total = static_cast(items.size()); + if (total > coretests_shm::k_max_tests) + { + st->tests_total.store(coretests_shm::k_max_tests, std::memory_order_release); + st->next_pos.store(0, std::memory_order_release); + st->stop_all.store(false, std::memory_order_release); + for (uint32_t i = 0; i < coretests_shm::k_max_tests; ++i) + st->order[i] = i; + return false; + } + + st->tests_total.store(total, std::memory_order_release); + st->next_pos.store(0, std::memory_order_release); + st->stop_all.store(false, std::memory_order_release); + + for (uint32_t pos = 0; pos < total; ++pos) + st->order[pos] = items[pos].idx; + + return true; +} diff --git a/tests/core_tests/parallel/parallel_test_runner.h b/tests/core_tests/parallel/parallel_test_runner.h index 0e1730148..b1dacd031 100644 --- a/tests/core_tests/parallel/parallel_test_runner.h +++ b/tests/core_tests/parallel/parallel_test_runner.h @@ -3,13 +3,23 @@ #include #include #include +#include #include #include #include +namespace pt = boost::property_tree; + +struct test_job +{ + std::string name; + std::function run; +}; + namespace coretests_shm { - constexpr uint32_t k_max_fails = 16384; + constexpr uint32_t k_max_fails = 4096; + constexpr uint32_t k_max_tests = 20000; constexpr uint32_t k_name_max = 256; struct fail_entry @@ -20,21 +30,45 @@ namespace coretests_shm struct shared_state { - std::atomic write_idx; + // fail reporting + std::atomic write_idx{0}; fail_entry entries[k_max_fails]; - void init() { write_idx.store(0, std::memory_order_relaxed); } + // work distribution + std::atomic next_pos{0}; + std::atomic tests_total{0}; + std::atomic stop_all{false}; + + uint32_t order[k_max_tests]; - void record_fail(uint32_t worker_id, const char* name) + void record_fail(uint32_t wid, const char* name) { - uint32_t idx = write_idx.fetch_add(1, std::memory_order_relaxed); - if (idx >= k_max_fails) + const uint32_t pos = write_idx.fetch_add(1, std::memory_order_acq_rel); + if (pos >= k_max_fails) return; - entries[idx].worker_id = worker_id; + entries[pos].worker_id = wid; + std::strncpy(entries[pos].test_name, name ? name : "", k_name_max - 1); + entries[pos].test_name[k_name_max - 1] = '\0'; + } - std::strncpy(entries[idx].test_name, name ? name : "", k_name_max - 1); - entries[idx].test_name[k_name_max - 1] = '\0'; + void init() + { + write_idx.store(0, std::memory_order_relaxed); + next_pos.store(0, std::memory_order_relaxed); + tests_total.store(0, std::memory_order_relaxed); + stop_all.store(false, std::memory_order_relaxed); + } + + bool try_take_next(uint32_t& out_test_idx) + { + const uint32_t total = tests_total.load(std::memory_order_acquire); + const uint32_t pos = next_pos.fetch_add(1, std::memory_order_acq_rel); + if (pos >= total) + return false; + + out_test_idx = order[pos]; + return true; } }; } @@ -63,40 +97,75 @@ class parallel_test_runner int exit_code = 0; }; - int run_parent_if_needed(int argc, char* argv[]) const; - std::filesystem::path get_worker_dir_path(uint32_t worker_id) const; + int run_parent_if_needed(int argc, char* argv[], const std::vector& g_test_jobs) const; bool write_worker_report(const worker_report& rep) const; - - std::filesystem::path get_taken_tests_log_path_for_this_process() const; void log_test_taken_by_this_process(const std::string& test_name) const; - std::filesystem::path get_workers_report_path() const; - bool write_workers_report_file(uint32_t processes, const std::vector& reps) const; - private: + struct worker_report_json + { + static constexpr const char* worker_id = "worker_id"; + static constexpr const char* processes = "processes"; + static constexpr const char* tests_count = "tests_count"; + static constexpr const char* unique_tests_count = "unique_tests_count"; + static constexpr const char* total_time_ms = "total_time_ms"; + static constexpr const char* skip_all_till_end = "skip_all_till_the_end"; + static constexpr const char* exit_code = "exit_code"; + static constexpr const char* format = "format"; + static constexpr const char* tests = "tests"; + + static constexpr const char* failed_tests = "failed_tests"; + static constexpr const char* tests_running_time = "tests_running_time"; + static constexpr const char* name = "name"; + static constexpr const char* ms = "ms"; + }; + + struct paths + { + static constexpr const char* default_run_root = "chaingen_runs"; + static constexpr const char* worker_dir_prefix = "w"; + }; + + struct files + { + static constexpr const char* taken_tests_log = "taken_tests.log"; + static constexpr const char* worker_report = "coretests_report.json"; + static constexpr const char* worker_log = "worker.log"; + }; + + struct cli_args + { + static constexpr const char* multiprocess_worker_id = "--multiprocess-worker-id"; + static constexpr const char* multiprocess_run = "--multiprocess-run"; + static constexpr const char* multiprocess_run_root = "--multiprocess-run-root"; + static constexpr const char* multiprocess_shm_name = "--multiprocess-shm-name"; + static constexpr const char* data_dir = "--data-dir"; + }; + const boost::program_options::variables_map& m_vm; - mutable std::mutex cout_mx; mutable std::mutex cerr_mx; std::filesystem::path get_run_root_path() const; std::filesystem::path get_worker_report_path(uint32_t worker_id) const; + std::filesystem::path get_worker_log_path(uint32_t worker_id) const; std::string make_worker_data_dir(const std::filesystem::path& run_root_abs, int worker_id) const; + std::filesystem::path get_worker_dir_path(uint32_t worker_id) const; + std::filesystem::path get_taken_tests_log_path_for_this_process() const; + std::filesystem::path get_workers_report_path() const; std::vector build_base_args_without_worker_specific(int argc, char* argv[]) const; - int run_workers_and_wait(int argc, char* argv[]) const; - + int run_workers_and_wait(int argc, char* argv[], const std::vector& g_test_jobs) const; int print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wall_time_ms, const coretests_shm::shared_state* shm_state) const; void print_worker_failure_reasons(uint32_t processes, const std::vector& worker_exit_codes) const; + bool fill_shm_work_order(coretests_shm::shared_state* st, const std::vector& jobs) const; + + bool write_workers_report_file(uint32_t processes, const std::vector& reps) const; bool write_worker_report_file(const worker_report& rep) const; bool read_worker_report_file(uint32_t worker_id, worker_report& rep) const; + void fill_worker_report_header(pt::ptree& root, const worker_report& rep) const; + void read_worker_report_header(const pt::ptree& root, uint32_t worker_id_fallback, worker_report& rep) const; - std::filesystem::path get_worker_log_path(uint32_t worker_id) const; -}; - -struct test_job -{ - std::string name; - std::function run; + bool read_workers_report_ms_map(const std::filesystem::path& path, std::unordered_map& out_ms_by_test) const; }; From eee3b7d4d5f529d163156153f592f7717ae501e2 Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Thu, 15 Jan 2026 04:44:24 +0300 Subject: [PATCH 17/28] minor fixs --- tests/core_tests/parallel/parallel_test_runner.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index ce28a2112..a5665cfd5 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -17,6 +17,8 @@ using namespace chaingen_args; namespace bp = boost::process; +constexpr const std::size_t k_worker_stdout_buffer_size = 64 * 1024; + parallel_test_runner::parallel_test_runner( const boost::program_options::variables_map& vm) : m_vm(vm) @@ -250,10 +252,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std const auto wall_start = std::chrono::steady_clock::now(); const std::string run_root = command_line::get_arg(m_vm, arg_run_root); std::vector base_args = build_base_args_without_worker_specific(argc, argv); - - std::filesystem::path run_root_abs = run_root.empty() - ? std::filesystem::path(paths::default_run_root) - : std::filesystem::path(run_root); + std::filesystem::path run_root_abs = run_root.empty() ? std::filesystem::path(paths::default_run_root) : std::filesystem::path(run_root); if (run_root_abs.is_relative()) run_root_abs = std::filesystem::absolute(run_root_abs); @@ -382,7 +381,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std { std::string line; std::string buffer; - buffer.reserve(64 * 1024); + buffer.reserve(k_worker_stdout_buffer_size); std::function flush = [&]() { @@ -404,7 +403,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std buffer.append(line); buffer.push_back('\n'); - if (buffer.size() >= 64 * 1024) + if (buffer.size() >= k_worker_stdout_buffer_size) flush(); } From 524d69e322176d02874241068968aecaf65ed7e0 Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Fri, 23 Jan 2026 21:13:50 +0300 Subject: [PATCH 18/28] list of tests and their execution time --- .../parallel/parallel_test_runner.cpp | 51 +- .../parallel/parallel_test_runner.h | 2 - .../parallel/tests_distribution_entry.h | 492 ++++++++++++++++++ 3 files changed, 497 insertions(+), 48 deletions(-) create mode 100644 tests/core_tests/parallel/tests_distribution_entry.h diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index a5665cfd5..d865834eb 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -13,6 +13,7 @@ #include "parallel_test_runner.h" #include "../chaingen_args.h" #include "../../src/common/command_line.h" +#include "tests_distribution_entry.h" using namespace chaingen_args; namespace bp = boost::process; @@ -711,66 +712,24 @@ bool parallel_test_runner::write_workers_report_file( uint32_t processes, const } } -bool parallel_test_runner::read_workers_report_ms_map(const std::filesystem::path& path, std::unordered_map& out_ms_by_test) const -{ - out_ms_by_test.clear(); - - try - { - if (!std::filesystem::exists(path)) - return false; - - pt::ptree root; - pt::read_json(path.string(), root); - - for (const auto& v : root.get_child("tests", pt::ptree())) - { - const auto& node = v.second; - const std::string name = node.get("name", ""); - const uint64_t ms = node.get("ms", 0); - if (!name.empty()) - out_ms_by_test[name] = ms; - } - - return !out_ms_by_test.empty(); - } - catch (...) - { - return false; - } -} - bool parallel_test_runner::fill_shm_work_order(coretests_shm::shared_state* st, const std::vector& jobs) const { if (!st) return false; - std::unordered_map ms_by_test; - const std::filesystem::path report_path = get_workers_report_path(); - (void)read_workers_report_ms_map(report_path, ms_by_test); - struct item_t { uint64_t ms; uint32_t idx; }; + std::vector items; items.reserve(jobs.size()); - uint64_t fallback = 1; - if (!ms_by_test.empty()) - { - std::vector known; - known.reserve(ms_by_test.size()); - for (const auto& kv : ms_by_test) known.push_back(kv.second); - std::sort(known.begin(), known.end()); - fallback = known[known.size() / 2]; - } - for (uint32_t i = 0; i < jobs.size(); ++i) { - const auto it = ms_by_test.find(jobs[i].name); - const uint64_t ms = (it != ms_by_test.end() ? it->second : fallback); + const uint64_t ms = get_test_estimated_ms(jobs[i].name); items.push_back(item_t{ms, i}); } - std::sort(items.begin(), items.end(), [](const item_t& a, const item_t& b) { + std::sort(items.begin(), items.end(), [](const item_t& a, const item_t& b) + { if (a.ms != b.ms) return a.ms > b.ms; return a.idx < b.idx; }); diff --git a/tests/core_tests/parallel/parallel_test_runner.h b/tests/core_tests/parallel/parallel_test_runner.h index b1dacd031..fb2b2072a 100644 --- a/tests/core_tests/parallel/parallel_test_runner.h +++ b/tests/core_tests/parallel/parallel_test_runner.h @@ -166,6 +166,4 @@ class parallel_test_runner bool read_worker_report_file(uint32_t worker_id, worker_report& rep) const; void fill_worker_report_header(pt::ptree& root, const worker_report& rep) const; void read_worker_report_header(const pt::ptree& root, uint32_t worker_id_fallback, worker_report& rep) const; - - bool read_workers_report_ms_map(const std::filesystem::path& path, std::unordered_map& out_ms_by_test) const; }; diff --git a/tests/core_tests/parallel/tests_distribution_entry.h b/tests/core_tests/parallel/tests_distribution_entry.h new file mode 100644 index 000000000..ab62fdb16 --- /dev/null +++ b/tests/core_tests/parallel/tests_distribution_entry.h @@ -0,0 +1,492 @@ +#pragma once + +#include +#include +#include +#include +#include + +struct test_time_hint { + std::string_view name; + uint64_t ms; +}; + +// Aggregated per-test timings are written to: /workers_report.json (default run_root is "chaingen_runs") +inline const std::vector k_test_time_hints = { + {"gen_alias_tests @ HF 4", 102170}, + {"gen_alias_tests @ HF 5", 102154}, + {"gen_alias_tests @ HF 6", 102083}, + {"gen_alias_tests @ HF 3", 19867}, + {"gen_alias_too_many_regs_in_block_template @ HF 3", 19632}, + {"pos_and_no_pow_blocks_between_output_and_stake @ HF 4", 8235}, + {"pos_and_no_pow_blocks_between_output_and_stake @ HF 5", 8215}, + {"pos_and_no_pow_blocks_between_output_and_stake @ HF 6", 8152}, + {"gen_pos_basic_tests (BC saveload)", 5721}, + {"hard_fork_6_intrinsic_payment_id_rpc_test", 5057}, + {"multiassets_basic_test @ HF 4", 5029}, + {"multiassets_basic_test @ HF 5", 5026}, + {"multiassets_basic_test @ HF 6", 5015}, + {"gen_alias_too_small_reward @ HF 4", 4517}, + {"gen_alias_too_small_reward @ HF 5", 4508}, + {"gen_alias_too_small_reward @ HF 6", 4500}, + {"input_refers_to_incompatible_by_type_output", 4349}, + {"wallet_rpc_thirdparty_custody @ HF 5", 4026}, + {"wallet_rpc_thirdparty_custody @ HF 6", 4007}, + {"hard_fork_6_intrinsic_payment_id_basic_test", 3932}, + {"asset_operations_and_chain_switching @ HF 5", 3801}, + {"zarcanum_in_alt_chain", 3781}, + {"asset_operations_and_chain_switching @ HF 6", 3778}, + {"escrow_custom_test", 3707}, + {"asset_operations_and_chain_switching @ HF 4", 3672}, + {"pos_altblocks_validation @ HF 4", 3596}, + {"pos_altblocks_validation @ HF 6", 3575}, + {"pos_altblocks_validation @ HF 5", 3546}, + {"zarcanum_basic_test", 3530}, + {"wallet_true_rpc_pos_mining @ HF 4", 3521}, + {"zarcanum_gen_time_balance", 3504}, + {"wallet_rpc_cold_signing @ HF 6", 3450}, + {"wallet_rpc_cold_signing @ HF 5", 3436}, + {"gen_wallet_alias_and_unconfirmed_txs @ HF 5", 3433}, + {"gen_wallet_alias_and_unconfirmed_txs @ HF 6", 3424}, + {"gen_wallet_alias_and_unconfirmed_txs @ HF 4", 3407}, + {"tx_pool_validation_and_chain_switch @ HF 4", 3368}, + {"tx_pool_validation_and_chain_switch @ HF 5", 3348}, + {"gen_wallet_alias_via_special_wallet_funcs @ HF 4", 3063}, + {"gen_wallet_alias_via_special_wallet_funcs @ HF 5", 3048}, + {"ionic_swap_basic_test @ HF 4", 3035}, + {"ionic_swap_basic_test @ HF 6", 3029}, + {"block_reward_in_alt_chain_basic @ HF 5", 3026}, + {"gen_wallet_alias_via_special_wallet_funcs @ HF 6", 3022}, + {"ionic_swap_basic_test @ HF 5", 3021}, + {"block_reward_in_alt_chain_basic @ HF 6", 3005}, + {"assets_and_explicit_native_coins_in_outs", 3002}, + {"gen_alias_switch_and_check_block_template @ HF 4", 2995}, + {"gen_alias_switch_and_check_block_template @ HF 5", 2986}, + {"assets_transfer_with_smallest_amount @ HF 4", 2982}, + {"ionic_swap_exact_amounts_test @ HF 4", 2954}, + {"assets_transfer_with_smallest_amount @ HF 5", 2953}, + {"ionic_swap_exact_amounts_test @ HF 6", 2945}, + {"gen_alias_switch_and_check_block_template @ HF 6", 2942}, + {"ionic_swap_exact_amounts_test @ HF 5", 2924}, + {"gen_wallet_save_load_and_balance @ HF 6", 2840}, + {"wallet_reorganize_and_trim_test @ HF 5", 2835}, + {"wallet_reorganize_and_trim_test @ HF 6", 2832}, + {"hard_fork_2_auditable_addresses_basics @ HF 6", 2810}, + {"hard_fork_2_auditable_addresses_basics @ HF 4", 2795}, + {"hard_fork_2_auditable_addresses_basics @ HF 5", 2793}, + {"wallet_reorganize_and_trim_test @ HF 4", 2791}, + {"gen_wallet_save_load_and_balance @ HF 5", 2779}, + {"block_reward_in_main_chain_basic @ HF 4", 2766}, + {"gen_wallet_save_load_and_balance @ HF 4", 2757}, + {"block_reward_in_main_chain_basic @ HF 5", 2749}, + {"block_reward_in_main_chain_basic @ HF 6", 2728}, + {"asset_operation_in_consolidated_tx @ HF 5", 2667}, + {"asset_operation_in_consolidated_tx @ HF 6", 2665}, + {"asset_operation_in_consolidated_tx @ HF 4", 2659}, + {"zarcanum_pos_block_math", 2657}, + {"hard_fork_2_awo_wallets_basic_test", 2613}, + {"block_choice_rule_bigger_fee @ HF 5", 2558}, + {"several_asset_emit_burn_txs_in_pool @ HF 5", 2557}, + {"block_reward_in_alt_chain_basic @ HF 4", 2544}, + {"block_choice_rule_bigger_fee @ HF 4", 2542}, + {"eth_signed_asset_via_rpc @ HF 5", 2540}, + {"eth_signed_asset_via_rpc @ HF 6", 2532}, + {"block_choice_rule_bigger_fee @ HF 6", 2528}, + {"asset_operation_and_hardfork_checks @ HF 4", 2523}, + {"several_asset_emit_burn_txs_in_pool @ HF 6", 2517}, + {"asset_operation_and_hardfork_checks @ HF 6", 2516}, + {"wallet_rpc_exchange_suite @ HF 4", 2504}, + {"asset_operation_and_hardfork_checks @ HF 5", 2487}, + {"assets_transfer_with_smallest_amount @ HF 6", 2472}, + {"gen_alias_strange_data @ HF 5", 2450}, + {"gen_alias_strange_data @ HF 4", 2438}, + {"chain_switching_when_out_spent_in_alt_chain_ref_id @ HF 6", 2435}, + {"chain_switching_when_out_spent_in_alt_chain_mixin @ HF 4", 2430}, + {"chain_switching_when_out_spent_in_alt_chain_ref_id @ HF 4", 2427}, + {"chain_switching_when_out_spent_in_alt_chain_ref_id @ HF 5", 2426}, + {"chain_switching_when_out_spent_in_alt_chain_mixin @ HF 5", 2425}, + {"gen_alias_strange_data @ HF 6", 2424}, + {"chain_switching_when_out_spent_in_alt_chain_mixin @ HF 6", 2405}, + {"gen_alias_same_alias_in_tx_pool @ HF 5", 2388}, + {"gen_alias_same_alias_in_tx_pool @ HF 6", 2381}, + {"gen_alias_same_alias_in_tx_pool @ HF 4", 2351}, + {"block_template_blacklist_test @ HF 5", 2348}, + {"block_template_blacklist_test @ HF 4", 2318}, + {"wallet_and_sweep_below @ HF 5", 2297}, + {"block_template_blacklist_test @ HF 6", 2284}, + {"gen_chain_switch_pow_pos", 2278}, + {"hardfork_4_wallet_transfer_with_mandatory_mixins @ HF 4", 2249}, + {"wallet_and_sweep_below @ HF 6", 2247}, + {"zarcanum_in_alt_chain_2 @ HF 4", 2242}, + {"hardfork_4_wallet_transfer_with_mandatory_mixins @ HF 6", 2237}, + {"wallet_and_sweep_below @ HF 4", 2226}, + {"hardfork_4_wallet_transfer_with_mandatory_mixins @ HF 5", 2218}, + {"gen_wallet_mine_pos_block @ HF 6", 2196}, + {"zarcanum_in_alt_chain_2 @ HF 6", 2194}, + {"escrow_incorrect_cancel_proposal", 2183}, + {"zarcanum_in_alt_chain_2 @ HF 5", 2181}, + {"alt_chain_and_block_tx_fee_median @ HF 5", 2124}, + {"alt_chain_and_block_tx_fee_median @ HF 4", 2107}, + {"hard_fork_4_consolidated_txs @ HF 5", 2106}, + {"alt_chain_and_block_tx_fee_median @ HF 6", 2102}, + {"isolate_auditable_and_proof @ HF 5", 2080}, + {"isolate_auditable_and_proof @ HF 6", 2079}, + {"hard_fork_4_consolidated_txs @ HF 6", 2077}, + {"escrow_wallet_test", 2076}, + {"hard_fork_5_tx_version @ HF 5", 2074}, + {"hard_fork_5_tx_version @ HF 6", 2071}, + {"tx_input_mixins", 2071}, + {"hard_fork_4_consolidated_txs @ HF 4", 2069}, + {"isolate_auditable_and_proof @ HF 4", 2056}, + {"hard_fork_2_awo_wallets_basic_test", 2041}, + {"eth_signed_asset_basics @ HF 6", 2033}, + {"wallet_true_rpc_pos_mining @ HF 5", 2024}, + {"eth_signed_asset_basics @ HF 5", 2022}, + {"zarcanum_block_with_txs", 2019}, + {"gen_alias_switch_and_tx_pool @ HF 4", 2017}, + {"wallet_true_rpc_pos_mining @ HF 6", 2015}, + {"gen_alias_switch_and_tx_pool @ HF 5", 2014}, + {"gen_alias_switch_and_tx_pool @ HF 6", 2007}, + {"assets_and_pos_mining @ HF 6", 1937}, + {"assets_and_pos_mining @ HF 4", 1929}, + {"zarcanum_txs_with_big_shuffled_decoy_set_shuffled", 1892}, + {"pos_fuse_test @ HF 4", 1877}, + {"pos_fuse_test @ HF 5", 1827}, + {"pos_fuse_test @ HF 6", 1814}, + {"escrow_w_and_fake_outputs", 1730}, + {"gen_wallet_mine_pos_block @ HF 4", 1688}, + {"gen_alias_concurrency_with_switch @ HF 6", 1674}, + {"gen_wallet_mine_pos_block @ HF 5", 1672}, + {"gen_alias_concurrency_with_switch @ HF 5", 1668}, + {"escrow_incorrect_proposal", 1667}, + {"gen_alias_concurrency_with_switch @ HF 4", 1642}, + {"wallet_rpc_alias_tests", 1642}, + {"hardfork_4_wallet_sweep_bare_outs", 1595}, + {"asset_depoyment_and_few_zc_utxos", 1593}, + {"prun_ring_signatures", 1560}, + {"gen_block_is_too_big @ HF 3", 1544}, + {"gen_block_is_too_big @ HF 0", 1540}, + {"asset_emission_and_unconfirmed_balance @ HF 4", 1528}, + {"attachment_isolation_test @ HF 4", 1522}, + {"attachment_isolation_test @ HF 5", 1520}, + {"asset_emission_and_unconfirmed_balance @ HF 6", 1512}, + {"asset_emission_and_unconfirmed_balance @ HF 5", 1511}, + {"escrow_incorrect_proposal_acceptance", 1510}, + {"gen_block_ts_not_checked @ HF 3", 1494}, + {"gen_block_ts_in_past @ HF 0", 1455}, + {"assets_and_pos_mining @ HF 5", 1424}, + {"gen_block_ts_not_checked @ HF 0", 1387}, + {"pos_minting_tx_packing", 1387}, + {"gen_block_ts_in_past @ HF 3", 1363}, + {"multisig_wallet_test", 1351}, + {"multisig_wallet_test_many_dst", 1332}, + {"gen_alias_update_for_free @ HF 5", 1331}, + {"gen_alias_update_for_free @ HF 6", 1320}, + {"gen_alias_update_for_free @ HF 4", 1296}, + {"alt_blocks_validation_and_same_new_amount_in_two_txs @ HF 6", 1281}, + {"alt_blocks_validation_and_same_new_amount_in_two_txs @ HF 5", 1273}, + {"hard_fork_1_locked_mining_test", 1264}, + {"alt_blocks_validation_and_same_new_amount_in_two_txs @ HF 4", 1256}, + {"gen_simple_chain_001", 1256}, + {"block_with_correct_prev_id_on_wrong_height @ HF 6", 1254}, + {"alt_blocks_with_the_same_txs @ HF 6", 1240}, + {"alt_blocks_with_the_same_txs @ HF 5", 1239}, + {"block_with_correct_prev_id_on_wrong_height @ HF 5", 1233}, + {"block_with_correct_prev_id_on_wrong_height @ HF 4", 1231}, + {"gen_alias_too_much_reward @ HF 5", 1227}, + {"alt_blocks_with_the_same_txs @ HF 4", 1224}, + {"gen_alias_too_much_reward @ HF 6", 1223}, + {"gen_alias_too_much_reward @ HF 4", 1216}, + {"escrow_altchain_meta_test<0>", 1214}, + {"hard_fork_1_pos_and_locked_coins", 1201}, + {"chain_switching_when_gindex_spent_in_both_chains @ HF 4", 1198}, + {"escrow_altchain_meta_test<5>", 1198}, + {"chain_switching_when_gindex_spent_in_both_chains @ HF 5", 1197}, + {"escrow_altchain_meta_test<1>", 1196}, + {"chain_switching_when_gindex_spent_in_both_chains @ HF 6", 1192}, + {"escrow_proposal_expiration", 1190}, + {"escrow_altchain_meta_test<6>", 1164}, + {"escrow_altchain_meta_test<4>", 1156}, + {"offers_updating_hack", 1152}, + {"gen_ring_signature_1", 1151}, + {"gen_tx_signatures_are_invalid", 1147}, + {"wallet_and_sweep_below @ HF 3", 1140}, + {"mix_in_spent_outs", 1136}, + {"wallet_watch_only_and_chain_switch @ HF 3", 1127}, + {"packing_outputs_on_pos_minting_wallet @ HF 3", 1123}, + {"gen_broken_attachments", 1117}, + {"hard_fork_2_auditable_addresses_basics @ HF 2", 1114}, + {"escrow_altchain_meta_test<8>", 1112}, + {"hard_fork_2_alias_update_using_old_tx", 1110}, + {"escrow_altchain_meta_test<7>", 1105}, + {"escrow_altchain_meta_test<2>", 1103}, + {"hard_fork_2_auditable_addresses_basics @ HF 3", 1103}, + {"escrow_cancellation_proposal_expiration", 1097}, + {"tx_coinbase_separate_sig_flag @ HF 5", 1097}, + {"wallet_unconfirmed_tx_expiration", 1093}, + {"escrow_proposal_and_accept_expiration", 1090}, + {"hard_fork_2_tx_extra_alias_entry_in_wallet", 1081}, + {"tx_coinbase_separate_sig_flag @ HF 4", 1080}, + {"tx_coinbase_separate_sig_flag @ HF 6", 1080}, + {"hard_fork_2_incorrect_alias_update", 1079}, + {"hard_fork_2_alias_update_using_old_tx", 1071}, + {"random_outs_and_burnt_coins", 1071}, + {"escrow_proposal_not_enough_money", 1066}, + {"escrow_cancellation_acceptance_expiration", 1064}, + {"hardfork_4_pop_tx_from_global_index @ HF 5", 1036}, + {"multisig_wallet_heterogenous_dst", 1032}, + {"hardfork_4_pop_tx_from_global_index @ HF 6", 1029}, + {"gen_pos_extra_nonce @ HF 6", 1026}, + {"hardfork_4_pop_tx_from_global_index @ HF 4", 1026}, + {"wallet_rpc_cold_signing @ HF 3", 1025}, + {"attachment_isolation_test @ HF 6", 1008}, + {"gen_pos_extra_nonce @ HF 5", 1008}, + {"gen_pos_extra_nonce @ HF 4", 1006}, + {"wallet_rpc_hardfork_verification @ HF 5", 976}, + {"gen_alias_tx_no_outs @ HF 5", 902}, + {"gen_alias_tx_no_outs @ HF 6", 883}, + {"gen_alias_tx_no_outs @ HF 4", 873}, + {"gen_alias_too_small_reward @ HF 3", 849}, + {"cumulative_difficulty_adjustment_test_alt", 803}, + {"gen_block_big_major_version", 781}, + {"gen_pos_incorrect_timestamp", 771}, + {"zarcanum_test_n_inputs_validation", 760}, + {"gen_wallet_alias_via_special_wallet_funcs @ HF 3", 755}, + {"cumulative_difficulty_adjustment_test", 711}, + {"gen_alias_in_coinbase @ HF 4", 711}, + {"gen_alias_in_coinbase @ HF 5", 709}, + {"gen_simple_chain_split_1", 700}, + {"gen_alias_in_coinbase @ HF 6", 698}, + {"tx_key_image_pool_conflict", 690}, + {"pos_altblocks_validation @ HF 3", 685}, + {"escrow_altchain_meta_test<3>", 682}, + {"hard_fork_1_checkpoint_basic_test", 682}, + {"gen_tx_unlock_time", 657}, + {"gen_wallet_save_load_and_balance @ HF 0", 657}, + {"offer_sig_validity_in_update_and_cancel", 657}, + {"gen_chain_switch_1", 653}, + {"multisig_and_coinbase", 652}, + {"gen_wallet_decrypted_payload_items", 644}, + {"gen_double_spend_in_the_same_block", 643}, + {"hard_fork_1_bad_pos_source", 640}, + {"mix_attr_tests", 640}, + {"multisig_out_make_and_spent_in_altchain", 636}, + {"gen_double_spend_in_different_blocks", 633}, + {"offer_removing_and_selected_output", 632}, + {"gen_wallet_save_load_and_balance @ HF 2", 629}, + {"gen_wallet_save_load_and_balance @ HF 3", 627}, + {"chain_switching_when_out_spent_in_alt_chain_mixin @ HF 3", 626}, + {"block_reward_in_main_chain_basic @ HF 3", 625}, + {"gen_tx_check_input_unlock_time", 624}, + {"gen_tx_extra_double_entry", 623}, + {"chain_switching_when_out_spent_in_alt_chain_ref_id @ HF 3", 622}, + {"alt_blocks_validation_and_same_new_amount_in_two_txs @ HF 3", 621}, + {"gen_double_spend_in_tx", 616}, + {"gen_double_spend_in_different_chains", 614}, + {"alt_chain_and_block_tx_fee_median @ HF 3", 610}, + {"hard_fork_1_unlock_time_2_in_normal_tx", 608}, + {"hard_fork_2_no_new_structures_before_hf", 608}, + {"hard_fork_1_pos_locked_height_vs_time", 606}, + {"gen_double_spend_in_alt_chain_in_the_same_block", 605}, + {"gen_wallet_mine_pos_block @ HF 3", 604}, + {"multisig_and_altchains", 604}, + {"gen_alias_switch_and_check_block_template @ HF 3", 600}, + {"wallet_unconfimed_tx_balance", 599}, + {"pow_pos_reorganize_specific_case", 598}, + {"gen_double_spend_in_alt_chain_in_different_blocks", 597}, + {"gen_double_spend_in_alt_chain_in_different_blocks", 594}, + {"hard_fork_2_incorrect_alias_update", 594}, + {"offers_tests", 594}, + {"gen_wallet_unlock_by_block_and_by_time", 593}, + {"gen_alias_concurrency_with_switch @ HF 3", 592}, + {"gen_ring_signature_2", 592}, + {"gen_wallet_alias_and_unconfirmed_txs @ HF 3", 591}, + {"gen_wallet_save_load_and_balance @ HF 1", 591}, + {"offers_filtering_1", 590}, + {"offers_multiple_update", 590}, + {"isolate_auditable_and_proof @ HF 3", 588}, + {"gen_alias_strange_data @ HF 3", 587}, + {"gen_double_spend_in_alt_chain_in_the_same_block", 587}, + {"gen_wallet_transfers_and_chain_switch", 587}, + {"hard_fork_1_chain_switch_pow_only", 587}, + {"gen_double_spend_in_the_same_block", 586}, + {"gen_double_spend_in_tx", 585}, + {"gen_wallet_payment_id", 583}, + {"chain_switching_and_tx_with_attachment_blobsize", 580}, + {"multisig_and_checkpoints @ HF 0", 580}, + {"hard_fork_4_consolidated_txs @ HF 3", 579}, + {"multisig_with_same_id_in_pool", 579}, + {"offer_lifecycle_via_tx_pool", 579}, + {"gen_crypted_attachments", 577}, + {"gen_uint_overflow_2", 577}, + {"gen_wallet_selecting_pos_entries", 577}, + {"wallet_chain_switch_with_spending_the_same_ki @ HF 3", 577}, + {"gen_wallet_basic_transfer", 576}, + {"ref_by_id_mixed_inputs_types", 576}, + {"gen_pos_coinstake_already_spent", 575}, + {"gen_wallet_refreshing_on_chain_switch", 574}, + {"pos_mining_with_decoys @ HF 3", 574}, + {"escrow_cancellation_and_tx_order", 573}, + {"offers_expiration_test", 573}, + {"block_reward_in_alt_chain_basic @ HF 3", 572}, + {"escrow_balance", 572}, + {"get_random_outs_test", 572}, + {"multisig_and_unlock_time", 572}, + {"multisig_minimum_sigs", 572}, + {"tx_expiration_time_and_chain_switching", 571}, + {"gen_pos_too_early_pos_block", 570}, + {"gen_wallet_transfers_and_outdated_unconfirmed_txs", 570}, + {"isolate_auditable_and_proof @ HF 2", 570}, + {"gen_checkpoints_set_after_switching_to_altchain", 568}, + {"gen_block_miner_tx_has_2_in @ HF 0", 567}, + {"gen_checkpoints_altchain_far_before_cp", 567}, + {"gen_wallet_oversized_payment_id", 566}, + {"gen_double_spend_in_different_blocks", 565}, + {"block_template_vs_invalid_txs_from_pool", 564}, + {"escrow_zero_amounts", 564}, + {"gen_wallet_refreshing_on_chain_switch_2", 563}, + {"gen_alias_switch_and_tx_pool @ HF 3", 562}, + {"multisig_n_participants_seq_signing", 561}, + {"gen_block_invalid_nonce @ HF 0", 560}, + {"ref_by_id_basics", 560}, + {"wallet_outputs_with_same_key_image", 560}, + {"wallet_rpc_transfer", 560}, + {"tx_pool_semantic_validation @ HF 3", 559}, + {"bad_chain_switching_with_rollback", 557}, + {"gen_tx_mixed_key_offest_not_exist", 557}, + {"multisig_and_checkpoints_bad_txs", 556}, + {"offer_cancellation_with_zero_fee", 556}, + {"gen_checkpoints_invalid_keyimage", 554}, + {"multisig_out_spent_in_altchain_case_b4", 554}, + {"gen_alias_same_alias_in_tx_pool @ HF 3", 552}, + {"gen_wallet_unconfirmed_tx_from_tx_pool", 552}, + {"tx_expiration_time", 552}, + {"alt_blocks_with_the_same_txs @ HF 3", 551}, + {"alt_chain_coins_pow_mined_then_spent", 550}, + {"gen_checkpoints_block_in_future_after_cp", 550}, + {"gen_checkpoints_pos_validation_on_altchain", 549}, + {"gen_tx_invalid_input_amount", 548}, + {"multisig_and_fake_outputs", 547}, + {"gen_wallet_offers_basic", 546}, + {"chain_switching_when_gindex_spent_in_both_chains @ HF 3", 545}, + {"gen_checkpoints_prun_txs_after_blockchain_load", 545}, + {"gen_tx_input_is_not_txin_to_key", 545}, + {"multisig_wallet_ms_to_ms", 545}, + {"gen_tx_has_inputs_no_outputs", 543}, + {"test_blockchain_vs_spent_keyimges", 542}, + {"gen_checkpoints_attachments_basic", 541}, + {"block_with_correct_prev_id_on_wrong_height @ HF 3", 540}, + {"gen_tx_key_offest_points_to_foreign_key", 539}, + {"gen_tx_output_with_zero_amount", 538}, + {"pos_wallet_big_block_test", 538}, + {"gen_checkpoints_altblock_before_and_after_cp", 537}, + {"gen_wallet_offers_size_limit", 537}, + {"gen_alias_update_for_free @ HF 3", 536}, + {"gen_tx_double_key_image", 536}, + {"gen_tx_input_wo_key_offsets", 536}, + {"gen_tx_no_inputs_has_outputs", 536}, + {"fill_tx_rpc_inputs", 535}, + {"wallet_sending_to_integrated_address", 535}, + {"gen_alias_too_much_reward @ HF 3", 534}, + {"gen_tx_key_image_not_derive_from_tx_key", 534}, + {"gen_wallet_unconfirmed_outdated_tx", 534}, + {"gen_block_miner_tx_with_txin_to_key @ HF 0", 533}, + {"gen_block_miner_tx_with_txin_to_key @ HF 3", 533}, + {"tx_expiration_time_and_block_template", 533}, + {"wallet_rpc_integrated_address_transfer", 532}, + {"gen_checkpoints_block_in_future", 531}, + {"gen_tx_txout_to_key_has_invalid_key", 531}, + {"gen_wallet_dust_to_account", 531}, + {"gen_tx_big_version", 528}, + {"test_blockchain_vs_spent_multisig_outs", 528}, + {"gen_block_miner_tx_has_2_in @ HF 3", 526}, + {"gen_no_attchments_in_coinbase_gentime", 525}, + {"gen_alias_tx_no_outs @ HF 3", 524}, + {"gen_no_attchments_in_coinbase @ HF 3", 524}, + {"gen_pos_min_allowed_height", 524}, + {"hardfork_4_wallet_transfer_with_mandatory_mixins @ HF 3", 524}, + {"gen_checkpoints_and_invalid_tx_to_pool", 523}, + {"gen_alias_tx_no_outs @ HF 2", 522}, + {"gen_pos_invalid_coinbase", 522}, + {"gen_tx_key_image_is_invalid", 521}, + {"gen_tx_sender_key_offest_not_exist", 521}, + {"multisig_wallet_same_dst_addr", 521}, + {"gen_alias_tx_no_outs @ HF 0", 520}, + {"gen_alias_tx_no_outs @ HF 1", 520}, + {"hard_fork_1_unlock_time_2_in_coinbase", 520}, + {"multisig_unconfirmed_transfer_and_multiple_scan_pool_calls", 520}, + {"gen_block_invalid_nonce @ HF 3", 519}, + {"gen_pos_extra_nonce @ HF 3", 519}, + {"gen_alias_in_coinbase @ HF 3", 514}, + {"gen_alias_reg_with_locked_money @ HF 1", 505}, + {"gen_alias_reg_with_locked_money @ HF 2", 505}, + {"gen_alias_reg_with_locked_money @ HF 3", 505}, + {"gen_alias_reg_with_locked_money @ HF 5", 505}, + {"gen_alias_reg_with_locked_money @ HF 6", 505}, + {"gen_block_big_major_version @ HF 0", 505}, + {"gen_block_has_invalid_tx @ HF 0", 505}, + {"gen_block_has_invalid_tx @ HF 3", 505}, + {"gen_block_height_is_high @ HF 0", 505}, + {"gen_block_height_is_high @ HF 3", 505}, + {"gen_block_miner_tx_has_2_tx_gen_in @ HF 0", 505}, + {"gen_block_miner_tx_has_no_out @ HF 3", 505}, + {"gen_block_miner_tx_has_out_to_initiator @ HF 0", 505}, + {"gen_block_miner_tx_out_is_big @ HF 3", 505}, + {"gen_block_miner_tx_out_is_small @ HF 0", 505}, + {"gen_block_no_miner_tx @ HF 0", 505}, + {"gen_block_unlock_time_is_high @ HF 3", 505}, + {"gen_block_unlock_time_is_timestamp_in_past @ HF 0", 505}, + {"gen_alias_reg_with_locked_money @ HF 0", 504}, + {"gen_alias_reg_with_locked_money @ HF 4", 504}, + {"gen_block_big_major_version @ HF 3", 504}, + {"gen_block_big_minor_version @ HF 0", 504}, + {"gen_block_big_minor_version @ HF 3", 504}, + {"gen_block_height_is_low @ HF 0", 504}, + {"gen_block_height_is_low @ HF 3", 504}, + {"gen_block_miner_tx_has_2_tx_gen_in @ HF 3", 504}, + {"gen_block_miner_tx_has_no_out @ HF 0", 504}, + {"gen_block_miner_tx_has_out_to_initiator @ HF 3", 504}, + {"gen_block_miner_tx_out_is_big @ HF 0", 504}, + {"gen_block_miner_tx_out_is_small @ HF 3", 504}, + {"gen_block_no_miner_tx @ HF 3", 504}, + {"gen_block_ts_in_future @ HF 0", 504}, + {"gen_block_ts_in_future @ HF 3", 504}, + {"gen_block_unlock_time_is_high @ HF 0", 504}, + {"gen_block_unlock_time_is_low @ HF 0", 504}, + {"gen_block_unlock_time_is_low @ HF 3", 504}, + {"gen_block_unlock_time_is_timestamp_in_future @ HF 0", 504}, + {"gen_block_unlock_time_is_timestamp_in_future @ HF 3", 504}, + {"gen_block_unlock_time_is_timestamp_in_past @ HF 3", 504}, + {"mined_balance_wallet_test", 504}, + {"wallet_rpc_integrated_address", 504}, + {"gen_block_wrong_version_agains_hardfork @ HF 0", 503}, + {"gen_block_wrong_version_agains_hardfork @ HF 3", 503}, + {"gen_tx_no_inputs_no_outputs", 503}, + {"one_block", 503}, + {"wallet_rpc_exchange_suite @ HF 3", 503}, + {"gen_tx_output_is_not_txout_to_key", 502}, +}; + +inline uint64_t get_test_time_ms(std::string_view test_name, uint64_t unknown_ms) +{ + for (const auto& e : k_test_time_hints) + if (e.name == test_name) return e.ms; + return unknown_ms; +} + +// Unknown tests should be considered the slowest to run first +inline uint64_t get_test_estimated_ms(std::string_view test_name) +{ + static const std::unordered_map m = []() + { + std::unordered_map r; + r.reserve(k_test_time_hints.size()); + for (const auto& e : k_test_time_hints) + r.emplace(e.name, e.ms); + return r; + }(); + + constexpr uint64_t k_unknown_ms = (std::numeric_limits::max)() / 4; + auto it = m.find(test_name); + return it == m.end() ? k_unknown_ms : it->second; +} \ No newline at end of file From e850648a0cb9a09f63c2855eb1189de60ca8f6ce Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Fri, 23 Jan 2026 21:32:59 +0300 Subject: [PATCH 19/28] delete structs with literals --- .../parallel/parallel_test_runner.cpp | 100 +++++++++--------- .../parallel/parallel_test_runner.h | 40 ------- 2 files changed, 50 insertions(+), 90 deletions(-) diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index d865834eb..ebd129253 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -40,7 +40,7 @@ std::filesystem::path parallel_test_runner::get_run_root_path() const { std::string run_root = command_line::get_arg(m_vm, arg_run_root); if (run_root.empty()) - run_root = paths::default_run_root; + run_root = "chaingen_runs"; std::filesystem::path run_root_path(run_root); @@ -52,17 +52,17 @@ std::filesystem::path parallel_test_runner::get_run_root_path() const std::filesystem::path parallel_test_runner::get_worker_dir_path(uint32_t worker_id) const { - return get_run_root_path() / (paths::worker_dir_prefix + std::to_string(worker_id)); + return get_run_root_path() / ("w" + std::to_string(worker_id)); } std::filesystem::path parallel_test_runner::get_worker_report_path(uint32_t worker_id) const { - return get_worker_dir_path(worker_id) / files::worker_report; + return get_worker_dir_path(worker_id) / "coretests_report.json"; } std::string parallel_test_runner::make_worker_data_dir(const std::filesystem::path& run_root_abs, int worker_id) const { - std::filesystem::path worker_dir = run_root_abs / (std::string(paths::worker_dir_prefix) + std::to_string(worker_id)); + std::filesystem::path worker_dir = run_root_abs / (std::string("w") + std::to_string(worker_id)); std::filesystem::create_directories(worker_dir); return worker_dir.string(); } @@ -71,8 +71,8 @@ std::string parallel_test_runner::make_worker_data_dir(const std::filesystem::pa std::vector parallel_test_runner::build_base_args_without_worker_specific(int argc, char* argv[]) const { static const std::set worker_specific_args = { - cli_args::multiprocess_worker_id, cli_args::multiprocess_run, cli_args::multiprocess_run_root, - cli_args::multiprocess_shm_name, cli_args::data_dir, + "--multiprocess-worker-id", "--multiprocess-run", "--multiprocess-run-root", + "--multiprocess-shm-name", "--data-dir", }; std::vector args; @@ -212,7 +212,7 @@ void parallel_test_runner::print_worker_failure_reasons(uint32_t processes, cons const std::filesystem::path worker_dir = get_worker_dir_path(i); const std::filesystem::path report_path = get_worker_report_path(i); - const std::filesystem::path log_path = worker_dir / files::worker_log; + const std::filesystem::path log_path = worker_dir / "worker.log"; std::cout << " report: " << (std::filesystem::exists(report_path) ? report_path.string() : ("missing (" + report_path.string() + ")")) << "\n"; std::cout << " logs dir: " << worker_dir.string() << "\n"; @@ -253,12 +253,12 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std const auto wall_start = std::chrono::steady_clock::now(); const std::string run_root = command_line::get_arg(m_vm, arg_run_root); std::vector base_args = build_base_args_without_worker_specific(argc, argv); - std::filesystem::path run_root_abs = run_root.empty() ? std::filesystem::path(paths::default_run_root) : std::filesystem::path(run_root); + std::filesystem::path run_root_abs = run_root.empty() ? std::filesystem::path("chaingen_runs") : std::filesystem::path(run_root); if (run_root_abs.is_relative()) run_root_abs = std::filesystem::absolute(run_root_abs); - base_args.emplace_back(cli_args::multiprocess_run_root); + base_args.emplace_back("--multiprocess-run-root"); base_args.emplace_back(run_root_abs.string()); std::string exe = (argv && argv[0]) ? std::string(argv[0]) : std::string(); @@ -324,7 +324,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std coretests_shm::shared_state* st = new (region.get_address()) coretests_shm::shared_state(); st->init(); - base_args.emplace_back(cli_args::multiprocess_shm_name); + base_args.emplace_back("--multiprocess-shm-name"); base_args.emplace_back(shm_name); struct worker_proc @@ -345,11 +345,11 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std { std::vector args = base_args; - args.emplace_back(cli_args::multiprocess_worker_id); + args.emplace_back("--multiprocess-worker-id"); args.emplace_back(std::to_string(i)); const std::string worker_data_dir = make_worker_data_dir(run_root_abs, static_cast(i)); - args.emplace_back(cli_args::data_dir); + args.emplace_back("--data-dir"); args.emplace_back(worker_data_dir); const std::filesystem::path worker_dir = get_worker_dir_path(i); @@ -357,7 +357,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std try { - const std::filesystem::path log_path = (worker_dir / files::worker_log).string(); + const std::filesystem::path log_path = (worker_dir / "worker.log").string(); worker_proc wp; wp.wid = i; @@ -475,25 +475,26 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std return rc; } -void parallel_test_runner::fill_worker_report_header(pt::ptree& root, const worker_report& rep) const { - root.put(worker_report_json::worker_id, rep.worker_id); - root.put(worker_report_json::processes, rep.processes); - root.put(worker_report_json::tests_count, static_cast(rep.tests_count)); - root.put(worker_report_json::unique_tests_count, static_cast(rep.unique_tests_count)); - root.put(worker_report_json::total_time_ms, rep.total_time_ms); - root.put(worker_report_json::skip_all_till_end, rep.skip_all_till_the_end); - root.put(worker_report_json::exit_code, rep.exit_code); +void parallel_test_runner::fill_worker_report_header(pt::ptree& root, const worker_report& rep) const +{ + root.put("worker_id", rep.worker_id); + root.put("processes", rep.processes); + root.put("tests_count", static_cast(rep.tests_count)); + root.put("unique_tests_count", static_cast(rep.unique_tests_count)); + root.put("total_time_ms", rep.total_time_ms); + root.put("skip_all_till_the_end", rep.skip_all_till_the_end); + root.put("exit_code", rep.exit_code); } void parallel_test_runner::read_worker_report_header(const pt::ptree& root, uint32_t worker_id_fallback, worker_report& rep) const { - rep.worker_id = root.get(worker_report_json::worker_id, worker_id_fallback); - rep.processes = root.get(worker_report_json::processes, 1); - rep.tests_count = static_cast(root.get(worker_report_json::tests_count, 0)); - rep.unique_tests_count = static_cast(root.get(worker_report_json::unique_tests_count, 0)); - rep.total_time_ms = root.get(worker_report_json::total_time_ms, 0); - rep.skip_all_till_the_end = root.get(worker_report_json::skip_all_till_end, false); - rep.exit_code = root.get(worker_report_json::exit_code, 1); + rep.worker_id = root.get("worker_id", worker_id_fallback); + rep.processes = root.get("processes", 1); + rep.tests_count = static_cast(root.get("tests_count", 0)); + rep.unique_tests_count = static_cast(root.get("unique_tests_count", 0)); + rep.total_time_ms = root.get("total_time_ms", 0); + rep.skip_all_till_the_end = root.get("skip_all_till_the_end", false); + rep.exit_code = root.get("exit_code", 1); } bool parallel_test_runner::write_worker_report_file(const worker_report& rep) const @@ -511,17 +512,17 @@ bool parallel_test_runner::write_worker_report_file(const worker_report& rep) co v.put("", s); failed_arr.push_back(std::make_pair("", v)); } - root.add_child(worker_report_json::failed_tests, failed_arr); + root.add_child("failed_tests", failed_arr); pt::ptree times_arr; for (const auto& it : rep.tests_running_time) { pt::ptree node; - node.put(worker_report_json::name, it.first); - node.put(worker_report_json::ms, it.second); + node.put("name", it.first); + node.put("ms", it.second); times_arr.push_back(std::make_pair("", node)); } - root.add_child(worker_report_json::tests_running_time, times_arr); + root.add_child("tests_running_time", times_arr); std::ofstream out(get_worker_report_path(rep.worker_id)); if (!out.is_open()) @@ -556,18 +557,18 @@ bool parallel_test_runner::read_worker_report_file(uint32_t worker_id, worker_re pt::read_json(path.string(), root); read_worker_report_header(root, worker_id, rep); - for (const auto& v : root.get_child(worker_report_json::failed_tests, pt::ptree())) + for (const auto& v : root.get_child("failed_tests", pt::ptree())) { const std::string name = v.second.get_value(); if (!name.empty()) rep.failed_tests.insert(name); } - for (const auto& v : root.get_child(worker_report_json::tests_running_time, pt::ptree())) + for (const auto& v : root.get_child("tests_running_time", pt::ptree())) { const auto& node = v.second; - const std::string name = node.get(worker_report_json::name, ""); - const uint64_t ms = node.get(worker_report_json::ms, 0); + const std::string name = node.get("name", ""); + const uint64_t ms = node.get("ms", 0); if (!name.empty()) rep.tests_running_time.emplace_back(name, ms); } @@ -590,7 +591,7 @@ bool parallel_test_runner::read_worker_report_file(uint32_t worker_id, worker_re std::filesystem::path parallel_test_runner::get_worker_log_path(uint32_t worker_id) const { - return get_worker_dir_path(worker_id) / files::worker_log; + return get_worker_dir_path(worker_id) / "worker.log"; } std::filesystem::path parallel_test_runner::get_taken_tests_log_path_for_this_process() const @@ -602,12 +603,12 @@ std::filesystem::path parallel_test_runner::get_taken_tests_log_path_for_this_pr { const uint32_t wid = static_cast(worker_id); std::filesystem::create_directories(get_worker_dir_path(wid)); - return get_worker_dir_path(wid) / files::taken_tests_log; + return get_worker_dir_path(wid) / "taken_tests.log"; } const std::string data_dir = command_line::get_arg(m_vm, command_line::arg_data_dir); std::filesystem::create_directories(data_dir); - return std::filesystem::path(data_dir) / files::taken_tests_log; + return std::filesystem::path(data_dir) / "taken_tests.log"; } void parallel_test_runner::log_test_taken_by_this_process(const std::string& test_name) const @@ -641,7 +642,7 @@ std::filesystem::path parallel_test_runner::get_workers_report_path() const return get_run_root_path() / WORKERS_REPORT_FILENAME; } -bool parallel_test_runner::write_workers_report_file( uint32_t processes, const std::vector& reps) const +bool parallel_test_runner::write_workers_report_file(uint32_t processes, const std::vector& reps) const { try { @@ -666,25 +667,24 @@ bool parallel_test_runner::write_workers_report_file( uint32_t processes, const tests.emplace_back(std::move(kv.first), kv.second); std::sort(tests.begin(), tests.end(), [](const auto& a, const auto& b) - { - if (a.second != b.second) return a.second > b.second; - return a.first < b.first; - } - ); + { + if (a.second != b.second) return a.second > b.second; + return a.first < b.first; + }); pt::ptree root; - root.put(worker_report_json::format, 1); - root.put(worker_report_json::processes, processes); + root.put("format", 1); + root.put("processes", processes); pt::ptree arr; for (const auto& t : tests) { pt::ptree node; - node.put(worker_report_json::name, t.first); - node.put(worker_report_json::ms, t.second); + node.put("name", t.first); + node.put("ms", t.second); arr.push_back(std::make_pair("", node)); } - root.add_child(worker_report_json::tests, arr); + root.add_child("tests", arr); std::filesystem::path path = get_workers_report_path(); std::ofstream out(path); diff --git a/tests/core_tests/parallel/parallel_test_runner.h b/tests/core_tests/parallel/parallel_test_runner.h index fb2b2072a..bb43170e0 100644 --- a/tests/core_tests/parallel/parallel_test_runner.h +++ b/tests/core_tests/parallel/parallel_test_runner.h @@ -102,46 +102,6 @@ class parallel_test_runner void log_test_taken_by_this_process(const std::string& test_name) const; private: - struct worker_report_json - { - static constexpr const char* worker_id = "worker_id"; - static constexpr const char* processes = "processes"; - static constexpr const char* tests_count = "tests_count"; - static constexpr const char* unique_tests_count = "unique_tests_count"; - static constexpr const char* total_time_ms = "total_time_ms"; - static constexpr const char* skip_all_till_end = "skip_all_till_the_end"; - static constexpr const char* exit_code = "exit_code"; - static constexpr const char* format = "format"; - static constexpr const char* tests = "tests"; - - static constexpr const char* failed_tests = "failed_tests"; - static constexpr const char* tests_running_time = "tests_running_time"; - static constexpr const char* name = "name"; - static constexpr const char* ms = "ms"; - }; - - struct paths - { - static constexpr const char* default_run_root = "chaingen_runs"; - static constexpr const char* worker_dir_prefix = "w"; - }; - - struct files - { - static constexpr const char* taken_tests_log = "taken_tests.log"; - static constexpr const char* worker_report = "coretests_report.json"; - static constexpr const char* worker_log = "worker.log"; - }; - - struct cli_args - { - static constexpr const char* multiprocess_worker_id = "--multiprocess-worker-id"; - static constexpr const char* multiprocess_run = "--multiprocess-run"; - static constexpr const char* multiprocess_run_root = "--multiprocess-run-root"; - static constexpr const char* multiprocess_shm_name = "--multiprocess-shm-name"; - static constexpr const char* data_dir = "--data-dir"; - }; - const boost::program_options::variables_map& m_vm; mutable std::mutex cerr_mx; From b3d4d7b8f7b6b0bff425f7f8d69f7e246d5312fe Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Fri, 23 Jan 2026 21:37:33 +0300 Subject: [PATCH 20/28] update defined for windows --- tests/core_tests/parallel/parallel_test_runner.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index ebd129253..3050520d4 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -1,4 +1,4 @@ -#if defined(_WIN32) +#if defined(_WIN32) || defined(_WIN64) || defined(__WINDOWS__) || defined(__MINGW32__) || defined(__MINGW64__) #include "chaingen.h" #else #include "../chaingen.h" @@ -300,7 +300,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std } const uint32_t parent_pid = -#ifdef _WIN32 +#if defined(_WIN32) || defined(_WIN64) || defined(__WINDOWS__) || defined(__MINGW32__) || defined(__MINGW64__) static_cast(::GetCurrentProcessId()); #else static_cast(::getpid()); From 7c08833e29ebc2bca28a876601a7ea6869d7178d Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Sat, 24 Jan 2026 17:29:54 +0300 Subject: [PATCH 21/28] minor fixs --- .../parallel/parallel_test_runner.cpp | 36 +++++++++---------- .../parallel/parallel_test_runner.h | 22 ++++++++++++ 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index 3050520d4..51fe0f7ec 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -1,4 +1,4 @@ -#if defined(_WIN32) || defined(_WIN64) || defined(__WINDOWS__) || defined(__MINGW32__) || defined(__MINGW64__) +#if defined(_WIN32) || defined(_WIN64) #include "chaingen.h" #else #include "../chaingen.h" @@ -40,7 +40,7 @@ std::filesystem::path parallel_test_runner::get_run_root_path() const { std::string run_root = command_line::get_arg(m_vm, arg_run_root); if (run_root.empty()) - run_root = "chaingen_runs"; + run_root = paths::default_run_root; std::filesystem::path run_root_path(run_root); @@ -52,17 +52,17 @@ std::filesystem::path parallel_test_runner::get_run_root_path() const std::filesystem::path parallel_test_runner::get_worker_dir_path(uint32_t worker_id) const { - return get_run_root_path() / ("w" + std::to_string(worker_id)); + return get_run_root_path() / (paths::worker_dir_prefix + std::to_string(worker_id)); } std::filesystem::path parallel_test_runner::get_worker_report_path(uint32_t worker_id) const { - return get_worker_dir_path(worker_id) / "coretests_report.json"; + return get_worker_dir_path(worker_id) / files::worker_report; } std::string parallel_test_runner::make_worker_data_dir(const std::filesystem::path& run_root_abs, int worker_id) const { - std::filesystem::path worker_dir = run_root_abs / (std::string("w") + std::to_string(worker_id)); + std::filesystem::path worker_dir = run_root_abs / (std::string(paths::worker_dir_prefix) + std::to_string(worker_id)); std::filesystem::create_directories(worker_dir); return worker_dir.string(); } @@ -71,8 +71,8 @@ std::string parallel_test_runner::make_worker_data_dir(const std::filesystem::pa std::vector parallel_test_runner::build_base_args_without_worker_specific(int argc, char* argv[]) const { static const std::set worker_specific_args = { - "--multiprocess-worker-id", "--multiprocess-run", "--multiprocess-run-root", - "--multiprocess-shm-name", "--data-dir", + cli_args::multiprocess_worker_id, cli_args::multiprocess_run, cli_args::multiprocess_run_root, + cli_args::multiprocess_shm_name, cli_args::data_dir, }; std::vector args; @@ -212,7 +212,7 @@ void parallel_test_runner::print_worker_failure_reasons(uint32_t processes, cons const std::filesystem::path worker_dir = get_worker_dir_path(i); const std::filesystem::path report_path = get_worker_report_path(i); - const std::filesystem::path log_path = worker_dir / "worker.log"; + const std::filesystem::path log_path = worker_dir / files::worker_log; std::cout << " report: " << (std::filesystem::exists(report_path) ? report_path.string() : ("missing (" + report_path.string() + ")")) << "\n"; std::cout << " logs dir: " << worker_dir.string() << "\n"; @@ -253,12 +253,12 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std const auto wall_start = std::chrono::steady_clock::now(); const std::string run_root = command_line::get_arg(m_vm, arg_run_root); std::vector base_args = build_base_args_without_worker_specific(argc, argv); - std::filesystem::path run_root_abs = run_root.empty() ? std::filesystem::path("chaingen_runs") : std::filesystem::path(run_root); + std::filesystem::path run_root_abs = run_root.empty() ? std::filesystem::path(paths::default_run_root) : std::filesystem::path(run_root); if (run_root_abs.is_relative()) run_root_abs = std::filesystem::absolute(run_root_abs); - base_args.emplace_back("--multiprocess-run-root"); + base_args.emplace_back(cli_args::multiprocess_run_root); base_args.emplace_back(run_root_abs.string()); std::string exe = (argv && argv[0]) ? std::string(argv[0]) : std::string(); @@ -300,7 +300,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std } const uint32_t parent_pid = -#if defined(_WIN32) || defined(_WIN64) || defined(__WINDOWS__) || defined(__MINGW32__) || defined(__MINGW64__) +#if defined(_WIN32) || defined(_WIN64) static_cast(::GetCurrentProcessId()); #else static_cast(::getpid()); @@ -324,7 +324,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std coretests_shm::shared_state* st = new (region.get_address()) coretests_shm::shared_state(); st->init(); - base_args.emplace_back("--multiprocess-shm-name"); + base_args.emplace_back(cli_args::multiprocess_shm_name); base_args.emplace_back(shm_name); struct worker_proc @@ -345,11 +345,11 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std { std::vector args = base_args; - args.emplace_back("--multiprocess-worker-id"); + args.emplace_back(cli_args::multiprocess_worker_id); args.emplace_back(std::to_string(i)); const std::string worker_data_dir = make_worker_data_dir(run_root_abs, static_cast(i)); - args.emplace_back("--data-dir"); + args.emplace_back(cli_args::data_dir); args.emplace_back(worker_data_dir); const std::filesystem::path worker_dir = get_worker_dir_path(i); @@ -357,7 +357,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std try { - const std::filesystem::path log_path = (worker_dir / "worker.log").string(); + const std::filesystem::path log_path = (worker_dir / files::worker_log).string(); worker_proc wp; wp.wid = i; @@ -591,7 +591,7 @@ bool parallel_test_runner::read_worker_report_file(uint32_t worker_id, worker_re std::filesystem::path parallel_test_runner::get_worker_log_path(uint32_t worker_id) const { - return get_worker_dir_path(worker_id) / "worker.log"; + return get_worker_dir_path(worker_id) / files::worker_log; } std::filesystem::path parallel_test_runner::get_taken_tests_log_path_for_this_process() const @@ -603,12 +603,12 @@ std::filesystem::path parallel_test_runner::get_taken_tests_log_path_for_this_pr { const uint32_t wid = static_cast(worker_id); std::filesystem::create_directories(get_worker_dir_path(wid)); - return get_worker_dir_path(wid) / "taken_tests.log"; + return get_worker_dir_path(wid) / files::taken_tests_log; } const std::string data_dir = command_line::get_arg(m_vm, command_line::arg_data_dir); std::filesystem::create_directories(data_dir); - return std::filesystem::path(data_dir) / "taken_tests.log"; + return std::filesystem::path(data_dir) / files::taken_tests_log; } void parallel_test_runner::log_test_taken_by_this_process(const std::string& test_name) const diff --git a/tests/core_tests/parallel/parallel_test_runner.h b/tests/core_tests/parallel/parallel_test_runner.h index bb43170e0..ff7d3cadf 100644 --- a/tests/core_tests/parallel/parallel_test_runner.h +++ b/tests/core_tests/parallel/parallel_test_runner.h @@ -102,6 +102,28 @@ class parallel_test_runner void log_test_taken_by_this_process(const std::string& test_name) const; private: + struct paths + { + static constexpr const char* default_run_root = "chaingen_runs"; + static constexpr const char* worker_dir_prefix = "w"; + }; + + struct files + { + static constexpr const char* taken_tests_log = "taken_tests.log"; + static constexpr const char* worker_report = "coretests_report.json"; + static constexpr const char* worker_log = "worker.log"; + }; + + struct cli_args + { + static constexpr const char* multiprocess_worker_id = "--multiprocess-worker-id"; + static constexpr const char* multiprocess_run = "--multiprocess-run"; + static constexpr const char* multiprocess_run_root = "--multiprocess-run-root"; + static constexpr const char* multiprocess_shm_name = "--multiprocess-shm-name"; + static constexpr const char* data_dir = "--data-dir"; + }; + const boost::program_options::variables_map& m_vm; mutable std::mutex cerr_mx; From 6374b7542e41448ccdb7303509ae8fe5b6c6d311 Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Sat, 24 Jan 2026 17:57:11 +0300 Subject: [PATCH 22/28] add constants --- .../parallel/parallel_test_runner.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index 51fe0f7ec..3066087ac 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -18,7 +18,13 @@ using namespace chaingen_args; namespace bp = boost::process; -constexpr const std::size_t k_worker_stdout_buffer_size = 64 * 1024; +namespace +{ + constexpr const int UNKNOWN_ERROR_EXIT_CODE = -999; + constexpr const int MS_PER_SECOND = 1000; + constexpr const int WORKER_STDOUT_BUFFER_SIZE = 64 * 1024; + constexpr const int RESERVE_UNIQUE_TESTS_HINT = 8192; +} parallel_test_runner::parallel_test_runner( const boost::program_options::variables_map& vm) @@ -167,7 +173,7 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process std::cout << " Total tests run: " << total_tests_count << "\n"; std::cout << " Failures: " << serious_failures_count << " (postponed failures: " << failed_postponed_tests_count << ")\n"; std::cout << " Postponed: " << postponed_tests.size() << "\n"; - std::cout << " Total time: " << (wall_time_ms / 1000) << " s. (" << (total_tests_count > 0 ? (wall_time_ms / total_tests_count) : 0) << " ms per test in average)\n"; + std::cout << " Total time: " << (wall_time_ms / MS_PER_SECOND) << " s. (" << (total_tests_count > 0 ? (wall_time_ms / total_tests_count) : 0) << " ms per test in average)\n"; if (!failed_tests_union.empty()) { @@ -196,7 +202,7 @@ void parallel_test_runner::print_worker_failure_reasons(uint32_t processes, cons for (uint32_t i = 0; i < processes; ++i) { - const int ec = (i < worker_exit_codes.size() ? worker_exit_codes[i] : -999); + const int ec = (i < worker_exit_codes.size() ? worker_exit_codes[i] : UNKNOWN_ERROR_EXIT_CODE); worker_report rep; const bool report_ok = read_worker_report_file(i, rep); @@ -382,7 +388,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std { std::string line; std::string buffer; - buffer.reserve(k_worker_stdout_buffer_size); + buffer.reserve(WORKER_STDOUT_BUFFER_SIZE); std::function flush = [&]() { @@ -404,7 +410,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std buffer.append(line); buffer.push_back('\n'); - if (buffer.size() >= k_worker_stdout_buffer_size) + if (buffer.size() >= WORKER_STDOUT_BUFFER_SIZE) flush(); } @@ -647,7 +653,7 @@ bool parallel_test_runner::write_workers_report_file(uint32_t processes, const s try { std::unordered_map ms_by_test; - ms_by_test.reserve(8192); + ms_by_test.reserve(RESERVE_UNIQUE_TESTS_HINT); for (const auto& r : reps) { From d70272cc0c0f4246044d0c2f7943c9c595758572 Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Mon, 26 Jan 2026 01:15:30 +0300 Subject: [PATCH 23/28] fix include --- tests/core_tests/parallel/parallel_test_runner.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index 3066087ac..123afde8b 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -1,8 +1,4 @@ -#if defined(_WIN32) || defined(_WIN64) - #include "chaingen.h" -#else - #include "../chaingen.h" -#endif +#include "../chaingen.h" #include #include From a92f70f2460ef24b61105f509938ce6513f4e019 Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Mon, 26 Jan 2026 14:01:28 +0300 Subject: [PATCH 24/28] fix cmakelist --- tests/CMakeLists.txt | 2 ++ tests/core_tests/parallel/parallel_test_runner.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 33dbb9e93..ee24b4faa 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -28,6 +28,8 @@ add_executable(net_load_tests_srv net_load_tests/srv.cpp) add_dependencies(coretests version) +target_include_directories(coretests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/core_tests) + target_link_libraries(coretests rpc wallet currency_core common crypto zlibstatic ethash ${CMAKE_THREAD_LIBS_INIT} ${Boost_LIBRARIES} OpenSSL::SSL OpenSSL::Crypto) target_link_libraries(functional_tests rpc wallet currency_core crypto common zlibstatic ethash libminiupnpc-static ${CMAKE_THREAD_LIBS_INIT} ${Boost_LIBRARIES} OpenSSL::SSL OpenSSL::Crypto) target_link_libraries(hash-tests crypto ethash) diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index 123afde8b..62f4e98f0 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -1,4 +1,4 @@ -#include "../chaingen.h" +#include "chaingen.h" #include #include From 15e6fd3984ada1cb4b610576926cb0c695b8d216 Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Mon, 26 Jan 2026 19:11:43 +0300 Subject: [PATCH 25/28] add include --- tests/core_tests/chaingen_args.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/core_tests/chaingen_args.cpp b/tests/core_tests/chaingen_args.cpp index 2455933e9..158149f46 100644 --- a/tests/core_tests/chaingen_args.cpp +++ b/tests/core_tests/chaingen_args.cpp @@ -1,3 +1,4 @@ +#include "chaingen.h" #include "chaingen_args.h" namespace chaingen_args From 4c8c5941e8e492b63d10ca4d489f0f089bae1889 Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Sun, 1 Feb 2026 22:24:00 +0300 Subject: [PATCH 26/28] run in single-process by default --- tests/core_tests/chaingen_main.cpp | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index f64beb7af..d61f46feb 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -1430,8 +1430,6 @@ int main(int argc, char* argv[]) if (!r) return 1; - g_runner = std::make_unique(g_vm); - if (command_line::get_arg(g_vm, command_line::arg_help)) { std::cout << desc_options << std::endl; @@ -1456,7 +1454,16 @@ int main(int argc, char* argv[]) const int32_t worker_id = command_line::get_arg(g_vm, arg_worker_id); const bool is_worker = (worker_id >= 0); - if (is_worker) + const uint32_t processes = command_line::get_arg(g_vm, arg_processes); + const bool multiprocess_enabled = + is_worker || (command_line::has_arg(g_vm, arg_processes) && processes > 1); + + if (multiprocess_enabled) + g_runner = std::make_unique(g_vm); + else + g_runner.reset(); + + if (is_worker && multiprocess_enabled) init_shared_fail_report_if_needed(); bool skip_all_till_the_end = false; @@ -1539,9 +1546,12 @@ int main(int argc, char* argv[]) is_test_eligible_to_run, is_hf_test_eligible_to_run); - const int parent_rc = g_runner->run_parent_if_needed(argc, argv, g_test_jobs); - if (parent_rc != parallel_test_runner::k_not_parent) - return parent_rc; + if (multiprocess_enabled) + { + const int parent_rc = g_runner->run_parent_if_needed(argc, argv, g_test_jobs); + if (parent_rc != parallel_test_runner::k_not_parent) + return parent_rc; + } // run run_registered_tests( @@ -1604,7 +1614,8 @@ int main(int argc, char* argv[]) rep.skip_all_till_the_end = skip_all_till_the_end; rep.exit_code = (serious_failures_count == 0 ? 0 : 1); - (void)g_runner->write_worker_report(rep); + if (g_runner) + (void)g_runner->write_worker_report(rep); } if (!is_worker) From a5153243edf80349c40c3797466e4491fc3948fc Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Mon, 2 Feb 2026 00:03:20 +0300 Subject: [PATCH 27/28] update report --- .../parallel/parallel_test_runner.cpp | 57 ++++++++++++++----- .../parallel/parallel_test_runner.h | 3 +- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index 62f4e98f0..6d0ad8c5f 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include "parallel_test_runner.h" #include "../chaingen_args.h" @@ -99,7 +101,7 @@ std::vector parallel_test_runner::build_base_args_without_worker_sp void fill_postponed_tests_set(std::set& postponed_tests); -int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wall_time_ms, const coretests_shm::shared_state* shm_state) const +int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wall_time_ms, const coretests_shm::shared_state* shm_state, const std::vector& jobs) const { std::vector reps; reps.reserve(processes); @@ -122,7 +124,7 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process size_t total_unique_tests_count = 0; std::set failed_tests_union; - + std::unordered_set executed_tests_union; for (const auto& r : reps) { if (r.exit_code != 0) @@ -131,6 +133,8 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process total_tests_count += r.tests_count; total_unique_tests_count += r.unique_tests_count; failed_tests_union.insert(r.failed_tests.begin(), r.failed_tests.end()); + for (const auto& it : r.tests_running_time) + executed_tests_union.insert(it.first); } std::set postponed_tests; @@ -147,9 +151,37 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process const coretests_shm::fail_entry& e = shm_state->entries[j]; if (e.test_name[0] != '\0') failed_tests_union.insert(std::string(e.test_name)); + if (e.test_name[0] != '\0') + executed_tests_union.insert(std::string(e.test_name)); } } + if (!jobs.empty()) + { + for (const auto& j : jobs) + { + const bool failed = failed_tests_union.count(j.name) != 0; + const bool executed = executed_tests_union.count(j.name) != 0; + + if (failed) + std::cout << concolor::magenta << j.name << concolor::normal << std::endl; + else if (executed) + std::cout << concolor::green << j.name << concolor::normal << std::endl; + else + std::cout << concolor::yellow << j.name << concolor::normal << std::endl; + } + std::cout << std::endl; + } + + // 2) Postponed tests list + if (!postponed_tests.empty()) + { + std::cout << concolor::yellow << postponed_tests.size() << " POSTPONED TESTS:" << std::endl; + for (const auto& el : postponed_tests) + std::cout << " " << el << std::endl; + std::cout << concolor::normal << std::endl; + } + size_t failed_postponed_tests_count = 0; for (const auto& t : failed_tests_union) if (postponed_tests.count(t) != 0) @@ -158,18 +190,13 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process const size_t serious_failures_count = failed_tests_union.size() >= failed_postponed_tests_count ? (failed_tests_union.size() - failed_postponed_tests_count) : 0; const bool ok = (!any_missing && failed_workers == 0 && serious_failures_count == 0); - std::cout << (ok ? concolor::green : concolor::magenta); - std::cout << "\nREPORT (aggregated):\n"; - std::cout << " Workers: " << processes << "\n"; - if (any_missing) - std::cout << " Warning: some worker reports are missing\n"; - std::cout << " Worker failures: " << failed_workers << "\n"; - - std::cout << " Unique tests run: " << total_unique_tests_count << "\n"; - std::cout << " Total tests run: " << total_tests_count << "\n"; - std::cout << " Failures: " << serious_failures_count << " (postponed failures: " << failed_postponed_tests_count << ")\n"; - std::cout << " Postponed: " << postponed_tests.size() << "\n"; - std::cout << " Total time: " << (wall_time_ms / MS_PER_SECOND) << " s. (" << (total_tests_count > 0 ? (wall_time_ms / total_tests_count) : 0) << " ms per test in average)\n"; + std::cout << (serious_failures_count == 0 ? concolor::green : concolor::magenta); + std::cout << "\nREPORT:\n"; + std::cout << " Unique tests run: " << total_unique_tests_count << std::endl; + std::cout << " Total tests run: " << total_tests_count << std::endl; + std::cout << " Failures: " << serious_failures_count << " (postponed failures: " << failed_postponed_tests_count << ")" << std::endl; + std::cout << " Postponed: " << postponed_tests.size() << std::endl; + std::cout << " Total time: " << (wall_time_ms / MS_PER_SECOND) << " s. (" << (total_tests_count > 0 ? (wall_time_ms / total_tests_count) : 0) << " ms per test in average)" << std::endl; if (!failed_tests_union.empty()) { @@ -447,7 +474,7 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std const auto wall_end = std::chrono::steady_clock::now(); const uint64_t wall_time_ms = static_cast(std::chrono::duration_cast(wall_end - wall_start).count()); - rc = print_aggregated_report_and_return_rc(processes, wall_time_ms, st); + rc = print_aggregated_report_and_return_rc(processes, wall_time_ms, st, g_test_jobs); if (failed_workers != 0 && rc == 0) rc = 1; diff --git a/tests/core_tests/parallel/parallel_test_runner.h b/tests/core_tests/parallel/parallel_test_runner.h index ff7d3cadf..ae8cf30af 100644 --- a/tests/core_tests/parallel/parallel_test_runner.h +++ b/tests/core_tests/parallel/parallel_test_runner.h @@ -7,6 +7,7 @@ #include #include #include +#include namespace pt = boost::property_tree; @@ -138,7 +139,7 @@ class parallel_test_runner std::vector build_base_args_without_worker_specific(int argc, char* argv[]) const; int run_workers_and_wait(int argc, char* argv[], const std::vector& g_test_jobs) const; - int print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wall_time_ms, const coretests_shm::shared_state* shm_state) const; + int print_aggregated_report_and_return_rc(uint32_t processes, uint64_t wall_time_ms, const coretests_shm::shared_state* shm_state, const std::vector& jobs) const; void print_worker_failure_reasons(uint32_t processes, const std::vector& worker_exit_codes) const; bool fill_shm_work_order(coretests_shm::shared_state* st, const std::vector& jobs) const; From 53c7c97eaca9cca359f39251fa115b8d765a3f9b Mon Sep 17 00:00:00 2001 From: dimmarvel Date: Tue, 3 Feb 2026 22:30:26 +0300 Subject: [PATCH 28/28] delete buffer --- .../parallel/parallel_test_runner.cpp | 57 ++++++------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/tests/core_tests/parallel/parallel_test_runner.cpp b/tests/core_tests/parallel/parallel_test_runner.cpp index 6d0ad8c5f..d8e1e34a7 100644 --- a/tests/core_tests/parallel/parallel_test_runner.cpp +++ b/tests/core_tests/parallel/parallel_test_runner.cpp @@ -20,7 +20,6 @@ namespace { constexpr const int UNKNOWN_ERROR_EXIT_CODE = -999; constexpr const int MS_PER_SECOND = 1000; - constexpr const int WORKER_STDOUT_BUFFER_SIZE = 64 * 1024; constexpr const int RESERVE_UNIQUE_TESTS_HINT = 8192; } @@ -164,22 +163,22 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process const bool executed = executed_tests_union.count(j.name) != 0; if (failed) - std::cout << concolor::magenta << j.name << concolor::normal << std::endl; + std::cout << concolor::magenta << j.name << concolor::normal << '\n'; else if (executed) - std::cout << concolor::green << j.name << concolor::normal << std::endl; + std::cout << concolor::green << j.name << concolor::normal << '\n'; else - std::cout << concolor::yellow << j.name << concolor::normal << std::endl; + std::cout << concolor::yellow << j.name << concolor::normal << '\n'; } - std::cout << std::endl; + std::cout << '\n'; } // 2) Postponed tests list if (!postponed_tests.empty()) { - std::cout << concolor::yellow << postponed_tests.size() << " POSTPONED TESTS:" << std::endl; + std::cout << concolor::yellow << postponed_tests.size() << " POSTPONED TESTS:\n"; for (const auto& el : postponed_tests) - std::cout << " " << el << std::endl; - std::cout << concolor::normal << std::endl; + std::cout << " " << el << '\n'; + std::cout << concolor::normal << '\n'; } size_t failed_postponed_tests_count = 0; @@ -192,11 +191,11 @@ int parallel_test_runner::print_aggregated_report_and_return_rc(uint32_t process std::cout << (serious_failures_count == 0 ? concolor::green : concolor::magenta); std::cout << "\nREPORT:\n"; - std::cout << " Unique tests run: " << total_unique_tests_count << std::endl; - std::cout << " Total tests run: " << total_tests_count << std::endl; - std::cout << " Failures: " << serious_failures_count << " (postponed failures: " << failed_postponed_tests_count << ")" << std::endl; - std::cout << " Postponed: " << postponed_tests.size() << std::endl; - std::cout << " Total time: " << (wall_time_ms / MS_PER_SECOND) << " s. (" << (total_tests_count > 0 ? (wall_time_ms / total_tests_count) : 0) << " ms per test in average)" << std::endl; + std::cout << " Unique tests run: " << total_unique_tests_count << '\n'; + std::cout << " Total tests run: " << total_tests_count << '\n'; + std::cout << " Failures: " << serious_failures_count << " (postponed failures: " << failed_postponed_tests_count << ")" << '\n'; + std::cout << " Postponed: " << postponed_tests.size() << '\n'; + std::cout << " Total time: " << (wall_time_ms / MS_PER_SECOND) << " s. (" << (total_tests_count > 0 ? (wall_time_ms / total_tests_count) : 0) << " ms per test in average)" << '\n'; if (!failed_tests_union.empty()) { @@ -410,34 +409,14 @@ int parallel_test_runner::run_workers_and_wait(int argc, char* argv[], const std wp.reader = std::thread([&, out = wp.out.get(), f = wp.file.get()]() { std::string line; - std::string buffer; - buffer.reserve(WORKER_STDOUT_BUFFER_SIZE); - - std::function flush = [&]() - { - if (!buffer.empty()) + while (std::getline(*out, line)) + { + (*f) << line << '\n'; { - (*f) << buffer; - - { - std::lock_guard lk(cout_mutex); - std::cout << buffer; - } - - buffer.clear(); + std::lock_guard lk(cout_mutex); + std::cout << line << '\n'; } - }; - - while (std::getline(*out, line)) - { - buffer.append(line); - buffer.push_back('\n'); - - if (buffer.size() >= WORKER_STDOUT_BUFFER_SIZE) - flush(); - } - - flush(); + } }); kids.emplace_back(std::move(wp));