diff --git a/go.mod b/go.mod index d79108a..6e37b99 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/jotaen/klog go 1.23 require ( - cloud.google.com/go v0.115.1 - github.com/alecthomas/kong v0.9.0 + cloud.google.com/go v0.116.0 + github.com/alecthomas/kong v1.4.0 github.com/jotaen/genie v0.0.1 github.com/jotaen/kong-completion v0.0.6 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 diff --git a/go.sum b/go.sum index f3b982d..19f5d07 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ -cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ= -cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= -github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= -github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= -github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.4.0 h1:UL7tzGMnnY0YRMMvJyITIRX1EpO6RbBRZDNcCevy3HA= +github.com/alecthomas/kong v1.4.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/klog/app/error.go b/klog/app/error.go index a106bad..25b6c2b 100644 --- a/klog/app/error.go +++ b/klog/app/error.go @@ -43,6 +43,8 @@ type Error interface { // Error returns the error message. Error() string + Is(error) bool + // Details returns additional details, such as a hint how to solve the problem. Details() string @@ -72,6 +74,11 @@ func (e AppError) Error() string { return e.message } +func (e AppError) Is(err error) bool { + _, ok := err.(AppError) + return ok +} + func (e AppError) Details() string { return e.details } @@ -101,6 +108,11 @@ func (pe parserErrors) Error() string { return fmt.Sprintf("%d parsing error(s)", len(pe.errors)) } +func (e parserErrors) Is(err error) bool { + _, ok := err.(parserErrors) + return ok +} + func (pe parserErrors) Details() string { return fmt.Sprintf("%d parsing error(s)", len(pe.errors)) } diff --git a/klog/app/main/cli.go b/klog/app/main/cli.go index bf5200a..cf68fe1 100644 --- a/klog/app/main/cli.go +++ b/klog/app/main/cli.go @@ -67,6 +67,8 @@ func Run(homeDir app.File, meta app.Meta, config app.Config, args []string) (int }), ) if nErr != nil { + // This code branch is not expected to be invoked in practice. If it were to + // happen, that most likely indicates a bug in the app setup. return app.GENERAL_ERROR.ToInt(), errors.New("Internal error: " + nErr.Error()) } @@ -95,15 +97,19 @@ func Run(homeDir app.File, meta app.Meta, config app.Config, args []string) (int kongCtx.BindTo(ctx, (*app.Context)(nil)) rErr := kongCtx.Run() - if rErr != nil { - switch e := rErr.(type) { - case app.ParserErrors: - return e.Code().ToInt(), util.PrettifyParsingError(e, styler) - case app.Error: - return e.Code().ToInt(), util.PrettifyAppError(e, config.IsDebug.Value()) - default: - return app.GENERAL_ERROR.ToInt(), errors.New("Error: " + e.Error()) - } + parserErrors := app.NewParserErrors(nil) + appError := app.NewError("", "", nil) + + switch { + case rErr == nil: + return 0, nil + case errors.As(rErr, &parserErrors): + return parserErrors.Code().ToInt(), util.PrettifyParsingError(parserErrors, styler) + case errors.As(rErr, &appError): + return appError.Code().ToInt(), util.PrettifyAppError(appError, config.IsDebug.Value()) + default: + // This is just a fallback clause; this code branch is not expected to be + // invoked in practice. + return app.GENERAL_ERROR.ToInt(), errors.New("Error: " + rErr.Error()) } - return 0, nil } diff --git a/klog/app/main/cli_test.go b/klog/app/main/cli_test.go index 23a6c71..1c3c1d3 100644 --- a/klog/app/main/cli_test.go +++ b/klog/app/main/cli_test.go @@ -7,240 +7,412 @@ import ( ) func TestHandleInputFiles(t *testing.T) { - out := (&Env{ + (&Env{ files: map[string]string{ "test.klg": "2020-01-01\nSome stuff\n\t1h\n", "foo.klg": "2021-03-02\n 2h #foo", }, - }).run( - []string{"print", "test.klg", "foo.klg"}, - []string{"tags", "foo.klg"}, + }).execute(t, + invocation{ + args: []string{"print", "test.klg", "foo.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "2020-01-01"), out) + assert.True(t, strings.Contains(out, "2021-03-02"), out) + }}, + invocation{ + args: []string{"tags", "foo.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "#foo 2h"), out) + }}, + ) +} + +func TestHandlesInvocationErrors(t *testing.T) { + (&Env{ + files: map[string]string{}, + }).execute(t, + invocation{ + args: []string{"print", "--foo"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 1, code) + assert.True(t, strings.Contains(out, "Invocation error: unknown flag --foo"), out) + }}, ) - // Out 0 like: `2020-01-01\nSome stuff\n 1h\n\n2021-03-02\n 2h` - assert.True(t, strings.Contains(out[0], "2020-01-01"), out) - assert.True(t, strings.Contains(out[0], "2021-03-02"), out) - // Out 1 like: `#foo 2h` - assert.True(t, strings.Contains(out[1], "#foo 2h"), out) } func TestPrintAppErrors(t *testing.T) { - out := (&Env{ + (&Env{ files: map[string]string{ "invalid.klg": "2020-01-01asdf", "valid.klg": "2020-01-01", }, - }).run( - []string{"print", "invalid.klg"}, - []string{"start", "valid.klg"}, - []string{"start", "valid.klg"}, + }).execute(t, + invocation{ + args: []string{"print", "invalid.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 8, code) + assert.True(t, strings.Contains(out, "[SYNTAX ERROR] in line 1 of file"), out) + assert.True(t, strings.Contains(out, "invalid.klg"), out) + assert.True(t, strings.Contains(out, "2020-01-01asdf"), out) + assert.True(t, strings.Contains(out, "^^^^^^^^^^^^^^"), out) + assert.True(t, strings.Contains(out, "Invalid date"), out) + }}, + invocation{ + args: []string{"start", "valid.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + }}, + invocation{ + args: []string{"start", "valid.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 8, code) + assert.True(t, strings.Contains(out, "Error: Manipulation failed"), out) + assert.True(t, strings.Contains(out, "There is already an open range in this record"), out) + }}, ) - // Out 0 should contain pretty-printed parsing errors. - assert.True(t, strings.Contains(out[0], "[SYNTAX ERROR] in line 1 of file"), out) - assert.True(t, strings.Contains(out[0], "invalid.klg"), out) - assert.True(t, strings.Contains(out[0], "2020-01-01asdf"), out) - assert.True(t, strings.Contains(out[0], "^^^^^^^^^^^^^^"), out) - assert.True(t, strings.Contains(out[0], "Invalid date"), out) - // Out 1 should go through without errors. - // Out 2 should then display logical error, since there is an open-range already. - assert.True(t, strings.Contains(out[2], "Error: Manipulation failed"), out) - assert.True(t, strings.Contains(out[2], "There is already an open range in this record"), out) } func TestConfigureAndUseBookmark(t *testing.T) { - klog := &Env{ + (&Env{ files: map[string]string{ "test.klg": "2020-01-01\nSome stuff\n\t1h7m\n", }, - } - out := klog.run( - []string{"bookmarks", "set", "test.klg", "tst"}, - []string{"bookmarks", "set", "test.klg", "tst"}, - []string{"bookmarks", "list"}, - []string{"total", "@tst"}, + }).execute(t, + invocation{ + args: []string{"bookmarks", "set", "test.klg", "tst"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "Created new bookmark"), out) + assert.True(t, strings.Contains(out, "@tst"), out) + assert.True(t, strings.Contains(out, "test.klg"), out) + }}, + invocation{ + args: []string{"bookmarks", "set", "test.klg", "tst"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "Changed bookmark"), out) + assert.True(t, strings.Contains(out, "@tst"), out) + }}, + invocation{ + args: []string{"bookmarks", "list"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "@tst"), out) + }}, + invocation{ + args: []string{"total", "@tst"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "1h7m"), out) + }}, ) - // Out 0 like: `Created new bookmark`, `@tst -> /tmp/.../test.klg` - assert.True(t, strings.Contains(out[0], "Created new bookmark"), out) - assert.True(t, strings.Contains(out[0], "@tst"), out) - assert.True(t, strings.Contains(out[0], "test.klg"), out) - // Out 1 like: `Changed bookmark`, `@tst -> /tmp/.../test.klg` - assert.True(t, strings.Contains(out[1], "Changed bookmark"), out) - assert.True(t, strings.Contains(out[1], "@tst"), out) - // Out 2 like: `@tst -> /tmp/.../test.klg` - assert.True(t, strings.Contains(out[2], "@tst"), out) - // Out 3 like: `Total: 1h7m` - assert.True(t, strings.Contains(out[3], "1h7m"), out) } func TestCreateBookmarkTargetFileOnDemand(t *testing.T) { - klog := &Env{} - out := klog.run( - []string{"bookmarks", "set", "--create", "test.klg", "tst"}, - []string{"bookmarks", "set", "--create", "test.klg", "tst"}, + (&Env{ + files: map[string]string{}, + }).execute(t, + invocation{ + args: []string{"bookmarks", "set", "--create", "test.klg", "tst"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "Created new bookmark and created target file:"), out) + assert.True(t, strings.Contains(out, "@tst"), out) + assert.True(t, strings.Contains(out, "test.klg"), out) + }}, + invocation{ + args: []string{"bookmarks", "set", "--create", "test.klg", "tst"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 1, code) + assert.True(t, strings.Contains(out, "Error: Cannot create file"), out) + assert.True(t, strings.Contains(out, "There is already a file at that location"), out) + }}, ) - // Out 0 like: `Created new bookmark`, `@tst -> /tmp/.../test.klg` - assert.True(t, strings.Contains(out[0], "Created new bookmark and created target file:"), out) - assert.True(t, strings.Contains(out[0], "@tst"), out) - assert.True(t, strings.Contains(out[0], "test.klg"), out) - // Out 1 like: `Error: Cannot create file`, `There is already a file at that location` - assert.True(t, strings.Contains(out[1], "Error: Cannot create file"), out) - assert.True(t, strings.Contains(out[1], "There is already a file at that location"), out) } func TestWriteToFile(t *testing.T) { - klog := &Env{ + (&Env{ files: map[string]string{ "test.klg": "2020-01-01\nSome stuff\n\t1h\n", }, - } - out := klog.run( - []string{"track", "--date", "2020-01-01", "30m", "test.klg"}, - []string{"total", "test.klg"}, + }).execute(t, + invocation{ + args: []string{"track", "--date", "2020-01-01", "30m", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + }}, + invocation{ + args: []string{"total", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "1h30m"), out) + assert.True(t, strings.Contains(out, "1 record"), out) + }}, ) - // Out 1 like: `Total 1h30m (In 1 record)` - assert.True(t, strings.Contains(out[1], "1h30m"), out) - assert.True(t, strings.Contains(out[1], "1 record"), out) } func TestDecodesDate(t *testing.T) { - klog := &Env{ + (&Env{ files: map[string]string{ "test.klg": "2020-01-01\nSome stuff\n\t1h7m\n", }, - } - out := klog.run( - []string{"total", "--date", "2020-1-1", "test.klg"}, - []string{"total", "--date", "2020-01-01", "test.klg"}, + }).execute(t, + invocation{ + args: []string{"total", "--date", "2020-1-1", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 1, code) + assert.True(t, strings.Contains(out, "`2020-1-1` is not a valid date"), out) + }}, + invocation{ + args: []string{"total", "--date", "2020-01-01", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "1h7m"), out) + }}, ) - assert.True(t, strings.Contains(out[0], "`2020-1-1` is not a valid date"), out) - assert.True(t, strings.Contains(out[1], "1h7m"), out) } func TestDecodesTime(t *testing.T) { - klog := &Env{ + (&Env{ files: map[string]string{ "test.klg": "2020-01-01\n\t9:00-?\n", }, - } - out := klog.run( - []string{"stop", "--date", "2020-01-01", "--time", "1:0", "test.klg"}, - []string{"stop", "--date", "2020-01-01", "--time", "10:00", "test.klg"}, - []string{"total", "test.klg"}, + }).execute(t, + invocation{ + args: []string{"stop", "--date", "2020-01-01", "--time", "1:0", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 1, code) + assert.True(t, strings.Contains(out, "`1:0` is not a valid time"), out) + }}, + invocation{ + args: []string{"stop", "--date", "2020-01-01", "--time", "10:00", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "9:00-10:00"), out) + }}, + invocation{ + args: []string{"total", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "1h"), out) + }}, ) - assert.True(t, strings.Contains(out[0], "`1:0` is not a valid time"), out) - assert.True(t, strings.Contains(out[1], "9:00-10:00"), out) - assert.True(t, strings.Contains(out[2], "1h"), out) } func TestDecodesShouldTotal(t *testing.T) { - klog := &Env{ + (&Env{ files: map[string]string{ "test.klg": "", }, - } - out := klog.run( - []string{"create", "--date", "2020-01-01", "--should", "asdf", "test.klg"}, - []string{"create", "--date", "2020-01-01", "--should", "5h1m!", "test.klg"}, - []string{"total", "--diff", "test.klg"}, + }).execute(t, + invocation{ + args: []string{"create", "--date", "2020-01-01", "--should", "asdf", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 1, code) + assert.True(t, strings.Contains(out, "`asdf` is not a valid should total"), out) + }}, + invocation{ + args: []string{"create", "--date", "2020-01-01", "--should", "5h1m!", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "5h1m!"), out) + }}, + invocation{ + args: []string{"total", "--diff", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "5h1m!"), out) + }}, ) - assert.True(t, strings.Contains(out[0], "`asdf` is not a valid should total"), out) - assert.True(t, strings.Contains(out[1], "5h1m!"), out) - assert.True(t, strings.Contains(out[2], "5h1m!"), out) } func TestDecodesPeriod(t *testing.T) { - klog := &Env{ + (&Env{ files: map[string]string{ "test.klg": "2000-01-05\n\t1h\n\n2000-05-24\n\t1h\n", }, - } - out := klog.run( - []string{"total", "--period", "2000", "test.klg"}, - []string{"total", "--period", "2000-01", "test.klg"}, - []string{"total", "--period", "2000-Q1", "test.klg"}, - []string{"total", "--period", "2000-W21", "test.klg"}, - []string{"total", "--period", "foo", "test.klg"}, + }).execute(t, + invocation{ + args: []string{"total", "--period", "2000", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "2h"), out) + }}, + invocation{ + args: []string{"total", "--period", "2000-01", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "1h"), out) + }}, + invocation{ + args: []string{"total", "--period", "2000-Q1", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "1h"), out) + }}, + invocation{ + args: []string{"total", "--period", "2000-W21", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "1h"), out) + }}, + invocation{ + args: []string{"total", "--period", "foo", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 1, code) + assert.True(t, strings.Contains(out, "`foo` is not a valid period"), out) + }}, ) - assert.True(t, strings.Contains(out[0], "2h"), out) - assert.True(t, strings.Contains(out[1], "1h"), out) - assert.True(t, strings.Contains(out[2], "1h"), out) - assert.True(t, strings.Contains(out[3], "1h"), out) - assert.True(t, strings.Contains(out[4], "`foo` is not a valid period"), out) } func TestDecodesRounding(t *testing.T) { - klog := &Env{ + (&Env{ files: map[string]string{ "test.klg": "2020-01-01", }, - } - out := klog.run( - []string{"start", "--round", "asdf", "test.klg"}, - []string{"start", "--round", "30m", "test.klg"}, + }).execute(t, + invocation{ + args: []string{"start", "--round", "asdf", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 1, code) + assert.True(t, strings.Contains(out, "`asdf` is not a valid rounding value"), out) + }}, + invocation{ + args: []string{"start", "--round", "30m", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "- ?"), out) + }}, ) - assert.True(t, strings.Contains(out[0], "`asdf` is not a valid rounding value"), out) - assert.True(t, strings.Contains(out[1], "- ?"), out) } func TestDecodesTags(t *testing.T) { - klog := &Env{ + (&Env{ files: map[string]string{ "test.klg": "2020-01-01\n#foo\n\n2020-01-02\n\t1h #bar=1", }, - } - out := klog.run( - []string{"print", "--tag", "asdf=asdf=asdf", "test.klg"}, - []string{"print", "--tag", "foo&bar", "test.klg"}, - []string{"print", "--tag", "foo", "test.klg"}, - []string{"print", "--tag", "bar=1", "test.klg"}, - []string{"print", "--tag", "#bar='1'", "test.klg"}, + }).execute(t, + invocation{ + args: []string{"print", "--tag", "asdf=asdf=asdf", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 1, code) + assert.True(t, strings.Contains(out, "`asdf=asdf=asdf` is not a valid tag"), out) + }}, + invocation{ + args: []string{"print", "--tag", "foo&bar", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 1, code) + assert.True(t, strings.Contains(out, "`foo&bar` is not a valid tag"), out) + }}, + invocation{ + args: []string{"print", "--tag", "foo", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "#foo"), out) + }}, + invocation{ + args: []string{"print", "--tag", "bar=1", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "#bar=1"), out) + }}, + invocation{ + args: []string{"print", "--tag", "#bar='1'", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "#bar=1"), out) + }}, ) - assert.True(t, strings.Contains(out[0], "`asdf=asdf=asdf` is not a valid tag"), out) - assert.True(t, strings.Contains(out[1], "`foo&bar` is not a valid tag"), out) - assert.True(t, strings.Contains(out[2], "#foo"), out) - assert.True(t, strings.Contains(out[3], "#bar=1"), out) - assert.True(t, strings.Contains(out[4], "#bar=1"), out) } func TestDecodesRecordSummary(t *testing.T) { - klog := &Env{ + (&Env{ files: map[string]string{ "test.klg": "2020-01-01\nTest.", }, - } - out := klog.run( - []string{"create", "--summary", "Foo", "test.klg"}, - []string{"create", "--summary", "Foo\nBar", "test.klg"}, - []string{"create", "--summary", "Foo\n\nBar", "test.klg"}, - []string{"create", "--summary", "Foo\n Bar", "test.klg"}, + }).execute(t, + invocation{ + args: []string{"create", "--summary", "Foo", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "Foo"), out) + }}, + invocation{ + args: []string{"create", "--summary", "Foo\nBar", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "Foo\nBar"), out) + }}, + invocation{ + args: []string{"create", "--summary", "Foo\n\nBar", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 1, code) + assert.True(t, strings.Contains(out, "A record summary cannot contain blank lines"), out) + }}, + invocation{ + args: []string{"create", "--summary", "Foo\n Bar", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 1, code) + assert.True(t, strings.Contains(out, "A record summary cannot contain blank lines"), out) + }}, ) - assert.True(t, strings.Contains(out[0], "Foo"), out) - assert.True(t, strings.Contains(out[1], "Foo\nBar"), out) - assert.True(t, strings.Contains(out[2], "A record summary cannot contain blank lines"), out) - assert.True(t, strings.Contains(out[3], "A record summary cannot contain blank lines"), out) } func TestDecodesEntryType(t *testing.T) { - klog := &Env{ + (&Env{ files: map[string]string{ "test.klg": "2020-01-01\n\t1h\n\t9:00-12:00", }, - } - out := klog.run( - []string{"total", "--entry-type", "duration", "test.klg"}, - []string{"total", "--entry-type", "DURATION", "test.klg"}, - []string{"total", "--entry-type", "duration-positive", "test.klg"}, - []string{"total", "--entry-type", "duration-negative", "test.klg"}, - []string{"total", "--entry-type", "open_range", "test.klg"}, - []string{"total", "--entry-type", "open-range", "test.klg"}, - []string{"total", "--entry-type", "range", "test.klg"}, - []string{"total", "--entry-type", "asdf", "test.klg"}, + }).execute(t, + invocation{ + args: []string{"total", "--entry-type", "duration", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "1h"), out) + }}, + invocation{ + args: []string{"total", "--entry-type", "DURATION", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "1h"), out) + }}, + invocation{ + args: []string{"total", "--entry-type", "duration-positive", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "1h"), out) + }}, + invocation{ + args: []string{"total", "--entry-type", "duration-negative", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "0m"), out) + }}, + invocation{ + args: []string{"total", "--entry-type", "open_range", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "0m"), out) + }}, + invocation{ + args: []string{"total", "--entry-type", "open-range", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "0m"), out) + }}, + invocation{ + args: []string{"total", "--entry-type", "range", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 0, code) + assert.True(t, strings.Contains(out, "3h"), out) + }}, + invocation{ + args: []string{"total", "--entry-type", "asdf", "test.klg"}, + test: func(t *testing.T, code int, out string) { + assert.Equal(t, 1, code) + assert.True(t, strings.Contains(out, "is not a valid entry type"), out) + }}, ) - assert.True(t, strings.Contains(out[0], "1h"), out) - assert.True(t, strings.Contains(out[1], "1h"), out) - assert.True(t, strings.Contains(out[2], "1h"), out) - assert.True(t, strings.Contains(out[3], "0m"), out) - assert.True(t, strings.Contains(out[4], "0m"), out) - assert.True(t, strings.Contains(out[5], "0m"), out) - assert.True(t, strings.Contains(out[6], "3h"), out) - assert.True(t, strings.Contains(out[7], "is not a valid entry type"), out) } diff --git a/klog/app/main/testutil_test.go b/klog/app/main/testutil_test.go index 02f200c..7535b20 100644 --- a/klog/app/main/testutil_test.go +++ b/klog/app/main/testutil_test.go @@ -3,15 +3,23 @@ package klog import ( "github.com/jotaen/klog/klog/app" tf "github.com/jotaen/klog/klog/app/cli/terminalformat" + "github.com/stretchr/testify/require" "io" "os" + "strings" + "testing" ) type Env struct { files map[string]string } -func (e *Env) run(invocation ...[]string) []string { +type invocation struct { + args []string + test func(t *testing.T, code int, out string) +} + +func (e *Env) execute(t *testing.T, is ...invocation) { // Create temp directory and change work dir to it. tmpDir, tErr := os.MkdirTemp("", "") assertNil(tErr) @@ -28,8 +36,7 @@ func (e *Env) run(invocation ...[]string) []string { oldStdout := os.Stdout // Run all commands one after the other. - outs := make([]string, len(invocation)) - for i, args := range invocation { + for _, invoke := range is { r, w, _ := os.Pipe() os.Stdout = w @@ -39,26 +46,24 @@ func (e *Env) run(invocation ...[]string) []string { License: "[License text]", Version: "v0.0", SrcHash: "abc1234", - }, config, args) + }, config, invoke.args) _ = w.Close() - if runErr != nil { - if code == 0 { - panic("App returned error, but exit code was 0") + + t.Run(strings.Join(invoke.args, "__"), func(t *testing.T) { + if runErr != nil { + require.NotEqual(t, 0, code, "App returned error, but exit code was 0") + } else { + out, _ := io.ReadAll(r) + invoke.test(t, code, tf.StripAllAnsiSequences(string(out))) } - outs[i] = runErr.Error() - continue - } - out, _ := io.ReadAll(r) - outs[i] = tf.StripAllAnsiSequences(string(out)) + }) } // Clean up temp dir. rErr := os.RemoveAll(tmpDir) assertNil(rErr) os.Stdout = oldStdout - - return outs } func assertNil(e error) {