diffgenome 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diffgenome/__init__.py +7 -0
- diffgenome/__main__.py +240 -0
- diffgenome/_collectors/go/dg/dg.go +623 -0
- diffgenome/_collectors/go/go.mod +3 -0
- diffgenome/_collectors/go/instrument/facts.go +346 -0
- diffgenome/_collectors/go/instrument/main.go +484 -0
- diffgenome/_collectors/node/instrument.js +289 -0
- diffgenome/_collectors/node/jest-setup.js +40 -0
- diffgenome/_collectors/node/package-lock.json +35 -0
- diffgenome/_collectors/node/package.json +11 -0
- diffgenome/_collectors/node/runtime.js +426 -0
- diffgenome/ambiguity.py +122 -0
- diffgenome/api.py +67 -0
- diffgenome/change.py +86 -0
- diffgenome/change_artifact.py +310 -0
- diffgenome/collect/__init__.py +2 -0
- diffgenome/collect/go_test.py +271 -0
- diffgenome/collect/node_jest.py +319 -0
- diffgenome/collect/py_monitoring.py +985 -0
- diffgenome/collect/py_runtime.py +116 -0
- diffgenome/collect/py_symbols.py +238 -0
- diffgenome/collect/pytest_plugin.py +130 -0
- diffgenome/compose.py +469 -0
- diffgenome/dependence.py +264 -0
- diffgenome/evaluate.py +669 -0
- diffgenome/frontends/__init__.py +0 -0
- diffgenome/frontends/python_ir.py +335 -0
- diffgenome/genome.py +1016 -0
- diffgenome/genome_pipeline.py +674 -0
- diffgenome/genome_prompt.py +33 -0
- diffgenome/genome_state.py +2118 -0
- diffgenome/graph.py +426 -0
- diffgenome/llm.py +189 -0
- diffgenome/model.py +364 -0
- diffgenome/mvp.py +398 -0
- diffgenome/probe.py +509 -0
- diffgenome/projection.py +308 -0
- diffgenome/py.typed +0 -0
- diffgenome/render.py +118 -0
- diffgenome/report.py +363 -0
- diffgenome/resolve.py +37 -0
- diffgenome/runtime.py +74 -0
- diffgenome/runtime_evidence.py +261 -0
- diffgenome/sandbox.py +166 -0
- diffgenome/serialize.py +96 -0
- diffgenome/sites.py +19 -0
- diffgenome/static_types.py +69 -0
- diffgenome/structure.py +462 -0
- diffgenome-0.1.0.dist-info/METADATA +139 -0
- diffgenome-0.1.0.dist-info/RECORD +53 -0
- diffgenome-0.1.0.dist-info/WHEEL +4 -0
- diffgenome-0.1.0.dist-info/entry_points.txt +2 -0
- diffgenome-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
// Package dg is the diffgenome Go runtime: instrumented code reports function entry,
|
|
2
|
+
// exit (results or panic) and test stimuli here, and one Execution (the diffgenome event
|
|
3
|
+
// protocol, JSON) is written per test or subtest. Copied into the workspace copy of the
|
|
4
|
+
// target as <module>/internal/diffgenome/dg so no network or module changes are needed.
|
|
5
|
+
//
|
|
6
|
+
// Attribution: per-goroutine call stacks keyed by goroutine id. Goroutines spawned by
|
|
7
|
+
// instrumented code start with no parent (their calls hang off the stimulus root); spawn
|
|
8
|
+
// causality is recorded as a limitation, not guessed.
|
|
9
|
+
package dg
|
|
10
|
+
|
|
11
|
+
import (
|
|
12
|
+
"bytes"
|
|
13
|
+
"crypto/sha256"
|
|
14
|
+
"encoding/hex"
|
|
15
|
+
"encoding/json"
|
|
16
|
+
"fmt"
|
|
17
|
+
"os"
|
|
18
|
+
"path/filepath"
|
|
19
|
+
"reflect"
|
|
20
|
+
"runtime"
|
|
21
|
+
"sort"
|
|
22
|
+
"strconv"
|
|
23
|
+
"strings"
|
|
24
|
+
"sync"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const (
|
|
28
|
+
maxArgs = 8
|
|
29
|
+
digestDepth = 3
|
|
30
|
+
digestWidth = 16
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
// Meta is what the instrumenter knows statically about a function.
|
|
34
|
+
type Meta struct {
|
|
35
|
+
Sym string
|
|
36
|
+
Origin string // "repo" | "test"
|
|
37
|
+
File string
|
|
38
|
+
Line int
|
|
39
|
+
Params []string
|
|
40
|
+
Claim string // for test doubles: the in-repo symbol this method stands in for
|
|
41
|
+
Relation string // how Claim was established, e.g. "mocks-interface:Store"
|
|
42
|
+
Recv any // the receiver, for state facts (nil for functions)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type symbol struct {
|
|
46
|
+
ID string `json:"id"`
|
|
47
|
+
Origin string `json:"origin"`
|
|
48
|
+
Location *location `json:"location"`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type location struct {
|
|
52
|
+
Path string `json:"path"`
|
|
53
|
+
Line int `json:"line"`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type node struct {
|
|
57
|
+
Type string `json:"type"`
|
|
58
|
+
ID int `json:"id"`
|
|
59
|
+
Parent *int `json:"parent"`
|
|
60
|
+
Symbol string `json:"symbol,omitempty"`
|
|
61
|
+
Collector int `json:"collector"`
|
|
62
|
+
Args [][3]string `json:"args"`
|
|
63
|
+
Thread int `json:"thread,omitempty"`
|
|
64
|
+
Outcome string `json:"outcome"`
|
|
65
|
+
Result string `json:"result"`
|
|
66
|
+
// substitution fields (present only on substitution nodes; see MarshalJSON)
|
|
67
|
+
Mechanism string `json:"mechanism,omitempty"`
|
|
68
|
+
Substitute string `json:"substitute,omitempty"`
|
|
69
|
+
Claimed *string `json:"-"`
|
|
70
|
+
Relation string `json:"relation,omitempty"`
|
|
71
|
+
Path []string `json:"-"`
|
|
72
|
+
State [][3]string `json:"-"`
|
|
73
|
+
StateAfter [][3]string `json:"-"`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// MarshalJSON emits exactly the protocol's fields for each node type.
|
|
77
|
+
func (n *node) MarshalJSON() ([]byte, error) {
|
|
78
|
+
m := map[string]any{"type": n.Type, "id": n.ID, "parent": n.Parent, "collector": n.Collector,
|
|
79
|
+
"args": n.Args, "outcome": n.Outcome, "result": n.Result, "state": n.State}
|
|
80
|
+
if n.Args == nil {
|
|
81
|
+
m["args"] = [][3]string{}
|
|
82
|
+
}
|
|
83
|
+
if n.State == nil {
|
|
84
|
+
m["state"] = [][3]string{}
|
|
85
|
+
}
|
|
86
|
+
if n.Type == "call" {
|
|
87
|
+
m["symbol"] = n.Symbol
|
|
88
|
+
m["thread"] = n.Thread
|
|
89
|
+
sa := n.StateAfter
|
|
90
|
+
if sa == nil {
|
|
91
|
+
sa = [][3]string{}
|
|
92
|
+
}
|
|
93
|
+
m["state_after"] = sa
|
|
94
|
+
} else {
|
|
95
|
+
m["mechanism"] = n.Mechanism
|
|
96
|
+
m["substitute"] = n.Substitute
|
|
97
|
+
m["claimed_target"] = n.Claimed
|
|
98
|
+
m["relation"] = n.Relation
|
|
99
|
+
p := n.Path
|
|
100
|
+
if p == nil {
|
|
101
|
+
p = []string{}
|
|
102
|
+
}
|
|
103
|
+
m["path"] = p
|
|
104
|
+
}
|
|
105
|
+
return json.Marshal(m)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type branchObs struct {
|
|
109
|
+
Site string `json:"site"`
|
|
110
|
+
Outcome bool `json:"outcome"`
|
|
111
|
+
Node int `json:"node"`
|
|
112
|
+
Seq int `json:"seq"`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type execution struct {
|
|
116
|
+
ref string
|
|
117
|
+
nodes []*node
|
|
118
|
+
branches []branchObs
|
|
119
|
+
symbols map[string]symbol
|
|
120
|
+
stacks map[int64][]int // goroutine id -> node ids
|
|
121
|
+
goroutine map[int64]int // goroutine id -> thread number
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Ctx is returned by Enter and consumed by Exit/Panic.
|
|
125
|
+
type Ctx struct {
|
|
126
|
+
id int
|
|
127
|
+
sub int // substitution wrapper node id, or -1
|
|
128
|
+
gid int64
|
|
129
|
+
exec *execution
|
|
130
|
+
recv any // receiver, for the state view on exit
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
var (
|
|
134
|
+
mu sync.Mutex
|
|
135
|
+
active []*execution
|
|
136
|
+
outDir = os.Getenv("DIFFGENOME_OUT")
|
|
137
|
+
stim = envOr("DIFFGENOME_STIMULUS", "existing_test")
|
|
138
|
+
rev = os.Getenv("DIFFGENOME_REVISION")
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
func envOr(k, d string) string {
|
|
142
|
+
if v := os.Getenv(k); v != "" {
|
|
143
|
+
return v
|
|
144
|
+
}
|
|
145
|
+
return d
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func goid() int64 {
|
|
149
|
+
var buf [64]byte
|
|
150
|
+
n := runtime.Stack(buf[:], false)
|
|
151
|
+
fields := bytes.Fields(buf[:n])
|
|
152
|
+
if len(fields) >= 2 {
|
|
153
|
+
id, _ := strconv.ParseInt(string(fields[1]), 10, 64)
|
|
154
|
+
return id
|
|
155
|
+
}
|
|
156
|
+
return 0
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ----------------------------------------------------------------------------- summaries
|
|
160
|
+
|
|
161
|
+
func shape(v any) string {
|
|
162
|
+
if v == nil {
|
|
163
|
+
return "nil"
|
|
164
|
+
}
|
|
165
|
+
rv := reflect.ValueOf(v)
|
|
166
|
+
switch rv.Kind() {
|
|
167
|
+
case reflect.Slice, reflect.Array, reflect.Map, reflect.String:
|
|
168
|
+
if rv.Kind() == reflect.String {
|
|
169
|
+
return "string"
|
|
170
|
+
}
|
|
171
|
+
return fmt.Sprintf("%s[%d]", rv.Type().String(), rv.Len())
|
|
172
|
+
case reflect.Ptr:
|
|
173
|
+
if rv.IsNil() {
|
|
174
|
+
return rv.Type().String() + "(nil)"
|
|
175
|
+
}
|
|
176
|
+
return rv.Type().String()
|
|
177
|
+
}
|
|
178
|
+
return rv.Type().String()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func canonical(v any, depth int) (string, bool) {
|
|
182
|
+
if v == nil {
|
|
183
|
+
return "nil", true
|
|
184
|
+
}
|
|
185
|
+
rv := reflect.ValueOf(v)
|
|
186
|
+
return canonicalValue(rv, depth)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func canonicalValue(rv reflect.Value, depth int) (string, bool) {
|
|
190
|
+
switch rv.Kind() {
|
|
191
|
+
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
192
|
+
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
|
|
193
|
+
return fmt.Sprintf("%s:%v", rv.Kind(), rv.Interface()), true
|
|
194
|
+
case reflect.String:
|
|
195
|
+
return "string:" + strconv.Quote(rv.String()), true
|
|
196
|
+
case reflect.Ptr, reflect.Interface:
|
|
197
|
+
if rv.IsNil() {
|
|
198
|
+
return "nil", true
|
|
199
|
+
}
|
|
200
|
+
if depth == 0 {
|
|
201
|
+
return "", false
|
|
202
|
+
}
|
|
203
|
+
return canonicalValue(rv.Elem(), depth-1)
|
|
204
|
+
case reflect.Slice, reflect.Array:
|
|
205
|
+
if depth == 0 || rv.Len() > digestWidth {
|
|
206
|
+
return "", false
|
|
207
|
+
}
|
|
208
|
+
parts := make([]string, 0, rv.Len())
|
|
209
|
+
for i := 0; i < rv.Len(); i++ {
|
|
210
|
+
c, ok := canonicalValue(rv.Index(i), depth-1)
|
|
211
|
+
if !ok {
|
|
212
|
+
return "", false
|
|
213
|
+
}
|
|
214
|
+
parts = append(parts, c)
|
|
215
|
+
}
|
|
216
|
+
return "array[" + strings.Join(parts, ",") + "]", true
|
|
217
|
+
case reflect.Map:
|
|
218
|
+
if depth == 0 || rv.Len() > digestWidth {
|
|
219
|
+
return "", false
|
|
220
|
+
}
|
|
221
|
+
items := make([]string, 0, rv.Len())
|
|
222
|
+
iter := rv.MapRange()
|
|
223
|
+
for iter.Next() {
|
|
224
|
+
k, ok1 := canonicalValue(iter.Key(), depth-1)
|
|
225
|
+
val, ok2 := canonicalValue(iter.Value(), depth-1)
|
|
226
|
+
if !ok1 || !ok2 {
|
|
227
|
+
return "", false
|
|
228
|
+
}
|
|
229
|
+
items = append(items, k+"="+val)
|
|
230
|
+
}
|
|
231
|
+
sort.Strings(items)
|
|
232
|
+
return "map{" + strings.Join(items, ",") + "}", true
|
|
233
|
+
case reflect.Struct:
|
|
234
|
+
if depth == 0 || rv.NumField() > digestWidth {
|
|
235
|
+
return "", false
|
|
236
|
+
}
|
|
237
|
+
t := rv.Type()
|
|
238
|
+
items := make([]string, 0, rv.NumField())
|
|
239
|
+
for i := 0; i < rv.NumField(); i++ {
|
|
240
|
+
f := t.Field(i)
|
|
241
|
+
if !f.IsExported() {
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
c, ok := canonicalValue(rv.Field(i), depth-1)
|
|
245
|
+
if !ok {
|
|
246
|
+
return "", false
|
|
247
|
+
}
|
|
248
|
+
items = append(items, f.Name+"="+c)
|
|
249
|
+
}
|
|
250
|
+
return t.String() + "{" + strings.Join(items, ",") + "}", true
|
|
251
|
+
}
|
|
252
|
+
return "", false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
func digest(v any) string {
|
|
256
|
+
c, ok := canonical(v, digestDepth)
|
|
257
|
+
if !ok {
|
|
258
|
+
return ""
|
|
259
|
+
}
|
|
260
|
+
h := sha256.Sum256([]byte(c))
|
|
261
|
+
return hex.EncodeToString(h[:])[:16]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
func bucket(rv reflect.Value) (string, bool) {
|
|
265
|
+
if !rv.IsValid() {
|
|
266
|
+
return "none", true
|
|
267
|
+
}
|
|
268
|
+
switch rv.Kind() {
|
|
269
|
+
case reflect.Bool:
|
|
270
|
+
if rv.Bool() {
|
|
271
|
+
return "bool:true", true
|
|
272
|
+
}
|
|
273
|
+
return "bool:false", true
|
|
274
|
+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
275
|
+
v := rv.Int()
|
|
276
|
+
if v == 0 {
|
|
277
|
+
return "num:zero", true
|
|
278
|
+
} else if v > 0 {
|
|
279
|
+
return "num:pos", true
|
|
280
|
+
}
|
|
281
|
+
return "num:neg", true
|
|
282
|
+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
283
|
+
if rv.Uint() == 0 {
|
|
284
|
+
return "num:zero", true
|
|
285
|
+
}
|
|
286
|
+
return "num:pos", true
|
|
287
|
+
case reflect.Float32, reflect.Float64:
|
|
288
|
+
v := rv.Float()
|
|
289
|
+
if v == 0 {
|
|
290
|
+
return "num:zero", true
|
|
291
|
+
} else if v > 0 {
|
|
292
|
+
return "num:pos", true
|
|
293
|
+
}
|
|
294
|
+
return "num:neg", true
|
|
295
|
+
case reflect.String:
|
|
296
|
+
if rv.Len() == 0 {
|
|
297
|
+
return "str:empty", true
|
|
298
|
+
}
|
|
299
|
+
return "str:nonempty", true
|
|
300
|
+
case reflect.Slice, reflect.Map, reflect.Array:
|
|
301
|
+
n := rv.Len()
|
|
302
|
+
if n == 0 {
|
|
303
|
+
return "coll:empty", true
|
|
304
|
+
} else if n == 1 {
|
|
305
|
+
return "coll:one", true
|
|
306
|
+
}
|
|
307
|
+
return "coll:many", true
|
|
308
|
+
case reflect.Ptr, reflect.Interface:
|
|
309
|
+
if rv.IsNil() {
|
|
310
|
+
return "none", true
|
|
311
|
+
}
|
|
312
|
+
return "obj:" + rv.Elem().Type().String(), true
|
|
313
|
+
case reflect.Struct:
|
|
314
|
+
return "obj:" + rv.Type().String(), true
|
|
315
|
+
case reflect.Func, reflect.Chan:
|
|
316
|
+
return "", false
|
|
317
|
+
}
|
|
318
|
+
return "", false
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
func stateFacts(recv any) [][3]string {
|
|
322
|
+
facts := [][3]string{}
|
|
323
|
+
if recv == nil {
|
|
324
|
+
return facts
|
|
325
|
+
}
|
|
326
|
+
rv := reflect.ValueOf(recv)
|
|
327
|
+
for rv.Kind() == reflect.Ptr {
|
|
328
|
+
if rv.IsNil() {
|
|
329
|
+
return facts
|
|
330
|
+
}
|
|
331
|
+
rv = rv.Elem()
|
|
332
|
+
}
|
|
333
|
+
facts = append(facts, [3]string{"self:type", "obj:" + rv.Type().String(), digest(rv.Type().String())})
|
|
334
|
+
if rv.Kind() != reflect.Struct {
|
|
335
|
+
return facts
|
|
336
|
+
}
|
|
337
|
+
t := rv.Type()
|
|
338
|
+
for i := 0; i < rv.NumField() && i < digestWidth; i++ {
|
|
339
|
+
f := t.Field(i)
|
|
340
|
+
b, ok := bucket(rv.Field(i))
|
|
341
|
+
if !ok {
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
d := ""
|
|
345
|
+
if strings.HasPrefix(b, "obj:") {
|
|
346
|
+
d = digest(b[4:])
|
|
347
|
+
}
|
|
348
|
+
facts = append(facts, [3]string{"self." + f.Name, b, d})
|
|
349
|
+
}
|
|
350
|
+
return facts
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
func argShapes(names []string, values []any) [][3]string {
|
|
354
|
+
out := [][3]string{}
|
|
355
|
+
for i, v := range values {
|
|
356
|
+
if i >= maxArgs {
|
|
357
|
+
break
|
|
358
|
+
}
|
|
359
|
+
name := fmt.Sprintf("arg%d", i)
|
|
360
|
+
if i < len(names) && names[i] != "" {
|
|
361
|
+
name = names[i]
|
|
362
|
+
}
|
|
363
|
+
out = append(out, [3]string{name, shape(v), digest(v)})
|
|
364
|
+
}
|
|
365
|
+
return out
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ----------------------------------------------------------------------------- executions
|
|
369
|
+
|
|
370
|
+
func current() *execution {
|
|
371
|
+
if len(active) == 0 {
|
|
372
|
+
return nil
|
|
373
|
+
}
|
|
374
|
+
return active[len(active)-1]
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
func (e *execution) intern(id, origin, file string, line int) {
|
|
378
|
+
if _, ok := e.symbols[id]; ok {
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
var loc *location
|
|
382
|
+
if file != "" {
|
|
383
|
+
loc = &location{Path: file, Line: line}
|
|
384
|
+
}
|
|
385
|
+
e.symbols[id] = symbol{ID: id, Origin: origin, Location: loc}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
func (e *execution) originOf(nodeID int) string {
|
|
389
|
+
n := e.nodes[nodeID]
|
|
390
|
+
if n.Type != "call" {
|
|
391
|
+
return "unknown"
|
|
392
|
+
}
|
|
393
|
+
if s, ok := e.symbols[n.Symbol]; ok {
|
|
394
|
+
return s.Origin
|
|
395
|
+
}
|
|
396
|
+
return "unknown"
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
func (e *execution) thread(gid int64) int {
|
|
400
|
+
if t, ok := e.goroutine[gid]; ok {
|
|
401
|
+
return t
|
|
402
|
+
}
|
|
403
|
+
t := len(e.goroutine)
|
|
404
|
+
e.goroutine[gid] = t
|
|
405
|
+
return t
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Begin marks the start of a stimulus (a test or subtest).
|
|
409
|
+
func Begin(ref string) {
|
|
410
|
+
mu.Lock()
|
|
411
|
+
defer mu.Unlock()
|
|
412
|
+
e := &execution{ref: ref, symbols: map[string]symbol{}, stacks: map[int64][]int{}, goroutine: map[int64]int{}}
|
|
413
|
+
sym := "go:" + ref
|
|
414
|
+
e.intern(sym, "test", "", 0)
|
|
415
|
+
e.nodes = append(e.nodes, &node{Type: "call", ID: 0, Parent: nil, Symbol: sym, Args: [][3]string{}, Outcome: "unknown"})
|
|
416
|
+
e.thread(goid())
|
|
417
|
+
active = append(active, e)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// End closes the innermost stimulus and writes its Execution.
|
|
421
|
+
func End(failed bool) {
|
|
422
|
+
mu.Lock()
|
|
423
|
+
defer mu.Unlock()
|
|
424
|
+
e := current()
|
|
425
|
+
if e == nil {
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
active = active[:len(active)-1]
|
|
429
|
+
if outDir == "" {
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
outcome := "passed"
|
|
433
|
+
if failed {
|
|
434
|
+
outcome = "failed"
|
|
435
|
+
}
|
|
436
|
+
syms := make([]symbol, 0, len(e.symbols))
|
|
437
|
+
for _, s := range e.symbols {
|
|
438
|
+
syms = append(syms, s)
|
|
439
|
+
}
|
|
440
|
+
sort.Slice(syms, func(i, j int) bool { return syms[i].ID < syms[j].ID })
|
|
441
|
+
revision := rev
|
|
442
|
+
var revPtr *string
|
|
443
|
+
if revision != "" {
|
|
444
|
+
revPtr = &revision
|
|
445
|
+
}
|
|
446
|
+
idRev := revision
|
|
447
|
+
if idRev == "" {
|
|
448
|
+
idRev = "unknown"
|
|
449
|
+
}
|
|
450
|
+
doc := map[string]any{
|
|
451
|
+
"id": e.ref + "@" + idRev,
|
|
452
|
+
"stimulus": stim,
|
|
453
|
+
"stimulus_ref": e.ref,
|
|
454
|
+
"outcome": outcome,
|
|
455
|
+
"revision": revPtr,
|
|
456
|
+
"collectors": []map[string]string{
|
|
457
|
+
{"name": "go-source-instrumentation", "plane": "symbol", "fidelity": "complete"},
|
|
458
|
+
},
|
|
459
|
+
"symbols": syms,
|
|
460
|
+
"nodes": e.nodes,
|
|
461
|
+
"branches": append([]branchObs{}, e.branches...),
|
|
462
|
+
"diagnostics": [][2]string{{"goroutines", strconv.Itoa(len(e.goroutine))}},
|
|
463
|
+
}
|
|
464
|
+
_ = os.MkdirAll(outDir, 0o755)
|
|
465
|
+
h := sha256.Sum256([]byte(e.ref))
|
|
466
|
+
name := strings.Map(func(r rune) rune {
|
|
467
|
+
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '.' || r == '-' {
|
|
468
|
+
return r
|
|
469
|
+
}
|
|
470
|
+
return '_'
|
|
471
|
+
}, e.ref)
|
|
472
|
+
if len(name) > 100 {
|
|
473
|
+
name = name[:100]
|
|
474
|
+
}
|
|
475
|
+
data, _ := json.MarshalIndent(doc, "", " ")
|
|
476
|
+
_ = os.WriteFile(filepath.Join(outDir, name+"-"+hex.EncodeToString(h[:])[:12]+".json"), append(data, '\n'), 0o644)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ----------------------------------------------------------------------------- calls
|
|
480
|
+
|
|
481
|
+
// Enter records a call. Test doubles (test-origin functions entered from repo code) get a
|
|
482
|
+
// substitution wrapper carrying the static claim, so they are non-leaf stand-ins.
|
|
483
|
+
func Enter(m Meta, values ...any) *Ctx {
|
|
484
|
+
mu.Lock()
|
|
485
|
+
defer mu.Unlock()
|
|
486
|
+
e := current()
|
|
487
|
+
if e == nil {
|
|
488
|
+
return nil
|
|
489
|
+
}
|
|
490
|
+
gid := goid()
|
|
491
|
+
e.intern(m.Sym, m.Origin, m.File, m.Line)
|
|
492
|
+
args := argShapes(m.Params, values)
|
|
493
|
+
stack := e.stacks[gid]
|
|
494
|
+
parent := 0
|
|
495
|
+
if len(stack) > 0 {
|
|
496
|
+
parent = stack[len(stack)-1]
|
|
497
|
+
}
|
|
498
|
+
sub := -1
|
|
499
|
+
if m.Origin == "test" && e.originOf(parent) == "repo" {
|
|
500
|
+
mech := "fake"
|
|
501
|
+
if strings.Contains(m.Sym, "<anon>") || !strings.Contains(strings.TrimPrefix(m.Sym, "go:"), ".") {
|
|
502
|
+
mech = "stub"
|
|
503
|
+
}
|
|
504
|
+
var claimed *string
|
|
505
|
+
relation := "none"
|
|
506
|
+
if m.Claim != "" {
|
|
507
|
+
c := m.Claim
|
|
508
|
+
claimed = &c
|
|
509
|
+
relation = m.Relation
|
|
510
|
+
e.intern(c, "repo", "", 0)
|
|
511
|
+
}
|
|
512
|
+
p := parent
|
|
513
|
+
n := &node{Type: "substitution", ID: len(e.nodes), Parent: &p, Collector: 0, Mechanism: mech,
|
|
514
|
+
Substitute: m.Sym, Claimed: claimed, Relation: relation, Path: []string{}, Args: args, Outcome: "unknown",
|
|
515
|
+
State: [][3]string{}}
|
|
516
|
+
e.nodes = append(e.nodes, n)
|
|
517
|
+
sub = n.ID
|
|
518
|
+
parent = sub
|
|
519
|
+
}
|
|
520
|
+
p := parent
|
|
521
|
+
n := &node{Type: "call", ID: len(e.nodes), Parent: &p, Symbol: m.Sym, Collector: 0, Args: args,
|
|
522
|
+
Thread: e.thread(gid), Outcome: "unknown", State: stateFacts(m.Recv)}
|
|
523
|
+
e.nodes = append(e.nodes, n)
|
|
524
|
+
e.stacks[gid] = append(stack, n.ID)
|
|
525
|
+
return &Ctx{id: n.ID, sub: sub, gid: gid, exec: e, recv: m.Recv}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
func settle(e *execution, id int, outcome, result string) {
|
|
529
|
+
if id < 0 || id >= len(e.nodes) {
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
n := e.nodes[id]
|
|
533
|
+
if n.Outcome != "unknown" {
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
n.Outcome = outcome
|
|
537
|
+
n.Result = result
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
func (e *execution) pop(gid int64, id int) {
|
|
541
|
+
stack := e.stacks[gid]
|
|
542
|
+
for i := len(stack) - 1; i >= 0; i-- {
|
|
543
|
+
if stack[i] == id {
|
|
544
|
+
e.stacks[gid] = stack[:i]
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Exit records a normal return. A trailing non-nil error result is the "raised" outcome
|
|
551
|
+
// in Go's convention; other results are digested.
|
|
552
|
+
func Exit(c *Ctx, results ...any) {
|
|
553
|
+
if c == nil {
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
mu.Lock()
|
|
557
|
+
defer mu.Unlock()
|
|
558
|
+
outcome := "returned"
|
|
559
|
+
vals := results
|
|
560
|
+
if n := len(results); n > 0 {
|
|
561
|
+
if err, ok := results[n-1].(error); ok && err != nil {
|
|
562
|
+
outcome = "returned-error:go:" + reflect.TypeOf(err).String()
|
|
563
|
+
c.exec.intern("go:"+reflect.TypeOf(err).String(), "external", "", 0)
|
|
564
|
+
vals = results[:n-1]
|
|
565
|
+
} else if results[n-1] == nil {
|
|
566
|
+
if _, isErrSlot := results[n-1].(error); !isErrSlot {
|
|
567
|
+
vals = results
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
res := ""
|
|
572
|
+
if outcome == "returned" {
|
|
573
|
+
if len(vals) == 1 {
|
|
574
|
+
res = digest(vals[0])
|
|
575
|
+
} else if len(vals) > 1 {
|
|
576
|
+
res = digest(vals)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
settle(c.exec, c.id, outcome, res)
|
|
580
|
+
if c.sub >= 0 {
|
|
581
|
+
settle(c.exec, c.sub, outcome, res)
|
|
582
|
+
}
|
|
583
|
+
c.exec.nodes[c.id].StateAfter = stateFacts(c.recv)
|
|
584
|
+
c.exec.pop(c.gid, c.id)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Branch records the outcome of the `if` condition at `site` (an id computed by the
|
|
588
|
+
// instrumenter from the condition's span in the ORIGINAL source) and returns it unchanged.
|
|
589
|
+
func Branch(site string, cond bool) bool {
|
|
590
|
+
mu.Lock()
|
|
591
|
+
defer mu.Unlock()
|
|
592
|
+
e := current()
|
|
593
|
+
if e == nil {
|
|
594
|
+
return cond
|
|
595
|
+
}
|
|
596
|
+
stack := e.stacks[goid()]
|
|
597
|
+
node := 0
|
|
598
|
+
if len(stack) > 0 {
|
|
599
|
+
node = stack[len(stack)-1]
|
|
600
|
+
}
|
|
601
|
+
e.branches = append(e.branches, branchObs{Site: site, Outcome: cond, Node: node, Seq: len(e.nodes)})
|
|
602
|
+
return cond
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Panic records a panic unwinding through the call.
|
|
606
|
+
func Panic(c *Ctx, r any) {
|
|
607
|
+
if c == nil {
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
mu.Lock()
|
|
611
|
+
defer mu.Unlock()
|
|
612
|
+
t := "go:panic"
|
|
613
|
+
if r != nil {
|
|
614
|
+
t = "go:" + reflect.TypeOf(r).String()
|
|
615
|
+
}
|
|
616
|
+
c.exec.intern(t, "external", "", 0)
|
|
617
|
+
settle(c.exec, c.id, "panic:"+t, "")
|
|
618
|
+
if c.sub >= 0 {
|
|
619
|
+
settle(c.exec, c.sub, "panic:"+t, "")
|
|
620
|
+
}
|
|
621
|
+
c.exec.nodes[c.id].StateAfter = stateFacts(c.recv)
|
|
622
|
+
c.exec.pop(c.gid, c.id)
|
|
623
|
+
}
|