diff --git a/cmd/algocfg/profileCommand.go b/cmd/algocfg/profileCommand.go index e629f2578a..0a368fc975 100644 --- a/cmd/algocfg/profileCommand.go +++ b/cmd/algocfg/profileCommand.go @@ -92,6 +92,7 @@ func init() { rootCmd.AddCommand(profileCmd) profileCmd.AddCommand(setProfileCmd) setProfileCmd.Flags().BoolVarP(&forceUpdate, "yes", "y", false, "Force updates to be written") + profileCmd.AddCommand(printProfileCmd) profileCmd.AddCommand(listProfileCmd) } @@ -133,6 +134,23 @@ var listProfileCmd = &cobra.Command{ }, } +var printProfileCmd = &cobra.Command{ + Use: "print", + Short: "Print config.json to stdout.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cfg, err := getConfigForArg(args[0]) + if err != nil { + reportErrorf("%v", err) + } + err = codecs.WriteNonDefaultValues(os.Stdout, cfg, config.GetDefaultLocal(), nil) + if err != nil { + reportErrorf("Error writing config file to stdout: %s", err) + } + fmt.Fprintf(os.Stdout, "\n") + }, +} + var setProfileCmd = &cobra.Command{ Use: "set", Short: "Set config.json file from a profile.", @@ -157,7 +175,7 @@ var setProfileCmd = &cobra.Command{ return } } - err = codecs.SaveNonDefaultValuesToFile(file, cfg, config.GetDefaultLocal(), nil, true) + err = codecs.SaveNonDefaultValuesToFile(file, cfg, config.GetDefaultLocal(), nil) if err != nil { reportErrorf("Error saving updated config file '%s' - %s", file, err) } diff --git a/cmd/algocfg/resetCommand.go b/cmd/algocfg/resetCommand.go index 24f9cf1dad..2ec8c55aad 100644 --- a/cmd/algocfg/resetCommand.go +++ b/cmd/algocfg/resetCommand.go @@ -63,7 +63,7 @@ var resetCmd = &cobra.Command{ } file := filepath.Join(dataDir, config.ConfigFilename) - err = codecs.SaveNonDefaultValuesToFile(file, cfg, defaults, nil, true) + err = codecs.SaveNonDefaultValuesToFile(file, cfg, defaults, nil) if err != nil { reportWarnf("Error saving updated config file '%s' - %s", file, err) anyError = true diff --git a/cmd/algocfg/setCommand.go b/cmd/algocfg/setCommand.go index 8367857592..58f7ee796d 100644 --- a/cmd/algocfg/setCommand.go +++ b/cmd/algocfg/setCommand.go @@ -66,7 +66,7 @@ var setCmd = &cobra.Command{ } file := filepath.Join(dataDir, config.ConfigFilename) - err = codecs.SaveNonDefaultValuesToFile(file, cfg, config.GetDefaultLocal(), nil, true) + err = codecs.SaveNonDefaultValuesToFile(file, cfg, config.GetDefaultLocal(), nil) if err != nil { reportWarnf("Error saving updated config file '%s' - %s", file, err) anyError = true diff --git a/config/localTemplate.go b/config/localTemplate.go index 74c0b2e08a..c1fd87201b 100644 --- a/config/localTemplate.go +++ b/config/localTemplate.go @@ -652,7 +652,7 @@ func (cfg Local) SaveAllToDisk(root string) error { func (cfg Local) SaveToFile(filename string) error { var alwaysInclude []string alwaysInclude = append(alwaysInclude, "Version") - return codecs.SaveNonDefaultValuesToFile(filename, cfg, defaultLocal, alwaysInclude, true) + return codecs.SaveNonDefaultValuesToFile(filename, cfg, defaultLocal, alwaysInclude) } // DNSSecuritySRVEnforced returns true if SRV response verification enforced diff --git a/util/codecs/json.go b/util/codecs/json.go index e283ef0624..8c2cebf087 100644 --- a/util/codecs/json.go +++ b/util/codecs/json.go @@ -18,6 +18,7 @@ package codecs import ( "bufio" + "bytes" "encoding/json" "fmt" "io" @@ -48,6 +49,16 @@ func LoadObjectFromFile(filename string, object interface{}) (err error) { return } +func writeBytes(writer io.Writer, object interface{}, prettyFormat bool) error { + var enc *json.Encoder + if prettyFormat { + enc = NewFormattedJSONEncoder(writer) + } else { + enc = json.NewEncoder(writer) + } + return enc.Encode(object) +} + // SaveObjectToFile implements the common pattern for saving an object to a file as json func SaveObjectToFile(filename string, object interface{}, prettyFormat bool) error { f, err := os.Create(filename) @@ -55,22 +66,13 @@ func SaveObjectToFile(filename string, object interface{}, prettyFormat bool) er return err } defer f.Close() - var enc *json.Encoder - if prettyFormat { - enc = NewFormattedJSONEncoder(f) - } else { - enc = json.NewEncoder(f) - } - err = enc.Encode(object) - return err + return writeBytes(f, object, prettyFormat) } -// SaveNonDefaultValuesToFile saves an object to a file as json, but only fields that are not +// WriteNonDefaultValues writes object to a writer as json, but only fields that are not // currently set to be the default value. // Optionally, you can specify an array of field names to always include. -func SaveNonDefaultValuesToFile(filename string, object, defaultObject interface{}, ignore []string, prettyFormat bool) error { - // Serialize object to temporary file. - // Read file into string array +func WriteNonDefaultValues(writer io.Writer, object, defaultObject interface{}, ignore []string) error { // Iterate one line at a time, parse Name // If ignore contains Name, don't delete // Use reflection to compare object[Name].value == defaultObject[Name].value @@ -78,25 +80,13 @@ func SaveNonDefaultValuesToFile(filename string, object, defaultObject interface // When done, ensure last value line doesn't include comma // Write string array to file. - file, err := os.CreateTemp("", "encsndv") - if err != nil { - return err - } - name := file.Name() - file.Close() - - defer os.Remove(name) - // Save object to file pretty-formatted so we can read one value-per-line - err = SaveObjectToFile(name, object, true) + var buf bytes.Buffer + err := writeBytes(&buf, object, true) if err != nil { return err } + content := buf.Bytes() - // Read lines from encoded file into string array - content, err := os.ReadFile(name) - if err != nil { - return err - } valueLines := strings.Split(string(content), "\n") // Create maps of the name->value pairs for the object and the defaults @@ -155,19 +145,30 @@ func SaveNonDefaultValuesToFile(filename string, object, defaultObject interface } } + combined := strings.Join(newFile, "\n") + combined = strings.TrimRight(combined, "\r\n ") + _, err = writer.Write([]byte(combined)) + return err +} + +// SaveNonDefaultValuesToFile saves an object to a file as json, but only fields that are not +// currently set to be the default value. +// Optionally, you can specify an array of field names to always include. +func SaveNonDefaultValuesToFile(filename string, object, defaultObject interface{}, ignore []string) error { outFile, err := os.Create(filename) if err != nil { return err } defer outFile.Close() writer := bufio.NewWriter(outFile) - combined := strings.Join(newFile, "\n") - combined = strings.TrimRight(combined, "\r\n ") - _, err = writer.WriteString(combined) - if err == nil { - writer.Flush() + + err = WriteNonDefaultValues(writer, object, defaultObject, ignore) + if err != nil { + return err } - return err + + writer.Flush() + return nil } func extractValueName(line string) (name string) { diff --git a/util/codecs/json_test.go b/util/codecs/json_test.go index 1f56531971..6bd4d53cd0 100644 --- a/util/codecs/json_test.go +++ b/util/codecs/json_test.go @@ -17,9 +17,15 @@ package codecs import ( - "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" + "bytes" + "os" + "path" "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/test/partitiontest" ) type testValue struct { @@ -30,6 +36,7 @@ type testValue struct { func TestIsDefaultValue(t *testing.T) { partitiontest.PartitionTest(t) + t.Parallel() a := require.New(t) @@ -52,3 +59,113 @@ func TestIsDefaultValue(t *testing.T) { a.False(isDefaultValue("Int", objectValues, defaultValues)) a.True(isDefaultValue("Missing", objectValues, defaultValues)) } + +func TestSaveObjectToFile(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + type TestType struct { + A uint64 + B string + } + + obj := TestType{1024, "test"} + + // prettyFormat = false + { + filename := path.Join(t.TempDir(), "test.json") + SaveObjectToFile(filename, obj, false) + data, err := os.ReadFile(filename) + require.NoError(t, err) + expected := `{"A":1024,"B":"test"} +` + require.Equal(t, expected, string(data)) + } + + // prettyFormat = true + { + filename := path.Join(t.TempDir(), "test.json") + SaveObjectToFile(filename, obj, true) + data, err := os.ReadFile(filename) + require.NoError(t, err) + expected := `{ + "A": 1024, + "B": "test" +} +` + require.Equal(t, expected, string(data)) + } + +} + +func TestWriteNonDefaultValue(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + type TestType struct { + Version uint32 + Archival bool + GossipFanout int + NetAddress string + ReconnectTime time.Duration + } + + defaultObject := TestType{ + Version: 1, + Archival: true, + GossipFanout: 50, + NetAddress: "Denver", + ReconnectTime: 60 * time.Second, + } + + testcases := []struct { + name string + in TestType + out string + ignore []string + }{ + { + name: "all defaults", + in: defaultObject, + out: `{ +}`, + }, { + name: "some defaults", + in: TestType{ + Version: 1, + Archival: false, + GossipFanout: 25, + NetAddress: "Denver", + ReconnectTime: 60 * time.Nanosecond, + }, + out: `{ + "Archival": false, + "GossipFanout": 25, + "ReconnectTime": 60 +}`, + }, { + name: "ignore", + in: defaultObject, + ignore: []string{"Version", "Archival", "GossipFanout", "NetAddress", "ReconnectTime"}, + out: `{ + "Version": 1, + "Archival": true, + "GossipFanout": 50, + "NetAddress": "Denver", + "ReconnectTime": 60000000000 +}`, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + a := require.New(t) + var writer bytes.Buffer + err := WriteNonDefaultValues(&writer, tc.in, defaultObject, tc.ignore) + a.NoError(err) + a.Equal(tc.out, writer.String()) + }) + } +}