diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7bb80b2..76c0bb8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -168,3 +168,4 @@ jobs: with: debug: true coverageLocations: ./cover.out:gocov + prefix: github.com/open-cmsis-pack/generator-bridge diff --git a/cmd/commands/commands_test.go b/cmd/commands/commands_test.go index 0cdb7f9..0112d6e 100644 --- a/cmd/commands/commands_test.go +++ b/cmd/commands/commands_test.go @@ -1,7 +1,62 @@ /* - * Copyright (c) 2023 Arm Limited. All rights reserved. + * Copyright (c) 2023-2024 Arm Limited. All rights reserved. * * SPDX-License-Identifier: Apache-2.0 */ -package commands_test +package commands + +import ( + "bytes" + "testing" + + "github.com/spf13/cobra" +) + +func Test_configureGlobalCmd(t *testing.T) { + type args struct { + cmd *cobra.Command + args []string + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"test", args{cmd: &cobra.Command{}}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + NewCli() + if err := configureGlobalCmd(tt.args.cmd, tt.args.args); (err != nil) != tt.wantErr { + t.Errorf("configureGlobalCmd() %s error = %v, wantErr %v", tt.name, err, tt.wantErr) + } + }) + } +} + +func Test_printVersionAndLicense(t *testing.T) { + tests := []struct { + name string + version string + copyright string + wantFile string + }{ + {"test", "v1.2.3", "copy", "generator-bridge version 1.2.3 copy\n"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + file := &bytes.Buffer{} + saveVersion := Version + saveCopyright := Copyright + Version = tt.version + Copyright = tt.copyright + printVersionAndLicense(file) + if gotFile := file.String(); gotFile != tt.wantFile { + t.Errorf("printVersionAndLicense() %s = %v, want %v", tt.name, gotFile, tt.wantFile) + } + Version = saveVersion + Copyright = saveCopyright + }) + } +} diff --git a/cmd/main.go b/cmd/main.go index 45948bf..654ff77 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -22,6 +22,8 @@ func main() { utils.StartSignalWatcher() start := time.Now() + log.Println("Command line:", os.Args[1:]) + commands.Version = version commands.Copyright = copyright cmd := commands.NewCli() diff --git a/internal/cbuild/cbuild.go b/internal/cbuild/cbuild.go index 2556522..5817851 100644 --- a/internal/cbuild/cbuild.go +++ b/internal/cbuild/cbuild.go @@ -40,7 +40,7 @@ type SubsystemType struct { Compiler string TrustZone string CoreName string - Packs []PackType + // Packs []PackType } type ParamsType struct { @@ -286,14 +286,14 @@ func ReadCbuildgen(name string, subsystem *SubsystemType) error { log.Infof("Found CBuildGen: board: %v, device: %v, core: %v, TZ: %v, compiler: %v, project: %v", subsystem.Board, subsystem.Device, subsystem.CoreName, subsystem.TrustZone, subsystem.Compiler, subsystem.Project) - for id := range cbuildGen.BuildGen.Packs { - genPack := cbuildGen.BuildGen.Packs[id] - var pack PackType - pack.Pack = genPack.Pack - pack.Path = genPack.Path - log.Infof("Found Pack: #%v Pack: %v, Path: %v", id, pack.Pack, pack.Path) - subsystem.Packs = append(subsystem.Packs, pack) - } + // for id := range cbuildGen.BuildGen.Packs { + // genPack := cbuildGen.BuildGen.Packs[id] + // var pack PackType + // pack.Pack = genPack.Pack + // pack.Path = genPack.Path + // log.Infof("Found Pack: #%v Pack: %v, Path: %v", id, pack.Pack, pack.Path) + // subsystem.Packs = append(subsystem.Packs, pack) + // } return nil } diff --git a/internal/common/common_test.go b/internal/common/common_test.go index 88b0ef6..de1f0f8 100644 --- a/internal/common/common_test.go +++ b/internal/common/common_test.go @@ -1,12 +1,49 @@ /* - * Copyright (c) 2023 Arm Limited. All rights reserved. + * Copyright (c) 2023-2024 Arm Limited. All rights reserved. * * SPDX-License-Identifier: Apache-2.0 */ -package common_test +package common -import "testing" +import ( + "reflect" + "testing" +) -func TestFoo(t *testing.T) { +func TestReadYml(t *testing.T) { + type TestYml struct { + Test []struct { + Xx string `yaml:"xx"` + } `yaml:"test"` + } + var testyml TestYml + + t1 := TestYml{Test: []struct { + Xx string `yaml:"xx"` + }{{Xx: "abc"}}} + + type args struct { + path string + out interface{} + } + tests := []struct { + name string + args args + want TestYml + wantErr bool + }{ + {"test", args{"../../testdata/test.yml", &testyml}, t1, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ReadYml(tt.args.path, tt.args.out); (err != nil) != tt.wantErr { + t.Errorf("ReadYml() %s error = %v, wantErr %v", tt.name, err, tt.wantErr) + } + xx := tt.want + if !reflect.DeepEqual(tt.args.out, &xx) { + t.Errorf("ReadYml() %s got = %v, want %v", tt.name, tt.args.out, tt.want) + } + }) + } } diff --git a/internal/stm32CubeMX/iniReader_test.go b/internal/stm32CubeMX/iniReader_test.go index 4d8d4da..eac384c 100644 --- a/internal/stm32CubeMX/iniReader_test.go +++ b/internal/stm32CubeMX/iniReader_test.go @@ -4,9 +4,27 @@ * SPDX-License-Identifier: Apache-2.0 */ -package stm32cubemx_test +package stm32cubemx -import "testing" +func ExamplePrintKeyValStr() { + PrintKeyValStr("key", "val") + // Output: + // + // key : val +} + +func ExamplePrintKeyValStrs() { + PrintKeyValStrs("key", []string{"val1", "val2"}) + // Output: + // + // key + // 0: val1 + // 1: val2 +} -func TestIniReader(t *testing.T) { +func ExamplePrintKeyValInt() { + PrintKeyValInt("key", 4711) + // Output: + // + // key : 4711 } diff --git a/internal/stm32CubeMX/mxDevice.go b/internal/stm32CubeMX/mxDevice.go index 129c8b7..29e6eaf 100644 --- a/internal/stm32CubeMX/mxDevice.go +++ b/internal/stm32CubeMX/mxDevice.go @@ -47,23 +47,17 @@ func ReadContexts(iocFile string, params cbuild.ParamsType) error { } workDir := path.Dir(iocFile) - workDirAbs, err := filepath.Abs(workDir) - if err != nil { - return err - } - mspName := deviceFamily + "xx_hal_msp.c" - // pinConfigMap, err := createPinConfigMap(mspName) - mspFolder := contextMap["ProjectManager"]["MainLocation"] - if mspFolder == "" { + mainFolder := contextMap["ProjectManager"]["MainLocation"] + if mainFolder == "" { return errors.New("main location missing") } + mspName := deviceFamily + "xx_hal_msp.c" var cfgPath string if len(contexts) == 0 { - msp := path.Join(workDirAbs, mspFolder, mspName) cfgPath = path.Join("drv_cfg", params.Subsystem[0].SubsystemIdx.Project) - err := writeMXdeviceH(contextMap, workDir, msp, cfgPath, "", params) + err := writeMXdeviceH(contextMap, workDir, mainFolder, mspName, cfgPath, "", params) if err != nil { return err } @@ -98,7 +92,6 @@ func ReadContexts(iocFile string, params cbuild.ParamsType) error { if len(contextFolder) == 0 { return errors.New("Cannot find context " + context) } - msp := path.Join(workDirAbs, contextFolder, mspFolder, mspName) for _, subsystem := range params.Subsystem { if subsystem.CoreName == coreName { if len(subsystem.TrustZone) == 0 { @@ -111,7 +104,7 @@ func ReadContexts(iocFile string, params cbuild.ParamsType) error { } } } - err := writeMXdeviceH(contextMap, workDir, msp, cfgPath, context, params) + err := writeMXdeviceH(contextMap, workDir, path.Join(contextFolder, mainFolder), mspName, cfgPath, context, params) if err != nil { return err } @@ -147,7 +140,20 @@ func createContextMap(iocFile string) (map[string]map[string]string, error) { return contextMap, nil } -func writeMXdeviceH(contextMap map[string]map[string]string, workDir string, msp string, cfgPath string, context string, params cbuild.ParamsType) error { +func writeMXdeviceH(contextMap map[string]map[string]string, workDir string, mainFolder string, mspName string, cfgPath string, context string, params cbuild.ParamsType) error { + workDirAbs, err := filepath.Abs(workDir) + if err != nil { + return err + } + + main := path.Join(workDirAbs, mainFolder, "main.c") + fMain, err := os.Open(main) + if err != nil { + return err + } + defer fMain.Close() + + msp := path.Join(workDirAbs, mainFolder, mspName) fMsp, err := os.Open(msp) if err != nil { return err @@ -181,11 +187,15 @@ func writeMXdeviceH(contextMap map[string]map[string]string, workDir string, msp } for _, peripheral := range peripherals { vmode := getVirtualMode(contextMap, peripheral) + i2cInfo, err := getI2cInfo(fMain, peripheral) + if err != nil { + return err + } pins, err := getPins(contextMap, fMsp, peripheral) if err != nil { return err } - err = mxDeviceWritePeripheralCfg(out, peripheral, vmode, pins) + err = mxDeviceWritePeripheralCfg(out, peripheral, vmode, i2cInfo, pins) if err != nil { return err } @@ -361,6 +371,46 @@ func getDigitAtEnd(pin string) string { return "" } +// Get i2c info (filter, coefficients) +func getI2cInfo(fMain *os.File, peripheral string) (map[string]string, error) { + info := make(map[string]string) + if strings.HasPrefix(peripheral, "I2C") { + _, err := fMain.Seek(0, 0) + if err != nil { + return nil, err + } + section := false + + mainScan := bufio.NewScanner(fMain) + mainScan.Split(bufio.ScanLines) + for mainScan.Scan() { + line := mainScan.Text() + if !section { + if strings.HasPrefix(line, "static void MX_"+peripheral+"_Init") && !strings.Contains(line, ";") { + section = true // Start of section: static void MX_I2Cx_Init + } + } else { // Parse section: static void MX_I2Cx_Init + if strings.HasPrefix(line, "}") { + break // End of section: static void MX_I2Cx_Init + } + if strings.Contains(line, "HAL_I2CEx_ConfigAnalogFilter") { + if strings.Contains(line, "I2C_ANALOGFILTER_ENABLE") { + info["ANF_ENABLE"] = "1" + } else { + info["ANF_ENABLE"] = "0" + } + } + if strings.Contains(line, "HAL_I2CEx_ConfigDigitalFilter") { + dnf := strings.Split(strings.Split(line, ",")[1], ")")[0] + dnf = strings.TrimRight(strings.TrimLeft(dnf, "\t "), "\t ") + info["DNF"] = dnf + } + } + } + } + return info, nil +} + func getPinConfiguration(fMsp *os.File, peripheral string, pin string, label string) (PinDefinition, error) { var pinInfo PinDefinition @@ -473,19 +523,33 @@ func mxDeviceWriteHeader(out *bufio.Writer, fName string) error { return err } -func mxDeviceWritePeripheralCfg(out *bufio.Writer, peripheral string, vmode string, pins map[string]PinDefinition) error { +func mxDeviceWritePeripheralCfg(out *bufio.Writer, peripheral string, vmode string, i2cInfo map[string]string, pins map[string]PinDefinition) error { + var err error + str := "\n/*------------------------------ " + peripheral if len(str) < 49 { str += strings.Repeat(" ", 49-len(str)) } str += "-----------------------------*/\n" - _, err := out.WriteString(str) - if err != nil { + if _, err = out.WriteString(str); err != nil { return err } if err = writeDefine(out, peripheral, "1\n"); err != nil { return err } + if i2cInfo != nil { + if _, err = out.WriteString("/* Filter Settings */\n"); err != nil { + return err + } + for item, value := range i2cInfo { + if err = writeDefine(out, peripheral+"_"+item, value); err != nil { + return err + } + } + if _, err = out.WriteString("\n"); err != nil { + return err + } + } if vmode != "" { if _, err = out.WriteString("/* Virtual mode */\n"); err != nil { return err @@ -498,13 +562,11 @@ func mxDeviceWritePeripheralCfg(out *bufio.Writer, peripheral string, vmode stri } } if len(pins) != 0 { - _, err = out.WriteString("/* Pins */\n") - if err != nil { + if _, err = out.WriteString("/* Pins */\n"); err != nil { return err } for pin, pinDef := range pins { - _, err = out.WriteString("\n/* " + pin + " */\n") - if err != nil { + if _, err = out.WriteString("\n/* " + pin + " */\n"); err != nil { return err } if err = writeDefine(out, pin+"_Pin", pinDef.p); err != nil { diff --git a/internal/stm32CubeMX/mxDevice_test.go b/internal/stm32CubeMX/mxDevice_test.go index e4b2b4a..de9b66b 100644 --- a/internal/stm32CubeMX/mxDevice_test.go +++ b/internal/stm32CubeMX/mxDevice_test.go @@ -59,13 +59,15 @@ func Test_writeMXdeviceH(t *testing.T) { var mcuContext1 = make(map[string]map[string]string) mcuContext1["Mcu"] = map[string]string{"IPs": "myContext"} var mcuContext2 = make(map[string]map[string]string) - mcuContext2["Mcu"] = map[string]string{"IPs": "IP1"} - mcuContext2["IP1"] = map[string]string{"IPs": "SPI2"} + mcuContext2["Mcu"] = map[string]string{"IP1": "SPI2"} + mcuContext2["SPI2"] = map[string]string{"VirtualModex": "ModeX"} + mcuContext2["P1"] = map[string]string{"Signal": "SPI2"} type args struct { contextMap map[string]map[string]string workDir string - msp string + mainFolder string + mspName string cfgPath string context string params cbuild.ParamsType @@ -75,16 +77,16 @@ func Test_writeMXdeviceH(t *testing.T) { args args wantErr bool }{ - {"Mcu", args{mcuContext2, "work", "../../testdata/stm32cubemx/test_msp.c", "../../testdata/cfg", "Mcu", cbuild.ParamsType{}}, false}, - {"wrong context", args{mcuContext0, "work", "../../testdata/stm32cubemx/test_msp.c", "../../testdata/cfg", "context", cbuild.ParamsType{}}, true}, - {"Mcu Context", args{mcuContext1, "work", "../../testdata/stm32cubemx/test_msp.c", "../../testdata/cfg", "Mcu", cbuild.ParamsType{}}, false}, - {"wrong myContext", args{mcuContext1, "work", "../../testdata/stm32cubemx/test_msp.c", "../../testdata/cfg", "context", cbuild.ParamsType{}}, true}, - {"wrong msp", args{mcuContext1, "work", "msp", "cfg", "context", cbuild.ParamsType{}}, true}, + {"Mcu", args{mcuContext2, "../../testdata/stm32cubemx", "", "test_msp.c", "cfg", "", cbuild.ParamsType{}}, false}, + {"wrong context", args{mcuContext0, "../../testdata/stm32cubemx", "", "test_msp.c", "cfg", "context", cbuild.ParamsType{}}, true}, + {"Mcu Context", args{mcuContext1, "../../testdata/stm32cubemx", "", "test_msp.c", "cfg", "Mcu", cbuild.ParamsType{}}, false}, + {"wrong myContext", args{mcuContext1, "../../testdata/stm32cubemx", "", "test_msp.c", "cfg", "context", cbuild.ParamsType{}}, true}, + {"wrong msp", args{mcuContext1, "", "", "msp", "cfg", "context", cbuild.ParamsType{}}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - defer os.RemoveAll(tt.args.cfgPath) - if err := writeMXdeviceH(tt.args.contextMap, tt.args.workDir, tt.args.msp, tt.args.cfgPath, tt.args.context, tt.args.params); (err != nil) != tt.wantErr { + defer os.RemoveAll(tt.args.workDir + "/../" + tt.args.cfgPath) + if err := writeMXdeviceH(tt.args.contextMap, tt.args.workDir, tt.args.mainFolder, tt.args.mspName, tt.args.cfgPath, tt.args.context, tt.args.params); (err != nil) != tt.wantErr { t.Errorf("writeMXdeviceH() %s error = %v, wantErr %v", tt.name, err, tt.wantErr) } }) @@ -329,6 +331,8 @@ func Test_replaceSpecialChars(t *testing.T) { } func Test_getDigitAtEnd(t *testing.T) { + t.Parallel() + type args struct { pin string } @@ -343,7 +347,9 @@ func Test_getDigitAtEnd(t *testing.T) { {"test4", args{"12"}, "12"}, } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { + t.Parallel() if got := getDigitAtEnd(tt.args.pin); got != tt.want { t.Errorf("getDigitAtEnd() = %v, want %v", got, tt.want) } @@ -512,6 +518,7 @@ func Test_mxDeviceWritePeripheralCfg(t *testing.T) { out *bufio.Writer peripheral string vmode string + i2cInfo map[string]string pins map[string]PinDefinition } tests := []struct { @@ -533,7 +540,7 @@ func Test_mxDeviceWritePeripheralCfg(t *testing.T) { t.Run(tt.name, func(t *testing.T) { tt.args.out = bufio.NewWriter(&b) - if err := mxDeviceWritePeripheralCfg(tt.args.out, tt.args.peripheral, tt.args.vmode, tt.args.pins); (err != nil) != tt.wantErr { + if err := mxDeviceWritePeripheralCfg(tt.args.out, tt.args.peripheral, tt.args.vmode, tt.args.i2cInfo, tt.args.pins); (err != nil) != tt.wantErr { t.Errorf("mxDeviceWritePeripheralCfg() error = %v, wantErr %v", err, tt.wantErr) } tt.args.out.Flush() diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index cb7cce3..b10cdf2 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -4,65 +4,157 @@ * SPDX-License-Identifier: Apache-2.0 */ -package utils_test +package utils import ( + "os" + "reflect" "testing" - - "github.com/open-cmsis-pack/generator-bridge/internal/utils" - "github.com/stretchr/testify/assert" ) -//var testDir string = "./Testing" - -func TestAddLine(t *testing.T) { - var text utils.TextBuilder - - text.AddLine("A line") - expected := "A line\n" - result := text.GetLine() - assert.Equal(t, expected, result) - - text.AddLine("A second line") - result = text.GetLine() - expected += "A second line\n" - assert.Equal(t, expected, result) +func TestAddQuotes(t *testing.T) { + type args struct { + text string + } + tests := []struct { + name string + args args + want string + }{ + {"test", args{"Test"}, "\"Test\""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := AddQuotes(tt.args.text); got != tt.want { + t.Errorf("AddQuotes() = %v, want %v", got, tt.want) + } + }) + } } -func TestAddQuotes(t *testing.T) { - text := "Test" - expected := "\"" + text + "\"" - result := utils.AddQuotes(text) - assert.Equal(t, expected, result) +func TestTextBuilder_AddLine(t *testing.T) { + var builder TextBuilder + var builder1 TextBuilder + var builder2 TextBuilder + + type args struct { + args []string + } + tests := []struct { + name string + tr *TextBuilder + args args + want string + }{ + {"nix", &builder, args{}, "\n"}, + {"1", &builder1, args{[]string{"A line"}}, "A line\n"}, + {"1+", &builder2, args{[]string{"A line", "and more"}}, "A line and more\n"}, + {"2", &builder1, args{[]string{"A second line"}}, "A line\nA second line\n"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.tr.AddLine(tt.args.args...) + if !reflect.DeepEqual(tt.tr.GetLine(), tt.want) { + t.Errorf("TestTextBuilder_AddLine() %s got = %v, want %v", tt.name, tt.tr.GetLine(), tt.want) + } + }) + } } -/* Test runs when "Debug" but fails in "Run" func TestFileExists(t *testing.T) { + t.Parallel() + type args struct { + filePath string + } + tests := []struct { + name string + args args + want bool + }{ + {"file", args{"../../testdata/test.yml"}, true}, + {"dir", args{"../../testdata"}, false}, + {"nix", args{"../../testdata/nix"}, false}, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := FileExists(tt.args.filePath); got != tt.want { + t.Errorf("FileExists() %s = %v, want %v", tt.name, got, tt.want) + } + }) + } +} - result := utils.DirExists(testDir) - expected := false - assert.Equal(t, expected, result) - - filename := filepath.Join(testDir, "fileexists.txt") - result = utils.FileExists(filename) - expected = false - assert.Equal(t, expected, result) - - err := os.Mkdir(testDir, 0755) - assert.Equal(t, nil, err) - - result = utils.DirExists(testDir) - expected = true - assert.Equal(t, expected, result) - - text := "Hello, World" - err = os.WriteFile(filename, []byte(text), 0755) - assert.NotEqual(t, nil, err) +func TestDirExists(t *testing.T) { + type args struct { + dirPath string + } + tests := []struct { + name string + args args + want bool + }{ + {"file", args{"../../testdata/test.yml"}, false}, + {"dir", args{"../../testdata"}, true}, + {"nix", args{"../../testdata/nix"}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := DirExists(tt.args.dirPath); got != tt.want { + t.Errorf("DirExists() %s = %v, want %v", tt.name, got, tt.want) + } + }) + } +} - result = utils.FileExists(filename) - expected = true - assert.Equal(t, expected, result) +func TestEnsureDir(t *testing.T) { + type args struct { + dirName string + } + tests := []struct { + name string + args args + remove string + wantErr bool + }{ + {"test", args{"../../testdata/1/2/3"}, "../../testdata/1", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer os.RemoveAll(tt.remove) + if err := EnsureDir(tt.args.dirName); (err != nil) != tt.wantErr { + t.Errorf("EnsureDir() %s error = %v, wantErr %v", tt.name, err, tt.wantErr) + } + }) + } +} - os.RemoveAll(testDir) +func TestConvertFilename(t *testing.T) { + type args struct { + outPath string + file string + relativePathAdd string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + {"test", args{"../../testdata", "test.ioc", "stm32cubemx"}, "./stm32cubemx/test.ioc", false}, + {"nix", args{"../../testdata", "nix", "stm32cubemx"}, "./stm32cubemx/nix", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ConvertFilename(tt.args.outPath, tt.args.file, tt.args.relativePathAdd) + if (err != nil) != tt.wantErr { + t.Errorf("ConvertFilename() %s error = %v, wantErr %v", tt.name, err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ConvertFilename() %s = %v, want %v", tt.name, got, tt.want) + } + }) + } } -*/ diff --git a/testdata/stm32cubemx/main.c b/testdata/stm32cubemx/main.c new file mode 100644 index 0000000..b6167fe --- /dev/null +++ b/testdata/stm32cubemx/main.c @@ -0,0 +1,14 @@ +static void MX_I2C1_Init(void) +{ + if (HAL_I2CEx_ConfigAnalogFilter(&hi2c1, I2C_ANALOGFILTER_ENABLE) != HAL_OK) + { + Error_Handler(); + } + + /** Configure Digital filter + */ + if (HAL_I2CEx_ConfigDigitalFilter(&hi2c1, 0) != HAL_OK) + { + Error_Handler(); + } +} diff --git a/testdata/test.yml b/testdata/test.yml new file mode 100644 index 0000000..7c12317 --- /dev/null +++ b/testdata/test.yml @@ -0,0 +1,2 @@ +test: + - xx: abc