Skip to content

Commit d3ae02c

Browse files
committed
add test for layers content
1 parent 643ffeb commit d3ae02c

File tree

3 files changed

+85
-3
lines changed

3 files changed

+85
-3
lines changed

internals/plan/export_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) 2024 Canonical Ltd
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License version 3 as
5+
// published by the Free Software Foundation.
6+
//
7+
// This program is distributed in the hope that it will be useful,
8+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10+
// GNU General Public License for more details.
11+
//
12+
// You should have received a copy of the GNU General Public License
13+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
14+
15+
package plan
16+
17+
var LayerBuiltins = layerBuiltins

internals/plan/plan.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"path/filepath"
2222
"reflect"
2323
"regexp"
24+
"slices"
2425
"strconv"
2526
"strings"
2627
"time"
@@ -70,11 +71,15 @@ const (
7071
// layerExtensions keeps a map of registered extensions.
7172
var layerExtensions = map[string]LayerSectionExtension{}
7273

74+
// layerBuiltins represents all the built-in layer sections. This list is used
75+
// for identifying built-in fields in this package. It is unit tested to match
76+
// the YAML fields exposed in the Layer type, to catch inconsistencies.
77+
var layerBuiltins = []string{"summary", "description", "services", "checks", "log-targets"}
78+
7379
// RegisterExtension adds a plan schema extension. All registrations must be
7480
// done before the plan library is used.
7581
func RegisterExtension(field string, ext LayerSectionExtension) {
76-
switch field {
77-
case "summary", "description", "services", "checks", "log-targets":
82+
if slices.Contains(layerBuiltins, field) {
7883
panic(fmt.Sprintf("internal error: extension %q already used as built-in field", field))
7984
}
8085
if _, ok := layerExtensions[field]; ok {
@@ -1226,6 +1231,11 @@ func ParseLayer(order int, label string, data []byte) (*Layer, error) {
12261231
"checks": &layer.Checks,
12271232
"log-targets": &layer.LogTargets,
12281233
}
1234+
// Make sure builtinSections contains the exact same fields as expected
1235+
// in the Layer type.
1236+
if !mapHasKeys(builtinSections, layerBuiltins) {
1237+
panic("internal error: parsed fields and layer fields differ")
1238+
}
12291239

12301240
layerSections := make(map[string]yaml.Node)
12311241
// Deliberately pre-allocate at least an empty yaml.Node for every
@@ -1245,7 +1255,7 @@ func ParseLayer(order int, label string, data []byte) (*Layer, error) {
12451255
}
12461256

12471257
for field, section := range layerSections {
1248-
if _, builtin := builtinSections[field]; builtin {
1258+
if slices.Contains(layerBuiltins, field) {
12491259
// The following issue prevents us from using the yaml.Node decoder
12501260
// with KnownFields = true behaviour. Once one of the proposals get
12511261
// merged, we can remove the intermediate Marshal step.
@@ -1312,6 +1322,20 @@ func ParseLayer(order int, label string, data []byte) (*Layer, error) {
13121322
return layer, err
13131323
}
13141324

1325+
// mapHasKeys returns true if the key list supplied is an exact match of the
1326+
// keys in the map (ordering is ignored).
1327+
func mapHasKeys[M ~map[K]V, K comparable, V any](inMap M, keyList []K) bool {
1328+
if len(inMap) != len(keyList) {
1329+
return false
1330+
}
1331+
for _, key := range keyList {
1332+
if _, ok := inMap[key]; !ok {
1333+
return false
1334+
}
1335+
}
1336+
return true
1337+
}
1338+
13151339
func validServiceAction(action ServiceAction, additionalValid ...ServiceAction) bool {
13161340
for _, v := range additionalValid {
13171341
if action == v {

internals/plan/plan_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ import (
1919
"fmt"
2020
"os"
2121
"path/filepath"
22+
"reflect"
23+
"slices"
2224
"strings"
2325
"time"
26+
"unicode"
2427

2528
. "gopkg.in/check.v1"
2629
"gopkg.in/yaml.v3"
@@ -2068,3 +2071,41 @@ func (s *S) TestStartStopOrderMultipleLanes(c *C) {
20682071
c.Assert(lanes[1], DeepEquals, []string{"srv2"})
20692072
c.Assert(lanes[2], DeepEquals, []string{"srv3"})
20702073
}
2074+
2075+
// TestLayerBuiltinCompatible ensures layerBuiltins used in the plan package
2076+
// reflects the same YAML fields as exposed in the Layer type.
2077+
func (s *S) TestLayerBuiltinCompatible(c *C) {
2078+
fields := structYamlFields(plan.Layer{})
2079+
c.Assert(len(fields), Equals, len(plan.LayerBuiltins))
2080+
for _, field := range structYamlFields(plan.Layer{}) {
2081+
c.Assert(slices.Contains(plan.LayerBuiltins, field), Equals, true)
2082+
}
2083+
}
2084+
2085+
// structYamlFields extracts the YAML fields from a struct. If the YAML tag
2086+
// is omitted, the field name with the first letter lower case will be used.
2087+
func structYamlFields(inStruct any) []string {
2088+
var fields []string
2089+
inStructType := reflect.TypeOf(inStruct)
2090+
for i := range inStructType.NumField() {
2091+
fieldType := inStructType.Field(i)
2092+
yamlTag := fieldType.Tag.Get("yaml")
2093+
if fieldType.IsExported() && yamlTag != "-" && !strings.Contains(yamlTag, ",inline") {
2094+
tag, _, _ := strings.Cut(fieldType.Tag.Get("yaml"), ",")
2095+
if tag == "" {
2096+
tag = firstLetterToLower(fieldType.Name)
2097+
}
2098+
fields = append(fields, tag)
2099+
}
2100+
}
2101+
return fields
2102+
}
2103+
2104+
func firstLetterToLower(s string) string {
2105+
if len(s) == 0 {
2106+
return s
2107+
}
2108+
r := []rune(s)
2109+
r[0] = unicode.ToLower(r[0])
2110+
return string(r)
2111+
}

0 commit comments

Comments
 (0)