Skip to content

Commit

Permalink
add test for layers content
Browse files Browse the repository at this point in the history
  • Loading branch information
flotter committed Aug 26, 2024
1 parent 643ffeb commit d3ae02c
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 3 deletions.
17 changes: 17 additions & 0 deletions internals/plan/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) 2024 Canonical Ltd
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package plan

var LayerBuiltins = layerBuiltins
30 changes: 27 additions & 3 deletions internals/plan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"path/filepath"
"reflect"
"regexp"
"slices"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -70,11 +71,15 @@ const (
// layerExtensions keeps a map of registered extensions.
var layerExtensions = map[string]LayerSectionExtension{}

// layerBuiltins represents all the built-in layer sections. This list is used
// for identifying built-in fields in this package. It is unit tested to match
// the YAML fields exposed in the Layer type, to catch inconsistencies.
var layerBuiltins = []string{"summary", "description", "services", "checks", "log-targets"}

// RegisterExtension adds a plan schema extension. All registrations must be
// done before the plan library is used.
func RegisterExtension(field string, ext LayerSectionExtension) {
switch field {
case "summary", "description", "services", "checks", "log-targets":
if slices.Contains(layerBuiltins, field) {
panic(fmt.Sprintf("internal error: extension %q already used as built-in field", field))
}
if _, ok := layerExtensions[field]; ok {
Expand Down Expand Up @@ -1226,6 +1231,11 @@ func ParseLayer(order int, label string, data []byte) (*Layer, error) {
"checks": &layer.Checks,
"log-targets": &layer.LogTargets,
}
// Make sure builtinSections contains the exact same fields as expected
// in the Layer type.
if !mapHasKeys(builtinSections, layerBuiltins) {
panic("internal error: parsed fields and layer fields differ")
}

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

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

// mapHasKeys returns true if the key list supplied is an exact match of the
// keys in the map (ordering is ignored).
func mapHasKeys[M ~map[K]V, K comparable, V any](inMap M, keyList []K) bool {
if len(inMap) != len(keyList) {
return false
}
for _, key := range keyList {
if _, ok := inMap[key]; !ok {
return false
}
}
return true
}

func validServiceAction(action ServiceAction, additionalValid ...ServiceAction) bool {
for _, v := range additionalValid {
if action == v {
Expand Down
41 changes: 41 additions & 0 deletions internals/plan/plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"time"
"unicode"

. "gopkg.in/check.v1"
"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -2068,3 +2071,41 @@ func (s *S) TestStartStopOrderMultipleLanes(c *C) {
c.Assert(lanes[1], DeepEquals, []string{"srv2"})
c.Assert(lanes[2], DeepEquals, []string{"srv3"})
}

// TestLayerBuiltinCompatible ensures layerBuiltins used in the plan package
// reflects the same YAML fields as exposed in the Layer type.
func (s *S) TestLayerBuiltinCompatible(c *C) {
fields := structYamlFields(plan.Layer{})
c.Assert(len(fields), Equals, len(plan.LayerBuiltins))
for _, field := range structYamlFields(plan.Layer{}) {
c.Assert(slices.Contains(plan.LayerBuiltins, field), Equals, true)
}
}

// structYamlFields extracts the YAML fields from a struct. If the YAML tag
// is omitted, the field name with the first letter lower case will be used.
func structYamlFields(inStruct any) []string {
var fields []string
inStructType := reflect.TypeOf(inStruct)
for i := range inStructType.NumField() {
fieldType := inStructType.Field(i)
yamlTag := fieldType.Tag.Get("yaml")
if fieldType.IsExported() && yamlTag != "-" && !strings.Contains(yamlTag, ",inline") {
tag, _, _ := strings.Cut(fieldType.Tag.Get("yaml"), ",")
if tag == "" {
tag = firstLetterToLower(fieldType.Name)
}
fields = append(fields, tag)
}
}
return fields
}

func firstLetterToLower(s string) string {
if len(s) == 0 {
return s
}
r := []rune(s)
r[0] = unicode.ToLower(r[0])
return string(r)
}

0 comments on commit d3ae02c

Please sign in to comment.