khatru: add tests and fix dispatcher.
This commit is contained in:
@@ -0,0 +1,261 @@
|
|||||||
|
package khatru
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDispatcherCandidates(t *testing.T) {
|
||||||
|
d := newDispatcher()
|
||||||
|
|
||||||
|
d.addSubscription(subscription{
|
||||||
|
id: "...",
|
||||||
|
filter: nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{9},
|
||||||
|
Tags: nostr.TagMap{"h": []string{"aaa"}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
d.addSubscription(subscription{
|
||||||
|
id: "...",
|
||||||
|
filter: nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{11},
|
||||||
|
Tags: nostr.TagMap{"h": []string{"aaa"}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
d.addSubscription(subscription{
|
||||||
|
id: "...",
|
||||||
|
filter: nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{9, 11, 1111},
|
||||||
|
Tags: nostr.TagMap{"h": []string{"aaa"}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
d.addSubscription(subscription{
|
||||||
|
id: "...",
|
||||||
|
filter: nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{9, 11, 1111},
|
||||||
|
Tags: nostr.TagMap{"h": []string{"bbb"}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
d.addSubscription(subscription{
|
||||||
|
id: "...",
|
||||||
|
filter: nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{9, 11, 1111},
|
||||||
|
Authors: []nostr.PubKey{
|
||||||
|
nostr.MustPubKeyFromHex("87f5650744bed197fcb170ae05fd8d1948a24b2aac34cedf7bdb1c47d6d93273"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
matched := 0
|
||||||
|
for range d.candidates(nostr.Event{
|
||||||
|
PubKey: nostr.MustPubKeyFromHex("87f5650744bed197fcb170ae05fd8d1948a24b2aac34cedf7bdb1c47d6d93273"),
|
||||||
|
ID: nostr.MustIDFromHex("87f5650744bed197fcb170ae05fd8d1948a24b2aac34cedf7bdb1c47d6d93273"),
|
||||||
|
Kind: 9,
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
Content: "hello",
|
||||||
|
Tags: nostr.Tags{
|
||||||
|
{"h", "aaa"},
|
||||||
|
},
|
||||||
|
}) {
|
||||||
|
matched++
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, 3, matched)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzDispatcherCandidates(f *testing.F) {
|
||||||
|
f.Add(1, 1, uint8(8), uint8(16))
|
||||||
|
f.Add(2, 3, uint8(32), uint8(32))
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, seed int, advance int, ops uint8, checks uint8) {
|
||||||
|
d := newDispatcher()
|
||||||
|
state := fuzzState{value: seed, advance: advance}
|
||||||
|
|
||||||
|
active := make(map[int]subscription)
|
||||||
|
activeSSIDs := make([]int, 0, int(ops))
|
||||||
|
nextSubID := 0
|
||||||
|
|
||||||
|
steps := int(ops) + 1
|
||||||
|
for range steps {
|
||||||
|
if len(activeSSIDs) == 0 || state.next(10) != 0 {
|
||||||
|
nextSubID++
|
||||||
|
sub := subscription{
|
||||||
|
id: strconv.Itoa(nextSubID),
|
||||||
|
filter: fuzzDispatcherFilter(&state),
|
||||||
|
}
|
||||||
|
|
||||||
|
ssid := d.addSubscription(sub)
|
||||||
|
active[ssid] = sub
|
||||||
|
activeSSIDs = append(activeSSIDs, ssid)
|
||||||
|
} else {
|
||||||
|
idx := state.next(len(activeSSIDs))
|
||||||
|
ssid := activeSSIDs[idx]
|
||||||
|
|
||||||
|
d.removeSubscription(ssid)
|
||||||
|
delete(active, ssid)
|
||||||
|
activeSSIDs = append(activeSSIDs[:idx], activeSSIDs[idx+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for range int(checks%7) + 1 {
|
||||||
|
event := fuzzDispatcherEvent(&state)
|
||||||
|
expected := expectedDispatcherCandidates(active, event)
|
||||||
|
actual := collectedDispatcherCandidates(&d, event)
|
||||||
|
require.Equalf(t, expected, actual, "seed=%d advance=%d event=%s active=%v", seed, advance, event.String(), active)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ssid := range activeSSIDs {
|
||||||
|
d.removeSubscription(ssid)
|
||||||
|
delete(active, ssid)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Empty(t, collectedDispatcherCandidates(&d, fuzzDispatcherEvent(&state)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type fuzzState struct {
|
||||||
|
value int
|
||||||
|
advance int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *fuzzState) next(n int) int {
|
||||||
|
if n <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
value := state.value % n
|
||||||
|
if value < 0 {
|
||||||
|
value += n
|
||||||
|
}
|
||||||
|
state.value += state.advance
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func fuzzDispatcherFilter(seed *fuzzState) nostr.Filter {
|
||||||
|
filter := nostr.Filter{
|
||||||
|
Authors: fuzzDispatcherAuthors(seed),
|
||||||
|
Kinds: fuzzDispatcherKinds(seed),
|
||||||
|
Tags: fuzzDispatcherTagMap(seed),
|
||||||
|
}
|
||||||
|
|
||||||
|
if seed.next(3) == 0 {
|
||||||
|
since := nostr.Timestamp(seed.next(6))
|
||||||
|
until := since + nostr.Timestamp(seed.next(6))
|
||||||
|
filter.Since = since
|
||||||
|
filter.Until = until
|
||||||
|
} else if seed.next(4) == 0 {
|
||||||
|
filter.Since = nostr.Timestamp(seed.next(8))
|
||||||
|
} else if seed.next(4) == 0 {
|
||||||
|
filter.Until = nostr.Timestamp(seed.next(8))
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func fuzzDispatcherAuthors(seed *fuzzState) []nostr.PubKey {
|
||||||
|
switch seed.next(4) {
|
||||||
|
case 0:
|
||||||
|
return nil
|
||||||
|
case 1:
|
||||||
|
return []nostr.PubKey{}
|
||||||
|
}
|
||||||
|
|
||||||
|
count := seed.next(3) + 1
|
||||||
|
authors := make([]nostr.PubKey, 0, count)
|
||||||
|
for range count {
|
||||||
|
pk := nostr.PubKey{byte(seed.next(4) + 1)}
|
||||||
|
if !slices.Contains(authors, pk) {
|
||||||
|
authors = append(authors, pk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return authors
|
||||||
|
}
|
||||||
|
|
||||||
|
func fuzzDispatcherKinds(seed *fuzzState) []nostr.Kind {
|
||||||
|
switch seed.next(4) {
|
||||||
|
case 0:
|
||||||
|
return nil
|
||||||
|
case 1:
|
||||||
|
return []nostr.Kind{}
|
||||||
|
}
|
||||||
|
|
||||||
|
count := seed.next(3) + 1
|
||||||
|
kinds := make([]nostr.Kind, 0, count)
|
||||||
|
for range count {
|
||||||
|
kind := nostr.Kind(seed.next(5) + 1)
|
||||||
|
if !slices.Contains(kinds, kind) {
|
||||||
|
kinds = append(kinds, kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return kinds
|
||||||
|
}
|
||||||
|
|
||||||
|
func fuzzDispatcherTagMap(seed *fuzzState) nostr.TagMap {
|
||||||
|
if seed.next(3) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := []string{"e", "p", "t"}
|
||||||
|
values := []string{"a", "b", "c", "d"}
|
||||||
|
|
||||||
|
count := seed.next(3)
|
||||||
|
if count == 0 {
|
||||||
|
return nostr.TagMap{}
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := make(nostr.TagMap, count)
|
||||||
|
start := seed.next(len(keys))
|
||||||
|
for i := range count {
|
||||||
|
idx := (start + i) % len(keys)
|
||||||
|
valueCount := seed.next(3) + 1
|
||||||
|
entries := make([]string, 0, valueCount)
|
||||||
|
for range valueCount {
|
||||||
|
value := values[seed.next(len(values))]
|
||||||
|
if !slices.Contains(entries, value) {
|
||||||
|
entries = append(entries, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags[keys[idx]] = entries
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func fuzzDispatcherEvent(seed *fuzzState) nostr.Event {
|
||||||
|
tags := make(nostr.Tags, 0, seed.next(4))
|
||||||
|
keys := []string{"e", "p", "t"}
|
||||||
|
values := []string{"a", "b", "c", "d"}
|
||||||
|
for range cap(tags) {
|
||||||
|
tags = append(tags, nostr.Tag{keys[seed.next(len(keys))], values[seed.next(len(values))]})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nostr.Event{
|
||||||
|
PubKey: nostr.PubKey{byte(seed.next(4) + 1)},
|
||||||
|
Kind: nostr.Kind(seed.next(5) + 1),
|
||||||
|
CreatedAt: nostr.Timestamp(seed.next(8)),
|
||||||
|
Tags: tags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectedDispatcherCandidates(active map[int]subscription, event nostr.Event) []string {
|
||||||
|
ids := make([]string, 0, len(active))
|
||||||
|
for _, sub := range active {
|
||||||
|
if sub.filter.Matches(event) {
|
||||||
|
ids = append(ids, sub.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(ids)
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectedDispatcherCandidates(d *dispatcher, event nostr.Event) []string {
|
||||||
|
ids := make([]string, 0, d.subscriptions.Size())
|
||||||
|
for sub := range d.candidates(event) {
|
||||||
|
ids = append(ids, sub.id)
|
||||||
|
}
|
||||||
|
slices.Sort(ids)
|
||||||
|
return ids
|
||||||
|
}
|
||||||
+14
-3
@@ -134,16 +134,27 @@ func (d *dispatcher) candidates(event nostr.Event) iter.Seq[subscription] {
|
|||||||
for _, ssid := range authorSubs.Slice() {
|
for _, ssid := range authorSubs.Slice() {
|
||||||
sub, _ := d.subscriptions.Load(ssid)
|
sub, _ := d.subscriptions.Load(ssid)
|
||||||
|
|
||||||
if kindSubs.Has(ssid) {
|
if kindSubs.Has(ssid) || sub.filter.Kinds == nil {
|
||||||
if filterMatchesTimestampConstraintsAndTags(sub.filter, event) {
|
if filterMatchesTimestampConstraintsAndTags(sub.filter, event) {
|
||||||
if !yield(sub) {
|
if !yield(sub) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// matched author but not tags, so this event doesn't qualify for any filter
|
}
|
||||||
|
|
||||||
|
for _, ssid := range kindSubs.Slice() {
|
||||||
|
sub, _ := d.subscriptions.Load(ssid)
|
||||||
|
|
||||||
|
if sub.filter.Authors != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if filterMatchesTimestampConstraintsAndTags(sub.filter, event) {
|
||||||
|
if !yield(sub) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if hasAuthorSubs {
|
} else if hasAuthorSubs {
|
||||||
for _, ssid := range authorSubs.Slice() {
|
for _, ssid := range authorSubs.Slice() {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package khatru
|
package khatru
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
@@ -125,10 +124,7 @@ func FuzzRandomListenerIdRemoving(f *testing.F) {
|
|||||||
}
|
}
|
||||||
require.Equal(t, len(subs)+extra, ssidCount)
|
require.Equal(t, len(subs)+extra, ssidCount)
|
||||||
|
|
||||||
rand.Shuffle(len(subs), func(i, j int) {
|
for _, wsidToRemove := range moduloOrder(subs, int(utw+ubs+ualf+ualef)) {
|
||||||
subs[i], subs[j] = subs[j], subs[i]
|
|
||||||
})
|
|
||||||
for _, wsidToRemove := range subs {
|
|
||||||
rl.removeListenerId(wsidToRemove.ws, wsidToRemove.id)
|
rl.removeListenerId(wsidToRemove.ws, wsidToRemove.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-8
@@ -1,7 +1,6 @@
|
|||||||
package khatru
|
package khatru
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -23,6 +22,18 @@ func idFromSeq(seq int, min, max int) string {
|
|||||||
return result.String()
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func moduloOrder[T any](items []T, seed int) []T {
|
||||||
|
remaining := append([]T(nil), items...)
|
||||||
|
ordered := make([]T, 0, len(items))
|
||||||
|
for len(remaining) > 0 {
|
||||||
|
idx := seed % len(remaining)
|
||||||
|
ordered = append(ordered, remaining[idx])
|
||||||
|
remaining = append(remaining[:idx], remaining[idx+1:]...)
|
||||||
|
seed++
|
||||||
|
}
|
||||||
|
return ordered
|
||||||
|
}
|
||||||
|
|
||||||
func TestListenerSetupAndRemoveOnce(t *testing.T) {
|
func TestListenerSetupAndRemoveOnce(t *testing.T) {
|
||||||
rl := NewRelay()
|
rl := NewRelay()
|
||||||
|
|
||||||
@@ -321,7 +332,7 @@ func TestRandomListenerClientRemoving(t *testing.T) {
|
|||||||
ws := websockets[i]
|
ws := websockets[i]
|
||||||
w := idFromSeqUpper(i)
|
w := idFromSeqUpper(i)
|
||||||
|
|
||||||
if rand.Intn(2) < 1 {
|
if (i+j)%2 == 0 {
|
||||||
l++
|
l++
|
||||||
rl.addListener(ws, w+":"+idFromSeqLower(j), f, cancel)
|
rl.addListener(ws, w+":"+idFromSeqLower(j), f, cancel)
|
||||||
}
|
}
|
||||||
@@ -374,12 +385,12 @@ func TestRandomListenerIdRemoving(t *testing.T) {
|
|||||||
ws := websockets[i]
|
ws := websockets[i]
|
||||||
w := idFromSeqUpper(i)
|
w := idFromSeqUpper(i)
|
||||||
|
|
||||||
if rand.Intn(2) < 1 {
|
if (i+j)%2 == 0 {
|
||||||
id := w + ":" + idFromSeqLower(j)
|
id := w + ":" + idFromSeqLower(j)
|
||||||
rl.addListener(ws, id, f, cancel)
|
rl.addListener(ws, id, f, cancel)
|
||||||
subs = append(subs, wsid{ws, id})
|
subs = append(subs, wsid{ws, id})
|
||||||
|
|
||||||
if rand.Intn(5) < 1 {
|
if (i+j)%5 == 0 {
|
||||||
rl.addListener(ws, id, f, cancel)
|
rl.addListener(ws, id, f, cancel)
|
||||||
extra++
|
extra++
|
||||||
}
|
}
|
||||||
@@ -394,10 +405,7 @@ func TestRandomListenerIdRemoving(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.Equal(t, len(subs)+extra, ssidCount)
|
require.Equal(t, len(subs)+extra, ssidCount)
|
||||||
|
|
||||||
rand.Shuffle(len(subs), func(i, j int) {
|
for _, wsidToRemove := range moduloOrder(subs, 20) {
|
||||||
subs[i], subs[j] = subs[j], subs[i]
|
|
||||||
})
|
|
||||||
for _, wsidToRemove := range subs {
|
|
||||||
rl.removeListenerId(wsidToRemove.ws, wsidToRemove.id)
|
rl.removeListenerId(wsidToRemove.ws, wsidToRemove.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package khatru
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"math"
|
"math"
|
||||||
"math/rand/v2"
|
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -14,13 +13,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func FuzzReplaceableEvents(f *testing.F) {
|
func FuzzReplaceableEvents(f *testing.F) {
|
||||||
f.Add(uint(1), uint(2))
|
f.Add(1, 1, uint(2))
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, seed uint, nevents uint) {
|
f.Fuzz(func(t *testing.T, seed int, advance int, nevents uint) {
|
||||||
if nevents == 0 {
|
if nevents == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state := fuzzState{value: seed, advance: advance}
|
||||||
|
|
||||||
relay := NewRelay()
|
relay := NewRelay()
|
||||||
store := &lmdb.LMDBBackend{Path: "/tmp/fuzz"}
|
store := &lmdb.LMDBBackend{Path: "/tmp/fuzz"}
|
||||||
store.Init()
|
store.Init()
|
||||||
@@ -67,12 +68,10 @@ func FuzzReplaceableEvents(f *testing.F) {
|
|||||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
rnd := rand.New(rand.NewPCG(uint64(seed), 0))
|
|
||||||
|
|
||||||
newest := nostr.Timestamp(0)
|
newest := nostr.Timestamp(0)
|
||||||
for range nevents {
|
for range nevents {
|
||||||
evt := createEvent(sk1, 0, `{"name":"blblbl"}`, nil)
|
evt := createEvent(sk1, 0, `{"name":"blblbl"}`, nil)
|
||||||
evt.CreatedAt = nostr.Timestamp(rnd.Int64() % math.MaxUint32)
|
evt.CreatedAt = nostr.Timestamp(state.next(math.MaxUint32))
|
||||||
evt.Sign(sk1)
|
evt.Sign(sk1)
|
||||||
err = client1.Publish(ctx, evt)
|
err = client1.Publish(ctx, evt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
go test fuzz v1
|
go test fuzz v1
|
||||||
uint(25)
|
int(25)
|
||||||
|
int(1)
|
||||||
uint(223)
|
uint(223)
|
||||||
|
|||||||
Reference in New Issue
Block a user