Skip to content

Commit

Permalink
Add flag to check if proto.lock is up to date
Browse files Browse the repository at this point in the history
  • Loading branch information
lidavidm authored and nilslice committed May 1, 2019
1 parent d1b4021 commit c1cc0b0
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 6 deletions.
15 changes: 14 additions & 1 deletion cmd/protolock/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Options:
--plugins comma-separated list of executable protolock plugin names
--lockdir [.] directory of proto.lock file
--protoroot [.] root of directory tree containing proto files
--uptodate [false] enforce that proto.lock file is up-to-date with proto files
`

var (
Expand All @@ -44,6 +45,7 @@ var (
plugins = options.String("plugins", "", "comma-separated list of executable protolock plugin names")
lockDir = options.String("lockdir", ".", "directory of proto.lock file")
protoRoot = options.String("protoroot", ".", "root of directory tree containing proto files")
upToDate = options.Bool("uptodate", false, "enforce that proto.lock file is up-to-date with proto files")
)

func main() {
Expand All @@ -58,7 +60,7 @@ func main() {
protolock.SetDebug(*debug)
protolock.SetStrict(*strict)

cfg, err := protolock.NewConfig(*lockDir, *protoRoot, *ignore)
cfg, err := protolock.NewConfig(*lockDir, *protoRoot, *ignore, *upToDate)
if err != nil {
fmt.Println(err)
os.Exit(1)
Expand Down Expand Up @@ -111,6 +113,17 @@ func main() {

func status(cfg *protolock.Config) {
report, err := protolock.Status(*cfg)
if err == protolock.ErrOutOfDate {
fmt.Println("[protolock]:", err)
fmt.Println("[protolock]: run 'protolock commit'")
// Only exit if flag provided for backwards
// compatibility
if cfg.UpToDate {
os.Exit(2)
}
// Don't report the error twice
err = nil
}
if err != protolock.ErrWarningsFound && err != nil {
fmt.Println("[protolock]:", err)
os.Exit(1)
Expand Down
4 changes: 3 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ type Config struct {
LockDir string
ProtoRoot string
Ignore string
UpToDate bool
}

func NewConfig(lockDir, protoRoot, ignores string) (*Config, error) {
func NewConfig(lockDir, protoRoot, ignores string, upToDate bool) (*Config, error) {
l, err := filepath.Abs(lockDir)
if err != nil {
return nil, err
Expand All @@ -25,6 +26,7 @@ func NewConfig(lockDir, protoRoot, ignores string) (*Config, error) {
LockDir: l,
ProtoRoot: p,
Ignore: ignores,
UpToDate: upToDate,
}, nil
}

Expand Down
2 changes: 1 addition & 1 deletion order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
const ignoreArg = ""

func TestOrder(t *testing.T) {
cfg, err := NewConfig(".", ".", ignoreArg)
cfg, err := NewConfig(".", ".", ignoreArg, false)
assert.NoError(t, err)

// verify that the re-production of the same Protolock encoded as json
Expand Down
14 changes: 12 additions & 2 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,9 @@ var (
ErrWarningsFound = errors.New("comparison found one or more warnings")
)

func Parse(r io.Reader) (Entry, error) {
func Parse(filename string, r io.Reader) (Entry, error) {
parser := proto.NewParser(r)
parser.Filename(filename)
def, err := parser.Parse()
if err != nil {
return Entry{}, err
Expand Down Expand Up @@ -557,7 +558,16 @@ func getUpdatedLock(cfg Config) (*Protolock, error) {
return nil, err
}

entry, err := Parse(f)
// Have the parser report the file path
friendlyPath := path
cwd, err := os.Getwd()
if err == nil {
relpath, err := filepath.Rel(cwd, path)
if err == nil {
friendlyPath = relpath
}
}
entry, err := Parse(friendlyPath, f)
if err != nil {
printIfErr(f.Close())
return nil, err
Expand Down
12 changes: 11 additions & 1 deletion status.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"os"
)

var ErrOutOfDate = errors.New("proto.lock file is not up-to-date with sources")

// Status will report on any issues encountered when comparing the updated tree
// of parsed proto files and the current proto.lock file.
func Status(cfg Config) (*Report, error) {
Expand All @@ -28,5 +30,13 @@ func Status(cfg Config) (*Report, error) {
return nil, err
}

return Compare(current, *updated)
report, err := Compare(current, *updated)
if err != nil {
return report, err
}

if !current.Equal(updated) {
err = ErrOutOfDate
}
return report, err
}
226 changes: 226 additions & 0 deletions uptodate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package protolock

import (
"reflect"
)

// Check whether one lockfile is equal to another.
func (p *Protolock) Equal(q *Protolock) bool {
// Check whether the two lockfiles have the same list of
// definitions, ignoring order.
return isPermutation(p.Definitions, q.Definitions, equalDefinitions)
}

// Check whether two slices are equal, ignoring ordering.
// Uses the provided comparator function to determine equality.
func isPermutation(as, bs interface{}, cmp func(x, y interface{}) bool) bool {
aKind := reflect.TypeOf(as).Kind()
bKind := reflect.TypeOf(bs).Kind()
if aKind != reflect.Array && aKind != reflect.Slice {
panic("isPermutation was given an argument that isn't an array or slice")
}
if bKind != reflect.Array && bKind != reflect.Slice {
panic("isPermutation was given an argument that isn't an array or slice")
}

// Get the lengths of the slices via reflection
aList := reflect.ValueOf(as)
bList := reflect.ValueOf(bs)
aLen := aList.Len()
bLen := bList.Len()

// Slices of different lengths are trivially inequal
if aLen != bLen {
return false
}

// Empty slices are trivially equal
if aLen == 0 {
return true
}

// Try to match each element in A to an element in B
// Keep track of which elements in B we've already matched
used := make([]bool, bLen)
for i := 0; i < aLen; i++ {
current := aList.Index(i).Interface()
found := false
for j := 0; j < bLen; j++ {
if used[j] {
continue
}
candidate := bList.Index(j).Interface()

if cmp(current, candidate) {
// Found a match, mark it as used
found = true
used[j] = true
break
}
}

if !found {
// Nothing in B (that was not already matched)
// matches the current element, slices are
// inequal
return false
}
}

return true
}

// Helper functions to determine equality of subparts of a lockfile.
// Some functions take interface{} because they're used with
// isPermutation.

func equalDefinitions(i, j interface{}) bool {
a := i.(Definition)
b := j.(Definition)
return a.Filepath == b.Filepath && equalEntries(a.Def, b.Def)
}

func equalEntries(a, b Entry) bool {
if a.Package != b.Package {
return false
}
if !isPermutation(a.Enums, b.Enums, equalEnums) {
return false
}
if !isPermutation(a.Messages, b.Messages, equalMessages) {
return false
}
if !isPermutation(a.Services, b.Services, equalServices) {
return false
}
if !isPermutation(a.Imports, b.Imports, equalImports) {
return false
}
return isPermutation(a.Options, b.Options, equalOptions)
}

func equalImports(i, j interface{}) bool {
// Struct has only primitive fields and no slice fields, fall
// back to default equality
a := i.(Import)
b := j.(Import)
return a == b
}

func equalPackage(i, j interface{}) bool {
// Struct has only primitive fields and no slice fields, fall
// back to default equality
a := i.(Package)
b := j.(Package)
return a == b
}

func equalOptions(i, j interface{}) bool {
a := i.(Option)
b := j.(Option)

if a.Name != b.Name || a.Value != b.Value {
return false
}
return isPermutation(a.Aggregated, b.Aggregated, equalOptions)
}

func equalMessages(i, j interface{}) bool {
a := i.(Message)
b := j.(Message)

if a.Name != b.Name || a.Filepath != b.Filepath {
return false
}
if !isPermutation(a.Fields, b.Fields, equalFields) {
return false
}
if !isPermutation(a.Maps, b.Maps, equalMaps) {
return false
}
if !isPermutation(a.ReservedIDs, b.ReservedIDs, equalPrimitives) {
return false
}
if !isPermutation(a.ReservedNames, b.ReservedNames, equalPrimitives) {
return false
}
if !isPermutation(a.Messages, b.Messages, equalMessages) {
return false
}
return isPermutation(a.Options, b.Options, equalOptions)
}

func equalEnumFields(i, j interface{}) bool {
a := i.(EnumField)
b := j.(EnumField)

if a.Name != b.Name || a.Integer != b.Integer {
return false
}
return isPermutation(a.Options, b.Options, equalOptions)
}

func equalEnums(i, j interface{}) bool {
a := i.(Enum)
b := j.(Enum)

if a.Name != b.Name || a.AllowAlias != b.AllowAlias {
return false
}
if !isPermutation(a.ReservedIDs, b.ReservedIDs, equalPrimitives) {
return false
}
if !isPermutation(a.ReservedNames, b.ReservedNames, equalPrimitives) {
return false
}
return isPermutation(a.EnumFields, b.EnumFields, equalEnumFields)
}

func equalMaps(i, j interface{}) bool {
a := i.(Map)
b := j.(Map)

return a.KeyType == b.KeyType && equalFields(a.Field, b.Field)
}

func equalFields(i, j interface{}) bool {
a := i.(Field)
b := j.(Field)

if a.ID != b.ID || a.Name != b.Name {
return false
}
if a.Type != b.Type || a.IsRepeated != b.IsRepeated {
return false
}
return isPermutation(a.Options, b.Options, equalOptions)
}

func equalServices(i, j interface{}) bool {
a := i.(Service)
b := j.(Service)

if a.Name != b.Name || a.Filepath != b.Filepath {
return false
}
return isPermutation(a.RPCs, b.RPCs, equalRPCs)
}

func equalRPCs(i, j interface{}) bool {
a := i.(RPC)
b := j.(RPC)

if a.Name != b.Name || a.InType != b.InType || a.OutType != b.OutType {
return false
}
if a.InStreamed != b.InStreamed || a.OutStreamed != b.OutStreamed {
return false
}

return isPermutation(a.Options, b.Options, equalOptions)
}

// Helper to compare primitive types in isPermutation
func equalPrimitives(i, j interface{}) bool {
return i == j
}
19 changes: 19 additions & 0 deletions uptodate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package protolock

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestIsPermutation(t *testing.T) {
assert := assert.New(t)
assert.True(isPermutation([]int{1, 2}, []int{2, 1}, equalPrimitives))
assert.True(isPermutation([]int{}, []int{}, equalPrimitives))
assert.False(isPermutation([]int{1, 2}, []int{2, 2}, equalPrimitives))
assert.False(isPermutation([]int{1, 2}, []int{2, 1, 2}, equalPrimitives))
assert.False(isPermutation([]int{1, 2}, []int{}, equalPrimitives))
assert.Panics(func() {
isPermutation(1, 2, equalPrimitives)
})
}

0 comments on commit c1cc0b0

Please sign in to comment.