From 4e5a0f8a295b5ad5d598f3fe2c9832ce3eeae569 Mon Sep 17 00:00:00 2001 From: hedhyw Date: Sat, 5 Oct 2024 10:09:09 +0300 Subject: [PATCH] test: cmd package --- cmd/jlv/main.go | 74 ++++++++--- cmd/jlv/main_test.go | 140 ++++++++++++++++++++ cmd/jlv/stdin_reader_mock.go | 30 ----- cmd/jlv/{stdin_reader.go => stdinreader.go} | 2 - 4 files changed, 193 insertions(+), 53 deletions(-) delete mode 100644 cmd/jlv/stdin_reader_mock.go rename cmd/jlv/{stdin_reader.go => stdinreader.go} (93%) diff --git a/cmd/jlv/main.go b/cmd/jlv/main.go index 7e86ed6..847deb8 100644 --- a/cmd/jlv/main.go +++ b/cmd/jlv/main.go @@ -2,8 +2,11 @@ package main import ( "context" + "errors" "flag" "fmt" + "io" + "io/fs" "os" "path" @@ -25,48 +28,80 @@ func main() { printVersion := flag.Bool("version", false, "Print version") flag.Parse() - if *printVersion { + err := runApp(applicationArguments{ + Stdout: os.Stdout, + Stdin: os.Stdin, + + ConfigPath: *configPath, + PrintVersion: *printVersion, + Args: flag.Args(), + + RunProgram: func(p *tea.Program) (tea.Model, error) { + return p.Run() + }, + }) + if err != nil { + fmt.Fprintln(os.Stderr, "Error: "+err.Error()) + os.Exit(1) + } +} + +type applicationArguments struct { + Stdout io.Writer + Stdin fs.File + + ConfigPath string + PrintVersion bool + Args []string + + RunProgram func(*tea.Program) (tea.Model, error) +} + +func runApp(args applicationArguments) (err error) { + if args.PrintVersion { // nolint: forbidigo // Version command. - print("github.com/hedhyw/json-log-viewer@" + version + "\n") + fmt.Fprintln(args.Stdout, "github.com/hedhyw/json-log-viewer@"+version) - return + return nil } - cfg, err := readConfig(*configPath) + cfg, err := readConfig(args.ConfigPath) if err != nil { - fatalf("Error reading config: %s\n", err) + return fmt.Errorf("reading config: %w", err) } fileName := "" var inputSource *source.Source - switch flag.NArg() { + switch len(args.Args) { case 0: // Tee stdin to a temp file, so that we can // lazy load the log entries using random access. fileName = "-" - stdIn, err := getStdinReader(os.Stdin) + stdin, err := getStdinReader(args.Stdin) if err != nil { - fatalf("Stdin: %s\n", err) + return fmt.Errorf("getting stdin: %w", err) } - inputSource, err = source.Reader(stdIn, cfg) + inputSource, err = source.Reader(stdin, cfg) if err != nil { - fatalf("Could not create temp flie: %s\n", err) + return fmt.Errorf("creating a temporary file: %w", err) } - defer inputSource.Close() + defer func() { err = errors.Join(err, inputSource.Close()) }() case 1: - fileName = flag.Arg(0) + fileName = args.Args[0] + inputSource, err = source.File(fileName, cfg) if err != nil { - fatalf("Could not create temp flie: %s\n", err) + return fmt.Errorf("reading file: %w", err) } - defer inputSource.Close() + defer func() { err = errors.Join(err, inputSource.Close()) }() default: - fatalf("Invalid arguments, usage: %s file.log\n", os.Args[0]) + // nolint: err113 // One time case. + return fmt.Errorf("invalid arguments, usage: %s file.log", os.Args[0]) } appModel := app.NewModel(fileName, cfg, version) @@ -80,14 +115,11 @@ func main() { } }) - if _, err := program.Run(); err != nil { - fatalf("Error running program: %s\n", err) + if _, err := args.RunProgram(program); err != nil { + return fmt.Errorf("running program: %w", err) } -} -func fatalf(message string, args ...any) { - fmt.Fprintf(os.Stderr, message, args...) - os.Exit(1) + return nil } // readConfig tries to read config from working directory or home directory. diff --git a/cmd/jlv/main_test.go b/cmd/jlv/main_test.go index 0ffbb5f..25f6642 100644 --- a/cmd/jlv/main_test.go +++ b/cmd/jlv/main_test.go @@ -8,10 +8,150 @@ import ( "os" "testing" + "github.com/hedhyw/json-log-viewer/internal/app" + "github.com/hedhyw/json-log-viewer/internal/pkg/config" + "github.com/hedhyw/json-log-viewer/internal/pkg/tests" + + tea "github.com/charmbracelet/bubbletea" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestRunAppVersion(t *testing.T) { + t.Parallel() + + var outputBuf bytes.Buffer + + err := runApp(applicationArguments{ + Stdout: &outputBuf, + PrintVersion: true, + }) + require.NoError(t, err) + + assert.Contains(t, outputBuf.String(), version) +} + +func TestRunAppRunProgramFailed(t *testing.T) { + t.Parallel() + + fileName := tests.RequireCreateFile(t, []byte(t.Name())) + + err := runApp(applicationArguments{ + Args: []string{fileName}, + RunProgram: func(*tea.Program) (tea.Model, error) { + return nil, tests.ErrTest + }, + }) + require.ErrorIs(t, err, tests.ErrTest) +} + +func TestRunAppRunProgramReadConfigInvalid(t *testing.T) { + t.Parallel() + + configPath := tests.RequireCreateFile(t, []byte("invalid config")) + + err := runApp(applicationArguments{ + ConfigPath: configPath, + RunProgram: func(*tea.Program) (tea.Model, error) { + t.Fatal("Should not run") + + return app.NewModel("", config.GetDefaultConfig(), version), nil + }, + }) + require.Error(t, err) +} + +func TestRunAppUnexpectedNumberOfArgs(t *testing.T) { + t.Parallel() + + err := runApp(applicationArguments{ + Args: []string{"1", "2", "3"}, + }) + require.Error(t, err) +} + +func TestRunAppReadFileSuccess(t *testing.T) { + t.Parallel() + + fileName := tests.RequireCreateFile(t, []byte(t.Name())) + + var isStarted bool + + err := runApp(applicationArguments{ + Args: []string{fileName}, + RunProgram: func(p *tea.Program) (tea.Model, error) { + assert.NotNil(t, p) + isStarted = true + + return app.NewModel("", config.GetDefaultConfig(), version), nil + }, + }) + require.NoError(t, err) + + assert.True(t, isStarted) +} + +func TestRunAppReadFileNotFound(t *testing.T) { + t.Parallel() + + err := runApp(applicationArguments{ + Args: []string{t.Name() + "not found"}, + RunProgram: func(*tea.Program) (tea.Model, error) { + t.Fatal("Should not run") + + return app.NewModel("", config.GetDefaultConfig(), version), nil + }, + }) + require.Error(t, err) +} + +func TestRunAppReadStdinSuccess(t *testing.T) { + t.Parallel() + + fakeStdin := fakeFile{ + Reader: bytes.NewReader([]byte(t.Name())), + StatFileInfo: fakeFileInfo{ + FileMode: os.ModeNamedPipe, + }, + } + + var isStarted bool + + err := runApp(applicationArguments{ + Args: []string{}, + Stdin: fakeStdin, + RunProgram: func(p *tea.Program) (tea.Model, error) { + assert.NotNil(t, p) + isStarted = true + + return app.NewModel("", config.GetDefaultConfig(), version), nil + }, + }) + require.NoError(t, err) + + assert.True(t, isStarted) +} + +func TestRunAppReadStdinStatFailed(t *testing.T) { + t.Parallel() + + fakeStdin := fakeFile{ + Reader: bytes.NewReader([]byte(t.Name())), + ErrStat: tests.ErrTest, + } + + err := runApp(applicationArguments{ + Args: []string{}, + Stdin: fakeStdin, + RunProgram: func(*tea.Program) (tea.Model, error) { + t.Fatal("Should not run") + + return app.NewModel("", config.GetDefaultConfig(), version), nil + }, + }) + require.ErrorIs(t, err, tests.ErrTest) +} + func TestGetStdinSource(t *testing.T) { t.Parallel() diff --git a/cmd/jlv/stdin_reader_mock.go b/cmd/jlv/stdin_reader_mock.go deleted file mode 100644 index c842800..0000000 --- a/cmd/jlv/stdin_reader_mock.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build mock_stdin - -package main - -import ( - "fmt" - "io" - "io/fs" - "os" - "time" -) - -func getStdinReader(defaultInput fs.File) (io.Reader, error) { - r, w, err := os.Pipe() - if err != nil { - return nil, fmt.Errorf("pipe: %w", err) - } - go func() { - defer w.Close() - for i := 0; ; i++ { - _, err := w.Write([]byte(fmt.Sprintf(`{"message": "Line %d"} - `, i))) - if err != nil { - fatalf("Write failed: %s\n", err) - } - time.Sleep(10 * time.Millisecond) - } - }() - return r, nil -} diff --git a/cmd/jlv/stdin_reader.go b/cmd/jlv/stdinreader.go similarity index 93% rename from cmd/jlv/stdin_reader.go rename to cmd/jlv/stdinreader.go index ed330d5..618e1c5 100644 --- a/cmd/jlv/stdin_reader.go +++ b/cmd/jlv/stdinreader.go @@ -1,5 +1,3 @@ -//go:build !mock_stdin - package main import (