Merge remote-tracking branch 'upstream/master' into master

* upstream/master:
  Skip mutated struct properties in bpdoc.

Test: TreeHugger
Change-Id: Ic0f28e81975cf353f487c78ca56d0a098acd0883
diff --git a/Blueprints b/Blueprints
index 35e4190..d58169e 100644
--- a/Blueprints
+++ b/Blueprints
@@ -125,6 +125,8 @@
         "bootstrap/bpdoc/reader.go",
     ],
     testSrcs: [
+        "bootstrap/bpdoc/bpdoc_test.go",
+        "bootstrap/bpdoc/properties_test.go",
         "bootstrap/bpdoc/reader_test.go",
     ],
 }
diff --git a/bootstrap/bpdoc/bpdoc.go b/bootstrap/bpdoc/bpdoc.go
index 8ce41cf..4acfc5d 100644
--- a/bootstrap/bpdoc/bpdoc.go
+++ b/bootstrap/bpdoc/bpdoc.go
@@ -173,6 +173,9 @@
 				// The field is not exported so just skip it.
 				continue
 			}
+			if proptools.HasTag(field, "blueprint", "mutated") {
+				continue
+			}
 
 			fieldValue := structValue.Field(i)
 
diff --git a/bootstrap/bpdoc/bpdoc_test.go b/bootstrap/bpdoc/bpdoc_test.go
new file mode 100644
index 0000000..687d97b
--- /dev/null
+++ b/bootstrap/bpdoc/bpdoc_test.go
@@ -0,0 +1,46 @@
+package bpdoc
+
+import (
+	"reflect"
+	"testing"
+)
+
+type parentProps struct {
+	A string
+
+	Child *childProps
+
+	Mutated *mutatedProps `blueprint:"mutated"`
+}
+
+type childProps struct {
+	B int
+
+	Child *grandchildProps
+}
+
+type grandchildProps struct {
+	C bool
+}
+
+type mutatedProps struct {
+	D int
+}
+
+func TestNestedPropertyStructs(t *testing.T) {
+	parent := parentProps{Child: &childProps{Child: &grandchildProps{}}, Mutated: &mutatedProps{}}
+
+	allStructs := nestedPropertyStructs(reflect.ValueOf(parent))
+
+	// mutated shouldn't be found because it's a mutated property.
+	expected := []string{"child", "child.child"}
+	if len(allStructs) != len(expected) {
+		t.Errorf("expected %d structs, got %d, all entries: %q",
+			len(expected), len(allStructs), allStructs)
+	}
+	for _, e := range expected {
+		if _, ok := allStructs[e]; !ok {
+			t.Errorf("missing entry %q, all entries: %q", e, allStructs)
+		}
+	}
+}
diff --git a/bootstrap/bpdoc/properties.go b/bootstrap/bpdoc/properties.go
index 23b1ffd..9256d8e 100644
--- a/bootstrap/bpdoc/properties.go
+++ b/bootstrap/bpdoc/properties.go
@@ -250,17 +250,23 @@
 	// len(props) times to this slice will overwrite the original slice contents
 	filtered := (*props)[:0]
 	for _, x := range *props {
-		tag := x.Tag.Get(key)
-		for _, entry := range strings.Split(tag, ",") {
-			if (entry == value) == !exclude {
-				filtered = append(filtered, x)
-			}
+		if hasTag(x.Tag, key, value) == !exclude {
+			filtered = append(filtered, x)
 		}
 	}
 
 	*props = filtered
 }
 
+func hasTag(tag reflect.StructTag, key, value string) bool {
+	for _, entry := range strings.Split(tag.Get(key), ",") {
+		if entry == value {
+			return true
+		}
+	}
+	return false
+}
+
 func formatText(text string) template.HTML {
 	var html template.HTML
 	lines := strings.Split(text, "\n")
diff --git a/bootstrap/bpdoc/properties_test.go b/bootstrap/bpdoc/properties_test.go
new file mode 100644
index 0000000..4045cb1
--- /dev/null
+++ b/bootstrap/bpdoc/properties_test.go
@@ -0,0 +1,58 @@
+// Copyright 2019 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package bpdoc
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestExcludeByTag(t *testing.T) {
+	r := NewReader(pkgFiles)
+	ps, err := r.PropertyStruct(pkgPath, "tagTestProps", reflect.ValueOf(tagTestProps{}))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	ps.ExcludeByTag("tag1", "a")
+
+	expected := []string{"c"}
+	actual := []string{}
+	for _, p := range ps.Properties {
+		actual = append(actual, p.Name)
+	}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("unexpected ExcludeByTag result, expected: %q, actual: %q", expected, actual)
+	}
+}
+
+func TestIncludeByTag(t *testing.T) {
+	r := NewReader(pkgFiles)
+	ps, err := r.PropertyStruct(pkgPath, "tagTestProps", reflect.ValueOf(tagTestProps{A: "B"}))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	ps.IncludeByTag("tag1", "c")
+
+	expected := []string{"b", "c"}
+	actual := []string{}
+	for _, p := range ps.Properties {
+		actual = append(actual, p.Name)
+	}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("unexpected IncludeByTag result, expected: %q, actual: %q", expected, actual)
+	}
+}
diff --git a/bootstrap/bpdoc/reader_test.go b/bootstrap/bpdoc/reader_test.go
index b8ff109..0d608b3 100644
--- a/bootstrap/bpdoc/reader_test.go
+++ b/bootstrap/bpdoc/reader_test.go
@@ -34,6 +34,13 @@
 	A string
 }
 
+// for properties_test.go
+type tagTestProps struct {
+	A string `tag1:"a,b" tag2:"c"`
+	B string `tag1:"a,c"`
+	C string `tag1:"b,c"`
+}
+
 var pkgPath string
 var pkgFiles map[string][]string