aboutsummaryrefslogtreecommitdiffstats
path: root/modules/util
diff options
context:
space:
mode:
Diffstat (limited to 'modules/util')
-rw-r--r--modules/util/error.go68
-rw-r--r--modules/util/filebuffer/file_backed_buffer.go35
-rw-r--r--modules/util/filebuffer/file_backed_buffer_test.go3
-rw-r--r--modules/util/legacy_test.go2
-rw-r--r--modules/util/map.go13
-rw-r--r--modules/util/map_test.go26
-rw-r--r--modules/util/paginate_test.go14
-rw-r--r--modules/util/path.go91
-rw-r--r--modules/util/path_test.go20
-rw-r--r--modules/util/remove.go6
-rw-r--r--modules/util/rotatingfilewriter/writer_test.go2
-rw-r--r--modules/util/runtime_test.go4
-rw-r--r--modules/util/sec_to_time.go65
-rw-r--r--modules/util/sec_to_time_test.go24
-rw-r--r--modules/util/shellquote_test.go10
-rw-r--r--modules/util/slice.go10
-rw-r--r--modules/util/string.go38
-rw-r--r--modules/util/string_test.go5
-rw-r--r--modules/util/time_str.go2
-rw-r--r--modules/util/truncate.go128
-rw-r--r--modules/util/truncate_test.go143
-rw-r--r--modules/util/util.go18
-rw-r--r--modules/util/util_test.go17
23 files changed, 467 insertions, 277 deletions
diff --git a/modules/util/error.go b/modules/util/error.go
index 0f3597147c..6b2721618e 100644
--- a/modules/util/error.go
+++ b/modules/util/error.go
@@ -10,56 +10,88 @@ import (
// Common Errors forming the base of our error system
//
-// Many Errors returned by Gitea can be tested against these errors
-// using errors.Is.
+// Many Errors returned by Gitea can be tested against these errors using "errors.Is".
var (
- ErrInvalidArgument = errors.New("invalid argument")
- ErrPermissionDenied = errors.New("permission denied")
- ErrAlreadyExist = errors.New("resource already exists")
- ErrNotExist = errors.New("resource does not exist")
+ ErrInvalidArgument = errors.New("invalid argument") // also implies HTTP 400
+ ErrPermissionDenied = errors.New("permission denied") // also implies HTTP 403
+ ErrNotExist = errors.New("resource does not exist") // also implies HTTP 404
+ ErrAlreadyExist = errors.New("resource already exists") // also implies HTTP 409
+
+ // ErrUnprocessableContent implies HTTP 422, the syntax of the request content is correct,
+ // but the server is unable to process the contained instructions
+ ErrUnprocessableContent = errors.New("unprocessable content")
)
-// SilentWrap provides a simple wrapper for a wrapped error where the wrapped error message plays no part in the error message
+// errorWrapper provides a simple wrapper for a wrapped error where the wrapped error message plays no part in the error message
// Especially useful for "untyped" errors created with "errors.New(…)" that can be classified as 'invalid argument', 'permission denied', 'exists already', or 'does not exist'
-type SilentWrap struct {
+type errorWrapper struct {
Message string
Err error
}
// Error returns the message
-func (w SilentWrap) Error() string {
+func (w errorWrapper) Error() string {
return w.Message
}
// Unwrap returns the underlying error
-func (w SilentWrap) Unwrap() error {
+func (w errorWrapper) Unwrap() error {
return w.Err
}
-// NewSilentWrapErrorf returns an error that formats as the given text but unwraps as the provided error
-func NewSilentWrapErrorf(unwrap error, message string, args ...any) error {
+type LocaleWrapper struct {
+ err error
+ TrKey string
+ TrArgs []any
+}
+
+// Error returns the message
+func (w LocaleWrapper) Error() string {
+ return w.err.Error()
+}
+
+// Unwrap returns the underlying error
+func (w LocaleWrapper) Unwrap() error {
+ return w.err
+}
+
+// ErrorWrap returns an error that formats as the given text but unwraps as the provided error
+func ErrorWrap(unwrap error, message string, args ...any) error {
if len(args) == 0 {
- return SilentWrap{Message: message, Err: unwrap}
+ return errorWrapper{Message: message, Err: unwrap}
}
- return SilentWrap{Message: fmt.Sprintf(message, args...), Err: unwrap}
+ return errorWrapper{Message: fmt.Sprintf(message, args...), Err: unwrap}
}
// NewInvalidArgumentErrorf returns an error that formats as the given text but unwraps as an ErrInvalidArgument
func NewInvalidArgumentErrorf(message string, args ...any) error {
- return NewSilentWrapErrorf(ErrInvalidArgument, message, args...)
+ return ErrorWrap(ErrInvalidArgument, message, args...)
}
// NewPermissionDeniedErrorf returns an error that formats as the given text but unwraps as an ErrPermissionDenied
func NewPermissionDeniedErrorf(message string, args ...any) error {
- return NewSilentWrapErrorf(ErrPermissionDenied, message, args...)
+ return ErrorWrap(ErrPermissionDenied, message, args...)
}
// NewAlreadyExistErrorf returns an error that formats as the given text but unwraps as an ErrAlreadyExist
func NewAlreadyExistErrorf(message string, args ...any) error {
- return NewSilentWrapErrorf(ErrAlreadyExist, message, args...)
+ return ErrorWrap(ErrAlreadyExist, message, args...)
}
// NewNotExistErrorf returns an error that formats as the given text but unwraps as an ErrNotExist
func NewNotExistErrorf(message string, args ...any) error {
- return NewSilentWrapErrorf(ErrNotExist, message, args...)
+ return ErrorWrap(ErrNotExist, message, args...)
+}
+
+// ErrorWrapLocale wraps an err with a translation key and arguments
+func ErrorWrapLocale(err error, trKey string, trArgs ...any) error {
+ return LocaleWrapper{err: err, TrKey: trKey, TrArgs: trArgs}
+}
+
+func ErrorAsLocale(err error) *LocaleWrapper {
+ var e LocaleWrapper
+ if errors.As(err, &e) {
+ return &e
+ }
+ return nil
}
diff --git a/modules/util/filebuffer/file_backed_buffer.go b/modules/util/filebuffer/file_backed_buffer.go
index 739543e297..0731ba30c8 100644
--- a/modules/util/filebuffer/file_backed_buffer.go
+++ b/modules/util/filebuffer/file_backed_buffer.go
@@ -7,16 +7,10 @@ import (
"bytes"
"errors"
"io"
- "math"
"os"
)
-var (
- // ErrInvalidMemorySize occurs if the memory size is not in a valid range
- ErrInvalidMemorySize = errors.New("Memory size must be greater 0 and lower math.MaxInt32")
- // ErrWriteAfterRead occurs if Write is called after a read operation
- ErrWriteAfterRead = errors.New("Write is unsupported after a read operation")
-)
+var ErrWriteAfterRead = errors.New("write is unsupported after a read operation") // occurs if Write is called after a read operation
type readAtSeeker interface {
io.ReadSeeker
@@ -30,34 +24,17 @@ type FileBackedBuffer struct {
maxMemorySize int64
size int64
buffer bytes.Buffer
+ tempDir string
file *os.File
reader readAtSeeker
}
// New creates a file backed buffer with a specific maximum memory size
-func New(maxMemorySize int) (*FileBackedBuffer, error) {
- if maxMemorySize < 0 || maxMemorySize > math.MaxInt32 {
- return nil, ErrInvalidMemorySize
- }
-
+func New(maxMemorySize int, tempDir string) *FileBackedBuffer {
return &FileBackedBuffer{
maxMemorySize: int64(maxMemorySize),
- }, nil
-}
-
-// CreateFromReader creates a file backed buffer and copies the provided reader data into it.
-func CreateFromReader(r io.Reader, maxMemorySize int) (*FileBackedBuffer, error) {
- b, err := New(maxMemorySize)
- if err != nil {
- return nil, err
+ tempDir: tempDir,
}
-
- _, err = io.Copy(b, r)
- if err != nil {
- return nil, err
- }
-
- return b, nil
}
// Write implements io.Writer
@@ -73,7 +50,7 @@ func (b *FileBackedBuffer) Write(p []byte) (int, error) {
n, err = b.file.Write(p)
} else {
if b.size+int64(len(p)) > b.maxMemorySize {
- b.file, err = os.CreateTemp("", "gitea-buffer-")
+ b.file, err = os.CreateTemp(b.tempDir, "gitea-buffer-")
if err != nil {
return 0, err
}
@@ -148,7 +125,7 @@ func (b *FileBackedBuffer) Seek(offset int64, whence int) (int64, error) {
func (b *FileBackedBuffer) Close() error {
if b.file != nil {
err := b.file.Close()
- os.Remove(b.file.Name())
+ _ = os.Remove(b.file.Name())
b.file = nil
return err
}
diff --git a/modules/util/filebuffer/file_backed_buffer_test.go b/modules/util/filebuffer/file_backed_buffer_test.go
index 16d5a1965f..3f13c6ac7b 100644
--- a/modules/util/filebuffer/file_backed_buffer_test.go
+++ b/modules/util/filebuffer/file_backed_buffer_test.go
@@ -21,7 +21,8 @@ func TestFileBackedBuffer(t *testing.T) {
}
for _, c := range cases {
- buf, err := CreateFromReader(strings.NewReader(c.Data), c.MaxMemorySize)
+ buf := New(c.MaxMemorySize, t.TempDir())
+ _, err := io.Copy(buf, strings.NewReader(c.Data))
assert.NoError(t, err)
assert.EqualValues(t, len(c.Data), buf.Size())
diff --git a/modules/util/legacy_test.go b/modules/util/legacy_test.go
index e732094c29..565fb7f284 100644
--- a/modules/util/legacy_test.go
+++ b/modules/util/legacy_test.go
@@ -17,7 +17,7 @@ import (
func TestCopyFile(t *testing.T) {
testContent := []byte("hello")
- tmpDir := os.TempDir()
+ tmpDir := t.TempDir()
now := time.Now()
srcFile := fmt.Sprintf("%s/copy-test-%d-src.txt", tmpDir, now.UnixMicro())
dstFile := fmt.Sprintf("%s/copy-test-%d-dst.txt", tmpDir, now.UnixMicro())
diff --git a/modules/util/map.go b/modules/util/map.go
new file mode 100644
index 0000000000..f307faad1f
--- /dev/null
+++ b/modules/util/map.go
@@ -0,0 +1,13 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package util
+
+func GetMapValueOrDefault[T any](m map[string]any, key string, defaultValue T) T {
+ if value, ok := m[key]; ok {
+ if v, ok := value.(T); ok {
+ return v
+ }
+ }
+ return defaultValue
+}
diff --git a/modules/util/map_test.go b/modules/util/map_test.go
new file mode 100644
index 0000000000..1a141cec88
--- /dev/null
+++ b/modules/util/map_test.go
@@ -0,0 +1,26 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package util
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetMapValueOrDefault(t *testing.T) {
+ testMap := map[string]any{
+ "key1": "value1",
+ "key2": 42,
+ "key3": nil,
+ }
+
+ assert.Equal(t, "value1", GetMapValueOrDefault(testMap, "key1", "default"))
+ assert.Equal(t, 42, GetMapValueOrDefault(testMap, "key2", 0))
+
+ assert.Equal(t, "default", GetMapValueOrDefault(testMap, "key4", "default"))
+ assert.Equal(t, 100, GetMapValueOrDefault(testMap, "key5", 100))
+
+ assert.Equal(t, "default", GetMapValueOrDefault(testMap, "key3", "default"))
+}
diff --git a/modules/util/paginate_test.go b/modules/util/paginate_test.go
index 6e69dd19cc..3dc5095071 100644
--- a/modules/util/paginate_test.go
+++ b/modules/util/paginate_test.go
@@ -13,23 +13,23 @@ func TestPaginateSlice(t *testing.T) {
stringSlice := []string{"a", "b", "c", "d", "e"}
result, ok := PaginateSlice(stringSlice, 1, 2).([]string)
assert.True(t, ok)
- assert.EqualValues(t, []string{"a", "b"}, result)
+ assert.Equal(t, []string{"a", "b"}, result)
result, ok = PaginateSlice(stringSlice, 100, 2).([]string)
assert.True(t, ok)
- assert.EqualValues(t, []string{}, result)
+ assert.Equal(t, []string{}, result)
result, ok = PaginateSlice(stringSlice, 3, 2).([]string)
assert.True(t, ok)
- assert.EqualValues(t, []string{"e"}, result)
+ assert.Equal(t, []string{"e"}, result)
result, ok = PaginateSlice(stringSlice, 1, 0).([]string)
assert.True(t, ok)
- assert.EqualValues(t, []string{"a", "b", "c", "d", "e"}, result)
+ assert.Equal(t, []string{"a", "b", "c", "d", "e"}, result)
result, ok = PaginateSlice(stringSlice, 1, -1).([]string)
assert.True(t, ok)
- assert.EqualValues(t, []string{"a", "b", "c", "d", "e"}, result)
+ assert.Equal(t, []string{"a", "b", "c", "d", "e"}, result)
type Test struct {
Val int
@@ -38,9 +38,9 @@ func TestPaginateSlice(t *testing.T) {
testVar := []*Test{{Val: 2}, {Val: 3}, {Val: 4}}
testVar, ok = PaginateSlice(testVar, 1, 50).([]*Test)
assert.True(t, ok)
- assert.EqualValues(t, []*Test{{Val: 2}, {Val: 3}, {Val: 4}}, testVar)
+ assert.Equal(t, []*Test{{Val: 2}, {Val: 3}, {Val: 4}}, testVar)
testVar, ok = PaginateSlice(testVar, 2, 2).([]*Test)
assert.True(t, ok)
- assert.EqualValues(t, []*Test{{Val: 4}}, testVar)
+ assert.Equal(t, []*Test{{Val: 4}}, testVar)
}
diff --git a/modules/util/path.go b/modules/util/path.go
index 1272f5af2e..0e56348978 100644
--- a/modules/util/path.go
+++ b/modules/util/path.go
@@ -36,9 +36,10 @@ func PathJoinRel(elem ...string) string {
elems[i] = path.Clean("/" + e)
}
p := path.Join(elems...)
- if p == "" {
+ switch p {
+ case "":
return ""
- } else if p == "/" {
+ case "/":
return "."
}
return p[1:]
@@ -140,81 +141,51 @@ func IsExist(path string) (bool, error) {
return false, err
}
-func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) {
- dir, err := os.Open(dirPath)
+func listDirRecursively(result *[]string, fsDir, recordParentPath string, opts *ListDirOptions) error {
+ dir, err := os.Open(fsDir)
if err != nil {
- return nil, err
+ return err
}
defer dir.Close()
fis, err := dir.Readdir(0)
if err != nil {
- return nil, err
+ return err
}
- statList := make([]string, 0)
for _, fi := range fis {
- if CommonSkip(fi.Name()) {
+ if opts.SkipCommonHiddenNames && IsCommonHiddenFileName(fi.Name()) {
continue
}
-
- relPath := path.Join(recPath, fi.Name())
- curPath := path.Join(dirPath, fi.Name())
+ relPath := path.Join(recordParentPath, fi.Name())
+ curPath := filepath.Join(fsDir, fi.Name())
if fi.IsDir() {
- if includeDir {
- statList = append(statList, relPath+"/")
- }
- s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
- if err != nil {
- return nil, err
- }
- statList = append(statList, s...)
- } else if !isDirOnly {
- statList = append(statList, relPath)
- } else if followSymlinks && fi.Mode()&os.ModeSymlink != 0 {
- link, err := os.Readlink(curPath)
- if err != nil {
- return nil, err
- }
-
- isDir, err := IsDir(link)
- if err != nil {
- return nil, err
+ if opts.IncludeDir {
+ *result = append(*result, relPath+"/")
}
- if isDir {
- if includeDir {
- statList = append(statList, relPath+"/")
- }
- s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
- if err != nil {
- return nil, err
- }
- statList = append(statList, s...)
+ if err = listDirRecursively(result, curPath, relPath, opts); err != nil {
+ return err
}
+ } else {
+ *result = append(*result, relPath)
}
}
- return statList, nil
+ return nil
}
-// StatDir gathers information of given directory by depth-first.
-// It returns slice of file list and includes subdirectories if enabled;
-// it returns error and nil slice when error occurs in underlying functions,
-// or given path is not a directory or does not exist.
-//
+type ListDirOptions struct {
+ IncludeDir bool // subdirectories are also included with suffix slash
+ SkipCommonHiddenNames bool
+}
+
+// ListDirRecursively gathers information of given directory by depth-first.
+// The paths are always in "dir/slash/file" format (not "\\" even in Windows)
// Slice does not include given path itself.
-// If subdirectories is enabled, they will have suffix '/'.
-func StatDir(rootPath string, includeDir ...bool) ([]string, error) {
- if isDir, err := IsDir(rootPath); err != nil {
+func ListDirRecursively(rootDir string, opts *ListDirOptions) (res []string, err error) {
+ if err = listDirRecursively(&res, rootDir, "", opts); err != nil {
return nil, err
- } else if !isDir {
- return nil, errors.New("not a directory or does not exist: " + rootPath)
- }
-
- isIncludeDir := false
- if len(includeDir) != 0 {
- isIncludeDir = includeDir[0]
}
- return statDir(rootPath, "", isIncludeDir, false, false)
+ return res, nil
}
func isOSWindows() bool {
@@ -265,8 +236,8 @@ func HomeDir() (home string, err error) {
return home, nil
}
-// CommonSkip will check a provided name to see if it represents file or directory that should not be watched
-func CommonSkip(name string) bool {
+// IsCommonHiddenFileName will check a provided name to see if it represents file or directory that should not be watched
+func IsCommonHiddenFileName(name string) bool {
if name == "" {
return true
}
@@ -275,9 +246,9 @@ func CommonSkip(name string) bool {
case '.':
return true
case 't', 'T':
- return name[1:] == "humbs.db"
+ return name[1:] == "humbs.db" // macOS
case 'd', 'D':
- return name[1:] == "esktop.ini"
+ return name[1:] == "esktop.ini" // Windows
}
return false
diff --git a/modules/util/path_test.go b/modules/util/path_test.go
index 6a38bf4ace..79c37e55f7 100644
--- a/modules/util/path_test.go
+++ b/modules/util/path_test.go
@@ -5,10 +5,12 @@ package util
import (
"net/url"
+ "os"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestFileURLToPath(t *testing.T) {
@@ -210,3 +212,21 @@ func TestCleanPath(t *testing.T) {
assert.Equal(t, c.expected, FilePathJoinAbs(c.elems[0], c.elems[1:]...), "case: %v", c.elems)
}
}
+
+func TestListDirRecursively(t *testing.T) {
+ tmpDir := t.TempDir()
+ _ = os.WriteFile(tmpDir+"/.config", nil, 0o644)
+ _ = os.Mkdir(tmpDir+"/d1", 0o755)
+ _ = os.WriteFile(tmpDir+"/d1/f-d1", nil, 0o644)
+ _ = os.Mkdir(tmpDir+"/d1/s1", 0o755)
+ _ = os.WriteFile(tmpDir+"/d1/s1/f-d1s1", nil, 0o644)
+ _ = os.Mkdir(tmpDir+"/d2", 0o755)
+
+ res, err := ListDirRecursively(tmpDir, &ListDirOptions{IncludeDir: true})
+ require.NoError(t, err)
+ assert.ElementsMatch(t, []string{".config", "d1/", "d1/f-d1", "d1/s1/", "d1/s1/f-d1s1", "d2/"}, res)
+
+ res, err = ListDirRecursively(tmpDir, &ListDirOptions{SkipCommonHiddenNames: true})
+ require.NoError(t, err)
+ assert.ElementsMatch(t, []string{"d1/f-d1", "d1/s1/f-d1s1"}, res)
+}
diff --git a/modules/util/remove.go b/modules/util/remove.go
index d1e38faf5f..3db0b5a796 100644
--- a/modules/util/remove.go
+++ b/modules/util/remove.go
@@ -15,7 +15,7 @@ const windowsSharingViolationError syscall.Errno = 32
// Remove removes the named file or (empty) directory with at most 5 attempts.
func Remove(name string) error {
var err error
- for i := 0; i < 5; i++ {
+ for range 5 {
err = os.Remove(name)
if err == nil {
break
@@ -44,7 +44,7 @@ func Remove(name string) error {
// RemoveAll removes the named file or (empty) directory with at most 5 attempts.
func RemoveAll(name string) error {
var err error
- for i := 0; i < 5; i++ {
+ for range 5 {
err = os.RemoveAll(name)
if err == nil {
break
@@ -73,7 +73,7 @@ func RemoveAll(name string) error {
// Rename renames (moves) oldpath to newpath with at most 5 attempts.
func Rename(oldpath, newpath string) error {
var err error
- for i := 0; i < 5; i++ {
+ for i := range 5 {
err = os.Rename(oldpath, newpath)
if err == nil {
break
diff --git a/modules/util/rotatingfilewriter/writer_test.go b/modules/util/rotatingfilewriter/writer_test.go
index 88392797b3..f6ea1d50ae 100644
--- a/modules/util/rotatingfilewriter/writer_test.go
+++ b/modules/util/rotatingfilewriter/writer_test.go
@@ -23,7 +23,7 @@ func TestCompressOldFile(t *testing.T) {
ng, err := os.OpenFile(nonGzip, os.O_CREATE|os.O_WRONLY, 0o660)
assert.NoError(t, err)
- for i := 0; i < 999; i++ {
+ for range 999 {
f.WriteString("This is a test file\n")
ng.WriteString("This is a test file\n")
}
diff --git a/modules/util/runtime_test.go b/modules/util/runtime_test.go
index 20f9063b0b..01dd034cea 100644
--- a/modules/util/runtime_test.go
+++ b/modules/util/runtime_test.go
@@ -18,14 +18,14 @@ func TestCallerFuncName(t *testing.T) {
func BenchmarkCallerFuncName(b *testing.B) {
// BenchmarkCaller/sprintf-12 12744829 95.49 ns/op
b.Run("sprintf", func(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
_ = fmt.Sprintf("aaaaaaaaaaaaaaaa %s %s %s", "bbbbbbbbbbbbbbbbbbb", b.Name(), "ccccccccccccccccccccc")
}
})
// BenchmarkCaller/caller-12 10625133 113.6 ns/op
// It is almost as fast as fmt.Sprintf
b.Run("caller", func(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
CallerFuncName(1)
}
})
diff --git a/modules/util/sec_to_time.go b/modules/util/sec_to_time.go
index ad0fb1a68b..646f33c82a 100644
--- a/modules/util/sec_to_time.go
+++ b/modules/util/sec_to_time.go
@@ -8,61 +8,23 @@ import (
"strings"
)
-// SecToTime converts an amount of seconds to a human-readable string. E.g.
-// 66s -> 1 minute 6 seconds
-// 52410s -> 14 hours 33 minutes
-// 563418 -> 6 days 12 hours
-// 1563418 -> 2 weeks 4 days
-// 3937125s -> 1 month 2 weeks
-// 45677465s -> 1 year 6 months
-func SecToTime(durationVal any) string {
- duration, _ := ToInt64(durationVal)
+// SecToHours converts an amount of seconds to a human-readable hours string.
+// This is stable for planning and managing timesheets.
+// Here it only supports hours and minutes, because a work day could contain 6 or 7 or 8 hours.
+// If the duration is less than 1 minute, it will be shown as seconds.
+func SecToHours(durationVal any) string {
+ seconds, _ := ToInt64(durationVal)
+ hours := seconds / 3600
+ minutes := (seconds / 60) % 60
formattedTime := ""
-
- // The following four variables are calculated by taking
- // into account the previously calculated variables, this avoids
- // pitfalls when using remainders. As that could lead to incorrect
- // results when the calculated number equals the quotient number.
- remainingDays := duration / (60 * 60 * 24)
- years := remainingDays / 365
- remainingDays -= years * 365
- months := remainingDays * 12 / 365
- remainingDays -= months * 365 / 12
- weeks := remainingDays / 7
- remainingDays -= weeks * 7
- days := remainingDays
-
- // The following three variables are calculated without depending
- // on the previous calculated variables.
- hours := (duration / 3600) % 24
- minutes := (duration / 60) % 60
- seconds := duration % 60
-
- // Extract only the relevant information of the time
- // If the time is greater than a year, it makes no sense to display seconds.
- switch {
- case years > 0:
- formattedTime = formatTime(years, "year", formattedTime)
- formattedTime = formatTime(months, "month", formattedTime)
- case months > 0:
- formattedTime = formatTime(months, "month", formattedTime)
- formattedTime = formatTime(weeks, "week", formattedTime)
- case weeks > 0:
- formattedTime = formatTime(weeks, "week", formattedTime)
- formattedTime = formatTime(days, "day", formattedTime)
- case days > 0:
- formattedTime = formatTime(days, "day", formattedTime)
- formattedTime = formatTime(hours, "hour", formattedTime)
- case hours > 0:
- formattedTime = formatTime(hours, "hour", formattedTime)
- formattedTime = formatTime(minutes, "minute", formattedTime)
- default:
- formattedTime = formatTime(minutes, "minute", formattedTime)
- formattedTime = formatTime(seconds, "second", formattedTime)
- }
+ formattedTime = formatTime(hours, "hour", formattedTime)
+ formattedTime = formatTime(minutes, "minute", formattedTime)
// The formatTime() function always appends a space at the end. This will be trimmed
+ if formattedTime == "" && seconds > 0 {
+ formattedTime = formatTime(seconds, "second", "")
+ }
return strings.TrimRight(formattedTime, " ")
}
@@ -76,6 +38,5 @@ func formatTime(value int64, name, formattedTime string) string {
} else if value > 1 {
formattedTime = fmt.Sprintf("%s%d %ss ", formattedTime, value, name)
}
-
return formattedTime
}
diff --git a/modules/util/sec_to_time_test.go b/modules/util/sec_to_time_test.go
index 4d1213a52c..84e767c6e0 100644
--- a/modules/util/sec_to_time_test.go
+++ b/modules/util/sec_to_time_test.go
@@ -9,22 +9,20 @@ import (
"github.com/stretchr/testify/assert"
)
-func TestSecToTime(t *testing.T) {
+func TestSecToHours(t *testing.T) {
second := int64(1)
minute := 60 * second
hour := 60 * minute
day := 24 * hour
- year := 365 * day
- assert.Equal(t, "1 minute 6 seconds", SecToTime(minute+6*second))
- assert.Equal(t, "1 hour", SecToTime(hour))
- assert.Equal(t, "1 hour", SecToTime(hour+second))
- assert.Equal(t, "14 hours 33 minutes", SecToTime(14*hour+33*minute+30*second))
- assert.Equal(t, "6 days 12 hours", SecToTime(6*day+12*hour+30*minute+18*second))
- assert.Equal(t, "2 weeks 4 days", SecToTime((2*7+4)*day+2*hour+16*minute+58*second))
- assert.Equal(t, "4 weeks", SecToTime(4*7*day))
- assert.Equal(t, "4 weeks 1 day", SecToTime((4*7+1)*day))
- assert.Equal(t, "1 month 2 weeks", SecToTime((6*7+3)*day+13*hour+38*minute+45*second))
- assert.Equal(t, "11 months", SecToTime(year-25*day))
- assert.Equal(t, "1 year 5 months", SecToTime(year+163*day+10*hour+11*minute+5*second))
+ assert.Equal(t, "1 minute", SecToHours(minute+6*second))
+ assert.Equal(t, "1 hour", SecToHours(hour))
+ assert.Equal(t, "1 hour", SecToHours(hour+second))
+ assert.Equal(t, "14 hours 33 minutes", SecToHours(14*hour+33*minute+30*second))
+ assert.Equal(t, "156 hours 30 minutes", SecToHours(6*day+12*hour+30*minute+18*second))
+ assert.Equal(t, "98 hours 16 minutes", SecToHours(4*day+2*hour+16*minute+58*second))
+ assert.Equal(t, "672 hours", SecToHours(4*7*day))
+ assert.Equal(t, "1 second", SecToHours(1))
+ assert.Equal(t, "2 seconds", SecToHours(2))
+ assert.Empty(t, SecToHours(nil)) // old behavior, empty means no output
}
diff --git a/modules/util/shellquote_test.go b/modules/util/shellquote_test.go
index 969998c592..4ef5ce6980 100644
--- a/modules/util/shellquote_test.go
+++ b/modules/util/shellquote_test.go
@@ -3,7 +3,11 @@
package util
-import "testing"
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
func TestShellEscape(t *testing.T) {
tests := []struct {
@@ -83,9 +87,7 @@ func TestShellEscape(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if got := ShellEscape(tt.toEscape); got != tt.want {
- t.Errorf("ShellEscape(%q):\nGot: %s\nWanted: %s", tt.toEscape, got, tt.want)
- }
+ assert.Equal(t, tt.want, ShellEscape(tt.toEscape))
})
}
}
diff --git a/modules/util/slice.go b/modules/util/slice.go
index 9c878c24be..aaa729c1c9 100644
--- a/modules/util/slice.go
+++ b/modules/util/slice.go
@@ -12,8 +12,7 @@ import (
// SliceContainsString sequential searches if string exists in slice.
func SliceContainsString(slice []string, target string, insensitive ...bool) bool {
if len(insensitive) != 0 && insensitive[0] {
- target = strings.ToLower(target)
- return slices.ContainsFunc(slice, func(t string) bool { return strings.ToLower(t) == target })
+ return slices.ContainsFunc(slice, func(t string) bool { return strings.EqualFold(t, target) })
}
return slices.Contains(slice, target)
@@ -71,3 +70,10 @@ func KeysOfMap[K comparable, V any](m map[K]V) []K {
}
return keys
}
+
+func SliceNilAsEmpty[T any](a []T) []T {
+ if a == nil {
+ return []T{}
+ }
+ return a
+}
diff --git a/modules/util/string.go b/modules/util/string.go
index cf50f591c6..b9b59df3ef 100644
--- a/modules/util/string.go
+++ b/modules/util/string.go
@@ -3,7 +3,10 @@
package util
-import "unsafe"
+import (
+ "strings"
+ "unsafe"
+)
func isSnakeCaseUpper(c byte) bool {
return 'A' <= c && c <= 'Z'
@@ -95,3 +98,36 @@ func UnsafeBytesToString(b []byte) string {
func UnsafeStringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
+
+// SplitTrimSpace splits the string at given separator and trims leading and trailing space
+func SplitTrimSpace(input, sep string) []string {
+ input = strings.TrimSpace(input)
+ var stringList []string
+ for s := range strings.SplitSeq(input, sep) {
+ if s = strings.TrimSpace(s); s != "" {
+ stringList = append(stringList, s)
+ }
+ }
+ return stringList
+}
+
+func asciiLower(b byte) byte {
+ if 'A' <= b && b <= 'Z' {
+ return b + ('a' - 'A')
+ }
+ return b
+}
+
+// AsciiEqualFold is from Golang https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/net/http/internal/ascii/print.go
+// ASCII only. In most cases for protocols, we should only use this but not [strings.EqualFold]
+func AsciiEqualFold(s, t string) bool { //nolint:revive // PascalCase
+ if len(s) != len(t) {
+ return false
+ }
+ for i := 0; i < len(s); i++ {
+ if asciiLower(s[i]) != asciiLower(t[i]) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/modules/util/string_test.go b/modules/util/string_test.go
index 0a4a8bbcfb..ff67b5c7d4 100644
--- a/modules/util/string_test.go
+++ b/modules/util/string_test.go
@@ -45,3 +45,8 @@ func TestToSnakeCase(t *testing.T) {
assert.Equal(t, expected, ToSnakeCase(input))
}
}
+
+func TestSplitTrimSpace(t *testing.T) {
+ assert.Equal(t, []string{"a", "b", "c"}, SplitTrimSpace("a\nb\nc", "\n"))
+ assert.Equal(t, []string{"a", "b"}, SplitTrimSpace("\r\na\n\r\nb\n\n", "\n"))
+}
diff --git a/modules/util/time_str.go b/modules/util/time_str.go
index 0fccfe82cc..81b132c3db 100644
--- a/modules/util/time_str.go
+++ b/modules/util/time_str.go
@@ -59,7 +59,7 @@ func TimeEstimateParse(timeStr string) (int64, error) {
unit := timeStr[match[4]:match[5]]
found := false
for _, u := range timeStrGlobalVars().units {
- if strings.ToLower(unit) == u.name {
+ if strings.EqualFold(unit, u.name) {
total += amount * u.num
found = true
break
diff --git a/modules/util/truncate.go b/modules/util/truncate.go
index f2edbdc673..52534d3cac 100644
--- a/modules/util/truncate.go
+++ b/modules/util/truncate.go
@@ -5,6 +5,7 @@ package util
import (
"strings"
+ "unicode"
"unicode/utf8"
)
@@ -14,43 +15,118 @@ const (
asciiEllipsis = "..."
)
-// SplitStringAtByteN splits a string at byte n accounting for rune boundaries. (Combining characters are not accounted for.)
-func SplitStringAtByteN(input string, n int) (left, right string) {
- if len(input) <= n {
- return input, ""
+func IsLikelyEllipsisLeftPart(s string) bool {
+ return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis)
+}
+
+func ellipsisDisplayGuessWidth(r rune) int {
+ // To make the truncated string as long as possible,
+ // CJK/emoji chars are considered as 2-ASCII width but not 3-4 bytes width.
+ // Here we only make the best guess (better than counting them in bytes),
+ // it's impossible to 100% correctly determine the width of a rune without a real font and render.
+ //
+ // ATTENTION: the guessed width can't be zero, more details in ellipsisDisplayString's comment
+ if r <= 255 {
+ return 1
}
- if !utf8.ValidString(input) {
- if n-3 < 0 {
- return input, ""
+ switch {
+ case r == '\u3000': /* ideographic (CJK) characters, still use 2 */
+ return 2
+ case unicode.Is(unicode.M, r), /* (Mark) */
+ unicode.Is(unicode.Cf, r), /* (Other, format) */
+ unicode.Is(unicode.Cs, r), /* (Other, surrogate) */
+ unicode.Is(unicode.Z /* (Space) */, r):
+ return 1
+ default:
+ return 2
+ }
+}
+
+// EllipsisDisplayString returns a truncated short string for display purpose.
+// The length is the approximate number of ASCII-width in the string (CJK/emoji are 2-ASCII width)
+// It appends "…" or "..." at the end of truncated string.
+// It guarantees the length of the returned runes doesn't exceed the limit.
+func EllipsisDisplayString(str string, limit int) string {
+ s, _, _, _ := ellipsisDisplayString(str, limit, ellipsisDisplayGuessWidth)
+ return s
+}
+
+// EllipsisDisplayStringX works like EllipsisDisplayString while it also returns the right part
+func EllipsisDisplayStringX(str string, limit int) (left, right string) {
+ return ellipsisDisplayStringX(str, limit, ellipsisDisplayGuessWidth)
+}
+
+func ellipsisDisplayStringX(str string, limit int, widthGuess func(rune) int) (left, right string) {
+ left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit, widthGuess)
+ if truncated {
+ right = str[offset:]
+ r, _ := utf8.DecodeRune(UnsafeStringToBytes(right))
+ encounterInvalid = encounterInvalid || r == utf8.RuneError
+ ellipsis := utf8Ellipsis
+ if encounterInvalid {
+ ellipsis = asciiEllipsis
}
- return input[:n-3] + asciiEllipsis, asciiEllipsis + input[n-3:]
+ right = ellipsis + right
}
+ return left, right
+}
- end := 0
- for end <= n-3 {
- _, size := utf8.DecodeRuneInString(input[end:])
- if end+size > n-3 {
+func ellipsisDisplayString(str string, limit int, widthGuess func(rune) int) (res string, offset int, truncated, encounterInvalid bool) {
+ if len(str) <= limit {
+ return str, len(str), false, false
+ }
+
+ // To future maintainers: this logic must guarantee that the length of the returned runes doesn't exceed the limit,
+ // because the returned string will also be used as database value. UTF-8 VARCHAR(10) could store 10 rune characters,
+ // So each rune must be countered as at least 1 width.
+ // Even if there are some special Unicode characters (zero-width, combining, etc.), they should NEVER be counted as zero.
+ pos, used := 0, 0
+ for i, r := range str {
+ encounterInvalid = encounterInvalid || r == utf8.RuneError
+ pos = i
+ runeWidth := widthGuess(r)
+ if used+runeWidth+3 > limit {
break
}
- end += size
+ used += runeWidth
+ offset += utf8.RuneLen(r)
}
- return input[:end] + utf8Ellipsis, utf8Ellipsis + input[end:]
+ // if the remaining are fewer than 3 runes, then maybe we could add them, no need to ellipse
+ if len(str)-pos <= 12 {
+ var nextCnt, nextWidth int
+ for _, r := range str[pos:] {
+ if nextCnt >= 4 {
+ break
+ }
+ nextWidth += widthGuess(r)
+ nextCnt++
+ }
+ if nextCnt <= 3 && used+nextWidth <= limit {
+ return str, len(str), false, false
+ }
+ }
+ if limit < 3 {
+ // if the limit is so small, do not add ellipsis
+ return str[:offset], offset, true, false
+ }
+ ellipsis := utf8Ellipsis
+ if encounterInvalid {
+ ellipsis = asciiEllipsis
+ }
+ return str[:offset] + ellipsis, offset, true, encounterInvalid
}
-// SplitTrimSpace splits the string at given separator and trims leading and trailing space
-func SplitTrimSpace(input, sep string) []string {
- // Trim initial leading & trailing space
- input = strings.TrimSpace(input)
- // replace CRLF with LF
- input = strings.ReplaceAll(input, "\r\n", "\n")
+func EllipsisTruncateRunes(str string, limit int) (left, right string) {
+ return ellipsisDisplayStringX(str, limit, func(r rune) int { return 1 })
+}
- var stringList []string
- for _, s := range strings.Split(input, sep) {
- // trim leading and trailing space
- stringList = append(stringList, strings.TrimSpace(s))
+// TruncateRunes returns a truncated string with given rune limit,
+// it returns input string if its rune length doesn't exceed the limit.
+func TruncateRunes(str string, limit int) string {
+ if utf8.RuneCountInString(str) < limit {
+ return str
}
-
- return stringList
+ return string([]rune(str)[:limit])
}
diff --git a/modules/util/truncate_test.go b/modules/util/truncate_test.go
index dfe1230fd4..6d71f38c0c 100644
--- a/modules/util/truncate_test.go
+++ b/modules/util/truncate_test.go
@@ -4,43 +4,128 @@
package util
import (
+ "fmt"
+ "strconv"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
)
-func TestSplitString(t *testing.T) {
- type testCase struct {
- input string
- n int
- leftSub string
- ellipsis string
+func TestEllipsisGuessDisplayWidth(t *testing.T) {
+ cases := []struct {
+ r string
+ want int
+ }{
+ {r: "a", want: 1},
+ {r: "é", want: 1},
+ {r: "测", want: 2},
+ {r: "⚽", want: 2},
+ {r: "☁️", want: 3}, // 2 runes, it has a mark
+ {r: "\u200B", want: 1}, // ZWSP
+ {r: "\u3000", want: 2}, // ideographic space
}
-
- test := func(tc []*testCase, f func(input string, n int) (left, right string)) {
- for _, c := range tc {
- l, r := f(c.input, c.n)
- if c.ellipsis != "" {
- assert.Equal(t, c.leftSub+c.ellipsis, l, "test split %q at %d, expected leftSub: %q", c.input, c.n, c.leftSub)
- assert.Equal(t, c.ellipsis+c.input[len(c.leftSub):], r, "test split %s at %d, expected rightSub: %q", c.input, c.n, c.input[len(c.leftSub):])
- } else {
- assert.Equal(t, c.leftSub, l, "test split %q at %d, expected leftSub: %q", c.input, c.n, c.leftSub)
- assert.Empty(t, r, "test split %q at %d, expected rightSub: %q", c.input, c.n, "")
+ for _, c := range cases {
+ t.Run(c.r, func(t *testing.T) {
+ w := 0
+ for _, r := range c.r {
+ w += ellipsisDisplayGuessWidth(r)
}
- }
+ assert.Equal(t, c.want, w, "hex=% x", []byte(c.r))
+ })
}
+}
+
+func TestEllipsisString(t *testing.T) {
+ cases := []struct {
+ limit int
+
+ input, left, right string
+ }{
+ {limit: 0, input: "abcde", left: "", right: "…abcde"},
+ {limit: 1, input: "abcde", left: "", right: "…abcde"},
+ {limit: 2, input: "abcde", left: "", right: "…abcde"},
+ {limit: 3, input: "abcde", left: "…", right: "…abcde"},
+ {limit: 4, input: "abcde", left: "a…", right: "…bcde"},
+ {limit: 5, input: "abcde", left: "abcde", right: ""},
+ {limit: 6, input: "abcde", left: "abcde", right: ""},
+ {limit: 7, input: "abcde", left: "abcde", right: ""},
- tc := []*testCase{
- {"abc123xyz", 0, "", utf8Ellipsis},
- {"abc123xyz", 1, "", utf8Ellipsis},
- {"abc123xyz", 4, "a", utf8Ellipsis},
- {"啊bc123xyz", 4, "", utf8Ellipsis},
- {"啊bc123xyz", 6, "啊", utf8Ellipsis},
- {"啊bc", 5, "啊bc", ""},
- {"啊bc", 6, "啊bc", ""},
- {"abc\xef\x03\xfe", 3, "", asciiEllipsis},
- {"abc\xef\x03\xfe", 4, "a", asciiEllipsis},
- {"\xef\x03", 1, "\xef\x03", ""},
+ // a CJK char or emoji is considered as 2-ASCII width, the ellipsis is 3-ASCII width
+ {limit: 0, input: "测试文本", left: "", right: "…测试文本"},
+ {limit: 1, input: "测试文本", left: "", right: "…测试文本"},
+ {limit: 2, input: "测试文本", left: "", right: "…测试文本"},
+ {limit: 3, input: "测试文本", left: "…", right: "…测试文本"},
+ {limit: 4, input: "测试文本", left: "…", right: "…测试文本"},
+ {limit: 5, input: "测试文本", left: "测…", right: "…试文本"},
+ {limit: 6, input: "测试文本", left: "测…", right: "…试文本"},
+ {limit: 7, input: "测试文本", left: "测试…", right: "…文本"},
+ {limit: 8, input: "测试文本", left: "测试文本", right: ""},
+ {limit: 9, input: "测试文本", left: "测试文本", right: ""},
+
+ {limit: 6, input: "测试abc", left: "测…", right: "…试abc"},
+ {limit: 7, input: "测试abc", left: "测试abc", right: ""}, // exactly 7-width
+ {limit: 8, input: "测试abc", left: "测试abc", right: ""},
+
+ {limit: 7, input: "测abc试啊", left: "测ab…", right: "…c试啊"},
+ {limit: 8, input: "测abc试啊", left: "测abc…", right: "…试啊"},
+ {limit: 9, input: "测abc试啊", left: "测abc试啊", right: ""}, // exactly 9-width
+ {limit: 10, input: "测abc试啊", left: "测abc试啊", right: ""},
}
- test(tc, SplitStringAtByteN)
+ for _, c := range cases {
+ t.Run(fmt.Sprintf("%s(%d)", c.input, c.limit), func(t *testing.T) {
+ left, right := EllipsisDisplayStringX(c.input, c.limit)
+ assert.Equal(t, c.left, left, "left")
+ assert.Equal(t, c.right, right, "right")
+ })
+ }
+
+ t.Run("LongInput", func(t *testing.T) {
+ left, right := EllipsisDisplayStringX(strings.Repeat("abc", 240), 90)
+ assert.Equal(t, strings.Repeat("abc", 29)+"…", left)
+ assert.Equal(t, "…"+strings.Repeat("abc", 211), right)
+ })
+
+ t.Run("InvalidUtf8", func(t *testing.T) {
+ invalidCases := []struct {
+ limit int
+ left, right string
+ }{
+ {limit: 0, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"},
+ {limit: 1, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"},
+ {limit: 2, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"},
+ {limit: 3, left: "...", right: "...\xef\x03\xfe\xef\x03\xfe"},
+ {limit: 4, left: "...", right: "...\xef\x03\xfe\xef\x03\xfe"},
+ {limit: 5, left: "\xef\x03\xfe...", right: "...\xef\x03\xfe"},
+ {limit: 6, left: "\xef\x03\xfe\xef\x03\xfe", right: ""},
+ {limit: 7, left: "\xef\x03\xfe\xef\x03\xfe", right: ""},
+ }
+ for _, c := range invalidCases {
+ t.Run(strconv.Itoa(c.limit), func(t *testing.T) {
+ left, right := EllipsisDisplayStringX("\xef\x03\xfe\xef\x03\xfe", c.limit)
+ assert.Equal(t, c.left, left, "left")
+ assert.Equal(t, c.right, right, "right")
+ })
+ }
+ })
+
+ t.Run("IsLikelyEllipsisLeftPart", func(t *testing.T) {
+ assert.True(t, IsLikelyEllipsisLeftPart("abcde…"))
+ assert.True(t, IsLikelyEllipsisLeftPart("abcde..."))
+ })
+}
+
+func TestTruncateRunes(t *testing.T) {
+ assert.Empty(t, TruncateRunes("", 0))
+ assert.Empty(t, TruncateRunes("", 1))
+
+ assert.Empty(t, TruncateRunes("ab", 0))
+ assert.Equal(t, "a", TruncateRunes("ab", 1))
+ assert.Equal(t, "ab", TruncateRunes("ab", 2))
+ assert.Equal(t, "ab", TruncateRunes("ab", 3))
+
+ assert.Empty(t, TruncateRunes("测试", 0))
+ assert.Equal(t, "测", TruncateRunes("测试", 1))
+ assert.Equal(t, "测试", TruncateRunes("测试", 2))
+ assert.Equal(t, "测试", TruncateRunes("测试", 3))
}
diff --git a/modules/util/util.go b/modules/util/util.go
index 1fb4cb21cb..dd8e073888 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -11,21 +11,10 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/modules/optional"
-
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
-// OptionalBoolParse get the corresponding optional.Option[bool] of a string using strconv.ParseBool
-func OptionalBoolParse(s string) optional.Option[bool] {
- v, e := strconv.ParseBool(s)
- if e != nil {
- return optional.None[bool]()
- }
- return optional.Some(v)
-}
-
// IsEmptyString checks if the provided string is empty
func IsEmptyString(s string) bool {
return len(strings.TrimSpace(s)) == 0
@@ -230,6 +219,13 @@ func IfZero[T comparable](v, def T) T {
return v
}
+func IfEmpty[T any](v, def []T) []T {
+ if len(v) == 0 {
+ return def
+ }
+ return v
+}
+
// OptionalArg helps the "optional argument" in Golang:
//
// func foo(optArg ...int) { return OptionalArg(optArg) }
diff --git a/modules/util/util_test.go b/modules/util/util_test.go
index 52b05acc5b..fe4125cdb5 100644
--- a/modules/util/util_test.go
+++ b/modules/util/util_test.go
@@ -8,8 +8,6 @@ import (
"strings"
"testing"
- "code.gitea.io/gitea/modules/optional"
-
"github.com/stretchr/testify/assert"
)
@@ -175,19 +173,6 @@ func Test_RandomBytes(t *testing.T) {
assert.NotEqual(t, bytes3, bytes4)
}
-func TestOptionalBoolParse(t *testing.T) {
- assert.Equal(t, optional.None[bool](), OptionalBoolParse(""))
- assert.Equal(t, optional.None[bool](), OptionalBoolParse("x"))
-
- assert.Equal(t, optional.Some(false), OptionalBoolParse("0"))
- assert.Equal(t, optional.Some(false), OptionalBoolParse("f"))
- assert.Equal(t, optional.Some(false), OptionalBoolParse("False"))
-
- assert.Equal(t, optional.Some(true), OptionalBoolParse("1"))
- assert.Equal(t, optional.Some(true), OptionalBoolParse("t"))
- assert.Equal(t, optional.Some(true), OptionalBoolParse("True"))
-}
-
// Test case for any function which accepts and returns a single string.
type StringTest struct {
in, out string
@@ -215,7 +200,7 @@ func TestToUpperASCII(t *testing.T) {
func BenchmarkToUpper(b *testing.B) {
for _, tc := range upperTests {
b.Run(tc.in, func(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
ToUpperASCII(tc.in)
}
})