| // Copyright 2016 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package vdltest |
| |
| import ( |
| "fmt" |
| "io" |
| "sort" |
| "strconv" |
| "strings" |
| "unicode/utf8" |
| |
| "v.io/v23/vdl" |
| ) |
| |
| // statsTable is a table of statistics, represented as a sequence of rows. Each |
| // row has a map of columns for easy updating. |
| type statsTable struct { |
| Rows []statRow |
| } |
| |
| // statsTableInfo holds info used for nice formatting. |
| type statsTableInfo struct { |
| Headers []string // Sorted column headers |
| RowNameSize int // Size of row.Name column |
| ColumnSizes statColumns // Size of each data column |
| } |
| |
| // ColumnSize returns the size of the column with the given header. |
| func (info statsTableInfo) ColumnSize(header string) int { |
| total := 0 |
| for index, size := range info.ColumnSizes.Map[header] { |
| if index > 0 { |
| total += 1 // account for comma between each value |
| } |
| total += size |
| } |
| return total |
| } |
| |
| func (t *statsTable) computeInfo() statsTableInfo { |
| var info statsTableInfo |
| // Gather max size information over all values in the table. |
| for _, row := range t.Rows { |
| if size := len(row.Name); size > info.RowNameSize { |
| info.RowNameSize = size |
| } |
| for header, slice := range row.Columns.Map { |
| for index, value := range slice { |
| // Update ColumnSizes with the size of the largest printed value. |
| old := info.ColumnSizes.Get(header, index) |
| if size := len(strconv.Itoa(value)); size > old { |
| info.ColumnSizes.Set(header, index, size) |
| } |
| } |
| } |
| } |
| // Collect Headers for sorting, and adjust ColumnSizes if necessary. |
| for header, sizes := range info.ColumnSizes.Map { |
| info.Headers = append(info.Headers, header) |
| // If the header size is bigger than the computed column size, add padding |
| // to ColumnSizes so that the table aligns correctly. Spread the padding |
| // evenly across each size, with leftover added to the first size. |
| // |
| // There's no need to adjust anything if the header size is smaller than |
| // what's computed; Print takes care of that. |
| // |
| // Note that we only trim spaces from the header to determine its size, but |
| // not to perform lookups. It's useful to use leading spaces in headers to |
| // force a specific header ordering, so we sort and perform lookups with the |
| // spaces intact, but remove them when printing. |
| if pad := len(strings.TrimSpace(header)) - info.ColumnSize(header); pad > 0 { |
| total, padPerSize := 0, pad/len(sizes) |
| for index := range sizes { |
| sizes[index] += padPerSize |
| total += padPerSize |
| } |
| sizes[0] += pad - total |
| } |
| } |
| sort.Strings(info.Headers) |
| return info |
| } |
| |
| func centerString(s string, size int) string { |
| if len(s) >= size { |
| return s |
| } |
| center := strings.Repeat(" ", (size-len(s))/2) |
| center += s |
| center += strings.Repeat(" ", size-len(center)) |
| return center |
| } |
| |
| func fp(w io.Writer, format string, args ...interface{}) error { |
| _, err := fmt.Fprintf(w, format, args...) |
| return err |
| } |
| |
| // Print prints t into a nicely formatted table. |
| func (t *statsTable) Print(w io.Writer) error { |
| info := t.computeInfo() |
| // Print column headers. |
| if err := t.printRow(w, info, statRowBreak); err != nil { |
| return err |
| } |
| if err := fp(w, "|"+strings.Repeat(" ", info.RowNameSize)); err != nil { |
| return err |
| } |
| for _, header := range info.Headers { |
| // Just like in computeInfo, we only trim spaces from the header for |
| // printing, but leave them intact for lookups. |
| centered := centerString(strings.TrimSpace(header), info.ColumnSize(header)) |
| if err := fp(w, "|"+centered); err != nil { |
| return err |
| } |
| } |
| if err := fp(w, "|\n"); err != nil { |
| return err |
| } |
| // Print each row. |
| if err := t.printRow(w, info, statRowBreak); err != nil { |
| return err |
| } |
| for _, row := range t.Rows { |
| if err := t.printRow(w, info, row); err != nil { |
| return err |
| } |
| } |
| return t.printRow(w, info, statRowBreak) |
| } |
| |
| func (t *statsTable) printRow(w io.Writer, info statsTableInfo, row statRow) error { |
| if row.IsBreak() { |
| // Print row break. |
| total := info.RowNameSize + 2 // +2 for table borders |
| for _, header := range info.Headers { |
| total += info.ColumnSize(header) + 1 // +1 for column borders |
| } |
| return fp(w, strings.Repeat("-", total)+"\n") |
| } |
| // Print each column. |
| if err := fp(w, "|%-[1]*[2]s", info.RowNameSize, row.Name); err != nil { |
| return err |
| } |
| for _, header := range info.Headers { |
| if err := fp(w, "|"); err != nil { |
| return err |
| } |
| for index, size := range info.ColumnSizes.Map[header] { |
| if index > 0 { |
| if err := fp(w, ","); err != nil { |
| return err |
| } |
| } |
| if err := fp(w, "%[1]*[2]d", size, row.Columns.Get(header, index)); err != nil { |
| return err |
| } |
| } |
| } |
| if err := fp(w, "|"); err != nil { |
| return err |
| } |
| // Print each property. |
| for _, prop := range row.Props { |
| if err := prop.Print(w); err != nil { |
| return err |
| } |
| } |
| return fp(w, "\n") |
| } |
| |
| // statRow represents a single row in a statsTable. Each row contains columns |
| // of data, as well as a list of properties. |
| type statRow struct { |
| Name string |
| Columns statColumns |
| Props []statProp |
| } |
| |
| var statRowBreak = statRow{Name: "-"} |
| |
| func (r *statRow) IsBreak() bool { |
| return r.Name == statRowBreak.Name && r.Columns.IsEmpty() && len(r.Props) == 0 |
| } |
| |
| func (r *statRow) PropIndex(name string) int { |
| for ix, prop := range r.Props { |
| if name == prop.Name { |
| return ix |
| } |
| } |
| return -1 |
| } |
| |
| func (r *statRow) UpdateProp(name string, value int, accumulate bool) { |
| index := r.PropIndex(name) |
| if index == -1 { |
| // Add a new property, init both min and max. |
| r.Props = append(r.Props, statProp{name, value, value}) |
| return |
| } |
| // Found an existing property, update it. |
| prop := &r.Props[index] |
| switch { |
| case accumulate: |
| // Min and max are always equal, so they act as a single value. |
| prop.Min += value |
| prop.Max += value |
| default: |
| if value < prop.Min { |
| prop.Min = value |
| } |
| if value > prop.Max { |
| prop.Max = value |
| } |
| } |
| } |
| |
| // statColumns holds the columns for a single statRow, represented as a map from |
| // column headers to int values. Each column may hold multiple values. |
| type statColumns struct { |
| Map map[string][]int |
| } |
| |
| func (x *statColumns) IsEmpty() bool { |
| return len(x.Map) == 0 |
| } |
| |
| func (x *statColumns) Get(header string, index int) int { |
| if len(x.Map[header]) <= index { |
| // The map entry doesn't exist, or the slice isn't big enough. Either way |
| // there isn't an existing value. |
| return 0 |
| } |
| return x.Map[header][index] |
| } |
| |
| func (x *statColumns) Set(header string, index, value int) { |
| if x.Map == nil { |
| x.Map = make(map[string][]int) |
| } |
| slice := x.Map[header] |
| for len(slice) <= index { |
| slice = append(slice, 0) |
| } |
| slice[index] = value |
| x.Map[header] = slice |
| } |
| |
| func (x *statColumns) Delta(header string, index, delta int) { |
| if len(x.Map[header]) <= index { |
| x.Set(header, index, delta) |
| return |
| } |
| slice := x.Map[header] |
| slice[index] += delta |
| x.Map[header] = slice |
| } |
| |
| // statProp is a single int property held in statRow. We maintain the min/max |
| // value of the property as it is collected. |
| type statProp struct { |
| Name string // Name of the property |
| Min int // Min value of the property |
| Max int // Max value of the property |
| } |
| |
| func (p statProp) Print(w io.Writer) error { |
| switch { |
| case p.Min == p.Max: |
| return fp(w, " [%s=%d]", p.Name, p.Max) |
| case p.Min == 0: |
| return fp(w, " [%s max=%d]", p.Name, p.Max) |
| default: |
| return fp(w, " [%s min=%d max=%d]", p.Name, p.Min, p.Max) |
| } |
| } |
| |
| // typePropFn gathers the value of a single type property, used to update the |
| // statProp in a statRow. |
| type typePropFn struct { |
| Name string // Name of the property |
| Fn func(*vdl.Type) int // Fn that extracts the property from a type |
| Accumulate bool // Accumulate results rather than min/max |
| } |
| |
| // typeStatsCollector collects a statsTable over all given Types. |
| type typeStatsCollector struct { |
| Types []*vdl.Type |
| Table statsTable |
| } |
| |
| // Collect collects stats across all types for the named stat row, given the |
| // predicate fn. We keep counts of the number of types that match the |
| // predicate, as well as the number of types containing a type that matches the |
| // predicate. |
| // |
| // The propFns are only run against types that match the predicate, where |
| // properties are either accumulated or compared for min/max. |
| func (c *typeStatsCollector) Collect(name string, fn func(*vdl.Type) bool, propFns ...typePropFn) { |
| row := statRow{Name: name} |
| for _, tt := range c.Types { |
| if fn(tt) { |
| // The predicate matches. Update the total count and the properties. The |
| // leading space in " Total" ensures the Total column shows up first. |
| row.Columns.Delta(" Total", 0, 1) |
| for _, propFn := range propFns { |
| row.UpdateProp(propFn.Name, propFn.Fn(tt), propFn.Accumulate) |
| } |
| } |
| // Update the contains count by walking through tt until the predicate |
| // matches. We invert the predicate since tt.Walk will early-exit when we |
| // return false; thus false means that the predicate matched. |
| contains := !tt.Walk(vdl.WalkAll, func(visit *vdl.Type) bool { |
| return !fn(visit) |
| }) |
| if contains { |
| row.Columns.Delta("Contains", 0, 1) |
| } |
| } |
| c.Table.Rows = append(c.Table.Rows, row) |
| } |
| |
| // PrintTypeStats prints statistics gathered from types into w. |
| func PrintTypeStats(w io.Writer, types ...*vdl.Type) error { |
| stats := typeStatsCollector{Types: types} |
| // Collect stats by kind. |
| for kind := vdl.Any; kind <= vdl.Union; kind++ { |
| var fns []typePropFn |
| switch kind { |
| case vdl.Array: |
| fns = append(fns, typePropFn{Name: "len", Fn: (*vdl.Type).Len}) |
| case vdl.List, vdl.Set, vdl.Map: |
| fn := func(tt *vdl.Type) int { |
| return predToProp(tt.Name() == "") |
| } |
| fns = append(fns, typePropFn{Name: "unnamed", Fn: fn, Accumulate: true}) |
| case vdl.Enum: |
| fns = append(fns, typePropFn{Name: "labels", Fn: (*vdl.Type).NumEnumLabel}) |
| case vdl.Struct, vdl.Union: |
| fns = append(fns, typePropFn{Name: "fields", Fn: (*vdl.Type).NumField}) |
| } |
| stats.Collect(kind.String(), func(tt *vdl.Type) bool { |
| return tt.Kind() == kind |
| }, fns...) |
| } |
| stats.Table.Rows = append(stats.Table.Rows, statRowBreak) |
| // Collect stats by type predicate. |
| stats.Collect("IsNamed", func(tt *vdl.Type) bool { |
| return tt.Name() != "" |
| }) |
| stats.Collect("IsUnnamed", func(tt *vdl.Type) bool { |
| return tt.Name() == "" |
| }) |
| stats.Collect("IsNumber", func(tt *vdl.Type) bool { |
| return tt.Kind().IsNumber() |
| }) |
| stats.Collect("IsErrorType", func(tt *vdl.Type) bool { |
| return tt == vdl.ErrorType || tt.Name() == "VError" |
| }) |
| stats.Collect("IsBytes", (*vdl.Type).IsBytes) |
| stats.Collect("IsPartOfCycle", (*vdl.Type).IsPartOfCycle) |
| stats.Collect("CanBeNamed", (*vdl.Type).CanBeNamed) |
| stats.Collect("CanBeKey", (*vdl.Type).CanBeKey) |
| stats.Collect("CanBeNil", (*vdl.Type).CanBeNil) |
| stats.Collect("CanBeOptional", (*vdl.Type).CanBeOptional) |
| // Print stats table. |
| if err := fp(w, "Types: %d\n", len(types)); err != nil { |
| return err |
| } |
| return stats.Table.Print(w) |
| } |
| |
| func predToProp(pred bool) int { |
| if pred { |
| return 1 |
| } |
| return 0 |
| } |
| |
| // entryPropFn gathers the value of a single entry property, used to update the |
| // statProp in a statRow. |
| type entryPropFn struct { |
| Name string // Name of the property |
| Fn func(EntryValue) int // Fn that extracts the property from an entry |
| Accumulate bool // Accumulate results rather than min/max |
| } |
| |
| // entryStatsCollector collects a statsTable over all given Entries. |
| type entryStatsCollector struct { |
| Entries []EntryValue |
| Table statsTable |
| } |
| |
| // Collect collects stats across all entries for the named stat row, given the |
| // predicate fn. |
| // |
| // The propFns are only run against entries that match the predicate, where |
| // properties are either accumulated or compared for min/max. |
| func (c *entryStatsCollector) Collect(name string, fn func(EntryValue) bool, propFns ...entryPropFn) { |
| const nonCanonicalPropName = "!can" |
| row := statRow{Name: name} |
| if name != "any" && name != "typeobject" { |
| // Ensure the "!can" property shows up first. |
| row.UpdateProp(nonCanonicalPropName, 0, false) |
| } |
| for i, e := range c.Entries { |
| if fn(e) { |
| // The predicate matches. Update the stats table. |
| subColumn := 1 |
| if e.IsCanonical() { |
| subColumn = 0 |
| // Track the non-canonical entries for a given target, which always |
| // follow immediately after the canonical entry. |
| if row.PropIndex(nonCanonicalPropName) != -1 { |
| noncanon := 0 |
| for j := i + 1; j < len(c.Entries); j++ { |
| if !vdl.EqualValue(e.Target, c.Entries[j].Target) { |
| break |
| } |
| noncanon++ |
| } |
| row.UpdateProp(nonCanonicalPropName, noncanon, false) |
| } |
| } |
| // The leading space in " Total" ensures the Total column shows up first. |
| // Strip trailing numbers in e.Label to aggregate Random{0,1,...} under |
| // the same column header. |
| row.Columns.Delta(" Total", subColumn, 1) |
| header := strings.TrimRight(e.Label, "0123456789") |
| row.Columns.Delta(header, subColumn, 1) |
| if e.Source.IsZero() { |
| row.Columns.Delta("isZero", subColumn, 1) |
| } |
| for _, propFn := range propFns { |
| row.UpdateProp(propFn.Name, propFn.Fn(e), propFn.Accumulate) |
| } |
| // Add some error checking. |
| if e.Source.Kind() == vdl.String { |
| // TODO(toddw): Walk through source and check all strings. |
| if !utf8.ValidString(e.Source.RawString()) { |
| row.UpdateProp("ERROR: INVALID UTF8", 1, true) |
| } |
| } |
| } |
| } |
| c.Table.Rows = append(c.Table.Rows, row) |
| } |
| |
| // PrintEntryStats prints statistics gathered from entries into w. |
| func PrintEntryStats(w io.Writer, entries ...EntryValue) error { |
| stats := entryStatsCollector{Entries: entries} |
| // Collect stats by summary stats. |
| stats.Collect("total", func(e EntryValue) bool { |
| return true |
| }) |
| stats.Collect("isZero", func(e EntryValue) bool { |
| return e.Source.IsZero() |
| }) |
| stats.Table.Rows = append(stats.Table.Rows, statRowBreak) |
| // Collect stats by source kind. |
| for kind := vdl.Any; kind <= vdl.Union; kind++ { |
| var fns []entryPropFn |
| switch kind { |
| case vdl.Array, vdl.List, vdl.Set, vdl.Map: |
| fns = append(fns, entryPropFn{Name: "len", Fn: func(e EntryValue) int { |
| return e.Source.Len() |
| }}) |
| case vdl.Any, vdl.Optional: |
| fn := func(e EntryValue) int { |
| return predToProp(e.Source.IsNil()) |
| } |
| fns = append(fns, entryPropFn{Name: "nil", Fn: fn, Accumulate: true}) |
| } |
| stats.Collect(kind.String(), func(e EntryValue) bool { |
| return e.Source.Kind() == kind |
| }, fns...) |
| } |
| stats.Table.Rows = append(stats.Table.Rows, statRowBreak) |
| // Collect stats by source type predicate. |
| stats.Collect("IsNamed", func(e EntryValue) bool { |
| return e.Source.Type().Name() != "" |
| }) |
| stats.Collect("IsUnnamed", func(e EntryValue) bool { |
| return e.Source.Type().Name() == "" |
| }) |
| stats.Collect("IsBytes", func(e EntryValue) bool { |
| return e.Source.Type().IsBytes() |
| }) |
| // Print stats table. |
| if err := fp(w, "Entries: %d\n", len(entries)); err != nil { |
| return err |
| } |
| if err := stats.Table.Print(w); err != nil { |
| return err |
| } |
| const footer = ` |
| Each column has a pair of entry counts (canonical,non-canonical), computed |
| from the Source value. An entry is canonical if Target == Source. |
| !can tracks the number of non-canonical entries for each unique target. |
| |
| +{Max,Min}: Target set to positive max and min values. |
| -{Max,Min}: Target set to negative max and min values. |
| Full: Target is entirely non-zero, except for cyclic types. |
| NilAny: Target is optional(nil), source is any(nil). |
| Random: Target is random value. |
| Zero: Target is zero value. |
| ` |
| return fp(w, footer) |
| } |