Repository navigation
Expand file tree
/
Copy pathengine_helpers_test.go
More file actions
232 lines (211 loc) · 6.29 KB
/
Copy pathengine_helpers_test.go
File metadata and controls
232 lines (211 loc) · 6.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
package dbgp
import (
"bufio"
"bytes"
"encoding/base64"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
"time"
)
// Helpers for tests that run against a real PHP with a DBGp debug engine:
// PHP Debugger or Xdebug. Both accept the xdebug.* ini settings used here.
// They speak DBGp directly (spec-conformant framing) rather than through
// Client, so fixture capture does not depend on the code under test.
const debuggeeScript = "testdata/php/basic.php"
// Line numbers in testdata/php/basic.php.
const (
lineAddBody = 24 // $sum = $a + $b;
lineAddReturn = 25 // return $sum;
lineCallAdd = 36 // $x = add(2, 3);
lineLoopBody = 39 // $y = $i * 2;
lineWarning = 42 // $missing = $undefinedVariable;
lineThrow = 44 // throw new RuntimeException("boom");
)
// knownBug skips a test that documents a known defect, unless DBGP_KNOWN_BUGS=1.
// Remove the call once the defect is fixed.
func knownBug(t *testing.T, id, summary string) {
t.Helper()
if os.Getenv("DBGP_KNOWN_BUGS") == "" {
t.Skipf("known bug %s: %s (set DBGP_KNOWN_BUGS=1 to run)", id, summary)
}
}
// requireDebugEngine skips the test unless php has a DBGp debug engine loaded:
// PHP Debugger (php_debugger) or Xdebug. With DBGP_REQUIRE_ENGINE=1 (as in CI)
// a missing engine fails the test instead.
func requireDebugEngine(t *testing.T) {
t.Helper()
skip := t.Skip
if os.Getenv("DBGP_REQUIRE_ENGINE") != "" {
skip = t.Fatal
}
if testing.Short() {
skip("skipping debug engine test in -short mode")
}
php, err := exec.LookPath("php")
if err != nil {
skip("php not found in PATH")
}
loaded := `exit(extension_loaded("php_debugger") || extension_loaded("xdebug") ? 0 : 1);`
// Debugging off: with it on, php would try to connect to whatever
// listens on the default port and wait for it.
if err := exec.Command(php, "-dxdebug.mode=off", "-r", loaded).Run(); err != nil {
skip("php has neither PHP Debugger nor Xdebug loaded")
}
}
// debuggeePath returns the absolute path of the debuggee script.
func debuggeePath(t *testing.T) string {
t.Helper()
path, err := filepath.Abs(debuggeeScript)
if err != nil {
t.Fatal(err)
}
return path
}
// debuggee is a running PHP process for the debuggee script.
type debuggee struct {
out bytes.Buffer // stdout and stderr; read only after exited
exited chan struct{}
}
// startDebuggee runs the debuggee script with the engine connecting to port.
// extraIni adds -d settings, e.g. "xdebug.idekey=agent". The process is
// killed and reaped at test cleanup.
func startDebuggee(t *testing.T, port int, extraIni ...string) *debuggee {
t.Helper()
return startDebuggeeScript(t, port, debuggeePath(t), extraIni...)
}
// startDebuggeeScript is startDebuggee for another copy of the script.
func startDebuggeeScript(t *testing.T, port int, script string, extraIni ...string) *debuggee {
t.Helper()
args := []string{
"-dxdebug.mode=debug",
"-dxdebug.start_with_request=yes",
"-dxdebug.client_host=127.0.0.1",
fmt.Sprintf("-dxdebug.client_port=%d", port),
"-dxdebug.log_level=0",
// Warnings reach the IDE as notifications; printing them too would put
// local paths into the recorded stdout stream.
"-ddisplay_errors=0",
}
for _, ini := range extraIni {
args = append(args, "-d"+ini)
}
d := &debuggee{exited: make(chan struct{})}
cmd := exec.Command("php", append(args, script)...)
cmd.Stdout = &d.out
cmd.Stderr = &d.out
if err := cmd.Start(); err != nil {
t.Fatalf("start php: %v", err)
}
go func() {
_ = cmd.Wait()
close(d.exited)
}()
t.Cleanup(func() {
_ = cmd.Process.Kill()
<-d.exited
})
return d
}
// rawSession is a minimal, spec-conformant DBGp IDE side.
type rawSession struct {
t *testing.T
conn net.Conn
reader *bufio.Reader
nextID int
init []byte
}
// newRawSession listens, starts the debuggee and reads its init packet.
func newRawSession(t *testing.T) *rawSession {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
startDebuggee(t, ln.Addr().(*net.TCPAddr).Port)
_ = ln.(*net.TCPListener).SetDeadline(time.Now().Add(10 * time.Second))
conn, err := ln.Accept()
if err != nil {
t.Fatalf("accept Xdebug connection: %v", err)
}
t.Cleanup(func() { _ = conn.Close() })
s := &rawSession{t: t, conn: conn, reader: bufio.NewReader(conn)}
s.init = s.read()
return s
}
// read reads one engine packet: length NUL xml NUL.
func (s *rawSession) read() []byte {
s.t.Helper()
_ = s.conn.SetReadDeadline(time.Now().Add(10 * time.Second))
data, err := readEnginePacket(s.reader)
if err != nil {
s.t.Fatalf("read packet: %v", err)
}
return data
}
// readEnginePacket reads a single engine-to-IDE packet.
func readEnginePacket(r *bufio.Reader) ([]byte, error) {
lenStr, err := r.ReadString(0)
if err != nil {
return nil, err
}
n, err := strconv.Atoi(strings.TrimSuffix(lenStr, "\x00"))
if err != nil {
return nil, fmt.Errorf("bad length %q: %w", lenStr, err)
}
data := make([]byte, n+1)
if _, err := io.ReadFull(r, data); err != nil {
return nil, err
}
if data[n] != 0 {
return nil, fmt.Errorf("missing NUL terminator")
}
return data[:n], nil
}
// sendRaw writes bytes to the engine unmodified.
func (s *rawSession) sendRaw(raw string) {
s.t.Helper()
if _, err := s.conn.Write([]byte(raw)); err != nil {
s.t.Fatalf("write: %v", err)
}
}
// command sends "name args -i N [-- base64(data)]" and returns the matching
// response, plus any stream/notify packets that arrived before it.
func (s *rawSession) command(name, args string, data ...string) (resp []byte, other [][]byte) {
s.t.Helper()
s.nextID++
cmd := name
if args != "" {
cmd += " " + args
}
cmd += fmt.Sprintf(" -i %d", s.nextID)
if len(data) > 0 {
cmd += " -- " + base64.StdEncoding.EncodeToString([]byte(data[0]))
}
s.sendRaw(cmd + "\x00")
want := fmt.Sprintf(`transaction_id="%d"`, s.nextID)
for {
p := s.read()
if bytes.Contains(p, []byte("<response")) && bytes.Contains(p, []byte(want)) {
return p, other
}
other = append(other, p)
}
}
var transactionIDAttr = regexp.MustCompile(`transaction_id="[^"]*"`)
// attr extracts the first value of an XML attribute from a packet.
func attr(packet []byte, name string) string {
m := regexp.MustCompile(`\s` + regexp.QuoteMeta(name) + `="([^"]*)"`).FindSubmatch(packet)
if m == nil {
return ""
}
return string(m[1])
}