ceremonyclient/lifecycle/common_test.go
Cassandra Heart c797d482f9
v2.1.0.5 (#457)
* wip: conversion of hotstuff from flow into Q-oriented model

* bulk of tests

* remaining non-integration tests

* add integration test, adjust log interface, small tweaks

* further adjustments, restore full pacemaker shape

* add component lifecycle management+supervisor

* further refinements

* resolve timeout hanging

* mostly finalized state for consensus

* bulk of engine swap out

* lifecycle-ify most types

* wiring nearly complete, missing needed hooks for proposals

* plugged in, vetting message validation paths

* global consensus, plugged in and verified

* app shard now wired in too

* do not decode empty keys.yml (#456)

* remove obsolete engine.maxFrames config parameter (#454)

* default to Info log level unless debug is enabled (#453)

* respect config's  "logging" section params, remove obsolete single-file logging (#452)

* Trivial code cleanup aiming to reduce Go compiler warnings (#451)

* simplify range traversal

* simplify channel read for single select case

* delete rand.Seed() deprecated in Go 1.20 and no-op as of Go 1.24

* simplify range traversal

* simplify channel read for single select case

* remove redundant type from array

* simplify range traversal

* simplify channel read for single select case

* RC slate

* finalize 2.1.0.5

* Update comments in StrictMonotonicCounter

Fix comment formatting and clarify description.

---------

Co-authored-by: Black Swan <3999712+blacks1ne@users.noreply.github.com>
2025-11-11 05:00:17 -06:00

339 lines
8.8 KiB
Go

package lifecycle_test
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"source.quilibrium.com/quilibrium/monorepo/lifecycle"
"source.quilibrium.com/quilibrium/monorepo/lifecycle/mocks"
"source.quilibrium.com/quilibrium/monorepo/lifecycle/unittest"
)
// TestAllReady tests that AllReady closes its returned Ready channel only once
// all input Component instances close their Ready channel.
func TestAllReady(t *testing.T) {
cases := []int{0, 1, 100}
for _, n := range cases {
t.Run(fmt.Sprintf("n=%d", n), func(t *testing.T) {
testAllReady(n, t)
})
}
}
// TestAllDone tests that AllDone closes its returned Done channel only once
// all input Component instances close their Done channel.
func TestAllDone(t *testing.T) {
cases := []int{0, 1, 100}
for _, n := range cases {
t.Run(fmt.Sprintf("n=%d", n), func(t *testing.T) {
testAllDone(n, t)
})
}
}
func testAllDone(n int, t *testing.T) {
components := make([]lifecycle.Component, n)
for i := 0; i < n; i++ {
c := mocks.NewComponent(t)
unittest.Componentify(&c.Mock)
components[i] = c
}
unittest.AssertClosesBefore(t, lifecycle.AllReady(components...), time.Second)
for _, component := range components {
mock := component.(*mocks.Component)
mock.AssertCalled(t, "Ready")
mock.AssertNotCalled(t, "Done")
}
}
func testAllReady(n int, t *testing.T) {
components := make([]lifecycle.Component, n)
for i := 0; i < n; i++ {
c := mocks.NewComponent(t)
unittest.Componentify(&c.Mock)
components[i] = c
}
unittest.AssertClosesBefore(t, lifecycle.AllDone(components...), time.Second)
for _, component := range components {
mock := component.(*mocks.Component)
mock.AssertCalled(t, "Done")
mock.AssertNotCalled(t, "Ready")
}
}
func TestMergeChannels(t *testing.T) {
t.Run("empty slice", func(t *testing.T) {
t.Parallel()
channels := make([]<-chan int, 0)
merged := lifecycle.MergeChannels(channels).(<-chan int)
_, ok := <-merged
assert.False(t, ok)
})
t.Run("empty array", func(t *testing.T) {
t.Parallel()
channels := []<-chan int{}
merged := lifecycle.MergeChannels(channels).(<-chan int)
_, ok := <-merged
assert.False(t, ok)
})
t.Run("nil slice", func(t *testing.T) {
t.Parallel()
var channels []<-chan int
merged := lifecycle.MergeChannels(channels).(<-chan int)
_, ok := <-merged
assert.False(t, ok)
})
t.Run("nil", func(t *testing.T) {
t.Parallel()
assert.Panics(t, func() {
lifecycle.MergeChannels(nil)
})
})
t.Run("map", func(t *testing.T) {
t.Parallel()
channels := make(map[string]<-chan int)
assert.Panics(t, func() {
lifecycle.MergeChannels(channels)
})
})
t.Run("string", func(t *testing.T) {
t.Parallel()
channels := "abcde"
assert.Panics(t, func() {
lifecycle.MergeChannels(channels)
})
})
t.Run("array of non-channel", func(t *testing.T) {
t.Parallel()
channels := []int{1, 2, 3}
assert.Panics(t, func() {
lifecycle.MergeChannels(channels)
})
})
t.Run("send channel", func(t *testing.T) {
t.Parallel()
channels := []chan<- int{make(chan int), make(chan int)}
assert.Panics(t, func() {
lifecycle.MergeChannels(channels)
})
})
t.Run("cast returned channel to send channel", func(t *testing.T) {
t.Parallel()
channels := []<-chan int{make(<-chan int), make(<-chan int)}
_, ok := lifecycle.MergeChannels(channels).(chan int)
assert.False(t, ok)
})
t.Run("happy path", func(t *testing.T) {
t.Parallel()
channels := []chan int{make(chan int), make(chan int), make(chan int)}
merged := lifecycle.MergeChannels(channels).(<-chan int)
for i, ch := range channels {
i := i
ch := ch
go func() {
ch <- i
close(ch)
}()
}
var elements []int
for i := range merged {
elements = append(elements, i)
}
assert.ElementsMatch(t, elements, []int{0, 1, 2})
})
}
func TestWaitClosed(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
t.Run("channel closed returns nil", func(t *testing.T) {
finished := make(chan struct{})
ch := make(chan struct{})
go func() {
err := lifecycle.WaitClosed(ctx, ch)
assert.NoError(t, err)
close(finished)
}()
close(ch)
select {
case <-finished:
case <-time.After(100 * time.Millisecond):
t.Error("timed out")
}
})
t.Run("context cancelled returns error", func(t *testing.T) {
testCtx, testCancel := context.WithCancel(ctx)
finished := make(chan struct{})
ch := make(chan struct{})
go func() {
err := lifecycle.WaitClosed(testCtx, ch)
assert.ErrorIs(t, err, context.Canceled)
close(finished)
}()
testCancel()
select {
case <-finished:
case <-time.After(100 * time.Millisecond):
t.Error("timed out")
}
})
t.Run("both conditions triggered returns nil", func(t *testing.T) {
// both conditions are met when WaitClosed is called. Since one is randomly selected,
// there is a 99.9% probability that each condition will be picked first at least once
// during this test.
for i := 0; i < 10; i++ {
testCtx, testCancel := context.WithCancel(ctx)
finished := make(chan struct{})
ch := make(chan struct{})
close(ch)
testCancel()
go func() {
err := lifecycle.WaitClosed(testCtx, ch)
assert.NoError(t, err)
close(finished)
}()
select {
case <-finished:
case <-time.After(100 * time.Millisecond):
t.Error("timed out")
}
}
})
}
func TestCheckClosed(t *testing.T) {
done := make(chan struct{})
assert.False(t, lifecycle.CheckClosed(done))
close(done)
assert.True(t, lifecycle.CheckClosed(done))
}
func TestWaitError(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
testErr := errors.New("test error channel")
t.Run("error received returns error", func(t *testing.T) {
finished := make(chan struct{})
ch := make(chan error)
go func() {
err := lifecycle.WaitError(ch, ctx.Done())
assert.ErrorIs(t, err, testErr)
close(finished)
}()
ch <- testErr
select {
case <-finished:
case <-time.After(100 * time.Millisecond):
t.Error("timed out")
}
})
t.Run("context cancelled returns error", func(t *testing.T) {
testCtx, testCancel := context.WithCancel(ctx)
finished := make(chan struct{})
ch := make(chan error)
go func() {
err := lifecycle.WaitError(ch, testCtx.Done())
assert.NoError(t, err)
close(finished)
}()
testCancel()
select {
case <-finished:
case <-time.After(100 * time.Millisecond):
t.Error("timed out")
}
})
t.Run("both conditions triggered returns error", func(t *testing.T) {
// both conditions are met when WaitError is called. Since one is randomly selected,
// there is a 99.9% probability that each condition will be picked first at least once
// during this test.
for i := 0; i < 10; i++ {
finished := make(chan struct{})
ch := make(chan error, 1) // buffered so we can add before starting
done := make(chan struct{})
ch <- testErr
close(done)
go func() {
err := lifecycle.WaitError(ch, done)
assert.ErrorIs(t, err, testErr)
close(finished)
}()
select {
case <-finished:
case <-time.After(100 * time.Millisecond):
t.Error("timed out")
}
}
})
}
// TestDetypeSlice tests that DetypeSlice returns a slice which is identical
// besides the element type information.
func TestDetypeSlice(t *testing.T) {
slice := []int{1, 2, 5, 3, 53, 1234}
detyped := lifecycle.DetypeSlice(slice)
assert.Equal(t, len(slice), len(detyped))
for i := range slice {
assert.Equal(t, slice[i], detyped[i].(int))
}
}
// TestSampleN contains a series of test cases to validate the behavior of the lifecycle.SampleN function.
// The test cases cover different scenarios:
// 1. "returns expected sample": Checks if the function returns the expected sample value when
// given a valid input.
// 2. "returns max value when sample greater than max": Verifies that the function returns the
// maximum allowed value when the calculated sample exceeds the maximum limit.
// 3. "returns 0 when n is less than or equal to 0": Asserts that the function returns 0 when
// the input 'n' is less than or equal to 0, which represents an invalid input.
func TestSampleN(t *testing.T) {
t.Run("returns expected sample", func(t *testing.T) {
n := 8
max := 5.0
percentage := .5
sample := lifecycle.SampleN(n, max, percentage)
assert.Equal(t, uint(4), sample)
})
t.Run("returns max value when sample greater than max", func(t *testing.T) {
n := 20
max := 5.0
percentage := .5
sample := lifecycle.SampleN(n, max, percentage)
assert.Equal(t, uint(max), sample)
})
t.Run("returns 0 when n is less than or equal to 0", func(t *testing.T) {
n := 0
max := 5.0
percentage := .5
sample := lifecycle.SampleN(n, max, percentage)
assert.Equal(t, uint(0), sample, "sample returned should be 0 when n == 0")
n = -1
sample = lifecycle.SampleN(n, max, percentage)
assert.Equal(t, uint(0), sample, "sample returned should be 0 when n < 0")
})
}