From f5e64ae30319ae62f50b47cfa1991364f967b49c Mon Sep 17 00:00:00 2001 From: wwakabobik Date: Fri, 30 Aug 2024 11:57:26 +0200 Subject: [PATCH] Added test_engines_plotly_reporter_draw_automation_state_report.py --- .github/workflows/master_tests.yml | 1 + .github/workflows/tests.yml | 1 + testrail_api_reporter/utils/reporter_utils.py | 2 +- tests/assets/expected_automation_state.png | Bin 0 -> 23322 bytes .../expected_automation_state_empty.png | Bin 0 -> 18493 bytes tests/conftest.py | 79 +++++++++++++- ...y_reporter_draw_automation_state_report.py | 97 ++++++++++++++++++ .../test_engines_plotly_reporter_init.py | 88 ++++++++++++++++ .../test_reporter_utils_logger_config.py | 45 +++++--- 9 files changed, 292 insertions(+), 21 deletions(-) create mode 100644 tests/assets/expected_automation_state.png create mode 100644 tests/assets/expected_automation_state_empty.png create mode 100644 tests/engines/test_engines_plotly_reporter_draw_automation_state_report.py create mode 100644 tests/engines/test_engines_plotly_reporter_init.py diff --git a/.github/workflows/master_tests.yml b/.github/workflows/master_tests.yml index 6b1d985..999a98b 100644 --- a/.github/workflows/master_tests.yml +++ b/.github/workflows/master_tests.yml @@ -26,6 +26,7 @@ jobs: pip install pytest-xdist pip install pytest-cov pip install faker + pip install pillow - name: Add 'testrail_api_reporter' to PYTHONPATH run: echo "PYTHONPATH=$PYTHONPATH:$(pwd)/testrail_api_reporter:." >> $GITHUB_ENV - name: Execute tests diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c2edd20..b987e14 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,6 +29,7 @@ jobs: pip install pytest-xdist pip install pytest-cov pip install faker + pip install pillow - name: Add 'testrail_api_reporter' to PYTHONPATH run: echo "PYTHONPATH=$PYTHONPATH:$(pwd)/testrail_api_reporter:." >> $GITHUB_ENV - name: Execute tests diff --git a/testrail_api_reporter/utils/reporter_utils.py b/testrail_api_reporter/utils/reporter_utils.py index 5c1d7ea..70783ec 100644 --- a/testrail_api_reporter/utils/reporter_utils.py +++ b/testrail_api_reporter/utils/reporter_utils.py @@ -7,7 +7,7 @@ import requests -def format_error(error: list | str) -> str: +def format_error(error: list | str | Exception) -> str: """ Service function for parse errors to human-readable format diff --git a/tests/assets/expected_automation_state.png b/tests/assets/expected_automation_state.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ce00cf406fd0d09f2c29620af1474d148f0b08 GIT binary patch literal 23322 zcmeIac{tSX+c#cPQK3@Ft_4M?7($j(N%lS4NEk7g#Ms7?gi0u~Zz;1dG&vHEX{T#=AKc9c*sKNEV-sg2)=lMEcuk*Z~>S(F5?Bm+E zW5*5_wVT)UcI;rl?AWn$g=r7?X4qtVc*hQb9ctIE7BIBTx`$| z1}1@r^p88cnJ|M)zx@Y%qbsI75;jYD;PU5JjL?POM^N4okabL8@dgrAdFi)7!SD--y;isScUob-lY9zvmdO1Q_ z_LuknaQKYjX^;2fak*`7I~mY+`^63@dn%YIkE}AL^Ux!L zzq;+oOT^X9;Tagv!UB^&_h10Kz~Y{Q$#}mHjL?o_2R#*bo>ip>0H!41?eB|q ztn3$B^e%f?H^+SQ=hKx(z`_Nf)~j^G=^Qj+0MGL~^q1$=HSNQ6XLfY2%q(}4 zWW63>|JARtV#9@icD*GX%+ zkC5=i_6Zss=YCw-e=P4lD`rboF}GYXE|Zd9U_4#zTr@pcDtraP?a`TM%%>Y4`T6Sj zZR~e#$UQ%-qQ6JdzRy$T*lj{pn8V=g&gxWrDrtfln9B*g?H{%mNMIl*?N%>U@GGCH zU9EU_EWEdU2xT}|6@He1{E1_@w*%|k`7MC=>Q>6)$7=7j`VQNVi@DmYo$J%D*amCv z=ZJ&VIji~CRr`9x(|Z3zFHYUxpj0Hfr`meiU>Eu2DgT*S8l_W3fowoh&*}H$SMWAq zFbjdHJtTrpiFdbXv|!h)4981A=w&gRQkkvMjmzVz&1rhjgU zH)4yFd&3&}WbOjV=A;Z~QbGn3#y3RaUH3B%qgZK~5N`#y`6Esnu89q-vu}PRqulT; z$x`btQ=~5!+hXYRl+xRUKrft9Kqoz*W1xW6W5;EyjA3jaW;VYb_ly;%nUo{B-WUt5 z7%#I1UWI$IV?Rlcf3^~_EK6UNbNlw*!D3j=E=DZ7*)G+}->IQ-Khl=uz(p+2k2-su zb`Bf_jLM&<?NfKQD=a&GoNc;D%v;czc~$tX;ByO_YX7P2ul!RKzQyZMk}Fv#nukI54`1bE*W2oV81%*Esli;? zDJ0{t8>H2nG!(KeIyULCyikA0=|yi!>K~?l7zIwHpc$-n@z}Gr%mq?-+hRgKwT?Lf zGA+vO$;NXzeUAJs7p>^2=e5j}SgLuTj6E2spPayPx{`_gu?Kl5^J#sx>=65r( zY7gsBV|9tp&Zt!fD@g*O%4C;ew)u$~mAkY90MmLuI{fp5kMJ{YZ_G*9wk1G%6)5Ck z^gO=ktdLpRk<`S!RpjAvZCTgJTcyMvSX1P6S=T8IUDU*)6-nv7ve(i?A@%)%ODAEty!e;qdh&iBR6L zog`zaXz`#)usM3Jf1qmj{$pqS8d<5sCPgsAnXY$+@0ttCPZpjy=nUp{e~xLK*^cgr zJ$`Mm7tiYKQhhUgnVBh369FFEo9oRnVu|7fx+*C4!z=64sksj(o8`uzOWMHf?0i>V zw#&_(qbjlcVW|`|3utEe(qu~};+TZd$6DVUXNCtF&yV?h4`Qj|u=FWZz=0nkH)b+L zEs2-`jBRL)m_?4dM2$gE=#eySrpL)(f6SYHeE5;RzIdKrhDkqp=VL*95Ja-FaW64& zFY7(ZJih8jq`E=LJzcX(w;8=^M80XMV;$r~sny#XX`I6mO56RxqhONult%u!k2!{h zIRcX_fbG{+dnk?JpAEZv_s2eG^<2^>P3^HLq^TjzcW17cS)JXR=yC$HiIH z!Fx8BExs##k1?#js{_KA^si$iz#Aar61?`~VL~4#ba_&e-NxU%Qjg#iOYgklg)|hYq-c*mS{*ZP(QoCes)%JRM#LyDZcGDq4Ck}6J&y(W9mv2xN`iFfvMExtDdDZr$s>P4{RkTqzHzUoLMtS_56M~d40T57sYO&qqy#Zm| z1HNDuk$Z3SHfgZNxU^s{E~8F>O_CdQsaKQTR^shh z8)PKv6N~&6PXx%ca(%qdaOw_&L)kn&s`Zl)6chgrBKc`bjkOV*E#@-SkFg^C;2f-b z?UJS|2;f#k9k-bPx)!?eHpehKoyeP*$%GNw5y3-yrkGHDdwqGbPb_RMhvXDiYBg>& z)Mx*mrNzZAwwz=6WB=Y)qSJ*E6GG0RSc`Wds@T_IW@QA+9OJpGtJP%p&r=<_levD~ zPlTarhvNuqeD6@E(9C?o`0B_LyJ%56_h=DagQFei-hQYd+*=(`lA0)tEx9*y%kYfx zt>lF&wbeO;@@fA&OTq!ltX((8X zYqLLeV{C3x#5m*e_F$R*+K}shNzCX{=CJ@7bGZ+f=qTdOuhsc?Ku;tm5k%ge2yCAI zntNM`f?sTKSfWFqdDM+k2yAK2w8ZA!h6mhMB4c1Z`Urc}3UXGcXM1zbJK3x-i9%9( zy))u+W>u+pNuy-iYJu~qmHMPb;lb*#js zIaMMec+CmkAv2jDfizmbD5CfN;nX?vj0;hj_8(B=rL?*aN2aBzL!DJ<@yq0MLw@SM zTV>8s3;a^L{52a{w~mPZgajXV@9%e={F0)%b>nirbQ@Z5F}Q^v4r12~<2W6?Kca7^ z)H=djU&+#1L`C}<*MzE4IM+T*(Eh2vf|AEDT@EpmwDMHmyb^KyAFgJY7#~`3nd6X^ z-Lw4@9dEy@kg1Jz@T)FMw!2}J64F+L17+^5VUeemJRiFh+*0rQn#T_`3#PXneNQD5 z&xe|2+1ey{qr{7S%kE@v%Pg6+`P(IJt$%rmEpb?t28`8}5Qf?dj*7o0yii zybAC3O^14J?wo9MokuS0;`fEhNNq+WUvTcV&4OVrom&xv#7I5Hr`jGOuh5f| zli>Bxb=7X`9aZ_=2Wu{-VG}%#+u#=YL;`roXFYB(!k= zJBKhJ?%sn)Hlx@nxxVDD((};osW~Dhm?tR-7we(^(alHo7@@At%YxV(7q!$leR5Hb zh|y`Izy^$s=3yBIMBLVu8zU^m!RLACxtjn08H@WcFezTDB21+qUJVi;<4?StgY;G9 zKHl~`7oXxuaq?Z9iIry5_NB7>E;hw_uRrw@#=zSSIM$1bGy8s@#~UlXS3jgNvNctm zI^TNFL7nTWB<5&yj>;UjieyRhhN5#UZ(M03O!CAK=X=0gigy+rmNqV|+T zVVe7gmGABfh4}Vdf}H(`!ouLx(<4F^5=j@J_`7!_&Cw=hF7WCQ$C0)uYX7_h@MYTq zrxt@vRF1RKlfZ)@X#V&U1@BS^D46-x`qEU_!-f1(>uF~s_j%~*T+a)WJ%tn0)Sd#| zy|$!_9}9*ca{+B;0ujR+HR~Tu*2*+V-loL|Uh91DkzN#=q;*_0g8KsxI(NYuN zW3TQcw00&yD#wG}N+pY?I#+2ZY{IZ-(1J7mc4+KYRy$jG?_;YWgUqSjGy5=>e#IXP z%>3r!ugK-!wyL2u#Zm^JN@AKioC?S}Fok*VG3r7k9y*3=F`DTdt*1!r0@-279MVjx zvRN@FzoWCs$6hRH`y6;rfIjxq-;)Qtrza!vYT^1k$Ctz93%i8-Gmo6!LmcjNGQe$* z&vzecg?&t3?}}Yt!i}C%?a3xvQ=T%x%r-}DCflP|&a%MDDmq@MQPh}VlG^AkW>u`?l-C!mdVw?LpYtfOs{nBzuSz-my_hr9W@9ChL~q(9oDE z)no}k5J6c<6-U@z+QwX#0;O^J7{M1^k(Z;mbvGND{6i_(e! zqQ!gfsAHQ|pVvRL+sic#%g0L~CtKqcRyt^nxP<$$r%;q5Rx&R9?9XMKi5Va-)TKD8 z#qNspZJj5L)~jA^IT9HGCxlywyih`9_4(i(yHg1y{X>y^=U2?5@E&uNWOw%}9mj10 z^{7BZk0XY3ve@m9uJ_X3syp9r1^<;7Kj`+Mi zdKRQ=^ZS26Dmwon3>NHGnG<2G)QQmUhBC@A5FdgZxfVmmu$F(a8%com&~iMo+kg(M zJ^!Rnv;d7EEd7i@1PtqV`ESFb*}1RpJPL9k>6E`s3HLPY&BJ{MJtN>4uhJB+71bqG zcq_=U#k0(>e13LB!zJ;c=dKO;kd_<3aa{xnS7Omlu+O3p&F2+;?=YafAeLh)sqECR zg{xr{BxQL|UOh)qK}(QEHY#Rm(6lKXjEc+BUC!VvkHA}$x;1#9Cux#+v|G#$D+ zYo{t|1ENKH&8g11sY~;i%kF@E7#!44A3!D^he)qA*AM+Z-rlp+1Zm%JC>O3n@gW{# zpYDT?N;?oP7wv~2%y`yti4uhh>?o3!3C7!wVFbOyQE$-;hQkFZ5Yi0 zqp%7C%k)Fxv4^ti2pfIDRWk`K`YRtWYgHh`S7q8-Q{(O5ctJ*%I#P-eJ7oQWahMAe z0clZAQD!pmFNu90zZbwX08^#B1FQhRu%DgPZg%s&_qZ-CnD=Fr*OPi6Id6tvtn)3c z+BmGB;`B2y&iL6nS5Q9M^BA&Hh?$wTAnfr zdhpI5=h`7iZB<5djQF9154?lbVJtN;v+vzp5GQ$^{TUJE{;p~VFfP-tjOPmK=jX{^ zwn@=qLN~a}Gf9Y-oX9{}kIMpCT2BEb)s|K7Lig7@0`$)M>E>pIVtP^dNX>JahIXZ75fINY#Nzur^HB*@+71hP8gJS~cqdD1o}% zAVxr1(p!x!u&W2GpXkU@WxUMWU^jwG`+|6RkboK#G)4dY5*{)Ev z7I0x>vYe%if+_Ck+?8xLJHIazzLJj9S6CZE7|*<9M$h7q+{o2Qj%=@}T(@o6NvExk zpOxpQ_dx&cEGufZS9WCb^I`g%Dyf9Yhll1!#F1;mnbHbGU40?Z|-x=yfDW z1LO5?Jcb3gp})Them=Kf^(r`k2X9vIQ0LM!wY@RoG>m%HUZ7({2dz~y9)UOW%5fFH zD+|VL=G%`?Ihj`bMP-DO5j4uRkv7b>3ycm&z5zJW%7*cG4mBQ1B=6si)zz*oK*tpH z0F1T?AAzU~MM;Prr?w_~`%_{QR+~?i5q%KfmV_);K6Mu480Y2;(I$mAvK`E?Q!AS; z;-JWEQ;5?Yf9c|4!kCA>=WrQi+Ly{UTOFNS3LCoNOB+W>@1UKZ_5wl8Roio$w=J$l z=j|T1_o@7fKG$e9YgRSuEAyNi0iW`xf-F`WnGx&U(OwgA%%#`&eRk-x zv&09DNraPfLvswe^z~ui!Z=+?`dqEUuLsunl4rrcSo$&=Z=#|*p3mB@*#^sKB(}e& zALcV00*f%ek>sU5M;2acQG$hK2kX?DQdz(M25n^Wkh>;XsckXi7%<*&Ys~T6ZT^;D zOA?d6JNb=A1J>7K=th52wfr_KMyO@E?;9fR7St35HjyuD@A`bY^+R=f{>F`b_l!qhV2KP&o z`^4qZrwMD5<&@=$;uiSeivkhoN`I(OD;KdaP%am?x;&o{cZ_#$w&Tngmr_ZhbR$1g zckcGI)#|KbBm{E^FS{oU_y8Ut0)SV9(5^Z{Z$($M_dfp?wlD()xUiV8iitJKb(Cmm?G(R_F-IuL_irct2&(wHOCBU{p#TO~qZIGP@~0p3 zRDJG$UDO{67yfqlZqY2^Xm0PD`q!re)_%Aq;%6XOM9I4FI^|=D$%f~`UHNK_5te18 zfN(yA9Mbn{lAQVSb*bgYK@*84#6&~KyJjen9-_;9ihCF=(9#!0bDMLU9gp7l`sB> z2;5V0Lk4+?AEfQ=LspLTsI~%tfTYY~f`CKHhD|M_`zEaRxVuH}-j&&?U2g3rw#Z>x zgMIrWTX&A-AKfOdwXD0L#gFfdWKGXMPKrOY;AYGoU$^MCXGTvs6*QHfQr+$fPp&4tZEfZF*jh%byW%ud+pSo6qSXK8cKI?dZJj@{Ea#4+ zAGPLfNfKc*w1N`>I5IuV^|t3{OD(lKJTJm60rmBFKt+DK3*mi5SUbFRnlp1vE)b&x zFdFsm80{>+>@P}_4TeuB~17a>1^b*@(A+o+cU4}T3G`S^nTOk=zQn@eEKiB zJm-2GXS^nVDT6j;b%R0r7g0=F(-MZK%k_D$GgW}iXEC{55ZY5m$fe%YtRg+=$j$tv z7Zx)P)Lhm+rMlLf*NsY!t(*I-S{Mk-vXk`V@P7#W{oMco28`j`tFaV5#k{z4Ca>gC z6gO^?yxYx??G5$a52iFq-D#usa!WYN=EPH~RS}vy8Aj>Je5}^_@S6t#x^{p13tcxc z8j<_cICG8Xz71ocXN<)y3=5KOO?RO4nVG%40I-IcoJQtDEl{j{J?cuEuXm}2_xozx zt}U@qv(Uj*Y^{%s)_c!QrYUPtv$J_+`*8f=&7SFgXjzT94e{`DE{s5A+>~}>%2KehLpJDZL}>9Uo0}$IoBNT zx7{3}lye`@k7;+Wqcqq($Jux~dncQN+N~(^Uo|eYsR&4dofDtkQF%&}o}XO!wLqJp zbwzE7kmoxE$ky5ccB zGk-P6fV$F6a&Dn*>KEe*dVM)8PTMx!-9@gg4?*g`yS1`A;5`V6!M>r#?;NL#pov$0 zQSSbSJm)e~OMGd5Ll*CDJyV#@Gs}!7lw}X?=16C%hfND6%D{|Bb7;k@@yBZowv zpP!y&o&z2D`PsNbG=! z#?NDN;^#4`Q)2R|whdJ@E7!sqX1(4Qv`-MMo!*J zw#mR}${+~-Sur2seDDzuBRn1lDR zH69hc@vp^=IthP&FH+IPcZTC7k!L9viwlKbmC4e4s(D6#wB;=)7u^lIHPw=tC}Ly~ zgH@uHa}p-J!KQ?kdIJ2anK!LsrPW^UHh!*uE_Ow{YZIiU_dT2wIE0ER(iiR zwbs}BdkDW?@;aor44rTK?!oSTS>NOBgiK}^5pd6_)O(Z!h$+@C!gTr7o`vr7N5I-x z=^xkY2NqK>Q~9+vHVjWK^{Uft)ROhnZ+`2>00B<5%#mUzx$Kbg<-Tf>K$U$5U(LpN zgj|jC&R+SgmCjb@@H`U|7x7c3)Sw^Ht2sq56lzi`(xlI&2ra(BaYQuZWTh)%@&F>w z^xb~-S1*`}j!cgefdDlq_tEnsT`9!#{@RJB+0S>3Kw3B>5wWR$RXNdT(mv_HQ_^+H zlmkvz3$`B+MnkCaTW$Kh#LacPYImIDy6c2av1@xF4N+!SOKsmX^Qq0piZmeEQ4%3# zts*c8&fjR?p$7JL`uSVXtW|CM9PT15ky81=edH9QOOFsIAw=huh7 zJ!(E`kazRLk#gn$nb%m?3P*Z|5Jh)KgFCeNbr}NoW4NWAdc!;vv!-OAX$zzTJWOz& zfF=eC&vZ+~45a276ufo`tj+?e3DuP@fA`lXyP1StVnAFQA|5yfmf{H%&=xC{7||Bb z0Oj3K$v{HU_?>(xHDo{vD|Ey&HD#kVLo@YlY{RPlg1M|4BJ~2jDqzoQRcKw}_cb2X zYxFek3#h0>>=AhIq>hPEXffg4K0tW_7V79%ATNpN7u`FvNPN(-1CnK?vR2~g$gKo# zH=b3z|0u?vl2l~V6loLe0k|peJKt=2a}H(pwQ)TYWC>WW`51H(RNYw7$@5(I^Ks`n zm^4Os<=mh7tvl5Sh0UiCRfL3@F|ekOBSa_1ZxF(>)V?fxLV)RcU(DU*X_no@3fJab zB+km6;$cIhG#_Js+a^y5@e0-FhjvhJICMFco_a8UfQ!B#l^xcWd=xfrqo1Uo(LG~) zEj;mxYJsu<16h>p^JdzTs)N?%%IatEBF#IE9Uy;wt9sO!tfhIpPZQ|?y(3@g-L}VK zxGEKfmA)mYexxwK<7R60Di9b%%+FwNuzbs!$KRc=Ro#~3dO3s`c77r>dKJ;ZCWdqNPbL;FQ1rzPKB=P)RHr&j zFVvhMcqnd~2kk%B3D|(1d>Qi-#lhuPHK9n4P&i33JAbsMz)#F+5TgkwkglHZTZZSQ zQ-@;fP&o*b_Mc^~cYt3Q+{=`1A#gB2Mz-<0I#;Wmwq|^q46Ljizw+wk)WRXj0o6QhwG$ae^|YwoB^aN49{UP^0tr)Aj5Satp$#alBm|?V?hZ z5}zNN^!!RMSeY09-=Y)$ z@m>GD;rxH)$njr4nfgz(KBLIu0=`;X-hIJn^wI8DGTH@4#D;5xuY^SK=%)2=swpNa z(6}vhDjz*NdN%G&a2@S-o80HV=w-jNw`CP0~y_}G`)3j*WT%G`#ItEp6_ zp08Y~frf@rdET_7)F%2L2` zwqD5_vknpLo;SyeWnZaTIJpShRA@Z(lP$QY3?z)5Pk2tAX9!@zG*$l|iip0>PufK+ zP0I%*m04!{I_a9Xgq7GBO+*_^xTW#I#@lTv1cyyRv)8XrmBfMSH&NY{uV)k(~qjh)5 ziJ;1kSCjja_iEj+)Y`aE?ZKPYRNkvNGd;@qGu{ZRz9zLSLpHt>g{Mb^*R!r$yQWM? z-uGmT+y^#>5Of33zDUDjzS&`tyM-mKNauCFW#$I6ooCzLsTL>6%;!4zF2SqM_Oj8A zzFad1L4`OR4v;yCYJ5$%{i)7{$@PTug@|i+C%(i$PByl^K4mQMA^=x1mq75&ReL+& zK2%^*Pk2xKTvh$Kv@Ggh#aIaDC2CocUYRKnx%+n4!KNYW`Aky~Wj$t=QcIVnkySaA z>ms)W&_vpVy27O~&YDYQXz?69wZBJS-$yDA+V)vo#=1`4&r1}%VFnV_5&>%l!$nDK zbc{37e}H^BjAuyni)Qu;-=v^p$v(tHihd&y|j(`K;o@=lMYB_%^ivB-icK#Ec5lE%}(}}5n zrvI-+;eV|EUmnGQ9F>?{y*3NPJ`rfiog5PJufaU^k6^B5qB_Fzjm#s?O;1m4Kzb@9 z^RQd>6+NfxBgyyXwCLnpVDZwKS1a}CLb-)8HfBrKYS?d;@3LB|Ut!O-IUs_W%ycTYS(=FNh~o>1VTu7v?c#=*2HD2aiN62#2LSKC9Dk-mW~ zuVg&LYgQUmy%$Q^AY@WubFyei%Ad(}vC{Bg1A5A%#25NMy8+WJfj{$;6nR8<&54*A zdf?sDM#ia+Z{&b#Iky&tT|KXzS>)&7Wk9jeTMJ+>*({!ZC8!>AS`tHWpYPhGBAja! zcB$04Pp5Tp&zcOIF}+;gCId`Va#hwNotg3CVl61Y=i~YQdJnylvng^>p1{nmz`G{RNa)$$=J{S*cLgS*s;*UMbt;;KHvjk zcSpcD23Jp*ZSz2p?pAbN%RVc)((t^Ja+YNZ z_xD`di6r^f8}G}EvB%>3+3E35U>w9hPP?X;Z!GCZVJb7U2Sn?$RrWBakTs`V5WPv$ z%e9+*3+x{5USZr_kd;UKiYKh*U$^I&Y+e3L9(?U86^mwBUMU)nF+qr-(b z&;%>LGoWNYU?Vc3>oHGBooV%H99rq`#P#gx>PLJ`i}f$9@cgm3>if5jM=X3&5j8yR z@ryp8%RC-`2}c0?w!0nKqd})G7jLp)U}`E~6WK4LQ-147Io!Vi?`;A;k3tr+34#srK;h4g=jYI z+RszxUOQgEmx@`L<=9EQL_c=tAN2ib-7UrV@e1ezW3{+*;3W`CR_BlWq7t+ObZR>} ztEw9m5>|SnOgU(%duX}Ixo@+@1u>vZpMXI{HTbSyi@cL4tjf1qi77vZ_;G`r?;~N5 zD4_~P7T`_RAAl|>_r)(e6)NvVN|W)9{))wm@eURwR#52+^Cu>}H`G4ob2TKK=gq(F zMmRsxOgpQpp+#-_9)N4bxM%lXhmYeNW{b{>n7z+4g?c6)L)^|Q_xB_$_>%?Kax@${ zsl%aumak4P5_L(ehG+Dyuw#?59tWa&SBt3g8Vhk9js08H-nqy_|KR8fbHIBbvGUfQn}&{z_owe&dj9n6VZbH=L(C_E`nn=M$6k~BW-WaahdzSS2Wprs%Wv5tuTlG z++KFVW9-dSk$9Ta(Cx~nEZFbVG19bV2O>u#(@>WdLG2c1{7COZbQiNywxUy6j;z1- zALwCdD}b>quuK#0)GySkFuTjv*Va>2)VU6`P0Q8GyMO5KjO~vbounK9{WOS69wCK6 zKv2LHpZaaV>_!y)JeGTc_Rdmz+06Vpdu;Wic+QsA!TT1s%f@AD-cRhVCPVBj-?lVc z#5k&%s3AqW za?)p~)p?l?2aCPR_yL<@;Cw>K3O4s|f5$I6t{8<*ZzB#)!-@Bup(IPsxJfJg6Bb11VTdCUN8UNn7Na5Xx*2A_W(0;s2OI zLauK;Yco9Gq?<~elHDpIaZGl^TPDNZ^@u#{^V33uTH6jq(hJ0U{))L@OdJgJqwf}d z>Q8M8J=^N)$-Yan?WzF1$Z@oXrQz@bT_a)r)to7i%m5=ff4zKtwQ)fy$&yL;jK_(| zCryV;a}#}-rkD>^v(EP|?5ggTm2+1e=>ck>b~427nGapgV*78Ztp6oq0E_*XD-8I5 zXZis2p?-WhWZM>LXi^MQ`?@rQI&7mTaU@Zx$*y?X|LuMeW1Yg2yY+kiZN+U9%gD+h z!Gl?K)d<>^p#kUD14Q>^E=vo^o-8`ehFU!ByYX3%3O5un?)<&vw(`bXiue4eR#n~& z9ss|c5x?L!8lYNlR731MA%&BVC92v zL`7?ZT1Pd|ZpjfKGftKbfAW)L*a=961IiNz0}cUwDQvh>mCD9v5Xu_ZQ{z+&im75y zd*}@_^dy1s<9`ad&)GAzl8=fX0T7Kz6nNR&rGXzFSAxkQ?(%a?vxwaar7P(62B&a)8d@JTU?T zmbk~v$#F6SuwniU!iGTqmk@R`iY@thZPJRhJL$6dXJ@TTe<908I%Jl=eRuKIYX6|U zq-(tl11mYQ$aMNBlg|i*=idj^XBUS2FP$W3xATT%e-mRQK5^s=q}NuTcJe(H(Gcb@ za8hh(s`=P2wn=#;+NN(&I?tnU!PzCBWCOr6I@eO2TL#fRu$m}^^zlFXyv=USmIX0E z+2m3d;bGe+v#gSF$->|g|4&>mOG2C7fN!l1ooAH`QH_$7i8H;LCI&3<$Wn|2RI z0$a-8m}~(B%?^g&-!wv{tYR)d=j}0pc^3f)99(( z41gmU9icUWVjz!5jFBdTwzc1dDahZ2sk+3kZ+IjU076a~68wk9`fpGl_Ww>8_}}l% z_=kr3AJK3!VPSt2s6?1B_KY?%KZ%3`py35}MtE=NgPjaytkk8y&{Yd2f2FIKF}mCG zk6Y-MO??DSHQX6tR=2G{bIw+5+2#LHP&|jQ-$TD>i%|vq(Q`%DXwFmMk2o657C2w; zLBhYK#DtMjil=g>vkPyLH69b2y6BIqWe>0?*h-z6h2xgWR4far5ue3Z0CZILl&x9>zgm z2s;0=BR~UGf^F~LCO+PxcQLk6m&~f|2Jw>+=bOeyB}~t=6B@D)DUS>@9;Wc>g!ABE zi3v>&yCb)KK&eafONGRx>6USy1N(lecv`gp$AmS=dysq6R6u7z$fbMG4%8fH4W_U9 zHL&V0Zn(xXr5lb=+Eq%O8nw16oigbcv^<#ll9JJN7SYxhlIr^oR0C;tZ*x}a$8K9A z+f62WM#H(UrmZwu_T^|~kBPkaP>e>`QnUAjdFg70e@kpvq0SyTGd$07_l}}Oh2Btv}&RcfEd@wqK zp&Wg6Dfy-Mzq4IT>*6WyPL`Qz9%yVIMtwOg_QShUA>?O|bpNz`VrHw!d7V=;sA-zq7;#pO^;NS#Bv8D^g9- znp}5wWtcQbdf!n+)d-akdz~Kp#Vz}M+kg-En*@57e*J9N(Hqx2k1az$kMwHOrl7mY z$2J_4uGrdI&M5$9o01q~|6RQlO5gs`B8NoaG(c}Zy;{DCoMP6ql#zRNykZ#(D4);{T&p(63}{-K zzobu4{9*Hw-gsR;tY?XYL$huj)ha*H=Lj3R{~!cxurA?L3EeWs3vx9E7q)7xLgkb$ z^(hWlHv~+87TxfxY*d8*(->hUZO(6L~O^XSFi4|e_1drkXlw&q8UjQ?;Nx zmomPMfbQ1$yu~PqT6#l!NvfsMsdlLT=OtUMiu4bdl7@4rAFe1&3JRG)@6Gj)a(Z!> z_Gz_otBP2V<6?&&Pek@6mM?VnU$HnjAAYxjeg-`OB(8tU_M9O=gX6s9v#HANDRt=e z&?7w`0z+2XLsAKoJ7H5&H37KCQj+`SX~`~WaPGx#t^+l~AKJNu*)Zz0G&R`P{ktOy z_ZMH}^kP*J3X(U0Un-7Zl(JfTMl&#M8oO@8$N8iA@pg`0Kkt|IYAZP<1DG3EILh-usW|Dtd``|v62|HlkVCN34wbe z-v?hXXVi+R`=*A5gU%1ibkGoAvrDvqp7|(X%GO0K(DKba z0j?_Q&(%^gdv5>=8&hmDdh&81k_9lzMca`r6VRd(=`dnJYfP2y5eLEvEI%LLdcZld z&MYlYuB7&Y6y*~qkzU4U_pFhvMjMs{Lyh6fS3hK04jfGK;h}-%rlQ@;8Vlp)Yl<6J z*$(*LYEkqv1>Jy}V|Vy0+`Xd2z1=m)omF>=DeI9r(M}_p3$Q+JTvfr_3{AeQ=IHiI zCZwsKYxo-Y(EtY-j!@-UhN;w+csB3S-dvi>AaAVEFpP^^|hWo<#v zwY-YuIC=x=kfln6&9GuT)_krRwkzo4pkCRxV$S4P`kA3mfZHN?WQzzAD}o+h5+J!O z?_n8(N!`!8FB1rtbw4r^C#W3YdM&4_h@P~1RVg4xQ709OU)660)t?Kv^YdQKn^+J= zw_pYW2aybxVq3Z%?%V|uMBl5st(SueI`R_pQLe_N_AdFRGVm-mcW@#WfDh?nP6;3j?JA0o+7 zpaxU$s38s7b*Hbvo&OS)zr)NC-q!UHR0HK03vX*rr=l`iFYZ`g*TxnYgIv@-2G0m( zS8j`KXq~p8M(kIR!9Dk-cjPf2YHGBKIongSuIMZ0^c-9(@#+n3s>OAx5=fyW6$;-)DXWwe4J6y$!-avBFQuFAdKGE>~iIlKxL zv7g>zsTBnJhz-uTvNFE8S=aiSPtoUeuY_BCixllNqOVv;@oLNCDNTQ8V^AH1;m<8a zQnln!j(ok*1F#sjBVSUD@x|}9-66&1C+yni(`*kZI$eOj^dPCdmwcbCkb9{z&_$9e z>=z)hUKvG3D9UKRK5t1B)vKse^R-Tj*i}mRET@5IIhCgV{LoL>ME|Ixm#=H6p7x2C;-iXJI~QWWX!2r44gM((;Gb*tP}uhUm;8u3IOy&gASVdx*y@K0Ft`$l=aqMtPo@ zykjc=WWnOm^^*!Eg=uNI!X|~cXUu|bpUtpdDk|J*V}F+Ku2{e~>(7Uy3x6L>QD))^ z)NHLIc6$BTE|$j7^bweN_hNSMVxzuV{m8sqWv^fL0o{m&XSCRL8@5T^So<(IySdsL zBAu-qaPa#Vz_*a~d}<;a{`E-VW_eU{V&?d6gLUgSt$oO&+&2C>!nzH~!jEZ&Cp|LG z{Q2LzAJc+&{+Iw5@2ty>mi?0u=UqSkdbh!i7VNRVrjy$3zn%|NIOe1EeQ@3kd}>2K zt?}0aep%p`1%6rJmj(WBEYJqo350$7#8vuz@7@fRw(YzA6{zoQ1wwCvLKj5tR9r%i z;(W=jyFoWw{}t%Jp1RQjy(xJ7D$^r7G}87s2(MZqtaf_zL3~+8$Y1H8Ps(6acm3d~ zw2^mvkFGlRmIJ-(ZI&S6sGHk?dw<69x3|{CYPJdb_IfIAG_okBwS9Yc@y{G$fZApa z@iM>JvR_&-{;xz7Bx^AG;7F3bB8n)>5gfDn^81ejacx6Me#+Kuo7C-r#3HR3t7$0z zV^3c;Z4mTOO-=wZQl0@WibEM~H6qP2GMI3g9F3sI-k59|<~#(^n)4x3xhkHww4`Am zyJhLV=>l^E346Vl2I5p~8HB+01EFKJ14W)gEHzN+7ng4UKA0<0z^d8_Bcma>E!EoGa$5Aagp z>)ds*LvExuBE>XC>Blt1^`b_+p?{Z4@`%+u5>$U!mSR%l$E5r8>9iVPFfBN^Q zz-RxS6k3tj0p))!C{i_@I-GQzLCO`D^{a2<;}c@*z1l}EsaqH?pZ)DOJvKJBo^@kR{p#Rpf5JD} z@Mjnm&M^k(g{Nv+|7o(hn0%3`u$3OX+qK@TLi;3UD`-dRp;*3UZu7-{$I||9dtkZR zc~uL0fo)v<*=zGY7SDqFC6fR-v9^r>*X7KR;UNA<7^MxJ@hlT5&;iWFj@9*$gjcfkYLvLN8#e>wN=ffghM z`@k0MERgTl*PenvHth^7Ke%|=GdVY8f>0n6P*>iL>&A2Y*OxjX5j$o$kKH`yZ9|66 zMyQ#OI`G*+(r+Y4^HAf%y?1O!Rcz(auV9Z)1;@=o6VtT#Gng%``8Q{R`*q!YhRZ*s zs8OrHS5xoJcF29kq-VNcGheFQMrF;(i4IgfLIfOY;vGKTrvEy?h2YT#Y|V-&>HNs* zIW_LryL&e?J~lYX1Pysi;t`m;yT)0Q$_9YD{k#xkYDAPxw zU719E4Dgm@N2v6*VAWYI0^nd@SmvPAyz6j{pogf9@5yX;FP zjem0Q8!Z8<^e_Ct^n0v-vc+Rs_I;h@7C{f6O~8~=V{0*$47fA-kU%qVL`2vBV|1WT35F&_b2zMrk-tU{q@{lykAn&~>h36d&m!EwDh%4G|c3QIi zw--me=Z1(SBWihfCJ&#GcZiWu(oeU$FO&?posPn3U_sJSO{pFin(; zU&!PKG1iayd@hgeLw&HITc^6N22j`+(Qb|R3Ciej9J8gDF9O-2SXGgsVj0zkA8{rTIFV;(=z_D;bdTobv#n>wIC)%vhge4yOTEt6}t&D$n} zG(OOin@n%!^Ip5oaQ{e#IWu3+Own6 zt+d5iuVx}%?+_wnTt^lyBneZ6?~L}S3mlo`vK$7~(gLqiGfr=&V?Ot48goZe6BW9; zkvmd+!FYM?FiHk@K~fF)%cOoQ71{9~RT((gUvXM9S}7_o!X~5AMa~DCt6GtbJ2d1G z{*pV)j?wkOrO0AExcN@f*XC1iS8C8Kum7VuYrUPWhn(l8%KZxr{L800flKC*X>*v=KOea4PcvE>Xh&-l08{C{o;$ zyn#7yimqg^hM?h}k-4OXgvnoHR<5wnI$ zoHspnglHcHNj(@@O4TrXBs(k7pn{+YtGF3F9|%<4h}4%K8bTdqs!sK#=XWm5!OhCy zN3U4nRFsAy6-L5l=-_+&!W~M|D|PJtmTA+uIk->*vV;MSuq9iHD$AG&E>4WyBMW6Z zCR?fmGGZA|JI5m@uX|cOfI2keUJRo%SP^O$({)Z>Kq*H=J>mKe(LjDPt#V%DDS=wO0dy=0!Nf^CVrRMBSUycdFBcWRt)ykAi66zZP8c5 z`oR-RX+3#p`;&UdJBe)FDmQ=T*XeG~k=PGx?Lq{tQS%c5`NAZ2;1L{oA4!d8t2g*z z1!KW8W*Q_QLCc8{3j3xg^4tdQjiXfcPnq~Ku8o6f#))%nO*;Kl&!S7ig0VwQbbanPQPP40{i&wO%2Qg~u&38QH&As3G`y(b8H|5q?*!o|VU-3zmnZoP zkYiKLaqxWA8nW7)4CL!VxOxELw{U+*y?$30KBkE>WtK{W9FL|ebLDR;p(NQ1ewg3U zS|hO(DlJ1rMMaF58yR2cFjT_!+DaMrVvL(MD)r$%;~qVwHEuwzq*mFgpyq`u7K)Bv zY}>}wdC5A7HXQTfr&2veq7KC>QSSRCCd~|2i`P^^uZ$4|7U;2>pgd11#}+a+Q*|U( zvuDHT^)-zD)kEreEEu1;zbLv!f}lU?g>ie*m9JL6@CWgH*zB;}Y(zp*KvlUc<;b1% zfaavFCj~Nw)fkLkO5;RKjW;IPJUWojG+NGP*E1*l@m}lB@l7yd!Cb4YtI~Tbt&y5B z-Fay96&?1a;5T95Kkyoe)|FRtU~fFUd1=WqxV_s68wDOn7An(Ejpu0C8|r)L*THh^+dGTbz1M<`m14O#31HW@A&)CRt|Ru zkTHs29*mv{E?sh9=^F7R%3mdqBWnj4Q`p(FnFqV2G`0a*Kv*G-h>lU7EQ{(*=GgJ2 z@KNI|T#GCi7dicVaG6SAiiIbo+cO@1AlVTBKlyjQG&`bn9qA;!B|9-3{%brdVJ`pn z7fN*FggbubS@i5`SI71$mS&y|duH?YvQ++Z=;Ku3!_zGPv)Nqf-Wch^l|) zlWr>u=Fkc3nD7M+UKRIX$ZCP(KjH7P$1@8!erV*iSpB6bY#9)ioiHRDxas(OPsT@& z5#6N^6-15dpm|1g+w7R~Wsjg_^vEn<4NDOgroiHS3yQ)%>|CY~1 z2#G`XcQHpVvcV~|#X9ureYr~!$;4g3psCJuB_p~%mm58pj?C2xE|f=_v^QKrEwavP zRlHS5k!HPwZ*1_w;YV8I;fM8$udRm5P(CYd<|moug@TT=0$G;a(rxV`Gp}YyiTIZx zaJ062p>>SC5%7+{Nh3qLD?z5oH`{R^!ekkFBdU(2;xFP6l8-uBnVIXP8WI_IB-SZ>{Ach8 z0O?TlAR}cu$1KaX#1#040et#W&-|*`gK@+`0W5$I9)y#<#h-;=<)dOu5AJ0j-~-9> z-M2f(D_QUG$H)s2c(nvaTDJ2wu$bc6tsO0sXU5YGZEl+Xn8okiPrlmsBzz-xqGBY> zbcE7M8h-Q0hZmEFO==45F<)&PE<~4t*+RikeA#%@Z3lJlE_-M6oGap4aQGylGC573 zHKhF-{OX$ucNHoW?)N5m7!?}-v5YN%n9H70|D#KgNty?d+5*bVre*7mM^7_-7Dp(ciwvWQRMHo_FN1CL<`cws*wh9VfT*fwr8QS6{={$$J2+T_yM=*~`1SxM(&x zT*iGibRoF*ia0Tj(V3d0nyTZS?+F#`d`*Bhxl9~5_nU7*(pBv+OgDpLbg~|GMlnN4 znpf-Tb0dwX2qP9eh#@kP=>c`hsN$HD%_D5-WQ+`@vRnz>y?;O`xuCCk!WfeySAYEq z!N8O|Yd<;274;7V=_1BgV4ZnU`kf2I_eoDp#}E7U98iz$Q}*=6du{U!{FM8(7A_%F z8s+5Y*;Cc<>Rwkk2w|SnWKFgZRnDmo%$%moD2E2PtMWi9wQe@evdPE>B0aCE9(PV2 zbwny?+xI2_dhZ?6QG8y5E3nh!h~=GPy5}+~T;!?}CWBg9T%Q@}d0v2Vja%RQd8XEr zRg<++x2!PVxyB%DjrI1kMQiweL3}0UXv3r;b&lJN zCvzuZgJx{DMiVS7UbD7kny^Z4PT?zK5uJ#GJ;b@DF>EGb(y5CU@^ErN zFGtkqES^{5%C|Pnu9(EEh8KN~o#^b{D!|knWZR5}R_TI1m~uN>sb~pQNo5xRRdz|} z9NyTd$sj(ZJy+#+X_=39=OTgz*W74y(^Ff>wC41|cUB6ytGQ)+qZTU|<8}K3I&M9f z>U=2T*D+<)Y#J>+LY4G4Rzy+4320CI5Amav_p;^ZmHONDDcszJxP~{nuErH&nT7Hw zM(BZWlKRFKv?a+SCWBE3n>_K4&H!Xql!P^UP_BGnJ#1mIhQh!lXeP|n=Y0Ng1=WO! zlAdHR(Q}llY6od$H*H_(6eK)eMXfOl2C{;8PLGCy2#u3@mFn}ca-{M;nN+)di!J-< zD>DM5*}EIPf&5$KGgPwbwZ8JYp2nO?7M%>`hVX|e%*d4|zx&DqYSu*dC+ zX&j%ETh@rV1irIRXpM(Xa&M(KFR(e-rXw?L0J_?Yw?5|F z2hN8r3+5?SrR5G?V#3u!K-)q*w?^ynD#N*aT#~M9-Cp*xKATfdoK0#0SU9Wj6C9=5Aw8h& z2di;QJTPpxDtruVS;__vZ1;T+&;#55#-IVyx}0iHWG@ceH$MfaP6uDJzpaW7bm?oL zi8(ISx0gPy(O%VVF@I^tVSXT`h-~~@%2GuA|D-X1?272P#iNA`d5p}T1mJ%JL-L1Qomd zSBMtm<=M=>7Y>pD>ETowNf}mO81|>HGf~k zb!;Ju7SnvErt?$R2ca^J;sIp{a$k*?7vc$-Xt$}FT(eW%?~S^Gy;OcAE9$9v_`EKn#-p3mf})nR zzBxns(PHQ@->{kP-D_TgMHm{qb{h=^L-6|t8tVf0^@|h6H1duIJh?9UyN6yLqa2Mz zg1Aq%xos)`(Nb^QF4&PHJ?4?%blTu-myicz2NfutF`r_@*_*FD^@G6Crot-j(mYZJ zPEJV2A%f|~8(|N28z>yPmiwm`fCsOP3)aDp7Vb%C9oRtTqjL3%p5)l;QLlurWI6c^ zqGT}neZODq9QmquuPf@_Emro4s5~CZu~&u_&kQqeU5~i(iqw&3(IznY72T_WnhHpg zTwCD>nGc-a zTkYn_esbNWiO&IoQ~d=rM8To@M6Bj6)eDyE*J`VIpaG)c>6^t)0Gy%l^{&-O>oA~P zC`msdGtf(9N4=nYn!SrGGH6EGHdu4K)F*aEXEmzp zoU(dg?L{a7G^&zGdv6{COMN!~6e4%KlpQKUnHt2Ki->|8*bZmxuf}_7K88 z=}(N-Xst-n9YF>AFAss@JmJQHI3A+7@frkV4At200)R+>i7dp9d3kf<%T|*e6f@E< z!s|0Go#SWf1O1$pl<~zi7E~Pxq@NnOhs>UQK{^+HF$ALi$I;lrNKu_mv zkD|LR&`&r$+8sHh?U%rtYWLO+f`k*>-h5K9hFI-}uzht49Uoloo2fj>=>Xc1CagSZ zpIK

|38S_5X(c1$1QW#4ar+#upi^y$w)S51!g9O!iF5Q<9Gnkdu#YR$yvG~Jk_@Z2a0y+-^vXu3A8hFc7-W%)2DgTXQ%D=07{>wvtdB`tZ z@?Scr`tnoX+=}-e%9G?}n-m$d4D^9bu?)5TBX@X7{Q{sU3^=-cBOpf!WdxTbR!Q22yBR4+Y4>Jjg1;78MP)#X&K<`R@6uAI&`7_F<>K9{| zP{BU7Zo4XJOJYY)2{-8}517Ta1gwH~Pp?(ih0efweATz40VD#bz1`3x`82c0H3ykG z*;Do5{GUx%dwx(3rL{1y0xRQGnHxN}S%?Q`QgmI9!<2!p(dK_}Y(~YjXcm4K(3wrq z-*go+U7I2YwnU)&Q*EDI@t=H;Iz@jua9J}mPkcXi=P_>HrRZ+I{oXw{eQbel0dt9U zmPZr^`!!`_V#A46@wehTwtv4bkd(EFkA%s~vdj#5aKpfxkJ{Ev#F8}0+>3_=j+M%V z&y$3~@k32E6#>(6NARP%oUN8QwbqRS`_v?Rp@YP6d_C8Clj=st@7h$UF<`XrJ7gULnb1p)nm?r1HNX#rqqD#)ApPCSW04ld7 zhgk{k*|)~P>;f=Q(C(Wr?S6NHq|$Y1^QVRp0adwM5`BoA?Oo z&DmN;%DStefC>VD(akm7HKz(N42;`P!O_4{G#=@~N_QMMmesPqK;*}j5}FZ@*LaX1 z{JSdz%?^wQa2Q51LKC&1|~FXwQW`3 zocu&~@h~882O7Y0`&InDQ%*eG_+?Xw~6%**2jHAWySDSk7{BIaVq%--S6z`!2z`WLwrI!!hDq|MJ)^63=daRyk2gDuaaku#%FejJ^-Hm|wF^VifN`Zy z)yQsm$%kWGdz#>Cjq2nm$pVCZvWQ^v9sh-K{}6V8zS^#7vHm%^`NxC!_)f$`TCyWZ!d)O7LvD z2#bpJ9Aa});Owz=JUJ?$o=xWg+pj|T%U0Gw`ZWVh^~T*w(NS#Gs5avkdM)P|uJ>{{ zNVn9XAiK-?g*m4mh9dQ(pRv@heVC?=OyeC?qKhnUTzmp=_#;|iKLUYw+BPb2IKq6xvfWVBS4VFBakhCmH6=)%kn|-_OL|8(16-@$a z>+wwcjLecIC7z5GE`y6`)gLe5MypoJtLEz2S9@c=qGA8G!20p^Z^vv+xfQoRmPN&q zdNbR(tMdUzGeqLT(z#`}9&RS=e`v}5N6MRVp;&ZE?-*eu0IRQBs!ZAItjAp456df! z>dvmWKe;F&10F&S{)!TF<3pu_ysY)1{eVQbPZIEO(^ZLSm)Y zn%9{w4I4$2N>C|MRjlBGSI{rV(a@?>dr5B$L*F&QZm6xXm#=`})*slmX+yG5L-R!u z#B@eL=`Pdu_}i$sR-OxI(M?R z0WZ=mkWrQFmp4N~XI_uAeL|>V7?nobnPn~7!W8VL?6rbPB21vF@pP-7{iM>`W;Oo~ zhquw=ByVBN0}9vhaZ|I}&)q&37+c3zvNKnPrZgj;D!VgF%_X^zd-I=O_?Lb3U$~>z zAInEJORi;wj!`OU-$V#$z@07@)zhOQg;>Q^kskyN zwxTg*pA=%pzq~m^Um1~HZr_M|)c;D@!>JGF{^?wmfSKdBIZ{Y9Amrv$&dtSw$FtSm z>gwm(LtuIkW7WpGEOTgGJx~O>WrIa$u#^GrGesG*+(Tm?c?zk9VVR0TSdKr?w)b?8 zu~6)xc;lo6f5^x!gCr`=Jsr>$T{$PuTPkq^Y`6tgKy%uMSE7EZsWRy~pcT2^YBtNw z>4j(HqBzKvoY3zL^xK0O6+`!sWs$3MCb1*Ib8yes7q{6PnS zEOdY0iS3(O_yN7)IpM?Q8Pk<3i>bSCLto)~=~srUJ8zdrWWYJB*rS)(=+jfp`3uIk zkgeK5D>|Me4>8Vk`*Ks}i?+lh?z(kbXL!F0VCoVYwenEAh=Ondw-4Z(%$*_VG^FHI z>g7@V>THudsI@SbhSLc8DdHNc9|Q-gJV6wU}K zW15?_I>wVUiq+4`g?NgRxZy-yu`l<=Hcw=x56@?sG7`R+}C3Z%~-0-hzvS!nN|fuVT!p?b2)T-76cNPF|qGmep~ z2=2aWNroh?_ald=KYKMlfmPA{teFAGq)dCcRc=;40%p*vnjaqm}YngQ1uR^5G_Anxl!L1H09P=C~U`UJT#1dP^ci#q*wZemoz z?paWPD*)8)8M$8m?%UzoZ*D0&Uzt$ip0<-k4oMP9gG!)!4SfSPH8p3arz;VB-pyK%!5 zAVt+6a?4@nUKOgERedt&VZ@!CU=fj-sq<$Y_C)3uTAfPoF8y3M@p8E{Gz z_2Sk@y|w9nE$5eY00;Z!JO8Sd{yU$nWv=6lKx5PKm$tyq0 str: """ @@ -18,7 +26,7 @@ def create_test_file() -> str: :return: filename :rtype: str """ - test_file = f"not_existing_{Faker().file_name()}" + test_file = f"not_existing_{fake.file_name()}" with open(test_file, "w", encoding="utf-8") as file: file.write("Test") assert path.exists(test_file) is True @@ -53,7 +61,7 @@ def case_stat() -> CaseStat: :return: CaseStat :rtype: CaseStat """ - return CaseStat(Faker().word()) + return CaseStat(fake.word()) @pytest.fixture @@ -78,8 +86,9 @@ def csv_file() -> str: Fixture to create random test file :return: filename + :rtype: str """ - test_file = f"not_existing_{Faker().file_name(extension='csv')}" + test_file = f"not_existing_{fake.file_name(extension='csv')}" with open(test_file, "w", encoding="utf-8") as file: file.write("") assert path.exists(test_file) is True @@ -89,3 +98,67 @@ def csv_file() -> str: remove(test_file) except FileNotFoundError: pass + + +@pytest.fixture +def compare_image(): + """ + Fixture to compare images using pixel threshold + + :return: comparison function + :rtype: function + """ + + def compare(actual: str, expected: str, threshold: int = 10) -> bool: + """ + Function to compare images using pixel threshold + + :param actual: filename with path to actual image + :type actual: str + :param expected: filename with path to expected image + :type actual: str + :param threshold: pixel difference tolerance between images - lesser is better + :type actual: int + :return: comparison result. True if images match. + :rtype: bool + """ + # Ensure that images exists + assert path.exists(actual) + assert path.exists(expected) + + # Load the generated image and the reference image + generated_image = Image.open(actual) + reference_image = Image.open(expected) + + # Compare the two images + diff = ImageChops.difference(generated_image, reference_image) + + # Count the number of pixels that are different + diff_pixels = sum(abs(r - g) + abs(g - b) + abs(b - a) + abs(a - r) > 20 for r, g, b, a in diff.getdata()) + + # Check that the number of different pixels is below the threshold + return diff_pixels < threshold + + return compare + + +@pytest.fixture +def random_type_platforms() -> list[dict]: + """ + Returns random list with type platforms dict + + :return: list with type platforms dict + :rtype: list[dict] + """ + return [{"name": fake.word(), "sections": [randint(1, 10000)]} for _ in range(randint(1, 5))] + + +@pytest.fixture +def random_plotly_reporter(random_type_platforms) -> PlotlyReporter: + """ + Returns PlotlyReporter object with random type platforms + + :return: PlotlyReporter object with random type platforms + :rtype: PlotlyReporter + """ + return PlotlyReporter(type_platforms=random_type_platforms) diff --git a/tests/engines/test_engines_plotly_reporter_draw_automation_state_report.py b/tests/engines/test_engines_plotly_reporter_draw_automation_state_report.py new file mode 100644 index 0000000..c075ecb --- /dev/null +++ b/tests/engines/test_engines_plotly_reporter_draw_automation_state_report.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +"""Tests for plotly_reporter module, the PlotlyReporter clas, draw_automation_state_report method""" + +from os import path, remove, getcwd +from random import choice + +import pytest +from faker import Faker + +from testrail_api_reporter.engines.plotly_reporter import ( # pylint: disable=import-error,no-name-in-module + PlotlyReporter, +) + + +fake = Faker() + + +@pytest.fixture +def random_expected_image(case_stat): + """ + Fixture that chooses random expected image for draw automation state + + :param case_stat: fixture returns empty CaseStat object + """ + if choice((False, True)): + case_stat.set_name("Automation State") + case_stat.total = 5905 + case_stat.automated = 19100 + case_stat.not_automated = 27205 + case_stat.not_applicable = 10092 + return {"filename": f"{getcwd()}/tests/assets/expected_automation_state.png", "data": [case_stat]} + else: + case_stat.set_name("Automation State") + return {"filename": f"{getcwd()}/tests/assets/expected_automation_state_empty.png", "data": [case_stat]} + + +def test_draw_automation_state_report_no_reports(caplog, random_plotly_reporter): + """ + Init PlotlyReporter and call draw_automation_state_report without reports should raise ValueError + + :param caplog: caplog fixture + :param random_plotly_reporter: fixture returns PlotlyReporter + """ + with pytest.raises(ValueError, match="No TestRail reports are provided, report aborted!"): + random_plotly_reporter.draw_automation_state_report( + filename=fake.file_name(extension=choice(("png", "jpg", "jpeg", "webp"))) + ) + + +def test_draw_automation_state_report_no_filename(caplog, random_plotly_reporter): + """ + Init PlotlyReporter and call draw_automation_state_report without filename should raise ValueError + + :param caplog: caplog fixture + :param random_plotly_reporter: fixture returns PlotlyReporter + """ + with pytest.raises(ValueError, match="No output filename is provided, report aborted!"): + random_plotly_reporter.draw_automation_state_report(reports=[fake.pydict()]) + + +def test_draw_automation_state_report_creates_file(caplog, case_stat, case_stat_random, random_plotly_reporter): + """ + Init PlotlyReporter and call draw_automation_state_report with valid parameters should create file + + :param caplog: caplog fixture + :param case_stat: fixture returns empty CaseStat object + :param case_stat_random: fixture returns filled with random data CaseStat object + :param random_plotly_reporter: fixture returns PlotlyReporter + """ + filename = fake.file_name(extension=choice(("png", "jpg", "jpeg", "webp"))) + try: + reports = [case_stat, case_stat_random] + random_plotly_reporter.draw_automation_state_report(filename=filename, reports=reports) + + assert path.exists(filename) + finally: + if path.exists(filename): + remove(filename) + + +def test_draw_automation_state_report_creates_correct_image(caplog, random_expected_image, compare_image): + """ + Init PlotlyReporter and call draw_automation_state_report with valid parameters should create correct image + + :param caplog: caplog fixture + :param random_expected_image: fixture, returns any of possible expected cases + :param compare_image: fixture, returns function to compare images + """ + type_platforms = [{"name": "Automation State", "sections": [42, 1024, 0]}] + filename = "actual_automation_state.png" + try: + plotly_reporter = PlotlyReporter(type_platforms=type_platforms) + plotly_reporter.draw_automation_state_report(filename=filename, reports=random_expected_image["data"]) + assert compare_image(actual=filename, expected=random_expected_image["filename"]) + finally: + if path.exists(filename): + remove(filename) diff --git a/tests/engines/test_engines_plotly_reporter_init.py b/tests/engines/test_engines_plotly_reporter_init.py new file mode 100644 index 0000000..22c90d4 --- /dev/null +++ b/tests/engines/test_engines_plotly_reporter_init.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +"""Tests for plotly_reporter module, the PlotlyReporter class, init method""" + +from logging import getLogger, INFO, WARNING, ERROR, FATAL +from os import path, remove +from random import randint, choice + +import pytest +from faker import Faker + +from testrail_api_reporter.engines.plotly_reporter import ( # pylint: disable=import-error,no-name-in-module + PlotlyReporter, +) +from testrail_api_reporter.utils.logger_config import ( # pylint: disable=import-error,no-name-in-module + setup_logger, + DEFAULT_LOGGING_LEVEL, +) + +fake = Faker() + + +def test_plotly_reporter_init_default_params(caplog): + """Init PlotlyReporter with default parameters""" + type_platforms = [{"name": fake.word(), "sections": [randint(1, 10000)]} for _ in range(randint(1, 5))] + + plotly_reporter = PlotlyReporter(type_platforms=type_platforms) + + logger = getLogger("PlotlyReporter") + assert logger.level == DEFAULT_LOGGING_LEVEL + assert path.exists("PlotlyReporter.log") + + attributes = vars(plotly_reporter) + assert attributes['_PlotlyReporter__pr_labels'] == ["Low", "Medium", "High", "Critical"] + assert attributes['_PlotlyReporter__pr_colors'] == ["rgb(173,216,230)", "rgb(34,139,34)", "rgb(255,255,51)", "rgb(255, 153, 153)"] + assert attributes['_PlotlyReporter__ar_colors'] == [ + "rgb(255, 153, 153)", + "rgb(255,255,51)", + "rgb(34,139,34)", + "rgb(173,216,230)", + "rgb(65,105,225)", + "rgb(192, 192, 192)", + ] + assert attributes['_PlotlyReporter__lines'] == {"color": "rgb(0,0,51)", "width": 1.5} + assert attributes['_PlotlyReporter__type_platforms'] == type_platforms + + +def test_plotly_reporter_init_custom_params(caplog): + """Init PlotlyReporter with custom parameters""" + logger_file = fake.file_name(extension="log") + logger_name = fake.name() + logger_level = choice((INFO, WARNING, ERROR, FATAL)) + try: + logger = setup_logger(logger_name, logger_file, level=logger_level) + type_platforms = [{"name": fake.word(), "sections": [randint(1, 10000)]} for _ in range(randint(1, 5))] + pr_labels = [fake.word() for _ in range(4)] + pr_colors = [f"rgb({randint(0, 255)},{randint(0, 255)},{randint(0, 255)})" for _ in range(4)] + ar_colors = [f"rgb({randint(0, 255)},{randint(0, 255)},{randint(0, 255)})" for _ in range(6)] + lines = {"color": f"rgb({randint(0, 255)},{randint(0, 255)},{randint(0, 255)})", "width": randint(1, 3)} + + plotly_reporter = PlotlyReporter( + pr_colors=pr_colors, + pr_labels=pr_labels, + ar_colors=ar_colors, + lines=lines, + type_platforms=type_platforms, + logger=logger, + log_level=INFO, + ) + + logger = getLogger(logger_name) + assert logger.level == logger_level + assert path.exists(logger_file) + + attributes = vars(plotly_reporter) + assert attributes['_PlotlyReporter__pr_labels'] == pr_labels + assert attributes['_PlotlyReporter__pr_colors'] == pr_colors + assert attributes['_PlotlyReporter__ar_colors'] == ar_colors + assert attributes['_PlotlyReporter__lines'] == lines + assert attributes['_PlotlyReporter__type_platforms'] == type_platforms + finally: + if path.exists(logger_file): + remove(logger_file) + + +def test_plotly_reporter_init_no_type_platforms(caplog): + """Init PlotlyReporter without type_platforms should raise ValueError""" + with pytest.raises(ValueError, match="Platform types is not provided, Plotly Reporter cannot be initialized!"): + PlotlyReporter() diff --git a/tests/utils/test_reporter_utils_logger_config.py b/tests/utils/test_reporter_utils_logger_config.py index 94c50cb..5428895 100644 --- a/tests/utils/test_reporter_utils_logger_config.py +++ b/tests/utils/test_reporter_utils_logger_config.py @@ -2,12 +2,15 @@ """Tests for the logger_config module""" from logging import DEBUG, INFO, WARNING, ERROR, FATAL, FileHandler, StreamHandler +from os import path, remove from random import choice, randint from faker import Faker -from testrail_api_reporter.utils.logger_config import setup_logger, DEFAULT_LOGGING_LEVEL - +from testrail_api_reporter.utils.logger_config import ( # pylint: disable=import-error,no-name-in-module + setup_logger, + DEFAULT_LOGGING_LEVEL, +) fake = Faker() @@ -15,26 +18,34 @@ def test_setup_logger_default_level(caplog): """Init logger with default level""" log_file = fake.file_name(extension="log") - logger = setup_logger(fake.name(), str(log_file)) + try: + logger = setup_logger(fake.name(), str(log_file)) - assert logger.level == DEFAULT_LOGGING_LEVEL - assert logger.level == DEBUG + assert logger.level == DEFAULT_LOGGING_LEVEL + assert logger.level == DEBUG - assert len(logger.handlers) == 2 - assert isinstance(logger.handlers[0], FileHandler) - assert isinstance(logger.handlers[1], StreamHandler) + assert len(logger.handlers) == 2 + assert isinstance(logger.handlers[0], FileHandler) + assert isinstance(logger.handlers[1], StreamHandler) - message = str(fake.random_letters(randint(1, 10))) * randint(1, 10) - logger.debug(message) - with open(log_file, "r") as f: - assert message in f.read() - assert message in caplog.text + message = str(fake.random_letters(randint(1, 10))) * randint(1, 10) + logger.debug(message) + with open(log_file, "r") as f: + assert message in f.read() + assert message in caplog.text + finally: + if path.exists(log_file): + remove(log_file) def test_setup_logger_custom_level(tmp_path): """Init logger with any other level""" log_file = fake.file_name(extension="log") - log_level = choice((INFO, WARNING, ERROR, FATAL)) - logger = setup_logger(fake.name(), str(log_file), level=log_level) - - assert logger.level == log_level + try: + log_level = choice((INFO, WARNING, ERROR, FATAL)) + logger = setup_logger(fake.name(), str(log_file), level=log_level) + + assert logger.level == log_level + finally: + if path.exists(log_file): + remove(log_file)