github.com/rajeev159/opa@v0.45.0/topdown/trace_test.go (about) 1 // Copyright 2016 The OPA Authors. All rights reserved. 2 // Use of this source code is governed by an Apache2 3 // license that can be found in the LICENSE file. 4 5 package topdown 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "reflect" 12 "strings" 13 "testing" 14 15 "github.com/open-policy-agent/opa/ast" 16 "github.com/open-policy-agent/opa/storage" 17 "github.com/open-policy-agent/opa/storage/inmem" 18 "github.com/open-policy-agent/opa/util" 19 ) 20 21 func TestEventEqual(t *testing.T) { 22 23 a := ast.NewValueMap() 24 a.Put(ast.String("foo"), ast.Number("1")) 25 b := ast.NewValueMap() 26 b.Put(ast.String("foo"), ast.Number("2")) 27 28 tests := []struct { 29 a *Event 30 b *Event 31 equal bool 32 }{ 33 {&Event{}, &Event{}, true}, 34 {&Event{Op: EvalOp}, &Event{Op: EnterOp}, false}, 35 {&Event{QueryID: 1}, &Event{QueryID: 2}, false}, 36 {&Event{ParentID: 1}, &Event{ParentID: 2}, false}, 37 {&Event{Node: ast.MustParseBody("true")}, &Event{Node: ast.MustParseBody("false")}, false}, 38 {&Event{Node: ast.MustParseBody("true")[0]}, &Event{Node: ast.MustParseBody("false")[0]}, false}, 39 {&Event{Node: ast.MustParseRule(`p = true { true }`)}, &Event{Node: ast.MustParseRule(`p = true { false }`)}, false}, 40 } 41 42 for _, tc := range tests { 43 if tc.a.Equal(tc.b) != tc.equal { 44 var s string 45 if tc.equal { 46 s = "==" 47 } else { 48 s = "!=" 49 } 50 t.Errorf("Expected %v %v %v", tc.a, s, tc.b) 51 } 52 } 53 54 } 55 56 func TestPrettyTrace(t *testing.T) { 57 module := `package test 58 59 p { q[x]; plus(x, 1, n) } 60 q[x] { x = data.a[_] }` 61 62 ctx := context.Background() 63 compiler := compileModules([]string{module}) 64 data := loadSmallTestData() 65 store := inmem.NewFromObject(data) 66 txn := storage.NewTransactionOrDie(ctx, store) 67 defer store.Abort(ctx, txn) 68 69 tracer := NewBufferTracer() 70 query := NewQuery(ast.MustParseBody("data.test.p = _")). 71 WithCompiler(compiler). 72 WithStore(store). 73 WithTransaction(txn). 74 WithTracer(tracer) 75 76 _, err := query.Run(ctx) 77 if err != nil { 78 panic(err) 79 } 80 81 expected := `Enter data.test.p = _ 82 | Eval data.test.p = _ 83 | Index data.test.p (matched 1 rule, early exit) 84 | Enter data.test.p 85 | | Eval data.test.q[x] 86 | | Index data.test.q (matched 1 rule) 87 | | Enter data.test.q 88 | | | Eval x = data.a[_] 89 | | | Exit data.test.q 90 | | Redo data.test.q 91 | | | Redo x = data.a[_] 92 | | | Exit data.test.q 93 | | Redo data.test.q 94 | | | Redo x = data.a[_] 95 | | | Exit data.test.q 96 | | Redo data.test.q 97 | | | Redo x = data.a[_] 98 | | | Exit data.test.q 99 | | Redo data.test.q 100 | | | Redo x = data.a[_] 101 | | Eval plus(x, 1, n) 102 | | Exit data.test.p early 103 | Exit data.test.p = _ 104 Redo data.test.p = _ 105 | Redo data.test.p = _ 106 | Redo data.test.p 107 | | Redo plus(x, 1, n) 108 | | Redo data.test.q[x] 109 ` 110 111 var buf bytes.Buffer 112 PrettyTrace(&buf, *tracer) 113 compareBuffers(t, expected, buf.String()) 114 } 115 116 func TestPrettyTraceWithLocation(t *testing.T) { 117 module := `package test 118 119 p { q[x]; plus(x, 1, n) } 120 q[x] { x = data.a[_] }` 121 122 ctx := context.Background() 123 compiler := compileModules([]string{module}) 124 data := loadSmallTestData() 125 store := inmem.NewFromObject(data) 126 txn := storage.NewTransactionOrDie(ctx, store) 127 defer store.Abort(ctx, txn) 128 129 tracer := NewBufferTracer() 130 query := NewQuery(ast.MustParseBody("data.test.p = _")). 131 WithCompiler(compiler). 132 WithStore(store). 133 WithTransaction(txn). 134 WithTracer(tracer) 135 136 _, err := query.Run(ctx) 137 if err != nil { 138 panic(err) 139 } 140 141 expected := `query:1 Enter data.test.p = _ 142 query:1 | Eval data.test.p = _ 143 query:1 | Index data.test.p (matched 1 rule, early exit) 144 query:3 | Enter data.test.p 145 query:3 | | Eval data.test.q[x] 146 query:3 | | Index data.test.q (matched 1 rule) 147 query:4 | | Enter data.test.q 148 query:4 | | | Eval x = data.a[_] 149 query:4 | | | Exit data.test.q 150 query:4 | | Redo data.test.q 151 query:4 | | | Redo x = data.a[_] 152 query:4 | | | Exit data.test.q 153 query:4 | | Redo data.test.q 154 query:4 | | | Redo x = data.a[_] 155 query:4 | | | Exit data.test.q 156 query:4 | | Redo data.test.q 157 query:4 | | | Redo x = data.a[_] 158 query:4 | | | Exit data.test.q 159 query:4 | | Redo data.test.q 160 query:4 | | | Redo x = data.a[_] 161 query:3 | | Eval plus(x, 1, n) 162 query:3 | | Exit data.test.p early 163 query:1 | Exit data.test.p = _ 164 query:1 Redo data.test.p = _ 165 query:1 | Redo data.test.p = _ 166 query:3 | Redo data.test.p 167 query:3 | | Redo plus(x, 1, n) 168 query:3 | | Redo data.test.q[x] 169 ` 170 171 var buf bytes.Buffer 172 PrettyTraceWithLocation(&buf, *tracer) 173 compareBuffers(t, expected, buf.String()) 174 } 175 176 func TestPrettyTraceWithLocationTruncatedPaths(t *testing.T) { 177 ctx := context.Background() 178 179 compiler := ast.MustCompileModules(map[string]string{ 180 "authz_bundle/com/foo/bar/baz/qux/acme/corp/internal/authz/policies/abac/v1/beta/policy.rego": `package test 181 182 import data.utils.q 183 184 p = true { q[x]; plus(x, 1, n) } 185 `, 186 "authz_bundle/com/foo/bar/baz/qux/acme/corp/internal/authz/policies/utils/utils.rego": `package utils 187 188 q[x] { x = data.a[_] } 189 `, 190 }) 191 data := loadSmallTestData() 192 store := inmem.NewFromObject(data) 193 txn := storage.NewTransactionOrDie(ctx, store) 194 defer store.Abort(ctx, txn) 195 196 tracer := NewBufferTracer() 197 query := NewQuery(ast.MustParseBody("data.test.p = _")). 198 WithCompiler(compiler). 199 WithStore(store). 200 WithTransaction(txn). 201 WithTracer(tracer) 202 203 _, err := query.Run(ctx) 204 if err != nil { 205 panic(err) 206 } 207 208 expected := `query:1 Enter data.test.p = _ 209 query:1 | Eval data.test.p = _ 210 query:1 | Index data.test.p (matched 1 rule, early exit) 211 authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:5 | Enter data.test.p 212 authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:5 | | Eval data.utils.q[x] 213 authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:5 | | Index data.utils.q (matched 1 rule) 214 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | Enter data.utils.q 215 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | | Eval x = data.a[_] 216 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | | Exit data.utils.q 217 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | Redo data.utils.q 218 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | | Redo x = data.a[_] 219 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | | Exit data.utils.q 220 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | Redo data.utils.q 221 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | | Redo x = data.a[_] 222 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | | Exit data.utils.q 223 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | Redo data.utils.q 224 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | | Redo x = data.a[_] 225 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | | Exit data.utils.q 226 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | Redo data.utils.q 227 authz_bundle/...ternal/authz/policies/utils/utils.rego:3 | | | Redo x = data.a[_] 228 authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:5 | | Eval plus(x, 1, n) 229 authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:5 | | Exit data.test.p early 230 query:1 | Exit data.test.p = _ 231 query:1 Redo data.test.p = _ 232 query:1 | Redo data.test.p = _ 233 authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:5 | Redo data.test.p 234 authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:5 | | Redo plus(x, 1, n) 235 authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:5 | | Redo data.utils.q[x] 236 ` 237 238 var buf bytes.Buffer 239 PrettyTraceWithLocation(&buf, *tracer) 240 compareBuffers(t, expected, buf.String()) 241 } 242 243 func TestPrettyTracePartialWithLocationTruncatedPaths(t *testing.T) { 244 ctx := context.Background() 245 246 compiler := ast.MustCompileModules(map[string]string{ 247 "authz_bundle/com/foo/bar/baz/qux/acme/corp/internal/authz/policies/rbac/v1/beta/policy.rego": ` 248 package example_rbac 249 250 default allow = false 251 252 allow { 253 data.utils.user_has_role[role_name] 254 255 data.utils.role_has_permission[role_name] 256 } 257 258 `, 259 "authz_bundle/com/foo/bar/baz/qux/acme/corp/internal/authz/policies/utils/user.rego": ` 260 package utils 261 262 user_has_role[role_name] { 263 role_binding = data.bindings[_] 264 role_binding.role = role_name 265 role_binding.user = input.subject.user 266 } 267 268 role_has_permission[role_name] { 269 role = data.roles[_] 270 role.name = role_name 271 role.operation = input.action.operation 272 role.resource = input.action.resource 273 } 274 `, 275 }) 276 277 var data map[string]interface{} 278 err := util.UnmarshalJSON([]byte(`{ 279 "roles": [ 280 { 281 "operation": "read", 282 "resource": "widgets", 283 "name": "widget-reader" 284 }, 285 { 286 "operation": "write", 287 "resource": "widgets", 288 "name": "widget-writer" 289 } 290 ], 291 "bindings": [ 292 { 293 "user": "inspector-alice", 294 "role": "widget-reader" 295 }, 296 { 297 "user": "maker-bob", 298 "role": "widget-writer" 299 } 300 ] 301 }`), &data) 302 if err != nil { 303 t.Fatalf("Unexpected error: %s", err) 304 } 305 306 store := inmem.NewFromObject(data) 307 txn := storage.NewTransactionOrDie(ctx, store) 308 defer store.Abort(ctx, txn) 309 310 tracer := NewBufferTracer() 311 query := NewQuery(ast.MustParseBody("data.example_rbac.allow")). 312 WithCompiler(compiler). 313 WithStore(store). 314 WithTransaction(txn). 315 WithUnknowns([]*ast.Term{ast.MustParseTerm("input")}). 316 WithTracer(tracer) 317 318 _, _, err = query.PartialRun(ctx) 319 if err != nil { 320 panic(err) 321 } 322 323 expected := `query:1 Enter data.example_rbac.allow 324 query:1 | Eval data.example_rbac.allow 325 query:1 | Index data.example_rbac.allow (matched 1 rule, early exit) 326 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6 | Enter data.example_rbac.allow 327 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:7 | | Eval data.utils.user_has_role[role_name] 328 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:7 | | Index data.utils.user_has_role (matched 1 rule) 329 authz_bundle/...ternal/authz/policies/utils/user.rego:4 | | Enter data.utils.user_has_role 330 authz_bundle/...ternal/authz/policies/utils/user.rego:5 | | | Eval role_binding = data.bindings[_] 331 authz_bundle/...ternal/authz/policies/utils/user.rego:6 | | | Eval role_binding.role = role_name 332 authz_bundle/...ternal/authz/policies/utils/user.rego:7 | | | Eval role_binding.user = input.subject.user 333 authz_bundle/...ternal/authz/policies/utils/user.rego:7 | | | Save "inspector-alice" = input.subject.user 334 authz_bundle/...ternal/authz/policies/utils/user.rego:4 | | | Exit data.utils.user_has_role 335 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9 | | Eval data.utils.role_has_permission[role_name] 336 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9 | | Index data.utils.role_has_permission (matched 1 rule) 337 authz_bundle/...ternal/authz/policies/utils/user.rego:10 | | Enter data.utils.role_has_permission 338 authz_bundle/...ternal/authz/policies/utils/user.rego:11 | | | Eval role = data.roles[_] 339 authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Eval role.name = role_name 340 authz_bundle/...ternal/authz/policies/utils/user.rego:13 | | | Eval role.operation = input.action.operation 341 authz_bundle/...ternal/authz/policies/utils/user.rego:13 | | | Save "read" = input.action.operation 342 authz_bundle/...ternal/authz/policies/utils/user.rego:14 | | | Eval role.resource = input.action.resource 343 authz_bundle/...ternal/authz/policies/utils/user.rego:14 | | | Save "widgets" = input.action.resource 344 authz_bundle/...ternal/authz/policies/utils/user.rego:10 | | | Exit data.utils.role_has_permission 345 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6 | | Exit data.example_rbac.allow 346 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6 | Redo data.example_rbac.allow 347 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9 | | Redo data.utils.role_has_permission[role_name] 348 authz_bundle/...ternal/authz/policies/utils/user.rego:10 | | Redo data.utils.role_has_permission 349 authz_bundle/...ternal/authz/policies/utils/user.rego:14 | | | Redo role.resource = input.action.resource 350 authz_bundle/...ternal/authz/policies/utils/user.rego:13 | | | Redo role.operation = input.action.operation 351 authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Redo role.name = role_name 352 authz_bundle/...ternal/authz/policies/utils/user.rego:11 | | | Redo role = data.roles[_] 353 authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Eval role.name = role_name 354 authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Fail role.name = role_name 355 authz_bundle/...ternal/authz/policies/utils/user.rego:11 | | | Redo role = data.roles[_] 356 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:7 | | Redo data.utils.user_has_role[role_name] 357 authz_bundle/...ternal/authz/policies/utils/user.rego:4 | | Redo data.utils.user_has_role 358 authz_bundle/...ternal/authz/policies/utils/user.rego:7 | | | Redo role_binding.user = input.subject.user 359 authz_bundle/...ternal/authz/policies/utils/user.rego:6 | | | Redo role_binding.role = role_name 360 authz_bundle/...ternal/authz/policies/utils/user.rego:5 | | | Redo role_binding = data.bindings[_] 361 authz_bundle/...ternal/authz/policies/utils/user.rego:6 | | | Eval role_binding.role = role_name 362 authz_bundle/...ternal/authz/policies/utils/user.rego:7 | | | Eval role_binding.user = input.subject.user 363 authz_bundle/...ternal/authz/policies/utils/user.rego:7 | | | Save "maker-bob" = input.subject.user 364 authz_bundle/...ternal/authz/policies/utils/user.rego:4 | | | Exit data.utils.user_has_role 365 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9 | | Eval data.utils.role_has_permission[role_name] 366 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9 | | Index data.utils.role_has_permission (matched 1 rule) 367 authz_bundle/...ternal/authz/policies/utils/user.rego:10 | | Enter data.utils.role_has_permission 368 authz_bundle/...ternal/authz/policies/utils/user.rego:11 | | | Eval role = data.roles[_] 369 authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Eval role.name = role_name 370 authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Fail role.name = role_name 371 authz_bundle/...ternal/authz/policies/utils/user.rego:11 | | | Redo role = data.roles[_] 372 authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Eval role.name = role_name 373 authz_bundle/...ternal/authz/policies/utils/user.rego:13 | | | Eval role.operation = input.action.operation 374 authz_bundle/...ternal/authz/policies/utils/user.rego:13 | | | Save "write" = input.action.operation 375 authz_bundle/...ternal/authz/policies/utils/user.rego:14 | | | Eval role.resource = input.action.resource 376 authz_bundle/...ternal/authz/policies/utils/user.rego:14 | | | Save "widgets" = input.action.resource 377 authz_bundle/...ternal/authz/policies/utils/user.rego:10 | | | Exit data.utils.role_has_permission 378 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6 | | Exit data.example_rbac.allow 379 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6 | Redo data.example_rbac.allow 380 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9 | | Redo data.utils.role_has_permission[role_name] 381 authz_bundle/...ternal/authz/policies/utils/user.rego:10 | | Redo data.utils.role_has_permission 382 authz_bundle/...ternal/authz/policies/utils/user.rego:14 | | | Redo role.resource = input.action.resource 383 authz_bundle/...ternal/authz/policies/utils/user.rego:13 | | | Redo role.operation = input.action.operation 384 authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Redo role.name = role_name 385 authz_bundle/...ternal/authz/policies/utils/user.rego:11 | | | Redo role = data.roles[_] 386 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:7 | | Redo data.utils.user_has_role[role_name] 387 authz_bundle/...ternal/authz/policies/utils/user.rego:4 | | Redo data.utils.user_has_role 388 authz_bundle/...ternal/authz/policies/utils/user.rego:7 | | | Redo role_binding.user = input.subject.user 389 authz_bundle/...ternal/authz/policies/utils/user.rego:6 | | | Redo role_binding.role = role_name 390 authz_bundle/...ternal/authz/policies/utils/user.rego:5 | | | Redo role_binding = data.bindings[_] 391 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4 | Enter data.example_rbac.allow 392 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4 | | Eval true 393 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4 | | Exit data.example_rbac.allow 394 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4 | Redo data.example_rbac.allow 395 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4 | | Redo true 396 query:1 | Save data.partial.example_rbac.allow = _ 397 query:1 | Save _ 398 query:1 | Exit data.example_rbac.allow 399 query:1 Redo data.example_rbac.allow 400 query:1 | Fail data.example_rbac.allow 401 ` 402 403 var buf bytes.Buffer 404 PrettyTraceWithLocation(&buf, *tracer) 405 compareBuffers(t, expected, buf.String()) 406 } 407 408 func TestTraceDuplicate(t *testing.T) { 409 // NOTE(sr): We're explicitly bypassing a caching optimization here: 410 // When the first query for a partial is `p[x]`, and `x` is not ground, 411 // we'll have the evaluation eval the full extent of the partial and 412 // cache that. Thus the second `p[1]` here will not trigger a duplicate 413 // event, because the query eval uses a different code path. 414 // Having `p[1]` queried first will side-step the caching optimization. 415 module := `package test 416 417 p[1] 418 p[2] 419 p[1] 420 ` 421 422 ctx := context.Background() 423 compiler := compileModules([]string{module}) 424 data := loadSmallTestData() 425 store := inmem.NewFromObject(data) 426 txn := storage.NewTransactionOrDie(ctx, store) 427 defer store.Abort(ctx, txn) 428 429 tracer := NewBufferTracer() 430 query := NewQuery(ast.MustParseBody("data.test.p[1]; data.test.p[x] = _")). 431 WithCompiler(compiler). 432 WithStore(store). 433 WithTransaction(txn). 434 WithTracer(tracer) 435 436 _, err := query.Run(ctx) 437 if err != nil { 438 panic(err) 439 } 440 441 n := 0 442 for _, event := range *tracer { 443 if event.Op == DuplicateOp { 444 n++ 445 } 446 } 447 448 if n != 1 { 449 t.Fatalf("Expected one duplicate event but got %v", n) 450 } 451 } 452 453 func TestTraceNote(t *testing.T) { 454 module := `package test 455 456 p { q[x]; plus(x, 1, n); trace(sprintf("n=%v", [n])) } 457 q[x] { x = data.a[_] }` 458 459 ctx := context.Background() 460 compiler := compileModules([]string{module}) 461 data := loadSmallTestData() 462 store := inmem.NewFromObject(data) 463 txn := storage.NewTransactionOrDie(ctx, store) 464 defer store.Abort(ctx, txn) 465 466 tracer := NewBufferTracer() 467 query := NewQuery(ast.MustParseBody("data.test.p = _")). 468 WithCompiler(compiler). 469 WithStore(store). 470 WithTransaction(txn). 471 WithTracer(tracer) 472 473 _, err := query.Run(ctx) 474 if err != nil { 475 panic(err) 476 } 477 478 expected := `Enter data.test.p = _ 479 | Eval data.test.p = _ 480 | Index data.test.p (matched 1 rule, early exit) 481 | Enter data.test.p 482 | | Eval data.test.q[x] 483 | | Index data.test.q (matched 1 rule) 484 | | Enter data.test.q 485 | | | Eval x = data.a[_] 486 | | | Exit data.test.q 487 | | Redo data.test.q 488 | | | Redo x = data.a[_] 489 | | | Exit data.test.q 490 | | Redo data.test.q 491 | | | Redo x = data.a[_] 492 | | | Exit data.test.q 493 | | Redo data.test.q 494 | | | Redo x = data.a[_] 495 | | | Exit data.test.q 496 | | Redo data.test.q 497 | | | Redo x = data.a[_] 498 | | Eval plus(x, 1, n) 499 | | Eval sprintf("n=%v", [n], __local0__) 500 | | Eval trace(__local0__) 501 | | Note "n=2" 502 | | Exit data.test.p early 503 | Exit data.test.p = _ 504 Redo data.test.p = _ 505 | Redo data.test.p = _ 506 | Redo data.test.p 507 | | Redo trace(__local0__) 508 | | Redo sprintf("n=%v", [n], __local0__) 509 | | Redo plus(x, 1, n) 510 | | Redo data.test.q[x] 511 ` 512 513 var buf bytes.Buffer 514 PrettyTrace(&buf, *tracer) 515 compareBuffers(t, expected, buf.String()) 516 } 517 518 func TestTraceNoteWithLocation(t *testing.T) { 519 module := `package test 520 521 p { q[x]; plus(x, 1, n); trace(sprintf("n=%v", [n])) } 522 q[x] { x = data.a[_] }` 523 524 ctx := context.Background() 525 compiler := compileModules([]string{module}) 526 data := loadSmallTestData() 527 store := inmem.NewFromObject(data) 528 txn := storage.NewTransactionOrDie(ctx, store) 529 defer store.Abort(ctx, txn) 530 531 tracer := NewBufferTracer() 532 query := NewQuery(ast.MustParseBody("data.test.p = _")). 533 WithCompiler(compiler). 534 WithStore(store). 535 WithTransaction(txn). 536 WithTracer(tracer) 537 538 _, err := query.Run(ctx) 539 if err != nil { 540 panic(err) 541 } 542 543 expected := `query:1 Enter data.test.p = _ 544 query:1 | Eval data.test.p = _ 545 query:1 | Index data.test.p (matched 1 rule, early exit) 546 query:3 | Enter data.test.p 547 query:3 | | Eval data.test.q[x] 548 query:3 | | Index data.test.q (matched 1 rule) 549 query:4 | | Enter data.test.q 550 query:4 | | | Eval x = data.a[_] 551 query:4 | | | Exit data.test.q 552 query:4 | | Redo data.test.q 553 query:4 | | | Redo x = data.a[_] 554 query:4 | | | Exit data.test.q 555 query:4 | | Redo data.test.q 556 query:4 | | | Redo x = data.a[_] 557 query:4 | | | Exit data.test.q 558 query:4 | | Redo data.test.q 559 query:4 | | | Redo x = data.a[_] 560 query:4 | | | Exit data.test.q 561 query:4 | | Redo data.test.q 562 query:4 | | | Redo x = data.a[_] 563 query:3 | | Eval plus(x, 1, n) 564 query:3 | | Eval sprintf("n=%v", [n], __local0__) 565 query:3 | | Eval trace(__local0__) 566 query:3 | | Note "n=2" 567 query:3 | | Exit data.test.p early 568 query:1 | Exit data.test.p = _ 569 query:1 Redo data.test.p = _ 570 query:1 | Redo data.test.p = _ 571 query:3 | Redo data.test.p 572 query:3 | | Redo trace(__local0__) 573 query:3 | | Redo sprintf("n=%v", [n], __local0__) 574 query:3 | | Redo plus(x, 1, n) 575 query:3 | | Redo data.test.q[x] 576 ` 577 578 var buf bytes.Buffer 579 PrettyTraceWithLocation(&buf, *tracer) 580 compareBuffers(t, expected, buf.String()) 581 } 582 583 func TestMultipleTracers(t *testing.T) { 584 585 ctx := context.Background() 586 587 buf1 := NewBufferTracer() 588 buf2 := NewBufferTracer() 589 q := NewQuery(ast.MustParseBody("a = 1")). 590 WithTracer(buf1). 591 WithTracer(buf2) 592 593 _, err := q.Run(ctx) 594 if err != nil { 595 t.Fatal(err) 596 } 597 598 if len(*buf1) != len(*buf2) { 599 t.Fatalf("Expected buffer lengths to be equal but got: %d and %d", len(*buf1), len(*buf2)) 600 } 601 602 for i := range *buf1 { 603 if !(*buf1)[i].Equal((*buf2)[i]) { 604 t.Fatalf("Expected all events to be equal but at index %d got %v and %v", i, (*buf1)[i], (*buf2)[i]) 605 } 606 } 607 608 } 609 610 func TestTraceRewrittenQueryVars(t *testing.T) { 611 module := `package test 612 613 y = [1, 2, 3]` 614 615 ctx := context.Background() 616 compiler := compileModules([]string{module}) 617 queryCompiler := compiler.QueryCompiler() 618 data := loadSmallTestData() 619 store := inmem.NewFromObject(data) 620 txn := storage.NewTransactionOrDie(ctx, store) 621 defer store.Abort(ctx, txn) 622 623 compiledQuery, err := queryCompiler.Compile(ast.MustParseBody("z := {a | a := data.y[_]}")) 624 if err != nil { 625 t.Fatalf("unexpected error: %s", err) 626 } 627 628 tracer := NewBufferTracer() 629 query := NewQuery(compiledQuery). 630 WithQueryCompiler(queryCompiler). 631 WithCompiler(compiler). 632 WithStore(store). 633 WithTransaction(txn). 634 WithTracer(tracer) 635 636 _, err = query.Run(ctx) 637 if err != nil { 638 panic(err) 639 } 640 641 foundQueryVar := false 642 643 for _, event := range *tracer { 644 if event.LocalMetadata != nil { 645 name, ok := event.LocalMetadata["__localq1__"] 646 if ok && name.Name == "z" { 647 foundQueryVar = true 648 break 649 } 650 } 651 } 652 653 if !foundQueryVar { 654 t.Error("Expected to find trace with rewritten var 'z' -> '__localq__") 655 } 656 657 // Rewrite the vars in the first event (which is a query) and verify that 658 // that vars have been mapped to user-provided names. 659 cpy := rewrite((*tracer)[0]) 660 node := cpy.Node.(ast.Body) 661 exp := ast.MustParseBody("z = {a | a = data.y[_]}") 662 663 if !node.Equal(exp) { 664 t.Errorf("Expected %v but got %v", exp, node) 665 } 666 } 667 668 func TestTraceRewrittenVars(t *testing.T) { 669 670 mustParse := func(s string) *ast.Expr { 671 return ast.MustParseBodyWithOpts(s, ast.ParserOptions{FutureKeywords: []string{"every"}})[0] 672 } 673 everyCheck := func(stmt string) func(*testing.T, *Event, *Event) { 674 return func(t *testing.T, _ *Event, output *Event) { 675 exp := mustParse(stmt) 676 if !exp.Equal(output.Node.(*ast.Expr)) { 677 t.Errorf("expected %v to equal %v", output, exp) 678 } 679 } 680 } 681 682 tests := []struct { 683 note string 684 evt *Event 685 exp func(*testing.T, *Event, *Event) 686 }{ 687 { 688 note: "issue 2022", 689 evt: &Event{ 690 Node: ast.NewExpr(ast.VarTerm("foo")), 691 LocalMetadata: map[ast.Var]VarMetadata{ 692 ast.Var("foo"): {Name: ast.Var("bar")}, 693 }, 694 }, 695 exp: func(t *testing.T, input *Event, output *Event) { 696 if input.Node == output.Node { 697 t.Fatal("expected node to have been copied") 698 } else if !output.Node.(*ast.Expr).Equal(ast.NewExpr(ast.VarTerm("bar"))) { 699 t.Fatal("expected copy to contain rewritten var") 700 } 701 }, 702 }, 703 { 704 note: "every: key/val rewritten", 705 evt: &Event{ 706 Node: mustParse(`every __local0__, __local1__ in __local2__ { __local1__ == __local0__ }`), 707 LocalMetadata: map[ast.Var]VarMetadata{ 708 ast.Var("__local0__"): {Name: ast.Var("k")}, 709 ast.Var("__local1__"): {Name: ast.Var("v")}, 710 }, 711 }, 712 exp: everyCheck(`every k, v in __local2__ { v == k }`), 713 }, 714 { 715 note: "every: key hidden if not rewritten", 716 evt: &Event{ 717 Node: mustParse(`every __local0__, __local1__ in __local2__ { __local1__ == 1 }`), 718 LocalMetadata: map[ast.Var]VarMetadata{ 719 ast.Var("__local1__"): {Name: ast.Var("v")}, 720 }, 721 }, 722 exp: everyCheck(`every v in __local2__ { v == 1 }`), 723 }, 724 { 725 note: "every: key hidden if rewritten to generated key", // NOTE(sr): this would happen for traceRedo 726 evt: &Event{ 727 Node: mustParse(`every __local0__, __local1__ in __local2__ { __local1__ == 1 }`), 728 LocalMetadata: map[ast.Var]VarMetadata{ 729 ast.Var("__local1__"): {Name: ast.Var("v")}, 730 ast.Var("__local0__"): {Name: ast.Var("__local0__")}, 731 }, 732 }, 733 exp: everyCheck(`every v in __local2__ { v == 1 }`), 734 }, 735 } 736 737 for _, tc := range tests { 738 t.Run(tc.note, func(t *testing.T) { 739 output := rewrite(tc.evt) 740 tc.exp(t, tc.evt, output) 741 }) 742 } 743 } 744 745 func TestTraceEveryEvaluation(t *testing.T) { 746 ctx := context.Background() 747 748 events := func(es ...string) []string { 749 return es 750 } 751 752 // NOTE(sr): String() on an *Event isn't stable, because iterating the underlying ast.ValueMap isn't. 753 // So we're stubbing out all captured events' value maps to be able to compare these as strings. 754 tests := []struct { 755 note string 756 query string 757 module string 758 exp []string // these need to be found, extra events captured are ignored 759 }{ 760 { 761 note: "empty domain", 762 query: "data.test.p = x", 763 module: `package test 764 p { every k, v in [] { k != v } }`, 765 exp: events( 766 `Enter every __local0__, __local1__ in __local2__ { neq(__local0__, __local1__) } {} (qid=2, pqid=1)`, 767 `Exit every __local0__, __local1__ in __local2__ { neq(__local0__, __local1__) } {} (qid=2, pqid=1)`, 768 ), 769 }, 770 { 771 note: "successful eval", 772 query: "data.test.p = x", 773 module: `package test 774 p { every k, v in [1] { k != v } }`, 775 exp: events( 776 `Enter every __local0__, __local1__ in __local2__ { neq(__local0__, __local1__) } {} (qid=2, pqid=1)`, 777 `Enter neq(__local0__, __local1__) {} (qid=3, pqid=2)`, 778 `Exit neq(__local0__, __local1__) {} (qid=3, pqid=2)`, 779 `Redo every __local0__, __local1__ in __local2__ { neq(__local0__, __local1__) } {} (qid=2, pqid=1)`, 780 `Exit every __local0__, __local1__ in __local2__ { neq(__local0__, __local1__) } {} (qid=2, pqid=1)`, 781 ), 782 }, 783 { 784 note: "failure in first body query", 785 query: "data.test.p = x", 786 module: `package test 787 p { every v in [1, 2] { 1 != v } }`, 788 exp: events( 789 `Enter every __local0__, __local1__ in __local2__ { neq(1, __local1__) } {} (qid=2, pqid=1)`, 790 `Enter neq(1, __local1__) {} (qid=3, pqid=2)`, 791 `Fail neq(1, __local1__) {} (qid=3, pqid=2)`, 792 `Fail every __local0__, __local1__ in __local2__ { neq(1, __local1__) } {} (qid=2, pqid=1)`, 793 `Redo every __local0__, __local1__ in __local2__ { neq(1, __local1__) } {} (qid=2, pqid=1)`, 794 ), 795 }, 796 { 797 note: "failure in last body query", 798 query: "data.test.p = x", 799 module: `package test 800 p { every v in [0, 1] { 1 != v } }`, 801 exp: events( 802 `Enter every __local0__, __local1__ in __local2__ { neq(1, __local1__) } {} (qid=2, pqid=1)`, 803 `Enter neq(1, __local1__) {} (qid=3, pqid=2)`, 804 `Exit neq(1, __local1__) {} (qid=3, pqid=2)`, 805 `Enter neq(1, __local1__) {} (qid=4, pqid=2)`, 806 `Fail neq(1, __local1__) {} (qid=4, pqid=2)`, 807 `Fail every __local0__, __local1__ in __local2__ { neq(1, __local1__) } {} (qid=2, pqid=1)`, 808 `Redo every __local0__, __local1__ in __local2__ { neq(1, __local1__) } {} (qid=2, pqid=1)`, 809 ), 810 }, 811 } 812 813 for _, tc := range tests { 814 815 opts := ast.CompileOpts{ParserOptions: ast.ParserOptions{FutureKeywords: []string{"every"}}} 816 compiler, err := ast.CompileModulesWithOpt(map[string]string{"test.rego": tc.module}, opts) 817 if err != nil { 818 t.Fatal(err) 819 } 820 queryCompiler := compiler.QueryCompiler() 821 822 compiledQuery, err := queryCompiler.Compile(ast.MustParseBody(tc.query)) 823 if err != nil { 824 t.Fatalf("unexpected error: %s", err) 825 } 826 827 buf := NewBufferTracer() 828 query := NewQuery(compiledQuery). 829 WithQueryCompiler(queryCompiler). 830 WithCompiler(compiler). 831 WithStore(inmem.New()). 832 WithQueryTracer(buf) 833 834 if _, err := query.Run(ctx); err != nil { 835 t.Fatalf("unexpected error: %s", err) 836 } 837 838 for _, exp := range tc.exp { 839 found := false 840 for _, act := range *buf { 841 act.Locals = nil 842 if act.String() == exp { 843 found = true 844 } 845 } 846 if !found { 847 t.Errorf("expected event %v, found none", exp) 848 } 849 } 850 if t.Failed() { 851 t.Log("captured events:") 852 for _, ev := range *buf { 853 t.Log(ev.String()) 854 } 855 } 856 } 857 } 858 859 func TestShortTraceFileNames(t *testing.T) { 860 longFilePath1 := "/really/long/file/path/longer/than/most/would/really/ever/be/policy.rego" 861 longFilePath1Similar := "/really/long/file/path/longer/than/most/policy.rego" 862 longFilePath2 := "GfjEjnMA6coNiPoMoRMVk7KeorGeRmjRkIYUsWtr564SQ7yDo4Yss2SoN8PMoe0TOfVaNFd1HQbC9NhK.rego" 863 longFilePath3 := "RqS50uWAOxqqHmzdKVM3OCVsZDb12FJikUYHhz9pNqMWx3wjeQBKY3UYXsJXzYGOzuYZbidag5SfKVdk.rego" 864 865 cases := []struct { 866 note string 867 trace []*Event 868 expectedNames map[string]string 869 expectedLongest int 870 }{ 871 { 872 note: "empty trace", 873 trace: nil, 874 expectedNames: map[string]string{}, 875 expectedLongest: 0, 876 }, 877 { 878 note: "no locations", 879 trace: []*Event{ 880 {Op: EnterOp, Node: ast.MustParseBody("true")}, 881 {Op: EvalOp, Node: ast.MustParseBody("true")}, 882 }, 883 expectedNames: map[string]string{}, 884 expectedLongest: 0, 885 }, 886 { 887 note: "no file names", 888 trace: []*Event{ 889 {Location: ast.NewLocation([]byte("foo1"), "", 1, 1)}, 890 {Location: ast.NewLocation([]byte("foo2"), "", 2, 1)}, 891 {Location: ast.NewLocation([]byte("foo100"), "", 100, 1)}, 892 {Location: ast.NewLocation([]byte("foo3"), "", 3, 1)}, 893 {Location: ast.NewLocation([]byte("foo4"), "", 4, 1)}, 894 }, 895 expectedNames: map[string]string{}, 896 expectedLongest: minLocationWidth + len(":100"), 897 }, 898 { 899 note: "single file name not shortened", 900 trace: []*Event{ 901 {Location: ast.NewLocation([]byte("foo1"), "policy.rego", 1, 1)}, 902 }, 903 expectedNames: map[string]string{ 904 "policy.rego": "policy.rego", 905 }, 906 expectedLongest: len("policy.rego:1"), 907 }, 908 { 909 note: "single file name not shortened different rows", 910 trace: []*Event{ 911 {Location: ast.NewLocation([]byte("foo1"), "policy.rego", 1, 1)}, 912 {Location: ast.NewLocation([]byte("foo1234"), "policy.rego", 1234, 1)}, 913 {Location: ast.NewLocation([]byte("foo12"), "policy.rego", 12, 1)}, 914 {Location: ast.NewLocation([]byte("foo123"), "policy.rego", 123, 1)}, 915 }, 916 expectedNames: map[string]string{ 917 "policy.rego": "policy.rego", 918 }, 919 expectedLongest: len("policy.rego:1234"), 920 }, 921 { 922 note: "multiple files name not shortened", 923 trace: []*Event{ 924 {Location: ast.NewLocation([]byte("a1"), "a.rego", 1, 1)}, 925 {Location: ast.NewLocation([]byte("a1234"), "a.rego", 1234, 1)}, 926 {Location: ast.NewLocation([]byte("x1"), "x.rego", 12, 1)}, 927 {Location: ast.NewLocation([]byte("foo123"), "policy.rego", 123, 1)}, 928 }, 929 expectedNames: map[string]string{ 930 "a.rego": "a.rego", 931 "x.rego": "x.rego", 932 "policy.rego": "policy.rego", 933 }, 934 expectedLongest: len("policy.rego:123"), 935 }, 936 { 937 note: "single file name shortened", 938 trace: []*Event{ 939 {Location: ast.NewLocation([]byte("foo1"), longFilePath1, 1, 1)}, 940 }, 941 expectedNames: map[string]string{ 942 longFilePath1: "/really/...h/longer/than/most/would/really/ever/be/policy.rego", 943 }, 944 expectedLongest: maxIdealLocationWidth, 945 }, 946 { 947 note: "single file name shortened different rows", 948 trace: []*Event{ 949 {Location: ast.NewLocation([]byte("foo1"), longFilePath1, 1, 1)}, 950 {Location: ast.NewLocation([]byte("foo1234"), longFilePath1, 1234, 1)}, 951 {Location: ast.NewLocation([]byte("foo123"), longFilePath1, 123, 1)}, 952 {Location: ast.NewLocation([]byte("foo12"), longFilePath1, 12, 1)}, 953 }, 954 expectedNames: map[string]string{ 955 longFilePath1: "/really/...onger/than/most/would/really/ever/be/policy.rego", 956 }, 957 expectedLongest: maxIdealLocationWidth, 958 }, 959 { 960 note: "multiple files name shortened different rows", 961 trace: []*Event{ 962 {Location: ast.NewLocation([]byte("similar1"), longFilePath1Similar, 1, 1)}, 963 {Location: ast.NewLocation([]byte("foo1234"), longFilePath1, 1234, 1)}, 964 {Location: ast.NewLocation([]byte("similar12"), longFilePath1Similar, 12, 1)}, 965 {Location: ast.NewLocation([]byte("foo123"), longFilePath1, 123, 1)}, 966 }, 967 expectedNames: map[string]string{ 968 longFilePath1: "/really/...onger/than/most/would/really/ever/be/policy.rego", 969 longFilePath1Similar: "/really/...onger/than/most/policy.rego", 970 }, 971 expectedLongest: maxIdealLocationWidth, 972 }, 973 { 974 note: "multiple files name cannot be shortened", 975 trace: []*Event{ 976 {Location: ast.NewLocation([]byte("foo1"), longFilePath2, 1, 1)}, 977 {Location: ast.NewLocation([]byte("foo1234"), longFilePath3, 1234, 1)}, 978 }, 979 expectedNames: map[string]string{ 980 longFilePath2: longFilePath2, 981 longFilePath3: longFilePath3, 982 }, 983 expectedLongest: len(longFilePath3 + ":1234"), 984 }, 985 { 986 note: "single file name shortened no leading slash", 987 trace: []*Event{ 988 {Location: ast.NewLocation([]byte("foo1"), longFilePath1[1:], 1, 1)}, 989 }, 990 expectedNames: map[string]string{ 991 longFilePath1[1:]: "really/...th/longer/than/most/would/really/ever/be/policy.rego", 992 }, 993 expectedLongest: maxIdealLocationWidth, 994 }, 995 } 996 997 for _, tc := range cases { 998 t.Run(tc.note, func(t *testing.T) { 999 actualNames, actualLongest := getShortenedFileNames(tc.trace) 1000 if actualLongest != tc.expectedLongest { 1001 t.Errorf("Expected longest location to be %d, got %d", tc.expectedLongest, actualLongest) 1002 } 1003 1004 if !reflect.DeepEqual(actualNames, tc.expectedNames) { 1005 t.Errorf("Expected %+v got %+v", tc.expectedNames, actualNames) 1006 } 1007 }) 1008 } 1009 } 1010 1011 func TestBufferTracerTraceConfig(t *testing.T) { 1012 ct := QueryTracer(NewBufferTracer()) 1013 conf := ct.Config() 1014 1015 expected := TraceConfig{ 1016 PlugLocalVars: true, 1017 } 1018 1019 if !reflect.DeepEqual(expected, conf) { 1020 t.Fatalf("Expected config: %+v, got %+v", expected, conf) 1021 } 1022 } 1023 1024 func TestTraceInput(t *testing.T) { 1025 ctx := context.Background() 1026 module := ` 1027 package test 1028 1029 rule = x { 1030 x = input.v 1031 } 1032 ` 1033 1034 compiler := compileModules([]string{module}) 1035 queryCompiler := compiler.QueryCompiler() 1036 1037 compiledQuery, err := queryCompiler.Compile(ast.MustParseBody("{x | v = [1, 2, 3][_]; x = data.test.rule with input.v as v}")) 1038 if err != nil { 1039 t.Fatalf("unexpected error: %s", err) 1040 } 1041 1042 tracer := NewBufferTracer() 1043 query := NewQuery(compiledQuery). 1044 WithQueryCompiler(queryCompiler). 1045 WithCompiler(compiler). 1046 WithStore(inmem.New()). 1047 WithQueryTracer(tracer) 1048 1049 if _, err := query.Run(ctx); err != nil { 1050 t.Fatalf("unexpected error: %s", err) 1051 } 1052 1053 num := 1 1054 for i, evt := range *tracer { 1055 if evt.Op == ExitOp && evt.HasRule() { 1056 input := evt.Input().Value 1057 expected := ast.NewObject([2]*ast.Term{ast.StringTerm("v"), ast.IntNumberTerm(num)}) 1058 if input.Compare(expected) != 0 { 1059 t.Errorf("%v != %v at index %d", input, expected, i) 1060 } 1061 num++ 1062 } 1063 } 1064 } 1065 1066 func TestTracePlug(t *testing.T) { 1067 ctx := context.Background() 1068 module := ` 1069 package test 1070 1071 rule[[a, b]] { 1072 a = [1, 2][_] 1073 b = [2, 1][_] 1074 } 1075 ` 1076 1077 compiler := compileModules([]string{module}) 1078 queryCompiler := compiler.QueryCompiler() 1079 1080 compiledQuery, err := queryCompiler.Compile(ast.MustParseBody("data.test.rule")) 1081 if err != nil { 1082 t.Fatalf("unexpected error: %s", err) 1083 } 1084 1085 tracer := plugRuleHeadKeyRecorder{} 1086 query := NewQuery(compiledQuery). 1087 WithQueryCompiler(queryCompiler). 1088 WithCompiler(compiler). 1089 WithStore(inmem.New()). 1090 WithQueryTracer(&tracer) 1091 1092 if _, err := query.Run(ctx); err != nil { 1093 t.Fatalf("unexpected error: %s", err) 1094 } 1095 1096 expected := []ast.Value{ 1097 ast.NewArray(ast.NumberTerm("1"), ast.NumberTerm("2")), 1098 ast.NewArray(ast.NumberTerm("1"), ast.NumberTerm("1")), 1099 ast.NewArray(ast.NumberTerm("2"), ast.NumberTerm("2")), 1100 ast.NewArray(ast.NumberTerm("2"), ast.NumberTerm("1")), 1101 } 1102 1103 if len(tracer) != len(expected) { 1104 t.Fatalf("unexpected result length %d", len(tracer)) 1105 } 1106 1107 for i, value := range tracer { 1108 if value.Compare(expected[i]) != 0 { 1109 t.Errorf("%v != %v at index %d", value, expected[i], i) 1110 } 1111 } 1112 } 1113 1114 type plugRuleHeadKeyRecorder []ast.Value 1115 1116 func (plugRuleHeadKeyRecorder) Enabled() bool { 1117 return true 1118 } 1119 1120 func (pr *plugRuleHeadKeyRecorder) TraceEvent(evt Event) { 1121 if evt.Op == ExitOp && evt.HasRule() { 1122 *pr = append(*pr, evt.Plug(evt.Node.(*ast.Rule).Head.Key).Value) 1123 } 1124 } 1125 1126 func (plugRuleHeadKeyRecorder) Config() TraceConfig { 1127 return TraceConfig{PlugLocalVars: false} 1128 } 1129 1130 func compareBuffers(t *testing.T, expected, actual string) { 1131 t.Helper() 1132 a := strings.Split(expected, "\n") 1133 b := strings.Split(actual, "\n") 1134 min := len(a) 1135 if min > len(b) { 1136 min = len(b) 1137 } 1138 1139 for i := 0; i < min; i++ { 1140 if a[i] != b[i] { 1141 t.Errorf("Line %v in trace is incorrect. Expected %q but got: %q", i+1, a[i], b[i]) 1142 } 1143 } 1144 1145 if len(a) < len(b) { 1146 t.Errorf("Extra lines in trace:\n%v", strings.Join(b[min:], "\n")) 1147 } else if len(b) < len(a) { 1148 t.Errorf("Missing lines in trace:\n%v", strings.Join(a[min:], "\n")) 1149 } 1150 1151 if t.Failed() { 1152 fmt.Println("Trace output:") 1153 fmt.Println(actual) 1154 } 1155 } 1156 1157 func TestPrettyTraceWithLocationForMetadataCall(t *testing.T) { 1158 module := `package test 1159 rule_no_output_var := rego.metadata.rule() 1160 1161 rule_with_output_var { 1162 foo := rego.metadata.rule() 1163 foo == {} 1164 } 1165 1166 chain_no_output_var := rego.metadata.chain() 1167 1168 chain_with_output_var { 1169 foo := rego.metadata.chain() 1170 foo == [] 1171 }` 1172 1173 ctx := context.Background() 1174 compiler := compileModules([]string{module}) 1175 data := loadSmallTestData() 1176 store := inmem.NewFromObject(data) 1177 txn := storage.NewTransactionOrDie(ctx, store) 1178 defer store.Abort(ctx, txn) 1179 1180 tracer := NewBufferTracer() 1181 query := NewQuery(ast.MustParseBody("data.test = _")). 1182 WithCompiler(compiler). 1183 WithStore(store). 1184 WithTransaction(txn). 1185 WithTracer(tracer) 1186 1187 _, err := query.Run(ctx) 1188 if err != nil { 1189 panic(err) 1190 } 1191 1192 expected := `query:1 Enter data.test = _ 1193 query:1 | Eval data.test = _ 1194 query:1 | Index data.test.chain_no_output_var (matched 1 rule) 1195 query:9 | Enter data.test.chain_no_output_var 1196 query:9 | | Eval __local8__ = [{"path": ["test", "chain_no_output_var"]}] 1197 query:9 | | Eval true 1198 query:9 | | Eval __local4__ = __local8__ 1199 query:9 | | Exit data.test.chain_no_output_var 1200 query:9 | Redo data.test.chain_no_output_var 1201 query:9 | | Redo __local4__ = __local8__ 1202 query:9 | | Redo true 1203 query:9 | | Redo __local8__ = [{"path": ["test", "chain_no_output_var"]}] 1204 query:1 | Index data.test.chain_with_output_var (matched 1 rule, early exit) 1205 query:11 | Enter data.test.chain_with_output_var 1206 query:12 | | Eval __local9__ = [{"path": ["test", "chain_with_output_var"]}] 1207 query:12 | | Eval __local5__ = __local9__ 1208 query:12 | | Eval foo = __local5__ 1209 query:13 | | Eval foo = [] 1210 query:13 | | Fail foo = [] 1211 query:12 | | Redo foo = __local5__ 1212 query:12 | | Redo __local5__ = __local9__ 1213 query:12 | | Redo __local9__ = [{"path": ["test", "chain_with_output_var"]}] 1214 query:1 | Index data.test.rule_no_output_var (matched 1 rule) 1215 query:2 | Enter data.test.rule_no_output_var 1216 query:2 | | Eval __local6__ = {} 1217 query:2 | | Eval true 1218 query:2 | | Eval __local2__ = __local6__ 1219 query:2 | | Exit data.test.rule_no_output_var 1220 query:2 | Redo data.test.rule_no_output_var 1221 query:2 | | Redo __local2__ = __local6__ 1222 query:2 | | Redo true 1223 query:2 | | Redo __local6__ = {} 1224 query:1 | Index data.test.rule_with_output_var (matched 1 rule, early exit) 1225 query:4 | Enter data.test.rule_with_output_var 1226 query:5 | | Eval __local7__ = {} 1227 query:5 | | Eval __local3__ = __local7__ 1228 query:5 | | Eval foo = __local3__ 1229 query:6 | | Eval foo = {} 1230 query:4 | | Exit data.test.rule_with_output_var early 1231 query:4 | Redo data.test.rule_with_output_var 1232 query:6 | | Redo foo = {} 1233 query:5 | | Redo foo = __local3__ 1234 query:5 | | Redo __local3__ = __local7__ 1235 query:5 | | Redo __local7__ = {} 1236 query:1 | Exit data.test = _ 1237 query:1 Redo data.test = _ 1238 query:1 | Redo data.test = _ 1239 ` 1240 1241 var buf bytes.Buffer 1242 PrettyTraceWithLocation(&buf, *tracer) 1243 compareBuffers(t, expected, buf.String()) 1244 }