diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2cc20e8b..a3710699 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,7 +39,12 @@ jobs: - name: Set up Node.js LTS for running JSON schema tests (using ajv) uses: actions/setup-node@v4 with: - node-version: 18 + node-version: "18" + + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.60 - name: Test run: make test-ci diff --git a/.golangci.yml b/.golangci.yml index e69de29b..e3ae4589 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -0,0 +1,27 @@ +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - contextcheck + - errname + - gocheckcompilerdirectives + - gosec + # - maintidx + - nilnil + - noctx + - nolintlint + - predeclared + - reassign + - sloglint + - spancheck + - unconvert + - unparam + - usestdlibvars + +linters-settings: + gosec: + excludes: + - G204 # Audit the use of command execution + - G404 # Insecure random number source (rand) diff --git a/calendar/event_parse.go b/calendar/event_parse.go index ffb489fb..8cdc8125 100644 --- a/calendar/event_parse.go +++ b/calendar/event_parse.go @@ -29,13 +29,13 @@ var ( expr *regexp.Regexp parseValues []parseFunc }{ - {regexpFullPattern, []parseFunc{parseWeekday(1), parseYear(2), parseMonth(3), parseDay(4), parseHour(5), parseMinute(6), parseSecond(7)}}, + {regexpFullPattern, []parseFunc{parseWeekday(), parseYear(2), parseMonth(3), parseDay(4), parseHour(5), parseMinute(6), parseSecond(7)}}, {regexpDatePattern, []parseFunc{parseYear(1), parseMonth(2), parseDay(3), setMidnight()}}, {regexpTimePattern, []parseFunc{parseHour(1), parseMinute(2), parseSecond(3)}}, {regexpDateTimePattern, []parseFunc{parseYear(1), parseMonth(2), parseDay(3), parseHour(4), parseMinute(5), parseSecond(6)}}, - {regexpWeekdayPattern, []parseFunc{parseWeekday(1), setMidnight()}}, - {regexpWeekdayDatePattern, []parseFunc{parseWeekday(1), parseYear(2), parseMonth(3), parseDay(4), setMidnight()}}, - {regexpWeekdayTimePattern, []parseFunc{parseWeekday(1), parseHour(2), parseMinute(3), parseSecond(4)}}, + {regexpWeekdayPattern, []parseFunc{parseWeekday(), setMidnight()}}, + {regexpWeekdayDatePattern, []parseFunc{parseWeekday(), parseYear(2), parseMonth(3), parseDay(4), setMidnight()}}, + {regexpWeekdayTimePattern, []parseFunc{parseWeekday(), parseHour(2), parseMinute(3), parseSecond(4)}}, } ) @@ -98,7 +98,10 @@ func parseSecond(index int) parseFunc { return func(e *Event, match []string) error { // second can be empty => it means zero if match[index] == "" { - e.Second.AddValue(0) + err := e.Second.AddValue(0) + if err != nil { + return fmt.Errorf("cannot parse second: %w", err) + } return nil } err := e.Second.Parse(strings.Trim(match[index], ":")) @@ -111,16 +114,25 @@ func parseSecond(index int) parseFunc { func setMidnight() parseFunc { return func(e *Event, match []string) error { - e.Hour.AddValue(0) - e.Minute.AddValue(0) - e.Second.AddValue(0) + err := e.Hour.AddValue(0) + if err != nil { + return err + } + err = e.Minute.AddValue(0) + if err != nil { + return err + } + err = e.Second.AddValue(0) + if err != nil { + return err + } return nil } } -func parseWeekday(index int) parseFunc { +func parseWeekday() parseFunc { return func(e *Event, match []string) error { - weekdays := strings.ToLower(match[index]) + weekdays := strings.ToLower(match[1]) // weekday is always the first match for dayIndex, day := range longWeekDay { weekdays = strings.ReplaceAll(weekdays, day, fmt.Sprintf("%02d", dayIndex)) } diff --git a/calendar/value.go b/calendar/value.go index a51abde0..ed704e1f 100644 --- a/calendar/value.go +++ b/calendar/value.go @@ -42,40 +42,40 @@ type Value struct { } // NewValue creates a new value -func NewValue(min, max int) *Value { +func NewValue(minValue, maxValue int) *Value { return &Value{ - minRange: min, - maxRange: max, + minRange: minValue, + maxRange: maxValue, } } // NewValueFromType creates a new value from a predefined type func NewValueFromType(t TypeValue) *Value { - min, max := 0, 0 + minValue, maxValue := 0, 0 switch t { case TypeWeekDay: - min = minDay - max = maxDay - 1 + minValue = minDay + maxValue = maxDay - 1 case TypeYear: - min = 2000 - max = 2200 + minValue = 2000 + maxValue = 2200 case TypeMonth: - min = 1 - max = 12 + minValue = 1 + maxValue = 12 case TypeDay: - min = 1 - max = 31 + minValue = 1 + maxValue = 31 case TypeHour: - max = 23 + maxValue = 23 case TypeMinute: - max = 59 + maxValue = 59 case TypeSecond: - max = 59 + maxValue = 59 } return &Value{ definedType: t, - minRange: min, - maxRange: max, + minRange: minValue, + maxRange: maxValue, } } @@ -157,8 +157,8 @@ func (v *Value) AddValue(value int) error { } // MustAddRange adds a range of values from min to max and panics if an error occurs -func (v *Value) MustAddRange(min int, max int) { - err := v.AddRange(min, max) +func (v *Value) MustAddRange(minValue int, maxValue int) { + err := v.AddRange(minValue, maxValue) if err != nil { panic(err) } diff --git a/calendar/value_test.go b/calendar/value_test.go index 0f7e0b50..47dd6a65 100644 --- a/calendar/value_test.go +++ b/calendar/value_test.go @@ -5,12 +5,13 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestEmptyValue(t *testing.T) { - min := 10 - max := 20 - value := NewValue(min, max) + minValue := 10 + maxValue := 20 + value := NewValue(minValue, maxValue) assert.False(t, value.HasValue()) assert.False(t, value.HasSingleValue()) assert.False(t, value.HasRange()) @@ -22,11 +23,12 @@ func TestEmptyValue(t *testing.T) { } func TestSingleValue(t *testing.T) { - min := 0 - max := 10 + minValue := 0 + maxValue := 10 entry := 5 - value := NewValue(min, max) - value.AddValue(entry) + value := NewValue(minValue, maxValue) + err := value.AddValue(entry) + require.NoError(t, err) assert.True(t, value.HasValue()) assert.True(t, value.HasSingleValue()) assert.False(t, value.HasRange()) @@ -38,12 +40,13 @@ func TestSingleValue(t *testing.T) { } func TestSimpleRangeValue(t *testing.T) { - min := 1 - max := 9 - entries := []int{min, max} - value := NewValue(min, max) + minValue := 1 + maxValue := 9 + entries := []int{minValue, maxValue} + value := NewValue(minValue, maxValue) for _, entry := range entries { - value.AddValue(entry) + err := value.AddValue(entry) + require.NoError(t, err) } assert.True(t, value.HasValue()) assert.False(t, value.HasSingleValue()) @@ -51,17 +54,18 @@ func TestSimpleRangeValue(t *testing.T) { assert.False(t, value.HasContiguousRange()) assert.False(t, value.HasLongContiguousRange()) assert.ElementsMatch(t, entries, value.GetRangeValues()) - assert.ElementsMatch(t, []Range{{min, min}, {max, max}}, value.GetRanges()) - assert.Equal(t, fmt.Sprintf("%02d,%02d", min, max), value.String()) + assert.ElementsMatch(t, []Range{{minValue, minValue}, {maxValue, maxValue}}, value.GetRanges()) + assert.Equal(t, fmt.Sprintf("%02d,%02d", minValue, maxValue), value.String()) } func TestContiguousRangeValue(t *testing.T) { - min := 10 - max := 20 + minValue := 10 + maxValue := 20 entries := []int{11, 12, 14} - value := NewValue(min, max) + value := NewValue(minValue, maxValue) for _, entry := range entries { - value.AddValue(entry) + err := value.AddValue(entry) + require.NoError(t, err) } assert.True(t, value.HasValue()) assert.False(t, value.HasSingleValue()) @@ -74,12 +78,13 @@ func TestContiguousRangeValue(t *testing.T) { } func TestComplexContiguousRanges(t *testing.T) { - min := 10 - max := 20 + minValue := 10 + maxValue := 20 entries := []int{10, 11, 14, 15, 16, 19, 20} - value := NewValue(min, max) + value := NewValue(minValue, maxValue) for _, entry := range entries { - value.AddValue(entry) + err := value.AddValue(entry) + require.NoError(t, err) } assert.True(t, value.HasValue()) assert.False(t, value.HasSingleValue()) @@ -92,12 +97,15 @@ func TestComplexContiguousRanges(t *testing.T) { } func TestAddRanges(t *testing.T) { - min := 10 - max := 20 + minValue := 10 + maxValue := 20 entries := []int{11, 12, 15} - value := NewValue(min, max) - value.AddRange(12, 11) // wrong order on purpose - value.AddRange(15, 15) + value := NewValue(minValue, maxValue) + err := value.AddRange(12, 11) // wrong order on purpose + require.NoError(t, err) + err = value.AddRange(15, 15) + require.NoError(t, err) + assert.True(t, value.HasValue()) assert.False(t, value.HasSingleValue()) assert.True(t, value.HasRange()) @@ -109,11 +117,11 @@ func TestAddRanges(t *testing.T) { } func TestAddValueOutOfRange(t *testing.T) { - min := 10 - max := 20 - entries := []int{min - 1, max + 1} + minValue := 10 + maxValue := 20 + entries := []int{minValue - 1, maxValue + 1} for _, entry := range entries { - value := NewValue(min, max) + value := NewValue(minValue, maxValue) assert.Panics(t, func() { value.MustAddValue(entry) }) @@ -121,12 +129,12 @@ func TestAddValueOutOfRange(t *testing.T) { } func TestAddRangeValuesOutOfRange(t *testing.T) { - min := 10 - max := 20 + minValue := 10 + maxValue := 20 - value := NewValue(min, max) + value := NewValue(minValue, maxValue) assert.Panics(t, func() { - value.MustAddRange(min-1, max-1) + value.MustAddRange(minValue-1, maxValue-1) }) } diff --git a/commands.go b/commands.go index 7f200aa3..99beb757 100644 --- a/commands.go +++ b/commands.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/rand" "encoding/base64" "errors" @@ -10,6 +11,7 @@ import ( "slices" "strconv" "strings" + "time" "github.com/creativeprojects/clog" "github.com/creativeprojects/resticprofile/config" @@ -167,7 +169,7 @@ func completeCommand(output io.Writer, ctx commandContext) error { // Parse requester as first argument. Format "[kind]:v[version]", e.g. "bash:v1" if len(args) > 0 { - matcher := regexp.MustCompile("^(bash|zsh):v(\\d+)$") + matcher := regexp.MustCompile(`^(bash|zsh):v(\d+)$`) if matches := matcher.FindStringSubmatch(args[0]); matches != nil { requester = matches[1] if v, err := strconv.Atoi(matches[2]); err == nil { @@ -570,7 +572,7 @@ func testElevationCommand(_ io.Writer, ctx commandContext) error { return nil } - return elevated(ctx.flags) + return elevated() } func retryElevated(err error, flags commandLineFlags) error { @@ -580,7 +582,7 @@ func retryElevated(err error, flags commandLineFlags) error { // maybe can find a better way than searching for the word "denied"? if platform.IsWindows() && !flags.isChild && strings.Contains(err.Error(), "denied") { clog.Info("restarting resticprofile in elevated mode...") - err := elevated(flags) + err := elevated() if err != nil { return err } @@ -589,7 +591,7 @@ func retryElevated(err error, flags commandLineFlags) error { return err } -func elevated(flags commandLineFlags) error { +func elevated() error { if !platform.IsWindows() { return errors.New("only available on Windows platform") } @@ -601,7 +603,9 @@ func elevated(flags commandLineFlags) error { } err = win.RunElevated(remote.GetPort()) if err != nil { - remote.StopServer() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + remote.StopServer(ctx) return err } diff --git a/commands_display.go b/commands_display.go index 5c3bfc75..2ea43477 100644 --- a/commands_display.go +++ b/commands_display.go @@ -117,7 +117,7 @@ func displayOwnCommandHelp(output io.Writer, commandName string, ctx commandCont out("\t%s %s\n\n", getCommonUsageHelpLine(command.name, command.needConfiguration && !command.noProfile), commandFlags) var flags = make([]string, 0, len(command.flags)) - for f, _ := range command.flags { + for f := range command.flags { flags = append(flags, f) } if len(flags) > 0 { diff --git a/commands_display_test.go b/commands_display_test.go index fd1e3d6f..4dcd673e 100644 --- a/commands_display_test.go +++ b/commands_display_test.go @@ -9,6 +9,7 @@ import ( "github.com/fatih/color" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var ansiColor = func() (c *color.Color) { @@ -299,12 +300,14 @@ https://creativeprojects.github.io/resticprofile/ func TestDisplayVersionVerbose1(t *testing.T) { buffer := &bytes.Buffer{} - displayVersion(buffer, commandContext{Context: Context{flags: commandLineFlags{verbose: true}}}) + err := displayVersion(buffer, commandContext{Context: Context{flags: commandLineFlags{verbose: true}}}) + require.NoError(t, err) assert.True(t, strings.Contains(buffer.String(), runtime.GOOS)) } func TestDisplayVersionVerbose2(t *testing.T) { buffer := &bytes.Buffer{} - displayVersion(buffer, commandContext{Context: Context{request: Request{arguments: []string{"-v"}}}}) + err := displayVersion(buffer, commandContext{Context: Context{request: Request{arguments: []string{"-v"}}}}) + require.NoError(t, err) assert.True(t, strings.Contains(buffer.String(), runtime.GOOS)) } diff --git a/commands_test.go b/commands_test.go index 3a6a9461..3b7d40c9 100644 --- a/commands_test.go +++ b/commands_test.go @@ -208,7 +208,7 @@ func TestGenerateCommand(t *testing.T) { contextWithArguments := func(args []string) commandContext { t.Helper() - return commandContext{Context: Context{request: Request{arguments: args}}} //nolint:exhaustivestruct + return commandContext{Context: Context{request: Request{arguments: args}}} } t.Run("--bash-completion", func(t *testing.T) { diff --git a/complete.go b/complete.go index 58dcbf17..583afd70 100644 --- a/complete.go +++ b/complete.go @@ -137,7 +137,7 @@ func (c *Completer) listProfileNames() (list []string) { if file, err := filesearch.FindConfigurationFile(filename); err == nil { if conf, err := config.LoadFile(file, format); err == nil { list = append(list, conf.GetProfileNames()...) - for name, _ := range conf.GetProfileGroups() { + for name := range conf.GetProfileGroups() { list = append(list, name) } } else { @@ -192,7 +192,7 @@ func (c *Completer) completeOwnCommandFlags(name, word string) (completions []st for _, command := range c.ownCommands { if command.name == name { - for names, _ := range command.flags { // e.g. "-q, --quiet, --size [size|" + for names := range command.flags { // e.g. "-q, --quiet, --size [size|" var flagNames []string for _, flag := range strings.Split(names, ",") { diff --git a/complete_test.go b/complete_test.go index 7971f1da..53e1d0d1 100644 --- a/complete_test.go +++ b/complete_test.go @@ -111,6 +111,7 @@ func TestCompleter(t *testing.T) { testValues := func(flagName string, expected []string) func(t *testing.T) { return func(t *testing.T) { + t.Run("ReturnsAllValues", func(t *testing.T) { actual := completer.Complete(newArgs(fmt.Sprintf("--%s", flagName), "")) assert.Equal(t, expected, actual) @@ -208,7 +209,7 @@ func TestCompleter(t *testing.T) { if !command.hide && !command.hideInCompletion { commands = append(commands, command.name) } - for flag, _ := range command.flags { + for flag := range command.flags { for _, v := range strings.Split(flag, ",") { v = strings.Split(strings.TrimSpace(v), " ")[0] commandValues[command.name] = append(commandValues[command.name], v) diff --git a/config/confidential_test.go b/config/confidential_test.go index 4dbc2498..125b69fc 100644 --- a/config/confidential_test.go +++ b/config/confidential_test.go @@ -76,7 +76,7 @@ func TestFmtStringDoesntLeakConfidentialValues(t *testing.T) { value := NewConfidentialValue("secret") value.hideValue() - assert.Equal(t, ConfidentialReplacement, fmt.Sprintf("%s", value)) + assert.Equal(t, ConfidentialReplacement, value.String()) assert.Equal(t, ConfidentialReplacement, fmt.Sprintf("%v", value)) assert.Equal(t, ConfidentialReplacement, value.String()) assert.Equal(t, "secret", value.Value()) @@ -227,7 +227,7 @@ profile: buffer := &bytes.Buffer{} assert.Nil(t, ShowStruct(buffer, profile, "p")) - result := regexp.MustCompile("\\s+").ReplaceAllString(buffer.String(), " ") + result := regexp.MustCompile(`\s+`).ReplaceAllString(buffer.String(), " ") result = strings.TrimSpace(result) assert.Contains(t, result, "my_value: val") diff --git a/config/config_mixins_test.go b/config/config_mixins_test.go index 2bb7b608..1d23159d 100644 --- a/config/config_mixins_test.go +++ b/config/config_mixins_test.go @@ -3,9 +3,10 @@ package config import ( "bytes" "fmt" + "testing" + "github.com/spf13/cast" "github.com/spf13/viper" - "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -119,6 +120,7 @@ func TestMixin(t *testing.T) { func TestRevolveAppendToListKeys(t *testing.T) { load := func(t *testing.T, config map[string]interface{}) *viper.Viper { + t.Helper() v := viper.New() require.NoError(t, v.MergeConfigMap(config)) return v @@ -259,6 +261,7 @@ mixins: source: ["${named-source}", "${another-source}"] ` load := func(t *testing.T, content string) *Config { + t.Helper() buffer := bytes.NewBufferString(base + content) cfg, err := Load(buffer, FormatYAML) require.NoError(t, err) diff --git a/config/info.go b/config/info.go index 6ca77f37..5f62620e 100644 --- a/config/info.go +++ b/config/info.go @@ -451,7 +451,8 @@ func configurePropertyFromType(p *basicPropertyInfo, valueType reflect.Type) { // joinEmpty joins strings tokens around an empty token, e.g. ["a", "start", "", "end", "b"] turns to ["a", "start-end", "b"] (glue -) // can be used to split strings by a delimiter and allow delimiter escaping by repeating the delimiter -func joinEmpty(input []string, glue string) (output []string) { +func joinEmpty(input []string) (output []string) { + const glue = ";" add := false output = make([]string, 0, len(input)) for i, s := range input { @@ -479,7 +480,7 @@ func configurePropertyFromTags(p *propertyInfo, field *reflect.StructField) { } // default value tag e.g. `default:"DefaultValue"` or `default:"Item1;Item2;Item3"` if value, found := field.Tag.Lookup("default"); found && len(value) > 0 { - p.defaults = joinEmpty(strings.Split(value, ";"), ";") + p.defaults = joinEmpty(strings.Split(value, ";")) } else if !found && !p.IsMultiType() && !p.CanBeNil() { if p.CanBeBool() { p.defaults = []string{"false"} @@ -500,11 +501,11 @@ func configureBasicPropertyFromTags(p *basicPropertyInfo, field *reflect.StructF } // example value tag e.g. `examples:"ExampleValue"` or `examples:"EV1;EV2;EV3"` if value, found := field.Tag.Lookup("examples"); found && len(value) > 0 { - p.examples = joinEmpty(strings.Split(value, ";"), ";") + p.examples = joinEmpty(strings.Split(value, ";")) } // enum value tag e.g. `enum:"PossibleValue1;PossibleValue2;PossibleValue3"` if value, found := field.Tag.Lookup("enum"); found && len(value) > 0 { - p.enum = joinEmpty(strings.Split(value, ";"), ";") + p.enum = joinEmpty(strings.Split(value, ";")) } // format tag e.g. `format:"time"` if value, found := field.Tag.Lookup("format"); found && formatPattern.MatchString(value) { @@ -516,7 +517,7 @@ func configureBasicPropertyFromTags(p *basicPropertyInfo, field *reflect.StructF } // value validation tag e.g. `range:"[-6:10]"` (min -6 to max 10) or `range:"[:10|" (min inf - max 10 exclusive)` if value, found := field.Tag.Lookup("range"); found && rangePattern.MatchString(value) { - if m := rangePattern.FindStringSubmatch(value); m != nil && len(m) == 5 { + if m := rangePattern.FindStringSubmatch(value); len(m) == 5 { if from, err := strconv.ParseFloat(m[2], 64); len(m[2]) > 0 && err == nil { p.from = &from } diff --git a/config/info_customizer.go b/config/info_customizer.go index fcb0f92f..2fd61c44 100644 --- a/config/info_customizer.go +++ b/config/info_customizer.go @@ -85,7 +85,7 @@ func init() { if sectionName == constants.CommandBackup { if propertyName != constants.ParameterHost { info.examples = info.examples[1:] // remove "true" from examples of backup section - note = fmt.Sprintf(`Boolean true is unsupported in section "backup".`) + note = `Boolean true is unsupported in section "backup".` } else { note += suffixDefaultTrueV2 } @@ -121,7 +121,6 @@ func init() { basic.mayNil = true } } - return }) // Profile or Group: special handling for ConfidentialValue diff --git a/config/info_test.go b/config/info_test.go index a22e1f72..7c7b010a 100644 --- a/config/info_test.go +++ b/config/info_test.go @@ -117,13 +117,14 @@ func TestJoinByEmpty(t *testing.T) { } for _, test := range tests { t.Run(test.from, func(t *testing.T) { - assert.Equal(t, test.to, strings.Join(joinEmpty(strings.Split(test.from, ";"), ";"), "|")) + assert.Equal(t, test.to, strings.Join(joinEmpty(strings.Split(test.from, ";")), "|")) }) } } func TestPropertySetFromType(t *testing.T) { type nestedData struct { + //nolint:unused i1 string `mapstructure:"nested-string"` } @@ -168,6 +169,7 @@ func TestPropertySetFromType(t *testing.T) { set := propertySetFromType(reflect.TypeOf(testData)) propertyInfo := func(t *testing.T, propertyName string) (info PropertyInfo) { + t.Helper() require.Contains(t, set.Properties(), propertyName) info = set.PropertyInfo(propertyName) require.NotNil(t, info) @@ -354,7 +356,7 @@ func TestNewProfileInfo(t *testing.T) { info := NewProfileInfo(false) assert.Contains(t, info.Properties(), "insecure-tls") - for name, _ := range NewProfile(nil, "").AllSections() { + for name := range NewProfile(nil, "").AllSections() { si := info.SectionInfo(name) require.NotNil(t, si, "section: %s", name) assert.True(t, si.IsCommandSection(), "section: %s", name) diff --git a/config/jsonschema/model.go b/config/jsonschema/model.go index 08a397ed..fbb8c521 100644 --- a/config/jsonschema/model.go +++ b/config/jsonschema/model.go @@ -405,8 +405,8 @@ func verifySchemaRegex(pattern string) (err error) { // walkTypes walks into all SchemaType items from start, passing them to callback (including start). // The callback can return item, nil or a new SchemaType as it would like to continue to walk deeper, // stop walking deeper or replace item with a new SchemaType. -func walkTypes(start SchemaType, callback func(item SchemaType) SchemaType) SchemaType { - return internalWalkTypes(make(map[SchemaType]bool), start, callback) +func walkTypes(start SchemaType, callback func(item SchemaType) SchemaType) { + internalWalkTypes(make(map[SchemaType]bool), start, callback) } func internalWalkTypes(into map[SchemaType]bool, current SchemaType, callback func(item SchemaType) SchemaType) SchemaType { diff --git a/config/jsonschema/model_test.go b/config/jsonschema/model_test.go index ced63ce1..6ede9d42 100644 --- a/config/jsonschema/model_test.go +++ b/config/jsonschema/model_test.go @@ -103,6 +103,7 @@ func TestTypeSerialization(t *testing.T) { } func extractType[T any](t *testing.T, st any) *T { + t.Helper() require.IsType(t, new(T), st) require.NotNil(t, st) return st.(*T) @@ -156,6 +157,7 @@ func TestReferences(t *testing.T) { func TestVerify(t *testing.T) { testBase := func(t *testing.T, base *schemaTypeBase) { + t.Helper() require.NotEmpty(t, validTypeNames) require.NotNil(t, base) diff --git a/config/jsonschema/schema.go b/config/jsonschema/schema.go index a3f3d99a..56cfaa11 100644 --- a/config/jsonschema/schema.go +++ b/config/jsonschema/schema.go @@ -79,7 +79,7 @@ func isCompatibleValue(schema SchemaType, value any) (ok bool) { func isDefaultValueForType(value any) bool { switch v := value.(type) { case bool: - return v == false + return !v case int64: return v == 0 case float64: diff --git a/config/jsonschema/schema_test.go b/config/jsonschema/schema_test.go index 77b5ae9e..e5e75b85 100644 --- a/config/jsonschema/schema_test.go +++ b/config/jsonschema/schema_test.go @@ -32,6 +32,7 @@ import ( // a pure go solution that is maintained and support required standards func findNpm(t *testing.T) string { + t.Helper() if npm, err := exec.LookPath("npm"); err != nil { t.Log("npm not found, some tests may be skipped") return "" @@ -44,8 +45,10 @@ func findNpm(t *testing.T) string { type npmRunnerFunc func(t *testing.T, args ...string) error func npmRunner(t *testing.T) npmRunnerFunc { + t.Helper() npmExecutable := findNpm(t) return func(t *testing.T, args ...string) (err error) { + t.Helper() t.Log(args) if npmExecutable != "" { cmd := exec.Command(npmExecutable, args...) @@ -69,6 +72,7 @@ func npmRunner(t *testing.T) npmRunnerFunc { var npm npmRunnerFunc func initNpmEnv(t *testing.T) { + t.Helper() if npm == nil { npm = npmRunner(&testing.T{}) if npm(t, "list", "ajv") != nil { @@ -79,6 +83,7 @@ func initNpmEnv(t *testing.T) { } func createSchema(t *testing.T, version config.Version) string { + t.Helper() file, err := os.Create(path.Join(t.TempDir(), fmt.Sprintf("schema-%d.json", version))) require.NoError(t, err) @@ -125,6 +130,7 @@ func TestJsonSchemaValidation(t *testing.T) { schema2 := createSchema(t, config.Version02) rewriteToJson := func(t *testing.T, filename string) string { + t.Helper() v := viper.New() v.SetConfigFile(filename) if strings.HasSuffix(filename, ".conf") { @@ -133,7 +139,7 @@ func TestJsonSchemaValidation(t *testing.T) { require.NoError(t, v.ReadInConfig()) v.SetConfigType("json") - filename = filepath.Join(t.TempDir(), fmt.Sprintf(filepath.Base(filename)+".json")) + filename = filepath.Join(t.TempDir(), filepath.Base(filename)+".json") require.NoError(t, v.WriteConfigAs(filename)) return filename } @@ -264,6 +270,7 @@ var propertySetDefaults = map[string]any{ } func setupMock(t *testing.T, m *mock.Mock, defs map[string]any) { + t.Helper() for method, value := range defs { if !m.IsMethodCallable(t, method) { m.On(method).Return(value).Maybe() @@ -284,6 +291,7 @@ func TestDescription(t *testing.T) { } assertDescription := func(t *testing.T, expected string, info *mocks.PropertyInfo) { + t.Helper() assert.Equal(t, expected, getDescription(info)) info.AssertExpectations(t) } @@ -478,6 +486,7 @@ func TestTypesFromPropertyInfo(t *testing.T) { // number range {info: np(config.NumericRange{From: util.CopyRef(1.0)}), types: []any{"number"}, check: func(t *testing.T, types []SchemaType) { + t.Helper() n := types[0].(*schemaNumber) assert.Equal(t, util.CopyRef(1.0), n.Minimum) assert.Nil(t, n.Maximum) @@ -485,6 +494,7 @@ func TestTypesFromPropertyInfo(t *testing.T) { assert.Nil(t, n.ExclusiveMaximum) }}, {info: np(config.NumericRange{To: util.CopyRef(1.0)}), types: []any{"number"}, check: func(t *testing.T, types []SchemaType) { + t.Helper() n := types[0].(*schemaNumber) assert.Nil(t, n.Minimum) assert.Equal(t, util.CopyRef(1.0), n.Maximum) @@ -497,6 +507,7 @@ func TestTypesFromPropertyInfo(t *testing.T) { FromExclusive: true, ToExclusive: true, }), types: []any{"number"}, check: func(t *testing.T, types []SchemaType) { + t.Helper() n := types[0].(*schemaNumber) assert.Equal(t, util.CopyRef(0.1), n.ExclusiveMinimum) assert.Equal(t, util.CopyRef(1.0), n.ExclusiveMaximum) @@ -506,20 +517,24 @@ func TestTypesFromPropertyInfo(t *testing.T) { // string {info: sp("some-format", ""), types: []any{"string"}, check: func(t *testing.T, types []SchemaType) { + t.Helper() s := types[0].(*schemaString) assert.Equal(t, stringFormat("some-format"), s.Format) // validation is not performed here }}, {info: sp("duration", ""), types: []any{"string"}, check: func(t *testing.T, types []SchemaType) { + t.Helper() s := types[0].(*schemaString) assert.Equal(t, stringFormat(""), s.Format) assert.Equal(t, durationPattern, s.Pattern) }}, {info: sp("duration", "custom-pattern"), types: []any{"string"}, check: func(t *testing.T, types []SchemaType) { + t.Helper() s := types[0].(*schemaString) assert.Equal(t, stringFormat(""), s.Format) assert.Equal(t, "custom-pattern", s.Pattern) }}, {info: sp("", "]some-pattern["), types: []any{"string"}, check: func(t *testing.T, types []SchemaType) { + t.Helper() s := types[0].(*schemaString) assert.Equal(t, "]some-pattern[", s.Pattern) // validation is not performed here }}, diff --git a/config/mocks/NamedPropertySet.go b/config/mocks/NamedPropertySet.go index d2a8fc80..4cface2c 100644 --- a/config/mocks/NamedPropertySet.go +++ b/config/mocks/NamedPropertySet.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.44.2. DO NOT EDIT. package mocks diff --git a/config/mocks/ProfileInfo.go b/config/mocks/ProfileInfo.go index df08131c..50942494 100644 --- a/config/mocks/ProfileInfo.go +++ b/config/mocks/ProfileInfo.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.44.2. DO NOT EDIT. package mocks diff --git a/config/mocks/PropertyInfo.go b/config/mocks/PropertyInfo.go index 19e15a1b..7ee42d0a 100644 --- a/config/mocks/PropertyInfo.go +++ b/config/mocks/PropertyInfo.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.44.2. DO NOT EDIT. package mocks diff --git a/config/mocks/SectionInfo.go b/config/mocks/SectionInfo.go index 38dd7bc9..8d43a28c 100644 --- a/config/mocks/SectionInfo.go +++ b/config/mocks/SectionInfo.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.44.2. DO NOT EDIT. package mocks diff --git a/config/path_test.go b/config/path_test.go index cb4cabd5..d5729eeb 100644 --- a/config/path_test.go +++ b/config/path_test.go @@ -106,6 +106,7 @@ func TestEvaluateSymlinks(t *testing.T) { var rawDir, dir string setup := func(t *testing.T) { + t.Helper() var err error rawDir = t.TempDir() dir, err = filepath.EvalSymlinks(rawDir) @@ -113,6 +114,7 @@ func TestEvaluateSymlinks(t *testing.T) { } link := func(t *testing.T, path, linkname string) { + t.Helper() _ = os.Mkdir(filepath.Join(rawDir, path), 0700) require.NoError(t, os.Symlink(filepath.Join(rawDir, path), filepath.Join(rawDir, linkname))) } diff --git a/config/profile.go b/config/profile.go index 0e34ca51..d968efaa 100644 --- a/config/profile.go +++ b/config/profile.go @@ -127,7 +127,7 @@ type InitSection struct { func (i *InitSection) IsEmpty() bool { return i == nil } -func (i *InitSection) resolve(p *Profile) { +func (i *InitSection) resolve(_ *Profile) { i.FromRepository.setValue(fixPath(i.FromRepository.Value(), expandEnv, expandUserHome)) } @@ -257,7 +257,7 @@ func (r *RetentionSection) resolve(profile *Profile) { if profile.config != nil && profile.config.version >= Version02 { // Auto-enable "after-backup" if nothing was specified explicitly and any "keep-" was configured if r.AfterBackup.IsUndefined() && r.BeforeBackup.IsUndefined() { - for name, _ := range r.OtherFlags { + for name := range r.OtherFlags { if strings.HasPrefix(name, "keep-") { r.AfterBackup = maybe.True() break diff --git a/config/profile_test.go b/config/profile_test.go index 59a7b955..f1aa77ad 100644 --- a/config/profile_test.go +++ b/config/profile_test.go @@ -22,12 +22,14 @@ import ( ) func runForVersions(t *testing.T, runner func(t *testing.T, version, prefix string)) { + t.Helper() t.Run("V1", func(t *testing.T) { runner(t, "version=1\n", "") }) t.Run("V2", func(t *testing.T) { runner(t, "version=2\n", "profiles.") }) } func TestNoProfile(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() testConfig := version + "" profile, err := getProfile("toml", testConfig, "profile", "") assert.ErrorIs(t, err, ErrNotFound) @@ -37,6 +39,7 @@ func TestNoProfile(t *testing.T) { func TestProfileNotFound(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() testConfig := version + "[" + prefix + "profile]\n" profile, err := getProfile("toml", testConfig, "other", "") assert.ErrorIs(t, err, ErrNotFound) @@ -46,6 +49,7 @@ func TestProfileNotFound(t *testing.T) { func TestEmptyProfile(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() testConfig := version + "[" + prefix + "profile]\n" profile, err := getProfile("toml", testConfig, "profile", "") if err != nil { @@ -58,6 +62,7 @@ func TestEmptyProfile(t *testing.T) { func TestNoInitializeValue(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() testConfig := version + "[" + prefix + "profile]\n" profile, err := getProfile("toml", testConfig, "profile", "") if err != nil { @@ -70,6 +75,7 @@ func TestNoInitializeValue(t *testing.T) { func TestInitializeValueFalse(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() testConfig := version + `[` + prefix + `profile] initialize = false ` @@ -84,6 +90,7 @@ initialize = false func TestInitializeValueTrue(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() testConfig := version + `[` + prefix + `profile] initialize = true ` @@ -98,6 +105,7 @@ initialize = true func TestInheritedInitializeValueTrue(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() testConfig := version + `[` + prefix + `parent] initialize = true @@ -115,6 +123,7 @@ inherit = "parent" func TestOverriddenInitializeValueFalse(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() testConfig := version + `[` + prefix + `parent] initialize = true @@ -133,6 +142,7 @@ inherit = "parent" func TestUnknownParent(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() testConfig := version + `[` + prefix + `profile] inherit = "parent" ` @@ -143,6 +153,7 @@ inherit = "parent" func TestMultiInheritance(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() testConfig := version + ` [` + prefix + `grand-parent] repository = "grand-parent" @@ -208,6 +219,7 @@ inherit = "parent" func TestProfileCommonFlags(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() assert := assert.New(t) testConfig := version + ` [` + prefix + `profile] @@ -231,6 +243,7 @@ repository = "test" func TestProfileOtherFlags(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() assert := assert.New(t) testConfig := version + ` [` + prefix + `profile] @@ -276,6 +289,7 @@ array2 = ["one", "two"] func TestEnvironmentInProfileRepo(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() testConfig := version + ` [` + prefix + `profile] repository = "~/$TEST_VAR" @@ -307,6 +321,7 @@ func TestEnvironmentInProfileRepo(t *testing.T) { func TestGetEnvironment(t *testing.T) { runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() testConfig := version + ` [` + prefix + `profile] # filling description with profile's env file to test auto-append @@ -339,7 +354,9 @@ func TestGetEnvironment(t *testing.T) { require.FileExists(t, envFile) assert.Contains(t, profile.EnvironmentFiles, envFile) - defer os.Truncate(envFile, 0) + defer func() { + _ = os.Truncate(envFile, 0) + }() assert.NoError(t, os.WriteFile(envFile, []byte("K2=V2"), 0600)) env := profile.GetEnvironment(false) @@ -361,6 +378,7 @@ func TestSetRootInProfileUnix(t *testing.T) { t.SkipNow() } runForVersions(t, func(t *testing.T, version, prefix string) { + t.Helper() testConfig := version + ` [` + prefix + `profile] base-dir = "~" @@ -602,6 +620,7 @@ source = "` + sourcePattern + `" func TestResolveSourcesWithFlagPrefixInBackup(t *testing.T) { backupSource := func(t *testing.T, source string) []string { + t.Helper() testConfig := ` [profile.backup] source = "` + source + `" @@ -693,6 +712,7 @@ func TestPathAndTagInRetention(t *testing.T) { flatBackupTags := func() []string { return []string{strings.Join(backupTags, ",")} } testProfileWithBase := func(t *testing.T, version Version, retention, baseDir string) *Profile { + t.Helper() prefix := "" if version > Version01 { prefix = "profiles." @@ -730,11 +750,13 @@ func TestPathAndTagInRetention(t *testing.T) { } testProfile := func(t *testing.T, version Version, retention string) *Profile { + t.Helper() return testProfileWithBase(t, version, retention, "") } flagGetter := func(flagName string) func(t *testing.T, profile *Profile) any { return func(t *testing.T, profile *Profile) any { + t.Helper() flags := profile.GetRetentionFlags().ToMap() assert.NotNil(t, flags) return flags[flagName] @@ -743,6 +765,7 @@ func TestPathAndTagInRetention(t *testing.T) { t.Run("AutoEnable", func(t *testing.T) { retentionDisabled := func(t *testing.T, profile *Profile) { + t.Helper() assert.False(t, profile.Retention.BeforeBackup.HasValue()) assert.False(t, profile.Retention.AfterBackup.HasValue()) } @@ -1471,7 +1494,7 @@ func TestGetInitStructFields(t *testing.T) { } func TestGetCopyStructFields(t *testing.T) { - copy := &CopySection{ + copySection := &CopySection{ Repository: NewConfidentialValue("dest-repo"), RepositoryFile: "dest-repo-file", PasswordFile: "dest-pw-file", @@ -1479,7 +1502,7 @@ func TestGetCopyStructFields(t *testing.T) { KeyHint: "dest-key-hint", } - copy.OtherFlags = map[string]any{"option": "opt=dest"} + copySection.OtherFlags = map[string]any{"option": "opt=dest"} profile := NewProfile(nil, "") profile.Repository = NewConfidentialValue("src-repo") @@ -1508,7 +1531,7 @@ func TestGetCopyStructFields(t *testing.T) { "repository-file": {"src-repo-file"}, "password-file": {"src-pw-file"}, "password-command": {"src-pw-command"}, - }, copy.getCommandFlags(profile).ToMap()) + }, copySection.getCommandFlags(profile).ToMap()) // init assert.Equal(t, map[string][]string{ @@ -1526,7 +1549,7 @@ func TestGetCopyStructFields(t *testing.T) { "repository-file": {"dest-repo-file"}, "password-file": {"dest-pw-file"}, "password-command": {"dest-pw-command"}, - }, copy.getInitFlags(profile).ToMap()) + }, copySection.getInitFlags(profile).ToMap()) }) t.Run("restic>=14", func(t *testing.T) { @@ -1547,7 +1570,7 @@ func TestGetCopyStructFields(t *testing.T) { "repository-file": {"dest-repo-file"}, "password-file": {"dest-pw-file"}, "password-command": {"dest-pw-command"}, - }, copy.getCommandFlags(profile).ToMap()) + }, copySection.getCommandFlags(profile).ToMap()) // init assert.Equal(t, map[string][]string{ @@ -1565,13 +1588,12 @@ func TestGetCopyStructFields(t *testing.T) { "repository-file": {"dest-repo-file"}, "password-file": {"dest-pw-file"}, "password-command": {"dest-pw-command"}, - }, copy.getInitFlags(profile).ToMap()) + }, copySection.getInitFlags(profile).ToMap()) }) t.Run("get-init-flags-from-profile", func(t *testing.T) { - p := &(*profile) - assert.Nil(t, p.GetCopyInitializeFlags()) - p.Copy = copy - assert.Equal(t, copy.getInitFlags(p).GetAll(), p.GetCopyInitializeFlags().GetAll()) + assert.Nil(t, profile.GetCopyInitializeFlags()) + profile.Copy = copySection + assert.Equal(t, copySection.getInitFlags(profile).GetAll(), profile.GetCopyInitializeFlags().GetAll()) }) } diff --git a/config/schedule_test.go b/config/schedule_test.go index d6028faa..884f85fb 100644 --- a/config/schedule_test.go +++ b/config/schedule_test.go @@ -17,6 +17,7 @@ import ( func TestNewSchedule(t *testing.T) { profile := func(t *testing.T, config string) (*Profile, ScheduleConfigOrigin) { + t.Helper() if !strings.Contains(config, "[default") { config += "\n[default]" } @@ -207,6 +208,7 @@ func TestNewSchedule(t *testing.T) { func TestNewScheduleFromGroup(t *testing.T) { group := func(t *testing.T, config string) (*Group, ScheduleConfigOrigin) { + t.Helper() config = "version = \"2\"\n\n" + config if !strings.Contains(config, "profiles =") { config += ` diff --git a/config/show.go b/config/show.go index 7b23a896..2bfbbdac 100644 --- a/config/show.go +++ b/config/show.go @@ -67,6 +67,9 @@ func showField(stack []string, display *Display, fieldType *reflect.StructField, if getStringer(fieldValue) != nil { // Pass as is. The struct has its own String() implementation err = showKeyValue(stack, display, key, fieldValue) + if err != nil { + break + } } if key == ",squash" { diff --git a/context_test.go b/context_test.go index a4621433..cecd55a7 100644 --- a/context_test.go +++ b/context_test.go @@ -14,7 +14,7 @@ func TestContextClone(t *testing.T) { binary: "test", } clone := ctx.clone() - assert.False(t, ctx == clone) // different pointers + assert.NotSame(t, ctx, clone) // different pointers assert.Equal(t, ctx, clone) // same values } diff --git a/crond/crontab_test.go b/crond/crontab_test.go index 4bf74f17..baeead57 100644 --- a/crond/crontab_test.go +++ b/crond/crontab_test.go @@ -160,6 +160,7 @@ func TestRemoveCrontab(t *testing.T) { func TestFromFile(t *testing.T) { file, err := filepath.Abs(filepath.Join(t.TempDir(), "crontab")) + require.NoError(t, err) crontab := NewCrontab([]Entry{NewEntry(calendar.NewEvent(func(event *calendar.Event) { event.Minute.MustAddValue(1) @@ -191,6 +192,8 @@ func getExpectedUser(crontab *Crontab) (expectedUser string) { func TestFromFileDetectsUserColumn(t *testing.T) { file, err := filepath.Abs(filepath.Join(t.TempDir(), "crontab")) + require.NoError(t, err) + userLine := `17 * * * * root cd / && run-parts --report /etc/cron.hourly` require.NoError(t, os.WriteFile(file, []byte("\n"+userLine+"\n"), 0600)) cmdLine := "resticprofile --no-ansi --config config.yaml --name profile backup" @@ -232,6 +235,8 @@ func TestNewCrontabWithCurrentUser(t *testing.T) { func TestNoLoadCurrentFromNoEditFile(t *testing.T) { file, err := filepath.Abs(filepath.Join(t.TempDir(), "crontab")) + require.NoError(t, err) + assert.NoError(t, os.WriteFile(file, []byte("# DO NOT EDIT THIS FILE \n#\n#\n"), 0600)) crontab := NewCrontab(nil) diff --git a/crond/io.go b/crond/io.go index afcf328c..85da19d4 100644 --- a/crond/io.go +++ b/crond/io.go @@ -46,6 +46,7 @@ func loadCrontabFile(file string) (content, charset string, err error) { return } +//nolint:unparam func saveCrontabFile(file, content, charset string) (err error) { if err = verifyCrontabFile(file); err != nil { return diff --git a/crond/mock/main.go b/crond/mock/main.go index f4e9ea7a..251c5a40 100644 --- a/crond/mock/main.go +++ b/crond/mock/main.go @@ -36,6 +36,10 @@ func main() { if _, err := io.Copy(sb, os.Stdin); err == nil { if stdin := strings.TrimSpace(sb.String()); stdin != crontab { err = fmt.Errorf("%q != %q", crontab, stdin) + if err != nil { + fmt.Fprintln(os.Stderr, err) + // os.Exit(1) + } } } } else { diff --git a/filesearch/filesearch_test.go b/filesearch/filesearch_test.go index dac8d0c5..383ce830 100644 --- a/filesearch/filesearch_test.go +++ b/filesearch/filesearch_test.go @@ -267,7 +267,7 @@ func TestFindResticBinaryWithTilde(t *testing.T) { require.NoError(t, err) tempFile.Close() defer func() { - fs.Remove(tempFile.Name()) + _ = fs.Remove(tempFile.Name()) }() search := filepath.Join("~", filepath.Base(tempFile.Name())) diff --git a/flags_test.go b/flags_test.go index b491c3c0..aee75594 100644 --- a/flags_test.go +++ b/flags_test.go @@ -79,6 +79,8 @@ func TestEnvOverrides(t *testing.T) { } load := func(t *testing.T, args ...string) commandLineFlags { + t.Helper() + _, loaded, err := loadFlags(args) assert.NoError(t, err) flags.resticArgs = loaded.resticArgs diff --git a/integration_test.go b/integration_test.go index e19d0488..9eb38254 100644 --- a/integration_test.go +++ b/integration_test.go @@ -19,7 +19,7 @@ import ( func TestFromConfigFileToCommandLine(t *testing.T) { files, err := filepath.Glob("./examples/integration_test.*") require.NoError(t, err) - require.Greater(t, len(files), 0) + require.NotEmpty(t, files) // we can use the same files to test a glob pattern globFiles := "\"" + strings.Join(files, "\" \"") + "\"" diff --git a/lock/lock_test.go b/lock/lock_test.go index de2d4e77..1bc92003 100644 --- a/lock/lock_test.go +++ b/lock/lock_test.go @@ -31,8 +31,9 @@ func TestMain(m *testing.M) { if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error building helper binary: %s\n", err) } - m.Run() + exitCode := m.Run() _ = os.Remove(helperBinary) + os.Exit(exitCode) } func getTempfile(t *testing.T) string { diff --git a/lock_test.go b/lock_test.go index 2438064e..dd6f80f6 100644 --- a/lock_test.go +++ b/lock_test.go @@ -43,7 +43,7 @@ func TestLockRunWithLock(t *testing.T) { return nil } lockfile := filepath.Join(t.TempDir(), "lockfile") - err := os.WriteFile(lockfile, []byte{}, 0644) + err := os.WriteFile(lockfile, []byte{}, 0o600) assert.NoError(t, err) assert.FileExists(t, lockfile) @@ -60,7 +60,7 @@ func TestLockRunWithLockAndForce(t *testing.T) { return nil } lockfile := filepath.Join(t.TempDir(), "lockfile") - err := os.WriteFile(lockfile, []byte{}, 0644) + err := os.WriteFile(lockfile, []byte{}, 0o600) assert.NoError(t, err) assert.FileExists(t, lockfile) @@ -77,7 +77,7 @@ func TestLockRunWithLockAndWait(t *testing.T) { return nil } lockfile := filepath.Join(t.TempDir(), "lockfile") - err := os.WriteFile(lockfile, []byte{}, 0644) + err := os.WriteFile(lockfile, []byte{}, 0o600) assert.NoError(t, err) assert.FileExists(t, lockfile) @@ -101,7 +101,7 @@ func TestLockRunWithLockAndCancel(t *testing.T) { return nil } lockfile := filepath.Join(t.TempDir(), "lockfile") - err := os.WriteFile(lockfile, []byte{}, 0644) + err := os.WriteFile(lockfile, []byte{}, 0o600) assert.NoError(t, err) assert.FileExists(t, lockfile) diff --git a/logger.go b/logger.go index c3a6d169..9bb26c9a 100644 --- a/logger.go +++ b/logger.go @@ -262,8 +262,8 @@ func newDeferredFileWriter(filename string, keepOpen bool, appender appendFunc) } } - addPendingData := func(max int) { - for ; max > 0; max-- { + addPendingData := func(size int) { + for ; size > 0; size-- { select { case data, ok := <-d.data: if ok { @@ -307,7 +307,3 @@ func newDeferredFileWriter(filename string, keepOpen bool, appender appendFunc) return d, lastError } - -func catch(f func() error) error { - return f() -} diff --git a/logger_test.go b/logger_test.go index 9fef0ad6..32fb6fe8 100644 --- a/logger_test.go +++ b/logger_test.go @@ -17,6 +17,8 @@ import ( ) func readTail(t *testing.T, filename string, count int) (lines []string) { + t.Helper() + file, e := os.Open(filename) require.NoError(t, e) defer file.Close() diff --git a/main.go b/main.go index 2dc85bf9..dda16e3c 100644 --- a/main.go +++ b/main.go @@ -72,7 +72,7 @@ func main() { // so we can see what's going on defer func() { term.Println("\n\nPress the Enter Key to continue...") - fmt.Scanln() + _, _ = fmt.Scanln() }() } @@ -543,9 +543,8 @@ func runProfile(ctx *Context) error { return nil } -func loadScheduledProfile(ctx *Context) error { - ctx.schedule, _ = ctx.profile.Schedules()[ctx.command] - return nil +func loadScheduledProfile(ctx *Context) { + ctx.schedule = ctx.profile.Schedules()[ctx.command] } // randomBool returns true for Heads and false for Tails diff --git a/main_test.go b/main_test.go index 3f6109ec..64ed9827 100644 --- a/main_test.go +++ b/main_test.go @@ -39,9 +39,10 @@ func TestMain(m *testing.M) { fmt.Fprintf(os.Stderr, "Error building shell/echo binary: %s\nCommand output: %s\n", err, string(output)) } - m.Run() + exitCode := m.Run() _ = os.Remove(mockBinary) _ = os.Remove(echoBinary) + os.Exit(exitCode) } func TestGetProfile(t *testing.T) { @@ -64,6 +65,7 @@ func TestGetProfile(t *testing.T) { require.NoError(t, err) getWd := func(t *testing.T) string { + t.Helper() dir, err := os.Getwd() require.NoError(t, err) return filepath.ToSlash(dir) @@ -72,6 +74,7 @@ func TestGetProfile(t *testing.T) { cwd := getWd(t) getProf := func(t *testing.T, name string) (profile *config.Profile, cleanup func()) { + t.Helper() var err error profile, cleanup, err = openProfile(c, name) require.NoError(t, err) diff --git a/monitor/error.go b/monitor/error.go index 5e678cc9..781a8da2 100644 --- a/monitor/error.go +++ b/monitor/error.go @@ -20,20 +20,17 @@ func IsWarning(err error) bool { return exitErr.ExitCode() == constants.ExitCodeWarning } // so far, internal warning is only used in unit tests - warn := &InternalWarning{} - if errors.As(err, &warn) { - return true - } - return false + warn := &InternalWarningError{} + return errors.As(err, &warn) } func IsError(err error) bool { return err != nil && !IsWarning(err) } -type InternalWarning struct { +type InternalWarningError struct { } -func (w InternalWarning) Error() string { +func (w InternalWarningError) Error() string { return "internal warning" } diff --git a/monitor/hook/sender.go b/monitor/hook/sender.go index 5cc27d0a..3a6df552 100644 --- a/monitor/hook/sender.go +++ b/monitor/hook/sender.go @@ -2,6 +2,7 @@ package hook import ( "bytes" + "context" "crypto/tls" "crypto/x509" "errors" @@ -46,13 +47,15 @@ func NewSender(certificates []string, userAgent string, timeout time.Duration, d if len(certificates) > 0 { transport := http.DefaultTransport.(*http.Transport).Clone() transport.TLSClientConfig = &tls.Config{ - RootCAs: getRootCAs(certificates), + RootCAs: getRootCAs(certificates), + MinVersion: tls.VersionTLS12, } client.Transport = transport } // another client for insecure requests transport := http.DefaultTransport.(*http.Transport).Clone() + //nolint:gosec transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} insecureClient := &http.Client{ Timeout: timeout, @@ -94,7 +97,7 @@ func (s *Sender) Send(cfg config.SendMonitoringSection, ctx Context) error { bodyReader = bytes.NewBufferString(body) } - req, err := http.NewRequest(method, url, bodyReader) + req, err := http.NewRequestWithContext(context.TODO(), method, url, bodyReader) if err != nil { return err } diff --git a/monitor/mocks/OutputAnalysis.go b/monitor/mocks/OutputAnalysis.go index b81bc16f..be451dce 100644 --- a/monitor/mocks/OutputAnalysis.go +++ b/monitor/mocks/OutputAnalysis.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.44.2. DO NOT EDIT. package mocks diff --git a/monitor/prom/metrics_test.go b/monitor/prom/metrics_test.go index b240e97c..9fe09416 100644 --- a/monitor/prom/metrics_test.go +++ b/monitor/prom/metrics_test.go @@ -11,7 +11,7 @@ import ( func TestSaveSingleBackup(t *testing.T) { p := NewMetrics("test", "", "", nil) p.BackupResults(StatusSuccess, monitor.Summary{ - Duration: time.Duration(11 * time.Second), + Duration: 11 * time.Second, BytesAdded: 100, BytesTotal: 1000, }) @@ -22,7 +22,7 @@ func TestSaveSingleBackup(t *testing.T) { func TestSaveSingleBackupWithConfigLabel(t *testing.T) { p := NewMetrics("test", "", "", map[string]string{"test_label": "test_value"}) p.BackupResults(StatusSuccess, monitor.Summary{ - Duration: time.Duration(11 * time.Second), + Duration: 11 * time.Second, BytesAdded: 100, BytesTotal: 1000, }) @@ -33,7 +33,7 @@ func TestSaveSingleBackupWithConfigLabel(t *testing.T) { func TestSaveBackupGroup(t *testing.T) { p := NewMetrics("test", "group", "", nil) p.BackupResults(StatusSuccess, monitor.Summary{ - Duration: time.Duration(11 * time.Second), + Duration: 11 * time.Second, BytesAdded: 100, BytesTotal: 1000, }) diff --git a/monitor/status/progress_test.go b/monitor/status/progress_test.go index 18821dc1..9238ef7a 100644 --- a/monitor/status/progress_test.go +++ b/monitor/status/progress_test.go @@ -95,7 +95,7 @@ func TestProgressWarningAsSuccess(t *testing.T) { }, } p := NewProgress(profile, status) - p.Summary(constants.CommandBackup, monitor.Summary{}, stderr, &monitor.InternalWarning{}) + p.Summary(constants.CommandBackup, monitor.Summary{}, stderr, &monitor.InternalWarningError{}) exists, err := afero.Exists(fs, filename) require.NoError(t, err) @@ -122,7 +122,7 @@ func TestProgressWarningAsError(t *testing.T) { }, } p := NewProgress(profile, status) - p.Summary(constants.CommandBackup, monitor.Summary{}, stderr, &monitor.InternalWarning{}) + p.Summary(constants.CommandBackup, monitor.Summary{}, stderr, &monitor.InternalWarningError{}) exists, err := afero.Exists(fs, filename) require.NoError(t, err) diff --git a/own_command_error_test.go b/own_command_error_test.go index 445c6a89..6d87957a 100644 --- a/own_command_error_test.go +++ b/own_command_error_test.go @@ -15,6 +15,6 @@ func TestOwnCommandError(t *testing.T) { assert.ErrorIs(t, err, wrap) var unwrap *ownCommandError - assert.True(t, errors.As(err, &unwrap)) + assert.ErrorAs(t, err, &unwrap) assert.Equal(t, 10, unwrap.ExitCode()) } diff --git a/remote/client.go b/remote/client.go index 9f46f79b..1b9cf9c4 100644 --- a/remote/client.go +++ b/remote/client.go @@ -43,8 +43,11 @@ func (c *Client) LogEntry(logEntry clog.LogEntry) error { } buffer := &bytes.Buffer{} encoder := json.NewEncoder(buffer) - encoder.Encode(log) - resp, err := c.client.Post(c.baseURL+logPath, "application/json", buffer) + err := encoder.Encode(log) + if err != nil { + return err + } + resp, err := c.client.Post(c.baseURL+logPath, "application/json", buffer) //nolint:noctx if err != nil { return err } @@ -55,7 +58,7 @@ func (c *Client) LogEntry(logEntry clog.LogEntry) error { // Term sends plain text terminal output func (c *Client) Term(p []byte) error { buffer := bytes.NewBuffer(p) - resp, err := c.client.Post(c.baseURL+termPath, "text/plain", buffer) + resp, err := c.client.Post(c.baseURL+termPath, "text/plain", buffer) //nolint:noctx if err != nil { return err } @@ -65,7 +68,7 @@ func (c *Client) Term(p []byte) error { // Done signals to the parent process that we're finished func (c *Client) Done() error { - resp, err := c.client.Get(c.baseURL + donePath) + resp, err := c.client.Get(c.baseURL + donePath) //nolint:noctx if err != nil { return nil } diff --git a/remote/handler.go b/remote/handler.go index 6d3560d8..d6a1195c 100644 --- a/remote/handler.go +++ b/remote/handler.go @@ -1,10 +1,12 @@ package remote import ( + "context" "encoding/json" "io" "net/http" "os" + "time" "github.com/creativeprojects/clog" ) @@ -36,7 +38,9 @@ func handlerFuncDone(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() // Just close the http server - StopServer() + ctx, cancel := context.WithTimeout(r.Context(), timeout*time.Second) + defer cancel() + StopServer(ctx) } func handlerFuncLog(w http.ResponseWriter, r *http.Request) { diff --git a/remote/server.go b/remote/server.go index 4c1594cd..a10b6b51 100644 --- a/remote/server.go +++ b/remote/server.go @@ -34,10 +34,14 @@ func StartServer(done chan interface{}) error { clog.Debugf("listening on port %d", port) server = &http.Server{ - Handler: getServeMux(), + Handler: getServeMux(), + ReadHeaderTimeout: 30 * time.Second, } go func() { - server.Serve(listener) + err := server.Serve(listener) + if err != http.ErrServerClosed { + clog.Errorf("error starting server: %v", err) + } close(done) }() @@ -50,12 +54,13 @@ func GetPort() int { } // StopServer gracefully asks the http server to shutdown -func StopServer() { +func StopServer(ctx context.Context) { if server != nil { // gracefully stop the http server - ctx, cancel := context.WithTimeout(context.Background(), timeout*time.Second) - defer cancel() - server.Shutdown(ctx) + err := server.Shutdown(ctx) + if err != nil { + clog.Warningf("error shutting down server: %v", err) + } } server = nil if listener != nil { diff --git a/remote/server_test.go b/remote/server_test.go new file mode 100644 index 00000000..71ea140d --- /dev/null +++ b/remote/server_test.go @@ -0,0 +1,53 @@ +package remote_test + +import ( + "context" + "testing" + "time" + + "github.com/creativeprojects/clog" + "github.com/creativeprojects/resticprofile/remote" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStartAndStopServer(t *testing.T) { + done := make(chan interface{}) + err := remote.StartServer(done) + require.NoError(t, err) + + assert.NotEmpty(t, remote.GetPort()) + + time.Sleep(1 * time.Second) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + remote.StopServer(ctx) + + <-done +} + +func TestClient(t *testing.T) { + done := make(chan interface{}) + err := remote.StartServer(done) + require.NoError(t, err) + + assert.NotEmpty(t, remote.GetPort()) + + time.Sleep(1 * time.Second) + + client := remote.NewClient(remote.GetPort()) + err = client.LogEntry(clog.LogEntry{ + Level: clog.LevelWarning, + Values: []interface{}{"Hello, World!"}, + }) + require.NoError(t, err) + + err = client.Term([]byte("Hello, World!")) + require.NoError(t, err) + + err = client.Done() + require.NoError(t, err) + + <-done +} diff --git a/restic/commands.go b/restic/commands.go index 21fe4d85..0ff07886 100644 --- a/restic/commands.go +++ b/restic/commands.go @@ -255,7 +255,7 @@ func ParseCommandsFromManPages(manualDir fs.FS, version string, baseVersion bool } func parseCommandsFromManPagesInto(manualDir fs.FS, version string, baseVersion bool, commands map[string]*command) error { - filePattern := regexp.MustCompile("^restic(|-.+)\\.1$") + filePattern := regexp.MustCompile(`^restic(|-.+)\.1$`) newCommands := map[string]*command{} if files, err := fs.ReadDir(manualDir, "."); err == nil { diff --git a/restic/commands_test.go b/restic/commands_test.go index c4b47233..97d9368d 100644 --- a/restic/commands_test.go +++ b/restic/commands_test.go @@ -20,6 +20,7 @@ import ( func descriptionIs(expected string) func(t *testing.T, cmd CommandIf, err error) { return func(t *testing.T, cmd CommandIf, err error) { + t.Helper() assert.NoError(t, err) assert.Equal(t, expected, cmd.GetDescription()) } @@ -27,6 +28,7 @@ func descriptionIs(expected string) func(t *testing.T, cmd CommandIf, err error) func optionNotExists(name string) func(t *testing.T, cmd CommandIf, err error) { return func(t *testing.T, cmd CommandIf, err error) { + t.Helper() _, found := cmd.Lookup(name) assert.False(t, found) assert.False(t, slices.ContainsFunc(cmd.GetOptions(), func(option Option) bool { return option.Name == name })) @@ -35,6 +37,7 @@ func optionNotExists(name string) func(t *testing.T, cmd CommandIf, err error) { func optionIs(name, description, def string, once bool) func(t *testing.T, cmd CommandIf, err error) { return func(t *testing.T, cmd CommandIf, err error) { + t.Helper() assert.NoError(t, err) for _, n := range strings.Split(name, ",") { option, found := cmd.Lookup(n) @@ -54,6 +57,7 @@ func optionIs(name, description, def string, once bool) func(t *testing.T, cmd C func all(checks ...func(t *testing.T, cmd CommandIf, err error)) func(t *testing.T, cmd CommandIf, err error) { return func(t *testing.T, cmd CommandIf, err error) { + t.Helper() require.NotEmpty(t, checks) for _, check := range checks { check(t, cmd, err) @@ -242,6 +246,7 @@ Ignored line`, var fixtures embed.FS func parseFixtures(t *testing.T) map[string]*command { + t.Helper() cmds := map[string]*command{} for i, version := range []string{"0.9", "0.10", "0.14"} { manualDir, err := fs.Sub(fixtures, path.Join("fixtures", version)) diff --git a/restic/downloader.go b/restic/downloader.go index 9d92ad15..88c96ce7 100644 --- a/restic/downloader.go +++ b/restic/downloader.go @@ -53,7 +53,7 @@ func newUpdater(os, arch string, key []byte, source sup.Source) *sup.Updater { return updater } -var versionCommandPattern = regexp.MustCompile("restic ([\\d.]+)[ -].+") +var versionCommandPattern = regexp.MustCompile(`restic ([\d.]+)[ -].+`) // GetVersion returns the version of the executable func GetVersion(executable string) (string, error) { diff --git a/restic/files.go b/restic/files.go index d5e0a483..f4696313 100644 --- a/restic/files.go +++ b/restic/files.go @@ -68,7 +68,7 @@ func loadEmbeddedOSExtensions(goos string, cmds map[string]*command) error { var extensions map[string][]Option if extensions, err = loadCommandExtensionsFromReader(file); err == nil { for _, options := range extensions { - for i, _ := range options { + for i := range options { if !slices.Contains(options[i].OnlyInOS, goos) { options[i].OnlyInOS = append(options[i].OnlyInOS, goos) } diff --git a/schedule/errors.go b/schedule/errors.go index 237c9820..c6ed980c 100644 --- a/schedule/errors.go +++ b/schedule/errors.go @@ -4,6 +4,6 @@ import "errors" // Generic errors var ( - ErrorServiceNotFound = errors.New("service not found") - ErrorServiceNotRunning = errors.New("service is not running") + ErrServiceNotFound = errors.New("service not found") + ErrServiceNotRunning = errors.New("service is not running") ) diff --git a/schedule/handler_crond.go b/schedule/handler_crond.go index 8064fcac..cf1f77bc 100644 --- a/schedule/handler_crond.go +++ b/schedule/handler_crond.go @@ -104,7 +104,7 @@ func (h *HandlerCrond) RemoveJob(job *Config, permission string) error { return err } if num == 0 { - return ErrorServiceNotFound + return ErrServiceNotFound } return nil } diff --git a/schedule/handler_darwin.go b/schedule/handler_darwin.go index 30debd84..9aa49e1e 100644 --- a/schedule/handler_darwin.go +++ b/schedule/handler_darwin.go @@ -211,7 +211,7 @@ func (h *HandlerLaunchd) RemoveJob(job *Config, permission string) error { } if _, err := os.Stat(filename); err != nil && os.IsNotExist(err) { - return ErrorServiceNotFound + return ErrServiceNotFound } // stop the service in case it's already running stop := exec.Command(launchctlBin, launchdStop, name) @@ -245,7 +245,7 @@ func (h *HandlerLaunchd) DisplayJobStatus(job *Config) error { cmd := exec.Command(launchctlBin, launchdList, getJobName(job.ProfileName, job.CommandName)) output, err := cmd.Output() if cmd.ProcessState.ExitCode() == codeServiceNotFound { - return ErrorServiceNotFound + return ErrServiceNotFound } if err != nil { return err @@ -362,7 +362,7 @@ func getCalendarIntervalsFromScheduleTree(tree []*treeElement) []CalendarInterva func fillInValueFromScheduleTreeElement(currentEntry *CalendarInterval, element *treeElement, entries *[]CalendarInterval) { setCalendarIntervalValueFromType(currentEntry, element.value, element.elementType) - if element.subElements == nil || len(element.subElements) == 0 { + if len(element.subElements) == 0 { // end of the line, this entry is finished *entries = append(*entries, *currentEntry) return diff --git a/schedule/handler_systemd.go b/schedule/handler_systemd.go index 3fedaf68..ded48e18 100644 --- a/schedule/handler_systemd.go +++ b/schedule/handler_systemd.go @@ -265,13 +265,13 @@ func runSystemctlCommand(timerName, command string, unitType systemd.UnitType, s } err := cmd.Run() if command == systemctlStatus && cmd.ProcessState.ExitCode() == codeStatusUnitNotFound { - return ErrorServiceNotFound + return ErrServiceNotFound } if command == systemctlStatus && cmd.ProcessState.ExitCode() == codeStatusNotRunning { - return ErrorServiceNotRunning + return ErrServiceNotRunning } if command == systemctlStop && cmd.ProcessState.ExitCode() == codeStopUnitNotFound { - return ErrorServiceNotFound + return ErrServiceNotFound } return err } diff --git a/schedule/handler_windows.go b/schedule/handler_windows.go index a7c6eddc..cc98c8ce 100644 --- a/schedule/handler_windows.go +++ b/schedule/handler_windows.go @@ -73,8 +73,8 @@ func (h *HandlerWindows) CreateJob(job *Config, schedules []*calendar.Event, per func (h *HandlerWindows) RemoveJob(job *Config, permission string) error { err := schtasks.Delete(job.ProfileName, job.CommandName) if err != nil { - if errors.Is(err, schtasks.ErrorNotRegistered) { - return ErrorServiceNotFound + if errors.Is(err, schtasks.ErrNotRegistered) { + return ErrServiceNotFound } return err } @@ -85,8 +85,8 @@ func (h *HandlerWindows) RemoveJob(job *Config, permission string) error { func (h *HandlerWindows) DisplayJobStatus(job *Config) error { err := schtasks.Status(job.ProfileName, job.CommandName) if err != nil { - if errors.Is(err, schtasks.ErrorNotRegistered) { - return ErrorServiceNotFound + if errors.Is(err, schtasks.ErrNotRegistered) { + return ErrServiceNotFound } return err } diff --git a/schedule/job.go b/schedule/job.go index 97dbe599..3af2392c 100644 --- a/schedule/job.go +++ b/schedule/job.go @@ -14,7 +14,7 @@ type Job struct { handler Handler } -var ErrorJobCanBeRemovedOnly = errors.New("job can be removed only") +var ErrJobCanBeRemovedOnly = errors.New("job can be removed only") // Accessible checks if the current user is permitted to access the job func (j *Job) Accessible() bool { @@ -25,7 +25,7 @@ func (j *Job) Accessible() bool { // Create a new job func (j *Job) Create() error { if j.RemoveOnly() { - return ErrorJobCanBeRemovedOnly + return ErrJobCanBeRemovedOnly } permission := getSchedulePermission(j.config.Permission) @@ -80,7 +80,7 @@ func (j *Job) RemoveOnly() bool { // Status of a job func (j *Job) Status() error { if j.RemoveOnly() { - return ErrorJobCanBeRemovedOnly + return ErrJobCanBeRemovedOnly } schedules, err := j.handler.ParseSchedules(j.config.Schedules) diff --git a/schedule/mocks/Handler.go b/schedule/mocks/Handler.go index 74971545..50ab4176 100644 --- a/schedule/mocks/Handler.go +++ b/schedule/mocks/Handler.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.44.2. DO NOT EDIT. package mocks diff --git a/schedule/removeonly_test.go b/schedule/removeonly_test.go index 80376e4f..7abc9d70 100644 --- a/schedule/removeonly_test.go +++ b/schedule/removeonly_test.go @@ -44,9 +44,9 @@ func TestRemoveOnlyJob(t *testing.T) { job := scheduler.NewJob(NewRemoveOnlyConfig(profile, "check")) - assert.Equal(t, ErrorJobCanBeRemovedOnly, job.Create()) - assert.Equal(t, ErrorJobCanBeRemovedOnly, job.Status()) + assert.Equal(t, ErrJobCanBeRemovedOnly, job.Create()) + assert.Equal(t, ErrJobCanBeRemovedOnly, job.Status()) assert.True(t, job.Accessible()) assert.True(t, job.RemoveOnly()) - assert.NotEqual(t, ErrorJobCanBeRemovedOnly, job.Remove()) + assert.NotEqual(t, ErrJobCanBeRemovedOnly, job.Remove()) } diff --git a/schedule/tree_darwin_test.go b/schedule/tree_darwin_test.go index cc4f3afb..a9a827e3 100644 --- a/schedule/tree_darwin_test.go +++ b/schedule/tree_darwin_test.go @@ -1,4 +1,5 @@ -//+build darwin +//go:build darwin +// +build darwin package schedule @@ -235,37 +236,3 @@ func TestGenerateTree(t *testing.T) { }) } } - -func BenchmarkManualSliceCopy(b *testing.B) { - max := 10000 - testData := make([]int, max) - for i := 0; i < max; i++ { - testData[i] = i - } - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - newData := make([]int, len(testData)) - for i := 0; i < len(testData); i++ { - newData[i] = testData[i] - } - assert.Len(b, newData, max) - } -} - -// This one performs much better, but notice how the slice should be pre-allocated -// Also doesn't work with allocation like newData := make([]int, 0, len(testData)) -func BenchmarkBuiltinSliceCopy(b *testing.B) { - max := 10000 - testData := make([]int, max) - for i := 0; i < max; i++ { - testData[i] = i - } - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - newData := make([]int, len(testData)) - copy(newData, testData) - assert.Len(b, newData, max) - } -} diff --git a/schedule_jobs.go b/schedule_jobs.go index 42269dda..3bf9ff91 100644 --- a/schedule_jobs.go +++ b/schedule_jobs.go @@ -77,7 +77,7 @@ func removeJobs(handler schedule.Handler, profileName string, configs []*config. // Try to remove the job err := job.Remove() if err != nil { - if errors.Is(err, schedule.ErrorServiceNotFound) { + if errors.Is(err, schedule.ErrServiceNotFound) { // Display a warning and keep going. Skip message for RemoveOnly jobs since they may not exist if !job.RemoveOnly() { clog.Warningf("service %s/%s not found", scheduleConfig.ProfileName, scheduleConfig.CommandName) @@ -108,12 +108,12 @@ func statusJobs(handler schedule.Handler, profileName string, configs []*config. job := scheduler.NewJob(scheduleConfig) err := job.Status() if err != nil { - if errors.Is(err, schedule.ErrorServiceNotFound) { + if errors.Is(err, schedule.ErrServiceNotFound) { // Display a warning and keep going clog.Warningf("service %s/%s not found", scheduleConfig.ProfileName, scheduleConfig.CommandName) continue } - if errors.Is(err, schedule.ErrorServiceNotRunning) { + if errors.Is(err, schedule.ErrServiceNotRunning) { // Display a warning and keep going clog.Warningf("service %s/%s is not running", scheduleConfig.ProfileName, scheduleConfig.CommandName) continue diff --git a/schedule_jobs_test.go b/schedule_jobs_test.go index 52781361..62d9db6f 100644 --- a/schedule_jobs_test.go +++ b/schedule_jobs_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/mock" ) +//nolint:unparam func configForJob(command string, at ...string) *config.Schedule { origin := config.ScheduleOrigin("profile", command) return config.NewDefaultSchedule(nil, origin, at...) @@ -137,7 +138,7 @@ func TestNoFailRemoveUnknownJob(t *testing.T) { handler.EXPECT().Init().Return(nil) handler.EXPECT().Close() handler.EXPECT().RemoveJob(mock.AnythingOfType("*schedule.Config"), mock.AnythingOfType("string")). - Return(schedule.ErrorServiceNotFound) + Return(schedule.ErrServiceNotFound) scheduleConfig := configForJob("backup", "sched") err := removeJobs(handler, "profile", []*config.Schedule{scheduleConfig}) @@ -151,7 +152,7 @@ func TestNoFailRemoveUnknownRemoveOnlyJob(t *testing.T) { handler.EXPECT().Init().Return(nil) handler.EXPECT().Close() handler.EXPECT().RemoveJob(mock.AnythingOfType("*schedule.Config"), mock.AnythingOfType("string")). - Return(schedule.ErrorServiceNotFound) + Return(schedule.ErrServiceNotFound) scheduleConfig := configForJob("backup") err := removeJobs(handler, "profile", []*config.Schedule{scheduleConfig}) diff --git a/schtasks/errors.go b/schtasks/errors.go index a0b51432..3323f2f8 100644 --- a/schtasks/errors.go +++ b/schtasks/errors.go @@ -1,4 +1,4 @@ -//+build windows +//go:build windows package schtasks @@ -6,5 +6,5 @@ import "errors" // Common errors var ( - ErrorNotRegistered = errors.New("task is not registered") + ErrNotRegistered = errors.New("task is not registered") ) diff --git a/schtasks/taskscheduler.go b/schtasks/taskscheduler.go index c20805e1..e02a3baf 100644 --- a/schtasks/taskscheduler.go +++ b/schtasks/taskscheduler.go @@ -60,8 +60,8 @@ var ( userPassword = "" ) -// ErrorNotConnected is returned by public functions if Connect was not called, was not successful or Close closed the connection. -var ErrorNotConnected = errors.New("local task scheduler not connected") +// ErrNotConnected is returned by public functions if Connect was not called, was not successful or Close closed the connection. +var ErrNotConnected = errors.New("local task scheduler not connected") // IsConnected returns whether a connection to the local task scheduler is established func IsConnected() bool { @@ -86,7 +86,7 @@ func Close() { // Create or update a task (if the name already exists in the Task Scheduler) func Create(config *Config, schedules []*calendar.Event, permission Permission) error { if !IsConnected() { - return ErrorNotConnected + return ErrNotConnected } if permission == SystemAccount { @@ -514,14 +514,14 @@ func createMonthlyTrigger(task *taskmaster.Definition, schedule *calendar.Event) // Delete a task func Delete(title, subtitle string) error { if !IsConnected() { - return ErrorNotConnected + return ErrNotConnected } taskName := getTaskPath(title, subtitle) err := taskService.DeleteTask(taskName) if err != nil { if strings.Contains(err.Error(), "doesn't exist") { - return fmt.Errorf("%w: %s", ErrorNotRegistered, taskName) + return fmt.Errorf("%w: %s", ErrNotRegistered, taskName) } return err } @@ -531,19 +531,19 @@ func Delete(title, subtitle string) error { // Status returns the status of a task func Status(title, subtitle string) error { if !IsConnected() { - return ErrorNotConnected + return ErrNotConnected } taskName := getTaskPath(title, subtitle) registeredTask, err := taskService.GetRegisteredTask(taskName) if err != nil { // if there's an error here, it is very likely that the task is not registered - return fmt.Errorf("%s: %w: %s", taskName, ErrorNotRegistered, err) + return fmt.Errorf("%s: %w: %s", taskName, ErrNotRegistered, err) } writer := tabwriter.NewWriter(term.GetOutput(), 2, 2, 2, ' ', tabwriter.AlignRight) fmt.Fprintf(writer, "Task:\t %s\n", registeredTask.Path) fmt.Fprintf(writer, "User:\t %s\n", registeredTask.Definition.Principal.UserID) - if registeredTask.Definition.Actions != nil && len(registeredTask.Definition.Actions) > 0 { + if len(registeredTask.Definition.Actions) > 0 { if action, ok := registeredTask.Definition.Actions[0].(taskmaster.ExecAction); ok { fmt.Fprintf(writer, "Working Dir:\t %v\n", action.WorkingDir) fmt.Fprintf(writer, "Exec:\t %v\n", action.Path+" "+action.Args) diff --git a/schtasks/taskscheduler_test.go b/schtasks/taskscheduler_test.go index 487863cc..7778ddba 100644 --- a/schtasks/taskscheduler_test.go +++ b/schtasks/taskscheduler_test.go @@ -321,7 +321,9 @@ func TestCreationOfTasks(t *testing.T) { // user logged in doesn't need a password err = createUserLoggedOnTask(scheduleConfig, schedules) assert.NoError(t, err) - defer Delete(scheduleConfig.ProfileName, scheduleConfig.CommandName) + defer func() { + _ = Delete(scheduleConfig.ProfileName, scheduleConfig.CommandName) + }() taskName := getTaskPath(scheduleConfig.ProfileName, scheduleConfig.CommandName) buffer, err := exportTask(taskName) diff --git a/shell/analyser.go b/shell/analyser.go index 9a4a3f78..d2aff573 100644 --- a/shell/analyser.go +++ b/shell/analyser.go @@ -36,10 +36,10 @@ type OutputAnalyser struct { } var outputAnalyserPatterns = map[string]*regexp.Regexp{ - "lock-failure,who": regexp.MustCompile("unable to create lock.+already locked.+?by (.+)$"), - "lock-failure,age": regexp.MustCompile("lock was created at.+\\(([^()]+)\\s+ago\\)"), - "lock-failure,stale": regexp.MustCompile("the\\W+unlock\\W+command can be used to remove stale locks"), - "lock-retry,max-wait": regexp.MustCompile("repo already locked, waiting up to (\\S+) for the lock"), + "lock-failure,who": regexp.MustCompile(`unable to create lock.+already locked.+?by (.+)$`), + "lock-failure,age": regexp.MustCompile(`lock was created at.+\(([^()]+)\s+ago\)`), + "lock-failure,stale": regexp.MustCompile(`the\W+unlock\W+command can be used to remove stale locks`), + "lock-retry,max-wait": regexp.MustCompile(`repo already locked, waiting up to (\S+) for the lock`), } func NewOutputAnalyser() *OutputAnalyser { diff --git a/shell/analyser_test.go b/shell/analyser_test.go index c275739b..689a22a4 100644 --- a/shell/analyser_test.go +++ b/shell/analyser_test.go @@ -53,6 +53,7 @@ func TestCustomErrorCallback(t *testing.T) { triggerLine := "--TRIGGER_CALLBACK--" init := func(t *testing.T, minCount, maxCalls int, stopOnError bool) error { + t.Helper() analyser = NewOutputAnalyser() invoked = 0 cbError = nil @@ -64,6 +65,7 @@ func TestCustomErrorCallback(t *testing.T) { } writeTrigger := func(t *testing.T) { + t.Helper() require.NoError(t, analyser.AnalyseStringLines(triggerLine)) } diff --git a/shell/command.go b/shell/command.go index e2f46513..a34c6e75 100644 --- a/shell/command.go +++ b/shell/command.go @@ -110,7 +110,7 @@ func (c *Command) Run() (monitor.Summary, string, error) { cmd.Stdin = c.Stdin cmd.Env = os.Environ() - if c.Environ != nil && len(c.Environ) > 0 { + if len(c.Environ) > 0 { cmd.Env = append(cmd.Env, c.Environ...) } diff --git a/shell/command_test.go b/shell/command_test.go index 934b1734..a1f66729 100644 --- a/shell/command_test.go +++ b/shell/command_test.go @@ -33,8 +33,9 @@ func TestMain(m *testing.M) { if output, err := cmd.CombinedOutput(); err != nil { fmt.Fprintf(os.Stderr, "Error building mock binary: %s\nCommand output: %s\n", err, string(output)) } - m.Run() + exitCode := m.Run() _ = os.Remove(mockBinary) + os.Exit(exitCode) } func TestRemoveQuotes(t *testing.T) { @@ -130,8 +131,8 @@ func TestShellArgumentsComposing(t *testing.T) { t.Parallel() exeWithSpace := filepath.Join(t.TempDir(), "some folder", "executable") - require.NoError(t, os.MkdirAll(filepath.Dir(exeWithSpace), 0700)) - require.NoError(t, os.WriteFile(exeWithSpace, []byte{0}, 0700)) + require.NoError(t, os.MkdirAll(filepath.Dir(exeWithSpace), 0o700)) + require.NoError(t, os.WriteFile(exeWithSpace, []byte{0}, 0o600)) tests := []struct { command string @@ -376,7 +377,7 @@ func TestInterruptShellCommand(t *testing.T) { // Will ask us to stop in 100ms go func() { time.Sleep(100 * time.Millisecond) - sigChan <- syscall.Signal(syscall.SIGINT) + sigChan <- syscall.SIGINT }() start := time.Now() _, _, err := cmd.Run() @@ -546,8 +547,10 @@ func TestCanAnalyseLockFailure(t *testing.T) { file, err := os.CreateTemp(".", "test-restic-lock-failure") require.NoError(t, err) - file.Write([]byte(ResticLockFailureOutput)) + _, err = file.Write([]byte(ResticLockFailureOutput)) + require.NoError(t, err) file.Close() + fileName := file.Name() defer os.Remove(fileName) diff --git a/shell/command_unix.go b/shell/command_unix.go index ca81c3d1..5cf7d10e 100644 --- a/shell/command_unix.go +++ b/shell/command_unix.go @@ -11,19 +11,7 @@ func (c *Command) propagateSignal(process *os.Process) { select { case <-c.sigChan: // We resend the signal to the child process - process.Signal(syscall.SIGINT) - return - case <-c.done: - return - } -} - -func (c *Command) propagateGroupSignal(process *os.Process) { - select { - case <-c.sigChan: - // We resend the signal to the child group - group, _ := os.FindProcess(-process.Pid) - group.Signal(syscall.SIGINT) + _ = process.Signal(syscall.SIGINT) return case <-c.done: return diff --git a/shell/json_summary_test.go b/shell/json_summary_test.go index 49f2a8f9..9a04f997 100644 --- a/shell/json_summary_test.go +++ b/shell/json_summary_test.go @@ -32,6 +32,7 @@ func TestScanJsonSummary(t *testing.T) { // Start writing into the pipe, line by line go func(t *testing.T) { + t.Helper() lines := strings.Split(resticOutput, "\n") for _, line := range lines { line = strings.TrimRight(line, "\r") @@ -84,6 +85,7 @@ Is there a repository at the following location? // Start writing into the pipe, line by line go func(t *testing.T) { + t.Helper() lines := strings.Split(resticOutput, "\n") for _, line := range lines { line = strings.TrimRight(line, "\r") diff --git a/shell/mock/main.go b/shell/mock/main.go index 4564fcdd..6994c349 100644 --- a/shell/mock/main.go +++ b/shell/mock/main.go @@ -73,7 +73,7 @@ func main() { stderr = err.Error() exit = 3 } else { - io.CopyN(os.Stderr, file, 1024) + _, _ = io.CopyN(os.Stderr, file, 1024) file.Close() stderr = "" } diff --git a/shell/plain_summary.go b/shell/plain_summary.go index 3b9d05d6..7470151a 100644 --- a/shell/plain_summary.go +++ b/shell/plain_summary.go @@ -20,7 +20,10 @@ var ScanBackupPlain ScanOutput = func(r io.Reader, summary *monitor.Summary, w i rawBytes, unit, duration := 0.0, "", "" scanner := bufio.NewScanner(r) for scanner.Scan() { - w.Write([]byte(scanner.Text() + eol)) + _, err := w.Write([]byte(scanner.Text() + eol)) + if err != nil { + return err + } // scan content - it's all right if the line does not match _, _ = fmt.Sscanf(scanner.Text(), "Files: %d new, %d changed, %d unmodified", &summary.FilesNew, &summary.FilesChanged, &summary.FilesUnmodified) _, _ = fmt.Sscanf(scanner.Text(), "Dirs: %d new, %d changed, %d unmodified", &summary.DirsNew, &summary.DirsChanged, &summary.DirsUnmodified) diff --git a/shell/plain_summary_test.go b/shell/plain_summary_test.go index cb4dffc4..db64de0c 100644 --- a/shell/plain_summary_test.go +++ b/shell/plain_summary_test.go @@ -39,7 +39,7 @@ snapshot 07ab30a5 saved lines := strings.Split(source, "\n") for _, line := range lines { line = strings.TrimRight(line, "\r") - writer.WriteString(line + platform.LineSeparator) + _, _ = writer.WriteString(line + platform.LineSeparator) } writer.Close() }() diff --git a/shell_command.go b/shell_command.go index 72417aed..7ab6adf3 100644 --- a/shell_command.go +++ b/shell_command.go @@ -82,7 +82,7 @@ func runShellCommand(command shellCommandDefinition) (summary monitor.Summary, s } shellCmd.Environ = os.Environ() - if command.env != nil && len(command.env) > 0 { + if len(command.env) > 0 { shellCmd.Environ = append(shellCmd.Environ, command.env...) } diff --git a/systemd/generate_test.go b/systemd/generate_test.go index 8662e13c..96a53f7a 100644 --- a/systemd/generate_test.go +++ b/systemd/generate_test.go @@ -438,12 +438,14 @@ func TestGenerateOnReadOnlyFs(t *testing.T) { } func assertNoFileExists(t *testing.T, filename string) { + t.Helper() exists, err := afero.Exists(fs, filename) require.NoError(t, err) assert.Falsef(t, exists, "file %q exists", filename) } func requireFileExists(t *testing.T, filename string) { + t.Helper() exists, err := afero.Exists(fs, filename) require.NoError(t, err) require.Truef(t, exists, "file %q does not exist", filename) diff --git a/term/term_test.go b/term/term_test.go index d8f72038..73807667 100644 --- a/term/term_test.go +++ b/term/term_test.go @@ -2,6 +2,7 @@ package term import ( "bytes" + "log" "os" "testing" @@ -71,7 +72,10 @@ func TestAskYesNo(t *testing.T) { func ExamplePrint() { SetOutput(os.Stdout) - Print("ExamplePrint") + _, err := Print("ExamplePrint") + if err != nil { + log.Fatal(err) + } // Output: ExamplePrint } diff --git a/update.go b/update.go index 4fd30237..ed89a0c6 100644 --- a/update.go +++ b/update.go @@ -48,11 +48,14 @@ func confirmAndSelfUpdate(quiet, debug bool, version string, prerelease bool) er if debug { selfupdate.SetLogger(clog.NewStandardLogger(clog.LevelDebug, clog.GetDefaultLogger())) } - updater, _ := selfupdate.NewUpdater( + updater, err := selfupdate.NewUpdater( selfupdate.Config{ Validator: &selfupdate.ChecksumValidator{UniqueFilename: "checksums.txt"}, Prerelease: prerelease, }) + if err != nil { + return fmt.Errorf("unable to create updater: %w", err) + } latest, found, err := updater.DetectLatest(context.Background(), selfupdate.NewRepositorySlug("creativeprojects", "resticprofile")) if err != nil { return fmt.Errorf("unable to detect latest version: %w", err) diff --git a/update_test.go b/update_test.go index 910c6e4a..f70717fc 100644 --- a/update_test.go +++ b/update_test.go @@ -3,12 +3,12 @@ package main import ( - "errors" "testing" "github.com/creativeprojects/clog" "github.com/creativeprojects/go-selfupdate" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestUpdate(t *testing.T) { @@ -20,7 +20,6 @@ func TestUpdate(t *testing.T) { defer clog.CloseTestLog() err := confirmAndSelfUpdate(true, true, "0.0.1", false) - assert.Error(t, err) - assert.Truef(t, errors.Is(err, selfupdate.ErrExecutableNotFoundInArchive), "error returned isn't wrapping %q but is instead: %q", selfupdate.ErrExecutableNotFoundInArchive, err) + require.ErrorIsf(t, err, selfupdate.ErrExecutableNotFoundInArchive, "error returned isn't wrapping %q but is instead: %q", selfupdate.ErrExecutableNotFoundInArchive, err) assert.Contains(t, err.Error(), "resticprofile.test") } diff --git a/util/decoder_test.go b/util/decoder_test.go index 8a14bb04..75f9ccfa 100644 --- a/util/decoder_test.go +++ b/util/decoder_test.go @@ -65,6 +65,7 @@ func TestDecoderOnErrorInput(t *testing.T) { func TestMustRewindToStart(t *testing.T) { buf := make([]byte, 1) read := func(t *testing.T, reader io.Reader) byte { + t.Helper() n, err := reader.Read(buf) assert.Equal(t, n, 1) assert.NoError(t, err) diff --git a/util/dotenv_test.go b/util/dotenv_test.go index 83423c8c..6ff07206 100644 --- a/util/dotenv_test.go +++ b/util/dotenv_test.go @@ -71,7 +71,7 @@ func TestGetEnvironmentFileErrors(t *testing.T) { assert.ErrorContains(t, ef.init("n"), fmt.Sprintf("illegal state, file %s already loaded", dotenv)) // error for invalid content - require.NoError(t, os.WriteFile(dotenv, []byte(" + _ ."), 0700)) + require.NoError(t, os.WriteFile(dotenv, []byte(" + _ ."), 0o600)) _, err = GetEnvironmentFile(dotenv) assert.ErrorContains(t, err, `unexpected character "+" in variable name near "+ _ ."`) } diff --git a/util/maybe/bool.go b/util/maybe/bool.go index 3114db8d..7381c80f 100644 --- a/util/maybe/bool.go +++ b/util/maybe/bool.go @@ -25,11 +25,11 @@ func (value Bool) IsTrue() bool { } func (value Bool) IsStrictlyFalse() bool { - return value.HasValue() && value.Value() == false + return value.HasValue() && !value.Value() } func (value Bool) IsFalseOrUndefined() bool { - return !value.HasValue() || value.Value() == false + return !value.HasValue() || !value.Value() } func (value Bool) IsUndefined() bool { @@ -37,7 +37,7 @@ func (value Bool) IsUndefined() bool { } func (value Bool) IsTrueOrUndefined() bool { - return !value.HasValue() || value.Value() == true + return !value.HasValue() || value.Value() } func BoolFromNilable(value *bool) Bool { diff --git a/util/tempdir.go b/util/tempdir.go index 84022577..2a3b4e6a 100644 --- a/util/tempdir.go +++ b/util/tempdir.go @@ -12,7 +12,7 @@ import ( var ( tempDirInitializer sync.Once tempDir string - tempDirErr error + errTempDir error ) const ( @@ -23,18 +23,18 @@ const ( // TempDir returns the path to a temporary directory that is stable within the same process and cleaned when shutdown.RunHooks is invoked func TempDir() (string, error) { tempDirInitializer.Do(func() { - tempDir, tempDirErr = createTempDir("") + tempDir, errTempDir = createTempDir("") if !shutdown.ContainsHook(tempDirHookTag) { shutdown.AddHook(func() { - removeTempDir(tempDir, tempDirErr) + removeTempDir(tempDir, errTempDir) tempDir = "" - tempDirErr = fmt.Errorf("illegal state: temp directory has been removed") + errTempDir = fmt.Errorf("illegal state: temp directory has been removed") }, tempDirHookTag) } }) - return tempDir, tempDirErr + return tempDir, errTempDir } // MustGetTempDir returns the dir from TempDir or panics if an error occurred @@ -49,9 +49,9 @@ func MustGetTempDir() string { // ClearTempDir removes the temporary directory (if present) and resets the state. // This is not safe for concurrent use and is meant for cleanup in unit tests only. func ClearTempDir() { - removeTempDir(tempDir, tempDirErr) + removeTempDir(tempDir, errTempDir) tempDir = "" - tempDirErr = nil + errTempDir = nil tempDirInitializer = sync.Once{} } @@ -84,5 +84,4 @@ func removeTempDir(tempDir string, tempDirErr error) { clog.Warningf("failed removing temporary directory %q: %s", tempDir, tempDirErr.Error()) } } - return } diff --git a/util/tempdir_test.go b/util/tempdir_test.go index a277b43c..cee02b53 100644 --- a/util/tempdir_test.go +++ b/util/tempdir_test.go @@ -69,7 +69,7 @@ func TestTempDir(t *testing.T) { t.Run("can-clear", func(t *testing.T) { ClearTempDir() dir := MustGetTempDir() - assert.NoError(t, os.WriteFile(filepath.Join(dir, "test.file"), []byte("data"), 0700)) + assert.NoError(t, os.WriteFile(filepath.Join(dir, "test.file"), []byte("data"), 0o600)) ClearTempDir() dir2 := MustGetTempDir() diff --git a/util/templates/functions.go b/util/templates/functions.go index b5ac9768..ddaf3a4c 100644 --- a/util/templates/functions.go +++ b/util/templates/functions.go @@ -47,7 +47,7 @@ func TemplateFuncs(funcs ...map[string]any) (templateFuncs map[string]any) { templateFuncs = map[string]any{ "contains": func(search any, src any) bool { return strings.Contains(toString(src), toString(search)) }, "matches": func(ptn string, src any) bool { return mustCompile(ptn).MatchString(toString(src)) }, - "replace": func(old, new, src string) string { return strings.ReplaceAll(src, old, new) }, + "replace": func(oldStr, newStr, src string) string { return strings.ReplaceAll(src, oldStr, newStr) }, "replaceR": func(ptn, repl, src string) string { return mustCompile(ptn).ReplaceAllString(src, repl) }, "lower": strings.ToLower, "upper": strings.ToUpper, @@ -125,8 +125,8 @@ func TempFile(name string) (filename string) { return } -// NotStrictlyPrivate indicates that a PrivateTempFile was successfully created but the OS reports that it can be accessed by others -var NotStrictlyPrivate = errors.New("the private temp file is not strictly accessible by owners only") +// ErrNotStrictlyPrivate indicates that a PrivateTempFile was successfully created but the OS reports that it can be accessed by others +var ErrNotStrictlyPrivate = errors.New("the private temp file is not strictly accessible by owners only") // PrivateTempFile is like TempFile but guarantees that the returned file can be accessed by owners only when err is nil func PrivateTempFile(name string) (filename string, err error) { @@ -134,7 +134,7 @@ func PrivateTempFile(name string) (filename string, err error) { const privateMode = os.FileMode(0600) if err = os.Chmod(filename, privateMode); err == nil { if stat, e := os.Stat(filename); e != nil || stat.Mode() != privateMode { - err = NotStrictlyPrivate + err = ErrNotStrictlyPrivate } } return @@ -152,7 +152,7 @@ func EnvFileFunc(receiverFunc EnvFileReceiverFunc) map[string]any { if len(envFile) == 0 { var err error envFile, err = PrivateTempFile(fmt.Sprintf("%s.env", profile)) - if err != nil && !errors.Is(err, NotStrictlyPrivate) { + if err != nil && !errors.Is(err, ErrNotStrictlyPrivate) { panic(fmt.Errorf("failed setting permissions for %s: %w", envFile, err)) } files[profile] = envFile diff --git a/wrapper.go b/wrapper.go index 6a5454c3..ff062f70 100644 --- a/wrapper.go +++ b/wrapper.go @@ -156,28 +156,41 @@ func (r *resticWrapper) getBackupAction() func() error { return func() (err error) { // Check before - if err == nil && r.profile.Backup != nil && r.profile.Backup.CheckBefore { + if r.profile.Backup != nil && r.profile.Backup.CheckBefore { err = r.runCheck() + if err != nil { + return + } } // Retention before - if err == nil && r.profile.Retention != nil && r.profile.Retention.BeforeBackup.IsTrue() { + if r.profile.Retention != nil && r.profile.Retention.BeforeBackup.IsTrue() { err = r.runRetention() + if err != nil { + return + } } // Backup command - if err == nil { - err = backupAction() + err = backupAction() + if err != nil { + return } // Retention after - if err == nil && r.profile.Retention != nil && r.profile.Retention.AfterBackup.IsTrue() { + if r.profile.Retention != nil && r.profile.Retention.AfterBackup.IsTrue() { err = r.runRetention() + if err != nil { + return + } } // Check after - if err == nil && r.profile.Backup != nil && r.profile.Backup.CheckAfter { + if r.profile.Backup != nil && r.profile.Backup.CheckAfter { err = r.runCheck() + if err != nil { + return + } } return @@ -554,7 +567,7 @@ func (r *resticWrapper) runCommand(command string) error { r.executionTime += summary.Duration r.summary(r.command, summary, stderr, err) - if err != nil && !r.canSucceedAfterError(command, summary, err) { + if err != nil && !r.canSucceedAfterError(command, err) { retry, interruptedError := r.canRetryAfterError(command, summary) if retry { continue @@ -794,7 +807,7 @@ func (r *resticWrapper) getErrorContext(err error) hook.ErrorContext { } // canSucceedAfterError returns true if an error reported by running restic in runCommand can be counted as success -func (r *resticWrapper) canSucceedAfterError(command string, summary monitor.Summary, err error) bool { +func (r *resticWrapper) canSucceedAfterError(command string, err error) bool { if err == nil { return true } diff --git a/wrapper_streamsource.go b/wrapper_streamsource.go index f5219106..ffb19096 100644 --- a/wrapper_streamsource.go +++ b/wrapper_streamsource.go @@ -24,6 +24,7 @@ func (r *resticWrapper) prepareStreamSource() (io.ReadCloser, error) { return r.prepareStdinStreamSource() } } + //nolint:nilnil return nil, nil } diff --git a/wrapper_test.go b/wrapper_test.go index 2ff8aaec..8fdd38b7 100644 --- a/wrapper_test.go +++ b/wrapper_test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "log" "math/rand" "os" "path" @@ -358,6 +359,7 @@ func TestFinallyProfile(t *testing.T) { } assertFileEquals := func(t *testing.T, expected string) { + t.Helper() content, err := os.ReadFile(testFile) require.NoError(t, err) assert.Equal(t, strings.TrimSpace(string(content)), expected) @@ -413,7 +415,10 @@ func Example_runProfile() { command: "test", } wrapper := newResticWrapper(ctx) - wrapper.runProfile() + err := wrapper.runProfile() + if err != nil { + log.Fatal(err) + } // Output: test } @@ -716,6 +721,7 @@ func TestBackupWithStreamSource(t *testing.T) { } run := func(t *testing.T, wrapper *resticWrapper) (string, error) { + t.Helper() file := path.Join(os.TempDir(), fmt.Sprintf("TestBackupWithStreamSource.%d.txt", rand.Int())) defer os.Remove(file) @@ -811,7 +817,7 @@ func TestBackupWithStreamSource(t *testing.T) { start := time.Now() _, err := run(t, wrapper) - assert.Less(t, time.Now().Sub(start), time.Second*12, "timeout, interrupt not sent to restic") + assert.Less(t, time.Since(start), time.Second*12, "timeout, interrupt not sent to restic") require.NotNil(t, err) assert.Contains(t, expectedInterruptedError, err.Error()) @@ -831,7 +837,7 @@ func TestBackupWithStreamSource(t *testing.T) { }() start := time.Now() _, err := run(t, wrapper) - assert.Less(t, time.Now().Sub(start), time.Second*5, "timeout, interrupt not sent to stdin-command") + assert.Less(t, time.Since(start), time.Second*5, "timeout, interrupt not sent to stdin-command") require.NotNil(t, err) assert.Error(t, err) @@ -882,7 +888,7 @@ func TestBackupWithResticLockFailureRetried(t *testing.T) { "storage ID c8a44e77" + platform.LineSeparator + "the `unlock` command can be used to remove stale locks" + platform.LineSeparator tempfile := filepath.Join(t.TempDir(), "TestBackupWithResticLockFailureRetried.txt") - err := os.WriteFile(tempfile, []byte(lockMessage), 0644) + err := os.WriteFile(tempfile, []byte(lockMessage), 0o600) require.NoError(t, err) defer os.Remove(tempfile) @@ -918,7 +924,7 @@ func TestBackupWithResticLockFailureCancelled(t *testing.T) { "storage ID c8a44e77" + platform.LineSeparator + "the `unlock` command can be used to remove stale locks" + platform.LineSeparator tempfile := filepath.Join(t.TempDir(), "TestBackupWithResticLockFailureCancelled.txt") - err := os.WriteFile(tempfile, []byte(lockMessage), 0644) + err := os.WriteFile(tempfile, []byte(lockMessage), 0o600) require.NoError(t, err) defer os.Remove(tempfile) @@ -1023,7 +1029,7 @@ func TestRunShellCommands(t *testing.T) { profile.Forget = &config.SectionWithScheduleAndMonitoring{} profile.Init = &config.InitSection{} profile.Prune = &config.SectionWithScheduleAndMonitoring{} - for name, _ := range profile.OtherSections { + for name := range profile.OtherSections { profile.OtherSections[name] = new(config.GenericSection) } @@ -1170,17 +1176,20 @@ func TestCanRetryAfterRemoteStaleLockFailure(t *testing.T) { assert.True(t, mockOutput.ContainsRemoteLockFailure()) retry, sleep := wrapper.canRetryAfterRemoteLockFailure(mockOutput) assert.False(t, retry) + assert.Equal(t, time.Duration(0), sleep) // Ignores stale lock when disabled lockedSince = constants.MinResticStaleLockAge retry, sleep = wrapper.canRetryAfterRemoteLockFailure(mockOutput) assert.False(t, retry) + assert.Equal(t, time.Duration(0), sleep) // Ignores non-stale lock lockedSince = constants.MinResticStaleLockAge - time.Nanosecond wrapper.global.ResticStaleLockAge = time.Millisecond retry, sleep = wrapper.canRetryAfterRemoteLockFailure(mockOutput) assert.False(t, retry) + assert.Equal(t, time.Duration(0), sleep) // Unlocks stale lock lockedSince = constants.MinResticStaleLockAge @@ -1193,16 +1202,19 @@ func TestCanRetryAfterRemoteStaleLockFailure(t *testing.T) { // Unlock is run only once retry, sleep = wrapper.canRetryAfterRemoteLockFailure(mockOutput) assert.False(t, retry) + assert.Equal(t, time.Duration(0), sleep) // Unlock is not run when ForceLock is disabled wrapper.doneTryUnlock = false retry, sleep = wrapper.canRetryAfterRemoteLockFailure(mockOutput) assert.True(t, retry) + assert.Equal(t, time.Duration(0), sleep) profile.ForceLock = false wrapper.doneTryUnlock = false retry, sleep = wrapper.canRetryAfterRemoteLockFailure(mockOutput) assert.False(t, retry) + assert.Equal(t, time.Duration(0), sleep) } func TestCanRetryAfterRemoteLockFailure(t *testing.T) { @@ -1229,6 +1241,7 @@ func TestCanRetryAfterRemoteLockFailure(t *testing.T) { assert.False(t, mockOutput.ContainsRemoteLockFailure()) retry, sleep := wrapper.canRetryAfterRemoteLockFailure(mockOutput) assert.False(t, retry) + assert.Equal(t, time.Duration(0), sleep) // No retry when lockWait is nil lockFailure = true @@ -1246,6 +1259,7 @@ func TestCanRetryAfterRemoteLockFailure(t *testing.T) { wrapper.global.ResticLockRetryAfter = constants.MinResticLockRetryDelay // enable remote lock retry retry, sleep = wrapper.canRetryAfterRemoteLockFailure(mockOutput) assert.False(t, retry) + assert.Equal(t, time.Duration(0), sleep) // Retry is acceptable when there is enough remaining time for the delay (ResticLockRetryAfter) wrapper.maxWaitOnLock(constants.MinResticLockRetryDelay + 50*time.Millisecond)