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() {
|
||||
sub, _ := d.subscriptions.Load(ssid)
|
||||
|
||||
if kindSubs.Has(ssid) {
|
||||
if kindSubs.Has(ssid) || sub.filter.Kinds == nil {
|
||||
if filterMatchesTimestampConstraintsAndTags(sub.filter, event) {
|
||||
if !yield(sub) {
|
||||
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
|
||||
}
|
||||
|
||||
if filterMatchesTimestampConstraintsAndTags(sub.filter, event) {
|
||||
if !yield(sub) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if hasAuthorSubs {
|
||||
for _, ssid := range authorSubs.Slice() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
@@ -125,10 +124,7 @@ func FuzzRandomListenerIdRemoving(f *testing.F) {
|
||||
}
|
||||
require.Equal(t, len(subs)+extra, ssidCount)
|
||||
|
||||
rand.Shuffle(len(subs), func(i, j int) {
|
||||
subs[i], subs[j] = subs[j], subs[i]
|
||||
})
|
||||
for _, wsidToRemove := range subs {
|
||||
for _, wsidToRemove := range moduloOrder(subs, int(utw+ubs+ualf+ualef)) {
|
||||
rl.removeListenerId(wsidToRemove.ws, wsidToRemove.id)
|
||||
}
|
||||
|
||||
|
||||
+16
-8
@@ -1,7 +1,6 @@
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -23,6 +22,18 @@ func idFromSeq(seq int, min, max int) 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) {
|
||||
rl := NewRelay()
|
||||
|
||||
@@ -321,7 +332,7 @@ func TestRandomListenerClientRemoving(t *testing.T) {
|
||||
ws := websockets[i]
|
||||
w := idFromSeqUpper(i)
|
||||
|
||||
if rand.Intn(2) < 1 {
|
||||
if (i+j)%2 == 0 {
|
||||
l++
|
||||
rl.addListener(ws, w+":"+idFromSeqLower(j), f, cancel)
|
||||
}
|
||||
@@ -374,12 +385,12 @@ func TestRandomListenerIdRemoving(t *testing.T) {
|
||||
ws := websockets[i]
|
||||
w := idFromSeqUpper(i)
|
||||
|
||||
if rand.Intn(2) < 1 {
|
||||
if (i+j)%2 == 0 {
|
||||
id := w + ":" + idFromSeqLower(j)
|
||||
rl.addListener(ws, id, f, cancel)
|
||||
subs = append(subs, wsid{ws, id})
|
||||
|
||||
if rand.Intn(5) < 1 {
|
||||
if (i+j)%5 == 0 {
|
||||
rl.addListener(ws, id, f, cancel)
|
||||
extra++
|
||||
}
|
||||
@@ -394,10 +405,7 @@ func TestRandomListenerIdRemoving(t *testing.T) {
|
||||
}
|
||||
require.Equal(t, len(subs)+extra, ssidCount)
|
||||
|
||||
rand.Shuffle(len(subs), func(i, j int) {
|
||||
subs[i], subs[j] = subs[j], subs[i]
|
||||
})
|
||||
for _, wsidToRemove := range subs {
|
||||
for _, wsidToRemove := range moduloOrder(subs, 20) {
|
||||
rl.removeListenerId(wsidToRemove.ws, wsidToRemove.id)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package khatru
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -14,13 +13,15 @@ import (
|
||||
)
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
state := fuzzState{value: seed, advance: advance}
|
||||
|
||||
relay := NewRelay()
|
||||
store := &lmdb.LMDBBackend{Path: "/tmp/fuzz"}
|
||||
store.Init()
|
||||
@@ -67,12 +68,10 @@ func FuzzReplaceableEvents(f *testing.F) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rnd := rand.New(rand.NewPCG(uint64(seed), 0))
|
||||
|
||||
newest := nostr.Timestamp(0)
|
||||
for range nevents {
|
||||
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)
|
||||
err = client1.Publish(ctx, evt)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
go test fuzz v1
|
||||
uint(25)
|
||||
int(25)
|
||||
int(1)
|
||||
uint(223)
|
||||
|
||||
Reference in New Issue
Block a user