Skip to content

Commit

Permalink
fix: improve timezone detection to work on macOS Sequoia, for ddev#6468
Browse files Browse the repository at this point in the history
… (ddev#6603)
  • Loading branch information
stasadev authored Oct 10, 2024
1 parent 9dadad6 commit e5bcf28
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 18 deletions.
21 changes: 3 additions & 18 deletions pkg/ddevapp/ddevapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -1187,28 +1187,13 @@ func (app *DdevApp) GetLocalTimezone() (string, error) {
timezone = os.Getenv("TZ")
} else {
localtimeFile := filepath.Join("/etc", "localtime")
timezoneFile, err := filepath.EvalSymlinks(localtimeFile)
var err error
timezone, err = filepath.EvalSymlinks(localtimeFile)
if err != nil {
return "", fmt.Errorf("unable to read timezone from %s file: %v", localtimeFile, err)
}
// /etc/localtime is a symlink to a file, for example:
// /usr/share/zoneinfo/Europe/London on Linux and WSL2
// /var/db/timezone/zoneinfo/Europe/London on macOS (the exact path to /zoneinfo/ may differ)
// We can search for anything after /zoneinfo/ in the file path
parts := strings.Split(strings.TrimSpace(timezoneFile), "/zoneinfo/")
if len(parts) != 2 {
return "", fmt.Errorf("unable to read timezone from %s file", timezoneFile)
}
timezone = parts[1]
if timezone == "" {
return "", fmt.Errorf("unable to read timezone from %s file", timezoneFile)
}
}
_, err := time.LoadLocation(timezone)
if err != nil {
return "", fmt.Errorf("failed to load timezone '%s': %v", timezone, err)
}
return timezone, nil
return util.GetTimezone(timezone)
}

// Start initiates docker-compose up
Expand Down
31 changes: 31 additions & 0 deletions pkg/util/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
osexec "os/exec"
"os/user"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
Expand Down Expand Up @@ -388,3 +389,33 @@ func Chmod(path string, mode os.FileMode) error {
}
return os.Chmod(path, mode)
}

// GetTimezone tries to find local timezone from the path, that can be
// either $TZ environment variable or /etc/localtime symlink
func GetTimezone(path string) (string, error) {
// Use case-insensitive search for /zoneinfo/ in the file path.
regex := regexp.MustCompile(`(?i)/.*?zoneinfo.*?/`)
parts := regex.Split(strings.TrimSpace(path), 2)
if len(parts) != 2 {
// If this is not a path, but timezone, return it.
_, err := time.LoadLocation(path)
if err == nil {
return path, nil
}
return "", fmt.Errorf("unable to read timezone from %s", path)
}
timezone := parts[1]
// Remove leading prefixes if they exist.
// https://stackoverflow.com/a/67888343/8097891
for _, prefix := range []string{"posix/", "right/"} {
timezone = strings.TrimPrefix(timezone, prefix)
}
if timezone == "" {
return "", fmt.Errorf("unable to read timezone from %s", path)
}
_, err := time.LoadLocation(timezone)
if err != nil {
return "", fmt.Errorf("failed to load timezone '%s': %v", timezone, err)
}
return timezone, nil
}
36 changes: 36 additions & 0 deletions pkg/util/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/ddev/ddev/pkg/output"
"github.com/ddev/ddev/pkg/util"
asrt "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestRandString ensures that RandString only generates string of the correct value and characters.
Expand Down Expand Up @@ -194,3 +195,38 @@ func TestArrayToReadableOutput(t *testing.T) {
_, err := util.ArrayToReadableOutput([]string{})
assert.EqualErrorf(err, "empty slice", "Expected error when passing an empty slice")
}

// TestGetTimezone tests GetTimezone
func TestGetTimezone(t *testing.T) {
testCases := []struct {
description string
input string
result string
error string
}{
{"$TZ env var", "Europe/London", "Europe/London", ""},
{"Linux /etc/localtime symlink", "/usr/share/zoneinfo/Europe/London", "Europe/London", ""},
{"Linux /etc/localtime posix symlink", "/usr/share/zoneinfo/posix/Europe/London", "Europe/London", ""},
{"Linux /etc/localtime right symlink", "/usr/share/zoneinfo/right/Europe/London", "Europe/London", ""},
{"/etc/localtime is not a symlink", "/etc/localtime", "", "unable to read timezone from /etc/localtime"},
{"macOS /etc/localtime symlink", "/var/db/timezone/zoneinfo/Europe/London", "Europe/London", ""},
{"macOS Sonoma /etc/localtime symlink", "/private/var/db/timezone/tz/2024a.1.0/zoneinfo/Europe/London", "Europe/London", ""},
{"macOS Sequoia /etc/localtime symlink", "/usr/share/zoneinfo.default/Europe/London", "Europe/London", ""},
{"Case-insensitive search for /zoneinfo/ in the path", "/path/to/TestZoneInfoTest/Europe/London", "Europe/London", ""},
{"Timezone has wrong format", "/Europe/London", "", "unable to read timezone from /Europe/London"},
{"Not real timezone", "Europe/Test", "", "unable to read timezone from Europe/Test"},
}

for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
timezone, err := util.GetTimezone(tc.input)
require.Equal(t, tc.result, timezone)
if tc.error == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tc.error)
}
})
}
}

0 comments on commit e5bcf28

Please sign in to comment.