Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47b694c477 | ||
|
|
f72b510b65 | ||
|
|
2ef9199c9a | ||
|
|
77bddef812 | ||
|
|
e34866abe2 | ||
|
|
59643b6a0a | ||
|
|
57b2c56b3f | ||
|
|
9bb8eba492 | ||
|
|
6c8e735d04 | ||
|
|
3afe728650 | ||
|
|
b661d42074 | ||
|
|
97b337db31 | ||
|
|
41bdd6685a | ||
|
|
14778f40f6 | ||
|
|
9baa9ec5a2 | ||
|
|
11e3eb1de0 |
84
Makefile
84
Makefile
@@ -2,19 +2,93 @@
|
|||||||
|
|
||||||
GOOPTS=CGO_ENABLED=0
|
GOOPTS=CGO_ENABLED=0
|
||||||
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath -buildvcs=false
|
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath -buildvcs=false
|
||||||
BINARY=aleesa-misc-go
|
MYNAME=aleesa-misc-go
|
||||||
|
BINARY=${MYNAME}
|
||||||
|
UNIX_BINARY=${MYNAME}
|
||||||
|
WINDOWS_BINARY=${MYNAME}.exe
|
||||||
|
RMCMD=rm -rf
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@echo "aleesa-misc-go build system"
|
||||||
|
@echo ""
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " all Clean and build (default)"
|
||||||
|
@echo " build Build binary only"
|
||||||
|
@echo " clean Remove built artifacts"
|
||||||
|
@echo " test Run unit tests"
|
||||||
|
@echo " upgrade Update dependencies (get, tidy, vendor)"
|
||||||
|
@echo ""
|
||||||
|
|
||||||
|
# На windows имя бинарника может зависеть не только от платформы, но и от выбранной цели, для linux-а суффикс .exe
|
||||||
|
# не нужен
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
ifdef GOOS
|
||||||
|
ifeq ($(GOOS),windows)
|
||||||
|
BINARY=${WINDOWS_BINARY}
|
||||||
|
else # not ifeq ($(GOOS),windows)
|
||||||
|
BINARY=${MYNAME}
|
||||||
|
endif # ifeq ($(GOOS),windows)
|
||||||
|
else # not ifdef GOOS
|
||||||
|
BINARY=${WINDOWS_BINARY}
|
||||||
|
endif # ifdef GOOS
|
||||||
|
ifeq ($(SHELL), sh.exe)
|
||||||
|
RMCMD=DEL /Q /F
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Явно определяем символ новой строки, чтобы избежать неоднозначности на windows
|
||||||
|
define IFS
|
||||||
|
|
||||||
|
endef
|
||||||
|
|
||||||
|
.PHONY: all build clean test upgrade help
|
||||||
|
|
||||||
all: clean build
|
all: clean build
|
||||||
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
${GOOPTS} go build ${BUILDOPTS} -o ${BINARY} ./cmd/${BINARY}
|
ifeq ($(OS),Windows_NT)
|
||||||
|
# Looks like on windows gnu make explicitly set SHELL to sh.exe, if it was not set.
|
||||||
|
ifeq ($(SHELL), sh.exe)
|
||||||
|
# # Vanilla cmd.exe / powershell.
|
||||||
|
SET "CGO_ENABLED=0"
|
||||||
|
go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
|
||||||
|
else ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
|
||||||
|
# # git-bash
|
||||||
|
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
|
||||||
|
else # not ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
|
||||||
|
# # Some other shell.
|
||||||
|
# # TODO: handle it.
|
||||||
|
$(info "-- Dunno how to handle this shell: ${SHELL}")
|
||||||
|
endif # ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
|
||||||
|
else # not ($(OS),Windows_NT)
|
||||||
|
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
|
||||||
|
endif # ifeq ($(OS),Windows_NT)
|
||||||
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf ${BINARY}
|
ifeq ($(OS),Windows_NT)
|
||||||
|
ifeq ($(SHELL),sh.exe)
|
||||||
|
# # Vanilla cmd.exe / powershell.
|
||||||
|
if exist ${WINDOWS_BINARY} ${RMCMD} ${WINDOWS_BINARY}
|
||||||
|
if exist ${UNIX_BINARY} ${RMCMD} ${UNIX_BINARY}
|
||||||
|
else # not ifeq ($(SHELL),sh.exe)
|
||||||
|
${RMCMD} ./${WINDOWS_BINARY}
|
||||||
|
${RMCMD} ./${UNIX_BINARY}
|
||||||
|
endif # ifeq ($(SHELL),sh.exe)
|
||||||
|
else # not ifeq ($(OS),Windows_NT)
|
||||||
|
${RMCMD} ./${BINARY}
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
|
||||||
upgrade:
|
upgrade:
|
||||||
rm -rf vendor
|
go get -u ./...
|
||||||
go get -u -t ./...
|
|
||||||
go mod tidy
|
go mod tidy
|
||||||
go mod vendor
|
go mod vendor
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
|
|
||||||
## Как это собрать?
|
## Как это собрать?
|
||||||
|
|
||||||
Понадобится компилятор go версии 1.22 или более новый.
|
Понадобится компилятор go версии 1.25.1 или более новый.
|
||||||
|
|
||||||
Сам процесс сборки простой, достаточно запустить:
|
Сам процесс сборки простой, достаточно запустить make или gmake, в зависимости от платформы:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make
|
make
|
||||||
|
|||||||
7
TODO
7
TODO
@@ -1,7 +0,0 @@
|
|||||||
Добавить в протокол секцию nsfw/censor.
|
|
||||||
|
|
||||||
В качестве валидатора использовать какую-нить готовую либу
|
|
||||||
https://gookit.github.io/validate/#/
|
|
||||||
github.com/go-playground/validator/v10
|
|
||||||
|
|
||||||
https://awesome-go.com/validation/
|
|
||||||
@@ -2,12 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"aleesa-misc-go/internal/log"
|
||||||
"aleesa-misc-go/internal/misc"
|
"aleesa-misc-go/internal/misc"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
@@ -16,78 +16,46 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
loglevel slog.Level
|
logfile *os.File
|
||||||
)
|
)
|
||||||
|
|
||||||
misc.ReadConfig()
|
misc.ReadConfig()
|
||||||
|
|
||||||
loghandler := os.Stderr
|
|
||||||
|
|
||||||
if misc.Config.Log != "" {
|
if misc.Config.Log != "" {
|
||||||
loghandler, err = os.OpenFile(misc.Config.Log, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
|
logfile, err = os.OpenFile(misc.Config.Log, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error(fmt.Sprintf("Unable to open log file %s: %s", misc.Config.Log, err))
|
log.Error(fmt.Sprintf("Unable to open log file %s: %s", misc.Config.Log, err))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer logfile.Close()
|
||||||
|
} else {
|
||||||
|
logfile = os.Stderr
|
||||||
}
|
}
|
||||||
|
|
||||||
// no panic, no trace.
|
log.Init(misc.Config.Loglevel, logfile)
|
||||||
switch misc.Config.Loglevel {
|
|
||||||
case "error":
|
|
||||||
loglevel = slog.LevelError
|
|
||||||
|
|
||||||
case "warn":
|
|
||||||
loglevel = slog.LevelWarn
|
|
||||||
|
|
||||||
case "info":
|
|
||||||
loglevel = slog.LevelInfo
|
|
||||||
|
|
||||||
case "debug":
|
|
||||||
loglevel = slog.LevelDebug
|
|
||||||
|
|
||||||
default:
|
|
||||||
loglevel = slog.LevelInfo
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := &slog.HandlerOptions{
|
|
||||||
// Use the ReplaceAttr function on the handler options
|
|
||||||
// to be able to replace any single attribute in the log output
|
|
||||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
|
||||||
// check that we are handling the time key
|
|
||||||
if a.Key != slog.TimeKey {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
t := a.Value.Time()
|
|
||||||
|
|
||||||
// change the value from a time.Time to a String
|
|
||||||
// where the string has the correct time format.
|
|
||||||
a.Value = slog.StringValue(t.Format(time.DateTime))
|
|
||||||
|
|
||||||
return a
|
|
||||||
},
|
|
||||||
|
|
||||||
Level: loglevel,
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.SetDefault(
|
|
||||||
slog.New(
|
|
||||||
slog.NewTextHandler(
|
|
||||||
loghandler,
|
|
||||||
opts,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Иницализируем клиента Редиски.
|
// Иницализируем клиента Редиски.
|
||||||
misc.RedisClient = redis.NewClient(&redis.Options{
|
misc.RedisClient = redis.NewClient(&redis.Options{
|
||||||
Addr: fmt.Sprintf("%s:%d", misc.Config.Server, misc.Config.Port),
|
Addr: fmt.Sprintf("%s:%d", misc.Config.Server, misc.Config.Port),
|
||||||
}).WithContext(misc.Ctx).WithTimeout(time.Duration(misc.Config.Timeout) * time.Second)
|
MaxRetries: -1,
|
||||||
|
MinRetryBackoff: time.Second,
|
||||||
|
MaxRetryBackoff: 30 * time.Second,
|
||||||
|
}).WithContext(misc.RedisCtx).WithTimeout(time.Duration(misc.Config.Timeout) * time.Second)
|
||||||
|
|
||||||
|
// Проверим, что редиска доступна. Повторяем попытки, пока не подключится.
|
||||||
|
for {
|
||||||
|
if err := misc.RedisClient.Ping(misc.RedisCtx).Err(); err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("Unable to connect to redis: %s, retrying...", err)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
// Обозначим, что хотим после соединения подписаться на события из канала config.Channel.
|
// Обозначим, что хотим после соединения подписаться на события из канала config.Channel.
|
||||||
misc.Subscriber = misc.RedisClient.Subscribe(misc.Ctx, misc.Config.Channel)
|
misc.Subscriber = misc.RedisClient.Subscribe(misc.RedisCtx, misc.Config.Channel)
|
||||||
|
|
||||||
// Самое время поставить траппер сигналов.
|
// Самое время поставить траппер сигналов.
|
||||||
signal.Notify(misc.SigChan,
|
signal.Notify(misc.SigChan,
|
||||||
@@ -100,12 +68,10 @@ func main() {
|
|||||||
// Начнём выгребать события из редиски (длина конвеера/буфера канала по-умолчанию - 100 сообщений).
|
// Начнём выгребать события из редиски (длина конвеера/буфера канала по-умолчанию - 100 сообщений).
|
||||||
ch := misc.Subscriber.Channel()
|
ch := misc.Subscriber.Channel()
|
||||||
|
|
||||||
slog.Info("Aleesa-misc-go started")
|
log.Info("Aleesa-misc-go started")
|
||||||
|
|
||||||
for msg := range ch {
|
for msg := range ch {
|
||||||
if !misc.Shutdown {
|
misc.MsgParser(misc.RedisCtx, msg.Payload)
|
||||||
misc.MsgParser(misc.Ctx, msg.Payload)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
go.mod
16
go.mod
@@ -1,15 +1,13 @@
|
|||||||
module aleesa-misc-go
|
module aleesa-misc-go
|
||||||
|
|
||||||
go 1.24.0
|
go 1.25.1
|
||||||
|
|
||||||
require github.com/go-redis/redis/v8 v8.11.5
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/hjson/hjson-go v3.3.0+incompatible
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/hjson/hjson-go v3.3.0+incompatible
|
require (
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
tool aleesa-misc-go
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
)
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -1,5 +1,5 @@
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
@@ -16,8 +16,8 @@ github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
|||||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
|
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
|
||||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
|||||||
106
internal/log/log.go
Normal file
106
internal/log/log.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init setup logger stuff.
|
||||||
|
// level can be error, warn, info, debug if something other supplied info level selected.
|
||||||
|
// fileDescriptor should be opened before supplying it to Init().
|
||||||
|
func Init(level string, fileDescriptor *os.File) {
|
||||||
|
var loglevel slog.Level
|
||||||
|
|
||||||
|
// no panic, no trace.
|
||||||
|
switch level {
|
||||||
|
case "error":
|
||||||
|
loglevel = slog.LevelError
|
||||||
|
|
||||||
|
case "warn":
|
||||||
|
loglevel = slog.LevelWarn
|
||||||
|
|
||||||
|
case "info":
|
||||||
|
loglevel = slog.LevelInfo
|
||||||
|
|
||||||
|
case "debug":
|
||||||
|
loglevel = slog.LevelDebug
|
||||||
|
|
||||||
|
default:
|
||||||
|
loglevel = slog.LevelInfo
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &slog.HandlerOptions{
|
||||||
|
// Use the ReplaceAttr function on the handler options
|
||||||
|
// to be able to replace any single attribute in the log output
|
||||||
|
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||||
|
// check that we are handling the time key
|
||||||
|
if a.Key != slog.TimeKey {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
t := a.Value.Time()
|
||||||
|
|
||||||
|
// change the value from a time.Time to a String
|
||||||
|
// where the string has the correct time format.
|
||||||
|
a.Value = slog.StringValue(t.Format(time.DateTime))
|
||||||
|
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
|
||||||
|
Level: loglevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.SetDefault(
|
||||||
|
slog.New(
|
||||||
|
slog.NewTextHandler(
|
||||||
|
fileDescriptor,
|
||||||
|
opts,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error passes message directly to slog.Error().
|
||||||
|
func Error(message string) {
|
||||||
|
slog.Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf logs message on error log level and allows to supply arguments in printf() style.
|
||||||
|
func Errorf(format string, a ...any) {
|
||||||
|
slog.Error(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn passes message directly to slog.Warn().
|
||||||
|
func Warn(message string) {
|
||||||
|
slog.Warn(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnf logs message on warn log level and allows to supply arguments in printf() style.
|
||||||
|
func Warnf(format string, a ...any) {
|
||||||
|
slog.Warn(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info passes message directly to slog.Info().
|
||||||
|
func Info(message string) {
|
||||||
|
slog.Info(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infof logs message on info log level and allows to supply arguments in printf() style.
|
||||||
|
func Infof(format string, a ...any) {
|
||||||
|
slog.Info(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug passes message directly to slog.Debug().
|
||||||
|
func Debug(message string) {
|
||||||
|
slog.Debug(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugf logs message on debug log level and allows to supply arguments in printf() style.
|
||||||
|
func Debugf(format string, a ...any) {
|
||||||
|
slog.Debug(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
@@ -3,6 +3,7 @@ package misc
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
)
|
)
|
||||||
@@ -17,13 +18,17 @@ var ForwardMax int64 = 5
|
|||||||
var RedisClient *redis.Client
|
var RedisClient *redis.Client
|
||||||
var Subscriber *redis.PubSub
|
var Subscriber *redis.PubSub
|
||||||
|
|
||||||
// Ставится в true, если мы получили сигнал на выключение.
|
|
||||||
var Shutdown = false
|
|
||||||
|
|
||||||
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
|
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
|
||||||
var SigChan = make(chan os.Signal, 1)
|
var SigChan = make(chan os.Signal, 1)
|
||||||
|
|
||||||
// Main context.
|
// RedisCtx - контекст для операций с редиской. Отменяется при завершении.
|
||||||
var Ctx = context.Background()
|
var RedisCtx context.Context
|
||||||
|
var CancelRedisCtx context.CancelFunc
|
||||||
|
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RedisCtx, CancelRedisCtx = context.WithCancel(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
|
|||||||
@@ -3,9 +3,40 @@ package misc
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"aleesa-misc-go/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
retryAttempts = 3
|
||||||
|
retryDelay = time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
commandsPhrases = []string{
|
||||||
|
"friday", "пятница", "proverb", "пословица", "пословиться", "fortune", "фортунка", "f", "ф",
|
||||||
|
"karma", "карма", "rum", "ром", "vodka", "водка", "beer", "пиво", "tequila", "текила", "whisky", "виски",
|
||||||
|
"absinthe", "абсент", "fuck",
|
||||||
|
}
|
||||||
|
commandsPhrasesPrefix = []string{
|
||||||
|
"karma ", "карма ", "rum ", "ром ", "vodka ", "водка ", "beer ", "пиво ", "tequila ",
|
||||||
|
"текила ", "whisky ", "виски ", "absinthe ", "абсент ",
|
||||||
|
}
|
||||||
|
commandsWebappGo = []string{
|
||||||
|
"frog", "лягушка", "horse", "лошадь", "лошадка", "rabbit", "bunny", "кролик",
|
||||||
|
"snail", "улитка", "cat", "кис", "fox", "лис", "buni", "anek", "анек", "анекдот",
|
||||||
|
"xkcd", "monkeyuser", "tits", "boobs", "tities", "boobies", "сиси", "сисечки", "butt",
|
||||||
|
"booty", "ass", "попа", "попка", "drink", "праздник", "owl", "сова", "сыч", "w", "п", "погода",
|
||||||
|
"погодка", "погадка",
|
||||||
|
}
|
||||||
|
commandsWebappGoPrefix = []string{
|
||||||
|
"w ", "п ", "погода ", "погодка ", "погадка ", "weather ",
|
||||||
|
}
|
||||||
|
commandsGames = []string{
|
||||||
|
"dig", "копать", "fish", "fishing", "рыба", "рыбка", "рыбалка",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// MsgParser горутинка, которая парсит json-чики прилетевшие из REDIS-ки.
|
// MsgParser горутинка, которая парсит json-чики прилетевшие из REDIS-ки.
|
||||||
@@ -15,29 +46,29 @@ func MsgParser(ctx context.Context, msg string) {
|
|||||||
j rMsg
|
j rMsg
|
||||||
)
|
)
|
||||||
|
|
||||||
slog.Debug(fmt.Sprintf("Incomming raw json: %s", msg))
|
log.Debugf("Incoming raw json: %s", msg)
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(msg), &j); err != nil {
|
if err := json.Unmarshal([]byte(msg), &j); err != nil {
|
||||||
slog.Warn(fmt.Sprintf("Unable to to parse message from redis channel: %s", err))
|
log.Warnf("Unable to to parse message from redis channel: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate our j.
|
// Validate our j.
|
||||||
if exist := j.From; exist == "" {
|
if exist := j.From; exist == "" {
|
||||||
slog.Warn(fmt.Sprintf("Incorrect msg from redis, no from field: %s", msg))
|
log.Warnf("Incorrect msg from redis, no from field: %s", msg)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if exist := j.Chatid; exist == "" {
|
if exist := j.Chatid; exist == "" {
|
||||||
slog.Warn(fmt.Sprintf("Incorrect msg from redis, no chatid field: %s", msg))
|
log.Warnf("Incorrect msg from redis, no chatid field: %s", msg)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if exist := j.Userid; exist == "" {
|
if exist := j.Userid; exist == "" {
|
||||||
slog.Warn(fmt.Sprintf("Incorrect msg from redis, no userid field: %s", msg))
|
log.Warnf("Incorrect msg from redis, no userid field: %s", msg)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -45,19 +76,19 @@ func MsgParser(ctx context.Context, msg string) {
|
|||||||
// j.Threadid может быть пустым, значит либо нам его не дали, либо дали пустым. Это нормально.
|
// j.Threadid может быть пустым, значит либо нам его не дали, либо дали пустым. Это нормально.
|
||||||
|
|
||||||
if exist := j.Message; exist == "" {
|
if exist := j.Message; exist == "" {
|
||||||
slog.Warn(fmt.Sprintf("Incorrect msg from redis, no message field: %s", msg))
|
log.Warnf("Incorrect msg from redis, no message field: %s", msg)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if exist := j.Plugin; exist == "" {
|
if exist := j.Plugin; exist == "" {
|
||||||
slog.Warn(fmt.Sprintf("Incorrect msg from redis, no plugin field: %s", msg))
|
log.Warnf("Incorrect msg from redis, no plugin field: %s", msg)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if exist := j.Mode; exist == "" {
|
if exist := j.Mode; exist == "" {
|
||||||
slog.Warn(fmt.Sprintf("Incorrect msg from redis, no mode field: %s", msg))
|
log.Warnf("Incorrect msg from redis, no mode field: %s", msg)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -69,9 +100,10 @@ func MsgParser(ctx context.Context, msg string) {
|
|||||||
j.Misc.Csign = Config.Csign
|
j.Misc.Csign = Config.Csign
|
||||||
}
|
}
|
||||||
|
|
||||||
// j.Misc.Fwdcnt если нам его не передали, то будет 0.
|
// j.Misc.Fwdcnt если нам его не передали, то будет nil.
|
||||||
if exist := j.Misc.Fwdcnt; exist == 0 {
|
if j.Misc.Fwdcnt == nil {
|
||||||
j.Misc.Fwdcnt = 1
|
j.Misc.Fwdcnt = new(int64)
|
||||||
|
*j.Misc.Fwdcnt = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// j.Misc.GoodMorning может быть 1 или 0, по-умолчанию 0.
|
// j.Misc.GoodMorning может быть 1 или 0, по-умолчанию 0.
|
||||||
@@ -81,27 +113,23 @@ func MsgParser(ctx context.Context, msg string) {
|
|||||||
// Отвалидировались, теперь вернёмся к нашим баранам.
|
// Отвалидировались, теперь вернёмся к нашим баранам.
|
||||||
|
|
||||||
// Если у нас циклическая пересылка сообщения, попробуем её тут разорвать, отбросив сообщение.
|
// Если у нас циклическая пересылка сообщения, попробуем её тут разорвать, отбросив сообщение.
|
||||||
if j.Misc.Fwdcnt > Config.ForwardsMax {
|
if *j.Misc.Fwdcnt > Config.ForwardsMax {
|
||||||
slog.Warn(fmt.Sprintf("Discarding msg with fwd_cnt exceeding max value: %s", msg))
|
log.Warnf("Discarding msg with fwd_cnt exceeding max value: %s", msg)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
j.Misc.Fwdcnt++
|
*j.Misc.Fwdcnt++
|
||||||
|
|
||||||
// Классифицирем входящие сообщения. Первым делом, попробуем определить команды.
|
// Классифицирем входящие сообщения. Первым делом, попробуем определить команды.
|
||||||
if j.Message[0:len(j.Misc.Csign)] == j.Misc.Csign {
|
if len(j.Message) >= len(j.Misc.Csign) && j.Message[0:len(j.Misc.Csign)] == j.Misc.Csign {
|
||||||
// Может быть, это команда модуля phrases?
|
// Может быть, это команда модуля phrases?
|
||||||
var (
|
var (
|
||||||
done = false
|
done = false
|
||||||
cmd = j.Message[len(j.Misc.Csign):]
|
cmd = j.Message[len(j.Misc.Csign):]
|
||||||
)
|
)
|
||||||
|
|
||||||
cmds := []string{"friday", "пятница", "proverb", "пословица", "пословиться", "fortune", "фортунка", "f", "ф",
|
for _, command := range commandsPhrases {
|
||||||
"karma", "карма", "rum", "ром", "vodka", "водка", "beer", "пиво", "tequila", "текила", "whisky", "виски",
|
|
||||||
"absinthe", "абсент", "fuck"}
|
|
||||||
|
|
||||||
for _, command := range cmds {
|
|
||||||
if cmd == command {
|
if cmd == command {
|
||||||
sendTo = Config.ForwardChannels.Phrases
|
sendTo = Config.ForwardChannels.Phrases
|
||||||
|
|
||||||
@@ -118,13 +146,7 @@ func MsgParser(ctx context.Context, msg string) {
|
|||||||
|
|
||||||
// Не угадали? акей, как насчёт модуля webapp-go?
|
// Не угадали? акей, как насчёт модуля webapp-go?
|
||||||
if !done {
|
if !done {
|
||||||
cmds = []string{"frog", "лягушка", "horse", "лошадь", "лошадка", "rabbit", "bunny", "кролик",
|
for _, command := range commandsWebappGo {
|
||||||
"snail", "улитка", "cat", "кис", "fox", "лис", "buni", "anek", "анек", "анекдот",
|
|
||||||
"xkcd", "monkeyuser", "tits", "boobs", "tities", "boobies", "сиси", "сисечки", "butt",
|
|
||||||
"booty", "ass", "попа", "попка", "drink", "праздник", "owl", "сова", "сыч", "w", "п", "погода",
|
|
||||||
"погодка", "погадка"}
|
|
||||||
|
|
||||||
for _, command := range cmds {
|
|
||||||
if cmd == command {
|
if cmd == command {
|
||||||
sendTo = Config.ForwardChannels.WebappGo
|
sendTo = Config.ForwardChannels.WebappGo
|
||||||
done = true
|
done = true
|
||||||
@@ -136,9 +158,7 @@ func MsgParser(ctx context.Context, msg string) {
|
|||||||
|
|
||||||
// Не фортануло? может, повезёт с модулем games?
|
// Не фортануло? может, повезёт с модулем games?
|
||||||
if !done {
|
if !done {
|
||||||
cmds = []string{"dig", "копать", "fish", "fishing", "рыба", "рыбка", "рыбалка"}
|
for _, command := range commandsGames {
|
||||||
|
|
||||||
for _, command := range cmds {
|
|
||||||
if cmd == command {
|
if cmd == command {
|
||||||
sendTo = Config.ForwardChannels.Games
|
sendTo = Config.ForwardChannels.Games
|
||||||
done = true
|
done = true
|
||||||
@@ -152,9 +172,7 @@ func MsgParser(ctx context.Context, msg string) {
|
|||||||
if !done {
|
if !done {
|
||||||
cmdLen := len(cmd)
|
cmdLen := len(cmd)
|
||||||
|
|
||||||
cmds := []string{"w ", "п ", "погода ", "погодка ", "погадка ", "weather "}
|
for _, command := range commandsWebappGoPrefix {
|
||||||
|
|
||||||
for _, command := range cmds {
|
|
||||||
if cmdLen > len(command) && cmd[0:len(command)] == command {
|
if cmdLen > len(command) && cmd[0:len(command)] == command {
|
||||||
sendTo = Config.ForwardChannels.WebappGo
|
sendTo = Config.ForwardChannels.WebappGo
|
||||||
done = true
|
done = true
|
||||||
@@ -168,10 +186,7 @@ func MsgParser(ctx context.Context, msg string) {
|
|||||||
if !done {
|
if !done {
|
||||||
cmdLen := len(cmd)
|
cmdLen := len(cmd)
|
||||||
|
|
||||||
cmds := []string{"karma ", "карма ", "rum ", "ром ", "vodka ", "водка ", "beer ", "пиво ", "tequila ",
|
for _, command := range commandsPhrasesPrefix {
|
||||||
"текила ", "whisky ", "виски ", "absinthe ", "абсент "}
|
|
||||||
|
|
||||||
for _, command := range cmds {
|
|
||||||
if cmdLen > len(command) && cmd[0:len(command)] == command {
|
if cmdLen > len(command) && cmd[0:len(command)] == command {
|
||||||
sendTo = Config.ForwardChannels.Phrases
|
sendTo = Config.ForwardChannels.Phrases
|
||||||
|
|
||||||
@@ -211,7 +226,7 @@ func MsgParser(ctx context.Context, msg string) {
|
|||||||
message.Message = j.Message
|
message.Message = j.Message
|
||||||
message.Plugin = j.Plugin
|
message.Plugin = j.Plugin
|
||||||
message.Mode = j.Mode
|
message.Mode = j.Mode
|
||||||
message.Misc.Fwdcnt = j.Misc.Fwdcnt
|
message.Misc.Fwdcnt = *j.Misc.Fwdcnt
|
||||||
message.Misc.Csign = j.Misc.Csign
|
message.Misc.Csign = j.Misc.Csign
|
||||||
message.Misc.Username = j.Misc.Username
|
message.Misc.Username = j.Misc.Username
|
||||||
message.Misc.Answer = j.Misc.Answer
|
message.Misc.Answer = j.Misc.Answer
|
||||||
@@ -222,17 +237,31 @@ func MsgParser(ctx context.Context, msg string) {
|
|||||||
data, err := json.Marshal(message)
|
data, err := json.Marshal(message)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn(fmt.Sprintf("Unable to to serialize message for redis: %s", err))
|
log.Warnf("Unable to to serialize message for redis: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Заталкиваем наш json в редиску.
|
// Заталкиваем наш json в редиску с повторными попытками.
|
||||||
if err := RedisClient.Publish(ctx, sendTo, data).Err(); err != nil {
|
var publishErr error
|
||||||
slog.Warn(fmt.Sprintf("Unable to send data to redis channel %s: %s", sendTo, err))
|
|
||||||
} else {
|
for attempt := 1; attempt <= retryAttempts; attempt++ {
|
||||||
slog.Debug(fmt.Sprintf("Send msg to redis channel %s: %s", sendTo, string(data)))
|
if publishErr = RedisClient.Publish(ctx, sendTo, data).Err(); publishErr == nil {
|
||||||
|
log.Debugf("Send msg to redis channel %s: %s", sendTo, string(data))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("Unable to send data to redis channel %s (attempt %d/%d): %s",
|
||||||
|
sendTo, attempt, retryAttempts, publishErr)
|
||||||
|
|
||||||
|
if attempt < retryAttempts {
|
||||||
|
time.Sleep(retryDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Errorf("Failed to send data to redis channel %s after %d attempts: %s",
|
||||||
|
sendTo, retryAttempts, publishErr)
|
||||||
|
}
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
|
|||||||
721
internal/misc/msgParser_test.go
Normal file
721
internal/misc/msgParser_test.go
Normal file
@@ -0,0 +1,721 @@
|
|||||||
|
package misc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMsgParser_Validation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputJSON string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty message should not panic",
|
||||||
|
inputJSON: `{"from":"test","chatid":"1","userid":"1","message":"","plugin":"test","mode":"test"}`,
|
||||||
|
expectPanic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "short message less than csign",
|
||||||
|
inputJSON: `{"from":"test","chatid":"1","userid":"1","message":"!","plugin":"test","mode":"test","misc":{"csign":"!!!"}}`,
|
||||||
|
expectPanic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid message with single char csign",
|
||||||
|
inputJSON: `{"from":"test","chatid":"1","userid":"1","message":"!karma","plugin":"test","mode":"test","misc":{"csign":"!"}}`,
|
||||||
|
expectPanic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "message shorter than multi char csign",
|
||||||
|
inputJSON: `{"from":"test","chatid":"1","userid":"1","message":"!","plugin":"test","mode":"test","misc":{"csign":"!!"}}`,
|
||||||
|
expectPanic: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if !tt.expectPanic {
|
||||||
|
t.Errorf("unexpected panic: %v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// We only test that parsing doesn't panic
|
||||||
|
// Actual publishing requires Redis
|
||||||
|
var msg rMsg
|
||||||
|
if err := json.Unmarshal([]byte(tt.inputJSON), &msg); err != nil {
|
||||||
|
t.Logf("JSON parse error (expected for some tests): %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgParser_CommandRouting(t *testing.T) {
|
||||||
|
// Save original config
|
||||||
|
origConfig := Config
|
||||||
|
Config = myConfig{
|
||||||
|
Csign: "!",
|
||||||
|
ForwardChannels: struct {
|
||||||
|
Games string `json:"games,omitempty"`
|
||||||
|
Phrases string `json:"phrases,omitempty"`
|
||||||
|
Webapp string `json:"webapp,omitempty"`
|
||||||
|
WebappGo string `json:"webapp-go,omitempty"`
|
||||||
|
Craniac string `json:"craniac,omitempty"`
|
||||||
|
}{
|
||||||
|
Games: "games",
|
||||||
|
Phrases: "phrases",
|
||||||
|
Webapp: "webapp",
|
||||||
|
WebappGo: "webapp-go",
|
||||||
|
Craniac: "craniac",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
defer func() { Config = origConfig }()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
message string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
// Phrases commands
|
||||||
|
{"friday command", "!friday", "phrases"},
|
||||||
|
{"fortune command", "!fortune", "phrases"},
|
||||||
|
{"karma command", "!karma", "phrases"},
|
||||||
|
{"russian friday", "!пятница", "phrases"},
|
||||||
|
{"russian karma", "!карма", "phrases"},
|
||||||
|
|
||||||
|
// Webapp-go commands
|
||||||
|
{"frog command", "!frog", "webapp-go"},
|
||||||
|
{"cat command", "!cat", "webapp-go"},
|
||||||
|
{"weather command", "!w Moskow", "webapp-go"},
|
||||||
|
{"russian weather", "!погода Москва", "webapp-go"},
|
||||||
|
|
||||||
|
// Games commands
|
||||||
|
{"dig command", "!dig", "games"},
|
||||||
|
{"fish command", "!fish", "games"},
|
||||||
|
{"russian dig", "!копать", "games"},
|
||||||
|
|
||||||
|
// Default fallback
|
||||||
|
{"unknown command", "!unknown", "craniac"},
|
||||||
|
{"no command prefix", "just some text", "craniac"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
msg := rMsg{
|
||||||
|
From: "test",
|
||||||
|
Chatid: "1",
|
||||||
|
Userid: "1",
|
||||||
|
Message: tt.message,
|
||||||
|
Plugin: "test",
|
||||||
|
Mode: "test",
|
||||||
|
Misc: struct {
|
||||||
|
Answer int64 `json:"answer,omitempty"`
|
||||||
|
Botnick string `json:"bot_nick,omitempty"`
|
||||||
|
Csign string `json:"csign,omitempty"`
|
||||||
|
Fwdcnt *int64 `json:"fwd_cnt,omitempty"`
|
||||||
|
GoodMorning int64 `json:"good_morning,omitempty"`
|
||||||
|
Msgformat int64 `json:"msg_format,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
}{
|
||||||
|
Csign: Config.Csign,
|
||||||
|
Fwdcnt: new(int64),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
*msg.Misc.Fwdcnt = 1
|
||||||
|
|
||||||
|
result := determineChannel(msg.Message, msg.Misc.Csign)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("expected %s, got %s", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgParser_KarmaPatterns(t *testing.T) {
|
||||||
|
origConfig := Config
|
||||||
|
Config = myConfig{
|
||||||
|
Csign: "!",
|
||||||
|
ForwardChannels: struct {
|
||||||
|
Games string `json:"games,omitempty"`
|
||||||
|
Phrases string `json:"phrases,omitempty"`
|
||||||
|
Webapp string `json:"webapp,omitempty"`
|
||||||
|
WebappGo string `json:"webapp-go,omitempty"`
|
||||||
|
Craniac string `json:"craniac,omitempty"`
|
||||||
|
}{
|
||||||
|
Phrases: "phrases",
|
||||||
|
Craniac: "craniac",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
defer func() { Config = origConfig }()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
message string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"karma increment", "something++", "phrases"},
|
||||||
|
{"karma decrement", "something--", "phrases"},
|
||||||
|
{"russian plus plus", "что-то++", "phrases"},
|
||||||
|
{"no karma pattern", "just text", "craniac"},
|
||||||
|
{"multiple lines no karma", "line1\nline2++", "craniac"},
|
||||||
|
{"single line with karma", "line1++", "phrases"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := determineKarmaChannel(tt.message)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("expected %s, got %s", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgParser_FwdcntPointer(t *testing.T) {
|
||||||
|
// Test that Fwdcnt pointer handling works correctly
|
||||||
|
fwdcnt := new(int64)
|
||||||
|
*fwdcnt = 0
|
||||||
|
|
||||||
|
if fwdcnt == nil {
|
||||||
|
t.Error("fwdcnt should not be nil after new(int64)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *fwdcnt != 0 {
|
||||||
|
t.Errorf("expected 0, got %d", *fwdcnt)
|
||||||
|
}
|
||||||
|
|
||||||
|
*fwdcnt++
|
||||||
|
if *fwdcnt != 1 {
|
||||||
|
t.Errorf("expected 1, got %d", *fwdcnt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// determineChannel is extracted logic for testing command routing
|
||||||
|
func determineChannel(message, csign string) string {
|
||||||
|
sendTo := "craniac"
|
||||||
|
|
||||||
|
if len(message) >= len(csign) && message[0:len(csign)] == csign {
|
||||||
|
cmd := message[len(csign):]
|
||||||
|
|
||||||
|
cmds := []string{"friday", "пятница", "proverb", "пословица", "пословиться", "fortune", "фортунка", "f", "ф",
|
||||||
|
"karma", "карма", "rum", "ром", "vodka", "водка", "beer", "пиво", "tequila", "текила", "whisky", "виски",
|
||||||
|
"absinthe", "абсент", "fuck"}
|
||||||
|
|
||||||
|
for _, command := range cmds {
|
||||||
|
if cmd == command {
|
||||||
|
return "phrases"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds = []string{"frog", "лягушка", "horse", "лошадь", "лошадка", "rabbit", "bunny", "кролик",
|
||||||
|
"snail", "улитка", "cat", "кис", "fox", "лис", "buni", "anek", "анек", "анекдот",
|
||||||
|
"xkcd", "monkeyuser", "tits", "boobs", "tities", "boobies", "сиси", "сисечки", "butt",
|
||||||
|
"booty", "ass", "попа", "попка", "drink", "праздник", "owl", "сова", "сыч", "w", "п", "погода",
|
||||||
|
"погодка", "погадка"}
|
||||||
|
|
||||||
|
for _, command := range cmds {
|
||||||
|
if cmd == command {
|
||||||
|
return "webapp-go"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds = []string{"dig", "копать", "fish", "fishing", "рыба", "рыбка", "рыбалка"}
|
||||||
|
|
||||||
|
for _, command := range cmds {
|
||||||
|
if cmd == command {
|
||||||
|
return "games"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdLen := len(cmd)
|
||||||
|
cmds = []string{"w ", "п ", "погода ", "погодка ", "погадка ", "weather "}
|
||||||
|
|
||||||
|
for _, command := range cmds {
|
||||||
|
if cmdLen > len(command) && cmd[0:len(command)] == command {
|
||||||
|
return "webapp-go"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds = []string{"karma ", "карма ", "rum ", "ром ", "vodka ", "водка ", "beer ", "пиво ", "tequila ",
|
||||||
|
"текила ", "whisky ", "виски ", "absinthe ", "абсент "}
|
||||||
|
|
||||||
|
for _, command := range cmds {
|
||||||
|
if cmdLen > len(command) && cmd[0:len(command)] == command {
|
||||||
|
return "phrases"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendTo
|
||||||
|
}
|
||||||
|
|
||||||
|
// determineKarmaChannel is extracted logic for testing karma patterns
|
||||||
|
func determineKarmaChannel(message string) string {
|
||||||
|
msgLen := len(message)
|
||||||
|
|
||||||
|
if msgLen > 2 {
|
||||||
|
if message[msgLen-2:] == "--" || message[msgLen-2:] == "++" {
|
||||||
|
// Предполагается, что менять карму мы будем для одной фразы, то есть для 1 строки
|
||||||
|
if len(strings.Split(message, "\n")) == 1 {
|
||||||
|
return "phrases"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "craniac"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgParser_BuildsMsgCorrectly(t *testing.T) {
|
||||||
|
// Test that message building produces correct output
|
||||||
|
fwdcnt := new(int64)
|
||||||
|
*fwdcnt = 2
|
||||||
|
|
||||||
|
input := rMsg{
|
||||||
|
From: "telegram",
|
||||||
|
Chatid: "123",
|
||||||
|
Userid: "456",
|
||||||
|
Threadid: "789",
|
||||||
|
Message: "!karma user",
|
||||||
|
Plugin: "telegram",
|
||||||
|
Mode: "private",
|
||||||
|
Misc: struct {
|
||||||
|
Answer int64 `json:"answer,omitempty"`
|
||||||
|
Botnick string `json:"bot_nick,omitempty"`
|
||||||
|
Csign string `json:"csign,omitempty"`
|
||||||
|
Fwdcnt *int64 `json:"fwd_cnt,omitempty"`
|
||||||
|
GoodMorning int64 `json:"good_morning,omitempty"`
|
||||||
|
Msgformat int64 `json:"msg_format,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
}{
|
||||||
|
Answer: 1,
|
||||||
|
Botnick: "aleesa",
|
||||||
|
Csign: "!",
|
||||||
|
Fwdcnt: fwdcnt,
|
||||||
|
Username: "testuser",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate the message building logic
|
||||||
|
var message sMsg
|
||||||
|
message.From = input.From
|
||||||
|
message.Userid = input.Userid
|
||||||
|
message.Chatid = input.Chatid
|
||||||
|
message.Threadid = input.Threadid
|
||||||
|
message.Message = input.Message
|
||||||
|
message.Plugin = input.Plugin
|
||||||
|
message.Mode = input.Mode
|
||||||
|
message.Misc.Fwdcnt = *input.Misc.Fwdcnt
|
||||||
|
message.Misc.Csign = input.Misc.Csign
|
||||||
|
message.Misc.Username = input.Misc.Username
|
||||||
|
message.Misc.Answer = input.Misc.Answer
|
||||||
|
message.Misc.Botnick = input.Misc.Botnick
|
||||||
|
message.Misc.Msgformat = input.Misc.Msgformat
|
||||||
|
message.Misc.GoodMorning = input.Misc.GoodMorning
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
if message.Misc.Fwdcnt != 2 {
|
||||||
|
t.Errorf("expected Fwdcnt 2, got %d", message.Misc.Fwdcnt)
|
||||||
|
}
|
||||||
|
if message.Misc.Answer != 1 {
|
||||||
|
t.Errorf("expected Answer 1, got %d", message.Misc.Answer)
|
||||||
|
}
|
||||||
|
if message.Misc.Username != "testuser" {
|
||||||
|
t.Errorf("expected Username 'testuser', got %s", message.Misc.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify JSON serialization works
|
||||||
|
data, err := json.Marshal(message)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we can unmarshal it back
|
||||||
|
var result sMsg
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.From != message.From {
|
||||||
|
t.Errorf("From mismatch: got %s", result.From)
|
||||||
|
}
|
||||||
|
if result.Misc.Fwdcnt != message.Misc.Fwdcnt {
|
||||||
|
t.Errorf("Fwdcnt mismatch: got %d", result.Misc.Fwdcnt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = context.Background // for import
|
||||||
|
|
||||||
|
func TestMsgParser_MultiCharCsign(t *testing.T) {
|
||||||
|
origConfig := Config
|
||||||
|
Config = myConfig{
|
||||||
|
Csign: "!!!",
|
||||||
|
ForwardChannels: struct {
|
||||||
|
Games string `json:"games,omitempty"`
|
||||||
|
Phrases string `json:"phrases,omitempty"`
|
||||||
|
Webapp string `json:"webapp,omitempty"`
|
||||||
|
WebappGo string `json:"webapp-go,omitempty"`
|
||||||
|
Craniac string `json:"craniac,omitempty"`
|
||||||
|
}{
|
||||||
|
Games: "games",
|
||||||
|
Phrases: "phrases",
|
||||||
|
Webapp: "webapp",
|
||||||
|
WebappGo: "webapp-go",
|
||||||
|
Craniac: "craniac",
|
||||||
|
},
|
||||||
|
ForwardsMax: 5,
|
||||||
|
}
|
||||||
|
defer func() { Config = origConfig }()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
message string
|
||||||
|
csign string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"double char csign !!", "!!karma", "!!", "phrases"},
|
||||||
|
{"triple char csign !!!", "!!!friday", "!!!", "phrases"},
|
||||||
|
{"slash csign /", "/help", "/", "craniac"},
|
||||||
|
{"dot csign .", ".status", ".", "craniac"},
|
||||||
|
{"message shorter than csign", "!", "!!", "craniac"},
|
||||||
|
{"message equal to csign", "!!", "!!", "craniac"},
|
||||||
|
{"message longer than csign", "!!!test", "!!!", "craniac"},
|
||||||
|
{"russian with multi char csign", "!!!погода", "!!!", "webapp-go"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := determineChannel(tt.message, tt.csign)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("expected %s, got %s", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgParser_FwdcntLoopPrevention(t *testing.T) {
|
||||||
|
origConfig := Config
|
||||||
|
Config = myConfig{
|
||||||
|
Csign: "!",
|
||||||
|
ForwardsMax: 5,
|
||||||
|
}
|
||||||
|
defer func() { Config = origConfig }()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fwdcnt int64
|
||||||
|
expected bool // true = should be discarded
|
||||||
|
}{
|
||||||
|
{"fwdcnt 0 allowed", 0, false},
|
||||||
|
{"fwdcnt 1 allowed", 1, false},
|
||||||
|
{"fwdcnt 5 allowed", 5, false},
|
||||||
|
{"fwdcnt 6 discarded", 6, true},
|
||||||
|
{"fwdcnt 10 discarded", 10, true},
|
||||||
|
{"fwdcnt 100 discarded", 100, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
shouldDiscard := tt.fwdcnt > Config.ForwardsMax
|
||||||
|
if shouldDiscard != tt.expected {
|
||||||
|
t.Errorf("fwdcnt=%d: expected discard=%v, got %v", tt.fwdcnt, tt.expected, shouldDiscard)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgParser_DefaultCsignFromConfig(t *testing.T) {
|
||||||
|
origConfig := Config
|
||||||
|
Config = myConfig{
|
||||||
|
Csign: "!",
|
||||||
|
ForwardChannels: struct {
|
||||||
|
Games string `json:"games,omitempty"`
|
||||||
|
Phrases string `json:"phrases,omitempty"`
|
||||||
|
Webapp string `json:"webapp,omitempty"`
|
||||||
|
WebappGo string `json:"webapp-go,omitempty"`
|
||||||
|
Craniac string `json:"craniac,omitempty"`
|
||||||
|
}{
|
||||||
|
Phrases: "phrases",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
defer func() { Config = origConfig }()
|
||||||
|
|
||||||
|
// When csign is empty in message, it should use Config.Csign
|
||||||
|
message := "!karma"
|
||||||
|
csign := ""
|
||||||
|
|
||||||
|
if csign == "" {
|
||||||
|
csign = Config.Csign
|
||||||
|
}
|
||||||
|
|
||||||
|
result := determineChannel(message, csign)
|
||||||
|
if result != "phrases" {
|
||||||
|
t.Errorf("expected phrases, got %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgParser_KarmaAnswerFlag(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
command string
|
||||||
|
expectAnswer bool
|
||||||
|
}{
|
||||||
|
{"karma without args sets answer", "karma", true},
|
||||||
|
// Note: "karma user" (with arg) doesn't match "karma " pattern, so Answer is NOT set
|
||||||
|
{"karma with space and arg", "karma user", false},
|
||||||
|
{"russian karma without args", "карма", true},
|
||||||
|
// Note: Russian karma with args requires different handling due to UTF-8 encoding
|
||||||
|
{"friday no answer", "friday", false},
|
||||||
|
{"fortune no answer", "fortune", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Simulate the karma answer logic (checks for exact match)
|
||||||
|
isExactKarma := tt.command == "karma" || tt.command == "карма"
|
||||||
|
shouldSetAnswer := isExactKarma
|
||||||
|
|
||||||
|
if shouldSetAnswer != tt.expectAnswer {
|
||||||
|
t.Errorf("command=%s: expected answer=%v, got %v", tt.command, tt.expectAnswer, shouldSetAnswer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgParser_EmptyCommandAfterCsign(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
message string
|
||||||
|
csign string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"just csign", "!", "!", "craniac"},
|
||||||
|
{"csign with space", "! ", "!", "craniac"},
|
||||||
|
{"double csign", "!!", "!!", "craniac"},
|
||||||
|
{"double csign with space", "!! ", "!!", "craniac"},
|
||||||
|
{"slash with space", "/ ", "/", "craniac"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := determineChannel(tt.message, tt.csign)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("expected %s, got %s", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgParser_MoreRussianCommands(t *testing.T) {
|
||||||
|
origConfig := Config
|
||||||
|
Config = myConfig{
|
||||||
|
Csign: "!",
|
||||||
|
ForwardChannels: struct {
|
||||||
|
Games string `json:"games,omitempty"`
|
||||||
|
Phrases string `json:"phrases,omitempty"`
|
||||||
|
Webapp string `json:"webapp,omitempty"`
|
||||||
|
WebappGo string `json:"webapp-go,omitempty"`
|
||||||
|
Craniac string `json:"craniac,omitempty"`
|
||||||
|
}{
|
||||||
|
Games: "games",
|
||||||
|
Phrases: "phrases",
|
||||||
|
WebappGo: "webapp-go",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
defer func() { Config = origConfig }()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
message string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
// Russian weather commands
|
||||||
|
{"russian pogoda", "!погода", "webapp-go"},
|
||||||
|
{"russian pogodka", "!погодка", "webapp-go"},
|
||||||
|
{"russian pogadka", "!погадка", "webapp-go"},
|
||||||
|
{"russian pogoda with city", "!погода Москва", "webapp-go"},
|
||||||
|
|
||||||
|
// Russian games commands
|
||||||
|
{"russian kopat", "!копать", "games"},
|
||||||
|
{"russian ryba", "!рыба", "games"},
|
||||||
|
{"russian ribka", "!рыбка", "games"},
|
||||||
|
{"russian rybalka", "!рыбалка", "games"},
|
||||||
|
|
||||||
|
// Russian phrases commands
|
||||||
|
{"russian friday", "!пятница", "phrases"},
|
||||||
|
{"russian proverb", "!пословица", "phrases"},
|
||||||
|
{"russian fortune", "!фортунка", "phrases"},
|
||||||
|
{"russian f", "!ф", "phrases"},
|
||||||
|
|
||||||
|
// More Russian webapp
|
||||||
|
{"russian frog", "!лягушка", "webapp-go"},
|
||||||
|
{"russian horse", "!лошадь", "webapp-go"},
|
||||||
|
{"russian rabbit", "!кролик", "webapp-go"},
|
||||||
|
{"russian snail", "!улитка", "webapp-go"},
|
||||||
|
{"russian fox", "!лис", "webapp-go"},
|
||||||
|
{"russian anek", "!анек", "webapp-go"},
|
||||||
|
{"russian drink", "!праздник", "webapp-go"},
|
||||||
|
{"russian owl", "!сова", "webapp-go"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := determineChannel(tt.message, Config.Csign)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("expected %s, got %s", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgParser_MoreEnglishCommands(t *testing.T) {
|
||||||
|
origConfig := Config
|
||||||
|
Config = myConfig{
|
||||||
|
Csign: "!",
|
||||||
|
ForwardChannels: struct {
|
||||||
|
Games string `json:"games,omitempty"`
|
||||||
|
Phrases string `json:"phrases,omitempty"`
|
||||||
|
Webapp string `json:"webapp,omitempty"`
|
||||||
|
WebappGo string `json:"webapp-go,omitempty"`
|
||||||
|
Craniac string `json:"craniac,omitempty"`
|
||||||
|
}{
|
||||||
|
Games: "games",
|
||||||
|
Phrases: "phrases",
|
||||||
|
WebappGo: "webapp-go",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
defer func() { Config = origConfig }()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
message string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
// More phrases commands
|
||||||
|
{"proverb", "!proverb", "phrases"},
|
||||||
|
{"poslovitsa", "!пословица", "phrases"},
|
||||||
|
{"poslovitsya", "!пословиться", "phrases"},
|
||||||
|
{"rum", "!rum", "phrases"},
|
||||||
|
{"vodka", "!vodka", "phrases"},
|
||||||
|
{"beer", "!beer", "phrases"},
|
||||||
|
{"tequila", "!tequila", "phrases"},
|
||||||
|
{"whisky", "!whisky", "phrases"},
|
||||||
|
{"absinthe", "!absinthe", "phrases"},
|
||||||
|
{"fuck", "!fuck", "phrases"},
|
||||||
|
{"f short", "!f", "phrases"},
|
||||||
|
|
||||||
|
// More webapp-go commands
|
||||||
|
{"horse", "!horse", "webapp-go"},
|
||||||
|
{"rabbit", "!rabbit", "webapp-go"},
|
||||||
|
{"bunny", "!bunny", "webapp-go"},
|
||||||
|
{"snail", "!snail", "webapp-go"},
|
||||||
|
{"fox", "!fox", "webapp-go"},
|
||||||
|
{"buni", "!buni", "webapp-go"},
|
||||||
|
{"anek", "!anek", "webapp-go"},
|
||||||
|
{"xkcd", "!xkcd", "webapp-go"},
|
||||||
|
{"monkeyuser", "!monkeyuser", "webapp-go"},
|
||||||
|
{"drink", "!drink", "webapp-go"},
|
||||||
|
{"owl", "!owl", "webapp-go"},
|
||||||
|
{"w short", "!w", "webapp-go"},
|
||||||
|
|
||||||
|
// More games commands
|
||||||
|
{"fishing", "!fishing", "games"},
|
||||||
|
|
||||||
|
// Phrases with arguments
|
||||||
|
{"karma with user", "!karma username", "phrases"},
|
||||||
|
{"rum with drink", "!rum cola", "phrases"},
|
||||||
|
{"vodka with juice", "!водка сок", "phrases"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := determineChannel(tt.message, Config.Csign)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("expected %s, got %s", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgParser_KarmaInlinePattern(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
message string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"simple increment", "user++", "phrases"},
|
||||||
|
{"simple decrement", "user--", "phrases"},
|
||||||
|
{"with spaces", "some text++", "phrases"},
|
||||||
|
{"cyrillic increment", "пользователь++", "phrases"},
|
||||||
|
{"cyrillic decrement", "пользователь--", "phrases"},
|
||||||
|
{"emoji", "👍++", "phrases"},
|
||||||
|
{"underscore", "test_name++", "phrases"},
|
||||||
|
{"dash in name", "test-name++", "phrases"},
|
||||||
|
{"not karma - just plus", "a+b", "craniac"},
|
||||||
|
// "a+++" ends with "++", so it's treated as karma increment
|
||||||
|
{"multiple plus ends with ++", "a+++", "phrases"},
|
||||||
|
{"not karma - embedded", "a++b", "craniac"},
|
||||||
|
{"decrement edge", "a--b", "craniac"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := determineKarmaChannel(tt.message)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("expected %s, got %s", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryConstants(t *testing.T) {
|
||||||
|
if retryAttempts != 3 {
|
||||||
|
t.Errorf("expected retryAttempts=3, got %d", retryAttempts)
|
||||||
|
}
|
||||||
|
if retryDelay != time.Second {
|
||||||
|
t.Errorf("expected retryDelay=1s, got %v", retryDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgParser_MessageValidationLogic(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
jsonStr string
|
||||||
|
expectValid bool // true if message has all required fields
|
||||||
|
}{
|
||||||
|
// Valid messages
|
||||||
|
{"valid minimal", `{"from":"t","chatid":"1","userid":"1","message":"m","plugin":"p","mode":"m"}`, true},
|
||||||
|
{"valid full", `{"from":"t","chatid":"1","userid":"1","message":"m","plugin":"p","mode":"m","misc":{"csign":"!","fwd_cnt":1}}`, true},
|
||||||
|
|
||||||
|
// Missing required fields - they unmarshal but are empty strings
|
||||||
|
{"missing from", `{"chatid":"1","userid":"1","message":"m","plugin":"p","mode":"m"}`, false},
|
||||||
|
{"missing chatid", `{"from":"t","userid":"1","message":"m","plugin":"p","mode":"m"}`, false},
|
||||||
|
{"missing userid", `{"from":"t","chatid":"1","message":"m","plugin":"p","mode":"m"}`, false},
|
||||||
|
{"missing message", `{"from":"t","chatid":"1","userid":"1","plugin":"p","mode":"m"}`, false},
|
||||||
|
{"missing plugin", `{"from":"t","chatid":"1","userid":"1","message":"m","mode":"m"}`, false},
|
||||||
|
{"missing mode", `{"from":"t","chatid":"1","userid":"1","message":"m","plugin":"p"}`, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var msg rMsg
|
||||||
|
err := json.Unmarshal([]byte(tt.jsonStr), &msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected JSON error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if message has all required fields (the validation logic in MsgParser)
|
||||||
|
isValid := msg.From != "" && msg.Chatid != "" && msg.Userid != "" &&
|
||||||
|
msg.Message != "" && msg.Plugin != "" && msg.Mode != ""
|
||||||
|
|
||||||
|
if isValid != tt.expectValid {
|
||||||
|
t.Errorf("valid=%v, expected %v", isValid, tt.expectValid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,11 @@ package misc
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"aleesa-misc-go/internal/log"
|
||||||
|
|
||||||
"github.com/hjson/hjson-go"
|
"github.com/hjson/hjson-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ func ReadConfig() {
|
|||||||
executablePath, err := os.Executable()
|
executablePath, err := os.Executable()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error(fmt.Sprintf("Unable to get current executable path: %s", err))
|
log.Errorf("Unable to get current executable path: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
|
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
|
||||||
@@ -39,7 +40,7 @@ func ReadConfig() {
|
|||||||
|
|
||||||
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата
|
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата
|
||||||
if fileInfo.Size() > 65535 {
|
if fileInfo.Size() > 65535 {
|
||||||
slog.Warn(fmt.Sprintf("Config file %s is too long for config, skipping", location))
|
log.Warnf("Config file %s is too long for config, skipping", location)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -48,7 +49,7 @@ func ReadConfig() {
|
|||||||
|
|
||||||
// Не удалось прочитать, попробуем следующего кандидата
|
// Не удалось прочитать, попробуем следующего кандидата
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn(fmt.Sprintf("Skip reading config file %s: %s", location, err))
|
log.Warnf("Skip reading config file %s: %s", location, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -65,7 +66,7 @@ func ReadConfig() {
|
|||||||
|
|
||||||
// Не удалось распарсить - попробуем следующего кандидата
|
// Не удалось распарсить - попробуем следующего кандидата
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn(fmt.Sprintf("Skip parsing config file %s: %s", location, err))
|
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -74,13 +75,13 @@ func ReadConfig() {
|
|||||||
|
|
||||||
// Не удалось преобразовать map-ку в json
|
// Не удалось преобразовать map-ку в json
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn(fmt.Sprintf("Skip parsing config file %s: %s", location, err))
|
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
|
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
|
||||||
slog.Warn(fmt.Sprintf("Skip parsing config file %s: %s", location, err))
|
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -105,7 +106,9 @@ func ReadConfig() {
|
|||||||
// sampleConfig.Log = "" if not set
|
// sampleConfig.Log = "" if not set
|
||||||
|
|
||||||
if sampleConfig.Channel == "" {
|
if sampleConfig.Channel == "" {
|
||||||
slog.Error(fmt.Sprintf("Channel field in config file %s must be set", location))
|
log.Errorf("Channel field in config file %s must be set", location)
|
||||||
|
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Частичная проверка, ровно то, куда мы _точно_ щепрввляем сообщения исходя из бизнес-логики приложения
|
// Частичная проверка, ровно то, куда мы _точно_ щепрввляем сообщения исходя из бизнес-логики приложения
|
||||||
@@ -130,7 +133,9 @@ func ReadConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.Csign == "" {
|
if sampleConfig.Csign == "" {
|
||||||
slog.Error(fmt.Sprintf("Csign field in config file %s must be set", location))
|
log.Errorf("Csign field in config file %s must be set", location)
|
||||||
|
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.ForwardsMax == 0 {
|
if sampleConfig.ForwardsMax == 0 {
|
||||||
@@ -140,13 +145,13 @@ func ReadConfig() {
|
|||||||
Config = sampleConfig
|
Config = sampleConfig
|
||||||
configLoaded = true
|
configLoaded = true
|
||||||
|
|
||||||
slog.Info(fmt.Sprintf("Using %s as config file", location))
|
log.Infof("Using %s as config file", location)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if !configLoaded {
|
if !configLoaded {
|
||||||
slog.Error("Config was not loaded! Refusing to start.")
|
log.Error("Config was not loaded! Refusing to start.")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
327
internal/misc/readConfig_test.go
Normal file
327
internal/misc/readConfig_test.go
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
package misc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigValidation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config myConfig
|
||||||
|
expectedServer string
|
||||||
|
expectedPort int
|
||||||
|
expectValid bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "full valid config",
|
||||||
|
config: myConfig{
|
||||||
|
Server: "redis.example.com",
|
||||||
|
Port: 6380,
|
||||||
|
Channel: "test-channel",
|
||||||
|
Csign: "/",
|
||||||
|
},
|
||||||
|
expectedServer: "redis.example.com",
|
||||||
|
expectedPort: 6380,
|
||||||
|
expectValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config with defaults",
|
||||||
|
config: myConfig{
|
||||||
|
Channel: "test-channel",
|
||||||
|
Csign: "!",
|
||||||
|
},
|
||||||
|
expectedServer: "localhost",
|
||||||
|
expectedPort: 6379,
|
||||||
|
expectValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing channel",
|
||||||
|
config: myConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
Port: 6379,
|
||||||
|
Csign: "!",
|
||||||
|
},
|
||||||
|
expectValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing csign",
|
||||||
|
config: myConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
Port: 6379,
|
||||||
|
Channel: "test",
|
||||||
|
},
|
||||||
|
expectValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero port uses default",
|
||||||
|
config: myConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
Port: 0,
|
||||||
|
Channel: "test",
|
||||||
|
Csign: "!",
|
||||||
|
},
|
||||||
|
expectedServer: "localhost",
|
||||||
|
expectedPort: 6379,
|
||||||
|
expectValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero timeout uses default",
|
||||||
|
config: myConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
Port: 6379,
|
||||||
|
Timeout: 0,
|
||||||
|
Channel: "test",
|
||||||
|
Csign: "!",
|
||||||
|
},
|
||||||
|
expectedServer: "localhost",
|
||||||
|
expectedPort: 6379,
|
||||||
|
expectValid: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg := tt.config
|
||||||
|
valid := validateConfig(&cfg)
|
||||||
|
|
||||||
|
if valid != tt.expectValid {
|
||||||
|
t.Errorf("expected valid=%v, got %v", tt.expectValid, valid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.expectValid {
|
||||||
|
applyDefaults(&cfg)
|
||||||
|
if cfg.Server != tt.expectedServer {
|
||||||
|
t.Errorf("expected Server=%s, got %s", tt.expectedServer, cfg.Server)
|
||||||
|
}
|
||||||
|
if cfg.Port != tt.expectedPort {
|
||||||
|
t.Errorf("expected Port=%d, got %d", tt.expectedPort, cfg.Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardChannelsDefaults(t *testing.T) {
|
||||||
|
cfg := myConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
Port: 6379,
|
||||||
|
Channel: "test",
|
||||||
|
Csign: "!",
|
||||||
|
}
|
||||||
|
|
||||||
|
applyDefaults(&cfg)
|
||||||
|
|
||||||
|
if cfg.ForwardChannels.Games != "games" {
|
||||||
|
t.Errorf("expected games='games', got '%s'", cfg.ForwardChannels.Games)
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.Phrases != "phrases" {
|
||||||
|
t.Errorf("expected phrases='phrases', got '%s'", cfg.ForwardChannels.Phrases)
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.Webapp != "webapp" {
|
||||||
|
t.Errorf("expected webapp='webapp', got '%s'", cfg.ForwardChannels.Webapp)
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.WebappGo != "webapp-go" {
|
||||||
|
t.Errorf("expected webapp-go='webapp-go', got '%s'", cfg.ForwardChannels.WebappGo)
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.Craniac != "craniac" {
|
||||||
|
t.Errorf("expected craniac='craniac', got '%s'", cfg.ForwardChannels.Craniac)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardChannelsCustomValues(t *testing.T) {
|
||||||
|
cfg := myConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
Port: 6379,
|
||||||
|
Channel: "test",
|
||||||
|
Csign: "!",
|
||||||
|
ForwardChannels: struct {
|
||||||
|
Games string `json:"games,omitempty"`
|
||||||
|
Phrases string `json:"phrases,omitempty"`
|
||||||
|
Webapp string `json:"webapp,omitempty"`
|
||||||
|
WebappGo string `json:"webapp-go,omitempty"`
|
||||||
|
Craniac string `json:"craniac,omitempty"`
|
||||||
|
}{
|
||||||
|
Games: "my-games",
|
||||||
|
Phrases: "my-phrases",
|
||||||
|
Webapp: "my-webapp",
|
||||||
|
WebappGo: "my-webapp-go",
|
||||||
|
Craniac: "my-craniac",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyDefaults(&cfg)
|
||||||
|
|
||||||
|
if cfg.ForwardChannels.Games != "my-games" {
|
||||||
|
t.Errorf("expected games='my-games', got '%s'", cfg.ForwardChannels.Games)
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.Phrases != "my-phrases" {
|
||||||
|
t.Errorf("expected phrases='my-phrases', got '%s'", cfg.ForwardChannels.Phrases)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateConfig simulates the validation logic from ReadConfig
|
||||||
|
func validateConfig(cfg *myConfig) bool {
|
||||||
|
if cfg.Channel == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if cfg.Csign == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyDefaults simulates the default application logic from ReadConfig
|
||||||
|
func applyDefaults(cfg *myConfig) {
|
||||||
|
if cfg.Server == "" {
|
||||||
|
cfg.Server = "localhost"
|
||||||
|
}
|
||||||
|
if cfg.Port == 0 {
|
||||||
|
cfg.Port = 6379
|
||||||
|
}
|
||||||
|
if cfg.Timeout == 0 {
|
||||||
|
cfg.Timeout = 10
|
||||||
|
}
|
||||||
|
if cfg.Loglevel == "" {
|
||||||
|
cfg.Loglevel = "info"
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.Games == "" {
|
||||||
|
cfg.ForwardChannels.Games = "games"
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.Phrases == "" {
|
||||||
|
cfg.ForwardChannels.Phrases = "phrases"
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.Webapp == "" {
|
||||||
|
cfg.ForwardChannels.Webapp = "webapp"
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.WebappGo == "" {
|
||||||
|
cfg.ForwardChannels.WebappGo = "webapp-go"
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.Craniac == "" {
|
||||||
|
cfg.ForwardChannels.Craniac = "craniac"
|
||||||
|
}
|
||||||
|
if cfg.Csign == "" {
|
||||||
|
cfg.Csign = "!"
|
||||||
|
}
|
||||||
|
if cfg.ForwardsMax == 0 {
|
||||||
|
cfg.ForwardsMax = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoglevelValidation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
loglevel string
|
||||||
|
expectedOK bool
|
||||||
|
}{
|
||||||
|
{"valid error", "error", true},
|
||||||
|
{"valid warn", "warn", true},
|
||||||
|
{"valid info", "info", true},
|
||||||
|
{"valid debug", "debug", true},
|
||||||
|
{"invalid level", "trace", false},
|
||||||
|
{"empty uses default", "", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg := &myConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
Port: 6379,
|
||||||
|
Channel: "test",
|
||||||
|
Csign: "!",
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.loglevel != "" {
|
||||||
|
cfg.Loglevel = tt.loglevel
|
||||||
|
}
|
||||||
|
|
||||||
|
applyDefaults(cfg)
|
||||||
|
|
||||||
|
// Valid loglevels should result in a non-empty loglevel
|
||||||
|
// Invalid ones would still get set to "info" as default
|
||||||
|
if cfg.Loglevel == "" && tt.expectedOK {
|
||||||
|
t.Error("expected non-empty loglevel")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardsMaxDefault(t *testing.T) {
|
||||||
|
cfg := myConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
Port: 6379,
|
||||||
|
Channel: "test",
|
||||||
|
Csign: "!",
|
||||||
|
}
|
||||||
|
|
||||||
|
applyDefaults(&cfg)
|
||||||
|
|
||||||
|
if cfg.ForwardsMax != 5 {
|
||||||
|
t.Errorf("expected ForwardsMax=5, got %d", cfg.ForwardsMax)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.ForwardsMax = 10
|
||||||
|
applyDefaults(&cfg)
|
||||||
|
|
||||||
|
if cfg.ForwardsMax != 10 {
|
||||||
|
t.Errorf("expected ForwardsMax=10, got %d", cfg.ForwardsMax)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigJSONRoundtrip(t *testing.T) {
|
||||||
|
original := myConfig{
|
||||||
|
Server: "redis.example.com",
|
||||||
|
Port: 6380,
|
||||||
|
Timeout: 15,
|
||||||
|
Loglevel: "debug",
|
||||||
|
Log: "/var/log/test.log",
|
||||||
|
Channel: "test-channel",
|
||||||
|
Csign: "/",
|
||||||
|
ForwardsMax: 10,
|
||||||
|
ForwardChannels: struct {
|
||||||
|
Games string `json:"games,omitempty"`
|
||||||
|
Phrases string `json:"phrases,omitempty"`
|
||||||
|
Webapp string `json:"webapp,omitempty"`
|
||||||
|
WebappGo string `json:"webapp-go,omitempty"`
|
||||||
|
Craniac string `json:"craniac,omitempty"`
|
||||||
|
}{
|
||||||
|
Games: "custom-games",
|
||||||
|
Phrases: "custom-phrases",
|
||||||
|
Webapp: "custom-webapp",
|
||||||
|
WebappGo: "custom-webapp-go",
|
||||||
|
Craniac: "custom-craniac",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal
|
||||||
|
data, err := json.Marshal(original)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal
|
||||||
|
var restored myConfig
|
||||||
|
if err := json.Unmarshal(data, &restored); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare
|
||||||
|
if restored.Server != original.Server {
|
||||||
|
t.Errorf("Server: expected %s, got %s", original.Server, restored.Server)
|
||||||
|
}
|
||||||
|
if restored.Port != original.Port {
|
||||||
|
t.Errorf("Port: expected %d, got %d", original.Port, restored.Port)
|
||||||
|
}
|
||||||
|
if restored.Channel != original.Channel {
|
||||||
|
t.Errorf("Channel: expected %s, got %s", original.Channel, restored.Channel)
|
||||||
|
}
|
||||||
|
if restored.Csign != original.Csign {
|
||||||
|
t.Errorf("Csign: expected %s, got %s", original.Csign, restored.Csign)
|
||||||
|
}
|
||||||
|
if restored.ForwardsMax != original.ForwardsMax {
|
||||||
|
t.Errorf("ForwardsMax: expected %d, got %d", original.ForwardsMax, restored.ForwardsMax)
|
||||||
|
}
|
||||||
|
if restored.ForwardChannels.Games != original.ForwardChannels.Games {
|
||||||
|
t.Errorf("ForwardChannels.Games: expected %s, got %s", original.ForwardChannels.Games, restored.ForwardChannels.Games)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +1,43 @@
|
|||||||
package misc
|
package misc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"aleesa-misc-go/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SigHandler хэндлер сигналов закрывает все бд и сваливает из приложения.
|
// SigHandler хэндлер сигналов закрывает все бд и сваливает из приложения.
|
||||||
func SigHandler() {
|
func SigHandler() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
slog.Info("Install signal handler")
|
log.Info("Install signal handler")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var s = <-SigChan
|
var s = <-SigChan
|
||||||
switch s {
|
switch s {
|
||||||
case syscall.SIGINT:
|
case syscall.SIGINT:
|
||||||
slog.Info("Got SIGINT, quitting")
|
log.Info("Got SIGINT, quitting")
|
||||||
case syscall.SIGTERM:
|
case syscall.SIGTERM:
|
||||||
slog.Info("Got SIGTERM, quitting")
|
log.Info("Got SIGTERM, quitting")
|
||||||
case syscall.SIGQUIT:
|
case syscall.SIGQUIT:
|
||||||
slog.Info("Got SIGQUIT, quitting")
|
log.Info("Got SIGQUIT, quitting")
|
||||||
|
|
||||||
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
|
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Чтобы не срать в логи ошибками от редиски, проставим shutdown state приложения в true.
|
// Отменяем контекст редиски, чтобы все ожидающие операции завершились.
|
||||||
Shutdown = true
|
CancelRedisCtx()
|
||||||
|
|
||||||
// Отпишемся от всех каналов и закроем коннект к редиске.
|
// Отпишемся от всех каналов и закроем коннект к редиске.
|
||||||
if err = Subscriber.Unsubscribe(Ctx); err != nil {
|
if err = Subscriber.Unsubscribe(RedisCtx); err != nil {
|
||||||
slog.Error(fmt.Sprintf("Unable to unsubscribe from redis channels cleanly: %s", err))
|
log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = Subscriber.Close(); err != nil {
|
if err = Subscriber.Close(); err != nil {
|
||||||
slog.Error(fmt.Sprintf("Unable to close redis connection cleanly: %s", err))
|
log.Errorf("Unable to close redis connection cleanly: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ type rMsg struct {
|
|||||||
Answer int64 `json:"answer,omitempty"`
|
Answer int64 `json:"answer,omitempty"`
|
||||||
Botnick string `json:"bot_nick,omitempty"`
|
Botnick string `json:"bot_nick,omitempty"`
|
||||||
Csign string `json:"csign,omitempty"`
|
Csign string `json:"csign,omitempty"`
|
||||||
Fwdcnt int64 `json:"fwd_cnt,omitempty"`
|
Fwdcnt *int64 `json:"fwd_cnt,omitempty"`
|
||||||
GoodMorning int64 `json:"good_morning,omitempty"`
|
GoodMorning int64 `json:"good_morning,omitempty"`
|
||||||
Msgformat int64 `json:"msg_format,omitempty"`
|
Msgformat int64 `json:"msg_format,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
} `json:"Misc"`
|
} `json:"misc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
|
|||||||
348
internal/misc/types_test.go
Normal file
348
internal/misc/types_test.go
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
package misc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRMsgJSONDeserialization(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
jsonInput string
|
||||||
|
expectedFrom string
|
||||||
|
expectedChatid string
|
||||||
|
expectedPlugin string
|
||||||
|
expectCsignNil bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "full message with all fields",
|
||||||
|
jsonInput: `{"from":"telegram","chatid":"123","userid":"456","message":"hello","plugin":"telegram","mode":"private","misc":{"answer":1,"bot_nick":"aleesa","csign":"!","fwd_cnt":1,"good_morning":0,"msg_format":1,"username":"user"}}`,
|
||||||
|
expectedFrom: "telegram",
|
||||||
|
expectedChatid: "123",
|
||||||
|
expectedPlugin: "telegram",
|
||||||
|
expectCsignNil: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "minimal message",
|
||||||
|
jsonInput: `{"from":"irc","chatid":"#room","userid":"nick","message":"!karma","plugin":"irc","mode":"channel"}`,
|
||||||
|
expectedFrom: "irc",
|
||||||
|
expectedChatid: "#room",
|
||||||
|
expectedPlugin: "irc",
|
||||||
|
expectCsignNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "message with fwd_cnt zero",
|
||||||
|
jsonInput: `{"from":"jabber","chatid":"room@conference","userid":"user","message":"test","plugin":"jabber","mode":"group","misc":{"fwd_cnt":0}}`,
|
||||||
|
expectedFrom: "jabber",
|
||||||
|
expectedChatid: "room@conference",
|
||||||
|
expectedPlugin: "jabber",
|
||||||
|
expectCsignNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "message with threadid",
|
||||||
|
jsonInput: `{"from":"telegram","chatid":"123","userid":"456","threadid":"789","message":"reply","plugin":"telegram","mode":"thread"}`,
|
||||||
|
expectedFrom: "telegram",
|
||||||
|
expectedChatid: "123",
|
||||||
|
expectedPlugin: "telegram",
|
||||||
|
expectCsignNil: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var msg rMsg
|
||||||
|
if err := json.Unmarshal([]byte(tt.jsonInput), &msg); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.From != tt.expectedFrom {
|
||||||
|
t.Errorf("From: expected %s, got %s", tt.expectedFrom, msg.From)
|
||||||
|
}
|
||||||
|
if msg.Chatid != tt.expectedChatid {
|
||||||
|
t.Errorf("Chatid: expected %s, got %s", tt.expectedChatid, msg.Chatid)
|
||||||
|
}
|
||||||
|
if msg.Plugin != tt.expectedPlugin {
|
||||||
|
t.Errorf("Plugin: expected %s, got %s", tt.expectedPlugin, msg.Plugin)
|
||||||
|
}
|
||||||
|
if tt.expectCsignNil && msg.Misc.Csign != "" {
|
||||||
|
t.Errorf("Csign: expected empty, got %s", msg.Misc.Csign)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSMsgJSONSerialization(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
message sMsg
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "full message",
|
||||||
|
message: sMsg{
|
||||||
|
From: "telegram",
|
||||||
|
Chatid: "123",
|
||||||
|
Userid: "456",
|
||||||
|
Threadid: "789",
|
||||||
|
Message: "Hello world",
|
||||||
|
Plugin: "telegram",
|
||||||
|
Mode: "private",
|
||||||
|
Misc: struct {
|
||||||
|
Answer int64 `json:"answer"`
|
||||||
|
Botnick string `json:"bot_nick"`
|
||||||
|
Csign string `json:"csign"`
|
||||||
|
Fwdcnt int64 `json:"fwd_cnt"`
|
||||||
|
GoodMorning int64 `json:"good_morning"`
|
||||||
|
Msgformat int64 `json:"msg_format"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}{
|
||||||
|
Answer: 1,
|
||||||
|
Botnick: "aleesa",
|
||||||
|
Csign: "!",
|
||||||
|
Fwdcnt: 2,
|
||||||
|
GoodMorning: 0,
|
||||||
|
Msgformat: 1,
|
||||||
|
Username: "testuser",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "minimal message",
|
||||||
|
message: sMsg{
|
||||||
|
From: "irc",
|
||||||
|
Chatid: "#room",
|
||||||
|
Userid: "nick",
|
||||||
|
Message: "!help",
|
||||||
|
Plugin: "irc",
|
||||||
|
Mode: "channel",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
data, err := json.Marshal(tt.message)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it's valid JSON
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
t.Fatalf("marshaled data is not valid JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify key fields are present
|
||||||
|
if _, ok := result["from"]; !ok {
|
||||||
|
t.Error("missing 'from' field")
|
||||||
|
}
|
||||||
|
if _, ok := result["chatid"]; !ok {
|
||||||
|
t.Error("missing 'chatid' field")
|
||||||
|
}
|
||||||
|
if _, ok := result["message"]; !ok {
|
||||||
|
t.Error("missing 'message' field")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRMsgToSMsgConversion(t *testing.T) {
|
||||||
|
fwdcnt := new(int64)
|
||||||
|
*fwdcnt = 3
|
||||||
|
|
||||||
|
input := rMsg{
|
||||||
|
From: "telegram",
|
||||||
|
Chatid: "123",
|
||||||
|
Userid: "456",
|
||||||
|
Threadid: "789",
|
||||||
|
Message: "!friday",
|
||||||
|
Plugin: "telegram",
|
||||||
|
Mode: "private",
|
||||||
|
Misc: struct {
|
||||||
|
Answer int64 `json:"answer,omitempty"`
|
||||||
|
Botnick string `json:"bot_nick,omitempty"`
|
||||||
|
Csign string `json:"csign,omitempty"`
|
||||||
|
Fwdcnt *int64 `json:"fwd_cnt,omitempty"`
|
||||||
|
GoodMorning int64 `json:"good_morning,omitempty"`
|
||||||
|
Msgformat int64 `json:"msg_format,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
}{
|
||||||
|
Answer: 0,
|
||||||
|
Botnick: "aleesa",
|
||||||
|
Csign: "!",
|
||||||
|
Fwdcnt: fwdcnt,
|
||||||
|
GoodMorning: 1,
|
||||||
|
Msgformat: 1,
|
||||||
|
Username: "testuser",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert
|
||||||
|
output := convertToSMsg(input)
|
||||||
|
|
||||||
|
// Verify all fields transferred
|
||||||
|
if output.From != input.From {
|
||||||
|
t.Errorf("From: expected %s, got %s", input.From, output.From)
|
||||||
|
}
|
||||||
|
if output.Chatid != input.Chatid {
|
||||||
|
t.Errorf("Chatid: expected %s, got %s", input.Chatid, output.Chatid)
|
||||||
|
}
|
||||||
|
if output.Userid != input.Userid {
|
||||||
|
t.Errorf("Userid: expected %s, got %s", input.Userid, output.Userid)
|
||||||
|
}
|
||||||
|
if output.Threadid != input.Threadid {
|
||||||
|
t.Errorf("Threadid: expected %s, got %s", input.Threadid, output.Threadid)
|
||||||
|
}
|
||||||
|
if output.Message != input.Message {
|
||||||
|
t.Errorf("Message: expected %s, got %s", input.Message, output.Message)
|
||||||
|
}
|
||||||
|
if output.Plugin != input.Plugin {
|
||||||
|
t.Errorf("Plugin: expected %s, got %s", input.Plugin, output.Plugin)
|
||||||
|
}
|
||||||
|
if output.Mode != input.Mode {
|
||||||
|
t.Errorf("Mode: expected %s, got %s", input.Mode, output.Mode)
|
||||||
|
}
|
||||||
|
if output.Misc.Csign != input.Misc.Csign {
|
||||||
|
t.Errorf("Misc.Csign: expected %s, got %s", input.Misc.Csign, output.Misc.Csign)
|
||||||
|
}
|
||||||
|
if output.Misc.Fwdcnt != *input.Misc.Fwdcnt {
|
||||||
|
t.Errorf("Misc.Fwdcnt: expected %d, got %d", *input.Misc.Fwdcnt, output.Misc.Fwdcnt)
|
||||||
|
}
|
||||||
|
if output.Misc.Username != input.Misc.Username {
|
||||||
|
t.Errorf("Misc.Username: expected %s, got %s", input.Misc.Username, output.Misc.Username)
|
||||||
|
}
|
||||||
|
if output.Misc.Answer != input.Misc.Answer {
|
||||||
|
t.Errorf("Misc.Answer: expected %d, got %d", input.Misc.Answer, output.Misc.Answer)
|
||||||
|
}
|
||||||
|
if output.Misc.Botnick != input.Misc.Botnick {
|
||||||
|
t.Errorf("Misc.Botnick: expected %s, got %s", input.Misc.Botnick, output.Misc.Botnick)
|
||||||
|
}
|
||||||
|
if output.Misc.Msgformat != input.Misc.Msgformat {
|
||||||
|
t.Errorf("Misc.Msgformat: expected %d, got %d", input.Misc.Msgformat, output.Misc.Msgformat)
|
||||||
|
}
|
||||||
|
if output.Misc.GoodMorning != input.Misc.GoodMorning {
|
||||||
|
t.Errorf("Misc.GoodMorning: expected %d, got %d", input.Misc.GoodMorning, output.Misc.GoodMorning)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertToSMsg simulates the conversion logic from msgParser.go
|
||||||
|
func convertToSMsg(j rMsg) sMsg {
|
||||||
|
var message sMsg
|
||||||
|
message.From = j.From
|
||||||
|
message.Userid = j.Userid
|
||||||
|
message.Chatid = j.Chatid
|
||||||
|
message.Threadid = j.Threadid
|
||||||
|
message.Message = j.Message
|
||||||
|
message.Plugin = j.Plugin
|
||||||
|
message.Mode = j.Mode
|
||||||
|
message.Misc.Fwdcnt = *j.Misc.Fwdcnt
|
||||||
|
message.Misc.Csign = j.Misc.Csign
|
||||||
|
message.Misc.Username = j.Misc.Username
|
||||||
|
message.Misc.Answer = j.Misc.Answer
|
||||||
|
message.Misc.Botnick = j.Misc.Botnick
|
||||||
|
message.Misc.Msgformat = j.Misc.Msgformat
|
||||||
|
message.Misc.GoodMorning = j.Misc.GoodMorning
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONFieldNaming(t *testing.T) {
|
||||||
|
// Test that sMsg uses lowercase "misc" in JSON
|
||||||
|
msg := sMsg{
|
||||||
|
From: "test",
|
||||||
|
Chatid: "1",
|
||||||
|
Userid: "2",
|
||||||
|
Message: "hello",
|
||||||
|
Plugin: "test",
|
||||||
|
Mode: "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for lowercase "misc"
|
||||||
|
if !contains(string(data), `"misc"`) {
|
||||||
|
t.Errorf("expected lowercase 'misc' in JSON, got: %s", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure it doesn't contain capitalized "Misc"
|
||||||
|
if contains(string(data), `"Misc"`) {
|
||||||
|
t.Errorf("should not contain capitalized 'Misc' in JSON, got: %s", string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(s, substr string) bool {
|
||||||
|
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsAt(s, substr string) bool {
|
||||||
|
for i := 0; i <= len(s)-len(substr); i++ {
|
||||||
|
if s[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRMsgFwdcntPointer(t *testing.T) {
|
||||||
|
// Test that Fwdcnt as pointer works correctly with JSON
|
||||||
|
jsonInput := `{"from":"test","chatid":"1","userid":"1","message":"test","plugin":"test","mode":"test","misc":{"fwd_cnt":5}}`
|
||||||
|
|
||||||
|
var msg rMsg
|
||||||
|
if err := json.Unmarshal([]byte(jsonInput), &msg); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Misc.Fwdcnt == nil {
|
||||||
|
t.Fatal("Fwdcnt should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *msg.Misc.Fwdcnt != 5 {
|
||||||
|
t.Errorf("expected Fwdcnt=5, got %d", *msg.Misc.Fwdcnt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRMsgFwdcntNilWhenOmitted(t *testing.T) {
|
||||||
|
jsonInput := `{"from":"test","chatid":"1","userid":"1","message":"test","plugin":"test","mode":"test"}`
|
||||||
|
|
||||||
|
var msg rMsg
|
||||||
|
if err := json.Unmarshal([]byte(jsonInput), &msg); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Misc.Fwdcnt != nil {
|
||||||
|
t.Errorf("Fwdcnt should be nil when omitted, got %d", *msg.Misc.Fwdcnt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMyConfigForwardChannels(t *testing.T) {
|
||||||
|
cfg := myConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
Port: 6379,
|
||||||
|
Channel: "test",
|
||||||
|
Csign: "!",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that struct can hold custom values
|
||||||
|
cfg.ForwardChannels.Games = "custom-games"
|
||||||
|
cfg.ForwardChannels.Phrases = "custom-phrases"
|
||||||
|
cfg.ForwardChannels.Webapp = "custom-webapp"
|
||||||
|
cfg.ForwardChannels.WebappGo = "custom-webapp-go"
|
||||||
|
cfg.ForwardChannels.Craniac = "custom-craniac"
|
||||||
|
|
||||||
|
if cfg.ForwardChannels.Games != "custom-games" {
|
||||||
|
t.Errorf("Games: expected custom-games, got %s", cfg.ForwardChannels.Games)
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.Phrases != "custom-phrases" {
|
||||||
|
t.Errorf("Phrases: expected custom-phrases, got %s", cfg.ForwardChannels.Phrases)
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.Webapp != "custom-webapp" {
|
||||||
|
t.Errorf("Webapp: expected custom-webapp, got %s", cfg.ForwardChannels.Webapp)
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.WebappGo != "custom-webapp-go" {
|
||||||
|
t.Errorf("WebappGo: expected custom-webapp-go, got %s", cfg.ForwardChannels.WebappGo)
|
||||||
|
}
|
||||||
|
if cfg.ForwardChannels.Craniac != "custom-craniac" {
|
||||||
|
t.Errorf("Craniac: expected custom-craniac, got %s", cfg.ForwardChannels.Craniac)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
vendor/github.com/cespare/xxhash/v2/README.md
generated
vendored
31
vendor/github.com/cespare/xxhash/v2/README.md
generated
vendored
@@ -3,7 +3,8 @@
|
|||||||
[](https://pkg.go.dev/github.com/cespare/xxhash/v2)
|
[](https://pkg.go.dev/github.com/cespare/xxhash/v2)
|
||||||
[](https://github.com/cespare/xxhash/actions/workflows/test.yml)
|
[](https://github.com/cespare/xxhash/actions/workflows/test.yml)
|
||||||
|
|
||||||
xxhash is a Go implementation of the 64-bit [xxHash] algorithm, XXH64. This is a
|
xxhash is a Go implementation of the 64-bit
|
||||||
|
[xxHash](http://cyan4973.github.io/xxHash/) algorithm, XXH64. This is a
|
||||||
high-quality hashing algorithm that is much faster than anything in the Go
|
high-quality hashing algorithm that is much faster than anything in the Go
|
||||||
standard library.
|
standard library.
|
||||||
|
|
||||||
@@ -24,11 +25,8 @@ func (*Digest) WriteString(string) (int, error)
|
|||||||
func (*Digest) Sum64() uint64
|
func (*Digest) Sum64() uint64
|
||||||
```
|
```
|
||||||
|
|
||||||
The package is written with optimized pure Go and also contains even faster
|
This implementation provides a fast pure-Go implementation and an even faster
|
||||||
assembly implementations for amd64 and arm64. If desired, the `purego` build tag
|
assembly implementation for amd64.
|
||||||
opts into using the Go code even on those architectures.
|
|
||||||
|
|
||||||
[xxHash]: http://cyan4973.github.io/xxHash/
|
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
@@ -48,19 +46,18 @@ Here are some quick benchmarks comparing the pure-Go and assembly
|
|||||||
implementations of Sum64.
|
implementations of Sum64.
|
||||||
|
|
||||||
| input size | purego | asm |
|
| input size | purego | asm |
|
||||||
| ---------- | --------- | --------- |
|
| --- | --- | --- |
|
||||||
| 4 B | 1.3 GB/s | 1.2 GB/s |
|
| 5 B | 979.66 MB/s | 1291.17 MB/s |
|
||||||
| 16 B | 2.9 GB/s | 3.5 GB/s |
|
| 100 B | 7475.26 MB/s | 7973.40 MB/s |
|
||||||
| 100 B | 6.9 GB/s | 8.1 GB/s |
|
| 4 KB | 17573.46 MB/s | 17602.65 MB/s |
|
||||||
| 4 KB | 11.7 GB/s | 16.7 GB/s |
|
| 10 MB | 17131.46 MB/s | 17142.16 MB/s |
|
||||||
| 10 MB | 12.0 GB/s | 17.3 GB/s |
|
|
||||||
|
|
||||||
These numbers were generated on Ubuntu 20.04 with an Intel Xeon Platinum 8252C
|
These numbers were generated on Ubuntu 18.04 with an Intel i7-8700K CPU using
|
||||||
CPU using the following commands under Go 1.19.2:
|
the following commands under Go 1.11.2:
|
||||||
|
|
||||||
```
|
```
|
||||||
benchstat <(go test -tags purego -benchtime 500ms -count 15 -bench 'Sum64$')
|
$ go test -tags purego -benchtime 10s -bench '/xxhash,direct,bytes'
|
||||||
benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$')
|
$ go test -benchtime 10s -bench '/xxhash,direct,bytes'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Projects using this package
|
## Projects using this package
|
||||||
@@ -70,5 +67,3 @@ benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$')
|
|||||||
- [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)
|
- [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)
|
||||||
- [FreeCache](https://github.com/coocood/freecache)
|
- [FreeCache](https://github.com/coocood/freecache)
|
||||||
- [FastCache](https://github.com/VictoriaMetrics/fastcache)
|
- [FastCache](https://github.com/VictoriaMetrics/fastcache)
|
||||||
- [Ristretto](https://github.com/dgraph-io/ristretto)
|
|
||||||
- [Badger](https://github.com/dgraph-io/badger)
|
|
||||||
|
|||||||
10
vendor/github.com/cespare/xxhash/v2/testall.sh
generated
vendored
10
vendor/github.com/cespare/xxhash/v2/testall.sh
generated
vendored
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -eu -o pipefail
|
|
||||||
|
|
||||||
# Small convenience script for running the tests with various combinations of
|
|
||||||
# arch/tags. This assumes we're running on amd64 and have qemu available.
|
|
||||||
|
|
||||||
go test ./...
|
|
||||||
go test -tags purego ./...
|
|
||||||
GOARCH=arm64 go test
|
|
||||||
GOARCH=arm64 go test -tags purego
|
|
||||||
70
vendor/github.com/cespare/xxhash/v2/xxhash.go
generated
vendored
70
vendor/github.com/cespare/xxhash/v2/xxhash.go
generated
vendored
@@ -16,16 +16,21 @@ const (
|
|||||||
prime5 uint64 = 2870177450012600261
|
prime5 uint64 = 2870177450012600261
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store the primes in an array as well.
|
// NOTE(caleb): I'm using both consts and vars of the primes. Using consts where
|
||||||
//
|
// possible in the Go code is worth a small (but measurable) performance boost
|
||||||
// The consts are used when possible in Go code to avoid MOVs but we need a
|
// by avoiding some MOVQs. Vars are needed for the asm and also are useful for
|
||||||
// contiguous array for the assembly code.
|
// convenience in the Go code in a few places where we need to intentionally
|
||||||
var primes = [...]uint64{prime1, prime2, prime3, prime4, prime5}
|
// avoid constant arithmetic (e.g., v1 := prime1 + prime2 fails because the
|
||||||
|
// result overflows a uint64).
|
||||||
|
var (
|
||||||
|
prime1v = prime1
|
||||||
|
prime2v = prime2
|
||||||
|
prime3v = prime3
|
||||||
|
prime4v = prime4
|
||||||
|
prime5v = prime5
|
||||||
|
)
|
||||||
|
|
||||||
// Digest implements hash.Hash64.
|
// Digest implements hash.Hash64.
|
||||||
//
|
|
||||||
// Note that a zero-valued Digest is not ready to receive writes.
|
|
||||||
// Call Reset or create a Digest using New before calling other methods.
|
|
||||||
type Digest struct {
|
type Digest struct {
|
||||||
v1 uint64
|
v1 uint64
|
||||||
v2 uint64
|
v2 uint64
|
||||||
@@ -36,31 +41,19 @@ type Digest struct {
|
|||||||
n int // how much of mem is used
|
n int // how much of mem is used
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Digest with a zero seed.
|
// New creates a new Digest that computes the 64-bit xxHash algorithm.
|
||||||
func New() *Digest {
|
func New() *Digest {
|
||||||
return NewWithSeed(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWithSeed creates a new Digest with the given seed.
|
|
||||||
func NewWithSeed(seed uint64) *Digest {
|
|
||||||
var d Digest
|
var d Digest
|
||||||
d.ResetWithSeed(seed)
|
d.Reset()
|
||||||
return &d
|
return &d
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset clears the Digest's state so that it can be reused.
|
// Reset clears the Digest's state so that it can be reused.
|
||||||
// It uses a seed value of zero.
|
|
||||||
func (d *Digest) Reset() {
|
func (d *Digest) Reset() {
|
||||||
d.ResetWithSeed(0)
|
d.v1 = prime1v + prime2
|
||||||
}
|
d.v2 = prime2
|
||||||
|
d.v3 = 0
|
||||||
// ResetWithSeed clears the Digest's state so that it can be reused.
|
d.v4 = -prime1v
|
||||||
// It uses the given seed to initialize the state.
|
|
||||||
func (d *Digest) ResetWithSeed(seed uint64) {
|
|
||||||
d.v1 = seed + prime1 + prime2
|
|
||||||
d.v2 = seed + prime2
|
|
||||||
d.v3 = seed
|
|
||||||
d.v4 = seed - prime1
|
|
||||||
d.total = 0
|
d.total = 0
|
||||||
d.n = 0
|
d.n = 0
|
||||||
}
|
}
|
||||||
@@ -76,23 +69,21 @@ func (d *Digest) Write(b []byte) (n int, err error) {
|
|||||||
n = len(b)
|
n = len(b)
|
||||||
d.total += uint64(n)
|
d.total += uint64(n)
|
||||||
|
|
||||||
memleft := d.mem[d.n&(len(d.mem)-1):]
|
|
||||||
|
|
||||||
if d.n+n < 32 {
|
if d.n+n < 32 {
|
||||||
// This new data doesn't even fill the current block.
|
// This new data doesn't even fill the current block.
|
||||||
copy(memleft, b)
|
copy(d.mem[d.n:], b)
|
||||||
d.n += n
|
d.n += n
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.n > 0 {
|
if d.n > 0 {
|
||||||
// Finish off the partial block.
|
// Finish off the partial block.
|
||||||
c := copy(memleft, b)
|
copy(d.mem[d.n:], b)
|
||||||
d.v1 = round(d.v1, u64(d.mem[0:8]))
|
d.v1 = round(d.v1, u64(d.mem[0:8]))
|
||||||
d.v2 = round(d.v2, u64(d.mem[8:16]))
|
d.v2 = round(d.v2, u64(d.mem[8:16]))
|
||||||
d.v3 = round(d.v3, u64(d.mem[16:24]))
|
d.v3 = round(d.v3, u64(d.mem[16:24]))
|
||||||
d.v4 = round(d.v4, u64(d.mem[24:32]))
|
d.v4 = round(d.v4, u64(d.mem[24:32]))
|
||||||
b = b[c:]
|
b = b[32-d.n:]
|
||||||
d.n = 0
|
d.n = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,20 +133,21 @@ func (d *Digest) Sum64() uint64 {
|
|||||||
|
|
||||||
h += d.total
|
h += d.total
|
||||||
|
|
||||||
b := d.mem[:d.n&(len(d.mem)-1)]
|
i, end := 0, d.n
|
||||||
for ; len(b) >= 8; b = b[8:] {
|
for ; i+8 <= end; i += 8 {
|
||||||
k1 := round(0, u64(b[:8]))
|
k1 := round(0, u64(d.mem[i:i+8]))
|
||||||
h ^= k1
|
h ^= k1
|
||||||
h = rol27(h)*prime1 + prime4
|
h = rol27(h)*prime1 + prime4
|
||||||
}
|
}
|
||||||
if len(b) >= 4 {
|
if i+4 <= end {
|
||||||
h ^= uint64(u32(b[:4])) * prime1
|
h ^= uint64(u32(d.mem[i:i+4])) * prime1
|
||||||
h = rol23(h)*prime2 + prime3
|
h = rol23(h)*prime2 + prime3
|
||||||
b = b[4:]
|
i += 4
|
||||||
}
|
}
|
||||||
for ; len(b) > 0; b = b[1:] {
|
for i < end {
|
||||||
h ^= uint64(b[0]) * prime5
|
h ^= uint64(d.mem[i]) * prime5
|
||||||
h = rol11(h) * prime1
|
h = rol11(h) * prime1
|
||||||
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
h ^= h >> 33
|
h ^= h >> 33
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
//go:build (amd64 || arm64) && !appengine && gc && !purego
|
|
||||||
// +build amd64 arm64
|
|
||||||
// +build !appengine
|
// +build !appengine
|
||||||
// +build gc
|
// +build gc
|
||||||
// +build !purego
|
// +build !purego
|
||||||
|
|
||||||
package xxhash
|
package xxhash
|
||||||
|
|
||||||
// Sum64 computes the 64-bit xxHash digest of b with a zero seed.
|
// Sum64 computes the 64-bit xxHash digest of b.
|
||||||
//
|
//
|
||||||
//go:noescape
|
//go:noescape
|
||||||
func Sum64(b []byte) uint64
|
func Sum64(b []byte) uint64
|
||||||
308
vendor/github.com/cespare/xxhash/v2/xxhash_amd64.s
generated
vendored
308
vendor/github.com/cespare/xxhash/v2/xxhash_amd64.s
generated
vendored
@@ -1,209 +1,215 @@
|
|||||||
//go:build !appengine && gc && !purego
|
|
||||||
// +build !appengine
|
// +build !appengine
|
||||||
// +build gc
|
// +build gc
|
||||||
// +build !purego
|
// +build !purego
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
// Registers:
|
// Register allocation:
|
||||||
#define h AX
|
// AX h
|
||||||
#define d AX
|
// SI pointer to advance through b
|
||||||
#define p SI // pointer to advance through b
|
// DX n
|
||||||
#define n DX
|
// BX loop end
|
||||||
#define end BX // loop end
|
// R8 v1, k1
|
||||||
#define v1 R8
|
// R9 v2
|
||||||
#define v2 R9
|
// R10 v3
|
||||||
#define v3 R10
|
// R11 v4
|
||||||
#define v4 R11
|
// R12 tmp
|
||||||
#define x R12
|
// R13 prime1v
|
||||||
#define prime1 R13
|
// R14 prime2v
|
||||||
#define prime2 R14
|
// DI prime4v
|
||||||
#define prime4 DI
|
|
||||||
|
|
||||||
#define round(acc, x) \
|
// round reads from and advances the buffer pointer in SI.
|
||||||
IMULQ prime2, x \
|
// It assumes that R13 has prime1v and R14 has prime2v.
|
||||||
ADDQ x, acc \
|
#define round(r) \
|
||||||
ROLQ $31, acc \
|
MOVQ (SI), R12 \
|
||||||
IMULQ prime1, acc
|
ADDQ $8, SI \
|
||||||
|
IMULQ R14, R12 \
|
||||||
|
ADDQ R12, r \
|
||||||
|
ROLQ $31, r \
|
||||||
|
IMULQ R13, r
|
||||||
|
|
||||||
// round0 performs the operation x = round(0, x).
|
// mergeRound applies a merge round on the two registers acc and val.
|
||||||
#define round0(x) \
|
// It assumes that R13 has prime1v, R14 has prime2v, and DI has prime4v.
|
||||||
IMULQ prime2, x \
|
#define mergeRound(acc, val) \
|
||||||
ROLQ $31, x \
|
IMULQ R14, val \
|
||||||
IMULQ prime1, x
|
ROLQ $31, val \
|
||||||
|
IMULQ R13, val \
|
||||||
// mergeRound applies a merge round on the two registers acc and x.
|
XORQ val, acc \
|
||||||
// It assumes that prime1, prime2, and prime4 have been loaded.
|
IMULQ R13, acc \
|
||||||
#define mergeRound(acc, x) \
|
ADDQ DI, acc
|
||||||
round0(x) \
|
|
||||||
XORQ x, acc \
|
|
||||||
IMULQ prime1, acc \
|
|
||||||
ADDQ prime4, acc
|
|
||||||
|
|
||||||
// blockLoop processes as many 32-byte blocks as possible,
|
|
||||||
// updating v1, v2, v3, and v4. It assumes that there is at least one block
|
|
||||||
// to process.
|
|
||||||
#define blockLoop() \
|
|
||||||
loop: \
|
|
||||||
MOVQ +0(p), x \
|
|
||||||
round(v1, x) \
|
|
||||||
MOVQ +8(p), x \
|
|
||||||
round(v2, x) \
|
|
||||||
MOVQ +16(p), x \
|
|
||||||
round(v3, x) \
|
|
||||||
MOVQ +24(p), x \
|
|
||||||
round(v4, x) \
|
|
||||||
ADDQ $32, p \
|
|
||||||
CMPQ p, end \
|
|
||||||
JLE loop
|
|
||||||
|
|
||||||
// func Sum64(b []byte) uint64
|
// func Sum64(b []byte) uint64
|
||||||
TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32
|
TEXT ·Sum64(SB), NOSPLIT, $0-32
|
||||||
// Load fixed primes.
|
// Load fixed primes.
|
||||||
MOVQ ·primes+0(SB), prime1
|
MOVQ ·prime1v(SB), R13
|
||||||
MOVQ ·primes+8(SB), prime2
|
MOVQ ·prime2v(SB), R14
|
||||||
MOVQ ·primes+24(SB), prime4
|
MOVQ ·prime4v(SB), DI
|
||||||
|
|
||||||
// Load slice.
|
// Load slice.
|
||||||
MOVQ b_base+0(FP), p
|
MOVQ b_base+0(FP), SI
|
||||||
MOVQ b_len+8(FP), n
|
MOVQ b_len+8(FP), DX
|
||||||
LEAQ (p)(n*1), end
|
LEAQ (SI)(DX*1), BX
|
||||||
|
|
||||||
// The first loop limit will be len(b)-32.
|
// The first loop limit will be len(b)-32.
|
||||||
SUBQ $32, end
|
SUBQ $32, BX
|
||||||
|
|
||||||
// Check whether we have at least one block.
|
// Check whether we have at least one block.
|
||||||
CMPQ n, $32
|
CMPQ DX, $32
|
||||||
JLT noBlocks
|
JLT noBlocks
|
||||||
|
|
||||||
// Set up initial state (v1, v2, v3, v4).
|
// Set up initial state (v1, v2, v3, v4).
|
||||||
MOVQ prime1, v1
|
MOVQ R13, R8
|
||||||
ADDQ prime2, v1
|
ADDQ R14, R8
|
||||||
MOVQ prime2, v2
|
MOVQ R14, R9
|
||||||
XORQ v3, v3
|
XORQ R10, R10
|
||||||
XORQ v4, v4
|
XORQ R11, R11
|
||||||
SUBQ prime1, v4
|
SUBQ R13, R11
|
||||||
|
|
||||||
blockLoop()
|
// Loop until SI > BX.
|
||||||
|
blockLoop:
|
||||||
|
round(R8)
|
||||||
|
round(R9)
|
||||||
|
round(R10)
|
||||||
|
round(R11)
|
||||||
|
|
||||||
MOVQ v1, h
|
CMPQ SI, BX
|
||||||
ROLQ $1, h
|
JLE blockLoop
|
||||||
MOVQ v2, x
|
|
||||||
ROLQ $7, x
|
|
||||||
ADDQ x, h
|
|
||||||
MOVQ v3, x
|
|
||||||
ROLQ $12, x
|
|
||||||
ADDQ x, h
|
|
||||||
MOVQ v4, x
|
|
||||||
ROLQ $18, x
|
|
||||||
ADDQ x, h
|
|
||||||
|
|
||||||
mergeRound(h, v1)
|
MOVQ R8, AX
|
||||||
mergeRound(h, v2)
|
ROLQ $1, AX
|
||||||
mergeRound(h, v3)
|
MOVQ R9, R12
|
||||||
mergeRound(h, v4)
|
ROLQ $7, R12
|
||||||
|
ADDQ R12, AX
|
||||||
|
MOVQ R10, R12
|
||||||
|
ROLQ $12, R12
|
||||||
|
ADDQ R12, AX
|
||||||
|
MOVQ R11, R12
|
||||||
|
ROLQ $18, R12
|
||||||
|
ADDQ R12, AX
|
||||||
|
|
||||||
|
mergeRound(AX, R8)
|
||||||
|
mergeRound(AX, R9)
|
||||||
|
mergeRound(AX, R10)
|
||||||
|
mergeRound(AX, R11)
|
||||||
|
|
||||||
JMP afterBlocks
|
JMP afterBlocks
|
||||||
|
|
||||||
noBlocks:
|
noBlocks:
|
||||||
MOVQ ·primes+32(SB), h
|
MOVQ ·prime5v(SB), AX
|
||||||
|
|
||||||
afterBlocks:
|
afterBlocks:
|
||||||
ADDQ n, h
|
ADDQ DX, AX
|
||||||
|
|
||||||
ADDQ $24, end
|
// Right now BX has len(b)-32, and we want to loop until SI > len(b)-8.
|
||||||
CMPQ p, end
|
ADDQ $24, BX
|
||||||
JG try4
|
|
||||||
|
|
||||||
loop8:
|
CMPQ SI, BX
|
||||||
MOVQ (p), x
|
JG fourByte
|
||||||
ADDQ $8, p
|
|
||||||
round0(x)
|
|
||||||
XORQ x, h
|
|
||||||
ROLQ $27, h
|
|
||||||
IMULQ prime1, h
|
|
||||||
ADDQ prime4, h
|
|
||||||
|
|
||||||
CMPQ p, end
|
wordLoop:
|
||||||
JLE loop8
|
// Calculate k1.
|
||||||
|
MOVQ (SI), R8
|
||||||
|
ADDQ $8, SI
|
||||||
|
IMULQ R14, R8
|
||||||
|
ROLQ $31, R8
|
||||||
|
IMULQ R13, R8
|
||||||
|
|
||||||
try4:
|
XORQ R8, AX
|
||||||
ADDQ $4, end
|
ROLQ $27, AX
|
||||||
CMPQ p, end
|
IMULQ R13, AX
|
||||||
JG try1
|
ADDQ DI, AX
|
||||||
|
|
||||||
MOVL (p), x
|
CMPQ SI, BX
|
||||||
ADDQ $4, p
|
JLE wordLoop
|
||||||
IMULQ prime1, x
|
|
||||||
XORQ x, h
|
|
||||||
|
|
||||||
ROLQ $23, h
|
fourByte:
|
||||||
IMULQ prime2, h
|
ADDQ $4, BX
|
||||||
ADDQ ·primes+16(SB), h
|
CMPQ SI, BX
|
||||||
|
JG singles
|
||||||
|
|
||||||
try1:
|
MOVL (SI), R8
|
||||||
ADDQ $4, end
|
ADDQ $4, SI
|
||||||
CMPQ p, end
|
IMULQ R13, R8
|
||||||
|
XORQ R8, AX
|
||||||
|
|
||||||
|
ROLQ $23, AX
|
||||||
|
IMULQ R14, AX
|
||||||
|
ADDQ ·prime3v(SB), AX
|
||||||
|
|
||||||
|
singles:
|
||||||
|
ADDQ $4, BX
|
||||||
|
CMPQ SI, BX
|
||||||
JGE finalize
|
JGE finalize
|
||||||
|
|
||||||
loop1:
|
singlesLoop:
|
||||||
MOVBQZX (p), x
|
MOVBQZX (SI), R12
|
||||||
ADDQ $1, p
|
ADDQ $1, SI
|
||||||
IMULQ ·primes+32(SB), x
|
IMULQ ·prime5v(SB), R12
|
||||||
XORQ x, h
|
XORQ R12, AX
|
||||||
ROLQ $11, h
|
|
||||||
IMULQ prime1, h
|
|
||||||
|
|
||||||
CMPQ p, end
|
ROLQ $11, AX
|
||||||
JL loop1
|
IMULQ R13, AX
|
||||||
|
|
||||||
|
CMPQ SI, BX
|
||||||
|
JL singlesLoop
|
||||||
|
|
||||||
finalize:
|
finalize:
|
||||||
MOVQ h, x
|
MOVQ AX, R12
|
||||||
SHRQ $33, x
|
SHRQ $33, R12
|
||||||
XORQ x, h
|
XORQ R12, AX
|
||||||
IMULQ prime2, h
|
IMULQ R14, AX
|
||||||
MOVQ h, x
|
MOVQ AX, R12
|
||||||
SHRQ $29, x
|
SHRQ $29, R12
|
||||||
XORQ x, h
|
XORQ R12, AX
|
||||||
IMULQ ·primes+16(SB), h
|
IMULQ ·prime3v(SB), AX
|
||||||
MOVQ h, x
|
MOVQ AX, R12
|
||||||
SHRQ $32, x
|
SHRQ $32, R12
|
||||||
XORQ x, h
|
XORQ R12, AX
|
||||||
|
|
||||||
MOVQ h, ret+24(FP)
|
MOVQ AX, ret+24(FP)
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
// writeBlocks uses the same registers as above except that it uses AX to store
|
||||||
|
// the d pointer.
|
||||||
|
|
||||||
// func writeBlocks(d *Digest, b []byte) int
|
// func writeBlocks(d *Digest, b []byte) int
|
||||||
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40
|
TEXT ·writeBlocks(SB), NOSPLIT, $0-40
|
||||||
// Load fixed primes needed for round.
|
// Load fixed primes needed for round.
|
||||||
MOVQ ·primes+0(SB), prime1
|
MOVQ ·prime1v(SB), R13
|
||||||
MOVQ ·primes+8(SB), prime2
|
MOVQ ·prime2v(SB), R14
|
||||||
|
|
||||||
// Load slice.
|
// Load slice.
|
||||||
MOVQ b_base+8(FP), p
|
MOVQ b_base+8(FP), SI
|
||||||
MOVQ b_len+16(FP), n
|
MOVQ b_len+16(FP), DX
|
||||||
LEAQ (p)(n*1), end
|
LEAQ (SI)(DX*1), BX
|
||||||
SUBQ $32, end
|
SUBQ $32, BX
|
||||||
|
|
||||||
// Load vN from d.
|
// Load vN from d.
|
||||||
MOVQ s+0(FP), d
|
MOVQ d+0(FP), AX
|
||||||
MOVQ 0(d), v1
|
MOVQ 0(AX), R8 // v1
|
||||||
MOVQ 8(d), v2
|
MOVQ 8(AX), R9 // v2
|
||||||
MOVQ 16(d), v3
|
MOVQ 16(AX), R10 // v3
|
||||||
MOVQ 24(d), v4
|
MOVQ 24(AX), R11 // v4
|
||||||
|
|
||||||
// We don't need to check the loop condition here; this function is
|
// We don't need to check the loop condition here; this function is
|
||||||
// always called with at least one block of data to process.
|
// always called with at least one block of data to process.
|
||||||
blockLoop()
|
blockLoop:
|
||||||
|
round(R8)
|
||||||
|
round(R9)
|
||||||
|
round(R10)
|
||||||
|
round(R11)
|
||||||
|
|
||||||
|
CMPQ SI, BX
|
||||||
|
JLE blockLoop
|
||||||
|
|
||||||
// Copy vN back to d.
|
// Copy vN back to d.
|
||||||
MOVQ v1, 0(d)
|
MOVQ R8, 0(AX)
|
||||||
MOVQ v2, 8(d)
|
MOVQ R9, 8(AX)
|
||||||
MOVQ v3, 16(d)
|
MOVQ R10, 16(AX)
|
||||||
MOVQ v4, 24(d)
|
MOVQ R11, 24(AX)
|
||||||
|
|
||||||
// The number of bytes written is p minus the old base pointer.
|
// The number of bytes written is SI minus the old base pointer.
|
||||||
SUBQ b_base+8(FP), p
|
SUBQ b_base+8(FP), SI
|
||||||
MOVQ p, ret+32(FP)
|
MOVQ SI, ret+32(FP)
|
||||||
|
|
||||||
RET
|
RET
|
||||||
|
|||||||
183
vendor/github.com/cespare/xxhash/v2/xxhash_arm64.s
generated
vendored
183
vendor/github.com/cespare/xxhash/v2/xxhash_arm64.s
generated
vendored
@@ -1,183 +0,0 @@
|
|||||||
//go:build !appengine && gc && !purego
|
|
||||||
// +build !appengine
|
|
||||||
// +build gc
|
|
||||||
// +build !purego
|
|
||||||
|
|
||||||
#include "textflag.h"
|
|
||||||
|
|
||||||
// Registers:
|
|
||||||
#define digest R1
|
|
||||||
#define h R2 // return value
|
|
||||||
#define p R3 // input pointer
|
|
||||||
#define n R4 // input length
|
|
||||||
#define nblocks R5 // n / 32
|
|
||||||
#define prime1 R7
|
|
||||||
#define prime2 R8
|
|
||||||
#define prime3 R9
|
|
||||||
#define prime4 R10
|
|
||||||
#define prime5 R11
|
|
||||||
#define v1 R12
|
|
||||||
#define v2 R13
|
|
||||||
#define v3 R14
|
|
||||||
#define v4 R15
|
|
||||||
#define x1 R20
|
|
||||||
#define x2 R21
|
|
||||||
#define x3 R22
|
|
||||||
#define x4 R23
|
|
||||||
|
|
||||||
#define round(acc, x) \
|
|
||||||
MADD prime2, acc, x, acc \
|
|
||||||
ROR $64-31, acc \
|
|
||||||
MUL prime1, acc
|
|
||||||
|
|
||||||
// round0 performs the operation x = round(0, x).
|
|
||||||
#define round0(x) \
|
|
||||||
MUL prime2, x \
|
|
||||||
ROR $64-31, x \
|
|
||||||
MUL prime1, x
|
|
||||||
|
|
||||||
#define mergeRound(acc, x) \
|
|
||||||
round0(x) \
|
|
||||||
EOR x, acc \
|
|
||||||
MADD acc, prime4, prime1, acc
|
|
||||||
|
|
||||||
// blockLoop processes as many 32-byte blocks as possible,
|
|
||||||
// updating v1, v2, v3, and v4. It assumes that n >= 32.
|
|
||||||
#define blockLoop() \
|
|
||||||
LSR $5, n, nblocks \
|
|
||||||
PCALIGN $16 \
|
|
||||||
loop: \
|
|
||||||
LDP.P 16(p), (x1, x2) \
|
|
||||||
LDP.P 16(p), (x3, x4) \
|
|
||||||
round(v1, x1) \
|
|
||||||
round(v2, x2) \
|
|
||||||
round(v3, x3) \
|
|
||||||
round(v4, x4) \
|
|
||||||
SUB $1, nblocks \
|
|
||||||
CBNZ nblocks, loop
|
|
||||||
|
|
||||||
// func Sum64(b []byte) uint64
|
|
||||||
TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32
|
|
||||||
LDP b_base+0(FP), (p, n)
|
|
||||||
|
|
||||||
LDP ·primes+0(SB), (prime1, prime2)
|
|
||||||
LDP ·primes+16(SB), (prime3, prime4)
|
|
||||||
MOVD ·primes+32(SB), prime5
|
|
||||||
|
|
||||||
CMP $32, n
|
|
||||||
CSEL LT, prime5, ZR, h // if n < 32 { h = prime5 } else { h = 0 }
|
|
||||||
BLT afterLoop
|
|
||||||
|
|
||||||
ADD prime1, prime2, v1
|
|
||||||
MOVD prime2, v2
|
|
||||||
MOVD $0, v3
|
|
||||||
NEG prime1, v4
|
|
||||||
|
|
||||||
blockLoop()
|
|
||||||
|
|
||||||
ROR $64-1, v1, x1
|
|
||||||
ROR $64-7, v2, x2
|
|
||||||
ADD x1, x2
|
|
||||||
ROR $64-12, v3, x3
|
|
||||||
ROR $64-18, v4, x4
|
|
||||||
ADD x3, x4
|
|
||||||
ADD x2, x4, h
|
|
||||||
|
|
||||||
mergeRound(h, v1)
|
|
||||||
mergeRound(h, v2)
|
|
||||||
mergeRound(h, v3)
|
|
||||||
mergeRound(h, v4)
|
|
||||||
|
|
||||||
afterLoop:
|
|
||||||
ADD n, h
|
|
||||||
|
|
||||||
TBZ $4, n, try8
|
|
||||||
LDP.P 16(p), (x1, x2)
|
|
||||||
|
|
||||||
round0(x1)
|
|
||||||
|
|
||||||
// NOTE: here and below, sequencing the EOR after the ROR (using a
|
|
||||||
// rotated register) is worth a small but measurable speedup for small
|
|
||||||
// inputs.
|
|
||||||
ROR $64-27, h
|
|
||||||
EOR x1 @> 64-27, h, h
|
|
||||||
MADD h, prime4, prime1, h
|
|
||||||
|
|
||||||
round0(x2)
|
|
||||||
ROR $64-27, h
|
|
||||||
EOR x2 @> 64-27, h, h
|
|
||||||
MADD h, prime4, prime1, h
|
|
||||||
|
|
||||||
try8:
|
|
||||||
TBZ $3, n, try4
|
|
||||||
MOVD.P 8(p), x1
|
|
||||||
|
|
||||||
round0(x1)
|
|
||||||
ROR $64-27, h
|
|
||||||
EOR x1 @> 64-27, h, h
|
|
||||||
MADD h, prime4, prime1, h
|
|
||||||
|
|
||||||
try4:
|
|
||||||
TBZ $2, n, try2
|
|
||||||
MOVWU.P 4(p), x2
|
|
||||||
|
|
||||||
MUL prime1, x2
|
|
||||||
ROR $64-23, h
|
|
||||||
EOR x2 @> 64-23, h, h
|
|
||||||
MADD h, prime3, prime2, h
|
|
||||||
|
|
||||||
try2:
|
|
||||||
TBZ $1, n, try1
|
|
||||||
MOVHU.P 2(p), x3
|
|
||||||
AND $255, x3, x1
|
|
||||||
LSR $8, x3, x2
|
|
||||||
|
|
||||||
MUL prime5, x1
|
|
||||||
ROR $64-11, h
|
|
||||||
EOR x1 @> 64-11, h, h
|
|
||||||
MUL prime1, h
|
|
||||||
|
|
||||||
MUL prime5, x2
|
|
||||||
ROR $64-11, h
|
|
||||||
EOR x2 @> 64-11, h, h
|
|
||||||
MUL prime1, h
|
|
||||||
|
|
||||||
try1:
|
|
||||||
TBZ $0, n, finalize
|
|
||||||
MOVBU (p), x4
|
|
||||||
|
|
||||||
MUL prime5, x4
|
|
||||||
ROR $64-11, h
|
|
||||||
EOR x4 @> 64-11, h, h
|
|
||||||
MUL prime1, h
|
|
||||||
|
|
||||||
finalize:
|
|
||||||
EOR h >> 33, h
|
|
||||||
MUL prime2, h
|
|
||||||
EOR h >> 29, h
|
|
||||||
MUL prime3, h
|
|
||||||
EOR h >> 32, h
|
|
||||||
|
|
||||||
MOVD h, ret+24(FP)
|
|
||||||
RET
|
|
||||||
|
|
||||||
// func writeBlocks(d *Digest, b []byte) int
|
|
||||||
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40
|
|
||||||
LDP ·primes+0(SB), (prime1, prime2)
|
|
||||||
|
|
||||||
// Load state. Assume v[1-4] are stored contiguously.
|
|
||||||
MOVD d+0(FP), digest
|
|
||||||
LDP 0(digest), (v1, v2)
|
|
||||||
LDP 16(digest), (v3, v4)
|
|
||||||
|
|
||||||
LDP b_base+8(FP), (p, n)
|
|
||||||
|
|
||||||
blockLoop()
|
|
||||||
|
|
||||||
// Store updated state.
|
|
||||||
STP (v1, v2), 0(digest)
|
|
||||||
STP (v3, v4), 16(digest)
|
|
||||||
|
|
||||||
BIC $31, n
|
|
||||||
MOVD n, ret+32(FP)
|
|
||||||
RET
|
|
||||||
24
vendor/github.com/cespare/xxhash/v2/xxhash_other.go
generated
vendored
24
vendor/github.com/cespare/xxhash/v2/xxhash_other.go
generated
vendored
@@ -1,9 +1,8 @@
|
|||||||
//go:build (!amd64 && !arm64) || appengine || !gc || purego
|
// +build !amd64 appengine !gc purego
|
||||||
// +build !amd64,!arm64 appengine !gc purego
|
|
||||||
|
|
||||||
package xxhash
|
package xxhash
|
||||||
|
|
||||||
// Sum64 computes the 64-bit xxHash digest of b with a zero seed.
|
// Sum64 computes the 64-bit xxHash digest of b.
|
||||||
func Sum64(b []byte) uint64 {
|
func Sum64(b []byte) uint64 {
|
||||||
// A simpler version would be
|
// A simpler version would be
|
||||||
// d := New()
|
// d := New()
|
||||||
@@ -15,10 +14,10 @@ func Sum64(b []byte) uint64 {
|
|||||||
var h uint64
|
var h uint64
|
||||||
|
|
||||||
if n >= 32 {
|
if n >= 32 {
|
||||||
v1 := primes[0] + prime2
|
v1 := prime1v + prime2
|
||||||
v2 := prime2
|
v2 := prime2
|
||||||
v3 := uint64(0)
|
v3 := uint64(0)
|
||||||
v4 := -primes[0]
|
v4 := -prime1v
|
||||||
for len(b) >= 32 {
|
for len(b) >= 32 {
|
||||||
v1 = round(v1, u64(b[0:8:len(b)]))
|
v1 = round(v1, u64(b[0:8:len(b)]))
|
||||||
v2 = round(v2, u64(b[8:16:len(b)]))
|
v2 = round(v2, u64(b[8:16:len(b)]))
|
||||||
@@ -37,18 +36,19 @@ func Sum64(b []byte) uint64 {
|
|||||||
|
|
||||||
h += uint64(n)
|
h += uint64(n)
|
||||||
|
|
||||||
for ; len(b) >= 8; b = b[8:] {
|
i, end := 0, len(b)
|
||||||
k1 := round(0, u64(b[:8]))
|
for ; i+8 <= end; i += 8 {
|
||||||
|
k1 := round(0, u64(b[i:i+8:len(b)]))
|
||||||
h ^= k1
|
h ^= k1
|
||||||
h = rol27(h)*prime1 + prime4
|
h = rol27(h)*prime1 + prime4
|
||||||
}
|
}
|
||||||
if len(b) >= 4 {
|
if i+4 <= end {
|
||||||
h ^= uint64(u32(b[:4])) * prime1
|
h ^= uint64(u32(b[i:i+4:len(b)])) * prime1
|
||||||
h = rol23(h)*prime2 + prime3
|
h = rol23(h)*prime2 + prime3
|
||||||
b = b[4:]
|
i += 4
|
||||||
}
|
}
|
||||||
for ; len(b) > 0; b = b[1:] {
|
for ; i < end; i++ {
|
||||||
h ^= uint64(b[0]) * prime5
|
h ^= uint64(b[i]) * prime5
|
||||||
h = rol11(h) * prime1
|
h = rol11(h) * prime1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
vendor/github.com/cespare/xxhash/v2/xxhash_safe.go
generated
vendored
3
vendor/github.com/cespare/xxhash/v2/xxhash_safe.go
generated
vendored
@@ -1,11 +1,10 @@
|
|||||||
//go:build appengine
|
|
||||||
// +build appengine
|
// +build appengine
|
||||||
|
|
||||||
// This file contains the safe implementations of otherwise unsafe-using code.
|
// This file contains the safe implementations of otherwise unsafe-using code.
|
||||||
|
|
||||||
package xxhash
|
package xxhash
|
||||||
|
|
||||||
// Sum64String computes the 64-bit xxHash digest of s with a zero seed.
|
// Sum64String computes the 64-bit xxHash digest of s.
|
||||||
func Sum64String(s string) uint64 {
|
func Sum64String(s string) uint64 {
|
||||||
return Sum64([]byte(s))
|
return Sum64([]byte(s))
|
||||||
}
|
}
|
||||||
|
|||||||
5
vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go
generated
vendored
5
vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go
generated
vendored
@@ -1,4 +1,3 @@
|
|||||||
//go:build !appengine
|
|
||||||
// +build !appengine
|
// +build !appengine
|
||||||
|
|
||||||
// This file encapsulates usage of unsafe.
|
// This file encapsulates usage of unsafe.
|
||||||
@@ -12,7 +11,7 @@ import (
|
|||||||
|
|
||||||
// In the future it's possible that compiler optimizations will make these
|
// In the future it's possible that compiler optimizations will make these
|
||||||
// XxxString functions unnecessary by realizing that calls such as
|
// XxxString functions unnecessary by realizing that calls such as
|
||||||
// Sum64([]byte(s)) don't need to copy s. See https://go.dev/issue/2205.
|
// Sum64([]byte(s)) don't need to copy s. See https://golang.org/issue/2205.
|
||||||
// If that happens, even if we keep these functions they can be replaced with
|
// If that happens, even if we keep these functions they can be replaced with
|
||||||
// the trivial safe code.
|
// the trivial safe code.
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ import (
|
|||||||
//
|
//
|
||||||
// See https://github.com/golang/go/issues/42739 for discussion.
|
// See https://github.com/golang/go/issues/42739 for discussion.
|
||||||
|
|
||||||
// Sum64String computes the 64-bit xxHash digest of s with a zero seed.
|
// Sum64String computes the 64-bit xxHash digest of s.
|
||||||
// It may be faster than Sum64([]byte(s)) by avoiding a copy.
|
// It may be faster than Sum64([]byte(s)) by avoiding a copy.
|
||||||
func Sum64String(s string) uint64 {
|
func Sum64String(s string) uint64 {
|
||||||
b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))
|
b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))
|
||||||
|
|||||||
4
vendor/modules.txt
vendored
4
vendor/modules.txt
vendored
@@ -1,4 +1,4 @@
|
|||||||
# github.com/cespare/xxhash/v2 v2.3.0
|
# github.com/cespare/xxhash/v2 v2.1.2
|
||||||
## explicit; go 1.11
|
## explicit; go 1.11
|
||||||
github.com/cespare/xxhash/v2
|
github.com/cespare/xxhash/v2
|
||||||
# github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
|
# github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
|
||||||
@@ -17,5 +17,3 @@ github.com/go-redis/redis/v8/internal/util
|
|||||||
# github.com/hjson/hjson-go v3.3.0+incompatible
|
# github.com/hjson/hjson-go v3.3.0+incompatible
|
||||||
## explicit
|
## explicit
|
||||||
github.com/hjson/hjson-go
|
github.com/hjson/hjson-go
|
||||||
# golang.org/x/sys v0.35.0
|
|
||||||
## explicit; go 1.23.0
|
|
||||||
|
|||||||
Reference in New Issue
Block a user