topdown: Improve pretty trace location details

Previously we had a hard set width for location info in pretty trace
outputs. This changes to use a dynamic width up to a reasonable max
and then starts to shorten paths as possible by swapping in `...` for
the longest common substring of all paths.

To get the longest substring this brings in a couple files from
https://github.com/vmarkovtsev/go-lcss which implements an efficient
algorithm for it (rather than us implementing something fancy from
scratch). It's pretty isolated and is unlikely to need any updates
over time so the maintenance should be low.

Fixes: #2143
Signed-off-by: Patrick East <east.patrick@gmail.com>
This commit is contained in:
Patrick East
2020-03-31 20:00:53 -07:00
parent adf3ffbc4b
commit 94cd44ed4e
7 changed files with 1296 additions and 141 deletions
+3
View File
@@ -0,0 +1,3 @@
# Longest Common Substring
Original source https://github.com/vmarkovtsev/go-lcss
+197
View File
@@ -0,0 +1,197 @@
package lcss
import "bytes"
// LongestCommonSubstring returns the longest substring which is present in all the given strings.
// https://en.wikipedia.org/wiki/Longest_common_substring_problem
// Not to be confused with the Longest Common Subsequence.
// Complexity:
// * time: sum of `n_i*log(n_i)` where `n_i` is the length of each string.
// * space: sum of `n_i`.
// Returns a byte slice which is never a nil.
//
// ### Algorithm.
// We build suffix arrays for each of the passed string and then follow the same procedure
// as in merge sort: pick the least suffix in the lexicographical order. It is possible
// because the suffix arrays are already sorted.
// We record the last encountered suffixes from each of the strings and measure the longest
// common prefix of those at each "merge sort" step.
// The string comparisons are optimized by maintaining the char-level prefix tree of the "heads"
// of the suffix array sequences.
func LongestCommonSubstring(strs ...[]byte) []byte {
strslen := len(strs)
if strslen == 0 {
return []byte{}
}
if strslen == 1 {
return strs[0]
}
suffixes := make([][]int, strslen)
for i, str := range strs {
suffixes[i] = qsufsort(str)
}
return lcss(strs, suffixes)
}
func lcss(strs [][]byte, suffixes [][]int) []byte {
strslen := len(strs)
if strslen == 0 {
return []byte{}
}
if strslen == 1 {
return strs[0]
}
minstrlen := len(strs[0]) // minimum length of the strings
for _, str := range strs {
if minstrlen > len(str) {
minstrlen = len(str)
}
}
heads := make([]int, strslen) // position in each suffix array
boilerplate := make([][]byte, strslen) // existing suffixes in the tree
boiling := 0 // indicates how many distinct suffix arrays are presented in `boilerplate`
var root charNode // the character tree built on the strings from `boilerplate`
lcs := []byte{} // our function's return value, `var lcss []byte` does *not* work
for {
mini := -1
var minSuffixStr []byte
for i, head := range heads {
if head >= len(suffixes[i]) {
// this suffix array has been scanned till the end
continue
}
suffix := strs[i][suffixes[i][head]:]
if minSuffixStr == nil {
// initialize
mini = i
minSuffixStr = suffix
} else if bytes.Compare(minSuffixStr, suffix) > 0 {
// the current suffix is the smallest in the lexicographical order
mini = i
minSuffixStr = suffix
}
}
if mini == -1 {
// all heads exhausted
break
}
if boilerplate[mini] != nil {
// if we already have a suffix from this string, replace it with the new one
root.Remove(boilerplate[mini])
} else {
// we track the number of distinct strings which have been touched
// when `boiling` becomes strslen we can start measuring the longest common prefix
boiling++
}
boilerplate[mini] = minSuffixStr
root.Add(minSuffixStr)
heads[mini]++
if boiling == strslen && root.LongestCommonPrefixLength() > len(lcs) {
// all heads > 0, the current common prefix of the suffixes is the longest
lcs = root.LongestCommonPrefix()
if len(lcs) == minstrlen {
// early exit - we will never find a longer substring
break
}
}
}
return lcs
}
// charNode builds a tree of individual characters.
// `used` is the counter for collecting garbage: those nodes which have `used`=0 are removed.
// The root charNode always remains intact apart from `children`.
// The tree supports 4 operations:
// 1. Add() a new string.
// 2. Remove() an existing string which was previously Add()-ed.
// 3. LongestCommonPrefixLength().
// 4. LongestCommonPrefix().
type charNode struct {
char byte
children []charNode
used int
}
// Add includes a new string into the tree. We start from the root and
// increment `used` of all the nodes we visit.
func (cn *charNode) Add(str []byte) {
head := cn
for i, char := range str {
found := false
for j, child := range head.children {
if child.char == char {
head.children[j].used++
head = &head.children[j] // -> child
found = true
break
}
}
if !found {
// add the missing nodes one by one
for _, char = range str[i:] {
head.children = append(head.children, charNode{char: char, children: nil, used: 1})
head = &head.children[len(head.children)-1]
}
break
}
}
}
// Remove excludes a node which was previously Add()-ed.
// We start from the root and decrement `used` of all the nodes we visit.
// If there is a node with `used`=0, we erase it from the parent's list of children
// and stop traversing the tree.
func (cn *charNode) Remove(str []byte) {
stop := false
head := cn
for _, char := range str {
for j, child := range head.children {
if child.char != char {
continue
}
head.children[j].used--
var parent *charNode
head, parent = &head.children[j], head // shift to the child
if head.used == 0 {
parent.children = append(parent.children[:j], parent.children[j+1:]...)
// we can skip deleting the rest of the nodes - they have been already discarded
stop = true
}
break
}
if stop {
break
}
}
}
// LongestCommonPrefixLength returns the length of the longest common prefix of the strings
// which are stored in the tree. We visit the children recursively starting from the root and
// stop if `used` value decreases or there is more than one child.
func (cn charNode) LongestCommonPrefixLength() int {
var result int
for head := cn; len(head.children) == 1 && head.children[0].used >= head.used; head = head.children[0] {
result++
}
return result
}
// LongestCommonPrefix returns the longest common prefix of the strings
// which are stored in the tree. We compute the length by calling LongestCommonPrefixLength()
// and then record the characters which we visit along the way from the root to the last node.
func (cn charNode) LongestCommonPrefix() []byte {
result := make([]byte, cn.LongestCommonPrefixLength())
if len(result) == 0 {
return result
}
var i int
for head := cn.children[0]; ; head = head.children[0] {
result[i] = head.char
i++
if i == len(result) {
break
}
}
return result
}
+242
View File
@@ -0,0 +1,242 @@
package lcss
import (
"reflect"
"testing"
)
func assertEqual(t *testing.T, expected, actual interface{}) {
t.Helper()
if !reflect.DeepEqual(expected, actual) {
t.Errorf("Expected %v got %v", expected, actual)
}
}
func TestCharNodeAdd(t *testing.T) {
node := &charNode{}
node.Add([]byte("abc"))
assertEqual(t, byte(0), node.char)
assertEqual(t, 0, node.used)
assertEqual(t, 1, len(node.children))
assertEqual(t, byte('a'), node.children[0].char)
assertEqual(t, 1, node.children[0].used)
assertEqual(t, 1, len(node.children[0].children))
assertEqual(t, byte('b'), node.children[0].children[0].char)
assertEqual(t, 1, node.children[0].children[0].used)
assertEqual(t, 1, len(node.children[0].children[0].children))
assertEqual(t, byte('c'), node.children[0].children[0].children[0].char)
assertEqual(t, 1, node.children[0].children[0].children[0].used)
assertEqual(t, 0, len(node.children[0].children[0].children[0].children))
node.Add([]byte{})
assertEqual(t, byte(0), node.char)
assertEqual(t, 0, node.used)
assertEqual(t, 1, len(node.children))
node.Add([]byte("abd"))
assertEqual(t, byte(0), node.char)
assertEqual(t, 0, node.used)
assertEqual(t, 1, len(node.children))
assertEqual(t, byte('a'), node.children[0].char)
assertEqual(t, 2, node.children[0].used)
assertEqual(t, 1, len(node.children[0].children))
assertEqual(t, byte('b'), node.children[0].children[0].char)
assertEqual(t, 2, node.children[0].children[0].used)
assertEqual(t, 2, len(node.children[0].children[0].children))
assertEqual(t, byte('c'), node.children[0].children[0].children[0].char)
assertEqual(t, 1, node.children[0].children[0].children[0].used)
assertEqual(t, 0, len(node.children[0].children[0].children[0].children))
assertEqual(t, byte('d'), node.children[0].children[0].children[1].char)
assertEqual(t, 1, node.children[0].children[0].children[1].used)
assertEqual(t, 0, len(node.children[0].children[0].children[1].children))
node.Add([]byte("abc"))
assertEqual(t, byte(0), node.char)
assertEqual(t, 0, node.used)
assertEqual(t, 1, len(node.children))
assertEqual(t, byte('a'), node.children[0].char)
assertEqual(t, 3, node.children[0].used)
assertEqual(t, 1, len(node.children[0].children))
assertEqual(t, byte('b'), node.children[0].children[0].char)
assertEqual(t, 3, node.children[0].children[0].used)
assertEqual(t, 2, len(node.children[0].children[0].children))
assertEqual(t, byte('c'), node.children[0].children[0].children[0].char)
assertEqual(t, 2, node.children[0].children[0].children[0].used)
assertEqual(t, 0, len(node.children[0].children[0].children[0].children))
assertEqual(t, byte('d'), node.children[0].children[0].children[1].char)
assertEqual(t, 1, node.children[0].children[0].children[1].used)
assertEqual(t, 0, len(node.children[0].children[0].children[1].children))
}
func TestCharNodeRemove(t *testing.T) {
node := &charNode{}
node.Add([]byte("abc"))
node.Remove([]byte("abc"))
assertEqual(t, byte(0), node.char)
assertEqual(t, 0, node.used)
assertEqual(t, 0, len(node.children))
node.Add([]byte("abc"))
node.Add([]byte("abd"))
node.Remove([]byte("abc"))
assertEqual(t, byte(0), node.char)
assertEqual(t, 0, node.used)
assertEqual(t, 1, len(node.children))
assertEqual(t, byte('a'), node.children[0].char)
assertEqual(t, 1, node.children[0].used)
assertEqual(t, 1, len(node.children[0].children))
assertEqual(t, byte('b'), node.children[0].children[0].char)
assertEqual(t, 1, node.children[0].children[0].used)
assertEqual(t, 1, len(node.children[0].children[0].children))
assertEqual(t, byte('d'), node.children[0].children[0].children[0].char)
assertEqual(t, 1, node.children[0].children[0].children[0].used)
assertEqual(t, 0, len(node.children[0].children[0].children[0].children))
node.Remove([]byte{})
assertEqual(t, byte(0), node.char)
assertEqual(t, 0, node.used)
assertEqual(t, 1, len(node.children))
assertEqual(t, byte('a'), node.children[0].char)
assertEqual(t, 1, node.children[0].used)
assertEqual(t, 1, len(node.children[0].children))
assertEqual(t, byte('b'), node.children[0].children[0].char)
assertEqual(t, 1, node.children[0].children[0].used)
assertEqual(t, 1, len(node.children[0].children[0].children))
assertEqual(t, byte('d'), node.children[0].children[0].children[0].char)
assertEqual(t, 1, node.children[0].children[0].children[0].used)
assertEqual(t, 0, len(node.children[0].children[0].children[0].children))
node.Add([]byte("ab"))
node.Remove([]byte("ab"))
assertEqual(t, byte(0), node.char)
assertEqual(t, 0, node.used)
assertEqual(t, 1, len(node.children))
assertEqual(t, byte('a'), node.children[0].char)
assertEqual(t, 1, node.children[0].used)
assertEqual(t, 1, len(node.children[0].children))
assertEqual(t, byte('b'), node.children[0].children[0].char)
assertEqual(t, 1, node.children[0].children[0].used)
assertEqual(t, 1, len(node.children[0].children[0].children))
assertEqual(t, byte('d'), node.children[0].children[0].children[0].char)
assertEqual(t, 1, node.children[0].children[0].children[0].used)
assertEqual(t, 0, len(node.children[0].children[0].children[0].children))
node.Add([]byte("ab"))
node.Remove([]byte("abd"))
assertEqual(t, byte(0), node.char)
assertEqual(t, 0, node.used)
assertEqual(t, 1, len(node.children))
assertEqual(t, byte('a'), node.children[0].char)
assertEqual(t, 1, node.children[0].used)
assertEqual(t, 1, len(node.children[0].children))
assertEqual(t, byte('b'), node.children[0].children[0].char)
assertEqual(t, 1, node.children[0].children[0].used)
assertEqual(t, 0, len(node.children[0].children[0].children))
}
func TestCharNodeLongestCommonPrefixLength(t *testing.T) {
node := &charNode{}
assertEqual(t, 0, node.LongestCommonPrefixLength())
node.Add([]byte("abc"))
assertEqual(t, 3, node.LongestCommonPrefixLength())
node.Add([]byte("abd"))
assertEqual(t, 2, node.LongestCommonPrefixLength())
node.Remove([]byte("abd"))
assertEqual(t, 3, node.LongestCommonPrefixLength())
node.Add([]byte("ab"))
assertEqual(t, 2, node.LongestCommonPrefixLength())
}
func TestCharNodeLongestCommonPrefix(t *testing.T) {
node := &charNode{}
assertEqual(t, []byte{}, node.LongestCommonPrefix())
node.Add([]byte("abc"))
assertEqual(t, []byte("abc"), node.LongestCommonPrefix())
node.Add([]byte("abd"))
assertEqual(t, []byte("ab"), node.LongestCommonPrefix())
node.Remove([]byte("abd"))
assertEqual(t, []byte("abc"), node.LongestCommonPrefix())
node.Add([]byte("ab"))
assertEqual(t, []byte("ab"), node.LongestCommonPrefix())
}
func TestCharNodeBug1(t *testing.T) {
node := &charNode{}
node.Add([]byte("a"))
node.Add([]byte("a"))
node.Remove([]byte("a"))
node.Add([]byte("abbara"))
node.Add([]byte("abr"))
assertEqual(t, byte(0), node.char)
assertEqual(t, 0, node.used)
assertEqual(t, 1, len(node.children))
assertEqual(t, byte('a'), node.children[0].char)
assertEqual(t, 3, node.children[0].used)
assertEqual(t, 1, len(node.children[0].children))
assertEqual(t, byte('b'), node.children[0].children[0].char)
assertEqual(t, 2, node.children[0].children[0].used)
assertEqual(t, 2, len(node.children[0].children[0].children))
assertEqual(t, byte('b'), node.children[0].children[0].children[0].char)
assertEqual(t, 1, node.children[0].children[0].children[0].used)
assertEqual(t, 1, len(node.children[0].children[0].children[0].children))
assertEqual(t, byte('r'), node.children[0].children[0].children[1].char)
assertEqual(t, 1, node.children[0].children[0].children[1].used)
assertEqual(t, 0, len(node.children[0].children[0].children[1].children))
assertEqual(t, 1, node.LongestCommonPrefixLength())
assertEqual(t, []byte("a"), node.LongestCommonPrefix())
}
func TestCharNodeBug2(t *testing.T) {
node := &charNode{}
node.Add([]byte("habrahabr"))
node.Add([]byte("bbara"))
node.Add([]byte("mraja"))
node.Remove([]byte("habrahabr"))
node.Add([]byte("r"))
node.Remove([]byte("bbara"))
node.Add([]byte("ra"))
node.Remove([]byte("r"))
node.Add([]byte("rahabr"))
node.Remove([]byte("bbara"))
node.Add([]byte("ra"))
node.Remove([]byte("mraja"))
node.Add([]byte("raja"))
assertEqual(t, byte(0), node.char)
assertEqual(t, 0, node.used)
assertEqual(t, 1, len(node.children))
assertEqual(t, byte('r'), node.children[0].char)
assertEqual(t, 3, node.children[0].used)
assertEqual(t, 1, len(node.children[0].children))
assertEqual(t, byte('a'), node.children[0].children[0].char)
assertEqual(t, 3, node.children[0].children[0].used)
assertEqual(t, 2, len(node.children[0].children[0].children))
assertEqual(t, byte('h'), node.children[0].children[0].children[0].char)
assertEqual(t, 1, node.children[0].children[0].children[0].used)
assertEqual(t, 1, len(node.children[0].children[0].children[0].children))
assertEqual(t, byte('j'), node.children[0].children[0].children[1].char)
assertEqual(t, 1, node.children[0].children[0].children[1].used)
assertEqual(t, 1, len(node.children[0].children[0].children[1].children))
assertEqual(t, []byte("ra"), node.LongestCommonPrefix())
}
func TestLongestCommonSubstring(t *testing.T) {
assertEqual(t, []byte{}, LongestCommonSubstring())
assertEqual(t, []byte("abc"), LongestCommonSubstring([]byte("abc")))
assertEqual(t, []byte{}, LongestCommonSubstring([]byte("abc"), []byte{}))
assertEqual(t, []byte("ab"), LongestCommonSubstring([]byte("abc"), []byte("abd")))
assertEqual(t, []byte("bc"), LongestCommonSubstring([]byte("abc"), []byte("dbc")))
assertEqual(t, []byte("ab"), LongestCommonSubstring([]byte("ab"), []byte("abd")))
assertEqual(t, []byte("ABC"), LongestCommonSubstring(
[]byte("ABABC"), []byte("BABCA"), []byte("ABCBA")))
assertEqual(t, []byte("ra"), LongestCommonSubstring(
[]byte("habrahabr"),
[]byte("abbara"),
[]byte("humraja")))
assertEqual(t, []byte("abcdez"), LongestCommonSubstring(
[]byte("zxabcdezy"),
[]byte("yzabcdezx"),
[]byte("abcdez"),
[]byte("zyzxabcdez")))
}
func TestLongestCommonSubstringWithSuffixArrays(t *testing.T) {
assertEqual(t, []byte{}, lcss(nil, nil))
assertEqual(t, []byte("abc"), lcss(
[][]byte{[]byte("abc")}, [][]int{{1, 2, 3}}))
}
+169
View File
@@ -0,0 +1,169 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This algorithm is based on "Faster Suffix Sorting"
// by N. Jesper Larsson and Kunihiko Sadakane
// paper: http://www.larsson.dogma.net/ssrev-tr.pdf
// code: http://www.larsson.dogma.net/qsufsort.c
// This algorithm computes the suffix array sa by computing its inverse.
// Consecutive groups of suffixes in sa are labeled as sorted groups or
// unsorted groups. For a given pass of the sorter, all suffixes are ordered
// up to their first h characters, and sa is h-ordered. Suffixes in their
// final positions and unambiguously sorted in h-order are in a sorted group.
// Consecutive groups of suffixes with identical first h characters are an
// unsorted group. In each pass of the algorithm, unsorted groups are sorted
// according to the group number of their following suffix.
// In the implementation, if sa[i] is negative, it indicates that i is
// the first element of a sorted group of length -sa[i], and can be skipped.
// An unsorted group sa[i:k] is given the group number of the index of its
// last element, k-1. The group numbers are stored in the inverse slice (inv),
// and when all groups are sorted, this slice is the inverse suffix array.
package lcss
import "sort"
// qsufsort constructs the suffix array for a given string.
func qsufsort(data []byte) []int {
// initial sorting by first byte of suffix
sa := sortedByFirstByte(data)
if len(sa) < 2 {
return sa
}
// initialize the group lookup table
// this becomes the inverse of the suffix array when all groups are sorted
inv := initGroups(sa, data)
// the index starts 1-ordered
sufSortable := &suffixSortable{sa: sa, inv: inv, h: 1}
for sa[0] > -len(sa) { // until all suffixes are one big sorted group
// The suffixes are h-ordered, make them 2*h-ordered
pi := 0 // pi is first position of first group
sl := 0 // sl is negated length of sorted groups
for pi < len(sa) {
if s := sa[pi]; s < 0 { // if pi starts sorted group
pi -= s // skip over sorted group
sl += s // add negated length to sl
} else { // if pi starts unsorted group
if sl != 0 {
sa[pi+sl] = sl // combine sorted groups before pi
sl = 0
}
pk := inv[s] + 1 // pk-1 is last position of unsorted group
sufSortable.sa = sa[pi:pk]
sort.Sort(sufSortable)
sufSortable.updateGroups(pi)
pi = pk // next group
}
}
if sl != 0 { // if the array ends with a sorted group
sa[pi+sl] = sl // combine sorted groups at end of sa
}
sufSortable.h *= 2 // double sorted depth
}
for i := range sa { // reconstruct suffix array from inverse
sa[inv[i]] = i
}
return sa
}
func sortedByFirstByte(data []byte) []int {
// total byte counts
var count [256]int
for _, b := range data {
count[b]++
}
// make count[b] equal index of first occurrence of b in sorted array
sum := 0
for b := range count {
count[b], sum = sum, count[b]+sum
}
// iterate through bytes, placing index into the correct spot in sa
sa := make([]int, len(data))
for i, b := range data {
sa[count[b]] = i
count[b]++
}
return sa
}
func initGroups(sa []int, data []byte) []int {
// label contiguous same-letter groups with the same group number
inv := make([]int, len(data))
prevGroup := len(sa) - 1
groupByte := data[sa[prevGroup]]
for i := len(sa) - 1; i >= 0; i-- {
if b := data[sa[i]]; b < groupByte {
if prevGroup == i+1 {
sa[i+1] = -1
}
groupByte = b
prevGroup = i
}
inv[sa[i]] = prevGroup
if prevGroup == 0 {
sa[0] = -1
}
}
// Separate out the final suffix to the start of its group.
// This is necessary to ensure the suffix "a" is before "aba"
// when using a potentially unstable sort.
lastByte := data[len(data)-1]
s := -1
for i := range sa {
if sa[i] >= 0 {
if data[sa[i]] == lastByte && s == -1 {
s = i
}
if sa[i] == len(sa)-1 {
sa[i], sa[s] = sa[s], sa[i]
inv[sa[s]] = s
sa[s] = -1 // mark it as an isolated sorted group
break
}
}
}
return inv
}
type suffixSortable struct {
sa []int
inv []int
h int
buf []int // common scratch space
}
func (x *suffixSortable) Len() int { return len(x.sa) }
func (x *suffixSortable) Less(i, j int) bool { return x.inv[x.sa[i]+x.h] < x.inv[x.sa[j]+x.h] }
func (x *suffixSortable) Swap(i, j int) { x.sa[i], x.sa[j] = x.sa[j], x.sa[i] }
func (x *suffixSortable) updateGroups(offset int) {
bounds := x.buf[0:0]
group := x.inv[x.sa[0]+x.h]
for i := 1; i < len(x.sa); i++ {
if g := x.inv[x.sa[i]+x.h]; g > group {
bounds = append(bounds, i)
group = g
}
}
bounds = append(bounds, len(x.sa))
x.buf = bounds
// update the group numberings after all new groups are determined
prev := 0
for _, b := range bounds {
for i := prev; i < b; i++ {
x.inv[x.sa[i]] = offset + b - 1
}
if b-prev == 1 {
x.sa[prev] = -1
}
prev = b
}
}
+28 -28
View File
@@ -1987,28 +1987,28 @@ func TestEvalTrace(t *testing.T) {
repl.OneShot(ctx, "trace")
repl.OneShot(ctx, `data.a[i].b.c[j] = x; data.a[k].b.c[x] = 1`)
expected := strings.TrimSpace(`
query:1 Enter data.a[i].b.c[j] = x; data.a[k].b.c[x] = 1
query:1 | Eval data.a[i].b.c[j] = x
query:1 | Eval data.a[k].b.c[x] = 1
query:1 | Fail data.a[k].b.c[x] = 1
query:1 | Redo data.a[i].b.c[j] = x
query:1 | Eval data.a[k].b.c[x] = 1
query:1 | Exit data.a[i].b.c[j] = x; data.a[k].b.c[x] = 1
query:1 Redo data.a[i].b.c[j] = x; data.a[k].b.c[x] = 1
query:1 | Redo data.a[k].b.c[x] = 1
query:1 | Redo data.a[i].b.c[j] = x
query:1 | Eval data.a[k].b.c[x] = 1
query:1 | Fail data.a[k].b.c[x] = 1
query:1 | Redo data.a[i].b.c[j] = x
query:1 | Eval data.a[k].b.c[x] = 1
query:1 | Fail data.a[k].b.c[x] = 1
query:1 | Redo data.a[i].b.c[j] = x
query:1 | Eval data.a[k].b.c[x] = 1
query:1 | Fail data.a[k].b.c[x] = 1
query:1 | Redo data.a[i].b.c[j] = x
query:1 | Eval data.a[k].b.c[x] = 1
query:1 | Fail data.a[k].b.c[x] = 1
query:1 | Redo data.a[i].b.c[j] = x
query:1 Enter data.a[i].b.c[j] = x; data.a[k].b.c[x] = 1
query:1 | Eval data.a[i].b.c[j] = x
query:1 | Eval data.a[k].b.c[x] = 1
query:1 | Fail data.a[k].b.c[x] = 1
query:1 | Redo data.a[i].b.c[j] = x
query:1 | Eval data.a[k].b.c[x] = 1
query:1 | Exit data.a[i].b.c[j] = x; data.a[k].b.c[x] = 1
query:1 Redo data.a[i].b.c[j] = x; data.a[k].b.c[x] = 1
query:1 | Redo data.a[k].b.c[x] = 1
query:1 | Redo data.a[i].b.c[j] = x
query:1 | Eval data.a[k].b.c[x] = 1
query:1 | Fail data.a[k].b.c[x] = 1
query:1 | Redo data.a[i].b.c[j] = x
query:1 | Eval data.a[k].b.c[x] = 1
query:1 | Fail data.a[k].b.c[x] = 1
query:1 | Redo data.a[i].b.c[j] = x
query:1 | Eval data.a[k].b.c[x] = 1
query:1 | Fail data.a[k].b.c[x] = 1
query:1 | Redo data.a[i].b.c[j] = x
query:1 | Eval data.a[k].b.c[x] = 1
query:1 | Fail data.a[k].b.c[x] = 1
query:1 | Redo data.a[i].b.c[j] = x
+---+---+---+---+
| i | j | k | x |
+---+---+---+---+
@@ -2030,12 +2030,12 @@ func TestEvalNotes(t *testing.T) {
repl.OneShot(ctx, "notes")
buffer.Reset()
repl.OneShot(ctx, "p")
expected := strings.TrimSpace(`query:1 Enter data.repl.p = _
query:1 | Enter data.repl.p
note | | Note "x = 2"
query:1 Redo data.repl.p = _
query:1 | Redo data.repl.p
note | | Note "x = 3"
expected := strings.TrimSpace(`query:1 Enter data.repl.p = _
query:1 | Enter data.repl.p
note | | Note "x = 2"
query:1 Redo data.repl.p = _
query:1 | Redo data.repl.p
note | | Note "x = 3"
true`)
expected += "\n"
if expected != buffer.String() {
+106 -7
View File
@@ -7,12 +7,20 @@ package topdown
import (
"fmt"
"io"
"path/filepath"
"strings"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/internal/lcss"
"github.com/open-policy-agent/opa/topdown/builtins"
)
const (
minLocationWidth = 5 // len("query")
maxIdealLocationWidth = 64
locationPadding = 4
)
// Op defines the types of tracing events.
type Op string
@@ -162,10 +170,16 @@ func PrettyTrace(w io.Writer, trace []*Event) {
// PrettyTraceWithLocation prints the trace to the writer and includes location information
func PrettyTraceWithLocation(w io.Writer, trace []*Event) {
depths := depths{}
filePathAliases, longest := getShortenedFileNames(trace)
// Always include some padding between the trace and location
locationWidth := longest + locationPadding
for _, event := range trace {
depth := depths.GetOrSet(event.QueryID, event.ParentID)
location := formatLocation(event)
fmt.Fprintln(w, fmt.Sprintf("%v %v", location, formatEvent(event, depth)))
location := formatLocation(event, filePathAliases)
fmt.Fprintf(w, "%-*s %s\n", locationWidth, location, formatEvent(event, depth))
}
}
@@ -206,21 +220,106 @@ func formatEventSpaces(event *Event, depth int) int {
return depth + 1
}
func formatLocation(event *Event) string {
// getShortenedFileNames will return a map of file paths to shortened aliases
// that were found in the trace. It also returns the longest location expected
func getShortenedFileNames(trace []*Event) (map[string]string, int) {
// Get a deduplicated list of all file paths
// and the longest file path size
fpAliases := map[string]string{}
var canShorten [][]byte
longestLocation := 0
for _, event := range trace {
if event.Location != nil {
if event.Location.File != "" {
// length of "<name>:<row>"
curLen := len(event.Location.File) + numDigits10(event.Location.Row) + 1
if curLen > longestLocation {
longestLocation = curLen
}
if _, ok := fpAliases[event.Location.File]; ok {
continue
}
// Only try and shorten the middle parts of paths, ex: bundle1/.../a/b/policy.rego
path := filepath.Dir(event.Location.File)
path = strings.TrimPrefix(path, string(filepath.Separator))
firstSlash := strings.IndexRune(path, filepath.Separator)
if firstSlash > 0 {
path = path[firstSlash+1:]
}
canShorten = append(canShorten, []byte(path))
// Default to just alias their full path
fpAliases[event.Location.File] = event.Location.File
} else {
// length of "<min width>:<row>"
curLen := minLocationWidth + numDigits10(event.Location.Row) + 1
if curLen > longestLocation {
longestLocation = curLen
}
}
}
}
if len(canShorten) > 0 && longestLocation > maxIdealLocationWidth {
// Find the longest common path segment..
var lcs string
if len(canShorten) > 1 {
lcs = string(lcss.LongestCommonSubstring(canShorten...))
} else {
lcs = string(canShorten[0])
}
// Don't just swap in the full LCSS, trim it down to be the least amount of
// characters to reach our "ideal" width boundary giving as much
// detail as possible without going too long.
diff := maxIdealLocationWidth - (longestLocation - len(lcs) + 3)
if diff > 0 {
if diff > len(lcs) {
lcs = ""
} else {
// Favor data on the right hand side of the path
lcs = lcs[:len(lcs)-diff]
}
}
// Swap in "..." for the longest common path, but if it makes things better
if len(lcs) > 3 {
for path := range fpAliases {
fpAliases[path] = strings.Replace(path, lcs, "...", 1)
}
// Drop the overall length down to match our substitution
longestLocation = longestLocation - (len(lcs) - 3)
}
}
return fpAliases, longestLocation
}
func numDigits10(n int) int {
if n < 10 {
return 1
}
return numDigits10(n/10) + 1
}
func formatLocation(event *Event, fileAliases map[string]string) string {
if event.Op == NoteOp {
return fmt.Sprintf("%-19v", "note")
return fmt.Sprintf("%v", "note")
}
location := event.Location
if location == nil {
return fmt.Sprintf("%-19v", "")
return ""
}
if location.File == "" {
return fmt.Sprintf("%-19v", fmt.Sprintf("%.15v:%v", "query", location.Row))
return fmt.Sprintf("query:%v", location.Row)
}
return fmt.Sprintf("%-19v", fmt.Sprintf("%.15v:%v", location.File, location.Row))
return fmt.Sprintf("%v:%v", fileAliases[location.File], location.Row)
}
// depths is a helper for computing the depth of an event. Events within the
+551 -106
View File
@@ -7,12 +7,15 @@ package topdown
import (
"bytes"
"context"
"fmt"
"reflect"
"strings"
"testing"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/storage"
"github.com/open-policy-agent/opa/storage/inmem"
"github.com/open-policy-agent/opa/util"
)
func TestEventEqual(t *testing.T) {
@@ -168,49 +171,49 @@ func TestPrettyTraceWithLocation(t *testing.T) {
panic(err)
}
expected := `query:1 Enter data.test.p = _
query:1 | Eval data.test.p = _
query:1 | Index data.test.p = _ (matched 1 rule)
query:3 | Enter data.test.p
query:3 | | Eval data.test.q[x]
query:3 | | Index data.test.q[x] (matched 1 rule)
query:4 | | Enter data.test.q
query:4 | | | Eval x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Exit data.test.p
query:1 | Exit data.test.p = _
query:1 Redo data.test.p = _
query:1 | Redo data.test.p = _
query:3 | Redo data.test.p
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Exit data.test.p
query:3 | Redo data.test.p
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Exit data.test.p
query:3 | Redo data.test.p
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Exit data.test.p
query:3 | Redo data.test.p
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
expected := `query:1 Enter data.test.p = _
query:1 | Eval data.test.p = _
query:1 | Index data.test.p = _ (matched 1 rule)
query:3 | Enter data.test.p
query:3 | | Eval data.test.q[x]
query:3 | | Index data.test.q[x] (matched 1 rule)
query:4 | | Enter data.test.q
query:4 | | | Eval x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Exit data.test.p
query:1 | Exit data.test.p = _
query:1 Redo data.test.p = _
query:1 | Redo data.test.p = _
query:3 | Redo data.test.p
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Exit data.test.p
query:3 | Redo data.test.p
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Exit data.test.p
query:3 | Redo data.test.p
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Exit data.test.p
query:3 | Redo data.test.p
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
`
a := strings.Split(expected, "\n")
@@ -236,6 +239,296 @@ query:4 | | | Redo x = data.a[_]
}
}
func TestPrettyTraceWithLocationTruncatedPaths(t *testing.T) {
ctx := context.Background()
compiler := ast.MustCompileModules(map[string]string{
"authz_bundle/com/foo/bar/baz/qux/acme/corp/internal/authz/policies/abac/v1/beta/policy.rego": `
package test
import data.utils.q
p = true { q[x]; plus(x, 1, n) }
`,
"authz_bundle/com/foo/bar/baz/qux/acme/corp/internal/authz/policies/utils/utils.rego": `
package utils
q[x] { x = data.a[_] }
`,
})
data := loadSmallTestData()
store := inmem.NewFromObject(data)
txn := storage.NewTransactionOrDie(ctx, store)
defer store.Abort(ctx, txn)
tracer := NewBufferTracer()
query := NewQuery(ast.MustParseBody("data.test.p = _")).
WithCompiler(compiler).
WithStore(store).
WithTransaction(txn).
WithTracer(tracer)
_, err := query.Run(ctx)
if err != nil {
panic(err)
}
expected := `query:1 Enter data.test.p = _
query:1 | Eval data.test.p = _
query:1 | Index data.test.p = _ (matched 1 rule)
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | Enter data.test.p
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Eval data.utils.q[x]
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Index data.utils.q[x] (matched 1 rule)
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | Enter data.utils.q
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | | Eval x = data.a[_]
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | | Exit data.utils.q
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Eval plus(x, 1, n)
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Exit data.test.p
query:1 | Exit data.test.p = _
query:1 Redo data.test.p = _
query:1 | Redo data.test.p = _
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | Redo data.test.p
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Redo plus(x, 1, n)
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Redo data.utils.q[x]
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | Redo data.utils.q
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | | Redo x = data.a[_]
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | | Exit data.utils.q
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Eval plus(x, 1, n)
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Exit data.test.p
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | Redo data.test.p
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Redo plus(x, 1, n)
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Redo data.utils.q[x]
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | Redo data.utils.q
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | | Redo x = data.a[_]
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | | Exit data.utils.q
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Eval plus(x, 1, n)
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Exit data.test.p
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | Redo data.test.p
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Redo plus(x, 1, n)
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Redo data.utils.q[x]
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | Redo data.utils.q
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | | Redo x = data.a[_]
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | | Exit data.utils.q
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Eval plus(x, 1, n)
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Exit data.test.p
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | Redo data.test.p
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Redo plus(x, 1, n)
authz_bundle/...ternal/authz/policies/abac/v1/beta/policy.rego:6 | | Redo data.utils.q[x]
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | Redo data.utils.q
authz_bundle/...ternal/authz/policies/utils/utils.rego:4 | | | Redo x = data.a[_]
`
a := strings.Split(expected, "\n")
var buf bytes.Buffer
PrettyTraceWithLocation(&buf, *tracer)
b := strings.Split(buf.String(), "\n")
min := len(a)
if min > len(b) {
min = len(b)
}
for i := 0; i < min; i++ {
if a[i] != b[i] {
t.Errorf("Line %v in trace is incorrect. Expected %v but got: %v", i+1, a[i], b[i])
}
}
if len(a) < len(b) {
t.Fatalf("Extra lines in trace:\n%v", strings.Join(b[min:], "\n"))
} else if len(b) < len(a) {
t.Fatalf("Missing lines in trace:\n%v", strings.Join(a[min:], "\n"))
}
}
func TestPrettyTracePartialWithLocationTruncatedPaths(t *testing.T) {
ctx := context.Background()
compiler := ast.MustCompileModules(map[string]string{
"authz_bundle/com/foo/bar/baz/qux/acme/corp/internal/authz/policies/rbac/v1/beta/policy.rego": `
package example_rbac
default allow = false
allow {
data.utils.user_has_role[role_name]
data.utils.role_has_permission[role_name]
}
`,
"authz_bundle/com/foo/bar/baz/qux/acme/corp/internal/authz/policies/utils/user.rego": `
package utils
user_has_role[role_name] {
role_binding = data.bindings[_]
role_binding.role = role_name
role_binding.user = input.subject.user
}
role_has_permission[role_name] {
role = data.roles[_]
role.name = role_name
role.operation = input.action.operation
role.resource = input.action.resource
}
`,
})
var data map[string]interface{}
err := util.UnmarshalJSON([]byte(`{
"roles": [
{
"operation": "read",
"resource": "widgets",
"name": "widget-reader"
},
{
"operation": "write",
"resource": "widgets",
"name": "widget-writer"
}
],
"bindings": [
{
"user": "inspector-alice",
"role": "widget-reader"
},
{
"user": "maker-bob",
"role": "widget-writer"
}
]
}`), &data)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
store := inmem.NewFromObject(data)
txn := storage.NewTransactionOrDie(ctx, store)
defer store.Abort(ctx, txn)
tracer := NewBufferTracer()
query := NewQuery(ast.MustParseBody("data.example_rbac.allow")).
WithCompiler(compiler).
WithStore(store).
WithTransaction(txn).
WithUnknowns([]*ast.Term{ast.MustParseTerm("input")}).
WithTracer(tracer)
_, _, err = query.PartialRun(ctx)
if err != nil {
panic(err)
}
expected := `query:1 Enter data.example_rbac.allow
query:1 | Eval data.example_rbac.allow
query:1 | Index data.example_rbac.allow (matched 1 rule)
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6 | Enter data.example_rbac.allow
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:7 | | Eval data.utils.user_has_role[role_name]
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:7 | | Index data.utils.user_has_role[role_name] (matched 1 rule)
authz_bundle/...ternal/authz/policies/utils/user.rego:4 | | Enter data.utils.user_has_role
authz_bundle/...ternal/authz/policies/utils/user.rego:5 | | | Eval role_binding = data.bindings[_]
authz_bundle/...ternal/authz/policies/utils/user.rego:6 | | | Eval role_binding.role = role_name
authz_bundle/...ternal/authz/policies/utils/user.rego:7 | | | Eval role_binding.user = input.subject.user
authz_bundle/...ternal/authz/policies/utils/user.rego:7 | | | Save "inspector-alice" = input.subject.user
authz_bundle/...ternal/authz/policies/utils/user.rego:4 | | | Exit data.utils.user_has_role
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9 | | Eval data.utils.role_has_permission[role_name]
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9 | | Index data.utils.role_has_permission[role_name] (matched 1 rule)
authz_bundle/...ternal/authz/policies/utils/user.rego:10 | | Enter data.utils.role_has_permission
authz_bundle/...ternal/authz/policies/utils/user.rego:11 | | | Eval role = data.roles[_]
authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Eval role.name = role_name
authz_bundle/...ternal/authz/policies/utils/user.rego:13 | | | Eval role.operation = input.action.operation
authz_bundle/...ternal/authz/policies/utils/user.rego:13 | | | Save "read" = input.action.operation
authz_bundle/...ternal/authz/policies/utils/user.rego:14 | | | Eval role.resource = input.action.resource
authz_bundle/...ternal/authz/policies/utils/user.rego:14 | | | Save "widgets" = input.action.resource
authz_bundle/...ternal/authz/policies/utils/user.rego:10 | | | Exit data.utils.role_has_permission
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6 | | Exit data.example_rbac.allow
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6 | Redo data.example_rbac.allow
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9 | | Redo data.utils.role_has_permission[role_name]
authz_bundle/...ternal/authz/policies/utils/user.rego:10 | | Redo data.utils.role_has_permission
authz_bundle/...ternal/authz/policies/utils/user.rego:14 | | | Redo role.resource = input.action.resource
authz_bundle/...ternal/authz/policies/utils/user.rego:13 | | | Redo role.operation = input.action.operation
authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Redo role.name = role_name
authz_bundle/...ternal/authz/policies/utils/user.rego:11 | | | Redo role = data.roles[_]
authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Eval role.name = role_name
authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Fail role.name = role_name
authz_bundle/...ternal/authz/policies/utils/user.rego:11 | | | Redo role = data.roles[_]
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:7 | | Redo data.utils.user_has_role[role_name]
authz_bundle/...ternal/authz/policies/utils/user.rego:4 | | Redo data.utils.user_has_role
authz_bundle/...ternal/authz/policies/utils/user.rego:7 | | | Redo role_binding.user = input.subject.user
authz_bundle/...ternal/authz/policies/utils/user.rego:6 | | | Redo role_binding.role = role_name
authz_bundle/...ternal/authz/policies/utils/user.rego:5 | | | Redo role_binding = data.bindings[_]
authz_bundle/...ternal/authz/policies/utils/user.rego:6 | | | Eval role_binding.role = role_name
authz_bundle/...ternal/authz/policies/utils/user.rego:7 | | | Eval role_binding.user = input.subject.user
authz_bundle/...ternal/authz/policies/utils/user.rego:7 | | | Save "maker-bob" = input.subject.user
authz_bundle/...ternal/authz/policies/utils/user.rego:4 | | | Exit data.utils.user_has_role
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9 | | Eval data.utils.role_has_permission[role_name]
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9 | | Index data.utils.role_has_permission[role_name] (matched 1 rule)
authz_bundle/...ternal/authz/policies/utils/user.rego:10 | | Enter data.utils.role_has_permission
authz_bundle/...ternal/authz/policies/utils/user.rego:11 | | | Eval role = data.roles[_]
authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Eval role.name = role_name
authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Fail role.name = role_name
authz_bundle/...ternal/authz/policies/utils/user.rego:11 | | | Redo role = data.roles[_]
authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Eval role.name = role_name
authz_bundle/...ternal/authz/policies/utils/user.rego:13 | | | Eval role.operation = input.action.operation
authz_bundle/...ternal/authz/policies/utils/user.rego:13 | | | Save "write" = input.action.operation
authz_bundle/...ternal/authz/policies/utils/user.rego:14 | | | Eval role.resource = input.action.resource
authz_bundle/...ternal/authz/policies/utils/user.rego:14 | | | Save "widgets" = input.action.resource
authz_bundle/...ternal/authz/policies/utils/user.rego:10 | | | Exit data.utils.role_has_permission
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6 | | Exit data.example_rbac.allow
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6 | Redo data.example_rbac.allow
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9 | | Redo data.utils.role_has_permission[role_name]
authz_bundle/...ternal/authz/policies/utils/user.rego:10 | | Redo data.utils.role_has_permission
authz_bundle/...ternal/authz/policies/utils/user.rego:14 | | | Redo role.resource = input.action.resource
authz_bundle/...ternal/authz/policies/utils/user.rego:13 | | | Redo role.operation = input.action.operation
authz_bundle/...ternal/authz/policies/utils/user.rego:12 | | | Redo role.name = role_name
authz_bundle/...ternal/authz/policies/utils/user.rego:11 | | | Redo role = data.roles[_]
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:7 | | Redo data.utils.user_has_role[role_name]
authz_bundle/...ternal/authz/policies/utils/user.rego:4 | | Redo data.utils.user_has_role
authz_bundle/...ternal/authz/policies/utils/user.rego:7 | | | Redo role_binding.user = input.subject.user
authz_bundle/...ternal/authz/policies/utils/user.rego:6 | | | Redo role_binding.role = role_name
authz_bundle/...ternal/authz/policies/utils/user.rego:5 | | | Redo role_binding = data.bindings[_]
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4 | Enter data.example_rbac.allow
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4 | | Eval true
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4 | | Exit data.example_rbac.allow
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4 | Redo data.example_rbac.allow
authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4 | | Redo true
query:1 | Save data.partial.example_rbac.allow = _
query:1 | Save _
query:1 | Exit data.example_rbac.allow
query:1 Redo data.example_rbac.allow
query:1 | Fail data.example_rbac.allow
`
a := strings.Split(expected, "\n")
var buf bytes.Buffer
PrettyTraceWithLocation(&buf, *tracer)
b := strings.Split(buf.String(), "\n")
min := len(a)
if min > len(b) {
min = len(b)
}
for i := 0; i < min; i++ {
if a[i] != b[i] {
t.Errorf("Line %v in trace is incorrect. Expected %v but got: %v", i+1, a[i], b[i])
}
}
if len(a) < len(b) {
t.Errorf("Extra lines in trace:\n%v", strings.Join(b[min:], "\n"))
} else if len(b) < len(a) {
t.Errorf("Missing lines in trace:\n%v", strings.Join(a[min:], "\n"))
}
if t.Failed() {
fmt.Println("Trace output:")
fmt.Println(buf.String())
}
}
func TestTraceNote(t *testing.T) {
module := `package test
@@ -374,69 +667,69 @@ func TestTraceNoteWithLocation(t *testing.T) {
panic(err)
}
expected := `query:1 Enter data.test.p = _
query:1 | Eval data.test.p = _
query:1 | Index data.test.p = _ (matched 1 rule)
query:3 | Enter data.test.p
query:3 | | Eval data.test.q[x]
query:3 | | Index data.test.q[x] (matched 1 rule)
query:4 | | Enter data.test.q
query:4 | | | Eval x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Eval sprintf("n= %v", [n], __local0__)
query:3 | | Eval trace(__local0__)
note | | Note "n= 2"
query:3 | | Exit data.test.p
query:1 | Exit data.test.p = _
query:1 Redo data.test.p = _
query:1 | Redo data.test.p = _
query:3 | Redo data.test.p
query:3 | | Redo trace(__local0__)
query:3 | | Redo sprintf("n= %v", [n], __local0__)
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Eval sprintf("n= %v", [n], __local0__)
query:3 | | Eval trace(__local0__)
note | | Note "n= 3"
query:3 | | Exit data.test.p
query:3 | Redo data.test.p
query:3 | | Redo trace(__local0__)
query:3 | | Redo sprintf("n= %v", [n], __local0__)
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Eval sprintf("n= %v", [n], __local0__)
query:3 | | Eval trace(__local0__)
note | | Note "n= 4"
query:3 | | Exit data.test.p
query:3 | Redo data.test.p
query:3 | | Redo trace(__local0__)
query:3 | | Redo sprintf("n= %v", [n], __local0__)
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Eval sprintf("n= %v", [n], __local0__)
query:3 | | Eval trace(__local0__)
note | | Note "n= 5"
query:3 | | Exit data.test.p
query:3 | Redo data.test.p
query:3 | | Redo trace(__local0__)
query:3 | | Redo sprintf("n= %v", [n], __local0__)
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
expected := `query:1 Enter data.test.p = _
query:1 | Eval data.test.p = _
query:1 | Index data.test.p = _ (matched 1 rule)
query:3 | Enter data.test.p
query:3 | | Eval data.test.q[x]
query:3 | | Index data.test.q[x] (matched 1 rule)
query:4 | | Enter data.test.q
query:4 | | | Eval x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Eval sprintf("n= %v", [n], __local0__)
query:3 | | Eval trace(__local0__)
note | | Note "n= 2"
query:3 | | Exit data.test.p
query:1 | Exit data.test.p = _
query:1 Redo data.test.p = _
query:1 | Redo data.test.p = _
query:3 | Redo data.test.p
query:3 | | Redo trace(__local0__)
query:3 | | Redo sprintf("n= %v", [n], __local0__)
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Eval sprintf("n= %v", [n], __local0__)
query:3 | | Eval trace(__local0__)
note | | Note "n= 3"
query:3 | | Exit data.test.p
query:3 | Redo data.test.p
query:3 | | Redo trace(__local0__)
query:3 | | Redo sprintf("n= %v", [n], __local0__)
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Eval sprintf("n= %v", [n], __local0__)
query:3 | | Eval trace(__local0__)
note | | Note "n= 4"
query:3 | | Exit data.test.p
query:3 | Redo data.test.p
query:3 | | Redo trace(__local0__)
query:3 | | Redo sprintf("n= %v", [n], __local0__)
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
query:4 | | | Exit data.test.q
query:3 | | Eval plus(x, 1, n)
query:3 | | Eval sprintf("n= %v", [n], __local0__)
query:3 | | Eval trace(__local0__)
note | | Note "n= 5"
query:3 | | Exit data.test.p
query:3 | Redo data.test.p
query:3 | | Redo trace(__local0__)
query:3 | | Redo sprintf("n= %v", [n], __local0__)
query:3 | | Redo plus(x, 1, n)
query:3 | | Redo data.test.q[x]
query:4 | | Redo data.test.q
query:4 | | | Redo x = data.a[_]
`
a := strings.Split(expected, "\n")
@@ -565,3 +858,155 @@ func TestTraceRewrittenVarsIssue2022(t *testing.T) {
t.Fatal("expected copy to contain rewritten var")
}
}
func TestShortTraceFileNames(t *testing.T) {
longFilePath1 := "/really/long/file/path/longer/than/most/would/really/ever/be/policy.rego"
longFilePath1Similar := "/really/long/file/path/longer/than/most/policy.rego"
longFilePath2 := "GfjEjnMA6coNiPoMoRMVk7KeorGeRmjRkIYUsWtr564SQ7yDo4Yss2SoN8PMoe0TOfVaNFd1HQbC9NhK.rego"
longFilePath3 := "RqS50uWAOxqqHmzdKVM3OCVsZDb12FJikUYHhz9pNqMWx3wjeQBKY3UYXsJXzYGOzuYZbidag5SfKVdk.rego"
cases := []struct {
note string
trace []*Event
expectedNames map[string]string
expectedLongest int
}{
{
note: "empty trace",
trace: nil,
expectedNames: map[string]string{},
expectedLongest: 0,
},
{
note: "no locations",
trace: []*Event{
{Op: EnterOp, Node: ast.MustParseBody("true")},
{Op: EvalOp, Node: ast.MustParseBody("true")},
},
expectedNames: map[string]string{},
expectedLongest: 0,
},
{
note: "no file names",
trace: []*Event{
{Location: ast.NewLocation([]byte("foo1"), "", 1, 1)},
{Location: ast.NewLocation([]byte("foo2"), "", 2, 1)},
{Location: ast.NewLocation([]byte("foo100"), "", 100, 1)},
{Location: ast.NewLocation([]byte("foo3"), "", 3, 1)},
{Location: ast.NewLocation([]byte("foo4"), "", 4, 1)},
},
expectedNames: map[string]string{},
expectedLongest: minLocationWidth + len(":100"),
},
{
note: "single file name not shortened",
trace: []*Event{
{Location: ast.NewLocation([]byte("foo1"), "policy.rego", 1, 1)},
},
expectedNames: map[string]string{
"policy.rego": "policy.rego",
},
expectedLongest: len("policy.rego:1"),
},
{
note: "single file name not shortened different rows",
trace: []*Event{
{Location: ast.NewLocation([]byte("foo1"), "policy.rego", 1, 1)},
{Location: ast.NewLocation([]byte("foo1234"), "policy.rego", 1234, 1)},
{Location: ast.NewLocation([]byte("foo12"), "policy.rego", 12, 1)},
{Location: ast.NewLocation([]byte("foo123"), "policy.rego", 123, 1)},
},
expectedNames: map[string]string{
"policy.rego": "policy.rego",
},
expectedLongest: len("policy.rego:1234"),
},
{
note: "multiple files name not shortened",
trace: []*Event{
{Location: ast.NewLocation([]byte("a1"), "a.rego", 1, 1)},
{Location: ast.NewLocation([]byte("a1234"), "a.rego", 1234, 1)},
{Location: ast.NewLocation([]byte("x1"), "x.rego", 12, 1)},
{Location: ast.NewLocation([]byte("foo123"), "policy.rego", 123, 1)},
},
expectedNames: map[string]string{
"a.rego": "a.rego",
"x.rego": "x.rego",
"policy.rego": "policy.rego",
},
expectedLongest: len("policy.rego:123"),
},
{
note: "single file name shortened",
trace: []*Event{
{Location: ast.NewLocation([]byte("foo1"), longFilePath1, 1, 1)},
},
expectedNames: map[string]string{
longFilePath1: "/really/...h/longer/than/most/would/really/ever/be/policy.rego",
},
expectedLongest: maxIdealLocationWidth,
},
{
note: "single file name shortened different rows",
trace: []*Event{
{Location: ast.NewLocation([]byte("foo1"), longFilePath1, 1, 1)},
{Location: ast.NewLocation([]byte("foo1234"), longFilePath1, 1234, 1)},
{Location: ast.NewLocation([]byte("foo123"), longFilePath1, 123, 1)},
{Location: ast.NewLocation([]byte("foo12"), longFilePath1, 12, 1)},
},
expectedNames: map[string]string{
longFilePath1: "/really/...onger/than/most/would/really/ever/be/policy.rego",
},
expectedLongest: maxIdealLocationWidth,
},
{
note: "multiple files name shortened different rows",
trace: []*Event{
{Location: ast.NewLocation([]byte("similar1"), longFilePath1Similar, 1, 1)},
{Location: ast.NewLocation([]byte("foo1234"), longFilePath1, 1234, 1)},
{Location: ast.NewLocation([]byte("similar12"), longFilePath1Similar, 12, 1)},
{Location: ast.NewLocation([]byte("foo123"), longFilePath1, 123, 1)},
},
expectedNames: map[string]string{
longFilePath1: "/really/...onger/than/most/would/really/ever/be/policy.rego",
longFilePath1Similar: "/really/...onger/than/most/policy.rego",
},
expectedLongest: maxIdealLocationWidth,
},
{
note: "multiple files name cannot be shortened",
trace: []*Event{
{Location: ast.NewLocation([]byte("foo1"), longFilePath2, 1, 1)},
{Location: ast.NewLocation([]byte("foo1234"), longFilePath3, 1234, 1)},
},
expectedNames: map[string]string{
longFilePath2: longFilePath2,
longFilePath3: longFilePath3,
},
expectedLongest: len(longFilePath3 + ":1234"),
},
{
note: "single file name shortened no leading slash",
trace: []*Event{
{Location: ast.NewLocation([]byte("foo1"), longFilePath1[1:], 1, 1)},
},
expectedNames: map[string]string{
longFilePath1[1:]: "really/...th/longer/than/most/would/really/ever/be/policy.rego",
},
expectedLongest: maxIdealLocationWidth,
},
}
for _, tc := range cases {
t.Run(tc.note, func(t *testing.T) {
actualNames, actualLongest := getShortenedFileNames(tc.trace)
if actualLongest != tc.expectedLongest {
t.Errorf("Expected longest location to be %d, got %d", tc.expectedLongest, actualLongest)
}
if !reflect.DeepEqual(actualNames, tc.expectedNames) {
t.Errorf("Expected %+v got %+v", tc.expectedNames, actualNames)
}
})
}
}