//+------------------------------------------------------------------+ //| Main.mqh | //| Copyright 2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property strict #ifndef MQLCYBYLEO_SRC_TESTER_BASE_MQH #define MQLCYBYLEO_SRC_TESTER_BASE_MQH //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ #include "Defines.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CMqlCIClassCi : public CLoggerBase { private: //--- Varialbes parametro string m_filename_compiled_log; // Nombre del archivo donde se pondra en caso de fallo de compilacion los logs(RUTA RELATIVA) // Nombre del archivo donde estara el resultado de la compilacion 0 \ 1.. si es 0 = flase (Se ubicara en mql5\\files\\...) // El py lo debera de leeer y marcar como "invalido" el action // La ruta a este archivo no neceista path completo sobr eel nomrbe (Se ubicara en mql5\\files\\...) string m_file_name_res; string m_file_name_out_json; // Nombre del archivo json (Se ubicara en mql5\\files\\...) string m_file_name_read_py; // Archivo de "check para py" este lo leera y sabra uqe ya es hora de emepzar los check uint m_max_timeout_ms_compile; // Maximo tiempo de espera para compilar int m_timeout_per_test_sec; // Maximo tiempo a esperar un solo test (segundos) // Expertos // Path relativo a Experts TestExpertCi m_experts_test[]; int m_experts_test_size; //--- TestingCiLogs m_event_log[]; // Registro de eventos de la ejecucion (tests + eventos del sistema) long m_last_chart_id; // ID del ultimo chart abierto para el test int m_real_test_passed_size; // Contador de tests que pasaron exitosamente int m_event_log_size; // Tamanio actual del array m_event_log datetime m_ex_start_data; // Fecha/hora de inicio del test actual datetime m_ex_end_date; // Fecha/hora de fin del test actual int m_current_idx_expert; // Indice del expert que se esta ejecutando actualmente //--- bool m_in_test; //--- Funcoines void BuildTestJson(); __forceinline void ExtractTextLogFile(const int idx); bool Compile(); // Compila todos los archivos void RunTest(); // Corre los test bool WriteInFileRes(int8_t flags); // Escribe el archivo binario las badenras de exito void RunPyAndFinish(); public: CMqlCIClassCi(void): m_in_test(false), m_last_chart_id(-1) {} ~CMqlCIClassCi(void) {} //--- bool Init(const MqlCiConfigCompiler& config, const TestExpertCi& arr_experts[]); //--- bool First(); void ChartEvent(const int32_t id, const long& lparam, const double& dparam, const string& sparam); bool Execute(); void TimerEvent(); // Check }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CMqlCIClassCi::Init(const MqlCiConfigCompiler &config, const TestExpertCi& arr_experts[]) { //--- ESperamos carga const long chart_id = ::ChartID(); ::Sleep(500); // Primera espera inicial (no tatno por que ya es el primero en cargar) while(true) { // COmprobacion de barras if(::ChartGetInteger(chart_id, CHART_VISIBLE_BARS) > 0 && ::SeriesInfoInteger(_Symbol, _Period, SERIES_SYNCHRONIZED)) { break; } // Espera para la siguiente iteracion Sleep(1000); } //--- CErramos todos los graficos que pudieran haber menos el actual CloseAllChartsExceptChart(chart_id); //--- m_filename_compiled_log = TERMINAL_MT5_ROOT + "Files\\" + config.filename_compiled_log; m_file_name_res = config.file_name_res; m_file_name_out_json = config.file_name_out_json; m_file_name_read_py = config.file_name_read_py; m_max_timeout_ms_compile = config.max_timeout_ms_compile; m_timeout_per_test_sec = config.max_timeout_esecution_per_test; //--- m_experts_test_size = ArrayCopyCts(m_experts_test, arr_experts); if(m_experts_test_size < 1) { //--- ::ArrayResize(m_event_log, m_event_log_size + 1); m_event_log[m_event_log_size].label = "Iniciciion test - Experts size"; m_event_log[m_event_log_size].res = CYBYLEO_CODE_ERROR; m_event_log[m_event_log_size].log_txt = ::StringFormat("Numero de expertos es invalido=%d", m_experts_test_size); LogError(m_event_log[m_event_log_size++].log_txt, FUNCION_ACTUAL); //--- WriteInFileRes(0); RunPyAndFinish(); return false; } //--- m_event_log_size = ::ArrayResize(m_event_log, 0); m_real_test_passed_size = 0; m_current_idx_expert = 0; m_last_chart_id = -1; m_in_test = false; //--- Print final} LogInfo("Iniciado CI - CD", FUNCION_ACTUAL); //--- return true; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CMqlCIClassCi::Compile(void) { for(int i = 0; i < m_experts_test_size; i++) { static string log_path; // EN caso la compilacion falle tenemos que escribir en el archivo compile el log del archivo que fallo y cerrar el terminal y abrir el py if(!CompileFileWithLogFile(TERMINAL_MT5_ROOT + m_experts_test[i].expert_mq5_path, log_path, m_max_timeout_ms_compile)) { LogError(::StringFormat("Fallo al compilar:\n%s", m_experts_test[i].expert_mq5_path), FUNCION_ACTUAL); LogError(::StringFormat("Log path:\n%s", log_path), FUNCION_ACTUAL); MTTESTER::FileMove(log_path, m_filename_compiled_log, true); // Movemos el archivo con el nombre del path return false; } } return true; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CMqlCIClassCi::WriteInFileRes(int8_t flags) { const int file_handle = ::FileOpen(m_file_name_res, FILE_BIN | FILE_WRITE); if(file_handle == INVALID_HANDLE) { // En caso falle no se creara asi que el py tiene dos cosas... digo 3 // Si [No se puede cargar el archivo por esta corrupta | el archivo tiene valor 0 | el archivo no existe] entonces fallo la compilacion LogError(::StringFormat("Fallo al crear el archivo = %s", m_file_name_res), FUNCION_ACTUAL); return false; } ::FileWriteInteger(file_handle, flags, sizeof(int8_t)); ::FileClose(file_handle); return true; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CMqlCIClassCi::RunTest(void) { //--- m_ex_start_data = ::TimeLocal(); //--- chekeamos el tamaño de parametros en caso sea menor a 2 entonces resize const int size_params = ArraySize(m_experts_test[m_current_idx_expert].params); if(size_params < 2) ArrayResize(m_experts_test[m_current_idx_expert].params, 2); // Minimo un tamaño de dos siempre //--- sobreesciemos los dos primeros parametros m_experts_test[m_current_idx_expert].params[0].type = TYPE_STRING; m_experts_test[m_current_idx_expert].params[0].string_value = m_experts_test[m_current_idx_expert].expert_ex5_name; m_experts_test[m_current_idx_expert].params[1].type = TYPE_LONG; m_experts_test[m_current_idx_expert].params[1].integer_value = ChartID(); //--- abrimos un nuevo graficos m_last_chart_id = ::ChartOpen(m_experts_test[m_current_idx_expert].symbol, m_experts_test[m_current_idx_expert].timeframe); Sleep(750); // Primera espera iniicla //--- Check sincronizacion del grafico while(true) { // COmprobacion de barras | Verificamos sincronizacion if(ChartGetInteger(m_last_chart_id, CHART_VISIBLE_BARS) > 0 && SeriesInfoInteger(m_experts_test[m_current_idx_expert].symbol, m_experts_test[m_current_idx_expert].timeframe, SERIES_SYNCHRONIZED)) { break; } // Espera para la siguiente iteracion Sleep(750); } // Log informativo LogInfo(StringFormat("Chart id = %I64d | EA a correr = %s", m_last_chart_id, m_experts_test[m_current_idx_expert].expert_ex5_name), FUNCION_ACTUAL); //--- if(!EXPERT::Run(m_last_chart_id, m_experts_test[m_current_idx_expert].params)) { //--- m_ex_end_date = TimeLocal(); //--- ::ArrayResize(m_event_log, m_event_log_size + 1); ExtractTextLogFile(m_event_log_size); // Todos los logs generados antes de correr el ea y ahora los capturamos m_event_log[m_event_log_size].label = "Ejecucion de EA"; m_event_log[m_event_log_size].res = CYBYLEO_CODE_ERROR; //--- LogError("Fallo al correr EA", FUNCION_ACTUAL); WriteInFileRes(CIBYLEO_FLAG_EXITO_COMPILACION); // Solo compilacion RunPyAndFinish(); // Corremos el py y finalizamos return; } m_in_test = true; // Empezamos } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CMqlCIClassCi::ChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_CUSTOM + CIBYLEO_ONEVENT_RES) // Ya recibimos { //--- m_in_test = false; m_ex_end_date = ::TimeLocal(); // Fin de la ejeuccion //--- Añadimos ::ArrayResize(m_event_log, m_event_log_size + 1); m_event_log[m_event_log_size].label = sparam; // Por convecion nombre del ea m_event_log[m_event_log_size].res = lparam; // Resultado // Extraemos el log del journal ExtractTextLogFile(m_event_log_size); // Extrae de start-end date m_event_log_size++; // Aumentamos // Cerramos el chart ::ChartClose(m_last_chart_id); //--- LogInfo(StringFormat("Recibiendo info de %s, res = %d, puntuacion = %.2f", sparam, lparam, dparam), FUNCION_ACTUAL); //--- if(lparam == CYBYLEO_CODE_ERROR) // Fallo al ejeuctar el test { // Para esto ya tendremos la tarea que fallo en el array asi qeu ya finalizariamos de una vez WriteInFileRes(CIBYLEO_FLAG_EXITO_COMPILACION); // Solo se jeucto la compilacion bien el test no RunPyAndFinish(); return; } //--- m_real_test_passed_size++; // ya pasamos esta tarea m_current_idx_expert++; // siguiente test //--- if(m_current_idx_expert < m_experts_test_size) { RunTest(); // Repetimos el proceso } else // Fin de las tareas hora de prepara el py { WriteInFileRes(CIBYLEO_FLAG_EXITO_COMPILACION | CIBYLEO_FLAG_EXITO_TEST); // Todo se ejeucto bien RunPyAndFinish(); } } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ __forceinline void CMqlCIClassCi::ExtractTextLogFile(const int idx) { ExtractLogLinesAsStr(m_ex_start_data, m_ex_start_data, m_ex_end_date, m_event_log[m_event_log_size].log_txt); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CMqlCIClassCi::RunPyAndFinish(void) { //--- ::ArrayResize(m_event_log, m_event_log_size + 1); m_event_log [m_event_log_size].label = "Ejecucion de python"; //--- ::ResetLastError(); const int fh = ::FileOpen(m_file_name_read_py, FILE_WRITE | FILE_BIN); if(fh == INVALID_HANDLE) { m_event_log[m_event_log_size].res = CYBYLEO_CODE_ERROR; m_event_log[m_event_log_size].log_txt = ::StringFormat("Fallo al abrir el archivo = %s, ultimo error = %d", m_file_name_read_py, ::GetLastError()); LogError(m_event_log [m_event_log_size].log_txt, FUNCION_ACTUAL); } else { FileWriteInteger(fh, 1, sizeof(int8_t)); FileClose(fh); m_event_log[m_event_log_size].res = CYBYLEO_CODE_SUCCES; m_event_log[m_event_log_size].log_txt = ""; } m_event_log_size++; // Aumentamos //--- BuildTestJson(); //--- Cerramos el terminal ::TerminalClose(CYBYLEO_TERMINAL_RET_CODE_SUCCESS); Sleep(10000); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CMqlCIClassCi::BuildTestJson(void) { //--- /* { "summary": { "tests_passed": 10, // Numero de tareas que se paran (real task passed) "tests_total": 12, // Numero de tareas totales }, // Log events.. "log_events": [ { "label": "Compilacion", "result": 1, "log_txt": "..." } { } ] } */ //--- Construccion del json CJsonBuilder builder; builder.Obj(); builder.Key("summary").Obj(); builder.Key("tests_passed").Val(m_real_test_passed_size); builder.Key("tests_total").Val(m_experts_test_size); builder.EndObj(); builder.Key("log_events").Arr(); for(int i = 0; i < m_event_log_size; i++) { builder.Obj(); builder.Key("label").Val(m_event_log[i].label); builder.Key("result").Val(m_event_log[i].res); builder.Key("log_txt").Val(m_event_log[i].log_txt); builder.EndObj(); } builder.EndArr(); builder.EndObj(); //--- ::ResetLastError(); const int fh = ::FileOpen(m_file_name_out_json, FILE_WRITE | FILE_TXT); if(fh == INVALID_HANDLE) { WriteInFileRes(CIBYLEO_FLAG_EXITO_COMPILACION); // Solo compilacion return; } ::FileWrite(fh, builder.Build()); ::FileClose(fh); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CMqlCIClassCi::First(void) { //--- Primero trataremos de compilar todo en caso falle crearemos el archivo res y lo pondremos en 0 y cerramos el terminal if(!Compile()) { WriteInFileRes(0); // Escribimos en el archivo result fallo (nada no compilo) RunPyAndFinish(); return false; } //--- Si ya se han compilado todos loa rchivos con exito entonces pasamos al test RunTest(); // Corremos el primer test (como minimo debera de haber un archivbo esto igualente se compurbea en Init) return true; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CMqlCIClassCi::TimerEvent(void) { if(!m_in_test) return; const datetime current_time = TimeLocal(); if(current_time - m_ex_start_data > m_timeout_per_test_sec) { LogError(StringFormat("Timeout en test: %s", m_experts_test[m_current_idx_expert].expert_ex5_name), FUNCION_ACTUAL); ::ChartClose(m_last_chart_id); WriteInFileRes(CIBYLEO_FLAG_EXITO_COMPILACION); RunPyAndFinish(); } } //--- #endif // MQLCYBYLEO_SRC_TESTER_BASE_MQH //+------------------------------------------------------------------+