Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
## Unreleased

- client: break latency ties with avg latency ([#362](https://github.com/malbeclabs/doublezero/pull/3692))
- e2e/qa: skip the per-type capacity check in `ValidDevices` when the onchain per-type max is zero. The chain treats `max_unicast_users == 0` (and the multicast equivalents) as "no per-type cap," so the QA filter falls back to the aggregate `max_users - users_count` check for those devices. Fixes the regression where `qa.alldevices` on mainnet-beta dropped from testing 85 devices to testing 3 (malbeclabs/infra#1294)

### Breaking

Expand Down
11 changes: 8 additions & 3 deletions e2e/internal/qa/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,10 @@ func (d *Device) capacityFor(userType DeviceUserType) (current, max int) {

// ValidDevices returns devices that pass filtering criteria for the given
// user type. A device is considered valid when it has at least minCapacity
// free slots in the type-specific bucket (e.g. unicast) AND in the aggregate
// users bucket — both are enforced onchain independently.
// free slots in the aggregate users bucket. The type-specific bucket
// (e.g. unicast) is also checked, but only when its onchain max is non-zero —
// onchain, a per-type max of 0 means the cap is unenforced (see
// smartcontract/programs/doublezero-serviceability/src/processors/user/create_core.rs).
//
// If skipCapacityCheck is true (e.g., when using a QA identity that bypasses
// on-chain capacity checks), devices are not filtered by available capacity.
Expand All @@ -145,7 +147,10 @@ func (t *Test) ValidDevices(userType DeviceUserType, minCapacity int, skipCapaci
// Skip capacity check if using QA identity (bypasses on-chain max_users check)
if !skipCapacityCheck {
typeCount, typeMax := device.capacityFor(userType)
if typeMax-typeCount < minCapacity {
// Mirror the onchain semantic: the per-type cap is only enforced
// when max > 0. A max of 0 means "no per-type cap" and we fall
// through to the aggregate check.
if typeMax > 0 && typeMax-typeCount < minCapacity {
t.log.Debug("Skipping device with insufficient type-specific capacity",
"device", device.Code,
"userType", userType,
Expand Down
209 changes: 209 additions & 0 deletions e2e/internal/qa/test_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package qa

import (
"io"
"log/slog"
"testing"

"github.com/stretchr/testify/require"
)

// newTestForValidDevices builds a minimal *Test from a slice of devices.
// ValidDevices only depends on t.log and t.devices.
func newTestForValidDevices(devices []*Device) *Test {
deviceMap := make(map[string]*Device, len(devices))
for _, d := range devices {
deviceMap[d.Code] = d
}
return &Test{
log: slog.New(slog.NewTextHandler(io.Discard, nil)),
devices: deviceMap,
}
}

// codesOf returns the sorted codes returned by ValidDevices, for easy
// comparison against expected sets.
func codesOf(devices []*Device) []string {
out := make([]string, 0, len(devices))
for _, d := range devices {
out = append(out, d.Code)
}
return out
}

func TestValidDevices_Unicast(t *testing.T) {
t.Parallel()

const minCapacity = 2

tests := []struct {
name string
devices []*Device
skipCapacityCheck bool
want []string
}{
{
name: "per-type max set with free slots is included",
devices: []*Device{
{Code: "alpha", MaxUsers: 96, UsersCount: 4, MaxUnicastUsers: 48, UnicastUsersCount: 4},
},
want: []string{"alpha"},
},
{
name: "per-type max set and saturated is excluded (preserves #3563 fix)",
devices: []*Device{
{Code: "nyc002-dz002", MaxUsers: 96, UsersCount: 29, MaxUnicastUsers: 29, UnicastUsersCount: 29},
},
want: []string{},
},
{
name: "per-type max set with fewer than minCapacity free slots is excluded",
devices: []*Device{
{Code: "bravo", MaxUsers: 96, UsersCount: 47, MaxUnicastUsers: 48, UnicastUsersCount: 47},
},
want: []string{},
},
{
name: "per-type max zero with users counted is included (regression fix)",
devices: []*Device{
{Code: "frankfurt-edge", MaxUsers: 96, UsersCount: 12, MaxUnicastUsers: 0, UnicastUsersCount: 12},
},
want: []string{"frankfurt-edge"},
},
{
name: "per-type max zero with aggregate cap saturated is excluded",
devices: []*Device{
{Code: "full", MaxUsers: 5, UsersCount: 4, MaxUnicastUsers: 0, UnicastUsersCount: 4},
},
want: []string{},
},
{
name: "device with test in code is excluded",
devices: []*Device{
{Code: "lab-test-1", MaxUsers: 96, UsersCount: 0, MaxUnicastUsers: 48, UnicastUsersCount: 0},
},
want: []string{},
},
{
name: "skipCapacityCheck includes saturated and zero-max devices",
devices: []*Device{
{Code: "alpha", MaxUsers: 96, UsersCount: 4, MaxUnicastUsers: 48, UnicastUsersCount: 4},
{Code: "nyc002-dz002", MaxUsers: 96, UsersCount: 29, MaxUnicastUsers: 29, UnicastUsersCount: 29},
{Code: "frankfurt-edge", MaxUsers: 96, UsersCount: 12, MaxUnicastUsers: 0, UnicastUsersCount: 12},
{Code: "full", MaxUsers: 5, UsersCount: 4, MaxUnicastUsers: 0, UnicastUsersCount: 4},
},
skipCapacityCheck: true,
want: []string{"alpha", "frankfurt-edge", "full", "nyc002-dz002"},
},
{
name: "mainnet-beta-like mix returns only those with free per-type or unset cap",
devices: []*Device{
{Code: "allnodes-fra1", MaxUsers: 96, UsersCount: 29, MaxUnicastUsers: 48, UnicastUsersCount: 29},
{Code: "fra-velia", MaxUsers: 96, UsersCount: 4, MaxUnicastUsers: 48, UnicastUsersCount: 4},
{Code: "frankry", MaxUsers: 128, UsersCount: 68, MaxUnicastUsers: 96, UnicastUsersCount: 68},
{Code: "nyc002-dz002", MaxUsers: 96, UsersCount: 29, MaxUnicastUsers: 29, UnicastUsersCount: 29},
{Code: "amsterdam-edge", MaxUsers: 96, UsersCount: 7, MaxUnicastUsers: 0, UnicastUsersCount: 7},
{Code: "tokyo-edge", MaxUsers: 96, UsersCount: 2, MaxUnicastUsers: 0, UnicastUsersCount: 2},
},
want: []string{"allnodes-fra1", "amsterdam-edge", "fra-velia", "frankry", "tokyo-edge"},
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tt := newTestForValidDevices(tc.devices)
got := codesOf(tt.ValidDevices(DeviceUserTypeUnicast, minCapacity, tc.skipCapacityCheck))
require.Equal(t, tc.want, got)
})
}
}

func TestValidDevices_MulticastPublisher(t *testing.T) {
t.Parallel()

const minCapacity = 1

tests := []struct {
name string
devices []*Device
want []string
}{
{
name: "per-type max set with free slots is included",
devices: []*Device{
{Code: "pub-ok", MaxUsers: 96, UsersCount: 0, MaxMulticastPublishers: 4, MulticastPublishersCount: 1},
},
want: []string{"pub-ok"},
},
{
name: "per-type max set and saturated is excluded",
devices: []*Device{
{Code: "pub-full", MaxUsers: 96, UsersCount: 0, MaxMulticastPublishers: 1, MulticastPublishersCount: 1},
},
want: []string{},
},
{
name: "per-type max zero with publishers counted is included (regression fix)",
devices: []*Device{
{Code: "pub-uncapped", MaxUsers: 96, UsersCount: 3, MaxMulticastPublishers: 0, MulticastPublishersCount: 3},
},
want: []string{"pub-uncapped"},
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tt := newTestForValidDevices(tc.devices)
got := codesOf(tt.ValidDevices(DeviceUserTypeMulticastPublisher, minCapacity, false))
require.Equal(t, tc.want, got)
})
}
}

func TestValidDevices_MulticastSubscriber(t *testing.T) {
t.Parallel()

const minCapacity = 1

tests := []struct {
name string
devices []*Device
want []string
}{
{
name: "per-type max set with free slots is included",
devices: []*Device{
{Code: "sub-ok", MaxUsers: 96, UsersCount: 0, MaxMulticastSubscribers: 8, MulticastSubscribersCount: 2},
},
want: []string{"sub-ok"},
},
{
name: "per-type max set and saturated is excluded",
devices: []*Device{
{Code: "sub-full", MaxUsers: 96, UsersCount: 0, MaxMulticastSubscribers: 2, MulticastSubscribersCount: 2},
},
want: []string{},
},
{
name: "per-type max zero with subscribers counted is included (regression fix)",
devices: []*Device{
{Code: "sub-uncapped", MaxUsers: 96, UsersCount: 5, MaxMulticastSubscribers: 0, MulticastSubscribersCount: 5},
},
want: []string{"sub-uncapped"},
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tt := newTestForValidDevices(tc.devices)
got := codesOf(tt.ValidDevices(DeviceUserTypeMulticastSubscriber, minCapacity, false))
require.Equal(t, tc.want, got)
})
}
}
Loading