Browse code

refactored nested suites builder

Steve Kuznetsov authored on 2016/01/29 01:30:21
Showing 6 changed files
... ...
@@ -47,3 +47,18 @@ func (t *TestSuite) SetDuration(duration string) error {
47 47
 	t.Duration = float64(int(parsedDuration.Seconds()*1000)) / 1000
48 48
 	return nil
49 49
 }
50
+
51
+// ByName implements sort.Interface for []*TestSuite based on the Name field
52
+type ByName []*TestSuite
53
+
54
+func (n ByName) Len() int {
55
+	return len(n)
56
+}
57
+
58
+func (n ByName) Swap(i, j int) {
59
+	n[i], n[j] = n[j], n[i]
60
+}
61
+
62
+func (n ByName) Less(i, j int) bool {
63
+	return n[i].Name < n[j].Name
64
+}
... ...
@@ -19,9 +19,8 @@ type flatTestSuitesBuilder struct {
19 19
 }
20 20
 
21 21
 // AddSuite adds a test suite to the test suites collection being built
22
-func (b *flatTestSuitesBuilder) AddSuite(suite *api.TestSuite) error {
22
+func (b *flatTestSuitesBuilder) AddSuite(suite *api.TestSuite) {
23 23
 	b.testSuites.Suites = append(b.testSuites.Suites, suite)
24
-	return nil
25 24
 }
26 25
 
27 26
 // Build releases the test suites collection being built at whatever current state it is in
... ...
@@ -63,9 +63,7 @@ func TestAddSuite(t *testing.T) {
63 63
 		}
64 64
 
65 65
 		for _, suite := range testCase.suitesToAdd {
66
-			if err := builder.AddSuite(suite); err != nil {
67
-				t.Errorf("%s: unexpected error adding test suite: %v", testCase.name, err)
68
-			}
66
+			builder.AddSuite(suite)
69 67
 		}
70 68
 
71 69
 		if expected, actual := testCase.expectedSuites, builder.Build(); !reflect.DeepEqual(expected, actual) {
... ...
@@ -5,7 +5,7 @@ import "github.com/openshift/origin/tools/junitreport/pkg/api"
5 5
 // TestSuitesBuilder knows how to aggregate data to form a collection of test suites.
6 6
 type TestSuitesBuilder interface {
7 7
 	// AddSuite adds a test suite to the collection
8
-	AddSuite(suite *api.TestSuite) error
8
+	AddSuite(suite *api.TestSuite)
9 9
 
10 10
 	// Build retuns the built structure
11 11
 	Build() *api.TestSuites
... ...
@@ -1,26 +1,27 @@
1 1
 package nested
2 2
 
3 3
 import (
4
+	"sort"
4 5
 	"strings"
5 6
 
6 7
 	"github.com/openshift/origin/tools/junitreport/pkg/api"
7 8
 	"github.com/openshift/origin/tools/junitreport/pkg/builder"
8
-	"github.com/openshift/origin/tools/junitreport/pkg/errors"
9 9
 )
10 10
 
11 11
 // NewTestSuitesBuilder returns a new nested test suites builder. All test suites consumed by
12 12
 // this builder will be added to a multitree of suites rooted at the suites with the given names.
13 13
 func NewTestSuitesBuilder(rootSuiteNames []string) builder.TestSuitesBuilder {
14
-	rootSuites := []*api.TestSuite{}
14
+	restrictedRoots := []*treeNode{}
15
+	nodes := map[string]*treeNode{}
15 16
 	for _, name := range rootSuiteNames {
16
-		rootSuites = append(rootSuites, &api.TestSuite{Name: name})
17
+		root := &treeNode{suite: &api.TestSuite{Name: name}}
18
+		restrictedRoots = append(restrictedRoots, root)
19
+		nodes[name] = root
17 20
 	}
18 21
 
19 22
 	return &nestedTestSuitesBuilder{
20
-		restrictedRoots: len(rootSuites) > 0, // i given they are the only roots allowed
21
-		testSuites: &api.TestSuites{
22
-			Suites: rootSuites,
23
-		},
23
+		restrictedRoots: restrictedRoots,
24
+		nodes:           nodes,
24 25
 	}
25 26
 }
26 27
 
... ...
@@ -31,151 +32,154 @@ const (
31 31
 
32 32
 // nestedTestSuitesBuilder is a test suites builder that nests suites under a root suite
33 33
 type nestedTestSuitesBuilder struct {
34
-	// restrictedRoots determines if the builder is able to add new roots to the tree or if all
35
-	// new suits are to be added only if they are leaves of the original set of roots created
36
-	// by the constructor
37
-	restrictedRoots bool
34
+	// restrictedRoots is the original set of roots created by the constructor, and is populated only
35
+	// if the builder is not able to add new roots to the tree, instead needing to add all nodes as
36
+	// children of these restricted roots
37
+	restrictedRoots []*treeNode
38 38
 
39
-	testSuites *api.TestSuites
39
+	nodes map[string]*treeNode
40 40
 }
41 41
 
42
-// AddSuite adds a test suite to the test suites collection being built if the suite is not in
43
-// the collection, otherwise it overwrites the current record of the suite in the collection. In
44
-// both cases, it updates the metrics of any parent suites to include those of the new suite. If
45
-// the parent of the test suite to be added is found in the collection, the test suite is added
46
-// as a child of that suite. Otherwise, parent suites are created by successively removing one
47
-// layer of package specificity until the root name is found. For instance, if the suite named
48
-// "root/package/subpackage/subsubpackage" were to be added to an empty collection, the suites
49
-// named "root", "root/package", and "root/package/subpackage" would be created and added first,
50
-// then the suite could be added as a child of the latter parent package. If roots are restricted,
51
-// then test suites to be added are asssumed to be nested under one of the root suites created by
52
-// the constructor method and the attempted addition of a suite not rooted in those suites will
53
-// fail silently to allow for selective tree-building given a root.
54
-func (b *nestedTestSuitesBuilder) AddSuite(suite *api.TestSuite) error {
55
-	if recordedSuite := b.findSuite(suite.Name); recordedSuite != nil {
56
-		// if we are trying to add a suite that already exists, we just need to overwrite our
57
-		// current record with the data in the new suite to be added
58
-		recordedSuite.NumTests = suite.NumTests
59
-		recordedSuite.NumSkipped = suite.NumSkipped
60
-		recordedSuite.NumFailed = suite.NumFailed
61
-		recordedSuite.Duration = suite.Duration
62
-		recordedSuite.Properties = suite.Properties
63
-		recordedSuite.TestCases = suite.TestCases
64
-		recordedSuite.Children = suite.Children
65
-		return nil
66
-	}
67
-
68
-	if err := b.addToParent(suite); err != nil {
69
-		if errors.IsSuiteOutOfBoundsError(err) {
70
-			// if we were trying to add something out of bounds, we ignore the request but do not
71
-			// throw an error so we can selectively build sub-trees with a set of specified roots
72
-			return nil
73
-		}
74
-		return err
75
-	}
42
+type treeNode struct {
43
+	// suite is the test suite in this node
44
+	suite *api.TestSuite
76 45
 
77
-	b.updateMetrics(suite)
46
+	// children are child nodes in the tree. this field will remain empty until the tree is built
47
+	children []*treeNode
78 48
 
79
-	return nil
49
+	// parent is the parent of this node. this field can be null
50
+	parent *treeNode
80 51
 }
81 52
 
82
-// addToParent will find or create the parent for the test suite and add the given suite as a child
83
-func (b *nestedTestSuitesBuilder) addToParent(child *api.TestSuite) error {
84
-	name := child.Name
85
-	if !b.isChildOfRoots(name) && b.restrictedRoots {
86
-		// if we were asked to add a new test suite that isn't a child of any current root,
87
-		// and we aren't allowed to add new roots, we can't fulfill this request
88
-		return errors.NewSuiteOutOfBoundsError(name)
53
+// AddSuite adds a suite, encapsulated in a treeNode, to the list of suites that this builder cares about.
54
+// If a suite already exists with the same name as that which is being added, the existing record is over-
55
+// written. If roots are restricted, then test suites to be added are asssumed to be nested under one of
56
+// the root suites created by the constructor method and the attempted addition of a suite not rooted in
57
+// those suites will fail silently to allow for selective tree-building given a root.
58
+func (b *nestedTestSuitesBuilder) AddSuite(suite *api.TestSuite) {
59
+	if !allowedToCreate(suite.Name, b.restrictedRoots) {
60
+		return
89 61
 	}
90 62
 
91
-	parentName := getParentName(name)
92
-	if len(parentName) == 0 {
93
-		// this suite does not have a parent, we just need to add it as a root
94
-		b.testSuites.Suites = append(b.testSuites.Suites, child)
95
-		return nil
63
+	oldVersion, exists := b.nodes[suite.Name]
64
+	if exists {
65
+		oldVersion.suite = suite
66
+		return
96 67
 	}
97 68
 
98
-	parent := b.findSuite(parentName)
99
-	if parent == nil {
100
-		// no parent is currently registered, we need to create it and add it to the tree
101
-		parent = &api.TestSuite{
102
-			Name:     parentName,
103
-			Children: []*api.TestSuite{child},
104
-		}
105
-
106
-		return b.addToParent(parent)
107
-	}
108
-
109
-	parent.Children = append(parent.Children, child)
110
-	return nil
69
+	b.nodes[suite.Name] = &treeNode{suite: suite}
111 70
 }
112 71
 
113
-// getParentName returns the name of the parent package, if it exists in the multitree
114
-func getParentName(name string) string {
115
-	if !strings.Contains(name, TestSuiteNameDelimiter) {
116
-		return ""
72
+// allowedToCreate determines if the given name is allowed to be created in light of the restricted roots
73
+func allowedToCreate(name string, restrictedRoots []*treeNode) bool {
74
+	if len(restrictedRoots) == 0 {
75
+		return true
117 76
 	}
118 77
 
119
-	delimeterIndex := strings.LastIndex(name, TestSuiteNameDelimiter)
120
-	return name[0:delimeterIndex]
121
-}
122
-
123
-func (b *nestedTestSuitesBuilder) isChildOfRoots(name string) bool {
124
-	for _, rootSuite := range b.testSuites.Suites {
125
-		if strings.HasPrefix(name, rootSuite.Name) {
78
+	for _, root := range restrictedRoots {
79
+		if strings.HasPrefix(name, root.suite.Name) {
126 80
 			return true
127 81
 		}
128 82
 	}
83
+
129 84
 	return false
130 85
 }
131 86
 
132
-// findSuite finds a test suite in a collection of test suites
133
-func (b *nestedTestSuitesBuilder) findSuite(name string) *api.TestSuite {
134
-	return findSuite(b.testSuites.Suites, name)
135
-}
87
+// Build builds an api.TestSuites from the list of nodes that is contained in the builder.
88
+func (b *nestedTestSuitesBuilder) Build() *api.TestSuites {
89
+	// build a tree from our list of nodes
90
+	nodesToAdd := []*treeNode{}
91
+	for _, node := range b.nodes {
92
+		// make a copy of which nodes we're interested in, otherwise we'll be concurrently modifying b.nodes
93
+		nodesToAdd = append(nodesToAdd, node)
94
+	}
136 95
 
137
-// findSuite walks a test suite tree to find a test suite with the given name
138
-func findSuite(suites []*api.TestSuite, name string) *api.TestSuite {
139
-	for _, suite := range suites {
140
-		if suite.Name == name {
141
-			return suite
96
+	for _, node := range nodesToAdd {
97
+		parentNode, exists := b.nodes[getParentName(node.suite.Name)]
98
+		if !exists {
99
+			makeParentsFor(node, b.nodes, b.restrictedRoots)
100
+			continue
142 101
 		}
143 102
 
144
-		if strings.HasPrefix(name, suite.Name) {
145
-			return findSuite(suite.Children, name)
103
+		parentNode.children = append(parentNode.children, node)
104
+		node.parent = parentNode
105
+	}
106
+
107
+	// find the tree's roots
108
+	roots := []*treeNode{}
109
+	for _, node := range b.nodes {
110
+		if node.parent == nil {
111
+			roots = append(roots, node)
146 112
 		}
147 113
 	}
148 114
 
149
-	return nil
150
-}
115
+	// update all metrics inside of test suites so they encompass those of their children
116
+	rootSuites := []*api.TestSuite{}
117
+	for _, root := range roots {
118
+		updateMetrics(root)
119
+		rootSuites = append(rootSuites, root.suite)
120
+	}
121
+
122
+	// we need to sort our children so that we can ensure reproducible output for testing
123
+	sort.Sort(api.ByName(rootSuites))
151 124
 
152
-// updateMetrics updates the metrics for all parents of a test suite
153
-func (b *nestedTestSuitesBuilder) updateMetrics(newSuite *api.TestSuite) {
154
-	updateMetrics(b.testSuites.Suites, newSuite)
125
+	return &api.TestSuites{Suites: rootSuites}
155 126
 }
156 127
 
157
-// updateMetrics walks a test suite tree to update metrics of parents of the given suite
158
-func updateMetrics(suites []*api.TestSuite, newSuite *api.TestSuite) {
159
-	for _, suite := range suites {
160
-		if suite.Name == newSuite.Name || !strings.HasPrefix(newSuite.Name, suite.Name) {
161
-			// if we're considering the suite itself or another suite that is not a pure
162
-			// prefix of the new suite, we are not considering a parent suite and therefore
163
-			// do not need to update any metrics
164
-			continue
165
-		}
128
+// makeParentsFor recursively creates parents for the child node until a parent is created that doesn't
129
+// contain the delimiter in its name or a restricted root is reached.
130
+func makeParentsFor(child *treeNode, register map[string]*treeNode, restrictedRoots []*treeNode) {
131
+	parentName := getParentName(child.suite.Name)
132
+	if parentName == "" {
133
+		// if there is no parent for this child, we give up
134
+		return
135
+	}
166 136
 
167
-		suite.NumTests += newSuite.NumTests
168
-		suite.NumSkipped += newSuite.NumSkipped
169
-		suite.NumFailed += newSuite.NumFailed
170
-		suite.Duration += newSuite.Duration
171
-		// we round to the millisecond on duration
172
-		suite.Duration = float64(int(suite.Duration*1000)) / 1000
137
+	if parentNode, exists := register[parentName]; exists {
138
+		// if the parent we're trying to add already exists, just use it
139
+		parentNode.children = append(parentNode.children, child)
140
+		child.parent = parentNode
141
+		return
142
+	}
143
+
144
+	if !allowedToCreate(parentName, restrictedRoots) {
145
+		// if the parent we're trying to create doesn't exist but we can't make it, give up
146
+		return
147
+	}
173 148
 
174
-		updateMetrics(suite.Children, newSuite)
149
+	parentNode := &treeNode{
150
+		suite:    &api.TestSuite{Name: parentName},
151
+		children: []*treeNode{child},
175 152
 	}
153
+	child.parent = parentNode
154
+	register[parentName] = parentNode
155
+
156
+	makeParentsFor(parentNode, register, restrictedRoots)
176 157
 }
177 158
 
178
-// Build releases the test suites collection being built at whatever current state it is in
179
-func (b *nestedTestSuitesBuilder) Build() *api.TestSuites {
180
-	return b.testSuites
159
+// getParentName returns the name of the parent package, regardless of if it exists in the multitree
160
+func getParentName(name string) string {
161
+	if !strings.Contains(name, TestSuiteNameDelimiter) {
162
+		return ""
163
+	}
164
+
165
+	delimeterIndex := strings.LastIndex(name, TestSuiteNameDelimiter)
166
+	return name[0:delimeterIndex]
167
+}
168
+
169
+// updateMetrics recursively updates all fields in a treeNode's TestSuite
170
+func updateMetrics(root *treeNode) {
171
+	for _, child := range root.children {
172
+		updateMetrics(child)
173
+		// we should be building a tree, so updates on children are independent and we can bring
174
+		// in the updated data for this child right away
175
+		root.suite.NumTests += child.suite.NumTests
176
+		root.suite.NumSkipped += child.suite.NumSkipped
177
+		root.suite.NumFailed += child.suite.NumFailed
178
+		root.suite.Duration += child.suite.Duration
179
+		root.suite.Children = append(root.suite.Children, child.suite)
180
+	}
181
+
182
+	// we need to sort our children so that we can ensure reproducible output for testing
183
+	sort.Sort(api.ByName(root.suite.Children))
181 184
 }
... ...
@@ -41,7 +41,7 @@ func TestAddSuite(t *testing.T) {
41 41
 	var testCases = []struct {
42 42
 		name           string
43 43
 		rootSuiteNames []string
44
-		seedSuites     *api.TestSuites
44
+		seedSuites     map[string]*treeNode
45 45
 		suiteToAdd     *api.TestSuite
46 46
 		expectedSuites *api.TestSuites
47 47
 	}{
... ...
@@ -92,9 +92,9 @@ func TestAddSuite(t *testing.T) {
92 92
 		},
93 93
 		{
94 94
 			name: "populated adding child",
95
-			seedSuites: &api.TestSuites{
96
-				Suites: []*api.TestSuite{
97
-					{
95
+			seedSuites: map[string]*treeNode{
96
+				"root": {
97
+					suite: &api.TestSuite{
98 98
 						Name: "root",
99 99
 					},
100 100
 				},
... ...
@@ -141,9 +141,9 @@ func TestAddSuite(t *testing.T) {
141 141
 		},
142 142
 		{
143 143
 			name: "populated overwriting record",
144
-			seedSuites: &api.TestSuites{
145
-				Suites: []*api.TestSuite{
146
-					{
144
+			seedSuites: map[string]*treeNode{
145
+				"root": {
146
+					suite: &api.TestSuite{
147 147
 						Name:     "root",
148 148
 						NumTests: 3,
149 149
 					},
... ...
@@ -168,12 +168,10 @@ func TestAddSuite(t *testing.T) {
168 168
 		builder := NewTestSuitesBuilder(testCase.rootSuiteNames)
169 169
 
170 170
 		if testCase.seedSuites != nil {
171
-			builder.(*nestedTestSuitesBuilder).testSuites = testCase.seedSuites
171
+			builder.(*nestedTestSuitesBuilder).nodes = testCase.seedSuites
172 172
 		}
173 173
 
174
-		if err := builder.AddSuite(testCase.suiteToAdd); err != nil {
175
-			t.Errorf("%s: unexpected error adding suite to suites: %v", testCase.name, err)
176
-		}
174
+		builder.AddSuite(testCase.suiteToAdd)
177 175
 
178 176
 		if actual, expected := builder.Build(), testCase.expectedSuites; !reflect.DeepEqual(actual, expected) {
179 177
 			t.Errorf("%s: did not get correct test suites after addition of test suite:\n\texpected:\n\t%s,\n\tgot\n\t%s", testCase.name, expected, actual)