Compare commits

..

353 Commits

Author SHA1 Message Date
Kevin Wan
07e3e14c0e chore: remove go build version in fuzz test (#3678) 2023-10-28 20:53:03 +08:00
Rene Leonhardt
34c5f6616c chore: upgrade go to 1.19 (#3677) 2023-10-28 12:12:04 +00:00
MarkJoyMa
32600f2619 fix: adjust log encode output mode (#3676) 2023-10-28 11:46:52 +00:00
dependabot[bot]
b07df1c344 chore(deps): bump go.etcd.io/etcd/client/v3 from 3.5.9 to 3.5.10 (#3675)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-28 13:16:25 +08:00
Kevin Wan
a1fca3a1da chore: upgrade go dependencies (#3657) 2023-10-28 00:19:22 +08:00
Kevin Wan
9394e59597 chore: update goctl version to 1.6.0 (#3674) 2023-10-27 21:59:35 +08:00
dependabot[bot]
f8adc71529 chore(deps): bump github.com/google/uuid from 1.3.1 to 1.4.0 (#3673)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-27 21:34:27 +08:00
MarkJoyMa
c05e03bb5a feat: add metrics (#3624) 2023-10-26 15:51:28 +00:00
Kevin Wan
199e86050e chore: simplify prometheus check (#3672) 2023-10-26 20:32:13 +08:00
#Suyghur
1e2a12b3d6 feat(metric): added Dec() and Sub() in GaugeVec interface (#3666) 2023-10-26 20:13:42 +08:00
Kevin Wan
922efbfc2d chore: refactor zrpc timeout (#3671) 2023-10-26 08:55:26 +08:00
vankillua
842c4d81cc feat: support the specified timeout of rpc methods (#2742)
Co-authored-by: hanzijian <hanzijian@52tt.com>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-10-25 13:01:57 +00:00
dependabot[bot]
2a335c7608 chore(deps): bump github.com/fullstorydev/grpcurl from 1.8.8 to 1.8.9 (#3668)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 15:15:30 +08:00
Bhargav Shirin Nalamati
35edd6b19d fixed typo: reds to redis (#3664) 2023-10-24 02:54:15 +00:00
Kevin Wan
36bbc6a2e2 chore: add error handling on registering event handlers to k8s (#3663) 2023-10-23 21:57:09 +08:00
唐小鸭
e20ccdd011 Support for resource injection (#3383)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-10-23 13:22:16 +00:00
Kevin Wan
c2ff00883a chore: update restful/grpc servers shutdown stages (#3662) 2023-10-23 13:03:05 +00:00
MarkJoyMa
00db97fcc1 feat: model add withSession (#3658) 2023-10-23 04:43:05 +00:00
7134g
117c3a9069 fix: multiple files import the same api file (#3642) 2023-10-23 04:04:52 +00:00
Kevin Wan
172ff407f3 chore: refactor mongo logs (#3660) 2023-10-23 11:03:55 +08:00
shenbaise9527
a242fec5e1 feat: support for disable mon logs like sqlx (#3606)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-10-22 15:00:02 +00:00
Kevin Wan
6286941ebf chore: add go-zero users (#3659) 2023-10-22 22:23:24 +08:00
Kevin Wan
42e0a6f90c chore: refactor errors to use errors.Is (#3654) 2023-10-21 00:00:57 +08:00
cary
81ae7d36b5 Support for adding ignore_columns parameters to the goctl model pg (#3427) 2023-10-20 08:58:18 +00:00
Kevin Wan
944e76edb9 chore: refactor errors (#3651) 2023-10-20 14:58:38 +08:00
MarkJoyMa
151768ef82 feat: optimize logx print error (#3649) 2023-10-19 13:46:52 +00:00
Surav Shrestha
50581c7f5c docs fix typo in core/logx/readme.md (#3650) 2023-10-19 13:33:25 +00:00
dependabot[bot]
54041ef9e4 chore(deps): bump google.golang.org/grpc from 1.58.2 to 1.59.0 in /tools/goctl (#3645) 2023-10-19 12:38:08 +08:00
dependabot[bot]
5a9ae5ef02 chore(deps): bump google.golang.org/grpc from 1.58.2 to 1.59.0 (#3647) 2023-10-19 12:26:36 +08:00
guonaihong
19de13bb04 Upgrade grpc-go,fix 0day problem. (#3623) 2023-10-19 03:45:59 +00:00
Kevin Wan
3ab4e82168 chore: upgrade go to 1.19 (#3648) 2023-10-19 11:30:37 +08:00
Armaan
619e838513 updated CONTRIBUTING.md with emojified , fun, precise and engaging text (#3643) 2023-10-19 03:06:38 +00:00
kesonan
423597a01c feat: export devserver.Config (#3638) 2023-10-17 15:38:21 +00:00
kesonan
d84dfe1b20 fix: goctl unit test (#3636) 2023-10-17 11:15:32 +00:00
Soham Tembhurne
87b7a1120d Update documenation section #background (#3634) 2023-10-16 00:11:17 +00:00
Kevin Wan
528af8a99d chore: update readme for Mac install instructions (#3633) 2023-10-16 08:08:42 +08:00
Soham Tembhurne
17fc68ac5a Update readme.md (#3630) 2023-10-15 23:45:25 +08:00
Kevin Wan
804a56bd14 fix: optimize logx for less GC objects (#3627) 2023-10-15 23:37:45 +08:00
Kevin Wan
88f60d7736 chore: refactor signal sigterm and sigint (#3632) 2023-10-15 23:24:17 +08:00
#Suyghur
95b7a3d3ce feat: add the SIGINT signal in signals.go to subscribe the user input ctrl+c to exit the application operation (#3611) 2023-10-15 22:58:15 +08:00
Kevin Wan
d71c0da7b7 chore: refactor error comparison (#3629) 2023-10-15 13:41:06 +00:00
Kevin Wan
fd070fec91 feat: retry with ctx deadline (#3626) 2023-10-15 13:39:44 +00:00
Kevin Wan
4f22034342 fix: unmarshal from number to string with incorrect error message (#3625) 2023-10-15 02:06:00 +00:00
Ikko Eltociear Ashimine
b731aa38af refactor: update builder.go (#3620) 2023-10-13 07:03:15 +00:00
dependabot[bot]
bf996a1812 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.30.5 to 2.31.0 (#3616)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-12 11:44:55 +08:00
dependabot[bot]
af7ce65244 chore(deps): bump golang.org/x/net from 0.15.0 to 0.17.0 in /tools/goctl (#3618)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-12 11:37:06 +08:00
dependabot[bot]
952db71835 chore(deps): bump golang.org/x/net from 0.16.0 to 0.17.0 (#3612)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-11 17:37:14 +08:00
Kevin Wan
abd1fa96a9 fix: UpdateStmt doesn't update the statement correctly in sqlx/bulkinserter.go (#3607) 2023-10-09 21:57:26 +08:00
Kevin Wan
5aedd9c076 chore: simplify parsing numbers with overflow (#3610) 2023-10-09 13:00:09 +00:00
Kevin Wan
ff230c4b1d chore: refactor goctl api (#3605) 2023-10-07 22:58:29 +08:00
kesonan
02c95108b9 optimize: fix experimental api (#3604) 2023-10-07 19:48:41 +08:00
dependabot[bot]
1ff541afe4 chore(deps): bump golang.org/x/net from 0.15.0 to 0.16.0 (#3603)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-07 10:50:49 +08:00
Kevin Wan
11a8cbc1e5 chore: rename noOpBreaker to nopBreaker (#3602) 2023-10-06 23:41:09 +08:00
dependabot[bot]
c063976822 chore(deps): bump golang.org/x/sys from 0.12.0 to 0.13.0 (#3601)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-06 22:04:14 +08:00
dependabot[bot]
cb707034ce chore(deps): bump github.com/jhump/protoreflect from 1.15.2 to 1.15.3 (#3600)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-05 15:26:21 +08:00
dependabot[bot]
f10db27efd chore(deps): bump github.com/prometheus/client_golang from 1.16.0 to 1.17.0 (#3594)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-28 11:38:20 +08:00
Kevin Wan
4878f90546 chore: update goctl version to 1.5.6 (#3593) 2023-09-27 22:55:50 +08:00
Kevin Wan
421e6617b1 chore: add more tests (#3592) 2023-09-27 22:33:27 +08:00
Kevin Wan
0ee7a271d3 fix: avoid float overflow in mapping.Unmarshal (#3590) 2023-09-26 13:46:34 +00:00
dependabot[bot]
af022b9655 chore(deps): bump google.golang.org/grpc from 1.58.1 to 1.58.2 in /tools/goctl (#3584) 2023-09-25 16:23:29 +08:00
dependabot[bot]
98d46261d9 chore(deps): bump google.golang.org/grpc from 1.58.1 to 1.58.2 (#3585)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-24 23:27:02 +08:00
Kevin Wan
4222fd97bc chore: add test for logging rotate size (#3587) 2023-09-24 22:28:03 +08:00
dependabot[bot]
814852f0b8 chore(deps): bump github.com/fullstorydev/grpcurl from 1.8.7 to 1.8.8 (#3586)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-24 19:04:34 +08:00
Kevin Wan
ded2888759 fix: avoid integer overflow in mapping.Unmarshal (#3582) 2023-09-21 14:22:33 +00:00
Kevin Wan
18d66a795d chore: add more tests (#3578) 2023-09-20 23:52:10 +08:00
Kevin Wan
4211672bfd chore: add more tests (#3577) 2023-09-20 00:01:26 +08:00
Kevin Wan
68df0c3620 chore: add more tests (#3575) 2023-09-18 11:01:46 +08:00
xt-inking
5e435b6a76 fix: avoid losing logs before closing (#3573) 2023-09-17 11:38:53 +00:00
Kevin Wan
0dcede6457 chore: refactor log limit in rest (#3572) 2023-09-16 22:33:30 +08:00
Awadabang
cc21f5fae2 update: limit logBrief http body size (#3498)
Co-authored-by: 常公征 <changgz@yealink.com>
2023-09-16 11:58:21 +00:00
dependabot[bot]
b22ad50d59 chore(deps): bump google.golang.org/grpc from 1.58.0 to 1.58.1 in /tools/goctl (#3568)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-16 16:52:01 +08:00
Kevin Wan
974252980c chore: upgrade grpc (#3570) 2023-09-15 23:21:22 +08:00
dependabot[bot]
8d83986d27 chore(deps): bump google.golang.org/grpc from 1.57.0 to 1.58.0 in /tools/goctl (#3546) 2023-09-12 20:00:52 +08:00
Kevin Wan
6821b0a7dd chore: upgrade grpc (#3558) 2023-09-12 10:30:26 +08:00
Kevin Wan
1ba1724c65 chore: refactor (#3545) 2023-09-06 22:36:43 +08:00
Xinwei Xiong
ca5a7df5b0 feat: Optimize Encoding Functions and Add Descriptive Comments (#3543) 2023-09-06 14:19:50 +00:00
dependabot[bot]
69a3024853 chore(deps): bump golang.org/x/net from 0.14.0 to 0.15.0 (#3544)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-06 20:28:03 +08:00
dependabot[bot]
fd3abf3717 chore(deps): bump golang.org/x/text from 0.12.0 to 0.13.0 in /tools/goctl (#3542)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-09-06 20:20:12 +08:00
dependabot[bot]
99b3750d10 chore(deps): bump golang.org/x/sys from 0.11.0 to 0.12.0 (#3541)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-06 00:02:00 +08:00
POABOB
33f6d7ebb8 fix: goctl pg gen will extract all fields when the same table name exists in different schemas (#3496) (#3517) 2023-09-04 20:48:26 +08:00
kesonan
c4ef9ceb68 Add api version (#3536) 2023-09-02 01:45:48 +00:00
dependabot[bot]
e95861f28a chore(deps): bump github.com/pelletier/go-toml/v2 from 2.0.9 to 2.1.0 (#3532)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-31 22:52:23 +08:00
Kevin Wan
d3cd7b17c0 Revert "add:func() QueryRowsPartial,QueryRowPartial into cachedsql.go" (#3523) 2023-08-27 21:36:14 +08:00
liumin-go
a50515496c add:func() QueryRowsPartial,QueryRowPartial into cachedsql.go (#3512)
Co-authored-by: 刘敏 <liumin@liumindeMac-mini.local>
2023-08-27 08:05:02 +00:00
Kevin Wan
0423313d9b feat: support json:"-" in mapping (#3521) 2023-08-27 16:04:38 +08:00
dependabot[bot]
7bbe7de05f chore(deps): bump github.com/google/uuid from 1.3.0 to 1.3.1 (#3511)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-24 14:59:49 +08:00
dependabot[bot]
83a451f2f4 chore(deps): bump github.com/jhump/protoreflect from 1.15.1 to 1.15.2 (#3518)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-24 14:39:50 +08:00
Kevin Wan
d2a874f21d chore: upgrade go-zero, and update goctl version (#3509) 2023-08-21 09:09:51 +08:00
Kevin Wan
fd85b24b25 Update readme-cn.md 2023-08-20 23:38:39 +08:00
Kevin Wan
14fcbd7658 fix #3499 (#3508) 2023-08-19 22:17:24 +08:00
Kevin Wan
cb3ffc76a3 fix: #3478 (#3493) 2023-08-14 14:22:22 +00:00
dependabot[bot]
45fbd7dc35 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.30.4 to 2.30.5 (#3490)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-10 19:53:17 +08:00
dependabot[bot]
af821cf794 chore(deps): bump golang.org/x/net from 0.13.0 to 0.14.0 (#3484)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-09 10:39:32 +08:00
dependabot[bot]
ec69950153 chore(deps): bump github.com/jackc/pgx/v5 from 5.4.2 to 5.4.3 (#3483)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-09 10:18:27 +08:00
Kevin Wan
ce5e78db53 chore: use jsonTagKey to replace json literals (#3479) 2023-08-06 22:00:24 +08:00
dependabot[bot]
ed75802eaa chore(deps): bump golang.org/x/text from 0.11.0 to 0.12.0 in /tools/goctl (#3477)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-06 09:57:19 +08:00
dependabot[bot]
76c92b571d chore(deps): bump golang.org/x/sys from 0.10.0 to 0.11.0 (#3476)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-06 00:19:00 +08:00
dependabot[bot]
a2e703c53e chore(deps): bump golang.org/x/net from 0.12.0 to 0.13.0 (#3463)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-08-04 20:39:17 +08:00
dependabot[bot]
ca698deb2a chore(deps): bump go.mongodb.org/mongo-driver from 1.12.0 to 1.12.1 (#3472)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-04 19:13:16 +08:00
guangwu
a9f4aab86b fix: "EXPRIMENTAL" is a misspelling of "EXPERIMENTAL" (#3462) 2023-08-02 23:59:37 +08:00
Kevin Wan
c3f57e9b0a chore: fix potential nil pointer errors (#3454) 2023-07-30 21:37:41 +08:00
Kevin Wan
ad4cce959d chore: add more tests (#3453) 2023-07-29 22:34:16 +08:00
Shyunn
279123f4a7 feat: add prometheus summary metrics (#3440)
Co-authored-by: chen quan <chenquan.dev@gmail.com>
2023-07-29 16:51:43 +08:00
dependabot[bot]
457eb1961b chore(deps): bump google.golang.org/grpc from 1.56.2 to 1.57.0 (#3445)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-28 22:18:14 +08:00
dependabot[bot]
63df384a4b chore(deps): bump google.golang.org/grpc from 1.56.2 to 1.57.0 in /tools/goctl (#3446)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-28 22:17:23 +08:00
MarkJoyMa
42bfa26e2b fix: remove mapping redundant error (#3439) 2023-07-24 00:10:50 +08:00
Kevin Wan
ff04356704 fix: format error should not trigger circuit breaker in sqlx (#3437) 2023-07-23 20:40:03 +08:00
MarkJoyMa
05db706c62 feat: optimize mapping error (#3438) 2023-07-23 12:10:41 +00:00
dependabot[bot]
ef2e0d859d chore(deps): bump go.uber.org/automaxprocs from 1.5.2 to 1.5.3 (#3435)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-22 00:03:07 +08:00
dependabot[bot]
05ec16ae9d chore(deps): bump github.com/gookit/color from 1.5.3 to 1.5.4 in /tools/goctl (#3433)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-21 23:27:49 +08:00
dependabot[bot]
13e685e0db chore(deps): bump github.com/emicklei/proto from 1.12.0 to 1.12.1 in /tools/goctl (#3431)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-21 23:12:06 +08:00
dependabot[bot]
c10f44b74e chore(deps): bump github.com/emicklei/proto from 1.11.2 to 1.12.0 in /tools/goctl (#3429) 2023-07-18 09:41:13 +08:00
Kevin Wan
57644420ed chore: update go-zero for goctl (#3426) 2023-07-14 21:38:42 +08:00
dependabot[bot]
b245159417 chore(deps): bump github.com/pelletier/go-toml/v2 from 2.0.8 to 2.0.9 (#3423)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-14 19:57:36 +08:00
dependabot[bot]
c26ea17669 chore(deps): bump github.com/iancoleman/strcase from 0.2.0 to 0.3.0 in /tools/goctl (#3424) 2023-07-14 13:20:06 +08:00
Kevin Wan
a7daff3587 chore: make servicegroup panic as demand (#3422) 2023-07-13 14:08:35 +00:00
dependabot[bot]
6719d06146 chore(deps): bump github.com/jackc/pgx/v5 from 5.4.1 to 5.4.2 (#3417)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-13 10:56:18 +08:00
Kevin Wan
0c6eaeda9f chore: coding style (#3413) 2023-07-12 01:08:09 +08:00
Xinyan Lu
b9c0c0f8b5 feat: add detail type mismatch info in number fields check (#3386) (#3387) 2023-07-11 16:29:42 +00:00
Kevin Wan
77da459165 chore: make test stable (#3412) 2023-07-11 16:20:41 +00:00
Kevin Wan
13cdbdc98b chore: avoid nested WithCodeResponseWriter (#3406) 2023-07-11 15:59:43 +00:00
guangwu
e8c1e6e09b fix: log format error (#3409) 2023-07-11 05:28:53 +00:00
guangwu
f1171e01f2 chore: slice replace loop (#3410) 2023-07-11 05:27:46 +00:00
cong
61e562d0c7 refactor(rest): keep rest log collector context key private (#3407) 2023-07-10 01:52:26 +00:00
chen quan
b71453985c feat(sqlx): support for custom Acceptable function (#3405) 2023-07-10 01:16:45 +00:00
Kevin Wan
31b9ba19a2 chore: refactor httpx.TimeoutHandler (#3400) 2023-07-09 07:04:59 +00:00
dependabot[bot]
3170afd57b chore(deps): bump google.golang.org/grpc from 1.56.1 to 1.56.2 in /tools/goctl (#3403)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-08 16:21:50 +08:00
dependabot[bot]
03e365a5d8 chore(deps): bump google.golang.org/grpc from 1.56.1 to 1.56.2 (#3402)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-08 15:34:45 +08:00
dependabot[bot]
7d4fce9588 chore(deps): bump golang.org/x/net from 0.11.0 to 0.12.0 (#3401)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-06 14:54:13 +08:00
扶桑花间
916cea858f 1. Fix w. (http. Flusher). Flush() error (#3388) 2023-07-05 15:27:15 +00:00
dependabot[bot]
a86942d532 chore(deps): bump golang.org/x/sys from 0.9.0 to 0.10.0 (#3396)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-05 22:49:07 +08:00
dependabot[bot]
f76c70ea9a chore(deps): bump golang.org/x/text from 0.10.0 to 0.11.0 in /tools/goctl (#3397)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-05 22:19:04 +08:00
MarkJoyMa
4cbfdb3d74 feat: optimize must log add stack (#3384) 2023-06-30 01:11:03 +00:00
dependabot[bot]
aefa6dfb50 chore(deps): bump google.golang.org/protobuf from 1.30.0 to 1.31.0 (#3376) 2023-06-30 09:06:20 +08:00
dependabot[bot]
9047029475 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.30.3 to 2.30.4 (#3381) 2023-06-30 09:05:05 +08:00
dependabot[bot]
f296c182f7 chore(deps): bump google.golang.org/protobuf from 1.30.0 to 1.31.0 in /tools/goctl (#3377)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-27 22:45:00 +08:00
Kevin Wan
40e7a4cd07 chore: refactor httpx.SetOkHandler (#3373) 2023-06-26 00:27:26 +08:00
Kevin Wan
92e5819e91 opt: improve logx performance (#3371) 2023-06-25 15:41:28 +08:00
2822132073
8d23ab158b fix In goctl new api, occur error invalid character 'A' looking for beginning of value (#3357) 2023-06-25 07:26:21 +00:00
唐小鸭
bcccfab824 [fix] The directory is not recognized when it is in a soft link (#3337) 2023-06-25 05:06:27 +00:00
dependabot[bot]
f7e701a634 chore(deps): bump google.golang.org/grpc from 1.56.0 to 1.56.1 (#3367) 2023-06-25 13:04:13 +08:00
dependabot[bot]
7c2d8e5cc2 chore(deps): bump google.golang.org/grpc from 1.56.0 to 1.56.1 in /tools/goctl (#3369) 2023-06-25 12:57:03 +08:00
dependabot[bot]
5b622d6265 chore(deps): bump go.mongodb.org/mongo-driver from 1.11.7 to 1.12.0 (#3370)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-23 14:50:48 +08:00
dependabot[bot]
c5510a4e1b chore(deps): bump github.com/jackc/pgx/v5 from 5.4.0 to 5.4.1 (#3363)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-21 15:16:06 +08:00
Kevin Wan
2a33b74b35 chore: coding style (#3362) 2023-06-17 22:59:00 +08:00
anqiansong
45bb547a81 (goctl)fix: #3328 (#3348)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-06-17 13:23:57 +00:00
Mikael
f5f5261556 add whether to generate rpc client option (#3361)
Co-authored-by: admin <admin@admindeMacBook-Pro.local>
2023-06-17 12:52:49 +00:00
Kevin Wan
b176d5d434 chore: add more tests (#3359) 2023-06-17 20:51:33 +08:00
MarkJoyMa
92f6c48349 fix: NewClientWithTarget miss default config (#3358) 2023-06-16 23:29:52 +08:00
dependabot[bot]
71e8230e65 chore(deps): bump google.golang.org/grpc from 1.55.0 to 1.56.0 (#3354) 2023-06-16 07:46:33 +08:00
dependabot[bot]
018fa8e0a0 chore(deps): bump google.golang.org/grpc from 1.55.0 to 1.56.0 in /tools/goctl (#3355) 2023-06-16 07:31:48 +08:00
dependabot[bot]
979fe9718a chore(deps): bump github.com/prometheus/client_golang from 1.15.1 to 1.16.0 (#3352) 2023-06-16 07:29:38 +08:00
Kevin Wan
f998803131 chore: refactor and add more tests (#3351) 2023-06-16 01:04:58 +08:00
TaoYu
1262266ac2 feat: httpx add common handler (#3269) 2023-06-15 15:31:15 +00:00
dependabot[bot]
9c32bf8478 chore(deps): bump github.com/zeromicro/ddl-parser from 1.0.4 to 1.0.5 in /tools/goctl (#3350) 2023-06-15 07:34:19 +08:00
dependabot[bot]
37ec7f6443 chore(deps): bump github.com/jackc/pgx/v5 from 5.3.1 to 5.4.0 (#3349) 2023-06-15 07:31:16 +08:00
dependabot[bot]
2fdc4dfc0f chore(deps): bump golang.org/x/net from 0.10.0 to 0.11.0 (#3346) 2023-06-14 07:05:24 +08:00
dependabot[bot]
4b2a6ba3de chore(deps): bump golang.org/x/text from 0.9.0 to 0.10.0 in /tools/goctl (#3342) 2023-06-13 07:37:52 +08:00
dependabot[bot]
7fa3f10f22 chore(deps): bump golang.org/x/sys from 0.8.0 to 0.9.0 (#3341) 2023-06-13 07:37:30 +08:00
elza
4a29a0b642 fix: fixed goctl api go --home parameter error when loading non-exist… (#3319)
Co-authored-by: yuanyou <yuanyou@kezaihui.com>
2023-06-12 16:00:41 +00:00
Kevin Wan
a62745a152 Update readme-cn.md 2023-06-12 23:35:33 +08:00
Kevin Wan
28314326e7 chore: more tests (#3340) 2023-06-12 23:29:23 +08:00
Kevin Wan
f6bdb6e1de chore: add more tests (#3338) 2023-06-12 01:22:20 +08:00
Kevin Wan
efa6940001 chore: improve logx gzip (#3332) 2023-06-09 22:50:59 +08:00
Ron_haur
da81d8f774 Fix: logx with Compress auto delete old logs (#3329)
Co-authored-by: haoran.ren <haoran.ren@mihoyo.com>
2023-06-08 11:08:04 +00:00
dependabot[bot]
fd84b27bdc chore(deps): bump go.mongodb.org/mongo-driver from 1.11.6 to 1.11.7 (#3325) 2023-06-08 07:21:49 +08:00
Kevin Wan
6b4d0d89c0 chore: add more tests (#3324) 2023-06-07 00:46:43 +08:00
Kevin Wan
d61a55f779 chore: update readme to remove upgrade parts. (#3318) 2023-06-04 23:34:38 +08:00
Kevin Wan
8ef4164209 chore: make test stable (#3317) 2023-06-04 23:20:58 +08:00
Kevin Wan
50e29e2075 chore: update go-zero for goctl (#3316) 2023-06-04 17:28:27 +08:00
Kevin Wan
452c9dbcaf chore: add more tests (#3315) 2023-06-01 21:08:44 +08:00
dependabot[bot]
3564e36a35 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.30.2 to 2.30.3 (#3309)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 10:42:26 +08:00
dependabot[bot]
e479e47634 chore(deps): bump github.com/stretchr/testify from 1.8.3 to 1.8.4 in /tools/goctl (#3306)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-31 11:28:15 +08:00
dependabot[bot]
ad921a6419 chore(deps): bump github.com/stretchr/testify from 1.8.3 to 1.8.4 (#3305)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-31 11:20:12 +08:00
Kevin Wan
44c8d6f269 chore: add more tests (#3304) 2023-05-30 23:27:27 +08:00
Kevin Wan
8a4cc4f98d chore: add more tests (#3299) 2023-05-29 23:44:36 +08:00
kangqi
e751736516 trace exporter: add new type file (#3298)
Co-authored-by: zhaikangqi <794556486@qq.com>
2023-05-29 18:24:59 +08:00
Kevin Wan
032f2419a2 Update readme-cn.md 2023-05-29 17:36:10 +08:00
Kevin Wan
84adc054bc chore: add more tests (#3296) 2023-05-29 07:39:41 +08:00
Kevin Wan
b92e706ce1 chore: refactor (#3295) 2023-05-28 21:31:36 +08:00
MiNG
1b5946346e feat: support optional otel global initialization for #3284 (#3292) 2023-05-28 11:41:48 +00:00
Kevin Wan
28d3905731 chore: add more tests (#3294) 2023-05-28 19:26:45 +08:00
hc
3726851c7f feat: sqlc add SetCacheWithExpire method (#3249)
Co-authored-by: luohancai <luohancai@taqu.cn>
2023-05-28 12:27:30 +08:00
Kevin Wan
2f2ddd373b chore: refactor retry (#3291) 2023-05-28 12:11:55 +08:00
Xiaoju Jiang
8d48e34eed update: expand the retry method to support timeout and interval control (#3283) 2023-05-28 10:17:50 +08:00
Kevin Wan
32f78668db chore: add more tests (#3290) 2023-05-27 23:57:33 +08:00
Kevin Wan
cd0f3726ed chore: add more tests (#3288) 2023-05-27 21:49:11 +08:00
me-cs
0217044900 update:Use the Milliseconds method of duration to get the number of milliseconds (#3285)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-05-26 14:32:46 +00:00
Kevin Wan
8b4382dcec chore: add more tests (#3286) 2023-05-26 22:30:03 +08:00
Kevin Wan
fa33329a44 chore: add more tests (#3282) 2023-05-26 00:21:47 +08:00
dependabot[bot]
d76a39ac26 chore(deps): bump github.com/pelletier/go-toml/v2 from 2.0.7 to 2.0.8 (#3280)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-25 16:50:36 +08:00
guangwu
76a7a17e57 typo (#3281) 2023-05-25 15:58:11 +08:00
Kevin Wan
4a2a8d9e45 chore: add more tests (#3279) 2023-05-24 23:58:45 +08:00
guangwu
ef26b39b4c misspelling (#3248) 2023-05-24 08:15:27 +00:00
anqiansong
3ca40001b4 feat(goctl): Add with session for model tpl (#3272) 2023-05-24 07:34:26 +00:00
Toby
278ae3d26a feat: add OtlpHttpPath config support for ZincObserve Telemetry (#3271)
Signed-off-by: Toby Yan <me@tobyan.com>
Co-authored-by: cong <zhangcong1992@gmail.com>
2023-05-23 03:11:58 +00:00
dependabot[bot]
fa1d6d50a8 chore(deps): bump github.com/stretchr/testify from 1.8.2 to 1.8.3 (#3267) 2023-05-22 09:41:17 +08:00
dependabot[bot]
0f4973be06 chore(deps): bump github.com/stretchr/testify from 1.8.2 to 1.8.3 in /tools/goctl (#3268) 2023-05-21 21:43:24 +08:00
Kevin Wan
a9aac7e420 chore: add more tests (#3265) 2023-05-19 23:29:30 +08:00
Kevin Wan
925cf8d3d1 chore: add more tests (#3261) 2023-05-19 12:15:43 +08:00
Kevin Wan
99ce24e2ab chore: add more tests (#3260) 2023-05-19 00:56:50 +08:00
Kevin Wan
701bb31ed2 chore: add more tests (#3259) 2023-05-18 23:43:50 +08:00
Kevin Wan
55e2c7ee83 chore: add more tests (#3258) 2023-05-18 23:11:32 +08:00
Kevin Wan
90839965fa chore: remove directive for tests (#3257) 2023-05-18 21:34:33 +08:00
Kevin Wan
f7228e9af1 chore: add more tests (#3256) 2023-05-18 12:24:04 +08:00
Kevin Wan
f95adae3c1 Update readme-cn.md 2023-05-18 11:00:03 +08:00
Kevin Wan
bff5b81ad9 feat: support using session to execute statements in transaction (#3252) 2023-05-17 14:15:24 +00:00
guangwu
f0bdfb928f fix call error func (#3245) 2023-05-12 04:34:08 +00:00
Kevin Wan
e4a1b7bb39 chore: format the code (#3243) 2023-05-12 10:38:05 +08:00
dependabot[bot]
b6906b5d21 chore(deps): bump go.etcd.io/etcd/client/v3 from 3.5.8 to 3.5.9 (#3242)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-12 08:27:46 +08:00
guangwu
116da96178 add ignore file (#3240) 2023-05-11 08:53:13 +00:00
lchjczw
9fa98c2bd3 api imports take the form of relative paths (#3201)
Co-authored-by: 李春华 <lichunhua@threesoft.cn>
2023-05-10 03:40:07 +00:00
Kevin Wan
b1c4c4736f chore: better comments (#3232) 2023-05-09 22:58:40 +08:00
Thirteen
ef410e8083 fix: generate client directory for goctl (#3166) 2023-05-08 21:03:48 +00:00
fondoger
c22bc1c8ea [dart-gen] Fix lists containing atomic types (#3210) 2023-05-08 21:00:46 +00:00
Toby
1853428011 feat: add otlptracegrpc otlptracehttp headers support for Uptrace (#3219)
Signed-off-by: Toby Yan <me@tobyan.com>
Co-authored-by: cong <zhangcong1992@gmail.com>
2023-05-08 20:58:29 +00:00
dependabot[bot]
3637e10815 chore(deps): bump golang.org/x/net from 0.9.0 to 0.10.0 (#3230) 2023-05-09 04:53:33 +08:00
Kevin Wan
93124329ac chore: add more tests (#3229) 2023-05-08 23:49:13 +08:00
yangtao
851a72f1cc Add RunSafe with context (#3224)
Co-authored-by: yangtao <mrynag8614@163.com>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-05-08 23:08:31 +08:00
SleeplessBot
a93c24ce84 Add method label for prometheus middleware metrics (#3226)
Co-authored-by: 蓝益尤 <lan.yiyou@intellif.com>
2023-05-08 12:59:20 +00:00
Kevin Wan
9f42eda9ff fix: timeout handler not implementing http.Flusher (#3225) 2023-05-08 18:07:02 +08:00
soasurs
8762a3b7ba fix: Errorv should generate JSON Object for content field in log (#3222)
Signed-off-by: soasurs <soasurs@gmail.com>
2023-05-08 09:16:44 +00:00
Kevin Wan
2684a157ff chore: remove fgprof, use pprof directly (#3220) 2023-05-07 18:21:10 +08:00
guangwu
63368d8b0c io/ioutil deprecated (#3217) 2023-05-06 09:50:54 +00:00
guangwu
4f13fe8188 io/ioutil deprecated (#3215) 2023-05-06 16:37:43 +08:00
Kevin Wan
9fc7874336 chore: optimize stat calculation (#3213) 2023-05-06 15:31:56 +08:00
dependabot[bot]
e6518521eb chore(deps): bump google.golang.org/grpc from 1.54.0 to 1.55.0 in /tools/goctl (#3209)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-06 14:29:51 +08:00
Kevin Wan
8f5a0a2de7 fix: remove etcd pings to avoid too-many-pings error (#3212) 2023-05-06 12:39:19 +08:00
ALMAS
774e8d1d08 feat: replaced color package to support Windows (#3207) 2023-05-05 13:09:54 +00:00
cong
8ad0668612 fix(zrpc): remove default keepalive params for NewClientWithTarget (#3208) 2023-05-05 13:00:59 +00:00
dependabot[bot]
8a043d2443 chore(deps): bump golang.org/x/sys from 0.7.0 to 0.8.0 (#3204)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-05 11:03:18 +08:00
dependabot[bot]
0e2ee97a02 chore(deps): bump go.mongodb.org/mongo-driver from 1.11.4 to 1.11.6 (#3205)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-05 10:41:23 +08:00
Kevin Wan
42300a7d83 chore: add more tests (#3203) 2023-05-04 23:43:34 +08:00
dependabot[bot]
fe97fab274 chore(deps): bump github.com/prometheus/client_golang from 1.15.0 to 1.15.1 (#3200)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-04 11:00:51 +08:00
dependabot[bot]
f93e752f98 chore(deps): bump github.com/emicklei/proto from 1.11.1 to 1.11.2 in /tools/goctl (#3199)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-04 10:24:24 +08:00
fondoger
3a66fc038f [dart-gen] Fix nullable list item issue (#3192) 2023-05-01 15:01:28 +08:00
Kevin Wan
b028ed058d chore: change port to 6060 by default in devserver (#3191) 2023-05-01 11:18:15 +08:00
guonaihong
1fd0c3992b fix panic (#3176) 2023-04-30 14:58:30 +00:00
Kevin Wan
1aebb3e5e4 chore: update readme (#3190) 2023-04-30 11:21:28 +08:00
Kevin Wan
8ffe4c01d1 chore: use logx.Must instead of log.Fatal (#3189) 2023-04-29 23:46:04 +08:00
Kevin Wan
a31256b327 chore: add more tests (#3187) 2023-04-29 22:59:07 +08:00
Kevin Wan
14caf5c799 chore: simplify tests with logtest (#3184) 2023-04-29 20:36:29 +08:00
chensy
c0f8a58ed7 no \t to space line (#3177)
Co-authored-by: chenjieping <chenjieping@kezaihui.com>
2023-04-29 08:35:04 +00:00
dependabot[bot]
3189ec7be6 chore(deps): bump github.com/go-sql-driver/mysql from 1.7.0 to 1.7.1 (#3174)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-28 23:27:01 +08:00
dependabot[bot]
f51e9f0ea7 chore(deps): bump github.com/go-sql-driver/mysql from 1.7.0 to 1.7.1 in /tools/goctl (#3175)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-27 23:37:28 +08:00
cong
ba9d510cdb fix(metrics): enable prometheus global switch when user enable devsever metrics (#3169) 2023-04-24 20:02:04 +08:00
Kevin Wan
8c9b619199 chore: update go-zero for goctl (#3165) 2023-04-24 13:02:26 +08:00
Kevin Wan
49f73265b9 chore: refactor (#3164) 2023-04-24 12:34:52 +08:00
Kevin Wan
7568674b2b chore: use %q instead of %s for unmarshaling (#3163) 2023-04-24 12:11:12 +08:00
Kevin Wan
3da740b7fc chore: remove unnecessary code (#3161) 2023-04-23 22:58:22 +08:00
chen quan
ce4eb6ed61 fix: fixed #2945 (#2953)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-04-23 14:22:03 +00:00
MarkJoyMa
9970ff55cd feat: refactor gateway code (#3160) 2023-04-23 22:05:10 +08:00
MarkJoyMa
d10740f871 feat: inheritance rewrite error prompt is more friendly (#3156) 2023-04-23 12:56:54 +00:00
Kevin Wan
027193dc99 chore: refactor gateway (#3157) 2023-04-22 23:25:51 +08:00
Kevin Wan
de1e0f2410 Update readme-cn.md 2023-04-22 22:36:55 +08:00
Kevin Wan
062073ce58 chore: update codecov to ignore mock files (#3155) 2023-04-22 14:07:15 +08:00
MarkJoyMa
e20b02f311 gateway: open timeout function cabinet (#3047)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2023-04-22 05:23:47 +00:00
Kevin Wan
02357d2616 chore: make error message more readable (#3154) 2023-04-22 13:03:59 +08:00
guangwu
489d69f779 Add debug message on unmarshal errors (#3153) 2023-04-21 13:12:02 +00:00
Kevin Wan
117611a170 fix: default value for keepalive set to 0 (#3152) 2023-04-21 03:15:05 +00:00
Kevin Wan
0a46ad7ac1 chore: fix go 1.18 dep problem (#3149) 2023-04-21 01:01:17 +08:00
Liam Hao
bf905eaff3 fix default grpc-gateway connect timeout (#3142) 2023-04-20 15:13:13 +00:00
dependabot[bot]
88cb35e3d5 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.30.1 to 2.30.2 (#3144)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-20 22:59:57 +08:00
fondoger
078825b4eb [dart-gen] Support Null-safe and omitempty json tag (#3134) 2023-04-17 05:06:52 +00:00
Kevin Wan
bbfce6abe9 chore: refactor max/min in fx (#3135) 2023-04-16 23:40:30 +08:00
suplalalala
0d11ce03a8 feature: two evaluate operate func addtion in package stream (#3129)
Co-authored-by: Riven <Riven.chen@hairobotics.com>
2023-04-16 15:14:25 +00:00
dependabot[bot]
757ed19dc5 chore(deps): bump k8s.io/apimachinery from 0.26.3 to 0.27.1 (#3131)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-15 22:29:46 +08:00
dependabot[bot]
c5fd074aac chore(deps): bump github.com/prometheus/client_golang from 1.14.0 to 1.15.0 (#3122)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-15 22:01:02 +08:00
LiKe
8fa0bd1f1c fix(goctl): test field (#3114) 2023-04-15 21:46:37 +08:00
dependabot[bot]
ede19a89ec chore(deps): bump go.etcd.io/etcd/client/v3 from 3.5.7 to 3.5.8 (#3123)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-15 21:24:11 +08:00
anqiansong
73664b92f0 fix(goctl): missing rpc --style flag (#3112) 2023-04-10 09:48:48 +00:00
Kevin Wan
8d9c2fa22a chore: update go-zero version in goctl (#3109) 2023-04-08 23:24:29 +08:00
cong
22fad4bb9c feat(trace): add trace test helpers (#3108) 2023-04-08 14:52:25 +00:00
lchjczw
189e9bd9da Fix the problem of package name for generated kt code. (#3082)
Co-authored-by: 李春华 <lichunhua@threesoft.cn>
2023-04-08 14:46:34 +00:00
Kevin Wan
98c9b5928a refactor: simplify zrpc stat config (#3107) 2023-04-08 14:45:05 +00:00
Kevin Wan
e13fd62d38 chore: coding style (#3106)
Co-authored-by: cong <zhangcong1992@gmail.com>
2023-04-08 21:37:53 +08:00
李登富
ffacae89eb fix:multipule flag client go package name (#3104)
Co-authored-by: lidengfu <lidengfu@excean.com>
2023-04-08 21:36:45 +08:00
cong
49135fe25e refactor(zrpc): prefer static config for zrpc server statinterceptor (#3105) 2023-04-08 13:03:32 +00:00
MarkJoyMa
2e6402f4b5 fix: fillDefault, handling of nested structs (#3072) 2023-04-08 11:43:13 +00:00
Kevin Wan
07f03ebd0c fix: should not conflict on lower members (#3095) 2023-04-08 14:47:57 +08:00
dependabot[bot]
92f2676afc chore(deps): bump golang.org/x/text from 0.8.0 to 0.9.0 in /tools/goctl (#3101) 2023-04-08 13:28:01 +08:00
dependabot[bot]
1807305e6d chore(deps): bump golang.org/x/net from 0.8.0 to 0.9.0 (#3100) 2023-04-08 13:27:14 +08:00
dependabot[bot]
38a97d4531 chore(deps): bump github.com/spf13/cobra from 1.6.1 to 1.7.0 in /tools/goctl (#3098)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-07 20:41:03 +08:00
dependabot[bot]
b9f98ecc4a chore(deps): bump go.mongodb.org/mongo-driver from 1.11.3 to 1.11.4 (#3097)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-07 20:22:51 +08:00
dependabot[bot]
1dc222f4b2 chore(deps): bump golang.org/x/sys from 0.6.0 to 0.7.0 (#3096)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-05 22:33:40 +08:00
cong
a79b8de24d feat(bloom): bloom support Ctx API (#3089) 2023-04-03 14:37:04 +00:00
cong
5da8a93c75 feat(redis): add ScriptRun API and migrate EvalCtx to ScriptRun for limit, lock and bloom (#3087) 2023-04-02 11:28:25 +08:00
cong
b49fc81618 refactor(redistest): simplify redistest.CreateRedis API (#3086) 2023-04-01 05:02:21 +00:00
cong
6a692453dc chore(ci): fail lint ci when gofmt or tidy check failed (#3085) 2023-04-01 03:41:17 +00:00
Kevin Wan
8d0cceb80c chore: add more tests (#3084) 2023-03-31 22:33:43 +08:00
heyehang
e06abf4f6f fixbug:superfluous response.WriteHeader (#3083) 2023-03-31 13:05:29 +00:00
Kevin Wan
ee555a85da chore: coding style (#3080) 2023-03-31 02:13:48 +00:00
anqiansong
1904af2323 feat(goctl): Support gateway sample generation (#3049) 2023-03-29 09:06:23 +00:00
cong
95b85336d6 refactor(redis): add NonBlock config, disable redis ping by default (#3073) 2023-03-29 02:28:12 +00:00
guoguangwu
ca4ce7bce8 fix : misspelled word (#3075) 2023-03-29 02:25:26 +00:00
Kevin Wan
9065eb90d9 chore: coding style (#3074) 2023-03-28 16:34:22 +00:00
anqiansong
50bc361430 feat(goctl): Add api parser (#2585) 2023-03-28 15:45:26 +00:00
Kevin Wan
455a6c8f97 Update readme-cn.md, add company users. 2023-03-27 12:22:32 +08:00
Kevin Wan
04434646eb chore: refactor zrpc setup (#3064) 2023-03-25 20:40:21 +08:00
dependabot[bot]
992a56e90b chore(deps): bump google.golang.org/grpc from 1.53.0 to 1.54.0 in /tools/goctl (#3061)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-24 16:14:44 +08:00
dependabot[bot]
ed4d5e5813 chore(deps): bump google.golang.org/grpc from 1.53.0 to 1.54.0 (#3060)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-24 11:44:26 +08:00
dependabot[bot]
fe85e7cb42 chore(deps): bump k8s.io/client-go from 0.26.2 to 0.26.3 (#3052)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-24 10:16:45 +08:00
Kevin Wan
9c6b516bb8 fix: #3058 (#3059) 2023-03-23 23:45:57 +08:00
dependabot[bot]
2e9063a9a1 chore(deps): bump go.mongodb.org/mongo-driver from 1.11.2 to 1.11.3 (#3054)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-23 22:46:50 +08:00
dependabot[bot]
c3648be533 chore(deps): bump go.uber.org/automaxprocs from 1.5.1 to 1.5.2 (#3051)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-23 22:23:47 +08:00
Kevin Wan
0ab06f62ca chore: add more tests (#3045) 2023-03-19 23:56:36 +08:00
Kevin Wan
6170d7b790 feat: rest validation on http requests (#3041) 2023-03-19 12:04:18 +00:00
cong
18d163c4f7 fix(executors): periodicalexecutor should handle crash correctly (#3043) 2023-03-18 15:04:15 +00:00
Snake
a561048d59 Fix bug: replace int and float with num type in dart (#3042)
Co-authored-by: zhoumingji <zhoumingji@cmsr.chinamobile.com>
2023-03-18 14:49:22 +00:00
dependabot[bot]
7a647ca40c chore(deps): bump google.golang.org/protobuf from 1.29.1 to 1.30.0 in /tools/goctl (#3037)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-17 20:40:36 +08:00
dependabot[bot]
3f6f14f976 chore(deps): bump google.golang.org/protobuf from 1.29.1 to 1.30.0 (#3036)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-17 20:25:43 +08:00
dependabot[bot]
a78d57bebd chore(deps): bump google.golang.org/protobuf from 1.28.2-0.20220831092852-f930b1dc76e8 to 1.29.1 in /tools/goctl (#3029)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-16 00:20:18 +08:00
dependabot[bot]
74452eb7b5 chore(deps): bump golang.org/x/text from 0.7.0 to 0.8.0 in /tools/goctl (#3028)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 23:56:58 +08:00
dependabot[bot]
a9e364a01a chore(deps): bump golang.org/x/net from 0.7.0 to 0.8.0 (#3027)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 23:32:23 +08:00
chen quan
29c2e20b41 chore(action): upgrade go 1.18 version in release (#3032) 2023-03-15 12:53:04 +00:00
dependabot[bot]
42c146bcbd chore(deps): bump google.golang.org/protobuf from 1.29.0 to 1.29.1 (#3026)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 11:30:19 +08:00
dependabot[bot]
b61e364458 chore(deps): bump github.com/zeromicro/go-zero from 1.4.4 to 1.5.0 in /tools/goctl (#3023) 2023-03-14 08:48:42 +08:00
dependabot[bot]
18a4dcb79f chore(deps): bump github.com/fatih/color from 1.14.1 to 1.15.0 (#3022) 2023-03-14 08:47:44 +08:00
Kevin Wan
60a13f1e53 chore: add more tests (#3018) 2023-03-12 20:42:50 +08:00
sniperwzq
3e093bf34e defines the method to customize http server (#2171) 2023-03-11 23:15:00 +08:00
Kevin Wan
211b9498ef chore: add more tests (#3016) 2023-03-11 22:22:39 +08:00
Kevin Wan
cca45be3c5 chore: refactor orm code (#3015) 2023-03-11 18:03:20 +08:00
YK.xiong
e735915d89 fix QueryRowsPartial getTaggedFieldValueMap func (#2884)
Co-authored-by: yongkun.xiong <weilone@vip.qq.com>
2023-03-11 07:28:09 +00:00
Kevin Wan
f77e2c9cfa chore: add more tests (#3014) 2023-03-11 14:57:56 +08:00
Shyunn
544aa7c432 Added zrpc server custom serverID for custom registration Key when the service is registered on ETCD. (#3008) 2023-03-11 14:34:28 +08:00
Kevin Wan
4cef2b412c fix: avoid unmarshal panic with incorrect map keys #3002 (#3013) 2023-03-11 07:53:57 +08:00
Kevin Wan
123c61ad12 chore: update readme (#3011) 2023-03-10 22:18:48 +08:00
Kevin Wan
fbf129d535 chore: add more tests (#3010) 2023-03-10 21:56:19 +08:00
Kevin Wan
c8a17a97be chore: add more tests (#3009) 2023-03-10 20:48:10 +08:00
Kevin Wan
3a493cd6a6 chore: add more tests (#3006) 2023-03-10 17:36:39 +08:00
Kevin Wan
7a0c04bc21 feat: unique redis addrs and trim spaces (#3004) 2023-03-10 16:09:07 +08:00
iyyzh
3c9fe0b381 init postgresql err (#3003) 2023-03-10 05:58:07 +00:00
文豆芽
f8b2dc8c9f reids cluster bug (#2986)
Co-authored-by: shaocongcong <shao.congcong@yalla.live>
2023-03-09 21:37:34 +08:00
taobig
37cb00d789 Export cache.Option param to NewXXXModel() (#2995) 2023-03-09 13:34:58 +00:00
dependabot[bot]
e3e7bc736b chore(deps): bump google.golang.org/protobuf from 1.28.2-0.20230222093303-bc1253ad3743 to 1.29.0 (#2993)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-09 19:56:14 +08:00
dependabot[bot]
fafbee24b8 chore(deps): bump github.com/golang/protobuf from 1.5.2 to 1.5.3 (#2992)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-09 16:24:37 +08:00
dependabot[bot]
8ec29d29ce chore(deps): bump golang.org/x/sys from 0.5.0 to 0.6.0 (#2983)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-09 15:23:02 +08:00
fabio
cb7f3e8a17 feat(redis):add LpopCount,RpopCount (#2990) 2023-03-09 05:45:45 +00:00
dependabot[bot]
03391b48ca chore(deps): bump github.com/alicebob/miniredis/v2 from 2.30.0 to 2.30.1 (#2991)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-09 13:34:48 +08:00
dependabot[bot]
d0dedb0624 chore(deps): bump github.com/jhump/protoreflect from 1.15.0 to 1.15.1 (#2982)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-08 23:55:43 +08:00
Snake
e136deb3a7 Update goclt dart gen: Add scheme config and content-type header (#2987)
Co-authored-by: zhoumingji <zhoumingji@cmsr.chinamobile.com>
2023-03-08 15:40:55 +00:00
anqiansong
a2592a17e9 format code 2023-03-08 15:32:08 +00:00
anqiansong
05abf4a2ff fix typo 2023-03-08 15:32:08 +00:00
anqiansong
d40000d4b9 fix typo 2023-03-08 15:32:08 +00:00
anqiansong
4620924105 Fix typo 2023-03-08 15:32:08 +00:00
Kevin Wan
a05fe7bf0a chore: remove optional in redis config (#2979) 2023-03-07 14:58:04 +08:00
kevin
dd347e96b0 chore: add comments 2023-03-07 01:27:41 +00:00
kevin
a972f400c6 fix: test failure 2023-03-07 01:27:41 +00:00
kevin.wan
fb7664a764 fix: config map with json tag 2023-03-07 01:27:41 +00:00
Kevin Wan
7d5d7d9085 chore: clear errors on conf conflict keys (#2972)
Co-authored-by: kevin.wan <kevin.wan@yijinin.com>
2023-03-06 12:45:17 +08:00
Kevin Wan
9911c11e9c Update FUNDING.yml 2023-03-05 22:47:17 +08:00
Kevin Wan
0d5a68869d fix: gateway conf doesn't work (#2968) 2023-03-05 22:19:58 +08:00
Kevin Wan
d9d79e930d Merge pull request from GHSA-fgxv-gw55-r5fq
* fix: Authorization Bypass Through User-Controlled Key

* chore: add not safe domain test
2023-03-04 23:34:11 +08:00
442 changed files with 24667 additions and 5075 deletions

View File

@@ -1,6 +1,13 @@
coverage:
status:
patch: true
project: false # disabled because project coverage is not stable
comment:
layout: "flags, files"
behavior: once
require_changes: true
ignore:
- "tools"
- "tools"
- "**/mock"
- "**/*_mock.go"
- "**/*test"

2
.github/FUNDING.yml vendored
View File

@@ -10,4 +10,4 @@ liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # https://gitee.com/kevwan/static/raw/master/images/sponsor.jpg
ethereum: 0x5052b7f6B937B02563996D23feb69b38D06Ca150 | kevwan
ethereum: # 0x5052b7f6B937B02563996D23feb69b38D06Ca150 | kevwan

View File

@@ -35,7 +35,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -12,12 +12,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Go 1.x
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: ^1.18
go-version: 1.19
check-latest: true
cache: true
id: go
@@ -29,8 +29,12 @@ jobs:
- name: Lint
run: |
go vet -stdmethods=false $(go list ./...)
go install mvdan.cc/gofumpt@latest
test -z "$(gofumpt -l -extra .)" || echo "Please run 'gofumpt -l -w -extra .'"
go mod tidy
if ! test -z "$(git status --porcelain)"; then
echo "Please run 'go mod tidy'"
exit 1
fi
- name: Test
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
@@ -43,13 +47,13 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout codebase
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Go 1.x
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
# use 1.18 to guarantee Go 1.18 compatibility
go-version: 1.18
# use 1.19 to guarantee Go 1.19 compatibility
go-version: 1.19
check-latest: true
cache: true
@@ -57,5 +61,5 @@ jobs:
run: |
go mod verify
go mod download
go test -v -race ./...
go test ./...
cd tools/goctl && go build -v goctl.go

View File

@@ -7,7 +7,7 @@ jobs:
close-issues:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v6
- uses: actions/stale@v8
with:
days-before-issue-stale: 365
days-before-issue-close: 90

View File

@@ -16,13 +16,13 @@ jobs:
- goarch: "386"
goos: darwin
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: zeromicro/go-zero-release-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: "https://dl.google.com/go/go1.17.5.linux-amd64.tar.gz"
goversion: "https://dl.google.com/go/go1.19.13.linux-amd64.tar.gz"
project_path: "tools/goctl"
binary_name: "goctl"
extra_files: tools/goctl/readme.md tools/goctl/readme-cn.md

View File

@@ -5,7 +5,7 @@ jobs:
name: runner / staticcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: reviewdog/action-staticcheck@v1
with:
github_token: ${{ secrets.github_token }}

4
.gitignore vendored
View File

@@ -12,11 +12,13 @@
# ignore
**/.idea
**/.vscode
**/.DS_Store
**/logs
**/adhoc
**/coverage.txt
# for test purpose
**/adhoc
go.work
go.work.sum

View File

@@ -1,102 +1,76 @@
# Contributing
# 🚀 Contributing to go-zero
Welcome to go-zero!
Welcome to the go-zero community! We're thrilled to have you here. Contributing to our project is a fantastic way to be a part of the go-zero journey. Let's make this guide exciting and fun!
- [Before you get started](#before-you-get-started)
- [Code of Conduct](#code-of-conduct)
- [Community Expectations](#community-expectations)
- [Getting started](#getting-started)
- [Your First Contribution](#your-first-contribution)
- [Find something to work on](#find-something-to-work-on)
- [Find a good first topic](#find-a-good-first-topic)
- [Work on an Issue](#work-on-an-issue)
- [File an Issue](#file-an-issue)
- [Contributor Workflow](#contributor-workflow)
- [Creating Pull Requests](#creating-pull-requests)
- [Code Review](#code-review)
- [Testing](#testing)
## 📜 Before You Dive In
# Before you get started
### 🤝 Code of Conduct
## Code of Conduct
Let's start on the right foot. Please take a moment to read and embrace our [Code of Conduct](/code-of-conduct.md). We're all about creating a welcoming and respectful environment.
Please make sure to read and observe our [Code of Conduct](/code-of-conduct.md).
### 🌟 Community Expectations
## Community Expectations
At go-zero, we're like a close-knit family, and we believe in creating a healthy, friendly, and productive atmosphere. It's all about sharing knowledge and building amazing things together.
go-zero is a community project driven by its community which strives to promote a healthy, friendly and productive environment.
go-zero is a web and rpc framework written in Go. It's born to ensure the stability of the busy sites with resilient design. Builtin goctl greatly improves the development productivity.
## 🚀 Getting Started
# Getting started
Get your adventure rolling! Here's how to begin:
- Fork the repository on GitHub.
- Make your changes on your fork repository.
- Submit a PR.
1. 🍴 **Fork the Repository**: Head over to the GitHub repository and fork it to your own space.
2. 🛠️ **Make Your Magic**: Work your magic in your forked repository. Create new features, squash bugs, or improve documentation - it's your world to conquer!
# Your First Contribution
3. 🚀 **Submit a PR (Pull Request)**: When you're ready to unveil your creation, submit a Pull Request. We can't wait to see your awesome work!
We will help you to contribute in different areas like filing issues, developing features, fixing critical bugs and
getting your work reviewed and merged.
## 🌟 Your First Contribution
If you have questions about the development process,
feel free to [file an issue](https://github.com/zeromicro/go-zero/issues/new/choose).
We're here to guide you on your quest to become a go-zero contributor. Whether you want to file issues, develop features, or tame some critical bugs, we've got you covered.
## Find something to work on
If you have questions or need guidance at any stage, don't hesitate to [open an issue](https://github.com/zeromicro/go-zero/issues/new/choose).
We are always in need of help, be it fixing documentation, reporting bugs or writing some code.
Look at places where you feel best coding practices aren't followed, code refactoring is needed or tests are missing.
Here is how you get started.
## 🔍 Find Something to Work On
### Find a good first topic
Ready to dive into the action? There are several ways to contribute:
[go-zero](https://github.com/zeromicro/go-zero) has beginner-friendly issues that provide a good first issue.
For example, [go-zero](https://github.com/zeromicro/go-zero) has
[help wanted](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) and
[good first issue](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
labels for issues that should not need deep knowledge of the system.
We can help new contributors who wish to work on such issues.
### 💼 Find a Good First Topic
Another good way to contribute is to find a documentation improvement, such as a missing/broken link.
Please see [Contributing](#contributing) below for the workflow.
Discover easy-entry issues labeled as [help wanted](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) or [good first issue](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). These issues are perfect for newcomers and don't require deep knowledge of the system. We're here to assist you with these tasks.
#### Work on an issue
### 🪄 Work on an Issue
When you are willing to take on an issue, just reply on the issue. The maintainer will assign it to you.
Once you've picked an issue that excites you, let us know by commenting on it. Our maintainers will assign it to you, and you can embark on your mission!
### File an Issue
### 📢 File an Issue
While we encourage everyone to contribute code, it is also appreciated when someone reports an issue.
Reporting an issue is just as valuable as code contributions. If you discover a problem, don't hesitate to [open an issue](https://github.com/zeromicro/go-zero/issues/new/choose). Be sure to follow our guidelines when submitting an issue.
Please follow the prompted submission guidelines while opening an issue.
## 🎯 Contributor Workflow
# Contributor Workflow
Here's a rough guide to your contributor journey:
Please do not ever hesitate to ask a question or send a pull request.
1. 🌱 Create a New Branch: Start by creating a topic branch, usually based on the 'master' branch. This is where your contribution will grow.
This is a rough outline of what a contributor's workflow looks like:
2. 💡 Make Commits: Commit your work in logical units. Each commit should tell a story.
- Create a topic branch from where to base the contribution. This is usually master.
- Make commits of logical units.
- Push changes in a topic branch to a personal fork of the repository.
- Submit a pull request to [go-zero](https://github.com/zeromicro/go-zero).
3. 🚀 Push Changes: Push the changes in your topic branch to your personal fork of the repository.
## Creating Pull Requests
4. 📦 Submit a Pull Request: When your creation is complete, submit a Pull Request to the [go-zero repository](https://github.com/zeromicro/go-zero).
Pull requests are often called simply "PR".
go-zero generally follows the standard [github pull request](https://help.github.com/articles/about-pull-requests/) process.
To submit a proposed change, please develop the code/fix and add new test cases.
After that, run these local verifications before submitting pull request to predict the pass or
fail of continuous integration.
## 🌠 Creating Pull Requests
* Format the code with `gofmt`
* Run the test with data race enabled `go test -race ./...`
Pull Requests (PRs) are your way of making a grand entrance with your contribution. Here's how to do it:
## Code Review
- 💼 Format Your Code: Ensure your code is beautifully formatted with `gofmt`.
- 🏃 Run Tests: Verify that your changes pass all the tests, including data race tests. Run `go test -race ./...` for the ultimate validation.
To make it easier for your PR to receive reviews, consider the reviewers will need you to:
## 👁️‍🗨️ Code Review
* follow [good coding guidelines](https://github.com/golang/go/wiki/CodeReviewComments).
* write [good commit messages](https://chris.beams.io/posts/git-commit/).
* break large changes into a logical series of smaller patches which individually make easily understandable changes, and in aggregate solve a broader issue.
Getting your PR reviewed is the final step before your contribution becomes part of go-zero's magical world. To make the process smooth, keep these things in mind:
- 🧙‍♀️ Follow Good Coding Practices: Stick to [good coding guidelines](https://github.com/golang/go/wiki/CodeReviewComments).
- 📝 Write Awesome Commit Messages: Craft [impressive commit messages](https://chris.beams.io/posts/git-commit/) - they're like spells in the wizard's book!
- 🔍 Break It Down: For larger changes, consider breaking them into a series of smaller, logical patches. Each patch should make an understandable and meaningful improvement.
Congratulations on your contribution journey! We're thrilled to have you as part of our go-zero community. Let's make amazing things together! 🌟
Now, go out there and start your adventure! If you have any more magical ideas to enhance this guide, please share them. 🔥

View File

@@ -1,6 +1,7 @@
package bloom
import (
"context"
"errors"
"strconv"
@@ -8,28 +9,29 @@ import (
"github.com/zeromicro/go-zero/core/stores/redis"
)
const (
// for detailed error rate table, see http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html
// maps as k in the error rate table
maps = 14
setScript = `
// for detailed error rate table, see http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html
// maps as k in the error rate table
const maps = 14
var (
// ErrTooLargeOffset indicates the offset is too large in bitset.
ErrTooLargeOffset = errors.New("too large offset")
setScript = redis.NewScript(`
for _, offset in ipairs(ARGV) do
redis.call("setbit", KEYS[1], offset, 1)
end
`
testScript = `
`)
testScript = redis.NewScript(`
for _, offset in ipairs(ARGV) do
if tonumber(redis.call("getbit", KEYS[1], offset)) == 0 then
return false
end
end
return true
`
`)
)
// ErrTooLargeOffset indicates the offset is too large in bitset.
var ErrTooLargeOffset = errors.New("too large offset")
type (
// A Filter is a bloom filter.
Filter struct {
@@ -38,8 +40,8 @@ type (
}
bitSetProvider interface {
check([]uint) (bool, error)
set([]uint) error
check(ctx context.Context, offsets []uint) (bool, error)
set(ctx context.Context, offsets []uint) error
}
)
@@ -58,14 +60,24 @@ func New(store *redis.Redis, key string, bits uint) *Filter {
// Add adds data into f.
func (f *Filter) Add(data []byte) error {
return f.AddCtx(context.Background(), data)
}
// AddCtx adds data into f with context.
func (f *Filter) AddCtx(ctx context.Context, data []byte) error {
locations := f.getLocations(data)
return f.bitSet.set(locations)
return f.bitSet.set(ctx, locations)
}
// Exists checks if data is in f.
func (f *Filter) Exists(data []byte) (bool, error) {
return f.ExistsCtx(context.Background(), data)
}
// ExistsCtx checks if data is in f with context.
func (f *Filter) ExistsCtx(ctx context.Context, data []byte) (bool, error) {
locations := f.getLocations(data)
isSet, err := f.bitSet.check(locations)
isSet, err := f.bitSet.check(ctx, locations)
if err != nil {
return false, err
}
@@ -111,13 +123,13 @@ func (r *redisBitSet) buildOffsetArgs(offsets []uint) ([]string, error) {
return args, nil
}
func (r *redisBitSet) check(offsets []uint) (bool, error) {
func (r *redisBitSet) check(ctx context.Context, offsets []uint) (bool, error) {
args, err := r.buildOffsetArgs(offsets)
if err != nil {
return false, err
}
resp, err := r.store.Eval(testScript, []string{r.key}, args)
resp, err := r.store.ScriptRunCtx(ctx, testScript, []string{r.key}, args)
if err == redis.Nil {
return false, nil
} else if err != nil {
@@ -132,22 +144,24 @@ func (r *redisBitSet) check(offsets []uint) (bool, error) {
return exists == 1, nil
}
// del only use for testing.
func (r *redisBitSet) del() error {
_, err := r.store.Del(r.key)
return err
}
// expire only use for testing.
func (r *redisBitSet) expire(seconds int) error {
return r.store.Expire(r.key, seconds)
}
func (r *redisBitSet) set(offsets []uint) error {
func (r *redisBitSet) set(ctx context.Context, offsets []uint) error {
args, err := r.buildOffsetArgs(offsets)
if err != nil {
return err
}
_, err = r.store.Eval(setScript, []string{r.key}, args)
_, err = r.store.ScriptRunCtx(ctx, setScript, []string{r.key}, args)
if err == redis.Nil {
return nil
}

View File

@@ -1,30 +1,31 @@
package bloom
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/redis/redistest"
)
func TestRedisBitSet_New_Set_Test(t *testing.T) {
store, clean, err := redistest.CreateRedis()
assert.Nil(t, err)
defer clean()
store := redistest.CreateRedis(t)
ctx := context.Background()
bitSet := newRedisBitSet(store, "test_key", 1024)
isSetBefore, err := bitSet.check([]uint{0})
isSetBefore, err := bitSet.check(ctx, []uint{0})
if err != nil {
t.Fatal(err)
}
if isSetBefore {
t.Fatal("Bit should not be set")
}
err = bitSet.set([]uint{512})
err = bitSet.set(ctx, []uint{512})
if err != nil {
t.Fatal(err)
}
isSetAfter, err := bitSet.check([]uint{512})
isSetAfter, err := bitSet.check(ctx, []uint{512})
if err != nil {
t.Fatal(err)
}
@@ -42,9 +43,7 @@ func TestRedisBitSet_New_Set_Test(t *testing.T) {
}
func TestRedisBitSet_Add(t *testing.T) {
store, clean, err := redistest.CreateRedis()
assert.Nil(t, err)
defer clean()
store := redistest.CreateRedis(t)
filter := New(store, "test_key", 64)
assert.Nil(t, filter.Add([]byte("hello")))
@@ -53,3 +52,51 @@ func TestRedisBitSet_Add(t *testing.T) {
assert.Nil(t, err)
assert.True(t, ok)
}
func TestFilter_Exists(t *testing.T) {
store, clean := redistest.CreateRedisWithClean(t)
rbs := New(store, "test", 64)
_, err := rbs.Exists([]byte{0, 1, 2})
assert.NoError(t, err)
clean()
rbs = New(store, "test", 64)
_, err = rbs.Exists([]byte{0, 1, 2})
assert.Error(t, err)
}
func TestRedisBitSet_check(t *testing.T) {
store, clean := redistest.CreateRedisWithClean(t)
ctx := context.Background()
rbs := newRedisBitSet(store, "test", 0)
assert.Error(t, rbs.set(ctx, []uint{0, 1, 2}))
_, err := rbs.check(ctx, []uint{0, 1, 2})
assert.Error(t, err)
rbs = newRedisBitSet(store, "test", 64)
_, err = rbs.check(ctx, []uint{0, 1, 2})
assert.NoError(t, err)
clean()
rbs = newRedisBitSet(store, "test", 64)
_, err = rbs.check(ctx, []uint{0, 1, 2})
assert.Error(t, err)
}
func TestRedisBitSet_set(t *testing.T) {
logx.Disable()
store, clean := redistest.CreateRedisWithClean(t)
ctx := context.Background()
rbs := newRedisBitSet(store, "test", 0)
assert.Error(t, rbs.set(ctx, []uint{0, 1, 2}))
rbs = newRedisBitSet(store, "test", 64)
assert.NoError(t, rbs.set(ctx, []uint{0, 1, 2}))
clean()
rbs = newRedisBitSet(store, "test", 64)
assert.Error(t, rbs.set(ctx, []uint{0, 1, 2}))
}

View File

@@ -46,7 +46,7 @@ type (
// DoWithAcceptable returns an error instantly if the Breaker rejects the request.
// If a panic occurs in the request, the Breaker handles it as an error
// and causes the same panic again.
// acceptable checks if it's a successful call, even if the err is not nil.
// acceptable checks if it's a successful call, even if the error is not nil.
DoWithAcceptable(req func() error, acceptable Acceptable) error
// DoWithFallback runs the given request if the Breaker accepts it.
@@ -59,7 +59,7 @@ type (
// DoWithFallbackAcceptable runs the fallback if the Breaker rejects the request.
// If a panic occurs in the request, the Breaker handles it as an error
// and causes the same panic again.
// acceptable checks if it's a successful call, even if the err is not nil.
// acceptable checks if it's a successful call, even if the error is not nil.
DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error
}
@@ -179,7 +179,7 @@ func (lt loggedThrottle) doReq(req func() error, fallback func(err error) error,
}
func (lt loggedThrottle) logError(err error) error {
if err == ErrServiceUnavailable {
if errors.Is(err, ErrServiceUnavailable) {
// if circuit open, not possible to have empty error window
stat.Report(fmt.Sprintf(
"proc(%s/%d), callee: %s, breaker is open and requests dropped\nlast errors:\n%s",

View File

@@ -59,7 +59,7 @@ func GetBreaker(name string) Breaker {
// NoBreakerFor disables the circuit breaker for the given name.
func NoBreakerFor(name string) {
lock.Lock()
breakers[name] = newNoOpBreaker()
breakers[name] = newNopBreaker()
lock.Unlock()
}

View File

@@ -30,7 +30,7 @@ func TestBreakersDoWithAcceptable(t *testing.T) {
assert.Equal(t, errDummy, GetBreaker("anyone").DoWithAcceptable(func() error {
return errDummy
}, func(err error) bool {
return err == nil || err == errDummy
return err == nil || errors.Is(err, errDummy)
}))
}
verify(t, func() bool {
@@ -45,12 +45,12 @@ func TestBreakersDoWithAcceptable(t *testing.T) {
}, func(err error) bool {
return err == nil
})
assert.True(t, err == errDummy || err == ErrServiceUnavailable)
assert.True(t, errors.Is(err, errDummy) || errors.Is(err, ErrServiceUnavailable))
}
verify(t, func() bool {
return ErrServiceUnavailable == Do("another", func() error {
return errors.Is(Do("another", func() error {
return nil
})
}), ErrServiceUnavailable)
})
}
@@ -75,12 +75,12 @@ func TestBreakersFallback(t *testing.T) {
}, func(err error) error {
return nil
})
assert.True(t, err == nil || err == errDummy)
assert.True(t, err == nil || errors.Is(err, errDummy))
}
verify(t, func() bool {
return ErrServiceUnavailable == Do("fallback", func() error {
return errors.Is(Do("fallback", func() error {
return nil
})
}), ErrServiceUnavailable)
})
}
@@ -94,12 +94,12 @@ func TestBreakersAcceptableFallback(t *testing.T) {
}, func(err error) bool {
return err == nil
})
assert.True(t, err == nil || err == errDummy)
assert.True(t, err == nil || errors.Is(err, errDummy))
}
verify(t, func() bool {
return ErrServiceUnavailable == Do("acceptablefallback", func() error {
return errors.Is(Do("acceptablefallback", func() error {
return nil
})
}), ErrServiceUnavailable)
})
}

View File

@@ -95,7 +95,7 @@ func TestGoogleBreakerAcceptable(t *testing.T) {
assert.Equal(t, errAcceptable, b.doReq(func() error {
return errAcceptable
}, nil, func(err error) bool {
return err == errAcceptable
return errors.Is(err, errAcceptable)
}))
}
@@ -105,7 +105,7 @@ func TestGoogleBreakerNotAcceptable(t *testing.T) {
assert.Equal(t, errAcceptable, b.doReq(func() error {
return errAcceptable
}, nil, func(err error) bool {
return err != errAcceptable
return !errors.Is(err, errAcceptable)
}))
}

View File

@@ -1,34 +1,34 @@
package breaker
const noOpBreakerName = "nopBreaker"
const nopBreakerName = "nopBreaker"
type noOpBreaker struct{}
type nopBreaker struct{}
func newNoOpBreaker() Breaker {
return noOpBreaker{}
func newNopBreaker() Breaker {
return nopBreaker{}
}
func (b noOpBreaker) Name() string {
return noOpBreakerName
func (b nopBreaker) Name() string {
return nopBreakerName
}
func (b noOpBreaker) Allow() (Promise, error) {
func (b nopBreaker) Allow() (Promise, error) {
return nopPromise{}, nil
}
func (b noOpBreaker) Do(req func() error) error {
func (b nopBreaker) Do(req func() error) error {
return req()
}
func (b noOpBreaker) DoWithAcceptable(req func() error, _ Acceptable) error {
func (b nopBreaker) DoWithAcceptable(req func() error, _ Acceptable) error {
return req()
}
func (b noOpBreaker) DoWithFallback(req func() error, _ func(err error) error) error {
func (b nopBreaker) DoWithFallback(req func() error, _ func(err error) error) error {
return req()
}
func (b noOpBreaker) DoWithFallbackAcceptable(req func() error, _ func(err error) error,
func (b nopBreaker) DoWithFallbackAcceptable(req func() error, _ func(err error) error,
_ Acceptable) error {
return req()
}

View File

@@ -8,8 +8,8 @@ import (
)
func TestNopBreaker(t *testing.T) {
b := newNoOpBreaker()
assert.Equal(t, noOpBreakerName, b.Name())
b := newNopBreaker()
assert.Equal(t, nopBreakerName, b.Name())
p, err := b.Allow()
assert.Nil(t, err)
p.Accept()

View File

@@ -2,6 +2,8 @@ package codec
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"testing"
@@ -21,3 +23,45 @@ func TestGzip(t *testing.T) {
assert.True(t, len(bs) < buf.Len())
assert.Equal(t, buf.Bytes(), actual)
}
func TestGunzip(t *testing.T) {
tests := []struct {
name string
input []byte
expected []byte
expectedErr error
}{
{
name: "valid input",
input: func() []byte {
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
gz.Write([]byte("hello"))
gz.Close()
return buf.Bytes()
}(),
expected: []byte("hello"),
expectedErr: nil,
},
{
name: "invalid input",
input: []byte("invalid input"),
expected: nil,
expectedErr: gzip.ErrHeader,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := Gunzip(test.input)
if !bytes.Equal(result, test.expected) {
t.Errorf("unexpected result: %v", result)
}
if !errors.Is(err, test.expectedErr) {
t.Errorf("unexpected error: %v", err)
}
})
}
}

View File

@@ -2,6 +2,7 @@ package codec
import (
"encoding/base64"
"os"
"testing"
"github.com/stretchr/testify/assert"
@@ -41,6 +42,7 @@ func TestCryption(t *testing.T) {
file, err := fs.TempFilenameWithText(priKey)
assert.Nil(t, err)
defer os.Remove(file)
dec, err := NewRsaDecrypter(file)
assert.Nil(t, err)
actual, err := dec.Decrypt(ret)

View File

@@ -29,6 +29,8 @@ func NewSafeMap() *SafeMap {
// Del deletes the value with the given key from m.
func (m *SafeMap) Del(key any) {
m.lock.Lock()
defer m.lock.Unlock()
if _, ok := m.dirtyOld[key]; ok {
delete(m.dirtyOld, key)
m.deletionOld++
@@ -52,7 +54,6 @@ func (m *SafeMap) Del(key any) {
m.dirtyNew = make(map[any]any)
m.deletionNew = 0
}
m.lock.Unlock()
}
// Get gets the value with the given key from m.
@@ -89,6 +90,8 @@ func (m *SafeMap) Range(f func(key, val any) bool) {
// Set sets the value into m with the given key.
func (m *SafeMap) Set(key, value any) {
m.lock.Lock()
defer m.lock.Unlock()
if m.deletionOld <= maxDeletion {
if _, ok := m.dirtyNew[key]; ok {
delete(m.dirtyNew, key)
@@ -102,7 +105,6 @@ func (m *SafeMap) Set(key, value any) {
}
m.dirtyNew[key] = value
}
m.lock.Unlock()
}
// Size returns the size of m.

View File

@@ -147,3 +147,65 @@ func TestSafeMap_Range(t *testing.T) {
assert.Equal(t, m.dirtyNew, newMap.dirtyNew)
assert.Equal(t, m.dirtyOld, newMap.dirtyOld)
}
func TestSetManyTimes(t *testing.T) {
const iteration = maxDeletion * 2
m := NewSafeMap()
for i := 0; i < iteration; i++ {
m.Set(i, i)
if i%3 == 0 {
m.Del(i / 2)
}
}
var count int
m.Range(func(k, v any) bool {
count++
return count < maxDeletion/2
})
assert.Equal(t, maxDeletion/2, count)
for i := 0; i < iteration; i++ {
m.Set(i, i)
if i%3 == 0 {
m.Del(i / 2)
}
}
for i := 0; i < iteration; i++ {
m.Set(i, i)
if i%3 == 0 {
m.Del(i / 2)
}
}
for i := 0; i < iteration; i++ {
m.Set(i, i)
if i%3 == 0 {
m.Del(i / 2)
}
}
count = 0
m.Range(func(k, v any) bool {
count++
return count < maxDeletion
})
assert.Equal(t, maxDeletion, count)
}
func TestSetManyTimesNew(t *testing.T) {
m := NewSafeMap()
for i := 0; i < maxDeletion*3; i++ {
m.Set(i, i)
}
for i := 0; i < maxDeletion*2; i++ {
m.Del(i)
}
for i := 0; i < maxDeletion*3; i++ {
m.Set(i+maxDeletion*3, i+maxDeletion*3)
}
for i := 0; i < maxDeletion*2; i++ {
m.Del(i + maxDeletion*2)
}
for i := 0; i < maxDeletion-copyThreshold+1; i++ {
m.Del(i + maxDeletion*2)
}
assert.Equal(t, 0, len(m.dirtyNew))
}

View File

@@ -13,7 +13,10 @@ import (
"github.com/zeromicro/go-zero/internal/encoding"
)
const jsonTagKey = "json"
const (
jsonTagKey = "json"
jsonTagSep = ','
)
var (
fillDefaultUnmarshaler = mapping.NewUnmarshaler(jsonTagKey, mapping.WithDefault())
@@ -70,13 +73,13 @@ func LoadConfig(file string, v any, opts ...Option) error {
// LoadFromJsonBytes loads config into v from content json bytes.
func LoadFromJsonBytes(content []byte, v any) error {
info, err := buildFieldsInfo(reflect.TypeOf(v))
info, err := buildFieldsInfo(reflect.TypeOf(v), "")
if err != nil {
return err
}
var m map[string]any
if err := jsonx.Unmarshal(content, &m); err != nil {
if err = jsonx.Unmarshal(content, &m); err != nil {
return err
}
@@ -124,13 +127,13 @@ func MustLoad(path string, v any, opts ...Option) {
}
}
func addOrMergeFields(info *fieldInfo, key string, child *fieldInfo) error {
func addOrMergeFields(info *fieldInfo, key string, child *fieldInfo, fullName string) error {
if prev, ok := info.children[key]; ok {
if child.mapField != nil {
return newDupKeyError(key)
return newConflictKeyError(fullName)
}
if err := mergeFields(prev, key, child.children); err != nil {
if err := mergeFields(prev, key, child.children, fullName); err != nil {
return err
}
} else {
@@ -140,27 +143,27 @@ func addOrMergeFields(info *fieldInfo, key string, child *fieldInfo) error {
return nil
}
func buildAnonymousFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.Type) error {
func buildAnonymousFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.Type, fullName string) error {
switch ft.Kind() {
case reflect.Struct:
fields, err := buildFieldsInfo(ft)
fields, err := buildFieldsInfo(ft, fullName)
if err != nil {
return err
}
for k, v := range fields.children {
if err = addOrMergeFields(info, k, v); err != nil {
if err = addOrMergeFields(info, k, v, fullName); err != nil {
return err
}
}
case reflect.Map:
elemField, err := buildFieldsInfo(mapping.Deref(ft.Elem()))
elemField, err := buildFieldsInfo(mapping.Deref(ft.Elem()), fullName)
if err != nil {
return err
}
if _, ok := info.children[lowerCaseName]; ok {
return newDupKeyError(lowerCaseName)
return newConflictKeyError(fullName)
}
info.children[lowerCaseName] = &fieldInfo{
@@ -169,7 +172,7 @@ func buildAnonymousFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.T
}
default:
if _, ok := info.children[lowerCaseName]; ok {
return newDupKeyError(lowerCaseName)
return newConflictKeyError(fullName)
}
info.children[lowerCaseName] = &fieldInfo{
@@ -180,14 +183,14 @@ func buildAnonymousFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.T
return nil
}
func buildFieldsInfo(tp reflect.Type) (*fieldInfo, error) {
func buildFieldsInfo(tp reflect.Type, fullName string) (*fieldInfo, error) {
tp = mapping.Deref(tp)
switch tp.Kind() {
case reflect.Struct:
return buildStructFieldsInfo(tp)
return buildStructFieldsInfo(tp, fullName)
case reflect.Array, reflect.Slice:
return buildFieldsInfo(mapping.Deref(tp.Elem()))
return buildFieldsInfo(mapping.Deref(tp.Elem()), fullName)
case reflect.Chan, reflect.Func:
return nil, fmt.Errorf("unsupported type: %s", tp.Kind())
default:
@@ -197,23 +200,23 @@ func buildFieldsInfo(tp reflect.Type) (*fieldInfo, error) {
}
}
func buildNamedFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.Type) error {
func buildNamedFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.Type, fullName string) error {
var finfo *fieldInfo
var err error
switch ft.Kind() {
case reflect.Struct:
finfo, err = buildFieldsInfo(ft)
finfo, err = buildFieldsInfo(ft, fullName)
if err != nil {
return err
}
case reflect.Array, reflect.Slice:
finfo, err = buildFieldsInfo(ft.Elem())
finfo, err = buildFieldsInfo(ft.Elem(), fullName)
if err != nil {
return err
}
case reflect.Map:
elemInfo, err := buildFieldsInfo(mapping.Deref(ft.Elem()))
elemInfo, err := buildFieldsInfo(mapping.Deref(ft.Elem()), fullName)
if err != nil {
return err
}
@@ -223,31 +226,37 @@ func buildNamedFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.Type)
mapField: elemInfo,
}
default:
finfo, err = buildFieldsInfo(ft)
finfo, err = buildFieldsInfo(ft, fullName)
if err != nil {
return err
}
}
return addOrMergeFields(info, lowerCaseName, finfo)
return addOrMergeFields(info, lowerCaseName, finfo, fullName)
}
func buildStructFieldsInfo(tp reflect.Type) (*fieldInfo, error) {
func buildStructFieldsInfo(tp reflect.Type, fullName string) (*fieldInfo, error) {
info := &fieldInfo{
children: make(map[string]*fieldInfo),
}
for i := 0; i < tp.NumField(); i++ {
field := tp.Field(i)
name := field.Name
if !field.IsExported() {
continue
}
name := getTagName(field)
lowerCaseName := toLowerCase(name)
ft := mapping.Deref(field.Type)
// flatten anonymous fields
if field.Anonymous {
if err := buildAnonymousFieldInfo(info, lowerCaseName, ft); err != nil {
if err := buildAnonymousFieldInfo(info, lowerCaseName, ft,
getFullName(fullName, lowerCaseName)); err != nil {
return nil, err
}
} else if err := buildNamedFieldInfo(info, lowerCaseName, ft); err != nil {
} else if err := buildNamedFieldInfo(info, lowerCaseName, ft,
getFullName(fullName, lowerCaseName)); err != nil {
return nil, err
}
}
@@ -255,15 +264,32 @@ func buildStructFieldsInfo(tp reflect.Type) (*fieldInfo, error) {
return info, nil
}
func mergeFields(prev *fieldInfo, key string, children map[string]*fieldInfo) error {
// getTagName get the tag name of the given field, if no tag name, use file.Name.
// field.Name is returned on tags like `json:""` and `json:",optional"`.
func getTagName(field reflect.StructField) string {
if tag, ok := field.Tag.Lookup(jsonTagKey); ok {
if pos := strings.IndexByte(tag, jsonTagSep); pos >= 0 {
tag = tag[:pos]
}
tag = strings.TrimSpace(tag)
if len(tag) > 0 {
return tag
}
}
return field.Name
}
func mergeFields(prev *fieldInfo, key string, children map[string]*fieldInfo, fullName string) error {
if len(prev.children) == 0 || len(children) == 0 {
return newDupKeyError(key)
return newConflictKeyError(fullName)
}
// merge fields
for k, v := range children {
if _, ok := prev.children[k]; ok {
return newDupKeyError(k)
return newConflictKeyError(fullName)
}
prev.children[k] = v
@@ -314,14 +340,22 @@ func toLowerCaseKeyMap(m map[string]any, info *fieldInfo) map[string]any {
return res
}
type dupKeyError struct {
type conflictKeyError struct {
key string
}
func newDupKeyError(key string) dupKeyError {
return dupKeyError{key: key}
func newConflictKeyError(key string) conflictKeyError {
return conflictKeyError{key: key}
}
func (e dupKeyError) Error() string {
return fmt.Sprintf("duplicated key %s", e.key)
func (e conflictKeyError) Error() string {
return fmt.Sprintf("conflict key %s, pay attention to anonymous fields", e.key)
}
func getFullName(parent, child string) string {
if len(parent) == 0 {
return child
}
return strings.Join([]string{parent, child}, ".")
}

View File

@@ -2,6 +2,7 @@ package conf
import (
"os"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
@@ -9,7 +10,7 @@ import (
"github.com/zeromicro/go-zero/core/hash"
)
var dupErr dupKeyError
var dupErr conflictKeyError
func TestLoadConfig_notExists(t *testing.T) {
assert.NotNil(t, Load("not_a_file", nil))
@@ -34,11 +35,11 @@ func TestConfigJson(t *testing.T) {
"c": "${FOO}",
"d": "abcd!@#$112"
}`
t.Setenv("FOO", "2")
for _, test := range tests {
test := test
t.Run(test, func(t *testing.T) {
os.Setenv("FOO", "2")
defer os.Unsetenv("FOO")
tmpfile, err := createTempFile(test, text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
@@ -80,8 +81,7 @@ b = 1
c = "${FOO}"
d = "abcd!@#$112"
`
os.Setenv("FOO", "2")
defer os.Unsetenv("FOO")
t.Setenv("FOO", "2")
tmpfile, err := createTempFile(".toml", text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
@@ -123,6 +123,24 @@ d = "abcd"
}
}
func TestConfigWithLower(t *testing.T) {
text := `a = "foo"
b = 1
`
tmpfile, err := createTempFile(".toml", text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
var val struct {
A string `json:"a"`
b int
}
if assert.NoError(t, Load(tmpfile, &val)) {
assert.Equal(t, "foo", val.A)
assert.Equal(t, 0, val.b)
}
}
func TestConfigJsonCanonical(t *testing.T) {
text := []byte(`{"a": "foo", "B": "bar"}`)
@@ -188,8 +206,7 @@ b = 1
c = "${FOO}"
d = "abcd!@#112"
`
os.Setenv("FOO", "2")
defer os.Unsetenv("FOO")
t.Setenv("FOO", "2")
tmpfile, err := createTempFile(".toml", text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
@@ -220,11 +237,10 @@ func TestConfigJsonEnv(t *testing.T) {
"c": "${FOO}",
"d": "abcd!@#$a12 3"
}`
t.Setenv("FOO", "2")
for _, test := range tests {
test := test
t.Run(test, func(t *testing.T) {
os.Setenv("FOO", "2")
defer os.Unsetenv("FOO")
tmpfile, err := createTempFile(test, text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
@@ -672,7 +688,7 @@ func Test_FieldOverwrite(t *testing.T) {
input := []byte(`{"Name": "hello"}`)
err := LoadFromJsonBytes(input, val)
assert.ErrorAs(t, err, &dupErr)
assert.Equal(t, newDupKeyError("name").Error(), err.Error())
assert.Equal(t, newConflictKeyError("name").Error(), err.Error())
}
validate(&St1{})
@@ -715,7 +731,7 @@ func Test_FieldOverwrite(t *testing.T) {
input := []byte(`{"Name": "hello"}`)
err := LoadFromJsonBytes(input, val)
assert.ErrorAs(t, err, &dupErr)
assert.Equal(t, newDupKeyError("name").Error(), err.Error())
assert.Error(t, err)
}
validate(&St0{})
@@ -1022,22 +1038,22 @@ func TestLoadNamedFieldOverwritten(t *testing.T) {
})
}
func createTempFile(ext, text string) (string, error) {
tmpfile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
if err != nil {
return "", err
}
func TestLoadLowerMemberShouldNotConflict(t *testing.T) {
type (
Redis struct {
db uint
}
if err := os.WriteFile(tmpfile.Name(), []byte(text), os.ModeTemporary); err != nil {
return "", err
}
Config struct {
db uint
Redis
}
)
filename := tmpfile.Name()
if err = tmpfile.Close(); err != nil {
return "", err
}
return filename, nil
var c Config
assert.NoError(t, LoadFromJsonBytes([]byte(`{}`), &c))
assert.Zero(t, c.db)
assert.Zero(t, c.Redis.db)
}
func TestFillDefaultUnmarshal(t *testing.T) {
@@ -1079,7 +1095,7 @@ func TestFillDefaultUnmarshal(t *testing.T) {
assert.Equal(t, st.C, "c")
})
t.Run("has vaue", func(t *testing.T) {
t.Run("has value", func(t *testing.T) {
type St struct {
A string `json:",default=a"`
B string
@@ -1091,3 +1107,201 @@ func TestFillDefaultUnmarshal(t *testing.T) {
assert.Error(t, err)
})
}
func TestConfigWithJsonTag(t *testing.T) {
t.Run("map with value", func(t *testing.T) {
var input = []byte(`[Value]
[Value.first]
Email = "foo"
[Value.second]
Email = "bar"`)
type Value struct {
Email string
}
type Config struct {
ValueMap map[string]Value `json:"Value"`
}
var c Config
if assert.NoError(t, LoadFromTomlBytes(input, &c)) {
assert.Len(t, c.ValueMap, 2)
}
})
t.Run("map with ptr value", func(t *testing.T) {
var input = []byte(`[Value]
[Value.first]
Email = "foo"
[Value.second]
Email = "bar"`)
type Value struct {
Email string
}
type Config struct {
ValueMap map[string]*Value `json:"Value"`
}
var c Config
if assert.NoError(t, LoadFromTomlBytes(input, &c)) {
assert.Len(t, c.ValueMap, 2)
}
})
t.Run("map with optional", func(t *testing.T) {
var input = []byte(`[Value]
[Value.first]
Email = "foo"
[Value.second]
Email = "bar"`)
type Value struct {
Email string
}
type Config struct {
Value map[string]Value `json:",optional"`
}
var c Config
if assert.NoError(t, LoadFromTomlBytes(input, &c)) {
assert.Len(t, c.Value, 2)
}
})
t.Run("map with empty tag", func(t *testing.T) {
var input = []byte(`[Value]
[Value.first]
Email = "foo"
[Value.second]
Email = "bar"`)
type Value struct {
Email string
}
type Config struct {
Value map[string]Value `json:" "`
}
var c Config
if assert.NoError(t, LoadFromTomlBytes(input, &c)) {
assert.Len(t, c.Value, 2)
}
})
}
func Test_getFullName(t *testing.T) {
assert.Equal(t, "a.b", getFullName("a", "b"))
assert.Equal(t, "a", getFullName("", "a"))
}
func Test_buildFieldsInfo(t *testing.T) {
type ParentSt struct {
Name string
M map[string]int
}
tests := []struct {
name string
t reflect.Type
ok bool
containsKey string
}{
{
name: "normal",
t: reflect.TypeOf(struct{ A string }{}),
ok: true,
},
{
name: "struct anonymous",
t: reflect.TypeOf(struct {
ParentSt
Name string
}{}),
ok: false,
containsKey: newConflictKeyError("name").Error(),
},
{
name: "struct ptr anonymous",
t: reflect.TypeOf(struct {
*ParentSt
Name string
}{}),
ok: false,
containsKey: newConflictKeyError("name").Error(),
},
{
name: "more struct anonymous",
t: reflect.TypeOf(struct {
Value struct {
ParentSt
Name string
}
}{}),
ok: false,
containsKey: newConflictKeyError("value.name").Error(),
},
{
name: "map anonymous",
t: reflect.TypeOf(struct {
ParentSt
M string
}{}),
ok: false,
containsKey: newConflictKeyError("m").Error(),
},
{
name: "map more anonymous",
t: reflect.TypeOf(struct {
Value struct {
ParentSt
M string
}
}{}),
ok: false,
containsKey: newConflictKeyError("value.m").Error(),
},
{
name: "struct slice anonymous",
t: reflect.TypeOf([]struct {
ParentSt
Name string
}{}),
ok: false,
containsKey: newConflictKeyError("name").Error(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := buildFieldsInfo(tt.t, "")
if tt.ok {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.Equal(t, err.Error(), tt.containsKey)
}
})
}
}
func createTempFile(ext, text string) (string, error) {
tmpFile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
if err != nil {
return "", err
}
if err := os.WriteFile(tmpFile.Name(), []byte(text), os.ModeTemporary); err != nil {
return "", err
}
filename := tmpFile.Name()
if err = tmpFile.Close(); err != nil {
return "", err
}
return filename, nil
}

View File

@@ -45,8 +45,7 @@ func TestPropertiesEnv(t *testing.T) {
assert.Nil(t, err)
defer os.Remove(tmpfile)
os.Setenv("FOO", "2")
defer os.Unsetenv("FOO")
t.Setenv("FOO", "2")
props, err := LoadProperties(tmpfile, UseEnv())
assert.Nil(t, err)

View File

@@ -13,6 +13,7 @@ var (
type EtcdConf struct {
Hosts []string
Key string
ID int64 `json:",optional"`
User string `json:",optional"`
Pass string `json:",optional"`
CertFile string `json:",optional"`
@@ -26,6 +27,11 @@ func (c EtcdConf) HasAccount() bool {
return len(c.User) > 0 && len(c.Pass) > 0
}
// HasID returns if ID provided.
func (c EtcdConf) HasID() bool {
return c.ID > 0
}
// HasTLS returns if TLS CertFile/CertKeyFile/CACertFile are provided.
func (c EtcdConf) HasTLS() bool {
return len(c.CertFile) > 0 && len(c.CertKeyFile) > 0 && len(c.CACertFile) > 0

View File

@@ -80,3 +80,90 @@ func TestEtcdConf_HasAccount(t *testing.T) {
assert.Equal(t, test.hasAccount, test.EtcdConf.HasAccount())
}
}
func TestEtcdConf_HasID(t *testing.T) {
tests := []struct {
EtcdConf
hasServerID bool
}{
{
EtcdConf: EtcdConf{
Hosts: []string{"any"},
ID: -1,
},
hasServerID: false,
},
{
EtcdConf: EtcdConf{
Hosts: []string{"any"},
ID: 0,
},
hasServerID: false,
},
{
EtcdConf: EtcdConf{
Hosts: []string{"any"},
ID: 10000,
},
hasServerID: true,
},
}
for _, test := range tests {
assert.Equal(t, test.hasServerID, test.EtcdConf.HasID())
}
}
func TestEtcdConf_HasTLS(t *testing.T) {
tests := []struct {
name string
conf EtcdConf
want bool
}{
{
name: "empty config",
conf: EtcdConf{},
want: false,
},
{
name: "missing CertFile",
conf: EtcdConf{
CertKeyFile: "key",
CACertFile: "ca",
},
want: false,
},
{
name: "missing CertKeyFile",
conf: EtcdConf{
CertFile: "cert",
CACertFile: "ca",
},
want: false,
},
{
name: "missing CACertFile",
conf: EtcdConf{
CertFile: "cert",
CertKeyFile: "key",
},
want: false,
},
{
name: "valid config",
conf: EtcdConf{
CertFile: "cert",
CertKeyFile: "key",
CACertFile: "ca",
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.conf.HasTLS()
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -1,12 +1,85 @@
package internal
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/stringx"
)
const (
certContent = `-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUEg9GVO2oaPn+YSmiqmFIuAo10WIwDQYJKoZIhvcNAQEM
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yMzAzMTExMzIxMjNaGA8yMTIz
MDIxNTEzMjEyM1owRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBALplXlWsIf0O/IgnIplmiZHKGnxyfyufyE2FBRNk
OofRqbKuPH8GNqbkvZm7N29fwTDAQ+mViAggCkDht4hOzoWJMA7KYJt8JnTSWL48
M1lcrpc9DL2gszC/JF/FGvyANbBtLklkZPFBGdHUX14pjrT937wqPtm+SqUHSvRT
B7bmwmm2drRcmhpVm98LSlV7uQ2EgnJgsLjBPITKUejLmVLHfgX0RwQ2xIpX9pS4
FCe1BTacwl2gGp7Mje7y4Mfv3o0ArJW6Tuwbjx59ZXwb1KIP71b7bT04AVS8ZeYO
UMLKKuB5UR9x9Rn6cLXOTWBpcMVyzDgrAFLZjnE9LPUolZMCAwEAAaNRME8wHwYD
VR0jBBgwFoAUeW8w8pmhncbRgTsl48k4/7wnfx8wCQYDVR0TBAIwADALBgNVHQ8E
BAMCBPAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBDAUAA4IBAQAI
y9xaoS88CLPBsX6mxfcTAFVfGNTRW9VN9Ng1cCnUR+YGoXGM/l+qP4f7p8ocdGwK
iYZErVTzXYIn+D27//wpY3klJk3gAnEUBT3QRkStBw7XnpbeZ2oPBK+cmDnCnZPS
BIF1wxPX7vIgaxs5Zsdqwk3qvZ4Djr2wP7LabNWTLSBKgQoUY45Liw6pffLwcGF9
UKlu54bvGze2SufISCR3ib+I+FLvqpvJhXToZWYb/pfI/HccuCL1oot1x8vx6DQy
U+TYxlZsKS5mdNxAX3dqEkEMsgEi+g/tzDPXJImfeCGGBhIOXLm8SRypiuGdEbc9
xkWYxRPegajuEZGvCqVs
-----END CERTIFICATE-----`
keyContent = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAumVeVawh/Q78iCcimWaJkcoafHJ/K5/ITYUFE2Q6h9Gpsq48
fwY2puS9mbs3b1/BMMBD6ZWICCAKQOG3iE7OhYkwDspgm3wmdNJYvjwzWVyulz0M
vaCzML8kX8Ua/IA1sG0uSWRk8UEZ0dRfXimOtP3fvCo+2b5KpQdK9FMHtubCabZ2
tFyaGlWb3wtKVXu5DYSCcmCwuME8hMpR6MuZUsd+BfRHBDbEilf2lLgUJ7UFNpzC
XaAansyN7vLgx+/ejQCslbpO7BuPHn1lfBvUog/vVvttPTgBVLxl5g5Qwsoq4HlR
H3H1Gfpwtc5NYGlwxXLMOCsAUtmOcT0s9SiVkwIDAQABAoIBAD5meTJNMgO55Kjg
ESExxpRcCIno+tHr5+6rvYtEXqPheOIsmmwb9Gfi4+Z3WpOaht5/Pz0Ppj6yGzyl
U//6AgGKb+BDuBvVcDpjwPnOxZIBCSHwejdxeQu0scSuA97MPS0XIAvJ5FEv7ijk
5Bht6SyGYURpECltHygoTNuGgGqmO+McCJRLE9L09lTBI6UQ/JQwWJqSr7wx6iPU
M1Ze/srIV+7cyEPu6i0DGjS1gSQKkX68Lqn1w6oE290O+OZvleO0gZ02fLDWCZke
aeD9+EU/Pw+rqm3H6o0szOFIpzhRp41FUdW9sybB3Yp3u7c/574E+04Z/e30LMKs
TCtE1QECgYEA3K7KIpw0NH2HXL5C3RHcLmr204xeBfS70riBQQuVUgYdmxak2ima
80RInskY8hRhSGTg0l+VYIH8cmjcUyqMSOELS5XfRH99r4QPiK8AguXg80T4VumY
W3Pf+zEC2ssgP/gYthV0g0Xj5m2QxktOF9tRw5nkg739ZR4dI9lm/iECgYEA2Dnf
uwEDGqHiQRF6/fh5BG/nGVMvrefkqx6WvTJQ3k/M/9WhxB+lr/8yH46TuS8N2b29
FoTf3Mr9T7pr/PWkOPzoY3P56nYbKU8xSwCim9xMzhBMzj8/N9ukJvXy27/VOz56
eQaKqnvdXNGtPJrIMDGHps2KKWlKLyAlapzjVTMCgYAA/W++tACv85g13EykfT4F
n0k4LbsGP9DP4zABQLIMyiY72eAncmRVjwrcW36XJ2xATOONTgx3gF3HjZzfaqNy
eD/6uNNllUTVEryXGmHgNHPL45VRnn6memCY2eFvZdXhM5W4y2PYaunY0MkDercA
+GTngbs6tBF88KOk04bYwQKBgFl68cRgsdkmnwwQYNaTKfmVGYzYaQXNzkqmWPko
xmCJo6tHzC7ubdG8iRCYHzfmahPuuj6EdGPZuSRyYFgJi5Ftz/nAN+84OxtIQ3zn
YWOgskQgaLh9YfsKsQ7Sf1NDOsnOnD5TX7UXl07fEpLe9vNCvAFiU8e5Y9LGudU5
4bYTAoGBAMdX3a3bXp4cZvXNBJ/QLVyxC6fP1Q4haCR1Od3m+T00Jth2IX2dk/fl
p6xiJT1av5JtYabv1dFKaXOS5s1kLGGuCCSKpkvFZm826aQ2AFm0XGqEQDLeei5b
A52Kpy/YJ+RkG4BTFtAooFq6DmA0cnoP6oPvG2h6XtDJwDTPInJb
-----END RSA PRIVATE KEY-----`
caContent = `-----BEGIN CERTIFICATE-----
MIIDbTCCAlWgAwIBAgIUBJvFoCowKich7MMfseJ+DYzzirowDQYJKoZIhvcNAQEM
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yMzAzMTExMzIxMDNaGA8yMTIz
MDIxNTEzMjEwM1owRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAO4to2YMYj0bxgr2FCiweSTSFuPx33zSw2x/s9Wf
OR41bm2DFsyYT5f3sOIKlXZEdLmOKty2e3ho3yC0EyNpVHdykkkHT3aDI17quZax
kYi/URqqtl1Z08A22txolc04hAZisg2BypGi3vql81UW1t3zyloGnJoIAeXR9uca
ljP6Bk3bwsxoVBLi1JtHrO0hHLQaeHmKhAyrys06X0LRdn7Px48yRZlt6FaLSa8X
YiRM0G44bVy/h6BkoQjMYGwVmCVk6zjJ9U7ZPFqdnDMNxAfR+hjDnYodqdLDMTTR
1NPVrnEnNwFx0AMLvgt/ba/45vZCEAmSZnFXFAJJcM7ai9ECAwEAAaNTMFEwHQYD
VR0OBBYEFHlvMPKZoZ3G0YE7JePJOP+8J38fMB8GA1UdIwQYMBaAFHlvMPKZoZ3G
0YE7JePJOP+8J38fMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggEB
AMX8dNulADOo9uQgBMyFb9TVra7iY0zZjzv4GY5XY7scd52n6CnfAPvYBBDnTr/O
BgNp5jaujb4+9u/2qhV3f9n+/3WOb2CmPehBgVSzlXqHeQ9lshmgwZPeem2T+8Tm
Nnc/xQnsUfCFszUDxpkr55+aLVM22j02RWqcZ4q7TAaVYL+kdFVMc8FoqG/0ro6A
BjE/Qn0Nn7ciX1VUjDt8l+k7ummPJTmzdi6i6E4AwO9dzrGNgGJ4aWL8cC6xYcIX
goVIRTFeONXSDno/oPjWHpIPt7L15heMpKBHNuzPkKx2YVqPHE5QZxWfS+Lzgx+Q
E2oTTM0rYKOZ8p6000mhvKI=
-----END CERTIFICATE-----`
)
func TestAccount(t *testing.T) {
endpoints := []string{
"192.168.0.2:2379",
@@ -32,3 +105,34 @@ func TestAccount(t *testing.T) {
assert.Equal(t, username, account.User)
assert.Equal(t, anotherPassword, account.Pass)
}
func TestTLSMethods(t *testing.T) {
certFile := createTempFile(t, []byte(certContent))
defer os.Remove(certFile)
keyFile := createTempFile(t, []byte(keyContent))
defer os.Remove(keyFile)
caFile := createTempFile(t, []byte(caContent))
defer os.Remove(caFile)
assert.NoError(t, AddTLS([]string{"foo"}, certFile, keyFile, caFile, false))
cfg, ok := GetTLS([]string{"foo"})
assert.True(t, ok)
assert.NotNil(t, cfg)
assert.Error(t, AddTLS([]string{"bar"}, "bad-file", keyFile, caFile, false))
assert.Error(t, AddTLS([]string{"bar"}, certFile, keyFile, "bad-file", false))
}
func createTempFile(t *testing.T, body []byte) string {
tmpFile, err := os.CreateTemp(os.TempDir(), "go-unit-*.tmp")
if err != nil {
t.Fatal(err)
}
tmpFile.Close()
if err = os.WriteFile(tmpFile.Name(), body, os.ModePerm); err != nil {
t.Fatal(err)
}
return tmpFile.Name()
}

View File

@@ -337,13 +337,11 @@ func (c *cluster) watchConnState(cli EtcdClient) {
// DialClient dials an etcd cluster with given endpoints.
func DialClient(endpoints []string) (EtcdClient, error) {
cfg := clientv3.Config{
Endpoints: endpoints,
AutoSyncInterval: autoSyncInterval,
DialTimeout: DialTimeout,
DialKeepAliveTime: dialKeepAliveTime,
DialKeepAliveTimeout: DialTimeout,
RejectOldCluster: true,
PermitWithoutStream: true,
Endpoints: endpoints,
AutoSyncInterval: autoSyncInterval,
DialTimeout: DialTimeout,
RejectOldCluster: true,
PermitWithoutStream: true,
}
if account, ok := GetAccount(endpoints); ok {
cfg.Username = account.User

View File

@@ -2,8 +2,10 @@ package internal
import (
"context"
"os"
"sync"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
@@ -14,6 +16,7 @@ import (
"go.etcd.io/etcd/api/v3/etcdserverpb"
"go.etcd.io/etcd/api/v3/mvccpb"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/mock/mockserver"
)
var mockLock sync.Mutex
@@ -242,3 +245,58 @@ func TestValueOnlyContext(t *testing.T) {
ctx.Done()
assert.Nil(t, ctx.Err())
}
func TestDialClient(t *testing.T) {
svr, err := mockserver.StartMockServers(1)
assert.NoError(t, err)
svr.StartAt(0)
certFile := createTempFile(t, []byte(certContent))
defer os.Remove(certFile)
keyFile := createTempFile(t, []byte(keyContent))
defer os.Remove(keyFile)
caFile := createTempFile(t, []byte(caContent))
defer os.Remove(caFile)
endpoints := []string{svr.Servers[0].Address}
AddAccount(endpoints, "foo", "bar")
assert.NoError(t, AddTLS(endpoints, certFile, keyFile, caFile, false))
old := DialTimeout
DialTimeout = time.Millisecond
defer func() {
DialTimeout = old
}()
_, err = DialClient(endpoints)
assert.Error(t, err)
}
func TestRegistry_Monitor(t *testing.T) {
svr, err := mockserver.StartMockServers(1)
assert.NoError(t, err)
svr.StartAt(0)
endpoints := []string{svr.Servers[0].Address}
GetRegistry().lock.Lock()
GetRegistry().clusters = map[string]*cluster{
getClusterKey(endpoints): {
listeners: map[string][]UpdateListener{},
values: map[string]map[string]string{
"foo": {
"bar": "baz",
},
},
},
}
GetRegistry().lock.Unlock()
assert.Error(t, GetRegistry().Monitor(endpoints, "foo", new(mockListener)))
}
type mockListener struct {
}
func (m *mockListener) OnAdd(_ KV) {
}
func (m *mockListener) OnDelete(_ KV) {
}

View File

@@ -9,7 +9,6 @@ const (
autoSyncInterval = time.Minute
coolDownInterval = time.Second
dialTimeout = 5 * time.Second
dialKeepAliveTime = 5 * time.Second
requestTimeout = 3 * time.Second
endpointsSeparator = ","
)

View File

@@ -1,7 +1,10 @@
package discov
import (
"context"
"errors"
"net"
"os"
"sync"
"testing"
"time"
@@ -13,6 +16,83 @@ import (
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stringx"
clientv3 "go.etcd.io/etcd/client/v3"
"golang.org/x/net/http2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/resolver/manual"
)
const (
certContent = `-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUEg9GVO2oaPn+YSmiqmFIuAo10WIwDQYJKoZIhvcNAQEM
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yMzAzMTExMzIxMjNaGA8yMTIz
MDIxNTEzMjEyM1owRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBALplXlWsIf0O/IgnIplmiZHKGnxyfyufyE2FBRNk
OofRqbKuPH8GNqbkvZm7N29fwTDAQ+mViAggCkDht4hOzoWJMA7KYJt8JnTSWL48
M1lcrpc9DL2gszC/JF/FGvyANbBtLklkZPFBGdHUX14pjrT937wqPtm+SqUHSvRT
B7bmwmm2drRcmhpVm98LSlV7uQ2EgnJgsLjBPITKUejLmVLHfgX0RwQ2xIpX9pS4
FCe1BTacwl2gGp7Mje7y4Mfv3o0ArJW6Tuwbjx59ZXwb1KIP71b7bT04AVS8ZeYO
UMLKKuB5UR9x9Rn6cLXOTWBpcMVyzDgrAFLZjnE9LPUolZMCAwEAAaNRME8wHwYD
VR0jBBgwFoAUeW8w8pmhncbRgTsl48k4/7wnfx8wCQYDVR0TBAIwADALBgNVHQ8E
BAMCBPAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBDAUAA4IBAQAI
y9xaoS88CLPBsX6mxfcTAFVfGNTRW9VN9Ng1cCnUR+YGoXGM/l+qP4f7p8ocdGwK
iYZErVTzXYIn+D27//wpY3klJk3gAnEUBT3QRkStBw7XnpbeZ2oPBK+cmDnCnZPS
BIF1wxPX7vIgaxs5Zsdqwk3qvZ4Djr2wP7LabNWTLSBKgQoUY45Liw6pffLwcGF9
UKlu54bvGze2SufISCR3ib+I+FLvqpvJhXToZWYb/pfI/HccuCL1oot1x8vx6DQy
U+TYxlZsKS5mdNxAX3dqEkEMsgEi+g/tzDPXJImfeCGGBhIOXLm8SRypiuGdEbc9
xkWYxRPegajuEZGvCqVs
-----END CERTIFICATE-----`
keyContent = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAumVeVawh/Q78iCcimWaJkcoafHJ/K5/ITYUFE2Q6h9Gpsq48
fwY2puS9mbs3b1/BMMBD6ZWICCAKQOG3iE7OhYkwDspgm3wmdNJYvjwzWVyulz0M
vaCzML8kX8Ua/IA1sG0uSWRk8UEZ0dRfXimOtP3fvCo+2b5KpQdK9FMHtubCabZ2
tFyaGlWb3wtKVXu5DYSCcmCwuME8hMpR6MuZUsd+BfRHBDbEilf2lLgUJ7UFNpzC
XaAansyN7vLgx+/ejQCslbpO7BuPHn1lfBvUog/vVvttPTgBVLxl5g5Qwsoq4HlR
H3H1Gfpwtc5NYGlwxXLMOCsAUtmOcT0s9SiVkwIDAQABAoIBAD5meTJNMgO55Kjg
ESExxpRcCIno+tHr5+6rvYtEXqPheOIsmmwb9Gfi4+Z3WpOaht5/Pz0Ppj6yGzyl
U//6AgGKb+BDuBvVcDpjwPnOxZIBCSHwejdxeQu0scSuA97MPS0XIAvJ5FEv7ijk
5Bht6SyGYURpECltHygoTNuGgGqmO+McCJRLE9L09lTBI6UQ/JQwWJqSr7wx6iPU
M1Ze/srIV+7cyEPu6i0DGjS1gSQKkX68Lqn1w6oE290O+OZvleO0gZ02fLDWCZke
aeD9+EU/Pw+rqm3H6o0szOFIpzhRp41FUdW9sybB3Yp3u7c/574E+04Z/e30LMKs
TCtE1QECgYEA3K7KIpw0NH2HXL5C3RHcLmr204xeBfS70riBQQuVUgYdmxak2ima
80RInskY8hRhSGTg0l+VYIH8cmjcUyqMSOELS5XfRH99r4QPiK8AguXg80T4VumY
W3Pf+zEC2ssgP/gYthV0g0Xj5m2QxktOF9tRw5nkg739ZR4dI9lm/iECgYEA2Dnf
uwEDGqHiQRF6/fh5BG/nGVMvrefkqx6WvTJQ3k/M/9WhxB+lr/8yH46TuS8N2b29
FoTf3Mr9T7pr/PWkOPzoY3P56nYbKU8xSwCim9xMzhBMzj8/N9ukJvXy27/VOz56
eQaKqnvdXNGtPJrIMDGHps2KKWlKLyAlapzjVTMCgYAA/W++tACv85g13EykfT4F
n0k4LbsGP9DP4zABQLIMyiY72eAncmRVjwrcW36XJ2xATOONTgx3gF3HjZzfaqNy
eD/6uNNllUTVEryXGmHgNHPL45VRnn6memCY2eFvZdXhM5W4y2PYaunY0MkDercA
+GTngbs6tBF88KOk04bYwQKBgFl68cRgsdkmnwwQYNaTKfmVGYzYaQXNzkqmWPko
xmCJo6tHzC7ubdG8iRCYHzfmahPuuj6EdGPZuSRyYFgJi5Ftz/nAN+84OxtIQ3zn
YWOgskQgaLh9YfsKsQ7Sf1NDOsnOnD5TX7UXl07fEpLe9vNCvAFiU8e5Y9LGudU5
4bYTAoGBAMdX3a3bXp4cZvXNBJ/QLVyxC6fP1Q4haCR1Od3m+T00Jth2IX2dk/fl
p6xiJT1av5JtYabv1dFKaXOS5s1kLGGuCCSKpkvFZm826aQ2AFm0XGqEQDLeei5b
A52Kpy/YJ+RkG4BTFtAooFq6DmA0cnoP6oPvG2h6XtDJwDTPInJb
-----END RSA PRIVATE KEY-----`
caContent = `-----BEGIN CERTIFICATE-----
MIIDbTCCAlWgAwIBAgIUBJvFoCowKich7MMfseJ+DYzzirowDQYJKoZIhvcNAQEM
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yMzAzMTExMzIxMDNaGA8yMTIz
MDIxNTEzMjEwM1owRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAO4to2YMYj0bxgr2FCiweSTSFuPx33zSw2x/s9Wf
OR41bm2DFsyYT5f3sOIKlXZEdLmOKty2e3ho3yC0EyNpVHdykkkHT3aDI17quZax
kYi/URqqtl1Z08A22txolc04hAZisg2BypGi3vql81UW1t3zyloGnJoIAeXR9uca
ljP6Bk3bwsxoVBLi1JtHrO0hHLQaeHmKhAyrys06X0LRdn7Px48yRZlt6FaLSa8X
YiRM0G44bVy/h6BkoQjMYGwVmCVk6zjJ9U7ZPFqdnDMNxAfR+hjDnYodqdLDMTTR
1NPVrnEnNwFx0AMLvgt/ba/45vZCEAmSZnFXFAJJcM7ai9ECAwEAAaNTMFEwHQYD
VR0OBBYEFHlvMPKZoZ3G0YE7JePJOP+8J38fMB8GA1UdIwQYMBaAFHlvMPKZoZ3G
0YE7JePJOP+8J38fMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggEB
AMX8dNulADOo9uQgBMyFb9TVra7iY0zZjzv4GY5XY7scd52n6CnfAPvYBBDnTr/O
BgNp5jaujb4+9u/2qhV3f9n+/3WOb2CmPehBgVSzlXqHeQ9lshmgwZPeem2T+8Tm
Nnc/xQnsUfCFszUDxpkr55+aLVM22j02RWqcZ4q7TAaVYL+kdFVMc8FoqG/0ro6A
BjE/Qn0Nn7ciX1VUjDt8l+k7ummPJTmzdi6i6E4AwO9dzrGNgGJ4aWL8cC6xYcIX
goVIRTFeONXSDno/oPjWHpIPt7L15heMpKBHNuzPkKx2YVqPHE5QZxWfS+Lzgx+Q
E2oTTM0rYKOZ8p6000mhvKI=
-----END CERTIFICATE-----`
)
func init() {
@@ -37,7 +117,7 @@ func TestPublisher_register(t *testing.T) {
assert.Nil(t, err)
}
func TestPublisher_registerWithId(t *testing.T) {
func TestPublisher_registerWithOptions(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
const id = 2
@@ -49,7 +129,15 @@ func TestPublisher_registerWithId(t *testing.T) {
ID: 1,
}, nil)
cli.EXPECT().Put(gomock.Any(), makeEtcdKey("thekey", id), "thevalue", gomock.Any())
pub := NewPublisher(nil, "thekey", "thevalue", WithId(id))
certFile := createTempFile(t, []byte(certContent))
defer os.Remove(certFile)
keyFile := createTempFile(t, []byte(keyContent))
defer os.Remove(keyFile)
caFile := createTempFile(t, []byte(caContent))
defer os.Remove(caFile)
pub := NewPublisher(nil, "thekey", "thevalue", WithId(id),
WithPubEtcdTLS(certFile, keyFile, caFile, true))
_, err := pub.register(cli)
assert.Nil(t, err)
}
@@ -169,3 +257,92 @@ func TestPublisher_Resume(t *testing.T) {
}()
<-publisher.resumeChan
}
func TestPublisher_keepAliveAsync(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
const id clientv3.LeaseID = 1
conn := createMockConn(t)
defer conn.Close()
cli := internal.NewMockEtcdClient(ctrl)
cli.EXPECT().ActiveConnection().Return(conn).AnyTimes()
cli.EXPECT().Close()
defer cli.Close()
cli.ActiveConnection()
restore := setMockClient(cli)
defer restore()
cli.EXPECT().Ctx().AnyTimes()
cli.EXPECT().KeepAlive(gomock.Any(), id)
cli.EXPECT().Grant(gomock.Any(), timeToLive).Return(&clientv3.LeaseGrantResponse{
ID: 1,
}, nil)
cli.EXPECT().Put(gomock.Any(), makeEtcdKey("thekey", int64(id)), "thevalue", gomock.Any())
var wg sync.WaitGroup
wg.Add(1)
cli.EXPECT().Revoke(gomock.Any(), id).Do(func(_, _ any) {
wg.Done()
})
pub := NewPublisher([]string{"the-endpoint"}, "thekey", "thevalue")
pub.lease = id
assert.Nil(t, pub.KeepAlive())
pub.Stop()
wg.Wait()
}
func createMockConn(t *testing.T) *grpc.ClientConn {
lis, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("Error while listening. Err: %v", err)
}
defer lis.Close()
lisAddr := resolver.Address{Addr: lis.Addr().String()}
lisDone := make(chan struct{})
dialDone := make(chan struct{})
// 1st listener accepts the connection and then does nothing
go func() {
defer close(lisDone)
conn, err := lis.Accept()
if err != nil {
t.Errorf("Error while accepting. Err: %v", err)
return
}
framer := http2.NewFramer(conn, conn)
if err := framer.WriteSettings(http2.Setting{}); err != nil {
t.Errorf("Error while writing settings. Err: %v", err)
return
}
<-dialDone // Close conn only after dial returns.
}()
r := manual.NewBuilderWithScheme("whatever")
r.InitialState(resolver.State{Addresses: []resolver.Address{lisAddr}})
client, err := grpc.DialContext(context.Background(), r.Scheme()+":///test.server",
grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(r))
close(dialDone)
if err != nil {
t.Fatalf("Dial failed. Err: %v", err)
}
timeout := time.After(1 * time.Second)
select {
case <-timeout:
t.Fatal("timed out waiting for server to finish")
case <-lisDone:
}
return client
}
func createTempFile(t *testing.T, body []byte) string {
tmpFile, err := os.CreateTemp(os.TempDir(), "go-unit-*.tmp")
if err != nil {
t.Fatal(err)
}
tmpFile.Close()
if err = os.WriteFile(tmpFile.Name(), body, os.ModePerm); err != nil {
t.Fatal(err)
}
return tmpFile.Name()
}

View File

@@ -78,7 +78,7 @@ func TestBulkExecutorFlush(t *testing.T) {
wait.Wait()
}
func TestBuldExecutorFlushSlowTasks(t *testing.T) {
func TestBulkExecutorFlushSlowTasks(t *testing.T) {
const total = 1500
lock := new(sync.Mutex)
result := make([]any, 0, 10000)

View File

@@ -81,7 +81,7 @@ func (pe *PeriodicalExecutor) Flush() bool {
}())
}
// Sync lets caller to run fn thread-safe with pe, especially for the underlying container.
// Sync lets caller run fn thread-safe with pe, especially for the underlying container.
func (pe *PeriodicalExecutor) Sync(fn func()) {
pe.lock.Lock()
defer pe.lock.Unlock()
@@ -116,7 +116,7 @@ func (pe *PeriodicalExecutor) addAndCheck(task any) (any, bool) {
}
func (pe *PeriodicalExecutor) backgroundFlush() {
threading.GoSafe(func() {
go func() {
// flush before quit goroutine to avoid missing tasks
defer pe.Flush()
@@ -144,7 +144,7 @@ func (pe *PeriodicalExecutor) backgroundFlush() {
}
}
}
})
}()
}
func (pe *PeriodicalExecutor) doneExecution() {
@@ -162,7 +162,9 @@ func (pe *PeriodicalExecutor) executeTasks(tasks any) bool {
ok := pe.hasTasks(tasks)
if ok {
pe.container.Execute(tasks)
threading.RunSafe(func() {
pe.container.Execute(tasks)
})
}
return ok

View File

@@ -108,25 +108,83 @@ func TestPeriodicalExecutor_Bulk(t *testing.T) {
lock.Unlock()
}
func TestPeriodicalExecutor_Panic(t *testing.T) {
// avoid data race
var lock sync.Mutex
ticker := timex.NewFakeTicker()
var (
executedTasks []int
expected []int
)
executor := NewPeriodicalExecutor(time.Millisecond, newContainer(time.Millisecond, func(tasks any) {
tt := tasks.([]int)
lock.Lock()
executedTasks = append(executedTasks, tt...)
lock.Unlock()
if tt[0] == 0 {
panic("test")
}
}))
executor.newTicker = func(duration time.Duration) timex.Ticker {
return ticker
}
for i := 0; i < 30; i++ {
executor.Add(i)
expected = append(expected, i)
}
ticker.Tick()
ticker.Tick()
time.Sleep(time.Millisecond)
lock.Lock()
assert.Equal(t, expected, executedTasks)
lock.Unlock()
}
func TestPeriodicalExecutor_FlushPanic(t *testing.T) {
var (
executedTasks []int
expected []int
lock sync.Mutex
)
executor := NewPeriodicalExecutor(time.Millisecond, newContainer(time.Millisecond, func(tasks any) {
tt := tasks.([]int)
lock.Lock()
executedTasks = append(executedTasks, tt...)
lock.Unlock()
if tt[0] == 0 {
panic("flush panic")
}
}))
for i := 0; i < 8; i++ {
executor.Add(i)
expected = append(expected, i)
}
executor.Flush()
lock.Lock()
assert.Equal(t, expected, executedTasks)
lock.Unlock()
}
func TestPeriodicalExecutor_Wait(t *testing.T) {
var lock sync.Mutex
executer := NewBulkExecutor(func(tasks []any) {
executor := NewBulkExecutor(func(tasks []any) {
lock.Lock()
defer lock.Unlock()
time.Sleep(10 * time.Millisecond)
}, WithBulkTasks(1), WithBulkInterval(time.Second))
for i := 0; i < 10; i++ {
executer.Add(1)
executor.Add(1)
}
executer.Flush()
executer.Wait()
executor.Flush()
executor.Wait()
}
func TestPeriodicalExecutor_WaitFast(t *testing.T) {
const total = 3
var cnt int
var lock sync.Mutex
executer := NewBulkExecutor(func(tasks []any) {
executor := NewBulkExecutor(func(tasks []any) {
defer func() {
cnt++
}()
@@ -135,10 +193,10 @@ func TestPeriodicalExecutor_WaitFast(t *testing.T) {
time.Sleep(10 * time.Millisecond)
}, WithBulkTasks(1), WithBulkInterval(10*time.Millisecond))
for i := 0; i < total; i++ {
executer.Add(2)
executor.Add(2)
}
executer.Flush()
executer.Wait()
executor.Flush()
executor.Wait()
assert.Equal(t, total, cnt)
}
@@ -151,13 +209,7 @@ func TestPeriodicalExecutor_Deadlock(t *testing.T) {
}
func TestPeriodicalExecutor_hasTasks(t *testing.T) {
ticker := timex.NewFakeTicker()
defer ticker.Stop()
exec := NewPeriodicalExecutor(time.Millisecond, newContainer(time.Millisecond, nil))
exec.newTicker = func(d time.Duration) timex.Ticker {
return ticker
}
assert.False(t, exec.hasTasks(nil))
assert.True(t, exec.hasTasks(1))
}

View File

@@ -74,6 +74,11 @@ func TestFirstLineShort(t *testing.T) {
assert.Equal(t, "first line", val)
}
func TestFirstLineError(t *testing.T) {
_, err := FirstLine("/tmp/does-not-exist")
assert.Error(t, err)
}
func TestLastLine(t *testing.T) {
filename, err := fs.TempFilenameWithText(text)
assert.Nil(t, err)
@@ -113,3 +118,8 @@ func TestLastLineWithLastNewlineShort(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, "last line", val)
}
func TestLastLineError(t *testing.T) {
_, err := LastLine("/tmp/does-not-exist")
assert.Error(t, err)
}

View File

@@ -1,5 +1,4 @@
//go:build windows
// +build windows
package fs

View File

@@ -1,5 +1,4 @@
//go:build linux || darwin
// +build linux darwin
package fs

View File

@@ -11,29 +11,29 @@ import (
// The file is kept as open, the caller should close the file handle,
// and remove the file by name.
func TempFileWithText(text string) (*os.File, error) {
tmpfile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text)))
tmpFile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text)))
if err != nil {
return nil, err
}
if err := os.WriteFile(tmpfile.Name(), []byte(text), os.ModeTemporary); err != nil {
if err := os.WriteFile(tmpFile.Name(), []byte(text), os.ModeTemporary); err != nil {
return nil, err
}
return tmpfile, nil
return tmpFile, nil
}
// TempFilenameWithText creates the file with the given content,
// and returns the filename (full path).
// The caller should remove the file after use.
func TempFilenameWithText(text string) (string, error) {
tmpfile, err := TempFileWithText(text)
tmpFile, err := TempFileWithText(text)
if err != nil {
return "", err
}
filename := tmpfile.Name()
if err = tmpfile.Close(); err != nil {
filename := tmpFile.Name()
if err = tmpFile.Close(); err != nil {
return "", err
}

View File

@@ -1,6 +1,11 @@
package fx
import "github.com/zeromicro/go-zero/core/errorx"
import (
"context"
"time"
"github.com/zeromicro/go-zero/core/errorx"
)
const defaultRetryTimes = 3
@@ -9,23 +14,70 @@ type (
RetryOption func(*retryOptions)
retryOptions struct {
times int
times int
interval time.Duration
timeout time.Duration
}
)
// DoWithRetry runs fn, and retries if failed. Default to retry 3 times.
// Note that if the fn function accesses global variables outside the function
// and performs modification operations, it is best to lock them,
// otherwise there may be data race issues
func DoWithRetry(fn func() error, opts ...RetryOption) error {
return retry(context.Background(), func(errChan chan error, retryCount int) {
errChan <- fn()
}, opts...)
}
// DoWithRetryCtx runs fn, and retries if failed. Default to retry 3 times.
// fn retryCount indicates the current number of retries, starting from 0
// Note that if the fn function accesses global variables outside the function
// and performs modification operations, it is best to lock them,
// otherwise there may be data race issues
func DoWithRetryCtx(ctx context.Context, fn func(ctx context.Context, retryCount int) error,
opts ...RetryOption) error {
return retry(ctx, func(errChan chan error, retryCount int) {
errChan <- fn(ctx, retryCount)
}, opts...)
}
func retry(ctx context.Context, fn func(errChan chan error, retryCount int), opts ...RetryOption) error {
options := newRetryOptions()
for _, opt := range opts {
opt(options)
}
var berr errorx.BatchError
var cancelFunc context.CancelFunc
if options.timeout > 0 {
ctx, cancelFunc = context.WithTimeout(ctx, options.timeout)
defer cancelFunc()
}
errChan := make(chan error, 1)
for i := 0; i < options.times; i++ {
if err := fn(); err != nil {
berr.Add(err)
} else {
return nil
go fn(errChan, i)
select {
case err := <-errChan:
if err != nil {
berr.Add(err)
} else {
return nil
}
case <-ctx.Done():
berr.Add(ctx.Err())
return berr.Err()
}
if options.interval > 0 {
select {
case <-ctx.Done():
berr.Add(ctx.Err())
return berr.Err()
case <-time.After(options.interval):
}
}
}
@@ -39,6 +91,18 @@ func WithRetry(times int) RetryOption {
}
}
func WithInterval(interval time.Duration) RetryOption {
return func(options *retryOptions) {
options.interval = interval
}
}
func WithTimeout(timeout time.Duration) RetryOption {
return func(options *retryOptions) {
options.timeout = timeout
}
}
func newRetryOptions() *retryOptions {
return &retryOptions{
times: defaultRetryTimes,

View File

@@ -1,8 +1,10 @@
package fx
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
@@ -12,31 +14,135 @@ func TestRetry(t *testing.T) {
return errors.New("any")
}))
var times int
times1 := 0
assert.Nil(t, DoWithRetry(func() error {
times++
if times == defaultRetryTimes {
times1++
if times1 == defaultRetryTimes {
return nil
}
return errors.New("any")
}))
times = 0
times2 := 0
assert.NotNil(t, DoWithRetry(func() error {
times++
if times == defaultRetryTimes+1 {
times2++
if times2 == defaultRetryTimes+1 {
return nil
}
return errors.New("any")
}))
total := 2 * defaultRetryTimes
times = 0
times3 := 0
assert.Nil(t, DoWithRetry(func() error {
times++
if times == total {
times3++
if times3 == total {
return nil
}
return errors.New("any")
}, WithRetry(total)))
}
func TestRetryWithTimeout(t *testing.T) {
assert.Nil(t, DoWithRetry(func() error {
return nil
}, WithTimeout(time.Millisecond*500)))
times1 := 0
assert.Nil(t, DoWithRetry(func() error {
times1++
if times1 == 1 {
return errors.New("any ")
}
time.Sleep(time.Millisecond * 150)
return nil
}, WithTimeout(time.Millisecond*250)))
total := defaultRetryTimes
times2 := 0
assert.Nil(t, DoWithRetry(func() error {
times2++
if times2 == total {
return nil
}
time.Sleep(time.Millisecond * 50)
return errors.New("any")
}, WithTimeout(time.Millisecond*50*(time.Duration(total)+2))))
assert.NotNil(t, DoWithRetry(func() error {
return errors.New("any")
}, WithTimeout(time.Millisecond*250)))
}
func TestRetryWithInterval(t *testing.T) {
times1 := 0
assert.NotNil(t, DoWithRetry(func() error {
times1++
if times1 == 1 {
return errors.New("any")
}
time.Sleep(time.Millisecond * 150)
return nil
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
times2 := 0
assert.NotNil(t, DoWithRetry(func() error {
times2++
if times2 == 2 {
return nil
}
time.Sleep(time.Millisecond * 150)
return errors.New("any ")
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
}
func TestRetryCtx(t *testing.T) {
t.Run("with timeout", func(t *testing.T) {
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error {
if retryCount == 0 {
return errors.New("any")
}
time.Sleep(time.Millisecond * 150)
return nil
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error {
if retryCount == 1 {
return nil
}
time.Sleep(time.Millisecond * 150)
return errors.New("any ")
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
})
t.Run("with deadline exceeded", func(t *testing.T) {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond*250))
defer cancel()
var times int
assert.Error(t, DoWithRetryCtx(ctx, func(ctx context.Context, retryCount int) error {
times++
time.Sleep(time.Millisecond * 150)
return errors.New("any")
}, WithInterval(time.Millisecond*150)))
assert.Equal(t, 1, times)
})
t.Run("with deadline not exceeded", func(t *testing.T) {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond*250))
defer cancel()
var times int
assert.NoError(t, DoWithRetryCtx(ctx, func(ctx context.Context, retryCount int) error {
times++
if times == defaultRetryTimes {
return nil
}
time.Sleep(time.Millisecond * 50)
return errors.New("any")
}))
assert.Equal(t, defaultRetryTimes, times)
})
}

View File

@@ -292,6 +292,18 @@ func (s Stream) Map(fn MapFunc, opts ...Option) Stream {
}, opts...)
}
// Max returns the maximum item from the underlying source.
func (s Stream) Max(less LessFunc) any {
var max any
for item := range s.source {
if max == nil || less(max, item) {
max = item
}
}
return max
}
// Merge merges all the items into a slice and generates a new stream.
func (s Stream) Merge() Stream {
var items []any
@@ -306,6 +318,18 @@ func (s Stream) Merge() Stream {
return Range(source)
}
// Min returns the minimum item from the underlying source.
func (s Stream) Min(less LessFunc) any {
var min any
for item := range s.source {
if min == nil || less(item, min) {
min = item
}
}
return min
}
// NoneMatch returns whether all elements of this stream don't match the provided predicate.
// May not evaluate the predicate on all elements if not necessary for determining the result.
// If the stream is empty then true is returned and the predicate is not evaluated.

View File

@@ -503,6 +503,83 @@ func TestStream_Concat(t *testing.T) {
})
}
func TestStream_Max(t *testing.T) {
runCheckedTest(t, func(t *testing.T) {
tests := []struct {
name string
elements []any
max any
}{
{
name: "no elements with nil",
},
{
name: "no elements",
elements: []any{},
max: nil,
},
{
name: "1 element",
elements: []any{1},
max: 1,
},
{
name: "multiple elements",
elements: []any{1, 2, 9, 5, 8},
max: 9,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
val := Just(test.elements...).Max(func(a, b any) bool {
return a.(int) < b.(int)
})
assetEqual(t, test.max, val)
})
}
})
}
func TestStream_Min(t *testing.T) {
runCheckedTest(t, func(t *testing.T) {
tests := []struct {
name string
elements []any
min any
}{
{
name: "no elements with nil",
min: nil,
},
{
name: "no elements",
elements: []any{},
min: nil,
},
{
name: "1 element",
elements: []any{1},
min: 1,
},
{
name: "multiple elements",
elements: []any{-1, 1, 2, 9, 5, 8},
min: -1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
val := Just(test.elements...).Min(func(a, b any) bool {
return a.(int) < b.(int)
})
assetEqual(t, test.min, val)
})
}
})
}
func BenchmarkParallelMapReduce(b *testing.B) {
b.ReportAllocs()

View File

@@ -32,6 +32,10 @@ func (bp *BufferPool) Get() *bytes.Buffer {
// Put returns buf into bp.
func (bp *BufferPool) Put(buf *bytes.Buffer) {
if buf == nil {
return
}
if buf.Cap() < bp.capability {
bp.pool.Put(buf)
}

View File

@@ -13,3 +13,26 @@ func TestBufferPool(t *testing.T) {
pool.Put(bytes.NewBuffer(make([]byte, 0, 2*capacity)))
assert.True(t, pool.Get().Cap() <= capacity)
}
func TestBufferPool_Put(t *testing.T) {
t.Run("with nil buf", func(t *testing.T) {
pool := NewBufferPool(1024)
pool.Put(nil)
val := pool.Get()
assert.IsType(t, new(bytes.Buffer), val)
})
t.Run("with less-cap buf", func(t *testing.T) {
pool := NewBufferPool(1024)
pool.Put(bytes.NewBuffer(make([]byte, 0, 512)))
val := pool.Get()
assert.IsType(t, new(bytes.Buffer), val)
})
t.Run("with more-cap buf", func(t *testing.T) {
pool := NewBufferPool(1024)
pool.Put(bytes.NewBuffer(make([]byte, 0, 1024<<1)))
val := pool.Get()
assert.IsType(t, new(bytes.Buffer), val)
})
}

View File

@@ -0,0 +1,12 @@
package iox
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNopCloser(t *testing.T) {
closer := NopCloser(nil)
assert.NoError(t, closer.Close())
}

View File

@@ -35,6 +35,16 @@ func KeepSpace() TextReadOption {
}
}
// LimitDupReadCloser returns two io.ReadCloser that read from the first will be written to the second.
// But the second io.ReadCloser is limited to up to n bytes.
// The first returned reader needs to be read first, because the content
// read from it will be written to the underlying buffer of the second reader.
func LimitDupReadCloser(reader io.ReadCloser, n int64) (io.ReadCloser, io.ReadCloser) {
var buf bytes.Buffer
tee := LimitTeeReader(reader, &buf, n)
return io.NopCloser(tee), io.NopCloser(&buf)
}
// ReadBytes reads exactly the bytes with the length of len(buf)
func ReadBytes(reader io.Reader, buf []byte) error {
var got int

View File

@@ -40,17 +40,22 @@ b`,
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
tmpfile, err := fs.TempFilenameWithText(test.input)
tmpFile, err := fs.TempFilenameWithText(test.input)
assert.Nil(t, err)
defer os.Remove(tmpfile)
defer os.Remove(tmpFile)
content, err := ReadText(tmpfile)
content, err := ReadText(tmpFile)
assert.Nil(t, err)
assert.Equal(t, test.expect, content)
})
}
}
func TestReadTextError(t *testing.T) {
_, err := ReadText("not-exist")
assert.NotNil(t, err)
}
func TestReadTextLines(t *testing.T) {
text := `1
@@ -59,9 +64,9 @@ func TestReadTextLines(t *testing.T) {
#a
3`
tmpfile, err := fs.TempFilenameWithText(text)
tmpFile, err := fs.TempFilenameWithText(text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
defer os.Remove(tmpFile)
tests := []struct {
options []TextReadOption
@@ -87,13 +92,18 @@ func TestReadTextLines(t *testing.T) {
for _, test := range tests {
t.Run(stringx.Rand(), func(t *testing.T) {
lines, err := ReadTextLines(tmpfile, test.options...)
lines, err := ReadTextLines(tmpFile, test.options...)
assert.Nil(t, err)
assert.Equal(t, test.expectLines, len(lines))
})
}
}
func TestReadTextLinesError(t *testing.T) {
_, err := ReadTextLines("not-exist")
assert.NotNil(t, err)
}
func TestDupReadCloser(t *testing.T) {
input := "hello"
reader := io.NopCloser(bytes.NewBufferString(input))
@@ -108,6 +118,29 @@ func TestDupReadCloser(t *testing.T) {
verify(r2)
}
func TestLimitDupReadCloser(t *testing.T) {
input := "hello world"
limitBytes := int64(4)
reader := io.NopCloser(bytes.NewBufferString(input))
r1, r2 := LimitDupReadCloser(reader, limitBytes)
verify := func(r io.Reader) {
output, err := io.ReadAll(r)
assert.Nil(t, err)
assert.Equal(t, input, string(output))
}
verifyLimit := func(r io.Reader, limit int64) {
output, err := io.ReadAll(r)
if limit < int64(len(input)) {
input = input[:limit]
}
assert.Nil(t, err)
assert.Equal(t, input, string(output))
}
verify(r1)
verifyLimit(r2, limitBytes)
}
func TestReadBytes(t *testing.T) {
reader := io.NopCloser(bytes.NewBufferString("helloworld"))
buf := make([]byte, 5)

35
core/iox/tee.go Normal file
View File

@@ -0,0 +1,35 @@
package iox
import "io"
// LimitTeeReader returns a Reader that writes up to n bytes to w what it reads from r.
// First n bytes reads from r performed through it are matched with
// corresponding writes to w. There is no internal buffering -
// the write must complete before the first n bytes read completes.
// Any error encountered while writing is reported as a read error.
func LimitTeeReader(r io.Reader, w io.Writer, n int64) io.Reader {
return &limitTeeReader{r, w, n}
}
type limitTeeReader struct {
r io.Reader
w io.Writer
n int64 // limit bytes remaining
}
func (t *limitTeeReader) Read(p []byte) (n int, err error) {
n, err = t.r.Read(p)
if n > 0 && t.n > 0 {
limit := int64(n)
if limit > t.n {
limit = t.n
}
if n, err := t.w.Write(p[:limit]); err != nil {
return n, err
}
t.n -= limit
}
return
}

40
core/iox/tee_test.go Normal file
View File

@@ -0,0 +1,40 @@
package iox
import (
"bytes"
"io"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLimitTeeReader(t *testing.T) {
limit := int64(4)
src := []byte("hello, world")
dst := make([]byte, len(src))
rb := bytes.NewBuffer(src)
wb := new(bytes.Buffer)
r := LimitTeeReader(rb, wb, limit)
if n, err := io.ReadFull(r, dst); err != nil || n != len(src) {
t.Fatalf("ReadFull(r, dst) = %d, %v; want %d, nil", n, err, len(src))
}
if !bytes.Equal(dst, src) {
t.Errorf("bytes read = %q want %q", dst, src)
}
if !bytes.Equal(wb.Bytes(), src[:limit]) {
t.Errorf("bytes written = %q want %q", wb.Bytes(), src)
}
n, err := r.Read(dst)
assert.Equal(t, 0, n)
assert.Equal(t, io.EOF, err)
rb = bytes.NewBuffer(src)
pr, pw := io.Pipe()
if assert.NoError(t, pr.Close()) {
r = LimitTeeReader(rb, pw, limit)
n, err := io.ReadFull(r, dst)
assert.Equal(t, 0, n)
assert.Equal(t, io.ErrClosedPipe, err)
}
}

View File

@@ -2,6 +2,7 @@ package iox
import (
"bytes"
"errors"
"io"
"os"
)
@@ -26,7 +27,7 @@ func CountLines(file string) (int, error) {
count += bytes.Count(buf[:c], lineSep)
switch {
case err == io.EOF:
case errors.Is(err, io.EOF):
if noEol {
count++
}

View File

@@ -24,3 +24,8 @@ func TestCountLines(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 4, lines)
}
func TestCountLinesError(t *testing.T) {
_, err := CountLines("not-exist")
assert.NotNil(t, err)
}

View File

@@ -3,6 +3,7 @@ package iox
import (
"strings"
"testing"
"testing/iotest"
"github.com/stretchr/testify/assert"
)
@@ -22,3 +23,10 @@ func TestScanner(t *testing.T) {
}
assert.EqualValues(t, []string{"1", "2", "3", "4"}, lines)
}
func TestBadScanner(t *testing.T) {
scanner := NewTextLineScanner(iotest.ErrReader(iotest.ErrTimeout))
assert.False(t, scanner.Scan())
_, err := scanner.Line()
assert.ErrorIs(t, err, iotest.ErrTimeout)
}

View File

@@ -9,21 +9,6 @@ import (
"github.com/zeromicro/go-zero/core/stores/redis"
)
// to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key
const periodScript = `local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call("INCRBY", KEYS[1], 1)
if current == 1 then
redis.call("expire", KEYS[1], window)
end
if current < limit then
return 1
elseif current == limit then
return 2
else
return 0
end`
const (
// Unknown means not initialized state.
Unknown = iota
@@ -39,8 +24,25 @@ const (
internalHitQuota = 2
)
// ErrUnknownCode is an error that represents unknown status code.
var ErrUnknownCode = errors.New("unknown status code")
var (
// ErrUnknownCode is an error that represents unknown status code.
ErrUnknownCode = errors.New("unknown status code")
// to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key
periodScript = redis.NewScript(`local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call("INCRBY", KEYS[1], 1)
if current == 1 then
redis.call("expire", KEYS[1], window)
end
if current < limit then
return 1
elseif current == limit then
return 2
else
return 0
end`)
)
type (
// PeriodOption defines the method to customize a PeriodLimit.
@@ -80,7 +82,7 @@ func (h *PeriodLimit) Take(key string) (int, error) {
// TakeCtx requests a permit with context, it returns the permit state.
func (h *PeriodLimit) TakeCtx(ctx context.Context, key string) (int, error) {
resp, err := h.limitStore.EvalCtx(ctx, periodScript, []string{h.keyPrefix + key}, []string{
resp, err := h.limitStore.ScriptRunCtx(ctx, periodScript, []string{h.keyPrefix + key}, []string{
strconv.Itoa(h.quota),
strconv.Itoa(h.calcExpireSeconds()),
})

View File

@@ -33,9 +33,7 @@ func TestPeriodLimit_RedisUnavailable(t *testing.T) {
}
func testPeriodLimit(t *testing.T, opts ...PeriodOption) {
store, clean, err := redistest.CreateRedis()
assert.Nil(t, err)
defer clean()
store := redistest.CreateRedis(t)
const (
seconds = 1

View File

@@ -15,10 +15,15 @@ import (
)
const (
// to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key
// KEYS[1] as tokens_key
// KEYS[2] as timestamp_key
script = `local rate = tonumber(ARGV[1])
tokenFormat = "{%s}.tokens"
timestampFormat = "{%s}.ts"
pingInterval = time.Millisecond * 100
)
// to be compatible with aliyun redis, we cannot use `local key = KEYS[1]` to reuse the key
// KEYS[1] as tokens_key
// KEYS[2] as timestamp_key
var script = redis.NewScript(`local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
@@ -45,11 +50,7 @@ end
redis.call("setex", KEYS[1], ttl, new_tokens)
redis.call("setex", KEYS[2], ttl, now)
return allowed`
tokenFormat = "{%s}.tokens"
timestampFormat = "{%s}.ts"
pingInterval = time.Millisecond * 100
)
return allowed`)
// A TokenLimiter controls how frequently events are allowed to happen with in one second.
type TokenLimiter struct {
@@ -110,7 +111,7 @@ func (lim *TokenLimiter) reserveN(ctx context.Context, now time.Time, n int) boo
return lim.rescueLimiter.AllowN(now, n)
}
resp, err := lim.store.EvalCtx(ctx,
resp, err := lim.store.ScriptRunCtx(ctx,
script,
[]string{
lim.tokenKey,

View File

@@ -70,9 +70,7 @@ func TestTokenLimit_Rescue(t *testing.T) {
}
func TestTokenLimit_Take(t *testing.T) {
store, clean, err := redistest.CreateRedis()
assert.Nil(t, err)
defer clean()
store := redistest.CreateRedis(t)
const (
total = 100
@@ -92,9 +90,7 @@ func TestTokenLimit_Take(t *testing.T) {
}
func TestTokenLimit_TakeBurst(t *testing.T) {
store, clean, err := redistest.CreateRedis()
assert.Nil(t, err)
defer clean()
store := redistest.CreateRedis(t)
const (
total = 100

View File

@@ -1,7 +1,6 @@
package logc
import (
"bytes"
"context"
"encoding/json"
"fmt"
@@ -11,14 +10,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/logx/logtest"
)
func TestAddGlobalFields(t *testing.T) {
var buf bytes.Buffer
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
Info(context.Background(), "hello")
buf.Reset()
@@ -34,155 +30,90 @@ func TestAddGlobalFields(t *testing.T) {
}
func TestAlert(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
Alert(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), "foo"), buf.String())
}
func TestError(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Error(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestErrorf(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Errorf(context.Background(), "foo %s", "bar")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestErrorv(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Errorv(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestErrorw(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Errorw(context.Background(), "foo", Field("a", "b"))
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestInfo(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Info(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestInfof(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Infof(context.Background(), "foo %s", "bar")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestInfov(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Infov(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestInfow(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Infow(context.Background(), "foo", Field("a", "b"))
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestDebug(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Debug(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestDebugf(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Debugf(context.Background(), "foo %s", "bar")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestDebugv(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Debugv(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestDebugw(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Debugw(context.Background(), "foo", Field("a", "b"))
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
@@ -204,48 +135,28 @@ func TestMisc(t *testing.T) {
}
func TestSlow(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Slow(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)), buf.String())
}
func TestSlowf(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Slowf(context.Background(), "foo %s", "bar")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)), buf.String())
}
func TestSlowv(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Slowv(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)), buf.String())
}
func TestSloww(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
buf := logtest.NewCollector(t)
file, line := getFileLine()
Sloww(context.Background(), "foo", Field("a", "b"))
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)), buf.String())

View File

@@ -23,7 +23,7 @@ type LogConf struct {
MaxContentLength uint32 `json:",optional"`
// Compress represents whether to compress the log file, default is `false`.
Compress bool `json:",optional"`
// Stdout represents whether to log statistics, default is `true`.
// Stat represents whether to log statistics, default is `true`.
Stat bool `json:",default=true"`
// KeepDays represents how many days the log files will be kept. Default to keep all files.
// Only take effect when Mode is `file` or `volume`, both work when Rotation is `daily` or `size`.
@@ -32,13 +32,13 @@ type LogConf struct {
StackCooldownMillis int `json:",default=100"`
// MaxBackups represents how many backup log files will be kept. 0 means all files will be kept forever.
// Only take effect when RotationRuleType is `size`.
// Even thougth `MaxBackups` sets 0, log files will still be removed
// Even though `MaxBackups` sets 0, log files will still be removed
// if the `KeepDays` limitation is reached.
MaxBackups int `json:",default=0"`
// MaxSize represents how much space the writing log file takes up. 0 means no limit. The unit is `MB`.
// Only take effect when RotationRuleType is `size`
MaxSize int `json:",default=0"`
// RotationRuleType represents the type of log rotation rule. Default is `daily`.
// Rotation represents the type of log rotation rule. Default is `daily`.
// daily: daily rotation.
// size: size limited rotation.
Rotation string `json:",default=daily,options=[daily,size]"`

40
core/logx/fs.go Normal file
View File

@@ -0,0 +1,40 @@
package logx
import (
"io"
"os"
)
var fileSys realFileSystem
type (
fileSystem interface {
Close(closer io.Closer) error
Copy(writer io.Writer, reader io.Reader) (int64, error)
Create(name string) (*os.File, error)
Open(name string) (*os.File, error)
Remove(name string) error
}
realFileSystem struct{}
)
func (fs realFileSystem) Close(closer io.Closer) error {
return closer.Close()
}
func (fs realFileSystem) Copy(writer io.Writer, reader io.Reader) (int64, error) {
return io.Copy(writer, reader)
}
func (fs realFileSystem) Create(name string) (*os.File, error) {
return os.Create(name)
}
func (fs realFileSystem) Open(name string) (*os.File, error) {
return os.Open(name)
}
func (fs realFileSystem) Remove(name string) error {
return os.Remove(name)
}

View File

@@ -68,22 +68,30 @@ func Close() error {
// Debug writes v into access log.
func Debug(v ...any) {
writeDebug(fmt.Sprint(v...))
if shallLog(DebugLevel) {
writeDebug(fmt.Sprint(v...))
}
}
// Debugf writes v with format into access log.
func Debugf(format string, v ...any) {
writeDebug(fmt.Sprintf(format, v...))
if shallLog(DebugLevel) {
writeDebug(fmt.Sprintf(format, v...))
}
}
// Debugv writes v into access log with json content.
func Debugv(v any) {
writeDebug(v)
if shallLog(DebugLevel) {
writeDebug(v)
}
}
// Debugw writes msg along with fields into access log.
func Debugw(msg string, fields ...LogField) {
writeDebug(msg, fields...)
if shallLog(DebugLevel) {
writeDebug(msg, fields...)
}
}
// Disable disables the logging.
@@ -99,35 +107,47 @@ func DisableStat() {
// Error writes v into error log.
func Error(v ...any) {
writeError(fmt.Sprint(v...))
if shallLog(ErrorLevel) {
writeError(fmt.Sprint(v...))
}
}
// Errorf writes v with format into error log.
func Errorf(format string, v ...any) {
writeError(fmt.Errorf(format, v...).Error())
if shallLog(ErrorLevel) {
writeError(fmt.Errorf(format, v...).Error())
}
}
// ErrorStack writes v along with call stack into error log.
func ErrorStack(v ...any) {
// there is newline in stack string
writeStack(fmt.Sprint(v...))
if shallLog(ErrorLevel) {
// there is newline in stack string
writeStack(fmt.Sprint(v...))
}
}
// ErrorStackf writes v along with call stack in format into error log.
func ErrorStackf(format string, v ...any) {
// there is newline in stack string
writeStack(fmt.Sprintf(format, v...))
if shallLog(ErrorLevel) {
// there is newline in stack string
writeStack(fmt.Sprintf(format, v...))
}
}
// Errorv writes v into error log with json content.
// No call stack attached, because not elegant to pack the messages.
func Errorv(v any) {
writeError(v)
if shallLog(ErrorLevel) {
writeError(v)
}
}
// Errorw writes msg along with fields into error log.
func Errorw(msg string, fields ...LogField) {
writeError(msg, fields...)
if shallLog(ErrorLevel) {
writeError(msg, fields...)
}
}
// Field returns a LogField for the given key and value.
@@ -170,22 +190,30 @@ func Field(key string, value any) LogField {
// Info writes v into access log.
func Info(v ...any) {
writeInfo(fmt.Sprint(v...))
if shallLog(InfoLevel) {
writeInfo(fmt.Sprint(v...))
}
}
// Infof writes v with format into access log.
func Infof(format string, v ...any) {
writeInfo(fmt.Sprintf(format, v...))
if shallLog(InfoLevel) {
writeInfo(fmt.Sprintf(format, v...))
}
}
// Infov writes v into access log with json content.
func Infov(v any) {
writeInfo(v)
if shallLog(InfoLevel) {
writeInfo(v)
}
}
// Infow writes msg along with fields into access log.
func Infow(msg string, fields ...LogField) {
writeInfo(msg, fields...)
if shallLog(InfoLevel) {
writeInfo(msg, fields...)
}
}
// Must checks if err is nil, otherwise logs the error and exits.
@@ -194,10 +222,15 @@ func Must(err error) {
return
}
msg := err.Error()
msg := fmt.Sprintf("%+v\n\n%s", err.Error(), debug.Stack())
log.Print(msg)
getWriter().Severe(msg)
os.Exit(1)
if ExitOnFatal.True() {
os.Exit(1)
} else {
panic(msg)
}
}
// MustSetup sets up logging with given config c. It exits on error.
@@ -264,42 +297,58 @@ func SetUp(c LogConf) (err error) {
// Severe writes v into severe log.
func Severe(v ...any) {
writeSevere(fmt.Sprint(v...))
if shallLog(SevereLevel) {
writeSevere(fmt.Sprint(v...))
}
}
// Severef writes v with format into severe log.
func Severef(format string, v ...any) {
writeSevere(fmt.Sprintf(format, v...))
if shallLog(SevereLevel) {
writeSevere(fmt.Sprintf(format, v...))
}
}
// Slow writes v into slow log.
func Slow(v ...any) {
writeSlow(fmt.Sprint(v...))
if shallLog(ErrorLevel) {
writeSlow(fmt.Sprint(v...))
}
}
// Slowf writes v with format into slow log.
func Slowf(format string, v ...any) {
writeSlow(fmt.Sprintf(format, v...))
if shallLog(ErrorLevel) {
writeSlow(fmt.Sprintf(format, v...))
}
}
// Slowv writes v into slow log with json content.
func Slowv(v any) {
writeSlow(v)
if shallLog(ErrorLevel) {
writeSlow(v)
}
}
// Sloww writes msg along with fields into slow log.
func Sloww(msg string, fields ...LogField) {
writeSlow(msg, fields...)
if shallLog(ErrorLevel) {
writeSlow(msg, fields...)
}
}
// Stat writes v into stat log.
func Stat(v ...any) {
writeStat(fmt.Sprint(v...))
if shallLogStat() && shallLog(InfoLevel) {
writeStat(fmt.Sprint(v...))
}
}
// Statf writes v with format into stat log.
func Statf(format string, v ...any) {
writeStat(fmt.Sprintf(format, v...))
if shallLogStat() && shallLog(InfoLevel) {
writeStat(fmt.Sprintf(format, v...))
}
}
// WithCooldownMillis customizes logging on writing call stack interval.
@@ -353,14 +402,16 @@ func createOutput(path string) (io.WriteCloser, error) {
return nil, ErrLogPathNotSet
}
var rule RotateRule
switch options.rotationRule {
case sizeRotationRule:
return NewLogger(path, NewSizeLimitRotateRule(path, backupFileDelimiter, options.keepDays,
options.maxSize, options.maxBackups, options.gzipEnabled), options.gzipEnabled)
rule = NewSizeLimitRotateRule(path, backupFileDelimiter, options.keepDays, options.maxSize,
options.maxBackups, options.gzipEnabled)
default:
return NewLogger(path, DefaultRotateRule(path, backupFileDelimiter, options.keepDays,
options.gzipEnabled), options.gzipEnabled)
rule = DefaultRotateRule(path, backupFileDelimiter, options.keepDays, options.gzipEnabled)
}
return NewLogger(path, rule, options.gzipEnabled)
}
func getWriter() Writer {
@@ -422,44 +473,58 @@ func shallLogStat() bool {
return atomic.LoadUint32(&disableStat) == 0
}
// writeDebug writes v into debug log.
// Not checking shallLog here is for performance consideration.
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function.
func writeDebug(val any, fields ...LogField) {
if shallLog(DebugLevel) {
getWriter().Debug(val, addCaller(fields...)...)
}
getWriter().Debug(val, addCaller(fields...)...)
}
// writeError writes v into error log.
// Not checking shallLog here is for performance consideration.
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function.
func writeError(val any, fields ...LogField) {
if shallLog(ErrorLevel) {
getWriter().Error(val, addCaller(fields...)...)
}
getWriter().Error(val, addCaller(fields...)...)
}
// writeInfo writes v into info log.
// Not checking shallLog here is for performance consideration.
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function.
func writeInfo(val any, fields ...LogField) {
if shallLog(InfoLevel) {
getWriter().Info(val, addCaller(fields...)...)
}
getWriter().Info(val, addCaller(fields...)...)
}
// writeSevere writes v into severe log.
// Not checking shallLog here is for performance consideration.
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function.
func writeSevere(msg string) {
if shallLog(SevereLevel) {
getWriter().Severe(fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
}
getWriter().Severe(fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
}
// writeSlow writes v into slow log.
// Not checking shallLog here is for performance consideration.
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function.
func writeSlow(val any, fields ...LogField) {
if shallLog(ErrorLevel) {
getWriter().Slow(val, addCaller(fields...)...)
}
getWriter().Slow(val, addCaller(fields...)...)
}
// writeStack writes v into stack log.
// Not checking shallLog here is for performance consideration.
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function.
func writeStack(msg string) {
if shallLog(ErrorLevel) {
getWriter().Stack(fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
}
getWriter().Stack(fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
}
// writeStat writes v into stat log.
// Not checking shallLog here is for performance consideration.
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function.
func writeStat(msg string) {
if shallLogStat() && shallLog(InfoLevel) {
getWriter().Stat(msg, addCaller()...)
}
getWriter().Stat(msg, addCaller()...)
}

View File

@@ -24,6 +24,10 @@ var (
_ Writer = (*mockWriter)(nil)
)
func init() {
ExitOnFatal.Set(false)
}
type mockWriter struct {
lock sync.Mutex
builder strings.Builder
@@ -208,6 +212,12 @@ func TestFileLineConsoleMode(t *testing.T) {
assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1)))
}
func TestMust(t *testing.T) {
assert.Panics(t, func() {
Must(errors.New("foo"))
})
}
func TestStructedLogAlert(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
@@ -574,26 +584,38 @@ func TestSetup(t *testing.T) {
atomic.StoreUint32(&encoding, jsonEncodingType)
}()
setupOnce = sync.Once{}
MustSetup(LogConf{
ServiceName: "any",
Mode: "console",
Encoding: "json",
TimeFormat: timeFormat,
})
setupOnce = sync.Once{}
MustSetup(LogConf{
ServiceName: "any",
Mode: "console",
TimeFormat: timeFormat,
})
setupOnce = sync.Once{}
MustSetup(LogConf{
ServiceName: "any",
Mode: "file",
Path: os.TempDir(),
})
setupOnce = sync.Once{}
MustSetup(LogConf{
ServiceName: "any",
Mode: "volume",
Path: os.TempDir(),
})
setupOnce = sync.Once{}
MustSetup(LogConf{
ServiceName: "any",
Mode: "console",
TimeFormat: timeFormat,
})
setupOnce = sync.Once{}
MustSetup(LogConf{
ServiceName: "any",
Mode: "console",

View File

@@ -0,0 +1,84 @@
package logtest
import (
"bytes"
"encoding/json"
"io"
"testing"
"github.com/zeromicro/go-zero/core/logx"
)
type Buffer struct {
buf *bytes.Buffer
t *testing.T
}
func Discard(t *testing.T) {
prev := logx.Reset()
logx.SetWriter(logx.NewWriter(io.Discard))
t.Cleanup(func() {
logx.SetWriter(prev)
})
}
func NewCollector(t *testing.T) *Buffer {
var buf bytes.Buffer
writer := logx.NewWriter(&buf)
prev := logx.Reset()
logx.SetWriter(writer)
t.Cleanup(func() {
logx.SetWriter(prev)
})
return &Buffer{
buf: &buf,
t: t,
}
}
func (b *Buffer) Bytes() []byte {
return b.buf.Bytes()
}
func (b *Buffer) Content() string {
var m map[string]interface{}
if err := json.Unmarshal(b.buf.Bytes(), &m); err != nil {
return ""
}
content, ok := m["content"]
if !ok {
return ""
}
switch val := content.(type) {
case string:
return val
default:
// err is impossible to be not nil, unmarshaled from b.buf.Bytes()
bs, _ := json.Marshal(content)
return string(bs)
}
}
func (b *Buffer) Reset() {
b.buf.Reset()
}
func (b *Buffer) String() string {
return b.buf.String()
}
func PanicOnFatal(t *testing.T) {
ok := logx.ExitOnFatal.CompareAndSwap(true, false)
if !ok {
return
}
t.Cleanup(func() {
logx.ExitOnFatal.CompareAndSwap(false, true)
})
}

View File

@@ -0,0 +1,44 @@
package logtest
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
)
func TestCollector(t *testing.T) {
const input = "hello"
c := NewCollector(t)
logx.Info(input)
assert.Equal(t, input, c.Content())
assert.Contains(t, c.String(), input)
c.Reset()
assert.Empty(t, c.Bytes())
}
func TestPanicOnFatal(t *testing.T) {
const input = "hello"
Discard(t)
logx.Info(input)
PanicOnFatal(t)
PanicOnFatal(t)
assert.Panics(t, func() {
logx.Must(errors.New("foo"))
})
}
func TestCollectorContent(t *testing.T) {
const input = "hello"
c := NewCollector(t)
c.buf.WriteString(input)
assert.Empty(t, c.Content())
c.Reset()
c.buf.WriteString(`{}`)
assert.Empty(t, c.Content())
c.Reset()
c.buf.WriteString(`{"content":1}`)
assert.Equal(t, "1", c.Content())
}

View File

@@ -40,7 +40,7 @@ type LogConf struct {
- `Compress`: whether or not to compress log files, only works with `file` mode.
- `KeepDays`: how many days that the log files are kept, after the given days, the outdated files will be deleted automatically. It has no effect on `console` mode.
- `StackCooldownMillis`: how many milliseconds to rewrite stacktrace again. Its used to avoid stacktrace flooding.
- `MaxBackups`: represents how many backup log files will be kept. 0 means all files will be kept forever. Only take effect when `Rotation` is `size`. NOTE: the level of option `KeepDays` will be higher. Even thougth `MaxBackups` sets 0, log files will still be removed if the `KeepDays` limitation is reached.
- `MaxBackups`: represents how many backup log files will be kept. 0 means all files will be kept forever. Only take effect when `Rotation` is `size`. NOTE: the level of option `KeepDays` will be higher. Even though `MaxBackups` sets 0, log files will still be removed if the `KeepDays` limitation is reached.
- `MaxSize`: represents how much space the writing log file takes up. 0 means no limit. The unit is `MB`. Only take effect when `Rotation` is `size`.
- `Rotation`: represents the type of log rotation rule. Default is `daily`.
- `daily` rotate the logs by day.

View File

@@ -41,67 +41,99 @@ type richLogger struct {
}
func (l *richLogger) Debug(v ...any) {
l.debug(fmt.Sprint(v...))
if shallLog(DebugLevel) {
l.debug(fmt.Sprint(v...))
}
}
func (l *richLogger) Debugf(format string, v ...any) {
l.debug(fmt.Sprintf(format, v...))
if shallLog(DebugLevel) {
l.debug(fmt.Sprintf(format, v...))
}
}
func (l *richLogger) Debugv(v any) {
l.debug(v)
if shallLog(DebugLevel) {
l.debug(v)
}
}
func (l *richLogger) Debugw(msg string, fields ...LogField) {
l.debug(msg, fields...)
if shallLog(DebugLevel) {
l.debug(msg, fields...)
}
}
func (l *richLogger) Error(v ...any) {
l.err(fmt.Sprint(v...))
if shallLog(ErrorLevel) {
l.err(fmt.Sprint(v...))
}
}
func (l *richLogger) Errorf(format string, v ...any) {
l.err(fmt.Sprintf(format, v...))
if shallLog(ErrorLevel) {
l.err(fmt.Sprintf(format, v...))
}
}
func (l *richLogger) Errorv(v any) {
l.err(fmt.Sprint(v))
if shallLog(ErrorLevel) {
l.err(v)
}
}
func (l *richLogger) Errorw(msg string, fields ...LogField) {
l.err(msg, fields...)
if shallLog(ErrorLevel) {
l.err(msg, fields...)
}
}
func (l *richLogger) Info(v ...any) {
l.info(fmt.Sprint(v...))
if shallLog(InfoLevel) {
l.info(fmt.Sprint(v...))
}
}
func (l *richLogger) Infof(format string, v ...any) {
l.info(fmt.Sprintf(format, v...))
if shallLog(InfoLevel) {
l.info(fmt.Sprintf(format, v...))
}
}
func (l *richLogger) Infov(v any) {
l.info(v)
if shallLog(InfoLevel) {
l.info(v)
}
}
func (l *richLogger) Infow(msg string, fields ...LogField) {
l.info(msg, fields...)
if shallLog(InfoLevel) {
l.info(msg, fields...)
}
}
func (l *richLogger) Slow(v ...any) {
l.slow(fmt.Sprint(v...))
if shallLog(ErrorLevel) {
l.slow(fmt.Sprint(v...))
}
}
func (l *richLogger) Slowf(format string, v ...any) {
l.slow(fmt.Sprintf(format, v...))
if shallLog(ErrorLevel) {
l.slow(fmt.Sprintf(format, v...))
}
}
func (l *richLogger) Slowv(v any) {
l.slow(v)
if shallLog(ErrorLevel) {
l.slow(v)
}
}
func (l *richLogger) Sloww(msg string, fields ...LogField) {
l.slow(msg, fields...)
if shallLog(ErrorLevel) {
l.slow(msg, fields...)
}
}
func (l *richLogger) WithCallerSkip(skip int) Logger {

View File

@@ -66,6 +66,9 @@ func TestTraceDebug(t *testing.T) {
l.WithDuration(time.Second).Debugv(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Debugv(testobj)
validateContentType(t, w.String(), map[string]any{}, true, true)
w.Reset()
l.WithDuration(time.Second).Debugw(testlog, Field("foo", "bar"))
validate(t, w.String(), true, true)
assert.True(t, strings.Contains(w.String(), "foo"), w.String())
@@ -103,6 +106,9 @@ func TestTraceError(t *testing.T) {
l.WithDuration(time.Second).Errorv(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Errorv(testobj)
validateContentType(t, w.String(), map[string]any{}, true, true)
w.Reset()
l.WithDuration(time.Second).Errorw(testlog, Field("basket", "ball"))
validate(t, w.String(), true, true)
assert.True(t, strings.Contains(w.String(), "basket"), w.String())
@@ -137,6 +143,9 @@ func TestTraceInfo(t *testing.T) {
l.WithDuration(time.Second).Infov(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Infov(testobj)
validateContentType(t, w.String(), map[string]any{}, true, true)
w.Reset()
l.WithDuration(time.Second).Infow(testlog, Field("basket", "ball"))
validate(t, w.String(), true, true)
assert.True(t, strings.Contains(w.String(), "basket"), w.String())
@@ -173,6 +182,9 @@ func TestTraceInfoConsole(t *testing.T) {
w.Reset()
l.WithDuration(time.Second).Infov(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Infov(testobj)
validateContentType(t, w.String(), map[string]any{}, true, true)
}
func TestTraceSlow(t *testing.T) {
@@ -204,6 +216,9 @@ func TestTraceSlow(t *testing.T) {
l.WithDuration(time.Second).Slowv(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Slowv(testobj)
validateContentType(t, w.String(), map[string]any{}, true, true)
w.Reset()
l.WithDuration(time.Second).Sloww(testlog, Field("basket", "ball"))
validate(t, w.String(), true, true)
assert.True(t, strings.Contains(w.String(), "basket"), w.String())
@@ -311,8 +326,32 @@ func validate(t *testing.T, body string, expectedTrace, expectedSpan bool) {
assert.Equal(t, expectedSpan, len(val.Span) > 0, body)
}
type mockValue struct {
Trace string `json:"trace"`
Span string `json:"span"`
Foo string `json:"foo"`
func validateContentType(t *testing.T, body string, expectedType any, expectedTrace, expectedSpan bool) {
var val mockValue
dec := json.NewDecoder(strings.NewReader(body))
for {
var doc mockValue
err := dec.Decode(&doc)
if err == io.EOF {
// all done
break
}
if err != nil {
continue
}
val = doc
}
assert.IsType(t, expectedType, val.Content, body)
assert.Equal(t, expectedTrace, len(val.Trace) > 0, body)
assert.Equal(t, expectedSpan, len(val.Span) > 0, body)
}
type mockValue struct {
Trace string `json:"trace"`
Span string `json:"span"`
Foo string `json:"foo"`
Content any `json:"content"`
}

View File

@@ -4,7 +4,6 @@ import (
"compress/gzip"
"errors"
"fmt"
"io"
"log"
"os"
"path"
@@ -237,7 +236,7 @@ func NewLogger(filename string, rule RotateRule, compress bool) (*RotateLogger,
rule: rule,
compress: compress,
}
if err := l.init(); err != nil {
if err := l.initialize(); err != nil {
return nil, err
}
@@ -281,7 +280,7 @@ func (l *RotateLogger) getBackupFilename() string {
return l.backup
}
func (l *RotateLogger) init() error {
func (l *RotateLogger) initialize() error {
l.backup = l.rule.BackupFileName()
if fileInfo, err := os.Stat(l.filename); err != nil {
@@ -299,6 +298,7 @@ func (l *RotateLogger) init() error {
if l.fp, err = os.OpenFile(l.filename, os.O_APPEND|os.O_WRONLY, defaultFileMode); err != nil {
return err
}
l.currentSize = fileInfo.Size()
}
@@ -382,7 +382,15 @@ func (l *RotateLogger) startWorker() {
case event := <-l.channel:
l.write(event)
case <-l.done:
return
// avoid losing logs before closing.
for {
select {
case event := <-l.channel:
l.write(event)
default:
return
}
}
}
}
}()
@@ -406,7 +414,7 @@ func (l *RotateLogger) write(v []byte) {
func compressLogFile(file string) {
start := time.Now()
Infof("compressing log file: %s", file)
if err := gzipFile(file); err != nil {
if err := gzipFile(file, fileSys); err != nil {
Errorf("compress error: %s", err)
} else {
Infof("compressed log file: %s, took %s", file, time.Since(start))
@@ -421,25 +429,37 @@ func getNowDateInRFC3339Format() string {
return time.Now().Format(fileTimeFormat)
}
func gzipFile(file string) error {
in, err := os.Open(file)
func gzipFile(file string, fsys fileSystem) (err error) {
in, err := fsys.Open(file)
if err != nil {
return err
}
defer in.Close()
defer func() {
if e := fsys.Close(in); e != nil {
Errorf("failed to close file: %s, error: %v", file, e)
}
if err == nil {
// only remove the original file when compression is successful
err = fsys.Remove(file)
}
}()
out, err := os.Create(fmt.Sprintf("%s%s", file, gzipExt))
out, err := fsys.Create(fmt.Sprintf("%s%s", file, gzipExt))
if err != nil {
return err
}
defer out.Close()
defer func() {
e := fsys.Close(out)
if err == nil {
err = e
}
}()
w := gzip.NewWriter(out)
if _, err = io.Copy(w, in); err != nil {
return err
} else if err = w.Close(); err != nil {
if _, err = fsys.Copy(w, in); err != nil {
// failed to copy, no need to close w
return err
}
return os.Remove(file)
return fsys.Close(w)
}

View File

@@ -1,8 +1,12 @@
package logx
import (
"errors"
"io"
"os"
"path"
"path/filepath"
"sync/atomic"
"syscall"
"testing"
"time"
@@ -13,18 +17,58 @@ import (
)
func TestDailyRotateRuleMarkRotated(t *testing.T) {
var rule DailyRotateRule
rule.MarkRotated()
assert.Equal(t, getNowDate(), rule.rotatedTime)
t.Run("daily rule", func(t *testing.T) {
var rule DailyRotateRule
rule.MarkRotated()
assert.Equal(t, getNowDate(), rule.rotatedTime)
})
t.Run("daily rule", func(t *testing.T) {
rule := DefaultRotateRule("test", "-", 1, false)
_, ok := rule.(*DailyRotateRule)
assert.True(t, ok)
})
}
func TestDailyRotateRuleOutdatedFiles(t *testing.T) {
var rule DailyRotateRule
assert.Empty(t, rule.OutdatedFiles())
rule.days = 1
assert.Empty(t, rule.OutdatedFiles())
rule.gzip = true
assert.Empty(t, rule.OutdatedFiles())
t.Run("no files", func(t *testing.T) {
var rule DailyRotateRule
assert.Empty(t, rule.OutdatedFiles())
rule.days = 1
assert.Empty(t, rule.OutdatedFiles())
rule.gzip = true
assert.Empty(t, rule.OutdatedFiles())
})
t.Run("bad files", func(t *testing.T) {
rule := DailyRotateRule{
filename: "[a-z",
}
assert.Empty(t, rule.OutdatedFiles())
rule.days = 1
assert.Empty(t, rule.OutdatedFiles())
rule.gzip = true
assert.Empty(t, rule.OutdatedFiles())
})
t.Run("temp files", func(t *testing.T) {
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
assert.NoError(t, err)
_ = f1.Close()
f2, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
assert.NoError(t, err)
_ = f2.Close()
t.Cleanup(func() {
_ = os.Remove(f1.Name())
_ = os.Remove(f2.Name())
})
rule := DailyRotateRule{
filename: path.Join(os.TempDir(), "go-zero-test-"),
days: 1,
}
assert.NotEmpty(t, rule.OutdatedFiles())
})
}
func TestDailyRotateRuleShallRotate(t *testing.T) {
@@ -34,20 +78,101 @@ func TestDailyRotateRuleShallRotate(t *testing.T) {
}
func TestSizeLimitRotateRuleMarkRotated(t *testing.T) {
var rule SizeLimitRotateRule
rule.MarkRotated()
assert.Equal(t, getNowDateInRFC3339Format(), rule.rotatedTime)
t.Run("size limit rule", func(t *testing.T) {
var rule SizeLimitRotateRule
rule.MarkRotated()
assert.Equal(t, getNowDateInRFC3339Format(), rule.rotatedTime)
})
t.Run("size limit rule", func(t *testing.T) {
rule := NewSizeLimitRotateRule("foo", "-", 1, 1, 1, false)
rule.MarkRotated()
assert.Equal(t, getNowDateInRFC3339Format(), rule.(*SizeLimitRotateRule).rotatedTime)
})
}
func TestSizeLimitRotateRuleOutdatedFiles(t *testing.T) {
var rule SizeLimitRotateRule
assert.Empty(t, rule.OutdatedFiles())
rule.days = 1
assert.Empty(t, rule.OutdatedFiles())
rule.gzip = true
assert.Empty(t, rule.OutdatedFiles())
rule.maxBackups = 0
assert.Empty(t, rule.OutdatedFiles())
t.Run("no files", func(t *testing.T) {
var rule SizeLimitRotateRule
assert.Empty(t, rule.OutdatedFiles())
rule.days = 1
assert.Empty(t, rule.OutdatedFiles())
rule.gzip = true
assert.Empty(t, rule.OutdatedFiles())
rule.maxBackups = 0
assert.Empty(t, rule.OutdatedFiles())
})
t.Run("bad files", func(t *testing.T) {
rule := SizeLimitRotateRule{
DailyRotateRule: DailyRotateRule{
filename: "[a-z",
},
}
assert.Empty(t, rule.OutdatedFiles())
rule.days = 1
assert.Empty(t, rule.OutdatedFiles())
rule.gzip = true
assert.Empty(t, rule.OutdatedFiles())
})
t.Run("temp files", func(t *testing.T) {
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
assert.NoError(t, err)
f2, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
assert.NoError(t, err)
boundary1 := time.Now().Add(time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
f3, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary1)
assert.NoError(t, err)
t.Cleanup(func() {
_ = f1.Close()
_ = os.Remove(f1.Name())
_ = f2.Close()
_ = os.Remove(f2.Name())
_ = f3.Close()
_ = os.Remove(f3.Name())
})
rule := SizeLimitRotateRule{
DailyRotateRule: DailyRotateRule{
filename: path.Join(os.TempDir(), "go-zero-test-"),
days: 1,
},
maxBackups: 3,
}
assert.NotEmpty(t, rule.OutdatedFiles())
})
t.Run("no backups", func(t *testing.T) {
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
assert.NoError(t, err)
f2, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
assert.NoError(t, err)
boundary1 := time.Now().Add(time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
f3, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary1)
assert.NoError(t, err)
t.Cleanup(func() {
_ = f1.Close()
_ = os.Remove(f1.Name())
_ = f2.Close()
_ = os.Remove(f2.Name())
_ = f3.Close()
_ = os.Remove(f3.Name())
})
rule := SizeLimitRotateRule{
DailyRotateRule: DailyRotateRule{
filename: path.Join(os.TempDir(), "go-zero-test-"),
days: 1,
},
}
assert.NotEmpty(t, rule.OutdatedFiles())
logger := new(RotateLogger)
logger.rule = &rule
logger.maybeDeleteOutdatedFiles()
assert.Empty(t, rule.OutdatedFiles())
})
}
func TestSizeLimitRotateRuleShallRotate(t *testing.T) {
@@ -61,14 +186,47 @@ func TestSizeLimitRotateRuleShallRotate(t *testing.T) {
}
func TestRotateLoggerClose(t *testing.T) {
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
if len(filename) > 0 {
defer os.Remove(filename)
}
logger, err := NewLogger(filename, new(DailyRotateRule), false)
assert.Nil(t, err)
assert.Nil(t, logger.Close())
t.Run("close", func(t *testing.T) {
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
if len(filename) > 0 {
defer os.Remove(filename)
}
logger, err := NewLogger(filename, new(DailyRotateRule), false)
assert.Nil(t, err)
_, err = logger.Write([]byte("foo"))
assert.Nil(t, err)
assert.Nil(t, logger.Close())
})
t.Run("close and write", func(t *testing.T) {
logger := new(RotateLogger)
logger.done = make(chan struct{})
close(logger.done)
_, err := logger.Write([]byte("foo"))
assert.ErrorIs(t, err, ErrLogFileClosed)
})
t.Run("close without losing logs", func(t *testing.T) {
text := "foo"
filename, err := fs.TempFilenameWithText(text)
assert.Nil(t, err)
if len(filename) > 0 {
defer os.Remove(filename)
}
logger, err := NewLogger(filename, new(DailyRotateRule), false)
assert.Nil(t, err)
msg := []byte("foo")
n := 100
for i := 0; i < n; i++ {
_, err = logger.Write(msg)
assert.Nil(t, err)
}
assert.Nil(t, logger.Close())
bs, err := os.ReadFile(filename)
assert.Nil(t, err)
assert.Equal(t, len(msg)*n+len(text), len(bs))
})
}
func TestRotateLoggerGetBackupFilename(t *testing.T) {
@@ -179,7 +337,7 @@ func TestRotateLoggerWithSizeLimitRotateRuleClose(t *testing.T) {
}
logger, err := NewLogger(filename, new(SizeLimitRotateRule), false)
assert.Nil(t, err)
assert.Nil(t, logger.Close())
_ = logger.Close()
}
func TestRotateLoggerGetBackupWithSizeLimitRotateRuleFilename(t *testing.T) {
@@ -295,6 +453,85 @@ func TestRotateLoggerWithSizeLimitRotateRuleWrite(t *testing.T) {
logger.write([]byte(`baz`))
}
func TestGzipFile(t *testing.T) {
err := errors.New("any error")
t.Run("gzip file open failed", func(t *testing.T) {
fsys := &fakeFileSystem{
openFn: func(name string) (*os.File, error) {
return nil, err
},
}
assert.ErrorIs(t, err, gzipFile("any", fsys))
assert.False(t, fsys.Removed())
})
t.Run("gzip file create failed", func(t *testing.T) {
fsys := &fakeFileSystem{
createFn: func(name string) (*os.File, error) {
return nil, err
},
}
assert.ErrorIs(t, err, gzipFile("any", fsys))
assert.False(t, fsys.Removed())
})
t.Run("gzip file copy failed", func(t *testing.T) {
fsys := &fakeFileSystem{
copyFn: func(writer io.Writer, reader io.Reader) (int64, error) {
return 0, err
},
}
assert.ErrorIs(t, err, gzipFile("any", fsys))
assert.False(t, fsys.Removed())
})
t.Run("gzip file last close failed", func(t *testing.T) {
var called int32
fsys := &fakeFileSystem{
closeFn: func(closer io.Closer) error {
if atomic.AddInt32(&called, 1) > 2 {
return err
}
return nil
},
}
assert.NoError(t, gzipFile("any", fsys))
assert.True(t, fsys.Removed())
})
t.Run("gzip file remove failed", func(t *testing.T) {
fsys := &fakeFileSystem{
removeFn: func(name string) error {
return err
},
}
assert.Error(t, err, gzipFile("any", fsys))
assert.True(t, fsys.Removed())
})
t.Run("gzip file everything ok", func(t *testing.T) {
fsys := &fakeFileSystem{}
assert.NoError(t, gzipFile("any", fsys))
assert.True(t, fsys.Removed())
})
}
func TestRotateLogger_WithExistingFile(t *testing.T) {
const body = "foo"
filename, err := fs.TempFilenameWithText(body)
assert.Nil(t, err)
if len(filename) > 0 {
defer os.Remove(filename)
}
rule := NewSizeLimitRotateRule(filename, "-", 1, 100, 3, false)
logger, err := NewLogger(filename, rule, false)
assert.Nil(t, err)
assert.Equal(t, int64(len(body)), logger.currentSize)
assert.Nil(t, logger.Close())
}
func BenchmarkRotateLogger(b *testing.B) {
filename := "./test.log"
filename2 := "./test2.log"
@@ -346,3 +583,53 @@ func BenchmarkRotateLogger(b *testing.B) {
}
})
}
type fakeFileSystem struct {
removed int32
closeFn func(closer io.Closer) error
copyFn func(writer io.Writer, reader io.Reader) (int64, error)
createFn func(name string) (*os.File, error)
openFn func(name string) (*os.File, error)
removeFn func(name string) error
}
func (f *fakeFileSystem) Close(closer io.Closer) error {
if f.closeFn != nil {
return f.closeFn(closer)
}
return nil
}
func (f *fakeFileSystem) Copy(writer io.Writer, reader io.Reader) (int64, error) {
if f.copyFn != nil {
return f.copyFn(writer, reader)
}
return 0, nil
}
func (f *fakeFileSystem) Create(name string) (*os.File, error) {
if f.createFn != nil {
return f.createFn(name)
}
return nil, nil
}
func (f *fakeFileSystem) Open(name string) (*os.File, error) {
if f.openFn != nil {
return f.openFn(name)
}
return nil, nil
}
func (f *fakeFileSystem) Remove(name string) error {
atomic.AddInt32(&f.removed, 1)
if f.removeFn != nil {
return f.removeFn(name)
}
return nil
}
func (f *fakeFileSystem) Removed() bool {
return atomic.LoadInt32(&f.removed) > 0
}

View File

@@ -12,6 +12,8 @@ import (
const testlog = "Stay hungry, stay foolish."
var testobj = map[string]any{"foo": "bar"}
func TestCollectSysLog(t *testing.T) {
CollectSysLog()
content := getContent(captureOutput(func() {

View File

@@ -1,6 +1,10 @@
package logx
import "errors"
import (
"errors"
"github.com/zeromicro/go-zero/core/syncx"
)
const (
// DebugLevel logs everything
@@ -61,6 +65,8 @@ var (
ErrLogPathNotSet = errors.New("log path must be set")
// ErrLogServiceNameNotSet is an error that indicates that the service name is not set.
ErrLogServiceNameNotSet = errors.New("log service name must be set")
// ExitOnFatal defines whether to exit on fatal errors, defined here to make it easier to test.
ExitOnFatal = syncx.ForAtomicBool(true)
truncatedField = Field(truncatedKey, true)
)

View File

@@ -7,6 +7,7 @@ import (
"io"
"log"
"path"
"runtime/debug"
"sync"
"sync/atomic"
@@ -332,11 +333,13 @@ func wrapLevelWithColor(level string) string {
func writeJson(writer io.Writer, info any) {
if content, err := json.Marshal(info); err != nil {
log.Println(err.Error())
log.Printf("err: %s\n\n%s", err.Error(), debug.Stack())
} else if writer == nil {
log.Println(string(content))
} else {
writer.Write(append(content, '\n'))
if _, err := writer.Write(append(content, '\n')); err != nil {
log.Println(err.Error())
}
}
}
@@ -384,7 +387,7 @@ func writePlainValue(writer io.Writer, level string, val any, fields ...string)
buf.WriteString(level)
buf.WriteByte(plainEncodingSep)
if err := json.NewEncoder(&buf).Encode(val); err != nil {
log.Println(err.Error())
log.Printf("err: %s\n\n%s", err.Error(), debug.Stack())
return
}

View File

@@ -97,6 +97,15 @@ func TestConsoleWriter(t *testing.T) {
w.(*concreteWriter).statLog = easyToCloseWriter{}
}
func TestNewFileWriter(t *testing.T) {
t.Run("access", func(t *testing.T) {
_, err := newFileWriter(LogConf{
Path: "/not-exists",
})
assert.Error(t, err)
})
}
func TestNopWriter(t *testing.T) {
assert.NotPanics(t, func() {
var w nopWriter
@@ -117,9 +126,23 @@ func TestWriteJson(t *testing.T) {
log.SetOutput(&buf)
writeJson(nil, "foo")
assert.Contains(t, buf.String(), "foo")
buf.Reset()
writeJson(hardToWriteWriter{}, "foo")
assert.Contains(t, buf.String(), "write error")
buf.Reset()
writeJson(nil, make(chan int))
assert.Contains(t, buf.String(), "unsupported type")
buf.Reset()
type C struct {
RC func()
}
writeJson(nil, C{
RC: func() {},
})
assert.Contains(t, buf.String(), "runtime/debug.Stack")
}
func TestWritePlainAny(t *testing.T) {
@@ -156,6 +179,14 @@ func TestWritePlainAny(t *testing.T) {
writePlainAny(hardToWriteWriter{}, levelFatal, "foo")
assert.Contains(t, buf.String(), "write error")
buf.Reset()
type C struct {
RC func()
}
writePlainAny(nil, levelError, C{
RC: func() {},
})
assert.Contains(t, buf.String(), "runtime/debug.Stack")
}
func TestLogWithLimitContentLength(t *testing.T) {

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"reflect"
"strconv"
"strings"
@@ -18,8 +19,10 @@ import (
)
const (
defaultKeyName = "key"
delimiter = '.'
defaultKeyName = "key"
delimiter = '.'
ignoreKey = "-"
numberTypeString = "number"
)
var (
@@ -49,6 +52,7 @@ type (
unmarshalOptions struct {
fillDefault bool
fromString bool
opaqueKeys bool
canonicalKey func(key string) string
}
)
@@ -72,7 +76,11 @@ func UnmarshalKey(m map[string]any, v any) error {
}
// Unmarshal unmarshals m into v.
func (u *Unmarshaler) Unmarshal(i any, v any) error {
func (u *Unmarshaler) Unmarshal(i, v any) error {
return u.unmarshal(i, v, "")
}
func (u *Unmarshaler) unmarshal(i, v any, fullName string) error {
valueType := reflect.TypeOf(v)
if valueType.Kind() != reflect.Ptr {
return errValueNotSettable
@@ -85,13 +93,13 @@ func (u *Unmarshaler) Unmarshal(i any, v any) error {
return errTypeMismatch
}
return u.UnmarshalValuer(mapValuer(iv), v)
return u.unmarshalValuer(mapValuer(iv), v, fullName)
case []any:
if elemType.Kind() != reflect.Slice {
return errTypeMismatch
}
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv)
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName)
default:
return errUnsupportedType
}
@@ -99,17 +107,21 @@ func (u *Unmarshaler) Unmarshal(i any, v any) error {
// UnmarshalValuer unmarshals m into v.
func (u *Unmarshaler) UnmarshalValuer(m Valuer, v any) error {
return u.unmarshalWithFullName(simpleValuer{current: m}, v, "")
return u.unmarshalValuer(simpleValuer{current: m}, v, "")
}
func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value, mapValue any) error {
func (u *Unmarshaler) unmarshalValuer(m Valuer, v any, fullName string) error {
return u.unmarshalWithFullName(simpleValuer{current: m}, v, fullName)
}
func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value, mapValue any, fullName string) error {
if !value.CanSet() {
return errValueNotSettable
}
fieldKeyType := fieldType.Key()
fieldElemType := fieldType.Elem()
targetValue, err := u.generateMap(fieldKeyType, fieldElemType, mapValue)
targetValue, err := u.generateMap(fieldKeyType, fieldElemType, mapValue, fullName)
if err != nil {
return err
}
@@ -143,19 +155,22 @@ func (u *Unmarshaler) fillMapFromString(value reflect.Value, mapValue any) error
return nil
}
func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, mapValue any) error {
func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, mapValue any, fullName string) error {
if !value.CanSet() {
return errValueNotSettable
}
refValue := reflect.ValueOf(mapValue)
if refValue.Kind() != reflect.Slice {
return newTypeMismatchErrorWithHint(fullName, reflect.Slice.String(), refValue.Type().String())
}
if refValue.IsNil() {
return nil
}
baseType := fieldType.Elem()
dereffedBaseType := Deref(baseType)
dereffedBaseKind := dereffedBaseType.Kind()
refValue := reflect.ValueOf(mapValue)
if refValue.IsNil() {
return nil
}
conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap())
if refValue.Len() == 0 {
value.Set(conv)
@@ -170,20 +185,27 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
}
valid = true
sliceFullName := fmt.Sprintf("%s[%d]", fullName, i)
switch dereffedBaseKind {
case reflect.Struct:
target := reflect.New(dereffedBaseType)
if err := u.Unmarshal(ithValue.(map[string]any), target.Interface()); err != nil {
val, ok := ithValue.(map[string]any)
if !ok {
return errTypeMismatch
}
if err := u.unmarshal(val, target.Interface(), sliceFullName); err != nil {
return err
}
SetValue(fieldType.Elem(), conv.Index(i), target.Elem())
case reflect.Slice:
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue); err != nil {
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil {
return err
}
default:
if err := u.fillSliceValue(conv, i, dereffedBaseKind, ithValue); err != nil {
if err := u.fillSliceValue(conv, i, dereffedBaseKind, ithValue, sliceFullName); err != nil {
return err
}
}
@@ -197,7 +219,7 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
}
func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.Value,
mapValue any) error {
mapValue any, fullName string) error {
var slice []any
switch v := mapValue.(type) {
case fmt.Stringer:
@@ -217,7 +239,7 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
conv := reflect.MakeSlice(reflect.SliceOf(baseFieldType), len(slice), cap(slice))
for i := 0; i < len(slice); i++ {
if err := u.fillSliceValue(conv, i, baseFieldKind, slice[i]); err != nil {
if err := u.fillSliceValue(conv, i, baseFieldKind, slice[i], fullName); err != nil {
return err
}
}
@@ -227,7 +249,7 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
}
func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
baseKind reflect.Kind, value any) error {
baseKind reflect.Kind, value any, fullName string) error {
ithVal := slice.Index(index)
switch v := value.(type) {
case fmt.Stringer:
@@ -235,7 +257,7 @@ func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
case string:
return setValueFromString(baseKind, ithVal, v)
case map[string]any:
return u.fillMap(ithVal.Type(), ithVal, value)
return u.fillMap(ithVal.Type(), ithVal, value, fullName)
default:
// don't need to consider the difference between int, int8, int16, int32, int64,
// uint, uint8, uint16, uint32, uint64, because they're handled as json.Number.
@@ -261,7 +283,7 @@ func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
}
func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value reflect.Value,
defaultValue string) error {
defaultValue, fullName string) error {
baseFieldType := Deref(derefedType.Elem())
baseFieldKind := baseFieldType.Kind()
defaultCacheLock.Lock()
@@ -279,16 +301,20 @@ func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value refle
defaultCacheLock.Unlock()
}
return u.fillSlice(derefedType, value, slice)
return u.fillSlice(derefedType, value, slice, fullName)
}
func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any) (reflect.Value, error) {
func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any, fullName string) (reflect.Value, error) {
mapType := reflect.MapOf(keyType, elemType)
valueType := reflect.TypeOf(mapValue)
if mapType == valueType {
return reflect.ValueOf(mapValue), nil
}
if keyType != valueType.Key() {
return emptyValue, errTypeMismatch
}
refValue := reflect.ValueOf(mapValue)
targetValue := reflect.MakeMapWithSize(mapType, refValue.Len())
dereffedElemType := Deref(elemType)
@@ -297,11 +323,12 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
for _, key := range refValue.MapKeys() {
keythValue := refValue.MapIndex(key)
keythData := keythValue.Interface()
mapFullName := fmt.Sprintf("%s[%s]", fullName, key.String())
switch dereffedElemKind {
case reflect.Slice:
target := reflect.New(dereffedElemType)
if err := u.fillSlice(elemType, target.Elem(), keythData); err != nil {
if err := u.fillSlice(elemType, target.Elem(), keythData, mapFullName); err != nil {
return emptyValue, err
}
@@ -313,7 +340,7 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
}
target := reflect.New(dereffedElemType)
if err := u.Unmarshal(keythMap, target.Interface()); err != nil {
if err := u.unmarshal(keythMap, target.Interface(), mapFullName); err != nil {
return emptyValue, err
}
@@ -324,7 +351,7 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
return emptyValue, errTypeMismatch
}
innerValue, err := u.generateMap(elemType.Key(), elemType.Elem(), keythMap)
innerValue, err := u.generateMap(elemType.Key(), elemType.Elem(), keythMap, mapFullName)
if err != nil {
return emptyValue, err
}
@@ -343,7 +370,12 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
return emptyValue, errTypeMismatch
}
targetValue.SetMapIndex(key, reflect.ValueOf(v))
val := reflect.ValueOf(v)
if !val.Type().AssignableTo(dereffedElemType) {
return emptyValue, errTypeMismatch
}
targetValue.SetMapIndex(key, val)
case json.Number:
target := reflect.New(dereffedElemType)
if err := setValueFromString(dereffedElemKind, target.Elem(), v.String()); err != nil {
@@ -408,6 +440,10 @@ func (u *Unmarshaler) processAnonymousField(field reflect.StructField, value ref
return err
}
if key == ignoreKey {
return nil
}
if options.optional() {
return u.processAnonymousFieldOptional(field, value, key, m, fullName)
}
@@ -466,7 +502,7 @@ func (u *Unmarshaler) processAnonymousStructFieldOptional(fieldType reflect.Type
return err
}
_, hasValue := getValue(m, fieldKey)
_, hasValue := getValue(m, fieldKey, u.opts.opaqueKeys)
if hasValue {
if !filled {
filled = true
@@ -486,7 +522,7 @@ func (u *Unmarshaler) processAnonymousStructFieldOptional(fieldType reflect.Type
}
if filled && required != requiredFilled {
return fmt.Errorf("%s is not fully set", key)
return fmt.Errorf("%q is not fully set", key)
}
return nil
@@ -509,8 +545,8 @@ func (u *Unmarshaler) processFieldNotFromString(fieldType reflect.Type, value re
vp valueWithParent, opts *fieldOptionsWithContext, fullName string) error {
derefedFieldType := Deref(fieldType)
typeKind := derefedFieldType.Kind()
valueKind := reflect.TypeOf(vp.value).Kind()
mapValue := vp.value
valueKind := reflect.TypeOf(mapValue).Kind()
switch {
case valueKind == reflect.Map && typeKind == reflect.Struct:
@@ -523,12 +559,14 @@ func (u *Unmarshaler) processFieldNotFromString(fieldType reflect.Type, value re
current: mapValuer(mv),
parent: vp.parent,
}, fullName)
case typeKind == reflect.Slice && valueKind == reflect.Slice:
return u.fillSlice(fieldType, value, mapValue, fullName)
case valueKind == reflect.Map && typeKind == reflect.Map:
return u.fillMap(fieldType, value, mapValue)
return u.fillMap(fieldType, value, mapValue, fullName)
case valueKind == reflect.String && typeKind == reflect.Map:
return u.fillMapFromString(value, mapValue)
case valueKind == reflect.String && typeKind == reflect.Slice:
return u.fillSliceFromString(fieldType, value, mapValue)
return u.fillSliceFromString(fieldType, value, mapValue, fullName)
case valueKind == reflect.String && derefedFieldType == durationType:
return fillDurationValue(fieldType, value, mapValue.(string))
default:
@@ -541,23 +579,16 @@ func (u *Unmarshaler) processFieldPrimitive(fieldType reflect.Type, value reflec
typeKind := Deref(fieldType).Kind()
valueKind := reflect.TypeOf(mapValue).Kind()
switch {
case typeKind == reflect.Slice && valueKind == reflect.Slice:
return u.fillSlice(fieldType, value, mapValue)
case typeKind == reflect.Map && valueKind == reflect.Map:
return u.fillMap(fieldType, value, mapValue)
switch v := mapValue.(type) {
case json.Number:
return u.processFieldPrimitiveWithJSONNumber(fieldType, value, v, opts, fullName)
default:
switch v := mapValue.(type) {
case json.Number:
return u.processFieldPrimitiveWithJSONNumber(fieldType, value, v, opts, fullName)
default:
if typeKind == valueKind {
if err := validateValueInOptions(mapValue, opts.options()); err != nil {
return err
}
return fillWithSameType(fieldType, value, mapValue, opts)
if typeKind == valueKind {
if err := validateValueInOptions(mapValue, opts.options()); err != nil {
return err
}
return fillWithSameType(fieldType, value, mapValue, opts)
}
}
@@ -580,25 +611,23 @@ func (u *Unmarshaler) processFieldPrimitiveWithJSONNumber(fieldType reflect.Type
target := reflect.New(Deref(fieldType)).Elem()
switch typeKind {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
iValue, err := v.Int64()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if err := setValueFromString(typeKind, target, v.String()); err != nil {
return err
}
case reflect.Float32:
fValue, err := v.Float64()
if err != nil {
return err
}
target.SetInt(iValue)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
iValue, err := v.Int64()
if err != nil {
return err
if fValue > math.MaxFloat32 {
return fmt.Errorf("parsing %q as float32: value out of range", v.String())
}
if iValue < 0 {
return fmt.Errorf("unmarshal %q with bad value %q", fullName, v.String())
}
target.SetUint(uint64(iValue))
case reflect.Float32, reflect.Float64:
target.SetFloat(fValue)
case reflect.Float64:
fValue, err := v.Float64()
if err != nil {
return err
@@ -606,7 +635,7 @@ func (u *Unmarshaler) processFieldPrimitiveWithJSONNumber(fieldType reflect.Type
target.SetFloat(fValue)
default:
return newTypeMismatchError(fullName)
return newTypeMismatchErrorWithHint(fullName, typeKind.String(), numberTypeString)
}
SetValue(fieldType, value, target)
@@ -691,11 +720,19 @@ func (u *Unmarshaler) processFieldWithEnvValue(fieldType reflect.Type, value ref
func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect.Value,
m valuerWithParent, fullName string) error {
if !field.IsExported() {
return nil
}
key, opts, err := u.parseOptionsWithContext(field, m, fullName)
if err != nil {
return err
}
if key == ignoreKey {
return nil
}
fullName = join(fullName, key)
if opts != nil && len(opts.EnvVar) > 0 {
envVal := proc.Env(opts.EnvVar)
@@ -710,12 +747,12 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
}
valuer := createValuer(m, opts)
mapValue, hasValue := getValue(valuer, canonicalKey)
mapValue, hasValue := getValue(valuer, canonicalKey, u.opts.opaqueKeys)
// When fillDefault is used, m is a null value, hasValue must be false, all priority judgments fillDefault.
if u.opts.fillDefault {
if !value.IsZero() {
return fmt.Errorf("set the default value, %s must be zero", fullName)
return fmt.Errorf("set the default value, %q must be zero", fullName)
}
return u.processNamedFieldWithoutValue(field.Type, value, opts, fullName)
} else if !hasValue {
@@ -736,11 +773,11 @@ func (u *Unmarshaler) processNamedFieldWithValue(fieldType reflect.Type, value r
return nil
}
return fmt.Errorf("field %s mustn't be nil", key)
return fmt.Errorf("field %q mustn't be nil", key)
}
if !value.CanSet() {
return fmt.Errorf("field %s is not settable", key)
return fmt.Errorf("field %q is not settable", key)
}
maybeNewValue(fieldType, value)
@@ -784,7 +821,7 @@ func (u *Unmarshaler) processNamedFieldWithValueFromString(fieldType reflect.Typ
}
if !stringx.Contains(options, checkValue) {
return fmt.Errorf(`value "%s" for field "%s" is not defined in options "%v"`,
return fmt.Errorf(`value "%s" for field %q is not defined in options "%v"`,
mapValue, key, options)
}
}
@@ -803,13 +840,18 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu
switch fieldKind {
case reflect.Array, reflect.Slice:
return u.fillSliceWithDefault(derefedType, value, defaultValue)
return u.fillSliceWithDefault(derefedType, value, defaultValue, fullName)
default:
return setValueFromString(fieldKind, value, defaultValue)
}
}
if u.opts.fillDefault {
if fieldType.Kind() != reflect.Ptr && fieldKind == reflect.Struct {
return u.processFieldNotFromString(fieldType, value, valueWithParent{
value: emptyMap,
}, opts, fullName)
}
return nil
}
@@ -846,7 +888,7 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu
func (u *Unmarshaler) unmarshalWithFullName(m valuerWithParent, v any, fullName string) error {
rv := reflect.ValueOf(v)
if err := ValidatePtr(&rv); err != nil {
if err := ValidatePtr(rv); err != nil {
return err
}
@@ -865,12 +907,9 @@ func (u *Unmarshaler) unmarshalWithFullName(m valuerWithParent, v any, fullName
numFields := baseType.NumField()
for i := 0; i < numFields; i++ {
field := baseType.Field(i)
if !field.IsExported() {
continue
}
if err := u.processField(field, valElem.Field(i), m, fullName); err != nil {
typeField := baseType.Field(i)
valueField := valElem.Field(i)
if err := u.processField(typeField, valueField, m, fullName); err != nil {
return err
}
}
@@ -899,6 +938,14 @@ func WithDefault() UnmarshalOption {
}
}
// WithOpaqueKeys customizes an Unmarshaler with opaque keys.
// Opaque keys are keys that are not processed by the unmarshaler.
func WithOpaqueKeys() UnmarshalOption {
return func(opt *unmarshalOptions) {
opt.opaqueKeys = true
}
}
func createValuer(v valuerWithParent, opts *fieldOptionsWithContext) valuerWithParent {
if opts.inherit() {
return recursiveValuer{
@@ -976,8 +1023,8 @@ func fillWithSameType(fieldType reflect.Type, value reflect.Value, mapValue any,
}
// getValue gets the value for the specific key, the key can be in the format of parentKey.childKey
func getValue(m valuerWithParent, key string) (any, bool) {
keys := readKeys(key)
func getValue(m valuerWithParent, key string, opaque bool) (any, bool) {
keys := readKeys(key, opaque)
return getValueWithChainedKeys(m, keys)
}
@@ -1024,14 +1071,23 @@ func join(elem ...string) string {
}
func newInitError(name string) error {
return fmt.Errorf("field %s is not set", name)
return fmt.Errorf("field %q is not set", name)
}
func newTypeMismatchError(name string) error {
return fmt.Errorf("type mismatch for field %s", name)
return fmt.Errorf("type mismatch for field %q", name)
}
func readKeys(key string) []string {
func newTypeMismatchErrorWithHint(name, expectType, actualType string) error {
return fmt.Errorf("type mismatch for field %q, expect %q, actual %q",
name, expectType, actualType)
}
func readKeys(key string, opaque bool) []string {
if opaque {
return []string{key}
}
cacheKeysLock.Lock()
keys, ok := cacheKeys[key]
cacheKeysLock.Unlock()

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,7 @@ const (
leftSquareBracket = '['
rightSquareBracket = ']'
segmentSeparator = ','
intSize = 32 << (^uint(0) >> 63) // 32 or 64
)
var (
@@ -79,7 +80,7 @@ func SetMapIndexValue(tp reflect.Type, value, key, target reflect.Value) {
}
// ValidatePtr validates v if it's a valid pointer.
func ValidatePtr(v *reflect.Value) error {
func ValidatePtr(v reflect.Value) error {
// sequence is very important, IsNil must be called after checking Kind() with reflect.Ptr,
// panic otherwise
if !v.IsValid() || v.Kind() != reflect.Ptr || v.IsNil() {
@@ -100,27 +101,30 @@ func convertTypeFromString(kind reflect.Kind, str string) (any, error) {
default:
return false, errTypeMismatch
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
intValue, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return 0, fmt.Errorf("the value %q cannot parsed as int", str)
}
return intValue, nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
uintValue, err := strconv.ParseUint(str, 10, 64)
if err != nil {
return 0, fmt.Errorf("the value %q cannot parsed as uint", str)
}
return uintValue, nil
case reflect.Float32, reflect.Float64:
floatValue, err := strconv.ParseFloat(str, 64)
if err != nil {
return 0, fmt.Errorf("the value %q cannot parsed as float", str)
}
return floatValue, nil
case reflect.Int:
return strconv.ParseInt(str, 10, intSize)
case reflect.Int8:
return strconv.ParseInt(str, 10, 8)
case reflect.Int16:
return strconv.ParseInt(str, 10, 16)
case reflect.Int32:
return strconv.ParseInt(str, 10, 32)
case reflect.Int64:
return strconv.ParseInt(str, 10, 64)
case reflect.Uint:
return strconv.ParseUint(str, 10, intSize)
case reflect.Uint8:
return strconv.ParseUint(str, 10, 8)
case reflect.Uint16:
return strconv.ParseUint(str, 10, 16)
case reflect.Uint32:
return strconv.ParseUint(str, 10, 32)
case reflect.Uint64:
return strconv.ParseUint(str, 10, 64)
case reflect.Float32:
return strconv.ParseFloat(str, 32)
case reflect.Float64:
return strconv.ParseFloat(str, 64)
case reflect.String:
return str, nil
default:
@@ -255,7 +259,7 @@ func parseGroupedSegments(val string) []string {
// don't modify returned fieldOptions, it's cached and shared among different calls.
func parseKeyAndOptions(tagName string, field reflect.StructField) (string, *fieldOptions, error) {
value := field.Tag.Get(tagName)
value := strings.TrimSpace(field.Tag.Get(tagName))
if len(value) == 0 {
return field.Name, nil, nil
}
@@ -370,10 +374,8 @@ func parseOption(fieldOpts *fieldOptions, fieldName, option string) error {
fieldOpts.Optional = true
fieldOpts.OptionalDep = segs[1]
default:
return fmt.Errorf("field %s has wrong optional", fieldName)
return fmt.Errorf("field %q has wrong optional", fieldName)
}
case option == optionalOption:
fieldOpts.Optional = true
case strings.HasPrefix(option, optionsOption):
val, err := parseProperty(fieldName, optionsOption, option)
if err != nil {
@@ -429,7 +431,7 @@ func parseOptions(val string) []string {
func parseProperty(field, tag, val string) (string, error) {
segs := strings.Split(val, equalToken)
if len(segs) != 2 {
return "", fmt.Errorf("field %s has wrong %s", field, tag)
return "", fmt.Errorf("field %q has wrong tag value %q", field, tag)
}
return strings.TrimSpace(segs[1]), nil
@@ -488,19 +490,22 @@ func setMatchedPrimitiveValue(kind reflect.Kind, value reflect.Value, v any) err
switch kind {
case reflect.Bool:
value.SetBool(v.(bool))
return nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
value.SetInt(v.(int64))
return nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
value.SetUint(v.(uint64))
return nil
case reflect.Float32, reflect.Float64:
value.SetFloat(v.(float64))
return nil
case reflect.String:
value.SetString(v.(string))
return nil
default:
return errUnsupportedType
}
return nil
}
func setValueFromString(kind reflect.Kind, value reflect.Value, str string) error {
@@ -577,7 +582,8 @@ func usingDifferentKeys(key string, field reflect.StructField) bool {
return false
}
func validateAndSetValue(kind reflect.Kind, value reflect.Value, str string, opts *fieldOptionsWithContext) error {
func validateAndSetValue(kind reflect.Kind, value reflect.Value, str string,
opts *fieldOptionsWithContext) error {
if !value.CanSet() {
return errValueNotSettable
}
@@ -628,7 +634,7 @@ func validateValueInOptions(val any, options []string) error {
switch v := val.(type) {
case string:
if !stringx.Contains(options, v) {
return fmt.Errorf(`error: value "%s" is not defined in options "%v"`, v, options)
return fmt.Errorf(`error: value %q is not defined in options "%v"`, v, options)
}
default:
if !stringx.Contains(options, Repr(v)) {

View File

@@ -144,6 +144,10 @@ func TestParseSegments(t *testing.T) {
input: "",
expect: []string{},
},
{
input: " ",
expect: []string{},
},
{
input: ",",
expect: []string{""},
@@ -214,30 +218,31 @@ func TestParseSegments(t *testing.T) {
func TestValidatePtrWithNonPtr(t *testing.T) {
var foo string
rve := reflect.ValueOf(foo)
assert.NotNil(t, ValidatePtr(&rve))
assert.NotNil(t, ValidatePtr(rve))
}
func TestValidatePtrWithPtr(t *testing.T) {
var foo string
rve := reflect.ValueOf(&foo)
assert.Nil(t, ValidatePtr(&rve))
assert.Nil(t, ValidatePtr(rve))
}
func TestValidatePtrWithNilPtr(t *testing.T) {
var foo *string
rve := reflect.ValueOf(foo)
assert.NotNil(t, ValidatePtr(&rve))
assert.NotNil(t, ValidatePtr(rve))
}
func TestValidatePtrWithZeroValue(t *testing.T) {
var s string
e := reflect.Zero(reflect.TypeOf(s))
assert.NotNil(t, ValidatePtr(&e))
assert.NotNil(t, ValidatePtr(e))
}
func TestSetValueNotSettable(t *testing.T) {
var i int
assert.NotNil(t, setValueFromString(reflect.Int, reflect.ValueOf(i), "1"))
assert.Error(t, setValueFromString(reflect.Int, reflect.ValueOf(i), "1"))
assert.Error(t, validateAndSetValue(reflect.Int, reflect.ValueOf(i), "1", nil))
}
func TestParseKeyAndOptionsErrors(t *testing.T) {
@@ -296,3 +301,36 @@ func TestSetValueFormatErrors(t *testing.T) {
})
}
}
func TestValidateValueRange(t *testing.T) {
t.Run("float", func(t *testing.T) {
assert.NoError(t, validateValueRange(1.2, nil))
})
t.Run("float number range", func(t *testing.T) {
assert.NoError(t, validateNumberRange(1.2, nil))
})
t.Run("bad float", func(t *testing.T) {
assert.Error(t, validateValueRange("a", &fieldOptionsWithContext{
Range: &numberRange{},
}))
})
t.Run("bad float validate", func(t *testing.T) {
var v struct {
Foo float32
}
assert.Error(t, validateAndSetValue(reflect.Int, reflect.ValueOf(&v).Elem().Field(0),
"1", &fieldOptionsWithContext{
Range: &numberRange{
left: 2,
right: 3,
},
}))
})
}
func TestSetMatchedPrimitiveValue(t *testing.T) {
assert.Error(t, setMatchedPrimitiveValue(reflect.Func, reflect.ValueOf(2), "1"))
}

View File

@@ -34,7 +34,7 @@ type (
recursiveValuer node
)
// Value gets the value assciated with the given key from mv.
// Value gets the value associated with the given key from mv.
func (mv mapValuer) Value(key string) (any, bool) {
v, ok := mv[key]
return v, ok

View File

@@ -1011,6 +1011,15 @@ func TestUnmarshalYamlMapRune(t *testing.T) {
assert.Equal(t, rune(3), v.Machine["node3"])
}
func TestUnmarshalYamlStringOfInt(t *testing.T) {
text := `password: 123456`
var v struct {
Password string `json:"password"`
}
reader := strings.NewReader(text)
assert.Error(t, UnmarshalYamlReader(reader, &v))
}
func TestUnmarshalYamlBadInput(t *testing.T) {
var v struct {
Any string

View File

@@ -3,7 +3,6 @@ package metric
import (
prom "github.com/prometheus/client_golang/prometheus"
"github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/prometheus"
)
type (
@@ -47,20 +46,16 @@ func NewCounterVec(cfg *CounterVecOpts) CounterVec {
return cv
}
func (cv *promCounterVec) Inc(labels ...string) {
if !prometheus.Enabled() {
return
}
cv.counter.WithLabelValues(labels...).Inc()
func (cv *promCounterVec) Add(v float64, labels ...string) {
update(func() {
cv.counter.WithLabelValues(labels...).Add(v)
})
}
func (cv *promCounterVec) Add(v float64, labels ...string) {
if !prometheus.Enabled() {
return
}
cv.counter.WithLabelValues(labels...).Add(v)
func (cv *promCounterVec) Inc(labels ...string) {
update(func() {
cv.counter.WithLabelValues(labels...).Inc()
})
}
func (cv *promCounterVec) close() bool {

View File

@@ -3,7 +3,6 @@ package metric
import (
prom "github.com/prometheus/client_golang/prometheus"
"github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/prometheus"
)
type (
@@ -16,8 +15,12 @@ type (
Set(v float64, labels ...string)
// Inc increments labels.
Inc(labels ...string)
// Dec decrements labels.
Dec(labels ...string)
// Add adds v to labels.
Add(v float64, labels ...string)
// Sub subtracts v to labels.
Sub(v float64, labels ...string)
close() bool
}
@@ -32,13 +35,12 @@ func NewGaugeVec(cfg *GaugeVecOpts) GaugeVec {
return nil
}
vec := prom.NewGaugeVec(
prom.GaugeOpts{
Namespace: cfg.Namespace,
Subsystem: cfg.Subsystem,
Name: cfg.Name,
Help: cfg.Help,
}, cfg.Labels)
vec := prom.NewGaugeVec(prom.GaugeOpts{
Namespace: cfg.Namespace,
Subsystem: cfg.Subsystem,
Name: cfg.Name,
Help: cfg.Help,
}, cfg.Labels)
prom.MustRegister(vec)
gv := &promGaugeVec{
gauge: vec,
@@ -50,28 +52,34 @@ func NewGaugeVec(cfg *GaugeVecOpts) GaugeVec {
return gv
}
func (gv *promGaugeVec) Inc(labels ...string) {
if !prometheus.Enabled() {
return
}
gv.gauge.WithLabelValues(labels...).Inc()
func (gv *promGaugeVec) Add(v float64, labels ...string) {
update(func() {
gv.gauge.WithLabelValues(labels...).Add(v)
})
}
func (gv *promGaugeVec) Add(v float64, labels ...string) {
if !prometheus.Enabled() {
return
}
func (gv *promGaugeVec) Dec(labels ...string) {
update(func() {
gv.gauge.WithLabelValues(labels...).Dec()
})
}
gv.gauge.WithLabelValues(labels...).Add(v)
func (gv *promGaugeVec) Inc(labels ...string) {
update(func() {
gv.gauge.WithLabelValues(labels...).Inc()
})
}
func (gv *promGaugeVec) Set(v float64, labels ...string) {
if !prometheus.Enabled() {
return
}
update(func() {
gv.gauge.WithLabelValues(labels...).Set(v)
})
}
gv.gauge.WithLabelValues(labels...).Set(v)
func (gv *promGaugeVec) Sub(v float64, labels ...string) {
update(func() {
gv.gauge.WithLabelValues(labels...).Sub(v)
})
}
func (gv *promGaugeVec) close() bool {

View File

@@ -40,6 +40,23 @@ func TestGaugeInc(t *testing.T) {
assert.Equal(t, float64(2), r)
}
func TestGaugeDec(t *testing.T) {
startAgent()
gaugeVec := NewGaugeVec(&GaugeVecOpts{
Namespace: "rpc_client",
Subsystem: "requests",
Name: "duration_ms",
Help: "rpc server requests duration(ms).",
Labels: []string{"path"},
})
defer gaugeVec.close()
gv, _ := gaugeVec.(*promGaugeVec)
gv.Dec("/users")
gv.Dec("/users")
r := testutil.ToFloat64(gv.gauge)
assert.Equal(t, float64(-2), r)
}
func TestGaugeAdd(t *testing.T) {
startAgent()
gaugeVec := NewGaugeVec(&GaugeVecOpts{
@@ -57,6 +74,23 @@ func TestGaugeAdd(t *testing.T) {
assert.Equal(t, float64(20), r)
}
func TestGaugeSub(t *testing.T) {
startAgent()
gaugeVec := NewGaugeVec(&GaugeVecOpts{
Namespace: "rpc_client",
Subsystem: "request",
Name: "duration_ms",
Help: "rpc server requests duration(ms).",
Labels: []string{"path"},
})
defer gaugeVec.close()
gv, _ := gaugeVec.(*promGaugeVec)
gv.Sub(-100, "/classroom")
gv.Sub(30, "/classroom")
r := testutil.ToFloat64(gv.gauge)
assert.Equal(t, float64(70), r)
}
func TestGaugeSet(t *testing.T) {
startAgent()
gaugeVec := NewGaugeVec(&GaugeVecOpts{

View File

@@ -3,24 +3,26 @@ package metric
import (
prom "github.com/prometheus/client_golang/prometheus"
"github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/prometheus"
)
type (
// A HistogramVecOpts is a histogram vector options.
HistogramVecOpts struct {
Namespace string
Subsystem string
Name string
Help string
Labels []string
Buckets []float64
Namespace string
Subsystem string
Name string
Help string
Labels []string
Buckets []float64
ConstLabels map[string]string
}
// A HistogramVec interface represents a histogram vector.
HistogramVec interface {
// Observe adds observation v to labels.
Observe(v int64, labels ...string)
// ObserveFloat allow to observe float64 values.
ObserveFloat(v float64, labels ...string)
close() bool
}
@@ -36,11 +38,12 @@ func NewHistogramVec(cfg *HistogramVecOpts) HistogramVec {
}
vec := prom.NewHistogramVec(prom.HistogramOpts{
Namespace: cfg.Namespace,
Subsystem: cfg.Subsystem,
Name: cfg.Name,
Help: cfg.Help,
Buckets: cfg.Buckets,
Namespace: cfg.Namespace,
Subsystem: cfg.Subsystem,
Name: cfg.Name,
Help: cfg.Help,
Buckets: cfg.Buckets,
ConstLabels: cfg.ConstLabels,
}, cfg.Labels)
prom.MustRegister(vec)
hv := &promHistogramVec{
@@ -54,11 +57,15 @@ func NewHistogramVec(cfg *HistogramVecOpts) HistogramVec {
}
func (hv *promHistogramVec) Observe(v int64, labels ...string) {
if !prometheus.Enabled() {
return
}
update(func() {
hv.histogram.WithLabelValues(labels...).Observe(float64(v))
})
}
hv.histogram.WithLabelValues(labels...).Observe(float64(v))
func (hv *promHistogramVec) ObserveFloat(v float64, labels ...string) {
update(func() {
hv.histogram.WithLabelValues(labels...).Observe(v)
})
}
func (hv *promHistogramVec) close() bool {

View File

@@ -6,7 +6,6 @@ import (
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/proc"
)
func TestNewHistogramVec(t *testing.T) {
@@ -15,7 +14,7 @@ func TestNewHistogramVec(t *testing.T) {
Help: "rpc server requests duration(ms).",
Buckets: []float64{1, 2, 3},
})
defer histogramVec.close()
defer histogramVec.(*promHistogramVec).close()
histogramVecNil := NewHistogramVec(nil)
assert.NotNil(t, histogramVec)
assert.Nil(t, histogramVecNil)
@@ -29,9 +28,10 @@ func TestHistogramObserve(t *testing.T) {
Buckets: []float64{1, 2, 3},
Labels: []string{"method"},
})
defer histogramVec.close()
defer histogramVec.(*promHistogramVec).close()
hv, _ := histogramVec.(*promHistogramVec)
hv.Observe(2, "/Users")
hv.ObserveFloat(1.1, "/Users")
metadata := `
# HELP counts rpc server requests duration(ms).
@@ -39,15 +39,13 @@ func TestHistogramObserve(t *testing.T) {
`
val := `
counts_bucket{method="/Users",le="1"} 0
counts_bucket{method="/Users",le="2"} 1
counts_bucket{method="/Users",le="3"} 1
counts_bucket{method="/Users",le="+Inf"} 1
counts_sum{method="/Users"} 2
counts_count{method="/Users"} 1
counts_bucket{method="/Users",le="2"} 2
counts_bucket{method="/Users",le="3"} 2
counts_bucket{method="/Users",le="+Inf"} 2
counts_sum{method="/Users"} 3.1
counts_count{method="/Users"} 2
`
err := testutil.CollectAndCompare(hv.histogram, strings.NewReader(metadata+val))
assert.Nil(t, err)
proc.Shutdown()
}

View File

@@ -1,5 +1,7 @@
package metric
import "github.com/zeromicro/go-zero/core/prometheus"
// A VectorOpts is a general configuration.
type VectorOpts struct {
Namespace string
@@ -8,3 +10,11 @@ type VectorOpts struct {
Help string
Labels []string
}
func update(fn func()) {
if !prometheus.Enabled() {
return
}
fn()
}

62
core/metric/summary.go Normal file
View File

@@ -0,0 +1,62 @@
package metric
import (
prom "github.com/prometheus/client_golang/prometheus"
"github.com/zeromicro/go-zero/core/proc"
)
type (
// A SummaryVecOpts is a summary vector options
SummaryVecOpts struct {
VecOpt VectorOpts
Objectives map[float64]float64
}
// A SummaryVec interface represents a summary vector.
SummaryVec interface {
// Observe adds observation v to labels.
Observe(v float64, labels ...string)
close() bool
}
promSummaryVec struct {
summary *prom.SummaryVec
}
)
// NewSummaryVec return a SummaryVec
func NewSummaryVec(cfg *SummaryVecOpts) SummaryVec {
if cfg == nil {
return nil
}
vec := prom.NewSummaryVec(
prom.SummaryOpts{
Namespace: cfg.VecOpt.Namespace,
Subsystem: cfg.VecOpt.Subsystem,
Name: cfg.VecOpt.Name,
Help: cfg.VecOpt.Help,
Objectives: cfg.Objectives,
},
cfg.VecOpt.Labels,
)
prom.MustRegister(vec)
sv := &promSummaryVec{
summary: vec,
}
proc.AddShutdownListener(func() {
sv.close()
})
return sv
}
func (sv *promSummaryVec) Observe(v float64, labels ...string) {
update(func() {
sv.summary.WithLabelValues(labels...).Observe(v)
})
}
func (sv *promSummaryVec) close() bool {
return prom.Unregister(sv.summary)
}

View File

@@ -0,0 +1,68 @@
package metric
import (
"strings"
"testing"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/proc"
)
func TestNewSummaryVec(t *testing.T) {
summaryVec := NewSummaryVec(&SummaryVecOpts{
VecOpt: VectorOpts{
Namespace: "http_server",
Subsystem: "requests",
Name: "duration_quantiles",
Help: "rpc client requests duration(ms) φ quantiles ",
Labels: []string{"method"},
},
Objectives: map[float64]float64{
0.5: 0.01,
0.9: 0.01,
},
})
defer summaryVec.close()
summaryVecNil := NewSummaryVec(nil)
assert.NotNil(t, summaryVec)
assert.Nil(t, summaryVecNil)
}
func TestSummaryObserve(t *testing.T) {
startAgent()
summaryVec := NewSummaryVec(&SummaryVecOpts{
VecOpt: VectorOpts{
Namespace: "http_server",
Subsystem: "requests",
Name: "duration_quantiles",
Help: "rpc client requests duration(ms) φ quantiles ",
Labels: []string{"method"},
},
Objectives: map[float64]float64{
0.3: 0.01,
0.6: 0.01,
1: 0.01,
},
})
defer summaryVec.close()
sv := summaryVec.(*promSummaryVec)
sv.Observe(100, "GET")
sv.Observe(200, "GET")
sv.Observe(300, "GET")
metadata := `
# HELP http_server_requests_duration_quantiles rpc client requests duration(ms) φ quantiles
# TYPE http_server_requests_duration_quantiles summary
`
val := `
http_server_requests_duration_quantiles{method="GET",quantile="0.3"} 100
http_server_requests_duration_quantiles{method="GET",quantile="0.6"} 200
http_server_requests_duration_quantiles{method="GET",quantile="1"} 300
http_server_requests_duration_quantiles_sum{method="GET"} 600
http_server_requests_duration_quantiles_count{method="GET"} 3
`
err := testutil.CollectAndCompare(sv.summary, strings.NewReader(metadata+val))
assert.Nil(t, err)
proc.Shutdown()
}

View File

@@ -1,5 +1,4 @@
//go:build fuzz
// +build fuzz
package mr

View File

@@ -3,7 +3,7 @@ package mr
import (
"context"
"errors"
"io/ioutil"
"io"
"log"
"runtime"
"sync/atomic"
@@ -17,7 +17,7 @@ import (
var errDummy = errors.New("dummy")
func init() {
log.SetOutput(ioutil.Discard)
log.SetOutput(io.Discard)
}
func TestFinish(t *testing.T) {
@@ -574,6 +574,7 @@ func TestMapReduceWithContext(t *testing.T) {
cancel()
}
writer.Write(i)
time.Sleep(time.Millisecond)
}, func(pipe <-chan int, cancel func(error)) {
for item := range pipe {
i := item

View File

@@ -1,7 +1,6 @@
package proc
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
@@ -21,13 +20,11 @@ func TestEnvInt(t *testing.T) {
val, ok := EnvInt("any")
assert.Equal(t, 0, val)
assert.False(t, ok)
err := os.Setenv("anyInt", "10")
assert.Nil(t, err)
t.Setenv("anyInt", "10")
val, ok = EnvInt("anyInt")
assert.Equal(t, 10, val)
assert.True(t, ok)
err = os.Setenv("anyString", "a")
assert.Nil(t, err)
t.Setenv("anyString", "a")
val, ok = EnvInt("anyString")
assert.Equal(t, 0, val)
assert.False(t, ok)

View File

@@ -1,7 +0,0 @@
//go:build windows
// +build windows
package proc
func dumpGoroutines() {
}

View File

@@ -1,5 +1,4 @@
//go:build linux || darwin
// +build linux darwin
package proc
@@ -19,7 +18,11 @@ const (
debugLevel = 2
)
func dumpGoroutines() {
type creator interface {
Create(name string) (file *os.File, err error)
}
func dumpGoroutines(ctor creator) {
command := path.Base(os.Args[0])
pid := syscall.Getpid()
dumpFile := path.Join(os.TempDir(), fmt.Sprintf("%s-%d-goroutines-%s.dump",
@@ -27,10 +30,16 @@ func dumpGoroutines() {
logx.Infof("Got dump goroutine signal, printing goroutine profile to %s", dumpFile)
if f, err := os.Create(dumpFile); err != nil {
if f, err := ctor.Create(dumpFile); err != nil {
logx.Errorf("Failed to dump goroutine profile, error: %v", err)
} else {
defer f.Close()
pprof.Lookup(goroutineProfile).WriteTo(f, debugLevel)
}
}
type fileCreator struct{}
func (fc fileCreator) Create(name string) (file *os.File, err error) {
return os.Create(name)
}

View File

@@ -1,23 +1,41 @@
//go:build linux || darwin
package proc
import (
"errors"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/logx/logtest"
)
func TestDumpGoroutines(t *testing.T) {
var buf strings.Builder
w := logx.NewWriter(&buf)
o := logx.Reset()
logx.SetWriter(w)
defer func() {
logx.Reset()
logx.SetWriter(o)
}()
t.Run("real file", func(t *testing.T) {
buf := logtest.NewCollector(t)
dumpGoroutines(fileCreator{})
assert.True(t, strings.Contains(buf.String(), ".dump"))
})
dumpGoroutines()
assert.True(t, strings.Contains(buf.String(), ".dump"))
t.Run("fake file", func(t *testing.T) {
const msg = "any message"
buf := logtest.NewCollector(t)
err := errors.New(msg)
dumpGoroutines(fakeCreator{
file: &os.File{},
err: err,
})
assert.True(t, strings.Contains(buf.String(), msg))
})
}
type fakeCreator struct {
file *os.File
err error
}
func (fc fakeCreator) Create(name string) (file *os.File, err error) {
return fc.file, fc.err
}

View File

@@ -1,5 +1,4 @@
//go:build windows
// +build windows
package proc

View File

@@ -1,5 +1,4 @@
//go:build linux || darwin
// +build linux darwin
package proc

View File

@@ -5,25 +5,16 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/logx/logtest"
)
func TestProfile(t *testing.T) {
var buf strings.Builder
w := logx.NewWriter(&buf)
o := logx.Reset()
logx.SetWriter(w)
defer func() {
logx.Reset()
logx.SetWriter(o)
}()
c := logtest.NewCollector(t)
profiler := StartProfile()
// start again should not work
assert.NotNil(t, StartProfile())
profiler.Stop()
// stop twice
profiler.Stop()
assert.True(t, strings.Contains(buf.String(), ".pprof"))
assert.True(t, strings.Contains(c.String(), ".pprof"))
}

View File

@@ -1,5 +1,4 @@
//go:build windows
// +build windows
package proc

View File

@@ -1,5 +1,4 @@
//go:build linux || darwin
// +build linux darwin
package proc
@@ -53,10 +52,10 @@ func WrapUp() {
wrapUpListeners.notifyListeners()
}
func gracefulStop(signals chan os.Signal) {
func gracefulStop(signals chan os.Signal, sig syscall.Signal) {
signal.Stop(signals)
logx.Info("Got signal SIGTERM, shutting down...")
logx.Infof("Got signal %d, shutting down...", sig)
go wrapUpListeners.notifyListeners()
time.Sleep(wrapUpTime)
@@ -64,7 +63,7 @@ func gracefulStop(signals chan os.Signal) {
time.Sleep(delayTimeBeforeForceQuit - wrapUpTime)
logx.Infof("Still alive after %v, going to force kill the process...", delayTimeBeforeForceQuit)
syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
_ = syscall.Kill(syscall.Getpid(), sig)
}
type listenerManager struct {
@@ -97,4 +96,6 @@ func (lm *listenerManager) notifyListeners() {
group.RunSafe(listener)
}
group.Wait()
lm.listeners = nil
}

View File

@@ -1,5 +1,4 @@
//go:build linux || darwin
// +build linux darwin
package proc
@@ -29,3 +28,33 @@ func TestShutdown(t *testing.T) {
called()
assert.Equal(t, 3, val)
}
func TestNotifyMoreThanOnce(t *testing.T) {
ch := make(chan struct{}, 1)
go func() {
var val int
called := AddWrapUpListener(func() {
val++
})
WrapUp()
WrapUp()
called()
assert.Equal(t, 1, val)
called = AddShutdownListener(func() {
val += 2
})
Shutdown()
Shutdown()
called()
assert.Equal(t, 3, val)
ch <- struct{}{}
}()
select {
case <-ch:
case <-time.After(time.Second):
t.Fatal("timeout, check error logs")
}
}

View File

@@ -1,5 +1,4 @@
//go:build windows
// +build windows
package proc

View File

@@ -1,5 +1,4 @@
//go:build linux || darwin
// +build linux darwin
package proc
@@ -21,13 +20,13 @@ func init() {
// https://golang.org/pkg/os/signal/#Notify
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGTERM)
signal.Notify(signals, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGTERM, syscall.SIGINT)
for {
v := <-signals
switch v {
case syscall.SIGUSR1:
dumpGoroutines()
dumpGoroutines(fileCreator{})
case syscall.SIGUSR2:
if profiler == nil {
profiler = StartProfile()
@@ -36,14 +35,11 @@ func init() {
profiler = nil
}
case syscall.SIGTERM:
select {
case <-done:
// already closed
default:
close(done)
}
gracefulStop(signals)
stopOnSignal()
gracefulStop(signals, syscall.SIGTERM)
case syscall.SIGINT:
stopOnSignal()
gracefulStop(signals, syscall.SIGINT)
default:
logx.Error("Got unregistered signal:", v)
}
@@ -55,3 +51,12 @@ func init() {
func Done() <-chan struct{} {
return done
}
func stopOnSignal() {
select {
case <-done:
// already closed
default:
close(done)
}
}

Some files were not shown because too many files have changed in this diff Show More