This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(make:*)",
|
||||
|
||||
"Bash(mkdir:*)",
|
||||
|
||||
"Bash(go build:*)",
|
||||
"Bash(go test:*)",
|
||||
"Bash(go get:*)",
|
||||
"Bash(go mod:*)",
|
||||
"Bash(go clean:*)",
|
||||
"Bash(go doc:*)",
|
||||
|
||||
"Bash(grep:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(rg:*)",
|
||||
"Bash(base64:*)",
|
||||
"Bash(sed:*)",
|
||||
"Bash(ls:*)",
|
||||
|
||||
"Bash(curl:*)",
|
||||
|
||||
"Bash(timeout 60s go test -v -count=1 ./...)",
|
||||
"Bash(timeout 60s go test -v -count=1 ./tests/integration/...)",
|
||||
"Bash(timeout 60s go test:*)",
|
||||
"Bash(timeout 300 make test)",
|
||||
"Bash(timeout 30s go test ./tests/integration -run:*)",
|
||||
|
||||
"Bash(done)",
|
||||
"Bash(awk:*)",
|
||||
|
||||
"WebFetch(domain:platform.openai.com)"
|
||||
],
|
||||
"deny": [
|
||||
],
|
||||
"defaultMode": "acceptEdits"
|
||||
},
|
||||
"env": {
|
||||
"CLAUDE_CODE_ENABLE_TELEMETRY": "0",
|
||||
"DISABLE_ERROR_REPORTING": "1",
|
||||
"DISABLE_TELEMETRY": "1"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
||||
|
||||
name: Build Docker and Deploy
|
||||
run-name: Build & Deploy ${{ gitea.ref }} on ${{ gitea.actor }}
|
||||
run-name: "[test]: ${{ github.event.head_commit.message }}"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
.claude-queue
|
||||
|
||||
##########################################################################
|
||||
|
||||
.idea/**/workspace.xml
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package bfcodegen
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProcessCSIDFileSimple(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package mymodels
|
||||
|
||||
type UserID string // @csid:type [USR]
|
||||
type OrderID string // @csid:type [ORD]
|
||||
`
|
||||
fp := writeTestFile(t, dir, "models.go", src)
|
||||
|
||||
ids, pkg, err := processCSIDFile(dir, fp, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, pkg, "mymodels")
|
||||
tst.AssertEqual(t, len(ids), 2)
|
||||
tst.AssertEqual(t, ids[0].Name, "UserID")
|
||||
tst.AssertEqual(t, ids[0].Prefix, "USR")
|
||||
tst.AssertEqual(t, ids[1].Name, "OrderID")
|
||||
tst.AssertEqual(t, ids[1].Prefix, "ORD")
|
||||
tst.AssertEqual(t, ids[0].FileRelative, "models.go")
|
||||
}
|
||||
|
||||
func TestProcessCSIDFilePrefixMustBeUppercase(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// lowercase prefix should not match the regex (only [A-Z0-9]{3})
|
||||
src := `package x
|
||||
|
||||
type FooID string // @csid:type [usr]
|
||||
`
|
||||
fp := writeTestFile(t, dir, "x.go", src)
|
||||
|
||||
ids, pkg, err := processCSIDFile(dir, fp, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, pkg, "x")
|
||||
tst.AssertEqual(t, len(ids), 0)
|
||||
}
|
||||
|
||||
func TestProcessCSIDFileGeneratedHeaderSkipped(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `// Code generated by csid-generate.go DO NOT EDIT.
|
||||
|
||||
package x
|
||||
|
||||
type SkipMeID string // @csid:type [SKP]
|
||||
`
|
||||
fp := writeTestFile(t, dir, "skip.go", src)
|
||||
|
||||
ids, pkg, err := processCSIDFile(dir, fp, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, pkg, "")
|
||||
tst.AssertEqual(t, len(ids), 0)
|
||||
}
|
||||
|
||||
func TestGenerateCharsetIDSpecsEndToEnd(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src1 := `package models
|
||||
|
||||
type EntityID string // @csid:type [ENT]
|
||||
type UserID string // @csid:type [USR]
|
||||
`
|
||||
writeTestFile(t, dir, "a_models.go", src1)
|
||||
|
||||
src2 := `package models
|
||||
|
||||
type OrderID string // @csid:type [ORD]
|
||||
`
|
||||
writeTestFile(t, dir, "b_models.go", src2)
|
||||
|
||||
dest := filepath.Join(dir, "csid_gen.go")
|
||||
|
||||
err := GenerateCharsetIDSpecs(dir, dest, CSIDGenOptions{DebugOutput: langext.PFalse})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
out, err := os.ReadFile(dest)
|
||||
tst.AssertNoErr(t, err)
|
||||
outStr := string(out)
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "package models"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "ChecksumCharsetIDGenerator"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "func NewUserID()"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "func NewOrderID()"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "func NewEntityID()"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, `prefixUserID`) && strings.Contains(outStr, `"USR"`))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, `prefixOrderID`) && strings.Contains(outStr, `"ORD"`))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, `prefixEntityID`) && strings.Contains(outStr, `"ENT"`))
|
||||
}
|
||||
|
||||
func TestGenerateCharsetIDSpecsIdempotentWhenUnchanged(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package models
|
||||
|
||||
type SomeID string // @csid:type [SOM]
|
||||
`
|
||||
writeTestFile(t, dir, "models.go", src)
|
||||
dest := filepath.Join(dir, "csid_gen.go")
|
||||
|
||||
err := GenerateCharsetIDSpecs(dir, dest, CSIDGenOptions{})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
content1, err := os.ReadFile(dest)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
err = GenerateCharsetIDSpecs(dir, dest, CSIDGenOptions{})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
content2, err := os.ReadFile(dest)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, string(content1), string(content2))
|
||||
}
|
||||
|
||||
func TestGenerateCharsetIDSpecsErrorsWithoutPackage(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `// Code generated by csid-generate.go DO NOT EDIT.
|
||||
|
||||
package x
|
||||
|
||||
type SkippedID string // @csid:type [SKP]
|
||||
`
|
||||
writeTestFile(t, dir, "z.go", src)
|
||||
dest := filepath.Join(dir, "csid_gen.go")
|
||||
|
||||
err := GenerateCharsetIDSpecs(dir, dest, CSIDGenOptions{})
|
||||
tst.AssertTrue(t, err != nil)
|
||||
}
|
||||
|
||||
func TestGenerateCharsetIDSpecsMissingDir(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "definitely-missing")
|
||||
err := GenerateCharsetIDSpecs(dir, filepath.Join(dir, "csid_gen.go"), CSIDGenOptions{})
|
||||
tst.AssertTrue(t, err != nil)
|
||||
}
|
||||
|
||||
func TestFmtCSIDOutputContainsAllNames(t *testing.T) {
|
||||
ids := []CSIDDef{
|
||||
{File: "a.go", FileRelative: "a.go", Name: "AlphaID", Prefix: "ALP"},
|
||||
{File: "b.go", FileRelative: "b.go", Name: "BetaID", Prefix: "BET"},
|
||||
}
|
||||
out := fmtCSIDOutput("CHK_XYZ", ids, "models")
|
||||
tst.AssertTrue(t, strings.Contains(out, "package models"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "CHK_XYZ"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "AlphaID"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "BetaID"))
|
||||
tst.AssertTrue(t, strings.Contains(out, `prefixAlphaID`) && strings.Contains(out, `"ALP"`))
|
||||
tst.AssertTrue(t, strings.Contains(out, `prefixBetaID`) && strings.Contains(out, `"BET"`))
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package bfcodegen
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProcessEnumFileBasicStringEnum(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package mymodels
|
||||
|
||||
type Color string // @enum:type
|
||||
|
||||
const (
|
||||
ColorRed Color = "red"
|
||||
ColorBlue Color = "blue"
|
||||
ColorGreen Color = "green"
|
||||
)
|
||||
`
|
||||
fp := writeTestFile(t, dir, "color.go", src)
|
||||
|
||||
enums, pkg, err := processEnumFile(dir, fp, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, pkg, "mymodels")
|
||||
tst.AssertEqual(t, len(enums), 1)
|
||||
tst.AssertEqual(t, enums[0].EnumTypeName, "Color")
|
||||
tst.AssertEqual(t, enums[0].Type, "string")
|
||||
tst.AssertEqual(t, len(enums[0].Values), 3)
|
||||
tst.AssertEqual(t, enums[0].Values[0].VarName, "ColorRed")
|
||||
tst.AssertEqual(t, enums[0].Values[0].Value, `"red"`)
|
||||
tst.AssertEqual(t, enums[0].Values[1].VarName, "ColorBlue")
|
||||
tst.AssertEqual(t, enums[0].Values[2].VarName, "ColorGreen")
|
||||
}
|
||||
|
||||
func TestProcessEnumFileIntEnum(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package m
|
||||
|
||||
type Priority int // @enum:type
|
||||
|
||||
const (
|
||||
PriorityLow Priority = 0
|
||||
PriorityMedium Priority = 1
|
||||
PriorityHigh Priority = 2
|
||||
)
|
||||
`
|
||||
fp := writeTestFile(t, dir, "prio.go", src)
|
||||
|
||||
enums, pkg, err := processEnumFile(dir, fp, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, pkg, "m")
|
||||
tst.AssertEqual(t, len(enums), 1)
|
||||
tst.AssertEqual(t, enums[0].EnumTypeName, "Priority")
|
||||
tst.AssertEqual(t, enums[0].Type, "int")
|
||||
tst.AssertEqual(t, len(enums[0].Values), 3)
|
||||
tst.AssertEqual(t, enums[0].Values[0].Value, "0")
|
||||
tst.AssertEqual(t, enums[0].Values[2].Value, "2")
|
||||
}
|
||||
|
||||
func TestProcessEnumFileWithDescriptions(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package m
|
||||
|
||||
type Status string // @enum:type
|
||||
|
||||
const (
|
||||
StatusActive Status = "active" // The active status
|
||||
StatusInactive Status = "inactive" // The inactive status
|
||||
)
|
||||
`
|
||||
fp := writeTestFile(t, dir, "s.go", src)
|
||||
|
||||
enums, _, err := processEnumFile(dir, fp, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, len(enums), 1)
|
||||
tst.AssertEqual(t, len(enums[0].Values), 2)
|
||||
|
||||
v0 := enums[0].Values[0]
|
||||
tst.AssertTrue(t, v0.Description != nil)
|
||||
tst.AssertEqual(t, *v0.Description, "The active status")
|
||||
tst.AssertTrue(t, v0.Data == nil)
|
||||
}
|
||||
|
||||
func TestProcessEnumFileWithDataComment(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package m
|
||||
|
||||
type Severity string // @enum:type
|
||||
|
||||
const (
|
||||
SeverityLow Severity = "low" // {"description": "Low severity", "weight": 1}
|
||||
SeverityHigh Severity = "high" // {"description": "High severity", "weight": 9}
|
||||
)
|
||||
`
|
||||
fp := writeTestFile(t, dir, "sev.go", src)
|
||||
|
||||
enums, _, err := processEnumFile(dir, fp, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, len(enums), 1)
|
||||
tst.AssertEqual(t, len(enums[0].Values), 2)
|
||||
|
||||
v0 := enums[0].Values[0]
|
||||
tst.AssertTrue(t, v0.Data != nil)
|
||||
tst.AssertTrue(t, v0.Description != nil)
|
||||
tst.AssertEqual(t, *v0.Description, "Low severity")
|
||||
}
|
||||
|
||||
func TestProcessEnumFileNonMatchingValuesNotAttached(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package m
|
||||
|
||||
type Color string // @enum:type
|
||||
|
||||
const (
|
||||
ColorRed Color = "red"
|
||||
)
|
||||
|
||||
const (
|
||||
OtherX OtherType = "x"
|
||||
)
|
||||
`
|
||||
fp := writeTestFile(t, dir, "c.go", src)
|
||||
|
||||
enums, _, err := processEnumFile(dir, fp, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, len(enums), 1)
|
||||
tst.AssertEqual(t, len(enums[0].Values), 1)
|
||||
tst.AssertEqual(t, enums[0].Values[0].VarName, "ColorRed")
|
||||
}
|
||||
|
||||
func TestProcessEnumFileGeneratedHeaderSkipped(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `// Code generated by enum-generate.go DO NOT EDIT.
|
||||
|
||||
package x
|
||||
|
||||
type Foo string // @enum:type
|
||||
`
|
||||
fp := writeTestFile(t, dir, "skip.go", src)
|
||||
|
||||
enums, pkg, err := processEnumFile(dir, fp, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, pkg, "")
|
||||
tst.AssertEqual(t, len(enums), 0)
|
||||
}
|
||||
|
||||
func TestTryParseDataCommentValid(t *testing.T) {
|
||||
m, ok := tryParseDataComment(`{"description": "hello", "weight": 5}`)
|
||||
tst.AssertTrue(t, ok)
|
||||
descr, _ := m["description"].(string)
|
||||
tst.AssertEqual(t, descr, "hello")
|
||||
weight, _ := m["weight"].(float64)
|
||||
tst.AssertEqual(t, weight, float64(5))
|
||||
}
|
||||
|
||||
func TestTryParseDataCommentBool(t *testing.T) {
|
||||
m, ok := tryParseDataComment(`{"a": true, "b": false}`)
|
||||
tst.AssertTrue(t, ok)
|
||||
a, _ := m["a"].(bool)
|
||||
tst.AssertTrue(t, a)
|
||||
b, _ := m["b"].(bool)
|
||||
tst.AssertFalse(t, b)
|
||||
}
|
||||
|
||||
func TestTryParseDataCommentRejectsNull(t *testing.T) {
|
||||
// null becomes a nil interface — its reflect.Kind is Invalid, not Pointer,
|
||||
// so it does not match any of the allowed kinds and is rejected.
|
||||
_, ok := tryParseDataComment(`{"x": null}`)
|
||||
tst.AssertFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestTryParseDataCommentInvalidJSON(t *testing.T) {
|
||||
_, ok := tryParseDataComment(`{not valid json}`)
|
||||
tst.AssertFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestTryParseDataCommentRejectsArrays(t *testing.T) {
|
||||
// arrays as values are not in the supported kinds list
|
||||
_, ok := tryParseDataComment(`{"x": [1, 2, 3]}`)
|
||||
tst.AssertFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestTryParseDataCommentRejectsObjects(t *testing.T) {
|
||||
_, ok := tryParseDataComment(`{"x": {"nested": 1}}`)
|
||||
tst.AssertFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestGenerateEnumSpecsEndToEnd(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package models
|
||||
|
||||
type Color string // @enum:type
|
||||
|
||||
const (
|
||||
ColorRed Color = "red"
|
||||
ColorGreen Color = "green"
|
||||
ColorBlue Color = "blue"
|
||||
)
|
||||
`
|
||||
writeTestFile(t, dir, "color.go", src)
|
||||
dest := filepath.Join(dir, "enum_gen.go")
|
||||
|
||||
err := GenerateEnumSpecs(dir, dest, EnumGenOptions{
|
||||
DebugOutput: langext.PFalse,
|
||||
GoFormat: langext.PTrue,
|
||||
})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
out, err := os.ReadFile(dest)
|
||||
tst.AssertNoErr(t, err)
|
||||
outStr := string(out)
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "package models"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "ChecksumEnumGenerator"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "ParseColor"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "ColorValues"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "ColorRed"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "ColorBlue"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "ColorGreen"))
|
||||
}
|
||||
|
||||
func TestGenerateEnumSpecsDeterministic(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package models
|
||||
|
||||
type Status string // @enum:type
|
||||
|
||||
const (
|
||||
StatusActive Status = "active" // The active one
|
||||
StatusOff Status = "off" // The off one
|
||||
)
|
||||
`
|
||||
writeTestFile(t, dir, "s.go", src)
|
||||
|
||||
s1, cs1, changed1, err := _generateEnumSpecs(dir, "", "N/A", true, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, changed1)
|
||||
|
||||
s2, cs2, changed2, err := _generateEnumSpecs(dir, "", "N/A", true, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, changed2)
|
||||
|
||||
tst.AssertEqual(t, cs1, cs2)
|
||||
tst.AssertEqual(t, s1, s2)
|
||||
}
|
||||
|
||||
func TestGenerateEnumSpecsNoChangeWhenChecksumMatches(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package models
|
||||
|
||||
type Status string // @enum:type
|
||||
|
||||
const (
|
||||
StatusActive Status = "active"
|
||||
)
|
||||
`
|
||||
writeTestFile(t, dir, "s.go", src)
|
||||
|
||||
_, cs, changed, err := _generateEnumSpecs(dir, "", "N/A", true, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, changed)
|
||||
|
||||
s2, cs2, changed2, err := _generateEnumSpecs(dir, "", cs, true, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertFalse(t, changed2)
|
||||
tst.AssertEqual(t, cs2, cs)
|
||||
tst.AssertEqual(t, s2, "")
|
||||
}
|
||||
|
||||
func TestGenerateEnumSpecsWithoutGoFormat(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package models
|
||||
|
||||
type Color string // @enum:type
|
||||
|
||||
const (
|
||||
ColorRed Color = "red"
|
||||
)
|
||||
`
|
||||
writeTestFile(t, dir, "c.go", src)
|
||||
|
||||
out, _, _, err := _generateEnumSpecs(dir, "", "N/A", false, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, strings.Contains(out, "ColorRed"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "package models"))
|
||||
}
|
||||
|
||||
func TestGenerateEnumSpecsErrorsWithoutPackage(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `// Code generated by enum-generate.go DO NOT EDIT.
|
||||
|
||||
package x
|
||||
|
||||
type Foo string // @enum:type
|
||||
`
|
||||
writeTestFile(t, dir, "z.go", src)
|
||||
|
||||
_, _, _, err := _generateEnumSpecs(dir, "", "N/A", false, false)
|
||||
tst.AssertTrue(t, err != nil)
|
||||
}
|
||||
|
||||
func TestGenerateEnumSpecsMissingDir(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "definitely-missing")
|
||||
_, _, _, err := _generateEnumSpecs(dir, "", "N/A", false, false)
|
||||
tst.AssertTrue(t, err != nil)
|
||||
}
|
||||
|
||||
func TestFmtEnumOutputContainsTypes(t *testing.T) {
|
||||
descr := "the red one"
|
||||
enums := []EnumDef{
|
||||
{
|
||||
File: "color.go",
|
||||
FileRelative: "color.go",
|
||||
EnumTypeName: "Color",
|
||||
Type: "string",
|
||||
Values: []EnumDefVal{
|
||||
{VarName: "ColorRed", Value: `"red"`, Description: &descr},
|
||||
{VarName: "ColorBlue", Value: `"blue"`, Description: &descr},
|
||||
},
|
||||
},
|
||||
}
|
||||
out := fmtEnumOutput("CHK1", enums, "models")
|
||||
tst.AssertTrue(t, strings.Contains(out, "package models"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "CHK1"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "ColorRed"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "ColorBlue"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "ParseColor"))
|
||||
}
|
||||
|
||||
func TestGenerateEnumSpecsSkipsGenFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package models
|
||||
|
||||
type Color string // @enum:type
|
||||
|
||||
const (
|
||||
ColorRed Color = "red"
|
||||
)
|
||||
`
|
||||
writeTestFile(t, dir, "c.go", src)
|
||||
|
||||
// generated file in same dir - should be filtered out
|
||||
gensrc := `package models
|
||||
|
||||
type ShouldBeIgnored string // @enum:type
|
||||
`
|
||||
writeTestFile(t, dir, "ignored_gen.go", gensrc)
|
||||
|
||||
out, _, _, err := _generateEnumSpecs(dir, filepath.Join(dir, "enum_gen.go"), "N/A", false, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, strings.Contains(out, "ColorRed"))
|
||||
tst.AssertFalse(t, strings.Contains(out, "ShouldBeIgnored"))
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package bfcodegen
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeTestFile(t *testing.T, dir string, name string, content string) string {
|
||||
t.Helper()
|
||||
p := filepath.Join(dir, name)
|
||||
err := os.WriteFile(p, []byte(content), 0o644)
|
||||
tst.AssertNoErr(t, err)
|
||||
return p
|
||||
}
|
||||
|
||||
func TestProcessIDFileSimple(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package mymodels
|
||||
|
||||
type UserID string // @id:type
|
||||
type OrderID string // @id:type
|
||||
`
|
||||
fp := writeTestFile(t, dir, "models.go", src)
|
||||
|
||||
ids, pkg, err := processIDFile(dir, fp, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, pkg, "mymodels")
|
||||
tst.AssertEqual(t, len(ids), 2)
|
||||
tst.AssertEqual(t, ids[0].Name, "UserID")
|
||||
tst.AssertEqual(t, ids[1].Name, "OrderID")
|
||||
tst.AssertEqual(t, ids[0].FileRelative, "models.go")
|
||||
}
|
||||
|
||||
func TestProcessIDFileNoMatches(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package x
|
||||
|
||||
type Foo string
|
||||
type Bar int
|
||||
type Baz string // not the right marker
|
||||
`
|
||||
fp := writeTestFile(t, dir, "x.go", src)
|
||||
|
||||
ids, pkg, err := processIDFile(dir, fp, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, pkg, "x")
|
||||
tst.AssertEqual(t, len(ids), 0)
|
||||
}
|
||||
|
||||
func TestProcessIDFileGeneratedHeaderSkipped(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `// Code generated by id-generate.go DO NOT EDIT.
|
||||
|
||||
package x
|
||||
|
||||
type SkipMeID string // @id:type
|
||||
`
|
||||
fp := writeTestFile(t, dir, "skip.go", src)
|
||||
|
||||
ids, pkg, err := processIDFile(dir, fp, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, pkg, "")
|
||||
tst.AssertEqual(t, len(ids), 0)
|
||||
}
|
||||
|
||||
func TestProcessIDFileMissingFile(t *testing.T) {
|
||||
_, _, err := processIDFile(t.TempDir(), filepath.Join(t.TempDir(), "does_not_exist.go"), false)
|
||||
tst.AssertTrue(t, err != nil)
|
||||
}
|
||||
|
||||
func TestGenerateIDSpecsEndToEnd(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src1 := `package models
|
||||
|
||||
type UserID string // @id:type
|
||||
type AnyID string // @id:type
|
||||
`
|
||||
writeTestFile(t, dir, "a_models.go", src1)
|
||||
|
||||
src2 := `package models
|
||||
|
||||
type OrderID string // @id:type
|
||||
`
|
||||
writeTestFile(t, dir, "b_models.go", src2)
|
||||
|
||||
dest := filepath.Join(dir, "id_gen.go")
|
||||
|
||||
err := GenerateIDSpecs(dir, dest, IDGenOptions{DebugOutput: langext.PFalse})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
out, err := os.ReadFile(dest)
|
||||
tst.AssertNoErr(t, err)
|
||||
outStr := string(out)
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "package models"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "ChecksumIDGenerator"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "func NewUserID()"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "func NewOrderID()"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "func NewAnyID()"))
|
||||
tst.AssertTrue(t, strings.Contains(outStr, "AsAny()"))
|
||||
}
|
||||
|
||||
func TestGenerateIDSpecsIdempotentWhenUnchanged(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package models
|
||||
|
||||
type SomeID string // @id:type
|
||||
`
|
||||
writeTestFile(t, dir, "models.go", src)
|
||||
dest := filepath.Join(dir, "id_gen.go")
|
||||
|
||||
err := GenerateIDSpecs(dir, dest, IDGenOptions{})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
stat1, err := os.Stat(dest)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
content1, err := os.ReadFile(dest)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
err = GenerateIDSpecs(dir, dest, IDGenOptions{})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
stat2, err := os.Stat(dest)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
content2, err := os.ReadFile(dest)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, stat1.ModTime().Equal(stat2.ModTime()), true)
|
||||
tst.AssertEqual(t, string(content1), string(content2))
|
||||
}
|
||||
|
||||
func TestGenerateIDSpecsRegeneratesAfterChange(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `package models
|
||||
|
||||
type FirstID string // @id:type
|
||||
`
|
||||
fp := writeTestFile(t, dir, "models.go", src)
|
||||
dest := filepath.Join(dir, "id_gen.go")
|
||||
|
||||
err := GenerateIDSpecs(dir, dest, IDGenOptions{})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
content1, err := os.ReadFile(dest)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, strings.Contains(string(content1), "FirstID"))
|
||||
tst.AssertFalse(t, strings.Contains(string(content1), "SecondID"))
|
||||
|
||||
src2 := `package models
|
||||
|
||||
type FirstID string // @id:type
|
||||
type SecondID string // @id:type
|
||||
`
|
||||
err = os.WriteFile(fp, []byte(src2), 0o644)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
err = GenerateIDSpecs(dir, dest, IDGenOptions{})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
content2, err := os.ReadFile(dest)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, strings.Contains(string(content2), "SecondID"))
|
||||
}
|
||||
|
||||
func TestGenerateIDSpecsErrorsWithoutPackage(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
src := `// Code generated by id-generate.go DO NOT EDIT.
|
||||
|
||||
package x
|
||||
|
||||
type SkippedID string // @id:type
|
||||
`
|
||||
writeTestFile(t, dir, "z.go", src)
|
||||
dest := filepath.Join(dir, "id_gen.go")
|
||||
|
||||
err := GenerateIDSpecs(dir, dest, IDGenOptions{})
|
||||
tst.AssertTrue(t, err != nil)
|
||||
}
|
||||
|
||||
func TestGenerateIDSpecsMissingDir(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "definitely-missing")
|
||||
err := GenerateIDSpecs(dir, filepath.Join(dir, "id_gen.go"), IDGenOptions{})
|
||||
tst.AssertTrue(t, err != nil)
|
||||
}
|
||||
|
||||
func TestFmtIDOutputContainsAllNames(t *testing.T) {
|
||||
ids := []IDDef{
|
||||
{File: "a.go", FileRelative: "a.go", Name: "AlphaID"},
|
||||
{File: "b.go", FileRelative: "b.go", Name: "BetaID"},
|
||||
}
|
||||
out := fmtIDOutput("CHK_ABC", ids, "models")
|
||||
tst.AssertTrue(t, strings.Contains(out, "package models"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "CHK_ABC"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "AlphaID"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "BetaID"))
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package cmdext
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRunnerInit(t *testing.T) {
|
||||
r := Runner("myprog")
|
||||
|
||||
if r == nil {
|
||||
t.Fatalf("Runner returned nil")
|
||||
}
|
||||
if r.program != "myprog" {
|
||||
t.Errorf("program == %v, want myprog", r.program)
|
||||
}
|
||||
if r.args == nil {
|
||||
t.Errorf("args is nil, want empty slice")
|
||||
}
|
||||
if len(r.args) != 0 {
|
||||
t.Errorf("len(args) == %v, want 0", len(r.args))
|
||||
}
|
||||
if r.env == nil {
|
||||
t.Errorf("env is nil, want empty slice")
|
||||
}
|
||||
if len(r.env) != 0 {
|
||||
t.Errorf("len(env) == %v, want 0", len(r.env))
|
||||
}
|
||||
if r.listener == nil {
|
||||
t.Errorf("listener is nil, want empty slice")
|
||||
}
|
||||
if len(r.listener) != 0 {
|
||||
t.Errorf("len(listener) == %v, want 0", len(r.listener))
|
||||
}
|
||||
if r.timeout != nil {
|
||||
t.Errorf("timeout == %v, want nil", r.timeout)
|
||||
}
|
||||
if r.enforceExitCodes != nil {
|
||||
t.Errorf("enforceExitCodes == %v, want nil", r.enforceExitCodes)
|
||||
}
|
||||
if r.enforceNoTimeout {
|
||||
t.Errorf("enforceNoTimeout == true, want false")
|
||||
}
|
||||
if r.enforceNoStderr {
|
||||
t.Errorf("enforceNoStderr == true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgSingle(t *testing.T) {
|
||||
r := Runner("p").Arg("a")
|
||||
if !reflect.DeepEqual(r.args, []string{"a"}) {
|
||||
t.Errorf("args == %v, want [a]", r.args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgMultiple(t *testing.T) {
|
||||
r := Runner("p").Arg("a").Arg("b").Arg("c")
|
||||
if !reflect.DeepEqual(r.args, []string{"a", "b", "c"}) {
|
||||
t.Errorf("args == %v, want [a b c]", r.args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgsAppendsAll(t *testing.T) {
|
||||
r := Runner("p").Args([]string{"x", "y"}).Args([]string{"z"})
|
||||
if !reflect.DeepEqual(r.args, []string{"x", "y", "z"}) {
|
||||
t.Errorf("args == %v, want [x y z]", r.args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgAndArgsMixed(t *testing.T) {
|
||||
r := Runner("p").Arg("a").Args([]string{"b", "c"}).Arg("d")
|
||||
if !reflect.DeepEqual(r.args, []string{"a", "b", "c", "d"}) {
|
||||
t.Errorf("args == %v, want [a b c d]", r.args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgsEmptySlice(t *testing.T) {
|
||||
r := Runner("p").Args([]string{})
|
||||
if len(r.args) != 0 {
|
||||
t.Errorf("len(args) == %v, want 0", len(r.args))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutSet(t *testing.T) {
|
||||
d := 500 * time.Millisecond
|
||||
r := Runner("p").Timeout(d)
|
||||
if r.timeout == nil {
|
||||
t.Fatalf("timeout is nil")
|
||||
}
|
||||
if *r.timeout != d {
|
||||
t.Errorf("timeout == %v, want %v", *r.timeout, d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutOverride(t *testing.T) {
|
||||
r := Runner("p").Timeout(1 * time.Second).Timeout(2 * time.Second)
|
||||
if *r.timeout != 2*time.Second {
|
||||
t.Errorf("timeout == %v, want 2s", *r.timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnv(t *testing.T) {
|
||||
r := Runner("p").Env("KEY", "VALUE")
|
||||
if !reflect.DeepEqual(r.env, []string{"KEY=VALUE"}) {
|
||||
t.Errorf("env == %v, want [KEY=VALUE]", r.env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvMultiple(t *testing.T) {
|
||||
r := Runner("p").Env("A", "1").Env("B", "2")
|
||||
if !reflect.DeepEqual(r.env, []string{"A=1", "B=2"}) {
|
||||
t.Errorf("env == %v, want [A=1 B=2]", r.env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvWithEmptyValue(t *testing.T) {
|
||||
r := Runner("p").Env("KEY", "")
|
||||
if !reflect.DeepEqual(r.env, []string{"KEY="}) {
|
||||
t.Errorf("env == %v, want [KEY=]", r.env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawEnv(t *testing.T) {
|
||||
r := Runner("p").RawEnv("FOO=BAR=BAZ")
|
||||
if !reflect.DeepEqual(r.env, []string{"FOO=BAR=BAZ"}) {
|
||||
t.Errorf("env == %v, want [FOO=BAR=BAZ]", r.env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvs(t *testing.T) {
|
||||
r := Runner("p").Envs([]string{"A=1", "B=2"})
|
||||
if !reflect.DeepEqual(r.env, []string{"A=1", "B=2"}) {
|
||||
t.Errorf("env == %v, want [A=1 B=2]", r.env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvMixed(t *testing.T) {
|
||||
r := Runner("p").Env("A", "1").RawEnv("B=2").Envs([]string{"C=3", "D=4"})
|
||||
if !reflect.DeepEqual(r.env, []string{"A=1", "B=2", "C=3", "D=4"}) {
|
||||
t.Errorf("env == %v, want [A=1 B=2 C=3 D=4]", r.env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureExitcodeSingle(t *testing.T) {
|
||||
r := Runner("p").EnsureExitcode(2)
|
||||
if r.enforceExitCodes == nil {
|
||||
t.Fatalf("enforceExitCodes is nil")
|
||||
}
|
||||
if !reflect.DeepEqual(*r.enforceExitCodes, []int{2}) {
|
||||
t.Errorf("enforceExitCodes == %v, want [2]", *r.enforceExitCodes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureExitcodeMultiple(t *testing.T) {
|
||||
r := Runner("p").EnsureExitcode(0, 1, 2)
|
||||
if r.enforceExitCodes == nil {
|
||||
t.Fatalf("enforceExitCodes is nil")
|
||||
}
|
||||
if !reflect.DeepEqual(*r.enforceExitCodes, []int{0, 1, 2}) {
|
||||
t.Errorf("enforceExitCodes == %v, want [0 1 2]", *r.enforceExitCodes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailOnExitCode(t *testing.T) {
|
||||
r := Runner("p").FailOnExitCode()
|
||||
if r.enforceExitCodes == nil {
|
||||
t.Fatalf("enforceExitCodes is nil")
|
||||
}
|
||||
if !reflect.DeepEqual(*r.enforceExitCodes, []int{0}) {
|
||||
t.Errorf("enforceExitCodes == %v, want [0]", *r.enforceExitCodes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailOnTimeoutFlag(t *testing.T) {
|
||||
r := Runner("p")
|
||||
if r.enforceNoTimeout {
|
||||
t.Errorf("enforceNoTimeout was true before set")
|
||||
}
|
||||
r = r.FailOnTimeout()
|
||||
if !r.enforceNoTimeout {
|
||||
t.Errorf("enforceNoTimeout == false after FailOnTimeout()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailOnStderrFlag(t *testing.T) {
|
||||
r := Runner("p")
|
||||
if r.enforceNoStderr {
|
||||
t.Errorf("enforceNoStderr was true before set")
|
||||
}
|
||||
r = r.FailOnStderr()
|
||||
if !r.enforceNoStderr {
|
||||
t.Errorf("enforceNoStderr == false after FailOnStderr()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListen(t *testing.T) {
|
||||
r := Runner("p").Listen(genericCommandListener{})
|
||||
if len(r.listener) != 1 {
|
||||
t.Errorf("len(listener) == %v, want 1", len(r.listener))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenMultiple(t *testing.T) {
|
||||
r := Runner("p").
|
||||
Listen(genericCommandListener{}).
|
||||
Listen(genericCommandListener{}).
|
||||
Listen(genericCommandListener{})
|
||||
if len(r.listener) != 3 {
|
||||
t.Errorf("len(listener) == %v, want 3", len(r.listener))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenStdoutAddsListener(t *testing.T) {
|
||||
r := Runner("p").ListenStdout(func(string) {})
|
||||
if len(r.listener) != 1 {
|
||||
t.Errorf("len(listener) == %v, want 1", len(r.listener))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenStdoutForwardsCalls(t *testing.T) {
|
||||
got := ""
|
||||
r := Runner("p").ListenStdout(func(s string) { got = s })
|
||||
if len(r.listener) != 1 {
|
||||
t.Fatalf("len(listener) == %v, want 1", len(r.listener))
|
||||
}
|
||||
r.listener[0].ReadStdoutLine("hello")
|
||||
if got != "hello" {
|
||||
t.Errorf("listener got %q, want hello", got)
|
||||
}
|
||||
// non-stdout methods should not panic and should not affect state
|
||||
r.listener[0].ReadStderrLine("nope")
|
||||
r.listener[0].ReadRawStdout([]byte("raw"))
|
||||
r.listener[0].ReadRawStderr([]byte("raw"))
|
||||
r.listener[0].Finished(0)
|
||||
r.listener[0].Timeout()
|
||||
if got != "hello" {
|
||||
t.Errorf("listener got mutated to %q, want hello", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenStderrAddsListener(t *testing.T) {
|
||||
r := Runner("p").ListenStderr(func(string) {})
|
||||
if len(r.listener) != 1 {
|
||||
t.Errorf("len(listener) == %v, want 1", len(r.listener))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenStderrForwardsCalls(t *testing.T) {
|
||||
got := ""
|
||||
r := Runner("p").ListenStderr(func(s string) { got = s })
|
||||
if len(r.listener) != 1 {
|
||||
t.Fatalf("len(listener) == %v, want 1", len(r.listener))
|
||||
}
|
||||
r.listener[0].ReadStderrLine("oops")
|
||||
if got != "oops" {
|
||||
t.Errorf("listener got %q, want oops", got)
|
||||
}
|
||||
r.listener[0].ReadStdoutLine("nope")
|
||||
if got != "oops" {
|
||||
t.Errorf("listener got mutated to %q, want oops", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChainReturnsSameInstance(t *testing.T) {
|
||||
r := Runner("p")
|
||||
if r.Arg("a") != r {
|
||||
t.Errorf("Arg returned different instance")
|
||||
}
|
||||
if r.Args([]string{"b"}) != r {
|
||||
t.Errorf("Args returned different instance")
|
||||
}
|
||||
if r.Timeout(time.Second) != r {
|
||||
t.Errorf("Timeout returned different instance")
|
||||
}
|
||||
if r.Env("K", "V") != r {
|
||||
t.Errorf("Env returned different instance")
|
||||
}
|
||||
if r.RawEnv("K=V") != r {
|
||||
t.Errorf("RawEnv returned different instance")
|
||||
}
|
||||
if r.Envs([]string{"K=V"}) != r {
|
||||
t.Errorf("Envs returned different instance")
|
||||
}
|
||||
if r.EnsureExitcode(0) != r {
|
||||
t.Errorf("EnsureExitcode returned different instance")
|
||||
}
|
||||
if r.FailOnExitCode() != r {
|
||||
t.Errorf("FailOnExitCode returned different instance")
|
||||
}
|
||||
if r.FailOnTimeout() != r {
|
||||
t.Errorf("FailOnTimeout returned different instance")
|
||||
}
|
||||
if r.FailOnStderr() != r {
|
||||
t.Errorf("FailOnStderr returned different instance")
|
||||
}
|
||||
if r.Listen(genericCommandListener{}) != r {
|
||||
t.Errorf("Listen returned different instance")
|
||||
}
|
||||
if r.ListenStdout(func(string) {}) != r {
|
||||
t.Errorf("ListenStdout returned different instance")
|
||||
}
|
||||
if r.ListenStderr(func(string) {}) != r {
|
||||
t.Errorf("ListenStderr returned different instance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeparateInstancesIndependent(t *testing.T) {
|
||||
r1 := Runner("p1").Arg("a")
|
||||
r2 := Runner("p2").Arg("b")
|
||||
|
||||
if r1.program != "p1" {
|
||||
t.Errorf("r1.program == %v, want p1", r1.program)
|
||||
}
|
||||
if r2.program != "p2" {
|
||||
t.Errorf("r2.program == %v, want p2", r2.program)
|
||||
}
|
||||
if !reflect.DeepEqual(r1.args, []string{"a"}) {
|
||||
t.Errorf("r1.args == %v, want [a]", r1.args)
|
||||
}
|
||||
if !reflect.DeepEqual(r2.args, []string{"b"}) {
|
||||
t.Errorf("r2.args == %v, want [b]", r2.args)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package cmdext
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenericListenerEmptyDoesNotPanic(t *testing.T) {
|
||||
l := genericCommandListener{}
|
||||
l.ReadRawStdout([]byte("x"))
|
||||
l.ReadRawStderr([]byte("x"))
|
||||
l.ReadStdoutLine("x")
|
||||
l.ReadStderrLine("x")
|
||||
l.Finished(0)
|
||||
l.Timeout()
|
||||
}
|
||||
|
||||
func TestGenericListenerReadRawStdout(t *testing.T) {
|
||||
var got []byte
|
||||
fn := func(b []byte) { got = append(got, b...) }
|
||||
l := genericCommandListener{_readRawStdout: &fn}
|
||||
|
||||
l.ReadRawStdout([]byte("hello"))
|
||||
l.ReadRawStdout([]byte(" world"))
|
||||
|
||||
if string(got) != "hello world" {
|
||||
t.Errorf("got %q, want %q", string(got), "hello world")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericListenerReadRawStderr(t *testing.T) {
|
||||
var got []byte
|
||||
fn := func(b []byte) { got = append(got, b...) }
|
||||
l := genericCommandListener{_readRawStderr: &fn}
|
||||
|
||||
l.ReadRawStderr([]byte("err"))
|
||||
|
||||
if string(got) != "err" {
|
||||
t.Errorf("got %q, want %q", string(got), "err")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericListenerReadStdoutLine(t *testing.T) {
|
||||
var got []string
|
||||
fn := func(s string) { got = append(got, s) }
|
||||
l := genericCommandListener{_readStdoutLine: &fn}
|
||||
|
||||
l.ReadStdoutLine("line1")
|
||||
l.ReadStdoutLine("line2")
|
||||
|
||||
if !reflect.DeepEqual(got, []string{"line1", "line2"}) {
|
||||
t.Errorf("got %v, want [line1 line2]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericListenerReadStderrLine(t *testing.T) {
|
||||
var got []string
|
||||
fn := func(s string) { got = append(got, s) }
|
||||
l := genericCommandListener{_readStderrLine: &fn}
|
||||
|
||||
l.ReadStderrLine("line1")
|
||||
l.ReadStderrLine("line2")
|
||||
|
||||
if !reflect.DeepEqual(got, []string{"line1", "line2"}) {
|
||||
t.Errorf("got %v, want [line1 line2]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericListenerFinished(t *testing.T) {
|
||||
var got int
|
||||
called := false
|
||||
fn := func(v int) { got = v; called = true }
|
||||
l := genericCommandListener{_finished: &fn}
|
||||
|
||||
l.Finished(42)
|
||||
|
||||
if !called {
|
||||
t.Errorf("Finished callback was not called")
|
||||
}
|
||||
if got != 42 {
|
||||
t.Errorf("got %v, want 42", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericListenerTimeout(t *testing.T) {
|
||||
called := false
|
||||
fn := func() { called = true }
|
||||
l := genericCommandListener{_timeout: &fn}
|
||||
|
||||
l.Timeout()
|
||||
|
||||
if !called {
|
||||
t.Errorf("Timeout callback was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericListenerOnlySpecifiedCalled(t *testing.T) {
|
||||
stdoutCalled := false
|
||||
stderrCalled := false
|
||||
stdoutFn := func(string) { stdoutCalled = true }
|
||||
stderrFn := func(string) { stderrCalled = true }
|
||||
l := genericCommandListener{_readStdoutLine: &stdoutFn, _readStderrLine: &stderrFn}
|
||||
|
||||
l.ReadStdoutLine("x")
|
||||
if !stdoutCalled {
|
||||
t.Errorf("stdout callback not called")
|
||||
}
|
||||
if stderrCalled {
|
||||
t.Errorf("stderr callback called when it shouldn't be")
|
||||
}
|
||||
|
||||
stdoutCalled = false
|
||||
l.ReadStderrLine("x")
|
||||
if stdoutCalled {
|
||||
t.Errorf("stdout callback called when it shouldn't be")
|
||||
}
|
||||
if !stderrCalled {
|
||||
t.Errorf("stderr callback not called")
|
||||
}
|
||||
|
||||
// these have no callbacks set; should be no-ops
|
||||
l.ReadRawStdout([]byte("x"))
|
||||
l.ReadRawStderr([]byte("x"))
|
||||
l.Finished(0)
|
||||
l.Timeout()
|
||||
}
|
||||
|
||||
func TestGenericListenerImplementsCommandListener(t *testing.T) {
|
||||
var _ CommandListener = genericCommandListener{}
|
||||
}
|
||||
|
||||
func TestGenericListenerEmptyByteSlice(t *testing.T) {
|
||||
calls := 0
|
||||
fn := func(b []byte) { calls++ }
|
||||
l := genericCommandListener{_readRawStdout: &fn}
|
||||
|
||||
l.ReadRawStdout([]byte{})
|
||||
l.ReadRawStdout(nil)
|
||||
|
||||
if calls != 2 {
|
||||
t.Errorf("calls == %v, want 2", calls)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
package confext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestApplyEnvOverridesPrefix(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 int `env:"V1"`
|
||||
V2 string `env:"V2"`
|
||||
}
|
||||
|
||||
data := testdata{V1: 1, V2: "x"}
|
||||
|
||||
t.Setenv("MYAPP_V1", "42")
|
||||
t.Setenv("MYAPP_V2", "hello")
|
||||
t.Setenv("V1", "111")
|
||||
t.Setenv("V2", "noprefix")
|
||||
|
||||
err := ApplyEnvOverrides("MYAPP_", &data, ".")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, data.V1, 42)
|
||||
tst.AssertEqual(t, data.V2, "hello")
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesUnexportedFieldsIgnored(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 int `env:"TEST_V1"`
|
||||
v2 int `env:"TEST_V2"`
|
||||
}
|
||||
|
||||
data := testdata{V1: 1, v2: 2}
|
||||
|
||||
t.Setenv("TEST_V1", "11")
|
||||
t.Setenv("TEST_V2", "22")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, data.V1, 11)
|
||||
tst.AssertEqual(t, data.v2, 2)
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesNoEnvTagIgnored(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 int `env:"TEST_V1"`
|
||||
V2 int ``
|
||||
}
|
||||
|
||||
data := testdata{V1: 1, V2: 2}
|
||||
|
||||
t.Setenv("TEST_V1", "11")
|
||||
t.Setenv("V2", "22")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, data.V1, 11)
|
||||
tst.AssertEqual(t, data.V2, 2)
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesDashTagIgnored(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 int `env:"TEST_V1"`
|
||||
V2 string `env:"-"`
|
||||
}
|
||||
|
||||
data := testdata{V1: 1, V2: "no"}
|
||||
|
||||
t.Setenv("TEST_V1", "11")
|
||||
t.Setenv("-", "yes")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, data.V1, 11)
|
||||
tst.AssertEqual(t, data.V2, "no")
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesEnvNotSetKeepsValue(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 int `env:"NOT_SET_INT_KEY_XYZ"`
|
||||
V2 string `env:"NOT_SET_STR_KEY_XYZ"`
|
||||
}
|
||||
|
||||
data := testdata{V1: 7, V2: "keep"}
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, data.V1, 7)
|
||||
tst.AssertEqual(t, data.V2, "keep")
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesBoolVariants(t *testing.T) {
|
||||
type testdata struct {
|
||||
B1 bool `env:"B1"`
|
||||
B2 bool `env:"B2"`
|
||||
B3 bool `env:"B3"`
|
||||
B4 bool `env:"B4"`
|
||||
B5 bool `env:"B5"`
|
||||
B6 bool `env:"B6"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("B1", "true")
|
||||
t.Setenv("B2", "false")
|
||||
t.Setenv("B3", "1")
|
||||
t.Setenv("B4", "0")
|
||||
t.Setenv("B5", " TRUE ")
|
||||
t.Setenv("B6", "FaLsE")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, data.B1, true)
|
||||
tst.AssertEqual(t, data.B2, false)
|
||||
tst.AssertEqual(t, data.B3, true)
|
||||
tst.AssertEqual(t, data.B4, false)
|
||||
tst.AssertEqual(t, data.B5, true)
|
||||
tst.AssertEqual(t, data.B6, false)
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesInvalidIntReturnsError(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 int `env:"BAD_INT"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("BAD_INT", "not_a_number")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid int, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesInvalidInt8ReturnsError(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 int8 `env:"BAD_INT8"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("BAD_INT8", "9999")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid int8, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesInvalidInt32ReturnsError(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 int32 `env:"BAD_INT32"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("BAD_INT32", "not_an_int32")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid int32, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesInvalidInt64ReturnsError(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 int64 `env:"BAD_INT64"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("BAD_INT64", "not_an_int64")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid int64, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesInvalidDurationReturnsError(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 time.Duration `env:"BAD_DUR"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("BAD_DUR", "not_a_duration")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid duration, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesInvalidTimeReturnsError(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 time.Time `env:"BAD_TIME"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("BAD_TIME", "not_a_time")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid time, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesInvalidBoolReturnsError(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 bool `env:"BAD_BOOL"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("BAD_BOOL", "yesno")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid bool, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesUnsupportedTypeReturnsError(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 []int `env:"UNSUPPORTED"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("UNSUPPORTED", "1,2,3")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for unsupported type, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesFloatUnsupportedReturnsError(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 float64 `env:"UNSUPPORTED_FLOAT"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("UNSUPPORTED_FLOAT", "1.5")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for float64, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesPointerInvalidReturnsError(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 *int `env:"PTR_BAD_INT"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("PTR_BAD_INT", "not_a_number")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid pointer int, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesPointerNotSetStaysNil(t *testing.T) {
|
||||
type testdata struct {
|
||||
V1 *int `env:"PTR_NOT_SET_KEY_ABC"`
|
||||
V2 *string `env:"PTR_NOT_SET_KEY_DEF"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
if data.V1 != nil {
|
||||
t.Errorf("expected V1 to remain nil, got %v", *data.V1)
|
||||
}
|
||||
if data.V2 != nil {
|
||||
t.Errorf("expected V2 to remain nil, got %v", *data.V2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesAliasBool(t *testing.T) {
|
||||
type aliasbool bool
|
||||
|
||||
type testdata struct {
|
||||
V1 aliasbool `env:"ALIAS_BOOL"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("ALIAS_BOOL", "true")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, data.V1, aliasbool(true))
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesNestedRecursiveError(t *testing.T) {
|
||||
type subdata struct {
|
||||
V1 int `env:"V1"`
|
||||
}
|
||||
type testdata struct {
|
||||
Sub subdata `env:"SUB"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("SUB.V1", "not_a_number")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
if err == nil {
|
||||
t.Errorf("expected error from nested struct invalid value, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesTimeFieldInsideStructIsParsed(t *testing.T) {
|
||||
type testdata struct {
|
||||
T time.Time `env:"MYTIME"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("MYTIME", "2023-01-02T03:04:05Z")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, data.T.Equal(time.Date(2023, 1, 2, 3, 4, 5, 0, time.UTC)), true)
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesPointerStringAlias(t *testing.T) {
|
||||
type aliasstr string
|
||||
|
||||
type testdata struct {
|
||||
V1 *aliasstr `env:"PTR_ALIAS_STR"`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("PTR_ALIAS_STR", "hello")
|
||||
|
||||
err := ApplyEnvOverrides("", &data, ".")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
if data.V1 == nil {
|
||||
t.Fatalf("expected V1 to be set")
|
||||
}
|
||||
tst.AssertEqual(t, *data.V1, aliasstr("hello"))
|
||||
}
|
||||
|
||||
func TestApplyEnvOverridesEmptyEnvTagOnSubstruct(t *testing.T) {
|
||||
type subdata struct {
|
||||
V1 int `env:"INNER"`
|
||||
}
|
||||
type testdata struct {
|
||||
Sub subdata `env:""`
|
||||
}
|
||||
|
||||
data := testdata{}
|
||||
|
||||
t.Setenv("INNER", "55")
|
||||
|
||||
err := ApplyEnvOverrides("PRE_", &data, "_")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, data.Sub.V1, 0)
|
||||
|
||||
t.Setenv("PRE_INNER", "77")
|
||||
|
||||
err = ApplyEnvOverrides("PRE_", &data, "_")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, data.Sub.V1, 77)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package cryptext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAESSimpleEmptyData(t *testing.T) {
|
||||
pw := []byte("password")
|
||||
enc, err := EncryptAESSimple(pw, []byte{}, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertNotEqual(t, enc, "")
|
||||
|
||||
dec, err := DecryptAESSimple(pw, enc)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, len(dec), 0)
|
||||
}
|
||||
|
||||
func TestAESSimpleEmptyPassword(t *testing.T) {
|
||||
pw := []byte{}
|
||||
plain := []byte("some content")
|
||||
|
||||
enc, err := EncryptAESSimple(pw, plain, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
dec, err := DecryptAESSimple(pw, enc)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(dec), string(plain))
|
||||
}
|
||||
|
||||
func TestAESSimpleWrongPassword(t *testing.T) {
|
||||
plain := []byte("Hello World")
|
||||
enc, err := EncryptAESSimple([]byte("right"), plain, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
_, err = DecryptAESSimple([]byte("wrong"), enc)
|
||||
if err == nil {
|
||||
t.Errorf("expected error when decrypting with wrong password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESSimpleInvalidBase32(t *testing.T) {
|
||||
_, err := DecryptAESSimple([]byte("pw"), "!!!not-base32!!!")
|
||||
if err == nil {
|
||||
t.Errorf("expected error on invalid base32 input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESSimpleInvalidJSON(t *testing.T) {
|
||||
// "AAAAAAAA" decodes to valid base32 but not valid JSON
|
||||
_, err := DecryptAESSimple([]byte("pw"), "AAAAAAAA")
|
||||
if err == nil {
|
||||
t.Errorf("expected error on invalid JSON payload")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESSimpleEmptyEncText(t *testing.T) {
|
||||
_, err := DecryptAESSimple([]byte("pw"), "")
|
||||
if err == nil {
|
||||
t.Errorf("expected error on empty text")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESSimpleLargeData(t *testing.T) {
|
||||
pw := []byte("hunter12")
|
||||
plain := []byte(strings.Repeat("ABCDEFGHIJ", 1024))
|
||||
|
||||
enc, err := EncryptAESSimple(pw, plain, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
dec, err := DecryptAESSimple(pw, enc)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(dec), string(plain))
|
||||
}
|
||||
|
||||
func TestAESSimpleBinaryData(t *testing.T) {
|
||||
pw := []byte("hunter12")
|
||||
plain := []byte{0x00, 0x01, 0x02, 0x7F, 0x80, 0xFE, 0xFF, 0x00, 0xAA, 0x55}
|
||||
|
||||
enc, err := EncryptAESSimple(pw, plain, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
dec, err := DecryptAESSimple(pw, enc)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertArrayEqual(t, dec, plain)
|
||||
}
|
||||
|
||||
func TestAESSimpleDifferentRoundsForEachCall(t *testing.T) {
|
||||
pw := []byte("hunter12")
|
||||
plain := []byte("Hello")
|
||||
|
||||
enc1, err := EncryptAESSimple(pw, plain, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
enc2, err := EncryptAESSimple(pw, plain, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
// Two separate encrypt calls on same plaintext should differ (random salt + IV)
|
||||
tst.AssertNotEqual(t, enc1, enc2)
|
||||
|
||||
// Both should decrypt back to the same plaintext
|
||||
d1, err := DecryptAESSimple(pw, enc1)
|
||||
tst.AssertNoErr(t, err)
|
||||
d2, err := DecryptAESSimple(pw, enc2)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(d1), string(plain))
|
||||
tst.AssertEqual(t, string(d2), string(plain))
|
||||
}
|
||||
|
||||
func TestAESSimpleVariableRounds(t *testing.T) {
|
||||
pw := []byte("hunter12")
|
||||
plain := []byte("rounds-test")
|
||||
|
||||
for _, r := range []int{16, 32, 64, 128, 256, 512, 1024} {
|
||||
enc, err := EncryptAESSimple(pw, plain, r)
|
||||
tst.AssertNoErr(t, err)
|
||||
dec, err := DecryptAESSimple(pw, enc)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(dec), string(plain))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESSimpleResultIsBase32(t *testing.T) {
|
||||
pw := []byte("hunter12")
|
||||
plain := []byte("Hello World")
|
||||
|
||||
enc, err := EncryptAESSimple(pw, plain, 64)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
for _, c := range enc {
|
||||
isUpper := c >= 'A' && c <= 'Z'
|
||||
isDigit := c >= '2' && c <= '7'
|
||||
if !(isUpper || isDigit) {
|
||||
t.Errorf("non-base32 character %q in output", c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package cryptext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStrSha256SameAsBytesSha256(t *testing.T) {
|
||||
inputs := []string{"", "a", "Hello World", "lorem ipsum dolor sit amet", "🎉 unicode"}
|
||||
for _, in := range inputs {
|
||||
tst.AssertEqual(t, StrSha256(in), BytesSha256([]byte(in)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrSha256Length(t *testing.T) {
|
||||
// SHA-256 hex output must always be 64 characters
|
||||
tst.AssertEqual(t, len(StrSha256("")), 64)
|
||||
tst.AssertEqual(t, len(StrSha256("x")), 64)
|
||||
tst.AssertEqual(t, len(StrSha256(strings.Repeat("x", 10000))), 64)
|
||||
}
|
||||
|
||||
func TestStrSha256Deterministic(t *testing.T) {
|
||||
v := "deterministic input"
|
||||
a := StrSha256(v)
|
||||
b := StrSha256(v)
|
||||
tst.AssertEqual(t, a, b)
|
||||
}
|
||||
|
||||
func TestStrSha256DifferentInputs(t *testing.T) {
|
||||
tst.AssertNotEqual(t, StrSha256("a"), StrSha256("b"))
|
||||
tst.AssertNotEqual(t, StrSha256("Hello"), StrSha256("hello"))
|
||||
tst.AssertNotEqual(t, StrSha256("Hello World"), StrSha256("Hello World "))
|
||||
}
|
||||
|
||||
func TestStrSha256IsHex(t *testing.T) {
|
||||
out := StrSha256("anything")
|
||||
for _, c := range out {
|
||||
isLowerHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')
|
||||
if !isLowerHex {
|
||||
t.Errorf("non-hex char %q in StrSha256 output", c)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytesSha256NilSameAsEmpty(t *testing.T) {
|
||||
tst.AssertEqual(t, BytesSha256(nil), BytesSha256([]byte{}))
|
||||
}
|
||||
|
||||
func TestBytesSha256KnownVectors(t *testing.T) {
|
||||
// "abc" => sha-256 standard vector
|
||||
tst.AssertEqual(t, StrSha256("abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
package cryptext
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPassHashInvalidEmpty(t *testing.T) {
|
||||
ph := PassHash("")
|
||||
tst.AssertFalse(t, ph.Valid())
|
||||
tst.AssertFalse(t, ph.HasTOTP())
|
||||
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
|
||||
}
|
||||
|
||||
func TestPassHashInvalidGarbage(t *testing.T) {
|
||||
for _, raw := range []string{
|
||||
"garbage",
|
||||
"99|nope",
|
||||
"abc|payload",
|
||||
"3|onlytwo",
|
||||
"4|onlytwo",
|
||||
"5|onlytwo",
|
||||
"2|notbase64!|notbase64!",
|
||||
"1|!!!notbase64!!!",
|
||||
"3|!!notb64|!!notb64|0",
|
||||
"3|abc|!!notb64|0",
|
||||
} {
|
||||
ph := PassHash(raw)
|
||||
if ph.Valid() {
|
||||
t.Errorf("expected %q to be invalid", raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassHashVerifyInvalid(t *testing.T) {
|
||||
ph := PassHash("garbage-value")
|
||||
tst.AssertFalse(t, ph.Verify("anything", nil))
|
||||
}
|
||||
|
||||
func TestPassHashUpgradeInvalid(t *testing.T) {
|
||||
ph := PassHash("garbage-value")
|
||||
_, err := ph.Upgrade("anything")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid PassHash upgrade")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassHashStringRoundtrip(t *testing.T) {
|
||||
ph, err := HashPassword("hunter2", nil)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, ph.String(), string(ph))
|
||||
}
|
||||
|
||||
func TestPassHashMarshalJSONEmpty(t *testing.T) {
|
||||
ph := PassHash("")
|
||||
data, err := json.Marshal(ph)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(data), `""`)
|
||||
}
|
||||
|
||||
func TestPassHashMarshalJSONMasked(t *testing.T) {
|
||||
ph, err := HashPassword("hunter2", nil)
|
||||
tst.AssertNoErr(t, err)
|
||||
data, err := json.Marshal(ph)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(data), `"*****"`)
|
||||
}
|
||||
|
||||
func TestPassHashDataV0(t *testing.T) {
|
||||
ph, err := HashPasswordV0("test123")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
v, seed, payload, hastotp, totpsecret, valid := ph.Data()
|
||||
tst.AssertTrue(t, valid)
|
||||
tst.AssertEqual(t, v, 0)
|
||||
tst.AssertEqual(t, len(seed), 0)
|
||||
tst.AssertEqual(t, string(payload), "test123")
|
||||
tst.AssertFalse(t, hastotp)
|
||||
tst.AssertEqual(t, len(totpsecret), 0)
|
||||
}
|
||||
|
||||
func TestPassHashDataV1(t *testing.T) {
|
||||
ph, err := HashPasswordV1("test123")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
v, seed, payload, hastotp, _, valid := ph.Data()
|
||||
tst.AssertTrue(t, valid)
|
||||
tst.AssertEqual(t, v, 1)
|
||||
tst.AssertEqual(t, len(seed), 0)
|
||||
tst.AssertEqual(t, len(payload), 32) // sha-256 is 32 bytes
|
||||
tst.AssertFalse(t, hastotp)
|
||||
}
|
||||
|
||||
func TestPassHashDataV2(t *testing.T) {
|
||||
ph, err := HashPasswordV2("test123")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
v, seed, payload, hastotp, _, valid := ph.Data()
|
||||
tst.AssertTrue(t, valid)
|
||||
tst.AssertEqual(t, v, 2)
|
||||
tst.AssertEqual(t, len(seed), 32)
|
||||
tst.AssertEqual(t, len(payload), 32)
|
||||
tst.AssertFalse(t, hastotp)
|
||||
}
|
||||
|
||||
func TestPassHashDataV3(t *testing.T) {
|
||||
ph, err := HashPasswordV3("test123", nil)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
v, seed, payload, hastotp, _, valid := ph.Data()
|
||||
tst.AssertTrue(t, valid)
|
||||
tst.AssertEqual(t, v, 3)
|
||||
tst.AssertEqual(t, len(seed), 32)
|
||||
tst.AssertEqual(t, len(payload), 32)
|
||||
tst.AssertFalse(t, hastotp)
|
||||
}
|
||||
|
||||
func TestPassHashDataV4(t *testing.T) {
|
||||
ph, err := HashPasswordV4("test123", nil)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
v, _, _, hastotp, _, valid := ph.Data()
|
||||
tst.AssertTrue(t, valid)
|
||||
tst.AssertEqual(t, v, 4)
|
||||
tst.AssertFalse(t, hastotp)
|
||||
}
|
||||
|
||||
func TestPassHashDataV5(t *testing.T) {
|
||||
ph, err := HashPasswordV5("test123", nil)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
v, _, _, hastotp, _, valid := ph.Data()
|
||||
tst.AssertTrue(t, valid)
|
||||
tst.AssertEqual(t, v, 5)
|
||||
tst.AssertFalse(t, hastotp)
|
||||
}
|
||||
|
||||
func TestPassHashLatestIsV5(t *testing.T) {
|
||||
ph, err := HashPassword("test", nil)
|
||||
tst.AssertNoErr(t, err)
|
||||
v, _, _, _, _, valid := ph.Data()
|
||||
tst.AssertTrue(t, valid)
|
||||
tst.AssertEqual(t, v, LatestPassHashVersion)
|
||||
tst.AssertEqual(t, v, 5)
|
||||
}
|
||||
|
||||
func TestPassHashUpgradeLatestIsNoop(t *testing.T) {
|
||||
ph, err := HashPassword("test", nil)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
|
||||
|
||||
ph2, err := ph.Upgrade("test")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(ph), string(ph2))
|
||||
}
|
||||
|
||||
func TestPassHashClearTOTPInvalid(t *testing.T) {
|
||||
_, err := PassHash("garbage").ClearTOTP()
|
||||
if err == nil {
|
||||
t.Errorf("expected error from ClearTOTP on invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassHashClearTOTPV0V1V2Noop(t *testing.T) {
|
||||
ph0, _ := HashPasswordV0("x")
|
||||
r0, err := ph0.ClearTOTP()
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(r0), string(ph0))
|
||||
|
||||
ph1, _ := HashPasswordV1("x")
|
||||
r1, err := ph1.ClearTOTP()
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(r1), string(ph1))
|
||||
|
||||
ph2, _ := HashPasswordV2("x")
|
||||
r2, err := ph2.ClearTOTP()
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(r2), string(ph2))
|
||||
}
|
||||
|
||||
func TestPassHashClearTOTPV3(t *testing.T) {
|
||||
secret := []byte{0x01, 0x02, 0x03}
|
||||
ph, err := HashPasswordV3("test123", secret)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, ph.HasTOTP())
|
||||
|
||||
cleared, err := ph.ClearTOTP()
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertFalse(t, cleared.HasTOTP())
|
||||
tst.AssertTrue(t, cleared.Valid())
|
||||
tst.AssertTrue(t, cleared.Verify("test123", nil))
|
||||
}
|
||||
|
||||
func TestPassHashClearTOTPV4(t *testing.T) {
|
||||
secret := []byte{0x01, 0x02, 0x03}
|
||||
ph, err := HashPasswordV4("test123", secret)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, ph.HasTOTP())
|
||||
|
||||
cleared, err := ph.ClearTOTP()
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertFalse(t, cleared.HasTOTP())
|
||||
tst.AssertTrue(t, cleared.Verify("test123", nil))
|
||||
}
|
||||
|
||||
func TestPassHashClearTOTPV5(t *testing.T) {
|
||||
secret := []byte{0x01, 0x02, 0x03}
|
||||
ph, err := HashPasswordV5("test123", secret)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, ph.HasTOTP())
|
||||
|
||||
cleared, err := ph.ClearTOTP()
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertFalse(t, cleared.HasTOTP())
|
||||
tst.AssertTrue(t, cleared.Verify("test123", nil))
|
||||
}
|
||||
|
||||
func TestPassHashWithTOTPInvalid(t *testing.T) {
|
||||
_, err := PassHash("garbage").WithTOTP([]byte{0x01})
|
||||
if err == nil {
|
||||
t.Errorf("expected error for WithTOTP on invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassHashWithTOTPV0V1V2Errors(t *testing.T) {
|
||||
ph0, _ := HashPasswordV0("x")
|
||||
if _, err := ph0.WithTOTP([]byte{0x01}); err == nil {
|
||||
t.Errorf("expected v0 not to support TOTP")
|
||||
}
|
||||
ph1, _ := HashPasswordV1("x")
|
||||
if _, err := ph1.WithTOTP([]byte{0x01}); err == nil {
|
||||
t.Errorf("expected v1 not to support TOTP")
|
||||
}
|
||||
ph2, _ := HashPasswordV2("x")
|
||||
if _, err := ph2.WithTOTP([]byte{0x01}); err == nil {
|
||||
t.Errorf("expected v2 not to support TOTP")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassHashWithTOTPV3V4V5(t *testing.T) {
|
||||
secret := []byte{0xDE, 0xAD, 0xBE, 0xEF}
|
||||
|
||||
ph3, _ := HashPasswordV3("pw", nil)
|
||||
tst.AssertFalse(t, ph3.HasTOTP())
|
||||
r3, err := ph3.WithTOTP(secret)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, r3.HasTOTP())
|
||||
|
||||
ph4, _ := HashPasswordV4("pw", nil)
|
||||
tst.AssertFalse(t, ph4.HasTOTP())
|
||||
r4, err := ph4.WithTOTP(secret)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, r4.HasTOTP())
|
||||
|
||||
ph5, _ := HashPasswordV5("pw", nil)
|
||||
tst.AssertFalse(t, ph5.HasTOTP())
|
||||
r5, err := ph5.WithTOTP(secret)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, r5.HasTOTP())
|
||||
}
|
||||
|
||||
func TestPassHashChangeInvalid(t *testing.T) {
|
||||
_, err := PassHash("garbage").Change("new-pw")
|
||||
if err == nil {
|
||||
t.Errorf("expected error from Change on invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassHashChangeKeepsVersion(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
hashed func() (PassHash, error)
|
||||
version int
|
||||
}{
|
||||
{"V0", func() (PassHash, error) { return HashPasswordV0("old") }, 0},
|
||||
{"V1", func() (PassHash, error) { return HashPasswordV1("old") }, 1},
|
||||
{"V2", func() (PassHash, error) { return HashPasswordV2("old") }, 2},
|
||||
{"V3", func() (PassHash, error) { return HashPasswordV3("old", nil) }, 3},
|
||||
{"V4", func() (PassHash, error) { return HashPasswordV4("old", nil) }, 4},
|
||||
{"V5", func() (PassHash, error) { return HashPasswordV5("old", nil) }, 5},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
ph, err := c.hashed()
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
changed, err := ph.Change("new-pw")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
v, _, _, _, _, valid := changed.Data()
|
||||
tst.AssertTrue(t, valid)
|
||||
tst.AssertEqual(t, v, c.version)
|
||||
|
||||
tst.AssertTrue(t, changed.Verify("new-pw", nil))
|
||||
tst.AssertFalse(t, changed.Verify("old", nil))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassHashChangeKeepsTOTPV3(t *testing.T) {
|
||||
secret := []byte{0xAB, 0xCD}
|
||||
ph, err := HashPasswordV3("old", secret)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, ph.HasTOTP())
|
||||
|
||||
changed, err := ph.Change("new")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, changed.HasTOTP())
|
||||
}
|
||||
|
||||
func TestPassHashV0Format(t *testing.T) {
|
||||
ph, err := HashPasswordV0("plaintext-pw")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, strings.HasPrefix(string(ph), "0|"))
|
||||
tst.AssertEqual(t, string(ph), "0|plaintext-pw")
|
||||
}
|
||||
|
||||
func TestPassHashV1Format(t *testing.T) {
|
||||
ph, err := HashPasswordV1("test")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, strings.HasPrefix(string(ph), "1|"))
|
||||
}
|
||||
|
||||
func TestPassHashV2Format(t *testing.T) {
|
||||
ph, err := HashPasswordV2("test")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, strings.HasPrefix(string(ph), "2|"))
|
||||
tst.AssertEqual(t, strings.Count(string(ph), "|"), 2)
|
||||
}
|
||||
|
||||
func TestPassHashV3Format(t *testing.T) {
|
||||
ph, err := HashPasswordV3("test", nil)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, strings.HasPrefix(string(ph), "3|"))
|
||||
tst.AssertEqual(t, strings.Count(string(ph), "|"), 3)
|
||||
tst.AssertTrue(t, strings.HasSuffix(string(ph), "|0"))
|
||||
}
|
||||
|
||||
func TestPassHashV4Format(t *testing.T) {
|
||||
ph, err := HashPasswordV4("test", nil)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, strings.HasPrefix(string(ph), "4|"))
|
||||
tst.AssertTrue(t, strings.HasSuffix(string(ph), "|0"))
|
||||
}
|
||||
|
||||
func TestPassHashV5Format(t *testing.T) {
|
||||
ph, err := HashPasswordV5("test", nil)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, strings.HasPrefix(string(ph), "5|"))
|
||||
tst.AssertTrue(t, strings.HasSuffix(string(ph), "|0"))
|
||||
}
|
||||
|
||||
func TestPassHashV5VerifyLongPassword(t *testing.T) {
|
||||
// V5 hashes via sha512 first → bcrypt's 72-byte limit shouldn't apply
|
||||
longPw := strings.Repeat("a", 200)
|
||||
ph, err := HashPasswordV5(longPw, nil)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertTrue(t, ph.Verify(longPw, nil))
|
||||
tst.AssertFalse(t, ph.Verify(longPw+"x", nil))
|
||||
}
|
||||
|
||||
func TestPassHashV5DifferentEachCall(t *testing.T) {
|
||||
ph1, err := HashPasswordV5("samepw", nil)
|
||||
tst.AssertNoErr(t, err)
|
||||
ph2, err := HashPasswordV5("samepw", nil)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
// Bcrypt salts internally — same password should produce different hashes
|
||||
tst.AssertNotEqual(t, string(ph1), string(ph2))
|
||||
|
||||
// Both must verify
|
||||
tst.AssertTrue(t, ph1.Verify("samepw", nil))
|
||||
tst.AssertTrue(t, ph2.Verify("samepw", nil))
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package cryptext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
mathrand "math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func TestPronouncablePasswordLength(t *testing.T) {
|
||||
for _, n := range []int{1, 2, 3, 5, 8, 13, 21, 50, 128} {
|
||||
pw := PronouncablePassword(n)
|
||||
tst.AssertEqual(t, len(pw), n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordZeroOrNegative(t *testing.T) {
|
||||
tst.AssertEqual(t, PronouncablePassword(0), "")
|
||||
tst.AssertEqual(t, PronouncablePassword(-1), "")
|
||||
tst.AssertEqual(t, PronouncablePassword(-1000), "")
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordSeededDeterministic(t *testing.T) {
|
||||
pw1 := PronouncablePasswordSeeded(42, 16)
|
||||
pw2 := PronouncablePasswordSeeded(42, 16)
|
||||
tst.AssertEqual(t, pw1, pw2)
|
||||
tst.AssertEqual(t, len(pw1), 16)
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordSeededDifferentSeeds(t *testing.T) {
|
||||
pw1 := PronouncablePasswordSeeded(1, 16)
|
||||
pw2 := PronouncablePasswordSeeded(2, 16)
|
||||
tst.AssertNotEqual(t, pw1, pw2)
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordExtEntropy(t *testing.T) {
|
||||
rng := mathrand.New(mathrand.NewSource(1))
|
||||
pw, entropy := PronouncablePasswordExt(rng, 32)
|
||||
tst.AssertEqual(t, len(pw), 32)
|
||||
if entropy <= 0 {
|
||||
t.Errorf("expected positive entropy, got %f", entropy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordExtZeroLen(t *testing.T) {
|
||||
rng := mathrand.New(mathrand.NewSource(1))
|
||||
pw, entropy := PronouncablePasswordExt(rng, 0)
|
||||
tst.AssertEqual(t, pw, "")
|
||||
tst.AssertEqual(t, entropy, float64(0))
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordCharacters(t *testing.T) {
|
||||
// Output should be only ASCII letters
|
||||
for i := range 50 {
|
||||
pw := PronouncablePasswordSeeded(int64(i), 32)
|
||||
for _, c := range pw {
|
||||
if !unicode.IsLetter(c) || c > unicode.MaxASCII {
|
||||
t.Errorf("non-letter or non-ASCII rune %q in password %q", c, pw)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordStartsUpper(t *testing.T) {
|
||||
for i := range 50 {
|
||||
pw := PronouncablePasswordSeeded(int64(i), 16)
|
||||
if pw == "" {
|
||||
continue
|
||||
}
|
||||
first := rune(pw[0])
|
||||
if !unicode.IsUpper(first) {
|
||||
t.Errorf("expected first letter uppercase in %q (seed %d)", pw, i)
|
||||
}
|
||||
if !strings.ContainsRune(ppStartChar, first) {
|
||||
t.Errorf("expected first letter from start-set in %q (seed %d)", pw, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPpMakeSet(t *testing.T) {
|
||||
set := ppMakeSet("ABC")
|
||||
tst.AssertTrue(t, set['A'])
|
||||
tst.AssertTrue(t, set['B'])
|
||||
tst.AssertTrue(t, set['C'])
|
||||
tst.AssertFalse(t, set['D'])
|
||||
tst.AssertEqual(t, len(set), 3)
|
||||
}
|
||||
|
||||
func TestPpMakeSetEmpty(t *testing.T) {
|
||||
set := ppMakeSet("")
|
||||
tst.AssertEqual(t, len(set), 0)
|
||||
}
|
||||
|
||||
func TestPpCharType(t *testing.T) {
|
||||
v, c := ppCharType('A')
|
||||
tst.AssertTrue(t, v)
|
||||
tst.AssertFalse(t, c)
|
||||
|
||||
v, c = ppCharType('B')
|
||||
tst.AssertFalse(t, v)
|
||||
tst.AssertTrue(t, c)
|
||||
|
||||
v, c = ppCharType('Y')
|
||||
tst.AssertTrue(t, v)
|
||||
tst.AssertFalse(t, c)
|
||||
|
||||
v, c = ppCharType('1')
|
||||
tst.AssertFalse(t, v)
|
||||
tst.AssertFalse(t, c)
|
||||
}
|
||||
|
||||
func TestPpCharsetRemove(t *testing.T) {
|
||||
set := ppMakeSet("AEIOU")
|
||||
out := ppCharsetRemove("ABCDEFG", set, false)
|
||||
tst.AssertEqual(t, out, "BCDFG")
|
||||
}
|
||||
|
||||
func TestPpCharsetRemoveEmptyDisallowed(t *testing.T) {
|
||||
set := ppMakeSet("AB")
|
||||
out := ppCharsetRemove("AB", set, false)
|
||||
// when result would be empty and allowEmpty=false, it returns the original
|
||||
tst.AssertEqual(t, out, "AB")
|
||||
}
|
||||
|
||||
func TestPpCharsetRemoveEmptyAllowed(t *testing.T) {
|
||||
set := ppMakeSet("AB")
|
||||
out := ppCharsetRemove("AB", set, true)
|
||||
tst.AssertEqual(t, out, "")
|
||||
}
|
||||
|
||||
func TestPpCharsetFilter(t *testing.T) {
|
||||
set := ppMakeSet("AEIOU")
|
||||
out := ppCharsetFilter("ABCDEFG", set, false)
|
||||
tst.AssertEqual(t, out, "AE")
|
||||
}
|
||||
|
||||
func TestPpCharsetFilterEmptyDisallowed(t *testing.T) {
|
||||
set := ppMakeSet("XYZ")
|
||||
out := ppCharsetFilter("ABC", set, false)
|
||||
tst.AssertEqual(t, out, "ABC") // returns original when result empty & not allowed
|
||||
}
|
||||
|
||||
func TestPpCharsetFilterEmptyAllowed(t *testing.T) {
|
||||
set := ppMakeSet("XYZ")
|
||||
out := ppCharsetFilter("ABC", set, true)
|
||||
tst.AssertEqual(t, out, "")
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordContinuationFollowsRules(t *testing.T) {
|
||||
// Make sure each continuation pair (lowercased) appears in ppContinuation
|
||||
// Note: when a new segment starts (uppercase letter mid-string), the continuation
|
||||
// check does not apply across the segment boundary.
|
||||
for s := range 30 {
|
||||
seed := int64(s)
|
||||
pw := PronouncablePasswordSeeded(seed, 32)
|
||||
if len(pw) < 2 {
|
||||
continue
|
||||
}
|
||||
runes := []byte(strings.ToUpper(pw))
|
||||
for i := 1; i < len(runes); i++ {
|
||||
// Detect new segment (original char was uppercase and it's not the first char)
|
||||
origUpper := pw[i] >= 'A' && pw[i] <= 'Z'
|
||||
if origUpper && i > 0 {
|
||||
continue
|
||||
}
|
||||
prev := runes[i-1]
|
||||
cur := runes[i]
|
||||
cont, ok := ppContinuation[prev]
|
||||
if !ok {
|
||||
t.Errorf("no continuation map for %q (pw=%q)", prev, pw)
|
||||
continue
|
||||
}
|
||||
if !strings.ContainsRune(cont, rune(cur)) {
|
||||
t.Errorf("invalid continuation %q -> %q in %q (seed %d)", prev, cur, pw, seed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package ctxext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type ctxKey string
|
||||
|
||||
const (
|
||||
keyString ctxKey = "string-key"
|
||||
keyInt ctxKey = "int-key"
|
||||
keyStruct ctxKey = "struct-key"
|
||||
keyPtr ctxKey = "ptr-key"
|
||||
keyMissing ctxKey = "missing-key"
|
||||
)
|
||||
|
||||
type sampleStruct struct {
|
||||
Name string
|
||||
N int
|
||||
}
|
||||
|
||||
func TestValueStringPresent(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), keyString, "hello")
|
||||
v, ok := Value[string](ctx, keyString)
|
||||
tst.AssertEqual(t, ok, true)
|
||||
tst.AssertEqual(t, v, "hello")
|
||||
}
|
||||
|
||||
func TestValueIntPresent(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), keyInt, 42)
|
||||
v, ok := Value[int](ctx, keyInt)
|
||||
tst.AssertEqual(t, ok, true)
|
||||
tst.AssertEqual(t, v, 42)
|
||||
}
|
||||
|
||||
func TestValueStructPresent(t *testing.T) {
|
||||
want := sampleStruct{Name: "abc", N: 7}
|
||||
ctx := context.WithValue(context.Background(), keyStruct, want)
|
||||
v, ok := Value[sampleStruct](ctx, keyStruct)
|
||||
tst.AssertEqual(t, ok, true)
|
||||
tst.AssertEqual(t, v.Name, "abc")
|
||||
tst.AssertEqual(t, v.N, 7)
|
||||
}
|
||||
|
||||
func TestValuePointerPresent(t *testing.T) {
|
||||
want := &sampleStruct{Name: "ptr", N: 99}
|
||||
ctx := context.WithValue(context.Background(), keyPtr, want)
|
||||
v, ok := Value[*sampleStruct](ctx, keyPtr)
|
||||
tst.AssertEqual(t, ok, true)
|
||||
tst.AssertEqual(t, v == want, true)
|
||||
tst.AssertEqual(t, v.Name, "ptr")
|
||||
}
|
||||
|
||||
func TestValueMissing(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
v, ok := Value[string](ctx, keyMissing)
|
||||
tst.AssertEqual(t, ok, false)
|
||||
tst.AssertEqual(t, v, "")
|
||||
}
|
||||
|
||||
func TestValueMissingInt(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
v, ok := Value[int](ctx, keyMissing)
|
||||
tst.AssertEqual(t, ok, false)
|
||||
tst.AssertEqual(t, v, 0)
|
||||
}
|
||||
|
||||
func TestValueMissingStruct(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
v, ok := Value[sampleStruct](ctx, keyMissing)
|
||||
tst.AssertEqual(t, ok, false)
|
||||
tst.AssertEqual(t, v.Name, "")
|
||||
tst.AssertEqual(t, v.N, 0)
|
||||
}
|
||||
|
||||
func TestValueMissingPointer(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
v, ok := Value[*sampleStruct](ctx, keyMissing)
|
||||
tst.AssertEqual(t, ok, false)
|
||||
tst.AssertEqual(t, v == nil, true)
|
||||
}
|
||||
|
||||
func TestValueWrongType(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), keyString, "hello")
|
||||
v, ok := Value[int](ctx, keyString)
|
||||
tst.AssertEqual(t, ok, false)
|
||||
tst.AssertEqual(t, v, 0)
|
||||
}
|
||||
|
||||
func TestValueWrongTypeStructToString(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), keyStruct, sampleStruct{Name: "x"})
|
||||
v, ok := Value[string](ctx, keyStruct)
|
||||
tst.AssertEqual(t, ok, false)
|
||||
tst.AssertEqual(t, v, "")
|
||||
}
|
||||
|
||||
func TestValueNilStoredAsInterface(t *testing.T) {
|
||||
var stored *sampleStruct = nil
|
||||
ctx := context.WithValue(context.Background(), keyPtr, stored)
|
||||
v, ok := Value[*sampleStruct](ctx, keyPtr)
|
||||
tst.AssertEqual(t, ok, true)
|
||||
tst.AssertEqual(t, v == nil, true)
|
||||
}
|
||||
|
||||
func TestValueEmptyString(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), keyString, "")
|
||||
v, ok := Value[string](ctx, keyString)
|
||||
tst.AssertEqual(t, ok, true)
|
||||
tst.AssertEqual(t, v, "")
|
||||
}
|
||||
|
||||
func TestValueZeroInt(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), keyInt, 0)
|
||||
v, ok := Value[int](ctx, keyInt)
|
||||
tst.AssertEqual(t, ok, true)
|
||||
tst.AssertEqual(t, v, 0)
|
||||
}
|
||||
|
||||
func TestValueWithStringKey(t *testing.T) {
|
||||
type stringKey string
|
||||
k := stringKey("my-key")
|
||||
ctx := context.WithValue(context.Background(), k, "value")
|
||||
v, ok := Value[string](ctx, k)
|
||||
tst.AssertEqual(t, ok, true)
|
||||
tst.AssertEqual(t, v, "value")
|
||||
}
|
||||
|
||||
func TestValueOrDefaultPresent(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), keyString, "hello")
|
||||
v := ValueOrDefault(ctx, keyString, "default")
|
||||
tst.AssertEqual(t, v, "hello")
|
||||
}
|
||||
|
||||
func TestValueOrDefaultIntPresent(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), keyInt, 42)
|
||||
v := ValueOrDefault(ctx, keyInt, -1)
|
||||
tst.AssertEqual(t, v, 42)
|
||||
}
|
||||
|
||||
func TestValueOrDefaultMissing(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
v := ValueOrDefault(ctx, keyMissing, "default")
|
||||
tst.AssertEqual(t, v, "default")
|
||||
}
|
||||
|
||||
func TestValueOrDefaultMissingInt(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
v := ValueOrDefault(ctx, keyMissing, 99)
|
||||
tst.AssertEqual(t, v, 99)
|
||||
}
|
||||
|
||||
func TestValueOrDefaultMissingStruct(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
def := sampleStruct{Name: "default", N: 1}
|
||||
v := ValueOrDefault(ctx, keyMissing, def)
|
||||
tst.AssertEqual(t, v.Name, "default")
|
||||
tst.AssertEqual(t, v.N, 1)
|
||||
}
|
||||
|
||||
func TestValueOrDefaultWrongType(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), keyString, "hello")
|
||||
v := ValueOrDefault(ctx, keyString, 7)
|
||||
tst.AssertEqual(t, v, 7)
|
||||
}
|
||||
|
||||
func TestValueOrDefaultWrongTypeStruct(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), keyStruct, sampleStruct{Name: "x"})
|
||||
def := "fallback"
|
||||
v := ValueOrDefault(ctx, keyStruct, def)
|
||||
tst.AssertEqual(t, v, "fallback")
|
||||
}
|
||||
|
||||
func TestValueOrDefaultEmptyStringStored(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), keyString, "")
|
||||
v := ValueOrDefault(ctx, keyString, "default")
|
||||
tst.AssertEqual(t, v, "")
|
||||
}
|
||||
|
||||
func TestValueOrDefaultZeroIntStored(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), keyInt, 0)
|
||||
v := ValueOrDefault(ctx, keyInt, 99)
|
||||
tst.AssertEqual(t, v, 0)
|
||||
}
|
||||
|
||||
func TestValueOrDefaultPointerPresent(t *testing.T) {
|
||||
want := &sampleStruct{Name: "p", N: 5}
|
||||
ctx := context.WithValue(context.Background(), keyPtr, want)
|
||||
def := &sampleStruct{Name: "def", N: 0}
|
||||
v := ValueOrDefault(ctx, keyPtr, def)
|
||||
tst.AssertEqual(t, v == want, true)
|
||||
}
|
||||
|
||||
func TestValueOrDefaultPointerMissing(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
def := &sampleStruct{Name: "def", N: 0}
|
||||
v := ValueOrDefault(ctx, keyMissing, def)
|
||||
tst.AssertEqual(t, v == def, true)
|
||||
}
|
||||
|
||||
func TestValueOrDefaultNilPointerStored(t *testing.T) {
|
||||
var stored *sampleStruct = nil
|
||||
ctx := context.WithValue(context.Background(), keyPtr, stored)
|
||||
def := &sampleStruct{Name: "def"}
|
||||
v := ValueOrDefault(ctx, keyPtr, def)
|
||||
tst.AssertEqual(t, v == nil, true)
|
||||
}
|
||||
|
||||
func TestValueNestedContext(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), keyString, "outer")
|
||||
ctx = context.WithValue(ctx, keyInt, 123)
|
||||
ctx = context.WithValue(ctx, keyString, "inner")
|
||||
|
||||
vs, oks := Value[string](ctx, keyString)
|
||||
tst.AssertEqual(t, oks, true)
|
||||
tst.AssertEqual(t, vs, "inner")
|
||||
|
||||
vi, oki := Value[int](ctx, keyInt)
|
||||
tst.AssertEqual(t, oki, true)
|
||||
tst.AssertEqual(t, vi, 123)
|
||||
}
|
||||
|
||||
func TestValueDifferentKeyTypesDoNotCollide(t *testing.T) {
|
||||
type keyA string
|
||||
type keyB string
|
||||
ctx := context.WithValue(context.Background(), keyA("k"), "a-val")
|
||||
ctx = context.WithValue(ctx, keyB("k"), "b-val")
|
||||
|
||||
va, oka := Value[string](ctx, keyA("k"))
|
||||
tst.AssertEqual(t, oka, true)
|
||||
tst.AssertEqual(t, va, "a-val")
|
||||
|
||||
vb, okb := Value[string](ctx, keyB("k"))
|
||||
tst.AssertEqual(t, okb, true)
|
||||
tst.AssertEqual(t, vb, "b-val")
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cursortoken
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSortDirectionToMongoASC(t *testing.T) {
|
||||
tst.AssertEqual(t, SortASC.ToMongo(), 1)
|
||||
}
|
||||
|
||||
func TestSortDirectionToMongoDESC(t *testing.T) {
|
||||
tst.AssertEqual(t, SortDESC.ToMongo(), -1)
|
||||
}
|
||||
|
||||
func TestSortDirectionToMongoEmpty(t *testing.T) {
|
||||
var sd SortDirection
|
||||
tst.AssertEqual(t, sd.ToMongo(), 0)
|
||||
}
|
||||
|
||||
func TestSortDirectionToMongoUnknown(t *testing.T) {
|
||||
sd := SortDirection("xyz")
|
||||
tst.AssertEqual(t, sd.ToMongo(), 0)
|
||||
}
|
||||
|
||||
func TestSortDirectionConstants(t *testing.T) {
|
||||
tst.AssertEqual(t, string(SortASC), "ASC")
|
||||
tst.AssertEqual(t, string(SortDESC), "DESC")
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package cursortoken
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStartToken(t *testing.T) {
|
||||
tok := Start()
|
||||
tst.AssertEqual(t, tok.Token(), "@start")
|
||||
tst.AssertTrue(t, tok.IsStart())
|
||||
tst.AssertFalse(t, tok.IsEnd())
|
||||
}
|
||||
|
||||
func TestEndToken(t *testing.T) {
|
||||
tok := End()
|
||||
tst.AssertEqual(t, tok.Token(), "@end")
|
||||
tst.AssertTrue(t, tok.IsEnd())
|
||||
tst.AssertFalse(t, tok.IsStart())
|
||||
}
|
||||
|
||||
func TestNewKeySortTokenBasic(t *testing.T) {
|
||||
tok := NewKeySortToken("alpha", "beta", SortASC, SortDESC, 50, Extra{})
|
||||
tst.AssertFalse(t, tok.IsEnd())
|
||||
tst.AssertFalse(t, tok.IsStart())
|
||||
str := tok.Token()
|
||||
tst.AssertTrue(t, strings.HasPrefix(str, "tok_"))
|
||||
}
|
||||
|
||||
func TestNewKeySortTokenRoundTrip(t *testing.T) {
|
||||
original := NewKeySortToken("primary-val", "secondary-val", SortASC, SortDESC, 25, Extra{})
|
||||
encoded := original.Token()
|
||||
|
||||
decoded, err := Decode(encoded)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
ks, ok := decoded.(CTKeySort)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, ks.ValuePrimary, "primary-val")
|
||||
tst.AssertEqual(t, ks.ValueSecondary, "secondary-val")
|
||||
tst.AssertEqual(t, ks.Direction, SortASC)
|
||||
tst.AssertEqual(t, ks.DirectionSecondary, SortDESC)
|
||||
tst.AssertEqual(t, ks.PageSize, 25)
|
||||
tst.AssertEqual(t, ks.Mode, CTMNormal)
|
||||
}
|
||||
|
||||
func TestKeySortTokenWithExtra(t *testing.T) {
|
||||
ts := time.Date(2024, 6, 15, 10, 30, 0, 0, time.UTC)
|
||||
id := "object-id-123"
|
||||
page := 7
|
||||
pageSize := 42
|
||||
|
||||
original := NewKeySortToken("p", "s", SortDESC, SortASC, 10, Extra{
|
||||
Timestamp: &ts,
|
||||
Id: &id,
|
||||
Page: &page,
|
||||
PageSize: &pageSize,
|
||||
})
|
||||
encoded := original.Token()
|
||||
|
||||
decoded, err := Decode(encoded)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
ks, ok := decoded.(CTKeySort)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertTrue(t, ks.Extra.Timestamp != nil)
|
||||
tst.AssertTrue(t, ks.Extra.Timestamp.Equal(ts))
|
||||
tst.AssertDeRefEqual(t, ks.Extra.Id, "object-id-123")
|
||||
tst.AssertDeRefEqual(t, ks.Extra.Page, 7)
|
||||
tst.AssertDeRefEqual(t, ks.Extra.PageSize, 42)
|
||||
}
|
||||
|
||||
func TestKeySortTokenStartRoundTrip(t *testing.T) {
|
||||
original := Start()
|
||||
decoded, err := Decode(original.Token())
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, decoded.IsStart())
|
||||
tst.AssertFalse(t, decoded.IsEnd())
|
||||
}
|
||||
|
||||
func TestKeySortTokenEndRoundTrip(t *testing.T) {
|
||||
original := End()
|
||||
decoded, err := Decode(original.Token())
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, decoded.IsEnd())
|
||||
tst.AssertFalse(t, decoded.IsStart())
|
||||
}
|
||||
|
||||
func TestKeySortTokenEmptyValues(t *testing.T) {
|
||||
tok := CTKeySort{Mode: CTMNormal}
|
||||
encoded := tok.Token()
|
||||
tst.AssertTrue(t, strings.HasPrefix(encoded, "tok_"))
|
||||
|
||||
decoded, err := Decode(encoded)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
ks, ok := decoded.(CTKeySort)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, ks.ValuePrimary, "")
|
||||
tst.AssertEqual(t, ks.ValueSecondary, "")
|
||||
tst.AssertEqual(t, ks.Direction, SortDirection(""))
|
||||
tst.AssertEqual(t, ks.DirectionSecondary, SortDirection(""))
|
||||
tst.AssertEqual(t, ks.PageSize, 0)
|
||||
}
|
||||
|
||||
func TestKeySortTokenOnlyTimestamp(t *testing.T) {
|
||||
ts := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
tok := CTKeySort{
|
||||
Mode: CTMNormal,
|
||||
Extra: Extra{Timestamp: &ts},
|
||||
}
|
||||
|
||||
decoded, err := Decode(tok.Token())
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
ks, ok := decoded.(CTKeySort)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertTrue(t, ks.Extra.Timestamp != nil)
|
||||
tst.AssertTrue(t, ks.Extra.Timestamp.Equal(ts))
|
||||
tst.AssertTrue(t, ks.Extra.Id == nil)
|
||||
tst.AssertTrue(t, ks.Extra.Page == nil)
|
||||
tst.AssertTrue(t, ks.Extra.PageSize == nil)
|
||||
}
|
||||
|
||||
func TestKeySortTokenSpecialChars(t *testing.T) {
|
||||
original := NewKeySortToken("hello world / @!#$%", "äöü€", SortASC, SortASC, 1, Extra{})
|
||||
decoded, err := Decode(original.Token())
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
ks, ok := decoded.(CTKeySort)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, ks.ValuePrimary, "hello world / @!#$%")
|
||||
tst.AssertEqual(t, ks.ValueSecondary, "äöü€")
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package cursortoken
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPageToken(t *testing.T) {
|
||||
tok := Page(5)
|
||||
tst.AssertEqual(t, tok.Token(), "$5")
|
||||
tst.AssertFalse(t, tok.IsEnd())
|
||||
tst.AssertFalse(t, tok.IsStart())
|
||||
}
|
||||
|
||||
func TestPageTokenOne(t *testing.T) {
|
||||
tok := Page(1)
|
||||
tst.AssertEqual(t, tok.Token(), "$1")
|
||||
tst.AssertFalse(t, tok.IsEnd())
|
||||
tst.AssertTrue(t, tok.IsStart())
|
||||
}
|
||||
|
||||
func TestPageTokenLarge(t *testing.T) {
|
||||
tok := Page(123456)
|
||||
tst.AssertEqual(t, tok.Token(), "$123456")
|
||||
}
|
||||
|
||||
func TestPageTokenZero(t *testing.T) {
|
||||
tok := Page(0)
|
||||
tst.AssertEqual(t, tok.Token(), "$0")
|
||||
tst.AssertFalse(t, tok.IsEnd())
|
||||
tst.AssertFalse(t, tok.IsStart())
|
||||
}
|
||||
|
||||
func TestPageEndToken(t *testing.T) {
|
||||
tok := PageEnd()
|
||||
tst.AssertEqual(t, tok.Token(), "$end")
|
||||
tst.AssertTrue(t, tok.IsEnd())
|
||||
tst.AssertFalse(t, tok.IsStart())
|
||||
}
|
||||
|
||||
func TestPaginatedStartMode(t *testing.T) {
|
||||
tok := CTPaginated{Mode: CTMStart, Page: 0}
|
||||
tst.AssertEqual(t, tok.Token(), "$1")
|
||||
tst.AssertTrue(t, tok.IsStart())
|
||||
tst.AssertFalse(t, tok.IsEnd())
|
||||
}
|
||||
|
||||
func TestPaginatedEndMode(t *testing.T) {
|
||||
tok := CTPaginated{Mode: CTMEnd, Page: 99}
|
||||
tst.AssertEqual(t, tok.Token(), "$end")
|
||||
tst.AssertTrue(t, tok.IsEnd())
|
||||
}
|
||||
|
||||
func TestPaginatedRoundTrip(t *testing.T) {
|
||||
for _, page := range []int{2, 3, 7, 100, 9999} {
|
||||
tok := Page(page)
|
||||
decoded, err := Decode(tok.Token())
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, decoded.Token(), tok.Token())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package cursortoken
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecodeEmpty(t *testing.T) {
|
||||
tok, err := Decode("")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, tok.IsStart())
|
||||
tst.AssertFalse(t, tok.IsEnd())
|
||||
tst.AssertEqual(t, tok.Token(), "@start")
|
||||
}
|
||||
|
||||
func TestDecodeAtStart(t *testing.T) {
|
||||
tok, err := Decode("@start")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, tok.IsStart())
|
||||
tst.AssertFalse(t, tok.IsEnd())
|
||||
}
|
||||
|
||||
func TestDecodeAtStartUppercase(t *testing.T) {
|
||||
tok, err := Decode("@START")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, tok.IsStart())
|
||||
}
|
||||
|
||||
func TestDecodeAtStartMixedCase(t *testing.T) {
|
||||
tok, err := Decode("@StArT")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, tok.IsStart())
|
||||
}
|
||||
|
||||
func TestDecodeAtEnd(t *testing.T) {
|
||||
tok, err := Decode("@end")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, tok.IsEnd())
|
||||
tst.AssertFalse(t, tok.IsStart())
|
||||
}
|
||||
|
||||
func TestDecodeAtEndUppercase(t *testing.T) {
|
||||
tok, err := Decode("@END")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, tok.IsEnd())
|
||||
}
|
||||
|
||||
func TestDecodeDollarEnd(t *testing.T) {
|
||||
tok, err := Decode("$end")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, tok.IsEnd())
|
||||
_, ok := tok.(CTPaginated)
|
||||
tst.AssertTrue(t, ok)
|
||||
}
|
||||
|
||||
func TestDecodeDollarEndUppercase(t *testing.T) {
|
||||
tok, err := Decode("$END")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, tok.IsEnd())
|
||||
}
|
||||
|
||||
func TestDecodeDollarPage(t *testing.T) {
|
||||
tok, err := Decode("$5")
|
||||
tst.AssertNoErr(t, err)
|
||||
pg, ok := tok.(CTPaginated)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, pg.Page, 5)
|
||||
tst.AssertEqual(t, pg.Mode, CTMNormal)
|
||||
}
|
||||
|
||||
func TestDecodeDollarPageOne(t *testing.T) {
|
||||
tok, err := Decode("$1")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, tok.IsStart())
|
||||
pg, ok := tok.(CTPaginated)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, pg.Page, 1)
|
||||
}
|
||||
|
||||
func TestDecodeDollarPageInvalid(t *testing.T) {
|
||||
_, err := Decode("$abc")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for invalid page")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeUnknownPrefix(t *testing.T) {
|
||||
_, err := Decode("foobar")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for unknown prefix")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeInvalidBase32(t *testing.T) {
|
||||
_, err := Decode("tok_!!!")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for invalid base32 body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeInvalidJSON(t *testing.T) {
|
||||
// "tok_" prefix with valid base32 but invalid JSON content
|
||||
_, err := Decode("tok_NBSWY3DP")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for invalid json body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeJustDollar(t *testing.T) {
|
||||
// "$" alone (length == 1) should fall through to the unknown-prefix branch
|
||||
_, err := Decode("$")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for bare $")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeKnownTokenContent(t *testing.T) {
|
||||
tok := NewKeySortToken("k1", "k2", SortASC, SortDESC, 33, Extra{})
|
||||
encoded := tok.Token()
|
||||
|
||||
decoded, err := Decode(encoded)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, decoded.Token(), encoded)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package dataext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeReadCloser struct {
|
||||
r *bytes.Reader
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newFakeReadCloser(data []byte) *fakeReadCloser {
|
||||
return &fakeReadCloser{r: bytes.NewReader(data)}
|
||||
}
|
||||
|
||||
func (f *fakeReadCloser) Read(p []byte) (int, error) {
|
||||
return f.r.Read(p)
|
||||
}
|
||||
|
||||
func (f *fakeReadCloser) Close() error {
|
||||
f.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestBufferedReadCloser_ReadAll(t *testing.T) {
|
||||
data := []byte("hello world")
|
||||
brc := NewBufferedReadCloser(newFakeReadCloser(data))
|
||||
|
||||
buf := make([]byte, 64)
|
||||
total := 0
|
||||
for {
|
||||
n, err := brc.Read(buf[total:])
|
||||
total += n
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !bytes.Equal(buf[:total], data) {
|
||||
t.Fatalf("got %q want %q", buf[:total], data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufferedReadCloser_BufferedAllThenRead(t *testing.T) {
|
||||
data := []byte("foobar baz")
|
||||
brc := NewBufferedReadCloser(newFakeReadCloser(data))
|
||||
|
||||
all, err := brc.BufferedAll()
|
||||
if err != nil {
|
||||
t.Fatalf("BufferedAll err: %v", err)
|
||||
}
|
||||
if !bytes.Equal(all, data) {
|
||||
t.Fatalf("BufferedAll got %q want %q", all, data)
|
||||
}
|
||||
|
||||
// after BufferedAll, Reset put us in BufferReading mode - we can read again
|
||||
out, err := io.ReadAll(brc)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAll err: %v", err)
|
||||
}
|
||||
if !bytes.Equal(out, data) {
|
||||
t.Fatalf("ReadAll got %q want %q", out, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufferedReadCloser_FullyReadResetReread(t *testing.T) {
|
||||
data := []byte("abcdefghij")
|
||||
brc := NewBufferedReadCloser(newFakeReadCloser(data))
|
||||
|
||||
out, err := io.ReadAll(brc)
|
||||
if err != nil {
|
||||
t.Fatalf("first ReadAll err: %v", err)
|
||||
}
|
||||
if !bytes.Equal(out, data) {
|
||||
t.Fatalf("first read got %q want %q", out, data)
|
||||
}
|
||||
|
||||
if err := brc.Reset(); err != nil {
|
||||
t.Fatalf("reset err: %v", err)
|
||||
}
|
||||
|
||||
out2, err := io.ReadAll(brc)
|
||||
if err != nil {
|
||||
t.Fatalf("second ReadAll err: %v", err)
|
||||
}
|
||||
if !bytes.Equal(out2, data) {
|
||||
t.Fatalf("after reset got %q want %q", out2, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufferedReadCloser_Close(t *testing.T) {
|
||||
data := []byte("xyz")
|
||||
inner := newFakeReadCloser(data)
|
||||
brc := NewBufferedReadCloser(inner)
|
||||
|
||||
if err := brc.Close(); err != nil {
|
||||
t.Fatalf("close err: %v", err)
|
||||
}
|
||||
if !inner.closed {
|
||||
t.Fatal("inner not closed")
|
||||
}
|
||||
|
||||
// double close should be no-op
|
||||
if err := brc.Close(); err != nil {
|
||||
t.Fatalf("second close err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufferedReadCloser_ResetWithoutRead(t *testing.T) {
|
||||
data := []byte("abc")
|
||||
brc := NewBufferedReadCloser(newFakeReadCloser(data))
|
||||
|
||||
if err := brc.Reset(); err != nil {
|
||||
t.Fatalf("reset err: %v", err)
|
||||
}
|
||||
|
||||
out, err := io.ReadAll(brc)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAll err: %v", err)
|
||||
}
|
||||
if !bytes.Equal(out, data) {
|
||||
t.Fatalf("got %q want %q", out, data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package dataext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCASMutex_LockUnlock(t *testing.T) {
|
||||
m := NewCASMutex()
|
||||
m.Lock()
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
func TestCASMutex_TryLock(t *testing.T) {
|
||||
m := NewCASMutex()
|
||||
if !m.TryLock() {
|
||||
t.Fatal("TryLock should succeed on fresh mutex")
|
||||
}
|
||||
if m.TryLock() {
|
||||
t.Fatal("TryLock should fail when already locked")
|
||||
}
|
||||
m.Unlock()
|
||||
if !m.TryLock() {
|
||||
t.Fatal("TryLock should succeed after Unlock")
|
||||
}
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
func TestCASMutex_TryLockWithTimeout(t *testing.T) {
|
||||
m := NewCASMutex()
|
||||
m.Lock()
|
||||
start := time.Now()
|
||||
if m.TryLockWithTimeout(20 * time.Millisecond) {
|
||||
t.Fatal("TryLockWithTimeout should fail when locked")
|
||||
}
|
||||
if time.Since(start) < 15*time.Millisecond {
|
||||
t.Fatal("TryLockWithTimeout returned too quickly")
|
||||
}
|
||||
m.Unlock()
|
||||
|
||||
if !m.TryLockWithTimeout(50 * time.Millisecond) {
|
||||
t.Fatal("TryLockWithTimeout should succeed when unlocked")
|
||||
}
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
func TestCASMutex_TryLockWithContext_Cancel(t *testing.T) {
|
||||
m := NewCASMutex()
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
if m.TryLockWithContext(ctx) {
|
||||
t.Fatal("expected lock to fail after cancel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCASMutex_RLockMultiple(t *testing.T) {
|
||||
m := NewCASMutex()
|
||||
if !m.RTryLock() {
|
||||
t.Fatal("RTryLock should succeed")
|
||||
}
|
||||
if !m.RTryLock() {
|
||||
t.Fatal("Second RTryLock should succeed")
|
||||
}
|
||||
if m.TryLock() {
|
||||
t.Fatal("Write TryLock should fail with read locks held")
|
||||
}
|
||||
m.RUnlock()
|
||||
m.RUnlock()
|
||||
if !m.TryLock() {
|
||||
t.Fatal("Write TryLock should succeed after read unlocks")
|
||||
}
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
func TestCASMutex_RLocker(t *testing.T) {
|
||||
m := NewCASMutex()
|
||||
rl := m.RLocker()
|
||||
rl.Lock()
|
||||
rl.Unlock()
|
||||
}
|
||||
|
||||
func TestCASMutex_Concurrent(t *testing.T) {
|
||||
m := NewCASMutex()
|
||||
var counter int64
|
||||
const n = 50
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
m.Lock()
|
||||
atomic.AddInt64(&counter, 1)
|
||||
m.Unlock()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
if atomic.LoadInt64(&counter) != n {
|
||||
t.Fatalf("counter=%d want %d", counter, n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCASMutex_RTryLockWithTimeout(t *testing.T) {
|
||||
m := NewCASMutex()
|
||||
m.Lock()
|
||||
if m.RTryLockWithTimeout(20 * time.Millisecond) {
|
||||
t.Fatal("RTryLockWithTimeout should fail when write-locked")
|
||||
}
|
||||
m.Unlock()
|
||||
if !m.RTryLockWithTimeout(20 * time.Millisecond) {
|
||||
t.Fatal("RTryLockWithTimeout should succeed when free")
|
||||
}
|
||||
m.RUnlock()
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package dataext
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func waitForCalls(t *testing.T, calls *int64, want int64, max time.Duration) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(max)
|
||||
for time.Now().Before(deadline) {
|
||||
if atomic.LoadInt64(calls) >= want {
|
||||
return
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelayedCombiningInvoker_SingleRequest(t *testing.T) {
|
||||
var calls int64
|
||||
d := NewDelayedCombiningInvoker(func() {
|
||||
atomic.AddInt64(&calls, 1)
|
||||
}, 20*time.Millisecond, 200*time.Millisecond)
|
||||
|
||||
d.Request()
|
||||
|
||||
waitForCalls(t, &calls, 1, 2*time.Second)
|
||||
if c := atomic.LoadInt64(&calls); c != 1 {
|
||||
t.Fatalf("calls=%d want 1", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelayedCombiningInvoker_TwoRequestsCombine(t *testing.T) {
|
||||
var calls int64
|
||||
d := NewDelayedCombiningInvoker(func() {
|
||||
atomic.AddInt64(&calls, 1)
|
||||
}, 50*time.Millisecond, 1*time.Second)
|
||||
|
||||
d.Request()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
d.Request()
|
||||
|
||||
waitForCalls(t, &calls, 1, 2*time.Second)
|
||||
if c := atomic.LoadInt64(&calls); c != 1 {
|
||||
t.Fatalf("calls=%d want 1 (should be combined)", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelayedCombiningInvoker_SequentialRuns(t *testing.T) {
|
||||
var calls int64
|
||||
d := NewDelayedCombiningInvoker(func() {
|
||||
atomic.AddInt64(&calls, 1)
|
||||
}, 20*time.Millisecond, 200*time.Millisecond)
|
||||
|
||||
d.Request()
|
||||
waitForCalls(t, &calls, 1, 2*time.Second)
|
||||
if c := atomic.LoadInt64(&calls); c != 1 {
|
||||
t.Fatalf("after first wait calls=%d want 1", c)
|
||||
}
|
||||
|
||||
// allow executorRunning to clear
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
d.Request()
|
||||
waitForCalls(t, &calls, 2, 2*time.Second)
|
||||
if c := atomic.LoadInt64(&calls); c != 2 {
|
||||
t.Fatalf("calls=%d want 2", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelayedCombiningInvoker_ExecuteNow(t *testing.T) {
|
||||
var calls int64
|
||||
d := NewDelayedCombiningInvoker(func() {
|
||||
atomic.AddInt64(&calls, 1)
|
||||
}, 5*time.Second, 30*time.Second)
|
||||
|
||||
d.Request()
|
||||
if !d.HasPendingRequests() {
|
||||
t.Fatal("should have pending requests")
|
||||
}
|
||||
|
||||
if !d.ExecuteNow() {
|
||||
t.Fatal("ExecuteNow should return true when running")
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if atomic.LoadInt64(&calls) >= 1 {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if c := atomic.LoadInt64(&calls); c != 1 {
|
||||
t.Fatalf("calls=%d want 1 (ExecuteNow should fire well before delay)", c)
|
||||
}
|
||||
|
||||
// allow internal state cleanup
|
||||
for i := 0; i < 100; i++ {
|
||||
if !d.HasPendingRequests() {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if d.ExecuteNow() {
|
||||
t.Fatal("ExecuteNow should return false when no pending")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelayedCombiningInvoker_Cancel(t *testing.T) {
|
||||
var calls int64
|
||||
d := NewDelayedCombiningInvoker(func() {
|
||||
atomic.AddInt64(&calls, 1)
|
||||
}, 500*time.Millisecond, 5*time.Second)
|
||||
|
||||
d.Request()
|
||||
d.CancelPendingRequests()
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if c := atomic.LoadInt64(&calls); c != 0 {
|
||||
t.Fatalf("calls=%d want 0 after cancel", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelayedCombiningInvoker_HasAndCountPending(t *testing.T) {
|
||||
d := NewDelayedCombiningInvoker(func() {
|
||||
// no-op
|
||||
}, 500*time.Millisecond, 5*time.Second)
|
||||
|
||||
if d.HasPendingRequests() {
|
||||
t.Fatal("should not have pending before any Request")
|
||||
}
|
||||
if d.CountPendingRequests() != 0 {
|
||||
t.Fatalf("count=%d want 0", d.CountPendingRequests())
|
||||
}
|
||||
|
||||
d.Request()
|
||||
if !d.HasPendingRequests() {
|
||||
t.Fatal("should have pending")
|
||||
}
|
||||
if d.CountPendingRequests() < 1 {
|
||||
t.Fatalf("count=%d want >=1", d.CountPendingRequests())
|
||||
}
|
||||
d.CancelPendingRequests()
|
||||
}
|
||||
|
||||
func TestDelayedCombiningInvoker_Listeners(t *testing.T) {
|
||||
var (
|
||||
startCount int64
|
||||
doneCount int64
|
||||
requestCount int64
|
||||
)
|
||||
|
||||
d := NewDelayedCombiningInvoker(func() {
|
||||
// no-op
|
||||
}, 20*time.Millisecond, 200*time.Millisecond)
|
||||
|
||||
d.RegisterOnExecutionStart(func(immediately bool) {
|
||||
atomic.AddInt64(&startCount, 1)
|
||||
})
|
||||
d.RegisterOnExecutionDone(func() {
|
||||
atomic.AddInt64(&doneCount, 1)
|
||||
})
|
||||
d.RegisterOnRequest(func(pending int, initial bool) {
|
||||
atomic.AddInt64(&requestCount, 1)
|
||||
})
|
||||
|
||||
d.Request()
|
||||
|
||||
waitForCalls(t, &doneCount, 1, 2*time.Second)
|
||||
|
||||
if atomic.LoadInt64(&startCount) != 1 {
|
||||
t.Fatalf("startCount=%d want 1", startCount)
|
||||
}
|
||||
if atomic.LoadInt64(&doneCount) != 1 {
|
||||
t.Fatalf("doneCount=%d want 1", doneCount)
|
||||
}
|
||||
if atomic.LoadInt64(&requestCount) != 1 {
|
||||
t.Fatalf("requestCount=%d want 1", requestCount)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package dataext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMultiMutex_LockDifferentKeys(t *testing.T) {
|
||||
mm := NewMultiMutex[string]()
|
||||
mm.Lock("a")
|
||||
mm.Lock("b")
|
||||
mm.Unlock("a")
|
||||
mm.Unlock("b")
|
||||
}
|
||||
|
||||
func TestMultiMutex_TryLockSameKey(t *testing.T) {
|
||||
mm := NewMultiMutex[string]()
|
||||
if !mm.TryLock("k") {
|
||||
t.Fatal("TryLock should succeed first time")
|
||||
}
|
||||
if mm.TryLock("k") {
|
||||
t.Fatal("TryLock should fail second time")
|
||||
}
|
||||
mm.Unlock("k")
|
||||
if !mm.TryLock("k") {
|
||||
t.Fatal("TryLock should succeed after unlock")
|
||||
}
|
||||
mm.Unlock("k")
|
||||
}
|
||||
|
||||
func TestMultiMutex_TryLockDifferentKeys(t *testing.T) {
|
||||
mm := NewMultiMutex[int]()
|
||||
if !mm.TryLock(1) {
|
||||
t.Fatal("TryLock(1) failed")
|
||||
}
|
||||
if !mm.TryLock(2) {
|
||||
t.Fatal("TryLock(2) failed - different keys should be independent")
|
||||
}
|
||||
mm.Unlock(1)
|
||||
mm.Unlock(2)
|
||||
}
|
||||
|
||||
func TestMultiMutex_RLockMultiple(t *testing.T) {
|
||||
mm := NewMultiMutex[string]()
|
||||
if !mm.RTryLock("k") {
|
||||
t.Fatal("first RTryLock failed")
|
||||
}
|
||||
if !mm.RTryLock("k") {
|
||||
t.Fatal("second RTryLock failed")
|
||||
}
|
||||
mm.RUnlock("k")
|
||||
mm.RUnlock("k")
|
||||
}
|
||||
|
||||
func TestMultiMutex_TryLockWithTimeout(t *testing.T) {
|
||||
mm := NewMultiMutex[string]()
|
||||
mm.Lock("k")
|
||||
if mm.TryLockWithTimeout("k", 10*time.Millisecond) {
|
||||
t.Fatal("expected timeout failure")
|
||||
}
|
||||
mm.Unlock("k")
|
||||
}
|
||||
|
||||
func TestMultiMutex_TryLockWithContext(t *testing.T) {
|
||||
mm := NewMultiMutex[string]()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
if !mm.TryLockWithContext(ctx, "k") {
|
||||
t.Fatal("TryLockWithContext should succeed on free key")
|
||||
}
|
||||
mm.Unlock("k")
|
||||
}
|
||||
|
||||
func TestMultiMutex_GetAndGetCAS(t *testing.T) {
|
||||
mm := NewMultiMutex[string]()
|
||||
l := mm.Get("a")
|
||||
if l == nil {
|
||||
t.Fatal("Get returned nil")
|
||||
}
|
||||
cas := mm.GetCAS("a")
|
||||
if cas == nil {
|
||||
t.Fatal("GetCAS returned nil")
|
||||
}
|
||||
rl := mm.RLocker("a")
|
||||
if rl == nil {
|
||||
t.Fatal("RLocker returned nil")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package dataext
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMutexSet_BasicLockUnlock(t *testing.T) {
|
||||
ms := NewMutexSet[string]()
|
||||
ms.Lock("a")
|
||||
ms.Unlock("a")
|
||||
ms.RLock("b")
|
||||
ms.RUnlock("b")
|
||||
}
|
||||
|
||||
func TestMutexSet_DifferentKeysIndependent(t *testing.T) {
|
||||
ms := NewMutexSet[int]()
|
||||
ms.Lock(1)
|
||||
ms.Lock(2)
|
||||
ms.Unlock(1)
|
||||
ms.Unlock(2)
|
||||
}
|
||||
|
||||
func TestMutexSet_SameKeyMutuallyExclusive(t *testing.T) {
|
||||
ms := NewMutexSet[string]()
|
||||
var counter int64
|
||||
const n = 50
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ms.Lock("shared")
|
||||
atomic.AddInt64(&counter, 1)
|
||||
ms.Unlock("shared")
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
if atomic.LoadInt64(&counter) != n {
|
||||
t.Fatalf("got %d want %d", counter, n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMutexSet_RLockMultiple(t *testing.T) {
|
||||
ms := NewMutexSet[string]()
|
||||
ms.RLock("k")
|
||||
ms.RLock("k")
|
||||
ms.RUnlock("k")
|
||||
ms.RUnlock("k")
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package dataext
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJsonOpt_NewAndEmpty(t *testing.T) {
|
||||
o := NewJsonOpt[int](42)
|
||||
if !o.IsSet() {
|
||||
t.Fatal("expected IsSet=true")
|
||||
}
|
||||
if o.IsUnset() {
|
||||
t.Fatal("expected IsUnset=false")
|
||||
}
|
||||
|
||||
e := EmptyJsonOpt[int]()
|
||||
if e.IsSet() {
|
||||
t.Fatal("expected IsSet=false")
|
||||
}
|
||||
if !e.IsUnset() {
|
||||
t.Fatal("expected IsUnset=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonOpt_Value(t *testing.T) {
|
||||
o := NewJsonOpt[string]("hello")
|
||||
v, ok := o.Value()
|
||||
if !ok || v != "hello" {
|
||||
t.Fatalf("got (%q,%v)", v, ok)
|
||||
}
|
||||
|
||||
e := EmptyJsonOpt[string]()
|
||||
v, ok = e.Value()
|
||||
if ok || v != "" {
|
||||
t.Fatalf("empty got (%q,%v)", v, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonOpt_ValueOrNil(t *testing.T) {
|
||||
o := NewJsonOpt[int](7)
|
||||
p := o.ValueOrNil()
|
||||
if p == nil || *p != 7 {
|
||||
t.Fatalf("expected ptr to 7")
|
||||
}
|
||||
e := EmptyJsonOpt[int]()
|
||||
if e.ValueOrNil() != nil {
|
||||
t.Fatal("expected nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonOpt_ValueDblPtrOrNil(t *testing.T) {
|
||||
o := NewJsonOpt[int](7)
|
||||
p := o.ValueDblPtrOrNil()
|
||||
if p == nil || *p == nil || **p != 7 {
|
||||
t.Fatalf("expected double ptr to 7")
|
||||
}
|
||||
e := EmptyJsonOpt[int]()
|
||||
if e.ValueDblPtrOrNil() != nil {
|
||||
t.Fatal("expected nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonOpt_MustValue(t *testing.T) {
|
||||
o := NewJsonOpt[int](9)
|
||||
if o.MustValue() != 9 {
|
||||
t.Fatal("MustValue wrong")
|
||||
}
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("expected panic")
|
||||
}
|
||||
}()
|
||||
EmptyJsonOpt[int]().MustValue()
|
||||
}
|
||||
|
||||
func TestJsonOpt_IfSet(t *testing.T) {
|
||||
called := false
|
||||
NewJsonOpt[int](1).IfSet(func(v int) {
|
||||
called = true
|
||||
if v != 1 {
|
||||
t.Fatalf("v=%d", v)
|
||||
}
|
||||
})
|
||||
if !called {
|
||||
t.Fatal("IfSet did not invoke fn")
|
||||
}
|
||||
|
||||
called = false
|
||||
EmptyJsonOpt[int]().IfSet(func(v int) { called = true })
|
||||
if called {
|
||||
t.Fatal("IfSet invoked fn on empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonOpt_MarshalJSON(t *testing.T) {
|
||||
o := NewJsonOpt[int](5)
|
||||
b, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(b) != "5" {
|
||||
t.Fatalf("got %s", b)
|
||||
}
|
||||
|
||||
e := EmptyJsonOpt[int]()
|
||||
b, err = json.Marshal(e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(b) != "null" {
|
||||
t.Fatalf("got %s", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonOpt_UnmarshalJSON(t *testing.T) {
|
||||
var o JsonOpt[int]
|
||||
if err := json.Unmarshal([]byte("42"), &o); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !o.IsSet() {
|
||||
t.Fatal("should be set")
|
||||
}
|
||||
if v, _ := o.Value(); v != 42 {
|
||||
t.Fatalf("got %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonOpt_StructWithJsonOpt(t *testing.T) {
|
||||
type S struct {
|
||||
A JsonOpt[int] `json:"a"`
|
||||
B JsonOpt[string] `json:"b"`
|
||||
}
|
||||
s := S{A: NewJsonOpt[int](1), B: EmptyJsonOpt[string]()}
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(b) != `{"a":1,"b":null}` {
|
||||
t.Fatalf("got %s", b)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package dataext
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStack_PushPop(t *testing.T) {
|
||||
s := NewStack[int](false, 4)
|
||||
s.Push(1)
|
||||
s.Push(2)
|
||||
s.Push(3)
|
||||
|
||||
if s.Length() != 3 {
|
||||
t.Fatalf("Length=%d", s.Length())
|
||||
}
|
||||
if s.Empty() {
|
||||
t.Fatal("should not be empty")
|
||||
}
|
||||
|
||||
v, err := s.Pop()
|
||||
if err != nil || v != 3 {
|
||||
t.Fatalf("Pop got (%d,%v)", v, err)
|
||||
}
|
||||
v, err = s.Pop()
|
||||
if err != nil || v != 2 {
|
||||
t.Fatalf("Pop got (%d,%v)", v, err)
|
||||
}
|
||||
v, err = s.Pop()
|
||||
if err != nil || v != 1 {
|
||||
t.Fatalf("Pop got (%d,%v)", v, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStack_PopEmpty(t *testing.T) {
|
||||
s := NewStack[int](false, 0)
|
||||
_, err := s.Pop()
|
||||
if !errors.Is(err, ErrEmptyStack) {
|
||||
t.Fatalf("expected ErrEmptyStack, got %v", err)
|
||||
}
|
||||
if !s.Empty() {
|
||||
t.Fatal("should be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStack_Peek(t *testing.T) {
|
||||
s := NewStack[string](false, 0)
|
||||
if _, err := s.Peek(); !errors.Is(err, ErrEmptyStack) {
|
||||
t.Fatalf("expected ErrEmptyStack got %v", err)
|
||||
}
|
||||
s.Push("a")
|
||||
s.Push("b")
|
||||
v, err := s.Peek()
|
||||
if err != nil || v != "b" {
|
||||
t.Fatalf("Peek got (%q,%v)", v, err)
|
||||
}
|
||||
if s.Length() != 2 {
|
||||
t.Fatal("Peek must not pop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStack_OptPopOptPeek(t *testing.T) {
|
||||
s := NewStack[int](false, 0)
|
||||
if s.OptPop() != nil {
|
||||
t.Fatal("OptPop on empty should return nil")
|
||||
}
|
||||
if s.OptPeek() != nil {
|
||||
t.Fatal("OptPeek on empty should return nil")
|
||||
}
|
||||
s.Push(7)
|
||||
if p := s.OptPeek(); p == nil || *p != 7 {
|
||||
t.Fatalf("OptPeek bad")
|
||||
}
|
||||
if p := s.OptPop(); p == nil || *p != 7 {
|
||||
t.Fatalf("OptPop bad")
|
||||
}
|
||||
if !s.Empty() {
|
||||
t.Fatal("should be empty after OptPop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStack_ThreadSafe(t *testing.T) {
|
||||
s := NewStack[int](true, 0)
|
||||
var wg sync.WaitGroup
|
||||
const n = 200
|
||||
wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
go func(v int) {
|
||||
defer wg.Done()
|
||||
s.Push(v)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
if s.Length() != n {
|
||||
t.Fatalf("Length=%d want %d", s.Length(), n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package dataext
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSyncMap_SetGet(t *testing.T) {
|
||||
m := NewSyncMap[string, int]()
|
||||
m.Set("a", 1)
|
||||
v, ok := m.Get("a")
|
||||
if !ok || v != 1 {
|
||||
t.Fatalf("got (%d,%v)", v, ok)
|
||||
}
|
||||
if _, ok := m.Get("missing"); ok {
|
||||
t.Fatal("expected missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncMap_SetIfNotContains(t *testing.T) {
|
||||
m := NewSyncMap[string, int]()
|
||||
if !m.SetIfNotContains("a", 1) {
|
||||
t.Fatal("first set should succeed")
|
||||
}
|
||||
if m.SetIfNotContains("a", 2) {
|
||||
t.Fatal("second set should fail")
|
||||
}
|
||||
v, _ := m.Get("a")
|
||||
if v != 1 {
|
||||
t.Fatalf("expected unchanged got %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncMap_SetIfNotContainsFunc(t *testing.T) {
|
||||
m := NewSyncMap[string, int]()
|
||||
calls := 0
|
||||
if !m.SetIfNotContainsFunc("a", func() int { calls++; return 5 }) {
|
||||
t.Fatal("first should succeed")
|
||||
}
|
||||
if m.SetIfNotContainsFunc("a", func() int { calls++; return 6 }) {
|
||||
t.Fatal("second should fail")
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Fatalf("calls=%d want 1", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncMap_GetAndSetIfNotContains(t *testing.T) {
|
||||
m := NewSyncMap[string, int]()
|
||||
if v := m.GetAndSetIfNotContains("a", 10); v != 10 {
|
||||
t.Fatalf("got %d", v)
|
||||
}
|
||||
if v := m.GetAndSetIfNotContains("a", 99); v != 10 {
|
||||
t.Fatalf("got %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncMap_GetAndSetIfNotContainsFunc(t *testing.T) {
|
||||
m := NewSyncMap[string, int]()
|
||||
calls := 0
|
||||
if v := m.GetAndSetIfNotContainsFunc("a", func() int { calls++; return 1 }); v != 1 {
|
||||
t.Fatalf("got %d", v)
|
||||
}
|
||||
if v := m.GetAndSetIfNotContainsFunc("a", func() int { calls++; return 2 }); v != 1 {
|
||||
t.Fatalf("got %d", v)
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Fatalf("calls=%d", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncMap_Delete(t *testing.T) {
|
||||
m := NewSyncMap[string, int]()
|
||||
m.Set("a", 1)
|
||||
if !m.Delete("a") {
|
||||
t.Fatal("delete existing returned false")
|
||||
}
|
||||
if m.Delete("a") {
|
||||
t.Fatal("delete missing returned true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncMap_DeleteIf(t *testing.T) {
|
||||
m := NewSyncMap[string, int]()
|
||||
m.Set("a", 1)
|
||||
m.Set("b", 2)
|
||||
m.Set("c", 3)
|
||||
rm := m.DeleteIf(func(k string, v int) bool { return v%2 == 1 })
|
||||
if rm != 2 {
|
||||
t.Fatalf("removed=%d", rm)
|
||||
}
|
||||
if m.Count() != 1 {
|
||||
t.Fatalf("count=%d", m.Count())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncMap_UpdateIfExists(t *testing.T) {
|
||||
m := NewSyncMap[string, int]()
|
||||
if m.UpdateIfExists("a", func(v int) int { return v + 1 }) {
|
||||
t.Fatal("should be false on missing key")
|
||||
}
|
||||
m.Set("a", 5)
|
||||
if !m.UpdateIfExists("a", func(v int) int { return v + 1 }) {
|
||||
t.Fatal("should be true on existing")
|
||||
}
|
||||
v, _ := m.Get("a")
|
||||
if v != 6 {
|
||||
t.Fatalf("v=%d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncMap_UpdateOrInsert(t *testing.T) {
|
||||
m := NewSyncMap[string, int]()
|
||||
if m.UpdateOrInsert("a", func(v int) int { return v + 1 }, 100) {
|
||||
t.Fatal("should return false on insert")
|
||||
}
|
||||
if v, _ := m.Get("a"); v != 100 {
|
||||
t.Fatalf("v=%d", v)
|
||||
}
|
||||
if !m.UpdateOrInsert("a", func(v int) int { return v + 1 }, 100) {
|
||||
t.Fatal("should return true on update")
|
||||
}
|
||||
if v, _ := m.Get("a"); v != 101 {
|
||||
t.Fatalf("v=%d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncMap_ClearContains(t *testing.T) {
|
||||
m := NewSyncMap[string, int]()
|
||||
m.Set("a", 1)
|
||||
if !m.Contains("a") {
|
||||
t.Fatal("Contains should be true")
|
||||
}
|
||||
m.Clear()
|
||||
if m.Contains("a") {
|
||||
t.Fatal("after Clear should be false")
|
||||
}
|
||||
if m.Count() != 0 {
|
||||
t.Fatalf("count=%d", m.Count())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncMap_GetAllKeysValues(t *testing.T) {
|
||||
m := NewSyncMap[string, int]()
|
||||
m.Set("a", 1)
|
||||
m.Set("b", 2)
|
||||
m.Set("c", 3)
|
||||
keys := m.GetAllKeys()
|
||||
sort.Strings(keys)
|
||||
if len(keys) != 3 || keys[0] != "a" || keys[2] != "c" {
|
||||
t.Fatalf("keys=%v", keys)
|
||||
}
|
||||
vals := m.GetAllValues()
|
||||
sort.Ints(vals)
|
||||
if len(vals) != 3 || vals[0] != 1 || vals[2] != 3 {
|
||||
t.Fatalf("vals=%v", vals)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncMap_Concurrent(t *testing.T) {
|
||||
m := NewSyncMap[int, int]()
|
||||
var wg sync.WaitGroup
|
||||
const n = 200
|
||||
wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
go func(k int) {
|
||||
defer wg.Done()
|
||||
m.Set(k, k*2)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
if m.Count() != n {
|
||||
t.Fatalf("count=%d want %d", m.Count(), n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package dataext
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSyncRingSet_AddAndContains(t *testing.T) {
|
||||
s := NewSyncRingSet[int](3)
|
||||
if !s.Add(1) {
|
||||
t.Fatal("first Add(1) should be true")
|
||||
}
|
||||
if s.Add(1) {
|
||||
t.Fatal("duplicate Add(1) should be false")
|
||||
}
|
||||
if !s.Contains(1) {
|
||||
t.Fatal("expected Contains(1)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncRingSet_CapacityEvicts(t *testing.T) {
|
||||
s := NewSyncRingSet[int](3)
|
||||
s.Add(1)
|
||||
s.Add(2)
|
||||
s.Add(3)
|
||||
s.Add(4) // should evict the oldest (1)
|
||||
if s.Contains(1) {
|
||||
t.Fatal("1 should have been evicted")
|
||||
}
|
||||
for _, v := range []int{2, 3, 4} {
|
||||
if !s.Contains(v) {
|
||||
t.Fatalf("expected %d", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncRingSet_Remove(t *testing.T) {
|
||||
s := NewSyncRingSet[string](3)
|
||||
s.Add("a")
|
||||
s.Add("b")
|
||||
if !s.Remove("a") {
|
||||
t.Fatal("remove existing failed")
|
||||
}
|
||||
if s.Remove("a") {
|
||||
t.Fatal("remove missing returned true")
|
||||
}
|
||||
if s.Contains("a") {
|
||||
t.Fatal("a should be gone")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncRingSet_AddAllRemoveAll(t *testing.T) {
|
||||
s := NewSyncRingSet[int](10)
|
||||
s.AddAll([]int{1, 2, 3, 2})
|
||||
out := s.Get()
|
||||
sort.Ints(out)
|
||||
if len(out) != 3 {
|
||||
t.Fatalf("got %v", out)
|
||||
}
|
||||
|
||||
s.RemoveAll([]int{1, 99})
|
||||
if s.Contains(1) {
|
||||
t.Fatal("1 should be removed")
|
||||
}
|
||||
if !s.Contains(2) || !s.Contains(3) {
|
||||
t.Fatal("2/3 should remain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncRingSet_AddIfNotContainsRemoveIfContains(t *testing.T) {
|
||||
s := NewSyncRingSet[string](5)
|
||||
if !s.AddIfNotContains("x") {
|
||||
t.Fatal("first should succeed")
|
||||
}
|
||||
if s.AddIfNotContains("x") {
|
||||
t.Fatal("second should fail")
|
||||
}
|
||||
if !s.RemoveIfContains("x") {
|
||||
t.Fatal("remove existing failed")
|
||||
}
|
||||
if s.RemoveIfContains("x") {
|
||||
t.Fatal("remove missing returned true")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package dataext
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSyncSet_Add(t *testing.T) {
|
||||
s := NewSyncSet[string]()
|
||||
if !s.Add("a") {
|
||||
t.Fatal("first add should be true")
|
||||
}
|
||||
if s.Add("a") {
|
||||
t.Fatal("duplicate add should be false")
|
||||
}
|
||||
if !s.Contains("a") {
|
||||
t.Fatal("Contains a should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncSet_AddAll(t *testing.T) {
|
||||
s := NewSyncSet[int]()
|
||||
s.AddAll([]int{1, 2, 3, 2})
|
||||
if !s.Contains(1) || !s.Contains(2) || !s.Contains(3) {
|
||||
t.Fatal("missing items")
|
||||
}
|
||||
if len(s.Get()) != 3 {
|
||||
t.Fatalf("got len %d", len(s.Get()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncSet_Remove(t *testing.T) {
|
||||
s := NewSyncSet[string]()
|
||||
s.Add("a")
|
||||
if !s.Remove("a") {
|
||||
t.Fatal("remove existing failed")
|
||||
}
|
||||
if s.Remove("a") {
|
||||
t.Fatal("remove missing returned true")
|
||||
}
|
||||
if s.Contains("a") {
|
||||
t.Fatal("still contains after remove")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncSet_RemoveAll(t *testing.T) {
|
||||
s := NewSyncSet[int]()
|
||||
s.AddAll([]int{1, 2, 3})
|
||||
s.RemoveAll([]int{1, 2, 99})
|
||||
if s.Contains(1) || s.Contains(2) {
|
||||
t.Fatal("should be removed")
|
||||
}
|
||||
if !s.Contains(3) {
|
||||
t.Fatal("3 should remain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncSet_Get(t *testing.T) {
|
||||
s := NewSyncSet[int]()
|
||||
s.AddAll([]int{3, 1, 2})
|
||||
out := s.Get()
|
||||
sort.Ints(out)
|
||||
if len(out) != 3 || out[0] != 1 || out[2] != 3 {
|
||||
t.Fatalf("out=%v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncSet_AddIfNotContainsRemoveIfContains(t *testing.T) {
|
||||
s := NewSyncSet[string]()
|
||||
if !s.AddIfNotContains("x") {
|
||||
t.Fatal("first AddIfNotContains failed")
|
||||
}
|
||||
if s.AddIfNotContains("x") {
|
||||
t.Fatal("second AddIfNotContains succeeded")
|
||||
}
|
||||
if !s.RemoveIfContains("x") {
|
||||
t.Fatal("RemoveIfContains failed")
|
||||
}
|
||||
if s.RemoveIfContains("x") {
|
||||
t.Fatal("RemoveIfContains on missing succeeded")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package dataext
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSingle(t *testing.T) {
|
||||
s := NewSingle[int](7)
|
||||
if s.V1 != 7 {
|
||||
t.Fatalf("V1=%d", s.V1)
|
||||
}
|
||||
if s.TupleLength() != 1 {
|
||||
t.Fatalf("len=%d", s.TupleLength())
|
||||
}
|
||||
if !reflect.DeepEqual(s.TupleValues(), []any{7}) {
|
||||
t.Fatalf("values=%v", s.TupleValues())
|
||||
}
|
||||
if NewTuple1[int](7).V1 != 7 {
|
||||
t.Fatal("NewTuple1 mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTuple(t *testing.T) {
|
||||
tp := NewTuple[int, string](1, "two")
|
||||
if tp.V1 != 1 || tp.V2 != "two" {
|
||||
t.Fatal("values wrong")
|
||||
}
|
||||
if tp.TupleLength() != 2 {
|
||||
t.Fatal("len wrong")
|
||||
}
|
||||
if !reflect.DeepEqual(tp.TupleValues(), []any{1, "two"}) {
|
||||
t.Fatalf("values=%v", tp.TupleValues())
|
||||
}
|
||||
if NewTuple2[int, string](1, "two") != tp {
|
||||
t.Fatal("NewTuple2 mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTriple(t *testing.T) {
|
||||
tr := NewTriple[int, string, bool](1, "x", true)
|
||||
if tr.TupleLength() != 3 {
|
||||
t.Fatal("len wrong")
|
||||
}
|
||||
if !reflect.DeepEqual(tr.TupleValues(), []any{1, "x", true}) {
|
||||
t.Fatalf("values=%v", tr.TupleValues())
|
||||
}
|
||||
if NewTuple3[int, string, bool](1, "x", true) != tr {
|
||||
t.Fatal("NewTuple3 mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuadruple(t *testing.T) {
|
||||
q := NewQuadruple[int, int, int, int](1, 2, 3, 4)
|
||||
if q.TupleLength() != 4 {
|
||||
t.Fatal("len wrong")
|
||||
}
|
||||
if !reflect.DeepEqual(q.TupleValues(), []any{1, 2, 3, 4}) {
|
||||
t.Fatalf("values=%v", q.TupleValues())
|
||||
}
|
||||
if NewTuple4[int, int, int, int](1, 2, 3, 4) != q {
|
||||
t.Fatal("NewTuple4 mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuintuple(t *testing.T) {
|
||||
q := NewQuintuple[int, int, int, int, int](1, 2, 3, 4, 5)
|
||||
if q.TupleLength() != 5 {
|
||||
t.Fatal("len wrong")
|
||||
}
|
||||
if !reflect.DeepEqual(q.TupleValues(), []any{1, 2, 3, 4, 5}) {
|
||||
t.Fatalf("values=%v", q.TupleValues())
|
||||
}
|
||||
if NewTuple5[int, int, int, int, int](1, 2, 3, 4, 5) != q {
|
||||
t.Fatal("NewTuple5 mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSextuple(t *testing.T) {
|
||||
s := NewSextuple[int, int, int, int, int, int](1, 2, 3, 4, 5, 6)
|
||||
if s.TupleLength() != 6 {
|
||||
t.Fatal("len wrong")
|
||||
}
|
||||
if !reflect.DeepEqual(s.TupleValues(), []any{1, 2, 3, 4, 5, 6}) {
|
||||
t.Fatalf("values=%v", s.TupleValues())
|
||||
}
|
||||
if NewTuple6[int, int, int, int, int, int](1, 2, 3, 4, 5, 6) != s {
|
||||
t.Fatal("NewTuple6 mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeptuple(t *testing.T) {
|
||||
s := NewSeptuple[int, int, int, int, int, int, int](1, 2, 3, 4, 5, 6, 7)
|
||||
if s.TupleLength() != 7 {
|
||||
t.Fatal("len wrong")
|
||||
}
|
||||
if !reflect.DeepEqual(s.TupleValues(), []any{1, 2, 3, 4, 5, 6, 7}) {
|
||||
t.Fatalf("values=%v", s.TupleValues())
|
||||
}
|
||||
if NewTuple7[int, int, int, int, int, int, int](1, 2, 3, 4, 5, 6, 7) != s {
|
||||
t.Fatal("NewTuple7 mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOctuple(t *testing.T) {
|
||||
o := NewOctuple[int, int, int, int, int, int, int, int](1, 2, 3, 4, 5, 6, 7, 8)
|
||||
if o.TupleLength() != 8 {
|
||||
t.Fatal("len wrong")
|
||||
}
|
||||
if !reflect.DeepEqual(o.TupleValues(), []any{1, 2, 3, 4, 5, 6, 7, 8}) {
|
||||
t.Fatalf("values=%v", o.TupleValues())
|
||||
}
|
||||
if NewTuple8[int, int, int, int, int, int, int, int](1, 2, 3, 4, 5, 6, 7, 8) != o {
|
||||
t.Fatal("NewTuple8 mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonuple(t *testing.T) {
|
||||
n := NewNonuple[int, int, int, int, int, int, int, int, int](1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
if n.TupleLength() != 9 {
|
||||
t.Fatal("len wrong")
|
||||
}
|
||||
if !reflect.DeepEqual(n.TupleValues(), []any{1, 2, 3, 4, 5, 6, 7, 8, 9}) {
|
||||
t.Fatalf("values=%v", n.TupleValues())
|
||||
}
|
||||
if NewTuple9[int, int, int, int, int, int, int, int, int](1, 2, 3, 4, 5, 6, 7, 8, 9) != n {
|
||||
t.Fatal("NewTuple9 mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueGroupInterface(t *testing.T) {
|
||||
var vg ValueGroup = NewTuple[int, string](1, "a")
|
||||
if vg.TupleLength() != 2 {
|
||||
t.Fatal("interface length wrong")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package enums
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockEnum struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (m mockEnum) Valid() bool { return m.name != "" }
|
||||
func (m mockEnum) ValuesAny() []any { return []any{mockEnum{name: "a"}, mockEnum{name: "b"}} }
|
||||
func (m mockEnum) ValuesMeta() []EnumMetaValue { return nil }
|
||||
func (m mockEnum) VarName() string { return m.name }
|
||||
func (m mockEnum) TypeName() string { return "mockEnum" }
|
||||
func (m mockEnum) PackageName() string { return "enums_test" }
|
||||
func (m mockEnum) String() string { return "str:" + m.name }
|
||||
func (m mockEnum) Description() string { return "desc:" + m.name }
|
||||
func (m mockEnum) DescriptionMeta() EnumDescriptionMetaValue {
|
||||
return EnumDescriptionMetaValue{VarName: m.name, Value: m, Description: "desc:" + m.name}
|
||||
}
|
||||
|
||||
func (m mockEnum) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(m.name)
|
||||
}
|
||||
|
||||
func TestMockEnumImplementsInterfaces(t *testing.T) {
|
||||
var _ Enum = mockEnum{}
|
||||
var _ StringEnum = mockEnum{}
|
||||
var _ DescriptionEnum = mockEnum{}
|
||||
}
|
||||
|
||||
func TestEnumValid(t *testing.T) {
|
||||
if !(mockEnum{name: "x"}).Valid() {
|
||||
t.Errorf("expected Valid() == true")
|
||||
}
|
||||
if (mockEnum{}).Valid() {
|
||||
t.Errorf("expected Valid() == false for zero value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumMetaValueJSON(t *testing.T) {
|
||||
desc := "the-description"
|
||||
mv := EnumMetaValue{
|
||||
VarName: "Foo",
|
||||
Value: mockEnum{name: "foo"},
|
||||
Description: &desc,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(mv)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(data, &got); err != nil {
|
||||
t.Fatalf("json.Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if got["varName"] != "Foo" {
|
||||
t.Errorf("varName == %v, want Foo", got["varName"])
|
||||
}
|
||||
if got["value"] != "foo" {
|
||||
t.Errorf("value == %v, want foo", got["value"])
|
||||
}
|
||||
if got["description"] != "the-description" {
|
||||
t.Errorf("description == %v, want the-description", got["description"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumMetaValueJSONNilDescription(t *testing.T) {
|
||||
mv := EnumMetaValue{
|
||||
VarName: "Foo",
|
||||
Value: mockEnum{name: "foo"},
|
||||
Description: nil,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(mv)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(data, &got); err != nil {
|
||||
t.Fatalf("json.Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if got["description"] != nil {
|
||||
t.Errorf("description == %v, want nil", got["description"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumDescriptionMetaValueJSON(t *testing.T) {
|
||||
mv := EnumDescriptionMetaValue{
|
||||
VarName: "Bar",
|
||||
Value: mockEnum{name: "bar"},
|
||||
Description: "bar-desc",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(mv)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(data, &got); err != nil {
|
||||
t.Fatalf("json.Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
expected := map[string]any{
|
||||
"varName": "Bar",
|
||||
"value": "bar",
|
||||
"description": "bar-desc",
|
||||
}
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("json output == %v, want %v", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumDataMetaValueMarshalJSON(t *testing.T) {
|
||||
desc := "data-desc"
|
||||
mv := EnumDataMetaValue{
|
||||
VarName: "Baz",
|
||||
Value: mockEnum{name: "baz"},
|
||||
Description: &desc,
|
||||
Data: map[string]any{
|
||||
"extra1": "hello",
|
||||
"extra2": float64(42),
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(mv)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(data, &got); err != nil {
|
||||
t.Fatalf("json.Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if got["varName"] != "Baz" {
|
||||
t.Errorf("varName == %v, want Baz", got["varName"])
|
||||
}
|
||||
if got["value"] != "baz" {
|
||||
t.Errorf("value == %v, want baz", got["value"])
|
||||
}
|
||||
if got["description"] != "data-desc" {
|
||||
t.Errorf("description == %v, want data-desc", got["description"])
|
||||
}
|
||||
if got["extra1"] != "hello" {
|
||||
t.Errorf("extra1 == %v, want hello", got["extra1"])
|
||||
}
|
||||
if got["extra2"] != float64(42) {
|
||||
t.Errorf("extra2 == %v, want 42", got["extra2"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumDataMetaValueMarshalJSONNilData(t *testing.T) {
|
||||
mv := EnumDataMetaValue{
|
||||
VarName: "Baz",
|
||||
Value: mockEnum{name: "baz"},
|
||||
Description: nil,
|
||||
Data: nil,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(mv)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(data, &got); err != nil {
|
||||
t.Fatalf("json.Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if got["varName"] != "Baz" {
|
||||
t.Errorf("varName == %v, want Baz", got["varName"])
|
||||
}
|
||||
if got["value"] != "baz" {
|
||||
t.Errorf("value == %v, want baz", got["value"])
|
||||
}
|
||||
if _, ok := got["description"]; !ok {
|
||||
t.Errorf("description key missing in JSON output")
|
||||
}
|
||||
if got["description"] != nil {
|
||||
t.Errorf("description == %v, want nil", got["description"])
|
||||
}
|
||||
if len(got) != 3 {
|
||||
t.Errorf("expected 3 keys with nil Data, got %d: %v", len(got), got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumDataMetaValueMarshalJSONDataDoesNotOverrideStandardFields(t *testing.T) {
|
||||
desc := "real-desc"
|
||||
mv := EnumDataMetaValue{
|
||||
VarName: "Real",
|
||||
Value: mockEnum{name: "real"},
|
||||
Description: &desc,
|
||||
Data: map[string]any{
|
||||
"varName": "ShouldBeOverwritten",
|
||||
"value": "ShouldBeOverwritten",
|
||||
"description": "ShouldBeOverwritten",
|
||||
"keep": "kept",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(mv)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(data, &got); err != nil {
|
||||
t.Fatalf("json.Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if got["varName"] != "Real" {
|
||||
t.Errorf("varName == %v, want Real (standard field must override Data)", got["varName"])
|
||||
}
|
||||
if got["value"] != "real" {
|
||||
t.Errorf("value == %v, want real (standard field must override Data)", got["value"])
|
||||
}
|
||||
if got["description"] != "real-desc" {
|
||||
t.Errorf("description == %v, want real-desc (standard field must override Data)", got["description"])
|
||||
}
|
||||
if got["keep"] != "kept" {
|
||||
t.Errorf("keep == %v, want kept", got["keep"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumDataMetaValueMarshalJSONEmptyData(t *testing.T) {
|
||||
mv := EnumDataMetaValue{
|
||||
VarName: "E",
|
||||
Value: mockEnum{name: "e"},
|
||||
Description: nil,
|
||||
Data: map[string]any{},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(mv)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(data, &got); err != nil {
|
||||
t.Fatalf("json.Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if got["varName"] != "E" {
|
||||
t.Errorf("varName == %v, want E", got["varName"])
|
||||
}
|
||||
if got["value"] != "e" {
|
||||
t.Errorf("value == %v, want e", got["value"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
package excelext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
type testRow struct {
|
||||
Name string
|
||||
Age int
|
||||
Score float64
|
||||
}
|
||||
|
||||
func openBytes(t *testing.T, data []byte) *excelize.File {
|
||||
t.Helper()
|
||||
f, err := excelize.OpenReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open xlsx bytes: %v", err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func cellValue(t *testing.T, f *excelize.File, sheet, axis string) string {
|
||||
t.Helper()
|
||||
v, err := f.GetCellValue(sheet, axis)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCellValue(%s, %s) failed: %v", sheet, axis, err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func TestNewExcelMapper(t *testing.T) {
|
||||
em, err := NewExcelMapper[testRow]()
|
||||
tst.AssertNoErr(t, err)
|
||||
if em == nil {
|
||||
t.Fatal("expected non-nil mapper")
|
||||
}
|
||||
tst.AssertEqual(t, em.SkipColumnHeader, false)
|
||||
tst.AssertEqual(t, len(em.colDefinitions), 0)
|
||||
tst.AssertEqual(t, len(em.wsHeader), 0)
|
||||
tst.AssertEqual(t, len(em.colFilter), 0)
|
||||
if em.StyleDate != nil || em.StyleHeader != nil {
|
||||
t.Errorf("expected styles to be nil before init")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitNewFileAndStyles(t *testing.T) {
|
||||
em, _ := NewExcelMapper[testRow]()
|
||||
f, err := em.InitNewFile("Sheet-Foo")
|
||||
tst.AssertNoErr(t, err)
|
||||
if f == nil {
|
||||
t.Fatal("expected non-nil file")
|
||||
}
|
||||
|
||||
sheets := f.GetSheetList()
|
||||
tst.AssertEqual(t, len(sheets), 1)
|
||||
tst.AssertEqual(t, sheets[0], "Sheet-Foo")
|
||||
|
||||
if em.StyleDate == nil || em.StyleDatetime == nil || em.StyleEUR == nil ||
|
||||
em.StylePercentage == nil || em.StyleHeader == nil || em.StyleWSHeader == nil {
|
||||
t.Errorf("expected all styles to be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddColumn(t *testing.T) {
|
||||
em, _ := NewExcelMapper[testRow]()
|
||||
em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name })
|
||||
em.AddColumn("Age", nil, langext.Ptr(12.0), func(r testRow) any { return r.Age })
|
||||
|
||||
tst.AssertEqual(t, len(em.colDefinitions), 2)
|
||||
tst.AssertEqual(t, em.colDefinitions[0].header, "Name")
|
||||
tst.AssertEqual(t, em.colDefinitions[1].header, "Age")
|
||||
if em.colDefinitions[1].width == nil || *em.colDefinitions[1].width != 12.0 {
|
||||
t.Errorf("expected width 12.0")
|
||||
}
|
||||
|
||||
val, err := em.colDefinitions[0].fn(testRow{Name: "Alice"})
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, val.(string), "Alice")
|
||||
}
|
||||
|
||||
func TestAddColumnErr(t *testing.T) {
|
||||
em, _ := NewExcelMapper[testRow]()
|
||||
sentinel := errors.New("boom")
|
||||
em.AddColumnErr("X", nil, nil, func(r testRow) (any, error) {
|
||||
if r.Age < 0 {
|
||||
return nil, sentinel
|
||||
}
|
||||
return r.Age, nil
|
||||
})
|
||||
|
||||
tst.AssertEqual(t, len(em.colDefinitions), 1)
|
||||
|
||||
v, err := em.colDefinitions[0].fn(testRow{Age: 5})
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, v.(int), 5)
|
||||
|
||||
_, err = em.colDefinitions[0].fn(testRow{Age: -1})
|
||||
if !errors.Is(err, sentinel) {
|
||||
t.Errorf("expected sentinel error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddWorksheetHeader(t *testing.T) {
|
||||
em, _ := NewExcelMapper[testRow]()
|
||||
em.AddWorksheetHeader("Title 1", nil)
|
||||
em.AddWorksheetHeader("Title 2", langext.Ptr(7))
|
||||
|
||||
tst.AssertEqual(t, len(em.wsHeader), 2)
|
||||
tst.AssertEqual(t, em.wsHeader[0].V1, "Title 1")
|
||||
tst.AssertEqual(t, em.wsHeader[1].V1, "Title 2")
|
||||
if em.wsHeader[1].V2 == nil || *em.wsHeader[1].V2 != 7 {
|
||||
t.Errorf("expected style ptr 7")
|
||||
}
|
||||
if em.wsHeader[0].V2 != nil {
|
||||
t.Errorf("expected nil style for first header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddFilter(t *testing.T) {
|
||||
em, _ := NewExcelMapper[testRow]()
|
||||
em.AddFilter(func(v testRow) bool { return v.Age >= 18 })
|
||||
em.AddFilter(func(v testRow) bool { return v.Score > 0 })
|
||||
tst.AssertEqual(t, len(em.colFilter), 2)
|
||||
}
|
||||
|
||||
func TestBuildBasic(t *testing.T) {
|
||||
em, _ := NewExcelMapper[testRow]()
|
||||
em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name })
|
||||
em.AddColumn("Age", nil, nil, func(r testRow) any { return r.Age })
|
||||
|
||||
rows := []testRow{
|
||||
{Name: "Alice", Age: 30},
|
||||
{Name: "Bob", Age: 25},
|
||||
}
|
||||
|
||||
data, err := em.Build("Sheet1", rows)
|
||||
tst.AssertNoErr(t, err)
|
||||
if len(data) == 0 {
|
||||
t.Fatal("expected non-empty xlsx output")
|
||||
}
|
||||
|
||||
f := openBytes(t, data)
|
||||
defer f.Close()
|
||||
|
||||
tst.AssertEqual(t, cellValue(t, f, "Sheet1", "A1"), "Name")
|
||||
tst.AssertEqual(t, cellValue(t, f, "Sheet1", "B1"), "Age")
|
||||
tst.AssertEqual(t, cellValue(t, f, "Sheet1", "A2"), "Alice")
|
||||
tst.AssertEqual(t, cellValue(t, f, "Sheet1", "B2"), "30")
|
||||
tst.AssertEqual(t, cellValue(t, f, "Sheet1", "A3"), "Bob")
|
||||
tst.AssertEqual(t, cellValue(t, f, "Sheet1", "B3"), "25")
|
||||
}
|
||||
|
||||
func TestBuildSkipColumnHeader(t *testing.T) {
|
||||
em, _ := NewExcelMapper[testRow]()
|
||||
em.SkipColumnHeader = true
|
||||
em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name })
|
||||
|
||||
rows := []testRow{{Name: "Alice"}, {Name: "Bob"}}
|
||||
|
||||
data, err := em.Build("Data", rows)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
f := openBytes(t, data)
|
||||
defer f.Close()
|
||||
|
||||
tst.AssertEqual(t, cellValue(t, f, "Data", "A1"), "Alice")
|
||||
tst.AssertEqual(t, cellValue(t, f, "Data", "A2"), "Bob")
|
||||
}
|
||||
|
||||
func TestBuildWithFilter(t *testing.T) {
|
||||
em, _ := NewExcelMapper[testRow]()
|
||||
em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name })
|
||||
em.AddFilter(func(v testRow) bool { return v.Age >= 18 })
|
||||
|
||||
rows := []testRow{
|
||||
{Name: "Alice", Age: 30},
|
||||
{Name: "Charlie", Age: 12},
|
||||
{Name: "Bob", Age: 25},
|
||||
}
|
||||
|
||||
data, err := em.Build("S", rows)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
f := openBytes(t, data)
|
||||
defer f.Close()
|
||||
|
||||
tst.AssertEqual(t, cellValue(t, f, "S", "A1"), "Name")
|
||||
tst.AssertEqual(t, cellValue(t, f, "S", "A2"), "Alice")
|
||||
tst.AssertEqual(t, cellValue(t, f, "S", "A3"), "Bob")
|
||||
tst.AssertEqual(t, cellValue(t, f, "S", "A4"), "")
|
||||
}
|
||||
|
||||
func TestBuildWithWorksheetHeader(t *testing.T) {
|
||||
em, _ := NewExcelMapper[testRow]()
|
||||
em.AddWorksheetHeader("My Big Title", nil)
|
||||
em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name })
|
||||
em.AddColumn("Age", nil, nil, func(r testRow) any { return r.Age })
|
||||
|
||||
rows := []testRow{{Name: "Alice", Age: 30}}
|
||||
|
||||
data, err := em.Build("S", rows)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
f := openBytes(t, data)
|
||||
defer f.Close()
|
||||
|
||||
tst.AssertEqual(t, cellValue(t, f, "S", "A1"), "My Big Title")
|
||||
tst.AssertEqual(t, cellValue(t, f, "S", "A3"), "Name")
|
||||
tst.AssertEqual(t, cellValue(t, f, "S", "B3"), "Age")
|
||||
tst.AssertEqual(t, cellValue(t, f, "S", "A4"), "Alice")
|
||||
tst.AssertEqual(t, cellValue(t, f, "S", "B4"), "30")
|
||||
}
|
||||
|
||||
func TestBuildHandlesNilPointer(t *testing.T) {
|
||||
type ptrRow struct {
|
||||
Name *string
|
||||
}
|
||||
|
||||
em, _ := NewExcelMapper[ptrRow]()
|
||||
em.AddColumn("Name", nil, nil, func(r ptrRow) any { return r.Name })
|
||||
|
||||
name := "Alice"
|
||||
rows := []ptrRow{
|
||||
{Name: &name},
|
||||
{Name: nil},
|
||||
}
|
||||
|
||||
data, err := em.Build("S", rows)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
f := openBytes(t, data)
|
||||
defer f.Close()
|
||||
|
||||
tst.AssertEqual(t, cellValue(t, f, "S", "A2"), "Alice")
|
||||
tst.AssertEqual(t, cellValue(t, f, "S", "A3"), "")
|
||||
}
|
||||
|
||||
func TestBuildPropagatesColumnError(t *testing.T) {
|
||||
em, _ := NewExcelMapper[testRow]()
|
||||
sentinel := errors.New("col fail")
|
||||
em.AddColumnErr("Bad", nil, nil, func(r testRow) (any, error) {
|
||||
return nil, sentinel
|
||||
})
|
||||
|
||||
_, err := em.Build("S", []testRow{{Name: "X"}})
|
||||
if err == nil {
|
||||
t.Fatal("expected error from column fn to propagate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildEmptyData(t *testing.T) {
|
||||
em, _ := NewExcelMapper[testRow]()
|
||||
em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name })
|
||||
|
||||
data, err := em.Build("S", []testRow{})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
f := openBytes(t, data)
|
||||
defer f.Close()
|
||||
|
||||
tst.AssertEqual(t, cellValue(t, f, "S", "A1"), "Name")
|
||||
tst.AssertEqual(t, cellValue(t, f, "S", "A2"), "")
|
||||
}
|
||||
|
||||
func TestBuildSingleSheetWithExistingFile(t *testing.T) {
|
||||
em, _ := NewExcelMapper[testRow]()
|
||||
em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name })
|
||||
|
||||
f, err := em.InitNewFile("S1")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
_, err = f.NewSheet("S2")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
err = em.BuildSingleSheet(f, "S2", []testRow{{Name: "Bob"}})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, cellValue(t, f, "S2", "A1"), "Name")
|
||||
tst.AssertEqual(t, cellValue(t, f, "S2", "A2"), "Bob")
|
||||
}
|
||||
|
||||
func TestBuildWithColumnWidthAndStyle(t *testing.T) {
|
||||
em, _ := NewExcelMapper[testRow]()
|
||||
|
||||
f, err := em.InitNewFile("S")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
em.AddColumn("Name", em.StyleHeader, langext.Ptr(20.5), func(r testRow) any { return r.Name })
|
||||
|
||||
err = em.BuildSingleSheet(f, "S", []testRow{{Name: "Alice"}})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
w, err := f.GetColWidth("S", "A")
|
||||
tst.AssertNoErr(t, err)
|
||||
if w < 20.0 || w > 21.0 {
|
||||
t.Errorf("expected column width near 20.5, got %v", w)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package excelext
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/rfctime"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
)
|
||||
|
||||
func TestCellAddress(t *testing.T) {
|
||||
tst.AssertEqual(t, c(1, 0), "A1")
|
||||
tst.AssertEqual(t, c(1, 1), "B1")
|
||||
tst.AssertEqual(t, c(2, 0), "A2")
|
||||
tst.AssertEqual(t, c(10, 25), "Z10")
|
||||
tst.AssertEqual(t, c(1, 26), "AA1")
|
||||
tst.AssertEqual(t, c(99, 27), "AB99")
|
||||
tst.AssertEqual(t, c(100, 51), "AZ100")
|
||||
tst.AssertEqual(t, c(1, 52), "BA1")
|
||||
}
|
||||
|
||||
func TestExcelizeOptTimeNil(t *testing.T) {
|
||||
got := excelizeOptTime(nil)
|
||||
if got != "" {
|
||||
t.Errorf("expected empty string for nil time, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExcelizeOptTimeValue(t *testing.T) {
|
||||
now := time.Date(2024, 5, 17, 13, 45, 30, 0, time.UTC)
|
||||
rt := rfctime.RFC3339NanoTime(now)
|
||||
got := excelizeOptTime(&rt)
|
||||
|
||||
gt, ok := got.(time.Time)
|
||||
if !ok {
|
||||
t.Fatalf("expected time.Time, got %T", got)
|
||||
}
|
||||
if !gt.Equal(now) {
|
||||
t.Errorf("expected %v, got %v", now, gt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExcelizeOptDateNil(t *testing.T) {
|
||||
got := excelizeOptDate(nil)
|
||||
if got != "" {
|
||||
t.Errorf("expected empty string for nil date, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExcelizeOptDateValue(t *testing.T) {
|
||||
d := rfctime.NewDate(time.Date(2025, 11, 3, 0, 0, 0, 0, time.UTC))
|
||||
got := excelizeOptDate(&d)
|
||||
|
||||
gt, ok := got.(time.Time)
|
||||
if !ok {
|
||||
t.Fatalf("expected time.Time, got %T", got)
|
||||
}
|
||||
if gt.Year() != 2025 || gt.Month() != 11 || gt.Day() != 3 {
|
||||
t.Errorf("unexpected date returned: %v", gt)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,836 @@
|
||||
package exerr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Builder / Constructor tests
|
||||
// ============================================================================
|
||||
|
||||
func TestNewBuildsExErr(t *testing.T) {
|
||||
err := New(TypeInternal, "boom").Build()
|
||||
tst.AssertTrue(t, err != nil)
|
||||
|
||||
ee, ok := err.(*ExErr)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, ee.Message, "boom")
|
||||
tst.AssertEqual(t, ee.Type, TypeInternal)
|
||||
tst.AssertEqual(t, ee.Category, CatSystem)
|
||||
tst.AssertEqual(t, ee.Severity, SevErr)
|
||||
tst.AssertTrue(t, ee.UniqueID != "")
|
||||
}
|
||||
|
||||
func TestWrapNilProducesInternalError(t *testing.T) {
|
||||
err := Wrap(nil, "msg").Build()
|
||||
tst.AssertTrue(t, err != nil)
|
||||
|
||||
ee, _ := err.(*ExErr)
|
||||
tst.AssertEqual(t, ee.Message, "msg")
|
||||
tst.AssertEqual(t, ee.Type, TypeInternal)
|
||||
}
|
||||
|
||||
func TestWrapForeignError(t *testing.T) {
|
||||
plain := errors.New("plain go error")
|
||||
err := Wrap(plain, "wrapped").Build()
|
||||
ee, _ := err.(*ExErr)
|
||||
|
||||
// outer is a wrap (TypeWrap), inner is the foreign error
|
||||
tst.AssertEqual(t, ee.Type, TypeWrap)
|
||||
tst.AssertEqual(t, ee.Category, CatWrap)
|
||||
tst.AssertEqual(t, ee.Message, "wrapped")
|
||||
tst.AssertTrue(t, ee.OriginalError != nil)
|
||||
tst.AssertEqual(t, ee.OriginalError.Category, CatForeign)
|
||||
tst.AssertEqual(t, ee.OriginalError.Message, "plain go error")
|
||||
}
|
||||
|
||||
func TestWrapExErrChainsDepth(t *testing.T) {
|
||||
e1 := New(TypeInternal, "level-1").Build()
|
||||
e2 := Wrap(e1, "level-2").Build()
|
||||
e3 := Wrap(e2, "level-3").Build()
|
||||
ee3 := e3.(*ExErr)
|
||||
|
||||
tst.AssertEqual(t, ee3.Depth(), 3)
|
||||
}
|
||||
|
||||
func TestGetReturnsExErr(t *testing.T) {
|
||||
plain := errors.New("foreign")
|
||||
b := Get(plain)
|
||||
tst.AssertTrue(t, b != nil)
|
||||
tst.AssertEqual(t, b.errorData.Category, CatForeign)
|
||||
tst.AssertEqual(t, b.errorData.Message, "foreign")
|
||||
}
|
||||
|
||||
func TestBuilderWithModifiers(t *testing.T) {
|
||||
err := New(TypeInternal, "msg").
|
||||
WithType(TypeAssert).
|
||||
WithStatuscode(418).
|
||||
WithMessage("teapot").
|
||||
WithSeverity(SevWarn).
|
||||
WithCategory(CatUser).
|
||||
Build()
|
||||
ee := err.(*ExErr)
|
||||
|
||||
tst.AssertEqual(t, ee.Type, TypeAssert)
|
||||
tst.AssertDeRefEqual(t, ee.StatusCode, 418)
|
||||
tst.AssertEqual(t, ee.Message, "teapot")
|
||||
tst.AssertEqual(t, ee.Severity, SevWarn)
|
||||
tst.AssertEqual(t, ee.Category, CatUser)
|
||||
}
|
||||
|
||||
func TestBuilderSeverityShortcuts(t *testing.T) {
|
||||
tst.AssertEqual(t, New(TypeInternal, "x").Err().Build().(*ExErr).Severity, SevErr)
|
||||
tst.AssertEqual(t, New(TypeInternal, "x").Warn().Build().(*ExErr).Severity, SevWarn)
|
||||
tst.AssertEqual(t, New(TypeInternal, "x").Info().Build().(*ExErr).Severity, SevInfo)
|
||||
}
|
||||
|
||||
func TestBuilderCategoryShortcuts(t *testing.T) {
|
||||
tst.AssertEqual(t, New(TypeInternal, "x").User().Build().(*ExErr).Category, CatUser)
|
||||
tst.AssertEqual(t, New(TypeInternal, "x").System().Build().(*ExErr).Category, CatSystem)
|
||||
}
|
||||
|
||||
func TestBuilderNoLog(t *testing.T) {
|
||||
b := New(TypeInternal, "x").NoLog()
|
||||
tst.AssertTrue(t, b.noLog)
|
||||
}
|
||||
|
||||
func TestBuilderExtra(t *testing.T) {
|
||||
err := New(TypeInternal, "x").Extra("k", 42).Build()
|
||||
ee := err.(*ExErr)
|
||||
v, ok := ee.GetExtra("k")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v.(int), 42)
|
||||
}
|
||||
|
||||
func TestBuilderMetaTypes(t *testing.T) {
|
||||
now := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
err := New(TypeInternal, "msg").
|
||||
Str("s", "value").
|
||||
Int("i", 7).
|
||||
Int8("i8", 8).
|
||||
Int16("i16", 16).
|
||||
Int32("i32", 32).
|
||||
Int64("i64", 64).
|
||||
Float32("f32", 1.5).
|
||||
Float64("f64", 2.5).
|
||||
Bool("b", true).
|
||||
Bytes("by", []byte{0xAA, 0xBB}).
|
||||
Time("t", now).
|
||||
Dur("d", 5*time.Second).
|
||||
Strs("strs", []string{"a", "b"}).
|
||||
Ints("ints", []int{1, 2, 3}).
|
||||
Ints32("ints32", []int32{4, 5}).
|
||||
Type("typ", "hello").
|
||||
Build()
|
||||
|
||||
ee := err.(*ExErr)
|
||||
|
||||
gotS, _ := ee.GetMetaString("s")
|
||||
tst.AssertEqual(t, gotS, "value")
|
||||
|
||||
gotI, _ := ee.GetMetaInt("i")
|
||||
tst.AssertEqual(t, gotI, 7)
|
||||
|
||||
gotB, _ := ee.GetMetaBool("b")
|
||||
tst.AssertEqual(t, gotB, true)
|
||||
|
||||
gotF32, _ := ee.GetMetaFloat32("f32")
|
||||
tst.AssertEqual(t, gotF32, float32(1.5))
|
||||
|
||||
gotF64, _ := ee.GetMetaFloat64("f64")
|
||||
tst.AssertEqual(t, gotF64, 2.5)
|
||||
|
||||
gotT, _ := ee.GetMetaTime("t")
|
||||
tst.AssertEqual(t, gotT.Equal(now), true)
|
||||
}
|
||||
|
||||
func TestBuilderInterfaceAndAny(t *testing.T) {
|
||||
type payload struct {
|
||||
A int `json:"a"`
|
||||
B string `json:"b"`
|
||||
}
|
||||
err := New(TypeInternal, "msg").Interface("p", payload{A: 1, B: "x"}).Any("p2", payload{A: 2, B: "y"}).Build()
|
||||
ee := err.(*ExErr)
|
||||
v1, ok := ee.GetMeta("p")
|
||||
tst.AssertTrue(t, ok)
|
||||
mv := v1.(AnyWrap)
|
||||
tst.AssertTrue(t, strings.Contains(mv.Json, "\"a\":1"))
|
||||
_, ok = ee.GetMeta("p2")
|
||||
tst.AssertTrue(t, ok)
|
||||
}
|
||||
|
||||
func TestBuilderStack(t *testing.T) {
|
||||
err := New(TypeInternal, "msg").Stack().Build()
|
||||
ee := err.(*ExErr)
|
||||
v, ok := ee.GetMetaString("@Stack")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertTrue(t, len(v) > 0)
|
||||
}
|
||||
|
||||
func TestBuilderErrs(t *testing.T) {
|
||||
in := []error{errors.New("first"), errors.New("second")}
|
||||
err := New(TypeInternal, "msg").Errs("errs", in).Build()
|
||||
ee := err.(*ExErr)
|
||||
|
||||
v0, ok := ee.GetMetaString("errs[0]")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertTrue(t, strings.Contains(v0, "first"))
|
||||
v1, ok := ee.GetMetaString("errs[1]")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertTrue(t, strings.Contains(v1, "second"))
|
||||
}
|
||||
|
||||
type stringerImpl struct{ s string }
|
||||
|
||||
func (s stringerImpl) String() string { return s.s }
|
||||
|
||||
func TestBuilderStringerAndId(t *testing.T) {
|
||||
err := New(TypeInternal, "msg").
|
||||
Stringer("s", stringerImpl{s: "hello"}).
|
||||
Id("id", stringerImpl{s: "abc-123"}).
|
||||
Build()
|
||||
ee := err.(*ExErr)
|
||||
|
||||
v, _ := ee.GetMetaString("s")
|
||||
tst.AssertEqual(t, v, "hello")
|
||||
|
||||
idv, ok := ee.GetMeta("id")
|
||||
tst.AssertTrue(t, ok)
|
||||
w := idv.(IDWrap)
|
||||
tst.AssertEqual(t, w.Value, "abc-123")
|
||||
}
|
||||
|
||||
func TestBuilderObjectID(t *testing.T) {
|
||||
oid := bson.NewObjectID()
|
||||
err := New(TypeInternal, "msg").ObjectID("oid", oid).Build()
|
||||
ee := err.(*ExErr)
|
||||
mv, ok := ee.Meta["oid"]
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, mv.DataType, MDTObjectID)
|
||||
tst.AssertEqual(t, mv.Value.(bson.ObjectID), oid)
|
||||
}
|
||||
|
||||
func TestBuilderStrPtr(t *testing.T) {
|
||||
s := "hello"
|
||||
err := New(TypeInternal, "msg").StrPtr("p", &s).StrPtr("n", nil).Build()
|
||||
ee := err.(*ExErr)
|
||||
_, ok := ee.Meta["p"]
|
||||
tst.AssertTrue(t, ok)
|
||||
_, ok = ee.Meta["n"]
|
||||
tst.AssertTrue(t, ok)
|
||||
}
|
||||
|
||||
func TestBuilderMetaCollision(t *testing.T) {
|
||||
err := New(TypeInternal, "msg").Str("k", "v1").Str("k", "v2").Str("k", "v3").Build()
|
||||
ee := err.(*ExErr)
|
||||
|
||||
v1, _ := ee.GetMetaString("k")
|
||||
tst.AssertEqual(t, v1, "v1")
|
||||
|
||||
v2, ok := ee.GetMetaString("k-2")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v2, "v2")
|
||||
|
||||
v3, ok := ee.GetMetaString("k-3")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v3, "v3")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FromError tests
|
||||
// ============================================================================
|
||||
|
||||
func TestFromErrorNil(t *testing.T) {
|
||||
ee := FromError(nil)
|
||||
tst.AssertTrue(t, ee != nil)
|
||||
tst.AssertEqual(t, ee.Category, CatForeign)
|
||||
tst.AssertEqual(t, ee.WrappedErrType, "nil")
|
||||
}
|
||||
|
||||
func TestFromErrorPassThrough(t *testing.T) {
|
||||
orig := New(TypeInternal, "msg").Build().(*ExErr)
|
||||
got := FromError(orig)
|
||||
tst.AssertTrue(t, orig == got)
|
||||
}
|
||||
|
||||
func TestFromErrorForeign(t *testing.T) {
|
||||
in := errors.New("standard")
|
||||
ee := FromError(in)
|
||||
tst.AssertEqual(t, ee.Category, CatForeign)
|
||||
tst.AssertEqual(t, ee.Message, "standard")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ExErr method tests
|
||||
// ============================================================================
|
||||
|
||||
func TestErrorReturnsRecursiveMessage(t *testing.T) {
|
||||
in := errors.New("orig")
|
||||
err := Wrap(in, "outer").Build()
|
||||
tst.AssertTrue(t, strings.Contains(err.Error(), "outer") || strings.Contains(err.Error(), "orig"))
|
||||
}
|
||||
|
||||
func TestUnwrapReturnsOriginalError(t *testing.T) {
|
||||
e1 := New(TypeInternal, "inner").Build().(*ExErr)
|
||||
e2 := Wrap(e1, "outer").Build().(*ExErr)
|
||||
tst.AssertEqual(t, e2.Unwrap().(*ExErr) == e1, true)
|
||||
}
|
||||
|
||||
func TestUnwrapForeign(t *testing.T) {
|
||||
in := errors.New("std")
|
||||
ee := FromError(in)
|
||||
u := ee.Unwrap()
|
||||
tst.AssertEqual(t, u.Error(), "std")
|
||||
}
|
||||
|
||||
func TestUnwrapNilWhenNoneAvailable(t *testing.T) {
|
||||
ee := New(TypeInternal, "msg").Build().(*ExErr)
|
||||
tst.AssertTrue(t, ee.Unwrap() == nil)
|
||||
}
|
||||
|
||||
func TestRecursiveMessageSkipsForeign(t *testing.T) {
|
||||
in := errors.New("foreign-msg")
|
||||
err := Wrap(in, "wrapped-msg").Build().(*ExErr)
|
||||
tst.AssertEqual(t, err.RecursiveMessage(), "wrapped-msg")
|
||||
}
|
||||
|
||||
func TestRecursiveMessageFallback(t *testing.T) {
|
||||
ee := &ExErr{Message: "self"}
|
||||
tst.AssertEqual(t, ee.RecursiveMessage(), "self")
|
||||
}
|
||||
|
||||
func TestRecursiveType(t *testing.T) {
|
||||
e1 := New(TypeAssert, "inner").Build().(*ExErr)
|
||||
e2 := Wrap(e1, "outer").Build().(*ExErr)
|
||||
tst.AssertEqual(t, e2.RecursiveType(), TypeAssert)
|
||||
}
|
||||
|
||||
func TestRecursiveStatuscode(t *testing.T) {
|
||||
e1 := New(TypeInternal, "inner").WithStatuscode(404).Build().(*ExErr)
|
||||
e2 := Wrap(e1, "outer").Build().(*ExErr)
|
||||
|
||||
got := e2.RecursiveStatuscode()
|
||||
tst.AssertDeRefEqual(t, got, 404)
|
||||
}
|
||||
|
||||
func TestRecursiveStatuscodeNil(t *testing.T) {
|
||||
e1 := New(TypeWrap, "x").Build().(*ExErr)
|
||||
tst.AssertTrue(t, e1.RecursiveStatuscode() == nil)
|
||||
}
|
||||
|
||||
func TestRecursiveCategory(t *testing.T) {
|
||||
e1 := New(TypeInternal, "inner").User().Build().(*ExErr)
|
||||
e2 := Wrap(e1, "outer").Build().(*ExErr)
|
||||
tst.AssertEqual(t, e2.RecursiveCategory(), CatUser)
|
||||
}
|
||||
|
||||
func TestRecursiveMeta(t *testing.T) {
|
||||
e1 := New(TypeInternal, "inner").Str("xkey", "xval").Build().(*ExErr)
|
||||
e2 := Wrap(e1, "outer").Build().(*ExErr)
|
||||
|
||||
mv := e2.RecursiveMeta("xkey")
|
||||
tst.AssertTrue(t, mv != nil)
|
||||
tst.AssertEqual(t, mv.Value.(string), "xval")
|
||||
tst.AssertTrue(t, e2.RecursiveMeta("nope") == nil)
|
||||
}
|
||||
|
||||
func TestDepth(t *testing.T) {
|
||||
e1 := New(TypeInternal, "1").Build().(*ExErr)
|
||||
tst.AssertEqual(t, e1.Depth(), 1)
|
||||
e2 := Wrap(e1, "2").Build().(*ExErr)
|
||||
tst.AssertEqual(t, e2.Depth(), 2)
|
||||
e3 := Wrap(e2, "3").Build().(*ExErr)
|
||||
tst.AssertEqual(t, e3.Depth(), 3)
|
||||
}
|
||||
|
||||
func TestGetMetaStringTypeMismatch(t *testing.T) {
|
||||
err := New(TypeInternal, "msg").Int("k", 1).Build().(*ExErr)
|
||||
_, ok := err.GetMetaString("k")
|
||||
tst.AssertFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestGetMetaMissing(t *testing.T) {
|
||||
err := New(TypeInternal, "msg").Build().(*ExErr)
|
||||
_, ok := err.GetMeta("missing")
|
||||
tst.AssertFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestGetExtraMissing(t *testing.T) {
|
||||
err := New(TypeInternal, "msg").Build().(*ExErr)
|
||||
_, ok := err.GetExtra("missing")
|
||||
tst.AssertFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestUniqueIDsCollects(t *testing.T) {
|
||||
e1 := New(TypeInternal, "1").Build().(*ExErr)
|
||||
e2 := Wrap(e1, "2").Build().(*ExErr)
|
||||
e3 := Wrap(e2, "3").Build().(*ExErr)
|
||||
ids := e3.UniqueIDs()
|
||||
tst.AssertEqual(t, len(ids), 3)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Format / Log
|
||||
// ============================================================================
|
||||
|
||||
func TestFormatLogShort(t *testing.T) {
|
||||
err := New(TypeAssert, "boom").Build().(*ExErr)
|
||||
out := err.FormatLog(LogPrintShort)
|
||||
tst.AssertTrue(t, strings.Contains(out, "boom"))
|
||||
tst.AssertTrue(t, strings.Contains(out, TypeAssert.Key))
|
||||
}
|
||||
|
||||
func TestFormatLogOverview(t *testing.T) {
|
||||
e1 := New(TypeAssert, "inner").Build().(*ExErr)
|
||||
e2 := Wrap(e1, "outer").Build().(*ExErr)
|
||||
out := e2.FormatLog(LogPrintOverview)
|
||||
tst.AssertTrue(t, strings.Contains(out, "outer"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "inner"))
|
||||
}
|
||||
|
||||
func TestFormatLogFull(t *testing.T) {
|
||||
err := New(TypeInternal, "boom").Str("k", "v").Build().(*ExErr)
|
||||
out := err.FormatLog(LogPrintFull)
|
||||
tst.AssertTrue(t, strings.Contains(out, "boom"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "k"))
|
||||
}
|
||||
|
||||
func TestFormatLogUnknownLevel(t *testing.T) {
|
||||
err := New(TypeInternal, "boom").Build().(*ExErr)
|
||||
out := err.FormatLog(LogPrintLevel("__nope__"))
|
||||
tst.AssertTrue(t, strings.HasPrefix(out, "[?["))
|
||||
}
|
||||
|
||||
func TestBuilderFormat(t *testing.T) {
|
||||
b := New(TypeInternal, "boom")
|
||||
out := b.Format(LogPrintShort)
|
||||
tst.AssertTrue(t, strings.Contains(out, "boom"))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// helper.go
|
||||
// ============================================================================
|
||||
|
||||
func TestIsTypeMatching(t *testing.T) {
|
||||
err := New(TypeAssert, "x").Build()
|
||||
tst.AssertTrue(t, IsType(err, TypeAssert))
|
||||
tst.AssertFalse(t, IsType(err, TypeNotImplemented))
|
||||
tst.AssertFalse(t, IsType(nil, TypeAssert))
|
||||
}
|
||||
|
||||
func TestIsTypeRecursive(t *testing.T) {
|
||||
e1 := New(TypeAssert, "inner").Build()
|
||||
e2 := Wrap(e1, "outer").Build()
|
||||
tst.AssertTrue(t, IsType(e2, TypeAssert))
|
||||
}
|
||||
|
||||
func TestIsFromIdentity(t *testing.T) {
|
||||
e := errors.New("x")
|
||||
tst.AssertTrue(t, IsFrom(e, e))
|
||||
tst.AssertFalse(t, IsFrom(nil, e))
|
||||
}
|
||||
|
||||
func TestIsFromForeign(t *testing.T) {
|
||||
src := errors.New("origmsg")
|
||||
wrap := Wrap(src, "outer").Build()
|
||||
tst.AssertTrue(t, IsFrom(wrap, src))
|
||||
|
||||
other := errors.New("other")
|
||||
tst.AssertFalse(t, IsFrom(wrap, other))
|
||||
}
|
||||
|
||||
func TestHasSourceMessage(t *testing.T) {
|
||||
src := errors.New("origmsg")
|
||||
wrap := Wrap(src, "outer").Build()
|
||||
tst.AssertTrue(t, HasSourceMessage(wrap, "origmsg"))
|
||||
tst.AssertFalse(t, HasSourceMessage(wrap, "other"))
|
||||
tst.AssertFalse(t, HasSourceMessage(nil, "x"))
|
||||
}
|
||||
|
||||
func TestMessageMatch(t *testing.T) {
|
||||
e := New(TypeInternal, "alpha-beta").Build()
|
||||
tst.AssertTrue(t, MessageMatch(e, func(s string) bool { return strings.Contains(s, "alpha") }))
|
||||
tst.AssertFalse(t, MessageMatch(e, func(s string) bool { return strings.Contains(s, "missing") }))
|
||||
tst.AssertFalse(t, MessageMatch(nil, func(s string) bool { return true }))
|
||||
}
|
||||
|
||||
func TestOriginalError(t *testing.T) {
|
||||
src := errors.New("the-source")
|
||||
wrap := Wrap(src, "outer").Build()
|
||||
got := OriginalError(wrap)
|
||||
tst.AssertEqual(t, got.Error(), "the-source")
|
||||
tst.AssertTrue(t, OriginalError(nil) == nil)
|
||||
}
|
||||
|
||||
func TestUniqueIDHelper(t *testing.T) {
|
||||
err := New(TypeInternal, "x").Build()
|
||||
id := UniqueID(err)
|
||||
tst.AssertTrue(t, id != nil)
|
||||
tst.AssertTrue(t, *id != "")
|
||||
|
||||
tst.AssertTrue(t, UniqueID(nil) == nil)
|
||||
|
||||
plain := errors.New("plain")
|
||||
tst.AssertTrue(t, UniqueID(plain) == nil)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// errors.Is / errors.As
|
||||
// ============================================================================
|
||||
|
||||
type customErr struct{ msg string }
|
||||
|
||||
func (c customErr) Error() string { return c.msg }
|
||||
|
||||
func TestErrorsAsForeign(t *testing.T) {
|
||||
src := customErr{msg: "x"}
|
||||
wrap := Wrap(src, "outer").Build()
|
||||
|
||||
var got customErr
|
||||
ok := errors.As(wrap, &got)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, got.msg, "x")
|
||||
}
|
||||
|
||||
func TestErrorsIsForeign(t *testing.T) {
|
||||
src := customErr{msg: "x"}
|
||||
wrap := Wrap(src, "outer").Build()
|
||||
|
||||
tst.AssertTrue(t, errors.Is(wrap, customErr{msg: "x"}))
|
||||
tst.AssertFalse(t, errors.Is(wrap, customErr{msg: "y"}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MetaValue serialize / deserialize roundtrip
|
||||
// ============================================================================
|
||||
|
||||
func TestMetaValueRoundtripPrimitives(t *testing.T) {
|
||||
cases := []MetaValue{
|
||||
{DataType: MDTString, Value: "hello"},
|
||||
{DataType: MDTInt, Value: 42},
|
||||
{DataType: MDTInt8, Value: int8(8)},
|
||||
{DataType: MDTInt16, Value: int16(16)},
|
||||
{DataType: MDTInt32, Value: int32(32)},
|
||||
{DataType: MDTInt64, Value: int64(64)},
|
||||
{DataType: MDTFloat32, Value: float32(1.5)},
|
||||
{DataType: MDTFloat64, Value: float64(2.5)},
|
||||
{DataType: MDTBool, Value: true},
|
||||
{DataType: MDTBool, Value: false},
|
||||
{DataType: MDTBytes, Value: []byte{0x01, 0x02, 0xAB}},
|
||||
{DataType: MDTStringArray, Value: []string{"a", "b"}},
|
||||
{DataType: MDTIntArray, Value: []int{1, 2, 3}},
|
||||
{DataType: MDTInt32Array, Value: []int32{4, 5}},
|
||||
{DataType: MDTNil, Value: nil},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
s, err := c.SerializeValue()
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
var dec MetaValue
|
||||
tst.AssertNoErr(t, dec.Deserialize(s, c.DataType))
|
||||
tst.AssertStrRepEqual(t, dec.Value, c.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaValueRoundtripStringPtr(t *testing.T) {
|
||||
v := "hello"
|
||||
mv := MetaValue{DataType: MDTStringPtr, Value: &v}
|
||||
s, err := mv.SerializeValue()
|
||||
tst.AssertNoErr(t, err)
|
||||
var dec MetaValue
|
||||
tst.AssertNoErr(t, dec.Deserialize(s, MDTStringPtr))
|
||||
tst.AssertEqual(t, *(dec.Value.(*string)), v)
|
||||
|
||||
mv2 := MetaValue{DataType: MDTStringPtr, Value: (*string)(nil)}
|
||||
s, err = mv2.SerializeValue()
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, s, "#")
|
||||
}
|
||||
|
||||
func TestMetaValueRoundtripTime(t *testing.T) {
|
||||
tm := time.Date(2025, 4, 27, 12, 34, 56, 12345, time.UTC)
|
||||
mv := MetaValue{DataType: MDTTime, Value: tm}
|
||||
s, err := mv.SerializeValue()
|
||||
tst.AssertNoErr(t, err)
|
||||
var dec MetaValue
|
||||
tst.AssertNoErr(t, dec.Deserialize(s, MDTTime))
|
||||
got := dec.Value.(time.Time)
|
||||
tst.AssertEqual(t, got.Unix(), tm.Unix())
|
||||
tst.AssertEqual(t, got.Nanosecond(), tm.Nanosecond())
|
||||
}
|
||||
|
||||
func TestMetaValueRoundtripDuration(t *testing.T) {
|
||||
d := 3*time.Second + 250*time.Millisecond
|
||||
mv := MetaValue{DataType: MDTDuration, Value: d}
|
||||
s, err := mv.SerializeValue()
|
||||
tst.AssertNoErr(t, err)
|
||||
var dec MetaValue
|
||||
tst.AssertNoErr(t, dec.Deserialize(s, MDTDuration))
|
||||
tst.AssertEqual(t, dec.Value.(time.Duration), d)
|
||||
}
|
||||
|
||||
func TestMetaValueRoundtripObjectID(t *testing.T) {
|
||||
oid := bson.NewObjectID()
|
||||
mv := MetaValue{DataType: MDTObjectID, Value: oid}
|
||||
s, err := mv.SerializeValue()
|
||||
tst.AssertNoErr(t, err)
|
||||
var dec MetaValue
|
||||
tst.AssertNoErr(t, dec.Deserialize(s, MDTObjectID))
|
||||
tst.AssertEqual(t, dec.Value.(bson.ObjectID), oid)
|
||||
}
|
||||
|
||||
func TestMetaValueDeserializeUnknownType(t *testing.T) {
|
||||
var mv MetaValue
|
||||
err := mv.Deserialize("x", metaDataType("unknown"))
|
||||
tst.AssertTrue(t, err != nil)
|
||||
}
|
||||
|
||||
func TestMetaValueDeserializeBadBool(t *testing.T) {
|
||||
var mv MetaValue
|
||||
err := mv.Deserialize("nope", MDTBool)
|
||||
tst.AssertTrue(t, err != nil)
|
||||
}
|
||||
|
||||
func TestMetaValueShortString(t *testing.T) {
|
||||
cases := []struct {
|
||||
mv MetaValue
|
||||
out string
|
||||
}{
|
||||
{MetaValue{DataType: MDTString, Value: "hello"}, "hello"},
|
||||
{MetaValue{DataType: MDTInt, Value: 42}, "42"},
|
||||
{MetaValue{DataType: MDTBool, Value: true}, "true"},
|
||||
{MetaValue{DataType: MDTNil, Value: nil}, "<<null>>"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
tst.AssertEqual(t, c.mv.ShortString(100), c.out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaValueValueString(t *testing.T) {
|
||||
mv := MetaValue{DataType: MDTInt, Value: 42}
|
||||
tst.AssertEqual(t, mv.ValueString(), "42")
|
||||
|
||||
mv = MetaValue{DataType: MDTString, Value: "ok"}
|
||||
tst.AssertEqual(t, mv.ValueString(), "ok")
|
||||
}
|
||||
|
||||
func TestMetaValueJSONMarshal(t *testing.T) {
|
||||
mv := MetaValue{DataType: MDTString, Value: "abc"}
|
||||
bin, err := json.Marshal(mv)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
var dec MetaValue
|
||||
tst.AssertNoErr(t, json.Unmarshal(bin, &dec))
|
||||
tst.AssertEqual(t, dec.DataType, MDTString)
|
||||
tst.AssertEqual(t, dec.Value.(string), "abc")
|
||||
}
|
||||
|
||||
func TestMetaValueJSONMarshalInvalidString(t *testing.T) {
|
||||
var mv MetaValue
|
||||
err := json.Unmarshal([]byte("\"badformat\""), &mv)
|
||||
tst.AssertTrue(t, err != nil)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MetaMap
|
||||
// ============================================================================
|
||||
|
||||
func TestMetaMapAny(t *testing.T) {
|
||||
mm := MetaMap{}
|
||||
tst.AssertFalse(t, mm.Any())
|
||||
mm.add("k", MDTString, "v")
|
||||
tst.AssertTrue(t, mm.Any())
|
||||
}
|
||||
|
||||
func TestMetaMapFormatOneLine(t *testing.T) {
|
||||
mm := MetaMap{}
|
||||
mm.add("k", MDTString, "v")
|
||||
out := mm.FormatOneLine(100)
|
||||
tst.AssertTrue(t, strings.Contains(out, "k"))
|
||||
tst.AssertTrue(t, strings.Contains(out, "v"))
|
||||
}
|
||||
|
||||
func TestMetaMapFormatMultiLine(t *testing.T) {
|
||||
mm := MetaMap{}
|
||||
mm.add("k1", MDTString, "v1")
|
||||
mm.add("gin_body", MDTString, "should-be-skipped")
|
||||
out := mm.FormatMultiLine("", " ", 100)
|
||||
tst.AssertTrue(t, strings.Contains(out, "k1"))
|
||||
tst.AssertFalse(t, strings.Contains(out, "should-be-skipped"))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// typeWrapper.go
|
||||
// ============================================================================
|
||||
|
||||
func TestIDWrap(t *testing.T) {
|
||||
w := newIDWrap(stringerImpl{s: "id-1"})
|
||||
tst.AssertEqual(t, w.Value, "id-1")
|
||||
tst.AssertFalse(t, w.IsNil)
|
||||
|
||||
s := w.Serialize()
|
||||
got := deserializeIDWrap(s)
|
||||
tst.AssertEqual(t, got.Value, "id-1")
|
||||
tst.AssertEqual(t, got.Type, w.Type)
|
||||
}
|
||||
|
||||
func TestIDWrapNil(t *testing.T) {
|
||||
var nilStringer fmt.Stringer = (*stringerImpl)(nil)
|
||||
w := newIDWrap(nilStringer)
|
||||
tst.AssertTrue(t, w.IsNil)
|
||||
|
||||
s := w.Serialize()
|
||||
got := deserializeIDWrap(s)
|
||||
tst.AssertTrue(t, got.IsNil)
|
||||
}
|
||||
|
||||
func TestAnyWrap(t *testing.T) {
|
||||
type p struct {
|
||||
X int `json:"x"`
|
||||
}
|
||||
w := newAnyWrap(p{X: 7})
|
||||
tst.AssertFalse(t, w.IsError)
|
||||
tst.AssertFalse(t, w.IsNil)
|
||||
tst.AssertTrue(t, strings.Contains(w.Json, "\"x\":7"))
|
||||
|
||||
s := w.Serialize()
|
||||
got := deserializeAnyWrap(s)
|
||||
tst.AssertEqual(t, got.IsError, false)
|
||||
tst.AssertTrue(t, strings.Contains(got.Json, "\"x\":7"))
|
||||
}
|
||||
|
||||
func TestAnyWrapNil(t *testing.T) {
|
||||
w := newAnyWrap(nil)
|
||||
tst.AssertTrue(t, w.IsNil)
|
||||
|
||||
s := w.Serialize()
|
||||
got := deserializeAnyWrap(s)
|
||||
tst.AssertTrue(t, got.IsNil)
|
||||
}
|
||||
|
||||
func TestAnyWrapDeserializeBad(t *testing.T) {
|
||||
got := deserializeAnyWrap("xx")
|
||||
tst.AssertTrue(t, got.IsError)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// dataType.go
|
||||
// ============================================================================
|
||||
|
||||
func TestNewTypeRegisters(t *testing.T) {
|
||||
custom := NewType("UNIT_TEST_CUSTOM_TYPE", new(503))
|
||||
tst.AssertEqual(t, custom.Key, "UNIT_TEST_CUSTOM_TYPE")
|
||||
tst.AssertDeRefEqual(t, custom.DefaultStatusCode, 503)
|
||||
|
||||
all := ListRegisteredTypes()
|
||||
found := false
|
||||
for _, et := range all {
|
||||
if et.Key == "UNIT_TEST_CUSTOM_TYPE" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
tst.AssertTrue(t, found)
|
||||
}
|
||||
|
||||
func TestErrorTypeJSONUnmarshalKnown(t *testing.T) {
|
||||
var et ErrorType
|
||||
tst.AssertNoErr(t, json.Unmarshal([]byte("\"NOT_IMPLEMENTED\""), &et))
|
||||
tst.AssertEqual(t, et.Key, TypeNotImplemented.Key)
|
||||
}
|
||||
|
||||
func TestErrorTypeJSONUnmarshalUnknown(t *testing.T) {
|
||||
var et ErrorType
|
||||
tst.AssertNoErr(t, json.Unmarshal([]byte("\"COMPLETELY_UNKNOWN_TYPE_QQQ\""), &et))
|
||||
tst.AssertEqual(t, et.Key, "COMPLETELY_UNKNOWN_TYPE_QQQ")
|
||||
tst.AssertTrue(t, et.DefaultStatusCode == nil)
|
||||
}
|
||||
|
||||
func TestCategoryAndSeverityJSONMarshal(t *testing.T) {
|
||||
bin, err := json.Marshal(CatUser)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(bin), "\"User\"")
|
||||
|
||||
var c ErrorCategory
|
||||
tst.AssertNoErr(t, json.Unmarshal(bin, &c))
|
||||
tst.AssertEqual(t, c, CatUser)
|
||||
|
||||
bin, err = json.Marshal(SevWarn)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(bin), "\"Warn\"")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// proxy.go
|
||||
// ============================================================================
|
||||
|
||||
func TestProxy(t *testing.T) {
|
||||
ee := New(TypeInternal, "x").Build().(*ExErr)
|
||||
p := Proxy{v: *ee}
|
||||
tst.AssertEqual(t, p.UniqueID(), ee.UniqueID)
|
||||
tst.AssertEqual(t, p.Get().Message, ee.Message)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// listener.go
|
||||
// ============================================================================
|
||||
|
||||
func TestRegisterListenerInvokedOnBuild(t *testing.T) {
|
||||
var gotMethod Method
|
||||
var gotErr *ExErr
|
||||
RegisterListener(func(method Method, v *ExErr, opt ListenerOpt) {
|
||||
if v != nil && strings.Contains(v.Message, "listener-marker-1") {
|
||||
gotMethod = method
|
||||
gotErr = v
|
||||
}
|
||||
})
|
||||
|
||||
_ = New(TypeInternal, "listener-marker-1").Build()
|
||||
|
||||
tst.AssertEqual(t, gotMethod, MethodBuild)
|
||||
tst.AssertTrue(t, gotErr != nil)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Initialized
|
||||
// ============================================================================
|
||||
|
||||
func TestInitialized(t *testing.T) {
|
||||
tst.AssertTrue(t, Initialized())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JSON output (toJson / ToAPIJson)
|
||||
// ============================================================================
|
||||
|
||||
func TestToAPIJsonContainsCoreFields(t *testing.T) {
|
||||
err := New(TypeInternal, "boom").Str("k", "v").Extra("ex", 1).Build().(*ExErr)
|
||||
out := err.ToAPIJson(false, true, true)
|
||||
|
||||
tst.AssertEqual(t, out["errorid"].(string), err.UniqueID)
|
||||
tst.AssertEqual(t, out["errorcode"].(string), TypeInternal.Key)
|
||||
tst.AssertEqual(t, out["category"].(string), CatSystem.Category)
|
||||
tst.AssertEqual(t, out["message"].(string), "boom")
|
||||
|
||||
_, hasData := out["__data"]
|
||||
tst.AssertTrue(t, hasData)
|
||||
|
||||
tst.AssertEqual(t, out["ex"].(int), 1)
|
||||
}
|
||||
|
||||
func TestToDefaultAPIJson(t *testing.T) {
|
||||
err := New(TypeInternal, "boom").Build().(*ExErr)
|
||||
out, jerr := err.ToDefaultAPIJson()
|
||||
tst.AssertNoErr(t, jerr)
|
||||
tst.AssertTrue(t, strings.Contains(out, "boom"))
|
||||
tst.AssertTrue(t, strings.Contains(out, err.UniqueID))
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package fsext
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPathExistsFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fp := filepath.Join(dir, "file.txt")
|
||||
if err := os.WriteFile(fp, []byte("hello"), 0644); err != nil {
|
||||
t.Fatalf("setup: %v", err)
|
||||
}
|
||||
|
||||
ok, err := PathExists(fp)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("PathExists(%q) = false, want true", fp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathExistsDirectory(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
ok, err := PathExists(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("PathExists(%q) = false, want true", dir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathExistsMissing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fp := filepath.Join(dir, "does_not_exist")
|
||||
|
||||
ok, err := PathExists(fp)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Errorf("PathExists(%q) = true, want false", fp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathExistsMissingNested(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fp := filepath.Join(dir, "nope", "still_nope", "file.txt")
|
||||
|
||||
ok, err := PathExists(fp)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Errorf("PathExists(%q) = true, want false", fp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileExistsFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fp := filepath.Join(dir, "file.txt")
|
||||
if err := os.WriteFile(fp, []byte("data"), 0644); err != nil {
|
||||
t.Fatalf("setup: %v", err)
|
||||
}
|
||||
|
||||
ok, err := FileExists(fp)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("FileExists(%q) = false, want true", fp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileExistsEmptyFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fp := filepath.Join(dir, "empty.txt")
|
||||
if err := os.WriteFile(fp, []byte{}, 0644); err != nil {
|
||||
t.Fatalf("setup: %v", err)
|
||||
}
|
||||
|
||||
ok, err := FileExists(fp)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("FileExists(%q) = false, want true", fp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileExistsDirectoryReturnsFalse(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
ok, err := FileExists(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Errorf("FileExists(%q) = true, want false (it's a directory)", dir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileExistsMissing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fp := filepath.Join(dir, "missing.txt")
|
||||
|
||||
ok, err := FileExists(fp)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Errorf("FileExists(%q) = true, want false", fp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectoryExistsDirectory(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
ok, err := DirectoryExists(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("DirectoryExists(%q) = false, want true", dir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectoryExistsNestedDirectory(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
nested := filepath.Join(dir, "a", "b", "c")
|
||||
if err := os.MkdirAll(nested, 0755); err != nil {
|
||||
t.Fatalf("setup: %v", err)
|
||||
}
|
||||
|
||||
ok, err := DirectoryExists(nested)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("DirectoryExists(%q) = false, want true", nested)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectoryExistsFileReturnsFalse(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fp := filepath.Join(dir, "file.txt")
|
||||
if err := os.WriteFile(fp, []byte("data"), 0644); err != nil {
|
||||
t.Fatalf("setup: %v", err)
|
||||
}
|
||||
|
||||
ok, err := DirectoryExists(fp)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Errorf("DirectoryExists(%q) = true, want false (it's a file)", fp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectoryExistsMissing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fp := filepath.Join(dir, "missing")
|
||||
|
||||
ok, err := DirectoryExists(fp)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Errorf("DirectoryExists(%q) = true, want false", fp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathExistsEmptyString(t *testing.T) {
|
||||
ok, err := PathExists("")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Errorf("PathExists(\"\") = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileExistsEmptyString(t *testing.T) {
|
||||
ok, err := FileExists("")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Errorf("FileExists(\"\") = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectoryExistsEmptyString(t *testing.T) {
|
||||
ok, err := DirectoryExists("")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Errorf("DirectoryExists(\"\") = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathExistsSymlinkToFile(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("symlinks require admin privileges on windows")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
target := filepath.Join(dir, "target.txt")
|
||||
if err := os.WriteFile(target, []byte("data"), 0644); err != nil {
|
||||
t.Fatalf("setup: %v", err)
|
||||
}
|
||||
link := filepath.Join(dir, "link.txt")
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Fatalf("setup: %v", err)
|
||||
}
|
||||
|
||||
ok, err := PathExists(link)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("PathExists(symlink) = false, want true")
|
||||
}
|
||||
|
||||
ok, err = FileExists(link)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("FileExists(symlink-to-file) = false, want true")
|
||||
}
|
||||
|
||||
ok, err = DirectoryExists(link)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Errorf("DirectoryExists(symlink-to-file) = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathExistsBrokenSymlink(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("symlinks require admin privileges on windows")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
link := filepath.Join(dir, "broken")
|
||||
if err := os.Symlink(filepath.Join(dir, "nonexistent_target"), link); err != nil {
|
||||
t.Fatalf("setup: %v", err)
|
||||
}
|
||||
|
||||
ok, err := PathExists(link)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Errorf("PathExists(broken-symlink) = true, want false")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestCreateBackgroundAppContext(t *testing.T) {
|
||||
ac := CreateBackgroundAppContext()
|
||||
if ac == nil {
|
||||
t.Fatalf("expected non-nil context")
|
||||
}
|
||||
if ac.GinContext != nil {
|
||||
t.Fatalf("background context should have no gin context")
|
||||
}
|
||||
if ac.Err() != nil {
|
||||
t.Fatalf("expected no error")
|
||||
}
|
||||
if _, ok := ac.Deadline(); ok {
|
||||
t.Fatalf("background context should have no deadline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAppContext_CopiesGinKeys(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
g, _ := gin.CreateTestContext(rec)
|
||||
g.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
g.Set("foo", "bar")
|
||||
g.Set("num", 42)
|
||||
|
||||
inner, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ac := CreateAppContext(g, inner, cancel)
|
||||
|
||||
if ac.Value("foo") != "bar" {
|
||||
t.Fatalf("expected key foo to be copied")
|
||||
}
|
||||
if ac.Value("num") != 42 {
|
||||
t.Fatalf("expected key num to be copied")
|
||||
}
|
||||
if ac.GinContext != g {
|
||||
t.Fatalf("expected GinContext to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppContext_Set(t *testing.T) {
|
||||
ac := CreateBackgroundAppContext()
|
||||
ac.Set("k", "v")
|
||||
if ac.Value("k") != "v" {
|
||||
t.Fatalf("expected Set to store value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppContext_Cancel(t *testing.T) {
|
||||
called := false
|
||||
cancel := func() { called = true }
|
||||
ac := &AppContext{
|
||||
inner: context.Background(),
|
||||
cancelFunc: cancel,
|
||||
}
|
||||
ac.Cancel()
|
||||
if !called {
|
||||
t.Fatalf("expected cancel function to be invoked")
|
||||
}
|
||||
if !ac.cancelled {
|
||||
t.Fatalf("expected cancelled flag set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppContext_DeadlineDoneErr(t *testing.T) {
|
||||
deadline := time.Now().Add(1 * time.Hour)
|
||||
inner, cancel := context.WithDeadline(context.Background(), deadline)
|
||||
defer cancel()
|
||||
|
||||
ac := &AppContext{inner: inner, cancelFunc: cancel}
|
||||
d, ok := ac.Deadline()
|
||||
if !ok {
|
||||
t.Fatalf("expected deadline ok")
|
||||
}
|
||||
if !d.Equal(deadline) {
|
||||
t.Fatalf("deadline mismatch")
|
||||
}
|
||||
if ac.Done() == nil {
|
||||
t.Fatalf("expected non-nil Done channel")
|
||||
}
|
||||
if ac.Err() != nil {
|
||||
t.Fatalf("expected no err yet")
|
||||
}
|
||||
|
||||
cancel()
|
||||
// After cancel, Err should return Canceled
|
||||
if !errors.Is(ac.Err(), context.Canceled) {
|
||||
t.Fatalf("expected context.Canceled, got %v", ac.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppContext_RequestURI(t *testing.T) {
|
||||
bg := CreateBackgroundAppContext()
|
||||
if bg.RequestURI() != "" {
|
||||
t.Fatalf("expected empty for background context")
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
g, _ := gin.CreateTestContext(rec)
|
||||
g.Request = httptest.NewRequest(http.MethodPost, "/foo/bar", nil)
|
||||
|
||||
inner, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ac := CreateAppContext(g, inner, cancel)
|
||||
|
||||
uri := ac.RequestURI()
|
||||
if uri != "POST :: /foo/bar" {
|
||||
t.Fatalf("expected POST :: /foo/bar, got %q", uri)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRedirectFound(t *testing.T) {
|
||||
hf := RedirectFound("/x")
|
||||
resp := hf(PreContext{})
|
||||
if resp == nil {
|
||||
t.Fatalf("expected response")
|
||||
}
|
||||
if resp.(InspectableHTTPResponse).Statuscode() != http.StatusFound {
|
||||
t.Fatalf("expected 302")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectTemporary(t *testing.T) {
|
||||
hf := RedirectTemporary("/x")
|
||||
resp := hf(PreContext{})
|
||||
if resp.(InspectableHTTPResponse).Statuscode() != http.StatusTemporaryRedirect {
|
||||
t.Fatalf("expected 307")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectPermanent(t *testing.T) {
|
||||
hf := RedirectPermanent("/x")
|
||||
resp := hf(PreContext{})
|
||||
if resp.(InspectableHTTPResponse).Statuscode() != http.StatusPermanentRedirect {
|
||||
t.Fatalf("expected 308")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestBodyBuffer_WrapsBody(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/", strings.NewReader("payload"))
|
||||
|
||||
original := c.Request.Body
|
||||
BodyBuffer(c)
|
||||
if c.Request.Body == original {
|
||||
t.Fatalf("expected body to be replaced with buffered reader")
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read err: %v", err)
|
||||
}
|
||||
if !bytes.Equal(data, []byte("payload")) {
|
||||
t.Fatalf("body mismatch: %q", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodyBuffer_NilBody(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
c.Request.Body = nil
|
||||
|
||||
// Should not panic
|
||||
BodyBuffer(c)
|
||||
if c.Request.Body != nil {
|
||||
t.Fatalf("expected nil body to remain nil")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestCorsMiddleware_SetsHeaders(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
mw := CorsMiddleware([]string{"X-Foo", "X-Bar"}, []string{"X-Exposed"})
|
||||
mw(c)
|
||||
|
||||
h := rec.Header()
|
||||
if h.Get("Access-Control-Allow-Origin") != "*" {
|
||||
t.Fatalf("expected Allow-Origin *")
|
||||
}
|
||||
if h.Get("Access-Control-Allow-Credentials") != "true" {
|
||||
t.Fatalf("expected Allow-Credentials true")
|
||||
}
|
||||
if h.Get("Access-Control-Allow-Headers") != "X-Foo, X-Bar" {
|
||||
t.Fatalf("expected Allow-Headers X-Foo, X-Bar got %q", h.Get("Access-Control-Allow-Headers"))
|
||||
}
|
||||
if h.Get("Access-Control-Expose-Headers") != "X-Exposed" {
|
||||
t.Fatalf("expected Expose-Headers X-Exposed got %q", h.Get("Access-Control-Expose-Headers"))
|
||||
}
|
||||
allowMethods := h.Get("Access-Control-Allow-Methods")
|
||||
for _, want := range []string{"OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE", "COUNT"} {
|
||||
if !strings.Contains(allowMethods, want) {
|
||||
t.Errorf("expected Allow-Methods to contain %q, got %q", want, allowMethods)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorsMiddleware_NoExposeHeader(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
mw := CorsMiddleware([]string{"X-Foo"}, []string{})
|
||||
mw(c)
|
||||
|
||||
if _, ok := rec.Header()["Access-Control-Expose-Headers"]; ok {
|
||||
t.Fatalf("expected Expose-Headers to be unset when empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorsMiddleware_OptionsAborts(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodOptions, "/", nil)
|
||||
|
||||
mw := CorsMiddleware([]string{"X-Foo"}, nil)
|
||||
mw(c)
|
||||
|
||||
if !c.IsAborted() {
|
||||
t.Fatalf("expected context aborted on OPTIONS")
|
||||
}
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 on OPTIONS, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorsMiddleware_NonOptionsContinues(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
mw := CorsMiddleware([]string{"X-Foo"}, nil)
|
||||
mw(c)
|
||||
|
||||
if c.IsAborted() {
|
||||
t.Fatalf("non-OPTIONS request should not be aborted")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestNewEngine_DefaultsApplied(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
if w == nil {
|
||||
t.Fatalf("expected non-nil wrapper")
|
||||
}
|
||||
if w.engine == nil {
|
||||
t.Fatalf("expected gin engine")
|
||||
}
|
||||
if w.allowCors {
|
||||
t.Fatalf("expected allowCors default false")
|
||||
}
|
||||
if w.bufferBody {
|
||||
t.Fatalf("expected bufferBody default false")
|
||||
}
|
||||
if !w.ginDebug {
|
||||
t.Fatalf("expected ginDebug default true")
|
||||
}
|
||||
if w.requestTimeout != 24*time.Hour {
|
||||
t.Fatalf("expected default 24h timeout, got %s", w.requestTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEngine_OptionsHonored(t *testing.T) {
|
||||
allowCors := true
|
||||
bufferBody := true
|
||||
suppress := true
|
||||
debug := false
|
||||
timeout := 5 * time.Second
|
||||
|
||||
w := NewEngine(Options{
|
||||
AllowCors: &allowCors,
|
||||
BufferBody: &bufferBody,
|
||||
SuppressGinLogs: &suppress,
|
||||
GinDebug: &debug,
|
||||
Timeout: &timeout,
|
||||
CorsAllowHeader: &[]string{"X-Custom"},
|
||||
})
|
||||
|
||||
if !w.allowCors {
|
||||
t.Fatalf("allowCors")
|
||||
}
|
||||
if !w.bufferBody {
|
||||
t.Fatalf("bufferBody")
|
||||
}
|
||||
if !w.suppressGinLogs {
|
||||
t.Fatalf("suppressGinLogs")
|
||||
}
|
||||
if w.ginDebug {
|
||||
t.Fatalf("ginDebug should be false")
|
||||
}
|
||||
if w.requestTimeout != timeout {
|
||||
t.Fatalf("timeout mismatch")
|
||||
}
|
||||
if !langext.ArrEqualsExact(w.corsAllowHeader, []string{"X-Custom"}) {
|
||||
t.Fatalf("expected custom allow header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEngine_BuildRequestBindError_DefaultIsErrorWrapper(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
if w.buildRequestBindError == nil {
|
||||
t.Fatalf("expected default builder")
|
||||
}
|
||||
resp := w.buildRequestBindError(nil, "URI", http.ErrAbortHandler)
|
||||
if resp == nil {
|
||||
t.Fatalf("expected response")
|
||||
}
|
||||
if resp.IsSuccess() {
|
||||
t.Fatalf("expected error response, not success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEngine_BuildRequestBindError_Custom(t *testing.T) {
|
||||
called := false
|
||||
custom := func(c *gin.Context, fieldtype string, err error) HTTPResponse {
|
||||
called = true
|
||||
return Status(http.StatusTeapot)
|
||||
}
|
||||
_ = custom // referenced below to avoid unused warning if signature mismatch
|
||||
w := NewEngine(Options{BuildRequestBindError: custom})
|
||||
resp := w.buildRequestBindError(nil, "URI", http.ErrAbortHandler)
|
||||
if !called {
|
||||
t.Fatalf("expected custom builder to be invoked")
|
||||
}
|
||||
if resp.(InspectableHTTPResponse).Statuscode() != http.StatusTeapot {
|
||||
t.Fatalf("expected 418 from custom builder")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeHTTP_RoundTrip(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.Routes().GET("/hello").Handle(func(p PreContext) HTTPResponse {
|
||||
return Text(http.StatusOK, "world")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/hello", nil)
|
||||
rec := w.ServeHTTP(req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
if rec.Body.String() != "world" {
|
||||
t.Fatalf("expected world, got %q", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestForwardRequest(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.Routes().GET("/fwd").Handle(func(p PreContext) HTTPResponse {
|
||||
return Text(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/fwd", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
w.ForwardRequest(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRoutes(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.Routes().GET("/a").Handle(func(p PreContext) HTTPResponse { return Status(200) })
|
||||
w.Routes().POST("/b").Handle(func(p PreContext) HTTPResponse { return Status(200) })
|
||||
|
||||
rs := w.ListRoutes()
|
||||
if len(rs) < 2 {
|
||||
t.Fatalf("expected at least 2 routes, got %d", len(rs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugPrintRoutes_NoPanic(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.Routes().GET("/x").Handle(func(p PreContext) HTTPResponse { return Status(200) })
|
||||
// just verify it doesn't panic
|
||||
w.DebugPrintRoutes()
|
||||
}
|
||||
|
||||
func TestCleanMiddlewareName(t *testing.T) {
|
||||
w := NewEngine(Options{
|
||||
DebugTrimHandlerPrefixes: []string{"customprefix."},
|
||||
DebugReplaceHandlerNames: map[string]string{"BadName": "GoodName"},
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"ginext.BodyBuffer", "[BodyBuffer]"},
|
||||
{"foo.(*GinRoutesWrapper).WithJSONFilter", "[JSONFilter]"},
|
||||
{"ginext.someThing", "someThing"},
|
||||
{"api.someThing", "someThing"},
|
||||
{"customprefix.thing", "thing"},
|
||||
{"BadName", "GoodName"},
|
||||
{"badname", "GoodName"},
|
||||
{"some.pkg.Func.func1", "some.pkg.Func"},
|
||||
{"some.pkg.Func.func1.2", "some.pkg.Func"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := w.cleanMiddlewareName(tc.in); got != tc.want {
|
||||
t.Errorf("cleanMiddlewareName(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestSetJSONFilter_StoresInContext(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
SetJSONFilter(c, "myfilter")
|
||||
|
||||
v := c.GetString(jsonFilterKey)
|
||||
if v != "myfilter" {
|
||||
t.Fatalf("expected filter to be stored, got %q", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetJSONFilter_Empty(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
SetJSONFilter(c, "")
|
||||
|
||||
v := c.GetString(jsonFilterKey)
|
||||
if v != "" {
|
||||
t.Fatalf("expected empty filter")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
|
||||
func newTestCtx() (*gin.Context, *httptest.ResponseRecorder) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
return c, rec
|
||||
}
|
||||
|
||||
func TestJSONResponse_Basics(t *testing.T) {
|
||||
r := JSON(http.StatusOK, map[string]any{"a": 1})
|
||||
if !r.IsSuccess() {
|
||||
t.Fatalf("expected IsSuccess true for 200")
|
||||
}
|
||||
ir, ok := r.(InspectableHTTPResponse)
|
||||
if !ok {
|
||||
t.Fatalf("expected InspectableHTTPResponse")
|
||||
}
|
||||
if ir.Statuscode() != http.StatusOK {
|
||||
t.Fatalf("statuscode mismatch: %d", ir.Statuscode())
|
||||
}
|
||||
if ir.ContentType() != "application/json" {
|
||||
t.Fatalf("expected content-type application/json, got %q", ir.ContentType())
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONResponse_Write(t *testing.T) {
|
||||
c, rec := newTestCtx()
|
||||
r := JSON(http.StatusCreated, map[string]any{"hello": "world"})
|
||||
r.Write(c)
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d", rec.Code)
|
||||
}
|
||||
if !bytes.Contains(rec.Body.Bytes(), []byte("\"hello\":\"world\"")) {
|
||||
t.Fatalf("unexpected body: %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONResponse_BodyString(t *testing.T) {
|
||||
c, _ := newTestCtx()
|
||||
r := JSON(http.StatusOK, map[string]any{"x": 42})
|
||||
ir := r.(InspectableHTTPResponse)
|
||||
body := ir.BodyString(c)
|
||||
if body == nil {
|
||||
t.Fatalf("expected body, got nil")
|
||||
}
|
||||
if !bytes.Contains([]byte(*body), []byte("\"x\":42")) {
|
||||
t.Fatalf("unexpected body: %q", *body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONResponse_FilterFromContext(t *testing.T) {
|
||||
c, _ := newTestCtx()
|
||||
SetJSONFilter(c, "abc")
|
||||
r := JSON(http.StatusOK, map[string]any{"x": 1})
|
||||
body := r.(InspectableHTTPResponse).BodyString(c)
|
||||
if body == nil {
|
||||
t.Fatalf("expected body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONResponse_FilterOverride(t *testing.T) {
|
||||
c, _ := newTestCtx()
|
||||
SetJSONFilter(c, "abc")
|
||||
r := JSONWithFilter(http.StatusOK, map[string]any{"x": 1}, "override")
|
||||
if r == nil {
|
||||
t.Fatalf("expected response")
|
||||
}
|
||||
if !r.IsSuccess() {
|
||||
t.Fatalf("expected success")
|
||||
}
|
||||
body := r.(InspectableHTTPResponse).BodyString(c)
|
||||
if body == nil {
|
||||
t.Fatalf("expected body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponse_IsSuccessRanges(t *testing.T) {
|
||||
cases := []struct {
|
||||
code int
|
||||
ok bool
|
||||
}{
|
||||
{100, false},
|
||||
{199, false},
|
||||
{200, true},
|
||||
{201, true},
|
||||
{299, true},
|
||||
{300, true},
|
||||
{399, true},
|
||||
{400, false},
|
||||
{500, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
r := JSON(tc.code, nil)
|
||||
if r.IsSuccess() != tc.ok {
|
||||
t.Errorf("status %d: expected IsSuccess=%v", tc.code, tc.ok)
|
||||
}
|
||||
r2 := Status(tc.code)
|
||||
if r2.IsSuccess() != tc.ok {
|
||||
t.Errorf("Status(%d): expected IsSuccess=%v", tc.code, tc.ok)
|
||||
}
|
||||
r3 := Text(tc.code, "x")
|
||||
if r3.IsSuccess() != tc.ok {
|
||||
t.Errorf("Text(%d): expected IsSuccess=%v", tc.code, tc.ok)
|
||||
}
|
||||
r4 := Data(tc.code, "text/plain", []byte("x"))
|
||||
if r4.IsSuccess() != tc.ok {
|
||||
t.Errorf("Data(%d): expected IsSuccess=%v", tc.code, tc.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponse_WithHeader(t *testing.T) {
|
||||
r := JSON(http.StatusOK, nil).
|
||||
WithHeader("X-Foo", "bar").
|
||||
WithHeader("X-Baz", "qux")
|
||||
headers := r.(InspectableHTTPResponse).Headers()
|
||||
if len(headers) != 2 {
|
||||
t.Fatalf("expected 2 headers, got %d", len(headers))
|
||||
}
|
||||
if headers[0] != "X-Foo=bar" || headers[1] != "X-Baz=qux" {
|
||||
t.Fatalf("headers wrong: %v", headers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponse_WithCookie_DoesNotPanic(t *testing.T) {
|
||||
r := JSON(http.StatusOK, nil).
|
||||
WithCookie("session", "abc", 3600, "/", "example.com", true, true)
|
||||
if r == nil {
|
||||
t.Fatalf("expected response")
|
||||
}
|
||||
c, rec := newTestCtx()
|
||||
r.Write(c)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
cookies := rec.Result().Cookies()
|
||||
if len(cookies) != 1 {
|
||||
t.Fatalf("expected 1 cookie, got %d", len(cookies))
|
||||
}
|
||||
if cookies[0].Name != "session" || cookies[0].Value != "abc" {
|
||||
t.Fatalf("cookie wrong: %+v", cookies[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextResponse(t *testing.T) {
|
||||
c, rec := newTestCtx()
|
||||
r := Text(http.StatusOK, "hello")
|
||||
if r.(InspectableHTTPResponse).ContentType() != "text/plain" {
|
||||
t.Fatalf("expected text/plain")
|
||||
}
|
||||
body := r.(InspectableHTTPResponse).BodyString(c)
|
||||
if body == nil || *body != "hello" {
|
||||
t.Fatalf("body mismatch")
|
||||
}
|
||||
r.Write(c)
|
||||
if rec.Body.String() != "hello" {
|
||||
t.Fatalf("write body mismatch: %q", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataResponse(t *testing.T) {
|
||||
c, rec := newTestCtx()
|
||||
payload := []byte{0x01, 0x02, 0x03}
|
||||
r := Data(http.StatusOK, "application/octet-stream", payload)
|
||||
if r.(InspectableHTTPResponse).ContentType() != "application/octet-stream" {
|
||||
t.Fatalf("contenttype mismatch")
|
||||
}
|
||||
r.Write(c)
|
||||
if !bytes.Equal(rec.Body.Bytes(), payload) {
|
||||
t.Fatalf("body mismatch")
|
||||
}
|
||||
body := r.(InspectableHTTPResponse).BodyString(c)
|
||||
if body == nil || *body != string(payload) {
|
||||
t.Fatalf("BodyString mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusResponse(t *testing.T) {
|
||||
c, rec := newTestCtx()
|
||||
r := Status(http.StatusNoContent)
|
||||
if r.(InspectableHTTPResponse).ContentType() != "" {
|
||||
t.Fatalf("expected empty content type")
|
||||
}
|
||||
if r.(InspectableHTTPResponse).BodyString(c) != nil {
|
||||
t.Fatalf("expected nil body")
|
||||
}
|
||||
r.Write(c)
|
||||
if c.Writer.Status() != http.StatusNoContent {
|
||||
t.Fatalf("expected 204, got %d", c.Writer.Status())
|
||||
}
|
||||
_ = rec
|
||||
}
|
||||
|
||||
func TestRedirectResponse(t *testing.T) {
|
||||
c, rec := newTestCtx()
|
||||
r := Redirect(http.StatusFound, "/elsewhere")
|
||||
if r.(InspectableHTTPResponse).Statuscode() != http.StatusFound {
|
||||
t.Fatalf("status mismatch")
|
||||
}
|
||||
if r.(InspectableHTTPResponse).ContentType() != "" {
|
||||
t.Fatalf("expected empty content type")
|
||||
}
|
||||
if r.(InspectableHTTPResponse).BodyString(c) != nil {
|
||||
t.Fatalf("expected nil body")
|
||||
}
|
||||
r.Write(c)
|
||||
if rec.Code != http.StatusFound {
|
||||
t.Fatalf("expected 302, got %d", rec.Code)
|
||||
}
|
||||
if rec.Header().Get("Location") != "/elsewhere" {
|
||||
t.Fatalf("expected Location header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotImplemented(t *testing.T) {
|
||||
r := NotImplemented()
|
||||
if r == nil {
|
||||
t.Fatalf("expected response")
|
||||
}
|
||||
if r.IsSuccess() {
|
||||
t.Fatalf("NotImplemented must not be success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_NotSuccess(t *testing.T) {
|
||||
r := Error(http.ErrAbortHandler)
|
||||
if r.IsSuccess() {
|
||||
t.Fatalf("error response must not be success")
|
||||
}
|
||||
herr, ok := r.(HTTPErrorResponse)
|
||||
if !ok {
|
||||
t.Fatalf("expected HTTPErrorResponse")
|
||||
}
|
||||
if herr.Error() == nil {
|
||||
t.Fatalf("expected non-nil err")
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_ContentTypeJSON(t *testing.T) {
|
||||
r := Error(http.ErrAbortHandler)
|
||||
if r.(InspectableHTTPResponse).ContentType() != "application/json" {
|
||||
t.Fatalf("expected application/json")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadData(t *testing.T) {
|
||||
c, rec := newTestCtx()
|
||||
payload := []byte("file content")
|
||||
r := DownloadData(http.StatusOK, "text/plain", "f.txt", payload)
|
||||
if r.(InspectableHTTPResponse).ContentType() != "text/plain" {
|
||||
t.Fatalf("contenttype mismatch")
|
||||
}
|
||||
if r.(InspectableHTTPResponse).Statuscode() != http.StatusOK {
|
||||
t.Fatalf("status mismatch")
|
||||
}
|
||||
body := r.(InspectableHTTPResponse).BodyString(c)
|
||||
if body == nil || *body != string(payload) {
|
||||
t.Fatalf("body mismatch")
|
||||
}
|
||||
r.Write(c)
|
||||
if rec.Header().Get("Content-Disposition") == "" {
|
||||
t.Fatalf("expected Content-Disposition header")
|
||||
}
|
||||
if !bytes.Contains([]byte(rec.Header().Get("Content-Disposition")), []byte("f.txt")) {
|
||||
t.Fatalf("Content-Disposition does not contain filename: %q", rec.Header().Get("Content-Disposition"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeekable(t *testing.T) {
|
||||
r := Seekable("foo.bin", "application/octet-stream", bytes.NewReader([]byte("xyz")))
|
||||
if !r.IsSuccess() {
|
||||
t.Fatalf("seekable must be success")
|
||||
}
|
||||
ir := r.(InspectableHTTPResponse)
|
||||
if ir.Statuscode() != 200 {
|
||||
t.Fatalf("expected 200")
|
||||
}
|
||||
if ir.ContentType() != "application/octet-stream" {
|
||||
t.Fatalf("contenttype mismatch")
|
||||
}
|
||||
body := ir.BodyString(nil)
|
||||
if body == nil || *body != "(seekable)" {
|
||||
t.Fatalf("BodyString mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFile_Builders(t *testing.T) {
|
||||
r := File("text/plain", "/tmp/this-file-should-not-exist-xyz")
|
||||
if !r.IsSuccess() {
|
||||
t.Fatalf("File must IsSuccess true")
|
||||
}
|
||||
if r.(InspectableHTTPResponse).Statuscode() != 200 {
|
||||
t.Fatalf("expected 200")
|
||||
}
|
||||
if r.(InspectableHTTPResponse).ContentType() != "text/plain" {
|
||||
t.Fatalf("contenttype mismatch")
|
||||
}
|
||||
r2 := Download("application/pdf", "/tmp/this-file-should-not-exist-xyz", "doc.pdf")
|
||||
if r2.(InspectableHTTPResponse).ContentType() != "application/pdf" {
|
||||
t.Fatalf("contenttype mismatch")
|
||||
}
|
||||
body := r.(InspectableHTTPResponse).BodyString(nil)
|
||||
if body != nil {
|
||||
t.Fatalf("expected nil body for nonexistent file")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestJoinPaths(t *testing.T) {
|
||||
cases := []struct {
|
||||
abs, rel, want string
|
||||
}{
|
||||
{"", "", ""},
|
||||
{"/api", "", "/api"},
|
||||
{"/api", "users", "/api/users"},
|
||||
{"/api/", "users", "/api/users"},
|
||||
{"/api", "/users", "/api/users"},
|
||||
{"/api/", "/users/", "/api/users/"},
|
||||
{"/api", "users/", "/api/users/"},
|
||||
{"", "/users", "/users"},
|
||||
{"/", "/", "/"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := joinPaths(tc.abs, tc.rel)
|
||||
if got != tc.want {
|
||||
t.Errorf("joinPaths(%q,%q)=%q want %q", tc.abs, tc.rel, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastChar(t *testing.T) {
|
||||
if lastChar("hello") != 'o' {
|
||||
t.Fatalf("expected 'o'")
|
||||
}
|
||||
if lastChar("/") != '/' {
|
||||
t.Fatalf("expected '/'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastChar_PanicsOnEmpty(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatalf("expected panic on empty string")
|
||||
}
|
||||
}()
|
||||
lastChar("")
|
||||
}
|
||||
|
||||
func sampleHandler(_ *gin.Context) {}
|
||||
|
||||
func TestNameOfFunction(t *testing.T) {
|
||||
name := nameOfFunction(sampleHandler)
|
||||
if name == "" {
|
||||
t.Fatalf("expected non-empty name")
|
||||
}
|
||||
// nameOfFunction strips path prefix, expecting form "ginext.sampleHandler" or similar
|
||||
if name != "ginext.sampleHandler" {
|
||||
t.Errorf("expected ginext.sampleHandler, got %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
type sampleStruct struct{}
|
||||
|
||||
func (s sampleStruct) Method(_ *gin.Context) {}
|
||||
|
||||
func TestNameOfFunction_StripsFmSuffix(t *testing.T) {
|
||||
s := sampleStruct{}
|
||||
// Method values get a "-fm" suffix that nameOfFunction should strip
|
||||
name := nameOfFunction(s.Method)
|
||||
if name == "" {
|
||||
t.Fatalf("expected non-empty name")
|
||||
}
|
||||
if got := name; got[len(got)-3:] == "-fm" {
|
||||
t.Errorf("expected -fm suffix to be stripped, got %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_GroupAndAbsPath(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes()
|
||||
if rw.absPath != "" {
|
||||
t.Fatalf("expected empty absPath")
|
||||
}
|
||||
g1 := rw.Group("/api")
|
||||
if g1.absPath != "/api" {
|
||||
t.Fatalf("expected /api, got %q", g1.absPath)
|
||||
}
|
||||
g2 := g1.Group("/v1")
|
||||
if g2.absPath != "/api/v1" {
|
||||
t.Fatalf("expected /api/v1, got %q", g2.absPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_UseAccumulatesMiddleware(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes()
|
||||
|
||||
mw1 := func(c *gin.Context) {}
|
||||
mw2 := func(c *gin.Context) {}
|
||||
|
||||
r1 := rw.Use(mw1)
|
||||
if len(r1.defaultHandler) != 1 {
|
||||
t.Fatalf("expected 1 handler after Use, got %d", len(r1.defaultHandler))
|
||||
}
|
||||
r2 := r1.Use(mw2)
|
||||
if len(r2.defaultHandler) != 2 {
|
||||
t.Fatalf("expected 2 handlers, got %d", len(r2.defaultHandler))
|
||||
}
|
||||
// Original parent should be unchanged
|
||||
if len(rw.defaultHandler) != 0 {
|
||||
t.Fatalf("expected parent to remain unchanged, got %d", len(rw.defaultHandler))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_GroupCopiesMiddleware(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes().Use(func(c *gin.Context) {})
|
||||
g := rw.Group("/x")
|
||||
if len(g.defaultHandler) != 1 {
|
||||
t.Fatalf("expected group to inherit middleware")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_MethodBuilders(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
build func(string) *GinRouteBuilder
|
||||
want string
|
||||
}{
|
||||
{"GET", rw.GET, http.MethodGet},
|
||||
{"POST", rw.POST, http.MethodPost},
|
||||
{"PUT", rw.PUT, http.MethodPut},
|
||||
{"PATCH", rw.PATCH, http.MethodPatch},
|
||||
{"DELETE", rw.DELETE, http.MethodDelete},
|
||||
{"HEAD", rw.HEAD, http.MethodHead},
|
||||
{"OPTIONS", rw.OPTIONS, http.MethodOptions},
|
||||
{"COUNT", rw.COUNT, "COUNT"},
|
||||
{"Any", rw.Any, "*"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
b := tc.build("/foo")
|
||||
if b.method != tc.want {
|
||||
t.Errorf("%s: expected method %q, got %q", tc.name, tc.want, b.method)
|
||||
}
|
||||
if b.relPath != "/foo" {
|
||||
t.Errorf("%s: expected relPath /foo, got %q", tc.name, b.relPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_RouteBuilderUseAppends(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes()
|
||||
b := rw.GET("/x")
|
||||
startCount := len(b.handlers)
|
||||
b.Use(func(c *gin.Context) {})
|
||||
if len(b.handlers) != startCount+1 {
|
||||
t.Fatalf("expected handlers to grow by 1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_RouteBuilderInheritsDefaultHandlers(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes().Use(func(c *gin.Context) {})
|
||||
b := rw.GET("/x")
|
||||
if len(b.handlers) != 1 {
|
||||
t.Fatalf("expected route to inherit default handler")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_WithJSONFilter_AddsHandler(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes().WithJSONFilter("xyz")
|
||||
if len(rw.defaultHandler) != 1 {
|
||||
t.Fatalf("expected json filter middleware to be added")
|
||||
}
|
||||
// invoke it to verify it sets the key
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rw.defaultHandler[0](c)
|
||||
if c.GetString(jsonFilterKey) != "xyz" {
|
||||
t.Fatalf("expected jsonFilterKey to be set to xyz")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_RouteBuilderWithJSONFilter(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
b := w.Routes().GET("/x").WithJSONFilter("abc")
|
||||
if len(b.handlers) != 1 {
|
||||
t.Fatalf("expected handler to be added")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_HandleRegistersAndStoresSpec(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.Routes().GET("/foo").Handle(func(p PreContext) HTTPResponse {
|
||||
return Status(http.StatusOK)
|
||||
})
|
||||
|
||||
if len(w.routeSpecs) != 1 {
|
||||
t.Fatalf("expected 1 route spec, got %d", len(w.routeSpecs))
|
||||
}
|
||||
spec := w.routeSpecs[0]
|
||||
if spec.Method != http.MethodGet {
|
||||
t.Fatalf("expected GET, got %s", spec.Method)
|
||||
}
|
||||
if spec.URL != "/foo" {
|
||||
t.Fatalf("expected /foo, got %s", spec.URL)
|
||||
}
|
||||
|
||||
// Hitting the route should serve our handler
|
||||
req := httptest.NewRequest(http.MethodGet, "/foo", nil)
|
||||
rec := w.ServeHTTP(req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_AnyRegistersAllMethods(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.Routes().Any("/wild").Handle(func(p PreContext) HTTPResponse {
|
||||
return Status(http.StatusOK)
|
||||
})
|
||||
|
||||
for _, m := range anyMethods {
|
||||
req := httptest.NewRequest(m, "/wild", nil)
|
||||
rec := w.ServeHTTP(req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("method %s: expected 200, got %d", m, rec.Code)
|
||||
}
|
||||
}
|
||||
if w.routeSpecs[0].Method != "ANY" {
|
||||
t.Fatalf("expected method label ANY, got %s", w.routeSpecs[0].Method)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_GroupedRoutes(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
api := w.Routes().Group("/api")
|
||||
api.GET("/ping").Handle(func(p PreContext) HTTPResponse {
|
||||
return Text(http.StatusOK, "pong")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/ping", nil)
|
||||
rec := w.ServeHTTP(req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
if rec.Body.String() != "pong" {
|
||||
t.Fatalf("expected pong, got %q", rec.Body.String())
|
||||
}
|
||||
if w.routeSpecs[0].URL != "/api/ping" {
|
||||
t.Fatalf("expected absPath /api/ping in spec, got %s", w.routeSpecs[0].URL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_NoRoute(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.NoRoute(func(p PreContext) HTTPResponse {
|
||||
return Status(http.StatusTeapot)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/missing", nil)
|
||||
rec := w.ServeHTTP(req)
|
||||
if rec.Code != http.StatusTeapot {
|
||||
t.Fatalf("expected 418, got %d", rec.Code)
|
||||
}
|
||||
|
||||
if len(w.routeSpecs) != 1 || w.routeSpecs[0].URL != "[NO_ROUTE]" {
|
||||
t.Fatalf("expected NO_ROUTE spec to be recorded")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package googleapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAttachmentDumpNormalWithFilename(t *testing.T) {
|
||||
a := MailAttachment{
|
||||
IsInline: false,
|
||||
ContentType: "text/plain",
|
||||
Filename: "hello.txt",
|
||||
Data: []byte("HelloWorld"),
|
||||
}
|
||||
|
||||
lines := a.dump()
|
||||
joined := strings.Join(lines, "\n")
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(joined, "Content-Type: text/plain; charset=UTF-8"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "Content-Transfer-Encoding: base64"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, `Content-Disposition: attachment;filename="hello.txt"`))
|
||||
tst.AssertFalse(t, strings.Contains(joined, "Content-Disposition: inline"))
|
||||
|
||||
expectedB64 := base64.StdEncoding.EncodeToString([]byte("HelloWorld"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, expectedB64))
|
||||
}
|
||||
|
||||
func TestAttachmentDumpInlineWithFilename(t *testing.T) {
|
||||
a := MailAttachment{
|
||||
IsInline: true,
|
||||
ContentType: "image/png",
|
||||
Filename: "img.png",
|
||||
Data: []byte{0x01, 0x02, 0x03, 0x04},
|
||||
}
|
||||
|
||||
lines := a.dump()
|
||||
joined := strings.Join(lines, "\n")
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(joined, "Content-Type: image/png; charset=UTF-8"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "Content-Transfer-Encoding: base64"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, `Content-Disposition: inline;filename="img.png"`))
|
||||
}
|
||||
|
||||
func TestAttachmentDumpNormalNoFilename(t *testing.T) {
|
||||
a := MailAttachment{
|
||||
IsInline: false,
|
||||
ContentType: "text/plain",
|
||||
Filename: "",
|
||||
Data: []byte("foo"),
|
||||
}
|
||||
|
||||
lines := a.dump()
|
||||
joined := strings.Join(lines, "\n")
|
||||
|
||||
tst.AssertTrue(t, langext.InArray("Content-Disposition: attachment", lines))
|
||||
tst.AssertFalse(t, strings.Contains(joined, "filename="))
|
||||
}
|
||||
|
||||
func TestAttachmentDumpInlineNoFilename(t *testing.T) {
|
||||
a := MailAttachment{
|
||||
IsInline: true,
|
||||
ContentType: "text/plain",
|
||||
Filename: "",
|
||||
Data: []byte("foo"),
|
||||
}
|
||||
|
||||
lines := a.dump()
|
||||
|
||||
tst.AssertTrue(t, langext.InArray("Content-Disposition: inline", lines))
|
||||
}
|
||||
|
||||
func TestAttachmentDumpNoContentType(t *testing.T) {
|
||||
a := MailAttachment{
|
||||
IsInline: false,
|
||||
ContentType: "",
|
||||
Filename: "x.bin",
|
||||
Data: []byte("x"),
|
||||
}
|
||||
|
||||
lines := a.dump()
|
||||
|
||||
for _, l := range lines {
|
||||
tst.AssertFalse(t, strings.HasPrefix(l, "Content-Type:"))
|
||||
}
|
||||
tst.AssertTrue(t, langext.InArray("Content-Transfer-Encoding: base64", lines))
|
||||
}
|
||||
|
||||
func TestAttachmentDumpEmptyData(t *testing.T) {
|
||||
a := MailAttachment{
|
||||
IsInline: false,
|
||||
ContentType: "application/octet-stream",
|
||||
Filename: "empty.bin",
|
||||
Data: []byte{},
|
||||
}
|
||||
|
||||
lines := a.dump()
|
||||
joined := strings.Join(lines, "\n")
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(joined, "Content-Type: application/octet-stream; charset=UTF-8"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "Content-Transfer-Encoding: base64"))
|
||||
}
|
||||
|
||||
func TestAttachmentDumpLongDataLineWrapped(t *testing.T) {
|
||||
// Data needs to result in > 80 base64 chars to test the wrapping.
|
||||
// 100 bytes => 136 base64 chars => should wrap into 2 lines (80 + 56).
|
||||
data := make([]byte, 100)
|
||||
for i := range data {
|
||||
data[i] = byte(i)
|
||||
}
|
||||
|
||||
a := MailAttachment{
|
||||
IsInline: false,
|
||||
ContentType: "application/octet-stream",
|
||||
Filename: "big.bin",
|
||||
Data: data,
|
||||
}
|
||||
|
||||
lines := a.dump()
|
||||
|
||||
// Find the base64 lines (everything after the headers).
|
||||
b64Lines := make([]string, 0)
|
||||
foundFirstHeader := false
|
||||
for _, l := range lines {
|
||||
if !foundFirstHeader && (strings.HasPrefix(l, "Content-") || l == "") {
|
||||
foundFirstHeader = true
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(l, "Content-") {
|
||||
continue
|
||||
}
|
||||
b64Lines = append(b64Lines, l)
|
||||
}
|
||||
|
||||
full := strings.Join(b64Lines, "")
|
||||
expected := base64.StdEncoding.EncodeToString(data)
|
||||
tst.AssertEqual(t, full, expected)
|
||||
|
||||
// Each line (except possibly last) should be 80 chars.
|
||||
for i, l := range b64Lines {
|
||||
if i < len(b64Lines)-1 {
|
||||
tst.AssertEqual(t, len(l), 80)
|
||||
} else {
|
||||
tst.AssertTrue(t, len(l) <= 80)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package googleapi
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMimeMailPlainOnly(t *testing.T) {
|
||||
mail := encodeMimeMail(
|
||||
"from@example.com",
|
||||
[]string{"to@example.com"},
|
||||
nil, nil, nil,
|
||||
"Test Subject",
|
||||
MailBody{Plain: "Hello plain body"},
|
||||
nil)
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(mail, "From: from@example.com"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "To: to@example.com"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Subject: Test Subject"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Content-Type: text/plain; charset=UTF-8"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Content-Transfer-Encoding: 7bit"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Hello plain body"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "MIME-Version: 1.0"))
|
||||
|
||||
// Each line must be terminated by CRLF.
|
||||
tst.AssertTrue(t, strings.Contains(mail, "\r\n"))
|
||||
}
|
||||
|
||||
func TestMimeMailHTMLOnly(t *testing.T) {
|
||||
mail := encodeMimeMail(
|
||||
"from@example.com",
|
||||
[]string{"to@example.com"},
|
||||
nil, nil, nil,
|
||||
"S",
|
||||
MailBody{HTML: "<p>Hi</p>"},
|
||||
nil)
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Content-Type: text/html; charset=UTF-8"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "<p>Hi</p>"))
|
||||
tst.AssertFalse(t, strings.Contains(mail, "multipart/"))
|
||||
}
|
||||
|
||||
func TestMimeMailAlternative(t *testing.T) {
|
||||
mail := encodeMimeMail(
|
||||
"from@example.com",
|
||||
[]string{"to@example.com"},
|
||||
nil, nil, nil,
|
||||
"S",
|
||||
MailBody{
|
||||
Plain: "Plain Body",
|
||||
HTML: "<p>HTML Body</p>",
|
||||
},
|
||||
nil)
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Content-Type: multipart/alternative;"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Plain Body"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "<p>HTML Body</p>"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Content-Type: text/plain; charset=UTF-8"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Content-Type: text/html; charset=UTF-8"))
|
||||
}
|
||||
|
||||
func TestMimeMailWithCC(t *testing.T) {
|
||||
mail := encodeMimeMail(
|
||||
"from@example.com",
|
||||
[]string{"to@example.com"},
|
||||
[]string{"cc@example.com"},
|
||||
nil, nil,
|
||||
"S",
|
||||
MailBody{Plain: "x"},
|
||||
nil)
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(mail, "cc@example.com"))
|
||||
}
|
||||
|
||||
func TestMimeMailWithBCC(t *testing.T) {
|
||||
mail := encodeMimeMail(
|
||||
"from@example.com",
|
||||
[]string{"to@example.com"},
|
||||
nil,
|
||||
[]string{"bcc@example.com"},
|
||||
nil,
|
||||
"S",
|
||||
MailBody{Plain: "x"},
|
||||
nil)
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Bcc: bcc@example.com"))
|
||||
}
|
||||
|
||||
func TestMimeMailWithReplyTo(t *testing.T) {
|
||||
mail := encodeMimeMail(
|
||||
"from@example.com",
|
||||
[]string{"to@example.com"},
|
||||
nil, nil,
|
||||
[]string{"reply@example.com"},
|
||||
"S",
|
||||
MailBody{Plain: "x"},
|
||||
nil)
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Reply-To: reply@example.com"))
|
||||
}
|
||||
|
||||
func TestMimeMailMultipleRecipients(t *testing.T) {
|
||||
mail := encodeMimeMail(
|
||||
"from@example.com",
|
||||
[]string{"a@example.com", "b@example.com", "c@example.com"},
|
||||
nil, nil, nil,
|
||||
"S",
|
||||
MailBody{Plain: "x"},
|
||||
nil)
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(mail, "a@example.com"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "b@example.com"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "c@example.com"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "a@example.com, b@example.com, c@example.com"))
|
||||
}
|
||||
|
||||
func TestMimeMailSubjectEncoding(t *testing.T) {
|
||||
mail := encodeMimeMail(
|
||||
"from@example.com",
|
||||
[]string{"to@example.com"},
|
||||
nil, nil, nil,
|
||||
"Hällö Wörld",
|
||||
MailBody{Plain: "x"},
|
||||
nil)
|
||||
|
||||
// Non-ASCII subject must be quoted-printable encoded.
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Subject: =?UTF-8?q?"))
|
||||
}
|
||||
|
||||
func TestMimeMailWithNormalAttachment(t *testing.T) {
|
||||
mail := encodeMimeMail(
|
||||
"from@example.com",
|
||||
[]string{"to@example.com"},
|
||||
nil, nil, nil,
|
||||
"S",
|
||||
MailBody{Plain: "Body"},
|
||||
[]MailAttachment{
|
||||
{Data: []byte("attached"), Filename: "f.txt", IsInline: false, ContentType: "text/plain"},
|
||||
})
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Content-Type: multipart/mixed;"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, `Content-Disposition: attachment;filename="f.txt"`))
|
||||
}
|
||||
|
||||
func TestMimeMailWithInlineAttachment(t *testing.T) {
|
||||
mail := encodeMimeMail(
|
||||
"from@example.com",
|
||||
[]string{"to@example.com"},
|
||||
nil, nil, nil,
|
||||
"S",
|
||||
MailBody{HTML: "<p>x</p>"},
|
||||
[]MailAttachment{
|
||||
{Data: []byte{1, 2, 3}, Filename: "img.png", IsInline: true, ContentType: "image/png"},
|
||||
})
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Content-Type: multipart/related;"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, `Content-Disposition: inline;filename="img.png"`))
|
||||
}
|
||||
|
||||
func TestMimeMailWithBothAttachmentTypes(t *testing.T) {
|
||||
mail := encodeMimeMail(
|
||||
"from@example.com",
|
||||
[]string{"to@example.com"},
|
||||
nil, nil, nil,
|
||||
"S",
|
||||
MailBody{HTML: "<p>x</p>"},
|
||||
[]MailAttachment{
|
||||
{Data: []byte{1}, Filename: "img.png", IsInline: true, ContentType: "image/png"},
|
||||
{Data: []byte{2}, Filename: "f.txt", IsInline: false, ContentType: "text/plain"},
|
||||
})
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Content-Type: multipart/mixed;"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Content-Type: multipart/related;"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, `Content-Disposition: inline;filename="img.png"`))
|
||||
tst.AssertTrue(t, strings.Contains(mail, `Content-Disposition: attachment;filename="f.txt"`))
|
||||
}
|
||||
|
||||
func TestMimeMailEmptyBody(t *testing.T) {
|
||||
mail := encodeMimeMail(
|
||||
"from@example.com",
|
||||
[]string{"to@example.com"},
|
||||
nil, nil, nil,
|
||||
"S",
|
||||
MailBody{},
|
||||
nil)
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(mail, "From: from@example.com"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "To: to@example.com"))
|
||||
tst.AssertTrue(t, strings.Contains(mail, "Subject: S"))
|
||||
// No body type was set, so no Content-Type for body should be present.
|
||||
tst.AssertFalse(t, strings.Contains(mail, "Content-Type: text/"))
|
||||
tst.AssertFalse(t, strings.Contains(mail, "Content-Type: multipart/"))
|
||||
}
|
||||
|
||||
func TestMimeMailHasDateHeader(t *testing.T) {
|
||||
mail := encodeMimeMail(
|
||||
"from@example.com",
|
||||
[]string{"to@example.com"},
|
||||
nil, nil, nil,
|
||||
"S",
|
||||
MailBody{Plain: "x"},
|
||||
nil)
|
||||
|
||||
tst.AssertTrue(t, strings.HasPrefix(mail, "Date: "))
|
||||
}
|
||||
|
||||
func TestDumpMailBodyPlainOnly(t *testing.T) {
|
||||
lines := dumpMailBody(MailBody{Plain: "Plain"}, false, false, "BOUND", "BOUNDALT")
|
||||
joined := strings.Join(lines, "\n")
|
||||
tst.AssertTrue(t, strings.Contains(joined, "--BOUND"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "Content-Type: text/plain; charset=UTF-8"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "Plain"))
|
||||
}
|
||||
|
||||
func TestDumpMailBodyHTMLOnly(t *testing.T) {
|
||||
lines := dumpMailBody(MailBody{HTML: "<p>x</p>"}, false, false, "BOUND", "BOUNDALT")
|
||||
joined := strings.Join(lines, "\n")
|
||||
tst.AssertTrue(t, strings.Contains(joined, "--BOUND"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "Content-Type: text/html; charset=UTF-8"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "<p>x</p>"))
|
||||
}
|
||||
|
||||
func TestDumpMailBodyEmpty(t *testing.T) {
|
||||
lines := dumpMailBody(MailBody{}, false, false, "BOUND", "BOUNDALT")
|
||||
joined := strings.Join(lines, "\n")
|
||||
// Default empty case still emits the boundary and a default Content-Type header.
|
||||
tst.AssertTrue(t, strings.Contains(joined, "--BOUND"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "Content-Type: text/plain; charset=UTF-8"))
|
||||
}
|
||||
|
||||
func TestDumpMailBodyMixedAlternative(t *testing.T) {
|
||||
// HTML+Plain with normal attachments and no inline → uses alternative sub-block.
|
||||
lines := dumpMailBody(MailBody{Plain: "P", HTML: "<p>H</p>"}, false, true, "BOUND", "BOUNDALT")
|
||||
joined := strings.Join(lines, "\n")
|
||||
tst.AssertTrue(t, strings.Contains(joined, "--BOUND"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "Content-Type: multipart/alternative; boundary=BOUNDALT"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "--BOUNDALT"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "P"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "<p>H</p>"))
|
||||
}
|
||||
|
||||
func TestDumpMailBodyMixedInline(t *testing.T) {
|
||||
// HTML+Plain with inline attachments → simplified to single HTML block.
|
||||
lines := dumpMailBody(MailBody{Plain: "P", HTML: "<p>H</p>"}, true, false, "BOUND", "BOUNDALT")
|
||||
tst.AssertEqual(t, len(lines), 2)
|
||||
tst.AssertEqual(t, lines[0], "--BOUND")
|
||||
tst.AssertEqual(t, lines[1], "<p>H</p>")
|
||||
}
|
||||
|
||||
func TestDumpMailBodyBothNoAttachments(t *testing.T) {
|
||||
lines := dumpMailBody(MailBody{Plain: "P", HTML: "<p>H</p>"}, false, false, "BOUND", "BOUNDALT")
|
||||
joined := strings.Join(lines, "\n")
|
||||
tst.AssertTrue(t, strings.Contains(joined, "--BOUND"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "Content-Type: text/plain; charset=UTF-8"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "Content-Type: text/html; charset=UTF-8"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "P"))
|
||||
tst.AssertTrue(t, strings.Contains(joined, "<p>H</p>"))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package googleapi
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewGoogleOAuthReturnsNonNil(t *testing.T) {
|
||||
auth := NewGoogleOAuth("cid", "csecret", "rtok")
|
||||
tst.AssertTrue(t, auth != nil)
|
||||
}
|
||||
|
||||
func TestNewGoogleOAuthFieldsSet(t *testing.T) {
|
||||
auth := NewGoogleOAuth("cid", "csecret", "rtok")
|
||||
c, ok := auth.(*oauth)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, c.clientID, "cid")
|
||||
tst.AssertEqual(t, c.clientSecret, "csecret")
|
||||
tst.AssertEqual(t, c.refreshToken, "rtok")
|
||||
tst.AssertTrue(t, c.accessToken == nil)
|
||||
tst.AssertTrue(t, c.expiryDate == nil)
|
||||
}
|
||||
|
||||
func TestOAuthAccessTokenCachedReturnsStored(t *testing.T) {
|
||||
c := &oauth{
|
||||
clientID: "cid",
|
||||
clientSecret: "csecret",
|
||||
refreshToken: "rtok",
|
||||
}
|
||||
|
||||
tok := "cached-token-value"
|
||||
expiry := time.Now().Add(1 * time.Hour)
|
||||
c.accessToken = &tok
|
||||
c.expiryDate = &expiry
|
||||
|
||||
got, err := c.AccessToken()
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, got, "cached-token-value")
|
||||
}
|
||||
|
||||
func TestOAuthAccessTokenCachedMultipleCalls(t *testing.T) {
|
||||
c := &oauth{
|
||||
clientID: "cid",
|
||||
clientSecret: "csecret",
|
||||
refreshToken: "rtok",
|
||||
}
|
||||
|
||||
tok := "another-token"
|
||||
expiry := time.Now().Add(30 * time.Minute)
|
||||
c.accessToken = &tok
|
||||
c.expiryDate = &expiry
|
||||
|
||||
for range 5 {
|
||||
got, err := c.AccessToken()
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, got, "another-token")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package googleapi
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewGoogleClientReturnsNonNil(t *testing.T) {
|
||||
auth := NewGoogleOAuth("cid", "csecret", "rtok")
|
||||
gc := NewGoogleClient(auth)
|
||||
tst.AssertTrue(t, gc != nil)
|
||||
}
|
||||
|
||||
func TestNewGoogleClientWiresOAuth(t *testing.T) {
|
||||
auth := NewGoogleOAuth("cid", "csecret", "rtok")
|
||||
gc := NewGoogleClient(auth)
|
||||
c, ok := gc.(*client)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertTrue(t, c.oauth == auth)
|
||||
}
|
||||
|
||||
func TestMailBodyZeroValue(t *testing.T) {
|
||||
b := MailBody{}
|
||||
tst.AssertEqual(t, b.Plain, "")
|
||||
tst.AssertEqual(t, b.HTML, "")
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package imageext
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
)
|
||||
|
||||
func TestImageFit_Valid(t *testing.T) {
|
||||
tst.AssertTrue(t, ImageFitStretch.Valid())
|
||||
tst.AssertTrue(t, ImageFitCover.Valid())
|
||||
tst.AssertTrue(t, ImageFitContainCenter.Valid())
|
||||
tst.AssertTrue(t, ImageFitContainTopLeft.Valid())
|
||||
tst.AssertTrue(t, ImageFitContainTopRight.Valid())
|
||||
tst.AssertTrue(t, ImageFitContainBottomLeft.Valid())
|
||||
tst.AssertTrue(t, ImageFitContainBottomRight.Valid())
|
||||
|
||||
tst.AssertFalse(t, ImageFit("UNKNOWN").Valid())
|
||||
tst.AssertFalse(t, ImageFit("").Valid())
|
||||
}
|
||||
|
||||
func TestImageFit_String(t *testing.T) {
|
||||
tst.AssertEqual(t, ImageFitStretch.String(), "STRETCH")
|
||||
tst.AssertEqual(t, ImageFitCover.String(), "COVER")
|
||||
tst.AssertEqual(t, ImageFitContainCenter.String(), "CONTAIN_CENTER")
|
||||
}
|
||||
|
||||
func TestImageFit_VarName(t *testing.T) {
|
||||
tst.AssertEqual(t, ImageFitStretch.VarName(), "ImageFitStretch")
|
||||
tst.AssertEqual(t, ImageFitContainBottomRight.VarName(), "ImageFitContainBottomRight")
|
||||
tst.AssertEqual(t, ImageFit("UNKNOWN").VarName(), "")
|
||||
}
|
||||
|
||||
func TestImageFit_TypeName(t *testing.T) {
|
||||
tst.AssertEqual(t, ImageFitStretch.TypeName(), "ImageFit")
|
||||
}
|
||||
|
||||
func TestImageFit_Values(t *testing.T) {
|
||||
values := ImageFitValues()
|
||||
tst.AssertEqual(t, len(values), 7)
|
||||
tst.AssertEqual(t, values[0], ImageFitStretch)
|
||||
}
|
||||
|
||||
func TestImageFit_ValuesAny(t *testing.T) {
|
||||
tst.AssertEqual(t, len(ImageFitStretch.ValuesAny()), 7)
|
||||
}
|
||||
|
||||
func TestImageFit_ValuesMeta(t *testing.T) {
|
||||
meta := ImageFitValuesMeta()
|
||||
tst.AssertEqual(t, len(meta), 7)
|
||||
tst.AssertEqual(t, meta[0].VarName, "ImageFitStretch")
|
||||
}
|
||||
|
||||
func TestParseImageFit(t *testing.T) {
|
||||
v, ok := ParseImageFit("COVER")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v, ImageFitCover)
|
||||
|
||||
v, ok = ParseImageFit("CONTAIN_TOPLEFT")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v, ImageFitContainTopLeft)
|
||||
|
||||
_, ok = ParseImageFit("BOGUS")
|
||||
tst.AssertFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestImageCompresson_Valid(t *testing.T) {
|
||||
tst.AssertTrue(t, CompressionPNGNone.Valid())
|
||||
tst.AssertTrue(t, CompressionPNGSpeed.Valid())
|
||||
tst.AssertTrue(t, CompressionPNGBest.Valid())
|
||||
tst.AssertTrue(t, CompressionJPEG100.Valid())
|
||||
tst.AssertTrue(t, CompressionJPEG1.Valid())
|
||||
|
||||
tst.AssertFalse(t, ImageCompresson("UNKNOWN").Valid())
|
||||
tst.AssertFalse(t, ImageCompresson("").Valid())
|
||||
}
|
||||
|
||||
func TestImageCompresson_String(t *testing.T) {
|
||||
tst.AssertEqual(t, CompressionPNGNone.String(), "PNG_NONE")
|
||||
tst.AssertEqual(t, CompressionJPEG90.String(), "JPEG_090")
|
||||
}
|
||||
|
||||
func TestImageCompresson_VarName(t *testing.T) {
|
||||
tst.AssertEqual(t, CompressionJPEG50.VarName(), "CompressionJPEG50")
|
||||
tst.AssertEqual(t, ImageCompresson("UNKNOWN").VarName(), "")
|
||||
}
|
||||
|
||||
func TestImageCompresson_TypeName(t *testing.T) {
|
||||
tst.AssertEqual(t, CompressionJPEG50.TypeName(), "ImageCompresson")
|
||||
}
|
||||
|
||||
func TestImageCompresson_Values(t *testing.T) {
|
||||
values := ImageCompressonValues()
|
||||
tst.AssertEqual(t, len(values), 12)
|
||||
}
|
||||
|
||||
func TestImageCompresson_ValuesAny(t *testing.T) {
|
||||
tst.AssertEqual(t, len(CompressionPNGBest.ValuesAny()), 12)
|
||||
}
|
||||
|
||||
func TestImageCompresson_ValuesMeta(t *testing.T) {
|
||||
meta := ImageCompressonValuesMeta()
|
||||
tst.AssertEqual(t, len(meta), 12)
|
||||
}
|
||||
|
||||
func TestParseImageCompresson(t *testing.T) {
|
||||
v, ok := ParseImageCompresson("PNG_BEST")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v, CompressionPNGBest)
|
||||
|
||||
v, ok = ParseImageCompresson("JPEG_080")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v, CompressionJPEG80)
|
||||
|
||||
_, ok = ParseImageCompresson("BOGUS")
|
||||
tst.AssertFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestAllPackageEnums(t *testing.T) {
|
||||
enums := AllPackageEnums()
|
||||
tst.AssertEqual(t, len(enums), 2)
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package imageext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
)
|
||||
|
||||
// makeRGBA creates a solid-color RGBA image of the given size.
|
||||
func makeRGBA(w, h int, c color.Color) *image.RGBA {
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
img.Set(x, y, c)
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
// makeGradient creates an RGBA image with a gradient pattern.
|
||||
// Useful for codecs that may behave oddly with uniform colors.
|
||||
func makeGradient(w, h int) *image.RGBA {
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
img.Set(x, y, color.RGBA{
|
||||
R: uint8((x * 255) / max1(w-1)),
|
||||
G: uint8((y * 255) / max1(h-1)),
|
||||
B: uint8(((x + y) * 255) / max1(w+h-2)),
|
||||
A: 255,
|
||||
})
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func max1(v int) int {
|
||||
if v <= 0 {
|
||||
return 1
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func TestCropImage_HalfRegion(t *testing.T) {
|
||||
src := makeGradient(100, 80)
|
||||
|
||||
out, err := CropImage(src, 0.0, 0.0, 0.5, 0.5)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 50)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 40)
|
||||
}
|
||||
|
||||
func TestCropImage_Offset(t *testing.T) {
|
||||
src := makeGradient(200, 100)
|
||||
|
||||
out, err := CropImage(src, 0.25, 0.5, 0.5, 0.5)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
// SubImage preserves coordinates of the parent image.
|
||||
tst.AssertEqual(t, out.Bounds().Min.X, 50)
|
||||
tst.AssertEqual(t, out.Bounds().Min.Y, 50)
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 100)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 50)
|
||||
}
|
||||
|
||||
func TestCropImage_Full(t *testing.T) {
|
||||
src := makeGradient(40, 40)
|
||||
|
||||
out, err := CropImage(src, 0.0, 0.0, 1.0, 1.0)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 40)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 40)
|
||||
}
|
||||
|
||||
func TestEncodeImage_AllCompressions(t *testing.T) {
|
||||
src := makeGradient(20, 16)
|
||||
|
||||
cases := []struct {
|
||||
comp ImageCompresson
|
||||
mime string
|
||||
signature []byte
|
||||
}{
|
||||
{CompressionPNGNone, "image/png", []byte{0x89, 'P', 'N', 'G'}},
|
||||
{CompressionPNGSpeed, "image/png", []byte{0x89, 'P', 'N', 'G'}},
|
||||
{CompressionPNGBest, "image/png", []byte{0x89, 'P', 'N', 'G'}},
|
||||
{CompressionJPEG100, "image/jpeg", []byte{0xFF, 0xD8, 0xFF}},
|
||||
{CompressionJPEG90, "image/jpeg", []byte{0xFF, 0xD8, 0xFF}},
|
||||
{CompressionJPEG80, "image/jpeg", []byte{0xFF, 0xD8, 0xFF}},
|
||||
{CompressionJPEG70, "image/jpeg", []byte{0xFF, 0xD8, 0xFF}},
|
||||
{CompressionJPEG60, "image/jpeg", []byte{0xFF, 0xD8, 0xFF}},
|
||||
{CompressionJPEG50, "image/jpeg", []byte{0xFF, 0xD8, 0xFF}},
|
||||
{CompressionJPEG25, "image/jpeg", []byte{0xFF, 0xD8, 0xFF}},
|
||||
{CompressionJPEG10, "image/jpeg", []byte{0xFF, 0xD8, 0xFF}},
|
||||
{CompressionJPEG1, "image/jpeg", []byte{0xFF, 0xD8, 0xFF}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(string(c.comp), func(t *testing.T) {
|
||||
buf, mime, err := EncodeImage(src, c.comp)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, mime, c.mime)
|
||||
tst.AssertTrue(t, buf.Len() > 0)
|
||||
|
||||
data := buf.Bytes()
|
||||
tst.AssertTrue(t, len(data) >= len(c.signature))
|
||||
tst.AssertTrue(t, bytes.Equal(data[:len(c.signature)], c.signature))
|
||||
|
||||
// Round-trip decode to confirm the bytes form a valid image.
|
||||
var dec image.Image
|
||||
var derr error
|
||||
if c.mime == "image/png" {
|
||||
dec, derr = png.Decode(bytes.NewReader(data))
|
||||
} else {
|
||||
dec, derr = jpeg.Decode(bytes.NewReader(data))
|
||||
}
|
||||
tst.AssertNoErr(t, derr)
|
||||
tst.AssertEqual(t, dec.Bounds().Dx(), 20)
|
||||
tst.AssertEqual(t, dec.Bounds().Dy(), 16)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeImage_UnknownCompression(t *testing.T) {
|
||||
src := makeRGBA(4, 4, color.White)
|
||||
|
||||
buf, mime, err := EncodeImage(src, ImageCompresson("UNKNOWN"))
|
||||
tst.AssertTrue(t, err != nil)
|
||||
tst.AssertEqual(t, mime, "")
|
||||
tst.AssertEqual(t, buf.Len(), 0)
|
||||
}
|
||||
|
||||
func TestObjectFitImage_Cover_SmallerThanBB(t *testing.T) {
|
||||
// Image (100x100) is smaller than the BB (200x100), so the output is
|
||||
// scaled down to the smaller-axis factor of 0.5: 100x50.
|
||||
src := makeGradient(100, 100)
|
||||
|
||||
out, rect, err := ObjectFitImage(src, 200, 100, ImageFitCover, color.Transparent)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 100)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 50)
|
||||
tst.AssertDeepEqual(t, rect, PercentageRectangle{0, 0, 1, 1})
|
||||
}
|
||||
|
||||
func TestObjectFitImage_Cover_LargerThanBB(t *testing.T) {
|
||||
// Image (400x200) is larger than the BB (200x100); fac is capped at 1, so
|
||||
// the output is exactly the BB size.
|
||||
src := makeGradient(400, 200)
|
||||
|
||||
out, _, err := ObjectFitImage(src, 200, 100, ImageFitCover, color.Transparent)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 200)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 100)
|
||||
}
|
||||
|
||||
func TestObjectFitImage_ContainCenter(t *testing.T) {
|
||||
// Image 100x100 in a BB of 200x100 -> output 200x100, drawn rect 100x100 centered.
|
||||
src := makeGradient(100, 100)
|
||||
|
||||
out, rect, err := ObjectFitImage(src, 200, 100, ImageFitContainCenter, color.Black)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 200)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 100)
|
||||
|
||||
// (200-100)/2 = 50 -> X=50/200 = 0.25, W=100/200=0.5, Y=0, H=1
|
||||
tst.AssertDeepEqual(t, rect, PercentageRectangle{X: 0.25, Y: 0, W: 0.5, H: 1})
|
||||
}
|
||||
|
||||
func TestObjectFitImage_ContainTopLeft(t *testing.T) {
|
||||
src := makeGradient(100, 100)
|
||||
|
||||
out, rect, err := ObjectFitImage(src, 200, 100, ImageFitContainTopLeft, color.Black)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 200)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 100)
|
||||
tst.AssertDeepEqual(t, rect, PercentageRectangle{X: 0, Y: 0, W: 0.5, H: 1})
|
||||
}
|
||||
|
||||
func TestObjectFitImage_ContainTopRight(t *testing.T) {
|
||||
src := makeGradient(100, 100)
|
||||
|
||||
out, rect, err := ObjectFitImage(src, 200, 100, ImageFitContainTopRight, color.Black)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 200)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 100)
|
||||
tst.AssertDeepEqual(t, rect, PercentageRectangle{X: 0.5, Y: 0, W: 0.5, H: 1})
|
||||
}
|
||||
|
||||
func TestObjectFitImage_ContainBottomLeft(t *testing.T) {
|
||||
// Image 200x100 in a BB of 100x100 (image is bigger so facOut is capped at 1)
|
||||
src := makeGradient(200, 100)
|
||||
|
||||
out, rect, err := ObjectFitImage(src, 100, 100, ImageFitContainBottomLeft, color.Black)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 100)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 100)
|
||||
// dw=100, dh=50 -> bottom-left rect: (0, 50, 100, 100) -> Y=0.5, H=0.5
|
||||
tst.AssertDeepEqual(t, rect, PercentageRectangle{X: 0, Y: 0.5, W: 1, H: 0.5})
|
||||
}
|
||||
|
||||
func TestObjectFitImage_ContainBottomRight(t *testing.T) {
|
||||
src := makeGradient(200, 100)
|
||||
|
||||
out, rect, err := ObjectFitImage(src, 100, 100, ImageFitContainBottomRight, color.Black)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 100)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 100)
|
||||
tst.AssertDeepEqual(t, rect, PercentageRectangle{X: 0, Y: 0.5, W: 1, H: 0.5})
|
||||
}
|
||||
|
||||
func TestObjectFitImage_Stretch(t *testing.T) {
|
||||
// Image 100x100 in BB 200x100 -> uses max(facW=0.5, facH=1.0) capped at 1, so result is 200x100.
|
||||
src := makeGradient(100, 100)
|
||||
|
||||
out, rect, err := ObjectFitImage(src, 200, 100, ImageFitStretch, color.Black)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 200)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 100)
|
||||
tst.AssertDeepEqual(t, rect, PercentageRectangle{0, 0, 1, 1})
|
||||
}
|
||||
|
||||
func TestObjectFitImage_Stretch_SmallImage(t *testing.T) {
|
||||
// Image 50x25 in BB 200x100 -> max(0.25, 0.25) = 0.25, output 50x25.
|
||||
src := makeGradient(50, 25)
|
||||
|
||||
out, _, err := ObjectFitImage(src, 200, 100, ImageFitStretch, color.Black)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 50)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 25)
|
||||
}
|
||||
|
||||
func TestObjectFitImage_UnknownFit(t *testing.T) {
|
||||
src := makeGradient(20, 20)
|
||||
|
||||
out, _, err := ObjectFitImage(src, 100, 100, ImageFit("BOGUS"), color.Black)
|
||||
tst.AssertTrue(t, err != nil)
|
||||
if out != nil {
|
||||
t.Errorf("expected nil image on error, got %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyAndDecodeImage_PNG(t *testing.T) {
|
||||
src := makeGradient(12, 8)
|
||||
buf := bytes.Buffer{}
|
||||
tst.AssertNoErr(t, png.Encode(&buf, src))
|
||||
|
||||
out, err := VerifyAndDecodeImage(&buf, "image/png")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 12)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 8)
|
||||
}
|
||||
|
||||
func TestVerifyAndDecodeImage_JPEG(t *testing.T) {
|
||||
src := makeGradient(16, 16)
|
||||
buf := bytes.Buffer{}
|
||||
tst.AssertNoErr(t, jpeg.Encode(&buf, src, &jpeg.Options{Quality: 90}))
|
||||
|
||||
out, err := VerifyAndDecodeImage(&buf, "image/jpeg")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, out.Bounds().Dx(), 16)
|
||||
tst.AssertEqual(t, out.Bounds().Dy(), 16)
|
||||
}
|
||||
|
||||
func TestVerifyAndDecodeImage_UnknownMime(t *testing.T) {
|
||||
out, err := VerifyAndDecodeImage(strings.NewReader("whatever"), "image/gif")
|
||||
tst.AssertTrue(t, err != nil)
|
||||
if out != nil {
|
||||
t.Errorf("expected nil image on error, got %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyAndDecodeImage_BadPNG(t *testing.T) {
|
||||
out, err := VerifyAndDecodeImage(strings.NewReader("not a png"), "image/png")
|
||||
tst.AssertTrue(t, err != nil)
|
||||
if out != nil {
|
||||
t.Errorf("expected nil image on error, got %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyAndDecodeImage_BadJPEG(t *testing.T) {
|
||||
out, err := VerifyAndDecodeImage(strings.NewReader("not a jpeg"), "image/jpeg")
|
||||
tst.AssertTrue(t, err != nil)
|
||||
if out != nil {
|
||||
t.Errorf("expected nil image on error, got %v", out)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package imageext
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
)
|
||||
|
||||
func TestPercentageRectangle_Of_FullRef(t *testing.T) {
|
||||
r := PercentageRectangle{X: 0.25, Y: 0.5, W: 0.5, H: 0.25}
|
||||
ref := Rectangle{X: 0, Y: 0, W: 100, H: 200}
|
||||
|
||||
got := r.Of(ref)
|
||||
tst.AssertDeepEqual(t, got, Rectangle{X: 25, Y: 100, W: 50, H: 50})
|
||||
}
|
||||
|
||||
func TestPercentageRectangle_Of_OffsetRef(t *testing.T) {
|
||||
r := PercentageRectangle{X: 0.5, Y: 0.5, W: 0.5, H: 0.5}
|
||||
ref := Rectangle{X: 10, Y: 20, W: 100, H: 100}
|
||||
|
||||
got := r.Of(ref)
|
||||
tst.AssertDeepEqual(t, got, Rectangle{X: 60, Y: 70, W: 50, H: 50})
|
||||
}
|
||||
|
||||
func TestPercentageRectangle_Of_Identity(t *testing.T) {
|
||||
r := PercentageRectangle{X: 0, Y: 0, W: 1, H: 1}
|
||||
ref := Rectangle{X: 5, Y: 6, W: 7, H: 8}
|
||||
|
||||
got := r.Of(ref)
|
||||
tst.AssertDeepEqual(t, got, ref)
|
||||
}
|
||||
|
||||
func TestCalcRelativeRect_FullInner(t *testing.T) {
|
||||
inner := image.Rect(0, 0, 100, 100)
|
||||
outer := image.Rect(0, 0, 100, 100)
|
||||
|
||||
got := calcRelativeRect(inner, outer)
|
||||
tst.AssertDeepEqual(t, got, PercentageRectangle{X: 0, Y: 0, W: 1, H: 1})
|
||||
}
|
||||
|
||||
func TestCalcRelativeRect_Centered(t *testing.T) {
|
||||
inner := image.Rect(50, 25, 150, 75)
|
||||
outer := image.Rect(0, 0, 200, 100)
|
||||
|
||||
got := calcRelativeRect(inner, outer)
|
||||
tst.AssertDeepEqual(t, got, PercentageRectangle{X: 0.25, Y: 0.25, W: 0.5, H: 0.5})
|
||||
}
|
||||
|
||||
func TestCalcRelativeRect_OffsetOuter(t *testing.T) {
|
||||
inner := image.Rect(120, 60, 220, 110)
|
||||
outer := image.Rect(100, 50, 300, 150)
|
||||
|
||||
got := calcRelativeRect(inner, outer)
|
||||
tst.AssertDeepEqual(t, got, PercentageRectangle{X: 0.1, Y: 0.1, W: 0.5, H: 0.5})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodeBase62Zero(t *testing.T) {
|
||||
tst.AssertEqual(t, EncodeBase62(0), "0")
|
||||
}
|
||||
|
||||
func TestEncodeBase62Small(t *testing.T) {
|
||||
tst.AssertEqual(t, EncodeBase62(1), "1")
|
||||
tst.AssertEqual(t, EncodeBase62(9), "9")
|
||||
tst.AssertEqual(t, EncodeBase62(10), "A")
|
||||
tst.AssertEqual(t, EncodeBase62(35), "Z")
|
||||
tst.AssertEqual(t, EncodeBase62(36), "a")
|
||||
tst.AssertEqual(t, EncodeBase62(61), "z")
|
||||
tst.AssertEqual(t, EncodeBase62(62), "10")
|
||||
}
|
||||
|
||||
func TestDecodeBase62Empty(t *testing.T) {
|
||||
_, err := DecodeBase62("")
|
||||
if err == nil {
|
||||
t.Errorf("expected error on empty input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeBase62Invalid(t *testing.T) {
|
||||
_, err := DecodeBase62("foo!bar")
|
||||
if err == nil {
|
||||
t.Errorf("expected error on invalid character")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeBase62Basic(t *testing.T) {
|
||||
v, err := DecodeBase62("0")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, v, uint64(0))
|
||||
|
||||
v, err = DecodeBase62("10")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, v, uint64(62))
|
||||
}
|
||||
|
||||
func TestEncodeDecodeBase62RoundTrip(t *testing.T) {
|
||||
for _, n := range []uint64{0, 1, 61, 62, 100, 12345, 1<<32 - 1, 1 << 40} {
|
||||
s := EncodeBase62(n)
|
||||
v, err := DecodeBase62(s)
|
||||
if err != nil {
|
||||
t.Errorf("decode error for %d (encoded %q): %v", n, s, err)
|
||||
continue
|
||||
}
|
||||
tst.AssertEqual(t, v, n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandBase62Length(t *testing.T) {
|
||||
for _, l := range []int{0, 1, 8, 32, 64} {
|
||||
s := RandBase62(l)
|
||||
tst.AssertEqual(t, len(s), l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandBase62Alphabet(t *testing.T) {
|
||||
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
s := RandBase62(256)
|
||||
for _, r := range s {
|
||||
if !strings.ContainsRune(alphabet, r) {
|
||||
t.Errorf("character %q not in base62 alphabet", string(r))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandBase62Distinct(t *testing.T) {
|
||||
a := RandBase62(32)
|
||||
b := RandBase62(32)
|
||||
if a == b {
|
||||
t.Errorf("two base62 random strings of length 32 should not be equal")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatBoolTrue(t *testing.T) {
|
||||
tst.AssertEqual(t, FormatBool(true, "yes", "no"), "yes")
|
||||
}
|
||||
|
||||
func TestFormatBoolFalse(t *testing.T) {
|
||||
tst.AssertEqual(t, FormatBool(false, "yes", "no"), "no")
|
||||
}
|
||||
|
||||
func TestConditional(t *testing.T) {
|
||||
tst.AssertEqual(t, Conditional(true, 1, 2), 1)
|
||||
tst.AssertEqual(t, Conditional(false, 1, 2), 2)
|
||||
tst.AssertEqual(t, Conditional(true, "a", "b"), "a")
|
||||
tst.AssertEqual(t, Conditional(false, "a", "b"), "b")
|
||||
}
|
||||
|
||||
func TestConditionalFn00(t *testing.T) {
|
||||
tst.AssertEqual(t, ConditionalFn00(true, 10, 20), 10)
|
||||
tst.AssertEqual(t, ConditionalFn00(false, 10, 20), 20)
|
||||
}
|
||||
|
||||
func TestConditionalFn10Lazy(t *testing.T) {
|
||||
called := false
|
||||
v := ConditionalFn10(false, func() int {
|
||||
called = true
|
||||
return 1
|
||||
}, 99)
|
||||
tst.AssertEqual(t, v, 99)
|
||||
tst.AssertEqual(t, called, false)
|
||||
|
||||
v = ConditionalFn10(true, func() int {
|
||||
called = true
|
||||
return 1
|
||||
}, 99)
|
||||
tst.AssertEqual(t, v, 1)
|
||||
tst.AssertEqual(t, called, true)
|
||||
}
|
||||
|
||||
func TestConditionalFn01Lazy(t *testing.T) {
|
||||
called := false
|
||||
v := ConditionalFn01(true, 1, func() int {
|
||||
called = true
|
||||
return 99
|
||||
})
|
||||
tst.AssertEqual(t, v, 1)
|
||||
tst.AssertEqual(t, called, false)
|
||||
|
||||
v = ConditionalFn01(false, 1, func() int {
|
||||
called = true
|
||||
return 99
|
||||
})
|
||||
tst.AssertEqual(t, v, 99)
|
||||
tst.AssertEqual(t, called, true)
|
||||
}
|
||||
|
||||
func TestConditionalFn11Lazy(t *testing.T) {
|
||||
calledT := false
|
||||
calledF := false
|
||||
|
||||
v := ConditionalFn11(true,
|
||||
func() int { calledT = true; return 1 },
|
||||
func() int { calledF = true; return 2 },
|
||||
)
|
||||
tst.AssertEqual(t, v, 1)
|
||||
tst.AssertEqual(t, calledT, true)
|
||||
tst.AssertEqual(t, calledF, false)
|
||||
|
||||
calledT = false
|
||||
calledF = false
|
||||
|
||||
v = ConditionalFn11(false,
|
||||
func() int { calledT = true; return 1 },
|
||||
func() int { calledF = true; return 2 },
|
||||
)
|
||||
tst.AssertEqual(t, v, 2)
|
||||
tst.AssertEqual(t, calledT, false)
|
||||
tst.AssertEqual(t, calledF, true)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatBytesToSI(t *testing.T) {
|
||||
tst.AssertEqual(t, FormatBytesToSI(0), "0 B")
|
||||
tst.AssertEqual(t, FormatBytesToSI(999), "999 B")
|
||||
tst.AssertEqual(t, FormatBytesToSI(1000), "1.0 kB")
|
||||
tst.AssertEqual(t, FormatBytesToSI(1500), "1.5 kB")
|
||||
tst.AssertEqual(t, FormatBytesToSI(1000*1000), "1.0 MB")
|
||||
tst.AssertEqual(t, FormatBytesToSI(1000*1000*1000), "1.0 GB")
|
||||
}
|
||||
|
||||
func TestFormatBytes(t *testing.T) {
|
||||
tst.AssertEqual(t, FormatBytes(0), "0 B")
|
||||
tst.AssertEqual(t, FormatBytes(1023), "1023 B")
|
||||
tst.AssertEqual(t, FormatBytes(1024), "1.0 KiB")
|
||||
tst.AssertEqual(t, FormatBytes(1024*1024), "1.0 MiB")
|
||||
tst.AssertEqual(t, FormatBytes(1024*1024*1024), "1.0 GiB")
|
||||
tst.AssertEqual(t, FormatBytes(1536), "1.5 KiB")
|
||||
}
|
||||
|
||||
func TestBytesXOR(t *testing.T) {
|
||||
a := []byte{0x01, 0x02, 0x03, 0xFF}
|
||||
b := []byte{0xFF, 0xFE, 0xFD, 0x00}
|
||||
|
||||
r, err := BytesXOR(a, b)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expected := []byte{0xFE, 0xFC, 0xFE, 0xFF}
|
||||
tst.AssertArrayEqual(t, r, expected)
|
||||
}
|
||||
|
||||
func TestBytesXORLengthMismatch(t *testing.T) {
|
||||
a := []byte{0x01, 0x02}
|
||||
b := []byte{0x01, 0x02, 0x03}
|
||||
|
||||
_, err := BytesXOR(a, b)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error on length mismatch, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytesXOREmpty(t *testing.T) {
|
||||
r, err := BytesXOR([]byte{}, []byte{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, len(r), 0)
|
||||
}
|
||||
|
||||
func TestBytesXORSelfIsZero(t *testing.T) {
|
||||
a := []byte{0xAB, 0xCD, 0xEF}
|
||||
r, err := BytesXOR(a, a)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
for i, v := range r {
|
||||
if v != 0 {
|
||||
t.Errorf("expected zero at index %d, got %#x", i, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type stringerImpl struct{ v string }
|
||||
|
||||
func (s stringerImpl) String() string { return s.v }
|
||||
|
||||
func TestCoalesceWithValue(t *testing.T) {
|
||||
v := 42
|
||||
tst.AssertEqual(t, Coalesce(&v, 0), 42)
|
||||
}
|
||||
|
||||
func TestCoalesceWithNil(t *testing.T) {
|
||||
var p *int
|
||||
tst.AssertEqual(t, Coalesce(p, 99), 99)
|
||||
}
|
||||
|
||||
func TestCoalesceOpt(t *testing.T) {
|
||||
v := 1
|
||||
w := 2
|
||||
tst.AssertDeRefEqual(t, CoalesceOpt(&v, &w), 1)
|
||||
|
||||
var p *int
|
||||
tst.AssertDeRefEqual(t, CoalesceOpt(p, &w), 2)
|
||||
|
||||
tst.AssertPtrEqual(t, CoalesceOpt[int](nil, nil), nil)
|
||||
}
|
||||
|
||||
func TestCoalesce3(t *testing.T) {
|
||||
v := 1
|
||||
w := 2
|
||||
tst.AssertEqual(t, Coalesce3(&v, &w, 99), 1)
|
||||
tst.AssertEqual(t, Coalesce3[int](nil, &w, 99), 2)
|
||||
tst.AssertEqual(t, Coalesce3[int](nil, nil, 99), 99)
|
||||
}
|
||||
|
||||
func TestCoalesce3Opt(t *testing.T) {
|
||||
v := 1
|
||||
tst.AssertDeRefEqual(t, Coalesce3Opt[int](nil, nil, &v), 1)
|
||||
tst.AssertPtrEqual(t, Coalesce3Opt[int](nil, nil, nil), nil)
|
||||
}
|
||||
|
||||
func TestCoalesce4(t *testing.T) {
|
||||
v := 1
|
||||
w := 2
|
||||
x := 3
|
||||
tst.AssertEqual(t, Coalesce4(&v, &w, &x, 99), 1)
|
||||
tst.AssertEqual(t, Coalesce4[int](nil, &w, &x, 99), 2)
|
||||
tst.AssertEqual(t, Coalesce4[int](nil, nil, &x, 99), 3)
|
||||
tst.AssertEqual(t, Coalesce4[int](nil, nil, nil, 99), 99)
|
||||
}
|
||||
|
||||
func TestCoalesce4Opt(t *testing.T) {
|
||||
v := 4
|
||||
tst.AssertDeRefEqual(t, Coalesce4Opt[int](nil, nil, nil, &v), 4)
|
||||
tst.AssertPtrEqual(t, Coalesce4Opt[int](nil, nil, nil, nil), nil)
|
||||
}
|
||||
|
||||
func TestCoalesceString(t *testing.T) {
|
||||
s := "hello"
|
||||
tst.AssertEqual(t, CoalesceString(&s, "def"), "hello")
|
||||
tst.AssertEqual(t, CoalesceString(nil, "def"), "def")
|
||||
}
|
||||
|
||||
func TestCoalesceInt(t *testing.T) {
|
||||
v := 5
|
||||
tst.AssertEqual(t, CoalesceInt(&v, 99), 5)
|
||||
tst.AssertEqual(t, CoalesceInt(nil, 99), 99)
|
||||
}
|
||||
|
||||
func TestCoalesceInt32(t *testing.T) {
|
||||
v := int32(7)
|
||||
tst.AssertEqual(t, CoalesceInt32(&v, 99), int32(7))
|
||||
tst.AssertEqual(t, CoalesceInt32(nil, 99), int32(99))
|
||||
}
|
||||
|
||||
func TestCoalesceBool(t *testing.T) {
|
||||
v := true
|
||||
tst.AssertEqual(t, CoalesceBool(&v, false), true)
|
||||
tst.AssertEqual(t, CoalesceBool(nil, true), true)
|
||||
tst.AssertEqual(t, CoalesceBool(nil, false), false)
|
||||
}
|
||||
|
||||
func TestCoalesceTime(t *testing.T) {
|
||||
now := time.Now()
|
||||
def := time.Unix(0, 0)
|
||||
tst.AssertEqual(t, CoalesceTime(&now, def), now)
|
||||
tst.AssertEqual(t, CoalesceTime(nil, def), def)
|
||||
}
|
||||
|
||||
func TestCoalesceStringer(t *testing.T) {
|
||||
s := stringerImpl{v: "hi"}
|
||||
tst.AssertEqual(t, CoalesceStringer(s, "def"), "hi")
|
||||
|
||||
var nilStringer *stringerImpl
|
||||
tst.AssertEqual(t, CoalesceStringer(nilStringer, "def"), "def")
|
||||
}
|
||||
|
||||
func TestCoalesceDefault(t *testing.T) {
|
||||
tst.AssertEqual(t, CoalesceDefault(0, 99), 99)
|
||||
tst.AssertEqual(t, CoalesceDefault(5, 99), 5)
|
||||
tst.AssertEqual(t, CoalesceDefault("", "def"), "def")
|
||||
tst.AssertEqual(t, CoalesceDefault("v", "def"), "v")
|
||||
}
|
||||
|
||||
func TestSafeCastMatching(t *testing.T) {
|
||||
var v any = "hello"
|
||||
tst.AssertEqual(t, SafeCast(v, "default"), "hello")
|
||||
}
|
||||
|
||||
func TestSafeCastMismatch(t *testing.T) {
|
||||
var v any = 42
|
||||
tst.AssertEqual(t, SafeCast(v, "default"), "default")
|
||||
}
|
||||
|
||||
func TestSafeCastNil(t *testing.T) {
|
||||
tst.AssertEqual(t, SafeCast(nil, 99), 99)
|
||||
}
|
||||
|
||||
func TestCoalesceDblPtrWithValue(t *testing.T) {
|
||||
v := 1
|
||||
pv := &v
|
||||
tst.AssertDeRefEqual(t, CoalesceDblPtr(&pv, nil), 1)
|
||||
}
|
||||
|
||||
func TestCoalesceDblPtrFallback(t *testing.T) {
|
||||
w := 2
|
||||
tst.AssertDeRefEqual(t, CoalesceDblPtr[int](nil, &w), 2)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompareIntArrLess(t *testing.T) {
|
||||
tst.AssertEqual(t, CompareIntArr([]int{1, 2, 3}, []int{1, 2, 4}), true)
|
||||
tst.AssertEqual(t, CompareIntArr([]int{0}, []int{1}), true)
|
||||
}
|
||||
|
||||
func TestCompareIntArrGreater(t *testing.T) {
|
||||
tst.AssertEqual(t, CompareIntArr([]int{1, 2, 5}, []int{1, 2, 4}), false)
|
||||
tst.AssertEqual(t, CompareIntArr([]int{2}, []int{1}), false)
|
||||
}
|
||||
|
||||
func TestCompareIntArrEqual(t *testing.T) {
|
||||
tst.AssertEqual(t, CompareIntArr([]int{1, 2, 3}, []int{1, 2, 3}), false)
|
||||
tst.AssertEqual(t, CompareIntArr([]int{}, []int{}), false)
|
||||
}
|
||||
|
||||
func TestCompareArrLess(t *testing.T) {
|
||||
tst.AssertEqual(t, CompareArr([]int{1, 2, 3}, []int{1, 2, 4}), -1)
|
||||
}
|
||||
|
||||
func TestCompareArrGreater(t *testing.T) {
|
||||
r := CompareArr([]int{1, 2, 5}, []int{1, 2, 4})
|
||||
if r <= 0 {
|
||||
t.Errorf("expected positive, got %d", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareArrEqual(t *testing.T) {
|
||||
tst.AssertEqual(t, CompareArr([]int{1, 2, 3}, []int{1, 2, 3}), 0)
|
||||
tst.AssertEqual(t, CompareArr([]int{}, []int{}), 0)
|
||||
}
|
||||
|
||||
func TestCompareString(t *testing.T) {
|
||||
tst.AssertEqual(t, CompareString("a", "b"), -1)
|
||||
tst.AssertEqual(t, CompareString("b", "a"), 1)
|
||||
tst.AssertEqual(t, CompareString("a", "a"), 0)
|
||||
}
|
||||
|
||||
func TestCompareInt(t *testing.T) {
|
||||
tst.AssertEqual(t, CompareInt(1, 2), -1)
|
||||
tst.AssertEqual(t, CompareInt(2, 1), 1)
|
||||
tst.AssertEqual(t, CompareInt(2, 2), 0)
|
||||
}
|
||||
|
||||
func TestCompareInt64(t *testing.T) {
|
||||
tst.AssertEqual(t, CompareInt64(int64(1), int64(2)), -1)
|
||||
tst.AssertEqual(t, CompareInt64(int64(2), int64(1)), 1)
|
||||
tst.AssertEqual(t, CompareInt64(int64(0), int64(0)), 0)
|
||||
}
|
||||
|
||||
func TestCompareGeneric(t *testing.T) {
|
||||
tst.AssertEqual(t, Compare(1, 2), -1)
|
||||
tst.AssertEqual(t, Compare(2, 1), 1)
|
||||
tst.AssertEqual(t, Compare(2, 2), 0)
|
||||
tst.AssertEqual(t, Compare("x", "y"), -1)
|
||||
tst.AssertEqual(t, Compare(3.5, 1.2), 1)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func floatEquals(a, b, eps float64) bool {
|
||||
return math.Abs(a-b) < eps
|
||||
}
|
||||
|
||||
func TestDegToRadZero(t *testing.T) {
|
||||
if !floatEquals(DegToRad(0), 0, 1e-9) {
|
||||
t.Errorf("expected 0, got %v", DegToRad(0))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDegToRad180(t *testing.T) {
|
||||
if !floatEquals(DegToRad(180), math.Pi, 1e-9) {
|
||||
t.Errorf("expected Pi, got %v", DegToRad(180))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDegToRad90(t *testing.T) {
|
||||
if !floatEquals(DegToRad(90), math.Pi/2, 1e-9) {
|
||||
t.Errorf("expected Pi/2, got %v", DegToRad(90))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRadToDegZero(t *testing.T) {
|
||||
// note: function is implemented as rad / (Pi*180), tests document actual behavior
|
||||
if !floatEquals(RadToDeg(0), 0, 1e-9) {
|
||||
t.Errorf("expected 0, got %v", RadToDeg(0))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeoDistanceSamePoint(t *testing.T) {
|
||||
d := GeoDistance(10.0, 50.0, 10.0, 50.0)
|
||||
if !floatEquals(d, 0, 1e-3) {
|
||||
t.Errorf("expected 0, got %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeoDistancePositive(t *testing.T) {
|
||||
// Berlin (~52.5200, 13.4050) to Munich (~48.1351, 11.5820)
|
||||
d := GeoDistance(13.4050, 52.5200, 11.5820, 48.1351)
|
||||
// Distance should be around ~500km - just check the order of magnitude.
|
||||
if d < 400000 || d > 700000 {
|
||||
t.Errorf("Berlin-Munich distance unexpected: got %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeoDistanceSymmetric(t *testing.T) {
|
||||
d1 := GeoDistance(10.0, 50.0, 11.0, 51.0)
|
||||
d2 := GeoDistance(11.0, 51.0, 10.0, 50.0)
|
||||
if !floatEquals(d1, d2, 1e-3) {
|
||||
t.Errorf("expected symmetry, got %v != %v", d1, d2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFuncChain(t *testing.T) {
|
||||
addOne := func(v int) int { return v + 1 }
|
||||
timesTwo := func(v int) int { return v * 2 }
|
||||
|
||||
chained := FuncChain(addOne, timesTwo)
|
||||
tst.AssertEqual(t, chained(3), 8)
|
||||
}
|
||||
|
||||
func TestFuncChainOrder(t *testing.T) {
|
||||
first := func(v string) string { return v + "A" }
|
||||
second := func(v string) string { return v + "B" }
|
||||
|
||||
chained := FuncChain(first, second)
|
||||
tst.AssertEqual(t, chained("X"), "XAB")
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteNopCloserWrite(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
wc := WriteNopCloser(&buf)
|
||||
|
||||
n, err := wc.Write([]byte("hello"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, n, 5)
|
||||
tst.AssertEqual(t, buf.String(), "hello")
|
||||
}
|
||||
|
||||
func TestWriteNopCloserClose(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
wc := WriteNopCloser(&buf)
|
||||
|
||||
err := wc.Close()
|
||||
if err != nil {
|
||||
t.Errorf("expected nil error from no-op Close, got %v", err)
|
||||
}
|
||||
|
||||
// Can still write after close (it's a no-op)
|
||||
_, err = wc.Write([]byte("after"))
|
||||
if err != nil {
|
||||
t.Errorf("expected to write after Close, got %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, buf.String(), "after")
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIterSingleValueSeq(t *testing.T) {
|
||||
seq := IterSingleValueSeq(42)
|
||||
|
||||
count := 0
|
||||
var got int
|
||||
for v := range seq {
|
||||
got = v
|
||||
count++
|
||||
}
|
||||
|
||||
tst.AssertEqual(t, count, 1)
|
||||
tst.AssertEqual(t, got, 42)
|
||||
}
|
||||
|
||||
func TestIterSingleValueSeqString(t *testing.T) {
|
||||
seq := IterSingleValueSeq("hello")
|
||||
|
||||
values := make([]string, 0)
|
||||
for v := range seq {
|
||||
values = append(values, v)
|
||||
}
|
||||
|
||||
tst.AssertEqual(t, len(values), 1)
|
||||
tst.AssertEqual(t, values[0], "hello")
|
||||
}
|
||||
|
||||
func TestIterSingleValueSeq2(t *testing.T) {
|
||||
seq := IterSingleValueSeq2("key", 42)
|
||||
|
||||
count := 0
|
||||
var k string
|
||||
var v int
|
||||
for kk, vv := range seq {
|
||||
k = kk
|
||||
v = vv
|
||||
count++
|
||||
}
|
||||
|
||||
tst.AssertEqual(t, count, 1)
|
||||
tst.AssertEqual(t, k, "key")
|
||||
tst.AssertEqual(t, v, 42)
|
||||
}
|
||||
|
||||
func TestIterSingleValueSeqEarlyBreak(t *testing.T) {
|
||||
seq := IterSingleValueSeq(1)
|
||||
|
||||
count := 0
|
||||
for range seq {
|
||||
count++
|
||||
break
|
||||
}
|
||||
|
||||
tst.AssertEqual(t, count, 1)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTryPrettyPrintJsonValid(t *testing.T) {
|
||||
in := `{"a":1,"b":2}`
|
||||
out := TryPrettyPrintJson(in)
|
||||
if !strings.Contains(out, "\n") {
|
||||
t.Errorf("expected pretty-printed result with newlines, got %q", out)
|
||||
}
|
||||
if !strings.Contains(out, `"a"`) {
|
||||
t.Errorf("expected key in result, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryPrettyPrintJsonInvalidPassThrough(t *testing.T) {
|
||||
in := `not valid json`
|
||||
tst.AssertEqual(t, TryPrettyPrintJson(in), in)
|
||||
}
|
||||
|
||||
func TestPrettyPrintJsonValid(t *testing.T) {
|
||||
out, ok := PrettyPrintJson(`{"a":1}`)
|
||||
tst.AssertEqual(t, ok, true)
|
||||
if !strings.Contains(out, "\n") {
|
||||
t.Errorf("expected formatted output, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrettyPrintJsonInvalid(t *testing.T) {
|
||||
in := `not json`
|
||||
out, ok := PrettyPrintJson(in)
|
||||
tst.AssertEqual(t, ok, false)
|
||||
tst.AssertEqual(t, out, in)
|
||||
}
|
||||
|
||||
func TestPatchJsonString(t *testing.T) {
|
||||
in := `{"a":1,"b":2}`
|
||||
out, err := PatchJson(in, "c", 3)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(out), &m); err != nil {
|
||||
t.Fatalf("invalid json result: %v", err)
|
||||
}
|
||||
if v, ok := m["c"].(float64); !ok || v != 3 {
|
||||
t.Errorf("expected c=3, got %v", m["c"])
|
||||
}
|
||||
if v, ok := m["a"].(float64); !ok || v != 1 {
|
||||
t.Errorf("expected a=1, got %v", m["a"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchJsonBytes(t *testing.T) {
|
||||
in := []byte(`{"a":1}`)
|
||||
out, err := PatchJson(in, "b", "hello")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(out, &m); err != nil {
|
||||
t.Fatalf("invalid json result: %v", err)
|
||||
}
|
||||
if v, ok := m["b"].(string); !ok || v != "hello" {
|
||||
t.Errorf("expected b=hello, got %v", m["b"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchJsonInvalid(t *testing.T) {
|
||||
_, err := PatchJson("not json", "k", "v")
|
||||
if err == nil {
|
||||
t.Errorf("expected error on invalid json")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchRemJson(t *testing.T) {
|
||||
in := `{"a":1,"b":2}`
|
||||
out, err := PatchRemJson(in, "a")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(out), &m); err != nil {
|
||||
t.Fatalf("invalid json result: %v", err)
|
||||
}
|
||||
if _, exists := m["a"]; exists {
|
||||
t.Errorf("expected key 'a' to be removed")
|
||||
}
|
||||
if v, ok := m["b"].(float64); !ok || v != 2 {
|
||||
t.Errorf("expected b=2, got %v", m["b"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchRemJsonMissingKey(t *testing.T) {
|
||||
in := `{"a":1}`
|
||||
out, err := PatchRemJson(in, "missing")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(out), &m); err != nil {
|
||||
t.Fatalf("invalid json: %v", err)
|
||||
}
|
||||
if v, ok := m["a"].(float64); !ok || v != 1 {
|
||||
t.Errorf("expected a=1, got %v", m["a"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalJsonOrPanic(t *testing.T) {
|
||||
tst.AssertEqual(t, MarshalJsonOrPanic(42), "42")
|
||||
tst.AssertEqual(t, MarshalJsonOrPanic("hi"), `"hi"`)
|
||||
}
|
||||
|
||||
func TestMarshalJsonOrPanicPanics(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("expected panic on un-marshalable input")
|
||||
}
|
||||
}()
|
||||
// channels can't be marshaled
|
||||
MarshalJsonOrPanic(make(chan int))
|
||||
}
|
||||
|
||||
func TestMarshalJsonOrDefault(t *testing.T) {
|
||||
tst.AssertEqual(t, MarshalJsonOrDefault(42, "def"), "42")
|
||||
tst.AssertEqual(t, MarshalJsonOrDefault(make(chan int), "def"), "def")
|
||||
}
|
||||
|
||||
func TestMarshalJsonOrNilSuccess(t *testing.T) {
|
||||
p := MarshalJsonOrNil(42)
|
||||
if p == nil {
|
||||
t.Fatalf("expected non-nil pointer")
|
||||
}
|
||||
tst.AssertEqual(t, *p, "42")
|
||||
}
|
||||
|
||||
func TestMarshalJsonOrNilError(t *testing.T) {
|
||||
p := MarshalJsonOrNil(make(chan int))
|
||||
if p != nil {
|
||||
t.Errorf("expected nil pointer on error, got %v", *p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalJsonIndentOrPanic(t *testing.T) {
|
||||
out := MarshalJsonIndentOrPanic(map[string]int{"a": 1}, "", " ")
|
||||
if !strings.Contains(out, "\n") {
|
||||
t.Errorf("expected indented output, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalJsonIndentOrDefault(t *testing.T) {
|
||||
out := MarshalJsonIndentOrDefault(make(chan int), "", " ", "DEF")
|
||||
tst.AssertEqual(t, out, "DEF")
|
||||
}
|
||||
|
||||
func TestMarshalJsonIndentOrNilSuccess(t *testing.T) {
|
||||
p := MarshalJsonIndentOrNil(map[string]int{"a": 1}, "", " ")
|
||||
if p == nil || !strings.Contains(*p, "\n") {
|
||||
t.Errorf("expected indented JSON pointer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalJsonIndentOrNilFailure(t *testing.T) {
|
||||
p := MarshalJsonIndentOrNil(make(chan int), "", " ")
|
||||
if p != nil {
|
||||
t.Errorf("expected nil pointer on error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTypeIsMap(t *testing.T) {
|
||||
h := H{"a": 1}
|
||||
out, err := json.Marshal(h)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, string(out), `{"a":1}`)
|
||||
}
|
||||
|
||||
func TestATypeIsArray(t *testing.T) {
|
||||
a := A{1, "x", true}
|
||||
out, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, string(out), `[1,"x",true]`)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMapKeyArr(t *testing.T) {
|
||||
m := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
keys := MapKeyArr(m)
|
||||
sort.Strings(keys)
|
||||
tst.AssertArrayEqual(t, keys, []string{"a", "b", "c"})
|
||||
}
|
||||
|
||||
func TestMapKeyArrEmpty(t *testing.T) {
|
||||
m := map[string]int{}
|
||||
keys := MapKeyArr(m)
|
||||
tst.AssertEqual(t, len(keys), 0)
|
||||
}
|
||||
|
||||
func TestMapValueArr(t *testing.T) {
|
||||
m := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
values := MapValueArr(m)
|
||||
sort.Ints(values)
|
||||
tst.AssertArrayEqual(t, values, []int{1, 2, 3})
|
||||
}
|
||||
|
||||
func TestArrToMap(t *testing.T) {
|
||||
type item struct {
|
||||
key string
|
||||
v int
|
||||
}
|
||||
arr := []item{{"a", 1}, {"b", 2}}
|
||||
m := ArrToMap(arr, func(i item) string { return i.key })
|
||||
tst.AssertEqual(t, len(m), 2)
|
||||
tst.AssertEqual(t, m["a"].v, 1)
|
||||
tst.AssertEqual(t, m["b"].v, 2)
|
||||
}
|
||||
|
||||
func TestArrToKVMap(t *testing.T) {
|
||||
arr := []int{1, 2, 3}
|
||||
m := ArrToKVMap(arr,
|
||||
func(v int) int { return v },
|
||||
func(v int) string {
|
||||
return [...]string{"", "one", "two", "three"}[v]
|
||||
},
|
||||
)
|
||||
tst.AssertEqual(t, m[1], "one")
|
||||
tst.AssertEqual(t, m[2], "two")
|
||||
tst.AssertEqual(t, m[3], "three")
|
||||
}
|
||||
|
||||
func TestArrToSet(t *testing.T) {
|
||||
arr := []string{"a", "b", "a", "c"}
|
||||
set := ArrToSet(arr)
|
||||
tst.AssertEqual(t, len(set), 3)
|
||||
tst.AssertEqual(t, set["a"], true)
|
||||
tst.AssertEqual(t, set["b"], true)
|
||||
tst.AssertEqual(t, set["c"], true)
|
||||
tst.AssertEqual(t, set["d"], false)
|
||||
}
|
||||
|
||||
func TestMapToArr(t *testing.T) {
|
||||
m := map[string]int{"a": 1, "b": 2}
|
||||
arr := MapToArr(m)
|
||||
tst.AssertEqual(t, len(arr), 2)
|
||||
roundTrip := make(map[string]int)
|
||||
for _, e := range arr {
|
||||
roundTrip[e.Key] = e.Value
|
||||
}
|
||||
tst.AssertEqual(t, roundTrip["a"], 1)
|
||||
tst.AssertEqual(t, roundTrip["b"], 2)
|
||||
}
|
||||
|
||||
func TestCopyMap(t *testing.T) {
|
||||
src := map[string]int{"a": 1, "b": 2}
|
||||
dst := CopyMap(src)
|
||||
tst.AssertEqual(t, len(dst), 2)
|
||||
tst.AssertEqual(t, dst["a"], 1)
|
||||
|
||||
// Mutating dst should not affect src
|
||||
dst["a"] = 99
|
||||
tst.AssertEqual(t, src["a"], 1)
|
||||
}
|
||||
|
||||
func TestForceMapNil(t *testing.T) {
|
||||
var m map[string]int
|
||||
res := ForceMap(m)
|
||||
if res == nil {
|
||||
t.Errorf("expected non-nil result")
|
||||
}
|
||||
tst.AssertEqual(t, len(res), 0)
|
||||
}
|
||||
|
||||
func TestForceMapNonNil(t *testing.T) {
|
||||
m := map[string]int{"x": 1}
|
||||
res := ForceMap(m)
|
||||
tst.AssertEqual(t, res["x"], 1)
|
||||
}
|
||||
|
||||
func TestForceJsonMapOrPanic(t *testing.T) {
|
||||
type s struct {
|
||||
A int `json:"a"`
|
||||
B string `json:"b"`
|
||||
}
|
||||
res := ForceJsonMapOrPanic(s{A: 1, B: "x"})
|
||||
if v, ok := res["a"].(float64); !ok || v != 1 {
|
||||
t.Errorf("expected a=1, got %v", res["a"])
|
||||
}
|
||||
if v, ok := res["b"].(string); !ok || v != "x" {
|
||||
t.Errorf("expected b=x, got %v", res["b"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestForceJsonMapOrPanicPanics(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("expected panic on un-marshalable input")
|
||||
}
|
||||
}()
|
||||
ForceJsonMapOrPanic(make(chan int))
|
||||
}
|
||||
|
||||
func TestMapMerge(t *testing.T) {
|
||||
base := map[string]int{"a": 1, "b": 2}
|
||||
a := map[string]int{"b": 22, "c": 3}
|
||||
b := map[string]int{"d": 4}
|
||||
|
||||
res := MapMerge(base, a, b)
|
||||
tst.AssertEqual(t, res["a"], 1)
|
||||
tst.AssertEqual(t, res["b"], 22) // overwritten
|
||||
tst.AssertEqual(t, res["c"], 3)
|
||||
tst.AssertEqual(t, res["d"], 4)
|
||||
tst.AssertEqual(t, len(res), 4)
|
||||
|
||||
// base must remain untouched
|
||||
tst.AssertEqual(t, base["b"], 2)
|
||||
}
|
||||
|
||||
func TestMapMergeNoExtras(t *testing.T) {
|
||||
base := map[string]int{"a": 1}
|
||||
res := MapMerge(base)
|
||||
tst.AssertEqual(t, res["a"], 1)
|
||||
tst.AssertEqual(t, len(res), 1)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMustSuccess(t *testing.T) {
|
||||
v := Must(42, nil)
|
||||
tst.AssertEqual(t, v, 42)
|
||||
|
||||
s := Must("hello", nil)
|
||||
tst.AssertEqual(t, s, "hello")
|
||||
}
|
||||
|
||||
func TestMustPanics(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("expected panic on error")
|
||||
}
|
||||
}()
|
||||
Must(0, errors.New("boom"))
|
||||
}
|
||||
|
||||
func TestMustBoolSuccess(t *testing.T) {
|
||||
v := MustBool(42, true)
|
||||
tst.AssertEqual(t, v, 42)
|
||||
}
|
||||
|
||||
func TestMustBoolPanics(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("expected panic on not ok")
|
||||
}
|
||||
}()
|
||||
MustBool(0, false)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeepCopyByJsonStruct(t *testing.T) {
|
||||
type item struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
src := item{Name: "alice", Age: 30}
|
||||
dst, err := DeepCopyByJson(src)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, dst.Name, "alice")
|
||||
tst.AssertEqual(t, dst.Age, 30)
|
||||
}
|
||||
|
||||
func TestDeepCopyByJsonSlice(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
dst, err := DeepCopyByJson(src)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertArrayEqual(t, dst, []int{1, 2, 3})
|
||||
|
||||
// Mutating the copy must not affect the source
|
||||
dst[0] = 99
|
||||
tst.AssertEqual(t, src[0], 1)
|
||||
}
|
||||
|
||||
func TestDeepCopyByJsonError(t *testing.T) {
|
||||
type bad struct {
|
||||
C chan int
|
||||
}
|
||||
_, err := DeepCopyByJson(bad{C: make(chan int)})
|
||||
if err == nil {
|
||||
t.Errorf("expected error for un-marshalable type")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileExistsTrue(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "f.txt")
|
||||
if err := os.WriteFile(path, []byte("hi"), 0o644); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, FileExists(path), true)
|
||||
}
|
||||
|
||||
func TestFileExistsFalse(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "missing.txt")
|
||||
tst.AssertEqual(t, FileExists(path), false)
|
||||
}
|
||||
|
||||
func TestFileExistsDirectoryReturnsFalse(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
tst.AssertEqual(t, FileExists(dir), false)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunPanicSafeNoPanic(t *testing.T) {
|
||||
called := false
|
||||
err := RunPanicSafe(func() {
|
||||
called = true
|
||||
})
|
||||
tst.AssertEqual(t, called, true)
|
||||
if err != nil {
|
||||
t.Errorf("expected nil err, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPanicSafeRecovers(t *testing.T) {
|
||||
err := RunPanicSafe(func() {
|
||||
panic("boom")
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error from panic")
|
||||
}
|
||||
pwe, ok := err.(PanicWrappedErr)
|
||||
if !ok {
|
||||
t.Fatalf("expected PanicWrappedErr, got %T", err)
|
||||
}
|
||||
tst.AssertEqual(t, pwe.RecoveredObj(), "boom")
|
||||
tst.AssertEqual(t, pwe.Error(), "A panic occured")
|
||||
}
|
||||
|
||||
func TestRunPanicSafeR1NoPanic(t *testing.T) {
|
||||
expected := errors.New("expected")
|
||||
err := RunPanicSafeR1(func() error {
|
||||
return expected
|
||||
})
|
||||
if err != expected {
|
||||
t.Errorf("expected original error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPanicSafeR1Panics(t *testing.T) {
|
||||
err := RunPanicSafeR1(func() error {
|
||||
panic("boom")
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected wrapped panic")
|
||||
}
|
||||
if _, ok := err.(PanicWrappedErr); !ok {
|
||||
t.Errorf("expected PanicWrappedErr, got %T", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPanicSafeR2NoPanic(t *testing.T) {
|
||||
v, err := RunPanicSafeR2(func() (int, error) {
|
||||
return 42, nil
|
||||
})
|
||||
tst.AssertEqual(t, v, 42)
|
||||
if err != nil {
|
||||
t.Errorf("expected nil err, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPanicSafeR2Panics(t *testing.T) {
|
||||
v, err := RunPanicSafeR2(func() (int, error) {
|
||||
panic("boom")
|
||||
})
|
||||
tst.AssertEqual(t, v, 0) // zero value
|
||||
if err == nil {
|
||||
t.Errorf("expected wrapped panic")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPanicSafeR3NoPanic(t *testing.T) {
|
||||
a, b, err := RunPanicSafeR3(func() (int, string, error) {
|
||||
return 1, "two", nil
|
||||
})
|
||||
tst.AssertEqual(t, a, 1)
|
||||
tst.AssertEqual(t, b, "two")
|
||||
if err != nil {
|
||||
t.Errorf("expected nil err, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPanicSafeR3Panics(t *testing.T) {
|
||||
a, b, err := RunPanicSafeR3(func() (int, string, error) {
|
||||
panic("boom")
|
||||
})
|
||||
tst.AssertEqual(t, a, 0)
|
||||
tst.AssertEqual(t, b, "")
|
||||
if err == nil {
|
||||
t.Errorf("expected wrapped panic")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPanicSafeR4NoPanic(t *testing.T) {
|
||||
a, b, c, err := RunPanicSafeR4(func() (int, string, bool, error) {
|
||||
return 1, "two", true, nil
|
||||
})
|
||||
tst.AssertEqual(t, a, 1)
|
||||
tst.AssertEqual(t, b, "two")
|
||||
tst.AssertEqual(t, c, true)
|
||||
if err != nil {
|
||||
t.Errorf("expected nil err, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPanicSafeR4Panics(t *testing.T) {
|
||||
a, b, c, err := RunPanicSafeR4(func() (int, string, bool, error) {
|
||||
panic("boom")
|
||||
})
|
||||
tst.AssertEqual(t, a, 0)
|
||||
tst.AssertEqual(t, b, "")
|
||||
tst.AssertEqual(t, c, false)
|
||||
if err == nil {
|
||||
t.Errorf("expected wrapped panic")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPtr(t *testing.T) {
|
||||
p := Ptr(42)
|
||||
if p == nil {
|
||||
t.Fatalf("expected non-nil")
|
||||
}
|
||||
tst.AssertEqual(t, *p, 42)
|
||||
}
|
||||
|
||||
func TestPtrString(t *testing.T) {
|
||||
p := Ptr("hi")
|
||||
tst.AssertEqual(t, *p, "hi")
|
||||
}
|
||||
|
||||
func TestPTrue(t *testing.T) {
|
||||
if PTrue == nil || *PTrue != true {
|
||||
t.Errorf("PTrue should point to true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPFalse(t *testing.T) {
|
||||
if PFalse == nil || *PFalse != false {
|
||||
t.Errorf("PFalse should point to false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDblPtr(t *testing.T) {
|
||||
pp := DblPtr(7)
|
||||
if pp == nil || *pp == nil {
|
||||
t.Fatalf("expected non-nil double pointer")
|
||||
}
|
||||
tst.AssertEqual(t, **pp, 7)
|
||||
}
|
||||
|
||||
func TestDblPtrIfNotNilWithValue(t *testing.T) {
|
||||
v := 5
|
||||
pp := DblPtrIfNotNil(&v)
|
||||
if pp == nil {
|
||||
t.Fatalf("expected non-nil double pointer")
|
||||
}
|
||||
tst.AssertEqual(t, **pp, 5)
|
||||
}
|
||||
|
||||
func TestDblPtrIfNotNilNil(t *testing.T) {
|
||||
pp := DblPtrIfNotNil[int](nil)
|
||||
if pp != nil {
|
||||
t.Errorf("expected nil for nil input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDblPtrNil(t *testing.T) {
|
||||
pp := DblPtrNil[int]()
|
||||
if pp == nil {
|
||||
t.Fatalf("expected non-nil outer pointer")
|
||||
}
|
||||
if *pp != nil {
|
||||
t.Errorf("expected inner pointer to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArrPtr(t *testing.T) {
|
||||
p := ArrPtr(1, 2, 3)
|
||||
if p == nil {
|
||||
t.Fatalf("expected non-nil pointer")
|
||||
}
|
||||
tst.AssertArrayEqual(t, *p, []int{1, 2, 3})
|
||||
}
|
||||
|
||||
func TestPtrInt32(t *testing.T) {
|
||||
p := PtrInt32(7)
|
||||
tst.AssertEqual(t, *p, int32(7))
|
||||
}
|
||||
|
||||
func TestPtrInt64(t *testing.T) {
|
||||
p := PtrInt64(7)
|
||||
tst.AssertEqual(t, *p, int64(7))
|
||||
}
|
||||
|
||||
func TestPtrFloat32(t *testing.T) {
|
||||
p := PtrFloat32(1.5)
|
||||
tst.AssertEqual(t, *p, float32(1.5))
|
||||
}
|
||||
|
||||
func TestPtrFloat64(t *testing.T) {
|
||||
p := PtrFloat64(2.5)
|
||||
tst.AssertEqual(t, *p, 2.5)
|
||||
}
|
||||
|
||||
func TestIsNilTrue(t *testing.T) {
|
||||
tst.AssertEqual(t, IsNil(nil), true)
|
||||
|
||||
var p *int
|
||||
tst.AssertEqual(t, IsNil(p), true)
|
||||
|
||||
var m map[string]int
|
||||
tst.AssertEqual(t, IsNil(m), true)
|
||||
|
||||
var s []int
|
||||
tst.AssertEqual(t, IsNil(s), true)
|
||||
|
||||
var c chan int
|
||||
tst.AssertEqual(t, IsNil(c), true)
|
||||
|
||||
var f func()
|
||||
tst.AssertEqual(t, IsNil(f), true)
|
||||
}
|
||||
|
||||
func TestIsNilFalse(t *testing.T) {
|
||||
v := 5
|
||||
tst.AssertEqual(t, IsNil(&v), false)
|
||||
tst.AssertEqual(t, IsNil(5), false)
|
||||
tst.AssertEqual(t, IsNil("hi"), false)
|
||||
tst.AssertEqual(t, IsNil(map[string]int{}), false)
|
||||
tst.AssertEqual(t, IsNil([]int{}), false)
|
||||
}
|
||||
|
||||
func TestPtrEqualsBothNil(t *testing.T) {
|
||||
tst.AssertEqual(t, PtrEquals[int](nil, nil), true)
|
||||
}
|
||||
|
||||
func TestPtrEqualsBothEqual(t *testing.T) {
|
||||
a := 5
|
||||
b := 5
|
||||
tst.AssertEqual(t, PtrEquals(&a, &b), true)
|
||||
}
|
||||
|
||||
func TestPtrEqualsBothDifferent(t *testing.T) {
|
||||
a := 5
|
||||
b := 6
|
||||
tst.AssertEqual(t, PtrEquals(&a, &b), false)
|
||||
}
|
||||
|
||||
func TestPtrEqualsOneNil(t *testing.T) {
|
||||
a := 5
|
||||
tst.AssertEqual(t, PtrEquals(&a, nil), false)
|
||||
tst.AssertEqual(t, PtrEquals[int](nil, &a), false)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRandBytesLength(t *testing.T) {
|
||||
for _, sz := range []int{0, 1, 16, 32, 1024} {
|
||||
b := RandBytes(sz)
|
||||
tst.AssertEqual(t, len(b), sz)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandBytesDistinct(t *testing.T) {
|
||||
a := RandBytes(32)
|
||||
b := RandBytes(32)
|
||||
|
||||
// Two cryptographic random sequences should not be equal in 32 bytes.
|
||||
if len(a) != 32 || len(b) != 32 {
|
||||
t.Fatalf("unexpected length")
|
||||
}
|
||||
|
||||
equal := true
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
equal = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if equal {
|
||||
t.Errorf("two consecutive 32-byte RandBytes calls returned identical results")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSortInPlace(t *testing.T) {
|
||||
arr := []int{3, 1, 2}
|
||||
Sort(arr)
|
||||
tst.AssertArrayEqual(t, arr, []int{1, 2, 3})
|
||||
}
|
||||
|
||||
func TestSortStrings(t *testing.T) {
|
||||
arr := []string{"c", "a", "b"}
|
||||
Sort(arr)
|
||||
tst.AssertArrayEqual(t, arr, []string{"a", "b", "c"})
|
||||
}
|
||||
|
||||
func TestAsSorted(t *testing.T) {
|
||||
src := []int{3, 1, 2}
|
||||
out := AsSorted(src)
|
||||
tst.AssertArrayEqual(t, out, []int{1, 2, 3})
|
||||
// original unchanged
|
||||
tst.AssertArrayEqual(t, src, []int{3, 1, 2})
|
||||
}
|
||||
|
||||
func TestSortStable(t *testing.T) {
|
||||
arr := []int{3, 1, 2, 1}
|
||||
SortStable(arr)
|
||||
tst.AssertArrayEqual(t, arr, []int{1, 1, 2, 3})
|
||||
}
|
||||
|
||||
func TestAsSortedStable(t *testing.T) {
|
||||
src := []int{3, 1, 2, 1}
|
||||
out := AsSortedStable(src)
|
||||
tst.AssertArrayEqual(t, out, []int{1, 1, 2, 3})
|
||||
tst.AssertArrayEqual(t, src, []int{3, 1, 2, 1})
|
||||
}
|
||||
|
||||
func TestIsSorted(t *testing.T) {
|
||||
tst.AssertEqual(t, IsSorted([]int{1, 2, 3}), true)
|
||||
tst.AssertEqual(t, IsSorted([]int{3, 2, 1}), false)
|
||||
tst.AssertEqual(t, IsSorted([]int{1, 1, 1}), true)
|
||||
tst.AssertEqual(t, IsSorted([]int{}), true)
|
||||
}
|
||||
|
||||
func TestSortSlice(t *testing.T) {
|
||||
arr := []int{3, 1, 2}
|
||||
SortSlice(arr, func(a, b int) bool { return a < b })
|
||||
tst.AssertArrayEqual(t, arr, []int{1, 2, 3})
|
||||
}
|
||||
|
||||
func TestAsSortedSlice(t *testing.T) {
|
||||
src := []int{3, 1, 2}
|
||||
out := AsSortedSlice(src, func(a, b int) bool { return a > b })
|
||||
tst.AssertArrayEqual(t, out, []int{3, 2, 1})
|
||||
tst.AssertArrayEqual(t, src, []int{3, 1, 2})
|
||||
}
|
||||
|
||||
func TestSortSliceStable(t *testing.T) {
|
||||
arr := []int{3, 1, 2, 1}
|
||||
SortSliceStable(arr, func(a, b int) bool { return a < b })
|
||||
tst.AssertArrayEqual(t, arr, []int{1, 1, 2, 3})
|
||||
}
|
||||
|
||||
func TestAsSortedSliceStable(t *testing.T) {
|
||||
src := []int{3, 1, 2, 1}
|
||||
out := AsSortedSliceStable(src, func(a, b int) bool { return a < b })
|
||||
tst.AssertArrayEqual(t, out, []int{1, 1, 2, 3})
|
||||
tst.AssertArrayEqual(t, src, []int{3, 1, 2, 1})
|
||||
}
|
||||
|
||||
func TestIsSliceSorted(t *testing.T) {
|
||||
tst.AssertEqual(t, IsSliceSorted([]int{1, 2, 3}, func(a, b int) bool { return a < b }), true)
|
||||
tst.AssertEqual(t, IsSliceSorted([]int{3, 2, 1}, func(a, b int) bool { return a < b }), false)
|
||||
}
|
||||
|
||||
type byKey struct {
|
||||
key int
|
||||
v string
|
||||
}
|
||||
|
||||
func TestSortBy(t *testing.T) {
|
||||
arr := []byKey{{3, "c"}, {1, "a"}, {2, "b"}}
|
||||
SortBy(arr, func(v byKey) int { return v.key })
|
||||
tst.AssertEqual(t, arr[0].v, "a")
|
||||
tst.AssertEqual(t, arr[1].v, "b")
|
||||
tst.AssertEqual(t, arr[2].v, "c")
|
||||
}
|
||||
|
||||
func TestAsSortedBy(t *testing.T) {
|
||||
src := []byKey{{3, "c"}, {1, "a"}, {2, "b"}}
|
||||
out := AsSortedBy(src, func(v byKey) int { return v.key })
|
||||
tst.AssertEqual(t, out[0].v, "a")
|
||||
tst.AssertEqual(t, out[2].v, "c")
|
||||
// source unchanged
|
||||
tst.AssertEqual(t, src[0].v, "c")
|
||||
}
|
||||
|
||||
func TestSortByStable(t *testing.T) {
|
||||
arr := []byKey{{1, "a1"}, {1, "a2"}, {0, "b"}}
|
||||
SortByStable(arr, func(v byKey) int { return v.key })
|
||||
tst.AssertEqual(t, arr[0].v, "b")
|
||||
// stable order for ties
|
||||
tst.AssertEqual(t, arr[1].v, "a1")
|
||||
tst.AssertEqual(t, arr[2].v, "a2")
|
||||
}
|
||||
|
||||
func TestAsSortedByStable(t *testing.T) {
|
||||
src := []byKey{{1, "a1"}, {1, "a2"}, {0, "b"}}
|
||||
out := AsSortedByStable(src, func(v byKey) int { return v.key })
|
||||
tst.AssertEqual(t, out[0].v, "b")
|
||||
tst.AssertEqual(t, out[1].v, "a1")
|
||||
tst.AssertEqual(t, out[2].v, "a2")
|
||||
// source unchanged
|
||||
tst.AssertEqual(t, src[0].v, "a1")
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package langext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewUUIDLength(t *testing.T) {
|
||||
u, err := NewUUID()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, len(u), 16)
|
||||
}
|
||||
|
||||
func TestNewUUIDVersionAndVariant(t *testing.T) {
|
||||
u, err := NewUUID()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// Version 4 is in upper nibble of byte 6
|
||||
tst.AssertEqual(t, u[6]&0xf0, byte(0x40))
|
||||
// Variant 10 in top two bits of byte 8
|
||||
tst.AssertEqual(t, u[8]&0xc0, byte(0x80))
|
||||
}
|
||||
|
||||
func TestNewUUIDRandomness(t *testing.T) {
|
||||
a, _ := NewUUID()
|
||||
b, _ := NewUUID()
|
||||
if a == b {
|
||||
t.Errorf("two UUIDs should not be equal")
|
||||
}
|
||||
}
|
||||
|
||||
var hexUUIDRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
|
||||
|
||||
func TestNewHexUUIDFormat(t *testing.T) {
|
||||
s, err := NewHexUUID()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, len(s), 36)
|
||||
if !hexUUIDRegex.MatchString(s) {
|
||||
t.Errorf("not a valid hex UUID format: %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustHexUUID(t *testing.T) {
|
||||
s := MustHexUUID()
|
||||
tst.AssertEqual(t, len(s), 36)
|
||||
if !hexUUIDRegex.MatchString(s) {
|
||||
t.Errorf("not a valid hex UUID format: %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
var upperHexRegex = regexp.MustCompile(`^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$`)
|
||||
|
||||
func TestNewUpperHexUUIDFormat(t *testing.T) {
|
||||
s, err := NewUpperHexUUID()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, len(s), 36)
|
||||
tst.AssertEqual(t, s, strings.ToUpper(s))
|
||||
if !upperHexRegex.MatchString(s) {
|
||||
t.Errorf("not a valid upper-hex UUID format: %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustUpperHexUUID(t *testing.T) {
|
||||
s := MustUpperHexUUID()
|
||||
tst.AssertEqual(t, len(s), 36)
|
||||
}
|
||||
|
||||
func TestNewRawHexUUIDFormat(t *testing.T) {
|
||||
s, err := NewRawHexUUID()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, len(s), 32)
|
||||
if strings.Contains(s, "-") {
|
||||
t.Errorf("raw hex should have no dashes: %q", s)
|
||||
}
|
||||
tst.AssertEqual(t, s, strings.ToUpper(s))
|
||||
}
|
||||
|
||||
func TestMustRawHexUUID(t *testing.T) {
|
||||
s := MustRawHexUUID()
|
||||
tst.AssertEqual(t, len(s), 32)
|
||||
}
|
||||
|
||||
func TestNewBracesUUID(t *testing.T) {
|
||||
s, err := NewBracesUUID()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, len(s), 38)
|
||||
tst.AssertEqual(t, string(s[37]), "}")
|
||||
}
|
||||
|
||||
func TestMustBracesUUID(t *testing.T) {
|
||||
s := MustBracesUUID()
|
||||
tst.AssertEqual(t, len(s), 38)
|
||||
}
|
||||
|
||||
func TestNewParensUUID(t *testing.T) {
|
||||
s, err := NewParensUUID()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tst.AssertEqual(t, len(s), 38)
|
||||
tst.AssertEqual(t, string(s[37]), ")")
|
||||
}
|
||||
|
||||
func TestMustParensUUID(t *testing.T) {
|
||||
s := MustParensUUID()
|
||||
tst.AssertEqual(t, len(s), 38)
|
||||
}
|
||||
|
||||
func TestUUIDsAreUnique(t *testing.T) {
|
||||
const count = 100
|
||||
seen := make(map[string]bool, count)
|
||||
for range count {
|
||||
s := MustHexUUID()
|
||||
if seen[s] {
|
||||
t.Errorf("collision in UUID set: %q", s)
|
||||
}
|
||||
seen[s] = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package mathext
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestClampIntWithinRange(t *testing.T) {
|
||||
if got := ClampInt(5, 1, 10); got != 5 {
|
||||
t.Errorf("ClampInt(5, 1, 10) = %v, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampIntBelowRange(t *testing.T) {
|
||||
if got := ClampInt(-3, 1, 10); got != 1 {
|
||||
t.Errorf("ClampInt(-3, 1, 10) = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampIntAboveRange(t *testing.T) {
|
||||
if got := ClampInt(15, 1, 10); got != 10 {
|
||||
t.Errorf("ClampInt(15, 1, 10) = %v, want 10", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampIntAtLowerBound(t *testing.T) {
|
||||
if got := ClampInt(1, 1, 10); got != 1 {
|
||||
t.Errorf("ClampInt(1, 1, 10) = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampIntAtUpperBound(t *testing.T) {
|
||||
if got := ClampInt(10, 1, 10); got != 10 {
|
||||
t.Errorf("ClampInt(10, 1, 10) = %v, want 10", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampInt32WithinRange(t *testing.T) {
|
||||
if got := ClampInt32(int32(5), int32(1), int32(10)); got != 5 {
|
||||
t.Errorf("ClampInt32(5, 1, 10) = %v, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampInt32BelowRange(t *testing.T) {
|
||||
if got := ClampInt32(int32(-3), int32(1), int32(10)); got != 1 {
|
||||
t.Errorf("ClampInt32(-3, 1, 10) = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampInt32AboveRange(t *testing.T) {
|
||||
if got := ClampInt32(int32(15), int32(1), int32(10)); got != 10 {
|
||||
t.Errorf("ClampInt32(15, 1, 10) = %v, want 10", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampFloat32WithinRange(t *testing.T) {
|
||||
if got := ClampFloat32(float32(5.5), float32(1.0), float32(10.0)); got != 5.5 {
|
||||
t.Errorf("ClampFloat32(5.5, 1.0, 10.0) = %v, want 5.5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampFloat32BelowRange(t *testing.T) {
|
||||
if got := ClampFloat32(float32(-1.5), float32(0.0), float32(10.0)); got != 0.0 {
|
||||
t.Errorf("ClampFloat32(-1.5, 0.0, 10.0) = %v, want 0.0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampFloat32AboveRange(t *testing.T) {
|
||||
if got := ClampFloat32(float32(11.5), float32(0.0), float32(10.0)); got != 10.0 {
|
||||
t.Errorf("ClampFloat32(11.5, 0.0, 10.0) = %v, want 10.0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampFloat64WithinRange(t *testing.T) {
|
||||
if got := ClampFloat64(5.5, 1.0, 10.0); got != 5.5 {
|
||||
t.Errorf("ClampFloat64(5.5, 1.0, 10.0) = %v, want 5.5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampFloat64BelowRange(t *testing.T) {
|
||||
if got := ClampFloat64(-1.5, 0.0, 10.0); got != 0.0 {
|
||||
t.Errorf("ClampFloat64(-1.5, 0.0, 10.0) = %v, want 0.0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampFloat64AboveRange(t *testing.T) {
|
||||
if got := ClampFloat64(11.5, 0.0, 10.0); got != 10.0 {
|
||||
t.Errorf("ClampFloat64(11.5, 0.0, 10.0) = %v, want 10.0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampGenericIntWithinRange(t *testing.T) {
|
||||
if got := Clamp(5, 1, 10); got != 5 {
|
||||
t.Errorf("Clamp(5, 1, 10) = %v, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampGenericIntBelowRange(t *testing.T) {
|
||||
if got := Clamp(-3, 1, 10); got != 1 {
|
||||
t.Errorf("Clamp(-3, 1, 10) = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampGenericIntAboveRange(t *testing.T) {
|
||||
if got := Clamp(15, 1, 10); got != 10 {
|
||||
t.Errorf("Clamp(15, 1, 10) = %v, want 10", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampGenericFloat64WithinRange(t *testing.T) {
|
||||
if got := Clamp(5.5, 1.0, 10.0); got != 5.5 {
|
||||
t.Errorf("Clamp(5.5, 1.0, 10.0) = %v, want 5.5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampGenericFloat64BelowRange(t *testing.T) {
|
||||
if got := Clamp(-2.0, 0.0, 10.0); got != 0.0 {
|
||||
t.Errorf("Clamp(-2.0, 0.0, 10.0) = %v, want 0.0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampGenericFloat64AboveRange(t *testing.T) {
|
||||
if got := Clamp(20.5, 0.0, 10.0); got != 10.0 {
|
||||
t.Errorf("Clamp(20.5, 0.0, 10.0) = %v, want 10.0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampOptNilFallback(t *testing.T) {
|
||||
var v *int = nil
|
||||
if got := ClampOpt(v, 7, 1, 10); got != 7 {
|
||||
t.Errorf("ClampOpt(nil, 7, 1, 10) = %v, want 7", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampOptValueWithinRange(t *testing.T) {
|
||||
val := 5
|
||||
if got := ClampOpt(&val, 7, 1, 10); got != 5 {
|
||||
t.Errorf("ClampOpt(&5, 7, 1, 10) = %v, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampOptValueBelowRange(t *testing.T) {
|
||||
val := -3
|
||||
if got := ClampOpt(&val, 7, 1, 10); got != 1 {
|
||||
t.Errorf("ClampOpt(&-3, 7, 1, 10) = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampOptValueAboveRange(t *testing.T) {
|
||||
val := 15
|
||||
if got := ClampOpt(&val, 7, 1, 10); got != 10 {
|
||||
t.Errorf("ClampOpt(&15, 7, 1, 10) = %v, want 10", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampOptFloat64Nil(t *testing.T) {
|
||||
var v *float64 = nil
|
||||
if got := ClampOpt(v, 2.5, 0.0, 10.0); got != 2.5 {
|
||||
t.Errorf("ClampOpt(nil, 2.5, 0.0, 10.0) = %v, want 2.5", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package mathext
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFloat64EpsilonEqExactlyEqual(t *testing.T) {
|
||||
if !Float64EpsilonEq(1.0, 1.0, 1e-9) {
|
||||
t.Errorf("Float64EpsilonEq(1.0, 1.0, 1e-9) = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloat64EpsilonEqWithinEpsilon(t *testing.T) {
|
||||
if !Float64EpsilonEq(1.0, 1.0+1e-10, 1e-9) {
|
||||
t.Errorf("Float64EpsilonEq(1.0, 1.0+1e-10, 1e-9) = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloat64EpsilonEqOutsideEpsilon(t *testing.T) {
|
||||
if Float64EpsilonEq(1.0, 1.1, 1e-9) {
|
||||
t.Errorf("Float64EpsilonEq(1.0, 1.1, 1e-9) = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloat64EpsilonEqAtEpsilonBoundary(t *testing.T) {
|
||||
if !Float64EpsilonEq(0.0, 0.5, 0.5) {
|
||||
t.Errorf("Float64EpsilonEq(0.0, 0.5, 0.5) = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloat64EpsilonEqNegativeDifference(t *testing.T) {
|
||||
if !Float64EpsilonEq(2.0, 2.0-1e-10, 1e-9) {
|
||||
t.Errorf("Float64EpsilonEq(2.0, 2.0-1e-10, 1e-9) = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloat64EpsilonEqLargeDifference(t *testing.T) {
|
||||
if Float64EpsilonEq(0.0, 100.0, 0.5) {
|
||||
t.Errorf("Float64EpsilonEq(0.0, 100.0, 0.5) = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloat64EpsilonEqNegativeNumbers(t *testing.T) {
|
||||
if !Float64EpsilonEq(-1.0, -1.0+1e-10, 1e-9) {
|
||||
t.Errorf("Float64EpsilonEq(-1.0, -1.0+1e-10, 1e-9) = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloat64EpsilonEqZeroEpsilonEqualValues(t *testing.T) {
|
||||
if !Float64EpsilonEq(3.14, 3.14, 0.0) {
|
||||
t.Errorf("Float64EpsilonEq(3.14, 3.14, 0.0) = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloat64EpsilonEqZeroEpsilonDifferentValues(t *testing.T) {
|
||||
if Float64EpsilonEq(3.14, 3.15, 0.0) {
|
||||
t.Errorf("Float64EpsilonEq(3.14, 3.15, 0.0) = true, want false")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package mathext
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSumFloat64HappyPath(t *testing.T) {
|
||||
values := []float64{1.0, 2.0, 3.0, 4.0}
|
||||
expected := 10.0
|
||||
if got := SumFloat64(values); got != expected {
|
||||
t.Errorf("SumFloat64(%v) = %v, want %v", values, got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSumFloat64Empty(t *testing.T) {
|
||||
values := []float64{}
|
||||
if got := SumFloat64(values); got != 0.0 {
|
||||
t.Errorf("SumFloat64(empty) = %v, want 0.0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSumFloat64Negatives(t *testing.T) {
|
||||
values := []float64{-1.0, -2.0, 3.0}
|
||||
expected := 0.0
|
||||
if got := SumFloat64(values); got != expected {
|
||||
t.Errorf("SumFloat64(%v) = %v, want %v", values, got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvgFloat64HappyPath(t *testing.T) {
|
||||
values := []float64{2.0, 4.0, 6.0, 8.0}
|
||||
expected := 5.0
|
||||
if got := AvgFloat64(values); got != expected {
|
||||
t.Errorf("AvgFloat64(%v) = %v, want %v", values, got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvgFloat64SingleValue(t *testing.T) {
|
||||
values := []float64{42.0}
|
||||
expected := 42.0
|
||||
if got := AvgFloat64(values); got != expected {
|
||||
t.Errorf("AvgFloat64(%v) = %v, want %v", values, got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvgFloat64EmptyReturnsNaN(t *testing.T) {
|
||||
values := []float64{}
|
||||
got := AvgFloat64(values)
|
||||
if !math.IsNaN(got) {
|
||||
t.Errorf("AvgFloat64(empty) = %v, want NaN", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxIntFirstLarger(t *testing.T) {
|
||||
if got := Max(5, 3); got != 5 {
|
||||
t.Errorf("Max(5, 3) = %v, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxIntSecondLarger(t *testing.T) {
|
||||
if got := Max(3, 5); got != 5 {
|
||||
t.Errorf("Max(3, 5) = %v, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxIntEqual(t *testing.T) {
|
||||
if got := Max(5, 5); got != 5 {
|
||||
t.Errorf("Max(5, 5) = %v, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxFloat64(t *testing.T) {
|
||||
if got := Max(2.7, 3.1); got != 3.1 {
|
||||
t.Errorf("Max(2.7, 3.1) = %v, want 3.1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxString(t *testing.T) {
|
||||
if got := Max("apple", "banana"); got != "banana" {
|
||||
t.Errorf(`Max("apple", "banana") = %v, want "banana"`, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMax3FirstLargest(t *testing.T) {
|
||||
if got := Max3(10, 5, 3); got != 10 {
|
||||
t.Errorf("Max3(10, 5, 3) = %v, want 10", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMax3MiddleLargest(t *testing.T) {
|
||||
if got := Max3(5, 10, 3); got != 10 {
|
||||
t.Errorf("Max3(5, 10, 3) = %v, want 10", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMax3LastLargest(t *testing.T) {
|
||||
if got := Max3(5, 3, 10); got != 10 {
|
||||
t.Errorf("Max3(5, 3, 10) = %v, want 10", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMax4(t *testing.T) {
|
||||
if got := Max4(1, 5, 3, 7); got != 7 {
|
||||
t.Errorf("Max4(1, 5, 3, 7) = %v, want 7", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMax4FirstLargest(t *testing.T) {
|
||||
if got := Max4(10, 5, 3, 7); got != 10 {
|
||||
t.Errorf("Max4(10, 5, 3, 7) = %v, want 10", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMax4ThirdLargest(t *testing.T) {
|
||||
if got := Max4(1, 5, 100, 7); got != 100 {
|
||||
t.Errorf("Max4(1, 5, 100, 7) = %v, want 100", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinIntFirstSmaller(t *testing.T) {
|
||||
if got := Min(3, 5); got != 3 {
|
||||
t.Errorf("Min(3, 5) = %v, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinIntSecondSmaller(t *testing.T) {
|
||||
if got := Min(5, 3); got != 3 {
|
||||
t.Errorf("Min(5, 3) = %v, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinIntEqual(t *testing.T) {
|
||||
if got := Min(5, 5); got != 5 {
|
||||
t.Errorf("Min(5, 5) = %v, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinFloat64(t *testing.T) {
|
||||
if got := Min(2.7, 3.1); got != 2.7 {
|
||||
t.Errorf("Min(2.7, 3.1) = %v, want 2.7", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMin3FirstSmallest(t *testing.T) {
|
||||
if got := Min3(1, 5, 10); got != 1 {
|
||||
t.Errorf("Min3(1, 5, 10) = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMin3MiddleSmallest(t *testing.T) {
|
||||
if got := Min3(5, 1, 10); got != 1 {
|
||||
t.Errorf("Min3(5, 1, 10) = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMin3LastSmallest(t *testing.T) {
|
||||
if got := Min3(5, 10, 1); got != 1 {
|
||||
t.Errorf("Min3(5, 10, 1) = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMin4(t *testing.T) {
|
||||
if got := Min4(7, 3, 5, 1); got != 1 {
|
||||
t.Errorf("Min4(7, 3, 5, 1) = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMin4FirstSmallest(t *testing.T) {
|
||||
if got := Min4(1, 5, 3, 7); got != 1 {
|
||||
t.Errorf("Min4(1, 5, 3, 7) = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMin4ThirdSmallest(t *testing.T) {
|
||||
if got := Min4(10, 5, 1, 7); got != 1 {
|
||||
t.Errorf("Min4(10, 5, 1, 7) = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbsPositiveInt(t *testing.T) {
|
||||
if got := Abs(5); got != 5 {
|
||||
t.Errorf("Abs(5) = %v, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbsNegativeInt(t *testing.T) {
|
||||
if got := Abs(-5); got != 5 {
|
||||
t.Errorf("Abs(-5) = %v, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbsZeroInt(t *testing.T) {
|
||||
if got := Abs(0); got != 0 {
|
||||
t.Errorf("Abs(0) = %v, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbsPositiveFloat64(t *testing.T) {
|
||||
if got := Abs(3.14); got != 3.14 {
|
||||
t.Errorf("Abs(3.14) = %v, want 3.14", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbsNegativeFloat64(t *testing.T) {
|
||||
if got := Abs(-3.14); got != 3.14 {
|
||||
t.Errorf("Abs(-3.14) = %v, want 3.14", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbsNegativeFloat32(t *testing.T) {
|
||||
if got := Abs(float32(-2.5)); got != float32(2.5) {
|
||||
t.Errorf("Abs(-2.5) = %v, want 2.5", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package mongoext
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
func TestFixTextSearchPipelineEmpty(t *testing.T) {
|
||||
pipeline := mongo.Pipeline{}
|
||||
result := FixTextSearchPipeline(pipeline)
|
||||
tst.AssertEqual(t, len(result), 0)
|
||||
}
|
||||
|
||||
func TestFixTextSearchPipelineNoTextSearch(t *testing.T) {
|
||||
pipeline := mongo.Pipeline{
|
||||
bson.D{{Key: "$match", Value: bson.M{"foo": "bar"}}},
|
||||
bson.D{{Key: "$sort", Value: bson.M{"baz": 1}}},
|
||||
}
|
||||
|
||||
result := FixTextSearchPipeline(pipeline)
|
||||
|
||||
tst.AssertEqual(t, len(result), 2)
|
||||
tst.AssertEqual(t, result[0][0].Key, "$match")
|
||||
tst.AssertEqual(t, result[1][0].Key, "$sort")
|
||||
}
|
||||
|
||||
func TestFixTextSearchPipelineMovesTextSearchToFront(t *testing.T) {
|
||||
pipeline := mongo.Pipeline{
|
||||
bson.D{{Key: "$match", Value: bson.M{"foo": "bar"}}},
|
||||
bson.D{{Key: "$sort", Value: bson.M{"baz": 1}}},
|
||||
bson.D{{Key: "$match", Value: bson.M{"$text": bson.M{"$search": "hello world"}}}},
|
||||
}
|
||||
|
||||
result := FixTextSearchPipeline(pipeline)
|
||||
|
||||
tst.AssertEqual(t, len(result), 3)
|
||||
|
||||
// $text/$search should be at front
|
||||
first := result[0]
|
||||
matchVal, ok := first[0].Value.(bson.M)
|
||||
tst.AssertTrue(t, ok)
|
||||
textVal, ok := matchVal["$text"].(bson.M)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, textVal["$search"].(string), "hello world")
|
||||
|
||||
// other entries should preserve order
|
||||
tst.AssertEqual(t, result[1][0].Key, "$match")
|
||||
tst.AssertEqual(t, result[2][0].Key, "$sort")
|
||||
}
|
||||
|
||||
func TestFixTextSearchPipelineMultipleTextSearches(t *testing.T) {
|
||||
pipeline := mongo.Pipeline{
|
||||
bson.D{{Key: "$sort", Value: bson.M{"baz": 1}}},
|
||||
bson.D{{Key: "$match", Value: bson.M{"$text": bson.M{"$search": "first"}}}},
|
||||
bson.D{{Key: "$match", Value: bson.M{"foo": "bar"}}},
|
||||
bson.D{{Key: "$match", Value: bson.M{"$text": bson.M{"$search": "second"}}}},
|
||||
}
|
||||
|
||||
result := FixTextSearchPipeline(pipeline)
|
||||
|
||||
tst.AssertEqual(t, len(result), 4)
|
||||
|
||||
// Last seen text-search should be prepended last, ending up at front.
|
||||
first := result[0][0].Value.(bson.M)
|
||||
tst.AssertEqual(t, first["$text"].(bson.M)["$search"].(string), "second")
|
||||
|
||||
second := result[1][0].Value.(bson.M)
|
||||
tst.AssertEqual(t, second["$text"].(bson.M)["$search"].(string), "first")
|
||||
|
||||
tst.AssertEqual(t, result[2][0].Key, "$sort")
|
||||
tst.AssertEqual(t, result[3][0].Key, "$match")
|
||||
}
|
||||
|
||||
func TestFixTextSearchPipelineMatchWithoutText(t *testing.T) {
|
||||
pipeline := mongo.Pipeline{
|
||||
bson.D{{Key: "$sort", Value: bson.M{"baz": 1}}},
|
||||
bson.D{{Key: "$match", Value: bson.M{"name": "alice"}}},
|
||||
}
|
||||
|
||||
result := FixTextSearchPipeline(pipeline)
|
||||
|
||||
tst.AssertEqual(t, len(result), 2)
|
||||
tst.AssertEqual(t, result[0][0].Key, "$sort")
|
||||
tst.AssertEqual(t, result[1][0].Key, "$match")
|
||||
}
|
||||
|
||||
func TestFixTextSearchPipelineTextWithoutSearch(t *testing.T) {
|
||||
// $text present but without $search key — should NOT be moved
|
||||
pipeline := mongo.Pipeline{
|
||||
bson.D{{Key: "$sort", Value: bson.M{"baz": 1}}},
|
||||
bson.D{{Key: "$match", Value: bson.M{"$text": bson.M{"$language": "en"}}}},
|
||||
}
|
||||
|
||||
result := FixTextSearchPipeline(pipeline)
|
||||
|
||||
tst.AssertEqual(t, len(result), 2)
|
||||
tst.AssertEqual(t, result[0][0].Key, "$sort")
|
||||
tst.AssertEqual(t, result[1][0].Key, "$match")
|
||||
}
|
||||
|
||||
func TestFixTextSearchPipelineMatchValueWrongType(t *testing.T) {
|
||||
// $match with non-bson.M value — function should keep entry in place
|
||||
pipeline := mongo.Pipeline{
|
||||
bson.D{{Key: "$match", Value: "not a map"}},
|
||||
bson.D{{Key: "$sort", Value: bson.M{"baz": 1}}},
|
||||
}
|
||||
|
||||
result := FixTextSearchPipeline(pipeline)
|
||||
|
||||
tst.AssertEqual(t, len(result), 2)
|
||||
tst.AssertEqual(t, result[0][0].Key, "$match")
|
||||
tst.AssertEqual(t, result[1][0].Key, "$sort")
|
||||
}
|
||||
|
||||
func TestFixTextSearchPipelinePreservesOriginal(t *testing.T) {
|
||||
original := mongo.Pipeline{
|
||||
bson.D{{Key: "$sort", Value: bson.M{"baz": 1}}},
|
||||
bson.D{{Key: "$match", Value: bson.M{"$text": bson.M{"$search": "x"}}}},
|
||||
}
|
||||
originalLen := len(original)
|
||||
originalFirstKey := original[0][0].Key
|
||||
|
||||
_ = FixTextSearchPipeline(original)
|
||||
|
||||
tst.AssertEqual(t, len(original), originalLen)
|
||||
tst.AssertEqual(t, original[0][0].Key, originalFirstKey)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package mongoext
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
)
|
||||
|
||||
func TestProjectionFromStructSimple(t *testing.T) {
|
||||
type model struct {
|
||||
ID string `bson:"_id"`
|
||||
Name string `bson:"name"`
|
||||
Age int `bson:"age"`
|
||||
}
|
||||
|
||||
res := ProjectionFromStruct(model{})
|
||||
|
||||
tst.AssertEqual(t, len(res), 3)
|
||||
tst.AssertEqual(t, res["_id"], 1)
|
||||
tst.AssertEqual(t, res["name"], 1)
|
||||
tst.AssertEqual(t, res["age"], 1)
|
||||
}
|
||||
|
||||
func TestProjectionFromStructIgnoresUntagged(t *testing.T) {
|
||||
type model struct {
|
||||
Tagged string `bson:"tagged"`
|
||||
Untagged string
|
||||
Other int `json:"other"`
|
||||
}
|
||||
|
||||
res := ProjectionFromStruct(model{})
|
||||
|
||||
tst.AssertEqual(t, len(res), 1)
|
||||
tst.AssertEqual(t, res["tagged"], 1)
|
||||
|
||||
if _, ok := res["Untagged"]; ok {
|
||||
t.Errorf("untagged field should not be in projection")
|
||||
}
|
||||
if _, ok := res["Other"]; ok {
|
||||
t.Errorf("non-bson-tagged field should not be in projection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectionFromStructWithOptions(t *testing.T) {
|
||||
type model struct {
|
||||
ID string `bson:"_id,omitempty"`
|
||||
Name string `bson:"name,omitempty"`
|
||||
Slug string `bson:"slug,inline"`
|
||||
}
|
||||
|
||||
res := ProjectionFromStruct(model{})
|
||||
|
||||
tst.AssertEqual(t, len(res), 3)
|
||||
tst.AssertEqual(t, res["_id"], 1)
|
||||
tst.AssertEqual(t, res["name"], 1)
|
||||
tst.AssertEqual(t, res["slug"], 1)
|
||||
}
|
||||
|
||||
func TestProjectionFromStructEmpty(t *testing.T) {
|
||||
type empty struct{}
|
||||
|
||||
res := ProjectionFromStruct(empty{})
|
||||
|
||||
tst.AssertEqual(t, len(res), 0)
|
||||
}
|
||||
|
||||
func TestProjectionFromStructPointerValues(t *testing.T) {
|
||||
type model struct {
|
||||
Name *string `bson:"name"`
|
||||
Tags []int `bson:"tags"`
|
||||
}
|
||||
|
||||
res := ProjectionFromStruct(model{})
|
||||
|
||||
tst.AssertEqual(t, len(res), 2)
|
||||
tst.AssertEqual(t, res["name"], 1)
|
||||
tst.AssertEqual(t, res["tags"], 1)
|
||||
}
|
||||
|
||||
func TestProjectionFromStructAllSkipped(t *testing.T) {
|
||||
type model struct {
|
||||
A string
|
||||
B int
|
||||
C bool
|
||||
}
|
||||
|
||||
res := ProjectionFromStruct(model{})
|
||||
|
||||
tst.AssertEqual(t, len(res), 0)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package mongoext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestCreateGoExtBsonRegistryNotNil(t *testing.T) {
|
||||
reg := CreateGoExtBsonRegistry()
|
||||
if reg == nil {
|
||||
t.Fatal("registry should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateGoExtBsonRegistryEmbeddedDocumentDecodesAsBsonM(t *testing.T) {
|
||||
reg := CreateGoExtBsonRegistry()
|
||||
|
||||
doc := bson.M{
|
||||
"name": "alice",
|
||||
"nested": bson.M{
|
||||
"key": "value",
|
||||
"num": int32(42),
|
||||
},
|
||||
}
|
||||
|
||||
raw, err := bson.Marshal(doc)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
dec := bson.NewDecoder(bson.NewDocumentReader(bytes.NewReader(raw)))
|
||||
dec.SetRegistry(reg)
|
||||
|
||||
var decoded map[string]any
|
||||
err = dec.Decode(&decoded)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
nested, ok := decoded["nested"].(bson.M)
|
||||
if !ok {
|
||||
t.Fatalf("expected nested to be bson.M, got %T", decoded["nested"])
|
||||
}
|
||||
|
||||
tst.AssertEqual(t, nested["key"].(string), "value")
|
||||
}
|
||||
|
||||
func TestCreateGoExtBsonRegistryStructFieldOfTypeAny(t *testing.T) {
|
||||
reg := CreateGoExtBsonRegistry()
|
||||
|
||||
type wrapper struct {
|
||||
Payload any `bson:"payload"`
|
||||
}
|
||||
|
||||
source := wrapper{Payload: bson.M{"x": "y"}}
|
||||
raw, err := bson.Marshal(source)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
dec := bson.NewDecoder(bson.NewDocumentReader(bytes.NewReader(raw)))
|
||||
dec.SetRegistry(reg)
|
||||
|
||||
var decoded wrapper
|
||||
err = dec.Decode(&decoded)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
payload, ok := decoded.Payload.(bson.M)
|
||||
if !ok {
|
||||
t.Fatalf("expected Payload to be bson.M, got %T", decoded.Payload)
|
||||
}
|
||||
tst.AssertEqual(t, payload["x"].(string), "y")
|
||||
}
|
||||
|
||||
func TestCreateGoExtBsonRegistryReturnsIndependentInstances(t *testing.T) {
|
||||
r1 := CreateGoExtBsonRegistry()
|
||||
r2 := CreateGoExtBsonRegistry()
|
||||
|
||||
if r1 == r2 {
|
||||
t.Error("expected each call to return a new registry instance")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package pagination
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
func TestCreateFilterReturnsNonNil(t *testing.T) {
|
||||
f := CreateFilter(mongo.Pipeline{}, bson.D{})
|
||||
if f == nil {
|
||||
t.Fatal("expected non-nil MongoFilter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFilterPreservesPipeline(t *testing.T) {
|
||||
pipeline := mongo.Pipeline{
|
||||
bson.D{{Key: "$match", Value: bson.D{{Key: "active", Value: true}}}},
|
||||
bson.D{{Key: "$limit", Value: 10}},
|
||||
}
|
||||
f := CreateFilter(pipeline, bson.D{})
|
||||
|
||||
got := f.FilterQuery(context.Background())
|
||||
if len(got) != len(pipeline) {
|
||||
t.Fatalf("expected pipeline len %d, got %d", len(pipeline), len(got))
|
||||
}
|
||||
for i := range pipeline {
|
||||
if len(got[i]) != len(pipeline[i]) {
|
||||
t.Errorf("stage %d: expected len %d, got %d", i, len(pipeline[i]), len(got[i]))
|
||||
}
|
||||
for j, e := range pipeline[i] {
|
||||
if got[i][j].Key != e.Key {
|
||||
t.Errorf("stage %d field %d: expected key %q, got %q", i, j, e.Key, got[i][j].Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFilterPreservesSort(t *testing.T) {
|
||||
sort := bson.D{
|
||||
{Key: "createdAt", Value: -1},
|
||||
{Key: "_id", Value: 1},
|
||||
}
|
||||
f := CreateFilter(mongo.Pipeline{}, sort)
|
||||
|
||||
got := f.Sort(context.Background())
|
||||
if len(got) != len(sort) {
|
||||
t.Fatalf("expected sort len %d, got %d", len(sort), len(got))
|
||||
}
|
||||
for i, e := range sort {
|
||||
if got[i].Key != e.Key {
|
||||
t.Errorf("field %d: expected key %q, got %q", i, e.Key, got[i].Key)
|
||||
}
|
||||
if got[i].Value != e.Value {
|
||||
t.Errorf("field %d: expected value %v, got %v", i, e.Value, got[i].Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFilterEmptyInputs(t *testing.T) {
|
||||
f := CreateFilter(mongo.Pipeline{}, bson.D{})
|
||||
|
||||
if got := f.FilterQuery(context.Background()); len(got) != 0 {
|
||||
t.Errorf("expected empty pipeline, got len %d", len(got))
|
||||
}
|
||||
if got := f.Sort(context.Background()); len(got) != 0 {
|
||||
t.Errorf("expected empty sort, got len %d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFilterNilInputs(t *testing.T) {
|
||||
f := CreateFilter(nil, nil)
|
||||
|
||||
if got := f.FilterQuery(context.Background()); got != nil {
|
||||
t.Errorf("expected nil pipeline, got %v", got)
|
||||
}
|
||||
if got := f.Sort(context.Background()); got != nil {
|
||||
t.Errorf("expected nil sort, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFilterImplementsMongoFilter(t *testing.T) {
|
||||
var _ MongoFilter = CreateFilter(mongo.Pipeline{}, bson.D{})
|
||||
}
|
||||
|
||||
func TestCreateFilterIgnoresContext(t *testing.T) {
|
||||
pipeline := mongo.Pipeline{bson.D{{Key: "$count", Value: "n"}}}
|
||||
sort := bson.D{{Key: "x", Value: 1}}
|
||||
f := CreateFilter(pipeline, sort)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
if got := f.FilterQuery(ctx); len(got) != 1 {
|
||||
t.Errorf("expected pipeline len 1 even with cancelled ctx, got %d", len(got))
|
||||
}
|
||||
if got := f.Sort(ctx); len(got) != 1 {
|
||||
t.Errorf("expected sort len 1 even with cancelled ctx, got %d", len(got))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package pagination
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCalcPaginationTotalPagesZeroItems(t *testing.T) {
|
||||
if got := CalcPaginationTotalPages(0, 10); got != 0 {
|
||||
t.Errorf("expected 0, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPaginationTotalPagesZeroLimit(t *testing.T) {
|
||||
if got := CalcPaginationTotalPages(100, 0); got != 0 {
|
||||
t.Errorf("expected 0, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPaginationTotalPagesZeroBoth(t *testing.T) {
|
||||
if got := CalcPaginationTotalPages(0, 0); got != 0 {
|
||||
t.Errorf("expected 0, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPaginationTotalPagesExactMultiple(t *testing.T) {
|
||||
if got := CalcPaginationTotalPages(100, 10); got != 10 {
|
||||
t.Errorf("expected 10, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPaginationTotalPagesPartialLastPage(t *testing.T) {
|
||||
if got := CalcPaginationTotalPages(101, 10); got != 11 {
|
||||
t.Errorf("expected 11, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPaginationTotalPagesSingleItem(t *testing.T) {
|
||||
if got := CalcPaginationTotalPages(1, 10); got != 1 {
|
||||
t.Errorf("expected 1, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPaginationTotalPagesItemsLessThanLimit(t *testing.T) {
|
||||
if got := CalcPaginationTotalPages(5, 10); got != 1 {
|
||||
t.Errorf("expected 1, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPaginationTotalPagesLimitOfOne(t *testing.T) {
|
||||
if got := CalcPaginationTotalPages(7, 1); got != 7 {
|
||||
t.Errorf("expected 7, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPaginationTotalPagesItemsEqualLimit(t *testing.T) {
|
||||
if got := CalcPaginationTotalPages(10, 10); got != 1 {
|
||||
t.Errorf("expected 1, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPaginationTotalPagesLargeNumbers(t *testing.T) {
|
||||
if got := CalcPaginationTotalPages(1_000_000, 250); got != 4000 {
|
||||
t.Errorf("expected 4000, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPaginationTotalPagesOneMoreThanMultiple(t *testing.T) {
|
||||
if got := CalcPaginationTotalPages(11, 5); got != 3 {
|
||||
t.Errorf("expected 3, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaginationStructFields(t *testing.T) {
|
||||
p := Pagination{
|
||||
Page: 2,
|
||||
Limit: 25,
|
||||
TotalPages: 4,
|
||||
TotalItems: 100,
|
||||
CurrentPageCount: 25,
|
||||
}
|
||||
if p.Page != 2 || p.Limit != 25 || p.TotalPages != 4 || p.TotalItems != 100 || p.CurrentPageCount != 25 {
|
||||
t.Errorf("unexpected pagination struct values: %+v", p)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package reflectext
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type aliasInt int
|
||||
type aliasString string
|
||||
type aliasFloat float64
|
||||
type aliasBool bool
|
||||
|
||||
type aliasIntSlice []int
|
||||
type aliasStringMap map[string]int
|
||||
type aliasArr [3]int
|
||||
type aliasIntPtr *int
|
||||
|
||||
type myStruct struct {
|
||||
A int
|
||||
B string
|
||||
}
|
||||
|
||||
func TestUnderlying_Primitives(t *testing.T) {
|
||||
cases := []struct {
|
||||
in reflect.Type
|
||||
want reflect.Kind
|
||||
}{
|
||||
{reflect.TypeFor[aliasInt](), reflect.Int},
|
||||
{reflect.TypeFor[aliasString](), reflect.String},
|
||||
{reflect.TypeFor[aliasFloat](), reflect.Float64},
|
||||
{reflect.TypeFor[aliasBool](), reflect.Bool},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := Underlying(c.in)
|
||||
if got.Kind() != c.want {
|
||||
t.Errorf("Underlying(%v).Kind() = %v, want %v", c.in, got.Kind(), c.want)
|
||||
}
|
||||
if got.Name() != "" && got != reflectBasicTypes[c.want] {
|
||||
t.Errorf("Underlying(%v) was not the basic type", c.in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderlying_UnnamedReturnsSelf(t *testing.T) {
|
||||
t1 := reflect.TypeFor[[]int]()
|
||||
got := Underlying(t1)
|
||||
if got != t1 {
|
||||
t.Errorf("Underlying of unnamed slice should be itself")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderlying_Slice(t *testing.T) {
|
||||
t1 := reflect.TypeFor[aliasIntSlice]()
|
||||
got := Underlying(t1)
|
||||
if got.Kind() != reflect.Slice {
|
||||
t.Errorf("expected slice kind, got %v", got.Kind())
|
||||
}
|
||||
if got.Elem().Kind() != reflect.Int {
|
||||
t.Errorf("expected element of int, got %v", got.Elem().Kind())
|
||||
}
|
||||
if got.Name() != "" {
|
||||
t.Errorf("underlying type should be unnamed, got name %q", got.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderlying_Map(t *testing.T) {
|
||||
t1 := reflect.TypeFor[aliasStringMap]()
|
||||
got := Underlying(t1)
|
||||
if got.Kind() != reflect.Map {
|
||||
t.Errorf("expected map kind, got %v", got.Kind())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderlying_Array(t *testing.T) {
|
||||
t1 := reflect.TypeFor[aliasArr]()
|
||||
got := Underlying(t1)
|
||||
if got.Kind() != reflect.Array {
|
||||
t.Errorf("expected array kind, got %v", got.Kind())
|
||||
}
|
||||
if got.Len() != 3 {
|
||||
t.Errorf("expected array len 3, got %d", got.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderlying_Pointer(t *testing.T) {
|
||||
t1 := reflect.TypeFor[aliasIntPtr]()
|
||||
got := Underlying(t1)
|
||||
if got.Kind() != reflect.Pointer {
|
||||
t.Errorf("expected pointer kind, got %v", got.Kind())
|
||||
}
|
||||
if got.Elem().Kind() != reflect.Int {
|
||||
t.Errorf("expected element kind int, got %v", got.Elem().Kind())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderlying_Func(t *testing.T) {
|
||||
type fnType func(a int, b string) (bool, error)
|
||||
t1 := reflect.TypeFor[fnType]()
|
||||
got := Underlying(t1)
|
||||
if got.Kind() != reflect.Func {
|
||||
t.Errorf("expected func kind, got %v", got.Kind())
|
||||
}
|
||||
if got.NumIn() != 2 || got.NumOut() != 2 {
|
||||
t.Errorf("unexpected in/out count: %d/%d", got.NumIn(), got.NumOut())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryCast_AliasToBase(t *testing.T) {
|
||||
v := aliasInt(42)
|
||||
got, ok := TryCast[int](v)
|
||||
if !ok {
|
||||
t.Errorf("expected ok cast")
|
||||
}
|
||||
if got != 42 {
|
||||
t.Errorf("expected 42, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryCast_SameType(t *testing.T) {
|
||||
got, ok := TryCast[int](42)
|
||||
if !ok {
|
||||
t.Errorf("expected ok cast")
|
||||
}
|
||||
if got != 42 {
|
||||
t.Errorf("expected 42, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryCast_StringSameType(t *testing.T) {
|
||||
got, ok := TryCast[string]("hello")
|
||||
if !ok {
|
||||
t.Errorf("expected ok cast")
|
||||
}
|
||||
if got != "hello" {
|
||||
t.Errorf("expected hello, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryCast_IncompatibleTypes(t *testing.T) {
|
||||
_, ok := TryCast[string](aliasInt(42))
|
||||
if ok {
|
||||
t.Errorf("expected fail cast int->string")
|
||||
}
|
||||
|
||||
_, ok = TryCast[int](aliasString("foo"))
|
||||
if ok {
|
||||
t.Errorf("expected fail cast string->int")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryCastType_AliasToBase(t *testing.T) {
|
||||
v := aliasInt(42)
|
||||
res, ok := TryCastType(v, reflect.TypeFor[int]())
|
||||
if !ok {
|
||||
t.Errorf("expected ok cast")
|
||||
}
|
||||
if i, isInt := res.(int); !isInt || i != 42 {
|
||||
t.Errorf("expected int(42), got %T:%v", res, res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryCastType_BaseToAlias(t *testing.T) {
|
||||
res, ok := TryCastType(42, reflect.TypeFor[aliasInt]())
|
||||
if !ok {
|
||||
t.Errorf("expected ok cast")
|
||||
}
|
||||
if i, isAlias := res.(aliasInt); !isAlias || i != aliasInt(42) {
|
||||
t.Errorf("expected aliasInt(42), got %T:%v", res, res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryCastType_Incompatible(t *testing.T) {
|
||||
_, ok := TryCastType("hello", reflect.TypeFor[int]())
|
||||
if ok {
|
||||
t.Errorf("expected fail cast string->int")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderlying_Struct(t *testing.T) {
|
||||
t1 := reflect.TypeFor[myStruct]()
|
||||
got := Underlying(t1)
|
||||
if got.Kind() != reflect.Struct {
|
||||
t.Errorf("expected struct kind, got %v", got.Kind())
|
||||
}
|
||||
if got.NumField() != 2 {
|
||||
t.Errorf("expected 2 fields, got %d", got.NumField())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
package reflectext
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestPSS_String(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
s, err := pss.ValueToString("hello")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if s != "hello" {
|
||||
t.Errorf("expected hello, got %q", s)
|
||||
}
|
||||
|
||||
v, err := pss.ValueFromString("world", reflect.TypeFor[string]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if v.(string) != "world" {
|
||||
t.Errorf("expected world, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_Int(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
s, err := pss.ValueToString(42)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if s != "42" {
|
||||
t.Errorf("expected 42, got %q", s)
|
||||
}
|
||||
|
||||
v, err := pss.ValueFromString("42", reflect.TypeFor[int]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if v.(int) != 42 {
|
||||
t.Errorf("expected 42, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_Int64(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
s, err := pss.ValueToString(int64(-100))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if s != "-100" {
|
||||
t.Errorf("expected -100, got %q", s)
|
||||
}
|
||||
|
||||
v, err := pss.ValueFromString("-100", reflect.TypeFor[int64]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if v.(int64) != -100 {
|
||||
t.Errorf("expected -100, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_Uint(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
s, err := pss.ValueToString(uint(123))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if s != "123" {
|
||||
t.Errorf("expected 123, got %q", s)
|
||||
}
|
||||
|
||||
v, err := pss.ValueFromString("123", reflect.TypeFor[uint]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if v.(uint) != 123 {
|
||||
t.Errorf("expected 123, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_Float(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
s, err := pss.ValueToString(3.14)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if s != "3.14" {
|
||||
t.Errorf("expected 3.14, got %q", s)
|
||||
}
|
||||
|
||||
v, err := pss.ValueFromString("3.14", reflect.TypeFor[float64]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if v.(float64) != 3.14 {
|
||||
t.Errorf("expected 3.14, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_Float32(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
v, err := pss.ValueFromString("1.5", reflect.TypeFor[float32]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if v.(float32) != 1.5 {
|
||||
t.Errorf("expected 1.5, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_Bool(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
s, err := pss.ValueToString(true)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if s != "true" {
|
||||
t.Errorf("expected true, got %q", s)
|
||||
}
|
||||
|
||||
s, err = pss.ValueToString(false)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if s != "false" {
|
||||
t.Errorf("expected false, got %q", s)
|
||||
}
|
||||
|
||||
v, err := pss.ValueFromString("true", reflect.TypeFor[bool]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if v.(bool) != true {
|
||||
t.Errorf("expected true, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_BoolInvalid(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
_, err := pss.ValueFromString("notabool", reflect.TypeFor[bool]())
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid bool")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_IntInvalid(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
_, err := pss.ValueFromString("notanint", reflect.TypeFor[int]())
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid int")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_FloatInvalid(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
_, err := pss.ValueFromString("notafloat", reflect.TypeFor[float64]())
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid float")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_Time(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
tm := time.Date(2023, 4, 5, 12, 30, 45, 0, time.UTC)
|
||||
s, err := pss.ValueToString(tm)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
v, err := pss.ValueFromString(s, reflect.TypeFor[time.Time]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !v.(time.Time).Equal(tm) {
|
||||
t.Errorf("expected %v, got %v", tm, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_TimeInvalid(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
_, err := pss.ValueFromString("not-a-time", reflect.TypeFor[time.Time]())
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid time")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_ObjectID(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
oid := bson.NewObjectID()
|
||||
s, err := pss.ValueToString(oid)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if s != oid.Hex() {
|
||||
t.Errorf("expected %v, got %v", oid.Hex(), s)
|
||||
}
|
||||
|
||||
v, err := pss.ValueFromString(oid.Hex(), reflect.TypeFor[bson.ObjectID]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if v.(bson.ObjectID) != oid {
|
||||
t.Errorf("expected %v, got %v", oid, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_ObjectIDInvalid(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
_, err := pss.ValueFromString("not-a-hex-id", reflect.TypeFor[bson.ObjectID]())
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid object id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_PointerNil(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
var p *int
|
||||
s, err := pss.ValueToString(p)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if s != "" {
|
||||
t.Errorf("expected empty string, got %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_PointerSet(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
x := 99
|
||||
p := &x
|
||||
s, err := pss.ValueToString(p)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if s != "99" {
|
||||
t.Errorf("expected 99, got %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_FromStringEmptyToPointer(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
v, err := pss.ValueFromString("", reflect.TypeFor[*int]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
pInt, ok := v.(*int)
|
||||
if !ok {
|
||||
t.Fatalf("expected *int, got %T", v)
|
||||
}
|
||||
if pInt != nil {
|
||||
t.Errorf("expected nil pointer, got %v", *pInt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_FromStringPointer(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
v, err := pss.ValueFromString("55", reflect.TypeFor[*int]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
pInt, ok := v.(*int)
|
||||
if !ok {
|
||||
t.Fatalf("expected *int, got %T", v)
|
||||
}
|
||||
if pInt == nil || *pInt != 55 {
|
||||
t.Errorf("expected pointer to 55, got %v", pInt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_FromStringEmptyToInt(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
v, err := pss.ValueFromString("", reflect.TypeFor[int]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if v.(int) != 0 {
|
||||
t.Errorf("expected 0, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
type psAliasInt int
|
||||
type psAliasString string
|
||||
|
||||
func TestPSS_AliasToString(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
v := psAliasInt(77)
|
||||
s, err := pss.ValueToString(v)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if s != "77" {
|
||||
t.Errorf("expected 77, got %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_AliasFromString(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
v, err := pss.ValueFromString("77", reflect.TypeFor[psAliasInt]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if v.(psAliasInt) != psAliasInt(77) {
|
||||
t.Errorf("expected 77, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_AliasStringToString(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
v := psAliasString("hello")
|
||||
s, err := pss.ValueToString(v)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if s != "hello" {
|
||||
t.Errorf("expected hello, got %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_AliasStringFromString(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
v, err := pss.ValueFromString("hello", reflect.TypeFor[psAliasString]())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if v.(psAliasString) != psAliasString("hello") {
|
||||
t.Errorf("expected hello, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSS_UnknownTypeToString(t *testing.T) {
|
||||
pss := PrimitiveStringSerializer{}
|
||||
|
||||
type unknownStruct struct{ X int }
|
||||
_, err := pss.ValueToString(unknownStruct{X: 1})
|
||||
if err == nil {
|
||||
t.Errorf("expected error for unknown type")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package rext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestW(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`\d+`))
|
||||
tst.AssertTrue(t, r != nil)
|
||||
tst.AssertEqual(t, r.String(), `\d+`)
|
||||
}
|
||||
|
||||
func TestIsMatchTrue(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`\d+`))
|
||||
tst.AssertTrue(t, r.IsMatch("abc 123 def"))
|
||||
}
|
||||
|
||||
func TestIsMatchFalse(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`\d+`))
|
||||
tst.AssertFalse(t, r.IsMatch("abc def"))
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`^foo(bar)?$`))
|
||||
tst.AssertEqual(t, r.String(), `^foo(bar)?$`)
|
||||
}
|
||||
|
||||
func TestGroupCountWrapper(t *testing.T) {
|
||||
r0 := W(regexp.MustCompile(`abc`))
|
||||
tst.AssertEqual(t, r0.GroupCount(), 0)
|
||||
|
||||
r1 := W(regexp.MustCompile(`(a)(b)(c)`))
|
||||
tst.AssertEqual(t, r1.GroupCount(), 3)
|
||||
|
||||
r2 := W(regexp.MustCompile(`(?P<x>\d+)-(?P<y>\d+)`))
|
||||
tst.AssertEqual(t, r2.GroupCount(), 2)
|
||||
}
|
||||
|
||||
func TestMatchFirstFound(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`(\d+)-(\d+)`))
|
||||
m, ok := r.MatchFirst("a 12-34 b 56-78 c")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, m.FullMatch().Value(), "12-34")
|
||||
tst.AssertEqual(t, m.GroupByIndex(1).Value(), "12")
|
||||
tst.AssertEqual(t, m.GroupByIndex(2).Value(), "34")
|
||||
}
|
||||
|
||||
func TestMatchFirstNotFound(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`\d+`))
|
||||
m, ok := r.MatchFirst("nothing here")
|
||||
tst.AssertFalse(t, ok)
|
||||
// zero-value match should be returned
|
||||
tst.AssertEqual(t, len(m.submatchesIndex), 0)
|
||||
}
|
||||
|
||||
func TestMatchAllMultiple(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`\d+`))
|
||||
matches := r.MatchAll("a 1 b 22 c 333")
|
||||
tst.AssertEqual(t, len(matches), 3)
|
||||
tst.AssertEqual(t, matches[0].FullMatch().Value(), "1")
|
||||
tst.AssertEqual(t, matches[1].FullMatch().Value(), "22")
|
||||
tst.AssertEqual(t, matches[2].FullMatch().Value(), "333")
|
||||
}
|
||||
|
||||
func TestMatchAllNone(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`\d+`))
|
||||
matches := r.MatchAll("abc")
|
||||
tst.AssertEqual(t, len(matches), 0)
|
||||
}
|
||||
|
||||
func TestMatchAllSingle(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`(?P<num>\d+)`))
|
||||
matches := r.MatchAll("only 42 here")
|
||||
tst.AssertEqual(t, len(matches), 1)
|
||||
tst.AssertEqual(t, matches[0].GroupByName("num").Value(), "42")
|
||||
}
|
||||
|
||||
func TestReplaceAllNonLiteralExpansion(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`(\w+)@(\w+)`))
|
||||
out := r.ReplaceAll("hi alice@example, hi bob@example", "$2/$1", false)
|
||||
tst.AssertEqual(t, out, "hi example/alice, hi example/bob")
|
||||
}
|
||||
|
||||
func TestReplaceAllLiteralNoExpansion(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`(\w+)@(\w+)`))
|
||||
out := r.ReplaceAll("hi alice@example", "$2/$1", true)
|
||||
tst.AssertEqual(t, out, "hi $2/$1")
|
||||
}
|
||||
|
||||
func TestReplaceAllFunc(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`\d+`))
|
||||
out := r.ReplaceAllFunc("a1 b22 c333", func(s string) string {
|
||||
return strings.Repeat("x", len(s))
|
||||
})
|
||||
tst.AssertEqual(t, out, "ax bxx cxxx")
|
||||
}
|
||||
|
||||
func TestRemoveAll(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`\s+`))
|
||||
out := r.RemoveAll(" hello world ")
|
||||
tst.AssertEqual(t, out, "helloworld")
|
||||
}
|
||||
|
||||
func TestRemoveAllNoMatch(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`\d+`))
|
||||
out := r.RemoveAll("no digits here")
|
||||
tst.AssertEqual(t, out, "no digits here")
|
||||
}
|
||||
|
||||
func TestRemoveAllDoesNotExpandPlaceholders(t *testing.T) {
|
||||
// removal uses literal replacement, so no expansion happens
|
||||
r := W(regexp.MustCompile(`(\w+)`))
|
||||
out := r.RemoveAll("abc")
|
||||
tst.AssertEqual(t, out, "")
|
||||
}
|
||||
|
||||
// --- RegexMatch ---
|
||||
|
||||
func TestRegexMatchFullMatch(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`b\w+d`))
|
||||
m, ok := r.MatchFirst("aa beard cc")
|
||||
tst.AssertTrue(t, ok)
|
||||
fm := m.FullMatch()
|
||||
tst.AssertEqual(t, fm.Value(), "beard")
|
||||
tst.AssertEqual(t, fm.Start(), 3)
|
||||
tst.AssertEqual(t, fm.End(), 8)
|
||||
}
|
||||
|
||||
func TestRegexMatchGroupCount(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`(a)(b)(c)`))
|
||||
m, ok := r.MatchFirst("abc")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, m.GroupCount(), 3)
|
||||
}
|
||||
|
||||
func TestRegexMatchGroupByIndex(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`(\w+)-(\w+)-(\w+)`))
|
||||
m, ok := r.MatchFirst("foo-bar-baz")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, m.GroupByIndex(0).Value(), "foo-bar-baz")
|
||||
tst.AssertEqual(t, m.GroupByIndex(1).Value(), "foo")
|
||||
tst.AssertEqual(t, m.GroupByIndex(2).Value(), "bar")
|
||||
tst.AssertEqual(t, m.GroupByIndex(3).Value(), "baz")
|
||||
}
|
||||
|
||||
func TestRegexMatchGroupByName(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`(?P<first>\w+)\s+(?P<last>\w+)`))
|
||||
m, ok := r.MatchFirst("John Doe")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, m.GroupByName("first").Value(), "John")
|
||||
tst.AssertEqual(t, m.GroupByName("last").Value(), "Doe")
|
||||
}
|
||||
|
||||
func TestRegexMatchGroupByNamePanics(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`(?P<x>\d+)`))
|
||||
m, ok := r.MatchFirst("99")
|
||||
tst.AssertTrue(t, ok)
|
||||
|
||||
defer func() {
|
||||
rec := recover()
|
||||
tst.AssertTrue(t, rec != nil)
|
||||
}()
|
||||
|
||||
_ = m.GroupByName("nonexistent")
|
||||
t.Fatal("expected panic")
|
||||
}
|
||||
|
||||
func TestGroupByNameOrEmptyMissingName(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`(?P<x>\d+)`))
|
||||
m, ok := r.MatchFirst("99")
|
||||
tst.AssertTrue(t, ok)
|
||||
|
||||
g := m.GroupByNameOrEmpty("not-present")
|
||||
tst.AssertTrue(t, g.IsEmpty())
|
||||
tst.AssertFalse(t, g.Exists())
|
||||
tst.AssertEqual(t, g.ValueOrEmpty(), "")
|
||||
tst.AssertPtrEqual(t, g.ValueOrNil(), nil)
|
||||
}
|
||||
|
||||
// --- RegexMatchGroup ---
|
||||
|
||||
func TestRegexMatchGroupAccessors(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`(\w+)`))
|
||||
m, ok := r.MatchFirst(" hello ")
|
||||
tst.AssertTrue(t, ok)
|
||||
|
||||
g := m.GroupByIndex(1)
|
||||
tst.AssertEqual(t, g.Value(), "hello")
|
||||
tst.AssertEqual(t, g.Start(), 2)
|
||||
tst.AssertEqual(t, g.End(), 7)
|
||||
s, e := g.Range()
|
||||
tst.AssertEqual(t, s, 2)
|
||||
tst.AssertEqual(t, e, 7)
|
||||
tst.AssertEqual(t, g.Length(), 5)
|
||||
}
|
||||
|
||||
// --- OptRegexMatchGroup ---
|
||||
|
||||
func TestOptRegexMatchGroupExisting(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`(?P<word>\w+)`))
|
||||
m, ok := r.MatchFirst(" hello ")
|
||||
tst.AssertTrue(t, ok)
|
||||
|
||||
g := m.GroupByNameOrEmpty("word")
|
||||
tst.AssertTrue(t, g.Exists())
|
||||
tst.AssertFalse(t, g.IsEmpty())
|
||||
tst.AssertEqual(t, g.Value(), "hello")
|
||||
tst.AssertEqual(t, g.ValueOrEmpty(), "hello")
|
||||
tst.AssertEqual(t, *g.ValueOrNil(), "hello")
|
||||
tst.AssertEqual(t, g.Start(), 2)
|
||||
tst.AssertEqual(t, g.End(), 7)
|
||||
s, e := g.Range()
|
||||
tst.AssertEqual(t, s, 2)
|
||||
tst.AssertEqual(t, e, 7)
|
||||
tst.AssertEqual(t, g.Length(), 5)
|
||||
}
|
||||
|
||||
func TestOptRegexMatchGroupOptionalNotMatched(t *testing.T) {
|
||||
// group2 is optional and won't match; group1 will
|
||||
r := W(regexp.MustCompile(`(?P<group1>A+)(?P<group2>B+)?`))
|
||||
m, ok := r.MatchFirst("AAA")
|
||||
tst.AssertTrue(t, ok)
|
||||
|
||||
g1 := m.GroupByNameOrEmpty("group1")
|
||||
tst.AssertTrue(t, g1.Exists())
|
||||
tst.AssertEqual(t, g1.ValueOrEmpty(), "AAA")
|
||||
|
||||
g2 := m.GroupByNameOrEmpty("group2")
|
||||
tst.AssertTrue(t, g2.IsEmpty())
|
||||
tst.AssertFalse(t, g2.Exists())
|
||||
tst.AssertEqual(t, g2.ValueOrEmpty(), "")
|
||||
tst.AssertPtrEqual(t, g2.ValueOrNil(), nil)
|
||||
}
|
||||
|
||||
// --- Misc combined behavior ---
|
||||
|
||||
func TestMultipleNamedGroupsAcrossMatches(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`(?P<k>\w+)=(?P<v>\d+)`))
|
||||
matches := r.MatchAll("a=1 b=22 c=333")
|
||||
tst.AssertEqual(t, len(matches), 3)
|
||||
|
||||
tst.AssertEqual(t, matches[0].GroupByName("k").Value(), "a")
|
||||
tst.AssertEqual(t, matches[0].GroupByName("v").Value(), "1")
|
||||
tst.AssertEqual(t, matches[1].GroupByName("k").Value(), "b")
|
||||
tst.AssertEqual(t, matches[1].GroupByName("v").Value(), "22")
|
||||
tst.AssertEqual(t, matches[2].GroupByName("k").Value(), "c")
|
||||
tst.AssertEqual(t, matches[2].GroupByName("v").Value(), "333")
|
||||
}
|
||||
|
||||
func TestEmptyMatchHaystack(t *testing.T) {
|
||||
r := W(regexp.MustCompile(`.*`))
|
||||
tst.AssertTrue(t, r.IsMatch(""))
|
||||
m, ok := r.MatchFirst("")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, m.FullMatch().Value(), "")
|
||||
tst.AssertEqual(t, m.FullMatch().Length(), 0)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package rfctime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
)
|
||||
|
||||
func TestDateString(t *testing.T) {
|
||||
d := Date{Year: 2023, Month: 5, Day: 7}
|
||||
tst.AssertEqual(t, d.String(), "2023-05-07")
|
||||
tst.AssertEqual(t, d.Serialize(), "2023-05-07")
|
||||
tst.AssertEqual(t, d.GoString(), "rfctime.Date{Year: 2023, Month: 5, Day: 7}")
|
||||
tst.AssertEqual(t, d.FormatStr(), "2006-01-02")
|
||||
}
|
||||
|
||||
func TestDateIsZero(t *testing.T) {
|
||||
tst.AssertEqual(t, Date{}.IsZero(), true)
|
||||
tst.AssertEqual(t, Date{Year: 1, Month: 1, Day: 1}.IsZero(), false)
|
||||
}
|
||||
|
||||
func TestDateNew(t *testing.T) {
|
||||
tm := time.Date(2023, 5, 7, 12, 30, 0, 0, time.UTC)
|
||||
d := NewDate(tm)
|
||||
tst.AssertEqual(t, d.Year, 2023)
|
||||
tst.AssertEqual(t, d.Month, 5)
|
||||
tst.AssertEqual(t, d.Day, 7)
|
||||
}
|
||||
|
||||
func TestDateTimeConversions(t *testing.T) {
|
||||
d := Date{Year: 2023, Month: 5, Day: 7}
|
||||
utc := d.TimeUTC()
|
||||
tst.AssertEqual(t, utc.Year(), 2023)
|
||||
tst.AssertEqual(t, utc.Month(), time.May)
|
||||
tst.AssertEqual(t, utc.Day(), 7)
|
||||
tst.AssertEqual(t, utc.Location(), time.UTC)
|
||||
|
||||
loc := d.TimeLocal()
|
||||
tst.AssertEqual(t, loc.Location(), time.Local)
|
||||
|
||||
custom := d.Time(time.UTC)
|
||||
tst.AssertEqual(t, custom.Hour(), 0)
|
||||
tst.AssertEqual(t, custom.Location(), time.UTC)
|
||||
}
|
||||
|
||||
func TestDateJSON(t *testing.T) {
|
||||
type Wrap struct {
|
||||
D Date `json:"d"`
|
||||
}
|
||||
w1 := Wrap{D: Date{Year: 2023, Month: 5, Day: 7}}
|
||||
b, err := json.Marshal(w1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, string(b), `{"d":"2023-05-07"}`)
|
||||
|
||||
var w2 Wrap
|
||||
if err := json.Unmarshal(b, &w2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, w2.D, w1.D)
|
||||
}
|
||||
|
||||
func TestDateJSONInvalid(t *testing.T) {
|
||||
var d Date
|
||||
if err := d.UnmarshalJSON([]byte(`"not-a-date"`)); err == nil {
|
||||
t.Errorf("expected parse error")
|
||||
}
|
||||
if err := d.UnmarshalJSON([]byte(`123`)); err == nil {
|
||||
t.Errorf("expected json error for number")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateText(t *testing.T) {
|
||||
d := Date{Year: 2023, Month: 5, Day: 7}
|
||||
b, err := d.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, string(b), "2023-05-07")
|
||||
|
||||
var d2 Date
|
||||
if err := d2.UnmarshalText(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, d2, d)
|
||||
|
||||
if err := d2.UnmarshalText([]byte("garbage")); err == nil {
|
||||
t.Errorf("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateBinaryGob(t *testing.T) {
|
||||
d := Date{Year: 2023, Month: 5, Day: 7}
|
||||
|
||||
bin, err := d.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var d2 Date
|
||||
if err := d2.UnmarshalBinary(bin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, d2, d)
|
||||
|
||||
gob, err := d.GobEncode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var d3 Date
|
||||
if err := d3.GobDecode(gob); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, d3, d)
|
||||
}
|
||||
|
||||
func TestDateAccessors(t *testing.T) {
|
||||
d := Date{Year: 2023, Month: 5, Day: 17}
|
||||
y, m, day := d.Date()
|
||||
tst.AssertEqual(t, y, 2023)
|
||||
tst.AssertEqual(t, m, time.May)
|
||||
tst.AssertEqual(t, day, 17)
|
||||
tst.AssertEqual(t, d.Weekday(), time.Wednesday)
|
||||
|
||||
wy, ww := d.ISOWeek()
|
||||
ey, ew := d.TimeUTC().ISOWeek()
|
||||
tst.AssertEqual(t, wy, ey)
|
||||
tst.AssertEqual(t, ww, ew)
|
||||
|
||||
tst.AssertEqual(t, d.YearDay(), d.TimeUTC().YearDay())
|
||||
tst.AssertEqual(t, d.Unix(), d.TimeUTC().Unix())
|
||||
tst.AssertEqual(t, d.UnixMilli(), d.TimeUTC().UnixMilli())
|
||||
tst.AssertEqual(t, d.UnixMicro(), d.TimeUTC().UnixMicro())
|
||||
tst.AssertEqual(t, d.UnixNano(), d.TimeUTC().UnixNano())
|
||||
tst.AssertEqual(t, d.Format("2006/01/02"), "2023/05/17")
|
||||
}
|
||||
|
||||
func TestDateAddDate(t *testing.T) {
|
||||
d := Date{Year: 2023, Month: 5, Day: 17}
|
||||
d2 := d.AddDate(1, 2, 3)
|
||||
tst.AssertEqual(t, d2.Year, 2024)
|
||||
tst.AssertEqual(t, d2.Month, 7)
|
||||
tst.AssertEqual(t, d2.Day, 20)
|
||||
}
|
||||
|
||||
func TestDateParseString(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
ok bool
|
||||
expected Date
|
||||
}{
|
||||
{"2023-05-07", true, Date{2023, 5, 7}},
|
||||
{"0001-01-01", true, Date{1, 1, 1}},
|
||||
{"2023-13-01", false, Date{}}, // bad month
|
||||
{"2023-12-32", false, Date{}}, // bad day
|
||||
{"2023-00-15", false, Date{}}, // month 0
|
||||
{"2023-05", false, Date{}}, // bad format
|
||||
{"2023-05-07-extra", false, Date{}},
|
||||
{"abcd-ef-gh", false, Date{}},
|
||||
{"-1-05-07", false, Date{}}, // negative year
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
var d Date
|
||||
err := d.ParseString(tc.input)
|
||||
if tc.ok {
|
||||
if err != nil {
|
||||
t.Errorf("ParseString(%q) failed: %v", tc.input, err)
|
||||
continue
|
||||
}
|
||||
tst.AssertEqual(t, d, tc.expected)
|
||||
} else if err == nil {
|
||||
t.Errorf("ParseString(%q) should have failed", tc.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNowDate(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
d := NowDate(time.UTC)
|
||||
tst.AssertEqual(t, d.Year, now.Year())
|
||||
tst.AssertEqual(t, d.Month, int(now.Month()))
|
||||
tst.AssertEqual(t, d.Day, now.Day())
|
||||
|
||||
dl := NowDateLoc()
|
||||
if dl.Year < 1970 {
|
||||
t.Errorf("NowDateLoc returned implausible year: %d", dl.Year)
|
||||
}
|
||||
|
||||
du := NowDateUTC()
|
||||
if du.Year < 1970 {
|
||||
t.Errorf("NowDateUTC returned implausible year: %d", du.Year)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package rfctime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/timeext"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
)
|
||||
|
||||
func TestRFC3339TimeRoundtripJSON(t *testing.T) {
|
||||
type Wrap struct {
|
||||
Value RFC3339Time `json:"v"`
|
||||
}
|
||||
|
||||
val := NewRFC3339(time.Unix(1675951556, 0).In(timeext.TimezoneBerlin))
|
||||
w1 := Wrap{val}
|
||||
|
||||
jstr1, err := json.Marshal(w1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(jstr1) != "{\"v\":\"2023-02-09T15:05:56+01:00\"}" {
|
||||
t.Errorf("unexpected json: %s", string(jstr1))
|
||||
}
|
||||
|
||||
w2 := Wrap{}
|
||||
if err := json.Unmarshal(jstr1, &w2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
jstr2, err := json.Marshal(w2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tst.AssertEqual(t, string(jstr1), string(jstr2))
|
||||
|
||||
if !w1.Value.EqualAny(w2.Value) {
|
||||
t.Errorf("time differs after roundtrip")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRFC3339TimeUnmarshalJSONInvalid(t *testing.T) {
|
||||
var v RFC3339Time
|
||||
if err := v.UnmarshalJSON([]byte(`"not-a-date"`)); err == nil {
|
||||
t.Errorf("expected error parsing invalid date")
|
||||
}
|
||||
if err := v.UnmarshalJSON([]byte(`12345`)); err == nil {
|
||||
t.Errorf("expected error for non-string json")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRFC3339TimeText(t *testing.T) {
|
||||
val := NewRFC3339(time.Date(2023, 2, 9, 15, 5, 56, 0, time.UTC))
|
||||
b, err := val.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, string(b), "2023-02-09T15:05:56Z")
|
||||
|
||||
var v2 RFC3339Time
|
||||
if err := v2.UnmarshalText(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !v2.Equal(val) {
|
||||
t.Errorf("text roundtrip mismatch")
|
||||
}
|
||||
|
||||
if err := v2.UnmarshalText([]byte("garbage")); err == nil {
|
||||
t.Errorf("expected error on bad text")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRFC3339TimeBinaryAndGob(t *testing.T) {
|
||||
val := NewRFC3339(time.Date(2023, 2, 9, 15, 5, 56, 123, time.UTC))
|
||||
|
||||
bin, err := val.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var v2 RFC3339Time
|
||||
if err := v2.UnmarshalBinary(bin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !v2.Equal(val) {
|
||||
t.Errorf("binary roundtrip mismatch")
|
||||
}
|
||||
|
||||
gob, err := val.GobEncode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var v3 RFC3339Time
|
||||
if err := v3.GobDecode(gob); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !v3.Equal(val) {
|
||||
t.Errorf("gob roundtrip mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRFC3339TimeAccessors(t *testing.T) {
|
||||
loc, _ := time.LoadLocation("UTC")
|
||||
tm := time.Date(2023, 5, 17, 14, 30, 45, 123456789, loc)
|
||||
val := NewRFC3339(tm)
|
||||
|
||||
tst.AssertEqual(t, val.Year(), 2023)
|
||||
tst.AssertEqual(t, val.Month(), time.May)
|
||||
tst.AssertEqual(t, val.Day(), 17)
|
||||
tst.AssertEqual(t, val.Hour(), 14)
|
||||
tst.AssertEqual(t, val.Minute(), 30)
|
||||
tst.AssertEqual(t, val.Second(), 45)
|
||||
tst.AssertEqual(t, val.Nanosecond(), 123456789)
|
||||
tst.AssertEqual(t, val.Weekday(), time.Wednesday)
|
||||
tst.AssertEqual(t, val.YearDay(), tm.YearDay())
|
||||
tst.AssertEqual(t, val.Unix(), tm.Unix())
|
||||
tst.AssertEqual(t, val.UnixMilli(), tm.UnixMilli())
|
||||
tst.AssertEqual(t, val.UnixMicro(), tm.UnixMicro())
|
||||
tst.AssertEqual(t, val.UnixNano(), tm.UnixNano())
|
||||
tst.AssertEqual(t, val.Location(), loc)
|
||||
tst.AssertEqual(t, val.Format(time.RFC3339), tm.Format(time.RFC3339))
|
||||
tst.AssertEqual(t, val.GoString(), tm.GoString())
|
||||
tst.AssertEqual(t, val.String(), tm.String())
|
||||
tst.AssertEqual(t, val.Serialize(), tm.Format(time.RFC3339))
|
||||
tst.AssertEqual(t, val.FormatStr(), time.RFC3339)
|
||||
|
||||
y, mo, d := val.Date()
|
||||
tst.AssertEqual(t, y, 2023)
|
||||
tst.AssertEqual(t, mo, time.May)
|
||||
tst.AssertEqual(t, d, 17)
|
||||
|
||||
wy, ww := val.ISOWeek()
|
||||
ey, ew := tm.ISOWeek()
|
||||
tst.AssertEqual(t, wy, ey)
|
||||
tst.AssertEqual(t, ww, ew)
|
||||
|
||||
h, m, s := val.Clock()
|
||||
tst.AssertEqual(t, h, 14)
|
||||
tst.AssertEqual(t, m, 30)
|
||||
tst.AssertEqual(t, s, 45)
|
||||
|
||||
tst.AssertEqual(t, val.IsZero(), false)
|
||||
tst.AssertEqual(t, RFC3339Time{}.IsZero(), true)
|
||||
}
|
||||
|
||||
func TestRFC3339TimeAddSub(t *testing.T) {
|
||||
tm := time.Date(2023, 5, 17, 14, 30, 45, 0, time.UTC)
|
||||
a := NewRFC3339(tm)
|
||||
b := a.Add(2 * time.Hour)
|
||||
|
||||
tst.AssertEqual(t, b.Sub(a), 2*time.Hour)
|
||||
tst.AssertEqual(t, b.After(a), true)
|
||||
tst.AssertEqual(t, a.Before(b), true)
|
||||
tst.AssertEqual(t, a.After(b), false)
|
||||
tst.AssertEqual(t, b.Before(a), false)
|
||||
|
||||
c := a.AddDate(1, 2, 3)
|
||||
tst.AssertEqual(t, c.Year(), 2024)
|
||||
tst.AssertEqual(t, c.Month(), time.July)
|
||||
tst.AssertEqual(t, c.Day(), 20)
|
||||
}
|
||||
|
||||
func TestRFC3339TimeEqual(t *testing.T) {
|
||||
tm := time.Date(2023, 5, 17, 14, 30, 45, 0, time.UTC)
|
||||
a := NewRFC3339(tm)
|
||||
b := NewRFC3339(tm)
|
||||
c := NewRFC3339(tm.Add(time.Second))
|
||||
|
||||
tst.AssertEqual(t, a.Equal(b), true)
|
||||
tst.AssertEqual(t, a.Equal(c), false)
|
||||
tst.AssertEqual(t, a.EqualAny(b), true)
|
||||
tst.AssertEqual(t, a.EqualAny(c), false)
|
||||
tst.AssertEqual(t, a.EqualAny(nil), false)
|
||||
// Cross-type comparison via tt()
|
||||
tst.AssertEqual(t, a.EqualAny(NewRFC3339Nano(tm)), true)
|
||||
tst.AssertEqual(t, a.EqualAny(tm), true)
|
||||
}
|
||||
|
||||
func TestRFC3339TimeToNano(t *testing.T) {
|
||||
tm := time.Date(2023, 5, 17, 14, 30, 45, 12345, time.UTC)
|
||||
a := NewRFC3339(tm)
|
||||
n := a.ToNano()
|
||||
tst.AssertEqual(t, n.UnixNano(), tm.UnixNano())
|
||||
}
|
||||
|
||||
func TestNowRFC3339(t *testing.T) {
|
||||
before := time.Now()
|
||||
v := NowRFC3339()
|
||||
after := time.Now()
|
||||
|
||||
if v.Time().Before(before.Add(-time.Second)) || v.Time().After(after.Add(time.Second)) {
|
||||
t.Errorf("NowRFC3339 not within expected range")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package rfctime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
)
|
||||
|
||||
func TestSecondsF64Basics(t *testing.T) {
|
||||
d := NewSecondsF64(2*time.Second + 500*time.Millisecond)
|
||||
tst.AssertEqual(t, d.Duration(), 2500*time.Millisecond)
|
||||
tst.AssertEqual(t, d.Seconds(), 2.5)
|
||||
tst.AssertEqual(t, d.Milliseconds(), int64(2500))
|
||||
tst.AssertEqual(t, d.Microseconds(), int64(2500000))
|
||||
tst.AssertEqual(t, d.Nanoseconds(), int64(2500000000))
|
||||
tst.AssertEqual(t, d.Minutes(), 2.5/60.0)
|
||||
tst.AssertEqual(t, d.Hours(), 2.5/3600.0)
|
||||
tst.AssertEqual(t, d.String(), (2500 * time.Millisecond).String())
|
||||
}
|
||||
|
||||
func TestSecondsF64JSON(t *testing.T) {
|
||||
type Wrap struct {
|
||||
D SecondsF64 `json:"d"`
|
||||
}
|
||||
|
||||
w1 := Wrap{D: NewSecondsF64(2500 * time.Millisecond)}
|
||||
b, err := json.Marshal(w1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, string(b), `{"d":2.5}`)
|
||||
|
||||
var w2 Wrap
|
||||
if err := json.Unmarshal(b, &w2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if math.Abs(float64(w2.D.Duration()-w1.D.Duration())) > float64(time.Microsecond) {
|
||||
t.Errorf("roundtrip mismatch: %v vs %v", w1.D, w2.D)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecondsF64UnmarshalJSONInvalid(t *testing.T) {
|
||||
var d SecondsF64
|
||||
if err := d.UnmarshalJSON([]byte(`"not-a-number"`)); err == nil {
|
||||
t.Errorf("expected error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package rfctime
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
)
|
||||
|
||||
func TestTimeNew(t *testing.T) {
|
||||
v := NewTime(14, 30, 45, 123)
|
||||
tst.AssertEqual(t, v.Hour, 14)
|
||||
tst.AssertEqual(t, v.Minute, 30)
|
||||
tst.AssertEqual(t, v.Second, 45)
|
||||
tst.AssertEqual(t, v.NanoSecond, 123)
|
||||
}
|
||||
|
||||
func TestTimeFromTS(t *testing.T) {
|
||||
tm := time.Date(2023, 5, 17, 14, 30, 45, 123456789, time.UTC)
|
||||
v := NewTimeFromTS(tm)
|
||||
tst.AssertEqual(t, v.Hour, 14)
|
||||
tst.AssertEqual(t, v.Minute, 30)
|
||||
tst.AssertEqual(t, v.Second, 45)
|
||||
tst.AssertEqual(t, v.NanoSecond, 123456789)
|
||||
}
|
||||
|
||||
func TestTimeSerialize(t *testing.T) {
|
||||
v := NewTime(14, 30, 45, 123456789)
|
||||
tst.AssertEqual(t, v.Serialize(), "0014:30:45.123456789")
|
||||
tst.AssertEqual(t, v.String(), "0014:30:45.123456789")
|
||||
tst.AssertEqual(t, v.GoString(), "rfctime.NewTime(14, 30, 45, 123456789)")
|
||||
tst.AssertEqual(t, v.FormatStr(), "15:04:05.999999999")
|
||||
}
|
||||
|
||||
func TestTimeSerializeShort(t *testing.T) {
|
||||
tst.AssertEqual(t, NewTime(14, 30, 0, 0).SerializeShort(), "14:30")
|
||||
tst.AssertEqual(t, NewTime(14, 30, 45, 0).SerializeShort(), "14:30:45")
|
||||
tst.AssertEqual(t, NewTime(14, 30, 45, 123).SerializeShort(), "14:30:45.000000123")
|
||||
tst.AssertEqual(t, NewTime(0, 0, 0, 0).SerializeShort(), "00:00")
|
||||
}
|
||||
|
||||
func TestTimeDeserialize(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
ok bool
|
||||
expected Time
|
||||
}{
|
||||
{"14:30", true, Time{Hour: 14, Minute: 30, Second: 0, NanoSecond: 0}},
|
||||
{"14:30:45", true, Time{Hour: 14, Minute: 30, Second: 45, NanoSecond: 0}},
|
||||
{"14:30:45.123", true, Time{Hour: 14, Minute: 30, Second: 45, NanoSecond: 123000000}},
|
||||
{"14:30:45.123456789", true, Time{Hour: 14, Minute: 30, Second: 45, NanoSecond: 123456789}},
|
||||
{"00:00:00.000000000", true, Time{Hour: 0, Minute: 0, Second: 0, NanoSecond: 0}},
|
||||
{"14", false, Time{}},
|
||||
{"14:30:45.123:extra", false, Time{}},
|
||||
{"ab:cd", false, Time{}},
|
||||
{"14:bb", false, Time{}},
|
||||
{"14:30:cc", false, Time{}},
|
||||
{"14:30:45.zz", false, Time{}},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
var v Time
|
||||
err := v.Deserialize(tc.input)
|
||||
if tc.ok {
|
||||
if err != nil {
|
||||
t.Errorf("Deserialize(%q) failed: %v", tc.input, err)
|
||||
continue
|
||||
}
|
||||
tst.AssertEqual(t, v, tc.expected)
|
||||
} else if err == nil {
|
||||
t.Errorf("Deserialize(%q) should have failed", tc.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNowTime(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
v := NowTime(time.UTC)
|
||||
// Within a couple of seconds
|
||||
if abs(v.Hour-now.Hour()) > 1 && !(now.Hour() == 23 && v.Hour == 0) {
|
||||
t.Errorf("NowTime hour mismatch: %d vs %d", v.Hour, now.Hour())
|
||||
}
|
||||
|
||||
vl := NowTimeLoc()
|
||||
if vl.Hour < 0 || vl.Hour > 23 {
|
||||
t.Errorf("NowTimeLoc invalid hour: %d", vl.Hour)
|
||||
}
|
||||
|
||||
vu := NowTimeUTC()
|
||||
if vu.Hour < 0 || vu.Hour > 23 {
|
||||
t.Errorf("NowTimeUTC invalid hour: %d", vu.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
package rfctime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
)
|
||||
|
||||
func TestUnixTimeRoundtripJSON(t *testing.T) {
|
||||
type Wrap struct {
|
||||
Value UnixTime `json:"v"`
|
||||
}
|
||||
|
||||
val := NewUnix(time.Unix(1675951556, 0).UTC())
|
||||
w1 := Wrap{val}
|
||||
|
||||
jstr1, err := json.Marshal(w1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, string(jstr1), `{"v":"1675951556"}`)
|
||||
|
||||
w2 := Wrap{}
|
||||
if err := json.Unmarshal(jstr1, &w2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, w2.Value.Unix(), val.Unix())
|
||||
}
|
||||
|
||||
func TestUnixTimeUnmarshalJSONInvalid(t *testing.T) {
|
||||
var v UnixTime
|
||||
if err := v.UnmarshalJSON([]byte(`"not-a-number"`)); err == nil {
|
||||
t.Errorf("expected parse error")
|
||||
}
|
||||
if err := v.UnmarshalJSON([]byte(`{}`)); err == nil {
|
||||
t.Errorf("expected json error on object")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnixTimeText(t *testing.T) {
|
||||
val := NewUnix(time.Unix(1675951556, 0))
|
||||
b, err := val.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, string(b), "1675951556")
|
||||
|
||||
var v2 UnixTime
|
||||
if err := v2.UnmarshalText(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, v2.Unix(), val.Unix())
|
||||
|
||||
if err := v2.UnmarshalText([]byte("garbage")); err == nil {
|
||||
t.Errorf("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnixTimeBinaryGob(t *testing.T) {
|
||||
val := NewUnix(time.Date(2023, 5, 17, 14, 30, 45, 0, time.UTC))
|
||||
|
||||
bin, err := val.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var v2 UnixTime
|
||||
if err := v2.UnmarshalBinary(bin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, v2.Unix(), val.Unix())
|
||||
|
||||
gob, err := val.GobEncode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var v3 UnixTime
|
||||
if err := v3.GobDecode(gob); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, v3.Unix(), val.Unix())
|
||||
}
|
||||
|
||||
func TestUnixTimeAccessors(t *testing.T) {
|
||||
tm := time.Date(2023, 5, 17, 14, 30, 45, 12345, time.UTC)
|
||||
val := NewUnix(tm)
|
||||
|
||||
tst.AssertEqual(t, val.Year(), 2023)
|
||||
tst.AssertEqual(t, val.Month(), time.May)
|
||||
tst.AssertEqual(t, val.Day(), 17)
|
||||
tst.AssertEqual(t, val.Hour(), 14)
|
||||
tst.AssertEqual(t, val.Minute(), 30)
|
||||
tst.AssertEqual(t, val.Second(), 45)
|
||||
tst.AssertEqual(t, val.Nanosecond(), 12345)
|
||||
tst.AssertEqual(t, val.Weekday(), time.Wednesday)
|
||||
tst.AssertEqual(t, val.Unix(), tm.Unix())
|
||||
tst.AssertEqual(t, val.UnixMilli(), tm.UnixMilli())
|
||||
tst.AssertEqual(t, val.UnixMicro(), tm.UnixMicro())
|
||||
tst.AssertEqual(t, val.UnixNano(), tm.UnixNano())
|
||||
tst.AssertEqual(t, val.Format(time.RFC3339), tm.Format(time.RFC3339))
|
||||
tst.AssertEqual(t, val.GoString(), tm.GoString())
|
||||
tst.AssertEqual(t, val.String(), tm.String())
|
||||
tst.AssertEqual(t, val.Serialize(), strconv.FormatInt(tm.Unix(), 10))
|
||||
tst.AssertEqual(t, val.IsZero(), false)
|
||||
tst.AssertEqual(t, UnixTime{}.IsZero(), true)
|
||||
|
||||
y, mo, d := val.Date()
|
||||
tst.AssertEqual(t, y, 2023)
|
||||
tst.AssertEqual(t, mo, time.May)
|
||||
tst.AssertEqual(t, d, 17)
|
||||
|
||||
wy, ww := val.ISOWeek()
|
||||
ey, ew := tm.ISOWeek()
|
||||
tst.AssertEqual(t, wy, ey)
|
||||
tst.AssertEqual(t, ww, ew)
|
||||
|
||||
h, m, s := val.Clock()
|
||||
tst.AssertEqual(t, h, 14)
|
||||
tst.AssertEqual(t, m, 30)
|
||||
tst.AssertEqual(t, s, 45)
|
||||
|
||||
tst.AssertEqual(t, val.YearDay(), tm.YearDay())
|
||||
}
|
||||
|
||||
func TestUnixTimeAddSubCompare(t *testing.T) {
|
||||
tm := time.Date(2023, 5, 17, 14, 30, 45, 0, time.UTC)
|
||||
a := NewUnix(tm)
|
||||
b := a.Add(time.Hour)
|
||||
|
||||
tst.AssertEqual(t, b.Sub(a), time.Hour)
|
||||
tst.AssertEqual(t, b.After(a), true)
|
||||
tst.AssertEqual(t, a.Before(b), true)
|
||||
|
||||
c := a.AddDate(0, 1, 0)
|
||||
tst.AssertEqual(t, c.Month(), time.June)
|
||||
|
||||
d := NewUnix(tm)
|
||||
tst.AssertEqual(t, a.Equal(d), true)
|
||||
tst.AssertEqual(t, a.EqualAny(d), true)
|
||||
tst.AssertEqual(t, a.EqualAny(b), false)
|
||||
tst.AssertEqual(t, a.EqualAny(nil), false)
|
||||
}
|
||||
|
||||
func TestNowUnix(t *testing.T) {
|
||||
before := time.Now()
|
||||
v := NowUnix()
|
||||
after := time.Now()
|
||||
|
||||
if v.Time().Before(before.Add(-time.Second)) || v.Time().After(after.Add(time.Second)) {
|
||||
t.Errorf("NowUnix not within expected range")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- UnixMilliTime ----------
|
||||
|
||||
func TestUnixMilliTimeRoundtripJSON(t *testing.T) {
|
||||
type Wrap struct {
|
||||
Value UnixMilliTime `json:"v"`
|
||||
}
|
||||
|
||||
val := NewUnixMilli(time.UnixMilli(1675951556789).UTC())
|
||||
w1 := Wrap{val}
|
||||
|
||||
jstr1, err := json.Marshal(w1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, string(jstr1), `{"v":"1675951556789"}`)
|
||||
|
||||
w2 := Wrap{}
|
||||
if err := json.Unmarshal(jstr1, &w2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, w2.Value.UnixMilli(), val.UnixMilli())
|
||||
}
|
||||
|
||||
func TestUnixMilliTimeText(t *testing.T) {
|
||||
val := NewUnixMilli(time.UnixMilli(1675951556789))
|
||||
b, err := val.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, string(b), "1675951556789")
|
||||
|
||||
var v2 UnixMilliTime
|
||||
if err := v2.UnmarshalText(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, v2.UnixMilli(), val.UnixMilli())
|
||||
}
|
||||
|
||||
func TestUnixMilliTimeUnmarshalJSONInvalid(t *testing.T) {
|
||||
var v UnixMilliTime
|
||||
if err := v.UnmarshalJSON([]byte(`"abc"`)); err == nil {
|
||||
t.Errorf("expected error")
|
||||
}
|
||||
if err := v.UnmarshalJSON([]byte(`[]`)); err == nil {
|
||||
t.Errorf("expected error on array")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnixMilliTimeAccessors(t *testing.T) {
|
||||
tm := time.Date(2023, 5, 17, 14, 30, 45, 1000000, time.UTC)
|
||||
val := NewUnixMilli(tm)
|
||||
|
||||
tst.AssertEqual(t, val.Year(), 2023)
|
||||
tst.AssertEqual(t, val.Serialize(), strconv.FormatInt(tm.UnixMilli(), 10))
|
||||
tst.AssertEqual(t, val.IsZero(), false)
|
||||
tst.AssertEqual(t, UnixMilliTime{}.IsZero(), true)
|
||||
|
||||
a := val.Add(time.Hour)
|
||||
tst.AssertEqual(t, a.Sub(val), time.Hour)
|
||||
tst.AssertEqual(t, a.After(val), true)
|
||||
tst.AssertEqual(t, val.Before(a), true)
|
||||
|
||||
d := NewUnixMilli(tm)
|
||||
tst.AssertEqual(t, val.Equal(d), true)
|
||||
tst.AssertEqual(t, val.EqualAny(d), true)
|
||||
tst.AssertEqual(t, val.EqualAny(nil), false)
|
||||
}
|
||||
|
||||
func TestNowUnixMilli(t *testing.T) {
|
||||
before := time.Now()
|
||||
v := NowUnixMilli()
|
||||
after := time.Now()
|
||||
|
||||
if v.Time().Before(before.Add(-time.Second)) || v.Time().After(after.Add(time.Second)) {
|
||||
t.Errorf("NowUnixMilli not within expected range")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- UnixNanoTime ----------
|
||||
|
||||
func TestUnixNanoTimeRoundtripJSON(t *testing.T) {
|
||||
type Wrap struct {
|
||||
Value UnixNanoTime `json:"v"`
|
||||
}
|
||||
|
||||
val := NewUnixNano(time.Unix(0, 1675951556820915171).UTC())
|
||||
w1 := Wrap{val}
|
||||
|
||||
jstr1, err := json.Marshal(w1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, string(jstr1), `{"v":"1675951556820915171"}`)
|
||||
|
||||
w2 := Wrap{}
|
||||
if err := json.Unmarshal(jstr1, &w2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, w2.Value.UnixNano(), val.UnixNano())
|
||||
}
|
||||
|
||||
func TestUnixNanoTimeText(t *testing.T) {
|
||||
val := NewUnixNano(time.Unix(0, 1675951556820915171))
|
||||
b, err := val.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, string(b), "1675951556820915171")
|
||||
|
||||
var v2 UnixNanoTime
|
||||
if err := v2.UnmarshalText(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tst.AssertEqual(t, v2.UnixNano(), val.UnixNano())
|
||||
|
||||
if err := v2.UnmarshalText([]byte("xyz")); err == nil {
|
||||
t.Errorf("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnixNanoTimeUnmarshalJSONInvalid(t *testing.T) {
|
||||
var v UnixNanoTime
|
||||
if err := v.UnmarshalJSON([]byte(`"abc"`)); err == nil {
|
||||
t.Errorf("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnixNanoTimeAccessors(t *testing.T) {
|
||||
tm := time.Date(2023, 5, 17, 14, 30, 45, 123456789, time.UTC)
|
||||
val := NewUnixNano(tm)
|
||||
|
||||
tst.AssertEqual(t, val.Year(), 2023)
|
||||
tst.AssertEqual(t, val.Nanosecond(), 123456789)
|
||||
tst.AssertEqual(t, val.Serialize(), strconv.FormatInt(tm.UnixNano(), 10))
|
||||
tst.AssertEqual(t, val.IsZero(), false)
|
||||
tst.AssertEqual(t, UnixNanoTime{}.IsZero(), true)
|
||||
|
||||
a := val.Add(2 * time.Second)
|
||||
tst.AssertEqual(t, a.Sub(val), 2*time.Second)
|
||||
tst.AssertEqual(t, a.After(val), true)
|
||||
tst.AssertEqual(t, val.Before(a), true)
|
||||
|
||||
c := val.AddDate(0, 0, 1)
|
||||
tst.AssertEqual(t, c.Day(), 18)
|
||||
|
||||
d := NewUnixNano(tm)
|
||||
tst.AssertEqual(t, val.Equal(d), true)
|
||||
tst.AssertEqual(t, val.EqualAny(d), true)
|
||||
tst.AssertEqual(t, val.EqualAny(nil), false)
|
||||
}
|
||||
|
||||
func TestNowUnixNano(t *testing.T) {
|
||||
before := time.Now()
|
||||
v := NowUnixNano()
|
||||
after := time.Now()
|
||||
|
||||
if v.Time().Before(before.Add(-time.Second)) || v.Time().After(after.Add(time.Second)) {
|
||||
t.Errorf("NowUnixNano not within expected range")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnixCrossTypeEqualAny(t *testing.T) {
|
||||
tm := time.Date(2023, 5, 17, 14, 30, 45, 0, time.UTC)
|
||||
u := NewUnix(tm)
|
||||
um := NewUnixMilli(tm)
|
||||
un := NewUnixNano(tm)
|
||||
r := NewRFC3339(tm)
|
||||
rn := NewRFC3339Nano(tm)
|
||||
|
||||
tst.AssertEqual(t, u.EqualAny(um), true)
|
||||
tst.AssertEqual(t, u.EqualAny(un), true)
|
||||
tst.AssertEqual(t, u.EqualAny(r), true)
|
||||
tst.AssertEqual(t, u.EqualAny(rn), true)
|
||||
tst.AssertEqual(t, u.EqualAny(tm), true)
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
package scn
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
c := New("my-token")
|
||||
if c == nil {
|
||||
t.Fatal("New returned nil")
|
||||
}
|
||||
tst.AssertEqual(t, c.token, "my-token")
|
||||
}
|
||||
|
||||
func TestNewEmptyToken(t *testing.T) {
|
||||
c := New("")
|
||||
if c == nil {
|
||||
t.Fatal("New returned nil")
|
||||
}
|
||||
tst.AssertEqual(t, c.token, "")
|
||||
}
|
||||
|
||||
func TestNewReturnsDistinctInstances(t *testing.T) {
|
||||
c1 := New("token-a")
|
||||
c2 := New("token-b")
|
||||
if c1 == c2 {
|
||||
t.Fatal("New should return distinct instances")
|
||||
}
|
||||
tst.AssertEqual(t, c1.token, "token-a")
|
||||
tst.AssertEqual(t, c2.token, "token-b")
|
||||
}
|
||||
|
||||
func TestConnectionMessage(t *testing.T) {
|
||||
c := New("tok")
|
||||
mb := c.Message("Hello")
|
||||
|
||||
if mb == nil {
|
||||
t.Fatal("Message returned nil")
|
||||
}
|
||||
if mb.conn != c {
|
||||
t.Error("MessageBuilder.conn does not point to source Connection")
|
||||
}
|
||||
tst.AssertEqual(t, mb.title, "Hello")
|
||||
if mb.content != nil {
|
||||
t.Error("expected content to be nil")
|
||||
}
|
||||
if mb.channel != nil {
|
||||
t.Error("expected channel to be nil")
|
||||
}
|
||||
if mb.time != nil {
|
||||
t.Error("expected time to be nil")
|
||||
}
|
||||
if mb.sendername != nil {
|
||||
t.Error("expected sendername to be nil")
|
||||
}
|
||||
if mb.priority != nil {
|
||||
t.Error("expected priority to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionTitle(t *testing.T) {
|
||||
c := New("tok")
|
||||
mb := c.Title("Hello")
|
||||
|
||||
if mb == nil {
|
||||
t.Fatal("Title returned nil")
|
||||
}
|
||||
if mb.conn != c {
|
||||
t.Error("MessageBuilder.conn does not point to source Connection")
|
||||
}
|
||||
tst.AssertEqual(t, mb.title, "Hello")
|
||||
if mb.content != nil {
|
||||
t.Error("expected content to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageAndTitleAreEquivalent(t *testing.T) {
|
||||
c := New("tok")
|
||||
mbMsg := c.Message("X")
|
||||
mbTitle := c.Title("X")
|
||||
|
||||
tst.AssertEqual(t, mbMsg.title, mbTitle.title)
|
||||
tst.AssertEqual(t, mbMsg.conn, mbTitle.conn)
|
||||
}
|
||||
|
||||
func TestBuilderChannel(t *testing.T) {
|
||||
c := New("tok")
|
||||
mb := c.Message("t")
|
||||
res := mb.Channel("foo-channel")
|
||||
|
||||
if res != mb {
|
||||
t.Error("Channel did not return same builder")
|
||||
}
|
||||
if mb.channel == nil {
|
||||
t.Fatal("expected channel to be set")
|
||||
}
|
||||
tst.AssertEqual(t, *mb.channel, "foo-channel")
|
||||
}
|
||||
|
||||
func TestBuilderContent(t *testing.T) {
|
||||
c := New("tok")
|
||||
mb := c.Message("t")
|
||||
res := mb.Content("body")
|
||||
|
||||
if res != mb {
|
||||
t.Error("Content did not return same builder")
|
||||
}
|
||||
if mb.content == nil {
|
||||
t.Fatal("expected content to be set")
|
||||
}
|
||||
tst.AssertEqual(t, *mb.content, "body")
|
||||
}
|
||||
|
||||
func TestBuilderTime(t *testing.T) {
|
||||
c := New("tok")
|
||||
mb := c.Message("t")
|
||||
now := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
res := mb.Time(now)
|
||||
|
||||
if res != mb {
|
||||
t.Error("Time did not return same builder")
|
||||
}
|
||||
if mb.time == nil {
|
||||
t.Fatal("expected time to be set")
|
||||
}
|
||||
if !mb.time.Equal(now) {
|
||||
t.Errorf("expected %v, got %v", now, *mb.time)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderSenderName(t *testing.T) {
|
||||
c := New("tok")
|
||||
mb := c.Message("t")
|
||||
res := mb.SenderName("alice")
|
||||
|
||||
if res != mb {
|
||||
t.Error("SenderName did not return same builder")
|
||||
}
|
||||
if mb.sendername == nil {
|
||||
t.Fatal("expected sendername to be set")
|
||||
}
|
||||
tst.AssertEqual(t, *mb.sendername, "alice")
|
||||
}
|
||||
|
||||
func TestBuilderPriority(t *testing.T) {
|
||||
c := New("tok")
|
||||
mb := c.Message("t")
|
||||
res := mb.Priority(2)
|
||||
|
||||
if res != mb {
|
||||
t.Error("Priority did not return same builder")
|
||||
}
|
||||
if mb.priority == nil {
|
||||
t.Fatal("expected priority to be set")
|
||||
}
|
||||
tst.AssertEqual(t, *mb.priority, 2)
|
||||
}
|
||||
|
||||
func TestBuilderChaining(t *testing.T) {
|
||||
c := New("tok")
|
||||
tt := time.Date(2030, 5, 6, 7, 8, 9, 0, time.UTC)
|
||||
|
||||
mb := c.Message("hello").
|
||||
Channel("ch").
|
||||
Content("content").
|
||||
Time(tt).
|
||||
SenderName("bob").
|
||||
Priority(7)
|
||||
|
||||
if mb == nil {
|
||||
t.Fatal("chained builder returned nil")
|
||||
}
|
||||
tst.AssertEqual(t, mb.title, "hello")
|
||||
if mb.channel == nil || *mb.channel != "ch" {
|
||||
t.Error("channel not set correctly")
|
||||
}
|
||||
if mb.content == nil || *mb.content != "content" {
|
||||
t.Error("content not set correctly")
|
||||
}
|
||||
if mb.time == nil || !mb.time.Equal(tt) {
|
||||
t.Error("time not set correctly")
|
||||
}
|
||||
if mb.sendername == nil || *mb.sendername != "bob" {
|
||||
t.Error("sendername not set correctly")
|
||||
}
|
||||
if mb.priority == nil || *mb.priority != 7 {
|
||||
t.Error("priority not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderOverwriteValues(t *testing.T) {
|
||||
c := New("tok")
|
||||
mb := c.Message("t").
|
||||
Channel("first").
|
||||
Content("first").
|
||||
SenderName("first").
|
||||
Priority(1)
|
||||
|
||||
mb.Channel("second").
|
||||
Content("second").
|
||||
SenderName("second").
|
||||
Priority(2)
|
||||
|
||||
tst.AssertEqual(t, *mb.channel, "second")
|
||||
tst.AssertEqual(t, *mb.content, "second")
|
||||
tst.AssertEqual(t, *mb.sendername, "second")
|
||||
tst.AssertEqual(t, *mb.priority, 2)
|
||||
}
|
||||
|
||||
func TestBuilderIndependentInstances(t *testing.T) {
|
||||
c := New("tok")
|
||||
|
||||
a := c.Message("A").Content("aa").Priority(1)
|
||||
b := c.Message("B").Content("bb").Priority(9)
|
||||
|
||||
if a == b {
|
||||
t.Fatal("expected distinct builders")
|
||||
}
|
||||
tst.AssertEqual(t, a.title, "A")
|
||||
tst.AssertEqual(t, b.title, "B")
|
||||
tst.AssertEqual(t, *a.content, "aa")
|
||||
tst.AssertEqual(t, *b.content, "bb")
|
||||
tst.AssertEqual(t, *a.priority, 1)
|
||||
tst.AssertEqual(t, *b.priority, 9)
|
||||
}
|
||||
|
||||
func TestBuilderTimeZonePreserved(t *testing.T) {
|
||||
c := New("tok")
|
||||
loc, err := time.LoadLocation("Europe/Berlin")
|
||||
if err != nil {
|
||||
t.Skipf("timezone db not available: %v", err)
|
||||
}
|
||||
tt := time.Date(2025, 6, 15, 12, 0, 0, 0, loc)
|
||||
|
||||
mb := c.Message("t").Time(tt)
|
||||
|
||||
if mb.time == nil {
|
||||
t.Fatal("expected time to be set")
|
||||
}
|
||||
if !mb.time.Equal(tt) {
|
||||
t.Errorf("expected %v, got %v", tt, *mb.time)
|
||||
}
|
||||
if mb.time.Unix() != tt.Unix() {
|
||||
t.Errorf("expected unix %d, got %d", tt.Unix(), mb.time.Unix())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderNegativePriority(t *testing.T) {
|
||||
c := New("tok")
|
||||
mb := c.Message("t").Priority(-1)
|
||||
if mb.priority == nil {
|
||||
t.Fatal("expected priority to be set")
|
||||
}
|
||||
tst.AssertEqual(t, *mb.priority, -1)
|
||||
}
|
||||
|
||||
func TestBuilderEmptyStrings(t *testing.T) {
|
||||
c := New("tok")
|
||||
mb := c.Message("").
|
||||
Channel("").
|
||||
Content("").
|
||||
SenderName("")
|
||||
|
||||
tst.AssertEqual(t, mb.title, "")
|
||||
if mb.channel == nil || *mb.channel != "" {
|
||||
t.Error("channel should be set to empty string (not nil)")
|
||||
}
|
||||
if mb.content == nil || *mb.content != "" {
|
||||
t.Error("content should be set to empty string (not nil)")
|
||||
}
|
||||
if mb.sendername == nil || *mb.sendername != "" {
|
||||
t.Error("sendername should be set to empty string (not nil)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorTypesAreDistinct(t *testing.T) {
|
||||
errs := []any{ErrAuthFailed, ErrQuota, ErrBadRequest, ErrInternalServerErr, ErrOther}
|
||||
for i := range errs {
|
||||
if errs[i] == nil {
|
||||
t.Errorf("error type at index %d is nil", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package sq
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildInsertStatementBasic(t *testing.T) {
|
||||
type r struct {
|
||||
ID string `db:"id"`
|
||||
Name string `db:"name"`
|
||||
}
|
||||
|
||||
q := fakeQueryable{}
|
||||
sqlstr, pp, err := BuildInsertStatement(q, "users", r{ID: "1", Name: "alice"})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertTrue(t, strings.HasPrefix(sqlstr, "INSERT INTO users ("))
|
||||
tst.AssertTrue(t, strings.Contains(sqlstr, "id"))
|
||||
tst.AssertTrue(t, strings.Contains(sqlstr, "name"))
|
||||
tst.AssertEqual(t, 2, len(pp))
|
||||
|
||||
values := []any{}
|
||||
for _, v := range pp {
|
||||
values = append(values, v)
|
||||
}
|
||||
|
||||
hasID, hasName := false, false
|
||||
for _, v := range values {
|
||||
if vs, ok := v.(string); ok {
|
||||
if vs == "1" {
|
||||
hasID = true
|
||||
}
|
||||
if vs == "alice" {
|
||||
hasName = true
|
||||
}
|
||||
}
|
||||
}
|
||||
tst.AssertTrue(t, hasID)
|
||||
tst.AssertTrue(t, hasName)
|
||||
}
|
||||
|
||||
func TestBuildInsertStatementSkipsUnexported(t *testing.T) {
|
||||
type r struct {
|
||||
ID string `db:"id"`
|
||||
hidden string `db:"hidden"` //nolint:unused
|
||||
}
|
||||
|
||||
q := fakeQueryable{}
|
||||
sqlstr, pp, err := BuildInsertStatement(q, "users", r{ID: "1"})
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, 1, len(pp))
|
||||
tst.AssertTrue(t, !strings.Contains(sqlstr, "hidden"))
|
||||
}
|
||||
|
||||
func TestBuildInsertStatementSkipsNoTagAndDash(t *testing.T) {
|
||||
type r struct {
|
||||
ID string `db:"id"`
|
||||
Skip1 string `db:"-"`
|
||||
Skip2 string
|
||||
}
|
||||
|
||||
q := fakeQueryable{}
|
||||
sqlstr, pp, err := BuildInsertStatement(q, "users", r{ID: "1", Skip1: "x", Skip2: "y"})
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, 1, len(pp))
|
||||
tst.AssertTrue(t, !strings.Contains(sqlstr, "Skip"))
|
||||
}
|
||||
|
||||
func TestBuildInsertStatementNoFields(t *testing.T) {
|
||||
type r struct {
|
||||
Skip string
|
||||
}
|
||||
q := fakeQueryable{}
|
||||
_, _, err := BuildInsertStatement(q, "x", r{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for no usable fields")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildInsertStatementNilPointer(t *testing.T) {
|
||||
type r struct {
|
||||
ID string `db:"id"`
|
||||
Note *string `db:"note"`
|
||||
}
|
||||
|
||||
q := fakeQueryable{}
|
||||
sqlstr, pp, err := BuildInsertStatement(q, "users", r{ID: "1", Note: nil})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
// Only id is parameterized; nil pointer becomes literal NULL
|
||||
tst.AssertEqual(t, 1, len(pp))
|
||||
tst.AssertTrue(t, strings.Contains(sqlstr, "NULL"))
|
||||
}
|
||||
|
||||
func TestBuildInsertStatementWithConverter(t *testing.T) {
|
||||
type r struct {
|
||||
ID string `db:"id"`
|
||||
Flag bool `db:"flag"`
|
||||
}
|
||||
|
||||
q := fakeQueryable{converters: []DBTypeConverter{ConverterBoolToBit}}
|
||||
_, pp, err := BuildInsertStatement(q, "users", r{ID: "1", Flag: true})
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, 2, len(pp))
|
||||
|
||||
foundOne := false
|
||||
for _, v := range pp {
|
||||
if vi, ok := v.(int64); ok && vi == 1 {
|
||||
foundOne = true
|
||||
}
|
||||
}
|
||||
tst.AssertTrue(t, foundOne)
|
||||
}
|
||||
|
||||
func TestBuildUpdateStatementBasic(t *testing.T) {
|
||||
type r struct {
|
||||
ID string `db:"id"`
|
||||
Name string `db:"name"`
|
||||
}
|
||||
|
||||
q := fakeQueryable{}
|
||||
sqlstr, pp, err := BuildUpdateStatement(q, "users", r{ID: "1", Name: "alice"}, "id")
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertTrue(t, strings.HasPrefix(sqlstr, "UPDATE users SET "))
|
||||
tst.AssertTrue(t, strings.Contains(sqlstr, "name = :"))
|
||||
tst.AssertTrue(t, strings.Contains(sqlstr, "(id = :"))
|
||||
tst.AssertEqual(t, 2, len(pp))
|
||||
}
|
||||
|
||||
func TestBuildUpdateStatementMissingID(t *testing.T) {
|
||||
type r struct {
|
||||
Name string `db:"name"`
|
||||
}
|
||||
|
||||
q := fakeQueryable{}
|
||||
_, _, err := BuildUpdateStatement(q, "users", r{Name: "alice"}, "id")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing id column")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUpdateStatementOnlyID(t *testing.T) {
|
||||
type r struct {
|
||||
ID string `db:"id"`
|
||||
}
|
||||
|
||||
q := fakeQueryable{}
|
||||
_, _, err := BuildUpdateStatement(q, "users", r{ID: "1"}, "id")
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no SET clauses")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUpdateStatementNilPointer(t *testing.T) {
|
||||
type r struct {
|
||||
ID string `db:"id"`
|
||||
Note *string `db:"note"`
|
||||
}
|
||||
|
||||
q := fakeQueryable{}
|
||||
sqlstr, _, err := BuildUpdateStatement(q, "users", r{ID: "1", Note: nil}, "id")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, strings.Contains(sqlstr, "note = NULL"))
|
||||
}
|
||||
|
||||
func TestBuildInsertMultipleStatementBasic(t *testing.T) {
|
||||
type r struct {
|
||||
ID string `db:"id"`
|
||||
Name string `db:"name"`
|
||||
}
|
||||
|
||||
q := fakeQueryable{}
|
||||
sqlstr, pp, err := BuildInsertMultipleStatement(q, "users", []r{
|
||||
{ID: "1", Name: "alice"},
|
||||
{ID: "2", Name: "bob"},
|
||||
})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertTrue(t, strings.Contains(sqlstr, `INSERT INTO "users"`))
|
||||
tst.AssertTrue(t, strings.Contains(sqlstr, `"id"`))
|
||||
tst.AssertTrue(t, strings.Contains(sqlstr, `"name"`))
|
||||
// 2 rows × 2 fields = 4 placeholders
|
||||
tst.AssertEqual(t, 4, len(pp))
|
||||
|
||||
// Two value tuples should appear -> exactly one "), (" separator
|
||||
tst.AssertEqual(t, 1, strings.Count(sqlstr, "), ("))
|
||||
}
|
||||
|
||||
func TestBuildInsertMultipleStatementEmpty(t *testing.T) {
|
||||
type r struct {
|
||||
ID string `db:"id"`
|
||||
}
|
||||
q := fakeQueryable{}
|
||||
_, _, err := BuildInsertMultipleStatement(q, "x", []r{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildInsertMultipleStatementNilPointer(t *testing.T) {
|
||||
type r struct {
|
||||
ID string `db:"id"`
|
||||
Note *string `db:"note"`
|
||||
}
|
||||
|
||||
q := fakeQueryable{}
|
||||
sqlstr, _, err := BuildInsertMultipleStatement(q, "users", []r{{ID: "1", Note: nil}})
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, strings.Contains(sqlstr, "NULL"))
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package sq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFnTrimCommentsLineOnly(t *testing.T) {
|
||||
// The line-only comment is replaced with an empty line.
|
||||
sql := "SELECT *\n-- this is a comment\nFROM users"
|
||||
pp := PP{}
|
||||
err := fnTrimComments(context.Background(), "QUERY", nil, &sql, &pp)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, !strings.Contains(sql, "comment"))
|
||||
tst.AssertTrue(t, strings.Contains(sql, "SELECT *"))
|
||||
tst.AssertTrue(t, strings.Contains(sql, "FROM users"))
|
||||
}
|
||||
|
||||
func TestFnTrimCommentsTrailing(t *testing.T) {
|
||||
sql := "SELECT * -- inline\nFROM users -- end"
|
||||
pp := PP{}
|
||||
err := fnTrimComments(context.Background(), "QUERY", nil, &sql, &pp)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, "SELECT *\nFROM users", sql)
|
||||
}
|
||||
|
||||
func TestFnTrimCommentsIndented(t *testing.T) {
|
||||
sql := "SELECT *\n -- indented comment\nFROM users"
|
||||
pp := PP{}
|
||||
err := fnTrimComments(context.Background(), "QUERY", nil, &sql, &pp)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, !strings.Contains(sql, "indented comment"))
|
||||
tst.AssertTrue(t, strings.Contains(sql, "SELECT *"))
|
||||
tst.AssertTrue(t, strings.Contains(sql, "FROM users"))
|
||||
}
|
||||
|
||||
func TestFnTrimCommentsTrimsTrailingWhitespace(t *testing.T) {
|
||||
sql := "SELECT * \t\nFROM users "
|
||||
pp := PP{}
|
||||
err := fnTrimComments(context.Background(), "QUERY", nil, &sql, &pp)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, "SELECT *\nFROM users", sql)
|
||||
}
|
||||
|
||||
func TestFnTrimCommentsNoComment(t *testing.T) {
|
||||
sql := "SELECT id\nFROM users\nWHERE id=1"
|
||||
pp := PP{}
|
||||
err := fnTrimComments(context.Background(), "QUERY", nil, &sql, &pp)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, "SELECT id\nFROM users\nWHERE id=1", sql)
|
||||
}
|
||||
|
||||
func TestFnTrimCommentsEmpty(t *testing.T) {
|
||||
sql := ""
|
||||
pp := PP{}
|
||||
err := fnTrimComments(context.Background(), "QUERY", nil, &sql, &pp)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, "", sql)
|
||||
}
|
||||
|
||||
func TestCommentTrimmerListener(t *testing.T) {
|
||||
sql := "SELECT *\n-- a comment\nFROM x"
|
||||
pp := PP{}
|
||||
err := CommentTrimmer.PreQuery(context.Background(), nil, &sql, &pp, PreQueryMeta{})
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertTrue(t, !strings.Contains(sql, "a comment"))
|
||||
|
||||
sql2 := "INSERT INTO x VALUES (1) -- xx"
|
||||
err = CommentTrimmer.PreExec(context.Background(), nil, &sql2, &pp, PreExecMeta{})
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, "INSERT INTO x VALUES (1)", sql2)
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package sq
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/rfctime"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConverterBoolToBit(t *testing.T) {
|
||||
v, err := ConverterBoolToBit.ModelToDB(true)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, int64(1), v.(int64))
|
||||
|
||||
v, err = ConverterBoolToBit.ModelToDB(false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, int64(0), v.(int64))
|
||||
|
||||
v, err = ConverterBoolToBit.DBToModel(int64(1))
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, true, v.(bool))
|
||||
|
||||
v, err = ConverterBoolToBit.DBToModel(int64(0))
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, false, v.(bool))
|
||||
|
||||
_, err = ConverterBoolToBit.DBToModel(int64(2))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for value not in {0,1}")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConverterTimeToUnixMillis(t *testing.T) {
|
||||
t0 := time.Date(2024, 6, 15, 12, 34, 56, int(789*time.Millisecond), time.UTC)
|
||||
expected := t0.UnixMilli()
|
||||
|
||||
v, err := ConverterTimeToUnixMillis.ModelToDB(t0)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, expected, v.(int64))
|
||||
|
||||
v, err = ConverterTimeToUnixMillis.DBToModel(expected)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, expected, v.(time.Time).UnixMilli())
|
||||
}
|
||||
|
||||
func TestConverterRFCUnixMilliTime(t *testing.T) {
|
||||
t0 := rfctime.NewUnixMilli(time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC))
|
||||
expected := t0.UnixMilli()
|
||||
|
||||
v, err := ConverterRFCUnixMilliTimeToUnixMillis.ModelToDB(t0)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, expected, v.(int64))
|
||||
|
||||
v, err = ConverterRFCUnixMilliTimeToUnixMillis.DBToModel(expected)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, expected, v.(rfctime.UnixMilliTime).UnixMilli())
|
||||
}
|
||||
|
||||
func TestConverterRFCUnixNanoTime(t *testing.T) {
|
||||
t0 := rfctime.NewUnixNano(time.Date(2020, 1, 2, 3, 4, 5, 123456789, time.UTC))
|
||||
expected := t0.UnixNano()
|
||||
|
||||
v, err := ConverterRFCUnixNanoTimeToUnixNanos.ModelToDB(t0)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, expected, v.(int64))
|
||||
|
||||
v, err = ConverterRFCUnixNanoTimeToUnixNanos.DBToModel(expected)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, expected, v.(rfctime.UnixNanoTime).UnixNano())
|
||||
}
|
||||
|
||||
func TestConverterRFCUnixTime(t *testing.T) {
|
||||
t0 := rfctime.NewUnix(time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC))
|
||||
expected := t0.Unix()
|
||||
|
||||
v, err := ConverterRFCUnixTimeToUnixSeconds.ModelToDB(t0)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, expected, v.(int64))
|
||||
|
||||
v, err = ConverterRFCUnixTimeToUnixSeconds.DBToModel(expected)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, expected, v.(rfctime.UnixTime).Unix())
|
||||
}
|
||||
|
||||
func TestConverterRFC339Time(t *testing.T) {
|
||||
t0 := rfctime.NewRFC3339(time.Date(2020, 6, 15, 9, 30, 45, 0, time.UTC))
|
||||
|
||||
v, err := ConverterRFC339TimeToString.ModelToDB(t0)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, "2020-06-15 09:30:45", v.(string))
|
||||
|
||||
v, err = ConverterRFC339TimeToString.DBToModel("2020-06-15 09:30:45")
|
||||
tst.AssertNoErr(t, err)
|
||||
tt := v.(rfctime.RFC3339Time).Time().UTC()
|
||||
tst.AssertEqual(t, 2020, tt.Year())
|
||||
tst.AssertEqual(t, time.June, tt.Month())
|
||||
tst.AssertEqual(t, 15, tt.Day())
|
||||
tst.AssertEqual(t, 9, tt.Hour())
|
||||
tst.AssertEqual(t, 30, tt.Minute())
|
||||
tst.AssertEqual(t, 45, tt.Second())
|
||||
|
||||
_, err = ConverterRFC339TimeToString.DBToModel("garbage")
|
||||
if err == nil {
|
||||
t.Fatal("expected parse error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConverterRFC339NanoTime(t *testing.T) {
|
||||
t0 := rfctime.NewRFC3339Nano(time.Date(2020, 6, 15, 9, 30, 45, 123456789, time.UTC))
|
||||
|
||||
v, err := ConverterRFC339NanoTimeToString.ModelToDB(t0)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, "2020-06-15 09:30:45.123456789", v.(string))
|
||||
|
||||
v, err = ConverterRFC339NanoTimeToString.DBToModel("2020-06-15 09:30:45.123456789")
|
||||
tst.AssertNoErr(t, err)
|
||||
tt := v.(rfctime.RFC3339NanoTime).Time().UTC()
|
||||
tst.AssertEqual(t, 123456789, tt.Nanosecond())
|
||||
|
||||
_, err = ConverterRFC339NanoTimeToString.DBToModel("not a date")
|
||||
if err == nil {
|
||||
t.Fatal("expected parse error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConverterRFCDate(t *testing.T) {
|
||||
d := rfctime.Date{Year: 2024, Month: 3, Day: 9}
|
||||
v, err := ConverterRFCDateToString.ModelToDB(d)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, "2024-03-09", v.(string))
|
||||
|
||||
v, err = ConverterRFCDateToString.DBToModel("2024-03-09")
|
||||
tst.AssertNoErr(t, err)
|
||||
d2 := v.(rfctime.Date)
|
||||
tst.AssertEqual(t, 2024, d2.Year)
|
||||
tst.AssertEqual(t, 3, d2.Month)
|
||||
tst.AssertEqual(t, 9, d2.Day)
|
||||
|
||||
_, err = ConverterRFCDateToString.DBToModel("invalid")
|
||||
if err == nil {
|
||||
t.Fatal("expected parse error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConverterRFCTime(t *testing.T) {
|
||||
tm := rfctime.NewTime(13, 30, 45, 0)
|
||||
v, err := ConverterRFCTimeToString.ModelToDB(tm)
|
||||
tst.AssertNoErr(t, err)
|
||||
roundtrip, err := ConverterRFCTimeToString.DBToModel(v.(string))
|
||||
tst.AssertNoErr(t, err)
|
||||
tm2 := roundtrip.(rfctime.Time)
|
||||
tst.AssertEqual(t, 13, tm2.Hour)
|
||||
tst.AssertEqual(t, 30, tm2.Minute)
|
||||
tst.AssertEqual(t, 45, tm2.Second)
|
||||
|
||||
_, err = ConverterRFCTimeToString.DBToModel("xx:xx:xx")
|
||||
if err == nil {
|
||||
t.Fatal("expected parse error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConverterRFCSecondsF64(t *testing.T) {
|
||||
d := 12*time.Second + 500*time.Millisecond
|
||||
s := rfctime.NewSecondsF64(d)
|
||||
v, err := ConverterRFCSecondsF64ToString.ModelToDB(s)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, 12.5, v.(float64))
|
||||
|
||||
v, err = ConverterRFCSecondsF64ToString.DBToModel(12.5)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, 12.5, v.(rfctime.SecondsF64).Seconds())
|
||||
}
|
||||
|
||||
func TestConverterJsonObjToString(t *testing.T) {
|
||||
tst.AssertEqual(t, "sq.JsonObj", ConverterJsonObjToString.ModelTypeString())
|
||||
tst.AssertEqual(t, "string", ConverterJsonObjToString.DBTypeString())
|
||||
|
||||
v, err := ConverterJsonObjToString.ModelToDB(JsonObj{"x": float64(1)})
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, `{"x":1}`, v.(string))
|
||||
|
||||
r, err := ConverterJsonObjToString.DBToModel(`{"x":1}`)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertStrRepEqual(t, r.(JsonObj)["x"], float64(1))
|
||||
}
|
||||
|
||||
func TestConverterJsonArrToString(t *testing.T) {
|
||||
tst.AssertEqual(t, "sq.JsonArr", ConverterJsonArrToString.ModelTypeString())
|
||||
tst.AssertEqual(t, "string", ConverterJsonArrToString.DBTypeString())
|
||||
|
||||
v, err := ConverterJsonArrToString.ModelToDB(JsonArr{float64(1), float64(2)})
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, `[1,2]`, v.(string))
|
||||
|
||||
r, err := ConverterJsonArrToString.DBToModel(`[1,2]`)
|
||||
tst.AssertNoErr(t, err)
|
||||
arr := r.(JsonArr)
|
||||
tst.AssertEqual(t, 2, len(arr))
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package sq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeQueryable struct {
|
||||
converters []DBTypeConverter
|
||||
}
|
||||
|
||||
func (f fakeQueryable) Exec(ctx context.Context, sqlstr string, prep PP) (sql.Result, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (f fakeQueryable) Query(ctx context.Context, sqlstr string, prep PP) (*sqlx.Rows, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (f fakeQueryable) ListConverter() []DBTypeConverter {
|
||||
return f.converters
|
||||
}
|
||||
|
||||
func TestNewDBTypeConverterTypeStrings(t *testing.T) {
|
||||
conv := NewDBTypeConverter(func(v bool) (int64, error) { return 0, nil }, func(v int64) (bool, error) { return false, nil })
|
||||
tst.AssertEqual(t, "bool", conv.ModelTypeString())
|
||||
tst.AssertEqual(t, "int64", conv.DBTypeString())
|
||||
}
|
||||
|
||||
func TestNewDBTypeConverterModelToDB(t *testing.T) {
|
||||
conv := NewDBTypeConverter(func(v bool) (int64, error) {
|
||||
if v {
|
||||
return 1, nil
|
||||
}
|
||||
return 0, nil
|
||||
}, func(v int64) (bool, error) {
|
||||
return v != 0, nil
|
||||
})
|
||||
|
||||
r, err := conv.ModelToDB(true)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, int64(1), r.(int64))
|
||||
|
||||
r, err = conv.ModelToDB(false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, int64(0), r.(int64))
|
||||
}
|
||||
|
||||
func TestNewDBTypeConverterDBToModel(t *testing.T) {
|
||||
conv := NewDBTypeConverter(func(v bool) (int64, error) { return 0, nil }, func(v int64) (bool, error) {
|
||||
return v != 0, nil
|
||||
})
|
||||
|
||||
r, err := conv.DBToModel(int64(1))
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, true, r.(bool))
|
||||
|
||||
r, err = conv.DBToModel(int64(0))
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, false, r.(bool))
|
||||
}
|
||||
|
||||
func TestNewDBTypeConverterTypeMismatch(t *testing.T) {
|
||||
conv := NewDBTypeConverter(func(v bool) (int64, error) { return 0, nil }, func(v int64) (bool, error) { return false, nil })
|
||||
|
||||
_, err := conv.ModelToDB("not a bool")
|
||||
if err == nil {
|
||||
t.Fatal("expected error on type mismatch in ModelToDB")
|
||||
}
|
||||
|
||||
_, err = conv.DBToModel("not int64")
|
||||
if err == nil {
|
||||
t.Fatal("expected error on type mismatch in DBToModel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAutoDBTypeConverter(t *testing.T) {
|
||||
conv := NewAutoDBTypeConverter(JsonObj{})
|
||||
|
||||
tst.AssertEqual(t, "sq.JsonObj", conv.ModelTypeString())
|
||||
tst.AssertEqual(t, "string", conv.DBTypeString())
|
||||
|
||||
r, err := conv.ModelToDB(JsonObj{"k": "v"})
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, `{"k":"v"}`, r.(string))
|
||||
|
||||
r, err = conv.DBToModel(`{"k":"v"}`)
|
||||
tst.AssertNoErr(t, err)
|
||||
parsed, ok := r.(JsonObj)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertStrRepEqual(t, parsed["k"], "v")
|
||||
}
|
||||
|
||||
func TestConvertValueToDBNoConverter(t *testing.T) {
|
||||
q := fakeQueryable{}
|
||||
|
||||
r, err := convertValueToDB(q, "hello")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, "hello", r.(string))
|
||||
|
||||
r, err = convertValueToDB(q, 42)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, 42, r.(int))
|
||||
}
|
||||
|
||||
func TestConvertValueToDBWithConverter(t *testing.T) {
|
||||
q := fakeQueryable{converters: []DBTypeConverter{ConverterBoolToBit}}
|
||||
|
||||
r, err := convertValueToDB(q, true)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, int64(1), r.(int64))
|
||||
|
||||
r, err = convertValueToDB(q, false)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, int64(0), r.(int64))
|
||||
}
|
||||
|
||||
func TestConvertValueToDBNilPointer(t *testing.T) {
|
||||
q := fakeQueryable{}
|
||||
|
||||
var s *string
|
||||
r, err := convertValueToDB(q, s)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, true, r == nil)
|
||||
}
|
||||
|
||||
func TestConvertValueToDBNonNilPointer(t *testing.T) {
|
||||
q := fakeQueryable{converters: []DBTypeConverter{ConverterBoolToBit}}
|
||||
|
||||
v := true
|
||||
r, err := convertValueToDB(q, &v)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, int64(1), r.(int64))
|
||||
}
|
||||
|
||||
func TestConvertValueToModelNoConverter(t *testing.T) {
|
||||
q := fakeQueryable{}
|
||||
|
||||
r, err := convertValueToModel(q, "hello", "string")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, "hello", r.(string))
|
||||
}
|
||||
|
||||
func TestConvertValueToModelWithConverter(t *testing.T) {
|
||||
q := fakeQueryable{converters: []DBTypeConverter{ConverterBoolToBit}}
|
||||
|
||||
r, err := convertValueToModel(q, int64(1), "bool")
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, true, r.(bool))
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package sq
|
||||
|
||||
import (
|
||||
ct "git.blackforestbytes.com/BlackForestBytes/goext/cursortoken"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewEmptyPaginateFilter(t *testing.T) {
|
||||
f := NewEmptyPaginateFilter()
|
||||
|
||||
pp := PP{}
|
||||
flt, join, joinTbl := f.SQL(pp)
|
||||
tst.AssertEqual(t, "1=1", flt)
|
||||
tst.AssertEqual(t, "", join)
|
||||
tst.AssertEqual(t, 0, len(joinTbl))
|
||||
tst.AssertEqual(t, 0, len(pp))
|
||||
|
||||
tst.AssertEqual(t, 0, len(f.Sort()))
|
||||
}
|
||||
|
||||
func TestNewSimplePaginateFilter(t *testing.T) {
|
||||
sortOrder := []FilterSort{
|
||||
{Field: "name", Direction: ct.SortASC},
|
||||
}
|
||||
filterParams := PP{"p1": "value1"}
|
||||
|
||||
f := NewSimplePaginateFilter("name = :p1", filterParams, sortOrder)
|
||||
|
||||
pp := PP{}
|
||||
flt, join, joinTbl := f.SQL(pp)
|
||||
tst.AssertEqual(t, "name = :p1", flt)
|
||||
tst.AssertEqual(t, "", join)
|
||||
tst.AssertEqual(t, 0, len(joinTbl))
|
||||
// filterParams should be merged into pp
|
||||
tst.AssertEqual(t, 1, len(pp))
|
||||
tst.AssertEqual(t, "value1", pp["p1"])
|
||||
|
||||
srt := f.Sort()
|
||||
tst.AssertEqual(t, 1, len(srt))
|
||||
tst.AssertEqual(t, "name", srt[0].Field)
|
||||
tst.AssertEqual(t, ct.SortASC, srt[0].Direction)
|
||||
}
|
||||
|
||||
func TestNewPaginateFilter(t *testing.T) {
|
||||
sortOrder := []FilterSort{
|
||||
{Field: "id", Direction: ct.SortDESC},
|
||||
}
|
||||
|
||||
called := 0
|
||||
f := NewPaginateFilter(func(params PP) (string, string, []string) {
|
||||
called++
|
||||
params.Add("hello")
|
||||
return "id > 0", "JOIN other ON other.id = main.id", []string{"other"}
|
||||
}, sortOrder)
|
||||
|
||||
pp := PP{}
|
||||
flt, join, joinTbl := f.SQL(pp)
|
||||
tst.AssertEqual(t, "id > 0", flt)
|
||||
tst.AssertEqual(t, "JOIN other ON other.id = main.id", join)
|
||||
tst.AssertEqual(t, 1, len(joinTbl))
|
||||
tst.AssertEqual(t, "other", joinTbl[0])
|
||||
tst.AssertEqual(t, 1, called)
|
||||
tst.AssertEqual(t, 1, len(pp))
|
||||
|
||||
srt := f.Sort()
|
||||
tst.AssertEqual(t, 1, len(srt))
|
||||
tst.AssertEqual(t, "id", srt[0].Field)
|
||||
tst.AssertEqual(t, ct.SortDESC, srt[0].Direction)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package sq
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJsonObjMarshalToDB(t *testing.T) {
|
||||
j := JsonObj{}
|
||||
out, err := j.MarshalToDB(JsonObj{"key": "value", "num": float64(7)})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
// JSON map ordering is not guaranteed - parse and verify
|
||||
roundtrip, err := j.UnmarshalToModel(out)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertStrRepEqual(t, roundtrip["key"], "value")
|
||||
tst.AssertStrRepEqual(t, roundtrip["num"], float64(7))
|
||||
}
|
||||
|
||||
func TestJsonObjUnmarshalToModelInvalid(t *testing.T) {
|
||||
j := JsonObj{}
|
||||
_, err := j.UnmarshalToModel("{not valid json}")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid JSON")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonArrMarshalToDB(t *testing.T) {
|
||||
j := JsonArr{}
|
||||
out, err := j.MarshalToDB(JsonArr{float64(1), "two", true})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
tst.AssertEqual(t, `[1,"two",true]`, out)
|
||||
}
|
||||
|
||||
func TestJsonArrRoundtrip(t *testing.T) {
|
||||
j := JsonArr{}
|
||||
out, err := j.MarshalToDB(JsonArr{"a", "b", "c"})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
r, err := j.UnmarshalToModel(out)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, 3, len(r))
|
||||
tst.AssertEqual(t, "a", r[0])
|
||||
tst.AssertEqual(t, "b", r[1])
|
||||
tst.AssertEqual(t, "c", r[2])
|
||||
}
|
||||
|
||||
func TestJsonArrUnmarshalInvalid(t *testing.T) {
|
||||
j := JsonArr{}
|
||||
_, err := j.UnmarshalToModel("not json")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid JSON")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoJsonRoundtrip(t *testing.T) {
|
||||
type inner struct {
|
||||
A int `json:"a"`
|
||||
B string `json:"b"`
|
||||
}
|
||||
|
||||
aj := AutoJson[inner]{Value: inner{A: 42, B: "foo"}}
|
||||
zero := AutoJson[inner]{}
|
||||
|
||||
out, err := zero.MarshalToDB(aj)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, `{"a":42,"b":"foo"}`, out)
|
||||
|
||||
r, err := zero.UnmarshalToModel(out)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, 42, r.Value.A)
|
||||
tst.AssertEqual(t, "foo", r.Value.B)
|
||||
}
|
||||
|
||||
func TestAutoJsonUnmarshalInvalid(t *testing.T) {
|
||||
type inner struct {
|
||||
A int `json:"a"`
|
||||
}
|
||||
zero := AutoJson[inner]{}
|
||||
_, err := zero.UnmarshalToModel("garbage")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid JSON")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package sq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewPrePingListener(t *testing.T) {
|
||||
called := 0
|
||||
expected := errors.New("ping err")
|
||||
l := NewPrePingListener(func(ctx context.Context, meta PrePingMeta) error {
|
||||
called++
|
||||
return expected
|
||||
})
|
||||
|
||||
err := l.PrePing(context.Background(), PrePingMeta{})
|
||||
tst.AssertEqual(t, 1, called)
|
||||
tst.AssertTrue(t, errors.Is(err, expected))
|
||||
|
||||
// Other handlers must not error / panic
|
||||
tst.AssertNoErr(t, l.PreTxBegin(context.Background(), 0, PreTxBeginMeta{}))
|
||||
tst.AssertNoErr(t, l.PreTxCommit(0, PreTxCommitMeta{}))
|
||||
tst.AssertNoErr(t, l.PreTxRollback(0, PreTxRollbackMeta{}))
|
||||
tst.AssertNoErr(t, l.PreQuery(context.Background(), nil, nil, nil, PreQueryMeta{}))
|
||||
tst.AssertNoErr(t, l.PreExec(context.Background(), nil, nil, nil, PreExecMeta{}))
|
||||
|
||||
// Post variants must not panic
|
||||
l.PostPing(nil, PostPingMeta{})
|
||||
l.PostTxBegin(0, nil, PostTxBeginMeta{})
|
||||
l.PostTxCommit(0, nil, PostTxCommitMeta{})
|
||||
l.PostTxRollback(0, nil, PostTxRollbackMeta{})
|
||||
l.PostQuery(nil, "", "", PP{}, nil, PostQueryMeta{})
|
||||
l.PostExec(nil, "", "", PP{}, nil, PostExecMeta{})
|
||||
}
|
||||
|
||||
func TestNewPreTxBeginListener(t *testing.T) {
|
||||
called := 0
|
||||
l := NewPreTxBeginListener(func(ctx context.Context, txid uint16, meta PreTxBeginMeta) error {
|
||||
called++
|
||||
tst.AssertEqual(t, uint16(7), txid)
|
||||
return nil
|
||||
})
|
||||
tst.AssertNoErr(t, l.PreTxBegin(context.Background(), 7, PreTxBeginMeta{}))
|
||||
tst.AssertEqual(t, 1, called)
|
||||
}
|
||||
|
||||
func TestNewPreTxCommitListener(t *testing.T) {
|
||||
called := 0
|
||||
l := NewPreTxCommitListener(func(txid uint16, meta PreTxCommitMeta) error {
|
||||
called++
|
||||
return nil
|
||||
})
|
||||
tst.AssertNoErr(t, l.PreTxCommit(0, PreTxCommitMeta{}))
|
||||
tst.AssertEqual(t, 1, called)
|
||||
}
|
||||
|
||||
func TestNewPreTxRollbackListener(t *testing.T) {
|
||||
called := 0
|
||||
l := NewPreTxRollbackListener(func(txid uint16, meta PreTxRollbackMeta) error {
|
||||
called++
|
||||
return nil
|
||||
})
|
||||
tst.AssertNoErr(t, l.PreTxRollback(0, PreTxRollbackMeta{}))
|
||||
tst.AssertEqual(t, 1, called)
|
||||
}
|
||||
|
||||
func TestNewPreQueryListener(t *testing.T) {
|
||||
called := 0
|
||||
l := NewPreQueryListener(func(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreQueryMeta) error {
|
||||
called++
|
||||
*sql = "modified"
|
||||
return nil
|
||||
})
|
||||
|
||||
sql := "original"
|
||||
pp := PP{}
|
||||
tst.AssertNoErr(t, l.PreQuery(context.Background(), nil, &sql, &pp, PreQueryMeta{}))
|
||||
tst.AssertEqual(t, 1, called)
|
||||
tst.AssertEqual(t, "modified", sql)
|
||||
}
|
||||
|
||||
func TestNewPreExecListener(t *testing.T) {
|
||||
called := 0
|
||||
l := NewPreExecListener(func(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreExecMeta) error {
|
||||
called++
|
||||
return nil
|
||||
})
|
||||
sql := "x"
|
||||
pp := PP{}
|
||||
tst.AssertNoErr(t, l.PreExec(context.Background(), nil, &sql, &pp, PreExecMeta{}))
|
||||
tst.AssertEqual(t, 1, called)
|
||||
}
|
||||
|
||||
func TestNewPreListenerBoth(t *testing.T) {
|
||||
queryCalls := 0
|
||||
execCalls := 0
|
||||
l := NewPreListener(func(ctx context.Context, cmdtype string, txID *uint16, sql *string, params *PP) error {
|
||||
switch cmdtype {
|
||||
case "QUERY":
|
||||
queryCalls++
|
||||
case "EXEC":
|
||||
execCalls++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
sql := "s"
|
||||
pp := PP{}
|
||||
tst.AssertNoErr(t, l.PreQuery(context.Background(), nil, &sql, &pp, PreQueryMeta{}))
|
||||
tst.AssertNoErr(t, l.PreExec(context.Background(), nil, &sql, &pp, PreExecMeta{}))
|
||||
|
||||
tst.AssertEqual(t, 1, queryCalls)
|
||||
tst.AssertEqual(t, 1, execCalls)
|
||||
}
|
||||
|
||||
func TestNewPostPingListener(t *testing.T) {
|
||||
called := 0
|
||||
l := NewPostPingListener(func(result error, meta PostPingMeta) {
|
||||
called++
|
||||
})
|
||||
l.PostPing(nil, PostPingMeta{})
|
||||
tst.AssertEqual(t, 1, called)
|
||||
}
|
||||
|
||||
func TestNewPostTxBeginListener(t *testing.T) {
|
||||
called := 0
|
||||
l := NewPostTxBeginListener(func(txid uint16, result error, meta PostTxBeginMeta) {
|
||||
called++
|
||||
})
|
||||
l.PostTxBegin(0, nil, PostTxBeginMeta{})
|
||||
tst.AssertEqual(t, 1, called)
|
||||
}
|
||||
|
||||
func TestNewPostTxCommitListener(t *testing.T) {
|
||||
called := 0
|
||||
l := NewPostTxCommitListener(func(txid uint16, result error, meta PostTxCommitMeta) {
|
||||
called++
|
||||
})
|
||||
l.PostTxCommit(0, nil, PostTxCommitMeta{})
|
||||
tst.AssertEqual(t, 1, called)
|
||||
}
|
||||
|
||||
func TestNewPostTxRollbackListener(t *testing.T) {
|
||||
called := 0
|
||||
l := NewPostTxRollbackListener(func(txid uint16, result error, meta PostTxRollbackMeta) {
|
||||
called++
|
||||
})
|
||||
l.PostTxRollback(0, nil, PostTxRollbackMeta{})
|
||||
tst.AssertEqual(t, 1, called)
|
||||
}
|
||||
|
||||
func TestNewPostQueryListener(t *testing.T) {
|
||||
called := 0
|
||||
l := NewPostQueryListener(func(txID *uint16, sqlOriginal string, sqlReal string, params PP, result error, meta PostQueryMeta) {
|
||||
called++
|
||||
})
|
||||
l.PostQuery(nil, "", "", PP{}, nil, PostQueryMeta{})
|
||||
tst.AssertEqual(t, 1, called)
|
||||
}
|
||||
|
||||
func TestNewPostExecListener(t *testing.T) {
|
||||
called := 0
|
||||
l := NewPostExecListener(func(txID *uint16, sqlOriginal string, sqlReal string, params PP, result error, meta PostExecMeta) {
|
||||
called++
|
||||
})
|
||||
l.PostExec(nil, "", "", PP{}, nil, PostExecMeta{})
|
||||
tst.AssertEqual(t, 1, called)
|
||||
}
|
||||
|
||||
func TestNewPostListenerBoth(t *testing.T) {
|
||||
queryCalls := 0
|
||||
execCalls := 0
|
||||
l := NewPostListener(func(cmdtype string, txID *uint16, sqlOriginal string, sqlReal string, result error, params PP) {
|
||||
switch cmdtype {
|
||||
case "QUERY":
|
||||
queryCalls++
|
||||
case "EXEC":
|
||||
execCalls++
|
||||
}
|
||||
})
|
||||
l.PostQuery(nil, "", "", PP{}, nil, PostQueryMeta{})
|
||||
l.PostExec(nil, "", "", PP{}, nil, PostExecMeta{})
|
||||
tst.AssertEqual(t, 1, queryCalls)
|
||||
tst.AssertEqual(t, 1, execCalls)
|
||||
}
|
||||
|
||||
func TestGenListenerNilHandlersDontPanic(t *testing.T) {
|
||||
// A listener constructed with one constructor only sets one handler.
|
||||
// Calls to other handlers should be safe no-ops.
|
||||
l := NewPostPingListener(func(result error, meta PostPingMeta) {})
|
||||
|
||||
// All Pre* return nil
|
||||
tst.AssertNoErr(t, l.PrePing(context.Background(), PrePingMeta{}))
|
||||
tst.AssertNoErr(t, l.PreTxBegin(context.Background(), 0, PreTxBeginMeta{}))
|
||||
tst.AssertNoErr(t, l.PreTxCommit(0, PreTxCommitMeta{}))
|
||||
tst.AssertNoErr(t, l.PreTxRollback(0, PreTxRollbackMeta{}))
|
||||
tst.AssertNoErr(t, l.PreQuery(context.Background(), nil, nil, nil, PreQueryMeta{}))
|
||||
tst.AssertNoErr(t, l.PreExec(context.Background(), nil, nil, nil, PreExecMeta{}))
|
||||
|
||||
// All Post* are no-ops
|
||||
l.PostTxBegin(0, nil, PostTxBeginMeta{})
|
||||
l.PostTxCommit(0, nil, PostTxCommitMeta{})
|
||||
l.PostTxRollback(0, nil, PostTxRollbackMeta{})
|
||||
l.PostQuery(nil, "", "", PP{}, nil, PostQueryMeta{})
|
||||
l.PostExec(nil, "", "", PP{}, nil, PostExecMeta{})
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package sq
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPPID(t *testing.T) {
|
||||
id := PPID()
|
||||
tst.AssertTrue(t, strings.HasPrefix(id, "p_"))
|
||||
if len(id) != 2+8 {
|
||||
t.Errorf("expected length 10, got %d (id=%q)", len(id), id)
|
||||
}
|
||||
|
||||
// uniqueness - very high probability with 8 base62 chars
|
||||
seen := map[string]bool{}
|
||||
for range 1000 {
|
||||
x := PPID()
|
||||
if seen[x] {
|
||||
t.Errorf("duplicate PPID: %s", x)
|
||||
}
|
||||
seen[x] = true
|
||||
}
|
||||
}
|
||||
|
||||
func TestPPAdd(t *testing.T) {
|
||||
pp := PP{}
|
||||
id1 := pp.Add(123)
|
||||
id2 := pp.Add("hello")
|
||||
|
||||
tst.AssertNotEqual(t, id1, id2)
|
||||
tst.AssertEqual(t, 123, pp[id1])
|
||||
tst.AssertEqual(t, "hello", pp[id2])
|
||||
tst.AssertEqual(t, 2, len(pp))
|
||||
}
|
||||
|
||||
func TestPPAddAll(t *testing.T) {
|
||||
a := PP{"a": 1, "b": 2}
|
||||
b := PP{"c": 3, "d": 4}
|
||||
a.AddAll(b)
|
||||
|
||||
tst.AssertEqual(t, 4, len(a))
|
||||
tst.AssertEqual(t, 1, a["a"])
|
||||
tst.AssertEqual(t, 2, a["b"])
|
||||
tst.AssertEqual(t, 3, a["c"])
|
||||
tst.AssertEqual(t, 4, a["d"])
|
||||
}
|
||||
|
||||
func TestPPAddAllOverwrite(t *testing.T) {
|
||||
a := PP{"a": 1, "b": 2}
|
||||
b := PP{"a": 99}
|
||||
a.AddAll(b)
|
||||
|
||||
tst.AssertEqual(t, 2, len(a))
|
||||
tst.AssertEqual(t, 99, a["a"])
|
||||
tst.AssertEqual(t, 2, a["b"])
|
||||
}
|
||||
|
||||
func TestJoin(t *testing.T) {
|
||||
a := PP{"a": 1, "b": 2}
|
||||
b := PP{"c": 3}
|
||||
c := PP{"d": 4, "a": 99}
|
||||
|
||||
r := Join(a, b, c)
|
||||
|
||||
tst.AssertEqual(t, 4, len(r))
|
||||
tst.AssertEqual(t, 99, r["a"])
|
||||
tst.AssertEqual(t, 2, r["b"])
|
||||
tst.AssertEqual(t, 3, r["c"])
|
||||
tst.AssertEqual(t, 4, r["d"])
|
||||
|
||||
// Source maps must remain unchanged
|
||||
tst.AssertEqual(t, 2, len(a))
|
||||
tst.AssertEqual(t, 1, a["a"])
|
||||
}
|
||||
|
||||
func TestJoinEmpty(t *testing.T) {
|
||||
r := Join()
|
||||
tst.AssertEqual(t, 0, len(r))
|
||||
}
|
||||
|
||||
func TestJoinSingle(t *testing.T) {
|
||||
a := PP{"a": 1}
|
||||
r := Join(a)
|
||||
tst.AssertEqual(t, 1, len(r))
|
||||
tst.AssertEqual(t, 1, r["a"])
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package syncext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAtomicGetSet(t *testing.T) {
|
||||
a := NewAtomic(42)
|
||||
|
||||
if v := a.Get(); v != 42 {
|
||||
t.Errorf("expected 42, got %d", v)
|
||||
}
|
||||
|
||||
old := a.Set(100)
|
||||
if old != 42 {
|
||||
t.Errorf("expected old value 42, got %d", old)
|
||||
}
|
||||
|
||||
if v := a.Get(); v != 100 {
|
||||
t.Errorf("expected 100, got %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicGetSetString(t *testing.T) {
|
||||
a := NewAtomic("hello")
|
||||
|
||||
if v := a.Get(); v != "hello" {
|
||||
t.Errorf("expected 'hello', got %q", v)
|
||||
}
|
||||
|
||||
old := a.Set("world")
|
||||
if old != "hello" {
|
||||
t.Errorf("expected old value 'hello', got %q", old)
|
||||
}
|
||||
|
||||
if v := a.Get(); v != "world" {
|
||||
t.Errorf("expected 'world', got %q", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicUpdate(t *testing.T) {
|
||||
a := NewAtomic(10)
|
||||
|
||||
a.Update(func(old int) int {
|
||||
return old * 2
|
||||
})
|
||||
|
||||
if v := a.Get(); v != 20 {
|
||||
t.Errorf("expected 20, got %d", v)
|
||||
}
|
||||
|
||||
a.Update(func(old int) int {
|
||||
return old + 5
|
||||
})
|
||||
|
||||
if v := a.Get(); v != 25 {
|
||||
t.Errorf("expected 25, got %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicCompareAndSwap(t *testing.T) {
|
||||
a := NewAtomic(5)
|
||||
|
||||
if !a.CompareAndSwap(5, 10) {
|
||||
t.Error("CAS should have succeeded")
|
||||
}
|
||||
if v := a.Get(); v != 10 {
|
||||
t.Errorf("expected 10, got %d", v)
|
||||
}
|
||||
|
||||
if a.CompareAndSwap(5, 20) {
|
||||
t.Error("CAS should have failed")
|
||||
}
|
||||
if v := a.Get(); v != 10 {
|
||||
t.Errorf("expected 10, got %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicWaitAlreadyMatching(t *testing.T) {
|
||||
a := NewAtomic(7)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
a.Wait(7)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// ok
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Error("Wait should return immediately if value already matches")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicWaitWithTimeoutNoMatch(t *testing.T) {
|
||||
a := NewAtomic(1)
|
||||
|
||||
err := a.WaitWithTimeout(50*time.Millisecond, 999)
|
||||
if err == nil {
|
||||
t.Error("expected timeout error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicWaitWithTimeoutMatchAfterSet(t *testing.T) {
|
||||
a := NewAtomic(1)
|
||||
|
||||
go func() {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
a.Set(99)
|
||||
}()
|
||||
|
||||
err := a.WaitWithTimeout(500*time.Millisecond, 99)
|
||||
if err != nil {
|
||||
t.Errorf("expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicWaitWithContextCancel(t *testing.T) {
|
||||
a := NewAtomic(1)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
err := a.WaitWithContext(ctx, 999)
|
||||
if err == nil {
|
||||
t.Error("expected ctx error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicWaitWithContextAlreadyCancelled(t *testing.T) {
|
||||
a := NewAtomic(1)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
err := a.WaitWithContext(ctx, 1)
|
||||
if err == nil {
|
||||
t.Error("expected ctx error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicWaitForChange(t *testing.T) {
|
||||
a := NewAtomic(1)
|
||||
|
||||
ch := a.WaitForChange()
|
||||
|
||||
go func() {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
a.Set(2)
|
||||
}()
|
||||
|
||||
select {
|
||||
case v := <-ch:
|
||||
if v != 2 {
|
||||
t.Errorf("expected 2, got %d", v)
|
||||
}
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Error("WaitForChange did not deliver")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicConcurrentSet(t *testing.T) {
|
||||
a := NewAtomic(0)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(v int) {
|
||||
defer wg.Done()
|
||||
a.Set(v)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
v := a.Get()
|
||||
if v < 0 || v >= 50 {
|
||||
t.Errorf("unexpected final value %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicConcurrentUpdate(t *testing.T) {
|
||||
a := NewAtomic(0)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
a.Update(func(old int) int { return old + 1 })
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if v := a.Get(); v != 100 {
|
||||
t.Errorf("expected 100, got %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicWaitWithTimeoutZero(t *testing.T) {
|
||||
a := NewAtomic(1)
|
||||
|
||||
err := a.WaitWithTimeout(0, 999)
|
||||
if err == nil {
|
||||
t.Error("expected error for zero timeout with non-matching value")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package syncext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAtomicBoolGetSet(t *testing.T) {
|
||||
b := NewAtomicBool(false)
|
||||
|
||||
if b.Get() {
|
||||
t.Error("expected false")
|
||||
}
|
||||
|
||||
old := b.Set(true)
|
||||
if old {
|
||||
t.Error("expected old value false")
|
||||
}
|
||||
|
||||
if !b.Get() {
|
||||
t.Error("expected true")
|
||||
}
|
||||
|
||||
old = b.Set(false)
|
||||
if !old {
|
||||
t.Error("expected old value true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicBoolWaitAlreadyMatching(t *testing.T) {
|
||||
b := NewAtomicBool(true)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
b.Wait(true)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// ok
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Error("Wait should return immediately if value already matches")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicBoolWaitWithTimeoutNoMatch(t *testing.T) {
|
||||
b := NewAtomicBool(false)
|
||||
|
||||
err := b.WaitWithTimeout(50*time.Millisecond, true)
|
||||
if err == nil {
|
||||
t.Error("expected timeout error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicBoolWaitWithTimeoutMatchAfterSet(t *testing.T) {
|
||||
b := NewAtomicBool(false)
|
||||
|
||||
go func() {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
b.Set(true)
|
||||
}()
|
||||
|
||||
err := b.WaitWithTimeout(500*time.Millisecond, true)
|
||||
if err != nil {
|
||||
t.Errorf("expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicBoolWaitWithContextCancel(t *testing.T) {
|
||||
b := NewAtomicBool(false)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
err := b.WaitWithContext(ctx, true)
|
||||
if err == nil {
|
||||
t.Error("expected ctx error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicBoolWaitWithContextAlreadyCancelled(t *testing.T) {
|
||||
b := NewAtomicBool(false)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
err := b.WaitWithContext(ctx, false)
|
||||
if err == nil {
|
||||
t.Error("expected ctx error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicBoolWaitWithContextMatching(t *testing.T) {
|
||||
b := NewAtomicBool(true)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
err := b.WaitWithContext(ctx, true)
|
||||
if err != nil {
|
||||
t.Errorf("expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicBoolConcurrentSet(t *testing.T) {
|
||||
b := NewAtomicBool(false)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(v bool) {
|
||||
defer wg.Done()
|
||||
b.Set(v)
|
||||
}(i%2 == 0)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package syncext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWriteChannelWithTimeoutSuccess(t *testing.T) {
|
||||
c := make(chan int, 1)
|
||||
|
||||
ok := WriteChannelWithTimeout(c, 42, 100*time.Millisecond)
|
||||
if !ok {
|
||||
t.Error("expected write to succeed")
|
||||
}
|
||||
|
||||
select {
|
||||
case v := <-c:
|
||||
if v != 42 {
|
||||
t.Errorf("expected 42, got %d", v)
|
||||
}
|
||||
default:
|
||||
t.Error("no value received")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteChannelWithTimeoutFull(t *testing.T) {
|
||||
c := make(chan int, 1)
|
||||
c <- 1
|
||||
|
||||
ok := WriteChannelWithTimeout(c, 2, 50*time.Millisecond)
|
||||
if ok {
|
||||
t.Error("expected write to timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteChannelWithTimeoutUnbuffered(t *testing.T) {
|
||||
c := make(chan int)
|
||||
|
||||
go func() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
<-c
|
||||
}()
|
||||
|
||||
ok := WriteChannelWithTimeout(c, 99, 200*time.Millisecond)
|
||||
if !ok {
|
||||
t.Error("expected write to succeed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteChannelWithTimeoutUnbufferedTimeout(t *testing.T) {
|
||||
c := make(chan int)
|
||||
|
||||
ok := WriteChannelWithTimeout(c, 99, 50*time.Millisecond)
|
||||
if ok {
|
||||
t.Error("expected timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteChannelWithContextSuccess(t *testing.T) {
|
||||
c := make(chan int, 1)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
err := WriteChannelWithContext(ctx, c, 7)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if v := <-c; v != 7 {
|
||||
t.Errorf("expected 7, got %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteChannelWithContextCancel(t *testing.T) {
|
||||
c := make(chan int)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
err := WriteChannelWithContext(ctx, c, 7)
|
||||
if err == nil {
|
||||
t.Error("expected ctx error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteChannelWithContextAlreadyCancelled(t *testing.T) {
|
||||
c := make(chan int)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
err := WriteChannelWithContext(ctx, c, 7)
|
||||
if err == nil {
|
||||
t.Error("expected ctx error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadNonBlockingEmpty(t *testing.T) {
|
||||
c := make(chan int, 1)
|
||||
|
||||
_, ok := ReadNonBlocking(c)
|
||||
if ok {
|
||||
t.Error("expected non-blocking read to return false on empty channel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadNonBlockingHasValue(t *testing.T) {
|
||||
c := make(chan int, 1)
|
||||
c <- 55
|
||||
|
||||
v, ok := ReadNonBlocking(c)
|
||||
if !ok {
|
||||
t.Error("expected non-blocking read to return true")
|
||||
}
|
||||
if v != 55 {
|
||||
t.Errorf("expected 55, got %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteNonBlockingSuccess(t *testing.T) {
|
||||
c := make(chan int, 1)
|
||||
|
||||
ok := WriteNonBlocking(c, 33)
|
||||
if !ok {
|
||||
t.Error("expected non-blocking write to succeed")
|
||||
}
|
||||
|
||||
if v := <-c; v != 33 {
|
||||
t.Errorf("expected 33, got %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteNonBlockingFull(t *testing.T) {
|
||||
c := make(chan int, 1)
|
||||
c <- 1
|
||||
|
||||
ok := WriteNonBlocking(c, 2)
|
||||
if ok {
|
||||
t.Error("expected non-blocking write to fail when full")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteNonBlockingUnbufferedNoReceiver(t *testing.T) {
|
||||
c := make(chan int)
|
||||
|
||||
ok := WriteNonBlocking(c, 1)
|
||||
if ok {
|
||||
t.Error("expected non-blocking write to fail without receiver")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package termext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
resetSeq = "[0m"
|
||||
redSeq = "[31m"
|
||||
greenSeq = "[32m"
|
||||
yellowSeq = "[33m"
|
||||
blueSeq = "[34m"
|
||||
purpleSeq = "[35m"
|
||||
cyanSeq = "[36m"
|
||||
graySeq = "[37m"
|
||||
whiteSeq = "[97m"
|
||||
)
|
||||
|
||||
func TestRedEmpty(t *testing.T) {
|
||||
tst.AssertEqual(t, Red(""), redSeq+resetSeq)
|
||||
}
|
||||
|
||||
func TestGreenEmpty(t *testing.T) {
|
||||
tst.AssertEqual(t, Green(""), greenSeq+resetSeq)
|
||||
}
|
||||
|
||||
func TestYellowEmpty(t *testing.T) {
|
||||
tst.AssertEqual(t, Yellow(""), yellowSeq+resetSeq)
|
||||
}
|
||||
|
||||
func TestBlueEmpty(t *testing.T) {
|
||||
tst.AssertEqual(t, Blue(""), blueSeq+resetSeq)
|
||||
}
|
||||
|
||||
func TestPurpleEmpty(t *testing.T) {
|
||||
tst.AssertEqual(t, Purple(""), purpleSeq+resetSeq)
|
||||
}
|
||||
|
||||
func TestCyanEmpty(t *testing.T) {
|
||||
tst.AssertEqual(t, Cyan(""), cyanSeq+resetSeq)
|
||||
}
|
||||
|
||||
func TestGrayEmpty(t *testing.T) {
|
||||
tst.AssertEqual(t, Gray(""), graySeq+resetSeq)
|
||||
}
|
||||
|
||||
func TestWhiteEmpty(t *testing.T) {
|
||||
tst.AssertEqual(t, White(""), whiteSeq+resetSeq)
|
||||
}
|
||||
|
||||
func TestColorsContainOriginalString(t *testing.T) {
|
||||
input := "hello world"
|
||||
tst.AssertTrue(t, strings.Contains(Red(input), input))
|
||||
tst.AssertTrue(t, strings.Contains(Green(input), input))
|
||||
tst.AssertTrue(t, strings.Contains(Yellow(input), input))
|
||||
tst.AssertTrue(t, strings.Contains(Blue(input), input))
|
||||
tst.AssertTrue(t, strings.Contains(Purple(input), input))
|
||||
tst.AssertTrue(t, strings.Contains(Cyan(input), input))
|
||||
tst.AssertTrue(t, strings.Contains(Gray(input), input))
|
||||
tst.AssertTrue(t, strings.Contains(White(input), input))
|
||||
}
|
||||
|
||||
func TestColorsEndWithReset(t *testing.T) {
|
||||
tst.AssertTrue(t, strings.HasSuffix(Red("x"), resetSeq))
|
||||
tst.AssertTrue(t, strings.HasSuffix(Green("x"), resetSeq))
|
||||
tst.AssertTrue(t, strings.HasSuffix(Yellow("x"), resetSeq))
|
||||
tst.AssertTrue(t, strings.HasSuffix(Blue("x"), resetSeq))
|
||||
tst.AssertTrue(t, strings.HasSuffix(Purple("x"), resetSeq))
|
||||
tst.AssertTrue(t, strings.HasSuffix(Cyan("x"), resetSeq))
|
||||
tst.AssertTrue(t, strings.HasSuffix(Gray("x"), resetSeq))
|
||||
tst.AssertTrue(t, strings.HasSuffix(White("x"), resetSeq))
|
||||
}
|
||||
|
||||
func TestColorsStartWithCorrectSequence(t *testing.T) {
|
||||
tst.AssertTrue(t, strings.HasPrefix(Red("x"), redSeq))
|
||||
tst.AssertTrue(t, strings.HasPrefix(Green("x"), greenSeq))
|
||||
tst.AssertTrue(t, strings.HasPrefix(Yellow("x"), yellowSeq))
|
||||
tst.AssertTrue(t, strings.HasPrefix(Blue("x"), blueSeq))
|
||||
tst.AssertTrue(t, strings.HasPrefix(Purple("x"), purpleSeq))
|
||||
tst.AssertTrue(t, strings.HasPrefix(Cyan("x"), cyanSeq))
|
||||
tst.AssertTrue(t, strings.HasPrefix(Gray("x"), graySeq))
|
||||
tst.AssertTrue(t, strings.HasPrefix(White("x"), whiteSeq))
|
||||
}
|
||||
|
||||
func TestColorsAreDistinct(t *testing.T) {
|
||||
input := "value"
|
||||
results := []string{
|
||||
Red(input),
|
||||
Green(input),
|
||||
Yellow(input),
|
||||
Blue(input),
|
||||
Purple(input),
|
||||
Cyan(input),
|
||||
Gray(input),
|
||||
White(input),
|
||||
}
|
||||
for i := 0; i < len(results); i++ {
|
||||
for j := i + 1; j < len(results); j++ {
|
||||
tst.AssertNotEqual(t, results[i], results[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanStringEmpty(t *testing.T) {
|
||||
tst.AssertEqual(t, CleanString(""), "")
|
||||
}
|
||||
|
||||
func TestCleanStringWithoutColors(t *testing.T) {
|
||||
input := "plain text without any colors"
|
||||
tst.AssertEqual(t, CleanString(input), input)
|
||||
}
|
||||
|
||||
func TestCleanStringMultipleColors(t *testing.T) {
|
||||
input := Red("foo") + " " + Green("bar") + " " + Blue("baz")
|
||||
tst.AssertEqual(t, CleanString(input), "foo bar baz")
|
||||
}
|
||||
|
||||
func TestCleanStringNested(t *testing.T) {
|
||||
input := Red(Green("inner"))
|
||||
tst.AssertEqual(t, CleanString(input), "inner")
|
||||
}
|
||||
|
||||
func TestCleanStringIdempotent(t *testing.T) {
|
||||
input := Yellow("hello") + Purple("world")
|
||||
cleaned := CleanString(input)
|
||||
tst.AssertEqual(t, CleanString(cleaned), cleaned)
|
||||
}
|
||||
|
||||
func TestCleanStringEmptyColorWraps(t *testing.T) {
|
||||
tst.AssertEqual(t, CleanString(Red("")), "")
|
||||
tst.AssertEqual(t, CleanString(Green("")), "")
|
||||
tst.AssertEqual(t, CleanString(Yellow("")), "")
|
||||
tst.AssertEqual(t, CleanString(Blue("")), "")
|
||||
tst.AssertEqual(t, CleanString(Purple("")), "")
|
||||
tst.AssertEqual(t, CleanString(Cyan("")), "")
|
||||
tst.AssertEqual(t, CleanString(Gray("")), "")
|
||||
tst.AssertEqual(t, CleanString(White("")), "")
|
||||
}
|
||||
|
||||
func TestCleanStringPreservesNonAnsiContent(t *testing.T) {
|
||||
input := "before " + Red("middle") + " after\nnewline\ttab"
|
||||
expected := "before middle after\nnewline\ttab"
|
||||
tst.AssertEqual(t, CleanString(input), expected)
|
||||
}
|
||||
|
||||
func TestCleanStringRemovesBareResetSequence(t *testing.T) {
|
||||
input := "abc" + resetSeq + "def"
|
||||
tst.AssertEqual(t, CleanString(input), "abcdef")
|
||||
}
|
||||
|
||||
func TestCleanStringUnicode(t *testing.T) {
|
||||
input := Red("héllo wörld 你好 🌍")
|
||||
tst.AssertEqual(t, CleanString(input), "héllo wörld 你好 🌍")
|
||||
}
|
||||
|
||||
func TestColorRoundTrip(t *testing.T) {
|
||||
cases := []string{"", "x", "hello", "multi\nline", "with spaces", "héllo", "🌈"}
|
||||
wrappers := []func(string) string{Red, Green, Yellow, Blue, Purple, Cyan, Gray, White}
|
||||
for _, c := range cases {
|
||||
for _, w := range wrappers {
|
||||
tst.AssertEqual(t, CleanString(w(c)), c)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package termext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSupportsColorsNoPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("SupportsColors panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
_ = SupportsColors()
|
||||
}
|
||||
|
||||
func TestSupportsColorsReturnsBool(t *testing.T) {
|
||||
v := SupportsColors()
|
||||
tst.AssertTrue(t, v == true || v == false)
|
||||
}
|
||||
|
||||
func TestSupportsColorsIsDeterministic(t *testing.T) {
|
||||
a := SupportsColors()
|
||||
b := SupportsColors()
|
||||
tst.AssertEqual(t, a, b)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package timeext
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFromNanoseconds(t *testing.T) {
|
||||
if got := FromNanoseconds(0); got != 0 {
|
||||
t.Errorf("expected 0, got %v", got)
|
||||
}
|
||||
if got := FromNanoseconds(1); got != time.Nanosecond {
|
||||
t.Errorf("expected 1ns, got %v", got)
|
||||
}
|
||||
if got := FromNanoseconds(1000); got != 1000*time.Nanosecond {
|
||||
t.Errorf("expected 1000ns, got %v", got)
|
||||
}
|
||||
if got := FromNanoseconds(int64(123456789)); got != time.Duration(123456789) {
|
||||
t.Errorf("expected 123456789ns, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromMicroseconds(t *testing.T) {
|
||||
if got := FromMicroseconds(1); got != time.Microsecond {
|
||||
t.Errorf("expected 1us, got %v", got)
|
||||
}
|
||||
if got := FromMicroseconds(1000); got != time.Millisecond {
|
||||
t.Errorf("expected 1ms, got %v", got)
|
||||
}
|
||||
if got := FromMicroseconds(2.5); got != time.Microsecond*2+time.Nanosecond*500 {
|
||||
t.Errorf("expected 2.5us, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromMilliseconds(t *testing.T) {
|
||||
if got := FromMilliseconds(1); got != time.Millisecond {
|
||||
t.Errorf("expected 1ms, got %v", got)
|
||||
}
|
||||
if got := FromMilliseconds(1000); got != time.Second {
|
||||
t.Errorf("expected 1s, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromSeconds(t *testing.T) {
|
||||
if got := FromSeconds(1); got != time.Second {
|
||||
t.Errorf("expected 1s, got %v", got)
|
||||
}
|
||||
if got := FromSeconds(60); got != time.Minute {
|
||||
t.Errorf("expected 1min, got %v", got)
|
||||
}
|
||||
if got := FromSeconds(0.5); got != 500*time.Millisecond {
|
||||
t.Errorf("expected 0.5s, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromMinutes(t *testing.T) {
|
||||
if got := FromMinutes(1); got != time.Minute {
|
||||
t.Errorf("expected 1min, got %v", got)
|
||||
}
|
||||
if got := FromMinutes(60); got != time.Hour {
|
||||
t.Errorf("expected 1h, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromHours(t *testing.T) {
|
||||
if got := FromHours(1); got != time.Hour {
|
||||
t.Errorf("expected 1h, got %v", got)
|
||||
}
|
||||
if got := FromHours(24); got != 24*time.Hour {
|
||||
t.Errorf("expected 24h, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromDays(t *testing.T) {
|
||||
if got := FromDays(1); got != 24*time.Hour {
|
||||
t.Errorf("expected 1d, got %v", got)
|
||||
}
|
||||
if got := FromDays(7); got != 7*24*time.Hour {
|
||||
t.Errorf("expected 7d, got %v", got)
|
||||
}
|
||||
if got := FromDays(0); got != 0 {
|
||||
t.Errorf("expected 0, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatNaturalDurationEnglish(t *testing.T) {
|
||||
tests := []struct {
|
||||
dur time.Duration
|
||||
want string
|
||||
}{
|
||||
{time.Second, "1 second ago"},
|
||||
{2 * time.Second, "2 seconds ago"},
|
||||
{30 * time.Second, "30 seconds ago"},
|
||||
{179 * time.Second, "179 seconds ago"},
|
||||
{180 * time.Second, "3 minutes ago"},
|
||||
{30 * time.Minute, "30 minutes ago"},
|
||||
{179 * time.Minute, "179 minutes ago"},
|
||||
{180 * time.Minute, "3 hours ago"},
|
||||
{24 * time.Hour, "24 hours ago"},
|
||||
{71 * time.Hour, "71 hours ago"},
|
||||
{72 * time.Hour, "3 days ago"},
|
||||
{20 * 24 * time.Hour, "20 days ago"},
|
||||
{21 * 24 * time.Hour, "3 weeks ago"},
|
||||
{11 * 7 * 24 * time.Hour, "11 weeks ago"},
|
||||
// The months tier divides hours by (24*7*30); the actual boundaries are unusual
|
||||
// but we capture the current observable behavior:
|
||||
{12 * 7 * 24 * time.Hour, "0 months ago"},
|
||||
{90 * 7 * 24 * time.Hour, "3 months ago"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := FormatNaturalDurationEnglish(tt.dur)
|
||||
if got != tt.want {
|
||||
t.Errorf("FormatNaturalDurationEnglish(%v) = %q; want %q", tt.dur, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatDurationGerman(t *testing.T) {
|
||||
tests := []struct {
|
||||
dur time.Duration
|
||||
want string
|
||||
}{
|
||||
{time.Second, "1s"},
|
||||
{30 * time.Second, "30s"},
|
||||
{179 * time.Second, "179s"},
|
||||
{180 * time.Second, "3min"},
|
||||
{30 * time.Minute, "30min"},
|
||||
{179 * time.Minute, "179min"},
|
||||
{180 * time.Minute, "3h"},
|
||||
{24 * time.Hour, "24h"},
|
||||
{71 * time.Hour, "71h"},
|
||||
{72 * time.Hour, "3 Tage"},
|
||||
{20 * 24 * time.Hour, "20 Tage"},
|
||||
{21 * 24 * time.Hour, "3 Wochen"},
|
||||
{11 * 7 * 24 * time.Hour, "11 Wochen"},
|
||||
{12 * 7 * 24 * time.Hour, "0 Monate"},
|
||||
{90 * 7 * 24 * time.Hour, "3 Monate"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := FormatDurationGerman(tt.dur)
|
||||
if got != tt.want {
|
||||
t.Errorf("FormatDurationGerman(%v) = %q; want %q", tt.dur, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package timeext
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMonthNameGermanShort3(t *testing.T) {
|
||||
tests := []struct {
|
||||
m time.Month
|
||||
want string
|
||||
}{
|
||||
{time.January, "Jan"},
|
||||
{time.February, "Feb"},
|
||||
{time.March, "Mär"},
|
||||
{time.April, "Apr"},
|
||||
{time.May, "Mai"},
|
||||
{time.June, "Jun"},
|
||||
{time.July, "Jul"},
|
||||
{time.August, "Aug"},
|
||||
{time.September, "Sep"},
|
||||
{time.October, "Okt"},
|
||||
{time.November, "Nov"},
|
||||
{time.December, "Dez"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := MonthNameGermanShort3(tt.m)
|
||||
if got != tt.want {
|
||||
t.Errorf("MonthNameGermanShort3(%v) = %q; want %q", tt.m, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonthNameGermanShort3_Invalid(t *testing.T) {
|
||||
got := MonthNameGermanShort3(time.Month(13))
|
||||
want := "%!Month(13)"
|
||||
if got != want {
|
||||
t.Errorf("MonthNameGermanShort3(13) = %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonthNameGermanLong(t *testing.T) {
|
||||
tests := []struct {
|
||||
m time.Month
|
||||
want string
|
||||
}{
|
||||
{time.January, "Januar"},
|
||||
{time.February, "Februar"},
|
||||
{time.March, "März"},
|
||||
{time.April, "April"},
|
||||
{time.May, "Mai"},
|
||||
{time.June, "Juni"},
|
||||
{time.July, "Juli"},
|
||||
{time.August, "August"},
|
||||
{time.September, "September"},
|
||||
{time.October, "Oktober"},
|
||||
{time.November, "November"},
|
||||
{time.December, "Dezember"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := MonthNameGermanLong(tt.m)
|
||||
if got != tt.want {
|
||||
t.Errorf("MonthNameGermanLong(%v) = %q; want %q", tt.m, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonthNameGermanLong_Invalid(t *testing.T) {
|
||||
got := MonthNameGermanLong(time.Month(0))
|
||||
want := "%!Month(0)"
|
||||
if got != want {
|
||||
t.Errorf("MonthNameGermanLong(0) = %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user