Compare commits

...

29 Commits

Author SHA1 Message Date
c13db6802e v0.0.102 2023-04-13 14:40:07 +02:00
c5e23ab451 v0.0.101 2023-04-08 19:39:13 +02:00
c266d9204b v0.0.100 2023-04-04 17:10:38 +02:00
2550691e2e v0.0.99 2023-03-31 13:33:06 +02:00
ca24e1d5bf v0.0.98 2023-03-29 20:25:03 +02:00
b156052e6f v0.0.97 2023-03-29 19:53:53 +02:00
dda2418255 v0.0.96 2023-03-29 19:53:10 +02:00
8e40deae6a add git-pull to Makefile 2023-03-28 16:30:56 +02:00
289b9f47a2 v0.0.95 2023-03-28 16:29:16 +02:00
007c44df85 v0.0.94 2023-03-21 16:00:15 +01:00
a6252f0743 v0.0.93 2023-03-15 15:41:55 +01:00
86c01659d7 base58 2023-03-15 14:00:48 +01:00
62acddda5e v0.0.91 2023-03-11 14:38:19 +01:00
ee325f67fd v0.0.90 2023-03-09 14:51:53 +01:00
dba0cd229e v0.0.89 2023-03-07 10:43:30 +01:00
ec4dba173f v0.0.88 2023-02-16 13:27:34 +01:00
22ce2d26f3 v0.0.87 2023-02-16 13:22:15 +01:00
4fd768e573 v0.0.86 2023-02-14 17:18:58 +01:00
bf16a8165f v0.0.85 2023-02-14 16:25:45 +01:00
9f5612248a fix fd0 read error on long stdout output (scanner buffer was too small) 2023-02-13 01:41:33 +01:00
4a2b830252 added more tests to cmdrunner (reproduce another ?? cmdrunner bug...) 2023-02-09 16:49:33 +01:00
c492c80881 v0.0.83 2023-02-09 15:06:37 +01:00
26dd16d021 v0.0.82 2023-02-09 15:01:54 +01:00
b0b43de8ca v0.0.81 2023-02-09 11:27:49 +01:00
94f72e4ddf v0.0.80 2023-02-09 11:16:23 +01:00
df4388e6dc v0.0.79 2023-02-08 18:55:51 +01:00
fd33b43f31 v0.0.78 2023-02-03 01:05:36 +01:00
be4de07eb8 v0.0.77 2023-02-03 00:59:54 +01:00
36ed474bfe v0.0.76 2023-01-31 23:46:35 +01:00
34 changed files with 1661 additions and 352 deletions

View File

@@ -7,6 +7,22 @@ set -o pipefail # Return value of a pipeline is the value of the last (rightmos
IFS=$'\n\t' # Set $IFS to only newline and tab. IFS=$'\n\t' # Set $IFS to only newline and tab.
function black() { echo -e "\x1B[30m $1 \x1B[0m"; }
function red() { echo -e "\x1B[31m $1 \x1B[0m"; }
function green() { echo -e "\x1B[32m $1 \x1B[0m"; }
function yellow(){ echo -e "\x1B[33m $1 \x1B[0m"; }
function blue() { echo -e "\x1B[34m $1 \x1B[0m"; }
function purple(){ echo -e "\x1B[35m $1 \x1B[0m"; }
function cyan() { echo -e "\x1B[36m $1 \x1B[0m"; }
function white() { echo -e "\x1B[37m $1 \x1B[0m"; }
if [ "$( git rev-parse --abbrev-ref HEAD )" != "master" ]; then
>&2 red "[ERROR] Can only create versions of <master>"
exit 1
fi
git pull --ff
curr_vers=$(git describe --tags --abbrev=0 | sed 's/v//g') curr_vers=$(git describe --tags --abbrev=0 | sed 's/v//g')
next_ver=$(echo "$curr_vers" | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}') next_ver=$(echo "$curr_vers" | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}')
@@ -18,7 +34,13 @@ echo ""
git add --verbose . git add --verbose .
git commit -a -m "v${next_ver}" msg="v${next_ver}"
if [ $# -gt 0 ]; then
msg="$1"
fi
git commit -a -m "${msg}"
git tag "v${next_ver}" git tag "v${next_ver}"

View File

@@ -2,6 +2,7 @@ package cmdext
import ( import (
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"time" "time"
) )
@@ -11,6 +12,8 @@ type CommandRunner struct {
timeout *time.Duration timeout *time.Duration
env []string env []string
listener []CommandListener listener []CommandListener
enforceExitCodes *[]int
enforceNoTimeout bool
} }
func Runner(program string) *CommandRunner { func Runner(program string) *CommandRunner {
@@ -20,6 +23,8 @@ func Runner(program string) *CommandRunner {
timeout: nil, timeout: nil,
env: make([]string, 0), env: make([]string, 0),
listener: make([]CommandListener, 0), listener: make([]CommandListener, 0),
enforceExitCodes: nil,
enforceNoTimeout: false,
} }
} }
@@ -53,6 +58,21 @@ func (r *CommandRunner) Envs(env []string) *CommandRunner {
return r return r
} }
func (r *CommandRunner) EnsureExitcode(arg ...int) *CommandRunner {
r.enforceExitCodes = langext.Ptr(langext.ForceArray(arg))
return r
}
func (r *CommandRunner) FailOnExitCode() *CommandRunner {
r.enforceExitCodes = langext.Ptr([]int{0})
return r
}
func (r *CommandRunner) FailOnTimeout() *CommandRunner {
r.enforceNoTimeout = true
return r
}
func (r *CommandRunner) Listen(lstr CommandListener) *CommandRunner { func (r *CommandRunner) Listen(lstr CommandListener) *CommandRunner {
r.listener = append(r.listener, lstr) r.listener = append(r.listener, lstr)
return r return r

View File

@@ -1,12 +1,17 @@
package cmdext package cmdext
import ( import (
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext" "gogs.mikescher.com/BlackForestBytes/goext/mathext"
"gogs.mikescher.com/BlackForestBytes/goext/syncext" "gogs.mikescher.com/BlackForestBytes/goext/syncext"
"os/exec" "os/exec"
"time" "time"
) )
var ErrExitCode = errors.New("process exited with an unexpected exitcode")
var ErrTimeout = errors.New("process did not exit after the specified timeout")
type CommandResult struct { type CommandResult struct {
StdOut string StdOut string
StdErr string StdErr string
@@ -31,6 +36,7 @@ func run(opt CommandRunner) (CommandResult, error) {
} }
preader := pipeReader{ preader := pipeReader{
lineBufferSize: langext.Ptr(128 * 1024 * 1024), // 128MB max size of a single line, is hopefully enough....
stdout: stdoutPipe, stdout: stdoutPipe,
stderr: stderrPipe, stderr: stderrPipe,
} }
@@ -55,14 +61,17 @@ func run(opt CommandRunner) (CommandResult, error) {
stdout, stderr, stdcombined, err := preader.Read(opt.listener) stdout, stderr, stdcombined, err := preader.Read(opt.listener)
if err != nil { if err != nil {
outputChan <- resultObj{stdout, stderr, stdcombined, err} outputChan <- resultObj{stdout, stderr, stdcombined, err}
_ = cmd.Process.Kill()
return
} }
err = cmd.Wait() err = cmd.Wait()
if err != nil { if err != nil {
outputChan <- resultObj{stdout, stderr, stdcombined, err} outputChan <- resultObj{stdout, stderr, stdcombined, err}
} else {
outputChan <- resultObj{stdout, stderr, stdcombined, nil}
} }
outputChan <- resultObj{stdout, stderr, stdcombined, nil}
}() }()
var timeoutChan <-chan time.Time = make(chan time.Time, 1) var timeoutChan <-chan time.Time = make(chan time.Time, 1)
@@ -81,21 +90,29 @@ func run(opt CommandRunner) (CommandResult, error) {
if fallback, ok := syncext.ReadChannelWithTimeout(outputChan, mathext.Min(32*time.Millisecond, *opt.timeout)); ok { if fallback, ok := syncext.ReadChannelWithTimeout(outputChan, mathext.Min(32*time.Millisecond, *opt.timeout)); ok {
// most of the time the cmd.Process.Kill() should also ahve finished the pipereader // most of the time the cmd.Process.Kill() should also ahve finished the pipereader
// and we can at least return the already collected stdout, stderr, etc // and we can at least return the already collected stdout, stderr, etc
return CommandResult{ res := CommandResult{
StdOut: fallback.stdout, StdOut: fallback.stdout,
StdErr: fallback.stderr, StdErr: fallback.stderr,
StdCombined: fallback.stdcombined, StdCombined: fallback.stdcombined,
ExitCode: -1, ExitCode: -1,
CommandTimedOut: true, CommandTimedOut: true,
}, nil }
if opt.enforceNoTimeout {
return res, ErrTimeout
}
return res, nil
} else { } else {
return CommandResult{ res := CommandResult{
StdOut: "", StdOut: "",
StdErr: "", StdErr: "",
StdCombined: "", StdCombined: "",
ExitCode: -1, ExitCode: -1,
CommandTimedOut: true, CommandTimedOut: true,
}, nil }
if opt.enforceNoTimeout {
return res, ErrTimeout
}
return res, nil
} }
case outobj := <-outputChan: case outobj := <-outputChan:
@@ -104,26 +121,34 @@ func run(opt CommandRunner) (CommandResult, error) {
for _, lstr := range opt.listener { for _, lstr := range opt.listener {
lstr.Finished(excode) lstr.Finished(excode)
} }
return CommandResult{ res := CommandResult{
StdOut: outobj.stdout, StdOut: outobj.stdout,
StdErr: outobj.stderr, StdErr: outobj.stderr,
StdCombined: outobj.stdcombined, StdCombined: outobj.stdcombined,
ExitCode: excode, ExitCode: excode,
CommandTimedOut: false, CommandTimedOut: false,
}, nil }
if opt.enforceExitCodes != nil && !langext.InArray(excode, *opt.enforceExitCodes) {
return res, ErrExitCode
}
return res, nil
} else if err != nil { } else if err != nil {
return CommandResult{}, err return CommandResult{}, err
} else { } else {
for _, lstr := range opt.listener { for _, lstr := range opt.listener {
lstr.Finished(0) lstr.Finished(0)
} }
return CommandResult{ res := CommandResult{
StdOut: outobj.stdout, StdOut: outobj.stdout,
StdErr: outobj.stderr, StdErr: outobj.stderr,
StdCombined: outobj.stdcombined, StdCombined: outobj.stdcombined,
ExitCode: 0, ExitCode: 0,
CommandTimedOut: false, CommandTimedOut: false,
}, nil }
if opt.enforceExitCodes != nil && !langext.InArray(0, *opt.enforceExitCodes) {
return res, ErrExitCode
}
return res, nil
} }
} }
} }

View File

@@ -1,6 +1,7 @@
package cmdext package cmdext
import ( import (
"fmt"
"testing" "testing"
"time" "time"
) )
@@ -11,6 +12,12 @@ func TestStdout(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("%v", err) t.Errorf("%v", err)
} }
if res1.CommandTimedOut {
t.Errorf("Timeout")
}
if res1.ExitCode != 0 {
t.Errorf("res1.ExitCode == %v", res1.ExitCode)
}
if res1.StdErr != "" { if res1.StdErr != "" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr) t.Errorf("res1.StdErr == '%v'", res1.StdErr)
} }
@@ -29,6 +36,12 @@ func TestStderr(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("%v", err) t.Errorf("%v", err)
} }
if res1.CommandTimedOut {
t.Errorf("Timeout")
}
if res1.ExitCode != 0 {
t.Errorf("res1.ExitCode == %v", res1.ExitCode)
}
if res1.StdErr != "error" { if res1.StdErr != "error" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr) t.Errorf("res1.StdErr == '%v'", res1.StdErr)
} }
@@ -49,6 +62,12 @@ func TestStdcombined(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("%v", err) t.Errorf("%v", err)
} }
if res1.CommandTimedOut {
t.Errorf("Timeout")
}
if res1.ExitCode != 0 {
t.Errorf("res1.ExitCode == %v", res1.ExitCode)
}
if res1.StdErr != "1\n3\n" { if res1.StdErr != "1\n3\n" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr) t.Errorf("res1.StdErr == '%v'", res1.StdErr)
} }
@@ -84,3 +103,221 @@ func TestPartialRead(t *testing.T) {
} }
} }
func TestPartialReadStderr(t *testing.T) {
res1, err := Runner("python").
Arg("-c").
Arg("import sys; import time; print(\"first message\", file=sys.stderr, flush=True); time.sleep(5); print(\"cant see me\", file=sys.stderr, flush=True);").
Timeout(100 * time.Millisecond).
Run()
if err != nil {
t.Errorf("%v", err)
}
if !res1.CommandTimedOut {
t.Errorf("!CommandTimedOut")
}
if res1.StdErr != "first message\n" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "first message\n" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestReadUnflushedStdout(t *testing.T) {
res1, err := Runner("python").Arg("-c").Arg("import sys; print(\"message101\", file=sys.stdout, end='')").Run()
if err != nil {
t.Errorf("%v", err)
}
if res1.CommandTimedOut {
t.Errorf("Timeout")
}
if res1.ExitCode != 0 {
t.Errorf("res1.ExitCode == %v", res1.ExitCode)
}
if res1.StdErr != "" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "message101" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "message101\n" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestReadUnflushedStderr(t *testing.T) {
res1, err := Runner("python").Arg("-c").Arg("import sys; print(\"message101\", file=sys.stderr, end='')").Run()
if err != nil {
t.Errorf("%v", err)
}
if res1.CommandTimedOut {
t.Errorf("Timeout")
}
if res1.ExitCode != 0 {
t.Errorf("res1.ExitCode == %v", res1.ExitCode)
}
if res1.StdErr != "message101" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "message101\n" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestPartialReadUnflushed(t *testing.T) {
t.SkipNow()
res1, err := Runner("python").
Arg("-c").
Arg("import sys; import time; print(\"first message\", end=''); time.sleep(5); print(\"cant see me\", end='');").
Timeout(100 * time.Millisecond).
Run()
if err != nil {
t.Errorf("%v", err)
}
if !res1.CommandTimedOut {
t.Errorf("!CommandTimedOut")
}
if res1.StdErr != "" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "first message" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "first message" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestPartialReadUnflushedStderr(t *testing.T) {
t.SkipNow()
res1, err := Runner("python").
Arg("-c").
Arg("import sys; import time; print(\"first message\", file=sys.stderr, end=''); time.sleep(5); print(\"cant see me\", file=sys.stderr, end='');").
Timeout(100 * time.Millisecond).
Run()
if err != nil {
t.Errorf("%v", err)
}
if !res1.CommandTimedOut {
t.Errorf("!CommandTimedOut")
}
if res1.StdErr != "first message" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "first message" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestListener(t *testing.T) {
res1, err := Runner("python").
Arg("-c").
Arg("import sys;" +
"import time;" +
"print(\"message 1\", flush=True);" +
"time.sleep(1);" +
"print(\"message 2\", flush=True);" +
"time.sleep(1);" +
"print(\"message 3\", flush=True);" +
"time.sleep(1);" +
"print(\"message 4\", file=sys.stderr, flush=True);" +
"time.sleep(1);" +
"print(\"message 5\", flush=True);" +
"time.sleep(1);" +
"print(\"final\");").
ListenStdout(func(s string) { fmt.Printf("@@STDOUT <<- %v (%v)\n", s, time.Now().Format(time.RFC3339Nano)) }).
ListenStderr(func(s string) { fmt.Printf("@@STDERR <<- %v (%v)\n", s, time.Now().Format(time.RFC3339Nano)) }).
Timeout(10 * time.Second).
Run()
if err != nil {
panic(err)
}
if res1.CommandTimedOut {
t.Errorf("Timeout")
}
if res1.ExitCode != 0 {
t.Errorf("res1.ExitCode == %v", res1.ExitCode)
}
}
func TestLongStdout(t *testing.T) {
res1, err := Runner("python").
Arg("-c").
Arg("import sys; import time; print(\"X\" * 125001 + \"\\n\"); print(\"Y\" * 125001 + \"\\n\"); print(\"Z\" * 125001 + \"\\n\");").
Timeout(5000 * time.Millisecond).
Run()
if err != nil {
t.Errorf("%v", err)
}
if res1.CommandTimedOut {
t.Errorf("Timeout")
}
if res1.ExitCode != 0 {
t.Errorf("res1.ExitCode == %v", res1.ExitCode)
}
if res1.StdErr != "" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if len(res1.StdOut) != 375009 {
t.Errorf("len(res1.StdOut) == '%v'", len(res1.StdOut))
}
}
func TestFailOnTimeout(t *testing.T) {
_, err := Runner("sleep").Arg("2").Timeout(200 * time.Millisecond).FailOnTimeout().Run()
if err != ErrTimeout {
t.Errorf("wrong err := %v", err)
}
}
func TestFailOnExitcode(t *testing.T) {
_, err := Runner("false").Timeout(200 * time.Millisecond).FailOnExitCode().Run()
if err != ErrExitCode {
t.Errorf("wrong err := %v", err)
}
}
func TestEnsureExitcode1(t *testing.T) {
_, err := Runner("false").Timeout(200 * time.Millisecond).EnsureExitcode(1).Run()
if err != nil {
t.Errorf("wrong err := %v", err)
}
}
func TestEnsureExitcode2(t *testing.T) {
_, err := Runner("false").Timeout(200*time.Millisecond).EnsureExitcode(0, 2, 3).Run()
if err != ErrExitCode {
t.Errorf("wrong err := %v", err)
}
}

View File

@@ -8,6 +8,7 @@ import (
) )
type pipeReader struct { type pipeReader struct {
lineBufferSize *int
stdout io.ReadCloser stdout io.ReadCloser
stderr io.ReadCloser stderr io.ReadCloser
} }
@@ -33,7 +34,6 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
buf := make([]byte, 128) buf := make([]byte, 128)
for true { for true {
n, out := pr.stdout.Read(buf) n, out := pr.stdout.Read(buf)
if n > 0 { if n > 0 {
txt := string(buf[:n]) txt := string(buf[:n])
stdout += txt stdout += txt
@@ -91,6 +91,9 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
wg.Add(1) wg.Add(1)
go func() { go func() {
scanner := bufio.NewScanner(stdoutBufferReader) scanner := bufio.NewScanner(stdoutBufferReader)
if pr.lineBufferSize != nil {
scanner.Buffer([]byte{}, *pr.lineBufferSize)
}
for scanner.Scan() { for scanner.Scan() {
txt := scanner.Text() txt := scanner.Text()
for _, lstr := range listener { for _, lstr := range listener {
@@ -98,6 +101,9 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
} }
combch <- combevt{txt, false} combch <- combevt{txt, false}
} }
if err := scanner.Err(); err != nil {
errch <- err
}
combch <- combevt{"", true} combch <- combevt{"", true}
wg.Done() wg.Done()
}() }()
@@ -107,6 +113,9 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
wg.Add(1) wg.Add(1)
go func() { go func() {
scanner := bufio.NewScanner(stderrBufferReader) scanner := bufio.NewScanner(stderrBufferReader)
if pr.lineBufferSize != nil {
scanner.Buffer([]byte{}, *pr.lineBufferSize)
}
for scanner.Scan() { for scanner.Scan() {
txt := scanner.Text() txt := scanner.Text()
for _, lstr := range listener { for _, lstr := range listener {
@@ -114,6 +123,9 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
} }
combch <- combevt{txt, false} combch <- combevt{txt, false}
} }
if err := scanner.Err(); err != nil {
errch <- err
}
combch <- combevt{"", true} combch <- combevt{"", true}
wg.Done() wg.Done()
}() }()

View File

@@ -22,10 +22,10 @@ import (
// //
// sub-structs are recursively parsed (if they have an env tag) and the env-variable keys are delimited by the delim parameter // sub-structs are recursively parsed (if they have an env tag) and the env-variable keys are delimited by the delim parameter
// sub-structs with `env:""` are also parsed, but the delimited is skipped (they are handled as if they were one level higher) // sub-structs with `env:""` are also parsed, but the delimited is skipped (they are handled as if they were one level higher)
func ApplyEnvOverrides[T any](c *T, delim string) error { func ApplyEnvOverrides[T any](prefix string, c *T, delim string) error {
rval := reflect.ValueOf(c).Elem() rval := reflect.ValueOf(c).Elem()
return processEnvOverrides(rval, delim, "") return processEnvOverrides(rval, delim, prefix)
} }
func processEnvOverrides(rval reflect.Value, delim string, prefix string) error { func processEnvOverrides(rval reflect.Value, delim string, prefix string) error {
@@ -70,103 +70,114 @@ func processEnvOverrides(rval reflect.Value, delim string, prefix string) error
continue continue
} }
if rvfield.Type() == reflect.TypeOf("") { if rvfield.Type().Kind() == reflect.Pointer {
rvfield.Set(reflect.ValueOf(envval)) newval, err := parseEnvToValue(envval, fullEnvKey, rvfield.Type().Elem())
if err != nil {
return err
}
// converts reflect.Value to pointer
ptrval := reflect.New(rvfield.Type().Elem())
ptrval.Elem().Set(newval)
rvfield.Set(ptrval)
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval) fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(int(0)) {
envint, err := strconv.ParseInt(envval, 10, bits.UintSize)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int (value := '%s')", fullEnvKey, envval))
}
rvfield.Set(reflect.ValueOf(int(envint)))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(int64(0)) {
envint, err := strconv.ParseInt(envval, 10, 64)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int64 (value := '%s')", fullEnvKey, envval))
}
rvfield.Set(reflect.ValueOf(int64(envint)))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(int32(0)) {
envint, err := strconv.ParseInt(envval, 10, 32)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", fullEnvKey, envval))
}
rvfield.Set(reflect.ValueOf(int32(envint)))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(int8(0)) {
envint, err := strconv.ParseInt(envval, 10, 8)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", fullEnvKey, envval))
}
rvfield.Set(reflect.ValueOf(int8(envint)))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(time.Duration(0)) {
dur, err := timeext.ParseDurationShortString(envval)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to duration (value := '%s')", fullEnvKey, envval))
}
rvfield.Set(reflect.ValueOf(dur))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, dur.String())
} else if rvfield.Type() == reflect.TypeOf(time.UnixMilli(0)) {
tim, err := time.Parse(time.RFC3339Nano, envval)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to time.time (value := '%s')", fullEnvKey, envval))
}
rvfield.Set(reflect.ValueOf(tim))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, tim.String())
} else if rvfield.Type().ConvertibleTo(reflect.TypeOf(int(0))) {
envint, err := strconv.ParseInt(envval, 10, 8)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to <%s, ,int> (value := '%s')", rvfield.Type().Name(), fullEnvKey, envval))
}
envcvl := reflect.ValueOf(envint).Convert(rvfield.Type())
rvfield.Set(envcvl)
fmt.Printf("[CONF] Overwrite config '%s' with '%v'\n", fullEnvKey, envcvl.Interface())
} else if rvfield.Type().ConvertibleTo(reflect.TypeOf("")) {
envcvl := reflect.ValueOf(envval).Convert(rvfield.Type())
rvfield.Set(envcvl)
fmt.Printf("[CONF] Overwrite config '%s' with '%v'\n", fullEnvKey, envcvl.Interface())
} else { } else {
return errors.New(fmt.Sprintf("Unknown kind/type in config: [ %s | %s ]", rvfield.Kind().String(), rvfield.Type().String()))
newval, err := parseEnvToValue(envval, fullEnvKey, rvfield.Type())
if err != nil {
return err
} }
rvfield.Set(newval)
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
}
} }
return nil return nil
} }
func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (reflect.Value, error) {
if rvtype == reflect.TypeOf("") {
return reflect.ValueOf(envval), nil
} else if rvtype == reflect.TypeOf(int(0)) {
envint, err := strconv.ParseInt(envval, 10, bits.UintSize)
if err != nil {
return reflect.Value{}, errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int (value := '%s')", fullEnvKey, envval))
}
return reflect.ValueOf(int(envint)), nil
} else if rvtype == reflect.TypeOf(int64(0)) {
envint, err := strconv.ParseInt(envval, 10, 64)
if err != nil {
return reflect.Value{}, errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int64 (value := '%s')", fullEnvKey, envval))
}
return reflect.ValueOf(int64(envint)), nil
} else if rvtype == reflect.TypeOf(int32(0)) {
envint, err := strconv.ParseInt(envval, 10, 32)
if err != nil {
return reflect.Value{}, errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", fullEnvKey, envval))
}
return reflect.ValueOf(int32(envint)), nil
} else if rvtype == reflect.TypeOf(int8(0)) {
envint, err := strconv.ParseInt(envval, 10, 8)
if err != nil {
return reflect.Value{}, errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", fullEnvKey, envval))
}
return reflect.ValueOf(int8(envint)), nil
} else if rvtype == reflect.TypeOf(time.Duration(0)) {
dur, err := timeext.ParseDurationShortString(envval)
if err != nil {
return reflect.Value{}, errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to duration (value := '%s')", fullEnvKey, envval))
}
return reflect.ValueOf(dur), nil
} else if rvtype == reflect.TypeOf(time.UnixMilli(0)) {
tim, err := time.Parse(time.RFC3339Nano, envval)
if err != nil {
return reflect.Value{}, errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to time.time (value := '%s')", fullEnvKey, envval))
}
return reflect.ValueOf(tim), nil
} else if rvtype.ConvertibleTo(reflect.TypeOf(int(0))) {
envint, err := strconv.ParseInt(envval, 10, 8)
if err != nil {
return reflect.Value{}, errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to <%s, ,int> (value := '%s')", rvtype.Name(), fullEnvKey, envval))
}
envcvl := reflect.ValueOf(envint).Convert(rvtype)
return envcvl, nil
} else if rvtype.ConvertibleTo(reflect.TypeOf("")) {
envcvl := reflect.ValueOf(envval).Convert(rvtype)
return envcvl, nil
} else {
return reflect.Value{}, errors.New(fmt.Sprintf("Unknown kind/type in config: [ %s | %s ]", rvtype.Kind().String(), rvtype.String()))
}
}

View File

@@ -2,6 +2,7 @@ package confext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/timeext" "gogs.mikescher.com/BlackForestBytes/goext/timeext"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"testing" "testing"
"time" "time"
) )
@@ -41,13 +42,13 @@ func TestApplyEnvOverridesNoop(t *testing.T) {
output := input output := input
err := ApplyEnvOverrides(&output, ".") err := ApplyEnvOverrides("", &output, ".")
if err != nil { if err != nil {
t.Errorf("%v", err) t.Errorf("%v", err)
t.FailNow() t.FailNow()
} }
assertEqual(t, input, output) tst.AssertEqual(t, input, output)
} }
func TestApplyEnvOverridesSimple(t *testing.T) { func TestApplyEnvOverridesSimple(t *testing.T) {
@@ -93,21 +94,21 @@ func TestApplyEnvOverridesSimple(t *testing.T) {
t.Setenv("TEST_V8", "1min4s") t.Setenv("TEST_V8", "1min4s")
t.Setenv("TEST_V9", "2009-11-10T23:00:00Z") t.Setenv("TEST_V9", "2009-11-10T23:00:00Z")
err := ApplyEnvOverrides(&data, ".") err := ApplyEnvOverrides("", &data, ".")
if err != nil { if err != nil {
t.Errorf("%v", err) t.Errorf("%v", err)
t.FailNow() t.FailNow()
} }
assertEqual(t, data.V1, 846) tst.AssertEqual(t, data.V1, 846)
assertEqual(t, data.V2, "hello_world") tst.AssertEqual(t, data.V2, "hello_world")
assertEqual(t, data.V3, 6) tst.AssertEqual(t, data.V3, 6)
assertEqual(t, data.V4, 333) tst.AssertEqual(t, data.V4, 333)
assertEqual(t, data.V5, -937) tst.AssertEqual(t, data.V5, -937)
assertEqual(t, data.V6, 70) tst.AssertEqual(t, data.V6, 70)
assertEqual(t, data.V7, "AAAAAA") tst.AssertEqual(t, data.V7, "AAAAAA")
assertEqual(t, data.V8, time.Second*64) tst.AssertEqual(t, data.V8, time.Second*64)
assertEqual(t, data.V9, time.Unix(1257894000, 0).UTC()) tst.AssertEqual(t, data.V9, time.Unix(1257894000, 0).UTC())
} }
func TestApplyEnvOverridesRecursive(t *testing.T) { func TestApplyEnvOverridesRecursive(t *testing.T) {
@@ -182,35 +183,83 @@ func TestApplyEnvOverridesRecursive(t *testing.T) {
t.Setenv("SUB_V3", "33min") t.Setenv("SUB_V3", "33min")
t.Setenv("SUB_V4", "2044-01-01T00:00:00Z") t.Setenv("SUB_V4", "2044-01-01T00:00:00Z")
err := ApplyEnvOverrides(&data, "_") err := ApplyEnvOverrides("", &data, "_")
if err != nil { if err != nil {
t.Errorf("%v", err) t.Errorf("%v", err)
t.FailNow() t.FailNow()
} }
assertEqual(t, data.V1, 999) tst.AssertEqual(t, data.V1, 999)
assertEqual(t, data.VX, "2") tst.AssertEqual(t, data.VX, "2")
assertEqual(t, data.V5, "no") tst.AssertEqual(t, data.V5, "no")
assertEqual(t, data.Sub1.V1, 3) tst.AssertEqual(t, data.Sub1.V1, 3)
assertEqual(t, data.Sub1.VX, "4") tst.AssertEqual(t, data.Sub1.VX, "4")
assertEqual(t, data.Sub1.V2, "5") tst.AssertEqual(t, data.Sub1.V2, "5")
assertEqual(t, data.Sub1.V8, time.Second*6) tst.AssertEqual(t, data.Sub1.V8, time.Second*6)
assertEqual(t, data.Sub1.V9, time.Unix(947206861, 0).UTC()) tst.AssertEqual(t, data.Sub1.V9, time.Unix(947206861, 0).UTC())
assertEqual(t, data.Sub2.V1, 846) tst.AssertEqual(t, data.Sub2.V1, 846)
assertEqual(t, data.Sub2.VX, "9") tst.AssertEqual(t, data.Sub2.VX, "9")
assertEqual(t, data.Sub2.V2, "222_hello_world") tst.AssertEqual(t, data.Sub2.V2, "222_hello_world")
assertEqual(t, data.Sub2.V8, time.Second*64) tst.AssertEqual(t, data.Sub2.V8, time.Second*64)
assertEqual(t, data.Sub2.V9, time.Unix(1257894000, 0).UTC()) tst.AssertEqual(t, data.Sub2.V9, time.Unix(1257894000, 0).UTC())
assertEqual(t, data.Sub3.V1, 33846) tst.AssertEqual(t, data.Sub3.V1, 33846)
assertEqual(t, data.Sub3.VX, "14") tst.AssertEqual(t, data.Sub3.VX, "14")
assertEqual(t, data.Sub3.V2, "33_hello_world") tst.AssertEqual(t, data.Sub3.V2, "33_hello_world")
assertEqual(t, data.Sub3.V8, time.Second*1984) tst.AssertEqual(t, data.Sub3.V8, time.Second*1984)
assertEqual(t, data.Sub3.V9, time.Unix(2015276400, 0).UTC()) tst.AssertEqual(t, data.Sub3.V9, time.Unix(2015276400, 0).UTC())
assertEqual(t, data.Sub4.V1, 11) tst.AssertEqual(t, data.Sub4.V1, 11)
assertEqual(t, data.Sub4.VX, "19") tst.AssertEqual(t, data.Sub4.VX, "19")
assertEqual(t, data.Sub4.V2, "22") tst.AssertEqual(t, data.Sub4.V2, "22")
assertEqual(t, data.Sub4.V8, time.Second*1980) tst.AssertEqual(t, data.Sub4.V8, time.Second*1980)
assertEqual(t, data.Sub4.V9, time.Unix(2335219200, 0).UTC()) tst.AssertEqual(t, data.Sub4.V9, time.Unix(2335219200, 0).UTC())
}
func TestApplyEnvOverridesPointer(t *testing.T) {
type aliasint int
type aliasstring string
type testdata struct {
V1 *int `env:"TEST_V1"`
VX *string ``
V2 *string `env:"TEST_V2"`
V3 *int8 `env:"TEST_V3"`
V4 *int32 `env:"TEST_V4"`
V5 *int64 `env:"TEST_V5"`
V6 *aliasint `env:"TEST_V6"`
VY *aliasint ``
V7 *aliasstring `env:"TEST_V7"`
V8 *time.Duration `env:"TEST_V8"`
V9 *time.Time `env:"TEST_V9"`
}
data := testdata{}
t.Setenv("TEST_V1", "846")
t.Setenv("TEST_V2", "hello_world")
t.Setenv("TEST_V3", "6")
t.Setenv("TEST_V4", "333")
t.Setenv("TEST_V5", "-937")
t.Setenv("TEST_V6", "070")
t.Setenv("TEST_V7", "AAAAAA")
t.Setenv("TEST_V8", "1min4s")
t.Setenv("TEST_V9", "2009-11-10T23:00:00Z")
err := ApplyEnvOverrides("", &data, ".")
if err != nil {
t.Errorf("%v", err)
t.FailNow()
}
tst.AssertDeRefEqual(t, data.V1, 846)
tst.AssertDeRefEqual(t, data.V2, "hello_world")
tst.AssertDeRefEqual(t, data.V3, 6)
tst.AssertDeRefEqual(t, data.V4, 333)
tst.AssertDeRefEqual(t, data.V5, -937)
tst.AssertDeRefEqual(t, data.V6, 70)
tst.AssertDeRefEqual(t, data.V7, "AAAAAA")
tst.AssertDeRefEqual(t, data.V8, time.Second*64)
tst.AssertDeRefEqual(t, data.V9, time.Unix(1257894000, 0).UTC())
} }
func assertEqual[T comparable](t *testing.T, actual T, expected T) { func assertEqual[T comparable](t *testing.T, actual T, expected T) {
@@ -218,3 +267,12 @@ func assertEqual[T comparable](t *testing.T, actual T, expected T) {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected) t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
} }
} }
func assertPtrEqual[T comparable](t *testing.T, actual *T, expected T) {
if actual == nil {
t.Errorf("values differ: Actual: NIL, Expected: '%v'", expected)
}
if *actual != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
}
}

View File

@@ -1,10 +1,13 @@
package cryptext package cryptext
import ( import (
"bytes"
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"crypto/rand" "crypto/rand"
"encoding/base64" "crypto/sha256"
"encoding/base32"
"encoding/json"
"errors" "errors"
"golang.org/x/crypto/scrypt" "golang.org/x/crypto/scrypt"
"io" "io"
@@ -12,35 +15,90 @@ import (
// https://stackoverflow.com/a/18819040/1761622 // https://stackoverflow.com/a/18819040/1761622
func EncryptAESSimple(password, text []byte) ([]byte, error) { type aesPayload struct {
Salt []byte `json:"s"`
IV []byte `json:"i"`
Data []byte `json:"d"`
Rounds int `json:"r"`
Version uint `json:"v"`
}
key, err := scrypt.Key(password, nil, 32768, 8, 1, 32) // this is not 100% correct, rounds too low and salt is missing func EncryptAESSimple(password []byte, data []byte, rounds int) (string, error) {
salt := make([]byte, 8)
_, err := io.ReadFull(rand.Reader, salt)
if err != nil { if err != nil {
return nil, err return "", err
}
key, err := scrypt.Key(password, salt, rounds, 8, 1, 32)
if err != nil {
return "", err
} }
block, err := aes.NewCipher(key) block, err := aes.NewCipher(key)
if err != nil { if err != nil {
return nil, err return "", err
} }
b := base64.StdEncoding.EncodeToString(text) h := sha256.New()
ciphertext := make([]byte, aes.BlockSize+len(b)) h.Write(data)
checksum := h.Sum(nil)
iv := ciphertext[:aes.BlockSize] if len(checksum) != 32 {
if _, err := io.ReadFull(rand.Reader, iv); err != nil { return "", errors.New("wrong cs size")
return nil, err
} }
ciphertext := make([]byte, 32+len(data))
iv := make([]byte, aes.BlockSize)
_, err = io.ReadFull(rand.Reader, iv)
if err != nil {
return "", err
}
combinedData := make([]byte, 0, 32+len(data))
combinedData = append(combinedData, checksum...)
combinedData = append(combinedData, data...)
cfb := cipher.NewCFBEncrypter(block, iv) cfb := cipher.NewCFBEncrypter(block, iv)
cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b)) cfb.XORKeyStream(ciphertext, combinedData)
return ciphertext, nil pl := aesPayload{
Salt: salt,
IV: iv,
Data: ciphertext,
Version: 1,
Rounds: rounds,
} }
func DecryptAESSimple(password, text []byte) ([]byte, error) { jbin, err := json.Marshal(pl)
if err != nil {
return "", err
}
key, err := scrypt.Key(password, nil, 32768, 8, 1, 32) // this is not 100% correct, rounds too low and salt is missing res := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(jbin)
return res, nil
}
func DecryptAESSimple(password []byte, encText string) ([]byte, error) {
jbin, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(encText)
if err != nil {
return nil, err
}
var pl aesPayload
err = json.Unmarshal(jbin, &pl)
if err != nil {
return nil, err
}
if pl.Version != 1 {
return nil, errors.New("unsupported version")
}
key, err := scrypt.Key(password, pl.Salt, pl.Rounds, 8, 1, 32) // this is not 100% correct, rounds too low and salt is missing
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -50,18 +108,24 @@ func DecryptAESSimple(password, text []byte) ([]byte, error) {
return nil, err return nil, err
} }
if len(text) < aes.BlockSize { dest := make([]byte, len(pl.Data))
return nil, errors.New("ciphertext too short")
cfb := cipher.NewCFBDecrypter(block, pl.IV)
cfb.XORKeyStream(dest, pl.Data)
if len(dest) < 32 {
return nil, errors.New("payload too small")
} }
iv := text[:aes.BlockSize] chck := dest[:32]
text = text[aes.BlockSize:] data := dest[32:]
cfb := cipher.NewCFBDecrypter(block, iv)
cfb.XORKeyStream(text, text)
data, err := base64.StdEncoding.DecodeString(string(text)) h := sha256.New()
if err != nil { h.Write(data)
return nil, err chck2 := h.Sum(nil)
if !bytes.Equal(chck, chck2) {
return nil, errors.New("checksum mismatch")
} }
return data, nil return data, nil

View File

@@ -1,6 +1,10 @@
package cryptext package cryptext
import "testing" import (
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"testing"
)
func TestEncryptAESSimple(t *testing.T) { func TestEncryptAESSimple(t *testing.T) {
@@ -8,15 +12,25 @@ func TestEncryptAESSimple(t *testing.T) {
str1 := []byte("Hello World") str1 := []byte("Hello World")
str2, err := EncryptAESSimple(pw, str1) str2, err := EncryptAESSimple(pw, str1, 512)
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Printf("%s\n", str2)
str3, err := DecryptAESSimple(pw, str2) str3, err := DecryptAESSimple(pw, str2)
if err != nil { if err != nil {
panic(err) panic(err)
} }
assertEqual(t, string(str1), string(str3)) tst.AssertEqual(t, string(str1), string(str3))
str4, err := EncryptAESSimple(pw, str3, 512)
if err != nil {
panic(err)
}
tst.AssertNotEqual(t, string(str2), string(str4))
} }

View File

@@ -1,25 +1,20 @@
package cryptext package cryptext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
func TestStrSha256(t *testing.T) { func TestStrSha256(t *testing.T) {
assertEqual(t, StrSha256(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") tst.AssertEqual(t, StrSha256(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
assertEqual(t, StrSha256("0"), "5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9") tst.AssertEqual(t, StrSha256("0"), "5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9")
assertEqual(t, StrSha256("80085"), "b3786e141d65638ad8a98173e26b5f6a53c927737b23ff31fb1843937250f44b") tst.AssertEqual(t, StrSha256("80085"), "b3786e141d65638ad8a98173e26b5f6a53c927737b23ff31fb1843937250f44b")
assertEqual(t, StrSha256("Hello World"), "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e") tst.AssertEqual(t, StrSha256("Hello World"), "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e")
} }
func TestBytesSha256(t *testing.T) { func TestBytesSha256(t *testing.T) {
assertEqual(t, BytesSha256([]byte{}), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") tst.AssertEqual(t, BytesSha256([]byte{}), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
assertEqual(t, BytesSha256([]byte{0}), "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d") tst.AssertEqual(t, BytesSha256([]byte{0}), "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d")
assertEqual(t, BytesSha256([]byte{128}), "76be8b528d0075f7aae98d6fa57a6d3c83ae480a8469e668d7b0af968995ac71") tst.AssertEqual(t, BytesSha256([]byte{128}), "76be8b528d0075f7aae98d6fa57a6d3c83ae480a8469e668d7b0af968995ac71")
assertEqual(t, BytesSha256([]byte{0, 1, 2, 4, 8, 16, 32, 64, 128, 255}), "55016a318ba538e00123c736b2a8b6db368d00e7e25727547655b653e5853603") tst.AssertEqual(t, BytesSha256([]byte{0, 1, 2, 4, 8, 16, 32, 64, 128, 255}), "55016a318ba538e00123c736b2a8b6db368d00e7e25727547655b653e5853603")
}
func assertEqual(t *testing.T, actual string, expected string) {
if actual != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
}
} }

View File

@@ -12,7 +12,7 @@ func init() {
} }
func TestResultCache1(t *testing.T) { func TestResultCache1(t *testing.T) {
cache := NewLRUMap[string](8) cache := NewLRUMap[string, string](8)
verifyLRUList(cache, t) verifyLRUList(cache, t)
key := randomKey() key := randomKey()
@@ -50,7 +50,7 @@ func TestResultCache1(t *testing.T) {
} }
func TestResultCache2(t *testing.T) { func TestResultCache2(t *testing.T) {
cache := NewLRUMap[string](8) cache := NewLRUMap[string, string](8)
verifyLRUList(cache, t) verifyLRUList(cache, t)
key1 := "key1" key1 := "key1"
@@ -150,7 +150,7 @@ func TestResultCache2(t *testing.T) {
} }
func TestResultCache3(t *testing.T) { func TestResultCache3(t *testing.T) {
cache := NewLRUMap[string](8) cache := NewLRUMap[string, string](8)
verifyLRUList(cache, t) verifyLRUList(cache, t)
key1 := "key1" key1 := "key1"
@@ -173,7 +173,7 @@ func TestResultCache3(t *testing.T) {
} }
// does a basic consistency check over the internal cache representation // does a basic consistency check over the internal cache representation
func verifyLRUList[TData any](cache *LRUMap[TData], t *testing.T) { func verifyLRUList[TKey comparable, TData any](cache *LRUMap[TKey, TData], t *testing.T) {
size := 0 size := 0
tailFound := false tailFound := false

View File

@@ -2,6 +2,7 @@ package dataext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
@@ -43,10 +44,10 @@ func TestObjectMerge(t *testing.T) {
valueMerge := ObjectMerge(valueA, valueB) valueMerge := ObjectMerge(valueA, valueB)
assertPtrEqual(t, "Field1", valueMerge.Field1, valueB.Field1) tst.AssertIdentPtrEqual(t, "Field1", valueMerge.Field1, valueB.Field1)
assertPtrEqual(t, "Field2", valueMerge.Field2, valueA.Field2) tst.AssertIdentPtrEqual(t, "Field2", valueMerge.Field2, valueA.Field2)
assertPtrEqual(t, "Field3", valueMerge.Field3, valueB.Field3) tst.AssertIdentPtrEqual(t, "Field3", valueMerge.Field3, valueB.Field3)
assertPtrEqual(t, "Field4", valueMerge.Field4, nil) tst.AssertIdentPtrEqual(t, "Field4", valueMerge.Field4, nil)
} }

View File

@@ -1,8 +1,8 @@
package dataext package dataext
import ( import (
"encoding/hex"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
@@ -18,14 +18,14 @@ func noErrStructHash(t *testing.T, dat any, opt ...StructHashOptions) []byte {
func TestStructHashSimple(t *testing.T) { func TestStructHashSimple(t *testing.T) {
assertEqual(t, "209bf774af36cc3a045c152d9f1269ef3684ad819c1359ee73ff0283a308fefa", noErrStructHash(t, "Hello")) tst.AssertHexEqual(t, "209bf774af36cc3a045c152d9f1269ef3684ad819c1359ee73ff0283a308fefa", noErrStructHash(t, "Hello"))
assertEqual(t, "c32f3626b981ae2997db656f3acad3f1dc9d30ef6b6d14296c023e391b25f71a", noErrStructHash(t, 0)) tst.AssertHexEqual(t, "c32f3626b981ae2997db656f3acad3f1dc9d30ef6b6d14296c023e391b25f71a", noErrStructHash(t, 0))
assertEqual(t, "01b781b03e9586b257d387057dfc70d9f06051e7d3c1e709a57e13cc8daf3e35", noErrStructHash(t, []byte{})) tst.AssertHexEqual(t, "01b781b03e9586b257d387057dfc70d9f06051e7d3c1e709a57e13cc8daf3e35", noErrStructHash(t, []byte{}))
assertEqual(t, "93e1dcd45c732fe0079b0fb3204c7c803f0921835f6bfee2e6ff263e73eed53c", noErrStructHash(t, []int{})) tst.AssertHexEqual(t, "93e1dcd45c732fe0079b0fb3204c7c803f0921835f6bfee2e6ff263e73eed53c", noErrStructHash(t, []int{}))
assertEqual(t, "54f637a376aad55b3160d98ebbcae8099b70d91b9400df23fb3709855d59800a", noErrStructHash(t, []int{1, 2, 3})) tst.AssertHexEqual(t, "54f637a376aad55b3160d98ebbcae8099b70d91b9400df23fb3709855d59800a", noErrStructHash(t, []int{1, 2, 3}))
assertEqual(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", noErrStructHash(t, nil)) tst.AssertHexEqual(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", noErrStructHash(t, nil))
assertEqual(t, "349a7db91aa78fd30bbaa7c7f9c7bfb2fcfe72869b4861162a96713a852f60d3", noErrStructHash(t, []any{1, "", nil})) tst.AssertHexEqual(t, "349a7db91aa78fd30bbaa7c7f9c7bfb2fcfe72869b4861162a96713a852f60d3", noErrStructHash(t, []any{1, "", nil}))
assertEqual(t, "ca51aab87808bf0062a4a024de6aac0c2bad54275cc857a4944569f89fd245ad", noErrStructHash(t, struct{}{})) tst.AssertHexEqual(t, "ca51aab87808bf0062a4a024de6aac0c2bad54275cc857a4944569f89fd245ad", noErrStructHash(t, struct{}{}))
} }
@@ -37,13 +37,13 @@ func TestStructHashSimpleStruct(t *testing.T) {
F3 *int F3 *int
} }
assertEqual(t, "a90bff751c70c738bb5cfc9b108e783fa9c19c0bc9273458e0aaee6e74aa1b92", noErrStructHash(t, t0{ tst.AssertHexEqual(t, "a90bff751c70c738bb5cfc9b108e783fa9c19c0bc9273458e0aaee6e74aa1b92", noErrStructHash(t, t0{
F1: 10, F1: 10,
F2: []string{"1", "2", "3"}, F2: []string{"1", "2", "3"},
F3: nil, F3: nil,
})) }))
assertEqual(t, "5d09090dc34ac59dd645f197a255f653387723de3afa1b614721ea5a081c675f", noErrStructHash(t, t0{ tst.AssertHexEqual(t, "5d09090dc34ac59dd645f197a255f653387723de3afa1b614721ea5a081c675f", noErrStructHash(t, t0{
F1: 10, F1: 10,
F2: []string{"1", "2", "3"}, F2: []string{"1", "2", "3"},
F3: langext.Ptr(99), F3: langext.Ptr(99),
@@ -64,7 +64,7 @@ func TestStructHashLayeredStruct(t *testing.T) {
SV3 t1_1 SV3 t1_1
} }
assertEqual(t, "fd4ca071fb40a288fee4b7a3dfdaab577b30cb8f80f81ec511e7afd72dc3b469", noErrStructHash(t, t1_2{ tst.AssertHexEqual(t, "fd4ca071fb40a288fee4b7a3dfdaab577b30cb8f80f81ec511e7afd72dc3b469", noErrStructHash(t, t1_2{
SV1: nil, SV1: nil,
SV2: nil, SV2: nil,
SV3: t1_1{ SV3: t1_1{
@@ -73,7 +73,7 @@ func TestStructHashLayeredStruct(t *testing.T) {
F15: false, F15: false,
}, },
})) }))
assertEqual(t, "3fbf7c67d8121deda075cc86319a4e32d71744feb2cebf89b43bc682f072a029", noErrStructHash(t, t1_2{ tst.AssertHexEqual(t, "3fbf7c67d8121deda075cc86319a4e32d71744feb2cebf89b43bc682f072a029", noErrStructHash(t, t1_2{
SV1: nil, SV1: nil,
SV2: &t1_1{}, SV2: &t1_1{},
SV3: t1_1{ SV3: t1_1{
@@ -82,7 +82,7 @@ func TestStructHashLayeredStruct(t *testing.T) {
F15: true, F15: true,
}, },
})) }))
assertEqual(t, "b1791ccd1b346c3ede5bbffda85555adcd8216b93ffca23f14fe175ec47c5104", noErrStructHash(t, t1_2{ tst.AssertHexEqual(t, "b1791ccd1b346c3ede5bbffda85555adcd8216b93ffca23f14fe175ec47c5104", noErrStructHash(t, t1_2{
SV1: &t1_1{}, SV1: &t1_1{},
SV2: &t1_1{}, SV2: &t1_1{},
SV3: t1_1{ SV3: t1_1{
@@ -101,7 +101,7 @@ func TestStructHashMap(t *testing.T) {
F2 map[string]int F2 map[string]int
} }
assertEqual(t, "d50c53ad1fafb448c33fddd5aca01a86a2edf669ce2ecab07ba6fe877951d824", noErrStructHash(t, t0{ tst.AssertHexEqual(t, "d50c53ad1fafb448c33fddd5aca01a86a2edf669ce2ecab07ba6fe877951d824", noErrStructHash(t, t0{
F1: 10, F1: 10,
F2: map[string]int{ F2: map[string]int{
"x": 1, "x": 1,
@@ -110,7 +110,7 @@ func TestStructHashMap(t *testing.T) {
}, },
})) }))
assertEqual(t, "d50c53ad1fafb448c33fddd5aca01a86a2edf669ce2ecab07ba6fe877951d824", noErrStructHash(t, t0{ tst.AssertHexEqual(t, "d50c53ad1fafb448c33fddd5aca01a86a2edf669ce2ecab07ba6fe877951d824", noErrStructHash(t, t0{
F1: 10, F1: 10,
F2: map[string]int{ F2: map[string]int{
"a": 99, "a": 99,
@@ -128,16 +128,9 @@ func TestStructHashMap(t *testing.T) {
m3["x"] = 1 m3["x"] = 1
m3["a"] = 2 m3["a"] = 2
assertEqual(t, "d50c53ad1fafb448c33fddd5aca01a86a2edf669ce2ecab07ba6fe877951d824", noErrStructHash(t, t0{ tst.AssertHexEqual(t, "d50c53ad1fafb448c33fddd5aca01a86a2edf669ce2ecab07ba6fe877951d824", noErrStructHash(t, t0{
F1: 10, F1: 10,
F2: m3, F2: m3,
})) }))
} }
func assertEqual(t *testing.T, expected string, actual []byte) {
actualStr := hex.EncodeToString(actual)
if actualStr != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actualStr, expected)
}
}

1
go.mod
View File

@@ -9,5 +9,6 @@ require (
require ( require (
github.com/jmoiron/sqlx v1.3.5 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect
go.mongodb.org/mongo-driver v1.11.1 // indirect
golang.org/x/crypto v0.4.0 // indirect golang.org/x/crypto v0.4.0 // indirect
) )

35
go.sum
View File

@@ -1,15 +1,50 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8=
go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -217,6 +217,15 @@ func ArrFirst[T any](arr []T, comp func(v T) bool) (T, bool) {
return *new(T), false return *new(T), false
} }
func ArrFirstOrNil[T any](arr []T, comp func(v T) bool) *T {
for _, v := range arr {
if comp(v) {
return Ptr(v)
}
}
return nil
}
func ArrLast[T any](arr []T, comp func(v T) bool) (T, bool) { func ArrLast[T any](arr []T, comp func(v T) bool) (T, bool) {
found := false found := false
result := *new(T) result := *new(T)
@@ -229,6 +238,22 @@ func ArrLast[T any](arr []T, comp func(v T) bool) (T, bool) {
return result, found return result, found
} }
func ArrLastOrNil[T any](arr []T, comp func(v T) bool) *T {
found := false
result := *new(T)
for _, v := range arr {
if comp(v) {
found = true
result = v
}
}
if found {
return Ptr(result)
} else {
return nil
}
}
func ArrFirstIndex[T comparable](arr []T, needle T) int { func ArrFirstIndex[T comparable](arr []T, needle T) int {
for i, v := range arr { for i, v := range arr {
if v == needle { if v == needle {
@@ -265,6 +290,46 @@ func ArrMap[T1 any, T2 any](arr []T1, conv func(v T1) T2) []T2 {
return r return r
} }
func MapMap[TK comparable, TV any, TR any](inmap map[TK]TV, conv func(k TK, v TV) TR) []TR {
r := make([]TR, 0, len(inmap))
for k, v := range inmap {
r = append(r, conv(k, v))
}
return r
}
func MapMapErr[TK comparable, TV any, TR any](inmap map[TK]TV, conv func(k TK, v TV) (TR, error)) ([]TR, error) {
r := make([]TR, 0, len(inmap))
for k, v := range inmap {
elem, err := conv(k, v)
if err != nil {
return nil, err
}
r = append(r, elem)
}
return r, nil
}
func ArrMapExt[T1 any, T2 any](arr []T1, conv func(idx int, v T1) T2) []T2 {
r := make([]T2, len(arr))
for i, v := range arr {
r[i] = conv(i, v)
}
return r
}
func ArrMapErr[T1 any, T2 any](arr []T1, conv func(v T1) (T2, error)) ([]T2, error) {
var err error
r := make([]T2, len(arr))
for i, v := range arr {
r[i], err = conv(v)
if err != nil {
return nil, err
}
}
return r, nil
}
func ArrFilterMap[T1 any, T2 any](arr []T1, filter func(v T1) bool, conv func(v T1) T2) []T2 { func ArrFilterMap[T1 any, T2 any](arr []T1, filter func(v T1) bool, conv func(v T1) T2) []T2 {
r := make([]T2, 0, len(arr)) r := make([]T2, 0, len(arr))
for _, v := range arr { for _, v := range arr {
@@ -275,6 +340,16 @@ func ArrFilterMap[T1 any, T2 any](arr []T1, filter func(v T1) bool, conv func(v
return r return r
} }
func ArrFilter[T any](arr []T, filter func(v T) bool) []T {
r := make([]T, 0, len(arr))
for _, v := range arr {
if filter(v) {
r = append(r, v)
}
}
return r
}
func ArrSum[T NumberConstraint](arr []T) T { func ArrSum[T NumberConstraint](arr []T) T {
var r T = 0 var r T = 0
for _, v := range arr { for _, v := range arr {
@@ -282,3 +357,43 @@ func ArrSum[T NumberConstraint](arr []T) T {
} }
return r return r
} }
func ArrFlatten[T1 any, T2 any](arr []T1, conv func(v T1) []T2) []T2 {
r := make([]T2, 0, len(arr))
for _, v1 := range arr {
r = append(r, conv(v1)...)
}
return r
}
func ArrFlattenDirect[T1 any](arr [][]T1) []T1 {
r := make([]T1, 0, len(arr))
for _, v1 := range arr {
r = append(r, v1...)
}
return r
}
func ArrCastToAny[T1 any](arr []T1) []any {
r := make([]any, len(arr))
for i, v := range arr {
r[i] = any(v)
}
return r
}
func ArrConcat[T any](arr ...[]T) []T {
c := 0
for _, v := range arr {
c += len(v)
}
r := make([]T, c)
i := 0
for _, av := range arr {
for _, v := range av {
r[i] = v
i++
}
}
return r
}

178
langext/base58.go Normal file
View File

@@ -0,0 +1,178 @@
package langext
import (
"bytes"
"errors"
"math/big"
)
// shamelessly stolen from https://github.com/btcsuite/
type B58Encoding struct {
bigRadix [11]*big.Int
bigRadix10 *big.Int
alphabet string
alphabetIdx0 byte
b58 [256]byte
}
var Base58DefaultEncoding = newBase58Encoding("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")
var Base58FlickrEncoding = newBase58Encoding("123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ")
var Base58RippleEncoding = newBase58Encoding("rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz")
var Base58BitcoinEncoding = newBase58Encoding("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")
func newBase58Encoding(alphabet string) *B58Encoding {
bigRadix10 := big.NewInt(58 * 58 * 58 * 58 * 58 * 58 * 58 * 58 * 58 * 58)
enc := &B58Encoding{
alphabet: alphabet,
alphabetIdx0: '1',
bigRadix: [...]*big.Int{
big.NewInt(0),
big.NewInt(58),
big.NewInt(58 * 58),
big.NewInt(58 * 58 * 58),
big.NewInt(58 * 58 * 58 * 58),
big.NewInt(58 * 58 * 58 * 58 * 58),
big.NewInt(58 * 58 * 58 * 58 * 58 * 58),
big.NewInt(58 * 58 * 58 * 58 * 58 * 58 * 58),
big.NewInt(58 * 58 * 58 * 58 * 58 * 58 * 58 * 58),
big.NewInt(58 * 58 * 58 * 58 * 58 * 58 * 58 * 58 * 58),
bigRadix10,
},
bigRadix10: bigRadix10,
}
b58 := make([]byte, 0, 256)
for i := byte(0); i < 32; i++ {
for j := byte(0); j < 8; j++ {
b := i*8 + j
idx := bytes.IndexByte([]byte(alphabet), b)
if idx == -1 {
b58 = append(b58, 255)
} else {
b58 = append(b58, byte(idx))
}
}
}
enc.b58 = *((*[256]byte)(b58))
return enc
}
func (enc *B58Encoding) EncodeString(src string) (string, error) {
v, err := enc.Encode([]byte(src))
if err != nil {
return "", err
}
return string(v), nil
}
func (enc *B58Encoding) Encode(src []byte) ([]byte, error) {
x := new(big.Int)
x.SetBytes(src)
// maximum length of output is log58(2^(8*len(b))) == len(b) * 8 / log(58)
maxlen := int(float64(len(src))*1.365658237309761) + 1
answer := make([]byte, 0, maxlen)
mod := new(big.Int)
for x.Sign() > 0 {
// Calculating with big.Int is slow for each iteration.
// x, mod = x / 58, x % 58
//
// Instead we can try to do as much calculations on int64.
// x, mod = x / 58^10, x % 58^10
//
// Which will give us mod, which is 10 digit base58 number.
// We'll loop that 10 times to convert to the answer.
x.DivMod(x, enc.bigRadix10, mod)
if x.Sign() == 0 {
// When x = 0, we need to ensure we don't add any extra zeros.
m := mod.Int64()
for m > 0 {
answer = append(answer, enc.alphabet[m%58])
m /= 58
}
} else {
m := mod.Int64()
for i := 0; i < 10; i++ {
answer = append(answer, enc.alphabet[m%58])
m /= 58
}
}
}
// leading zero bytes
for _, i := range src {
if i != 0 {
break
}
answer = append(answer, enc.alphabetIdx0)
}
// reverse
alen := len(answer)
for i := 0; i < alen/2; i++ {
answer[i], answer[alen-1-i] = answer[alen-1-i], answer[i]
}
return answer, nil
}
func (enc *B58Encoding) DecodeString(src string) (string, error) {
v, err := enc.Decode([]byte(src))
if err != nil {
return "", err
}
return string(v), nil
}
func (enc *B58Encoding) Decode(src []byte) ([]byte, error) {
answer := big.NewInt(0)
scratch := new(big.Int)
for t := src; len(t) > 0; {
n := len(t)
if n > 10 {
n = 10
}
total := uint64(0)
for _, v := range t[:n] {
if v > 255 {
return []byte{}, errors.New("invalid char in input")
}
tmp := enc.b58[v]
if tmp == 255 {
return []byte{}, errors.New("invalid char in input")
}
total = total*58 + uint64(tmp)
}
answer.Mul(answer, enc.bigRadix[n])
scratch.SetUint64(total)
answer.Add(answer, scratch)
t = t[n:]
}
tmpval := answer.Bytes()
var numZeros int
for numZeros = 0; numZeros < len(src); numZeros++ {
if src[numZeros] != enc.alphabetIdx0 {
break
}
}
flen := numZeros + len(tmpval)
val := make([]byte, flen)
copy(val[numZeros:], tmpval)
return val, nil
}

67
langext/base58_test.go Normal file
View File

@@ -0,0 +1,67 @@
package langext
import (
"testing"
)
func _encStr(t *testing.T, enc *B58Encoding, v string) string {
v, err := enc.EncodeString(v)
if err != nil {
t.Error(err)
}
return v
}
func _decStr(t *testing.T, enc *B58Encoding, v string) string {
v, err := enc.DecodeString(v)
if err != nil {
t.Error(err)
}
return v
}
func TestBase58DefaultEncoding(t *testing.T) {
tst.AssertEqual(t, _encStr(t, Base58DefaultEncoding, "Hello"), "9Ajdvzr")
tst.AssertEqual(t, _encStr(t, Base58DefaultEncoding, "If debugging is the process of removing software bugs, then programming must be the process of putting them in."), "48638SMcJuah5okqPx4kCVf5d8QAdgbdNf28g7ReY13prUENNbMyssjq5GjsrJHF5zeZfqs4uJMUJHr7VbrU4XBUZ2Fw9DVtqtn9N1eXucEWSEZahXV6w4ysGSWqGdpeYTJf1MdDzTg8vfcQViifJjZX")
}
func TestBase58DefaultDecoding(t *testing.T) {
tst.AssertEqual(t, _decStr(t, Base58DefaultEncoding, "9Ajdvzr"), "Hello")
tst.AssertEqual(t, _decStr(t, Base58DefaultEncoding, "48638SMcJuah5okqPx4kCVf5d8QAdgbdNf28g7ReY13prUENNbMyssjq5GjsrJHF5zeZfqs4uJMUJHr7VbrU4XBUZ2Fw9DVtqtn9N1eXucEWSEZahXV6w4ysGSWqGdpeYTJf1MdDzTg8vfcQViifJjZX"), "If debugging is the process of removing software bugs, then programming must be the process of putting them in.")
}
func TestBase58RippleEncoding(t *testing.T) {
tst.AssertEqual(t, _encStr(t, Base58RippleEncoding, "Hello"), "9wjdvzi")
tst.AssertEqual(t, _encStr(t, Base58RippleEncoding, "If debugging is the process of removing software bugs, then programming must be the process of putting them in."), "h3as3SMcJu26nokqPxhkUVCnd3Qwdgbd4Cp3gfReYrsFi7N44bMy11jqnGj1iJHEnzeZCq1huJM7JHifVbi7hXB7ZpEA9DVtqt894reXucNWSNZ26XVaAhy1GSWqGdFeYTJCrMdDzTg3vCcQV55CJjZX")
}
func TestBase58RippleDecoding(t *testing.T) {
tst.AssertEqual(t, _decStr(t, Base58RippleEncoding, "9wjdvzi"), "Hello")
tst.AssertEqual(t, _decStr(t, Base58RippleEncoding, "h3as3SMcJu26nokqPxhkUVCnd3Qwdgbd4Cp3gfReYrsFi7N44bMy11jqnGj1iJHEnzeZCq1huJM7JHifVbi7hXB7ZpEA9DVtqt894reXucNWSNZ26XVaAhy1GSWqGdFeYTJCrMdDzTg3vCcQV55CJjZX"), "If debugging is the process of removing software bugs, then programming must be the process of putting them in.")
}
func TestBase58BitcoinEncoding(t *testing.T) {
tst.AssertEqual(t, _encStr(t, Base58BitcoinEncoding, "Hello"), "9Ajdvzr")
tst.AssertEqual(t, _encStr(t, Base58BitcoinEncoding, "If debugging is the process of removing software bugs, then programming must be the process of putting them in."), "48638SMcJuah5okqPx4kCVf5d8QAdgbdNf28g7ReY13prUENNbMyssjq5GjsrJHF5zeZfqs4uJMUJHr7VbrU4XBUZ2Fw9DVtqtn9N1eXucEWSEZahXV6w4ysGSWqGdpeYTJf1MdDzTg8vfcQViifJjZX")
}
func TestBase58BitcoinDecoding(t *testing.T) {
tst.AssertEqual(t, _decStr(t, Base58BitcoinEncoding, "9Ajdvzr"), "Hello")
tst.AssertEqual(t, _decStr(t, Base58BitcoinEncoding, "48638SMcJuah5okqPx4kCVf5d8QAdgbdNf28g7ReY13prUENNbMyssjq5GjsrJHF5zeZfqs4uJMUJHr7VbrU4XBUZ2Fw9DVtqtn9N1eXucEWSEZahXV6w4ysGSWqGdpeYTJf1MdDzTg8vfcQViifJjZX"), "If debugging is the process of removing software bugs, then programming must be the process of putting them in.")
}
func TestBase58FlickrEncoding(t *testing.T) {
tst.AssertEqual(t, _encStr(t, Base58FlickrEncoding, "Hello"), "9aJCVZR")
tst.AssertEqual(t, _encStr(t, Base58FlickrEncoding, "If debugging is the process of removing software bugs, then programming must be the process of putting them in."), "48638rmBiUzG5NKQoX4KcuE5C8paCFACnE28F7qDx13PRtennAmYSSJQ5gJSRihf5ZDyEQS4UimtihR7uARt4wbty2fW9duTQTM9n1DwUBevreyzGwu6W4YSgrvQgCPDxsiE1mCdZsF8VEBpuHHEiJyw")
}
func TestBase58FlickrDecoding(t *testing.T) {
tst.AssertEqual(t, _decStr(t, Base58FlickrEncoding, "9aJCVZR"), "Hello")
tst.AssertEqual(t, _decStr(t, Base58FlickrEncoding, "48638rmBiUzG5NKQoX4KcuE5C8paCFACnE28F7qDx13PRtennAmYSSJQ5gJSRihf5ZDyEQS4UimtihR7uARt4wbty2fW9duTQTM9n1DwUBevreyzGwu6W4YSgrvQgCPDxsiE1mCdZsF8VEBpuHHEiJyw"), "If debugging is the process of removing software bugs, then programming must be the process of putting them in.")
}
func tst.AssertEqual(t *testing.T, actual string, expected string) {
if actual != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
}
}

View File

@@ -60,3 +60,12 @@ func CoalesceStringer(s fmt.Stringer, def string) string {
return s.String() return s.String()
} }
} }
func SafeCast[T any](v any, def T) T {
switch r := v.(type) {
case T:
return r
default:
return def
}
}

View File

@@ -1,6 +1,9 @@
package rext package rext
import "regexp" import (
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"regexp"
)
type Regex interface { type Regex interface {
IsMatch(haystack string) bool IsMatch(haystack string) bool
@@ -29,6 +32,10 @@ type RegexMatchGroup struct {
end int end int
} }
type OptRegexMatchGroup struct {
v *RegexMatchGroup
}
func W(rex *regexp.Regexp) Regex { func W(rex *regexp.Regexp) Regex {
return &regexWrapper{rex: rex, subnames: rex.SubexpNames()} return &regexWrapper{rex: rex, subnames: rex.SubexpNames()}
} }
@@ -97,7 +104,7 @@ func (m RegexMatch) GroupByIndex(idx int) RegexMatchGroup {
return RegexMatchGroup{haystack: m.haystack, start: m.submatchesIndex[idx*2], end: m.submatchesIndex[idx*2+1]} return RegexMatchGroup{haystack: m.haystack, start: m.submatchesIndex[idx*2], end: m.submatchesIndex[idx*2+1]}
} }
// GroupByName returns the value of a matched group (group 0 == whole match) // GroupByName returns the value of a matched group (panics if not found!)
func (m RegexMatch) GroupByName(name string) RegexMatchGroup { func (m RegexMatch) GroupByName(name string) RegexMatchGroup {
for idx, subname := range m.subnames { for idx, subname := range m.subnames {
if subname == name { if subname == name {
@@ -107,6 +114,16 @@ func (m RegexMatch) GroupByName(name string) RegexMatchGroup {
panic("failed to find regex-group by name") panic("failed to find regex-group by name")
} }
// GroupByName returns the value of a matched group (returns empty OptRegexMatchGroup if not found)
func (m RegexMatch) GroupByNameOrEmpty(name string) OptRegexMatchGroup {
for idx, subname := range m.subnames {
if subname == name && (m.submatchesIndex[idx*2] != -1 || m.submatchesIndex[idx*2+1] != -1) {
return OptRegexMatchGroup{&RegexMatchGroup{haystack: m.haystack, start: m.submatchesIndex[idx*2], end: m.submatchesIndex[idx*2+1]}}
}
}
return OptRegexMatchGroup{}
}
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
func (g RegexMatchGroup) Value() string { func (g RegexMatchGroup) Value() string {
@@ -128,3 +145,47 @@ func (g RegexMatchGroup) Range() (int, int) {
func (g RegexMatchGroup) Length() int { func (g RegexMatchGroup) Length() int {
return g.end - g.start return g.end - g.start
} }
// ---------------------------------------------------------------------------------------------------------------------
func (g OptRegexMatchGroup) Value() string {
return g.v.Value()
}
func (g OptRegexMatchGroup) ValueOrEmpty() string {
if g.v == nil {
return ""
}
return g.v.Value()
}
func (g OptRegexMatchGroup) ValueOrNil() *string {
if g.v == nil {
return nil
}
return langext.Ptr(g.v.Value())
}
func (g OptRegexMatchGroup) IsEmpty() bool {
return g.v == nil
}
func (g OptRegexMatchGroup) Exists() bool {
return g.v != nil
}
func (g OptRegexMatchGroup) Start() int {
return g.v.Start()
}
func (g OptRegexMatchGroup) End() int {
return g.v.End()
}
func (g OptRegexMatchGroup) Range() (int, int) {
return g.v.Range()
}
func (g OptRegexMatchGroup) Length() int {
return g.v.Length()
}

47
rext/wrapper_test.go Normal file
View File

@@ -0,0 +1,47 @@
package rext
import (
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"regexp"
"testing"
)
func TestGroupByNameOrEmpty1(t *testing.T) {
regex1 := W(regexp.MustCompile("0(?P<group1>A+)B(?P<group2>C+)0"))
match1, ok1 := regex1.MatchFirst("Hello 0AAAABCCC0 Bye.")
tst.AssertTrue(t, ok1)
tst.AssertFalse(t, match1.GroupByNameOrEmpty("group1").IsEmpty())
tst.AssertEqual(t, match1.GroupByNameOrEmpty("group1").ValueOrEmpty(), "AAAA")
tst.AssertEqual(t, *match1.GroupByNameOrEmpty("group1").ValueOrNil(), "AAAA")
tst.AssertFalse(t, match1.GroupByNameOrEmpty("group2").IsEmpty())
tst.AssertEqual(t, match1.GroupByNameOrEmpty("group2").ValueOrEmpty(), "CCC")
tst.AssertEqual(t, *match1.GroupByNameOrEmpty("group2").ValueOrNil(), "CCC")
}
func TestGroupByNameOrEmpty2(t *testing.T) {
regex1 := W(regexp.MustCompile("0(?P<group1>A+)B(?P<group2>C+)(?P<group3>C+)?0"))
match1, ok1 := regex1.MatchFirst("Hello 0AAAABCCC0 Bye.")
tst.AssertTrue(t, ok1)
tst.AssertFalse(t, match1.GroupByNameOrEmpty("group1").IsEmpty())
tst.AssertEqual(t, match1.GroupByNameOrEmpty("group1").ValueOrEmpty(), "AAAA")
tst.AssertEqual(t, *match1.GroupByNameOrEmpty("group1").ValueOrNil(), "AAAA")
tst.AssertFalse(t, match1.GroupByNameOrEmpty("group2").IsEmpty())
tst.AssertEqual(t, match1.GroupByNameOrEmpty("group2").ValueOrEmpty(), "CCC")
tst.AssertEqual(t, *match1.GroupByNameOrEmpty("group2").ValueOrNil(), "CCC")
tst.AssertTrue(t, match1.GroupByNameOrEmpty("group3").IsEmpty())
tst.AssertEqual(t, match1.GroupByNameOrEmpty("group3").ValueOrEmpty(), "")
tst.AssertPtrEqual(t, match1.GroupByNameOrEmpty("group3").ValueOrNil(), nil)
}

View File

@@ -3,6 +3,52 @@ package rfctime
import "time" import "time"
type RFCTime interface { type RFCTime interface {
AnyTime
Time() time.Time
Serialize() string
After(u AnyTime) bool
Before(u AnyTime) bool
Equal(u AnyTime) bool
Sub(u AnyTime) time.Duration
}
type AnyTime interface {
MarshalJSON() ([]byte, error)
MarshalBinary() ([]byte, error)
GobEncode() ([]byte, error)
MarshalText() ([]byte, error)
IsZero() bool
Date() (year int, month time.Month, day int)
Year() int
Month() time.Month
Day() int
Weekday() time.Weekday
ISOWeek() (year, week int)
Clock() (hour, min, sec int)
Hour() int
Minute() int
Second() int
Nanosecond() int
YearDay() int
Unix() int64
UnixMilli() int64
UnixMicro() int64
UnixNano() int64
Format(layout string) string
GoString() string
String() string
Location() *time.Location
}
type RFCDuration interface {
Time() time.Time Time() time.Time
Serialize() string Serialize() string
@@ -18,9 +64,9 @@ type RFCTime interface {
MarshalText() ([]byte, error) MarshalText() ([]byte, error)
UnmarshalText(data []byte) error UnmarshalText(data []byte) error
After(u RFCTime) bool After(u AnyTime) bool
Before(u RFCTime) bool Before(u AnyTime) bool
Equal(u RFCTime) bool Equal(u AnyTime) bool
IsZero() bool IsZero() bool
Date() (year int, month time.Month, day int) Date() (year int, month time.Month, day int)
Year() int Year() int
@@ -34,7 +80,7 @@ type RFCTime interface {
Second() int Second() int
Nanosecond() int Nanosecond() int
YearDay() int YearDay() int
Sub(u RFCTime) time.Duration Sub(u AnyTime) time.Duration
Unix() int64 Unix() int64
UnixMilli() int64 UnixMilli() int64
UnixMicro() int64 UnixMicro() int64
@@ -43,3 +89,13 @@ type RFCTime interface {
GoString() string GoString() string
String() string String() string
} }
func tt(v AnyTime) time.Time {
if r, ok := v.(time.Time); ok {
return r
}
if r, ok := v.(RFCTime); ok {
return r.Time()
}
return time.Unix(0, v.UnixNano()).In(v.Location())
}

50
rfctime/interface_test.go Normal file
View File

@@ -0,0 +1,50 @@
package rfctime
import (
"testing"
"time"
)
func TestAnyTimeInterface(t *testing.T) {
var v AnyTime
v = NowRFC3339Nano()
tst.AssertEqual(t, v.String(), v.String())
v = NowRFC3339()
tst.AssertEqual(t, v.String(), v.String())
v = NowUnix()
tst.AssertEqual(t, v.String(), v.String())
v = NowUnixMilli()
tst.AssertEqual(t, v.String(), v.String())
v = NowUnixNano()
tst.AssertEqual(t, v.String(), v.String())
v = time.Now()
tst.AssertEqual(t, v.String(), v.String())
}
func TestRFCTimeInterface(t *testing.T) {
var v RFCTime
v = NowRFC3339Nano()
tst.AssertEqual(t, v.String(), v.String())
v = NowRFC3339()
tst.AssertEqual(t, v.String(), v.String())
v = NowUnix()
tst.AssertEqual(t, v.String(), v.String())
v = NowUnixMilli()
tst.AssertEqual(t, v.String(), v.String())
v = NowUnixNano()
tst.AssertEqual(t, v.String(), v.String())
}

View File

@@ -2,6 +2,10 @@ package rfctime
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsontype"
"time" "time"
) )
@@ -61,6 +65,27 @@ func (t *RFC3339Time) UnmarshalText(data []byte) error {
return nil return nil
} }
func (t *RFC3339Time) UnmarshalBSONValue(bt bsontype.Type, data []byte) error {
if bt == bsontype.Null {
*t = RFC3339Time{}
return nil
}
if bt != bsontype.DateTime {
return errors.New(fmt.Sprintf("cannot unmarshal %v into RFC3339Time", bt))
}
var tt time.Time
err := bson.Unmarshal(data, &tt)
if err != nil {
return err
}
*t = RFC3339Time(tt)
return nil
}
func (t RFC3339Time) MarshalBSONValue() (bsontype.Type, []byte, error) {
return bson.MarshalValue(time.Time(t))
}
func (t RFC3339Time) Serialize() string { func (t RFC3339Time) Serialize() string {
return t.Time().Format(t.FormatStr()) return t.Time().Format(t.FormatStr())
} }
@@ -69,16 +94,16 @@ func (t RFC3339Time) FormatStr() string {
return time.RFC3339 return time.RFC3339
} }
func (t RFC3339Time) After(u RFCTime) bool { func (t RFC3339Time) After(u AnyTime) bool {
return t.Time().After(u.Time()) return t.Time().After(tt(u))
} }
func (t RFC3339Time) Before(u RFCTime) bool { func (t RFC3339Time) Before(u AnyTime) bool {
return t.Time().Before(u.Time()) return t.Time().Before(tt(u))
} }
func (t RFC3339Time) Equal(u RFCTime) bool { func (t RFC3339Time) Equal(u AnyTime) bool {
return t.Time().Equal(u.Time()) return t.Time().Equal(tt(u))
} }
func (t RFC3339Time) IsZero() bool { func (t RFC3339Time) IsZero() bool {
@@ -137,8 +162,8 @@ func (t RFC3339Time) Add(d time.Duration) RFC3339Time {
return RFC3339Time(t.Time().Add(d)) return RFC3339Time(t.Time().Add(d))
} }
func (t RFC3339Time) Sub(u RFCTime) time.Duration { func (t RFC3339Time) Sub(u AnyTime) time.Duration {
return t.Time().Sub(u.Time()) return t.Time().Sub(tt(u))
} }
func (t RFC3339Time) AddDate(years int, months int, days int) RFC3339Time { func (t RFC3339Time) AddDate(years int, months int, days int) RFC3339Time {
@@ -173,6 +198,10 @@ func (t RFC3339Time) String() string {
return t.Time().String() return t.Time().String()
} }
func (t RFC3339Time) Location() *time.Location {
return t.Time().Location()
}
func NewRFC3339(t time.Time) RFC3339Time { func NewRFC3339(t time.Time) RFC3339Time {
return RFC3339Time(t) return RFC3339Time(t)
} }

View File

@@ -2,6 +2,10 @@ package rfctime
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsontype"
"time" "time"
) )
@@ -61,6 +65,27 @@ func (t *RFC3339NanoTime) UnmarshalText(data []byte) error {
return nil return nil
} }
func (t *RFC3339NanoTime) UnmarshalBSONValue(bt bsontype.Type, data []byte) error {
if bt == bsontype.Null {
*t = RFC3339NanoTime{}
return nil
}
if bt != bsontype.DateTime {
return errors.New(fmt.Sprintf("cannot unmarshal %v into RFC3339NanoTime", bt))
}
var tt time.Time
err := bson.RawValue{Type: bt, Value: data}.Unmarshal(&tt)
if err != nil {
return err
}
*t = RFC3339NanoTime(tt)
return nil
}
func (t RFC3339NanoTime) MarshalBSONValue() (bsontype.Type, []byte, error) {
return bson.MarshalValue(time.Time(t))
}
func (t RFC3339NanoTime) Serialize() string { func (t RFC3339NanoTime) Serialize() string {
return t.Time().Format(t.FormatStr()) return t.Time().Format(t.FormatStr())
} }
@@ -69,16 +94,16 @@ func (t RFC3339NanoTime) FormatStr() string {
return time.RFC3339Nano return time.RFC3339Nano
} }
func (t RFC3339NanoTime) After(u RFCTime) bool { func (t RFC3339NanoTime) After(u AnyTime) bool {
return t.Time().After(u.Time()) return t.Time().After(tt(u))
} }
func (t RFC3339NanoTime) Before(u RFCTime) bool { func (t RFC3339NanoTime) Before(u AnyTime) bool {
return t.Time().Before(u.Time()) return t.Time().Before(tt(u))
} }
func (t RFC3339NanoTime) Equal(u RFCTime) bool { func (t RFC3339NanoTime) Equal(u AnyTime) bool {
return t.Time().Equal(u.Time()) return t.Time().Equal(tt(u))
} }
func (t RFC3339NanoTime) IsZero() bool { func (t RFC3339NanoTime) IsZero() bool {
@@ -137,8 +162,8 @@ func (t RFC3339NanoTime) Add(d time.Duration) RFC3339NanoTime {
return RFC3339NanoTime(t.Time().Add(d)) return RFC3339NanoTime(t.Time().Add(d))
} }
func (t RFC3339NanoTime) Sub(u RFCTime) time.Duration { func (t RFC3339NanoTime) Sub(u AnyTime) time.Duration {
return t.Time().Sub(u.Time()) return t.Time().Sub(tt(u))
} }
func (t RFC3339NanoTime) AddDate(years int, months int, days int) RFC3339NanoTime { func (t RFC3339NanoTime) AddDate(years int, months int, days int) RFC3339NanoTime {
@@ -173,6 +198,10 @@ func (t RFC3339NanoTime) String() string {
return t.Time().String() return t.Time().String()
} }
func (t RFC3339NanoTime) Location() *time.Location {
return t.Time().Location()
}
func NewRFC3339Nano(t time.Time) RFC3339NanoTime { func NewRFC3339Nano(t time.Time) RFC3339NanoTime {
return RFC3339NanoTime(t) return RFC3339NanoTime(t)
} }

View File

@@ -2,6 +2,7 @@ package rfctime
import ( import (
"encoding/json" "encoding/json"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"testing" "testing"
"time" "time"
) )
@@ -12,7 +13,7 @@ func TestRoundtrip(t *testing.T) {
Value RFC3339NanoTime `json:"v"` Value RFC3339NanoTime `json:"v"`
} }
val1 := NewRFC3339Nano(time.Now()) val1 := NewRFC3339Nano(time.Unix(0, 1675951556820915171))
w1 := Wrap{val1} w1 := Wrap{val1}
jstr1, err := json.Marshal(w1) jstr1, err := json.Marshal(w1)
@@ -20,7 +21,8 @@ func TestRoundtrip(t *testing.T) {
panic(err) panic(err)
} }
if string(jstr1) != "{\"v\":\"2023-01-29T20:32:36.149692117+01:00\"}" { if string(jstr1) != "{\"v\":\"2023-02-09T15:05:56.820915171+01:00\"}" {
t.Errorf(string(jstr1))
t.Errorf("repr differs") t.Errorf("repr differs")
} }
@@ -36,16 +38,10 @@ func TestRoundtrip(t *testing.T) {
panic(err) panic(err)
} }
assertEqual(t, string(jstr1), string(jstr2)) tst.AssertEqual(t, string(jstr1), string(jstr2))
if !w1.Value.Equal(&w2.Value) { if !w1.Value.Equal(&w2.Value) {
t.Errorf("time differs") t.Errorf("time differs")
} }
} }
func assertEqual(t *testing.T, actual string, expected string) {
if actual != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
}
}

59
rfctime/seconds.go Normal file
View File

@@ -0,0 +1,59 @@
package rfctime
import (
"encoding/json"
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"time"
)
type SecondsF64 time.Duration
func (d SecondsF64) Duration() time.Duration {
return time.Duration(d)
}
func (d SecondsF64) String() string {
return d.Duration().String()
}
func (d SecondsF64) Nanoseconds() int64 {
return d.Duration().Nanoseconds()
}
func (d SecondsF64) Microseconds() int64 {
return d.Duration().Microseconds()
}
func (d SecondsF64) Milliseconds() int64 {
return d.Duration().Milliseconds()
}
func (d SecondsF64) Seconds() float64 {
return d.Duration().Seconds()
}
func (d SecondsF64) Minutes() float64 {
return d.Duration().Minutes()
}
func (d SecondsF64) Hours() float64 {
return d.Duration().Hours()
}
func (d *SecondsF64) UnmarshalJSON(data []byte) error {
var secs float64 = 0
if err := json.Unmarshal(data, &secs); err != nil {
return err
}
*d = SecondsF64(timeext.FromSeconds(secs))
return nil
}
func (d SecondsF64) MarshalJSON() ([]byte, error) {
secs := d.Seconds()
return json.Marshal(secs)
}
func NewSecondsF64(t time.Duration) SecondsF64 {
return SecondsF64(t)
}

View File

@@ -63,16 +63,16 @@ func (t UnixTime) Serialize() string {
return strconv.FormatInt(t.Time().Unix(), 10) return strconv.FormatInt(t.Time().Unix(), 10)
} }
func (t UnixTime) After(u RFCTime) bool { func (t UnixTime) After(u AnyTime) bool {
return t.Time().After(u.Time()) return t.Time().After(tt(u))
} }
func (t UnixTime) Before(u RFCTime) bool { func (t UnixTime) Before(u AnyTime) bool {
return t.Time().Before(u.Time()) return t.Time().Before(tt(u))
} }
func (t UnixTime) Equal(u RFCTime) bool { func (t UnixTime) Equal(u AnyTime) bool {
return t.Time().Equal(u.Time()) return t.Time().Equal(tt(u))
} }
func (t UnixTime) IsZero() bool { func (t UnixTime) IsZero() bool {
@@ -131,8 +131,8 @@ func (t UnixTime) Add(d time.Duration) UnixTime {
return UnixTime(t.Time().Add(d)) return UnixTime(t.Time().Add(d))
} }
func (t UnixTime) Sub(u RFCTime) time.Duration { func (t UnixTime) Sub(u AnyTime) time.Duration {
return t.Time().Sub(u.Time()) return t.Time().Sub(tt(u))
} }
func (t UnixTime) AddDate(years int, months int, days int) UnixTime { func (t UnixTime) AddDate(years int, months int, days int) UnixTime {
@@ -167,6 +167,10 @@ func (t UnixTime) String() string {
return t.Time().String() return t.Time().String()
} }
func (t UnixTime) Location() *time.Location {
return t.Time().Location()
}
func NewUnix(t time.Time) UnixTime { func NewUnix(t time.Time) UnixTime {
return UnixTime(t) return UnixTime(t)
} }

View File

@@ -63,16 +63,16 @@ func (t UnixMilliTime) Serialize() string {
return strconv.FormatInt(t.Time().UnixMilli(), 10) return strconv.FormatInt(t.Time().UnixMilli(), 10)
} }
func (t UnixMilliTime) After(u RFCTime) bool { func (t UnixMilliTime) After(u AnyTime) bool {
return t.Time().After(u.Time()) return t.Time().After(tt(u))
} }
func (t UnixMilliTime) Before(u RFCTime) bool { func (t UnixMilliTime) Before(u AnyTime) bool {
return t.Time().Before(u.Time()) return t.Time().Before(tt(u))
} }
func (t UnixMilliTime) Equal(u RFCTime) bool { func (t UnixMilliTime) Equal(u AnyTime) bool {
return t.Time().Equal(u.Time()) return t.Time().Equal(tt(u))
} }
func (t UnixMilliTime) IsZero() bool { func (t UnixMilliTime) IsZero() bool {
@@ -131,8 +131,8 @@ func (t UnixMilliTime) Add(d time.Duration) UnixMilliTime {
return UnixMilliTime(t.Time().Add(d)) return UnixMilliTime(t.Time().Add(d))
} }
func (t UnixMilliTime) Sub(u RFCTime) time.Duration { func (t UnixMilliTime) Sub(u AnyTime) time.Duration {
return t.Time().Sub(u.Time()) return t.Time().Sub(tt(u))
} }
func (t UnixMilliTime) AddDate(years int, months int, days int) UnixMilliTime { func (t UnixMilliTime) AddDate(years int, months int, days int) UnixMilliTime {
@@ -167,6 +167,10 @@ func (t UnixMilliTime) String() string {
return t.Time().String() return t.Time().String()
} }
func (t UnixMilliTime) Location() *time.Location {
return t.Time().Location()
}
func NewUnixMilli(t time.Time) UnixMilliTime { func NewUnixMilli(t time.Time) UnixMilliTime {
return UnixMilliTime(t) return UnixMilliTime(t)
} }

View File

@@ -63,16 +63,16 @@ func (t UnixNanoTime) Serialize() string {
return strconv.FormatInt(t.Time().UnixNano(), 10) return strconv.FormatInt(t.Time().UnixNano(), 10)
} }
func (t UnixNanoTime) After(u RFCTime) bool { func (t UnixNanoTime) After(u AnyTime) bool {
return t.Time().After(u.Time()) return t.Time().After(tt(u))
} }
func (t UnixNanoTime) Before(u RFCTime) bool { func (t UnixNanoTime) Before(u AnyTime) bool {
return t.Time().Before(u.Time()) return t.Time().Before(tt(u))
} }
func (t UnixNanoTime) Equal(u RFCTime) bool { func (t UnixNanoTime) Equal(u AnyTime) bool {
return t.Time().Equal(u.Time()) return t.Time().Equal(tt(u))
} }
func (t UnixNanoTime) IsZero() bool { func (t UnixNanoTime) IsZero() bool {
@@ -131,8 +131,8 @@ func (t UnixNanoTime) Add(d time.Duration) UnixNanoTime {
return UnixNanoTime(t.Time().Add(d)) return UnixNanoTime(t.Time().Add(d))
} }
func (t UnixNanoTime) Sub(u RFCTime) time.Duration { func (t UnixNanoTime) Sub(u AnyTime) time.Duration {
return t.Time().Sub(u.Time()) return t.Time().Sub(tt(u))
} }
func (t UnixNanoTime) AddDate(years int, months int, days int) UnixNanoTime { func (t UnixNanoTime) AddDate(years int, months int, days int) UnixNanoTime {
@@ -167,6 +167,10 @@ func (t UnixNanoTime) String() string {
return t.Time().String() return t.Time().String()
} }
func (t UnixNanoTime) Location() *time.Location {
return t.Time().Location()
}
func NewUnixNano(t time.Time) UnixNanoTime { func NewUnixNano(t time.Time) UnixNanoTime {
return UnixNanoTime(t) return UnixNanoTime(t)
} }

View File

@@ -14,26 +14,26 @@ func TestSupportsColors(t *testing.T) {
} }
func TestColor(t *testing.T) { func TestColor(t *testing.T) {
assertEqual(t, Red("test"), "\033[31mtest\u001B[0m") tst.AssertEqual(t, Red("test"), "\033[31mtest\u001B[0m")
assertEqual(t, Green("test"), "\033[32mtest\u001B[0m") tst.AssertEqual(t, Green("test"), "\033[32mtest\u001B[0m")
assertEqual(t, Yellow("test"), "\033[33mtest\u001B[0m") tst.AssertEqual(t, Yellow("test"), "\033[33mtest\u001B[0m")
assertEqual(t, Blue("test"), "\033[34mtest\u001B[0m") tst.AssertEqual(t, Blue("test"), "\033[34mtest\u001B[0m")
assertEqual(t, Purple("test"), "\033[35mtest\u001B[0m") tst.AssertEqual(t, Purple("test"), "\033[35mtest\u001B[0m")
assertEqual(t, Cyan("test"), "\033[36mtest\u001B[0m") tst.AssertEqual(t, Cyan("test"), "\033[36mtest\u001B[0m")
assertEqual(t, Gray("test"), "\033[37mtest\u001B[0m") tst.AssertEqual(t, Gray("test"), "\033[37mtest\u001B[0m")
assertEqual(t, White("test"), "\033[97mtest\u001B[0m") tst.AssertEqual(t, White("test"), "\033[97mtest\u001B[0m")
assertEqual(t, CleanString(Red("test")), "test") tst.AssertEqual(t, CleanString(Red("test")), "test")
assertEqual(t, CleanString(Green("test")), "test") tst.AssertEqual(t, CleanString(Green("test")), "test")
assertEqual(t, CleanString(Yellow("test")), "test") tst.AssertEqual(t, CleanString(Yellow("test")), "test")
assertEqual(t, CleanString(Blue("test")), "test") tst.AssertEqual(t, CleanString(Blue("test")), "test")
assertEqual(t, CleanString(Purple("test")), "test") tst.AssertEqual(t, CleanString(Purple("test")), "test")
assertEqual(t, CleanString(Cyan("test")), "test") tst.AssertEqual(t, CleanString(Cyan("test")), "test")
assertEqual(t, CleanString(Gray("test")), "test") tst.AssertEqual(t, CleanString(Gray("test")), "test")
assertEqual(t, CleanString(White("test")), "test") tst.AssertEqual(t, CleanString(White("test")), "test")
} }
func assertEqual(t *testing.T, actual string, expected string) { func tst.AssertEqual(t *testing.T, actual string, expected string) {
if actual != expected { if actual != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected) t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
} }

View File

@@ -7,56 +7,56 @@ import (
func TestParseDurationShortString(t *testing.T) { func TestParseDurationShortString(t *testing.T) {
assertPDSSEqual(t, FromSeconds(1), "1s") tst.AssertPDSSEqual(t, FromSeconds(1), "1s")
assertPDSSEqual(t, FromSeconds(1), "1sec") tst.AssertPDSSEqual(t, FromSeconds(1), "1sec")
assertPDSSEqual(t, FromSeconds(1), "1second") tst.AssertPDSSEqual(t, FromSeconds(1), "1second")
assertPDSSEqual(t, FromSeconds(1), "1seconds") tst.AssertPDSSEqual(t, FromSeconds(1), "1seconds")
assertPDSSEqual(t, FromSeconds(100), "100second") tst.AssertPDSSEqual(t, FromSeconds(100), "100second")
assertPDSSEqual(t, FromSeconds(100), "100seconds") tst.AssertPDSSEqual(t, FromSeconds(100), "100seconds")
assertPDSSEqual(t, FromSeconds(1883639.77), "1883639.77second") tst.AssertPDSSEqual(t, FromSeconds(1883639.77), "1883639.77second")
assertPDSSEqual(t, FromSeconds(1883639.77), "1883639.77seconds") tst.AssertPDSSEqual(t, FromSeconds(1883639.77), "1883639.77seconds")
assertPDSSEqual(t, FromSeconds(50), "50s") tst.AssertPDSSEqual(t, FromSeconds(50), "50s")
assertPDSSEqual(t, FromSeconds(50), "50sec") tst.AssertPDSSEqual(t, FromSeconds(50), "50sec")
assertPDSSEqual(t, FromSeconds(1), "1second") tst.AssertPDSSEqual(t, FromSeconds(1), "1second")
assertPDSSEqual(t, FromSeconds(50), "50seconds") tst.AssertPDSSEqual(t, FromSeconds(50), "50seconds")
assertPDSSEqual(t, FromMinutes(10), "10m") tst.AssertPDSSEqual(t, FromMinutes(10), "10m")
assertPDSSEqual(t, FromMinutes(10), "10min") tst.AssertPDSSEqual(t, FromMinutes(10), "10min")
assertPDSSEqual(t, FromMinutes(1), "1minute") tst.AssertPDSSEqual(t, FromMinutes(1), "1minute")
assertPDSSEqual(t, FromMinutes(10), "10minutes") tst.AssertPDSSEqual(t, FromMinutes(10), "10minutes")
assertPDSSEqual(t, FromMinutes(10.5), "10.5minutes") tst.AssertPDSSEqual(t, FromMinutes(10.5), "10.5minutes")
assertPDSSEqual(t, FromMilliseconds(100), "100ms") tst.AssertPDSSEqual(t, FromMilliseconds(100), "100ms")
assertPDSSEqual(t, FromMilliseconds(100), "100milliseconds") tst.AssertPDSSEqual(t, FromMilliseconds(100), "100milliseconds")
assertPDSSEqual(t, FromMilliseconds(100), "100millisecond") tst.AssertPDSSEqual(t, FromMilliseconds(100), "100millisecond")
assertPDSSEqual(t, FromNanoseconds(99235), "99235ns") tst.AssertPDSSEqual(t, FromNanoseconds(99235), "99235ns")
assertPDSSEqual(t, FromNanoseconds(99235), "99235nanoseconds") tst.AssertPDSSEqual(t, FromNanoseconds(99235), "99235nanoseconds")
assertPDSSEqual(t, FromNanoseconds(99235), "99235nanosecond") tst.AssertPDSSEqual(t, FromNanoseconds(99235), "99235nanosecond")
assertPDSSEqual(t, FromMicroseconds(99235), "99235us") tst.AssertPDSSEqual(t, FromMicroseconds(99235), "99235us")
assertPDSSEqual(t, FromMicroseconds(99235), "99235microseconds") tst.AssertPDSSEqual(t, FromMicroseconds(99235), "99235microseconds")
assertPDSSEqual(t, FromMicroseconds(99235), "99235microsecond") tst.AssertPDSSEqual(t, FromMicroseconds(99235), "99235microsecond")
assertPDSSEqual(t, FromHours(1), "1h") tst.AssertPDSSEqual(t, FromHours(1), "1h")
assertPDSSEqual(t, FromHours(1), "1hour") tst.AssertPDSSEqual(t, FromHours(1), "1hour")
assertPDSSEqual(t, FromHours(2), "2hours") tst.AssertPDSSEqual(t, FromHours(2), "2hours")
assertPDSSEqual(t, FromDays(1), "1d") tst.AssertPDSSEqual(t, FromDays(1), "1d")
assertPDSSEqual(t, FromDays(1), "1day") tst.AssertPDSSEqual(t, FromDays(1), "1day")
assertPDSSEqual(t, FromDays(10), "10days") tst.AssertPDSSEqual(t, FromDays(10), "10days")
assertPDSSEqual(t, FromDays(1), "1days") tst.AssertPDSSEqual(t, FromDays(1), "1days")
assertPDSSEqual(t, FromDays(10), "10day") tst.AssertPDSSEqual(t, FromDays(10), "10day")
assertPDSSEqual(t, FromDays(1)+FromMinutes(10), "1d10m") tst.AssertPDSSEqual(t, FromDays(1)+FromMinutes(10), "1d10m")
assertPDSSEqual(t, FromDays(1)+FromMinutes(10)+FromSeconds(200), "1d10m200sec") tst.AssertPDSSEqual(t, FromDays(1)+FromMinutes(10)+FromSeconds(200), "1d10m200sec")
assertPDSSEqual(t, FromDays(1)+FromMinutes(10), "1d:10m") tst.AssertPDSSEqual(t, FromDays(1)+FromMinutes(10), "1d:10m")
assertPDSSEqual(t, FromDays(1)+FromMinutes(10), "1d 10m") tst.AssertPDSSEqual(t, FromDays(1)+FromMinutes(10), "1d 10m")
assertPDSSEqual(t, FromDays(1)+FromMinutes(10), "1d,10m") tst.AssertPDSSEqual(t, FromDays(1)+FromMinutes(10), "1d,10m")
assertPDSSEqual(t, FromDays(1)+FromMinutes(10), "1d, 10m") tst.AssertPDSSEqual(t, FromDays(1)+FromMinutes(10), "1d, 10m")
assertPDSSEqual(t, FromDays(1)+FromSeconds(1000), "1d 1000seconds") tst.AssertPDSSEqual(t, FromDays(1)+FromSeconds(1000), "1d 1000seconds")
assertPDSSEqual(t, FromDays(1), "86400s") tst.AssertPDSSEqual(t, FromDays(1), "86400s")
} }
func assertPDSSEqual(t *testing.T, expected time.Duration, fmt string) { func assertPDSSEqual(t *testing.T, expected time.Duration, fmt string) {

65
tst/assertions.go Normal file
View File

@@ -0,0 +1,65 @@
package tst
import (
"encoding/hex"
"testing"
)
func AssertEqual[T comparable](t *testing.T, actual T, expected T) {
if actual != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
}
}
func AssertNotEqual[T comparable](t *testing.T, actual T, expected T) {
if actual == expected {
t.Errorf("values do not differ: Actual: '%v', Expected: '%v'", actual, expected)
}
}
func AssertDeRefEqual[T comparable](t *testing.T, actual *T, expected T) {
if actual == nil {
t.Errorf("values differ: Actual: NIL, Expected: '%v'", expected)
}
if *actual != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
}
}
func AssertPtrEqual[T comparable](t *testing.T, actual *T, expected *T) {
if actual == nil && expected == nil {
return
}
if actual != nil && expected != nil {
if *actual != *expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", *actual, *expected)
} else {
return
}
}
if actual == nil && expected != nil {
t.Errorf("values differ: Actual: nil, Expected: not-nil")
}
if actual != nil && expected == nil {
t.Errorf("values differ: Actual: not-nil, Expected: nil")
}
}
func AssertHexEqual(t *testing.T, expected string, actual []byte) {
actualStr := hex.EncodeToString(actual)
if actualStr != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actualStr, expected)
}
}
func AssertTrue(t *testing.T, value bool) {
if !value {
t.Error("value should be true")
}
}
func AssertFalse(t *testing.T, value bool) {
if value {
t.Error("value should be false")
}
}

48
tst/identAssertions.go Normal file
View File

@@ -0,0 +1,48 @@
package tst
import (
"testing"
)
func AssertIdentEqual[T comparable](t *testing.T, ident string, actual T, expected T) {
if actual != expected {
t.Errorf("[%s] values differ: Actual: '%v', Expected: '%v'", ident, actual, expected)
}
}
func AssertIdentNotEqual[T comparable](t *testing.T, ident string, actual T, expected T) {
if actual == expected {
t.Errorf("[%s] values do not differ: Actual: '%v', Expected: '%v'", ident, actual, expected)
}
}
func AssertIdentPtrEqual[T comparable](t *testing.T, ident string, actual *T, expected *T) {
if actual == nil && expected == nil {
return
}
if actual != nil && expected != nil {
if *actual != *expected {
t.Errorf("[%s] values differ: Actual: '%v', Expected: '%v'", ident, *actual, *expected)
} else {
return
}
}
if actual == nil && expected != nil {
t.Errorf("[%s] values differ: Actual: nil, Expected: not-nil", ident)
}
if actual != nil && expected == nil {
t.Errorf("[%s] values differ: Actual: not-nil, Expected: nil", ident)
}
}
func AssertIdentTrue(t *testing.T, ident string, value bool) {
if !value {
t.Errorf("[%s] value should be true", ident)
}
}
func AssertIdentFalse(t *testing.T, ident string, value bool) {
if !value {
t.Errorf("[%s] value should be false", ident)
}
}