• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

playwright-community / playwright-go / 19022173124

03 Nov 2025 02:26AM UTC coverage: 80.895% (-0.02%) from 80.917%
19022173124

Pull #567

github

flimzy
Implement closeWasCalled with atomic.Bool to prevent data race
Pull Request #567: Fix data race between routing and closing context

5 of 5 new or added lines in 2 files covered. (100.0%)

2 existing lines in 1 file now uncovered.

7482 of 9249 relevant lines covered (80.9%)

5070.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.39
/connection.go
1
package playwright
2

3
import (
4
        "errors"
5
        "fmt"
6
        "reflect"
7
        "regexp"
8
        "strconv"
9
        "strings"
10
        "sync"
11
        "sync/atomic"
12
        "time"
13

14
        "github.com/go-stack/stack"
15
        "github.com/playwright-community/playwright-go/internal/safe"
16
)
17

18
var (
19
        pkgSourcePathPattern = regexp.MustCompile(`.+[\\/]playwright-go[\\/][^\\/]+\.go`)
20
        apiNameTransform     = regexp.MustCompile(`(?U)\(\*(.+)(Impl)?\)`)
21
)
22

23
type connection struct {
24
        transport    transport
25
        apiZone      sync.Map
26
        objects      *safe.SyncMap[string, *channelOwner]
27
        lastID       atomic.Uint32
28
        rootObject   *rootChannelOwner
29
        callbacks    *safe.SyncMap[uint32, *protocolCallback]
30
        afterClose   func()
31
        onClose      func() error
32
        isRemote     bool
33
        localUtils   *localUtilsImpl
34
        tracingCount atomic.Int32
35
        abort        chan struct{}
36
        abortOnce    sync.Once
37
        err          *safeValue[error] // for event listener error
38
        closedError  *safeValue[error]
39
}
40

41
func (c *connection) Start() (*Playwright, error) {
107✔
42
        go func() {
214✔
43
                for {
201,547✔
44
                        msg, err := c.transport.Poll()
201,440✔
45
                        if err != nil {
201,539✔
46
                                _ = c.transport.Close()
99✔
47
                                c.cleanup(err)
99✔
48
                                return
99✔
49
                        }
99✔
50
                        c.Dispatch(msg)
201,333✔
51
                }
52
        }()
53

54
        c.onClose = func() error {
230✔
55
                if err := c.transport.Close(); err != nil {
155✔
56
                        return err
32✔
57
                }
32✔
58
                return nil
91✔
59
        }
60

61
        return c.rootObject.initialize()
107✔
62
}
63

64
func (c *connection) Stop() error {
123✔
65
        if err := c.onClose(); err != nil {
155✔
66
                return err
32✔
67
        }
32✔
68
        c.cleanup()
91✔
69
        return nil
91✔
70
}
71

72
func (c *connection) cleanup(cause ...error) {
286✔
73
        if len(cause) > 0 {
385✔
74
                c.closedError.Set(fmt.Errorf("%w: %w", ErrTargetClosed, cause[0]))
99✔
75
        } else {
286✔
76
                c.closedError.Set(ErrTargetClosed)
187✔
77
        }
187✔
78
        if c.afterClose != nil {
572✔
79
                c.afterClose()
286✔
80
        }
286✔
81
        c.abortOnce.Do(func() {
385✔
82
                select {
99✔
83
                case <-c.abort:
×
84
                default:
99✔
85
                        close(c.abort)
99✔
86
                }
87
        })
88
}
89

90
func (c *connection) Dispatch(msg *message) {
201,333✔
91
        if c.closedError.Get() != nil {
201,333✔
UNCOV
92
                return
×
UNCOV
93
        }
×
94
        method := msg.Method
201,333✔
95
        if msg.ID != 0 {
251,173✔
96
                cb, _ := c.callbacks.LoadAndDelete(uint32(msg.ID))
49,840✔
97
                if cb.noReply {
51,058✔
98
                        return
1,218✔
99
                }
1,218✔
100
                if msg.Error != nil {
49,280✔
101
                        cb.SetError(parseError(msg.Error.Error))
658✔
102
                } else {
48,622✔
103
                        cb.SetResult(c.replaceGuidsWithChannels(msg.Result).(map[string]interface{}))
47,964✔
104
                }
47,964✔
105
                return
48,622✔
106
        }
107
        object, _ := c.objects.Load(msg.GUID)
151,493✔
108
        if method == "__create__" {
193,316✔
109
                c.createRemoteObject(
41,823✔
110
                        object, msg.Params["type"].(string), msg.Params["guid"].(string), msg.Params["initializer"],
41,823✔
111
                )
41,823✔
112
                return
41,823✔
113
        }
41,823✔
114
        if object == nil {
109,670✔
115
                return
×
116
        }
×
117
        if method == "__adopt__" {
127,942✔
118
                child, ok := c.objects.Load(msg.Params["guid"].(string))
18,272✔
119
                if !ok {
18,272✔
120
                        return
×
121
                }
×
122
                object.adopt(child)
18,272✔
123
                return
18,272✔
124
        }
125
        if method == "__dispose__" {
113,052✔
126
                reason, ok := msg.Params["reason"]
21,654✔
127
                if ok {
21,654✔
128
                        object.dispose(reason.(string))
×
129
                } else {
21,654✔
130
                        object.dispose()
21,654✔
131
                }
21,654✔
132
                return
21,654✔
133
        }
134
        if object.objectType == "JsonPipe" {
73,425✔
135
                object.channel.Emit(method, msg.Params)
3,681✔
136
        } else {
69,744✔
137
                object.channel.Emit(method, c.replaceGuidsWithChannels(msg.Params))
66,063✔
138
        }
66,063✔
139
}
140

141
func (c *connection) LocalUtils() *localUtilsImpl {
424✔
142
        return c.localUtils
424✔
143
}
424✔
144

145
func (c *connection) createRemoteObject(parent *channelOwner, objectType string, guid string, initializer interface{}) interface{} {
41,823✔
146
        initializer = c.replaceGuidsWithChannels(initializer)
41,823✔
147
        result := createObjectFactory(parent, objectType, guid, initializer.(map[string]interface{}))
41,823✔
148
        return result
41,823✔
149
}
41,823✔
150

151
func (c *connection) WrapAPICall(cb func() (interface{}, error), isInternal bool) (interface{}, error) {
59,056✔
152
        if _, ok := c.apiZone.Load("apiZone"); ok {
68,309✔
153
                return cb()
9,253✔
154
        }
9,253✔
155
        c.apiZone.Store("apiZone", serializeCallStack(isInternal))
49,803✔
156
        return cb()
49,803✔
157
}
158

159
func (c *connection) replaceGuidsWithChannels(payload interface{}) interface{} {
682,332✔
160
        if payload == nil {
682,332✔
161
                return nil
×
162
        }
×
163
        v := reflect.ValueOf(payload)
682,332✔
164
        if v.Kind() == reflect.Slice {
706,019✔
165
                listV := payload.([]interface{})
23,687✔
166
                for i := 0; i < len(listV); i++ {
107,519✔
167
                        listV[i] = c.replaceGuidsWithChannels(listV[i])
83,832✔
168
                }
83,832✔
169
                return listV
23,687✔
170
        }
171
        if v.Kind() == reflect.Map {
1,009,913✔
172
                mapV := payload.(map[string]interface{})
351,268✔
173
                if guid, hasGUID := mapV["guid"]; hasGUID {
439,062✔
174
                        if channelOwner, ok := c.objects.Load(guid.(string)); ok {
175,374✔
175
                                return channelOwner.channel
87,580✔
176
                        }
87,580✔
177
                }
178
                for key := range mapV {
706,338✔
179
                        mapV[key] = c.replaceGuidsWithChannels(mapV[key])
442,650✔
180
                }
442,650✔
181
                return mapV
263,688✔
182
        }
183
        return payload
307,377✔
184
}
185

186
func (c *connection) sendMessageToServer(object *channelOwner, method string, params interface{}, noReply bool) (cb *protocolCallback) {
49,851✔
187
        cb = newProtocolCallback(noReply, c.abort)
49,851✔
188

49,851✔
189
        if err := c.closedError.Get(); err != nil {
49,854✔
190
                cb.SetError(err)
3✔
191
                return
3✔
192
        }
3✔
193
        if object.wasCollected {
49,848✔
194
                cb.SetError(errors.New("The object has been collected to prevent unbounded heap growth."))
×
195
                return
×
196
        }
×
197

198
        id := c.lastID.Add(1)
49,848✔
199
        c.callbacks.Store(id, cb)
49,848✔
200
        var (
49,848✔
201
                metadata = make(map[string]interface{}, 0)
49,848✔
202
                stack    = make([]map[string]interface{}, 0)
49,848✔
203
        )
49,848✔
204
        apiZone, ok := c.apiZone.LoadAndDelete("apiZone")
49,848✔
205
        if ok {
99,585✔
206
                for k, v := range apiZone.(parsedStackTrace).metadata {
188,467✔
207
                        metadata[k] = v
138,730✔
208
                }
138,730✔
209
                stack = append(stack, apiZone.(parsedStackTrace).frames...)
49,737✔
210
        }
211
        metadata["wallTime"] = time.Now().UnixMilli()
49,848✔
212
        message := map[string]interface{}{
49,848✔
213
                "id":       id,
49,848✔
214
                "guid":     object.guid,
49,848✔
215
                "method":   method,
49,848✔
216
                "params":   params, // channel.MarshalJSON will replace channel with guid
49,848✔
217
                "metadata": metadata,
49,848✔
218
        }
49,848✔
219
        if c.tracingCount.Load() > 0 && len(stack) > 0 && object.guid != "localUtils" {
50,040✔
220
                c.LocalUtils().AddStackToTracingNoReply(id, stack)
192✔
221
        }
192✔
222

223
        if err := c.transport.Send(message); err != nil {
49,853✔
224
                cb.SetError(fmt.Errorf("could not send message: %w", err))
5✔
225
                return
5✔
226
        }
5✔
227

228
        return
49,843✔
229
}
230

231
func (c *connection) setInTracing(isTracing bool) {
112✔
232
        if isTracing {
168✔
233
                c.tracingCount.Add(1)
56✔
234
        } else {
112✔
235
                c.tracingCount.Add(-1)
56✔
236
        }
56✔
237
}
238

239
type parsedStackTrace struct {
240
        frames   []map[string]interface{}
241
        metadata map[string]interface{}
242
}
243

244
func serializeCallStack(isInternal bool) parsedStackTrace {
49,803✔
245
        st := stack.Trace().TrimRuntime()
49,803✔
246
        if len(st) == 0 { // https://github.com/go-stack/stack/issues/27
49,803✔
247
                st = stack.Trace()
×
248
        }
×
249

250
        lastInternalIndex := 0
49,803✔
251
        for i, s := range st {
340,439✔
252
                if pkgSourcePathPattern.MatchString(s.Frame().File) {
520,958✔
253
                        lastInternalIndex = i
230,322✔
254
                }
230,322✔
255
        }
256
        apiName := ""
49,803✔
257
        if !isInternal {
86,974✔
258
                apiName = fmt.Sprintf("%n", st[lastInternalIndex])
37,171✔
259
        }
37,171✔
260
        st = st.TrimBelow(st[lastInternalIndex])
49,803✔
261

49,803✔
262
        callStack := make([]map[string]interface{}, 0)
49,803✔
263
        for i, s := range st {
158,865✔
264
                if i == 0 {
158,865✔
265
                        continue
49,803✔
266
                }
267
                callStack = append(callStack, map[string]interface{}{
59,259✔
268
                        "file":     s.Frame().File,
59,259✔
269
                        "line":     s.Frame().Line,
59,259✔
270
                        "column":   0,
59,259✔
271
                        "function": s.Frame().Function,
59,259✔
272
                })
59,259✔
273
        }
274
        metadata := make(map[string]interface{})
49,803✔
275
        if len(st) > 1 {
89,080✔
276
                metadata["location"] = serializeCallLocation(st[1])
39,277✔
277
        }
39,277✔
278
        apiName = apiNameTransform.ReplaceAllString(apiName, "$1")
49,803✔
279
        if len(apiName) > 1 {
86,974✔
280
                apiName = strings.ToUpper(apiName[:1]) + apiName[1:]
37,171✔
281
        }
37,171✔
282
        metadata["apiName"] = apiName
49,803✔
283
        metadata["isInternal"] = isInternal
49,803✔
284
        return parsedStackTrace{
49,803✔
285
                metadata: metadata,
49,803✔
286
                frames:   callStack,
49,803✔
287
        }
49,803✔
288
}
289

290
func serializeCallLocation(caller stack.Call) map[string]interface{} {
39,277✔
291
        line, _ := strconv.Atoi(fmt.Sprintf("%d", caller))
39,277✔
292
        return map[string]interface{}{
39,277✔
293
                "file": fmt.Sprintf("%s", caller),
39,277✔
294
                "line": line,
39,277✔
295
        }
39,277✔
296
}
39,277✔
297

298
func newConnection(transport transport, localUtils ...*localUtilsImpl) *connection {
107✔
299
        connection := &connection{
107✔
300
                abort:       make(chan struct{}, 1),
107✔
301
                callbacks:   safe.NewSyncMap[uint32, *protocolCallback](),
107✔
302
                objects:     safe.NewSyncMap[string, *channelOwner](),
107✔
303
                transport:   transport,
107✔
304
                isRemote:    false,
107✔
305
                err:         &safeValue[error]{},
107✔
306
                closedError: &safeValue[error]{},
107✔
307
        }
107✔
308
        if len(localUtils) > 0 {
203✔
309
                connection.localUtils = localUtils[0]
96✔
310
                connection.isRemote = true
96✔
311
        }
96✔
312
        connection.rootObject = newRootChannelOwner(connection)
107✔
313
        return connection
107✔
314
}
315

316
func fromChannel(v interface{}) interface{} {
94,882✔
317
        return v.(*channel).object
94,882✔
318
}
94,882✔
319

320
func fromNullableChannel(v interface{}) interface{} {
30,136✔
321
        if v == nil {
42,852✔
322
                return nil
12,716✔
323
        }
12,716✔
324
        return fromChannel(v)
17,420✔
325
}
326

327
type protocolCallback struct {
328
        done    chan struct{}
329
        noReply bool
330
        abort   <-chan struct{}
331
        once    sync.Once
332
        value   map[string]interface{}
333
        err     error
334
}
335

336
func (pc *protocolCallback) setResultOnce(result map[string]interface{}, err error) {
48,638✔
337
        pc.once.Do(func() {
97,276✔
338
                pc.value = result
48,638✔
339
                pc.err = err
48,638✔
340
                close(pc.done)
48,638✔
341
        })
48,638✔
342
}
343

344
func (pc *protocolCallback) waitResult() {
49,859✔
345
        if pc.noReply {
51,077✔
346
                return
1,218✔
347
        }
1,218✔
348
        select {
48,641✔
349
        case <-pc.done: // wait for result
48,636✔
350
                return
48,636✔
351
        case <-pc.abort:
5✔
352
                select {
5✔
353
                case <-pc.done:
2✔
354
                        return
2✔
355
                default:
3✔
356
                        pc.err = errors.New("Connection closed")
3✔
357
                        return
3✔
358
                }
359
        }
360
}
361

362
func (pc *protocolCallback) SetError(err error) {
674✔
363
        pc.setResultOnce(nil, err)
674✔
364
}
674✔
365

366
func (pc *protocolCallback) SetResult(result map[string]interface{}) {
47,964✔
367
        pc.setResultOnce(result, nil)
47,964✔
368
}
47,964✔
369

370
func (pc *protocolCallback) GetResult() (map[string]interface{}, error) {
4,511✔
371
        pc.waitResult()
4,511✔
372
        return pc.value, pc.err
4,511✔
373
}
4,511✔
374

375
// GetResultValue returns value if the map has only one element
376
func (pc *protocolCallback) GetResultValue() (interface{}, error) {
45,348✔
377
        pc.waitResult()
45,348✔
378
        if len(pc.value) == 0 { // empty map treated as nil
71,602✔
379
                return nil, pc.err
26,254✔
380
        }
26,254✔
381
        if len(pc.value) == 1 {
38,188✔
382
                for key := range pc.value {
38,188✔
383
                        return pc.value[key], pc.err
19,094✔
384
                }
19,094✔
385
        }
386

387
        return pc.value, pc.err
×
388
}
389

390
func newProtocolCallback(noReply bool, abort <-chan struct{}) *protocolCallback {
49,859✔
391
        if noReply {
51,077✔
392
                return &protocolCallback{
1,218✔
393
                        noReply: true,
1,218✔
394
                        abort:   abort,
1,218✔
395
                }
1,218✔
396
        }
1,218✔
397
        return &protocolCallback{
48,641✔
398
                done:  make(chan struct{}, 1),
48,641✔
399
                abort: abort,
48,641✔
400
        }
48,641✔
401
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc