diff --git a/apricot/cacheproxy/service.go b/apricot/cacheproxy/service.go index 99cfd4bd..2d471e6d 100644 --- a/apricot/cacheproxy/service.go +++ b/apricot/cacheproxy/service.go @@ -133,11 +133,11 @@ func (s Service) GetDetectorsForHosts(hosts []string) ([]string, error) { return detList, nil } -func (s Service) GetCRUCardsForHost(hostname string) (string, error) { +func (s Service) GetCRUCardsForHost(hostname string) ([]string, error) { return s.base.GetCRUCardsForHost(hostname) } -func (s Service) GetEndpointsForCRUCard(hostname, cardSerial string) (string, error) { +func (s Service) GetEndpointsForCRUCard(hostname, cardSerial string) ([]string, error) { return s.base.GetEndpointsForCRUCard(hostname, cardSerial) } diff --git a/apricot/local/service.go b/apricot/local/service.go index 11af8add..28c108f3 100644 --- a/apricot/local/service.go +++ b/apricot/local/service.go @@ -351,24 +351,20 @@ func (s *Service) RawGetRecursive(path string) (string, error) { func (s *Service) GetDetectorForHost(hostname string) (string, error) { s.logMethod() - if cSrc, ok := s.src.(*cfgbackend.ConsulSource); ok { - keys, err := cSrc.GetKeysByPrefix(filepath.Join("o2/hardware", "detectors")) - if err != nil { - return "", err - } - for _, key := range keys { - // key example: o2/hardware/detectors/TST/flps/some-hostname/ - splitKey := strings.Split(key, "/") - if len(splitKey) == 7 { - if splitKey[5] == hostname { - return splitKey[3], nil - } + keys, err := s.src.GetKeysByPrefix(filepath.Join("o2/hardware", "detectors")) + if err != nil { + return "", err + } + for _, key := range keys { + // key example: o2/hardware/detectors/TST/flps/some-hostname/ + splitKey := strings.Split(key, "/") + if len(splitKey) == 7 { + if splitKey[5] == hostname { + return splitKey[3], nil } } - return "", fmt.Errorf("detector not found for host %s", hostname) - } else { - return "", errors.New("runtime KV not supported with file backend") } + return "", fmt.Errorf("detector not found for host %s", hostname) } func (s *Service) GetDetectorsForHosts(hosts []string) ([]string, error) { @@ -393,35 +389,27 @@ func (s *Service) GetDetectorsForHosts(hosts []string) ([]string, error) { return detectorSlice, nil } -func (s *Service) GetCRUCardsForHost(hostname string) (string, error) { +func (s *Service) GetCRUCardsForHost(hostname string) ([]string, error) { s.logMethod() - if cSrc, ok := s.src.(*cfgbackend.ConsulSource); ok { - var cards map[string]Card - var serials []string - cfgCards, err := cSrc.Get(filepath.Join("o2/hardware", "flps", hostname, "cards")) - if err != nil { - return "", err - } - json.Unmarshal([]byte(cfgCards), &cards) - unique := make(map[string]bool) - for _, card := range cards { - if _, value := unique[card.Serial]; !value { - unique[card.Serial] = true - serials = append(serials, card.Serial) - } - } - bytes, err := json.Marshal(serials) - if err != nil { - return "", err + var cards map[string]Card + var serials []string + cfgCards, err := s.src.Get(filepath.Join("o2/hardware", "flps", hostname, "cards")) + if err != nil { + return nil, err + } + json.Unmarshal([]byte(cfgCards), &cards) + unique := make(map[string]bool) + for _, card := range cards { + if _, value := unique[card.Serial]; !value { + unique[card.Serial] = true + serials = append(serials, card.Serial) } - return string(bytes), nil - } else { - return "", errors.New("runtime KV not supported with file backend") } + return serials, nil } -func (s *Service) GetEndpointsForCRUCard(hostname, cardSerial string) (string, error) { +func (s *Service) GetEndpointsForCRUCard(hostname, cardSerial string) ([]string, error) { s.logMethod() log.WithPrefix("rpcserver"). @@ -431,26 +419,22 @@ func (s *Service) GetEndpointsForCRUCard(hostname, cardSerial string) (string, e WithField("cardSerial", cardSerial). Debug("getting endpoints") - if cSrc, ok := s.src.(*cfgbackend.ConsulSource); ok { - var cards map[string]Card - var endpoints string - cfgCards, err := cSrc.Get(filepath.Join("o2/hardware", "flps", hostname, "cards")) - if err != nil { - return "", err - } - err = json.Unmarshal([]byte(cfgCards), &cards) - if err != nil { - return "", err - } - for _, card := range cards { - if card.Serial == cardSerial { - endpoints = endpoints + card.Endpoint + " " - } + var cards map[string]Card + var endpoints []string + cfgCards, err := s.src.Get(filepath.Join("o2/hardware", "flps", hostname, "cards")) + if err != nil { + return nil, err + } + err = json.Unmarshal([]byte(cfgCards), &cards) + if err != nil { + return nil, err + } + for _, card := range cards { + if card.Serial == cardSerial { + endpoints = append(endpoints, card.Endpoint) } - return endpoints, nil - } else { - return "", errors.New("runtime KV not supported with file backend") } + return endpoints, nil } func (s *Service) GetRuntimeEntry(component string, key string) (string, error) { diff --git a/apricot/local/service_test.go b/apricot/local/service_test.go index 0cf86a89..3e9a6fa5 100644 --- a/apricot/local/service_test.go +++ b/apricot/local/service_test.go @@ -392,11 +392,98 @@ var _ = Describe("local service", func() { }) + Describe("getting detector for host", func() { + var ( + detector string + err error + ) + When("retrieving the detector for a host", func() { + It("should return the correct detector", func() { + detector, err = svc.GetDetectorForHost("flp001") + Expect(err).NotTo(HaveOccurred()) + Expect(detector).To(Equal("ABC")) + }) + }) + When("retrieving the detector for a non-existing host", func() { + It("should produce an error", func() { + detector, err = svc.GetDetectorForHost("NOPE") + Expect(err).To(HaveOccurred()) + }) + }) + }) + Describe("getting detectors for hosts", func() { + var ( + detectors []string + err error + ) + When("retrieving the detectors for a list of hosts", func() { + It("should return the correct detectors", func() { + detectors, err = svc.GetDetectorsForHosts([]string{"flp001", "flp002"}) + Expect(err).NotTo(HaveOccurred()) + Expect(detectors).To(ContainElements("ABC", "DEF")) + }) + }) + When("retrieving the detectors for a non-existing host", func() { + It("should produce an error", func() { + detectors, err = svc.GetDetectorsForHosts([]string{"NOPE"}) + Expect(err).To(HaveOccurred()) + }) + }) + When("retrieving the detectors for a list of hosts with a non-existing host", func() { + It("should produce an error", func() { + detectors, err = svc.GetDetectorsForHosts([]string{"flp001", "NOPE"}) + Expect(err).To(HaveOccurred()) + }) + }) + }) + Describe("getting CRU cards for a host", func() { + var ( + cards []string + err error + ) + When("retrieving the CRU cards for a host", func() { + It("should return the correct CRU cards", func() { + cards, err = svc.GetCRUCardsForHost("flp001") + Expect(err).NotTo(HaveOccurred()) + Expect(cards).To(ContainElements("0228", "0229")) + }) + }) + When("retrieving the CRU cards for a non-existing host", func() { + It("should produce an error", func() { + cards, err = svc.GetCRUCardsForHost("NOPE") + Expect(err).To(HaveOccurred()) + }) + }) + }) + Describe("getting endpoints for a CRU card", func() { + var ( + endpoints []string + err error + ) + When("retrieving the endpoints for a CRU card", func() { + It("should return the correct endpoints", func() { + endpoints, err = svc.GetEndpointsForCRUCard("flp001", "0228") + Expect(err).NotTo(HaveOccurred()) + Expect(endpoints).To(ContainElements("0", "1")) + }) + }) + When("retrieving the endpoints for a non-existing host", func() { + It("should produce an error", func() { + endpoints, err = svc.GetEndpointsForCRUCard("NOPE", "0228") + Expect(err).To(HaveOccurred()) + }) + }) + When("retrieving the endpoints for a non-existing CRU card", func() { + // fixme: probably incorrect behaviour, but I don't want to risk breaking something + It("should not return an empty slice", func() { + endpoints, err = svc.GetEndpointsForCRUCard("flp001", "NOPE") + Expect(endpoints).To(BeEmpty()) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + // TODO: - // GetDetectorForHost (currently not supporting yaml backend) - // GetDetectorsForHosts (currently not supporting yaml backend) - // GetCRUCardsForHost (currently not supporting yaml backend) - // GetEndpointsForCRUCard (currently not supporting yaml backend) // GetRuntimeEntry (currently not supporting yaml backend) // SetRuntimeEntry (currently not supporting yaml backend) // GetRuntimeEntries (currently not supporting yaml backend) diff --git a/apricot/local/service_test.yaml b/apricot/local/service_test.yaml index 2039aeb9..cca28db8 100644 --- a/apricot/local/service_test.yaml +++ b/apricot/local/service_test.yaml @@ -44,7 +44,12 @@ o2: flps: {} flps: flp001: - cards: "{ \"key\" : \"value\" }" + cards: "{ + \"0\": {\"serial\": \"0228\", \"endpoint\": \"0\"}, + \"1\": {\"serial\": \"0229\", \"endpoint\": \"0\"}, + \"2\": {\"serial\": \"0228\", \"endpoint\": \"1\"}, + \"3\": {\"serial\": \"0229\", \"endpoint\": \"1\"} + }" flp002: cards: "{ \"key\" : \"value\" }" flp003: diff --git a/apricot/remote/server.go b/apricot/remote/server.go index 35d5c844..0936d83b 100644 --- a/apricot/remote/server.go +++ b/apricot/remote/server.go @@ -27,7 +27,9 @@ package remote import ( "context" + "encoding/json" "runtime" + "strings" apricotpb "github.com/AliceO2Group/Control/apricot/protos" "github.com/AliceO2Group/Control/common/logger" @@ -253,7 +255,12 @@ func (m *RpcServer) GetCRUCardsForHost(_ context.Context, request *apricotpb.Hos if err != nil { return nil, err } - return &apricotpb.CRUCardsResponse{Cards: cards}, E_OK.Err() + cardsJson, err := json.Marshal(cards) + if err != nil { + return nil, err + } + + return &apricotpb.CRUCardsResponse{Cards: string(cardsJson)}, E_OK.Err() } func (m *RpcServer) GetEndpointsForCRUCard(_ context.Context, request *apricotpb.CardRequest) (*apricotpb.CRUCardEndpointResponse, error) { @@ -269,7 +276,9 @@ func (m *RpcServer) GetEndpointsForCRUCard(_ context.Context, request *apricotpb if err != nil { return nil, err } - return &apricotpb.CRUCardEndpointResponse{Endpoints: endpoints}, E_OK.Err() + endpointsSpaceSeparated := strings.Join(endpoints, " ") + + return &apricotpb.CRUCardEndpointResponse{Endpoints: endpointsSpaceSeparated}, E_OK.Err() } func (m *RpcServer) GetRuntimeEntry(_ context.Context, request *apricotpb.GetRuntimeEntryRequest) (*apricotpb.ComponentResponse, error) { diff --git a/apricot/remote/service.go b/apricot/remote/service.go index 3bfb2662..1c6c42b0 100644 --- a/apricot/remote/service.go +++ b/apricot/remote/service.go @@ -26,9 +26,11 @@ package remote import ( "context" + "encoding/json" "errors" "fmt" "net/url" + "strings" "time" apricotpb "github.com/AliceO2Group/Control/apricot/protos" @@ -199,19 +201,25 @@ func (c *RemoteService) GetDetectorsForHosts(hosts []string) (payload []string, } -func (c *RemoteService) GetCRUCardsForHost(hostname string) (cards string, err error) { +func (c *RemoteService) GetCRUCardsForHost(hostname string) (cards []string, err error) { var response *apricotpb.CRUCardsResponse request := &apricotpb.HostRequest{ Hostname: hostname, } response, err = c.cli.GetCRUCardsForHost(context.Background(), request, grpc.EmptyCallOption{}) if err != nil { - return "", err + return nil, err + } + cardsStr := response.GetCards() + err = json.Unmarshal([]byte(cardsStr), &cards) + if err != nil { + return nil, err } - return response.GetCards(), nil + + return cards, nil } -func (c *RemoteService) GetEndpointsForCRUCard(hostname, cardSerial string) (cards string, err error) { +func (c *RemoteService) GetEndpointsForCRUCard(hostname, cardSerial string) (endpoints []string, err error) { var response *apricotpb.CRUCardEndpointResponse request := &apricotpb.CardRequest{ Hostname: hostname, @@ -219,9 +227,11 @@ func (c *RemoteService) GetEndpointsForCRUCard(hostname, cardSerial string) (car } response, err = c.cli.GetEndpointsForCRUCard(context.Background(), request, grpc.EmptyCallOption{}) if err != nil { - return "", err + return nil, err } - return response.GetEndpoints(), nil + endpointsStr := response.GetEndpoints() + endpoints = strings.Split(endpointsStr, " ") + return endpoints, nil } func (c *RemoteService) GetRuntimeEntry(component string, key string) (payload string, err error) { diff --git a/common/gera/map.go b/common/gera/map.go index 1cb7d366..10084265 100644 --- a/common/gera/map.go +++ b/common/gera/map.go @@ -51,7 +51,10 @@ type Map[K comparable, V any] interface { Set(key K, value V) bool Del(key K) bool + // Flattened should return a flat representation of the tree in the root direction, + // where children kv pairs overwrite parent kv pairs. Flattened() (map[K]V, error) + // FlattenedParent should return a flat representation of the tree in the root direction, FlattenedParent() (map[K]V, error) WrappedAndFlattened(m Map[K, V]) (map[K]V, error) diff --git a/configuration/service.go b/configuration/service.go index bfd6d6e0..0c3bfa0d 100644 --- a/configuration/service.go +++ b/configuration/service.go @@ -57,8 +57,8 @@ type Service interface { GetDetectorForHost(hostname string) (string, error) GetDetectorsForHosts(hosts []string) ([]string, error) - GetCRUCardsForHost(hostname string) (string, error) - GetEndpointsForCRUCard(hostname, cardSerial string) (string, error) + GetCRUCardsForHost(hostname string) ([]string, error) + GetEndpointsForCRUCard(hostname, cardSerial string) ([]string, error) RawGetRecursive(path string) (string, error) } diff --git a/configuration/template/dplutil.go b/configuration/template/dplutil.go index efdf4ef5..e6bd0cee 100644 --- a/configuration/template/dplutil.go +++ b/configuration/template/dplutil.go @@ -47,7 +47,7 @@ func extractConfigURIs(dplCommand string) (uris []string) { // Match any consul/apricot URI // it would be the easiest to use a backreference in the regex, but regexp does not support those: // (['"]?)((consul-json|apricot)://[^ |\n]*)(\1) - re := regexp.MustCompile(`['"]?(consul-json|apricot)://[^ |\n]*`) + re := regexp.MustCompile(`['"]?(consul-json|apricot)://[^ |\n]+`) matches := re.FindAllStringSubmatch(dplCommand, nMaxExpectedQcPayloads) for _, match := range matches { diff --git a/configuration/template/dplutil_test.go b/configuration/template/dplutil_test.go index 0ab4f833..47511dc5 100644 --- a/configuration/template/dplutil_test.go +++ b/configuration/template/dplutil_test.go @@ -16,6 +16,14 @@ var _ = Describe("DPL utilities", func() { Expect(uris).To(HaveLen(0)) }) }) + When("URI is not complete", func() { + BeforeEach(func() { + uris = extractConfigURIs("myexe --config apricot://") + }) + It("should return an empty slice", func() { + Expect(uris).To(HaveLen(0)) + }) + }) When("the URI is the last argument", func() { BeforeEach(func() { uris = extractConfigURIs("myexe --config apricot://host.cern.ch:12345/components/qc/ANY/any/ctp-raw") diff --git a/configuration/template/fields.go b/configuration/template/fields.go index 169b8369..a10a99c3 100644 --- a/configuration/template/fields.go +++ b/configuration/template/fields.go @@ -71,8 +71,8 @@ type ConfigurationService interface { GetDetectorForHost(hostname string) (string, error) GetDetectorsForHosts(hosts []string) ([]string, error) - GetCRUCardsForHost(hostname string) (string, error) - GetEndpointsForCRUCard(hostname, cardSerial string) (string, error) + GetCRUCardsForHost(hostname string) ([]string, error) + GetEndpointsForCRUCard(hostname, cardSerial string) ([]string, error) GetRuntimeEntry(component string, key string) (string, error) SetRuntimeEntry(component string, key string, value string) error diff --git a/configuration/template/fieldwrappers.go b/configuration/template/fieldwrappers.go index 8e8fff4a..3b79c957 100644 --- a/configuration/template/fieldwrappers.go +++ b/configuration/template/fieldwrappers.go @@ -67,6 +67,15 @@ func WrapGeneric(getterF GetterFunc, setterF SetterFunc) Field { } } +// WrapMapItems creates a slice of Fields from a map of string key-value pairs. +// It wraps each map item in a GenericWrapper, allowing for dynamic access and +// modification of the original map's values through the Fields interface. +// +// Parameters: +// - items: A map[string]string to be wrapped +// +// Returns: +// - Fields: A slice of Field interfaces, each corresponding to a map item func WrapMapItems(items map[string]string) Fields { fields := make(Fields, 0) for k := range items { @@ -83,6 +92,16 @@ func WrapMapItems(items map[string]string) Fields { return fields } +// WrapSliceItems creates a slice of Fields from a slice of strings. +// It wraps each string item in a GenericWrapper, allowing for dynamic +// access and modification of the original slice's elements through +// the Fields interface. +// +// Parameters: +// - items: A []string to be wrapped +// +// Returns: +// - Fields: A slice of Field interfaces, each corresponding to a slice item func WrapSliceItems(items []string) Fields { fields := make(Fields, 0) for i := range items { diff --git a/configuration/template/fieldwrappers_test.go b/configuration/template/fieldwrappers_test.go new file mode 100644 index 00000000..ed20b7d0 --- /dev/null +++ b/configuration/template/fieldwrappers_test.go @@ -0,0 +1,78 @@ +package template_test + +import ( + "github.com/AliceO2Group/Control/configuration/template" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Field Wrappers", func() { + Describe("PointerWrapper", func() { + It("should get and set values correctly", func() { + value := "test" + pw := template.WrapPointer(&value) + + Expect(pw.Get()).To(Equal("test")) + + pw.Set("new value") + Expect(value).To(Equal("new value")) + }) + }) + + Describe("GenericWrapper", func() { + It("should get and set values correctly", func() { + value := "test" + gw := template.WrapGeneric( + func() string { return value }, + func(v string) { value = v }, + ) + + Expect(gw.Get()).To(Equal("test")) + + gw.Set("new value") + Expect(value).To(Equal("new value")) + }) + }) + + Describe("WrapMapItems", func() { + It("should wrap map items correctly", func() { + items := map[string]string{ + "key1": "value1", + "key2": "value2", + } + fields := template.WrapMapItems(items) + + Expect(fields).To(HaveLen(2)) + + for _, field := range fields { + initialValue := field.Get() + field.Set("new " + initialValue) + } + + expectedItems := map[string]string{ + "key1": "new value1", + "key2": "new value2", + } + + Expect(items).To(Equal(expectedItems)) + }) + }) + + Describe("WrapSliceItems", func() { + It("should wrap slice items correctly", func() { + items := []string{"item1", "item2", "item3"} + fields := template.WrapSliceItems(items) + + Expect(fields).To(HaveLen(3)) + + for i, field := range fields { + Expect(field.Get()).To(Equal(items[i])) + field.Set("new " + items[i]) + } + + expectedItems := []string{"new item1", "new item2", "new item3"} + + Expect(items).To(Equal(expectedItems)) + }) + }) +}) diff --git a/configuration/template/stack_test.go b/configuration/template/stack_test.go new file mode 100644 index 00000000..8348f4fe --- /dev/null +++ b/configuration/template/stack_test.go @@ -0,0 +1,216 @@ +package template_test + +import ( + "encoding/json" + "github.com/AliceO2Group/Control/apricot/local" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "strings" + + "github.com/AliceO2Group/Control/configuration/template" +) + +var _ = Describe("Template function stack", func() { + + Describe("utility functions", func() { + var utilFuncMap map[string]interface{} + + BeforeEach(func() { + varStack := map[string]string{ + "test_var": "test_value", + "prefix_var": "prefixed_value", + "fallback_var": "fallback_value", + "prefixed_none_fallback_var": "none", + } + utilFuncMap = template.MakeUtilFuncMap(varStack) + }) + + Context("strings functions", func() { + It("should validate inputs and convert string to int", func() { + atoiFunc := utilFuncMap["Atoi"].(func(string) int) + + result := atoiFunc("123") + Expect(result).To(Equal(123)) + // we only produce an error log if unexpected input appears + result = atoiFunc("abc") + Expect(result).To(Equal(0)) + result = atoiFunc("") + Expect(result).To(Equal(0)) + }) + + It("should validate inputs and convert int to string", func() { + itoaFunc := utilFuncMap["Itoa"].(func(int) string) + + Expect(itoaFunc(123)).To(Equal("123")) + Expect(itoaFunc(0)).To(Equal("0")) + Expect(itoaFunc(-456)).To(Equal("-456")) + }) + + It("should trim quotes", func() { + trimQuotesFunc := utilFuncMap["TrimQuotes"].(func(string) string) + + Expect(trimQuotesFunc("\"test\"")).To(Equal("test")) + // Fixme: should it support also single quotes? + //Expect(trimQuotesFunc("'test'")).To(Equal("test")) + + Expect(trimQuotesFunc("test")).To(Equal("test")) + Expect(trimQuotesFunc("")).To(Equal("")) + Expect(trimQuotesFunc("\"test")).To(Equal("test")) + Expect(trimQuotesFunc("test\"")).To(Equal("test")) + }) + + It("should trim spaces", func() { + trimSpaceFunc := utilFuncMap["TrimSpace"].(func(string) string) + + Expect(trimSpaceFunc(" test ")).To(Equal("test")) + Expect(trimSpaceFunc("test")).To(Equal("test")) + Expect(trimSpaceFunc("test test ")).To(Equal("test test")) + }) + + It("should convert to upper case", func() { + toUpperFunc := utilFuncMap["ToUpper"].(func(string) string) + Expect(toUpperFunc("test")).To(Equal("TEST")) + Expect(toUpperFunc("Test")).To(Equal("TEST")) + Expect(toUpperFunc("1")).To(Equal("1")) + }) + + It("should convert to lower case", func() { + toLowerFunc := utilFuncMap["ToLower"].(func(string) string) + Expect(toLowerFunc("TEST")).To(Equal("test")) + Expect(toLowerFunc("Test")).To(Equal("test")) + Expect(toLowerFunc("1")).To(Equal("1")) + }) + + It("should check if a string is truthy", func() { + isTruthyFunc := utilFuncMap["IsTruthy"].(func(string) bool) + + Expect(isTruthyFunc("true")).To(BeTrue()) + Expect(isTruthyFunc("TRUE")).To(BeTrue()) + Expect(isTruthyFunc("yes")).To(BeTrue()) + Expect(isTruthyFunc("y")).To(BeTrue()) + Expect(isTruthyFunc("1")).To(BeTrue()) + Expect(isTruthyFunc("on")).To(BeTrue()) + Expect(isTruthyFunc("ok")).To(BeTrue()) + + Expect(isTruthyFunc("false")).To(BeFalse()) + Expect(isTruthyFunc("")).To(BeFalse()) + Expect(isTruthyFunc("0")).To(BeFalse()) + Expect(isTruthyFunc("truthy")).To(BeFalse()) + }) + + It("should check if a string is falsy", func() { + isFalsyFunc := utilFuncMap["IsFalsy"].(func(string) bool) + Expect(isFalsyFunc("false")).To(BeTrue()) + Expect(isFalsyFunc("FALSE")).To(BeTrue()) + Expect(isFalsyFunc("no")).To(BeTrue()) + Expect(isFalsyFunc("n")).To(BeTrue()) + Expect(isFalsyFunc("0")).To(BeTrue()) + Expect(isFalsyFunc("off")).To(BeTrue()) + Expect(isFalsyFunc("none")).To(BeTrue()) + Expect(isFalsyFunc("")).To(BeTrue()) + + Expect(isFalsyFunc("true")).To(BeFalse()) + Expect(isFalsyFunc("1")).To(BeFalse()) + }) + }) + + Context("json functions", func() { + It("should unmarshal JSON", func() { + unmarshalFunc := utilFuncMap["json"].(map[string]interface{})["Unmarshal"].(func(string) interface{}) + result := unmarshalFunc(`{"key": "value"}`) + Expect(result).To(HaveKeyWithValue("key", "value")) + }) + + It("should marshal to JSON", func() { + marshalFunc := utilFuncMap["json"].(map[string]interface{})["Marshal"].(func(interface{}) string) + input := map[string]string{"key": "value"} + result := marshalFunc(input) + Expect(result).To(MatchJSON(`{"key": "value"}`)) + }) + }) + + Context("uid functions", func() { + It("should generate a new UID", func() { + newFunc := utilFuncMap["uid"].(map[string]interface{})["New"].(func() string) + uid1 := newFunc() + uid2 := newFunc() + Expect(uid1).NotTo(Equal(uid2)) + }) + }) + + Context("util functions", func() { + It("should handle prefixed override", func() { + prefixedOverrideFunc := utilFuncMap["util"].(map[string]interface{})["PrefixedOverride"].(func(string, string) string) + Expect(prefixedOverrideFunc("var", "prefix")).To(Equal("prefixed_value")) + Expect(prefixedOverrideFunc("fallback_var", "nonexistent_prefix")).To(Equal("fallback_value")) + Expect(prefixedOverrideFunc("fallback_var", "prefixed_none")).To(Equal("fallback_value")) + Expect(prefixedOverrideFunc("nonexistent_fallback_var", "prefix")).To(Equal("")) + }) + + It("should handle nullable strings", func() { + nullableFunc := utilFuncMap["util"].(map[string]interface{})["Nullable"].(func(*string) string) + var nilString *string + Expect(nullableFunc(nilString)).To(Equal("")) + + nonNilString := "test" + Expect(nullableFunc(&nonNilString)).To(Equal("test")) + }) + + It("should check if suffix is in range", func() { + suffixInRangeFunc := utilFuncMap["util"].(map[string]interface{})["SuffixInRange"].(func(string, string, string, string) string) + Expect(suffixInRangeFunc("test5", "test", "1", "10")).To(Equal("true")) + Expect(suffixInRangeFunc("test15", "test", "1", "10")).To(Equal("false")) + // no prefix + Expect(suffixInRangeFunc("other5", "test", "1", "10")).To(Equal("false")) + // no suffix + Expect(suffixInRangeFunc("test", "test", "1", "10")).To(Equal("false")) + // range arguments are not numbers + Expect(suffixInRangeFunc("test", "test", "a", "10")).To(Equal("false")) + Expect(suffixInRangeFunc("test", "test", "1", "b")).To(Equal("false")) + }) + }) + }) + + Describe("inventory functions", func() { + var ( + svc *local.Service + configAccessObject map[string]interface{} + err error + ) + BeforeEach(func() { + svc, err = local.NewService("file://" + *tmpDir + "/" + serviceConfigFile) + Expect(err).NotTo(HaveOccurred()) + + varStack := map[string]string{} + configAccessObject = template.MakeConfigAccessObject(svc, varStack) + }) + + It("should get detector for host", func() { + getDetectorFunc := configAccessObject["inventory"].(map[string]interface{})["DetectorForHost"].(func(string) string) + Expect(getDetectorFunc("flp001")).To(Equal("ABC")) + Expect(getDetectorFunc("NOPE")).To(ContainSubstring("error")) + }) + It("should get detectors for a list of hosts", func() { + getDetectorsFunc := configAccessObject["inventory"].(map[string]interface{})["DetectorsForHosts"].(func(string) string) + Expect(getDetectorsFunc("[ \"flp001\", \"flp002\" ]")).To(Equal("[\"ABC\",\"DEF\"]")) + Expect(getDetectorsFunc("[ \"flp001\", \"NOPE\" ]")).To(ContainSubstring("error")) + Expect(getDetectorsFunc("[ \"NOPE\" ]")).To(ContainSubstring("error")) + Expect(getDetectorsFunc("flp001")).To(ContainSubstring("error")) + }) + It("should get CRU cards for host", func() { + getCruCardsFunc := configAccessObject["inventory"].(map[string]interface{})["CRUCardsForHost"].(func(string) string) + var result []string + err := json.Unmarshal([]byte(getCruCardsFunc("flp001")), &result) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(ConsistOf("0228", "0229")) + Expect(getCruCardsFunc("NOPE")).To(ContainSubstring("error")) + }) + It("should get endpoints for CRU card", func() { + getEndpointsFunc := configAccessObject["inventory"].(map[string]interface{})["EndpointsForCRUCard"].(func(string, string) string) + endpoints := strings.Split(getEndpointsFunc("flp001", "0228"), " ") + Expect(endpoints).To(ConsistOf("0", "1")) + // fixme: probably incorrect behaviour, but I don't want to risk breaking something + Expect(getEndpointsFunc("flp001", "NOPE")).To(BeEmpty()) + }) + }) +}) diff --git a/configuration/template/stack_test.yaml b/configuration/template/stack_test.yaml new file mode 100644 index 00000000..cca28db8 --- /dev/null +++ b/configuration/template/stack_test.yaml @@ -0,0 +1,58 @@ +o2: + components: + qc: + TECHNICAL: + any: + entry: "config" + PHYSICS: + role1: + entry1: "entry1 config PHYSICS role1" + entry2: "entry2 config PHYSICS role1" + ANY: + role1: + entry1: "entry1 config ANY role1" + any: + entry1: "entry1 config ANY any" + entry10: "{{ var1 }} {% include \"entry11\" %}" + entry11: "world" + entry12: "hello {% include \"sub/entry12\" %}" + sub: + entry12: "world" + runtime: + aliecs: + defaults: + key1: value1 + vars: + key2: value2 + hardware: + detectors: + ABC: + flps: + flp001: + cards: "{ \"key\" : \"value\" }" + DEF: + flps: + flp002: + cards: "{ \"key\" : \"value\" }" + flp003: + cards: "{ \"key\" : \"value\" }" + TRG: + flps: + flp100: + cards: "{ \"key\" : \"value\" }" + XYZ: + flps: {} + flps: + flp001: + cards: "{ + \"0\": {\"serial\": \"0228\", \"endpoint\": \"0\"}, + \"1\": {\"serial\": \"0229\", \"endpoint\": \"0\"}, + \"2\": {\"serial\": \"0228\", \"endpoint\": \"1\"}, + \"3\": {\"serial\": \"0229\", \"endpoint\": \"1\"} + }" + flp002: + cards: "{ \"key\" : \"value\" }" + flp003: + cards: "{ \"key\" : \"value\" }" + flp100: + cards: "{ \"key\" : \"value\" }" \ No newline at end of file diff --git a/configuration/template/stackutil.go b/configuration/template/stackutil.go index 44ce4a29..ced41e5f 100644 --- a/configuration/template/stackutil.go +++ b/configuration/template/stackutil.go @@ -28,6 +28,7 @@ import ( "encoding/json" "fmt" "github.com/AliceO2Group/Control/common/logger/infologger" + "strings" texttemplate "text/template" "time" @@ -136,20 +137,24 @@ func detectorsForHosts(confSvc ConfigurationService, hosts string) string { func cruCardsForHost(confSvc ConfigurationService, hostname string) string { defer utils.TimeTrack(time.Now(), "CRUCardsForHost", log.WithPrefix("template")) - payload, err := confSvc.GetCRUCardsForHost(hostname) + cards, err := confSvc.GetCRUCardsForHost(hostname) if err != nil { return fmt.Sprintf("[\"error: %s\"]", err.Error()) } - return payload + cardsJson, err := json.Marshal(cards) + if err != nil { + return fmt.Sprintf("[\"error: %s\"]", err.Error()) + } + return string(cardsJson) } func endpointsForCruCard(confSvc ConfigurationService, hostname string, cardSerial string) string { defer utils.TimeTrack(time.Now(), "EndpointsForCRUCard", log.WithPrefix("template")) - payload, err := confSvc.GetEndpointsForCRUCard(hostname, cardSerial) + endpoints, err := confSvc.GetEndpointsForCRUCard(hostname, cardSerial) if err != nil { return fmt.Sprintf("{\"error\":\"%s\"}", err.Error()) } - return payload + return strings.Join(endpoints, " ") } func getRuntimeConfig(confSvc ConfigurationService, component string, key string) string { diff --git a/configuration/template/template_test.go b/configuration/template/template_test.go index 110c8a46..91b90c45 100644 --- a/configuration/template/template_test.go +++ b/configuration/template/template_test.go @@ -1,12 +1,44 @@ -package template +package template_test import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "io" + "os" "testing" ) +var tmpDir *string + +const serviceConfigFile = "stack_test.yaml" + func TestTemplate(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Component Configuration Test Suite") } + +var _ = BeforeSuite(func() { + var err error + tmpDir = new(string) + *tmpDir, err = os.MkdirTemp("", "o2control-local-service") + Expect(err).NotTo(HaveOccurred()) + + // copy config files + configFiles := []string{serviceConfigFile} + for _, configFile := range configFiles { + from, err := os.Open("./" + configFile) + Expect(err).NotTo(HaveOccurred()) + defer from.Close() + + to, err := os.OpenFile(*tmpDir+"/"+configFile, os.O_RDWR|os.O_CREATE, 0666) + Expect(err).NotTo(HaveOccurred()) + defer to.Close() + + _, err = io.Copy(to, from) + Expect(err).NotTo(HaveOccurred()) + } +}) + +var _ = AfterSuite(func() { + os.RemoveAll(*tmpDir) +}) diff --git a/core/workflow/aggregatorrole_test.go b/core/workflow/aggregatorrole_test.go index 3f3e1ff3..f01f7cf3 100644 --- a/core/workflow/aggregatorrole_test.go +++ b/core/workflow/aggregatorrole_test.go @@ -7,15 +7,16 @@ import ( ) var _ = Describe("aggregator role", func() { - var _ = Describe("processing templates", func() { - var root Role - var repo repos.Repo - var configStack map[string]string + var root Role + var repo repos.Repo + var configStack map[string]string - BeforeEach(func() { - _, repo, _ = repos.NewRepo("/home/user/git/ControlWorkflows", "", "/var/lib/o2/aliecs/repos") - configStack = make(map[string]string) - }) + BeforeEach(func() { + _, repo, _ = repos.NewRepo("/home/user/git/ControlWorkflows", "", "/var/lib/o2/aliecs/repos") + configStack = make(map[string]string) + }) + + var _ = Describe("processing templates", func() { When("an aggregator role is empty", func() { BeforeEach(func() {