From cc092514674baf2c98fe0b7e06f363ddca7ec00f Mon Sep 17 00:00:00 2001 From: patrickkusebauch Date: Thu, 28 Dec 2023 16:12:57 +0100 Subject: [PATCH] 90DaysOfDevOps 2024 - day 15 Using code dependency analysis to decide what to test --- 2024/Images/day15-01.png | Bin 0 -> 36747 bytes 2024/day15.md | 285 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 2024/Images/day15-01.png diff --git a/2024/Images/day15-01.png b/2024/Images/day15-01.png new file mode 100644 index 0000000000000000000000000000000000000000..8861fb341e8e7336b55c3b65e9694f7dd93ca47f GIT binary patch literal 36747 zcmagGc{tW>_dR@7ib|$b$~={XD3#2alafdZnNld3WiFCABBF$ld6ps)5h5i+iIOQ~ z9wSBGb#_0${Dck1KLh?X;u$ zoBm{G3kt60d6UE<81o?2`&i0+b7OYG{V#RmZyOr6zCECC(4t;pmEu=+k1OWUY96W8 zg@J_|8Y(Gk8XNyC_!|w5(g+GRn)gU^hW)wme4$Zh^8WLMmy^#IzPh_ADH*f)nZ(5R zUz2TT6s4^e4aqF{UWflMCn2;bhW{=Ty0d+iDDCW6W>!}7hYvUW{MLOfci%=isjt5= zL#H$I>&>*qO|-O+Qc_aH-+%thBF#a+ZrwTt8Y)q7@heZ*s?+z}TVhEKzVmiKVDH|# z9R1>x=g)^`W(t0N?m&I|^y&1>Oa*iEl)mc)dfAavI?CGG+Tk?#K4UAZ4g2@+PkHtA ztHE^p`}cIt&dwj2o3+k{(6(|&fBJIQL?<~nm(JDI_079?%14f@y87)!U|*kUWMt%{ z^mLkmfdPxFS9K$|(NG2Xn&P`092}HQ#Y9DK1O~2mbaLWmpfSF3h1Sr}@W=4*5dmhO zrXfWx2Kh5*&g}MkcFuCBHZ9=UUd%V#(4elP ztDBUXs**u>?AWo-pFcmkHu)|8$&;HoIl>PgKKziT$yriTQrXf{m|Sty&o#39?2K6q z9bfd@@aSm&wl*DuGUrXtpFa=sJ)^6;j?y#jLjJbAT;__kwaKMR{@=>o_e7BIx^m^p zqqMZT=`>fjQyI)#wro+aFE1)$F234TMV9i^DW9q;1@VIiS5s&=ZQ2s}p{8c-^6#HF z{r#!Ge*MbDKqK*2T;tH|2+nHhoCgnf7(BOMYiVhzcI=p=~s~8(| zX=rGa(B0SKL_BKff44sz5fQ<_$msp%jhaEJ<9gEq7R1}s^t5+Hh1}Bea_{KqMpjnV z-0E?$k&zMdwIwAI1}{Cf+u7L(h=}-+`0eUqWMd2I=`ogG`08+F=%y<(iP4&Ov4`wN z+IA}`DRBu3Za{G67Zxhm+KT%4_%JXq-0)sQt)QrQbM)l>KVM6oHsbqAYdlzBJzHAxV3YG=qo(J0T@$mqwGeo?d{F*Wz08Wlt}+8(Ufi*5BR5D=yBQtQvMPPOJ2OWF)h%zdx_EG+RbS zhUotN)sK(f6}|MHErdnPXZ-7&1{P9A(aelDlt8pqkESYhZ4v(Rd_9nUYjszjF(~k^y!mS z>ncu82hO|nu6;3G%NiqUvReE=$EHxX!tdWNjCDTU_-B6nrk+qO9wpy?K%JIdGII1o z%=@|s&Q_VO61z2{?Rj;%hGoVVF8JQsxXpCp>+}4lPX{E$Q-j0R_#`}ji9Wm1;p6K| z6|#N*I!=$NRVtxu^-dNVLnckLW~I@{Coh)gzx+T5hq6og*}Pm_w|4DXy>hn$$2>G* zao)uo1~p#S)%7P_|9iy2VZX}l9k(hg<@t8*v}fmNJ&HXlUHjZ16#sO4Zf>rG^BBuB z>o0ytJwgU0ce*%&EsMp>YDrKO6cqH#E>6wPR!dF3NOn23uGKTYIzFWH?c2B2*=L{F z4m51RpD-kN{oWlC5>i4JtmUBOTkgJKS`)N|i)U%BRg-4p#(qhmR5S8tZc?7V#r^2m zgKBG4@GPd->9_~KF6`S*e%++n4~a?P*s+bzo;`Evy|ry>d7(QwKR>^9hfi~-m0}OuC6A{Ny<1y!*eWcfqFtJ-7h`G?D~{)6A$t@{7LDVKrOS^qE;3^M?OAIOgy4reD(C%xasL>svF-tX5Sr5 z+pCv1OBX^6zmbJ9{nvJmWE4v+WlmufVofA?jl+4j9GO$;sN`_uO#n>8-(~C@xCp&vTrJ zldN$PV6Xb}g>~qCoT7;d_r-S)Qsx+pRm$_f*QH0jcp)vMmrvPqCinHj!wx#-rt->`uc9zjJ@i4S>3a%_^0G-;X%we)gJl}-|k+NSkkT6-qT7dQ+I45m6R zC1kvqVB}S)?Cjh@PG@ZxXV{M)7HW=#&X>D!b91kH_39O#**89(gTx*RMw&=>QfB6cxA%l^1_y5}divDF)YNCXzm8W> zP+e>AO*n6m>#U3Pg-e&r@Xk2I+9B>M%L~XVC(oSm|NL36=GQ%`A!!MERHu;KxH_x=0#v|s!QT~oc)1s;p` z+2_l5eEj(F;p<9i-dy?@`!w9QFf%hGG^Jt79JW^DET*HfQQe^2NlD><_wexD`N?ig zCv3&bA(NrkNa$arRZ&8Z{{H=&nSnum+urkh1$r9m*RTKH+k2$5vy+*feYtG_HL||G zJ}^5w`v-QPk10?&%7K@<)ywL5)+!*E=rGn74fR@CdIU zH!~|E5r(u~gCtnXh(|+d(;Zl|dbP#M-{qiol#%Z}J@Uw4jx*nnX6EFi%*{D&*sy^I z#l6&Jx=(R*LW3tf0LRe+=j&lxll0v+$beS;{+wQ1i6_h3OAH1U4E^KxGEkLKjVSqZLI+0`49Wl<4DU8aoR+o zJX2BaP{?~GD^{*<3Cqiy+bGw_UwL4$B!Qm;gH!u zO~+Q*5VeQ>*99RQ`)V6Nx83k9hQc%sR7* z(w=8DIrXcv`pp}fr%#_gYgZ1l2Gig6aK8*zMb=`2wDW>^QK2a;}RyqG>1@ z#qji^s#7GE&gu!P=dqT{N3AqZomW+*MuuYweEe>cv#aZ2iuAO@c>KIcKJwi@w}pw@ zSTT;Ec5?C>Gcz*_E32gE&$lbJ4StyE$~tFfH_aQpK6R8N4%Stx zR;e95+B;^ynnyudd98@i%ddlT$U+p#ix)2R z_IdjAf~A#}>2zOBdU|@L-zL_nKa&;Y?B_Jxx^?S_VAQh?2dA!5r~Kk#e;q;3q!=mn z3m5oSuU;*Y5$me*Fz)Jwm|c8ZgX>?v_WhJ=`1a`XHW33ZN?X}Ao6cez&Mn)v-BsdA z^zm8!d;W{fQGNYbocp6Z;R5A0y;ZlUZall%rH)OAjEz0knx?r+N~+A?*@bb#EoL^h zuM^+m$AwpnZJGc-Q@+JZIgg!ua7{n%G>7=ruM$ifZt3XgD3PRr=xJzdywWahWo4x% zz|0g@mrSDG`hCTV7Y~tx-*j}`N48f+uF^SuTKUT}YhD={ncw|kavVGKFR5#4?&1hj z-F4z&&ttE_`}yBcTNhDm>py(Bg9X+(U#zh+IVOhX+)Izci;IilK-O5Vh{Qzh#b3WZ zL>qZ^3=YuYtHo`;Nf#CraC^(kdwY9F-nk&bxL@Yi2d*@>O?;St;{`8^5{3&9LEC zKxn8Ra!fn7Q^~8F)X6w#T{0{QMbfil*yG?P**~<%Ozht_-_u$@ug0rV{$pgMFR$Wn z6+oq^=Wj>xLx&vLnOdvIJ{PRvyuKuM(7N+AQ^F22#JAM)pM|>N)-=W~TW$du6^+a{ ziN+lfi8ccqaQQP{TyXK-ju*4z_d_6wVRTgvDVJGT^ zf7G5ceLzG0@?1T@QmlvF<^;CxJx_aI!w+TaiDF#W8mXbW^h4-A9!l7)23qFF(^DNf zbOSuu-qNyE*UfJYF^1 zBo_u`?!dky*$=^#Z_&(CQdX9XF0;A0`7_Q)R@sQ?$ZdYP)GxrSHGh}?kabB--5J7m zm!0F=s_TSZ% zR8@I*?p%kYw0YMF0C{Ra-=sOJRes^>DH3?ooyQ;eu3EE>3!lL;Py|l@<~qdwK7p&s zk>gYEt>Jl8t0^3eJ}jTSw)W=pZo~Wplhe@AtxIT{UvW!9Q#baloE7PwRZvin0lm=T z>=3VzP>Aqmnd(W>9{4HoyaE_OoYN_(s8lsJ()QPd)fHK`iXx;fE?;(F=V=X`nVq#; z`enx^?ZSW~?0ft6rt|LO2O1OPI1#d>L)c3C702<;1LPyUy}fEDPWWYH?B03eVE_uP z+1!t2dQVx7WZB23cK7x7+x5L(zx1=+=*|20eovns*r^#GvRgaV7@zv^;X{w4h94{G z0KL3q0cEG#jjs3347^3>Ou9rhH5#9rH^tCYA&BkSch0gX`2q#t(Rt9^3^gUFT3ZXV zv9pu@CO==4mi1r=`=<*O?6=C<)2Fv2B_%Ple!zZ!1E_3jqDT4$#J>q->VCs=8=B-W z1jy@}n%FM;Sa|WAazDGvVRkq&dV}g-1o9 zwV+c~RYiYT-QK>9gM)+g(YJ1`ZSQr~|M~Oho5n_^bLV#C8a(%T8@-qJz=17j_0eOI zE+gNhTGVaM0{Fb3`hH_SK&_~z0v{8%tSm?LULzF)12!fmCgww*o)s4Q{MVU_S$yE4 z-62ln`FqYAYh9<6MIa)8FrgfVhlkZPHC564WA9L+!7cbBH&iz@X*O@TW^3Ccc|~QJ z{76&gix+H!DmZ>z+09L|rKJT;t~Wp!Xx7BGKokx!!Qz6q5as~l9Gc$81Ha0l+kd043<z?^UL`S(}GJt)lz4jzsD^n$BDv6p>Tvz@7c2lNW--9#;PA5v$la4F}{3x<3Eb1 zt*z+oc}HQUK-)j_)~F7<{lL$Bec`$p&=4)Zik+R9y1F`ZLL5NAnY{BsaX!@77*IYX z{_1EPss%Zc{=h&)_2|*{INbnLzDR;Q(EYoh2hq~i^=oN41vI4~w6)j`U`DL*-8*jP z4JO9M6mNNoqM{-T3k!IoZ7M1%AKKf`7(5YR-Vo(>XynHa0sbu+Izfawl0=)u9i6H-hsDS^=}r z`{M@~%?6F)W4nv`*E@!4F z^D_l5sRgTD=PY zinx21v?E%{Dz8B4xw*Ni3u}Wxt62fn5;r$4rcq}sEa#^shWA6Y=tG9AL-{JO{&MiC z#OvZ~ljrf@sL>AMYz|7Y`MGW#%imR({SeBrfsXGCb3( zDhJ=ihCh55g7a#}zM)#0)Vjg#U=9gr#2Tgc^*JF&$_4m@)3FOY<}tZJ zIF+vIhityRMq4`lc5g+`LihE$`SCAX4n zc*1Frr)u#xjZSJ03-a?TRXII++365UY= z0*b@WaHLW+ti@h?1TB+`yF2gh-GTfsRX>;5@$TEdzug5}+(#<=68~r&6J%3YwneC~YjvhT4bI^JRQbGZC z2?fM~J)*P3eL>{+&ryGXJ2Mct2L9Ee)+YG);1S_NDHF(dIFV=j+!c22-1%$lNz+u< zwQg>-&}0RVXAjhgt{meD@4^3A0}gF!qHNN%+Hcc`wzlt*LQnbb){9DE7RJ~e%PK0iBS?1 zu^m=d;_zWR_EW`-U=%<*aHD9QJ9mzJT3GLG*zj{y^l#T!JbiE7GC{E@ahc{h=D~gi zZzdol^cuM24GvhcFCdd@snh6I5Uf+x^m4`^oj$+xl;Yv#Z3ZRdT=HZM_#(66mXu$> z@>*xlE-FpR08IUni8)bC=e|$ogpLmLdC#AxSy);7LAgS7&?|M^4eHbzta#hUk7#`C z*)JAzZw>Ap83_X9D9&0N6CGXWMW6blpgLZo>4=hE@l|#O1qB8sCLe%&;}4IH{Q8j` zM(9}(a#d&_Mn%im*Q{AX2q`pP)TgwyIrZb0IYI=f+H*VDLjrmxC+R_ZKa{#Q5pLcw`6``)E$A{*P*_dqzs5!lMC%@Y zSxV3*;Z|2rNn{H^FaiC^6$<^4VL5GFA2$36ot`&n%aZ5Mxep)S{`c=+gs|CKDk@{- z5k0gvd`#=xdqri~2$Iy%*AGTxvwh=Mp~}qDS+--J#bO>juxHl|gLoiGkofH2I|Ki! zIa`31uJ=gec*d~wbb*Jmo;xmo%3hVwwDo6hC6t#vy4jW6j6A&2S0jwwM4u@emqsBy zRe`0yc<~}YU2Tk*SqWXBYo+^SIW_8p$FE+0^3}%Vt9<|^`)Y#4+~=>Lm8S&$^&3d%&;NIZ0^HS4V)03hU@?LJ><>e(*BVZ3{j*@LEmbWH&%&bRdL6Luh-)*Wyvoq5BH}=#Y=n1m2@%%5Z zk{tb`tLzXtQb;~-4EL5|MDoBMqJ^m(`Iv>+G~xo?uaI~L%?({rPR?!e#n+dnsR@HZ zR+h&3^WEVVrp=qhtUJZg+VwZdttiRMQ`))3;w~)xnua*1ifH^V2@ZezY@(Su+KG5a zGF`W8*DfeWx9}iWk!=AW>yQWY-@mz}joIKH=P%H1*$sly7s z!nYr)dnTw6U0~BKOG+6RLl0^%iwka7^X!ee?5#%zt|MN3B{l6;hJ@{J>;XV!zs~s?%ur# z*3H7w(vF?3wLBjM`{UjE3nbb|AG3b*PA+b4Z+_X-sN1ZEcz|j_Yz|2>r?Q#~%c-PX z?S6TC`~Hihqr(fmT3VcaP8T>_rJHLI7$;aBSPA4ai=?9f@ImkJaJ`dW$PVz(uOZij;=}6! z@zqKFa}~=(oK{V}QKqt9$}%dn#86 z=|7_dZNd;-^RL)wsFW|gzsHi=3Ser|S!8)fjgMr;e;_DOOVLA?ll9Qm*A7iG{+n12 z_Mi+@qLHV3mCC#m6?N;#&2@Hc{K;2I=xKwZr$#HUI{B^K4w}lnUon>e0ewKh_kbZ$ zMB5xdqeLZ}5C(NAx7w@?$GdU!9~8b1pwL=iL5JNfnZR;@Co3dPQ3yB;s4_Wu!eFJ zbel1(t{RGY_`Q36z+Rl-NdC!ntgIVxICB-vOG&~c7zeyh*x0+X+S)hKY-Nh>OO-N? zmJ}VgpYBrzEEOETb=k^FL4Y|4=cLSeyb2}$aLAqET+mpgvuEDA6`bbtcl+u>*{g&1 zzD)0X!*ZZ@Vya5|JZRaA?;m!IBo?AO1*~h5Szv>>mS$&fuYfibT;ij|#Ad5fC=H|2 z_0nQUQFXsjN^M6!3BS1jju)?bB0=^xqAu{}o0#~j*RQDnd24`x&vo&kQWwr*3(c_n zsz|A%@#Tts%X#($2n@h|pRlm7ARaTfOkw?J#sCDAM-sH%S!eb-e+zSE8gun(tUhq? zU{zNaJ7holajS1Wb^VEG^+3XU2L=WP@mz4rL?>Z7*mxgCggf;YP(KLd@>Sr5UN+(# zH*3Z*fi6bX%j>V=X3m5r>VzhL1QF3{!eoC3bQRy#3M3^7>MWEF@DUWog5a+EyQ)O& zhxDwib4+mr5bYKg7AG~XPd&w-LT_NsLtvR$SkRz_L+grV$G@J9Je+xa>eQ(zl$&D7 zruM?bRTLv5Bf{ysK(PaXy{Pe1OvC+SdwViAiO7*qKaqBBqy7>R8NbgEs6;QLVWh3y zI{5tZqo1WtauD&`GOwJi0}M{g$=Srt&JIyV=8wObjvxE{m@!izO6OYWC+ooX5CcYG z;l7j=WgbSN|1C^(od{MmGU9|bE_(2wAD9UUHO6S?A=}ifpb16N^Z|R`;vO3t3jzOD zcD4|Ndc3T>NVFn02(?7|yw{@ni(h@g_=bt1U-u`lqeS1jHu3cc7#6U)=tQ*)4Rb#C zW@TmFRO36Pu{pRVJtqfh&w&87g7~&ipJeB!q}tCpt9ZM|Ii8D11;I)SBM2I86iu32 z3Q#$!fqn|&pJ|}&zX=`!3ghQiK}mY4p#||BJ9gNDX}WOnBFUF{TLA$9d5}fw$Bu0o zfC7vovkGms-nnyCEiFu7*U78mC*Uq%-gx~j`hvB9c{S1;9$js1zG!G_Mxn~KwYMws zgv$$UO@`N_z$7Cl=aw2Dq4Xj6kaY|S+5m2V7(vA76Hn6MFxHS#0OF29!7&#_lSXzH za=4Fa%|*9Vd@u91ZF`)*-{^By*8{)J9~}r`kud3P)o@d;f=ApMONm+ zBO?l23<6A>!s>2<`$ypoD*ooS5iL0yMa%tO;2;bQyAGzwxI_+k36Gu)rrvB1egP7j z>2l=T0YnO-Jv}4C8;@;h7{^l(ti+?7WYe+T;|W_A0)v~3aQNPT*5>r}NfU*`@ZEv-byfHR0!^2pDg`GIyf zt^4xr8^`?oysfP*!QI%+n%-UGOnNhAMpzA$4%^`tei#6F@iK5K;Q-%&a2;anta5p404!Ar#r>@mm5`3RN5p)D+iK)~Pw!7*nLE3_# zsUX|P+u0TL(P%;vLTr(&sm8}dS$J>V`T27t*o4r~(4fZ_Ha1~|cx!Gh*u_^DAGZX- zADtm^ly^V?&Fj~%<$1y@v6)1*;1(6lKUUnQqrCGj_nti)Ng5q^12BLc`Q&LnOWuV^q)IBt2#Q)ilL|q)E<_Uly&gmojUp)nG^h> z7TibR#!?27&Dm@xU!;_mm$%H24M%_ClxChh#4iUNtetkE7g1YvfBz*era)sevzutE z(e)Dc7jo2)q{dS%v?@^5w-E^+vJ_e@NU+}UKX60-!hgi3_DD8fO-o8zjVy$megp(2 zp(LZCQY0>!U%3+EwfsBLru!u<0?Ozin?bN6I{ghd))gHESqiQna$y}Re)3o)pRqP$@uUkB*uRnGkJ9I1vjz7NTDVzkB_qKzeu}| zj_r+8K$i*<0g)p}v-~=6b9!#BUvY79Uj{wAQU?C%x-ct2bs?q@2spbV42ism?NKRU z9Kp|$ejgA1s;bJT|IPi0?;up()YlU@izeO=Xp;QVxnM*~(7*?jHd$HOAKew#&s^nq zp8OsSV-vfSvoR!a7~N{2DT%_a@nUJ_?bvXiAovs#@Pu|pd#_|^xf_a|*!902jZpBN zJS>oZvHPn{O-+eG3P30ksTvJDG0@DU|YnfYmBnS#7lZQs6~cz^)AMn*?T zMFYQ9lK~FrQEr>Um{+}~&{i|--sfl5tWduQ-Ov~(wQc{!M&qGz2@nvrfYAh9gMhdJ z3j}~fp!wT-KYx5OhI0=NDh77;+puYU9~kgP;Wn!90{5-$C^$1U^}{(XD`w)TU=$B1 zhW}CCIL=72Yj}KMoPgttk(t@|&K(Akgyjy;2qP35=#fEi$0cQCtbcN;srH5IEYZ@T zv2dJ($qJooUs{5f0?Z<*$7B6~K<5LxGYbn(*?Ebhyo0m`kE;sb86XYw`mphIv99{A zvTIucr_jX)1O^`Q^ejK6`kZj1FbJLI&bU64m6KD0UONy!ssI!|`SOp*H_wGG-D|=o zr5q5Y2=svZipYHhF@EaZp>AW)*arTe5?fkZ?S4&}ut_=7!XQDcMbpz-*>eye{etFYvh?E!Z4$=MIIZs^`^BG0PS{jX23IaMi_a_xZh34%Q_Z_xYZi^s@4>b;&MG zDZcpmNq?{?*qsQRj{E3{fAHo*{Lu4>wK?U3dWYq-oPa}7i@In^vA0B?)KWzb)xb5@jH%@9AyMW+# z!J@hYW=2>{?bt(^GIZ2IQtX6tbhSKIGi_xtVTC(TsmjTMwpAF}rxWBPx}6ZpNgP#6 z;Oe%vwmI=9+F{>!!E=JfoevF_q|->G`bcAaJvZh1&`?^$?}-Jp%}kWl@<3mLc@=wh z3kves*x11Cl?=KKJtiX~PJalTFlyS`cfswd?%uuoVeMTg)(H8XU`$(@o4Mg`LDHBT zPD^M(1RuVAd<$y(meIHUerPN0*+p6j(Qoi|Zw zX6Tk8!FGS>ld@elZQ;sX-LsVvOTe*|GZw|dMG9T3$GbjwR0 z_Q41wwCJ=SF33md;_u>Na4ehA=FTf1)EDVVU*QF~LI-d@=E8{vHvTMfnc2;7^3IV&czD@zWmjmk%D;~2zKOd2rnB<_uzXIT4o2P`3xhY2mfcfv%$9yIR%SohPfh&RIMT6ILizWW*~ zs9?K*@E=Gz38G7o%lzdn;@;j2Xi3`57^!O_gcFVWghY3h=r9NYNNn;bJ}^+Ew6Mf{ z!iMF8$6kV)LjuOgwD$I|Uqhga62WC9*0-C8#)UKu^;fJc?}ZX&`*+Ee5Nyz*=&#?*ES$O93HE&Z zjvX!IUy4tkvq6pqt~~{G&d7*w&Uh!&fL1zX^Dtn$IIIV-PF%3EQU|;B2q`vY0en>} zmg5BQ3z#G2($Z3KsGOZITpeQH&Hd5$0(`M{n)!7}ln4g)R=BA8~P!56$SS=3&RaW20-G7Vyt%9-rdun6v z-``!^-Mu*S7h+%XM@QI3g2KYmz^UBz_Fe_s$d1GIk6RAch2~b&B%kw|d@)NWeEtOt zK}es%hNY26d?ZjqZr`>9%i}ol^|12&d7(*AkUE0!|Edw24Z4+m2M(m1eR8oG=P4a} z3UqZf2y_>$t&fA-O(u?;n9ed!mx%7~THW_0b7nP1Ko(xOa6t*Au!~-JuKVZKw{L?7 zn-cP7+`tN}LrXgK0{*mvpoPL=V2Fy2E*6x7HLphf8VCfY;2KBlJrfO;xZ{tLDTQ@m zoVzG<1JOn;0yDL>B!8k$E?o_3#aiaivDrh6G% zrDJ5I3Ff2-CdAe9T3T8`_WwUlKGWEQ!vX>Uh%co4Pp z;Dt*#XxU{rN8@zsp5(*ILm8J`k+}RxsK&m%&%~cN7Na7X`WuR;;N}6I$CI7Rrv88V z6}xg5WvrKl>nBaBm+SM2iXvg?f78$q6v`=g0!BjQ;N7rFxJaMRn)Mk!`%S+ADn~P5 zgqqOSRuIHAl-rzg@yKHuNN8Z;j}fmDr3lPuid($npD7`KnUB_S*NIV%OEzI8KQzK{ z|6*6Udo&&#IbCDGiGKS&Uy$O-;vz^8ln=m(`cXD#1@Qa1;$BFsIrIOXg;zE7%7HNK zKUkryfl53n)7JbI;09wUMV-lj`{2)|QZApz(_6s2mNSC1k#K4zR>u9{s*~yul@<;% zNlHq7A0O`;yrlcld>1`>bRKxBI-g!XllGAqre_uwCW{v3vhID~#>u(zxe!YN3j0*Z zXvlZ!N2m83K5!t4atEcLY;YmFK2Jut;;_RY14Yud=fDk#DVlQRh zx*xlbG%0xdnu7x`#SsZ2W%|U46W>QhGQ}s37SlIHEVW@(&Ow)(gB$YcfsD)Xvv5_f zVa!FWWI&>7l;cu_%(3exthMw(LbMEq;6vI36e)CK9NOyJ7`$q4z64> zhdY6y{PY;jC^Ti%R^r6L1o@jR-q_98nwAkyUKa`*g+Y+#Q=fh9@ca1>IQRLCP zr2L$Krw2Yfz*lMz3bgPpt)}3#bIHoSe77L$lH7N%Z0_xVeT@XvZy+)jh!9XKwHk@( zY|Yg5ggs4b5V`o2~T079|#0QzWncnUWYOeJ{5O+=2fDmvjC1_8EyJup!)PL#}FXcKRWzSx5s8Uw7?H?@m%RFT3=b7##%&d4JPL1xBf2^ zD9U%&$Zi_EXXPE(%NQ;~r;Wl@yx0*)gT(4K($uvb zWk>SB;f^m~`f4YX#fX%EKKw9hfX)@&o zS(%xg27dYx-?^qP|P)u*2B>XMb7kaGR(=wvvQP)oGE zA3s2gi`-vAs8V~&PkqqUy(HnaBmr;^*Hr$&vA>Rl1`h4LTh9P;8LT-W zxM4jEx;1-rC=FiZwHvoSLL<(4@Jb~r1r*Od-2v1oVsq;GmRJQlAW^}=uS&SiJWevu z(_;bgi-92hcV_>W0hL8c8z2C-AjzM-#n`&f71yt)JrhY#240)0-<8mVLIo+-WbH`Mb|PFD*4ixluQV0t)j$WGy(JL^zP* zWM_wfDdYTjy7U;C1hKTR_yD)sZs2mP2`Mk6d_e$%i$oR96w9A4;q7Ol zbe1~p`S|J6DVVg5C&RyCpG)F{?tMpp&c9hr@JIzv0m#^1m(uYT$y_dU<>>CT_wsR~+ z)9`8WuI81lt`wQ_j(9SE6;-NK?8G~Y1flAje04i%WqFcx`*&%?$SW-{;jok|1J>>Y z{3g&E_2vWeM4rGXsbWr&!m}^CV{Kk`T+}Swiu5vv&o|##wFZi17}ln~yF0;o;%mpq z34XY{ieU1rank&st9DP0EmQ`pS%)mJPn(~YlzaedAX%O(_xIOL7wK;Ce{ZZ{+!Ni(!dv;>TZ*M12D~D?BYGZDvY_M+&jp zAg~_%@{nbhbo^nWTzYkem&a}5`H%&S4q;`-kB2>VUzpG}j3_JHvPbt0uqmcL(jUKs z%EEw#2JN|>eXx|%tE)A2j%zU}Q3QhnVz&s!8`Ul2o%MO?=>b^TVKl3Ql93|y?d`=M zog}+DK=uVR4YlI8Gh5u~*(Brv*~xea@~dEuz9Afjw!*NlEG;dmaWNDP4Kf?fN>*&e z^3;M(EAaf|?6Lf_9fId`GLeP!0Ck0MODJ%`upK+F-%}@U%#UY`hC-&~0Lfq{;-x#{ zsV(=>&QgImj+3zS0r-&jkWD~WH;iPWTS`5;`BdI_RV7F8&pQrSF%SRv@u80Mx&}%( z{0ffwU^wn3C-a`w*KY+YTJ0nehS@D8kcCNLbrBgDbkST{p1eK`u1tt=U!!(J{RL)L z)?*lRBAF1MU}j=cz~@H-;@d}}mjVJ%9jrg+d!s4%Info1>7s7)`22U$$@a zNFD56waAI_79j6oa9mu3YnhPgzl!FQdCQiM{Q7yctx;+zr=TVzA*n?`!&pT=^c$w4 z-Izw;&62F-wHn&QU-k%VLMEPoMf}3T=yh|>-8y=Ar+3q2OPt<)YG9=vq>oo{a1)ga zhk%QV>&9q){8{i8|A#G;kzq&9fS3AzrejXGrXzmCM>lNT$hdubAXs>a<9&!L+iTbA zK$MA^H!#EJn!Z1C*)Zp>QSjr(d>->1m&l0W?}f=QoH}COQ4d=%DUAmYYl0d{{@FTO zmK)f56a2F-`yqZ%bgE$-${@!9`-mSIB7i_dZ@N};>6|tnOKLJ)uf5QRgM0x2r~%@k z_3~i06cErSE9>$l|7Mv8qhR#XSGr2K01WwK3YwT6TxSPgb#=vI5_9D=Bx68NcK7+Q zBcSBqrpZUy1z)@wU6z4=8WUt+a7+OK0rE;pRPPhy!T_hB3vh{wG9W7ai;EAzd$}d7 z)|ABwj4x62iD|mF7B0qh7!3v@V3Bqa{h!wgIv$+bnfr<+qL$8K6HVH44S%7Nw4EJ1 zj%O$HpM;P+Co}Iq?15hKRFWS=^MAYmSpPR~-pGTff=`DyB!4X~k|_l*FE5N&8=v`NIr^=B z(hKVRF3or)7Z(X4xFh}i`u*FqF-D98Ei6A$ujfMWEC|;O7ND!%p&|bdA5Nl)eN|I) zYn0)mE)+8)zB`!Cz&KLPy0n4aA4zIPB_*Q^C8ecFVCacdz{bYr*4TR?)eHnKaibu; zlQs?K)Nc8=6EU2_G)hGw*v}D=dDr+gBr=a}~!6v4lhzflfdsB;e}@UjVxU%VB%I+EDgj7&EGo>PAnN zJ;hK7v=d@V1T%MbsxmM%{GUcd4b6}YMIoo@*SeRW_aPPtjCO!} zl*fQ6)C{8M!aiS9Z7VD!#4L3hihK|+oz)Akg~sm-k1*M-e3>6SIp|<`U|v5ua zh#HDMF%82abc2lG^k%X#Cg$fe0F(rTg{dM^iQ6BhY%B#86%`jo2!N`<{hJ8i7I{=! zdd@Ped|(B?8UVHSFxV;OeU7%aH}I4&+Bd`ZK!iDP>+FnL+GsQWQxC=9n?N_asfI|Z zNl85LCJHueyrqDP2n0fRe7#PE9xJ9=vt_s5%;jTHKOmzpv;IaxA|f|X4<-g)k;fk# zT!&2}WIq~oQBlzz$+1hHKoAlw7*ZOzArgd`3czlxgDMIa4l?E*XBw3A9)vO;C|B`P z{0%ieD+k5TGo^s)3pQIpvm2Y6uf=FS8SuxehqH{2jTv9PN{1E)&cyjQaf+v{u3UKy zF3EQ2Jv+Hyp`?TbR~`_5?tiK%_~RgCaOF`>3dj;nCT@{BWIaALmp?ltwD=U0ZOXd3 z_jtAw4**UMC}SKm%7ir+C=fJ2M4kWq<;$Lf48p8*BJ^U=p_9?cVB=t^R)myC)aa8Z zZ@@ds%*@;ai8`ps%-Z@k@*LDeYJeF@X=!P)JW%J~%dSb@0e%w?r*%b1|6soGb+{$8 z@PH7rQHxR*cCfGrFeToEN~A627^o=g6+sGE6aMQ#N&;b zYs^Rg=aYb86`JTqwCUjRkk-jX89`L-5o729rdB%2CQDM^uXrw5_B8r_ z#dQgH5@_i7>eo-X$#S?B`PoivWU`NQu%B>%CuvkzlZ)XI)LT#l@X)MBk2jhV(^kHK zy=nZ#-PE*v8r==Y!kQjXmd3Xf9jlAXg)uUlAqb2Isq|J)lloByh3YdxEEf_Ewm~jCN&e(F3LFlT^m7(9jdfWtS6H+}(7$ zx?5(kHzQG@dP08SJ9Gr2mW7ai?x?50VgY!Hp;C;K^P?FszkU}_ze#s%dxj6*^>NwK2 zJ~cHpuRL}hm^ubp0djgifcrs*X@ev^Cw&=h|2?Xzl}U3+e5u_}9y1!a@29zZKl`6{5oA8@U1Ca7(W- zqVYrEh=SU&t=XrZ(CPaie~}vE{%d=&YMm4Zy15GJH@4?(GacTa0<&9d42Qb5AHuJrncrwvOnb_DgIX&L` z3eFiassg4<%xOdiUR&@xKBIcR3@wzXZU^9VBG*Ho!F?r_A3sKostK!iJ**^#4ODH? zYrR{~=bO$ZSaC_TRun8Mo2&=R%HKbG*U@t{;`=Yd8iDI%wn{pWsX44(&n@WRFu5K1 z;uXSj8oZimTe_A(o#ixHeQ=F$2dPdxl+bb>uv4JvWs2%vIg0_EgI7M^0JF_%Ru?)T zc_K^e&@NFxEtop|QO!lpzj)&$q@c#CIzZh6d7L=m$sH1m2)VF)Z#2AUX z571U%*RPV>U$ouHVKri*8M9aaMmYi*T55|rX5PMkUyB<;)}Y+se^xwx{Pw7tb;A>f z)pEaj)?kU-%FZ3+_7yl4D^!7cL9{aLB+dHui5s8TD|&d4zwmHI(pMnRMQck!m1!(%It>=zVI&g{ z7Yw56#Fnw-2tL6a4s=z5{LtbzKa^JpPT!DNgi&B%Nz03edg1Mxnvm z_7Wc$<~qq(V|xJ&ch1S!-4&QuB;y%`a6%_p>BKg}L`H+bj{^vVVyXlKkI{ovObXvK zk4G9gJ0B!wXhIR9&FaP1Z-SW-sI(SHA|F4DMk(Omr&B*h6C(#q6y$y;6A=7p9}HA^ zj-NcVD<5zl{u(fba_!-G4*9IT6)+&OLGd6y137*K){tg@6Ok z?H5o+yyIoeP-bi$9I8owI5sAo8GYwj>c^{#hENX3n6cNdUTWOg6FMqk{}ZH$ zX-}m$@wuVy)j6@HWX5j(oT0tz7JmON`TY$Xmv21@eDdT88BZa$KGje*%Pk4BXnNTs zY$@nppq}_bV4zff{mP~pFYT?w(<_l^4+-em^ap`P04f z*B>fl4?4jVLtSfm(6OW?qve!kGph|rJnZdzr@bY!VG@z|3-6b2*}8LXtmE#egwjzg zJO-Nnx;d=A8xcW5Ps=1HbqPhMr@#Lu2qBD%_>W5Xu7Mm)ngql<8CXQDg^jBDGHm%M z*pT<^*;e@PZz6`hR+g{j_A}6RKr8{cesb|$6__4;(;6~HAfylwWVvNv2z?qEJ_34% zl4VfG&E`HY>5ZNDK~lQfRa!$nxWRII;2PMBC6EDR{tn}nHBM|v3TSW%%*1r^7NGP> zkPOhdpOeR4C0x1$phX^=*s~D9c)hjTF=K=jNgVwzaZLuf>H-_mOX`mq{(u}3OdJ#6 zx`$WrU#)=E*hKVQWd0baAC|jXtN~e;-jl)BAt*Cs2a#fwkgK1EChNc&MG3206IQ1P zqb1yD<}2U8c(qUK5YqD&u(v5lM3vCDFnxAiqlg1aPFUU>Z(KZO3xj@`e=7{IG=lnNp4FTU%}or8~+ql^GqWf#A1 zCcO@)=Nx0Zo!TA<2xLf!_>O=P{(Uk@IjWmnAB3*<%9m$3r_bkj+YTXoYGDUx&e142x^#YA&SsA50}UIls!xd#TL-K|LO=eY1el)lP_h7IHjl#ROz}6Wqpq zq}`UP^b3pNpEeQ;2-Y9hl;Qi-lH*pqukI)z7zBpsvF>RSjF^m+zL1 zD2ZfJL;6n}xTqtBhUMRs-ES#y<4J~%xweqbjodf^wz&pV3XBX46o5zaT|3g;qQy%g ziva1Ylas4Bc-DhLY^eaDNBEfPufn>H95WxJK4BXaS2PTNj+GIp* zW>V0}T=H5R*d`$*RXm*YQWC6u@5TxsZ*qYbI&QtdKs9arAgG#NWCwDG3ouJka`LDQ zDKuo%20+Me?uS-r1XIhw42?x|(2#^~!jE>&=^Dj>*Z_bSzh!j&>|v&lB8AHoqdUSZX*KGEwP811DaguQrh&t{18H~X-xcICZk8)V! zsM?oArh#IIp%dWgjKTC7sA}y--?J+Iz1KpV;wK3G^(qy$kUnR>riZ9Z-Rd%$xf zfGJ&Rae2Cq6GX`obbzSrr*Lr=xu*;Z+~J9=ND`!khVnONGg{OP1-QI!8g@7#o#Jal zwt2-d@NhniHUiUx9rTBMWXrD1kL&9;ApuslHr>K$rzLk7Xonvv!0lbgVJh<;2UUc& z_M>~CfmOtGpF$_cdwQ2eGTDN{2!7LkyP`QpeH_O=GeUKw+}MBVy)hsyTN=-Cr-cba z#0yN>H^ltTEO$WpBcvFzlDfK@T5Q$$s=eUq$Z@m&S?+sM;5C_(qY!ogw|>+GzKKQZ zR>f#CNx~$>B9eiF_Qt*M8y~-oI}Z3$`5uq=%tfsKvoPt8Y4WxV?dKth`(Qr5laLSw z7L4@RC)~|1UR*~ZZ8GNE&23_kbp0`gyqK&;;-W0cr zP_T#vlxEVuqdA4w?T3YRi>Ynt7YB4?AQq0mOK^rU5@QiNFevg0XLBMshQG0R z1IU0EIln(b8;yn*RVWb=iXjChc$IMN;d<0N$wz9+SbX??7-0_kh1n?h_34mW5L!3t z`pLYGqCed&T)F@XkXXiX{pNrC4pMc>n_Edv}wPnoZ;6=x7XYh5ljp`IXxt zAs-6NRB^daB^=tabvqljHlAI?Bv>VGLqYUa!SVe(;E^cWXwv+Fn1^B>j6qgumnjOe z7B;KUK6B4bJbDMKBH$%X+CMJj*>_~>eAn5F9vB$~%MtZ%dC%_M7|@6)U$vE)nOsc+ zzjdMg@uNq>%NIcwze3IjR`bKe0=eMJ%BrDbGO+CoSU!+<DYMz4xS4R*8}#tjv( zOF6Y0QACkm{*eS{wDXn-+^|RnU2rQ1Pk8$IBkfTRUS2P+j6OL5h-rfO0{emz4EAR= z)DdXz-nhX9#Fxd4T==eu1{*+CGMbHilbD=btxWYi0|O5rsELk%nPYOFCYH>w)X^I^ zf#9N+W?ZX-5%X8IwW+T}9=E?@aqpM=8xlgx$H&KoJ_LikX#T5VYQ@EEgjR&Fh8z$a z^6+s}{va)g8Biba=^k8_pr)=~g}X7y)gIV;iYC1?E+V)ALJ4fi$+KtIsBCzMyfGXK;~_V+A1E2*z82I&aAd2bhNRnNalD*AT_9Rqhs3bClvP6XM^WgqGe!u%M z_dheyb-mx`d7Q`b+D-+J+tSi96WD>C6K_XIXy}MdPZm4;%Ci+y9w&nSm!Q(0u;*P6 zhgVN@cv8R#n>i_$Xze`Qdj5?Cz#VZzSKG*nI&|nzP5JKLJPagmc}!0JVS?zx~y(gL#>h^bd_OG50jF(rw_binUJX0=5tE`$jcauMJRAQ+hfiO?Kpu)Jyh zm?{79!RRa&BKRycPB|$47JmO0WST1pta8?0y{ZIz?6`KG=D^Aq#M}~&f-S+j&ExKT z)~mqB8=+Z>OG-T8Emx4`C|fW&(J%t%6iiVqG;0|JTufNhV(F@X3)6aNOt?Kl&?Wq8^X6es2t_dK%8%RN)Ui z)>|S_#uyp)>b0&9?Z%N~$5L3%r+BW(`qv{Ijl3cL+@+_jEn#2mLdz$n>voV1k=ag)zSh94fH<1Y*ZM27! zS#_DZpDL3^4op3MWOL4K zEalueUxhQC=@K^DbY`=nf-&z5&&f0!N7?`9wC6hWbwLS$b5TqGyT4(OrR6CJUZ_Z& z!$ZHtd|uZOSdR(+NTcro&J=%EOiKlOO5Ex?$BrL=X4Mg)?$oI@Y%DLr20)V|&~I;W zHR-c^@y$2*)>|sZj~zSp`KgFUIK09h_-&xxI-Ws6IQ|QY~VUT?M{cn+bmQCx%H{3PpLghyU3TK$=4VPRS^{V)E zD4#Wr2{E?R)^7s4K$^>^iuojP_rBO0Ps*d)#_DBVly zpgEWz5kvhiXa}n4=;YJDmLQg>Oq+nDUwPk?KHa(vn}2h`b-q#@Pm7MH(hD<6mo$3d z+={wl{Osx%S6wI(x{>TBGTPjsA9WdeMfg>sP3?GyA2=BUKU4#)+| zx&=D-`wim^KwDD+X z?fr2>37yw$1raJcX72c>3(Tcy!jx-}?#J&h;*plX9?H`iyrOoWeXf7aQk0Rm1&85~ z>#X2yp7Du*b-vD=!feqQ*whnvLzc=)uQ=&)c4KD^jZ+-vJ|@*G9fM7J#*QmN8WFc?)24gN zRzI(-G^11WnK$pYNCVA#dYY`(f*v-riO>I~>F~tWN+)lE@iduotvarM{Fv^uu@88V zTwB9(22(OgbMx~xNC(7&Z`8c9>l2O$p;44SitHF+7Tv|sNVSZ_l?iLo#JvHD_%nM3|r$WI{N6aLg_V;Mo3CJ zW{7iLL&GCkZ!k3qn<@P(A`AHGNR;5S>8F5V{qN=fqCkxMiNY@FbBxh{RNZYN(TId4 z4l>lCaN!bV%{HN8I(`21Y=D91i{Ej$Qwk?q-D+f%zXb_@d$0AS>7IvJgW6Gf^fX1s zD=$xQuzA;_d2Ufhl;Aki;aBOsJ+ct}?bvi93HNxTAASLDE0rgg>;i9!_QO4Y1^Zgf;zuRU(zK*@gn-||F?x4NN zVcXv#Ev*IbhFx$RKJJ6|Zb~dH?Q|NR={SsyA#4FjJp`Rn%7joadKT|Te>j2oQ)vE3 zasp3LjwEV#YZG@|%oA7XRf_BG){`fDQ|hj_FnkfRgFaq4$QSf+t?5FT9RX zxY&KAb-??w)JAB-zZLyI|J#0&;h{#*u3Wg_xwliGUvJUmfwzni+*^9<`68Ure)v-xc}cPCFid9J16!}eoy zK-m2OC{?GY(ID9-oXXNnGUtV&+HjW@uauciT#s_;yMy`vysPZ>nG@1?= z5T2a;dc2O*IaF`V>q6X2ad4PhUkoFT-f2Po;%BIxe<=<6sKtm%6!xkT-yY0{N(yp- z@2_g)Af&zclai*i;`5~;tG^9-e&;XaRmcwqFpdJy6OT|@_AVPZys1LlCa}0$l=;wR znBYin++e#M0=m^{=f)dFac4MzMN@|FUL;S+ZW(>QxGdRw&iDG4h6Q)t?)^T)!O5wa zqU1%%(wh%aFw#6ctM5lI(;Uo6VoGVQnbIYD6G;78QpVtsg+F5D)M?i56&lTj|*J1%3FzC=_tk?;lGT`oeyt_p>@NFTC5WkuPZH|xE zksxxoSj=rB=6fP-*$;C=(Oo>_`EEkkBsqZ@_U?rxzTzIBg*{`~HD9~DzBi%^$>WE4 z|AdR@G(nM2Bo-_FmE15=N}<1%-DbeBzy&wO(NwB)bOWG``m|uhT?_(yhOTz?xVa#b zF+-w9VKh}*wTgXga`MH*C;1tAZj+(Beo^cp`#!4(>bn*nMgKveh??n!n< zUik3gw5kEWxV`8%6#d2axHyALYyYKoW#eOThJzW*dab^f)IuqfsYf$#0r&x;h1o;2 z--LSt%9`S)@bUJ}kxL6GS<|t;|3efJf9w2(3$r%a47QI!O(y*q!>PK3kfL!HF9ux6 z-yiexx09h9ndXY)?DaD#kFQm)-1hOE$;nDFFgJ1M0Nwi7X?f})L^qO5CUqCZu>ZHl z296u&W%F+x6uF@h((>qX zFEv<3dv!5%+l=6c|CQ5JQ+v}_U499x4PBU3f256J(A)7Z6Px$e)Zg$UF%*E#e{ymhhR z(4p~oDC5xTn$wgL>r%`#U4r#~*PcI5A39{a2X8o3t0%49aUR5RxXK!K+S8h&dP|+Q z_}y96X{rV;e&irZnH0OkssvZLU|>QwwNosLMcWfDfP56pF-#M$&8?iAk%md?UAo+0 z$oMDKZ(DYr-m{TRushS!gG2qs*?Aj$o0AW0q?l6WYkd;DwpFuc%_Q}7kN2Ep2{A(1 zKQnKIK219qNhl{cu)iql9ch@UGwc?Rg;X1iEqyL zCh;c1dNC9Xy$`w0c@kb=49eqYeRUb!;_OeGZ`J z`a;flf*DI^edtsE59;J0j>C~{-nr0CELpfv4&?Vw9oL{tFylp1NG)$JM2jyIArMzg zfYHEv%ewdKl_I~49$x16Sm%OXClBz~p}zZX;O8+Kw!-;7Z) zqsuFu?Ct4mmH5bymn?thQj$KtUAK49C4TV5d^>~iYoi7@= zQa4BE)yuN7Orh~dwkh-%c!$sn8T$pm0#i@cKa$FMp2iY>h+YG9C>z-sVkRHDEMH)s zTWD{axbqfkC{e!XkX``$tf6|R;qI`&J%t@H3zWSC2Y@q#f>u_^XM^-cB0_UX3r(!@ zcxKCPk@=h2Px zm5flagl6!L)$-T3NJ=fCFRA`3HoXt?fv2?V{mjff=%MMvqM(l!YmF}liJ4B`s!Z-F zX=y%+IQEz(5fK7BT|(alVqR(0bLB(^cz^mnfs+IVQAo2-a~pvJX+39pvVObD;(X5u zr(Xo44Ov4oVe_S?-^kX9YHT_ISlC)5I7jJ@SnEz)TU>v?tv9r?j>;_`mYlCl51}F$ zXXKEi1NpOYyLT_qifWJBnpm3AfVe;>!t+o7BoU6#*%8!nE-|UUcPyrTB_KO7eZv@n zM1u|S{%^)=9Xt-A5gUTOx4isM)DjObt+4QGzovt0F!Tt_UnV<9eb8oGvs^W8?MMv& zEs!;0v%SjUSOyyhHKqx{GIMmyZnVuauLN|UaO8r`VvGzY2GgDQ4iv2n+;buFusQIm zgi>LH#;#n>#7A=;!Zs6mH_^8e-y)%F zTFfE>KyyU8=GoO%9wPsfcc=t#@A%AgoWsnSo8^%S`VA>7w~tZzgbD`bvVa0OA6e{0 zONK@0xEv1jy{ExUGYgqakULM}_AE98KLno%OO3^<}>+6wBh`+@~+=%$hr+dtU< z>1}(}@Zu}01`@_{@S2AuVN_9kh6;ugOC~*a9^PJh_2ZVj|9S9GIIh)ou!pk_Vw#y)9g`G zPIj{Ic5=k&$~+CF`20MYILBr8QZ9p(%PGTLa~`=NpNyK_!X~~AzeXMzu`5+2DZMBS z%JPe8Dr<&>(2`5?J}eVxH@eGT1J+F$`(10cr9sbviejoWNOcGGOAIu5a_*(vh$4gmfJ>fGj!u zNA~wkji5K1vjW8W5EwW_;+wF2;~Y?s!#0d5tpMIgd4U(3@JydkfGW&95a#gLipfJO zL|?tZ?##fg7iR2=U*3OMtKA-H8~Sd4Q3Xz$snh zix-OmwGyU?mN)zUeRUrCnZ@T|Ab%zp-c1_c=~0gf>LcqeSvLeqnE#At;m#cr7^(Px zD;htU(R9gH;w^|dMR@B{N(DHgVshz_>Pc6oEf3#(aiAg7Z;7U>5X}U10mtIuo4Aar z^;yk}c{gFB1<*r#F9@Cym z->zL#C|8Nv0$j93h(QadB9}{6hz2VA=46z*V$INAtCobkWd!7b+;2%^BRihLVh6r` zK5~?bU(@g%@$|JdTY_v|j>4OYE}3UFr!?IfY$6gqi+uLo)pNq+d%rC#C>XwS<%Rw0 zbnSF07^buTR*^7$nk`Wp&;*Fw7$(Vf*Korb!H&TPk`TXU2|3Fk$1^L-`2KAKZ3lJY z=F$~3FuA`eTa|U3loEBidFt^v2t!!uO(&qE6C^ZGs?w|Ze&@~b5fM{|LohrN` zO=y^bB38$*klJ02dXmCpdS^XI|btKdlf%V_2A!{ z84ca9N}nM2Z^A4JvRs_%*WTvaCr_1MRKK_H7IwH>Qj833Y{x4FYDsbtqu?hXf%_O> zMXo8-8x>`tR)ku%Zc>bnrz8iMk{(o!(mzVi96l~Y=Bj9rLzJ1;PM^I}FoZnT>9+aF$ zHnwB70sm7!W6jk4yy;!UhskLwmw}ReVTYHJ%so|zec2f<21zmTAQAH#qI<4PdoLk- z%wc$=wi!d9Q2sCr7uMcAUF#EJdBrBp{<#_jQIof5t>{K0_uIyCc zlRTtPYF)ahcIwngvF|C{Vzd2?sQedK(NoYG@VO43r<)XOD4@eyHKUA>KQ4tE8|D&`d#!L%FnTv8F`OI2ao>|+yD}8eo==xn;g;KQykcf!LVQ`s~uB~g=gO5U?Y@(pT+~~5^poG$3 z2Ka+LQ%SC2;Y?$%>KcvptMo+%gb~RoV;X62T3FkpuK@vFNB1um76D7M8!D`{b;J7a ziy8ZRDX#M)jV{r^NsTObm6k2-m@u)&Cr!muqJ$G@BrtPn+rH19XM?z;RZRz-nyr3O)*I`(!Bp)&27 zHd95n&tChR`Dja-Vnp#08?Sl{=MR@5{L0_V!7dhY@Y`P@r{o>$E>5`r7KRz6O*5^! zBm4h%m<#L0XZr*#TAT6xYa5D+lxwvM_Wu>^RnV5Nhu!yXGu{>7kG_F{3aiBV{6bxL z_;rG6n3`_c$kAzJc%@O|(AgD+tDNttAX@<*6IT%N>3bC6)z+B>?(_1z}X9~eCYTj_{%&|ghOmwuBh#X!umRuRHeLSugVu#EhyuK zM~$_7*)M zEZX7@M@C*?5rOxKYEGtEqQ!tv1qcFg$rZN%w~$ku+`-C!Sa0-M;Uf$yVgA=KnTx89e+{HAEMd?UAgs*7 zeo<2z2E2v{75+i4Dxnf_1Z&CI`zi2_#gpc`2XgFh3_gUv1Eflc87SBJ%F|Wav2H=k z6}2Bej6{*Wh_E|3JLUGMy1E4F?3RIldyd=X?=*F46Bs!L5!inT)iT5)YlrQ-Z~XupTbEyr=M@g0F+-En0q15=xhLUmyEDQj@uJ>cIgA1nxlUFA#cui@ad23d|S@YKG zZsdVi$gSamI|AyN(jwwE8em9QQyodLi2NH2#t$@^#UG;7*nZ^ija(qp-c$9zFcH@06?f?@pPkk#U2q3HidR6 zW^b&T?U;%fn+Hu4482k&2%iy-pMC=I$C_Qc9*xic_I3#5zu*Sw>lbq4GSCfGRf#Ss zd{5N{{8%X5nGv?#c2HLh7lx5JN93T$2=(~8n3S`Dfp|swAG?OB+3p%S=v^b7mS9v8 zp-o4BmHtDr#`q2dfVTyGluJs4Fkk`WG|&KHBRhRGs~p>#L>;rUWhf(LUv)A6VAdP0aD@n~D8@=e*%AY+8 zAnTr`|k+5lHu_S*6-RHtxAqmgDp3ldAcDDc5eqwr(DH^Ke)9UYUzLyh={q#C@7_p^9Q!~pXkg@y z`VO5Zy`(3TxrU`C?1H4mT```lW~<`YVU63DeiJ>^C9>Q3 zF78}Z6XCjOFPr>ky-Vc|vL7Ye8=wY`#s5RR-xcna5%>|_HXNt$m50Mk_&hF=PTQrf z%1MUC6XNX5?U}3Q18`XWQKcp@HFO8wHELehD`y%xAvThxBCa@!rsJrdv30~{yTgWk zLZDj5-1+lo_gsD-vV%EDwj2^3fxpd{9HAROD7OP~D9-b_xr5(+x|2>VQDvbf>7SaS z&~g}4CvpTr30WOGzMQFi#W%A5oWyCB`Q0t-cEYh^ zf<_KNvSQ9;Ai)EoD=yaHDa9lfyajF?Aw&MHorzn9=l?ZWY;8lif=?lLCC0{2K12_~ zB`Yc@F9=cq9-_RrjM(mmH-vUlkmB%`7d`V@-`X{x1%VK9`zKC^g?Q1zH}{wDq{|Pvj@e9$b_nwbY&4uZTiC zU$gN)T41i?5a&Fo3knJT@#?jI`yp8BY;IKmJzVUDZCkCJq^Y#On#=PjSGS*TTfagjDzDidsUC)|e>+VxPZs zDb!3kH!gcwVrwp>Ky@H<2)}I3lwe+mvS&-gCXUxaDBdyiUNsrUUY9!{ftRtB9eUz< zm5NkWroeYVu7w;2gvjr4?Q@0o50+f%FU9S4QPp-Gh2XqFr|`iI3H)>_>vGmb^T2P1 z^C{>RAPK^%z`1%|vU*dR5e2Pq-MX|IMWTH6UY_mgi=vb(op4P^D#z9@?p(b#j6N30 zfLHu&1mtpzD8yOYxcb#&w=oXib(CxKZp55TU7O?>s^OSC(Kcb|!#xK(jC4$EXWM&c z^PWo!!ovs7vb!JdKGko_w3O)Sjb5q;J$^mtJ$+M5|8^-m`{--hUp$aja_5a+%t`Oy zn`tRQ)ho05rY%`^C*|0>C22uV00O&~WN^xjN^IM4)2~7V@zUx2!Q9L1{D+P=ik-c1 z;a%Ssz4Z2%ew@{Cn^3FFqJ*=1pDn&1#zf|(=r@($8boXvQsA_j2UmPyNiUHZ@>lNz z(_PbT*rCF54w1x2lZV3|qod*eo2hq3+Sxs+o7K>&^mS^9zqa*MndGyxPc5#zVRnv& zl2F=d(<~N$7=)!Q_1rln1wEMDsRhQPN4Z5vnD_+MWZ559qxYJ)q|SM^I&803O3CHL z&G>a`Mw?AJYk1>P!O9AOlv&?Hf@kXKwTm`gi$Vl6 zeAV%Z8Ml5vtEw2Kjq&9Be1R*ceo?4#(1p`KL4b^(VidcEV(VqT^VIAd)dX&^UwG+p zVr0RSCw=LNnp1|_%M*2Wu8MB_{%qN$Uw2OENnN~Gk=IjCuOKF3&^=sVBM3Ih;x~wL zZtBhAc*I*m5S=)2V#=`x>5(Js?ArI^I`S*7Q8>l`Bphmn1;0XyEB2vHQR}OQ&cF56 z!9e})?`JI+Jq;^f+4zuYPVjy>c(>8Iyy5p($G%2rBy0m&3DVpx>zk2SR4v91UISg_ zwW3Aq*Ux#H+<2-{;SNlB%DJ~Vd4b+@T0VC|LA9@7j(h(u(Ytp2hxOa;{iM{o+bLeX7%EqwJevu#m8;f^QQh+u6DHt8e4Fma7M(&SR(nu086U(75)W zSJHabEE_CY6;Ji`K96XuzI%y-fZnSa@sy*9N#&A*zpqtx61OIOhQw(hQ2Oyew9Q|~ zmY!nX_AgD>0zdj{d@!!X8p-E{nF6rCWo(?I);9W0qhrHFZctSYm0I(+T!YFWy5Pmz z&W8#&$k}5P)P)@a-JKqZ0$ID>p|V-vEa!`yrU+bwY@&{qFTQYj=fpoCb`3i>I4msIDvsyU|7w zGp@%ipMH||!jeAx;CHE(Zs~z_UN$FBm-ut9bojWWH950!8f#&Vsg7-4Z)f++t>?_( z%%V3pC)kg6Vz&YrqLuPEb78$*YKI+Ms&VMeuq(!@6x^sd+1F)mfT)@J8T5?q%eg204 zWggr3Cx#gw^UrCLFC{;SRc`t6fpkI%1#?)LYVF%Aopp)8z1sM2C2eJGW5W`r#{{lB z0mUD_WJ$k&=MThpoly94U*SthWXTUS)zkZI@@{@Z5G;KwVlYKj2xnLK{5Gk-6M9}1 zy$ZMFaghkji9Xlva2P|B3oi3`ejesQ)-FW2`&pNQfBxA7^@fd4Y0{zgAo_pWAF+eg zOLghPKRbHp>+N5#VMcH%$#Ns+&Nbd^6SVQpok^F2zH}tUt?JdQ9K?*&D&=O)%KxxJ z8{Sq!V?ALs`lJ=gTuQc}umPYShaHW2qvuMgU%$Sp^ivv9k?RB98>4wkcB}Qa>$GkA vb*XbOo3m(V)3Vj+t%uv$b>H9Yd$X~wo*z~W>~TfG|Hh7-WS4B~zUKb{U6>q6 literal 0 HcmV?d00001 diff --git a/2024/day15.md b/2024/day15.md index e69de29..48af89a 100644 --- a/2024/day15.md +++ b/2024/day15.md @@ -0,0 +1,285 @@ +Using code dependency analysis to decide what to test +=================== + +By [Patrick Kusebauch](https://github.com/patrickkusebauch) + +> [!IMPORTANT] +> Find out how to save 90+% of your test runtime and resources by eliminating 90+% of your tests while keeping your test +> coverage and confidence. Save over 40% of your CI pipeline runtime overall. + +## Introduction + +Tests are expensive to run and the larger the code base the more expensive it becomes to run them all. At some point +your test runtime might even become so long it will be impossible to run them all on every commit as your rate of +incoming commits might be higher than your ability to test them. But how else can you have confidence that your +introduced changes have not broken some existing code? + +Even if your situation is not that dire yet, the time it takes to run test makes it hard to get fast feedback on your +changes. It might even force you to compromise on other development techniques. To lump several changes into larger +commits, because there is no time to test each small individual change (like type fixing, refactoring, documentation +etc.). You might like to do trunk-based development, but have feature branches instead, so that you can open PRs and +test a whole slew of changes all at once. Your DORA metrics are compromised by your slow rate of development. Instead of +being reactive to customer needs, you have to plan your projects and releases months in advance because that's how often +you are able to fully test all the changes. + +Slow testing can have huge consequences on how the whole development process looks like. While speeding up test +execution per-se is very individual problem in every project, there is another technique that can be applied everywhere. +You have to become more picky about what tests to run. So how do you decide what to test? + +## Theory + +### What is code dependency analysis? + +Code dependency analysis is the process of (usually statically) analysing the code to determine what code is used by +other code. The most common example of this is analysing the specified dependencies of a project to determine potential +vulnerabilities. This is what tools like [OWASP Dependency Check](https://owasp.org/www-project-dependency-check/) do. +Another use case is to generate a Software Bill of Materials (SBOM) for a project. + +There is one other use case that not many people talk about. That is using code dependency analysis to create a Directed +Acyclic Graph (DAG) of the various components/modules/domains of a project. This DAG can then be used to determine how +changes to one component will affect other components. + +Imagine you have a project with the following structure of components: + +![Project Structure](Images/day15-01.png) + +The `Supportive` component depends on the `Analyser` and `OutputFormatter` components. The `Analyser` in turn depends on +3 other components - `Ast`, `Layer` and `References`. Lastly `References` depend on the `Ast` component. + +If you make a change to the `OutputFormatter` component you will want to run the **contract tests** +for `OutputFormatter` and **integration tests** for `Supportive` but no tests for `Ast`. If you make changes +to `References` you will want to run the **contract tests** for `References`, **integration tests** for `Analyser` and +`Supportive` but no tests for `Layer` or `OutputFormatter`. In fact, there is no one module that you can change that +would require you to run all the tests. + +> [!NOTE] +> By **contract tests** I mean tests that test the defined API of the component. In other words what the component +> promises (by contract) to the outside users to always be true about the usage of the component. Such a test mocks out +> all outside interaction with any other component. +> +> By contrast, **integration tests** in this context mean tests that test that the interaction with a dependent +> component is properly programmed. For that reason the underlying (dependent) component is not mocked out. + +### How do you create the dependency DAG? + +There are very few tools that can do this as of today, even though the concept is very simple. So simple you can do it +yourself if there is no tool available for your language of choice. + +You need to parse and lex the code to create an Abstract Syntax Tree (AST) and then walk the AST of every file to find +the dependencies. The same functionality your IDE does any time you "Find references..." or what your language server +sends over [LSP (Language Server Protocol)](https://en.wikipedia.org/wiki/Language_Server_Protocol). + +You group the dependencies by predefined components/modules/domains, and then combine all the dependencies into a single +graph. + +### How do you use the DAG to decide what to test? + +Once you have the DAG there is a 4-step process to run your testing: + +1. Get the list of changed files (for example by running `git diff`) +2. Feed the list to the dependency analysis tool to get the list of changed components (and optionally the list of + depending components as well for integration testing) +3. Feed the list to your testing tool of choice to run the test-suites corresponding to each changed component +4. Revel in how much time you have saved on testing. + +## Practice + +This is not just some theoretical idea, but rather something you can try out yourself today. If you are lucky, there is +already an open-source tool in your language of choice that lets you do it today. If you are not, the following +demonstration will give you enough guidance to write it yourself. If you do, please let me know, I would love to see it. + +The tool that I have used today for demonstration is [deptrac](https://qossmic.github.io/deptrac/), and it is written in +PHP and for PHP. + +All you have to do to create a DAG is to specify the modules/domains: + +```yaml +# deptrac.yaml +deptrac: + paths: + - src + + layers: + - name: Analyser + collectors: + - type: directory + value: src/Analyser/.* + - name: Ast + collectors: + - type: directory + value: src/Ast/.* + - name: Layer + collectors: + - type: directory + value: src/Layer/.* + - name: References + collectors: + - type: directory + value: src/References/.* + - name: Contract + collectors: + - type: directory + value: src/Contract/.* +``` + +### The 4-step process + +Once you have the DAG you can use combine it with the list of changed files to determine what modules/domains to test. A +simple git command will give you the list of changed files: + +```bash +git diff --name-only +``` + +You can then use this list to find the modules/domains that have changed and then use the DAG to find the modules that +depend on those modules. + +```bash +# to get the list of changed components +git diff --name-only | xargs php deptrac.php changed-files + +# to get the list of changed modules with the depending components +git diff --name-only | xargs php deptrac.php changed-files --with-dependencies +``` + +If you pick the popular PHPUnit framework for your testing and +follow [their recommendation for organizing code](https://docs.phpunit.de/en/10.5/organizing-tests.html), it will be +very easy for you to create a test-suite per component. To run a test for a component you just have to pass the +parameter `--testsuite {componentName}` to the PHPUnit executable: + +```bash +git diff --name-only |\ +xargs php deptrac.php changed-files |\ +sed 's/;/ --testsuite /g; s/^/--testsuite /g' |\ +xargs ./vendor/bin/phpunit +``` + +Or if you have integration test for the dependent modules, and decide to name you integration test-suites +as `{componentName}Integration`: + +```bash +git diff --name-only |\ +xargs php deptrac.php changed-files --with-dependencies |\ +sed '1s/;/ --testsuite /g; 2s/;/Integration --testsuite /g; /./ { s/^/--testsuite /; 2s/$/Integration/; }' |\ +sed ':a;N;$!ba;s/\n/ /g' |\ +xargs ./vendor/bin/phpunit +``` + +### Real life comparison results + +I have run the following script a set of changes to compare what the saving were: + +```shell +# Compare timing +iterations=10 + +total_time_with=0 +for ((i = 1; i <= $iterations; i++)); do + # Run the command + runtime=$( + TIMEFORMAT='%R' + time (./vendor/bin/phpunit >/dev/null 2>&1) 2>&1 + ) + + miliseconds=$(echo "$runtime" | tr ',' '.') + total_time_with=$(echo "$total_time_with + $miliseconds * 1000" | bc) +done + +average_time_with=$(echo "$total_time_with / $iterations" | bc) +echo "Average time (not using deptrac): $average_time_with ms" + +# Compare test coverage +tests_with=$(./vendor/bin/phpunit | grep -oP 'OK \(\K\d+') +echo "Executed tests (not using deptrac): $tests_with tests" + +echo "" + +total_time_without=0 +for ((i = 1; i <= $iterations; i++)); do + # Run the command + runtime=$( + TIMEFORMAT='%R' + time ( + git diff --name-only | + xargs php deptrac.php changed-files --with-dependencies | + sed '1s/;/ --testsuite /g; 2s/;/Integration --testsuite /g; /./ { s/^/--testsuite /; 2s/$/Integration/; }' | + sed ':a;N;$!ba;s/\n/ /g' | + xargs ./vendor/bin/phpunit >/dev/null 2>&1 + ) 2>&1 + ) + + miliseconds=$(echo "$runtime" | tr ',' '.') + total_time_without=$(echo "$total_time_without + $miliseconds * 1000" | bc) +done + +average_time_without=$(echo "$total_time_without / $iterations" | bc) +echo "Average time (using deptrac): $average_time_without ms" +tests_execution_without=$(git diff --name-only | + xargs php deptrac.php changed-files --with-dependencies | + sed '1s/;/ --testsuite /g; 2s/;/Integration --testsuite /g; /./ { s/^/--testsuite /; 2s/$/Integration/; }' | + sed ':a;N;$!ba;s/\n/ /g' | + xargs ./vendor/bin/phpunit) +tests_without=$(echo "$tests_execution_without" | grep -oP 'OK \(\K\d+') +tests_execution_without_time=$(echo "$tests_execution_without" | grep -oP 'Time: 00:\K\d+\.\d+') +echo "Executed tests (using deptrac): $tests_without tests" + +execution_time=$(echo "$tests_execution_without_time * 1000" | bc | awk '{gsub(/\.?0+$/, ""); print}') +echo "Time to find tests to execute (using deptrac): $(echo "$average_time_without - $tests_execution_without_time * 1000" | bc | awk '{gsub(/\.?0+$/, ""); print}') ms" +echo "Time to execute tests (using deptrac): $execution_time ms" + +echo "" + +percentage=$(echo "scale=3; $tests_without / $tests_with * 100" | bc | awk '{gsub(/\.?0+$/, ""); print}') +echo "Percentage of tests not needing execution given the changed files: $(echo "100 - $percentage" | bc)%" +percentage=$(echo "scale=3; $execution_time / $average_time_with * 100" | bc | awk '{gsub(/\.?0+$/, ""); print}') +echo "Time saved on testing: $(echo "$average_time_with - $execution_time" | bc) ms ($(echo "100 - $percentage" | bc)%)" +percentage=$(echo "scale=3; $average_time_without / $average_time_with * 100" | bc | awk '{gsub(/\.?0+$/, ""); print}') +echo "Time saved overall: $(echo "$average_time_with - $average_time_without" | bc) ms ($(echo "100 - $percentage" | bc)%)" +``` + +with the following results: + +``` +Average time (not using deptrac): 984 ms +Executed tests (not using deptrac): 721 tests + +Average time (using deptrac): 559 ms +Executed tests (using deptrac): 21 tests +Time to find tests to execute (using deptrac): 491 ms +Time to execute tests (using deptrac): 68 ms + +Percentage of tests not needing execution given the changed files: 97.1% +Time saved on testing: 916 ms (93.1%) +Time saved overall: 425 ms (43.2%) +``` + +Some interesting observations: + +- Only **3% of the tests** that normally run on the PR needed to be run to cover the change with tests. That is a + **saving of 700 tests** in this case. +- **Test execution time has decreased by 93%**. You are mostly left with the constant cost of set-up and tear-down of + the testing framework. +- **Pipeline overall time has decreased by 43%**. Since the analysis time grows orders of magnitude slower that test + runtime (it is not completely constant more files still means more to statically analyse), the number is only bound to + be better the larger the codebase is. + +And these saving apply to arguable the worst possible SUT (System Under Test): + +- It is a **small application**, so it is hard to get the saving of skipping testing of vast number of components as it + would be the case for large codebases. +- It is a **CLI script**, so it has no database, no external APIs to call, minimal slow I/O tests. Those are the tests + you want skipping the most, and they are barely present here. + +## Conclusion + +Code dependency analysis is a very useful tool for deciding what to test. It is not a silver bullet, but it can help you +reduce the number of tests you run and the time it takes to run them. It can also help you decide what tests to run in +your CI pipeline. It is not a replacement for a good test suite, but it can help you make your test suite more +efficient. + +## References + +- [deptrac](https://qossmic.github.io/deptrac/) +- [deptracpy](https://patrickkusebauch.github.io/deptracpy/) + +See you on [Day 16](day16.md).