diff --git a/chloggen/cmd/cmd_test.go b/chloggen/cmd/cmd_test.go index b1321946..90acc3c6 100644 --- a/chloggen/cmd/cmd_test.go +++ b/chloggen/cmd/cmd_test.go @@ -15,7 +15,9 @@ package cmd import ( + "bytes" "fmt" + "io" "os" "path/filepath" "strings" @@ -126,3 +128,24 @@ func writeEntryYAML(ctx chlog.Context, filename string, entry *chlog.Entry) erro path := filepath.Join(ctx.ChloggenDir, filename) return os.WriteFile(path, entryBytes, os.FileMode(0755)) } + +func runCobra(t *testing.T, args ...string) (string, string) { + cmd := rootCmd() + + outBytes := bytes.NewBufferString("") + cmd.SetOut(outBytes) + + errBytes := bytes.NewBufferString("") + cmd.SetErr(errBytes) + + cmd.SetArgs(args) + cmd.Execute() // nolint:errcheck + + out, ioErr := io.ReadAll(outBytes) + require.NoError(t, ioErr, "read stdout") + + err, ioErr := io.ReadAll(errBytes) + require.NoError(t, ioErr, "read stderr") + + return string(out), string(err) +} diff --git a/chloggen/cmd/new.go b/chloggen/cmd/new.go index e0cb0fd9..ce0409a9 100644 --- a/chloggen/cmd/new.go +++ b/chloggen/cmd/new.go @@ -15,61 +15,54 @@ package cmd import ( - "fmt" - "log" "os" "path/filepath" "strings" "github.com/spf13/cobra" - - "go.opentelemetry.io/build-tools/chloggen/internal/chlog" ) var ( filename string ) -var newCmd = &cobra.Command{ - Use: "new", - Short: "Creates new change file", - RunE: func(cmd *cobra.Command, args []string) error { - return initialize(chlogCtx, filename) - }, -} - -func initialize(ctx chlog.Context, filename string) error { - path := filepath.Join(ctx.ChloggenDir, cleanFileName(filename)) - var pathWithExt string - switch ext := filepath.Ext(path); ext { - case ".yaml": - pathWithExt = path - case ".yml": - pathWithExt = strings.TrimSuffix(path, ".yml") + ".yaml" - default: - pathWithExt = path + ".yaml" - } +func newCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "new", + Short: "Creates new change file", + RunE: func(cmd *cobra.Command, args []string) error { + path := filepath.Join(chlogCtx.ChloggenDir, cleanFileName(filename)) + var pathWithExt string + switch ext := filepath.Ext(path); ext { + case ".yaml": + pathWithExt = path + case ".yml": + pathWithExt = strings.TrimSuffix(path, ".yml") + ".yaml" + default: + pathWithExt = path + ".yaml" + } - templateBytes, err := os.ReadFile(filepath.Clean(ctx.TemplateYAML)) - if err != nil { - return err + templateBytes, err := os.ReadFile(filepath.Clean(chlogCtx.TemplateYAML)) + if err != nil { + return err + } + err = os.WriteFile(pathWithExt, templateBytes, os.FileMode(0755)) + if err != nil { + return err + } + cmd.Printf("Changelog entry template copied to: %s\n", pathWithExt) + return nil + }, } - err = os.WriteFile(pathWithExt, templateBytes, os.FileMode(0755)) - if err != nil { - return err + cmd.Flags().StringVarP(&filename, "filename", "f", "", "name of the file to add") + if err := cmd.MarkFlagRequired("filename"); err != nil { + cmd.PrintErrf("could not mark filename flag as required: %v", err) + os.Exit(1) } - fmt.Printf("Changelog entry template copied to: %s\n", pathWithExt) - return nil + return cmd } func cleanFileName(filename string) string { replace := strings.NewReplacer("/", "_", "\\", "_") return replace.Replace(filename) } - -func init() { - newCmd.Flags().StringVarP(&filename, "filename", "f", "", "name of the file to add") - if err := newCmd.MarkFlagRequired("filename"); err != nil { - log.Fatalf("could not mark filename flag as required: %v", err) - } -} diff --git a/chloggen/cmd/new_test.go b/chloggen/cmd/new_test.go index 4309290a..bdd16ef5 100644 --- a/chloggen/cmd/new_test.go +++ b/chloggen/cmd/new_test.go @@ -15,53 +15,69 @@ package cmd import ( + "fmt" + "path/filepath" "testing" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" "go.opentelemetry.io/build-tools/chloggen/internal/chlog" ) -func TestNew(t *testing.T) { - tests := []struct { - name string - filename string - }{ - { - name: "no_extension", - filename: "my-change", - }, - { - name: "yaml_extension", - filename: "some-change.yaml", - }, - { - name: "yml_extension", - filename: "some-change.yml", - }, - { - name: "replace_forward_slash", - filename: "replace/forward/slash", - }, - { - name: "name_with_period", - filename: "not.an.extension", - }, - { - name: "bad_extension", - filename: "my-change.txt", - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - ctx := setupTestDir(t, []*chlog.Entry{}) - require.NoError(t, initialize(ctx, tc.filename)) - require.Error(t, validate(ctx), "The new entry should not be valid without user input") - }) - } +const newUsage = `Usage: + chloggen new [flags] + +Flags: + -f, --filename string name of the file to add + -h, --help help for new + +Global Flags: + --chloggen-directory string directory containing unreleased change log entries (default: .chloggen)` + +func TestNewCommand(t *testing.T) { + var out, err string + + out, err = runCobra(t, "new", "--help") + assert.Contains(t, out, newUsage) + assert.Empty(t, err) + + out, err = runCobra(t, "new") + assert.Contains(t, out, newUsage) + assert.Contains(t, err, `required flag(s) "filename" not set`) + + out, err = runCobra(t, "new", "--filename", "my-change") + assert.Contains(t, out, newUsage) + assert.Contains(t, err, `no such file or directory`) + + // Set up a test directory to which we will write new files + chlogCtx = setupTestDir(t, []*chlog.Entry{}) + + out, err = runCobra(t, "new", "--filename", "my-change") + assert.Contains(t, out, fmt.Sprintf("Changelog entry template copied to: %s", filepath.Join(chlogCtx.ChloggenDir, "my-change.yaml"))) + assert.Empty(t, err) + + out, err = runCobra(t, "new", "--filename", "some-change.yaml") + assert.Contains(t, out, fmt.Sprintf("Changelog entry template copied to: %s", filepath.Join(chlogCtx.ChloggenDir, "some-change.yaml"))) + assert.Empty(t, err) + + out, err = runCobra(t, "new", "--filename", "some-change.yml") + assert.Contains(t, out, fmt.Sprintf("Changelog entry template copied to: %s", filepath.Join(chlogCtx.ChloggenDir, "some-change.yaml"))) + assert.Empty(t, err) + + out, err = runCobra(t, "new", "--filename", "replace/forward/slash") + assert.Contains(t, out, fmt.Sprintf("Changelog entry template copied to: %s", filepath.Join(chlogCtx.ChloggenDir, "replace_forward_slash.yaml"))) + assert.Empty(t, err) + + out, err = runCobra(t, "new", "--filename", "not.an.extension") + assert.Contains(t, out, fmt.Sprintf("Changelog entry template copied to: %s", filepath.Join(chlogCtx.ChloggenDir, "not.an.extension.yaml"))) + assert.Empty(t, err) + + out, err = runCobra(t, "new", "--filename", "my-change.txt") + assert.Contains(t, out, fmt.Sprintf("Changelog entry template copied to: %s", filepath.Join(chlogCtx.ChloggenDir, "my-change.txt.yaml"))) + assert.Empty(t, err) } func TestCleanFilename(t *testing.T) { - require.Equal(t, "fix_some_bug", cleanFileName("fix/some_bug")) - require.Equal(t, "fix_some_bug", cleanFileName("fix\\some_bug")) + assert.Equal(t, "fix_some_bug", cleanFileName("fix/some_bug")) + assert.Equal(t, "fix_some_bug", cleanFileName("fix\\some_bug")) } diff --git a/chloggen/cmd/root.go b/chloggen/cmd/root.go index 6978362a..3e38b0ed 100644 --- a/chloggen/cmd/root.go +++ b/chloggen/cmd/root.go @@ -25,29 +25,36 @@ var ( chlogCtx chlog.Context ) -var rootCmd = &cobra.Command{ - Use: "chloggen", - Short: "Updates CHANGELOG.MD to include all new changes", - Long: `chloggen is a tool used to automate the generation of CHANGELOG files using individual yaml files as the source.`, +func rootCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "chloggen", + Short: "Updates CHANGELOG.MD to include all new changes", + Long: `chloggen is a tool used to automate the generation of CHANGELOG files using individual yaml files as the source.`, + } + cmd.PersistentFlags().StringVar(&chloggenDir, "chloggen-directory", "", "directory containing unreleased change log entries (default: .chloggen)") + cmd.AddCommand(newCmd()) + cmd.AddCommand(updateCmd()) + cmd.AddCommand(validateCmd()) + return cmd } func Execute() { - cobra.CheckErr(rootCmd.Execute()) + cobra.CheckErr(rootCmd().Execute()) +} + +func init() { + cobra.OnInitialize(initConfig) } func initConfig() { + // Don't override if already set in tests + var uninitialized chlog.Context + if chlogCtx != uninitialized { + return + } + if chloggenDir == "" { chloggenDir = ".chloggen" } chlogCtx = chlog.New(chlog.RepoRoot(), chlog.WithChloggenDir(chloggenDir)) } - -func init() { - cobra.OnInitialize(initConfig) - - rootCmd.PersistentFlags().StringVar(&chloggenDir, "chloggen-directory", "", "directory containing unreleased change log entries (default: .chloggen)") - - rootCmd.AddCommand(newCmd) - rootCmd.AddCommand(updateCmd) - rootCmd.AddCommand(validateCmd) -} diff --git a/chloggen/cmd/root_test.go b/chloggen/cmd/root_test.go new file mode 100644 index 00000000..5e018a4f --- /dev/null +++ b/chloggen/cmd/root_test.go @@ -0,0 +1,51 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +const rootUsage = `chloggen is a tool used to automate the generation of CHANGELOG files using individual yaml files as the source. + +Usage: + chloggen [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + help Help about any command + new Creates new change file + update Updates CHANGELOG.MD to include all new changes + validate Validates the files in the changelog directory + +Flags: + --chloggen-directory string directory containing unreleased change log entries (default: .chloggen) + -h, --help help for chloggen + +Use "chloggen [command] --help" for more information about a command.` + +func TestRootCommand(t *testing.T) { + var out, err string + + out, err = runCobra(t) + assert.Contains(t, out, rootUsage) + assert.Empty(t, err) + + out, err = runCobra(t, "--help") + assert.Contains(t, out, rootUsage) + assert.Empty(t, err) +} diff --git a/chloggen/cmd/update.go b/chloggen/cmd/update.go index dc239485..ed5df674 100644 --- a/chloggen/cmd/update.go +++ b/chloggen/cmd/update.go @@ -35,67 +35,63 @@ var ( dry bool ) -var updateCmd = &cobra.Command{ - Use: "update", - Short: "Updates CHANGELOG.MD to include all new changes", - RunE: func(cmd *cobra.Command, args []string) error { - return update(chlogCtx, version, dry) - }, -} - -func update(ctx chlog.Context, version string, dry bool) error { - entries, err := chlog.ReadEntries(ctx) - if err != nil { - return err - } - - if len(entries) == 0 { - return fmt.Errorf("no entries to add to the changelog") - } - - chlogUpdate, err := chlog.GenerateSummary(version, entries) - if err != nil { - return err - } - - if dry { - fmt.Printf("Generated changelog updates:") - fmt.Println(chlogUpdate) - return nil - } - - oldChlogBytes, err := os.ReadFile(filepath.Clean(ctx.ChangelogMD)) - if err != nil { - return err - } - chlogParts := bytes.Split(oldChlogBytes, []byte(insertPoint)) - if len(chlogParts) != 2 { - return fmt.Errorf("expected one instance of %s", insertPoint) +func updateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Updates CHANGELOG.MD to include all new changes", + RunE: func(cmd *cobra.Command, args []string) error { + entries, err := chlog.ReadEntries(chlogCtx) + if err != nil { + return err + } + + if len(entries) == 0 { + return fmt.Errorf("no entries to add to the changelog") + } + + chlogUpdate, err := chlog.GenerateSummary(version, entries) + if err != nil { + return err + } + + if dry { + cmd.Printf("Generated changelog updates:") + cmd.Println(chlogUpdate) + return nil + } + + oldChlogBytes, err := os.ReadFile(filepath.Clean(chlogCtx.ChangelogMD)) + if err != nil { + return err + } + chlogParts := bytes.Split(oldChlogBytes, []byte(insertPoint)) + if len(chlogParts) != 2 { + return fmt.Errorf("expected one instance of %s", insertPoint) + } + + chlogHeader, chlogHistory := string(chlogParts[0]), string(chlogParts[1]) + + var chlogBuilder strings.Builder + chlogBuilder.WriteString(chlogHeader) + chlogBuilder.WriteString(insertPoint) + chlogBuilder.WriteString(chlogUpdate) + chlogBuilder.WriteString(chlogHistory) + + tmpMD := chlogCtx.ChangelogMD + ".tmp" + if err = os.WriteFile(filepath.Clean(tmpMD), []byte(chlogBuilder.String()), 0600); err != nil { + return err + } + + if err = os.Rename(tmpMD, chlogCtx.ChangelogMD); err != nil { + return err + } + + cmd.Printf("Finished updating %s\n", chlogCtx.ChangelogMD) + + return chlog.DeleteEntries(chlogCtx) + }, } - - chlogHeader, chlogHistory := string(chlogParts[0]), string(chlogParts[1]) - - var chlogBuilder strings.Builder - chlogBuilder.WriteString(chlogHeader) - chlogBuilder.WriteString(insertPoint) - chlogBuilder.WriteString(chlogUpdate) - chlogBuilder.WriteString(chlogHistory) - - tmpMD := ctx.ChangelogMD + ".tmp" - if err = os.WriteFile(filepath.Clean(tmpMD), []byte(chlogBuilder.String()), 0600); err != nil { - return err - } - - if err = os.Rename(tmpMD, ctx.ChangelogMD); err != nil { - return err - } - - fmt.Printf("Finished updating %s\n", ctx.ChangelogMD) - - return chlog.DeleteEntries(ctx) -} - -func init() { - updateCmd.Flags().StringVarP(&version, "version", "v", "vTODO", "will be rendered directly into the update text") - updateCmd.Flags().BoolVarP(&dry, "dry", "d", false, "will generate the update text and print to stdout") + cmd.Flags().StringVarP(&version, "version", "v", "vTODO", "will be rendered directly into the update text") + cmd.Flags().BoolVarP(&dry, "dry", "d", false, "will generate the update text and print to stdout") + return cmd } diff --git a/chloggen/cmd/update_test.go b/chloggen/cmd/update_test.go index f7168f76..bb52afa5 100644 --- a/chloggen/cmd/update_test.go +++ b/chloggen/cmd/update_test.go @@ -15,11 +15,13 @@ package cmd import ( + "fmt" "os" "path/filepath" "runtime" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/build-tools/chloggen/internal/chlog" @@ -85,26 +87,39 @@ func TestUpdateE2E(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - ctx := setupTestDir(t, tc.entries) + chlogCtx = setupTestDir(t, tc.entries) - require.NoError(t, update(ctx, tc.version, tc.dry)) + args := []string{"update", "--version", tc.version} + if tc.dry { + args = append(args, "--dry") + } + + var out, err string + out, err = runCobra(t, args...) + + assert.Empty(t, err) + if tc.dry { + assert.Contains(t, out, "Generated changelog updates:") + } else { + assert.Contains(t, out, fmt.Sprintf("Finished updating %s", chlogCtx.ChangelogMD)) + } - actualBytes, err := os.ReadFile(ctx.ChangelogMD) - require.NoError(t, err) + actualBytes, ioErr := os.ReadFile(chlogCtx.ChangelogMD) + require.NoError(t, ioErr) expectedChangelogMD := filepath.Join("testdata", tc.name+".md") - expectedBytes, err := os.ReadFile(filepath.Clean(expectedChangelogMD)) - require.NoError(t, err) + expectedBytes, ioErr := os.ReadFile(filepath.Clean(expectedChangelogMD)) + require.NoError(t, ioErr) require.Equal(t, string(expectedBytes), string(actualBytes)) - remainingYAMLs, err := filepath.Glob(filepath.Join(ctx.ChloggenDir, "*.yaml")) - require.NoError(t, err) + remainingYAMLs, ioErr := filepath.Glob(filepath.Join(chlogCtx.ChloggenDir, "*.yaml")) + require.NoError(t, ioErr) if tc.dry { require.Equal(t, 1+len(tc.entries), len(remainingYAMLs)) } else { require.Equal(t, 1, len(remainingYAMLs)) - require.Equal(t, ctx.TemplateYAML, remainingYAMLs[0]) + require.Equal(t, chlogCtx.TemplateYAML, remainingYAMLs[0]) } }) } diff --git a/chloggen/cmd/validate.go b/chloggen/cmd/validate.go index 6653eed9..0379db28 100644 --- a/chloggen/cmd/validate.go +++ b/chloggen/cmd/validate.go @@ -15,7 +15,6 @@ package cmd import ( - "fmt" "os" "github.com/spf13/cobra" @@ -23,28 +22,27 @@ import ( "go.opentelemetry.io/build-tools/chloggen/internal/chlog" ) -var validateCmd = &cobra.Command{ - Use: "validate", - Short: "Validates the files in the changelog directory", - RunE: func(cmd *cobra.Command, args []string) error { - return validate(chlogCtx) - }, -} - -func validate(ctx chlog.Context) error { - if _, err := os.Stat(ctx.ChloggenDir); err != nil { - return err - } +func validateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "validate", + Short: "Validates the files in the changelog directory", + RunE: func(cmd *cobra.Command, args []string) error { + if _, err := os.Stat(chlogCtx.ChloggenDir); err != nil { + return err + } - entries, err := chlog.ReadEntries(ctx) - if err != nil { - return err - } - for _, entry := range entries { - if err = entry.Validate(); err != nil { - return err - } + entries, err := chlog.ReadEntries(chlogCtx) + if err != nil { + return err + } + for _, entry := range entries { + if err = entry.Validate(); err != nil { + return err + } + } + cmd.Printf("PASS: all files in %s/ are valid\n", chlogCtx.ChloggenDir) + return nil + }, } - fmt.Printf("PASS: all files in %s/ are valid\n", ctx.ChloggenDir) - return nil + return cmd } diff --git a/chloggen/cmd/validate_test.go b/chloggen/cmd/validate_test.go index a14084af..26d20629 100644 --- a/chloggen/cmd/validate_test.go +++ b/chloggen/cmd/validate_test.go @@ -15,9 +15,10 @@ package cmd import ( + "fmt" "testing" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" "go.opentelemetry.io/build-tools/chloggen/internal/chlog" ) @@ -118,13 +119,15 @@ func TestValidateE2E(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - ctx := setupTestDir(t, tc.entries) + chlogCtx = setupTestDir(t, tc.entries) + + out, err := runCobra(t, "validate") - err := validate(ctx) if tc.wantErr != "" { - require.Regexp(t, tc.wantErr, err) + assert.Regexp(t, tc.wantErr, err) } else { - require.NoError(t, err) + assert.Empty(t, err) + assert.Contains(t, out, fmt.Sprintf("PASS: all files in %s/ are valid", chlogCtx.ChloggenDir)) } }) }