//+------------------------------------------------------------------+ //| ParserTest.mqh | //| Copyright 2026, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, MetaQuotes Ltd." #property link "https://www.mql5.com" #property strict #ifndef JSONPARSERBYLEO_WF_INC_PARSERTEST_MQH #define JSONPARSERBYLEO_WF_INC_PARSERTEST_MQH //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ #include "Def.mqh" #resource "parser_roundtrip.json" as const string g_parser_roundtrip_json //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ namespace TSN { //+------------------------------------------------------------------+ //| Helper compartido: compara dos parsers slot a slot en m_cinta. | //| No es un ICiTest, es una funcion de apoyo usada por varios tests.| //+------------------------------------------------------------------+ bool JPBL_CintaIguales(const TSN::CJsonParser& a, const TSN::CJsonParser& b, string& diff) { if(a.m_cinta_pos != b.m_cinta_pos) { diff = StringFormat("m_cinta_pos distinto: a=%d b=%d", a.m_cinta_pos, b.m_cinta_pos); return false; } for(int i = 0; i < a.m_cinta_pos; i++) { if(a.m_cinta[i] != b.m_cinta[i]) { diff = StringFormat("slot %d distinto: a=%d b=%d", i, a.m_cinta[i], b.m_cinta[i]); return false; } } return true; } //+------------------------------------------------------------------+ //| 1. ToJsonAsm produce un dump no vacio de un JSON valido | //+------------------------------------------------------------------+ class CTestParser_ToJsonAsmNotEmpty : public ICiTest { public: inline string Name() const override final { return "Parser_ToJsonAsm_NotEmpty"; } inline int Importance() const override final { return 85; } bool Run(string &msg) override final { TSN::CJsonParser p; p.Assing(g_parser_roundtrip_json); p.CorrectPadding(); if(!p.Parse()) { msg = "Parse() del fixture fallo"; return false; } uchar buf[]; int written = p.ToJsonAsm(buf); if(written <= 0) { msg = StringFormat("ToJsonAsm devolvio %d bytes", written); return false; } return true; } }; //+------------------------------------------------------------------+ //| 2. Round-trip completo: Parse -> ToJsonAsm -> ParseAssembly -> | //| misma cinta que el parseo original | //+------------------------------------------------------------------+ class CTestParser_JsonAsmRoundTripSameCinta : public ICiTest { public: inline string Name() const override final { return "Parser_JsonAsm_RoundTrip_SameCinta"; } inline int Importance() const override final { return 95; } bool Run(string &msg) override final { TSN::CJsonParser p1; p1.Assing(g_parser_roundtrip_json); p1.CorrectPadding(); if(!p1.Parse()) { msg = "Parse() original fallo"; return false; } //--- Dump a jsonasm uchar buf[]; p1.ToJsonAsm(buf); string asm_text = CharArrayToString(buf, 0, WHOLE_ARRAY, CP_UTF8); //--- Reparseamos el dump con un parser nuevo TSN::CJsonParser p2; p2.Assing(asm_text); p2.CorrectPadding(); if(!p2.ParseAssembly()) { msg = "ParseAssembly() del dump fallo"; return false; } //--- La cinta debe coincidir en tipos y estructura TSN::CJsonNode r1 = p1.GetRoot(); TSN::CJsonNode r2 = p2.GetRoot(); if(r1.GetType() != r2.GetType()) { msg = "Tipo de root distinto tras round-trip jsonasm"; return false; } return true; } }; //+------------------------------------------------------------------+ //| 3. Round-trip jsonasm preserva valores accesibles via API | //+------------------------------------------------------------------+ class CTestParser_JsonAsmRoundTripPreservesValues : public ICiTest { public: inline string Name() const override final { return "Parser_JsonAsm_RoundTrip_PreservesValues"; } inline int Importance() const override final { return 95; } bool Run(string &msg) override final { TSN::CJsonParser p1; p1.Assing(g_parser_roundtrip_json); p1.CorrectPadding(); p1.Parse(); //--- uchar buf[]; p1.ToJsonAsm(buf); string asm_text = CharArrayToString(buf, 0, WHOLE_ARRAY, CP_UTF8); //--- TSN::CJsonParser p2; p2.Assing(asm_text); p2.CorrectPadding(); if(!p2.ParseAssembly()) { msg = "ParseAssembly() fallo"; return false; } //--- TSN::CJsonNode root2 = p2.GetRoot(); string symbol = root2["symbol"].ToString("__MISSING__"); double bid = root2["bid"].ToDouble(-1.0); bool active = root2["active"].ToBool(false); long neg = root2["neg"].ToInt(0); string deep = root2["meta"]["nested"]["deep"].ToString("__MISSING__"); //--- if(symbol != "EURUSD" || MathAbs(bid - 1.2345) > 0.0001 || !active || neg != -42 || deep != "ok") { msg = StringFormat("valores no preservados: symbol=%s bid=%.4f active=%s neg=%d deep=%s", symbol, bid, active ? "true" : "false", neg, deep); return false; } return true; } }; //+------------------------------------------------------------------+ //| 4. Round-trip jsonasm preserva arrays y objetos vacios | //+------------------------------------------------------------------+ class CTestParser_JsonAsmRoundTripEmptyContainers : public ICiTest { public: inline string Name() const override final { return "Parser_JsonAsm_RoundTrip_EmptyContainers"; } inline int Importance() const override final { return 80; } bool Run(string &msg) override final { TSN::CJsonParser p1; p1.Assing(g_parser_roundtrip_json); p1.CorrectPadding(); p1.Parse(); uchar buf[]; p1.ToJsonAsm(buf); string asm_text = CharArrayToString(buf, 0, WHOLE_ARRAY, CP_UTF8); TSN::CJsonParser p2; p2.Assing(asm_text); p2.CorrectPadding(); p2.ParseAssembly(); TSN::CJsonNode root2 = p2.GetRoot(); string keys[]; int c1 = root2["empty_obj"].GetKeys(keys); TSN::CJsonNode arr = root2["empty_arr"]; bool arr_in_range = arr.InRangeSafe(0); if(c1 != 0 || arr_in_range) { msg = StringFormat("empty_obj keys=%d, empty_arr InRangeSafe(0)=%s", c1, arr_in_range ? "true" : "false"); return false; } return true; } }; //+------------------------------------------------------------------+ //| 5. Round-trip jsonasm preserva arrays con multiples elementos | //+------------------------------------------------------------------+ class CTestParser_JsonAsmRoundTripArray : public ICiTest { public: inline string Name() const override final { return "Parser_JsonAsm_RoundTrip_Array"; } inline int Importance() const override final { return 85; } bool Run(string &msg) override final { TSN::CJsonParser p1; p1.Assing(g_parser_roundtrip_json); p1.CorrectPadding(); p1.Parse(); uchar buf[]; p1.ToJsonAsm(buf); string asm_text = CharArrayToString(buf, 0, WHOLE_ARRAY, CP_UTF8); TSN::CJsonParser p2; p2.Assing(asm_text); p2.CorrectPadding(); p2.ParseAssembly(); TSN::CJsonNode tags = p2.GetRoot()["tags"]; string first = tags.At(0).ToString("__MISSING__"); string second = tags.At(1).ToString("__MISSING__"); if(first != "fx" || second != "major") { msg = StringFormat("esperado 'fx'/'major', obtenido '%s'/'%s'", first, second); return false; } return true; } }; //+------------------------------------------------------------------+ //| 6. Round-trip jsonasm con doble vuelta (asm de un asm-reparseado)| //| debe seguir siendo estable (idempotencia del formato) | //+------------------------------------------------------------------+ class CTestParser_JsonAsmDoubleRoundTripStable : public ICiTest { public: inline string Name() const override final { return "Parser_JsonAsm_DoubleRoundTrip_Stable"; } inline int Importance() const override final { return 75; } bool Run(string &msg) override final { TSN::CJsonParser p1; p1.Assing(g_parser_roundtrip_json); p1.CorrectPadding(); p1.Parse(); //--- primera vuelta uchar buf1[]; p1.ToJsonAsm(buf1); string asm1 = CharArrayToString(buf1, 0, WHOLE_ARRAY, CP_UTF8); TSN::CJsonParser p2; p2.Assing(asm1); p2.CorrectPadding(); p2.ParseAssembly(); //--- segunda vuelta: dump de p2 y reparseo en p3 uchar buf2[]; p2.ToJsonAsm(buf2); string asm2 = CharArrayToString(buf2, 0, WHOLE_ARRAY, CP_UTF8); TSN::CJsonParser p3; p3.Assing(asm2); p3.CorrectPadding(); if(!p3.ParseAssembly()) { msg = "ParseAssembly() de la segunda vuelta fallo"; return false; } string symbol = p3.GetRoot()["symbol"].ToString("__MISSING__"); if(symbol != "EURUSD") { msg = StringFormat("esperado 'EURUSD' tras doble round-trip, obtenido '%s'", symbol); return false; } return true; } }; //+------------------------------------------------------------------+ //| 7. ParseAssembly detecta null correctamente tras el dump | //+------------------------------------------------------------------+ class CTestParser_JsonAsmRoundTripNull : public ICiTest { public: inline string Name() const override final { return "Parser_JsonAsm_RoundTrip_Null"; } inline int Importance() const override final { return 80; } bool Run(string &msg) override final { TSN::CJsonParser p1; p1.Assing(g_parser_roundtrip_json); p1.CorrectPadding(); p1.Parse(); uchar buf[]; p1.ToJsonAsm(buf); string asm_text = CharArrayToString(buf, 0, WHOLE_ARRAY, CP_UTF8); TSN::CJsonParser p2; p2.Assing(asm_text); p2.CorrectPadding(); p2.ParseAssembly(); if(!p2.GetRoot()["nothing"].IsNull()) { msg = "IsNull() sobre 'nothing' tras round-trip devolvio false"; return false; } return true; } }; //+------------------------------------------------------------------+ //| 8. SaveCompiled + AssingFile -> misma cinta exacta (comparacion | //| binaria completa via helper JPBL_CintaIguales) | //+------------------------------------------------------------------+ class CTestParser_JsonTcRoundTripSameCinta : public ICiTest { public: inline string Name() const override final { return "Parser_JsonTc_RoundTrip_SameCinta"; } inline int Importance() const override final { return 95; } bool Run(string &msg) override final { TSN::CJsonParser p1; p1.Assing(g_parser_roundtrip_json); p1.CorrectPadding(); if(!p1.Parse()) { msg = "Parse() original fallo"; return false; } //--- Guardamos comprimido (embebido: cinta + raw + tablas) const string cache_file = "TbpCi_TestCache_Basic.jsontc"; if(!p1.SaveCompiled(cache_file, false)) { msg = "SaveCompiled() fallo"; return false; } //--- Recargamos en un parser nuevo, sin reparsear TSN::CJsonParser p2; const int result = p2.AssingFile(cache_file, false); if(result <= 0) { msg = StringFormat("AssingFile() del .jsontc fallo, result=%d", result); return false; } //--- La cinta debe ser IDENTICA slot a slot (mismo m_raw embebido) string diff; if(!JPBL_CintaIguales(p1, p2, diff)) { msg = diff; return false; } return true; } }; //+------------------------------------------------------------------+ //| 9. jsontc round-trip preserva valores via API (sin reparseo) | //+------------------------------------------------------------------+ class CTestParser_JsonTcRoundTripPreservesValues : public ICiTest { public: inline string Name() const override final { return "Parser_JsonTc_RoundTrip_PreservesValues"; } inline int Importance() const override final { return 95; } bool Run(string &msg) override final { TSN::CJsonParser p1; p1.Assing(g_parser_roundtrip_json); p1.CorrectPadding(); p1.Parse(); const string cache_file = "TbpCi_TestCache_Values.jsontc"; if(!p1.SaveCompiled(cache_file, false)) { msg = "SaveCompiled() fallo"; return false; } TSN::CJsonParser p2; if(p2.AssingFile(cache_file, false) <= 0) { msg = "AssingFile() fallo"; return false; } TSN::CJsonNode root2 = p2.GetRoot(); string symbol = root2["symbol"].ToString("__MISSING__"); double bid = root2["bid"].ToDouble(-1.0); string deep = root2["meta"]["nested"]["deep"].ToString("__MISSING__"); if(symbol != "EURUSD" || MathAbs(bid - 1.2345) > 0.0001 || deep != "ok") { msg = StringFormat("valores no preservados tras .jsontc: symbol=%s bid=%.4f deep=%s", symbol, bid, deep); return false; } return true; } }; //+------------------------------------------------------------------+ //| 10. jsontc preserva las tablas de perfect-hash ya calentadas | //| (warm-up antes de guardar, luego el acceso debe seguir O(1) | //| sin recalcular, y el resultado debe ser correcto) | //+------------------------------------------------------------------+ class CTestParser_JsonTcPreservesHashTables : public ICiTest { public: inline string Name() const override final { return "Parser_JsonTc_PreservesHashTables"; } inline int Importance() const override final { return 85; } bool Run(string &msg) override final { TSN::CJsonParser p1; p1.Assing(g_parser_roundtrip_json); p1.CorrectPadding(); p1.Parse(); //--- Warm-up: forzamos que 'meta' se promueva a perfect-hash antes de guardar TSN::CJsonNode meta = p1.GetRoot()["meta"]; for(int i = 0; i < 10; i++) meta["id"]; const string cache_file = "TbpCi_TestCache_Hash.jsontc"; if(!p1.SaveCompiled(cache_file, false)) { msg = "SaveCompiled() fallo"; return false; } TSN::CJsonParser p2; if(p2.AssingFile(cache_file, false) <= 0) { msg = "AssingFile() fallo"; return false; } //--- m_tables_size debe haberse restaurado (al menos 1 tabla: 'meta') if(p2.m_tables_size < 1) { msg = StringFormat("esperado m_tables_size >= 1, obtenido %d", p2.m_tables_size); return false; } //--- Y el acceso debe seguir siendo correcto long id = p2.GetRoot()["meta"]["id"].ToInt(-1); if(id != 7) { msg = StringFormat("esperado id=7, obtenido %d", id); return false; } return true; } }; //+------------------------------------------------------------------+ //| 11. SaveCompiled con referencia externa (no embebido) + reload | //+------------------------------------------------------------------+ class CTestParser_JsonTcExternalRefRoundTrip : public ICiTest { public: inline string Name() const override final { return "Parser_JsonTc_ExternalRef_RoundTrip"; } inline int Importance() const override final { return 75; } bool Run(string &msg) override final { //--- Primero escribimos el .json fuente en Files\ (relativo al terminal) const string json_file = "TbpCi_TestSource_ExternalRef.json"; int fh = FileOpen(json_file, FILE_WRITE | FILE_TXT | FILE_ANSI); if(fh == INVALID_HANDLE) { msg = "No se pudo crear el .json fuente de prueba"; return false; } FileWriteString(fh, g_parser_roundtrip_json); FileClose(fh); //--- Parseamos normalmente TSN::CJsonParser p1; if(p1.AssingFile(json_file, false) != 0) { msg = "AssingFile() del .json fuente fallo, valor diferente a 0"; return false; } p1.CorrectPadding(); if(!p1.Parse()) { msg = "Parse() del .json fuente fallo"; return false; } //--- Guardamos con referencia externa (no embebe el raw, guarda la ruta) const string cache_file = "TbpCi_TestCache_ExternalRef.jsontc"; if(!p1.SaveCompiled(cache_file, false, json_file, false)) { msg = "SaveCompiled() con referencia externa fallo"; return false; } //--- Recargamos: debe volver a abrir el .json referenciado + aplicar la cinta cacheada TSN::CJsonParser p2; if(p2.AssingFile(cache_file, false) != 1) { msg = "AssingFile() del .jsontc con ref externa fallo, esperado valor 1"; return false; } string symbol = p2.GetRoot()["symbol"].ToString("__MISSING__"); if(symbol != "EURUSD") { msg = StringFormat("esperado 'EURUSD', obtenido '%s'", symbol); return false; } return true; } }; //+------------------------------------------------------------------+ //| 12. jsontc round-trip preserva arrays anidados | //+------------------------------------------------------------------+ class CTestParser_JsonTcRoundTripNestedArray : public ICiTest { public: inline string Name() const override final { return "Parser_JsonTc_RoundTrip_NestedAccess"; } inline int Importance() const override final { return 80; } bool Run(string &msg) override final { TSN::CJsonParser p1; p1.Assing(g_parser_roundtrip_json); p1.CorrectPadding(); p1.Parse(); const string cache_file = "TbpCi_TestCache_Nested.jsontc"; p1.SaveCompiled(cache_file, false); TSN::CJsonParser p2; p2.AssingFile(cache_file, false); TSN::CJsonNode tags2 = p2.GetRoot()["tags"]; if(!tags2.ExistInArrayStr("major") || tags2.ExistInArrayStr("no_existe")) { msg = "ExistInArrayStr sobre datos recargados de .jsontc fallo"; return false; } return true; } }; //+------------------------------------------------------------------+ //| 13. Comparativa cruzada: jsonasm vs jsontc deben producir el | //| mismo resultado semantico partiendo del mismo JSON original | //+------------------------------------------------------------------+ class CTestParser_CrossFormatConsistency : public ICiTest { public: inline string Name() const override final { return "Parser_CrossFormat_JsonAsmVsJsonTc_Consistency"; } inline int Importance() const override final { return 90; } bool Run(string &msg) override final { //--- Parser base TSN::CJsonParser base; base.Assing(g_parser_roundtrip_json); base.CorrectPadding(); base.Parse(); //--- Via jsonasm uchar buf[]; base.ToJsonAsm(buf); string asm_text = CharArrayToString(buf, 0, WHOLE_ARRAY, CP_UTF8); TSN::CJsonParser via_asm; via_asm.Assing(asm_text); via_asm.CorrectPadding(); via_asm.ParseAssembly(); //--- Via jsontc const string cache_file = "TbpCi_TestCache_CrossFormat.jsontc"; base.SaveCompiled(cache_file, false); TSN::CJsonParser via_tc; via_tc.AssingFile(cache_file, false); //--- Ambos deben devolver el mismo valor para varias claves string s_asm = via_asm.GetRoot()["symbol"].ToString("__A__"); string s_tc = via_tc.GetRoot()["symbol"].ToString("__B__"); double b_asm = via_asm.GetRoot()["bid"].ToDouble(-1.0); double b_tc = via_tc.GetRoot()["bid"].ToDouble(-2.0); if(s_asm != s_tc || MathAbs(b_asm - b_tc) > 0.00001) { msg = StringFormat("inconsistencia entre formatos: symbol asm='%s' tc='%s', bid asm=%.5f tc=%.5f", s_asm, s_tc, b_asm, b_tc); return false; } return true; } }; //+------------------------------------------------------------------+ //| 14. GetStep / GetStepPure consistentes para un entero (2 slots) | //+------------------------------------------------------------------+ class CTestParser_GetStepInteger : public ICiTest { public: inline string Name() const override final { return "Parser_GetStep_Integer_TwoSlots"; } inline int Importance() const override final { return 70; } bool Run(string &msg) override final { TSN::CJsonParser p; p.Assing("{\"n\":42}"); p.CorrectPadding(); p.Parse(); // root(OBJ) en 0,1 ; KEY "n" en 2 ; INTEGER en 3,4 if(p.GetStep(3) != 2 || p.GetStepPure(3) != 2) { msg = StringFormat("esperado step=2 para INTEGER, obtenido GetStep=%d GetStepPure=%d", p.GetStep(3), p.GetStepPure(3)); return false; } return true; } }; //+------------------------------------------------------------------+ //| 15. Unescape (string) resuelve secuencias basicas | //+------------------------------------------------------------------+ class CTestParser_UnescapeBasic : public ICiTest { public: inline string Name() const override final { return "Parser_Unescape_BasicSequences"; } inline int Importance() const override final { return 80; } bool Run(string &msg) override final { TSN::CJsonParser p; p.Assing("{\"k\":\"a\\nb\\tc\"}"); p.CorrectPadding(); p.Parse(); string val = p.GetRoot()["k"].ToString("__MISSING__"); if(StringFind(val, "\n") < 0 || StringFind(val, "\t") < 0) { msg = StringFormat("Unescape no resolvio correctamente, obtenido '%s'", val); return false; } return true; } }; //+------------------------------------------------------------------+ //| 16. Parse() detecta JSON malformado (clave sin cerrar) y setea | //| LastErr() en consecuencia | //+------------------------------------------------------------------+ class CTestParser_MalformedKeyNotClosed : public ICiTest { public: inline string Name() const override final { return "Parser_Parse_MalformedKeyNotClosed"; } inline int Importance() const override final { return 85; } bool Run(string &msg) override final { TSN::CJsonParser p; p.Assing("{\"unclosed_key : 1}"); p.CorrectPadding(); bool ok = p.Parse(); if(ok) { msg = "Parse() de clave sin cerrar devolvio true (esperado false)"; return false; } if(p.LastErr() == TSN_JSON_NOT_ERR) { msg = "LastErr() sigue en TSN_JSON_NOT_ERR tras fallo de parseo"; return false; } return true; } }; //+------------------------------------------------------------------+ //| 17. Parse() detecta JSON incompleto (sin cerrar el objeto raiz) | //+------------------------------------------------------------------+ class CTestParser_MalformedUnfinishedJson : public ICiTest { public: inline string Name() const override final { return "Parser_Parse_UnfinishedJson"; } inline int Importance() const override final { return 85; } bool Run(string &msg) override final { TSN::CJsonParser p; p.Assing("{\"a\":1,\"b\":2"); p.CorrectPadding(); bool ok = p.Parse(); if(ok) { msg = "Parse() de JSON sin cerrar devolvio true (esperado false)"; return false; } return true; } }; //+------------------------------------------------------------------+ //| 18. Parse() sobre JSON valido simple retorna true y accesible | //+------------------------------------------------------------------+ class CTestParser_ValidSimpleParseOk : public ICiTest { public: inline string Name() const override final { return "Parser_Parse_ValidSimple_Ok"; } inline int Importance() const override final { return 90; } bool Run(string &msg) override final { TSN::CJsonParser p; p.Assing("{\"a\":1}"); p.CorrectPadding(); if(!p.Parse()) { msg = "Parse() de JSON simple valido devolvio false"; return false; } if(p.LastErr() != TSN_JSON_NOT_ERR) { msg = "LastErr() indica error tras un parseo exitoso"; return false; } return true; } }; } //+------------------------------------------------------------------+ #endif // JSONPARSERBYLEO_WF_INC_PARSERTEST_MQH