mirror of
https://github.com/duke-git/lancet.git
synced 2026-11-16 05:12:11 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d561d4da77 | ||
|
|
ed237c18b3 | ||
|
|
4c9c628a31 | ||
|
|
5839ab33a9 | ||
|
|
b09d6af4a4 | ||
|
|
d859b91c57 | ||
|
|
6eb12896e3 | ||
|
|
0112b99cd9 | ||
|
|
961a76b6d4 | ||
|
|
c403dd3a2a | ||
|
|
b4302b2d15 | ||
|
|
1715d14cc8 | ||
|
|
0eabb67ed4 | ||
|
|
afc0d3c676 | ||
|
|
9c56f5f525 | ||
|
|
285e69edd7 | ||
|
|
e393f0e14f | ||
|
|
8dd57ef490 | ||
|
|
7e02d7e4e6 | ||
|
|
9f7d939e5a | ||
|
|
03695c886c | ||
|
|
9c401c4043 | ||
|
|
f7acd40303 | ||
|
|
eb4b10cc15 |
No files matched your search
@@ -6,7 +6,6 @@
|
||||

|
||||
[](https://github.com/duke-git/lancet/releases)
|
||||
[](https://pkg.go.dev/github.com/duke-git/lancet/v2)
|
||||
[](https://goreportcard.com/report/github.com/duke-git/lancet/v2)
|
||||
[](https://github.com/duke-git/lancet/actions/workflows/codecov.yml)
|
||||
[](https://codecov.io/gh/duke-git/lancet)
|
||||
[](https://github.com/duke-git/lancet/blob/main/LICENSE)
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||

|
||||
[](https://github.com/duke-git/lancet/releases)
|
||||
[](https://pkg.go.dev/github.com/duke-git/lancet/v2)
|
||||
[](https://goreportcard.com/report/github.com/duke-git/lancet/v2)
|
||||
[](https://github.com/duke-git/lancet/actions/workflows/codecov.yml)
|
||||
[](https://codecov.io/gh/duke-git/lancet)
|
||||
[](https://github.com/duke-git/lancet/blob/main/LICENSE)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package algorithm
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLRUCacheReuseAfterDeletingLastEntry(t *testing.T) {
|
||||
cache := NewLRUCache[int, string](1)
|
||||
cache.Put(1, "one")
|
||||
if !cache.Delete(1) || cache.Len() != 0 {
|
||||
t.Fatal("deleting the sole entry must empty the cache")
|
||||
}
|
||||
cache.Put(2, "two")
|
||||
cache.Put(3, "three")
|
||||
if cache.Len() != 1 {
|
||||
t.Fatalf("Len() = %d, want capacity 1", cache.Len())
|
||||
}
|
||||
if _, ok := cache.Get(2); ok {
|
||||
t.Fatal("the least recently used replacement entry was not evicted")
|
||||
}
|
||||
if value, ok := cache.Get(3); !ok || value != "three" {
|
||||
t.Fatalf("Get(3) = %q, %v", value, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUCacheEmptyAndReuse(t *testing.T) {
|
||||
for _, capacity := range []int{0, 1, 2, 4} {
|
||||
t.Run(strconv.Itoa(capacity), func(t *testing.T) {
|
||||
defer func() {
|
||||
if value := recover(); value != nil {
|
||||
t.Errorf("cache reuse panicked: %v", value)
|
||||
}
|
||||
}()
|
||||
cache := NewLRUCache[int, int](capacity)
|
||||
for round := 0; round < 4; round++ {
|
||||
for i := 0; i < capacity+2; i++ {
|
||||
cache.Put(i, round*10+i)
|
||||
if cache.Len() > capacity {
|
||||
t.Fatalf("Len() = %d exceeds capacity %d", cache.Len(), capacity)
|
||||
}
|
||||
}
|
||||
for i := 0; i < capacity+2; i++ {
|
||||
cache.Delete(i)
|
||||
}
|
||||
if cache.Len() != 0 {
|
||||
t.Fatalf("Len() = %d after deleting every key", cache.Len())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUCacheEndpointDeletionKeepsRecency(t *testing.T) {
|
||||
cache := NewLRUCache[int, int](3)
|
||||
cache.Put(1, 10)
|
||||
cache.Put(2, 20)
|
||||
cache.Put(3, 30)
|
||||
cache.Delete(1)
|
||||
cache.Delete(3)
|
||||
cache.Put(4, 40)
|
||||
cache.Put(5, 50)
|
||||
cache.Get(2)
|
||||
cache.Put(6, 60)
|
||||
if cache.Len() != 3 {
|
||||
t.Fatalf("Len() = %d, want 3", cache.Len())
|
||||
}
|
||||
if _, ok := cache.Get(4); ok {
|
||||
t.Fatal("Get(2) should make 4 the least recently used entry")
|
||||
}
|
||||
for key, want := range map[int]int{2: 20, 5: 50, 6: 60} {
|
||||
if got, ok := cache.Get(key); !ok || got != want {
|
||||
t.Fatalf("Get(%d) = %d, %v; want %d", key, got, ok, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,12 +102,14 @@ func (l *LRUCache[K, V]) addNode(node *lruNode[K, V]) {
|
||||
}
|
||||
|
||||
func (l *LRUCache[K, V]) deleteNode(node *lruNode[K, V]) K {
|
||||
if node == l.tail {
|
||||
l.tail = l.tail.pre
|
||||
} else if node == l.head {
|
||||
l.head = l.head.next
|
||||
if node.pre == nil {
|
||||
l.head = node.next
|
||||
} else {
|
||||
node.pre.next = node.next
|
||||
}
|
||||
if node.next == nil {
|
||||
l.tail = node.pre
|
||||
} else {
|
||||
node.next.pre = node.pre
|
||||
}
|
||||
return node.key
|
||||
|
||||
+10
-1
@@ -90,7 +90,16 @@ func (c *Channel[T]) Take(ctx context.Context, valueStream <-chan T, number int)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case takeStream <- <-valueStream:
|
||||
case v, ok := <-valueStream:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case takeStream <- v:
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -82,6 +82,34 @@ func TestTake(t *testing.T) {
|
||||
assert.Equal(3, <-intStream)
|
||||
}
|
||||
|
||||
func TestTake_sourceClosedOrCanceled(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := internal.NewAssert(t, "TestTake_sourceClosedOrCanceled")
|
||||
|
||||
c := NewChannel[int]()
|
||||
|
||||
// the source ends before number values: no zero values are produced
|
||||
ctx := context.Background()
|
||||
var got []int
|
||||
for v := range c.Take(ctx, c.Generate(ctx, 1, 2), 5) {
|
||||
got = append(got, v)
|
||||
}
|
||||
assert.Equal([]int{1, 2}, got)
|
||||
|
||||
// the source is idle: canceling ctx closes the stream
|
||||
ctx2, cancel := context.WithCancel(context.Background())
|
||||
idle := make(chan int)
|
||||
stream := c.Take(ctx2, idle, 3)
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case _, ok := <-stream:
|
||||
assert.Equal(false, ok)
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("Take did not stop after the context was canceled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFanIn(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := internal.NewAssert(t, "TestFanIn")
|
||||
|
||||
@@ -214,14 +214,17 @@ func NewTryKeyedLocker[K comparable]() *TryKeyedLocker[K] {
|
||||
// Play: https://go.dev/play/p/VG9qLvyetE2
|
||||
func (l *TryKeyedLocker[K]) TryLock(key K) bool {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
lock, ok := l.locks[key]
|
||||
if !ok {
|
||||
lock = &casMutex{}
|
||||
l.locks[key] = lock
|
||||
}
|
||||
l.mu.Unlock()
|
||||
|
||||
// TryLock must run while l.mu is held. Otherwise Unlock can delete this entry
|
||||
// from the map before the lock is taken, and a later TryLock for the same key
|
||||
// would create a second entry and succeed while this one is still held.
|
||||
return lock.TryLock()
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -228,3 +229,34 @@ func TestTryKeyedLocker_ParallelTry(t *testing.T) {
|
||||
assert.Equal(5, count)
|
||||
assert.Equal(0, len(locker.locks))
|
||||
}
|
||||
|
||||
func TestTryKeyedLocker_MutualExclusionUnderContention(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
locker := NewTryKeyedLocker[string]()
|
||||
|
||||
var holders, violations int32
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for g := 0; g < 8; g++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 20000; i++ {
|
||||
if !locker.TryLock("key") {
|
||||
continue
|
||||
}
|
||||
if atomic.AddInt32(&holders, 1) > 1 {
|
||||
atomic.AddInt32(&violations, 1)
|
||||
}
|
||||
atomic.AddInt32(&holders, -1)
|
||||
locker.Unlock("key")
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if v := atomic.LoadInt32(&violations); v != 0 {
|
||||
t.Fatalf("the lock was held by more than one goroutine %d times", v)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package datastructure_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
hashmap "github.com/duke-git/lancet/v2/datastructure/hashmap"
|
||||
)
|
||||
|
||||
type collisionKey string
|
||||
|
||||
func TestHashMapCollisionStorage(t *testing.T) {
|
||||
cases := map[string]func(*testing.T){
|
||||
"distinct colliding keys": func(t *testing.T) {
|
||||
h := hashmap.NewHashMapWithCapacity(0, 1)
|
||||
keys := []any{1, "1", collisionKey("1")}
|
||||
for i, k := range keys {
|
||||
h.Put(k, i+10)
|
||||
}
|
||||
for i, k := range keys {
|
||||
if h.Get(k) != i+10 || !h.Contains(k) {
|
||||
t.Fatalf("lost key %T(%v): %v", k, k, h.Get(k))
|
||||
}
|
||||
}
|
||||
if h.Size() != 3 {
|
||||
t.Fatalf("size=%d", h.Size())
|
||||
}
|
||||
},
|
||||
"overwrite cardinality": func(t *testing.T) {
|
||||
h := hashmap.NewHashMap()
|
||||
h.Put("same", 1)
|
||||
h.Put("same", 2)
|
||||
if h.Size() != 1 || !reflect.DeepEqual(h.Keys(), []any{"same"}) || !reflect.DeepEqual(h.Values(), []any{2}) {
|
||||
t.Fatalf("overwrite size/keys/values: %d %v %v", h.Size(), h.Keys(), h.Values())
|
||||
}
|
||||
},
|
||||
"delete absent colliding key": func(t *testing.T) {
|
||||
h := hashmap.NewHashMap()
|
||||
h.Put(1, "kept")
|
||||
h.Delete("1")
|
||||
if h.Size() != 1 || h.Get(1) != "kept" {
|
||||
t.Fatalf("absent delete removed another key: size=%d get=%v", h.Size(), h.Get(1))
|
||||
}
|
||||
},
|
||||
"delete every chain position": func(t *testing.T) {
|
||||
for _, remove := range []any{1, "1", collisionKey("1")} {
|
||||
h := hashmap.NewHashMapWithCapacity(0, 4)
|
||||
keys := []any{1, "1", collisionKey("1")}
|
||||
for i, k := range keys {
|
||||
h.Put(k, i)
|
||||
}
|
||||
h.Delete(remove)
|
||||
if h.Contains(remove) || h.Size() != 2 {
|
||||
t.Fatalf("deletion failed: %T size=%d", remove, h.Size())
|
||||
}
|
||||
for i, k := range keys {
|
||||
if !reflect.DeepEqual(k, remove) && h.Get(k) != i {
|
||||
t.Fatalf("delete lost other key: %T", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete preserves bucket indexes": func(t *testing.T) {
|
||||
h := hashmap.NewHashMap()
|
||||
for i := 0; i < 100; i++ {
|
||||
h.Put(fmt.Sprintf("key-%d", i), i)
|
||||
}
|
||||
for i := 0; i < 100; i += 2 {
|
||||
h.Delete(fmt.Sprintf("key-%d", i))
|
||||
}
|
||||
for i := 1; i < 100; i += 2 {
|
||||
if h.Get(fmt.Sprintf("key-%d", i)) != i {
|
||||
t.Fatalf("lost untouched bucket key %d", i)
|
||||
}
|
||||
}
|
||||
if h.Size() != 50 {
|
||||
t.Fatalf("size=%d", h.Size())
|
||||
}
|
||||
},
|
||||
"resize and enumerate chains": func(t *testing.T) {
|
||||
h := hashmap.NewHashMapWithCapacity(0, 1)
|
||||
h.Put(1, 1)
|
||||
h.Put("1", 2)
|
||||
h.Put(collisionKey("1"), 3)
|
||||
for i := 2; i <= 64; i++ {
|
||||
h.Put(i, i)
|
||||
}
|
||||
seen := map[string]any{}
|
||||
h.Iterate(func(k, v any) { seen[fmt.Sprintf("%T:%v", k, k)] = v })
|
||||
if h.Size() != 66 || len(seen) != 66 || len(h.Keys()) != 66 || len(h.Values()) != 66 {
|
||||
t.Fatalf("size=%d enumerated=%d", h.Size(), len(seen))
|
||||
}
|
||||
if seen["string:1"] != 2 || h.Get(collisionKey("1")) != 3 {
|
||||
t.Fatal("collision chain lost during growth")
|
||||
}
|
||||
},
|
||||
"filter colliding values": func(t *testing.T) {
|
||||
h := hashmap.NewHashMapWithCapacity(0, 4)
|
||||
h.Put(1, 1)
|
||||
h.Put("1", 2)
|
||||
h.Put(collisionKey("1"), 3)
|
||||
f := h.FilterByValue(func(v any) bool { return v.(int)%2 == 1 })
|
||||
if f == nil || f.Size() != 2 || f.Get(1) != 1 || f.Get(collisionKey("1")) != 3 || f.Contains("1") {
|
||||
t.Fatal("filter skipped or corrupted collision chain")
|
||||
}
|
||||
if h.FilterByValue(func(any) bool { return false }) != nil {
|
||||
t.Fatal("empty-filter nil contract changed")
|
||||
}
|
||||
},
|
||||
"zero capacity and deep equal keys": func(t *testing.T) {
|
||||
h := hashmap.NewHashMapWithCapacity(0, 0)
|
||||
if h.Get("missing") != nil || h.Contains("missing") {
|
||||
t.Fatal("empty lookup")
|
||||
}
|
||||
h.Delete("missing")
|
||||
h.Put([]int{1, 2}, "first")
|
||||
h.Put([]int{1, 2}, "second")
|
||||
if h.Get([]int{1, 2}) != "second" || h.Size() != 1 {
|
||||
t.Fatal("deep equal overwrite failed")
|
||||
}
|
||||
h.Delete([]int{1, 2})
|
||||
if h.Size() != 0 {
|
||||
t.Fatal("deep equal delete failed")
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, fn := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("public map operation panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
fn(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,9 @@ func NewHashMapWithCapacity(size, capacity uint64) *HashMap {
|
||||
|
||||
// Get return the value of given key in hashmap
|
||||
func (hm *HashMap) Get(key any) any {
|
||||
if hm.size == 0 {
|
||||
return nil
|
||||
}
|
||||
hashValue := hm.hash(key)
|
||||
node := hm.table[hashValue]
|
||||
for node != nil {
|
||||
@@ -66,41 +69,55 @@ func (hm *HashMap) GetOrDefault(key any, defaultValue any) any {
|
||||
|
||||
// Put new key value in hashmap
|
||||
func (hm *HashMap) Put(key any, value any) {
|
||||
hm.putValue(hm.hash(key), key, value)
|
||||
}
|
||||
|
||||
func (hm *HashMap) putValue(hash uint64, key, value any) {
|
||||
if hm.capacity == 0 {
|
||||
hm.capacity = defaultMapCapacity
|
||||
hm.table = make([]*mapNode, defaultMapCapacity)
|
||||
}
|
||||
hm.putValue(hm.hash(key), key, value)
|
||||
}
|
||||
|
||||
node := hm.table[hash]
|
||||
if node == nil {
|
||||
hm.table[hash] = newMapNode(key, value)
|
||||
} else if node.key == key {
|
||||
hm.table[hash] = newMapNodeWithNext(key, value, node)
|
||||
} else {
|
||||
hm.resize()
|
||||
hm.putValue(hash, value, value)
|
||||
func (hm *HashMap) putValue(hash uint64, key, value any) {
|
||||
for node := hm.table[hash]; node != nil; node = node.next {
|
||||
if reflect.DeepEqual(node.key, key) {
|
||||
node.value = value
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if hm.size >= hm.capacity {
|
||||
hm.resize()
|
||||
hash = hm.hash(key)
|
||||
}
|
||||
hm.table[hash] = newMapNodeWithNext(key, value, hm.table[hash])
|
||||
hm.size++
|
||||
}
|
||||
|
||||
// Delete item by given key in hashmap
|
||||
func (hm *HashMap) Delete(key any) {
|
||||
hash := hm.hash(key)
|
||||
node := hm.table[hash]
|
||||
if node == nil {
|
||||
if hm.size == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
hm.table = append(hm.table[:hash], hm.table[hash+1:]...)
|
||||
hm.size--
|
||||
hash := hm.hash(key)
|
||||
var previous *mapNode
|
||||
for node := hm.table[hash]; node != nil; node = node.next {
|
||||
if reflect.DeepEqual(node.key, key) {
|
||||
if previous == nil {
|
||||
hm.table[hash] = node.next
|
||||
} else {
|
||||
previous.next = node.next
|
||||
}
|
||||
hm.size--
|
||||
return
|
||||
}
|
||||
previous = node
|
||||
}
|
||||
}
|
||||
|
||||
// Contains checks if given key is in hashmap or not
|
||||
func (hm *HashMap) Contains(key any) bool {
|
||||
if hm.size == 0 {
|
||||
return false
|
||||
}
|
||||
node := hm.table[hm.hash(key)]
|
||||
for node != nil {
|
||||
if reflect.DeepEqual(node.key, key) {
|
||||
@@ -113,12 +130,10 @@ func (hm *HashMap) Contains(key any) bool {
|
||||
|
||||
// Iterate executes iteratee funcation for every key and value pair of hashmap (random order)
|
||||
func (hm *HashMap) Iterate(iteratee func(key, value any)) {
|
||||
if hm.size > 0 {
|
||||
for i := 0; i < len(hm.table); i++ {
|
||||
item := hm.table[i]
|
||||
if item != nil {
|
||||
iteratee(item.key, item.value)
|
||||
}
|
||||
for _, node := range hm.table {
|
||||
for node != nil {
|
||||
iteratee(node.key, node.value)
|
||||
node = node.next
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,17 +143,14 @@ func (hm *HashMap) Iterate(iteratee func(key, value any)) {
|
||||
// otherwise it returns the HashMap with selected values.
|
||||
func (hm *HashMap) FilterByValue(perdicate func(value any) bool) *HashMap {
|
||||
var filteredHM *HashMap
|
||||
if hm.size > 0 {
|
||||
for i := 0; i < len(hm.table); i++ {
|
||||
item := hm.table[i]
|
||||
if item != nil && perdicate(item.value) {
|
||||
if filteredHM == nil {
|
||||
filteredHM = NewHashMap()
|
||||
}
|
||||
filteredHM.Put(item.key, item.value)
|
||||
hm.Iterate(func(key, value any) {
|
||||
if perdicate(value) {
|
||||
if filteredHM == nil {
|
||||
filteredHM = NewHashMap()
|
||||
}
|
||||
filteredHM.Put(key, value)
|
||||
}
|
||||
}
|
||||
})
|
||||
return filteredHM
|
||||
}
|
||||
|
||||
@@ -172,18 +184,16 @@ func (hm *HashMap) Values() []any {
|
||||
|
||||
func (hm *HashMap) resize() {
|
||||
hm.capacity <<= 1
|
||||
|
||||
tempTable := hm.table
|
||||
|
||||
hm.table = make([]*mapNode, hm.capacity)
|
||||
|
||||
for i := 0; i < len(tempTable); i++ {
|
||||
node := tempTable[i]
|
||||
if node == nil {
|
||||
continue
|
||||
for _, node := range tempTable {
|
||||
for node != nil {
|
||||
next := node.next
|
||||
hash := hm.hash(node.key)
|
||||
node.next = hm.table[hash]
|
||||
hm.table[hash] = node
|
||||
node = next
|
||||
}
|
||||
|
||||
hm.table[hm.hash(node.key)] = node
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,13 +211,6 @@ func (hm *HashMap) hash(key any) uint64 {
|
||||
return (hm.capacity - 1) & (hashValue ^ (hashValue >> 16))
|
||||
}
|
||||
|
||||
func newMapNode(key, value any) *mapNode {
|
||||
return &mapNode{
|
||||
key: key,
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func newMapNodeWithNext(key, value any, next *mapNode) *mapNode {
|
||||
return &mapNode{
|
||||
key: key,
|
||||
|
||||
@@ -87,6 +87,7 @@ func (dl *DoublyLink[T]) InsertAt(index int, value T) {
|
||||
newNode := datastructure.NewLinkNode(value)
|
||||
newNode.Next = current.Next
|
||||
newNode.Pre = current
|
||||
current.Next.Pre = newNode
|
||||
|
||||
current.Next = newNode
|
||||
dl.length++
|
||||
@@ -106,7 +107,9 @@ func (dl *DoublyLink[T]) DeleteAtHead() {
|
||||
|
||||
current := dl.Head
|
||||
dl.Head = current.Next
|
||||
dl.Head.Pre = nil
|
||||
if dl.Head != nil {
|
||||
dl.Head.Pre = nil
|
||||
}
|
||||
dl.length--
|
||||
}
|
||||
|
||||
@@ -119,6 +122,7 @@ func (dl *DoublyLink[T]) DeleteAtTail() {
|
||||
current := dl.Head
|
||||
if current.Next == nil {
|
||||
dl.DeleteAtHead()
|
||||
return
|
||||
}
|
||||
|
||||
for current.Next.Next != nil {
|
||||
@@ -132,27 +136,26 @@ func (dl *DoublyLink[T]) DeleteAtTail() {
|
||||
// DeleteAt delete value in doubly linklist at index
|
||||
// param `index` should be [0, len(DoublyLink)-1]
|
||||
func (dl *DoublyLink[T]) DeleteAt(index int) {
|
||||
if dl.Head == nil {
|
||||
if dl.Head == nil || index < 0 || index >= dl.length {
|
||||
return
|
||||
}
|
||||
|
||||
current := dl.Head
|
||||
if current.Next == nil || index == 0 {
|
||||
if index == 0 {
|
||||
dl.DeleteAtHead()
|
||||
return
|
||||
}
|
||||
|
||||
if index == dl.length-1 {
|
||||
dl.DeleteAtTail()
|
||||
}
|
||||
|
||||
if index < 0 || index > dl.length-1 {
|
||||
return
|
||||
}
|
||||
|
||||
current := dl.Head
|
||||
i := 0
|
||||
for current != nil {
|
||||
if i == index-1 {
|
||||
current.Next = current.Next.Next
|
||||
current.Next.Pre = current
|
||||
dl.length--
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package datastructure
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mutableIntLink interface {
|
||||
InsertAtHead(int)
|
||||
InsertAtTail(int)
|
||||
DeleteAtHead()
|
||||
DeleteAtTail()
|
||||
DeleteAt(int)
|
||||
Reverse()
|
||||
Values() []int
|
||||
Size() int
|
||||
IsEmpty() bool
|
||||
}
|
||||
|
||||
func TestLinkDeletionBoundaries(t *testing.T) {
|
||||
for name, newLink := range map[string]func() mutableIntLink{
|
||||
"singly": func() mutableIntLink { return NewSinglyLink[int]() },
|
||||
"doubly": func() mutableIntLink { return NewDoublyLink[int]() },
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
size int
|
||||
delete func(mutableIntLink)
|
||||
want []int
|
||||
}{
|
||||
{"empty head", 0, func(l mutableIntLink) { l.DeleteAtHead() }, []int{}},
|
||||
{"empty tail", 0, func(l mutableIntLink) { l.DeleteAtTail() }, []int{}},
|
||||
{"empty index", 0, func(l mutableIntLink) { l.DeleteAt(0) }, []int{}},
|
||||
{"singleton head", 1, func(l mutableIntLink) { l.DeleteAtHead() }, []int{}},
|
||||
{"singleton tail", 1, func(l mutableIntLink) { l.DeleteAtTail() }, []int{}},
|
||||
{"singleton index", 1, func(l mutableIntLink) { l.DeleteAt(0) }, []int{}},
|
||||
{"singleton negative index", 1, func(l mutableIntLink) { l.DeleteAt(-1) }, []int{1}},
|
||||
{"singleton large index", 1, func(l mutableIntLink) { l.DeleteAt(1) }, []int{1}},
|
||||
{"two item head index", 2, func(l mutableIntLink) { l.DeleteAt(0) }, []int{2}},
|
||||
{"two item tail index", 2, func(l mutableIntLink) { l.DeleteAt(1) }, []int{1}},
|
||||
{"middle index", 4, func(l mutableIntLink) { l.DeleteAt(1) }, []int{1, 3, 4}},
|
||||
{"tail index", 4, func(l mutableIntLink) { l.DeleteAt(3) }, []int{1, 2, 3}},
|
||||
{"negative index", 4, func(l mutableIntLink) { l.DeleteAt(-1) }, []int{1, 2, 3, 4}},
|
||||
{"large index", 4, func(l mutableIntLink) { l.DeleteAt(4) }, []int{1, 2, 3, 4}},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer func() {
|
||||
if value := recover(); value != nil {
|
||||
t.Errorf("list mutation panicked: %v", value)
|
||||
}
|
||||
}()
|
||||
l := newLink()
|
||||
for i := 1; i <= tt.size; i++ {
|
||||
l.InsertAtTail(i)
|
||||
}
|
||||
tt.delete(l)
|
||||
if got := l.Values(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("Values() = %v, want %v", got, tt.want)
|
||||
}
|
||||
if l.Size() != len(tt.want) || l.IsEmpty() != (len(tt.want) == 0) {
|
||||
t.Fatalf("Size() = %d, IsEmpty() = %v, want length %d", l.Size(), l.IsEmpty(), len(tt.want))
|
||||
}
|
||||
if doubly, ok := l.(*DoublyLink[int]); ok {
|
||||
current := doubly.Head
|
||||
for i := 0; current != nil; i++ {
|
||||
if i >= l.Size() {
|
||||
t.Fatal("cycle in list after deletion")
|
||||
}
|
||||
if current.Next != nil && current.Next.Pre != current {
|
||||
t.Fatal("deletion did not preserve reciprocal links")
|
||||
}
|
||||
current = current.Next
|
||||
}
|
||||
}
|
||||
l.Reverse()
|
||||
wantReverse := make([]int, len(tt.want))
|
||||
for i, value := range tt.want {
|
||||
wantReverse[len(tt.want)-1-i] = value
|
||||
}
|
||||
if got := l.Values(); !reflect.DeepEqual(got, wantReverse) {
|
||||
t.Fatalf("Reverse Values() = %v, want %v", got, wantReverse)
|
||||
}
|
||||
l.Reverse()
|
||||
l.InsertAtHead(9)
|
||||
if got := l.Values(); !reflect.DeepEqual(got, append([]int{9}, tt.want...)) {
|
||||
t.Fatalf("reused list Values() = %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoublyLinkMiddleMutationBacklinks(t *testing.T) {
|
||||
l := NewDoublyLink[int]()
|
||||
l.InsertAtTail(1)
|
||||
l.InsertAtTail(3)
|
||||
l.InsertAt(1, 2)
|
||||
if l.Head.Next.Next.Pre != l.Head.Next {
|
||||
t.Fatal("middle insertion did not update the next node's Pre link")
|
||||
}
|
||||
l.Reverse()
|
||||
if got := l.Values(); !reflect.DeepEqual(got, []int{3, 2, 1}) {
|
||||
t.Fatalf("Reverse() after insertion = %v", got)
|
||||
}
|
||||
l.DeleteAt(1)
|
||||
if l.Head.Next.Pre != l.Head {
|
||||
t.Fatal("middle deletion did not update the next node's Pre link")
|
||||
}
|
||||
l.Reverse()
|
||||
if got := l.Values(); !reflect.DeepEqual(got, []int{1, 3}) {
|
||||
t.Fatalf("Reverse() after deletion = %v", got)
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,7 @@ func (sl *SinglyLink[T]) DeleteAtTail() {
|
||||
current := sl.Head
|
||||
if current.Next == nil {
|
||||
sl.DeleteAtHead()
|
||||
return
|
||||
}
|
||||
|
||||
for current.Next.Next != nil {
|
||||
@@ -116,22 +117,20 @@ func (sl *SinglyLink[T]) DeleteAtTail() {
|
||||
// DeleteAt delete value in singly linklist at index
|
||||
// param `index` should be [0, len(SinglyLink)-1]
|
||||
func (sl *SinglyLink[T]) DeleteAt(index int) {
|
||||
if sl.Head == nil {
|
||||
if sl.Head == nil || index < 0 || index >= sl.length {
|
||||
return
|
||||
}
|
||||
current := sl.Head
|
||||
if current.Next == nil || index == 0 {
|
||||
if index == 0 {
|
||||
sl.DeleteAtHead()
|
||||
return
|
||||
}
|
||||
|
||||
if index == sl.length-1 {
|
||||
sl.DeleteAtTail()
|
||||
}
|
||||
|
||||
if index < 0 || index > sl.length-1 {
|
||||
return
|
||||
}
|
||||
|
||||
current := sl.Head
|
||||
i := 0
|
||||
for current != nil {
|
||||
if i == index-1 {
|
||||
|
||||
@@ -354,6 +354,7 @@ func (c *CopyOnWriteList[T]) ForEach(f func(T)) {
|
||||
func (c *CopyOnWriteList[T]) Sort(compare func(o1 T, o2 T) bool) {
|
||||
lock := c.lock
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
list := c.getList()
|
||||
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
@@ -374,6 +375,5 @@ func (c *CopyOnWriteList[T]) SubList(start int, end int) (newList []T) {
|
||||
}
|
||||
newList = make([]T, end-start)
|
||||
copy(newList, list[start:end])
|
||||
c.setList(newList)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package datastructure_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
list "github.com/duke-git/lancet/v2/datastructure/list"
|
||||
)
|
||||
|
||||
func TestCopyOnWriteSortReleasesLock(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
values []int
|
||||
panicCompare bool
|
||||
sort bool
|
||||
}{
|
||||
{"sorted values", []int{3, 1, 2}, false, true},
|
||||
{"empty values", []int{}, false, true},
|
||||
{"comparator panic", []int{3, 1, 2}, true, true},
|
||||
{"without sort control", []int{1, 2, 3}, false, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := list.NewCopyOnWriteList(tc.values)
|
||||
marker := new(int)
|
||||
if tc.sort {
|
||||
func() {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if tc.panicCompare {
|
||||
if r != marker {
|
||||
t.Errorf("panic identity changed: %v", r)
|
||||
}
|
||||
} else if r != nil {
|
||||
t.Errorf("unexpected panic: %v", r)
|
||||
}
|
||||
}()
|
||||
c.Sort(func(a, b int) bool {
|
||||
if tc.panicCompare {
|
||||
panic(marker)
|
||||
}
|
||||
return a < b
|
||||
})
|
||||
}()
|
||||
}
|
||||
if tc.sort && !tc.panicCompare {
|
||||
got := []int{}
|
||||
c.ForEach(func(v int) { got = append(got, v) })
|
||||
want := []int{1, 2, 3}
|
||||
if len(tc.values) == 0 {
|
||||
want = []int{}
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("sort got=%v want=%v", got, want)
|
||||
}
|
||||
}
|
||||
done := make(chan struct{})
|
||||
go func() { c.Add(4); close(done) }()
|
||||
select {
|
||||
case <-done:
|
||||
if c.Size() != len(tc.values)+1 {
|
||||
t.Fatal("next mutation did not persist")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Sort left the mutation lock held")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package datastructure_test
|
||||
|
||||
import (
|
||||
list "github.com/duke-git/lancet/v2/datastructure/list"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCopyOnWriteSubListPreservesReceiver(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
start, end int
|
||||
want []int
|
||||
}{
|
||||
{"middle", 1, 3, []int{2, 3}}, {"empty range", 2, 2, []int{}}, {"prefix", 0, 2, []int{1, 2}}, {"suffix", 2, 4, []int{3, 4}}, {"full control", 0, 4, []int{1, 2, 3, 4}}, {"invalid control", 3, 1, []int{}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := list.NewCopyOnWriteList([]int{1, 2, 3, 4})
|
||||
got := c.SubList(tc.start, tc.end)
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Fatalf("sublist=%v want%v", got, tc.want)
|
||||
}
|
||||
values := []int{}
|
||||
c.ForEach(func(v int) { values = append(values, v) })
|
||||
if c.Size() != 4 || !reflect.DeepEqual(values, []int{1, 2, 3, 4}) {
|
||||
t.Fatalf("SubList discarded source values: %v", values)
|
||||
}
|
||||
if len(got) > 0 {
|
||||
got[0] = 99
|
||||
}
|
||||
if *c.Get(0) != 1 || *c.Get(1) != 2 {
|
||||
t.Fatal("returned slice aliases receiver")
|
||||
}
|
||||
c.Add(5)
|
||||
if c.Size() != 5 || *c.Get(4) != 5 {
|
||||
t.Fatal("later append changed")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ func (t *BSTree[T]) Insert(data T) {
|
||||
|
||||
// DeletetNode delete data into BSTree
|
||||
func (t *BSTree[T]) Delete(data T) {
|
||||
deleteTreeNode(t.root, data, t.comparator)
|
||||
t.root = deleteTreeNode(t.root, data, t.comparator)
|
||||
}
|
||||
|
||||
// NodeLevel get node level in BSTree
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestBSTree_PostOrderTraverse(t *testing.T) {
|
||||
bstree.Insert(4)
|
||||
|
||||
acturl := bstree.PostOrderTraverse()
|
||||
assert.Equal([]int{5, 2, 4, 7, 6}, acturl)
|
||||
assert.Equal([]int{4, 2, 5, 7, 6}, acturl)
|
||||
}
|
||||
|
||||
func TestBSTree_InOrderTraverse(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package datastructure_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
tree "github.com/duke-git/lancet/v2/datastructure/tree"
|
||||
)
|
||||
|
||||
type postorderComparator struct{}
|
||||
|
||||
func (postorderComparator) Compare(a, b any) int {
|
||||
x, y := a.(int), b.(int)
|
||||
if x < y {
|
||||
return -1
|
||||
}
|
||||
if x > y {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func TestBSTreePostOrderVisitsDescendantsBeforeParents(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
input, want []int
|
||||
}{
|
||||
{"balanced", []int{4, 2, 6, 1, 3, 5, 7}, []int{1, 3, 2, 5, 7, 6, 4}},
|
||||
{"left chain", []int{5, 4, 3, 2}, []int{2, 3, 4, 5}},
|
||||
{"right chain", []int{1, 2, 3, 4}, []int{4, 3, 2, 1}},
|
||||
{"duplicate values", []int{5, 5, 5, 6}, []int{6, 5, 5, 5}},
|
||||
{"single root", []int{7}, []int{7}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tr := tree.NewBSTree(tc.input[0], postorderComparator{})
|
||||
for _, v := range tc.input[1:] {
|
||||
tr.Insert(v)
|
||||
}
|
||||
if got := tr.PostOrderTraverse(); !reflect.DeepEqual(got, tc.want) {
|
||||
t.Fatalf("postorder=%v want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestBSTreePostOrderEmptyControl(t *testing.T) {
|
||||
tr := tree.NewBSTree(7, postorderComparator{})
|
||||
tr.Delete(7)
|
||||
if got := tr.PostOrderTraverse(); len(got) != 0 {
|
||||
t.Fatalf("empty traversal=%v", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package datastructure
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBSTreeDeleteReplacesRoot(t *testing.T) {
|
||||
for i, tt := range []struct {
|
||||
values []int
|
||||
remove int
|
||||
want []int
|
||||
}{
|
||||
{nil, 4, nil},
|
||||
{[]int{2, 1, 3}, 4, []int{1, 2, 3}},
|
||||
{[]int{6, 5, 7}, 4, []int{5, 6, 7}},
|
||||
{[]int{2, 6, 1, 3, 5, 7}, 4, []int{1, 2, 3, 5, 6, 7}},
|
||||
{[]int{2, 6}, 9, []int{2, 4, 6}},
|
||||
{[]int{2, 6}, 2, []int{4, 6}},
|
||||
{[]int{2, 6, 1}, 2, []int{1, 4, 6}},
|
||||
{[]int{2, 6, 1, 3}, 2, []int{1, 3, 4, 6}},
|
||||
} {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
tree := NewBSTree(4, &intComparator{})
|
||||
for _, value := range tt.values {
|
||||
tree.Insert(value)
|
||||
}
|
||||
tree.Delete(tt.remove)
|
||||
checkBSTValues(t, tree, tt.want)
|
||||
tree.Delete(tt.remove)
|
||||
checkBSTValues(t, tree, tt.want)
|
||||
tree.Insert(8)
|
||||
want := append(append([]int(nil), tt.want...), 8)
|
||||
sort.Ints(want)
|
||||
checkBSTValues(t, tree, want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkBSTValues(t *testing.T, tree *BSTree[int], want []int) {
|
||||
t.Helper()
|
||||
inOrder := tree.InOrderTraverse()
|
||||
if !sort.IntsAreSorted(inOrder) {
|
||||
t.Fatalf("in-order traversal is not sorted: %v", inOrder)
|
||||
}
|
||||
for name, got := range map[string][]int{
|
||||
"in-order": inOrder,
|
||||
"pre-order": tree.PreOrderTraverse(),
|
||||
"post-order": tree.PostOrderTraverse(),
|
||||
"level-order": tree.LevelOrderTraverse(),
|
||||
} {
|
||||
sort.Ints(got)
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("%s traversal = %v, want values %v", name, got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("%s traversal = %v, want values %v", name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(want) == 0 && tree.Depth() != 0 {
|
||||
t.Fatalf("empty tree depth = %d", tree.Depth())
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ func preOrderTraverse[T any](node *datastructure.TreeNode[T]) []T {
|
||||
func postOrderTraverse[T any](node *datastructure.TreeNode[T]) []T {
|
||||
data := []T{}
|
||||
if node != nil {
|
||||
data = append(data, preOrderTraverse(node.Left)...)
|
||||
data = append(data, preOrderTraverse(node.Right)...)
|
||||
data = append(data, postOrderTraverse(node.Left)...)
|
||||
data = append(data, postOrderTraverse(node.Right)...)
|
||||
data = append(data, node.Value)
|
||||
}
|
||||
return data
|
||||
@@ -69,6 +69,9 @@ func inOrderTraverse[T any](node *datastructure.TreeNode[T]) []T {
|
||||
// }
|
||||
|
||||
func levelOrderTraverse[T any](root *datastructure.TreeNode[T], traversal *[]T) {
|
||||
if root == nil {
|
||||
return
|
||||
}
|
||||
var q []*datastructure.TreeNode[T] // queue
|
||||
var n *datastructure.TreeNode[T] // temp node
|
||||
|
||||
@@ -102,7 +105,6 @@ func insertTreeNode[T any](rootNode, newNode *datastructure.TreeNode[T], compara
|
||||
}
|
||||
}
|
||||
|
||||
// todo, delete root node failed
|
||||
func deleteTreeNode[T any](node *datastructure.TreeNode[T], data T, comparator constraints.Comparator) *datastructure.TreeNode[T] {
|
||||
if node == nil {
|
||||
return nil
|
||||
|
||||
@@ -188,15 +188,13 @@ func GetTodayEndTime() string {
|
||||
// GetZeroHourTimestamp return timestamp of zero hour (timestamp of 00:00).
|
||||
// Play: https://go.dev/play/p/QmL2oIaGE3q
|
||||
func GetZeroHourTimestamp() int64 {
|
||||
ts := time.Now().Format("2006-01-02")
|
||||
t, _ := time.Parse("2006-01-02", ts)
|
||||
return t.UTC().Unix() - 8*3600
|
||||
return BeginOfDay(time.Now()).Unix()
|
||||
}
|
||||
|
||||
// GetNightTimestamp return timestamp of zero hour (timestamp of 23:59).
|
||||
// Play: https://go.dev/play/p/UolysR3MYP1
|
||||
func GetNightTimestamp() int64 {
|
||||
return GetZeroHourTimestamp() + 86400 - 1
|
||||
return EndOfDay(time.Now()).Unix()
|
||||
}
|
||||
|
||||
// FormatTimeToStr convert time to string.
|
||||
|
||||
@@ -943,3 +943,28 @@ func TestBetweenSeconds(t *testing.T) {
|
||||
assert.Equal(int64(86400), result1)
|
||||
assert.Equal(int64(-86400), result2)
|
||||
}
|
||||
|
||||
func TestGetZeroHourAndNightTimestampLocalZone(t *testing.T) {
|
||||
// not parallel: this test replaces time.Local
|
||||
assert := internal.NewAssert(t, "TestGetZeroHourAndNightTimestampLocalZone")
|
||||
|
||||
oldLocal := time.Local
|
||||
defer func() { time.Local = oldLocal }()
|
||||
|
||||
for _, offset := range []int{-5 * 3600, 0, 8 * 3600, 9 * 3600} {
|
||||
time.Local = time.FixedZone("test", offset)
|
||||
|
||||
before := time.Now()
|
||||
zero := GetZeroHourTimestamp()
|
||||
night := GetNightTimestamp()
|
||||
after := time.Now()
|
||||
|
||||
if before.Day() != after.Day() {
|
||||
continue
|
||||
}
|
||||
|
||||
assert.Equal(BeginOfDay(before).Unix(), zero)
|
||||
assert.Equal(EndOfDay(before).Unix(), night)
|
||||
assert.Equal(int64(86399), night-zero)
|
||||
}
|
||||
}
|
||||
+11
-6
@@ -56,12 +56,16 @@ func (eb *EventBus[T]) Subscribe(topic string, listener func(eventData T), async
|
||||
listenersInterface, _ := eb.listeners.LoadOrStore(topic, []*EventListener[T]{})
|
||||
listeners := listenersInterface.([]*EventListener[T])
|
||||
|
||||
listeners = append(listeners, el)
|
||||
sort.Slice(listeners, func(i, j int) bool {
|
||||
return listeners[i].priority > listeners[j].priority
|
||||
// Build a new slice instead of appending in place: Publish iterates over the
|
||||
// stored slice after releasing the lock, so it must never be modified.
|
||||
updated := make([]*EventListener[T], len(listeners), len(listeners)+1)
|
||||
copy(updated, listeners)
|
||||
updated = append(updated, el)
|
||||
sort.Slice(updated, func(i, j int) bool {
|
||||
return updated[i].priority > updated[j].priority
|
||||
})
|
||||
|
||||
eb.listeners.Store(topic, listeners)
|
||||
eb.listeners.Store(topic, updated)
|
||||
}
|
||||
|
||||
// Unsubscribe unsubscribes from an event with a specific event topic and listener function.
|
||||
@@ -91,10 +95,11 @@ func (eb *EventBus[T]) Unsubscribe(topic string, listener func(eventData T)) {
|
||||
// Publish publishes an event with a specific event topic and data payload.
|
||||
// Play: https://go.dev/play/p/gHTtVexFSH9
|
||||
func (eb *EventBus[T]) Publish(event Event[T]) {
|
||||
// The lock is only held to read the listeners, so that a listener can call
|
||||
// Subscribe, Unsubscribe or Publish without deadlocking.
|
||||
eb.mu.RLock()
|
||||
defer eb.mu.RUnlock()
|
||||
|
||||
listenersInterface, exists := eb.listeners.Load(event.Topic)
|
||||
eb.mu.RUnlock()
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -220,3 +220,58 @@ func TestEventBus_GetEvents(t *testing.T) {
|
||||
assert.Equal(2, len(events))
|
||||
assert.Equal([]string{"event1", "event2"}, events)
|
||||
}
|
||||
|
||||
func TestEventBus_ListenerCanChangeSubscriptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := internal.NewAssert(t, "TestEventBus_ListenerCanChangeSubscriptions")
|
||||
|
||||
eb := NewEventBus[int]()
|
||||
|
||||
var self func(eventData int)
|
||||
self = func(eventData int) {
|
||||
eb.Unsubscribe("event1", self)
|
||||
eb.Subscribe("event2", func(eventData int) {}, false, 0, nil)
|
||||
}
|
||||
eb.Subscribe("event1", self, false, 0, nil)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
eb.Publish(Event[int]{Topic: "event1", Payload: 1})
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Publish did not return when a listener changed the subscriptions")
|
||||
}
|
||||
|
||||
assert.Equal(0, eb.GetListenersCount("event1"))
|
||||
assert.Equal(1, eb.GetListenersCount("event2"))
|
||||
}
|
||||
|
||||
func TestEventBus_PublishWhileSubscribing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
eb := NewEventBus[int]()
|
||||
eb.Subscribe("event1", func(eventData int) {}, false, 1, nil)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 200; i++ {
|
||||
eb.Publish(Event[int]{Topic: "event1", Payload: i})
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 200; i++ {
|
||||
eb.Subscribe("event1", func(eventData int) {}, false, i%7, nil)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
+17
-2
@@ -92,7 +92,7 @@ func DecimalBytes(size float64, precision ...int) string {
|
||||
pointPosition = precision[0]
|
||||
}
|
||||
|
||||
size, unit := calculateByteSize(size, 1000.0, decimalByteUnits)
|
||||
size, unit := calculateRoundedByteSize(size, 1000.0, decimalByteUnits, pointPosition)
|
||||
|
||||
return roundToToString(size, pointPosition) + unit
|
||||
}
|
||||
@@ -106,7 +106,7 @@ func BinaryBytes(size float64, precision ...int) string {
|
||||
pointPosition = precision[0]
|
||||
}
|
||||
|
||||
size, unit := calculateByteSize(size, 1024.0, binaryByteUnits)
|
||||
size, unit := calculateRoundedByteSize(size, 1024.0, binaryByteUnits, pointPosition)
|
||||
|
||||
return roundToToString(size, pointPosition) + unit
|
||||
}
|
||||
@@ -121,6 +121,21 @@ func calculateByteSize(size float64, base float64, byteUnits []string) (float64,
|
||||
return size, byteUnits[i]
|
||||
}
|
||||
|
||||
// calculateRoundedByteSize works like calculateByteSize, but moves to the next unit
|
||||
// when the value rounds up to the base at the given (non-negative) precision (e.g. 999999 bytes
|
||||
// with precision 2 gives 1MB instead of 1000KB).
|
||||
func calculateRoundedByteSize(size float64, base float64, byteUnits []string, precision int) (float64, string) {
|
||||
size, unit := calculateByteSize(size, base, byteUnits)
|
||||
i := 0
|
||||
for byteUnits[i] != unit {
|
||||
i++
|
||||
}
|
||||
if precision >= 0 && i < len(byteUnits)-1 && mathutil.RoundToFloat(size, precision) >= base {
|
||||
return size / base, byteUnits[i+1]
|
||||
}
|
||||
return size, unit
|
||||
}
|
||||
|
||||
func roundToToString(x float64, max ...int) string {
|
||||
pointPosition := 4
|
||||
if len(max) > 0 {
|
||||
|
||||
@@ -28,6 +28,10 @@ func TestDecimalBytes(t *testing.T) {
|
||||
|
||||
assert.Equal("4MB", DecimalBytes(4000000, 0))
|
||||
assert.Equal("4MB", DecimalBytes(4000000, 1))
|
||||
|
||||
assert.Equal("1MB", DecimalBytes(999999, 2))
|
||||
assert.Equal("1GB", DecimalBytes(999999999, 2))
|
||||
assert.Equal("999.99KB", DecimalBytes(999990, 2))
|
||||
}
|
||||
|
||||
func TestBinaryBytes(t *testing.T) {
|
||||
@@ -43,6 +47,9 @@ func TestBinaryBytes(t *testing.T) {
|
||||
assert.Equal("10KiB", BinaryBytes(10240, 0))
|
||||
assert.Equal("10KiB", BinaryBytes(10240, 1))
|
||||
assert.Equal("10KiB", BinaryBytes(10240, 2))
|
||||
|
||||
assert.Equal("1MiB", BinaryBytes(1048575, 2))
|
||||
assert.Equal("1023.99KiB", BinaryBytes(1048566, 2))
|
||||
}
|
||||
|
||||
func TestParseDecimalBytes(t *testing.T) {
|
||||
|
||||
@@ -32,9 +32,11 @@ func Comma[T constraints.Float | constraints.Integer | string](value T, prefixSy
|
||||
numString = numString[1:]
|
||||
}
|
||||
|
||||
index := strings.Index(numString, ".")
|
||||
if index == -1 {
|
||||
index = len(numString)
|
||||
// Only the leading run of digits gets commas, so that an exponent ("1e+21")
|
||||
// or any other suffix that ParseFloat accepts is left as it is.
|
||||
index := 0
|
||||
for index < len(numString) && numString[index] >= '0' && numString[index] <= '9' {
|
||||
index++
|
||||
}
|
||||
|
||||
for index > 3 {
|
||||
|
||||
@@ -35,6 +35,12 @@ func TestComma(t *testing.T) {
|
||||
assert.Equal("-999", Comma(-999, ""))
|
||||
assert.Equal("-1,000", Comma(-1000, ""))
|
||||
assert.Equal("-1,234,567", Comma(-1234567, ""))
|
||||
|
||||
assert.Equal("1e+21", Comma("1e+21", ""))
|
||||
assert.Equal("$12,345,678e3", Comma("12345678e3", "$"))
|
||||
assert.Equal("1_000", Comma("1_000", ""))
|
||||
assert.Equal("1,234,567.", Comma("1234567.", ""))
|
||||
assert.Equal(".12345", Comma(".12345", ""))
|
||||
}
|
||||
|
||||
func TestPretty(t *testing.T) {
|
||||
|
||||
+9
-1
@@ -706,10 +706,18 @@ func ToMarkdownTable(data []map[string]interface{}, headerMap map[string]string,
|
||||
headers = make([]string, len(columnOrder))
|
||||
copy(headers, columnOrder)
|
||||
} else {
|
||||
// 否则按自然顺序提取headers
|
||||
// 否则按每行键的字典序提取headers
|
||||
seen := make(map[string]bool)
|
||||
for _, row := range data {
|
||||
// Map iteration order is random, so visit the keys of each row in sorted
|
||||
// order to get the same columns in the same order on every call.
|
||||
keys := make([]string, 0, len(row))
|
||||
for k := range row {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
if !seen[k] {
|
||||
seen[k] = true
|
||||
headers = append(headers, k)
|
||||
|
||||
@@ -997,3 +997,23 @@ func TestToMarkdownTableWithHeaderMap(t *testing.T) {
|
||||
fmt.Printf("%s", result)
|
||||
// 验证中文表头是否正确显示
|
||||
}
|
||||
|
||||
func TestToMarkdownTableDefaultColumnOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []map[string]interface{}{
|
||||
{"name": "Alice", "age": 25, "salary": 50000, "city": "Seoul"},
|
||||
{"name": "Bob", "zip": "04524", "age": 30},
|
||||
}
|
||||
|
||||
expected := "| age | city | name | salary | zip |\n" +
|
||||
"|---|---|---|---|---|\n" +
|
||||
"| 25 | Seoul | Alice | 50,000 | |\n" +
|
||||
"| 30 | | Bob | | 04524 |\n"
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
if result := ToMarkdownTable(data, nil, nil); result != expected {
|
||||
t.Fatalf("call %d: expected:\n%s\ngot:\n%s", i, expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-4
@@ -452,9 +452,18 @@ func Combination(n, k uint) uint {
|
||||
return 0
|
||||
}
|
||||
|
||||
nFactorial := Factorial(n)
|
||||
kFactorial := Factorial(k)
|
||||
nMinusKFactorial := Factorial(n - k)
|
||||
// C(n, k) == C(n, n-k), so the loop below runs min(k, n-k) times.
|
||||
if k > n-k {
|
||||
k = n - k
|
||||
}
|
||||
|
||||
return nFactorial / (kFactorial * nMinusKFactorial)
|
||||
// Build C(n-k+i, i) for i = 1..k. Each step is an exact integer, and dividing
|
||||
// by the gcd first avoids the overflow of computing the factorials of n and k.
|
||||
result := uint(1)
|
||||
for i := uint(1); i <= k; i++ {
|
||||
g := gcd(result, i)
|
||||
result = result / g * ((n - k + i) / (i / g))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package mathutil
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/bits"
|
||||
"testing"
|
||||
|
||||
"github.com/duke-git/lancet/v2/internal"
|
||||
@@ -577,9 +578,39 @@ func TestCombination(t *testing.T) {
|
||||
{0, 0, 1},
|
||||
{3, 2, 3},
|
||||
{4, 2, 6},
|
||||
{5, 6, 0},
|
||||
{5, 5, 1},
|
||||
{5, 0, 1},
|
||||
{21, 10, 352716},
|
||||
{34, 17, 2333606220},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
assert.Equal(tt.expected, Combination(tt.n, tt.k))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombinationLarge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if bits.UintSize < 64 {
|
||||
t.Skip("the expected values do not fit in a 32-bit uint")
|
||||
}
|
||||
|
||||
assert := internal.NewAssert(t, "TestCombinationLarge")
|
||||
|
||||
tests := []struct {
|
||||
n uint
|
||||
k uint
|
||||
expected uint64
|
||||
}{
|
||||
{40, 20, 137846528820},
|
||||
{60, 30, 118264581564861424},
|
||||
{62, 31, 465428353255261088},
|
||||
{64, 32, 1832624140942590534},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
assert.Equal(tt.expected, uint64(Combination(tt.n, tt.k)))
|
||||
}
|
||||
}
|
||||
+36
-10
@@ -49,10 +49,6 @@ func (p *Promise[T]) run() {
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if !p.pending {
|
||||
return
|
||||
}
|
||||
|
||||
if err := recover(); err != nil {
|
||||
p.reject(errors.New(fmt.Sprint(err)))
|
||||
}
|
||||
@@ -177,7 +173,7 @@ func All[T any](promises []*Promise[T]) *Promise[[]T] {
|
||||
|
||||
return New(func(resolve func([]T), reject func(error)) {
|
||||
valsChan := make(chan tuple[T, int], len(promises))
|
||||
errsChan := make(chan error, 1)
|
||||
errsChan := make(chan error, len(promises))
|
||||
|
||||
for idx, p := range promises {
|
||||
idx := idx
|
||||
@@ -212,8 +208,10 @@ func Race[T any](promises []*Promise[T]) *Promise[T] {
|
||||
}
|
||||
|
||||
return New(func(resolve func(T), reject func(error)) {
|
||||
valsChan := make(chan T, 1)
|
||||
errsChan := make(chan error, 1)
|
||||
// every promise may settle, so each channel holds all of them. A smaller
|
||||
// buffer leaves the callbacks of the losing promises blocked forever.
|
||||
valsChan := make(chan T, len(promises))
|
||||
errsChan := make(chan error, len(promises))
|
||||
|
||||
for _, p := range promises {
|
||||
_ = Then(p, func(data T) T {
|
||||
@@ -243,7 +241,7 @@ func Any[T any](promises []*Promise[T]) *Promise[T] {
|
||||
}
|
||||
|
||||
return New(func(resolve func(T), reject func(error)) {
|
||||
valsChan := make(chan T, 1)
|
||||
valsChan := make(chan T, len(promises))
|
||||
errsChan := make(chan tuple[error, int], len(promises))
|
||||
|
||||
for idx, p := range promises {
|
||||
@@ -270,10 +268,38 @@ func Any[T any](promises []*Promise[T]) *Promise[T] {
|
||||
}
|
||||
|
||||
errCombo := errs[0]
|
||||
for _, err := range errs[1:] {
|
||||
errCombo = internal.JoinError(err)
|
||||
if len(errs) > 1 {
|
||||
errCombo = &anyError{errs: errs}
|
||||
}
|
||||
|
||||
reject(errCombo)
|
||||
})
|
||||
}
|
||||
|
||||
// anyError is the error of Any when every promise is rejected. It keeps errors.Is and
|
||||
// errors.As working for each member, also on Go versions that cannot traverse Unwrap() []error.
|
||||
type anyError struct {
|
||||
errs []error
|
||||
}
|
||||
|
||||
func (e *anyError) Error() string {
|
||||
return internal.JoinError(e.errs...).Error()
|
||||
}
|
||||
|
||||
func (e *anyError) Is(target error) bool {
|
||||
for _, err := range e.errs {
|
||||
if errors.Is(err, target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *anyError) As(target any) bool {
|
||||
for _, err := range e.errs {
|
||||
if errors.As(err, target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package promise
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -242,8 +243,44 @@ func TestAny(t *testing.T) {
|
||||
assert.IsNotNil(err)
|
||||
})
|
||||
|
||||
t.Run("OnlyRejectedJoinsAllErrors", func(_ *testing.T) {
|
||||
err1 := errors.New("error1")
|
||||
err2 := &testPathError{msg: "error2"}
|
||||
p1 := New(func(resolve func(string), reject func(error)) {
|
||||
reject(err1)
|
||||
})
|
||||
p2 := New(func(resolve func(string), reject func(error)) {
|
||||
reject(err2)
|
||||
})
|
||||
|
||||
_, err := Any([]*Promise[string]{p1, p2}).Await()
|
||||
|
||||
assert.IsNotNil(err)
|
||||
assert.Equal("error1\nerror2", err.Error())
|
||||
assert.Equal(true, errors.Is(err, err1))
|
||||
assert.Equal(true, errors.Is(err, err2))
|
||||
|
||||
var target *testPathError
|
||||
assert.Equal(true, errors.As(err, &target))
|
||||
assert.Equal(true, target == err2)
|
||||
})
|
||||
|
||||
t.Run("SingleRejectedKeepsError", func(_ *testing.T) {
|
||||
e := errors.New("only")
|
||||
p1 := New(func(resolve func(string), reject func(error)) {
|
||||
reject(e)
|
||||
})
|
||||
|
||||
_, err := Any([]*Promise[string]{p1}).Await()
|
||||
|
||||
assert.Equal(true, err == e)
|
||||
})
|
||||
}
|
||||
|
||||
type testPathError struct{ msg string }
|
||||
|
||||
func (e *testPathError) Error() string { return e.msg }
|
||||
|
||||
func TestRace(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestPromise_Race")
|
||||
|
||||
@@ -310,3 +347,39 @@ func TestRace(t *testing.T) {
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestRaceAnyAllDoNotLeakGoroutines(t *testing.T) {
|
||||
// not parallel: it counts the goroutines of the whole test binary
|
||||
assert := internal.NewAssert(t, "TestRaceAnyAllDoNotLeakGoroutines")
|
||||
|
||||
newResolved := func(n int) []*Promise[int] {
|
||||
ps := make([]*Promise[int], 0, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
ps = append(ps, New(func(resolve func(int), reject func(error)) { resolve(n) }))
|
||||
}
|
||||
return ps
|
||||
}
|
||||
newRejected := func() []*Promise[int] {
|
||||
ps := make([]*Promise[int], 0, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
ps = append(ps, New(func(resolve func(int), reject func(error)) { reject(errors.New("e")) }))
|
||||
}
|
||||
return ps
|
||||
}
|
||||
|
||||
before := runtime.NumGoroutine()
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
_, _ = Race(newResolved(i)).Await()
|
||||
_, _ = Race(newRejected()).Await()
|
||||
_, _ = Any(newResolved(i)).Await()
|
||||
_, _ = All(newRejected()).Await()
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for runtime.NumGoroutine() > before && time.Now().Before(deadline) {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
assert.Equal(true, runtime.NumGoroutine() <= before)
|
||||
}
|
||||
+2
-1
@@ -81,7 +81,8 @@ func RetryWithExponentialWithJitterBackoff(interval time.Duration, base uint64,
|
||||
panic("programming error: retry maxJitter should not be lower to 0")
|
||||
}
|
||||
|
||||
if base%2 == 0 {
|
||||
// only a power of two can be replaced by a bit shift without changing the multiplier
|
||||
if base != 0 && base&(base-1) == 0 {
|
||||
return func(rc *RetryConfig) {
|
||||
rc.backoffStrategy = &shiftExponentialWithJitter{
|
||||
interval: interval,
|
||||
|
||||
@@ -258,3 +258,20 @@ func TestCancelRetry(t *testing.T) {
|
||||
assert.IsNotNil(err)
|
||||
assert.Equal(4, number)
|
||||
}
|
||||
|
||||
func TestRetryExponentialBackoffMultiplier(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := internal.NewAssert(t, "TestRetryExponentialBackoffMultiplier")
|
||||
|
||||
for _, base := range []uint64{2, 3, 4, 6, 8, 10} {
|
||||
rc := &RetryConfig{}
|
||||
RetryWithExponentialWithJitterBackoff(time.Millisecond, base, 0)(rc)
|
||||
|
||||
want := time.Millisecond
|
||||
for i := 0; i < 3; i++ {
|
||||
assert.Equal(want, rc.backoffStrategy.CalculateInterval())
|
||||
want *= time.Duration(base)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -373,7 +373,7 @@ func (s Stream[T]) Max(less func(a, b T) bool) (T, bool) {
|
||||
}
|
||||
|
||||
for i, v := range s.source {
|
||||
if less(v, max) || i == 0 {
|
||||
if i == 0 || less(v, max) {
|
||||
max = v
|
||||
}
|
||||
}
|
||||
@@ -391,7 +391,7 @@ func (s Stream[T]) Min(less func(a, b T) bool) (T, bool) {
|
||||
}
|
||||
|
||||
for i, v := range s.source {
|
||||
if less(v, min) || i == 0 {
|
||||
if i == 0 || less(v, min) {
|
||||
min = v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,3 +423,20 @@ func TestStream_ToMap(t *testing.T) {
|
||||
assert.EqualValues(expected, m)
|
||||
|
||||
}
|
||||
|
||||
func TestStream_MaxMinDoNotCompareWithZeroValue(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestStream_MaxMinDoNotCompareWithZeroValue")
|
||||
|
||||
type Person struct {
|
||||
Age int
|
||||
}
|
||||
s := FromSlice([]*Person{{Age: 20}, {Age: 10}, {Age: 30}})
|
||||
|
||||
max, ok := s.Max(func(a, b *Person) bool { return a.Age > b.Age })
|
||||
assert.Equal(true, ok)
|
||||
assert.Equal(30, max.Age)
|
||||
|
||||
min, ok := s.Min(func(a, b *Person) bool { return a.Age < b.Age })
|
||||
assert.Equal(true, ok)
|
||||
assert.Equal(10, min.Age)
|
||||
}
|
||||
+10
-16
@@ -60,25 +60,22 @@ func isDigit(r rune) bool {
|
||||
return r >= '0' && r <= '9'
|
||||
}
|
||||
|
||||
// isLower checks if a character is lower case ('a' to 'z')
|
||||
// isLower checks if a character is a lower case letter
|
||||
func isLower(r rune) bool {
|
||||
return r >= 'a' && r <= 'z'
|
||||
return unicode.IsLower(r)
|
||||
}
|
||||
|
||||
// isUpper checks if a character is upper case ('A' to 'Z')
|
||||
// isUpper checks if a character is an upper case letter
|
||||
func isUpper(r rune) bool {
|
||||
return r >= 'A' && r <= 'Z'
|
||||
return unicode.IsUpper(r)
|
||||
}
|
||||
|
||||
// toLower converts a character 'A' to 'Z' to its lower case
|
||||
// toLower converts a character to its lower case
|
||||
func toLower(r rune) rune {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return r + 32
|
||||
}
|
||||
return r
|
||||
return unicode.ToLower(r)
|
||||
}
|
||||
|
||||
// toLowerAll converts a character 'A' to 'Z' to its lower case
|
||||
// toLowerAll converts characters to their lower case
|
||||
func toLowerAll(rs []rune) []rune {
|
||||
for i := range rs {
|
||||
rs[i] = toLower(rs[i])
|
||||
@@ -86,15 +83,12 @@ func toLowerAll(rs []rune) []rune {
|
||||
return rs
|
||||
}
|
||||
|
||||
// toUpper converts a character 'a' to 'z' to its upper case
|
||||
// toUpper converts a character to its upper case
|
||||
func toUpper(r rune) rune {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return r - 32
|
||||
}
|
||||
return r
|
||||
return unicode.ToUpper(r)
|
||||
}
|
||||
|
||||
// toUpperAll converts a character 'a' to 'z' to its upper case
|
||||
// toUpperAll converts characters to their upper case
|
||||
func toUpperAll(rs []rune) []rune {
|
||||
for i := range rs {
|
||||
rs[i] = toUpper(rs[i])
|
||||
|
||||
@@ -23,6 +23,8 @@ func TestCamelCase(t *testing.T) {
|
||||
{"$foo%", "foo"},
|
||||
{" $#$Foo 22 bar ", "foo22Bar"},
|
||||
{"Foo-#1😄$_%^&*(1bar", "foo11Bar"},
|
||||
{"Привет Мир", "приветМир"},
|
||||
{"Köln Straße", "kölnStraße"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -47,6 +49,8 @@ func TestUpperCamelCase(t *testing.T) {
|
||||
{"$foo%", "Foo"},
|
||||
{" $#$Foo 22 bar ", "Foo22Bar"},
|
||||
{"Foo-#1😄$_%^&*(1bar", "Foo11Bar"},
|
||||
{"Привет Мир", "ПриветМир"},
|
||||
{"Köln Straße", "KölnStraße"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -99,6 +103,8 @@ func TestKebabCase(t *testing.T) {
|
||||
{"$foo@Bar", "foo-bar"},
|
||||
{" $#$Foo 22 bar ", "foo-22-bar"},
|
||||
{"Foo-#1😄$_%^&*(1bar", "foo-1-1-bar"},
|
||||
{"Привет Мир", "привет-мир"},
|
||||
{"Köln Straße", "köln-straße"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -127,6 +133,8 @@ func TestUpperKebabCase(t *testing.T) {
|
||||
{"$foo@Bar", "FOO-BAR"},
|
||||
{" $#$Foo 22 bar ", "FOO-22-BAR"},
|
||||
{"Foo-#1😄$_%^&*(1bar", "FOO-1-1-BAR"},
|
||||
{"Привет Мир", "ПРИВЕТ-МИР"},
|
||||
{"Köln Straße", "KÖLN-STRAßE"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -155,6 +163,8 @@ func TestSnakeCase(t *testing.T) {
|
||||
{"$foo@Bar", "foo_bar"},
|
||||
{" $#$Foo 22 bar ", "foo_22_bar"},
|
||||
{"Foo-#1😄$_%^&*(1bar", "foo_1_1_bar"},
|
||||
{"Привет Мир", "привет_мир"},
|
||||
{"Köln Straße", "köln_straße"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -183,6 +193,8 @@ func TestUpperSnakeCase(t *testing.T) {
|
||||
{"$foo@Bar", "FOO_BAR"},
|
||||
{" $#$Foo 22 bar ", "FOO_22_BAR"},
|
||||
{"Foo-#1😄$_%^&*(1bar", "FOO_1_1_BAR"},
|
||||
{"Привет Мир", "ПРИВЕТ_МИР"},
|
||||
{"Köln Straße", "KÖLN_STRAßE"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
+1
-1
@@ -252,7 +252,7 @@ func parseProcessInfo(output []byte, pid int) (*ProcessInfo, error) {
|
||||
Memory: fields[2],
|
||||
State: fields[3],
|
||||
User: fields[4],
|
||||
Cmd: fields[5],
|
||||
Cmd: strings.Join(fields[5:], " "),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package system
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -142,3 +143,21 @@ func TestGetProcessInfo(t *testing.T) {
|
||||
|
||||
t.Log(processInfo)
|
||||
}
|
||||
|
||||
func TestParseProcessInfo_commandWithSpaces(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("parses ps output")
|
||||
}
|
||||
|
||||
assert := internal.NewAssert(t, "TestParseProcessInfo_commandWithSpaces")
|
||||
|
||||
output := []byte(" PID %CPU %MEM STAT USER COMM\n 4242 0.1 0.2 S root Google Chrome Helper\n")
|
||||
|
||||
info, err := parseProcessInfo(output, 4242)
|
||||
assert.IsNil(err)
|
||||
assert.Equal("Google Chrome Helper", info.Cmd)
|
||||
assert.Equal("root", info.User)
|
||||
assert.Equal("S", info.State)
|
||||
}
|
||||
@@ -87,6 +87,10 @@ func (e *XError) Error() string {
|
||||
return msg
|
||||
}
|
||||
|
||||
if msg == "" {
|
||||
return cause.Error()
|
||||
}
|
||||
|
||||
msg = fmt.Sprintf("%s: %v", msg, cause.Error())
|
||||
|
||||
return msg
|
||||
|
||||
@@ -115,3 +115,15 @@ func TestXError_Info(t *testing.T) {
|
||||
assert.Equal(cause, errInfo.Cause)
|
||||
assert.Equal("high", errInfo.Values["level"])
|
||||
}
|
||||
|
||||
func TestWrap_withoutMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := internal.NewAssert(t, "TestWrap_withoutMessage")
|
||||
|
||||
cause := errors.New("wrong password")
|
||||
|
||||
assert.Equal("wrong password", Wrap(cause).Error())
|
||||
assert.Equal("wrong password", Wrap(Wrap(cause)).Error())
|
||||
assert.Equal("wrong password", New("").Wrap(cause).Error())
|
||||
}
|
||||
Reference in new issue
Block a user