mirror of
https://github.com/chenasraf/sofmani.git
synced 2026-05-17 17:28:04 +00:00
feat: add frequency option to limit how often installers run
This commit is contained in:
@@ -146,6 +146,7 @@ The following flags are supported to customize behavior:
|
|||||||
| `-s`, `--summary` | Enable installation summary (default). |
|
| `-s`, `--summary` | Enable installation summary (default). |
|
||||||
| `-S`, `--no-summary` | Disable installation summary. |
|
| `-S`, `--no-summary` | Disable installation summary. |
|
||||||
| `-f`, `--filter` | Filter by installer name (can be used multiple times) |
|
| `-f`, `--filter` | Filter by installer name (can be used multiple times) |
|
||||||
|
| `--ignore-frequency` | Ignore frequency limits and run all installers. |
|
||||||
| `-h`, `--help` | Display help information and exit. |
|
| `-h`, `--help` | Display help information and exit. |
|
||||||
| `-v`, `--version` | Display version information and exit. |
|
| `-v`, `--version` | Display version information and exit. |
|
||||||
|
|
||||||
@@ -222,6 +223,7 @@ See [Installer Configuration](./docs/installer-configuration.md#categories) for
|
|||||||
| `env_shell` | Object (optional) | Shell to use for command executions. See `env_shell` subfields below. |
|
| `env_shell` | Object (optional) | Shell to use for command executions. See `env_shell` subfields below. |
|
||||||
| `env_shell.macos` | String (optional) | Shell to use for macOS command executions. If not specified, the default shell will be used. |
|
| `env_shell.macos` | String (optional) | Shell to use for macOS command executions. If not specified, the default shell will be used. |
|
||||||
| `env_shell.linux` | String (optional) | Shell to use for Linux command executions. If not specified, the default shell will be used. |
|
| `env_shell.linux` | String (optional) | Shell to use for Linux command executions. If not specified, the default shell will be used. |
|
||||||
|
| `frequency` | String (optional) | Limits how often the installer runs. After a successful install/update, the next run is skipped until the duration elapses. Supports units: `s`, `m`, `h`, `d`, `w` (e.g., `1d`, `1w`, `12h`). Use `--ignore-frequency` to bypass. |
|
||||||
| `skip_summary` | Boolean or Object | Exclude this installer from the summary. Set to `true` to skip both install/update summaries, or use `{install: true}` / `{update: true}` for granular control. Useful for installers that always run. |
|
| `skip_summary` | Boolean or Object | Exclude this installer from the summary. Set to `true` to skip both install/update summaries, or use `{install: true}` / `{update: true}` for granular control. Useful for installers that always run. |
|
||||||
|
|
||||||
### Supported `type` of Installers
|
### Supported `type` of Installers
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ type AppConfig struct {
|
|||||||
MachineAliases *map[string]string `json:"machine_aliases" yaml:"machine_aliases"`
|
MachineAliases *map[string]string `json:"machine_aliases" yaml:"machine_aliases"`
|
||||||
// Filter is a list of installer names to filter by.
|
// Filter is a list of installer names to filter by.
|
||||||
Filter []string
|
Filter []string
|
||||||
|
// IgnoreFrequency overrides frequency checks, running all installers regardless.
|
||||||
|
IgnoreFrequency bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRepoUpdateMode returns the repo update mode for the given installer type,
|
// GetRepoUpdateMode returns the repo update mode for the given installer type,
|
||||||
@@ -93,6 +95,8 @@ type AppCliConfig struct {
|
|||||||
ShowLogFile bool
|
ShowLogFile bool
|
||||||
// ShowMachineID indicates that only the machine ID should be shown.
|
// ShowMachineID indicates that only the machine ID should be shown.
|
||||||
ShowMachineID bool
|
ShowMachineID bool
|
||||||
|
// IgnoreFrequency overrides frequency checks, running all installers regardless.
|
||||||
|
IgnoreFrequency bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppConfigDefaults provides default configurations for installer types.
|
// AppConfigDefaults provides default configurations for installer types.
|
||||||
@@ -131,6 +135,7 @@ func ParseConfig(overrides *AppCliConfig) (*AppConfig, error) {
|
|||||||
appConfig.Summary = overrides.Summary
|
appConfig.Summary = overrides.Summary
|
||||||
}
|
}
|
||||||
appConfig.Filter = overrides.Filter
|
appConfig.Filter = overrides.Filter
|
||||||
|
appConfig.IgnoreFrequency = overrides.IgnoreFrequency
|
||||||
return appConfig, nil
|
return appConfig, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("unsupported config file extension %s (filename: %s)", ext, file)
|
return nil, fmt.Errorf("unsupported config file extension %s (filename: %s)", ext, file)
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ type InstallerData struct {
|
|||||||
SkipSummary *SkipSummary `json:"skip_summary" yaml:"skip_summary"`
|
SkipSummary *SkipSummary `json:"skip_summary" yaml:"skip_summary"`
|
||||||
// Verbose enables verbose output for the installer's native commands.
|
// Verbose enables verbose output for the installer's native commands.
|
||||||
Verbose *bool `json:"verbose" yaml:"verbose"`
|
Verbose *bool `json:"verbose" yaml:"verbose"`
|
||||||
|
// Frequency is a prettified duration (e.g. "1d", "1w", "3m") that limits how often
|
||||||
|
// the installer runs. After a successful install/update, the next run will be skipped
|
||||||
|
// until the frequency period has elapsed.
|
||||||
|
Frequency *string `json:"frequency" yaml:"frequency"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallerType represents the type of an installer.
|
// InstallerType represents the type of an installer.
|
||||||
|
|||||||
39
cmd/root.go
39
cmd/root.go
@@ -14,15 +14,16 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// Flag variables
|
// Flag variables
|
||||||
debug bool
|
debug bool
|
||||||
noDebug bool
|
noDebug bool
|
||||||
update bool
|
update bool
|
||||||
noUpdate bool
|
noUpdate bool
|
||||||
summary bool
|
summary bool
|
||||||
noSummary bool
|
noSummary bool
|
||||||
filter []string
|
filter []string
|
||||||
logFile string
|
logFile string
|
||||||
machineID bool
|
machineID bool
|
||||||
|
ignoreFrequency bool
|
||||||
|
|
||||||
// The parsed CLI config
|
// The parsed CLI config
|
||||||
cliConfig *appconfig.AppCliConfig
|
cliConfig *appconfig.AppCliConfig
|
||||||
@@ -129,6 +130,9 @@ func init() {
|
|||||||
|
|
||||||
// Machine ID flag
|
// Machine ID flag
|
||||||
rootCmd.Flags().BoolVarP(&machineID, "machine-id", "m", false, "Show machine ID and exit")
|
rootCmd.Flags().BoolVarP(&machineID, "machine-id", "m", false, "Show machine ID and exit")
|
||||||
|
|
||||||
|
// Ignore frequency flag
|
||||||
|
rootCmd.Flags().BoolVar(&ignoreFrequency, "ignore-frequency", false, "Ignore frequency limits and run all installers")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetVersion sets the version for the root command.
|
// SetVersion sets the version for the root command.
|
||||||
@@ -142,14 +146,15 @@ func SetVersion(version string) {
|
|||||||
// buildCliConfig creates an AppCliConfig from the parsed Cobra flags.
|
// buildCliConfig creates an AppCliConfig from the parsed Cobra flags.
|
||||||
func buildCliConfig(cmd *cobra.Command, args []string) *appconfig.AppCliConfig {
|
func buildCliConfig(cmd *cobra.Command, args []string) *appconfig.AppCliConfig {
|
||||||
config := &appconfig.AppCliConfig{
|
config := &appconfig.AppCliConfig{
|
||||||
ConfigFile: "",
|
ConfigFile: "",
|
||||||
Debug: nil,
|
Debug: nil,
|
||||||
CheckUpdates: nil,
|
CheckUpdates: nil,
|
||||||
Summary: nil,
|
Summary: nil,
|
||||||
Filter: filter,
|
Filter: filter,
|
||||||
LogFile: nil,
|
LogFile: nil,
|
||||||
ShowLogFile: false,
|
ShowLogFile: false,
|
||||||
ShowMachineID: machineID,
|
ShowMachineID: machineID,
|
||||||
|
IgnoreFrequency: ignoreFrequency,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle debug flag
|
// Handle debug flag
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ You can call `sofmani` with the following flags to alter the behavior for the cu
|
|||||||
| `-f`, `--filter` | Filter by installer name (can be used multiple times)\* |
|
| `-f`, `--filter` | Filter by installer name (can be used multiple times)\* |
|
||||||
| `-l`, `--log-file` | Set log file path, or show current path if no value. |
|
| `-l`, `--log-file` | Set log file path, or show current path if no value. |
|
||||||
| `-m`, `--machine-id` | Show machine ID and exit. |
|
| `-m`, `--machine-id` | Show machine ID and exit. |
|
||||||
|
| `--ignore-frequency` | Ignore frequency limits and run all installers. |
|
||||||
| `-h`, `--help` | Display help information and exit. |
|
| `-h`, `--help` | Display help information and exit. |
|
||||||
| `-v`, `--version` | Display version information and exit. |
|
| `-v`, `--version` | Display version information and exit. |
|
||||||
|
|
||||||
|
|||||||
@@ -293,6 +293,44 @@ These fields are shared by all installer types. Some fields may vary in behavior
|
|||||||
verbose: true
|
verbose: true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **`frequency`**
|
||||||
|
- **Type**: String (optional)
|
||||||
|
- **Description**: Limits how often the installer runs. After a successful install or update, the
|
||||||
|
next run will be skipped until the specified duration has elapsed. The timestamp of the last
|
||||||
|
successful run is stored in the sofmani cache directory.
|
||||||
|
- **Format**: A prettified duration string. Multiple components can be combined. Supported units:
|
||||||
|
- `s` — seconds (e.g., `60s`)
|
||||||
|
- `m` — minutes (e.g., `30m`)
|
||||||
|
- `h` — hours (e.g., `12h`)
|
||||||
|
- `d` — days (e.g., `1d`)
|
||||||
|
- `w` — weeks (e.g., `1w`)
|
||||||
|
- Combined: `1d12h`, `1w2d`
|
||||||
|
- **Default**: Not set (installer runs every time).
|
||||||
|
- **Note**: Use the `--ignore-frequency` CLI flag to bypass frequency checks for all installers.
|
||||||
|
- **Examples**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Only check for updates once a day
|
||||||
|
- name: neovim
|
||||||
|
type: brew
|
||||||
|
frequency: 1d
|
||||||
|
|
||||||
|
# Only run once a week
|
||||||
|
- name: sync-dotfiles
|
||||||
|
type: rsync
|
||||||
|
frequency: 1w
|
||||||
|
opts:
|
||||||
|
source: ~/.dotfiles/.config
|
||||||
|
destination: ~/.config
|
||||||
|
|
||||||
|
# Run at most every 12 hours
|
||||||
|
- name: my-tool
|
||||||
|
type: shell
|
||||||
|
frequency: 12h
|
||||||
|
opts:
|
||||||
|
command: ./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
- **`skip_summary`**
|
- **`skip_summary`**
|
||||||
- **Type**: Boolean or Object (optional)
|
- **Type**: Boolean or Object (optional)
|
||||||
- **Description**: Exclude this installer from the installation summary. Useful for installers
|
- **Description**: Exclude this installer from the installation summary. Useful for installers
|
||||||
|
|||||||
73
installer/frequency.go
Normal file
73
installer/frequency.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package installer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/chenasraf/sofmani/logger"
|
||||||
|
"github.com/chenasraf/sofmani/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// frequencyCacheFileName returns the cache file name for a given installer name,
|
||||||
|
// escaping characters that are not safe for file names.
|
||||||
|
func frequencyCacheFileName(name string) string {
|
||||||
|
replacer := strings.NewReplacer(
|
||||||
|
"/", "__",
|
||||||
|
"\\", "__",
|
||||||
|
":", "__",
|
||||||
|
" ", "_",
|
||||||
|
)
|
||||||
|
return "freq_" + replacer.Replace(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkFrequency checks whether enough time has passed since the last successful run
|
||||||
|
// for an installer with the given name and frequency string.
|
||||||
|
// Returns true if the installer should run (frequency elapsed or no previous run).
|
||||||
|
func checkFrequency(name string, frequency string) (bool, error) {
|
||||||
|
dur, err := utils.ParsePrettyDuration(frequency)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir, err := utils.GetCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
return true, nil // if we can't get cache dir, just run
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheFile := filepath.Join(cacheDir, frequencyCacheFileName(name))
|
||||||
|
data, err := os.ReadFile(cacheFile)
|
||||||
|
if err != nil {
|
||||||
|
// No previous run recorded
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ts, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
// Corrupt cache file, just run
|
||||||
|
logger.Debug("Invalid frequency cache for %s, will run", logger.H(name))
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRun := time.Unix(ts, 0)
|
||||||
|
if time.Since(lastRun) < dur {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeFrequencyTimestamp writes the current timestamp to the frequency cache file
|
||||||
|
// for the given installer name.
|
||||||
|
func writeFrequencyTimestamp(name string) error {
|
||||||
|
cacheDir, err := utils.GetCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheFile := filepath.Join(cacheDir, frequencyCacheFileName(name))
|
||||||
|
ts := strconv.FormatInt(time.Now().Unix(), 10)
|
||||||
|
return os.WriteFile(cacheFile, []byte(ts), 0644)
|
||||||
|
}
|
||||||
78
installer/frequency_test.go
Normal file
78
installer/frequency_test.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package installer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/chenasraf/sofmani/utils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFrequencyCacheFileName(t *testing.T) {
|
||||||
|
assert.Equal(t, "freq_my-installer", frequencyCacheFileName("my-installer"))
|
||||||
|
assert.Equal(t, "freq_path__to__thing", frequencyCacheFileName("path/to/thing"))
|
||||||
|
assert.Equal(t, "freq_has_spaces", frequencyCacheFileName("has spaces"))
|
||||||
|
assert.Equal(t, "freq_back__slash", frequencyCacheFileName("back\\slash"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckFrequency_NoPreviousRun(t *testing.T) {
|
||||||
|
shouldRun, err := checkFrequency("nonexistent-installer-test", "1d")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, shouldRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckFrequency_RecentRun(t *testing.T) {
|
||||||
|
name := "test-freq-recent"
|
||||||
|
cacheDir, err := utils.GetCacheDir()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cacheFile := filepath.Join(cacheDir, frequencyCacheFileName(name))
|
||||||
|
ts := strconv.FormatInt(time.Now().Unix(), 10)
|
||||||
|
err = os.WriteFile(cacheFile, []byte(ts), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = os.Remove(cacheFile) }()
|
||||||
|
|
||||||
|
shouldRun, err := checkFrequency(name, "1d")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, shouldRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckFrequency_ExpiredRun(t *testing.T) {
|
||||||
|
name := "test-freq-expired"
|
||||||
|
cacheDir, err := utils.GetCacheDir()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cacheFile := filepath.Join(cacheDir, frequencyCacheFileName(name))
|
||||||
|
ts := strconv.FormatInt(time.Now().Add(-48*time.Hour).Unix(), 10)
|
||||||
|
err = os.WriteFile(cacheFile, []byte(ts), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = os.Remove(cacheFile) }()
|
||||||
|
|
||||||
|
shouldRun, err := checkFrequency(name, "1d")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, shouldRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFrequencyTimestamp(t *testing.T) {
|
||||||
|
name := "test-freq-write"
|
||||||
|
cacheDir, err := utils.GetCacheDir()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cacheFile := filepath.Join(cacheDir, frequencyCacheFileName(name))
|
||||||
|
defer func() { _ = os.Remove(cacheFile) }()
|
||||||
|
|
||||||
|
err = writeFrequencyTimestamp(name)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(cacheFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ts, err := strconv.ParseInt(string(data), 10, 64)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.InDelta(t, time.Now().Unix(), ts, 2)
|
||||||
|
}
|
||||||
@@ -263,6 +263,18 @@ func RunInstaller(config *appconfig.AppConfig, installer IInstaller) (*summary.I
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check frequency limits (unless --ignore-frequency is set)
|
||||||
|
if !config.IgnoreFrequency && info.Frequency != nil && *info.Frequency != "" {
|
||||||
|
shouldRun, err := checkFrequency(name, *info.Frequency)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to check frequency for %s: %v", logger.H(name), err)
|
||||||
|
} else if !shouldRun {
|
||||||
|
logger.Debug("%s: skipping due to frequency %s", logger.H(name), *info.Frequency)
|
||||||
|
result.Action = summary.ActionSkipped
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.Debug("Checking %s: %s", logger.H(string(info.Type)), logger.H(name))
|
logger.Debug("Checking %s: %s", logger.H(string(info.Type)), logger.H(name))
|
||||||
installed, err := installer.CheckIsInstalled()
|
installed, err := installer.CheckIsInstalled()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -330,6 +342,14 @@ func RunInstaller(config *appconfig.AppConfig, installer IInstaller) (*summary.I
|
|||||||
result.Action = summary.ActionInstalled
|
result.Action = summary.ActionInstalled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write frequency timestamp on successful install/update
|
||||||
|
if info.Frequency != nil && *info.Frequency != "" &&
|
||||||
|
(result.Action == summary.ActionInstalled || result.Action == summary.ActionUpgraded) {
|
||||||
|
if err := writeFrequencyTimestamp(name); err != nil {
|
||||||
|
logger.Warn("Failed to write frequency timestamp for %s: %v", logger.H(name), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Collect child results for group/manifest installers
|
// Collect child results for group/manifest installers
|
||||||
if provider, ok := installer.(IChildResultsProvider); ok {
|
if provider, ok := installer.(IChildResultsProvider); ok {
|
||||||
result.Children = provider.GetChildResults()
|
result.Children = provider.GetChildResults()
|
||||||
|
|||||||
60
utils/duration.go
Normal file
60
utils/duration.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParsePrettyDuration parses a human-friendly duration string like "1d", "2w", "3m", "60s", "1h".
|
||||||
|
// Supported units: s (seconds), m (minutes), h (hours), d (days), w (weeks).
|
||||||
|
// Multiple components can be combined, e.g. "1d12h".
|
||||||
|
func ParsePrettyDuration(s string) (time.Duration, error) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return 0, fmt.Errorf("empty duration string")
|
||||||
|
}
|
||||||
|
|
||||||
|
var total time.Duration
|
||||||
|
remaining := s
|
||||||
|
|
||||||
|
for len(remaining) > 0 {
|
||||||
|
// Find the first non-digit character
|
||||||
|
i := 0
|
||||||
|
for i < len(remaining) && remaining[i] >= '0' && remaining[i] <= '9' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
return 0, fmt.Errorf("invalid duration %q: expected number", s)
|
||||||
|
}
|
||||||
|
if i >= len(remaining) {
|
||||||
|
return 0, fmt.Errorf("invalid duration %q: missing unit", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
num, err := strconv.ParseInt(remaining[:i], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid duration %q: %w", s, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
unit := remaining[i]
|
||||||
|
remaining = remaining[i+1:]
|
||||||
|
|
||||||
|
switch unit {
|
||||||
|
case 's':
|
||||||
|
total += time.Duration(num) * time.Second
|
||||||
|
case 'm':
|
||||||
|
total += time.Duration(num) * time.Minute
|
||||||
|
case 'h':
|
||||||
|
total += time.Duration(num) * time.Hour
|
||||||
|
case 'd':
|
||||||
|
total += time.Duration(num) * 24 * time.Hour
|
||||||
|
case 'w':
|
||||||
|
total += time.Duration(num) * 7 * 24 * time.Hour
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("invalid duration %q: unknown unit %q", s, string(unit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
41
utils/duration_test.go
Normal file
41
utils/duration_test.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParsePrettyDuration(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected time.Duration
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"60s", 60 * time.Second, false},
|
||||||
|
{"5m", 5 * time.Minute, false},
|
||||||
|
{"2h", 2 * time.Hour, false},
|
||||||
|
{"1d", 24 * time.Hour, false},
|
||||||
|
{"1w", 7 * 24 * time.Hour, false},
|
||||||
|
{"3d", 3 * 24 * time.Hour, false},
|
||||||
|
{"1d12h", 36 * time.Hour, false},
|
||||||
|
{"1w2d", 9 * 24 * time.Hour, false},
|
||||||
|
{"", 0, true},
|
||||||
|
{"abc", 0, true},
|
||||||
|
{"5", 0, true},
|
||||||
|
{"5x", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
result, err := ParsePrettyDuration(tt.input)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user