Files
compute-blade-agent/pkg/ledengine/ledengine_test.go
Cedric Kienzler 781ded8e43 feat(bladectl)!: add more bladectl commands (#91)
This PR introduces a comprehensive set of new subcommands to bladectl, expanding its capabilities for querying and managing compute blade state. It also includes an internal refactor to simplify interface management across the gRPC API.

* `get`
	* `fan`: Returns current fan speed.
	* `identify`: Indicates whether the identify mode is active.
	* `stealth`: Shows if stealth mode is currently enabled.
	* `status`: Prints a full blade status report.
	* `temperature`: Retrieves current SoC temperature.
	* `critical`: Shows whether critical mode is active.
	* `power`: Reports the current power source (e.g., PoE+ or USB).
* `set`
	* `stealth`: Enables stealth mode.
* `remove`
	* `stealth`: Disables stealth mode.
* `describe`
	* `fan`: Outputs the current fan curve configuration.
* `monitor`: plot some charts about the state of the compute-blade-agent

* **gRPC API refactor**: The gRPC service definitions previously located in `internal/api` have been folded into `internal/agent`. This eliminates redundant interface declarations and ensures that all ComputeBladeAgent implementations are directly compatible with the gRPC API.
This reduces duplication and improves long-term maintainability and clarity of the interface contract.

```bash
bladectl set fan --percent 90 --blade 1 --blade 2
bladectl unset identify --blade 1 --blade 2 --blade 3 --blade 4
bladectl set stealth --blade 1 --blade 2 --blade 3 --blade 4
bladectl get status --blade 1 --blade 2 --blade 3 --blade 4
┌───────┬─────────────┬────────────────────┬───────────────┬──────────────┬──────────┬───────────────┬──────────────┐
│ BLADE │ TEMPERATURE │ FAN SPEED OVERRIDE │ FAN SPEED     │ STEALTH MODE │ IDENTIFY │ CRITICAL MODE │ POWER STATUS │
├───────┼─────────────┼────────────────────┼───────────────┼──────────────┼──────────┼───────────────┼──────────────┤
│ 1     │ 50°C        │ 90%                │ 5825 RPM(90%) │ Active       │ Off      │ Off           │ poe+         │
│ 2     │ 48°C        │ 90%                │ 5825 RPM(90%) │ Active       │ Off      │ Off           │ poe+         │
│ 3     │ 49°C        │ Not set            │ 4643 RPM(56%) │ Active       │ Off      │ Off           │ poe+         │
│ 4     │ 49°C        │ Not set            │ 4774 RPM(58%) │ Active       │ Off      │ Off           │ poe+         │
└───────┴─────────────┴────────────────────┴───────────────┴──────────────┴──────────┴───────────────┴──────────────┘
bladectl rm stealth --blade 1 --blade 2 --blade 3 --blade 4
bladectl rm fan --blade 1 --blade 2 --blade 3 --blade 4
bladectl get status --blade 1 --blade 2 --blade 3 --blade 4
┌───────┬─────────────┬────────────────────┬───────────────┬──────────────┬──────────┬───────────────┬──────────────┐
│ BLADE │ TEMPERATURE │ FAN SPEED OVERRIDE │ FAN SPEED     │ STEALTH MODE │ IDENTIFY │ CRITICAL MODE │ POWER STATUS │
├───────┼─────────────┼────────────────────┼───────────────┼──────────────┼──────────┼───────────────┼──────────────┤
│ 1     │ 51°C        │ Not set            │ 5177 RPM(66%) │ Off          │ Off      │ Off           │ poe+         │
│ 2     │ 49°C        │ Not set            │ 5177 RPM(58%) │ Off          │ Off      │ Off           │ poe+         │
│ 3     │ 50°C        │ Not set            │ 4659 RPM(60%) │ Off          │ Off      │ Off           │ poe+         │
│ 4     │ 48°C        │ Not set            │ 4659 RPM(54%) │ Off          │ Off      │ Off           │ poe+         │
└───────┴─────────────┴────────────────────┴───────────────┴──────────────┴──────────┴───────────────┴──────────────┘
```

when having multiple compute-blades in your bladeconfig:

```yaml
blades:
    - name: 1
      blade:
        server: blade-pi1:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
    - name: 2
      blade:
        server: blade-pi2:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
    - name: 3
      blade:
        server: blade-pi3:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
    - name: 4
      blade:
        server: blade-pi4:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
    - name: 4
      blade:
        server: blade-pi4:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
current-blade: 1
```

Fixes #4, #9 (partially), should help with #5

* test: improve unit-testing

* fix: pin github.com/warthog618/gpiod

---------

Co-authored-by: Cedric Kienzler <cedric@specht-labs.de>
2025-06-06 23:03:43 +02:00

294 lines
7.1 KiB
Go

package ledengine_test
import (
"context"
"errors"
"reflect"
"sync"
"testing"
"time"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/ledengine"
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
"github.com/stretchr/testify/assert"
)
func TestNewStaticPattern(t *testing.T) {
t.Parallel()
type args struct {
color led.Color
}
tests := []struct {
name string
args args
want ledengine.BlinkPattern
}{
{
"Green",
args{led.Color{Green: 255}},
ledengine.BlinkPattern{
BaseColor: led.Color{Green: 255},
ActiveColor: led.Color{Green: 255},
Delays: []time.Duration{time.Hour},
},
},
{
"Red",
args{led.Color{Red: 255}},
ledengine.BlinkPattern{
BaseColor: led.Color{Red: 255},
ActiveColor: led.Color{Red: 255},
Delays: []time.Duration{time.Hour},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ledengine.NewStaticPattern(tt.args.color); !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewStaticPattern() = %v, want %v", got, tt.want)
}
})
}
}
func TestNewBurstPattern(t *testing.T) {
t.Parallel()
type args struct {
baseColor led.Color
burstColor led.Color
}
tests := []struct {
name string
args args
want ledengine.BlinkPattern
}{
{
"Green <-> Red",
args{
baseColor: led.Color{Green: 255},
burstColor: led.Color{Red: 255},
},
ledengine.BlinkPattern{
BaseColor: led.Color{Green: 255},
ActiveColor: led.Color{Red: 255},
Delays: []time.Duration{
500 * time.Millisecond, // 750ms off
100 * time.Millisecond, // 100ms on
100 * time.Millisecond, // 100ms off
100 * time.Millisecond, // 100ms on
100 * time.Millisecond, // 100ms off
100 * time.Millisecond, // 100ms on
},
},
},
{
"Green <-> Green (valid, but no visual effect)",
args{
baseColor: led.Color{Green: 255},
burstColor: led.Color{Green: 255},
},
ledengine.BlinkPattern{
BaseColor: led.Color{Green: 255},
ActiveColor: led.Color{Green: 255},
Delays: []time.Duration{
500 * time.Millisecond, // 750ms off
100 * time.Millisecond, // 100ms on
100 * time.Millisecond, // 100ms off
100 * time.Millisecond, // 100ms on
100 * time.Millisecond, // 100ms off
100 * time.Millisecond, // 100ms on
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ledengine.NewBurstPattern(tt.args.baseColor, tt.args.burstColor); !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewBurstPattern() = %v, want %v", got, tt.want)
}
})
}
}
func TestNewLedEngine(t *testing.T) {
t.Parallel()
engine := ledengine.Options{
Clock: util.RealClock{},
LedIdx: 0,
Hal: &hal.ComputeBladeHalMock{},
}
assert.NotNil(t, engine)
}
func TestLedEngine_NewLedEngineWithoutClock(t *testing.T) {
opts := ledengine.Options{
Clock: nil,
LedIdx: 0,
Hal: &hal.ComputeBladeHalMock{},
}
engine := ledengine.NewLedEngine(opts)
assert.NotNil(t, engine)
}
func Test_LedEngine_SetPattern_WhileRunning(t *testing.T) {
t.Parallel()
clk := util.MockClock{}
clkAfterChan := make(chan time.Time)
clk.On("After", time.Hour).Times(2).Return(clkAfterChan)
cbMock := hal.ComputeBladeHalMock{}
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
opts := ledengine.Options{
Hal: &cbMock,
Clock: &clk,
LedIdx: 0,
}
engine := ledengine.NewLedEngine(opts)
ctx, cancel := context.WithCancel(context.Background())
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
t.Log("LedEngine.Run() started")
err := engine.Run(ctx)
assert.ErrorIs(t, err, context.Canceled)
t.Log("LedEngine.Run() exited")
}()
// We want to change the pattern while the engine is running
time.Sleep(5 * time.Millisecond)
// Set pattern
t.Log("Setting pattern")
err := engine.SetPattern(ledengine.NewStaticPattern(led.Color{Red: 255}))
assert.NoError(t, err)
t.Log("Canceling context")
cancel()
wg.Wait()
clk.AssertExpectations(t)
cbMock.AssertExpectations(t)
}
func Test_LedEngine_SetPattern_BeforeRun(t *testing.T) {
t.Parallel()
clk := util.MockClock{}
clkAfterChan := make(chan time.Time)
clk.On("After", time.Hour).Once().Return(clkAfterChan)
cbMock := hal.ComputeBladeHalMock{}
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
opts := ledengine.Options{
Hal: &cbMock,
Clock: &clk,
LedIdx: 0,
}
engine := ledengine.NewLedEngine(opts)
// We want to change the pattern BEFORE the engine is started
t.Log("Setting pattern")
err := engine.SetPattern(ledengine.NewStaticPattern(led.Color{Red: 255}))
assert.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
t.Log("LedEngine.Run() started")
err := engine.Run(ctx)
assert.ErrorIs(t, err, context.DeadlineExceeded)
t.Log("LedEngine.Run() exited")
}()
t.Log("Waiting for context to timeout")
wg.Wait()
clk.AssertExpectations(t)
cbMock.AssertExpectations(t)
}
func Test_LedEngine_SetPattern_SetLedFailureInPattern(t *testing.T) {
t.Parallel()
clk := util.MockClock{}
clkAfterChan := make(chan time.Time)
clk.On("After", time.Hour).Once().Return(clkAfterChan)
cbMock := hal.ComputeBladeHalMock{}
call0 := cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(errors.New("failure")).NotBefore(call0)
opts := ledengine.Options{
Hal: &cbMock,
Clock: &clk,
LedIdx: 0,
}
engine := ledengine.NewLedEngine(opts)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
t.Log("LedEngine.Run() started")
err := engine.Run(ctx)
assert.Error(t, err)
t.Log("LedEngine.Run() exited")
}()
time.Sleep(5 * time.Millisecond)
// Time tick -> SetLed() fails
clkAfterChan <- time.Now()
t.Log("Waiting for context to timeout")
wg.Wait()
clk.AssertExpectations(t)
cbMock.AssertExpectations(t)
}
func Test_LedEngine_SetPattern_NoDelay(t *testing.T) {
t.Parallel()
clk := util.MockClock{}
clkAfterChan := make(chan time.Time)
clk.On("After", time.Hour).Once().Return(clkAfterChan)
cbMock := hal.ComputeBladeHalMock{}
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
opts := ledengine.Options{
Hal: &cbMock,
Clock: &clk,
LedIdx: 0,
}
engine := ledengine.NewLedEngine(opts)
invalidPattern := ledengine.NewStaticPattern(led.Color{Red: 255})
invalidPattern.Delays = []time.Duration{}
// We want to change the pattern BEFORE the engine is started
t.Log("Setting pattern")
err := engine.SetPattern(invalidPattern)
assert.Error(t, err)
assert.ErrorContains(t, err, "pattern must have at least one delay")
}