From a76dce70620cc2855aaeb03dc2ba1ed847e7a9f6 Mon Sep 17 00:00:00 2001 From: Ethan Zimbelman Date: Sun, 14 Jan 2024 15:46:39 -0800 Subject: [PATCH] test: perform asserts on uniquely named tests (#67) --- .github/workflows/tests.yml | 2 +- CHANGELOG.md | 1 + cmd/etime.go | 5 +- internal/display/format_test.go | 50 ++++-------- internal/display/templates/usage_mock.go | 10 +-- internal/program/flags.go | 4 +- internal/program/flags_test.go | 8 +- main.go | 3 +- pkg/energy/energy_test.go | 86 ++++++------------- pkg/times/time_test.go | 100 ++++++++--------------- 10 files changed, 99 insertions(+), 170 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e61dd21..2ec7aed 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: - name: Check formatting run: test -z $(gofmt -l $(find . -name '*.go')) - name: Run tests - run: go test ./... + run: go test -v ./... measurement: name: Monitor energy usage diff --git a/CHANGELOG.md b/CHANGELOG.md index 0583f86..99ed3fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Maintenance +- Perform asserts on uniquely named tests across packages - Refactor command usage templating into a templates package - Separate concerns of a single internal package into many - Increment the end license year to include this new year diff --git a/cmd/etime.go b/cmd/etime.go index 64181bb..c0a26bf 100644 --- a/cmd/etime.go +++ b/cmd/etime.go @@ -1,7 +1,6 @@ package etime import ( - "os" "os/exec" "github.com/zimeg/emporia-time/internal/program" @@ -18,8 +17,8 @@ type CommandResult struct { } // Setup prepares the command and client with provided inputs -func Setup() (command program.Command, client emporia.Emporia, err error) { - command = program.ParseFlags(os.Args) +func Setup(arguments []string) (command program.Command, client emporia.Emporia, err error) { + command = program.ParseFlags(arguments) if command.Flags.Help { return command, client, err } diff --git a/internal/display/format_test.go b/internal/display/format_test.go index 120b151..3cef598 100644 --- a/internal/display/format_test.go +++ b/internal/display/format_test.go @@ -2,74 +2,60 @@ package display import ( "testing" + + "github.com/stretchr/testify/assert" ) func TestFormatSeconds(t *testing.T) { - tests := []struct { - Title string + tests := map[string]struct { Seconds float64 Expected string }{ - { - Title: "the zero time is preserved", + "the zero time is preserved": { Seconds: 0.0, Expected: "0.00", }, - { - Title: "single digit seconds are matched", + "single digit seconds are matched": { Seconds: 8.05, Expected: "8.05", }, - { - Title: "many seconds passed without formatting", + "many seconds passed without formatting": { Seconds: 12.8, Expected: "12.80", }, - { - Title: "a minute and a few seconds passed", + "a minute and a few seconds passed": { Seconds: 64.46, Expected: "1:04.46", }, - { - Title: "an amount of time greater than a minute", + "an amount of time greater than a minute": { Seconds: 222.22, Expected: "3:42.22", }, - { - Title: "many minutes passed and are formatted", + "many minutes passed and are formatted": { Seconds: 1342.22, Expected: "22:22.22", }, - { - Title: "slightly past one whole hour", + "slightly past one whole hour": { Seconds: 3663.36, Expected: "1:01:03.36", }, - { - Title: "around a third of an hour from seconds", + "around a third of an hour from seconds": { Seconds: 5025.67, Expected: "1:23:45.67", }, - { - Title: "multiple hours were measured", + "multiple hours were measured": { Seconds: 52331.98, Expected: "14:32:11.98", }, - { - Title: "multiple days were counted and shown in hours", + "multiple days were counted and shown in hours": { Seconds: 314159.27, Expected: "87:15:59.27", }, } - for _, tt := range tests { - actual := FormatSeconds(tt.Seconds) - if actual != tt.Expected { - t.Errorf("A time is not formatted correctly!\nTEST: '%s'\nINPUT: %.2f\nEXPECT: %+v\nACTUAL: %+v", - tt.Title, - tt.Seconds, - tt.Expected, - actual, - ) - } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + actual := FormatSeconds(tt.Seconds) + assert.Equalf(t, tt.Expected, actual, "Failed to format %.2f seconds", tt.Seconds) + }) } } diff --git a/internal/display/templates/usage_mock.go b/internal/display/templates/usage_mock.go index af20e68..4e2062b 100644 --- a/internal/display/templates/usage_mock.go +++ b/internal/display/templates/usage_mock.go @@ -9,17 +9,17 @@ type mockUsageStatistics struct { Sureness float64 } -// GetReal returns the joules in a mocked result +// GetReal returns the real time of a mocked command func (results mockUsageStatistics) GetReal() float64 { return results.Real } -// GetUser returns the joules in a mocked result +// GetUser returns the user time of a mocked command func (results mockUsageStatistics) GetUser() float64 { return results.User } -// GetSys returns the joules in a mocked result +// GetSys returns the sys time of a mocked command func (results mockUsageStatistics) GetSys() float64 { return results.Sys } @@ -29,12 +29,12 @@ func (results mockUsageStatistics) GetJoules() float64 { return results.Joules } -// GetWatts returns the joules in a mocked result +// GetWatts returns the watts in a mocked result func (results mockUsageStatistics) GetWatts() float64 { return results.Watts } -// GetSureness returns the joules in a mocked result +// GetSureness returns the sureness of a mocked result func (results mockUsageStatistics) GetSureness() float64 { return results.Sureness } diff --git a/internal/program/flags.go b/internal/program/flags.go index 31d2a7a..c10f1e2 100644 --- a/internal/program/flags.go +++ b/internal/program/flags.go @@ -37,8 +37,8 @@ func ParseFlags(arguments []string) Command { flagset.Usage = templates.PrintHelpMessage flagset.Parse(arguments[1:]) - if len(arguments) <= 1 || flags.Help { - templates.PrintHelpMessage() + if len(arguments) <= 1 { + flags.Help = true } return Command{Args: flagset.Args(), Flags: flags} } diff --git a/internal/program/flags_test.go b/internal/program/flags_test.go index 304f220..6143085 100644 --- a/internal/program/flags_test.go +++ b/internal/program/flags_test.go @@ -10,7 +10,6 @@ func TestParseFlags(t *testing.T) { tests := map[string]struct { arguments []string command Command - makesExit bool }{ "plain arguments are treated as a command": { arguments: []string{"etime", "sleep", "12"}, @@ -58,6 +57,13 @@ func TestParseFlags(t *testing.T) { Flags: Flags{Help: true}, }, }, + "help is noticed when no arguments are provided": { + arguments: []string{"etime"}, + command: Command{ + Args: []string{}, + Flags: Flags{Help: true}, + }, + }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { diff --git a/main.go b/main.go index 91bb98b..dae8920 100644 --- a/main.go +++ b/main.go @@ -12,10 +12,11 @@ import ( // main manages the lifecycle of this program func main() { - command, client, err := etime.Setup() + command, client, err := etime.Setup(os.Args) if err != nil { log.Fatalf("Error: %s", err) } else if command.Flags.Help { + templates.PrintHelpMessage() os.Exit(0) } if available, err := emporia.EmporiaStatus(); err != nil { diff --git a/pkg/energy/energy_test.go b/pkg/energy/energy_test.go index 2c8386c..b22672c 100644 --- a/pkg/energy/energy_test.go +++ b/pkg/energy/energy_test.go @@ -3,119 +3,85 @@ package energy import ( "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestScaleKWhToWs(t *testing.T) { - tests := []struct { - Title string + tests := map[string]struct { KWh float64 ExpectedWs float64 }{ - { - "ensure the zero value is zero", + "ensure the zero value is zero": { 0, 0, }, - { - "convert a single KWh to Ws", + "convert a single KWh to Ws": { 1, 1 * 1000 * 3600, }, - { - "convert 1000 KWh to Ws", + "convert 1000 KWh to Ws": { 1000, 1000 * 1000 * 3600, }, } - - for _, tt := range tests { - actual := ScaleKWhToWs(tt.KWh) - if tt.ExpectedWs != actual { - t.Fatalf("An unexpected energy conversion was found!\nTEST: '%s'\nEXPECT: %8f\nACTUAL: %8f", - tt.Title, - tt.ExpectedWs, - actual, - ) - } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + actual := ScaleKWhToWs(tt.KWh) + assert.Equal(t, tt.ExpectedWs, actual) + }) } } func TestExtrapolateUsage(t *testing.T) { - tests := []struct { - Title string + tests := map[string]struct { Measurements []float64 Duration time.Duration ExpectedResult EnergyResult }{ - { - "handle the measurements of instant commands", + "handle the measurements of instant commands": { []float64{0}, time.Duration(0 * float64(time.Second)), EnergyResult{Joules: 0, Watts: 0, Sureness: 1}, }, - { - "return unsure results if no measurements are returned", + "return unsure results if no measurements are returned": { []float64{}, time.Duration(3 * float64(time.Second)), EnergyResult{Joules: 0, Watts: 0, Sureness: 0}, }, - { - "return unsure results if all measurements are zero", + "return unsure results if all measurements are zero": { []float64{0, 0, 0}, time.Duration(3 * float64(time.Second)), EnergyResult{Joules: 0, Watts: 0, Sureness: 0}, }, - { - "confidently compute results for complete measurements", + "confidently compute results for complete measurements": { []float64{3.64, 4.2, 2}, // sum=9.84, avg=3.28 time.Duration(3 * float64(time.Second)), EnergyResult{Joules: 9.84, Watts: 3.28, Sureness: 1}, }, - { - "extrapolate a missing second of measured results", + "extrapolate a missing second of measured results": { []float64{3, 4, 6, 3}, // sum=16, avg=4 time.Duration(5 * float64(time.Second)), EnergyResult{Joules: 20, Watts: 4, Sureness: 0.8}, }, - { - "extrapolate a half second of undermeasured results", + "extrapolate a half second of undermeasured results": { []float64{3, 4, 6, 3}, // sum=16, avg=4 time.Duration(4.5 * float64(time.Second)), EnergyResult{Joules: 18, Watts: 4, Sureness: 4 / 4.5}, }, - { - "extrapolate a half second of overmeasured results", + "extrapolate a half second of overmeasured results": { []float64{3, 4, 6, 3, 4}, // sum=20, avg=4 time.Duration(4.5 * float64(time.Second)), EnergyResult{Joules: 18, Watts: 4, Sureness: 1}, }, } - - for _, tt := range tests { - actual := ExtrapolateUsage(EnergyMeasurement{ - Chart: tt.Measurements, - Duration: tt.Duration, + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + measurement := EnergyMeasurement{Chart: tt.Measurements, Duration: tt.Duration} + actual := ExtrapolateUsage(measurement) + assert.Equal(t, tt.ExpectedResult.Joules, actual.Joules) + assert.Equal(t, tt.ExpectedResult.Watts, actual.Watts) + assert.Equal(t, tt.ExpectedResult.Sureness, actual.Sureness) }) - if tt.ExpectedResult.Joules != actual.Joules { - t.Fatalf("An unexpected joule estimation was found!\nFAILED: '%s'\nEXPECT: %8f\nACTUAL: %8f", - tt.Title, - tt.ExpectedResult.Joules, - actual.Joules, - ) - } - if tt.ExpectedResult.Watts != actual.Watts { - t.Fatalf("An unexpected watt estimation was found!\nFAILED: '%s'\nEXPECT: %8f\nACTUAL: %8f", - tt.Title, - tt.ExpectedResult.Watts, - actual.Watts, - ) - } - if tt.ExpectedResult.Sureness != actual.Sureness { - t.Fatalf("An unexpected sureness score was found!\nFAILED: '%s'\nEXPECT: %8f\nACTUAL: %8f", - tt.Title, - tt.ExpectedResult.Sureness, - actual.Sureness, - ) - } } } diff --git a/pkg/times/time_test.go b/pkg/times/time_test.go index b004cd6..43ae3c8 100644 --- a/pkg/times/time_test.go +++ b/pkg/times/time_test.go @@ -4,18 +4,18 @@ import ( "bytes" "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestParseTimeResults(t *testing.T) { - tests := []struct { - Title string + tests := map[string]struct { Output []string Times CommandTime CmdOutput []string Error error }{ - { - "check a command like sleep without any outputs", + "check a command like sleep without any outputs": { []string{ "real 1.00", "user 1.00", @@ -29,8 +29,7 @@ func TestParseTimeResults(t *testing.T) { []string{}, nil, }, - { - "capture and remove time values from the output", + "capture and remove time values from the output": { []string{ "example command output!", "real 3754.56", @@ -47,8 +46,7 @@ func TestParseTimeResults(t *testing.T) { }, nil, }, - { - "prefer the latest outputs for timing information", + "prefer the latest outputs for timing information": { []string{ "this command outputs something familiar", "real 3.00", @@ -75,8 +73,7 @@ func TestParseTimeResults(t *testing.T) { }, nil, }, - { - "ensure the parser doesnt break on unexpected values", + "ensure the parser doesnt break on unexpected values": { []string{ "gathering example account information", "user goatish_burr", @@ -100,79 +97,52 @@ func TestParseTimeResults(t *testing.T) { nil, }, } - - for _, tt := range tests { - buff := bytes.NewBufferString(strings.Join(tt.Output, "\n")) - times, cmd, err := parseTimeResults(*buff) - stderr := bytes.NewBufferString(strings.Join(tt.CmdOutput, "\n")) - - if tt.Error != err { - if tt.Error != nil && err != nil && tt.Error.Error() != err.Error() { - t.Fatalf("An unexpected error was encountered!\nTEST: '%s'\nEXPECT: %+v\nACTUAL: %+v", - tt.Title, - tt.Error, - err, - ) + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + buff := bytes.NewBufferString(strings.Join(tt.Output, "\n")) + stdout := bytes.NewBufferString(strings.Join(tt.CmdOutput, "\n")) + times, cmd, err := parseTimeResults(*buff) + if tt.Error != nil { + assert.Error(t, err) + assert.Equal(t, tt.Error, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.Times, times) + if len(stdout.Bytes()) != 0 { + assert.Equal(t, stdout.Bytes(), cmd.Bytes()) + } else { + assert.Nil(t, cmd.Bytes()) + } } - } - if tt.Times != times { - t.Fatalf("Not all times were collected!\nTEST: '%s'\nEXPECT: %+v\nACTUAL: %+v", - tt.Title, - tt.Times, - times, - ) - } - if !bytes.Equal(stderr.Bytes(), cmd.Bytes()) { - t.Fatalf("The correct command output was not retained!\nTEST: '%s'\nEXPECT: %+v\nACTUAL: %+v", - tt.Title, - stderr.String(), - cmd.String(), - ) - } + }) } } func TestParseTimeValue(t *testing.T) { - tests := []struct { - Title string + tests := map[string]struct { Value []string Expected []float64 }{ - { - "retain the necessary leading zero", + "retain the necessary leading zero": { []string{"0.00", "0.08"}, []float64{0.0, 0.08}, }, - { - "ignore padding on seconds as needed", + "ignore padding on seconds as needed": { []string{"2.00", "4.00", "6.02", "12.80", "6.10"}, []float64{2.0, 4.0, 6.02, 12.8, 6.1}, }, - { - "parse times greater than sixty seconds", + "parse times greater than sixty seconds": { []string{"573.66", "261.01", "850.70", "86405.12"}, []float64{573.66, 261.01, 850.7, 86405.12}, }, } - - for _, tt := range tests { - for ii, val := range tt.Value { - trimmed, err := parseTimeValue(val) - if err != nil { - t.Fatalf("An unexpected error was found!\nTEST: '%s'\nEXPECT: %+v\nACTUAL: %+v", - tt.Title, - nil, - err, - ) - } - if trimmed != tt.Expected[ii] { - t.Fatalf("The formatting of time seems off for '%s'!\nTEST: '%s'\nEXPECT: %+v\nACTUAL: %+v", - val, - tt.Title, - tt.Expected[ii], - trimmed, - ) + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + for ii, val := range tt.Value { + trimmed, err := parseTimeValue(val) + assert.NoError(t, err) + assert.Equal(t, tt.Expected[ii], trimmed) } - } + }) } }