Skip to content

Commit 4ba29f7

Browse files
committed
Add end call participant RPC.
1 parent 4c00317 commit 4ba29f7

11 files changed

Lines changed: 291 additions & 169 deletions

File tree

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ require (
1111
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731
1212
github.com/livekit/media-sdk v0.0.0-20260612175532-3d4d26d136c9
1313
github.com/livekit/mediatransportutil v0.0.0-20260608063931-a3417d38cda0
14-
github.com/livekit/protocol v1.46.7-0.20260610064410-e286afe70eb0
14+
github.com/livekit/protocol v1.47.1-0.20260618140803-db77a56cf894
1515
github.com/livekit/psrpc v0.7.2
16-
github.com/livekit/server-sdk-go/v2 v2.16.7-0.20260608025623-a5da15b13baa
16+
github.com/livekit/server-sdk-go/v2 v2.16.7-0.20260618140743-3776341a116e
1717
github.com/livekit/sipgo v0.13.2-0.20260519205735-a5b4a38b6ceb
1818
github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12
1919
github.com/ory/dockertest/v3 v3.12.0
2020
github.com/pion/rtp v1.10.2
2121
github.com/pion/sdp/v3 v3.0.18
22-
github.com/pion/webrtc/v4 v4.2.11
22+
github.com/pion/webrtc/v4 v4.2.14
2323
github.com/prometheus/client_golang v1.23.2
2424
github.com/sirupsen/logrus v1.9.4
2525
github.com/stretchr/testify v1.11.1
@@ -102,7 +102,7 @@ require (
102102
github.com/pion/mdns/v2 v2.1.0 // indirect
103103
github.com/pion/randutil v0.1.0 // indirect
104104
github.com/pion/rtcp v1.2.16 // indirect
105-
github.com/pion/sctp v1.9.5 // indirect
105+
github.com/pion/sctp v1.10.0 // indirect
106106
github.com/pion/srtp/v3 v3.0.11 // indirect
107107
github.com/pion/stun/v3 v3.1.4 // indirect
108108
github.com/pion/transport/v4 v4.0.2 // indirect

go.sum

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,12 @@ github.com/livekit/media-sdk v0.0.0-20260612175532-3d4d26d136c9 h1:GVMkuNwXQ74kV
134134
github.com/livekit/media-sdk v0.0.0-20260612175532-3d4d26d136c9/go.mod h1:TuYRjSepaakL6ATsM9V2VMuksewW1PlhA32BG7Pxty0=
135135
github.com/livekit/mediatransportutil v0.0.0-20260608063931-a3417d38cda0 h1:XHNNzebIKZRkLimla/hFGrAIX5EMWHctrgt3hLw7s+I=
136136
github.com/livekit/mediatransportutil v0.0.0-20260608063931-a3417d38cda0/go.mod h1:o8CFmAdrVwzJNOCsQCLUzXRjokkufNshnQHOe4fRaqU=
137-
github.com/livekit/protocol v1.46.7-0.20260610064410-e286afe70eb0 h1:aNazCl+gTEmF88tVsISOvtnfZM/K9IbqAn2WvZVmh4Y=
138-
github.com/livekit/protocol v1.46.7-0.20260610064410-e286afe70eb0/go.mod h1:jO+y05AU9Ec4JswDyuzKCZ4bhziOS0CzMqgnbj60Dzs=
137+
github.com/livekit/protocol v1.47.1-0.20260618140803-db77a56cf894 h1:OH1Fejt3yDQXG2bYs1LkaSxifdVlm61eG3yrzrLW6Jo=
138+
github.com/livekit/protocol v1.47.1-0.20260618140803-db77a56cf894/go.mod h1:jO+y05AU9Ec4JswDyuzKCZ4bhziOS0CzMqgnbj60Dzs=
139139
github.com/livekit/psrpc v0.7.2 h1:6oZ+NODJ2pLyaT6VqDq1F4Qc/3TpDUSpyphj/P9MhQc=
140140
github.com/livekit/psrpc v0.7.2/go.mod h1:rAI+m2+/cb4x9RXhLRtUx5ZwdfjjXOl4zi46IjEetaw=
141-
github.com/livekit/server-sdk-go/v2 v2.16.7-0.20260608025623-a5da15b13baa h1:B19yilP7+JjekKMD0WejMh1Kvypdxpr5yxQZiFStRD0=
142-
github.com/livekit/server-sdk-go/v2 v2.16.7-0.20260608025623-a5da15b13baa/go.mod h1:SWJD68Rfcwrhze09EYaRiur7ESCBuu0u4fpK+0BGEYo=
141+
github.com/livekit/server-sdk-go/v2 v2.16.7-0.20260618140743-3776341a116e h1:PJZ+9COhAT8sCIo6zJCtYaDeJBQcCUN8H0GyEe2xMMM=
142+
github.com/livekit/server-sdk-go/v2 v2.16.7-0.20260618140743-3776341a116e/go.mod h1:fuOvpz1rjH2XgsaXiVKSzW1tPw7es5dmBjr4GrX7xd8=
143143
github.com/livekit/sipgo v0.13.2-0.20260519205735-a5b4a38b6ceb h1:HmgaJMGs0Nco/Z+XMc9f+xFgrbood9yJsIBtl1OY76M=
144144
github.com/livekit/sipgo v0.13.2-0.20260519205735-a5b4a38b6ceb/go.mod h1:aDa6mbFktNzA1D917RhFlIB5IOfNBTmrwt+/lX960j0=
145145
github.com/mackerelio/go-osstat v0.2.7 h1:TCavZi10wF49bT6iQZ9eT2keGZQpC69MTDfdJej5e94=
@@ -196,8 +196,8 @@ github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
196196
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
197197
github.com/pion/rtp v1.10.2 h1:l+f6tTDcAH6xwepaAoW791ddhuYsJlqRATOzirO04Mo=
198198
github.com/pion/rtp v1.10.2/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk=
199-
github.com/pion/sctp v1.9.5 h1:QoSFB/drmAsmSeSFNQNI3xx010nW4HsycCZckRVWWag=
200-
github.com/pion/sctp v1.9.5/go.mod h1:N20Dq6LY+JvJDAh9VVh1JELngb2rQ8dPgds5yBWiPgw=
199+
github.com/pion/sctp v1.10.0 h1:qeoD6swF/2M5bYRcAGayqSbTKX3m4AW29CiQxG1+Pfg=
200+
github.com/pion/sctp v1.10.0/go.mod h1:N20Dq6LY+JvJDAh9VVh1JELngb2rQ8dPgds5yBWiPgw=
201201
github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI=
202202
github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8=
203203
github.com/pion/srtp/v3 v3.0.11 h1:GiESUr54/K4UuPigfq/CvWUed80JenQAHXn0C2MQQIQ=
@@ -208,12 +208,10 @@ github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkY
208208
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
209209
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
210210
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
211-
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
212-
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
213211
github.com/pion/turn/v5 v5.0.8 h1:pZUCtmwWCMkrRKqh/8pL3WoGADXBe0/lOPkN7oqFjK8=
214212
github.com/pion/turn/v5 v5.0.8/go.mod h1:1VwvxElZaOdJU0liJ/WUSm/Tsh+n2OxS5ISSDxgOWxU=
215-
github.com/pion/webrtc/v4 v4.2.11 h1:QUX1QZKlNIn4O7U5JxLPGP0sV5RTncZkzu9SPR3jVNU=
216-
github.com/pion/webrtc/v4 v4.2.11/go.mod h1:s/rAiyy77GyRFrZMx+Ls6aua26dIBPudH8/ZHYbIRWY=
213+
github.com/pion/webrtc/v4 v4.2.14 h1:Q6zMs+fSDsYuhZcNlvFGBxCOMHVV9oYcDa6O9/HIGTc=
214+
github.com/pion/webrtc/v4 v4.2.14/go.mod h1:87NVKP86+g4OMrRxWhjWfUjeXP4JrV6RTlUrIW+/Jak=
217215
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
218216
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
219217
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

pkg/sip/client.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,11 @@ func (c *Client) onBye(req *sip.Request, tx sip.ServerTransaction) bool {
350350
call.log.Infow("BYE from remote")
351351
go func(call *outboundCall) {
352352
call.cc.AcceptBye(req, tx)
353-
call.CloseWithReason(ctx, CallHangup, stats.Success("bye"), livekit.DisconnectReason_CLIENT_INITIATED)
353+
call.CloseWith(ctx, EndCall{
354+
Status: CallHangup,
355+
Term: stats.Success("bye"),
356+
Reason: livekit.DisconnectReason_CLIENT_INITIATED,
357+
})
354358
}(call)
355359
return true
356360
}

pkg/sip/errors.go

Lines changed: 51 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ import (
2121
// inviteFailure is the verdict for a failed outbound INVITE: how to record
2222
// the call, how to bucket the SLI, and which error to surface back.
2323
type inviteFailure struct {
24-
status CallStatus
25-
term stats.Termination
26-
reason livekit.DisconnectReason
27-
reportErr error // nil skips writing SIPCallInfo.Error
24+
EndCall
2825
returnErr error
2926
}
3027

@@ -54,18 +51,20 @@ func (e SDPError) GRPCStatus() *status.Status {
5451

5552
func (e SDPError) ClassifyInvite() inviteFailure {
5653
res := inviteFailure{
57-
status: callRejected,
58-
reason: livekit.DisconnectReason_MEDIA_FAILURE,
59-
reportErr: e.Err,
54+
EndCall: EndCall{
55+
Status: callRejected,
56+
Reason: livekit.DisconnectReason_MEDIA_FAILURE,
57+
Report: e.Err,
58+
},
6059
returnErr: e,
6160
}
6261
switch {
6362
case errors.Is(e.Err, sdp.ErrNoCommonMedia):
64-
res.term = stats.ClientError("no-common-codec")
63+
res.Term = stats.ClientError("no-common-codec")
6564
case errors.Is(e.Err, sdp.ErrNoCommonCrypto):
66-
res.term = stats.ClientError("encryption-required")
65+
res.Term = stats.ClientError("encryption-required")
6766
default:
68-
res.term = stats.ClientError("sdp-error")
67+
res.Term = stats.ClientError("sdp-error")
6968
}
7069
return res
7170
}
@@ -89,10 +88,12 @@ func (e transactionTimeoutError) ClassifyInvite() inviteFailure {
8988
reason = "no-final-response"
9089
}
9190
return inviteFailure{
92-
status: callUnavailable,
93-
term: stats.ClientError(reason),
94-
reason: livekit.DisconnectReason_SIP_TRUNK_FAILURE,
95-
reportErr: e, // keep so the customer sees their destination didn't complete
91+
EndCall: EndCall{
92+
Status: callUnavailable,
93+
Term: stats.ClientError(reason),
94+
Reason: livekit.DisconnectReason_SIP_TRUNK_FAILURE,
95+
Report: e, // keep so the customer sees their destination didn't complete
96+
},
9697
returnErr: psrpc.NewError(psrpc.Canceled, e),
9798
}
9899
}
@@ -107,106 +108,108 @@ func classifyInviteError(err error) inviteFailure {
107108
}
108109

109110
res := inviteFailure{
110-
status: callDropped,
111-
term: stats.ServerError("invite-failed"),
112-
reason: livekit.DisconnectReason_UNKNOWN_REASON,
113-
reportErr: err,
111+
EndCall: EndCall{
112+
Status: callDropped,
113+
Term: stats.ServerError("invite-failed"),
114+
Reason: livekit.DisconnectReason_UNKNOWN_REASON,
115+
Report: err,
116+
},
114117
returnErr: err,
115118
}
116119

117120
if sipStatus, ok := errors.AsType[*livekit.SIPStatus](err); ok {
118121
code := int(sipStatus.Code)
119122
switch code {
120123
case int(sip.StatusUnauthorized), int(sip.StatusProxyAuthRequired):
121-
res.status, res.term, res.reason = callRejected, stats.ClientError("auth-required"), livekit.DisconnectReason_USER_REJECTED
122-
res.reportErr = nil
124+
res.Status, res.Term, res.Reason = callRejected, stats.ClientError("auth-required"), livekit.DisconnectReason_USER_REJECTED
125+
res.Report = nil
123126
case int(sip.StatusForbidden):
124-
res.status, res.term, res.reason = callRejected, stats.ClientError("forbidden"), livekit.DisconnectReason_USER_REJECTED
125-
res.reportErr = nil
127+
res.Status, res.Term, res.Reason = callRejected, stats.ClientError("forbidden"), livekit.DisconnectReason_USER_REJECTED
128+
res.Report = nil
126129
case int(sip.StatusNotFound):
127-
res.status, res.term, res.reason = callUnavailable, stats.ClientError("not-found"), livekit.DisconnectReason_USER_UNAVAILABLE
128-
res.reportErr = nil
130+
res.Status, res.Term, res.Reason = callUnavailable, stats.ClientError("not-found"), livekit.DisconnectReason_USER_UNAVAILABLE
131+
res.Report = nil
129132
case int(sip.StatusRequestTimeout):
130-
res.status, res.term, res.reason = callUnavailable, stats.ClientError("request-timeout"), livekit.DisconnectReason_USER_UNAVAILABLE
131-
res.reportErr = nil
133+
res.Status, res.Term, res.Reason = callUnavailable, stats.ClientError("request-timeout"), livekit.DisconnectReason_USER_UNAVAILABLE
134+
res.Report = nil
132135
case int(sip.StatusTemporarilyUnavailable):
133-
res.status, res.term, res.reason = callUnavailable, stats.ClientError("unavailable"), livekit.DisconnectReason_USER_UNAVAILABLE
134-
res.reportErr = nil
136+
res.Status, res.Term, res.Reason = callUnavailable, stats.ClientError("unavailable"), livekit.DisconnectReason_USER_UNAVAILABLE
137+
res.Report = nil
135138
case int(sip.StatusBusyHere):
136-
res.status, res.term, res.reason = callRejected, stats.ClientError("busy"), livekit.DisconnectReason_USER_REJECTED
137-
res.reportErr = nil
139+
res.Status, res.Term, res.Reason = callRejected, stats.ClientError("busy"), livekit.DisconnectReason_USER_REJECTED
140+
res.Report = nil
138141
case int(sip.StatusNotAcceptableHere):
139-
res.status, res.term, res.reason = callRejected, stats.ClientError("not-acceptable"), livekit.DisconnectReason_USER_REJECTED
140-
res.reportErr = nil
142+
res.Status, res.Term, res.Reason = callRejected, stats.ClientError("not-acceptable"), livekit.DisconnectReason_USER_REJECTED
143+
res.Report = nil
141144
default:
142145
switch {
143146
case code >= 400 && code < 500:
144-
res.status, res.term, res.reason = callRejected, stats.ClientError(fmt.Sprintf("client-error-%d", code)), livekit.DisconnectReason_USER_UNAVAILABLE
145-
res.reportErr = nil
147+
res.Status, res.Term, res.Reason = callRejected, stats.ClientError(fmt.Sprintf("client-error-%d", code)), livekit.DisconnectReason_USER_UNAVAILABLE
148+
res.Report = nil
146149
case code >= 500 && code < 600:
147150
// Some upstreams (notably Twilio) return a 5xx when the customer's own trunk exceeds its configured CPS or
148151
// concurrent-call cap. That's a customer-side rate limit, not upstream infrastructure breakage, so it must not count
149152
// against the server-error SLI. Match on the response body — brittle, so kept narrow to the known phrases.
150153
body := strings.ToLower(sipStatus.GetStatus())
151154
switch {
152155
case strings.Contains(body, "cps limit exceeded"):
153-
res.status, res.term, res.reason = callRejected, stats.ClientError("cps-limit-exceeded"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
156+
res.Status, res.Term, res.Reason = callRejected, stats.ClientError("cps-limit-exceeded"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
154157
// keep reportErr so the customer can see they hit their cap
155158
case strings.Contains(body, "concurrent call limit exceeded"):
156-
res.status, res.term, res.reason = callRejected, stats.ClientError("concurrent-limit-exceeded"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
159+
res.Status, res.Term, res.Reason = callRejected, stats.ClientError("concurrent-limit-exceeded"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
157160
// keep reportErr so the customer can see they hit their cap
158161
default:
159162
// Carrier-side 5xx; keep reportErr for the detail.
160-
res.status, res.term, res.reason = callDropped, stats.UpstreamError(fmt.Sprintf("upstream-server-error-%d", code)), livekit.DisconnectReason_SIP_TRUNK_FAILURE
163+
res.Status, res.Term, res.Reason = callDropped, stats.UpstreamError(fmt.Sprintf("upstream-server-error-%d", code)), livekit.DisconnectReason_SIP_TRUNK_FAILURE
161164
}
162165
case code >= 600 && code < 700:
163-
res.status, res.term, res.reason = callRejected, stats.ClientError(fmt.Sprintf("global-decline-%d", code)), livekit.DisconnectReason_USER_REJECTED
164-
res.reportErr = nil
166+
res.Status, res.Term, res.Reason = callRejected, stats.ClientError(fmt.Sprintf("global-decline-%d", code)), livekit.DisconnectReason_USER_REJECTED
167+
res.Report = nil
165168
}
166169
}
167170
return res
168171
}
169172

170173
if errors.Is(err, ErrSIPRequestTimeout) {
171-
res.status, res.term, res.reason = callUnavailable, stats.ClientError("no-answer"), livekit.DisconnectReason_USER_UNAVAILABLE
172-
res.reportErr = nil
174+
res.Status, res.Term, res.Reason = callUnavailable, stats.ClientError("no-answer"), livekit.DisconnectReason_USER_UNAVAILABLE
175+
res.Report = nil
173176
return res
174177
}
175178

176179
// Context cancellation / deadline. Check before *net.OpError because an
177180
// op error can wrap a context error, and the context cause is more
178181
// informative.
179182
if errors.Is(err, context.DeadlineExceeded) {
180-
res.status, res.term, res.reason = callDropped, stats.ServerError("deadline-exceeded"), livekit.DisconnectReason_UNKNOWN_REASON
183+
res.Status, res.Term, res.Reason = callDropped, stats.ServerError("deadline-exceeded"), livekit.DisconnectReason_UNKNOWN_REASON
181184
res.returnErr = psrpc.NewError(psrpc.DeadlineExceeded, err)
182185
return res
183186
}
184187
if errors.Is(err, context.Canceled) {
185-
res.status, res.term, res.reason = callRejected, stats.ClientError("canceled"), livekit.DisconnectReason_USER_UNAVAILABLE
186-
res.reportErr = nil
188+
res.Status, res.Term, res.Reason = callRejected, stats.ClientError("canceled"), livekit.DisconnectReason_USER_UNAVAILABLE
189+
res.Report = nil
187190
res.returnErr = psrpc.NewError(psrpc.Canceled, err)
188191
return res
189192
}
190193

191194
// Specific net error types before *net.OpError (which wraps them).
192195
if _, ok := errors.AsType[*net.AddrError](err); ok {
193-
res.status, res.term, res.reason = callDropped, stats.ClientError("address-error"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
196+
res.Status, res.Term, res.Reason = callDropped, stats.ClientError("address-error"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
194197
res.returnErr = psrpc.NewError(psrpc.InvalidArgument, err)
195198
return res
196199
}
197200
if _, ok := errors.AsType[*net.DNSError](err); ok {
198-
res.status, res.term, res.reason = callDropped, stats.ClientError("dns-resolution"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
201+
res.Status, res.Term, res.Reason = callDropped, stats.ClientError("dns-resolution"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
199202
res.returnErr = psrpc.NewError(psrpc.InvalidArgument, err)
200203
return res
201204
}
202205
if _, ok := errors.AsType[*net.OpError](err); ok {
203-
res.status, res.term, res.reason = callDropped, stats.ServerError("network-error"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
206+
res.Status, res.Term, res.Reason = callDropped, stats.ServerError("network-error"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
204207
res.returnErr = psrpc.NewError(psrpc.Unavailable, err)
205208
return res
206209
}
207210

208211
if errors.Is(err, ErrAuthMaxRetry) || errors.Is(err, ErrAuthMissingCreds) || errors.Is(err, ErrAuthNoHeader) {
209-
res.status, res.term, res.reason = callRejected, stats.ClientError("auth-failed"), livekit.DisconnectReason_USER_REJECTED
212+
res.Status, res.Term, res.Reason = callRejected, stats.ClientError("auth-failed"), livekit.DisconnectReason_USER_REJECTED
210213
// keep reportErr so the auth detail is recorded
211214
return res
212215
}

pkg/sip/errors_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,13 @@ func TestClassifyInviteError(t *testing.T) {
9595
for _, tc := range cases {
9696
t.Run(tc.name, func(t *testing.T) {
9797
res := classifyInviteError(tc.err)
98-
require.Equal(t, tc.wantStatus, res.status, "status")
99-
require.Equal(t, tc.wantTerm, res.term, "termination")
100-
require.Equal(t, tc.wantReason, res.reason, "disconnect reason")
98+
require.Equal(t, tc.wantStatus, res.Status, "status")
99+
require.Equal(t, tc.wantTerm, res.Term, "termination")
100+
require.Equal(t, tc.wantReason, res.Reason, "disconnect reason")
101101
if tc.wantReport {
102-
require.NotNil(t, res.reportErr, "reportErr expected non-nil")
102+
require.NotNil(t, res.Report, "reportErr expected non-nil")
103103
} else {
104-
require.Nil(t, res.reportErr, "reportErr expected nil")
104+
require.Nil(t, res.Report, "reportErr expected nil")
105105
}
106106
})
107107
}

0 commit comments

Comments
 (0)