From 0e7de9400c3f4688d60dc2866d834028b46adb9b Mon Sep 17 00:00:00 2001 From: xfy911 Date: Tue, 9 Jun 2026 18:16:35 +0800 Subject: [PATCH] =?UTF-8?q?test(healthcheck):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=B8=BB=E5=8A=A8=E5=81=A5=E5=BA=B7=E6=A3=80=E6=9F=A5=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95=EF=BC=8817=E9=A1=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 覆盖 healthcheck_parse_status 状态码解析(正常/异常/边界) - 覆盖 healthcheck_build_request 请求构建(默认路径/自定义路径/空路径) - 覆盖 healthcheck_update_state 状态机(阈值恢复/阈值失败/计数器重置/时间戳) - 覆盖 healthcheck_probe_once NULL 防护 - 使用 #include healthcheck.c 白盒测试技巧访问静态函数 - Makefile: 新增 test_healthcheck 编译规则 - .cocoon-plan.md: 标记主动健康检查完成,更新单元测试总数 159 编译零警告,159 个单元测试 + 106 项集成测试全部通过。 --- .cocoon-plan.md | 18 ++- Makefile | 4 + tests/unit/test_healthcheck | Bin 0 -> 76520 bytes tests/unit/test_healthcheck.c | 240 ++++++++++++++++++++++++++++++++++ 4 files changed, 259 insertions(+), 3 deletions(-) create mode 100755 tests/unit/test_healthcheck create mode 100644 tests/unit/test_healthcheck.c diff --git a/.cocoon-plan.md b/.cocoon-plan.md index 7f63744..ad166f1 100644 --- a/.cocoon-plan.md +++ b/.cocoon-plan.md @@ -58,9 +58,9 @@ > **注意**:用户已指示恢复 cocoon 开发,继续推进新功能。不暂停。 -- 所有 Phase 1-4 核心功能已完成(反向代理负载均衡 + Prometheus 指标 + 虚拟主机已加入) +- 所有 Phase 1-4 核心功能已完成(反向代理负载均衡 + Prometheus 指标 + 虚拟主机 + 主动健康检查已加入) - 编译通过,零警告 -- **142 个单元测试全部通过** + **106 项集成测试全部通过**(新增 3 项虚拟主机测试) +- **159 个单元测试全部通过**(新增 17 项 healthcheck 单元测试) + **106 项集成测试全部通过**(新增 3 项虚拟主机测试) - 当前仅做编译和测试验证,不添加新代码 ## 待办池(活跃) @@ -69,10 +69,22 @@ - 后端发送 Connection: close 时正确关闭连接而非归还 3. **HTTP/2 代理流式转发** — 当前为非流式(先收集完整响应),未来改为流式 4. ~~虚拟主机 / 多站点~~ ✅ 2026-06-09 — 基于 Host 头匹配,HTTP/1.1 + HTTP/2 均支持 -5. 主动健康检查 — 后台线程定期探测后端(当前为被动式) +5. ~~主动健康检查~~ ✅ 2026-06-09 — 后台线程定期探测后端,4 项集成测试 + 17 项单元测试覆盖 ## 最近行动记录 +- 2026-06-09 18:00: **主动健康检查单元测试** + - 新增 `tests/unit/test_healthcheck.c`:17 项单元测试 + - `healthcheck_parse_status`:覆盖 200/404/500/100/302/503 正常解析、长度不足、非 HTTP 协议、非法状态码 + - `healthcheck_build_request`:覆盖默认路径 `/health`、自定义路径 `/ready`、空路径回退 + - `healthcheck_update_state`:覆盖健康阈值恢复(连续 2 次成功)、不健康阈值标记(连续 3 次失败)、计数器重置、单次失败/成功不触发状态变更、`last_check` 时间戳更新 + - `healthcheck_probe_once`:NULL 后端返回 false + - 使用 `#include "healthcheck.c"` 白盒测试技巧访问静态函数 + - `Makefile`:新增 `test_healthcheck` 编译规则(链接 proxy.c、http.c、platform.c 等依赖) + - 编译零警告,159 个单元测试 + 106 项集成测试全部通过 + - `.cocoon-plan.md`:标记主动健康检查完成 + - 提交已推送到 `main` + - 2026-06-09 15:00: **虚拟主机 / 多站点支持** - `cocoon.h`: 新增 `cocoon_vhost_t` 结构体(`server_name[256]` + `root_dir[512]`),`cocoon_config_t` 新增 `vhosts[COCOON_MAX_VHOSTS]` 和 `num_vhosts` - `config.c`: JSON 解析器新增 `vhosts` 数组解析,每个对象支持 `server_name` 和 `root_dir` 字段 diff --git a/Makefile b/Makefile index 6a8dbe5..a3841b3 100644 --- a/Makefile +++ b/Makefile @@ -108,6 +108,10 @@ $(UNIT_TEST_DIR)/test_websocket: $(UNIT_TEST_DIR)/test_websocket.c websocket.c l $(UNIT_TEST_DIR)/test_log: $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) -lm +# 单元测试编译规则 +$(UNIT_TEST_DIR)/test_healthcheck: $(UNIT_TEST_DIR)/test_healthcheck.c proxy.c proxy_tls.c http.c log.c platform.c $(UNITY_SRC) + $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_healthcheck.c proxy.c proxy_tls.c http.c log.c platform.c $(UNITY_SRC) $(LDFLAGS) + $(UNIT_TEST_DIR)/test_config: $(UNIT_TEST_DIR)/test_config.c config.c log.c access_log.c http.c $(UNITY_SRC) $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_config.c config.c log.c access_log.c http.c $(UNITY_SRC) -lm diff --git a/tests/unit/test_healthcheck b/tests/unit/test_healthcheck new file mode 100755 index 0000000000000000000000000000000000000000..efdaa30bc1e3698acd9df8b248c5623df5ba5fa7 GIT binary patch literal 76520 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%0A{NU` zhT)i>$k*a)%4w84;a-)tcw6=RZ!pO~1H_(^so{SMqwt>P(0+FH(J+d>iUV zowW=tq>iLud@pSAwp_|Oebfn{>)ul!x;U(rk8)iLUIs8iNk4_t3FI<^#Q|KnW)2p`GEVwzX(IX;bnoWLohRn+4PhDrWdK1dUpYKBw#;f^H>WW@qHH#l*PjrABvWgU&Zgja5ONFJwBeGU~jCWWI#Tvy1lgYMJNX zPvV{7D&Kn_SJO09+;qh1a>tQ)CJPQQFs)~a%Yibe-2z{R34zUrBB9vzP^{$gkcFDy zN*`u!0(#cie2sVf%@Ezn!83SSj3x{d6*+aW?P! zyaSxe_8fH#M@;)Niq8h)*=a`|Ni?ILxeff~AF=+So#4j-!Tfj9avvcA?x%zCLSPY} z4B%P$!99Q15lkZkXR$DQhTR~-*jaL|BK=UWb>0T^dK~IQ_4*-<2@1>=Vhpp-FHu31 z9&Z>T0`Xsq{JSK7gDS$HlwP#+ByYeZ(lHQ94VN+T$en;xU6oVS^1G{iYrLRwEu;n8lpPi)zip*`A8{6J9w{thnm*AhC%z&txax%@oAd5zvnE1Gd<)lbXPFQp)s6 z^KW4iYoA3|z|FwkQE6?^pHG;i zTN^7}1Y24Ekomu+(41nW1rDn z>o70lI+P*Pgg&wdnbc`zG?V3!(gG@2;L9i#%gZJD_=LUjd}!ml?hGvl_)Tx;0!1J)hW(H+@m1R>$0k7Nwg=ujdeZF!RJ1*{>%Uoy>Mf+$eN{0-6p>u8scs!2YXb z$*zy*8!?D$nINSC(R05=#w;Ba#uTB-6Ir>8d~m zrU&K?J@Ohk`tbeod2vKQv3=KmPi+o`qLKKX} zzbHbA7&}`C(McEVD6iV~0;~jGyCubZ5I;dOG$ho-5)HJ`2YIQ3V<|n;q+uiC$m4gV6nPybekh?oCtUCi_orVF z1)=g4?0!>l+)AO3{04FU8fpQr)nxTO8bxtctDGH?Dc2!hE+VMvh{U0Q7EA>#z9^`> zLEc!Ww3X{H36^|AO~V>8%@L5LPD)TM37=aq@cUh7x`^|qMbN6d)I2i}O}Qg^6W?LB znvPi-J>4-%(a+lGp|Aq=$3lzz3=Lj*`c~j+Y4EHfD!`w7iWluPo<;2Hi3ey)`&D#( z4ZkYM#c?tE_!KmhJ+v=-6guF8griJK+a|bn0rkU7Y_VRmL|d&O~xO=0trYFXZw@ z#F1t8g`Bqh+v2i)uXo2hC|kZ@tveWzZMtCdr7yub5kT#-O$VEzF(2zIeTcAKED~egb|Xd z1&?+%lhWXBM_PhDD{3yd3T zu9WWdxy?DqFXeA{oxy5|Lb7cCWFh)FCV8F_AhYJLAsa;TGU^mkd^BZ8+BtOKlVk8! z_cO#hhex|?->27*aMZ(k;l2!MyB9O6ly5*u=tzck=2$_>EgMg*MSR*r zX6}lJGr>BT$}KYM5X_`EV2SQ(=_M{o#tUl|c2!;*{~*wOt(cb?JJJ0%t?7Izk80@0 zHS}4Io~fZLINC$=C-kf}kN~~?1zo^1W2wG-lu7j{6xJQ7^*%&DQ?kH<(lQKbt0_Oy zZr0M`c$(;@hz#;lhc&4&)UDY#r0YW8Tl)(1R+;sEif~ok39{xVE;sHzZdh9&yM=NJxd$@tMjpljK zKv~zZwsO_COKxWBCz2c}w5BiW4gl>y4prG(rz!at6j?RXgz)w@ylL$NPn`rR2HEW*W+QS1X4uZ5YZSl8ehBtf1bC3QFEiR^KNX#K?6J!zL39!H>3R zd|Mkd{z)m}OUVSMx35J)>x+-&P%|?Vz>ZocNxrQM&P_y0%mh-AMk<30f_h~O2Vlm6 z8l?d=Q&IhNZHyIM6)c!l=8p;<_6G&ugftBQ7C!7_pH##?Y2}q zXbQrDuSQ*-)pUvOiG_l;!}}JxAZ8BKk$F0<(D`alD@$v)7uGQ>Oq)tON}sdzn|~#;5@9qB2w8|LpHZQ7O@%cBN?=|xC-lO zTpiBeU`yL8K6W{{C?*ggN{CPWJR)68;v$kC*Dw++As-oiF-a1UtmY)RRQ$+`J!A4~gDVF9)t6|sTvI!={2i?1|4+>D(x(bQ3T)>__n@JW{ zd%M3RE}LXFE$nV`?{?rg7ac2WiFO`ZfprT!4IKxXcU#;v$Mj;%yTF74g6O7ysRBTL zMFm+>G}I%yqk$^~@_`D`eLYWqprW)ybkjcv0(@D?R3g5C5&sPHCEBv7$T>ixF0)S+ z-B)v9YEdCk$1v(oICaGg#-R(tz07e%`K3e`$#6c7t1K-n6x~OSKnn*P4#xF7Y+ZEc zYPgvW;_wT@-3c63VtRq--pkWxPA?9Y&<8kNt;N)#J4z%ky#VcxdA84d8Vo3^Ne7dV z9-*aA)zc4Y=?dxAm-3L7K0;6bMoZtw)As-*{x|9A8@2Sec={$Sy}h3Pp_aaor|a?^ zTCK_VB1?A^qwH6;yqop3CwSUuq&=sly|1S|%+m%UZM~NEH>6>OgtPnR=|Hg~;8+)7 zRTlv_x$}3jG;^ke3WS8em~3oGMeA$Ow;|OkBM{yEO5KcnJf7q;=L6ML#~6a(M1dK}8ft76Z2wjBE zl*OG*X&nOnmm;)QnGTMg8mnC+ms-V4Q)zFwI4)XwWhGS^SKnyACsOIgz#hxNltl^X zOn_Xc+zhnMYwr6PX2jG@5p{vf?d$=W>s%Yhb$Fk{-rG8QxXzgX+5w_M6Y#K+H}gTFJH!HgactOGg4#1^k5qEk zI&R@dITCDis3Rw?OiXwo4m<=g;kCGG_Ix*P344AVx0F3UjjP48RnyYujn5so7kJ$J zpznoH%i>c4*ffcy_h^JjB9TgQ>?AyuEo71u!w#x65?lTxZN2gnT7t(r|L^731Z5Qn zitjbQI5#LgCi~I6o;;Lo`lHHpM`dVz!P@Kpu0F2x!?T- zb(p!+_twI;P1Z8a@BE+F=PJ~vjc#WDVSQ+YqR{u&!q&(6iLpLj^QlK+!xUz?)c0oS zPKF-uq52!=90ZR=FtL_hH1rjDJ^*8)6Qf-xZq+}rl~1uG`4F|b3Cxw3kkL~s zuM?lC6_?eX-sRb`itt$F(kK)e*kg5?;pk%Om60E}@YLn*`V8asAy_>GGNXKN zww6zA41pK0yaD;u5v|j`BVu|BCj=o~00|Z&q52M{XIwH}@{UM?w+!$HJi!PD@7LOJ z&4NM*IFC*z&e&aaTiW+d>>;)~CF{oCI*#DiTIHSOP-EzjUktXIE!awzzff*L{rL{S ztjm;pm(mih5x2gT%UyeqJNimE>O|QCK2P*~Eub)5HwHGaRnG4lC*w$2GcM35`NR{3 zWWjDaw&z#T0aZBF!z<9eF@g{c0>HSLk2B`m6gxDElcp5(#;u>L9PKQwArX!-l4rNG zN@_%G3*(Cef2t+;1wXX@F}5v4Ec z(co%NMqTyjWf9`jE}CjWF)sFnn!;+v)v$gb`z6l3`1*sD5~Vy#r8JTo!D55Ww~Y3K zn-|xtC+~tqzns2m7WOuo2c7%@!>y&2!9;ZoAnkr5DzFwobwN4yKEf3ge}al7aU`NJ z_-?I1UHW~?W=jq~kd50x@|SE^%6SEei6H+e9MON{-(+KSJ>J=ct|Q?FTp%7h7IL9+ z8MTQOeB`brgoxc3)(yBN3WmD-(kjF zR;6IN z$^sp6V$CCTVa)_Cj#%G_y&xY5^4spOT&g?*YD2nnFLlTaLCNQA zqlhi;eB(Yi5KW)3QkCI6@wgL-<&>C0i7T}6i#k$!?R(3{-pAs>bZr%%!~%&~=>^#! z%Ui^TZu=<5VEA0To9rr${n znbbvCoPpw`16Iw%CbJ&Mx@-nknqZ%K)}AP=fGMz{cg&-jI#RD zM`3}i{EM+kcY%u6hjtB+Z5Cz20Uuv_43W{{Vn|UCAS)+?XE&oT5z{CEUe~XPk~YDo~quD^Q!8P_lCJU1E1!=}4*G6uMi_6TYPc zUwS22C`V}$4Iv~@JQSc<&a&=Nwz4U9$^;~ka#;3>_o<7Ktjh;R!->&rAZ>b^^o>(? zzV!aUfpQVBt>xEz6Yqke8YY^5YOHH~5YQFK2Gbg)l8sN6^bc~iEcZ}2|Z zgAjo}xg3L=q0c$`kQpW@nQ40~wC?!~a-w^_tbG4YD^lmz-1i2?7iBdFP*edG#ro;L zfM|MIuP^Vf&vK$6M1=qFy#gY7vsHh+4E7o3JTSj%j%$V9l=BZEXm;+e;32xFVOVRf zkH*C}fX(%>qI(fuB)D;LK5eULqf-@-Sf&4=A8+cSkFVa8=TQWA>wi2x)%^=K5A*8mL)SEH{oOyeAfbb2(gz5N1*THM41K)6#j0B&eozP zcc7N7$64I2YCX6@45UDSV9C!t| zW0b#t#&p)i@G+f5_rFQguHQR4rYWxysW-(2GG67UfM7!cb+Qy}&mpxRQYSLjq=GU~ zFvn#*{k@XU^GAU~{S{tFYaisbBBg1a1}lR(-TqSD{{BMAuj=-P@iZ{>M(0j|9gmR-Ae0^&89Ko7c;Kr`TpDJ>l zO_!}r#=Fvh9MbkC*HM$>1~lVTlxOvP@3`LT`N8=EGkK&cOF#t@{pK)zbg6#r1j;`j zDNQwc{KPU2hrl;!hqW%cyeRgod$^tG?t>03_Nx^ie+=_$F@B?%(|Jicp*nBJ+)rFU zhlYubh?rM2dr90VF6a#6U&ZLoZxan6GKo#1`2&LI{}U^KwPa;3g?g~vqkTM6^eiX!;7S4p z&nnmf58A5Pr?h?>O1_ZDed*i!@LU@CwIJ8@6~cZ(kbQ?B`(wZ9^sts|CV^Sd)B#{U_ZMme*;DDR7SB^oPQX#rObT@!R^P@WD?`I!;-d%iTf&# zs%s7N87%N~LhJG=SWwk@B`{q^d&@tZ{y}ulM@sT;bwJQx!Z2|hIAf?Nte`*YM#ejX zTH=ETpldtn`t+;J2QC4^vs+w9OX{@{5Bsl3U=gH%zm)Rpap#$glH1oAZeJUHQB|f* z37cK@%pNB75afE;1CGCnFV)fKAtEdg1U%LF2;0;bh#&ibXf(71z-24 zs7r#CX$L2pa|DR(YB6Y09?Du-gzBNkp&4-1F-ke}3gd!TalCm_8pfN?Xnw@`F~ZZk zm9Idg{u0z{>o#%T4BPq|lVbIsntqdysvl5^@!QlB;6^5p0x4A!SSa5t3Gk=!h0VD! zZ-!igqo3q5FT$G%l8Shet6%nXcc7;1b|>jIj;N*eIBK}Mfht2_&v%~gvTw!NF|la z)j342t|ZqBFF^Cb6$Xo!1mxQ@mC3gkk3X01DKrD*=tFWmdm<>`;~1cUk0kt@0Dg~B zPIxBYV#?WQo{nOSdV2`4a{eD-IMA>(o$&A$F%H8mb2rx%yvhX7Q#IPe;UsDmj^mvdH zBiIebX4W{bBQp`BRe_?|G&?Z)8Y>MEB<;2j6mqT#nx^u9%ru?4?v>?qI0cqbQD5ifTu)#;ASZ zmr$tzAPuS1wqv1{QWH7xv0E-8e)VGFPMr8{jTpTJ%2yuN2wCxw>Tx)bi{;OPp+Kk~ zkyWuGgWe0*z*H{LFi(OB`v;qX_DlBB3!?%0S1cZvwhz~z*hdXi2QkKz80(JeVjNeN z{h@tCUaTDtGXJma!-kT$eUy_djph*rt?lDZz{<|W|3mvY0G*;@a|R)^dL&q}AHv#4 zH;{(d$5Lc$T`Ao@wxnD{?7Em(vkzG#*6ib6jqnfcqc@ko23eJafc$%uYasuh+K0ZM zV!=588Y|Fgd;B(qOr^&uJm_rpXdH5dFT!RCo6@l(!Z>J6j^%OceDtM}&)5sD?_LAS zOnFn5yg5t$0%z!4KZzFC&y$-h7@u8dOpXVo{1{vQpp_BGkcD7NCwX_4+|0*2IbpAI|vgF^I(gX8zv0q9ZQpNalSbersju!jvc;o@G-=;_G z7IZD#M2Wyz&Rw{s8ezdAOnhuMN{7JR@Lx7^eJ94B{M91fvrEkRO-yW>u@w9sV*FY# z@jf@Bml*%8nDf2Z@0y9g&WIKh4~x&O6BAYOx!Tlqj(>pYGlCXmf-j1FxEoeBV$N4$ zzaD=fHuKr}QE$5Qes12=71nY?Nbi3=gm-VsRY>ApXYz9_QBFdl1BsdPiOht(7P$di z7vlUyD1loZ8J$f&(aP?cf_BG}z_{+dKef7J=zN#7y45hRc_hb?gKRS2RvE^Jx;#RD z!;x8n(id2|Lw1Vl)oA_bgJU|+*KvG*SWMh1F4#@OvkiZ+ z|0<5(q|&`C^wio)lwspv^48g)B>%^0q94^ulp#nYX{JINbvn&1 za6Sr@#N=1Xh`=}uRk?yQSrTA!@EMjkgqWN?z?oc6Oya?W`pef`gc2h776fX0B)Dp+ zcijsv;<6Uy74T3uP$h!il4aK1u}-^b}TgKkE5bdmjH;!n&b_i>xt0ut79#7RGiiATf* z&!Z$UQDI$cI+fOM|HDz-sOJ-tVr*k$X^0C)_)9Y|g{x{FRrUzJsR%KF7US!b+qvVJ zL@7IX3bG;eqr@@avxnIW454mfYeU!oSQ%Z7HBRE*$|rf9*g779fqlx?e}P=%4~p{( zpjUBxgF15U!hy$Lj<%4j$vjJ&7ujB%4+Bhn##Eu^hk7m3xzaVHHq*}$>^9<*j( zJW}HkB#G|*T?}4xcEMFI#T#NE`zkZ32Nh6X%7v=pv1G1hmAV2vaX6|ext4fXgFK!M z@F>=J%;?QV0AhO&P%|z^3nGI0U55BvqnP-u_*`8^>Q=|gAkzBWcNj)A`%64ReXjSJ zn;=-f-PDHl2!37$Hq?gSi}Rrr8I9(<$G7f*_X1W9c&P%;sgPkx2=l92GjDp` zP=TQ4_e0yr{3tRj${u3{Zf#uwwJ#!IT;ctpQ@($K3njau@o|NcSh5LUIHu_j#weg% z2XHdrNJgu3ZHVcbpGF!{Z^L^Ty^F=45xfiCcIMrz+s?v+NBe=_3WCi9#fqK@Xl_|` zoi$h9qN36g}_bou;Y5Bf+BQ zYH&ku$)HiXzS?!T9Wj{9cOb3>Kg*|djtK0m#O_&~1*BQtKWe4xwl)2$niZ6Ylo?Uz z5SswPi#%7$CoWBovvfe{!-DwtT6G39!%WDpKtZ`Tk{#e5E^(bOsS}V?$l~F|qz+8f zETFg#r#PmC8q$~k9kiun{fPEH3Ufh%BM;x(-p_9McV*@nrQC>Qo`?)DA%lkR62L3@ z*P`w-VgvoNl;RYd?+o|F;H?DzyLHUhFLTYk{_HhFpf(f7~j7QWX^pQwk!rNdI9 zX9em^+np4EIyv`73OFWglhPW+r)qI{QIgHoL=CI$NzRJ2Pw`uMo0M=OK-IKMi-kn7 zAQcNtDye&zL|sXu+aM8KZ4{l6TFPyh{^dF)PQ%msU@^x(OZX@qj~G+5GKW&|7Nf)w z&qgy{TP0_r$0~mZtSnqe?tstdTGw|Al2PBSc(ktX&hkstcL`DdFX}6y)zFUD!59_W z_rdyRaBsPZ@B=kG)i<8w-ynQX9nb39g;H332NKUla|eiItZ!$)%Hjnf^~LuVvgHPT zRRHbzO(06JWenB7$5uQt(XA$ko?39TV6B9&%G%IxsL|6di0%#0DO>xDquxf*g17L3 zh8xYI3$BBR5bTR?vRAtDvNehlC+I&5u5k;t%Oy6J+>^Au`Y{a=Pv~fJ_ zx)AB~fxYN{mx}blhI`Qo$weN3R)i(+$yhaHmS+2> zm78EBFoyFWG8zN%Q@kExkp3Fdi1!PecM0*XMRxGE26&Ud0B_|wB)+Qg-n^UQ70vkY zWUS*_*M+u~SNft(LA%#luS{K)<-J>QH+=18Ei7XWN}6#p-+jjBO;I?nPhlj7nhCtWi*$q3*p@X{2rwR z>ZfK{f(olN#jvmJRm_q7MwAA7+=o7+1m=(A(q_YvWT|$SBK2_f@Q696fT$K!xpG)vIXq9 zC>u%n{L9onluv3tMJTL)H7ea*ZUL;^<6@I5?k_E=wa%sZ#zNDxe7vCJiXC=2Uu2^# z%4hyG_MZBQ}W;4D%c+iA8agy?Bv#QturAQ8|Bn78;eo7bD#G zE(4t-cEb~O#}m@_)TE^a?r%^Y+Q=6Rfm?Rs-J3Em8^(pPQcxP_Q!wHn4UdyZ;ocN` zAnhHbA)}QtuBD9hPdn8h7pR5=s60A{!wrG7BBar({R0xA(L5gsq-kA;j{{bE zJVssu^8~Ih$2bt)rxB9)DO~(5h&(B2Op4v2Cl#&%qBRgQ4oEeJ`HCtq7g9eHJ!fEA z>;h!k3DNT<1N7hozOR$Mi1Hoz8u`d;wTh1!qhmI2$~QJRgH9>C@DM$8%@K!+;PcTC z>`(&&!#F68LfrR};%Q=C!Cug24S1nQRW?5LE$_gkDuuV(NN?O}!aE~IdT%~KAGGU0 z-*ogGf$Z672Sm4-IC#^0QqlNON~3us-he|qkLvGB-;MZ_(ih^RA(q{#`-$^z#4XNY zOmTq}R6U8RJ*cd%3vdUsz=56Zq4Q%rkYqgH9Efe|F{b;#SU>iOujy@*@bw0qY_4j3 zvUvdt)lc+*DtNMa1^)d3`Zv&&tZ#_oxbhw~)3XpwpI;ZWR^F#rJV+t-wk-7EAMHiad?%cA@mh- z=1khR!Qp~C;CheYR>L_=VR$=a%BNUAL_6U7_^AV&@pRDH>RNBYk(vsXt^s83vC#Y1 zAyWhBr_{;@O&ZPQs5oUZ)gEU-Nz3jXOii6Z!JA+#dOQ**QGKT(kp?brR2-eK@GK=E zed%M6W=S|^VXH4?D2O1Ph1qcdq#xxgevkYG#YvA^PoHAr+F@%=AU>}hWq)3xaqScv z*A8AfI^Qsl8bQHDN9$=uf{$$l#m{Mq2e&Vz}|u2!m%3+)v zJI4=-Ur#xF>Cu#XKHgb2sI@09t)!TdM;XoYpBbU&A7aeEZyU+vR~{xUHkwBvhtV#_ z5{DmWlLYCf!GR_#$9a);2t_H~xWF$E^IJ){eN{}Ji3l+%%^L4(!Mo`daQH;y@E+$d zmpE((5k$Q=z+nmJa0USm zW|M0}8f~TCXeE@PiiEl7fEHH+Ww;v|&~z7+7b%B=9J>Fa z@f;Ty+BgQ`Z=@pze@)I?GZv;l0<~eS@4$nv^U{krW(r}@+4y{+P)@&#rH7xh*+{ri4YKDGLhpP2ojhWi?#U@3$J+7yAt~-&c1giyeAph ze~h^UXMgrl#{N}8F!m}Tz}`gU-jv5Fp(DzjsBT5drRI~gC7h3cir%~dv~Z7f3EY?d z697q0d5S%H5Pt}}5D)s{)uX*Ci_t4dZwNugBH!D={tcC*crg8?jYO9 z$?gh}JqNOh*zW>@hp{qfxW0pEUjS{>j-dYl`ztA5UvQLgpKTH=f$p>IgBRuZ*;+ER z3DFQFvajm3%Kt5r1NnOf^3(i?=U<`c_s>H9UKos8<$vCo|9g-y6Ik$jp!}J7{S5>1D24&$Q_=T8MLC8Wd}(dN$_H&#vxrF9?Rb zyWyr=q@0}5iOKzv1nng`c3{%L*pa1<*x{wll7h}1J9iYuj+OfM@0ZkfRGGa5@42b{ zl7J0l$eWTMJL*nG8NekcCB+UaEpZf;IPI-d1|;=2u#U}|S!U07*b8EZTWn*qV#nuI z#O90{mn8%f2xNFhAtX+-=M_7q3CBF_|Y@Uo39>xse1p4)dyc# zb!%+z3ZAB{ed*w$*AKk*f%5U|guS(QK_JtCzxfWVdb$;s5ybYrJ(g+{+q)n(zIR1} zmSNE=2S4#1TDA7jTVA!Q>c9eC%80C-SWPaL2?|XX63gv*1+xT}#$JO-QfYIVgZ7dF;Xw7GL+`wQaNa8iUh^H8_xgcV%k`!r#$~a09ZbucLj`u4AoxZ=$kXE%ueiK+&ao;FPZ9?24pe1f-cQ7OVtHW zD$6{|+6DVpJgz=p4I>fuuYUDV^?c<`_ko4a!x*{X!a=#};0rPb4!j6*-+?FJKlJol zR1BGhAZ1xHh0&wN<_M$5!^`qg>fVA964(INM=KfGM16z@3@>; zjW|}PjurB?dK5FE3Dm4cc+hSy>zh|xG{Y_w74|JDEwT5Vp6AG)M$q)qf}+AAdqLld zBB)=;%g?u$Ir`d5@`1;vmZp>o1@^+?Jcm8N-vK=d<#{Dj?ZT8d)l_QBt8nzq)(Q(zbCy<`hAx_(H`Si_0KVe{>zL_C#P^{F z$L3EX%XOqX9ff^UTb1}g-i*A8{PLnQM{6{`8YOU6^vx*7iM~M<%8HZFhobl!J`dH0 zKGwvcnSJqTs1l(Nx=o}HLiG>6@;CYx6ysf1oL5v5%ABQ4&nthh0RL7{XrfVVRH6|s zD=2IQKU`GS3Z6=c8AS#5(!}Xy1GTqG`;_TIp0l8ckkB4~EuD%tLkm^rRe;KXn4VWs zR0x03hd#809tBbGN1)GgJ%In?<688|Ec!&210T7XULZ_;SeRZmK%nBZtU}h9F{8!^ z<1J%G3f7UsN5MLWjvFELzNc^R>3!itdrPH>lP~gf2lJFjxJ*yZW_zXkRf$H}UJkERz#;k{( zvAiw`y22Q}viu|XEhfxZjo=_-L4A)#MokZzCwpe8(a|0M^a-Ayh%X)zxeE^Z+5e}# zuaAzaxbl2z3DCODvj~Jyh_myCn5-CVA#5ijh{<&8TefTqO9l^dcxtO#Qsb7o=?^UN z5d>x&C)h*@o{6#p!6e?`5Cej+K>-{@_=qMkiRL78#>udWb`SRPY{-r>VRy}j&D-Ct zd+YUkPu;+rnK@_waDjZ^`_-*mRkx~c-KzJx9!e2M?yT3RpXB0MRI=BnAK_eiF69y~ zo{5HIseyEMI1!f#%fq*$kW9txY~rQ@O>{6(u|g_uXJT*-lpKG2e<7Do4|8;>_{4A~ zKc=GX0wyIb&y!yv6Ne2{3R%3J}a9z(oDxl^#!wsIS!KB|H; zH2yhVU+IM>;ge4sJaP1kr=FjzJ8|!WuOHoisoUX#@hAUs>g2;mOT?di_967Z6Mz1# zfB4d)oXC|y85QLm+Anh~I^mg@Fj1{{sjf4PS zDHebISe}$=alWiS9%jA_|M9uKZeIWR+-H?g{KKZQjM><>sx6(}70bpkbK{CKJ$H#0 z?9Qh1X@K_ez`J)kW;rSksKV#UwuhI*-)R0SJDY2sfKa#>1QF=%l%%Y z>v~XkysE@{1{GuRQeWmP>GhW2lg{gLgdva3y-(iB&p&kXv-hxOnPa7u$YwEs0_9VD zQ22?@J*Xw=o=OuR^+r8?7yW9D_}uC!dz@Ryq0C*y%~MRQU_r?ZUxmNR*&Y8^%?~=G zg5+|msF2xoYE?9qO7B{=Ih!0zV%?>d1*}-_a3YvJ4n0-VLd@@{T(SC3>KpJMUeHz{ zu<$e7)t17L27f*t&y5w$4i<*tU04jZ6-LzJ zQf>{ab8sY`O~l>%HlAa00$9D6==2RltSTK4Q#x$L`gF`{N#r5c>d9lNXY~{^87v~L zmSm1RGM-O$YUcmg+EqJ}`FG~z6e2%efF!Fso9IvGurMjg=J>W$8Vi<@L908Rgep8a zb9@i2YKLMY@psL#bL3{Mt+0hb60P2BESXmYK@Ahx;p7Odoi@erU|hnc#2|9MIRT$_ zAW6Cq+`Xmk7tIW6Mf>vwc(xfTC|q0k@7AWRU3fOjp}1F~Ir3?1N5aRWd5KzomM&P) z+)lLc#)Q?IhL1UnZ2=@m=ZBz?mDY7b$^Id=>7taA!my|>O0cetrQmg`U83uflmv~L z%Ev5Wfz;u2cFfwC%&Apoy=Q34wTLvY5^U3)N%Si8y|C=+d!)yN4tU6fgT|obOy8o^rxVELEi+O1g*xt z_hHcGpwplYptGPYpp8?I3)%si2i*p`7jy{pe$Y|SDbT&3FM&>i9tWKUeFOA3s6qSu zpcjCi2CV}fdJ^>nodi7yTKz4@IR@GYdWzyftMF`P5_CCe^;76CptYbwppBs8pdFx- zpmqNldIvoYT8&Njj&D28TF}#=y`Z&EBOPetH26VBL8m~gzk_t3v!GS)haR3mI?!3r z4$zLHj&m5a@w=!G#e>#e2>K%G4_fyU`Zwq-=xd-uFC+gyfZSIg2efV$`UCCw0s05g zSD_zJdLFz2&jXL+F=IDq?N6Xz(7kx$;vmuEXeZFopFw|khCK9hj5E;MU!Wg?&Vn8y zeBN>9K~Lkw@Rb)q?n%Uho(A0qT8kg?90VN&ohBS~4s_@g>H}K+500}GkK=1WEzlv* zwV)KB78NRP*M3O={eOC6^!Hk1!J<`aXvyEE$5&o8O93yt;Jtt8~Pe-jA7Qa znYHVeg?Ckt8-IGym4Ccu#l=9C-0h%bvs6L?5h%Yr{^o#BQF;j^b07X{3bkcFYhii|lJ(>+& z*NA}U5ay8y;@Lo1jVv*%qpB?15m&p@aUP&JRo*UQ=-27{0FQ&`0C<*E;-<;0`FyC! zti89g$+Y%YHJNq$&Wf51d(MuUjl0ijH>a&Gb74hguEIRMVxu{JNgrqwNa*z?W@EG2 z5H;(N5CR~m$*gXIO{c#1I`W8&I1Z*X6*}-QeIsifu-i#bZSReQ*?Y8`Y^R{G)!qmk!EQI_W7ODJ~=I9<=8|$gxY+mxBS-m;jXI6K-$U3UVSX_?zdkyg* z^x|(hunu7NP=1?9M=jJgttHxqbaW2-Pa~Qu& za)TKOeW~OC+#C)aDA^~X;TGT!5$G$e&_oFsMWFUOf-+6sT%1qpfXx6q0PJyG)7aY+ z!gRE|ax3Nxs9B6^d)R0}^c&zMUvxXh1J&tXvNiiF*PGTp(#0OC)9$m^n=_%xThN!O za8ZvriNc(ya;$|9+W=n`u8#_2Pdb3D1NJ_Wt?UWZ z^x3(f?n~Ol=}4D3-PdK#B`QB&VIEIhZ_X#$@o~^rb0N`Yo=&uw;{%-(yO&}+5!(kE z1#)YdW>EE9j=@t4pIO&4j^Z=`TLFyTw0u{)nW^lqFh`Ml#U^t!YG#rpE8P*UycBFr zV7p$ikv=Dp&l`8c|HC!qv!B{(UnSbZ({H;mkh~qYlLnG^q@rDr{B_8({v19pZ2r5J zxi7T7WGUB&jR=xAV4YF*8OOO5*JQiyC3~@-<}2w#Fo3(y4&4ywI?DV}k_i6tkP(55 zOQk&04+An<%}WN+ zJ8^#!{R2J5HEeC+X0x%^j5M1aQM0?v+}3LLfsgzcS|={YIxdQ9*5^*t{zWxktw3Bi z;%=lqU$ika=RJ#3i;-y|4H%91>&4Z?f5e(ZY^Rj9IHz=&nb0o-WkW?t$Fwee3j1hE z814z9HR=?wrMRX**fg*s9zLqWF<^&*Jx@#<%=wDYW2%)Y?iAv>?#KGejeFLOt0KV< zIL;5+BRT<+Cf_ z5bjZRSGWzmjpR%sejnn|eN`ar2(Z1tu%vde8DMt@_kb6U)W@v6{3$!UQ6XKJez zQ26|Vj&nJ#*}wf1l-H)(st!;K;ujD$K%mB{=S9y1mFWFAMBu4%2 zsvQ`cSTw?y>ts_?ep8~G+Ptlb3 zI^q9Y0wCSz$KJz+0XepZlV&ZdqcW#-d8F%p$Z;Mnl@2p9sZ6EYP)tX9K7w?!NHjz%@ES4J@YWFZAu&8Q^yjjy6HwHzMziVXS@{0Ob+iaL93Qq+MIX5ycW_2x!8)9%&8j}LM8 zQ=A&laIjH#YF;Wc^=aB4d>uRogl7+}1$Sfp)qqlo?a;hOz`G55pYMhDICzhNmp=8! zbGFEhR93yoHEk=AuGfb{f6Z5IVGGf3 zf_EHyp;vo(D{G+^;%-y&YB9j?f5ve#uFvD@FI3cDliJH@0q^`#$DtFjxZih>4Mr#1 zU3HDw7y8-~)!o>tq3%@PQKUKYe2K9V`LsJw$fF|OL*UKKygM)Hl=gYozTh|yxjI$* zNyR-2_=%qO!M<2Ym8yJ~V-lWxNo;7ZYjf1x61q>DX&$$b8opm<@KQ>m0y6A5`}J7vgRP`rCIEM`|FRnc+q|u_0PQ}MPb0f zR2s{%*IkRB65N1m3WPNPivxq2RUp~|ECWofF?z_}%l_X@|vVSyhO&~6hxCbeY{ImoNN^^LG;wk?r#J`DnxN|BH_7boKU~PiYd~qDu${#t- z*`kMip^dIzPQ=$z1b zp$kHdKQdIP5o!r-5E>EMEwoQ)M(DWE384ptP6?e6Iwy2q=z>t=V#!~qCA2|kL}<6r zKA{<*<3cBd9uztybVlf$(0QQ?LX8hg{z5IG4MHPAyM^`%%?KSAIwAC+&?%uaLg$3e z3tbSZUdTfcYUrUbf+e&;XhdkY&_1CVq2od)gdP++C3HsUoKVi?z5iRiz`P+z@{Mov zMSFAeRaV{B9r(3b!Mb8i{TlqX|6>JpyZY0s*VZ?zt-s<@iFl{nDiB#^{ASVb3}{?e zAZA}Br?!|F*!nEV{}JFJ>@anUU42gY|5W3bGw`zD+XSEaCjCy z^MCM1`MUFoM4XL;6|lm3KG6J^054VkRlt?rBFuT8B!~#UAo!@@J%VSV3~1ce{DNye z=LDZ=V*Uw{vtRJ;W(L%IO1OMj@TpeD^*l)5!&Bw#VqCpjgUg=*FIBG-W$-u3;Qu7@ zCx%(RdiIY?I_tXvFZb3;yXbj^4h$!}M(`?5X1pJGsq$FBRr#~bsORmEDmfP!-2wS+ zW#nuUITPxEjbW(ggAmy#c;787Up)iF<;{YRs~2AQxhHW<2tFhDU7XB#7@B_gu!L>i|3*f50w=w^dOpFSQXOeG;e9ceqT@n6%!L{G6ZaXu8Tb*OoXUaoM=FZ&r) z&n1x|Scd-@kv}K%BW_jryE6R$Q}`{JXCDy$ zi!i`T)pw1;YmBL_%&6Y^!(~+P8ns$64BhUV1&@4_`C}q~dl~s@;Wu*3sO`gDW%$1a zyask&>qo>JkCfr3kH4z+y_q=&B*CkKPl*B^!uX`{E5XNi@x3n3?-b6M_umUKpp^V; z7(GuSFBSY?pk7xAKDUY)Q^J3(;5EX(L-4HN){P9Pcjj<;r{I|^;*D8arD}&ST{YvRS zqwpG|VI50Xzm34r8e6a)CPxyhxkFa3vC;YwOMl0iwOTNDnyymM6sP`psc@_+^>OZvL zLb#KOaiPNH#zX$N41Tr98LwyNDdB5a)Lhy+R2GqMjxU7I;OUbV-gI}p|7*}a`-7X!1 zXRc=XJ(90oMowP%=Y)Ta@b4FVM)WN1h4DxkIo}1Y?6BBFOXSRnobFFD5EFd9jGWWL z-w@Wa08h zf-j6Pj&7#HYrv^p4$koX3MR&Hh2Pl1fcm`$F29Q|MdP&ZGR8k5{2`O^8EMCH!QUtN zLJI>(uEK{DE;k;sM)(^#8C1Wg!DU++{_Vn_xrh1nyt$_g{{v<4C(7V2mBD|haLhX& z=X~E}Vw@oR7D6Li0toqi895(V#(JCo1~c9u{Qp7l?m-6B?;dd3tZ-rSkWT@pc`E2< zCPhx)Uox|LpAVOxDI@0_!atE^er@MxG=4qfHWTB=W#s&~GWc(`oN*SQ=euRPUp>P3 zwIcrmf~y}51Jic$GT_v2SIc?|Zkq~MDSo-}kS!u7@^uE)`*FBT3O<))JSu5!6@1}N z2Gl#XxV%SjYm9Me9^*mav<`|aFb+3Kg(n5?6TG&9@oC_+E(rR)F9R=SmwvAJFEYk| zBZ7tevJC$}mBE+7aFvq(hYGJT7NmW({x26iQqLLc`E-35IdS2i+ROY0q?~t@;ivbV zOO@vkaBAPk3oPJ|M9z20@YC}ivP(6eW=8dnH!goG_`)8>rP++LkSXym+|D@IR5)MZ za^oSZ%iwKg@K2V(Q^0FH>t&tAsN%=E^=pi%cR_J^K=6!gls+s89xWsP1>x^|h#4Oh zetI98>N{UxU_fv>hk@`3@k6v<`j+4|tC&&Sfn{ob1b+a(L~!d0=GW`B4T9GM`a?qD zQV0*Zr3`*Roj|&brk3&y?Hh)EZ+OX}|j-;KB(P6cVPH*;{<&1+Wi-$NH-U#pM zlfH2XVZuO(&A2 z5?SyCfC8N)25xnFS$7skg=yIep)=dIByu<;HP?g|9brBXN{768VxsZ*#@t}%Ko8Dm zSLZ`>f}VV~Cll*O0hCnS@;Po-Y(h&0u+>FOl}DfT9vzAEiDpQRFhB#3TL4O0OYSuLrY|F z-sYmBQt^t>Tt8Qs9C+s5yQiwT2(;*4%XxC@I5|E|P8S`8a=FxzYwjH%x#r%|d24)g zO(EI8Qx145Mq{k+^t7RL40W!b%Os#a{4a>hA%PgTZCj!nTkY0OEw+uj_Dx&u)(!zX zTDBN=%b#tEZtQF}?DnqBP0=oUb6ZlKZR%>ZWsqRl3?v8jU^IGqy6iz5iHoBr z)7i-8?$%9&;6(+DDy58Q4re5D3*qGFDA^R`4fc_=ol5gj(| zJ||GO0*a(s7EN~ZK!$A9fUkyY>eosYy@pt)9!TQc<6I)Kla=V5454iOJpyTPsb(gP znSt7yj+a!%*d9(|?onsi`&nt~DbYN>Gnz?jJ~ikX`9QmKGY*W#G5U0?%UK6w32ac59iTW zy@n61DWgjUmTKu1J_TJ(>IHnWpnoJ^RF-GxtCmt$1LlKGA&vZjpz>@xH&SMFvgL~v z-B6pp8p8cDk*5;0VU&6Yu^LvM4yk6>9_0aGF8BGOIIBHlI|DfkqLucCOZsN0x6 z6R_yecu%HkwB#_2irSP*YS4t>%ZFx=6zSZD!Wt)c^wSaWSu_c{svEmTS3RG_ko?$M zU*jcnaG|}8$9b!w@Wh3gj?7d#Z@yUk^_=<#G7(uCulK|D7#u7E0xQ^b1Kygwc z`!TSnE?!=I!vRx8R{7i|OseNwEocz>R)Je@Dg?K=s(6b!a#tE1ZZEADa_9t7j~j7* zt(z1oL(#WpyozZ`;`n>26i*I%;w@*`lppBLjZ5b$pB-l80^^iV#Ih~;1V^Bsp(RV0 z`uB8fey~?5M;Q*JZWPaCj9!jz!H%*z?^p^Qi{lFh1$RDD_KS?1M5_@)N*GD(3K({e z-HG+zR7m0j0Lc;bgb|D!SH*7Yu(Pxk)9gD;0mzp=$P={JfEP_aFmb^d38sP$a;8~a zeUiW}gnUZKZ*IMxW1tG=h6?bzc8vsTsZXtFS3=8tciAp);sB^19uQOjAHIhuPH~Z0vl~&>kt(Xr78H zGb^hDr9u743@Q`LQ{`YC6wAeB>PU&vdRUJ0lvFG`cZ97Y)UDZxjR&(XEl77?aKnP(!$DpbM~#%lPDJSs zlDVHA^KNfpLgY!z`x%yCNnFJnYAOmA9D7b>{2L{x6}?xFc^ZQ`_{Q8&Y*E>p5LZIE z@2;-btikXgJDC|#kIXV?ecD#DzL9Ms{X|}nHw(Wb>uM;)dUb& z`M6TB@R72qcJ}E+A!>`msLzcJ=VLoSv2ap!NMB*rNn|rdJxph!esH8v4=bC&j>?z^ zQgimIWL%(VQ|Bt$n^x>YvD}bRA0Hb*7ECcis~GGb(B~F>H#XvEo0JmBRVI};>XnC6 zkIVW&>};tEcpyeSeKpOfr|n{Jr`6U&ePT#9h=<~krteq=?-X+ieT}SQ!$}C^htO_3w2wwKSv9VEXHT|1bQ;PM2T*4pCEG{<9^0u>3LL^e&7} zuYaeg>8!k1K!kLs)9d%llfbCTI=%kgqo&71A7D`-sQ;UQ(r*ZLdi}dWO}{Sbbon)( zru4X*ev_#0tr||C>6E0`ZLG^Lu(2N(q*I+<|4vd<{X0pWzfP~~|0PM^Bm(vCE;ZG^ z)1>s2CYb&qV5Ac*->`5)!BYPk)W5wU*#7jG%$we_m|N4CKmt(@o_KS37{r@?eg)&2 zYCd6i(`kGvkbXhZYwA7Yo@D5BntmscUcW!9>4keJRWayv8lm^uyye&LF>2~Q-;)FZ zbr>&8dfoo|J!wtb@SNKlEbk4jU&SBguhZ+_H)u-VF7bxIt?~DW^rk=jedgAbzO&#B zy8meG1TN?quO4RlcWask`Srb~zd>5OlH{h>zmwBc^MOSLeXr^N1xkHa=dXXqp{Z63 zjXw%Hoo>f(0;Br7>3>aJI2TUnq*g4$Zh9@}Y$W!kci$&k-~^VW(?qA!_e+tOFrB}p z(+TY(;htc5uIchT$-g0hYdNa|xEAN7fhwvL`uA*oVgNn_bOZi$`L+F_=i0jLDxv%{ f73Lha;d!?==zKLs@BNfYUwtuiUKB_WxHbMC&6^jA literal 0 HcmV?d00001 diff --git a/tests/unit/test_healthcheck.c b/tests/unit/test_healthcheck.c new file mode 100644 index 0000000..b73e94b --- /dev/null +++ b/tests/unit/test_healthcheck.c @@ -0,0 +1,240 @@ +#include "unity.h" +#include "healthcheck.h" +#include "proxy.h" +#include +#include +#include +#include +#include +#include +#include + +/* 白盒测试:包含 healthcheck.c 以访问静态函数 */ +#define UNIT_TEST +#include "healthcheck.c" +#undef UNIT_TEST + +/* Unity 测试框架要求的 setUp/tearDown */ +void setUp(void) { } +void tearDown(void) { } + +/* ===== healthcheck_parse_status ===== */ + +void test_parse_status_200(void) { + const char *resp = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"; + int status = healthcheck_parse_status(resp, strlen(resp)); + TEST_ASSERT_EQUAL(200, status); +} + +void test_parse_status_404(void) { + const char *resp = "HTTP/1.1 404 Not Found\r\n\r\n"; + int status = healthcheck_parse_status(resp, strlen(resp)); + TEST_ASSERT_EQUAL(404, status); +} + +void test_parse_status_500(void) { + const char *resp = "HTTP/1.0 500 Internal Server Error\r\n\r\n"; + int status = healthcheck_parse_status(resp, strlen(resp)); + TEST_ASSERT_EQUAL(500, status); +} + +void test_parse_status_too_short(void) { + TEST_ASSERT_EQUAL(-1, healthcheck_parse_status("HTTP/1.1", 8)); + TEST_ASSERT_EQUAL(-1, healthcheck_parse_status("", 0)); + TEST_ASSERT_EQUAL(-1, healthcheck_parse_status("HTTP", 4)); +} + +void test_parse_status_not_http(void) { + const char *resp = "SSH-2.0-OpenSSH_8.0\r\n"; + TEST_ASSERT_EQUAL(-1, healthcheck_parse_status(resp, strlen(resp))); +} + +void test_parse_status_malformed_status(void) { + const char *resp = "HTTP/1.1 abc OK\r\n\r\n"; + TEST_ASSERT_EQUAL(-1, healthcheck_parse_status(resp, strlen(resp))); +} + +void test_parse_status_edge_cases(void) { + /* 100 Continue */ + const char *resp = "HTTP/1.1 100 Continue\r\n\r\n"; + TEST_ASSERT_EQUAL(100, healthcheck_parse_status(resp, strlen(resp))); + + /* 302 Found */ + const char *resp2 = "HTTP/1.1 302 Found\r\n\r\n"; + TEST_ASSERT_EQUAL(302, healthcheck_parse_status(resp2, strlen(resp2))); + + /* 503 Service Unavailable */ + const char *resp3 = "HTTP/1.1 503 Service Unavailable\r\n\r\n"; + TEST_ASSERT_EQUAL(503, healthcheck_parse_status(resp3, strlen(resp3))); +} + +/* ===== healthcheck_build_request ===== */ + +void test_build_request_default_path(void) { + cocoon_proxy_backend_t backend = {0}; + strncpy(backend.target_host, "localhost", sizeof(backend.target_host) - 1); + backend.target_port = 3000; + + char buf[512]; + healthcheck_build_request(&backend, NULL, buf, sizeof(buf)); + + TEST_ASSERT_TRUE(strstr(buf, "GET /health HTTP/1.1") != NULL); + TEST_ASSERT_TRUE(strstr(buf, "Host: localhost:3000") != NULL); + TEST_ASSERT_TRUE(strstr(buf, "Connection: close") != NULL); + TEST_ASSERT_TRUE(strstr(buf, "Cocoon-Healthcheck/1.0") != NULL); +} + +void test_build_request_custom_path(void) { + cocoon_proxy_backend_t backend = {0}; + strncpy(backend.target_host, "api.example.com", sizeof(backend.target_host) - 1); + backend.target_port = 8080; + + char buf[512]; + healthcheck_build_request(&backend, "/ready", buf, sizeof(buf)); + + TEST_ASSERT_TRUE(strstr(buf, "GET /ready HTTP/1.1") != NULL); + TEST_ASSERT_TRUE(strstr(buf, "Host: api.example.com:8080") != NULL); +} + +void test_build_request_empty_path_uses_default(void) { + cocoon_proxy_backend_t backend = {0}; + strncpy(backend.target_host, "127.0.0.1", sizeof(backend.target_host) - 1); + backend.target_port = 5000; + + char buf[512]; + healthcheck_build_request(&backend, "", buf, sizeof(buf)); + + TEST_ASSERT_TRUE(strstr(buf, "GET /health HTTP/1.1") != NULL); +} + +/* ===== healthcheck_update_state ===== */ + +void test_update_state_healthy_threshold(void) { + cocoon_proxy_backend_t backend = {0}; + backend.healthy = false; + + /* 第1次成功 — 仍不健康 */ + healthcheck_update_state(&backend, true); + TEST_ASSERT_FALSE(backend.healthy); + TEST_ASSERT_EQUAL(1, backend.success_count); + + /* 第2次成功 — 达到阈值,恢复健康 */ + healthcheck_update_state(&backend, true); + TEST_ASSERT_TRUE(backend.healthy); + TEST_ASSERT_EQUAL(2, backend.success_count); + TEST_ASSERT_EQUAL(0, backend.fail_count); +} + +void test_update_state_unhealthy_threshold(void) { + cocoon_proxy_backend_t backend = {0}; + backend.healthy = true; + + /* 连续3次失败 — 达到阈值,标记不健康 */ + healthcheck_update_state(&backend, false); + TEST_ASSERT_TRUE(backend.healthy); /* 第1次,仍健康 */ + TEST_ASSERT_EQUAL(1, backend.fail_count); + + healthcheck_update_state(&backend, false); + TEST_ASSERT_TRUE(backend.healthy); /* 第2次,仍健康 */ + TEST_ASSERT_EQUAL(2, backend.fail_count); + + healthcheck_update_state(&backend, false); + TEST_ASSERT_FALSE(backend.healthy); /* 第3次,标记不健康 */ + TEST_ASSERT_EQUAL(3, backend.fail_count); + TEST_ASSERT_EQUAL(0, backend.success_count); +} + +void test_update_state_reset_counts(void) { + cocoon_proxy_backend_t backend = {0}; + backend.healthy = true; + + /* 先失败1次 */ + healthcheck_update_state(&backend, false); + TEST_ASSERT_EQUAL(1, backend.fail_count); + + /* 然后成功1次 — fail_count 重置 */ + healthcheck_update_state(&backend, true); + TEST_ASSERT_TRUE(backend.healthy); + TEST_ASSERT_EQUAL(0, backend.fail_count); + TEST_ASSERT_EQUAL(1, backend.success_count); + + /* 再失败1次 — success_count 重置 */ + healthcheck_update_state(&backend, false); + TEST_ASSERT_TRUE(backend.healthy); /* 只失败1次,仍健康 */ + TEST_ASSERT_EQUAL(1, backend.fail_count); + TEST_ASSERT_EQUAL(0, backend.success_count); +} + +void test_update_state_stays_healthy_after_single_failure(void) { + cocoon_proxy_backend_t backend = {0}; + backend.healthy = true; + + /* 只失败1次,不应标记不健康 */ + healthcheck_update_state(&backend, false); + TEST_ASSERT_TRUE(backend.healthy); + TEST_ASSERT_EQUAL(1, backend.fail_count); + + /* 再次成功 */ + healthcheck_update_state(&backend, true); + TEST_ASSERT_TRUE(backend.healthy); +} + +void test_update_state_stays_unhealthy_after_single_success(void) { + cocoon_proxy_backend_t backend = {0}; + backend.healthy = false; + + /* 只成功1次,不应标记健康 */ + healthcheck_update_state(&backend, true); + TEST_ASSERT_FALSE(backend.healthy); + TEST_ASSERT_EQUAL(1, backend.success_count); +} + +void test_update_state_last_check_updated(void) { + cocoon_proxy_backend_t backend = {0}; + time_t before = time(NULL); + + healthcheck_update_state(&backend, true); + + time_t after = time(NULL); + TEST_ASSERT_TRUE(backend.last_check >= before); + TEST_ASSERT_TRUE(backend.last_check <= after); +} + +/* ===== healthcheck_probe_once ===== */ + +void test_probe_once_null_backend(void) { + TEST_ASSERT_FALSE(healthcheck_probe_once(NULL, 1000)); +} + +/* ===== 主函数 ===== */ + +int main(void) { + UNITY_BEGIN(); + + /* parse_status */ + RUN_TEST(test_parse_status_200); + RUN_TEST(test_parse_status_404); + RUN_TEST(test_parse_status_500); + RUN_TEST(test_parse_status_too_short); + RUN_TEST(test_parse_status_not_http); + RUN_TEST(test_parse_status_malformed_status); + RUN_TEST(test_parse_status_edge_cases); + + /* build_request */ + RUN_TEST(test_build_request_default_path); + RUN_TEST(test_build_request_custom_path); + RUN_TEST(test_build_request_empty_path_uses_default); + + /* update_state */ + RUN_TEST(test_update_state_healthy_threshold); + RUN_TEST(test_update_state_unhealthy_threshold); + RUN_TEST(test_update_state_reset_counts); + RUN_TEST(test_update_state_stays_healthy_after_single_failure); + RUN_TEST(test_update_state_stays_unhealthy_after_single_success); + RUN_TEST(test_update_state_last_check_updated); + + /* probe_once */ + RUN_TEST(test_probe_once_null_backend); + + return UNITY_END(); +}