From de5638076c3be960fca6a660fa392612a19dacb9 Mon Sep 17 00:00:00 2001 From: Pipeline Agent Date: Sat, 15 Aug 2026 11:23:29 +0800 Subject: [PATCH] =?UTF-8?q?develop:=20=E9=87=8D=E5=81=9A=20fault-log-analy?= =?UTF-8?q?zer=20=E5=88=86=E6=9E=90=E6=A8=A1=E5=9D=97=EF=BC=88=E9=81=B5?= =?UTF-8?q?=E5=AE=88=E6=A8=A1=E5=9D=97=E5=BC=80=E5=8F=91=E8=A7=84=E8=8C=83?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__pycache__/api.cpython-310.pyc | Bin 8177 -> 8143 bytes .../__pycache__/config.cpython-310.pyc | Bin 0 -> 3199 bytes .../__pycache__/models.cpython-310.pyc | Bin 8250 -> 8398 bytes .../__pycache__/parser.cpython-310.pyc | Bin 4435 -> 4782 bytes tests/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 185 bytes tests/__pycache__/_bootstrap.cpython-310.pyc | Bin 0 -> 682 bytes tests/__pycache__/test_api.cpython-310.pyc | Bin 0 -> 5678 bytes .../test_classifier.cpython-310.pyc | Bin 0 -> 2366 bytes .../__pycache__/test_cluster.cpython-310.pyc | Bin 0 -> 4525 bytes tests/__pycache__/test_config.cpython-310.pyc | Bin 0 -> 1743 bytes .../__pycache__/test_filters.cpython-310.pyc | Bin 0 -> 2793 bytes .../test_fingerprint.cpython-310.pyc | Bin 0 -> 3281 bytes tests/__pycache__/test_models.cpython-310.pyc | Bin 0 -> 4097 bytes tests/__pycache__/test_parser.cpython-310.pyc | Bin 3556 -> 4506 bytes .../__pycache__/test_pipeline.cpython-310.pyc | Bin 0 -> 2151 bytes .../test_root_cause.cpython-310.pyc | Bin 0 -> 4334 bytes .../__pycache__/test_storage.cpython-310.pyc | Bin 0 -> 6281 bytes .../__pycache__/test_workers.cpython-310.pyc | Bin 0 -> 3055 bytes tests/test_api.py | 130 +++++++++++++++++ tests/test_classifier.py | 48 +++++++ tests/test_cluster.py | 89 ++++++++++++ tests/test_pipeline.py | 56 ++++++++ tests/test_root_cause.py | 87 ++++++++++++ tests/test_storage.py | 131 ++++++++++++++++++ tests/test_workers.py | 72 ++++++++++ 25 files changed, 613 insertions(+) create mode 100644 src/fault_log_analyzer/__pycache__/config.cpython-310.pyc create mode 100644 tests/__pycache__/__init__.cpython-310.pyc create mode 100644 tests/__pycache__/_bootstrap.cpython-310.pyc create mode 100644 tests/__pycache__/test_api.cpython-310.pyc create mode 100644 tests/__pycache__/test_classifier.cpython-310.pyc create mode 100644 tests/__pycache__/test_cluster.cpython-310.pyc create mode 100644 tests/__pycache__/test_config.cpython-310.pyc create mode 100644 tests/__pycache__/test_filters.cpython-310.pyc create mode 100644 tests/__pycache__/test_fingerprint.cpython-310.pyc create mode 100644 tests/__pycache__/test_models.cpython-310.pyc create mode 100644 tests/__pycache__/test_pipeline.cpython-310.pyc create mode 100644 tests/__pycache__/test_root_cause.cpython-310.pyc create mode 100644 tests/__pycache__/test_storage.cpython-310.pyc create mode 100644 tests/__pycache__/test_workers.cpython-310.pyc create mode 100644 tests/test_api.py create mode 100644 tests/test_classifier.py create mode 100644 tests/test_cluster.py create mode 100644 tests/test_pipeline.py create mode 100644 tests/test_root_cause.py create mode 100644 tests/test_storage.py create mode 100644 tests/test_workers.py diff --git a/src/fault_log_analyzer/__pycache__/api.cpython-310.pyc b/src/fault_log_analyzer/__pycache__/api.cpython-310.pyc index 45aea26cf6db9933943c1cbc96f5a0917f5eaeeb..c14f5d4a14849a300fc28339152e005d43397c24 100644 GIT binary patch delta 3107 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 diff --git a/src/fault_log_analyzer/__pycache__/config.cpython-310.pyc b/src/fault_log_analyzer/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2934e408193ba92de23c183d9fd497f22394df75 GIT binary patch literal 3199 zcma)8TW=f372XT)qNqD{^Chv}I81F)aoV&F1huq8yMiTAA*rg}A{eYWD~a}!OV2K? zij`2Ka-+Dd*!@Uw%vf7t(W>v+ZX|NQ3DKfVj&X2N#LroH00-fNe~E??rh>oGof z%Urr;k{gcWftq#VTI`O++-C2R*|5EfwzGWEteN&&(_%c~8!t1bQ70AJj9}DFGjo;q z`y+xAaN;@jO1T-6LW-WFX1E#8ay>VbPG?+J_QweVBCT{}%CbD^`Wlan+h+9!H7EKoy{kcllxCM- zzI1C0yQPa-e(XzT*>l)hGip~{?`O#T8$4pUL=?wu4z5()i%0AE#TuUU?Cb{bhDoJH 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 delta 2754 zcmb7GUrd`-6#wqm7E1rEFh)yDR|N|D1`aiBGM(-p%s*p*h5dDoAzy()2ixrj%O*>+ z#Hn$AOuQz>L>G85*@F*rCMG1Bs4sikgDzR@gNqs;eDdAI;Q5_)YwcR1kni{1d+xdS zoO^!foUeCwuk7;9dA%-;{u~D~(;YXxn{_dzdi4FbuINJ|W21$=nkLdF4Hh+1o7d7# zdW#udx=d}DWJ}tzc0nwPC1L8zLWUQGshPG*dZT9A1GHl4OSVO^EH3Da`m`M<$W9tL ztg7j1({YvMOHRy|#js{n4P-UhK-T_PU0wCA$_+G>>j{h+b=MwKe#c<2j;4NL7f#_4 zetKNOCp;~)slyM^GBCaSYOSOFPdTF>vwR@wYmxH$wXjhlLZ`E-CT@z#T zDQna^cRC^a)Z@Me1NX{SpB)F`~YA8FbF8Q zH2j%_95+8-yAK9O$lexfZWMV#3G~An`mFwJwd!AKjQDV740q3ah0!SBbppdJ`H26$+i-yDE-hDjXiT}DbONJF^>JxNb7=yDfLrmXA8WQI{>EuB}H?ZWOvka;qJW; z{gG!#c7wAupR7r2EM#n@xZhS^gr7KBz9mUI!thmYNgNVybME!f!$z=uMjg-FgcFeB&b bhfqMh|5$6-;}?3Ap2qwWqj!LwjrHOme@gS2 diff --git a/src/fault_log_analyzer/__pycache__/parser.cpython-310.pyc b/src/fault_log_analyzer/__pycache__/parser.cpython-310.pyc index a83861ffa32a2da3c00c754b23887a9ccab49a63..c123b24545a517f118220cdfe9c88a1da65f2a54 100644 GIT binary patch delta 1745 zcmZ8h?N1y<9NyX8+uPf{J1FuFma_%ZlQ(H;Ya(KWs;#ERKpX7EdagHnWa0LX&d!05 zBUh#Rsl<{sHt7e2*jP=BiNRQcHvSPNepo+RUiw92jEUds?>C1hVJCU^nP+}8znN!V zem?R|SG*9584`SFe=Se7HmtQ>GhD zMcr5`2AZCV+v;i!jjWMWtzZVQLxP#W>I91ddrYtxuzDI#HPD(=Bdtv}(O%j_o3E>> zX4*&FXgjd}7TQk}^a-G?^l92jyMVRP0eXxkfwj})w43$-dz>ocQu4&gKhM|8fle)M zbH}v<9dV(MUw?tD%T2AYKGqHpDAV@*R}wn{{O_y2slKXn+v8q;U!Q+T9VvHWj7&LG zfMf{3e;;xbM*|z@DPSqQD53rxYqLlSR;1RjjQMn3qxmTpHon-t_5Ie3&7IrV6PGTICZP84)^~sWc;n%FKb5yWSWe_EXV&hw<}79>w(s5B z`S{DAyx>#uij*3oc z%S%IOFH}4Yz>wo4O4w7_7z)o?xuVTN{vCDTZ~#)NvP6x-VmQ<|EHo=G$?T;6mwL1# z!j&l%)X9Tb7;vCEotPU5C z!-RM5e!l(b&8<7PckV6=XZ`!3(-)-O*>k5yCZS!D&U|zRyp;JuvNkXr&&hk-%4h6A zaq>I}Wpf3qvim_S%L*>q2?6emaG4#00sKL&s}B8Q34kEvapughh@aIu%iXA`+Mm{wtyGRO4wXn0H^t$x~AXbywUY7rVRLh}km1_#FU|1^@PUPc{`v&5c5sD`l@ zv1%gVy-79&Ds?hEP$q5868`IE7wPk7%~8_nZ<;+B%+2rxVLC>eYzVoyaX4(O1a87z!r=l5$0VraFKa zN}P9t+b-M^?kw0J`aSU$Lf2o4*ORsb$&8_#iwsS6o0_|VD2+ZcsQ#atjs&HuforOY~ztCbt60B460kb#PahJ~tXPem0r zwpCSOuDWMHRZUc4E4EZs8`Z->IGv`2OgbI-)3t}n7qD4)CMF;r4UNywIQhv+a(oQ} z%`H$n=ZU>!5$qzi7SCo7Mt_`~&E7`+ErfRvG6)vJWrPWYHxTfd6fX-LPzqFRArMJ; xv(Lu=_e&pw2^4eoFuu1QTA@l&qQmj8g;Fo8ieVU}Qd9+tp%`H(9ma`x>|gT9l!yQT delta 1388 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 IZJ-(dAFZLD^Z)<= literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_classifier.cpython-310.pyc b/tests/__pycache__/test_classifier.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a2d0844d67174c89808112447aaa55ae3dccb958 GIT binary patch literal 2366 zcmZ`)&2Jk;6yMohd)JObf(dO?pt!V@T9BAj2ysBDQdAIvI#Sw`FUxjk>};}M%#2%Q z6@*YCm5U{gNN`AiOYR&HIq-iBN7{z`0X@J4-kWvee0f*%X7=sOoB6%>es9vr$*KX* z)}LRmJ}DcR1HzR%FMePDzjLs2n>T zCw4n7d{59ab+$;S@`*uRTG=vag^|_jrq!8XHCkOW`TLA)Pwp5nm)z5>Sv!DTjds&i zF7+joVa$|^GEWoMBx>qozaPoP$QL5)hK%Dxt@=Fesfr%mU(c8_0uv1l9~{@9EkYC5 zK}gt*n$)89h9Lt(Cb2Wp*=Fy0g5zv>YBd zhH*f^3>>H|^DybAO-I$bcr-6cdtOMDvyuvlgNbgK^cc^0m`GKM*h3a6CuTzUJ*Fno zAn0?>sOL*n4Whn~jKiv?%}ZdP+s{0k5*+RDO=MPb5P zk5axSGCyFV^=|9$Kfc@F`}N6hdryCP{OrfypMLxJ+2E%ZXIUy*`m}R!+BrY*qxGEe zmSjQ-Exq8#!}Hm?a$|puAu>!If!mv8^iIRL{13kX>Hyk_2LarMCf))@eTh2Mg}zKHv&ix&G$?x*25h&)ky;!NswN{EU#wUw2ov8pUB-Tf?#B1R!YUjZxpRS@}$N9sn#Q^-icU&o(s zpuprkhHgq96g^o$Ln|6u9id$t3vIOG3@#T;Xu^L0gAp!jpO+C>ss<)V5<*@a%MG{9 zFX2&VQ2_4^ei?*v{bXIP0JD`DEHsIx2Kx~k8em!)J!8lK0t_&vMS&4J@DB6eZ0A$`WGzV_C5>Taf&zqzrD{c@ek$5f zfB98V={!9*hLc4M(;|Y`V6Fz`l1n7NIu=wioPP!b7C;@pWf(|f)6g8&2p~SdZsaep zy3kmlu}^49pediwIG}NlSYK)LD~Jcfn@=wFfva7BAE$hs-#}e`F-;N{$PhTpS+_43 zRrTOV=M8oFOp@wUfJs5xOTyfL5J5p0dbDX3yl;R>HP!0_4ZOflXo&aQxqh6{emphv zEm$tF{FX^wzZXb;bF5QOEp9hm<$9hbi>Iod7pJrzG0#&q&%+v2{NZnceSQViAZEAeqyo2CZt=J literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_cluster.cpython-310.pyc b/tests/__pycache__/test_cluster.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6110c7b584a18a24bfb60738f97f92fa6ea0d943 GIT binary patch literal 4525 zcmb7H&5s;M74Pbv>FJ%B{qzD}W5UXbk__NoI}k{~vK?$E5MTnf70{rl)!S7w?sb1m zs(YMxv=*RUDO^_K58$vyT=t4MAadaUD2|Yiy&*+hatXgz-SfFK#OYDJ>3&^Z_1^D& zR;I17zd{?A$r&t_;wX3LDMmc@u~g{7$6 zDo1w9jw-E6O`)Sho)Vg=^vTxDe#YOjZj2jr~1(3|xGxki*xVgtN#w}jr zrOe#5c=@oz?L+&H(Wo?)+3u?Ht=qTXxE-8S4C931OKfCk{UB~94O`XQw51y+9XH^r zaxY0Ux~a8;xFcj&263j!p?DxdRf$BJdL5x^iSPHM6x{W)MoBJUO=%J#A(hq3{D!40 zAR*5XqeP1y8}ck3{`(flHs9_BT@eD9?ROG+Kka(HNVhL-fBcU>eDwZb|NXc3Klt-| zAO7+0AN>Bk4-fyeEk!p;x3!!XA?HOe_QHn%cRLelmTKzyxf!;)50w*n_XR1mf~`^_ zCL8@$nEk0d2d0lPaghesG1BuO!XWo-X?zcimPPpDzr@SH)mCTjV(Tee`-JV_A|%Qf z+FUK%WX8U6#K>G7lbhV?;Quju#P-;c-eiJn?ioi`R@yW7881CH8BBdt&TL-h_5qB3 z)7WSG>@lOL$j!_=#dZ|)Pe_RgL5M*?n9?AO&BVzGxS6FAU*u zrBsA%WjzqSvXWjlE$!NA3CPf?19FyqnXRz%tpDtjiGB**% z|M|HJOnufw@^V}^_9EfBs_ME?!h0dz*Ik!z&fmyw65-NNj!B8+SMh>IYJ)h2!|Jx? z%?++kp25WV!A1VvD)S1w+EMGI%MG1B?+ml$1oRYfw6l(~r*MNfF^~)qWkEsr5p4t5 z8&*@2%|F7x^`C3UW)3_uBfLKceo{lVq%jXTpbp@zWT#Ikrk-N`r)SBRJNj_`7ZBGX zp&_mgcpcc~TwM8G>?gklQV5Qx?-A=IB2)4%YI#v^b`zxTZ_@Mcz7j?#06KUjS?mAHt;^)r6Uwb8HygIROdq^JMjadmeI~ybmcKS|dHU-C;v7sf>9^;mjXj zlV67j$ELDG7fM7y?51AS4MlqL8LneX4po)cF;o7K$jGExp;fIADpv5xN`NOfZ_IpY08USBA=?xePSoyaWJHioJpPYtxw z7JdbvXbYbs&7PUIFljdZNfRB$RI0d18~=pJ&xkaMjGSwfFRtMQC76^BJUU;Lol?Cl zi9a`E$nW4SO4%w2H*r*$4>DCKY9R(8bVR9uO(y_LQoofafW<-J?|7Miujn9@>5DN% zbd=<7GBMlL;r$xc(oot2D8n`mWudEdl88rglc zzmKRhGWVzrwsgoKun2*`2*Pz#a9q>+) zWGT)ZUFG!RAR|4;PPOMo=j6fpbF1)<+}X(S^Y*4fca+4{#*B~9XQNo>hh{Q>a?Pt5iVr`3{q C`y{de literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_config.cpython-310.pyc b/tests/__pycache__/test_config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1bac8dc39ab476e82e1d0d7ffb3073dbe913381 GIT binary patch literal 1743 zcmZux-)|c=9JlXBa=9dHmzDt%h-n}}B$i}D2z5eqp$es1msYt<6A_Yi#7=t2UG8RI zRyFE`#3qfGiHYYuw1}6zBZSxk|A%=&8XM~$fW&wJzP_aGZOkWs&Y!RCufMiGUx&q_ zjiB&fza0EyBJ@y^bTNQjgeqD96jAJ>&h(Ev7`WzZf!@)vvgy7Nm>mBdRB2@7j z07Gi|jz%%nZlR7&b!xyLmUU1$BkeX5@yri~;fF=}u%NC$72g8HXn-m9U?Wt&qkr=O zMzOZ1@1q-d7ws9lcwkavU&Cm|GSp15EX|~tMYAcEqgIO9G^f~AI3@$Wo5_JW=o}Is zZ(|Gxg3)K{SP(>SH~+RnQDLL&9uG(D`*1W}pSgTu`C(sFxVt^ElUC0i`*9yO0p-c^ z0bC!FnScc$-|_CgflxWm)qbTxy>85P)wFs%3<$WEc~=O=&wNV+Z(z>c_rYV z#9`!h<*Af4S!-s`cm1)587C|fa^a8!-iQb{hUE9`%GGH7rPaubnD32Pbvxuk5xHF^sfp6 zA4x+SWN0D9vb30D7Cn(-xs>>{flcS=d|#XO=XbS1VIQRQ0*ST%C$W>wNjBK2sQaGi zwaWVvzbB2Q+EPuL!H&4*Pfl;eade?tW#Zh0rSrAgyT^v}i9s4MAG2~+E?%mCa;Z+% zH&z|es$V7Tjm@bGk`PA|F(RfF2wulO@teiWuBIDX@QnNs%QS1y#BmIpYhc!o6;g= zBeGaNn$7~@B7wp8QTn8>>I4RrU z{EIM*MaVXG2^-Qo1*d3E}1fX>KaqE@EaIeU(hExGUzsfBqNKpJYD f7X>G+52t(jNTsAuHw6)D1zgZdx^=pMjeq|Gu|dQg literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_filters.cpython-310.pyc b/tests/__pycache__/test_filters.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5fb0deb34fcf17b9dea8b4c296fbd5d220ff15a GIT binary patch literal 2793 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_models.cpython-310.pyc b/tests/__pycache__/test_models.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd21e2012e1699a5e7f2b71f9fad5c0ac34044da GIT binary patch literal 4097 zcma)9&2J<}74PbgnVv7(JK1a!2#b|0kP*zLRx?%OZEsKa zq`GDkkGw)?BPBvu;)n!?al&Qq91weeKLE~1#SyZL51gW0ECl>s^?cZ4dr9~7oBHne ze)W5=UZ=HM*}^6N_S5YfRm=K2RgPXRDp&DjzkuKtXR+1J|5=-%ZpU`dZreSl?J#P0 zVz=kDJ!ZXXahH4VSlknAyKvWO7tvDSMYI&hEk0U&UP4Q0+)_eInOD$K8Ml;0g;#Ic z@@2tB)qM-^U~ibGQ9FP}R;yuapNC4Qs3){fa+oHf!E~XQauH`88g=xtQMrmIy96OD zvZ8Hs#_e~kw!h&yrO~id6vkt@5$wliG@m#}`JqMwn zAGdL1E$-ee++n-ch$(yI?6Q5vy?vXI!n%dl!2xC-&}s*?4U$TS=dY|@dA_;2hTrDe z>IVB%PKlVWKQRGzvK`epjYFLPnBK+T9VM)QdTMuV<}P zwZIdgH;j^QD#866G=rR6qA80;-N`b+Z~=`Oy`sz(*b*CFJk7e<@ME^o&U$!^ZC^nF zHdu;{z`T9j#-_Y(&DHI^ZW!aVhUFwx&9F(7*H**P)tN$*EJrM-D91vj!F_o)9 zK*Y^^Q1=r75AQR_2RG`YPL1ZVebeHw_r|)}h;t zn`?*8&E}f6yM67gZ*1JyI4tofyS3aM#IbP!N%2S$WG15AWAbs7wHX!AsG-xr0#4ZP z4;|H9J8;n5sOYNk`v86)L|hkdrf_Z)D%~jA60$F&L}@P;w?(XrJ&|SMme7^7(-}x9 zcn~UG&KFQSeWA;pc#tU}%~FV2x1$aPXo>JcOa>9R)x>g{UjS+I+aCvaGNgLf*!Q6{4|NDNxVQ}MsQOyEWd~ja)FPC ze7;p>l3LDWxcm%y!R8C3$3~i}YvCXhXVaPDpP}=6D9q@Ll1*o{A+*2entF-O-#vTF z=G8sd5ZRd$*_{&E16d1GB2%q6C9bR z_2xIqfBm5UuWxD(>`K(3Bf!sd#OeU1@c^f82kl#b`7^HTKfDU-UBfcpj3+`BdIFo- z(H=tDKxhXn9zM}Zm(78<9E+}6PLpLWh=&bZUZr<`2?F~>R03ZK#bC_cH0efMBt%)v zFr@=JfQ>^nDmiIOQ}DIFJ{~2Xr%_)eLAd8MT}KHTiA26iZQp{J(o}Zj*G#wUoFMAc z$B7!e6{(xSM4vUx%7|DK+Ai5uX8)(~Z@-k2wN7OH^64a*W9#viPvLG0?>UvbFQQ|W zI=@bWYG?8`Be+jS=J||^`^dZsyo88d)=!f9H`GlbzeDNVl;kIKFglnG%SoFV%ZcHJ z$<`A)?mkZ0Z7*l=I2SWS9MEC&@PnT{{PnLM+~1eaqNe?hj8xPKV{F-FYH;cN z(#3wL5Z)5)XW$@)>`XF`l8I%T2!c5U3>ge9Xgatzj!)_cpcDrt2?w1|hKDbs(Zqy0 zyU2#modrd{m$T4&>^vjVo(>J81oaa@@*hmyoBDmmr}rUn?e+arW@VLMOtWV{&efMj(M< zqoVN`|495UwvOZ)mq;ta5B4^a(h_}#Wh(6TbAp&STNlL$$4Nz7h7JF{<~ z!vIN9KF>^%o4S(PDf{h(Q4$1=8uH(t;E2wU`1G|uNFoFcRLCv!ET^73F6lJ+*ct;x zMqCS#I#CvEQUViS%`1%B+NHOgrk?g+%#-ey=))tUBpW)~ORQ?wo%&M!iTY!Hy~^DG F{trt+-_`&C literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_parser.cpython-310.pyc b/tests/__pycache__/test_parser.cpython-310.pyc index bf24c1d26132acd81997caec092961205b721ae3..a58b64d98eddf5c96a098d1b746589a429387b07 100644 GIT binary patch literal 4506 zcma)A&2JmW72j{JXzIg?>%?u^u30+`3sWRz*^yO2m18@pi-dI@83>4?ixp=mt+XG^ z%u=qm5_(90^b(+dfDdudi|#oT4bb-Qm|hB`Nvm_viw{ZOo8`AiwWRiOXLe@x{ocIy zdv6ra&gK>P{N^9O-}}6vD4%0v_+vn06E1!Lf+$1{lt%WeHdJV9ffniwJv16dXg18y zYFMiBrb2XLe4r46s(ZQj^@a^SCb6K$>h{b)Pmb8oV|RP<^c_WX28~U) z_+1cGK@1Izs6_ifY3P~O4I|UFVP+aOEHXp#@H|J(kpleMWR}dq?+lrLM=33c#Vt_b z0cy6@)26Ay^1Tnf6YY2hF1`XH0bK8^N9v*Sv3jWPX+(RbsY{UAb>;sr^LEW3TJ;*nR9LyKXuIFXT1|Tb+aNf|?djTXtz|(sWC|0xM0mv<|j4 z`nJ{U{<{}DI}Bf5GNlz6#sxcvViv_5ig^@hoJ_m`cVba01i+iSE~m{nA{>Wt3j_iI z7+0sk7@0K{$ycw!Qd5zj2X%d>>n8zv!QbEf>+26JiRc8O92;Y>gs~fU*hEO@haiQ1 z5cvE8tngJ2(W07OIU7+nX(}N8-Kl^i1ic=ZFhXe;Z~gPtPd8O1+ZcTe!9?8`hIEg- zDCU=7A&%i^tDyLg&Q@H`n5s9s2+KeqrY{Tv(dh-E)RDaiBiKtQru#}W4#PNdTQLj0 zolCJF-%h^RDaNo5PD z#+YYclBV>d126E2%V^SJ5qBe?jUtTZNNZrxYtt+`Y2t|Xs5x*~#*3?PgFGB4p>m{# z>Jczvq8+Np$;3F+z{kyx3}8(iFdB#Yo|*BabQr77&=dg9Mv#~kAP0as8(i{YQ$;iwx7<3*-PP9(%bwF#JC%Y`}?Cf5J(K7rO)Qj3#7Me7< zUdjvGbt5mNt}F7c8^)v);C;b$kvCn6NF|gs)+n1zkRE!h?|y?(-&G{2GY=KV?G7%8NqZIo^tQMm8Vq-rxM6LjyNo zZZ;GOB=^)hQWND6)1)R7qc)Nz(H)F_($w}0Vqr@hiE*vz$1#!icp|y*^R=02-K1h~|%$A+qr3CK0v-qtXk~ zhHDOwLz?XR(T=n25mtgnC4CHYNz0-J3!HI?mnB4LP?!D|&6WVp7K=l-Yu=y2EXmRZ zM7MMr-C2`IprT{-p>9AU13JN-dUcj6*=5w7%)UDTw@6d0|E}28wYcUKiO=_)RwoFG z*PNo%4SV0Xy>p7j_4mf zyMZ~|9+Vtz%clY1KZ8je(>(;XETX`e<`wrjX=bGA{)G3=jC#mMjfn~H&gZ83-2ztuk*o!Z}i ztl%37oJxjimuQFm{UmyJ-zA82?M@s;w3+xosTj&o2`%BbR&&}hOPtl}YSn3Zen5%i zwGzsl>kbqV%%?mx*6%;qNX_;7qqjGXHHJC}yIYq;KrNV^gdXx8p(pKN$Z#?YF3K=~ z!$yGod*CtO+YTsy2$SUb!2stk^b79+_KYV_%^NYv2vcGqH(YpwIn=%j>-F?{*CCzS z@^!j$dk-tyHT$TcpeP ztP1aG+v)h^T?B}>vJsp!y0N3+aQJjwS741vxG>YgHV-fwb@me&D@>U|g_WgF&>C5R zy$+&_01lFP>iBd{=7U1LT_*#>a?+-x`%JYFlE0$F<{3*JZ-{* zU%{0fBxQk9Fc|h?SrvMIb? zxkWmw#4l`A|BeS7g=LitS@|ZZwlMHzlXMjJdREkJU{T6Zcc{d)m$XHF@yg;=`+Pw) GzWEmXi6D^x delta 1367 zcmZuwNpI6Y6!v(_WOI{H(k&`Wn<4`(EoCXC>`1_&0SP3K5wfD#(6n|E#tusl6a*)3 z)rd<45*#W~mE|8GE?g^dLzWO1E?c_vtj zElks=k`V=qYDK^jS+~T%LaiuRqU)Bpov>mvB0FIVuaj>CU?*1vD-NK?SFxr7Us1F3 z?+WA)AEJX0{7f(+Rvi2Zex3~SMQND7C%Yt>Df}zxO)ZIm?AgxkK(zcNfoJYaZvI{D8npyqk_zP;+4>z8hlnMKDX_34f`*Vn*tP-f{FKzuhVF(8 zKuLzo=J{)}lg~*5mstncfY6a({e0om*(+*D%R1rvKPs>VY2WsI)2>xZQ>I&XoU)h3 z&~AX){&ilJUzc}?EXs$K+*Pb{73ZZx<Kuk$YGT|wus_RxO=IttTi~e@Z z%>t~Kzo^{mKt7$Yu!7Akcd5Okn~$kuQbDKuMI?VQ%r)nkP0@%7X)S^iZx)B`qPd zJSrr!enH?rl5dCc76-KGc{cMeRiLdkDO~SPAo(?C6EP|uWce>GOUC(%_F?oW82+|B z4*S;FwVTN7no}&Wp3ST3M6f z%?o|yS^GS%rG}jrZH^<=1j1>At+Y7;j$`mPwNXJEvhKquU)Rd?)@1Twq~02NZC QQ_M)2j!X|tCy4y}4-5AQkN^Mx diff --git a/tests/__pycache__/test_pipeline.cpython-310.pyc b/tests/__pycache__/test_pipeline.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef9b2c0531e1bcde04a553139916d83f35b39736 GIT binary patch literal 2151 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 TSv*y%TB}#<)2;`q|JT0(0o^lG literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_storage.cpython-310.pyc b/tests/__pycache__/test_storage.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f0ca169c1cac2c49323a4e1607e3eb76892584d GIT binary patch literal 6281 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 aQ6X#H6}IMHVJl(BTkWo1iPn0|fA(*YQWR4F literal 0 HcmV?d00001 diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..7902550 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,130 @@ +import _bootstrap # noqa: F401 + +import json +import time +import unittest +import urllib.error +import urllib.request +from datetime import datetime, timezone + +from fault_log_analyzer.api import FaultLogApiServer +from fault_log_analyzer.models import FaultLog, RootCause +from fault_log_analyzer.storage import ( + InMemoryFaultLogRepository, + InMemoryFaultTypeRepository, + InMemoryFilterRuleRepository, + InMemoryRootCauseRepository, +) + + +class TestFaultLogApiServer(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.fl_repo = InMemoryFaultLogRepository() + cls.rc_repo = InMemoryRootCauseRepository() + cls.ft_repo = InMemoryFaultTypeRepository() + cls.fr_repo = InMemoryFilterRuleRepository() + + cls.fl_repo.save( + FaultLog( + fault_log_id="fl-1", + host_id="h-1", + fingerprint="fp", + level="ERROR", + message="disk full", + occurred_at=datetime(2025, 1, 1, tzinfo=timezone.utc), + fault_type="disk_full", + ) + ) + cls.rc_repo.save(RootCause(fault_log_id="fl-1", cause_type="disk_full", confidence=0.9)) + + cls.server = FaultLogApiServer( + cls.fl_repo, cls.rc_repo, cls.ft_repo, cls.fr_repo, host="127.0.0.1", port=0 + ) + cls.server.start() + # 等待监听线程就绪 + time.sleep(0.05) + + @classmethod + def tearDownClass(cls): + cls.server.stop() + + def _get(self, path): + with urllib.request.urlopen(f"http://127.0.0.1:{self.server.port}{path}", timeout=5) as resp: + return json.loads(resp.read().decode("utf-8")) + + def _post(self, path, body): + req = urllib.request.Request( + f"http://127.0.0.1:{self.server.port}{path}", + data=json.dumps(body).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=5) as resp: + return json.loads(resp.read().decode("utf-8")) + + def test_healthz(self): + data = self._get("/healthz") + self.assertEqual(data["code"], 0) + self.assertEqual(data["data"]["status"], "ok") + + def test_readyz(self): + data = self._get("/readyz") + self.assertEqual(data["code"], 0) + self.assertIn("deps", data["data"]) + + def test_list_fault_logs(self): + data = self._get("/api/v1/fault-logs") + self.assertEqual(data["data"]["total"], 1) + self.assertEqual(data["data"]["items"][0]["fault_log_id"], "fl-1") + + def test_list_fault_logs_filter(self): + data = self._get("/api/v1/fault-logs?host_id=h-1&level=ERROR") + self.assertEqual(data["data"]["total"], 1) + + def test_get_fault_log(self): + data = self._get("/api/v1/fault-logs/fl-1") + self.assertEqual(data["data"]["fault_log_id"], "fl-1") + + def test_get_fault_log_404(self): + with self.assertRaises(urllib.error.HTTPError) as ctx: + self._get("/api/v1/fault-logs/nope") + self.assertEqual(ctx.exception.code, 404) + + def test_get_root_cause(self): + data = self._get("/api/v1/fault-logs/fl-1/root-cause") + self.assertEqual(data["data"]["cause_type"], "disk_full") + + def test_list_fault_types(self): + data = self._get("/api/v1/fault-types") + self.assertEqual(data["code"], 0) + self.assertIn("items", data["data"]) + + def test_create_fault_type(self): + data = self._post("/api/v1/fault-types", {"fault_type": "oom", "name": "OOM"}) + self.assertEqual(data["code"], 0) + self.assertEqual(data["data"]["fault_type"], "oom") + + def test_create_fault_type_missing_fields(self): + with self.assertRaises(urllib.error.HTTPError) as ctx: + self._post("/api/v1/fault-types", {"name": "OOM"}) + self.assertEqual(ctx.exception.code, 400) + + def test_list_fault_filters(self): + data = self._get("/api/v1/fault-filters") + self.assertEqual(data["code"], 0) + + def test_create_fault_filter(self): + data = self._post( + "/api/v1/fault-filters", {"name": "disk", "level": "ERROR", "pattern": "disk"} + ) + self.assertEqual(data["code"], 0) + + def test_unknown_route_404(self): + with self.assertRaises(urllib.error.HTTPError) as ctx: + self._get("/nope") + self.assertEqual(ctx.exception.code, 404) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_classifier.py b/tests/test_classifier.py new file mode 100644 index 0000000..b9a2241 --- /dev/null +++ b/tests/test_classifier.py @@ -0,0 +1,48 @@ +import _bootstrap # noqa: F401 + +import unittest +from datetime import datetime, timezone + +from fault_log_analyzer.classifier import FaultClassifier +from fault_log_analyzer.models import FaultLog, FaultType + + +def make_log(message, cluster_id=""): + return FaultLog( + fault_log_id="fl-1", + host_id="h-1", + fingerprint="fp", + level="ERROR", + message=message, + occurred_at=datetime(2025, 1, 1, tzinfo=timezone.utc), + cluster_id=cluster_id, + ) + + +class TestFaultClassifier(unittest.TestCase): + def test_pattern_match(self): + c = FaultClassifier([FaultType(fault_type="disk_full", name="磁盘满", pattern=r"no space|disk full")]) + self.assertEqual(c.match(make_log("No space left on device")), "disk_full") + + def test_name_match(self): + c = FaultClassifier([FaultType(fault_type="oom", name="OOMKilled")]) + self.assertEqual(c.match(make_log("OOMKilled process")), "oom") + + def test_cluster_type_map_priority(self): + c = FaultClassifier([FaultType(fault_type="disk_full", name="磁盘满", pattern="")]) + self.assertEqual(c.match(make_log("anything", cluster_id="c1"), {"c1": "disk_full"}), "disk_full") + + def test_no_match(self): + c = FaultClassifier([]) + self.assertIsNone(c.match(make_log("unknown message"))) + + def test_guess_candidate(self): + c = FaultClassifier() + self.assertEqual(c.guess_candidate("No space left on device"), "disk_full") + self.assertEqual(c.guess_candidate("Out of memory"), "oom") + self.assertEqual(c.guess_candidate("Connection refused"), "connection_refused") + self.assertEqual(c.guess_candidate("nothing recognizable"), "unknown") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_cluster.py b/tests/test_cluster.py new file mode 100644 index 0000000..bbd44ac --- /dev/null +++ b/tests/test_cluster.py @@ -0,0 +1,89 @@ +import _bootstrap # noqa: F401 + +import unittest +from datetime import datetime, timezone + +from fault_log_analyzer.cluster import ( + ClusterEngine, + TfidfVectorizer, + cosine_distance, + cosine_similarity, + dbscan, +) +from fault_log_analyzer.models import FaultLog + + +def make_log(fid, message, host="h-1"): + return FaultLog( + fault_log_id=fid, + host_id=host, + fingerprint="fp", + level="ERROR", + message=message, + occurred_at=datetime(2025, 1, 1, tzinfo=timezone.utc), + ) + + +class TestTfidfVectorizer(unittest.TestCase): + def test_fit_transform(self): + vec = TfidfVectorizer().fit([["a", "b"], ["b", "c"]]) + out = vec.transform([["a", "b"], ["b", "c"]]) + self.assertEqual(len(out), 2) + self.assertTrue(all(isinstance(v, dict) for v in out)) + + def test_empty_documents(self): + vec = TfidfVectorizer().fit([]) + self.assertEqual(vec.transform([]), []) + + +class TestCosineSimilarity(unittest.TestCase): + def test_identical(self): + self.assertEqual(cosine_similarity({"a": 1.0}, {"a": 1.0}), 1.0) + + def test_disjoint(self): + self.assertEqual(cosine_similarity({"a": 1.0}, {"b": 1.0}), 0.0) + + def test_distance(self): + self.assertAlmostEqual(cosine_distance({"a": 1.0}, {"a": 1.0}), 0.0) + + +class TestDbscan(unittest.TestCase): + def test_empty(self): + result = dbscan([]) + self.assertEqual(result.labels, []) + self.assertEqual(result.cluster_ids, {}) + + def test_single_noise(self): + result = dbscan([{"a": 1.0}], eps=0.75, min_samples=2) + self.assertEqual(result.labels, [-1]) + + def test_two_similar_cluster(self): + result = dbscan([{"a": 1.0}, {"a": 1.0}], eps=0.75, min_samples=1) + self.assertEqual(result.labels[0], result.labels[1]) + self.assertNotEqual(result.labels[0], -1) + + def test_two_dissimilar_noise(self): + result = dbscan([{"a": 1.0}, {"b": 1.0}], eps=0.75, min_samples=2) + self.assertEqual(result.labels, [-1, -1]) + + +class TestClusterEngine(unittest.TestCase): + def test_cluster_batch_empty(self): + ce = ClusterEngine() + self.assertEqual(ce.cluster_batch([]), {}) + + def test_cluster_batch_groups_identical(self): + ce = ClusterEngine(eps=0.75, min_samples=1) + logs = [make_log("fl1", "No space left on device"), make_log("fl2", "No space left on device")] + assignment = ce.cluster_batch(logs) + self.assertEqual(assignment["fl1"], assignment["fl2"]) + + def test_incremental_match_existing_cluster(self): + ce = ClusterEngine(eps=0.75, min_samples=1) + first = ce.cluster_batch([make_log("fl1", "No space left on device")]) + second = ce.cluster_batch([make_log("fl2", "No space left on device")]) + self.assertEqual(second["fl2"], first["fl1"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py new file mode 100644 index 0000000..c7de89e --- /dev/null +++ b/tests/test_pipeline.py @@ -0,0 +1,56 @@ +import _bootstrap # noqa: F401 + +import unittest + +from fault_log_analyzer.filters import CaptureFilter +from fault_log_analyzer.pipeline import CapturePipeline +from fault_log_analyzer.storage import InMemoryDedupCache, InMemoryLogSink + + +class TestCapturePipeline(unittest.TestCase): + def test_process_raw_fault(self): + produced = [] + sink = InMemoryLogSink() + p = CapturePipeline( + capture_filter=CaptureFilter(), + dedup=InMemoryDedupCache(), + log_sink=sink, + fault_producer=produced.append, + ) + raw = { + "timestamp": "2025-01-01T10:00:00Z", + "level": "ERROR", + "message": "disk full", + "host_id": "h-1", + } + fl = p.process_raw(raw) + self.assertIsNotNone(fl) + self.assertEqual(fl.host_id, "h-1") + self.assertEqual(fl.level, "ERROR") + self.assertEqual(len(sink.search()), 1) + self.assertEqual(len(produced), 1) + + def test_process_raw_non_fault(self): + p = CapturePipeline(capture_filter=CaptureFilter()) + self.assertIsNone(p.process_raw({"level": "INFO", "message": "hello"})) + + def test_dedup_same_fingerprint(self): + p = CapturePipeline(capture_filter=CaptureFilter(), dedup=InMemoryDedupCache()) + raw = {"level": "ERROR", "message": "disk full"} + self.assertIsNotNone(p.process_raw(raw)) + self.assertIsNone(p.process_raw(raw)) + + def test_process_batch(self): + p = CapturePipeline(capture_filter=CaptureFilter()) + out = p.process_batch( + [ + {"level": "ERROR", "message": "disk full"}, + {"level": "INFO", "message": "ok"}, + ] + ) + self.assertEqual(len(out), 1) + self.assertEqual(out[0].message, "disk full") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_root_cause.py b/tests/test_root_cause.py new file mode 100644 index 0000000..6e81c8c --- /dev/null +++ b/tests/test_root_cause.py @@ -0,0 +1,87 @@ +import _bootstrap # noqa: F401 + +import unittest +from datetime import datetime, timezone + +from fault_log_analyzer.models import Event, FaultLog +from fault_log_analyzer.root_cause import RootCauseAnalyzer, RootCauseRule + + +def make_log(message="No space left on device", fid="fl-1", host="h-1", trace_id=""): + return FaultLog( + fault_log_id=fid, + host_id=host, + fingerprint="fp", + level="ERROR", + message=message, + occurred_at=datetime(2025, 1, 1, 10, 0, 0, tzinfo=timezone.utc), + trace_id=trace_id, + ) + + +def make_event(event_id="e-1", host="h-1", metric="disk_used_percent", value=97.2, minute=1): + return Event( + event_id=event_id, + host_id=host, + metric=metric, + value=value, + fired_at=datetime(2025, 1, 1, 10, minute, 0, tzinfo=timezone.utc), + ) + + +class TestRootCauseRule(unittest.TestCase): + def test_match_log(self): + rule = RootCauseRule("disk_full", r"no space left on device") + self.assertTrue(rule.match_log("No space left on device")) + self.assertFalse(rule.match_log("connection refused")) + + def test_match_metric_threshold(self): + rule = RootCauseRule("disk_full", metric_pattern=r"disk.*used", metric_threshold=90.0) + self.assertTrue(rule.match_metric("disk_used_percent", 95.0)) + self.assertFalse(rule.match_metric("disk_used_percent", 80.0)) + self.assertFalse(rule.match_metric("cpu_usage", 95.0)) + + +class TestRootCauseAnalyzer(unittest.TestCase): + def test_disk_full_with_metric_event(self): + rc = RootCauseAnalyzer().analyze( + make_log("No space left on device"), + [make_event()], + ) + self.assertEqual(rc.cause_type, "disk_full") + self.assertGreaterEqual(rc.confidence, 0.95) + self.assertTrue(any(e.type == "event" for e in rc.evidence)) + self.assertTrue(any(e.type == "log" for e in rc.evidence)) + + def test_rule_without_matching_event(self): + rc = RootCauseAnalyzer().analyze( + make_log("No space left on device"), + [make_event(metric="cpu_usage", value=80.0)], + ) + self.assertEqual(rc.cause_type, "disk_full") + # 日志规则命中但指标事件不匹配,置信度为基础值 + self.assertAlmostEqual(rc.confidence, 0.95) + + def test_unknown_with_related_event(self): + rc = RootCauseAnalyzer().analyze( + make_log("mysterious failure"), + [make_event(metric="cpu_usage", value=50.0)], + ) + self.assertEqual(rc.cause_type, "related_cpu_usage") + self.assertEqual(rc.confidence, 0.4) + + def test_unknown_with_trace_only(self): + rc = RootCauseAnalyzer().analyze(make_log("mysterious failure", trace_id="t-1"), []) + self.assertEqual(rc.cause_type, "trace_linked") + self.assertEqual(rc.confidence, 0.3) + + def test_event_out_of_window_ignored(self): + old_event = make_event(event_id="e-old", minute=20) # 超出 ±5 分钟窗口 + rc = RootCauseAnalyzer().analyze(make_log("No space left on device"), [old_event]) + # 日志规则仍命中,但没有指标事件证据增强 + self.assertEqual(rc.cause_type, "disk_full") + self.assertFalse(any(e.type == "event" for e in rc.evidence)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_storage.py b/tests/test_storage.py new file mode 100644 index 0000000..40fd739 --- /dev/null +++ b/tests/test_storage.py @@ -0,0 +1,131 @@ +import _bootstrap # noqa: F401 + +import unittest +from datetime import datetime, timezone + +from fault_log_analyzer.models import ( + Event, + FaultFilterRule, + FaultLog, + FaultType, + RootCause, +) +from fault_log_analyzer.storage import ( + InMemoryDedupCache, + InMemoryEventRepository, + InMemoryFaultLogRepository, + InMemoryFaultTypeRepository, + InMemoryFilterRuleRepository, + InMemoryLogSink, + InMemoryRootCauseRepository, +) + + +def make_log(fid, host="h-1", fault_type="disk_full", level="ERROR", message="disk full", minute=0): + return FaultLog( + fault_log_id=fid, + host_id=host, + fingerprint=f"fp-{fid}", + level=level, + message=message, + occurred_at=datetime(2025, 1, 1, 0, minute, 0, tzinfo=timezone.utc), + fault_type=fault_type, + ) + + +class TestInMemoryFaultLogRepository(unittest.TestCase): + def setUp(self): + self.repo = InMemoryFaultLogRepository() + self.repo.save(make_log("fl1", host="h-1", fault_type="disk_full", level="ERROR", message="disk full", minute=1)) + self.repo.save(make_log("fl2", host="h-2", fault_type="oom", level="FATAL", message="oom killed", minute=2)) + + def test_get(self): + self.assertIsNotNone(self.repo.get("fl1")) + self.assertIsNone(self.repo.get("nope")) + + def test_list_filter_by_host(self): + total, items = self.repo.list(host_id="h-1") + self.assertEqual(total, 1) + self.assertEqual(items[0].fault_log_id, "fl1") + + def test_list_filter_by_fault_type(self): + total, items = self.repo.list(fault_type="oom") + self.assertEqual(total, 1) + self.assertEqual(items[0].fault_log_id, "fl2") + + def test_list_filter_by_level_and_keyword(self): + total, _ = self.repo.list(level="fatal") + self.assertEqual(total, 1) + total, items = self.repo.list(keyword="disk") + self.assertEqual(total, 1) + self.assertEqual(items[0].fault_log_id, "fl1") + + def test_pagination(self): + total, items = self.repo.list(page=1, page_size=1) + self.assertEqual(total, 2) + self.assertEqual(len(items), 1) + + +class TestInMemoryRootCauseRepository(unittest.TestCase): + def test_save_and_get(self): + repo = InMemoryRootCauseRepository() + repo.save(RootCause(fault_log_id="fl1", cause_type="disk_full", confidence=0.9)) + self.assertEqual(repo.get("fl1").cause_type, "disk_full") + self.assertIsNone(repo.get("nope")) + + +class TestInMemoryFaultTypeRepository(unittest.TestCase): + def test_add_list_get(self): + repo = InMemoryFaultTypeRepository() + repo.add(FaultType(fault_type="disk_full", name="磁盘满")) + self.assertEqual(repo.get("disk_full").name, "磁盘满") + self.assertEqual(len(repo.list()), 1) + + def test_add_empty_raises(self): + repo = InMemoryFaultTypeRepository() + with self.assertRaises(ValueError): + repo.add(FaultType(fault_type="", name="x")) + + +class TestInMemoryFilterRuleRepository(unittest.TestCase): + def test_add_assigns_id(self): + repo = InMemoryFilterRuleRepository() + rule = repo.add(FaultFilterRule(name="disk", pattern="disk")) + self.assertIsNotNone(rule.id) + self.assertEqual(len(repo.list()), 1) + + +class TestInMemoryEventRepository(unittest.TestCase): + def test_list_by_host_window(self): + e1 = Event(event_id="e1", host_id="h-1", metric="cpu", value=1.0, + fired_at=datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc)) + e2 = Event(event_id="e2", host_id="h-2", metric="cpu", value=1.0, + fired_at=datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc)) + repo = InMemoryEventRepository([e1, e2]) + start = datetime(2024, 12, 31, tzinfo=timezone.utc) + end = datetime(2025, 1, 2, tzinfo=timezone.utc) + self.assertEqual(len(repo.list_by_host("h-1", start, end)), 1) + self.assertEqual(repo.list_by_host("h-1", start, end)[0].event_id, "e1") + + +class TestInMemoryLogSink(unittest.TestCase): + def test_write_and_search(self): + sink = InMemoryLogSink() + sink.write(make_log("fl1", host="h-1", level="ERROR", message="disk full")) + sink.write(make_log("fl2", host="h-2", level="FATAL", message="oom")) + self.assertEqual(len(sink.search()), 2) + self.assertEqual(len(sink.search(host_id="h-1")), 1) + self.assertEqual(len(sink.search(level="fatal")), 1) + self.assertEqual(len(sink.search(keyword="disk")), 1) + + +class TestInMemoryDedupCache(unittest.TestCase): + def test_seen_before(self): + cache = InMemoryDedupCache() + self.assertFalse(cache.seen_before("fp1")) + self.assertTrue(cache.seen_before("fp1")) + self.assertFalse(cache.seen_before("fp2")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_workers.py b/tests/test_workers.py new file mode 100644 index 0000000..8fb8e1d --- /dev/null +++ b/tests/test_workers.py @@ -0,0 +1,72 @@ +import _bootstrap # noqa: F401 + +import unittest +from datetime import datetime, timezone + +from fault_log_analyzer.models import Event, FaultLog, FaultType +from fault_log_analyzer.storage import ( + InMemoryEventRepository, + InMemoryFaultLogRepository, + InMemoryFaultTypeRepository, + InMemoryRootCauseRepository, +) +from fault_log_analyzer.workers import ClusteringWorker, RootCauseWorker + + +def make_log(fid, message, host="h-1"): + return FaultLog( + fault_log_id=fid, + host_id=host, + fingerprint=f"fp-{fid}", + level="ERROR", + message=message, + occurred_at=datetime(2025, 1, 1, 10, 0, 0, tzinfo=timezone.utc), + ) + + +class TestClusteringWorker(unittest.TestCase): + def test_run_classifies_and_persists(self): + fl_repo = InMemoryFaultLogRepository() + ft_repo = InMemoryFaultTypeRepository([FaultType(fault_type="disk_full", name="磁盘满", pattern=r"no space")]) + worker = ClusteringWorker(fl_repo, ft_repo, eps=0.75, min_samples=1) + logs = [make_log("fl1", "No space left on device")] + out = worker.run(logs) + self.assertEqual(out[0].fault_type, "disk_full") + self.assertNotEqual(out[0].cluster_id, "") + persisted = fl_repo.get("fl1") + self.assertEqual(persisted.fault_type, "disk_full") + self.assertEqual(persisted.cluster_id, out[0].cluster_id) + + def test_run_empty(self): + worker = ClusteringWorker(InMemoryFaultLogRepository(), InMemoryFaultTypeRepository()) + self.assertEqual(worker.run([]), []) + + def test_guess_candidate_when_no_type_matches(self): + fl_repo = InMemoryFaultLogRepository() + worker = ClusteringWorker(fl_repo, InMemoryFaultTypeRepository(), eps=0.75, min_samples=1) + out = worker.run([make_log("fl1", "No space left on device")]) + self.assertEqual(out[0].fault_type, "disk_full") + + +class TestRootCauseWorker(unittest.TestCase): + def test_run_persists_root_cause(self): + rc_repo = InMemoryRootCauseRepository() + event_repo = InMemoryEventRepository( + [ + Event( + event_id="e1", + host_id="h-1", + metric="disk_used_percent", + value=97.0, + fired_at=datetime(2025, 1, 1, 10, 1, 0, tzinfo=timezone.utc), + ) + ] + ) + worker = RootCauseWorker(rc_repo, event_repo, window_minutes=5) + results = worker.run([make_log("fl1", "No space left on device")]) + self.assertEqual(results[0].cause_type, "disk_full") + self.assertEqual(rc_repo.get("fl1").cause_type, "disk_full") + + +if __name__ == "__main__": + unittest.main()