From e58b0fa01536bbec3f87438ab100c6da7b7a42ab Mon Sep 17 00:00:00 2001 From: Jan Eitzinger Date: Tue, 30 Sep 2025 09:01:54 +0200 Subject: [PATCH] Add ui config tests and fix bugs --- internal/config/config_test.go | 1 + internal/repository/userConfig.go | 16 ++---- web/configSchema.go | 49 +++++++++++------ web/web.go | 57 +++++++++++++++++--- web/webConfig_test.go | 89 +++++++++++++++++++++++++++++++ 5 files changed, 177 insertions(+), 35 deletions(-) create mode 100644 web/webConfig_test.go diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7dc76c3..35e1c65 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -2,6 +2,7 @@ // All rights reserved. This file is part of cc-backend. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. + package config import ( diff --git a/internal/repository/userConfig.go b/internal/repository/userConfig.go index 9df6af3..744c2ea 100644 --- a/internal/repository/userConfig.go +++ b/internal/repository/userConfig.go @@ -26,7 +26,7 @@ var ( type UserCfgRepo struct { DB *sqlx.DB Lookup *sqlx.Stmt - uiDefaults web.WebConfig + uiDefaults map[string]any cache *lrucache.Cache lock sync.RWMutex } @@ -43,7 +43,7 @@ func GetUserCfgRepo() *UserCfgRepo { userCfgRepoInstance = &UserCfgRepo{ DB: db.DB, Lookup: lookupConfigStmt, - uiDefaults: web.UIDefaults, + uiDefaults: web.UIDefaultsMap, cache: lrucache.New(1024), } }) @@ -55,13 +55,12 @@ func GetUserCfgRepo() *UserCfgRepo { // user or return the plain default config. func (uCfg *UserCfgRepo) GetUIConfig(user *schema.User) (map[string]any, error) { if user == nil { - uCfg.lock.RLock() copy := make(map[string]any, len(uCfg.uiDefaults)) maps.Copy(copy, uCfg.uiDefaults) - uCfg.lock.RUnlock() return copy, nil } + // Is the cache invalidated in case the options are changed? data := uCfg.cache.Get(user.Username, func() (any, time.Duration, int) { uiconfig := make(map[string]any, len(uCfg.uiDefaults)) maps.Copy(uiconfig, uCfg.uiDefaults) @@ -113,15 +112,6 @@ func (uCfg *UserCfgRepo) UpdateConfig( user *schema.User, ) error { if user == nil { - var val any - if err := json.Unmarshal([]byte(value), &val); err != nil { - cclog.Warn("Error while unmarshaling raw user config json") - return err - } - - uCfg.lock.Lock() - defer uCfg.lock.Unlock() - uCfg.uiDefaults[key] = val return nil } diff --git a/web/configSchema.go b/web/configSchema.go index b4b83f2..5690b63 100644 --- a/web/configSchema.go +++ b/web/configSchema.go @@ -5,8 +5,7 @@ package web -var configSchema = ` - { +const configSchema = `{ "type": "object", "properties": { "jobList": { @@ -21,8 +20,7 @@ var configSchema = ` "description": "If footprint bars are shown as first column by default.", "type": "boolean" } - }, - "required": ["usePaging", "showFootprint"] + } }, "nodeList": { "description": "Node list defaults. Applies to node list view.", @@ -32,8 +30,7 @@ var configSchema = ` "description": "If classic paging is used instead of continuous scrolling by default.", "type": "boolean" } - }, - "required": ["usePaging"] + } }, "jobView": { "description": "Job view defaults.", @@ -55,8 +52,7 @@ var configSchema = ` "description": "If the job metric statistics table is shown by default.", "type": "boolean" } - }, - "required": ["showFootprint"] + } }, "metricConfig": { "description": "Global initial metric selections for primary views of all clusters.", @@ -95,9 +91,33 @@ var configSchema = ` "name": { "description": "The name of the cluster." }, - "subClusters" { + "jobListMetrics": { + "description": "Initial metrics shown for new users in job lists (User and jobs view) for subcluster.", + "type": "array", + "items": { + "type": "string", + "minItems": 1 + } + }, + "jobViewPlotMetrics": { + "description": "Initial metrics shown for new users as job view timeplots for subcluster.", + "type": "array", + "items": { + "type": "string", + "minItems": 1 + } + }, + "jobViewTableMetrics": { + "description": "Initial metrics shown for new users in job view statistics table for subcluster.", + "type": "array", + "items": { + "type": "string", + "minItems": 1 + } + }, + "subClusters": { "description": "The array of overrides per subcluster.", - "type":"array", + "type": "array", "items": { "type": "object", "properties": { @@ -138,8 +158,7 @@ var configSchema = ` "required": ["name", "subClusters"], "minItems": 1 } - }, - "required": ["jobListMetrics", "jobViewPlotMetrics", "jobViewTableMetrics"] + } } }, "plotConfiguration": { @@ -165,7 +184,7 @@ var configSchema = ` "type": "string" } } - }, - "required": ["colorBackground", "plotsPerRow", "lineWidth"] + } } - }` + } +}` diff --git a/web/web.go b/web/web.go index 495e365..8c0af8b 100644 --- a/web/web.go +++ b/web/web.go @@ -13,7 +13,6 @@ import ( "io/fs" "net/http" "strings" - "sync" "github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/pkg/archive" @@ -75,8 +74,6 @@ type PlotConfiguration struct { ColorScheme []string `json:"colorScheme"` } -var initOnce sync.Once - var UIDefaults = WebConfig{ JobList: JobListConfig{ UsePaging: false, @@ -104,6 +101,8 @@ var UIDefaults = WebConfig{ }, } +var UIDefaultsMap map[string]any + // // map[string]any{ // "analysis_view_histogramMetrics": []string{"flops_any", "mem_bw", "mem_used"}, @@ -117,16 +116,60 @@ var UIDefaults = WebConfig{ // "status_view_selectedTopProjectCategory": "totalJobs", // } -func Init(rawConfig json.RawMessage, disableArchive bool) error { +func Init(rawConfig json.RawMessage) error { var err error - initOnce.Do(func() { + if rawConfig != nil { config.Validate(configSchema, rawConfig) if err = json.Unmarshal(rawConfig, &UIDefaults); err != nil { cclog.Warn("Error while unmarshaling raw config json") - return + return err } - }) + } + + UIDefaultsMap = make(map[string]any) + + UIDefaultsMap["joblist_usePaging"] = UIDefaults.JobList.UsePaging + UIDefaultsMap["joblist_showFootprint"] = UIDefaults.JobList.ShowFootprint + UIDefaultsMap["nodelist_usePaging"] = UIDefaults.NodeList.UsePaging + UIDefaultsMap["jobview_showPolarPlot"] = UIDefaults.JobView.ShowPolarPlot + UIDefaultsMap["jobview_showFootprint"] = UIDefaults.JobView.ShowFootprint + UIDefaultsMap["jobview_showRoofline"] = UIDefaults.JobView.ShowRoofline + UIDefaultsMap["jobview_showStatTable"] = UIDefaults.JobView.ShowStatTable + + UIDefaultsMap["metricConfig_jobListMetrics"] = UIDefaults.MetricConfig.JobListMetrics + UIDefaultsMap["metricConfig_jobViewPlotMetrics"] = UIDefaults.MetricConfig.JobViewPlotMetrics + UIDefaultsMap["metricConfig_jobViewTableMetrics"] = UIDefaults.MetricConfig.JobViewTableMetrics + + UIDefaultsMap["plotConfiguration_colorBackground"] = UIDefaults.PlotConfiguration.ColorBackground + UIDefaultsMap["plotConfiguration_plotsPerRow"] = UIDefaults.PlotConfiguration.PlotsPerRow + UIDefaultsMap["plotConfiguration_lineWidth"] = UIDefaults.PlotConfiguration.LineWidth + UIDefaultsMap["plotConfiguration_colorScheme"] = UIDefaults.PlotConfiguration.ColorScheme + + for _, c := range UIDefaults.MetricConfig.Clusters { + if c.JobListMetrics != nil { + UIDefaultsMap["metricConfig_jobListMetrics:"+c.Name] = c.JobListMetrics + } + if c.JobViewPlotMetrics != nil { + UIDefaultsMap["metricConfig_jobViewPlotMetrics:"+c.Name] = c.JobViewPlotMetrics + } + if c.JobViewTableMetrics != nil { + UIDefaultsMap["metricConfig_jobViewTableMetrics:"+c.Name] = c.JobViewTableMetrics + } + + for _, sc := range c.SubClusters { + suffix := strings.Join([]string{c.Name, sc.Name}, ":") + if sc.JobListMetrics != nil { + UIDefaultsMap["metricConfig_jobListMetrics:"+suffix] = sc.JobListMetrics + } + if sc.JobViewPlotMetrics != nil { + UIDefaultsMap["metricConfig_jobViewPlotMetrics:"+suffix] = sc.JobViewPlotMetrics + } + if sc.JobViewTableMetrics != nil { + UIDefaultsMap["metricConfig_jobViewTableMetrics:"+suffix] = sc.JobViewTableMetrics + } + } + } return err } diff --git a/web/webConfig_test.go b/web/webConfig_test.go new file mode 100644 index 0000000..6074f50 --- /dev/null +++ b/web/webConfig_test.go @@ -0,0 +1,89 @@ +// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. +// All rights reserved. This file is part of cc-backend. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package web + +import ( + "encoding/json" + "fmt" + "testing" + + ccconf "github.com/ClusterCockpit/cc-lib/ccConfig" +) + +func TestInit(t *testing.T) { + fp := "../../configs/config.json" + ccconf.Init(fp) + cfg := ccconf.GetPackageConfig("web") + + Init(cfg) + + if UIDefaultsMap["nodelist_usePaging"] == false { + t.Errorf("wrong option\ngot: %v \nwant: true", UIDefaultsMap["NodeList_UsePaging"]) + } +} + +func TestSimpleDefaults(t *testing.T) { + const s = `{ + "joblist": { + "showFootprint": false + } + }` + + Init(json.RawMessage(s)) + + if UIDefaultsMap["joblist_usePaging"] == true { + t.Errorf("wrong option\ngot: %v \nwant: false", UIDefaultsMap["NodeList_UsePaging"]) + } +} + +func TestOverwrite(t *testing.T) { + const s = `{ + "metricConfig": { + "jobListMetrics": ["flops_sp", "flops_dp"], + "clusters": [ + { + "name": "fritz", + "jobListMetrics": ["flops_any", "mem_bw", "load"], + "subClusters": [ + { + "name": "icelake", + "jobListMetrics": ["flops_any", "mem_bw", "power", "load"], + "jobViewPlotMetrics": ["load"] + } + ] + } + ] + } +}` + + Init(json.RawMessage(s)) + + fmt.Printf("%+v", UIDefaultsMap) + v, ok := UIDefaultsMap["metricConfig_jobListMetrics"].([]string) + if ok { + if v[0] != "flops_sp" { + t.Errorf("wrong metric\ngot: %s \nwant: flops_sp", v[0]) + } + } else { + t.Errorf("missing Key\nkey: metricConfig_jobListMetrics") + } + v, ok = UIDefaultsMap["metricConfig_jobListMetrics:fritz"].([]string) + if ok { + if v[0] != "flops_any" { + t.Errorf("wrong metric\ngot: %s \nwant: flops_any", v[0]) + } + } else { + t.Errorf("missing Key\nkey: metricConfig_jobListMetrics:fritz") + } + v, ok = UIDefaultsMap["metricConfig_jobListMetrics:fritz:icelake"].([]string) + if ok { + if v[3] != "load" { + t.Errorf("wrong metric\ngot: %s \nwant: load", v[3]) + } + } else { + t.Errorf("missing Key\nkey: metricConfig_jobListMetrics:fritz:icelake") + } +}