test(integration): extend app test
This commit is contained in:
parent
65db62166e
commit
9314506c75
@ -12,14 +12,13 @@ import (
|
|||||||
"git.netflux.io/rob/octoplex/internal/mediaserver"
|
"git.netflux.io/rob/octoplex/internal/mediaserver"
|
||||||
"git.netflux.io/rob/octoplex/internal/multiplexer"
|
"git.netflux.io/rob/octoplex/internal/multiplexer"
|
||||||
"git.netflux.io/rob/octoplex/internal/terminal"
|
"git.netflux.io/rob/octoplex/internal/terminal"
|
||||||
"github.com/gdamore/tcell/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunParams holds the parameters for running the application.
|
// RunParams holds the parameters for running the application.
|
||||||
type RunParams struct {
|
type RunParams struct {
|
||||||
Config config.Config
|
Config config.Config
|
||||||
DockerClient container.DockerClient
|
DockerClient container.DockerClient
|
||||||
Screen tcell.Screen
|
Screen *terminal.Screen // Screen may be nil.
|
||||||
ClipboardAvailable bool
|
ClipboardAvailable bool
|
||||||
ConfigFilePath string
|
ConfigFilePath string
|
||||||
BuildInfo domain.BuildInfo
|
BuildInfo domain.BuildInfo
|
||||||
|
@ -4,47 +4,161 @@ package app_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.netflux.io/rob/octoplex/internal/app"
|
"git.netflux.io/rob/octoplex/internal/app"
|
||||||
"git.netflux.io/rob/octoplex/internal/config"
|
"git.netflux.io/rob/octoplex/internal/config"
|
||||||
"git.netflux.io/rob/octoplex/internal/domain"
|
"git.netflux.io/rob/octoplex/internal/domain"
|
||||||
|
"git.netflux.io/rob/octoplex/internal/terminal"
|
||||||
"git.netflux.io/rob/octoplex/internal/testhelpers"
|
"git.netflux.io/rob/octoplex/internal/testhelpers"
|
||||||
dockerclient "github.com/docker/docker/client"
|
dockerclient "github.com/docker/docker/client"
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIntegration(t *testing.T) {
|
func TestIntegration(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
logger := testhelpers.NewTestLogger()
|
logger := testhelpers.NewTestLogger().With("component", "integration")
|
||||||
dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
|
dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Fetching the screen contents is tricky at this level of the test pyramid,
|
||||||
|
// because we need to:
|
||||||
|
//
|
||||||
|
// 1. Somehow capture the screen contents, which is only available via the
|
||||||
|
// tcell.SimulationScreen, and...
|
||||||
|
// 2. Do so without triggering data races.
|
||||||
|
//
|
||||||
|
// We can achieve this by passing a channel into the terminal actor, which
|
||||||
|
// will send screen captures after each render. This can be stored locally
|
||||||
|
// and asserted against when needed.
|
||||||
|
var (
|
||||||
|
screenCells []tcell.SimCell
|
||||||
|
screenWidth int
|
||||||
|
screenMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
getContents := func() []string {
|
||||||
|
screenMu.Lock()
|
||||||
|
defer screenMu.Unlock()
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
for n, _ := range screenCells {
|
||||||
|
y := n / screenWidth
|
||||||
|
|
||||||
|
if y > len(lines)-1 {
|
||||||
|
lines = append(lines, "")
|
||||||
|
}
|
||||||
|
lines[y] += string(screenCells[n].Runes[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
screen := tcell.NewSimulationScreen("")
|
||||||
|
screenCaptureC := make(chan terminal.ScreenCapture, 1)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case capture := <-screenCaptureC:
|
||||||
|
screenMu.Lock()
|
||||||
|
screenCells = capture.Cells
|
||||||
|
screenWidth = capture.Width
|
||||||
|
screenMu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
require.NoError(t, app.Run(ctx, app.RunParams{
|
err := app.Run(ctx, app.RunParams{
|
||||||
Config: config.Config{},
|
Config: config.Config{
|
||||||
|
// We use the mediaserver as the destination server, just because it is
|
||||||
|
// reachable from the docker network via mediaserver:1935.
|
||||||
|
Destinations: []config.Destination{
|
||||||
|
{
|
||||||
|
Name: "Local server 1",
|
||||||
|
URL: "rtmp://mediaserver:1935/live/dest1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Local server 2",
|
||||||
|
URL: "rtmp://mediaserver:1935/live/dest2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
DockerClient: dockerClient,
|
DockerClient: dockerClient,
|
||||||
Screen: tcell.NewSimulationScreen(""),
|
Screen: &terminal.Screen{
|
||||||
|
Screen: screen,
|
||||||
|
Width: 160,
|
||||||
|
Height: 25,
|
||||||
|
CaptureC: screenCaptureC,
|
||||||
|
},
|
||||||
ClipboardAvailable: false,
|
ClipboardAvailable: false,
|
||||||
BuildInfo: domain.BuildInfo{Version: "0.0.1", GoVersion: "go1.16.3"},
|
BuildInfo: domain.BuildInfo{Version: "0.0.1", GoVersion: "go1.16.3"},
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
}))
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
done <- struct{}{}
|
done <- struct{}{}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// For now, just launch the app and wait for a few seconds.
|
// Wait for mediaserver container to start:
|
||||||
// This is mostly useful to verify there are no obvious data races (when
|
time.Sleep(5 * time.Second)
|
||||||
// running with -race).
|
|
||||||
// See https://github.com/rivo/tview/wiki/Concurrency.
|
// Start streaming a test video to the app:
|
||||||
//
|
testhelpers.StreamFLV(t, "rtmp://localhost:1935/live")
|
||||||
// TODO: test more user journeys.
|
time.Sleep(10 * time.Second)
|
||||||
time.Sleep(time.Second * 5)
|
|
||||||
|
// Start destinations:
|
||||||
|
screen.PostEvent(tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone))
|
||||||
|
screen.PostEvent(tcell.NewEventKey(tcell.KeyDown, ' ', tcell.ModNone))
|
||||||
|
screen.PostEvent(tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone))
|
||||||
|
|
||||||
|
time.Sleep(15 * time.Second)
|
||||||
|
|
||||||
|
contents := getContents()
|
||||||
|
require.True(t, len(contents) > 2, "expected at least 3 lines of output")
|
||||||
|
|
||||||
|
require.Contains(t, contents[2], "Status receiving", "expected mediaserver status to be receiving")
|
||||||
|
require.Contains(t, contents[3], "Tracks H264", "expected mediaserver tracks to be H264")
|
||||||
|
require.Contains(t, contents[4], "Health healthy", "expected mediaserver to be healthy")
|
||||||
|
|
||||||
|
require.Contains(t, contents[2], "Local server 1", "expected local server 1 to be present")
|
||||||
|
assert.Contains(t, contents[2], "sending", "expected local server 1 to be sending")
|
||||||
|
assert.Contains(t, contents[2], "healthy", "expected local server 1 to be healthy")
|
||||||
|
|
||||||
|
require.Contains(t, contents[3], "Local server 2", "expected local server 2 to be present")
|
||||||
|
assert.Contains(t, contents[3], "sending", "expected local server 2 to be sending")
|
||||||
|
assert.Contains(t, contents[3], "healthy", "expected local server 2 to be healthy")
|
||||||
|
|
||||||
|
// Stop destinations:
|
||||||
|
screen.PostEvent(tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone))
|
||||||
|
screen.PostEvent(tcell.NewEventKey(tcell.KeyUp, ' ', tcell.ModNone))
|
||||||
|
screen.PostEvent(tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone))
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
contents = getContents()
|
||||||
|
require.True(t, len(contents) > 2, "expected at least 3 lines of output")
|
||||||
|
|
||||||
|
require.Contains(t, contents[2], "Local server 1", "expected local server 1 to be present")
|
||||||
|
assert.Contains(t, contents[2], "exited", "expected local server 1 to have exited")
|
||||||
|
|
||||||
|
require.Contains(t, contents[3], "Local server 2", "expected local server 2 to be present")
|
||||||
|
assert.Contains(t, contents[3], "exited", "expected local server 2 to have exited")
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// - Source error
|
||||||
|
// - Destination error
|
||||||
|
// - Additional features (copy URL, etc.)
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
<-done
|
<-done
|
||||||
|
@ -1,110 +0,0 @@
|
|||||||
//go:build integration
|
|
||||||
|
|
||||||
package mediaserver_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.netflux.io/rob/octoplex/internal/container"
|
|
||||||
"git.netflux.io/rob/octoplex/internal/mediaserver"
|
|
||||||
"git.netflux.io/rob/octoplex/internal/testhelpers"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
const component = "mediaserver"
|
|
||||||
|
|
||||||
func TestIntegrationMediaServerStartStop(t *testing.T) {
|
|
||||||
logger := testhelpers.NewTestLogger()
|
|
||||||
apiClient, err := client.NewClientWithOpts(client.FromEnv)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
containerClient, err := container.NewClient(t.Context(), apiClient, logger)
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() { require.NoError(t, containerClient.Close()) })
|
|
||||||
|
|
||||||
running, err := containerClient.ContainerRunning(t.Context(), containerClient.ContainersWithLabels(map[string]string{container.LabelComponent: component}))
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.False(t, running)
|
|
||||||
|
|
||||||
// We need to avoid clashing with other integration tests, e.g. multiplexer.
|
|
||||||
const (
|
|
||||||
apiPort = 9999
|
|
||||||
rtmpPort = 1937
|
|
||||||
)
|
|
||||||
|
|
||||||
rtmpURL := fmt.Sprintf("rtmp://localhost:%d/live", rtmpPort)
|
|
||||||
|
|
||||||
mediaServer := mediaserver.StartActor(t.Context(), mediaserver.StartActorParams{
|
|
||||||
RTMPPort: rtmpPort,
|
|
||||||
APIPort: apiPort,
|
|
||||||
FetchIngressStateInterval: 500 * time.Millisecond,
|
|
||||||
ChanSize: 1,
|
|
||||||
ContainerClient: containerClient,
|
|
||||||
Logger: logger,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() { mediaServer.Close() })
|
|
||||||
testhelpers.ChanDiscard(mediaServer.C())
|
|
||||||
|
|
||||||
require.Eventually(
|
|
||||||
t,
|
|
||||||
func() bool {
|
|
||||||
running, err = containerClient.ContainerRunning(t.Context(), containerClient.ContainersWithLabels(map[string]string{container.LabelComponent: component}))
|
|
||||||
return err == nil && running
|
|
||||||
},
|
|
||||||
time.Second*10,
|
|
||||||
time.Second,
|
|
||||||
"container not in RUNNING state",
|
|
||||||
)
|
|
||||||
|
|
||||||
state := mediaServer.State()
|
|
||||||
assert.False(t, state.Live)
|
|
||||||
assert.Equal(t, rtmpURL, state.RTMPURL)
|
|
||||||
|
|
||||||
testhelpers.StreamFLV(t, rtmpURL)
|
|
||||||
|
|
||||||
require.Eventually(
|
|
||||||
t,
|
|
||||||
func() bool {
|
|
||||||
currState := mediaServer.State()
|
|
||||||
return currState.Live &&
|
|
||||||
!currState.LiveChangedAt.IsZero() &&
|
|
||||||
currState.Container.HealthState == "healthy"
|
|
||||||
},
|
|
||||||
time.Second*5,
|
|
||||||
time.Second,
|
|
||||||
"actor not healthy and/or in LIVE state",
|
|
||||||
)
|
|
||||||
|
|
||||||
require.Eventually(
|
|
||||||
t,
|
|
||||||
func() bool {
|
|
||||||
currState := mediaServer.State()
|
|
||||||
return len(currState.Tracks) == 1 && currState.Tracks[0] == "H264"
|
|
||||||
},
|
|
||||||
time.Second*5,
|
|
||||||
time.Second,
|
|
||||||
"tracks not updated",
|
|
||||||
)
|
|
||||||
|
|
||||||
require.Eventually(
|
|
||||||
t,
|
|
||||||
func() bool {
|
|
||||||
currState := mediaServer.State()
|
|
||||||
return currState.Container.RxRate > 500
|
|
||||||
},
|
|
||||||
time.Second*10,
|
|
||||||
time.Second,
|
|
||||||
"rxRate not updated",
|
|
||||||
)
|
|
||||||
|
|
||||||
mediaServer.Close()
|
|
||||||
|
|
||||||
running, err = containerClient.ContainerRunning(t.Context(), containerClient.ContainersWithLabels(map[string]string{container.LabelComponent: component}))
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.False(t, running)
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
//go:build integration
|
|
||||||
|
|
||||||
package multiplexer_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.netflux.io/rob/octoplex/internal/container"
|
|
||||||
"git.netflux.io/rob/octoplex/internal/mediaserver"
|
|
||||||
"git.netflux.io/rob/octoplex/internal/multiplexer"
|
|
||||||
"git.netflux.io/rob/octoplex/internal/testhelpers"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
const component = "multiplexer"
|
|
||||||
|
|
||||||
func TestIntegrationMultiplexer(t *testing.T) {
|
|
||||||
logger := testhelpers.NewTestLogger()
|
|
||||||
apiClient, err := client.NewClientWithOpts(client.FromEnv)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
containerClient, err := container.NewClient(t.Context(), apiClient, logger)
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() { require.NoError(t, containerClient.Close()) })
|
|
||||||
|
|
||||||
running, err := containerClient.ContainerRunning(t.Context(), containerClient.ContainersWithLabels(map[string]string{container.LabelComponent: component}))
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.False(t, running)
|
|
||||||
|
|
||||||
// We need to avoid clashing with other integration tests, e.g. mediaserver.
|
|
||||||
const (
|
|
||||||
apiPort = 9998
|
|
||||||
rtmpPort = 19350
|
|
||||||
)
|
|
||||||
|
|
||||||
srv := mediaserver.StartActor(t.Context(), mediaserver.StartActorParams{
|
|
||||||
RTMPPort: rtmpPort,
|
|
||||||
APIPort: apiPort,
|
|
||||||
FetchIngressStateInterval: 250 * time.Millisecond,
|
|
||||||
ContainerClient: containerClient,
|
|
||||||
ChanSize: 1,
|
|
||||||
Logger: logger,
|
|
||||||
})
|
|
||||||
defer srv.Close()
|
|
||||||
testhelpers.ChanDiscard(srv.C())
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
testhelpers.StreamFLV(t, srv.State().RTMPURL)
|
|
||||||
|
|
||||||
require.Eventually(
|
|
||||||
t,
|
|
||||||
func() bool { return srv.State().Live },
|
|
||||||
time.Second*10,
|
|
||||||
time.Second,
|
|
||||||
"source not live",
|
|
||||||
)
|
|
||||||
|
|
||||||
mp := multiplexer.NewActor(t.Context(), multiplexer.NewActorParams{
|
|
||||||
SourceURL: srv.State().RTMPInternalURL,
|
|
||||||
ChanSize: 1,
|
|
||||||
ContainerClient: containerClient,
|
|
||||||
Logger: logger,
|
|
||||||
})
|
|
||||||
defer mp.Close()
|
|
||||||
testhelpers.ChanDiscard(mp.C())
|
|
||||||
|
|
||||||
requireListeners(t, srv, 0)
|
|
||||||
|
|
||||||
mp.StartDestination("rtmp://mediaserver:19350/destination/test1")
|
|
||||||
mp.StartDestination("rtmp://mediaserver:19350/destination/test2")
|
|
||||||
mp.StartDestination("rtmp://mediaserver:19350/destination/test3")
|
|
||||||
requireListeners(t, srv, 3)
|
|
||||||
|
|
||||||
mp.StopDestination("rtmp://mediaserver:19350/destination/test3")
|
|
||||||
requireListeners(t, srv, 2)
|
|
||||||
|
|
||||||
mp.StopDestination("rtmp://mediaserver:19350/destination/test2")
|
|
||||||
mp.StopDestination("rtmp://mediaserver:19350/destination/test1")
|
|
||||||
requireListeners(t, srv, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func requireListeners(t *testing.T, srv *mediaserver.Actor, expected int) {
|
|
||||||
require.Eventually(
|
|
||||||
t,
|
|
||||||
func() bool { return srv.State().Listeners == expected },
|
|
||||||
time.Second*10,
|
|
||||||
time.Second,
|
|
||||||
"expected %d listeners", expected,
|
|
||||||
)
|
|
||||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -45,6 +46,8 @@ type UI struct {
|
|||||||
// tview state
|
// tview state
|
||||||
|
|
||||||
app *tview.Application
|
app *tview.Application
|
||||||
|
screen tcell.Screen
|
||||||
|
screenCaptureC chan<- ScreenCapture
|
||||||
pages *tview.Pages
|
pages *tview.Pages
|
||||||
sourceViews sourceViews
|
sourceViews sourceViews
|
||||||
destView *tview.Table
|
destView *tview.Table
|
||||||
@ -56,6 +59,21 @@ type UI struct {
|
|||||||
allowQuit bool
|
allowQuit bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Screen represents a terminal screen. This includes its desired dimensions,
|
||||||
|
// which is required to initialize the tcell.SimulationScreen.
|
||||||
|
type Screen struct {
|
||||||
|
Screen tcell.Screen
|
||||||
|
Width, Height int
|
||||||
|
CaptureC chan<- ScreenCapture
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScreenCapture represents a screen capture, which is used for integration
|
||||||
|
// testing with the tcell.SimulationScreen.
|
||||||
|
type ScreenCapture struct {
|
||||||
|
Cells []tcell.SimCell
|
||||||
|
Width, Height int
|
||||||
|
}
|
||||||
|
|
||||||
// StartParams contains the parameters for starting a new terminal user
|
// StartParams contains the parameters for starting a new terminal user
|
||||||
// interface.
|
// interface.
|
||||||
type StartParams struct {
|
type StartParams struct {
|
||||||
@ -64,7 +82,7 @@ type StartParams struct {
|
|||||||
ClipboardAvailable bool
|
ClipboardAvailable bool
|
||||||
ConfigFilePath string
|
ConfigFilePath string
|
||||||
BuildInfo domain.BuildInfo
|
BuildInfo domain.BuildInfo
|
||||||
Screen tcell.Screen
|
Screen *Screen // Screen may be nil.
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultChanSize = 64
|
const defaultChanSize = 64
|
||||||
@ -76,9 +94,17 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) {
|
|||||||
|
|
||||||
app := tview.NewApplication()
|
app := tview.NewApplication()
|
||||||
|
|
||||||
|
var screen tcell.Screen
|
||||||
|
var screenCaptureC chan<- ScreenCapture
|
||||||
|
if params.Screen != nil {
|
||||||
|
screen = params.Screen.Screen
|
||||||
|
screenCaptureC = params.Screen.CaptureC
|
||||||
// Allow the tcell screen to be overridden for integration tests. If
|
// Allow the tcell screen to be overridden for integration tests. If
|
||||||
// params.Screen is nil, the real terminal is used.
|
// params.Screen is nil, the real terminal is used.
|
||||||
app.SetScreen(params.Screen)
|
app.SetScreen(screen)
|
||||||
|
// SetSize must be called after SetScreen:
|
||||||
|
screen.SetSize(params.Screen.Width, params.Screen.Height)
|
||||||
|
}
|
||||||
|
|
||||||
sidebar := tview.NewFlex()
|
sidebar := tview.NewFlex()
|
||||||
sidebar.SetDirection(tview.FlexRow)
|
sidebar.SetDirection(tview.FlexRow)
|
||||||
@ -166,6 +192,8 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) {
|
|||||||
buildInfo: params.BuildInfo,
|
buildInfo: params.BuildInfo,
|
||||||
logger: params.Logger,
|
logger: params.Logger,
|
||||||
app: app,
|
app: app,
|
||||||
|
screen: screen,
|
||||||
|
screenCaptureC: screenCaptureC,
|
||||||
pages: pages,
|
pages: pages,
|
||||||
sourceViews: sourceViews{
|
sourceViews: sourceViews{
|
||||||
url: urlTextView,
|
url: urlTextView,
|
||||||
@ -201,6 +229,10 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) {
|
|||||||
return event
|
return event
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if ui.screenCaptureC != nil {
|
||||||
|
app.SetAfterDrawFunc(ui.captureScreen)
|
||||||
|
}
|
||||||
|
|
||||||
go ui.run(ctx)
|
go ui.run(ctx)
|
||||||
|
|
||||||
return ui, nil
|
return ui, nil
|
||||||
@ -297,6 +329,25 @@ func (ui *UI) AllowQuit() {
|
|||||||
ui.allowQuit = true
|
ui.allowQuit = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// captureScreen captures the screen and sends it to the screenCaptureC
|
||||||
|
// channel, which must have been set in StartParams.
|
||||||
|
//
|
||||||
|
// This is required for integration testing because GetContents() must be
|
||||||
|
// called inside the tview goroutine to avoid data races.
|
||||||
|
func (ui *UI) captureScreen(screen tcell.Screen) {
|
||||||
|
simScreen, ok := screen.(tcell.SimulationScreen)
|
||||||
|
if !ok {
|
||||||
|
ui.logger.Error("simulation screen not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
cells, w, h := simScreen.GetContents()
|
||||||
|
ui.screenCaptureC <- ScreenCapture{
|
||||||
|
Cells: slices.Clone(cells),
|
||||||
|
Width: w,
|
||||||
|
Height: h,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SetState sets the state of the terminal user interface.
|
// SetState sets the state of the terminal user interface.
|
||||||
func (ui *UI) SetState(state domain.AppState) {
|
func (ui *UI) SetState(state domain.AppState) {
|
||||||
if state.Source.ExitReason != "" {
|
if state.Source.ExitReason != "" {
|
||||||
|
@ -27,6 +27,8 @@ func StreamFLV(t *testing.T, destURL string) {
|
|||||||
"-f", "flv",
|
"-f", "flv",
|
||||||
destURL,
|
destURL,
|
||||||
)
|
)
|
||||||
|
// Uncomment to view output:
|
||||||
|
// cmd.Stderr = os.Stderr
|
||||||
require.NoError(t, cmd.Start())
|
require.NoError(t, cmd.Start())
|
||||||
|
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
|
@ -16,12 +16,12 @@ alias = "ti"
|
|||||||
[tasks.test_ci]
|
[tasks.test_ci]
|
||||||
description = "Run tests in CI"
|
description = "Run tests in CI"
|
||||||
dir = "{{cwd}}"
|
dir = "{{cwd}}"
|
||||||
run = "go test -v -count 1 -parallel 1 -race ./..."
|
run = "go test -v -count 1 -race ./..."
|
||||||
|
|
||||||
[tasks.test_integration_ci]
|
[tasks.test_integration_ci]
|
||||||
description = "Run integration tests in CI"
|
description = "Run integration tests in CI"
|
||||||
dir = "{{cwd}}"
|
dir = "{{cwd}}"
|
||||||
run = "go test -v -race -tags=integration -run TestIntegration ./..."
|
run = "go test -v -count 1 -race -parallel 1 -tags=integration -run TestIntegration ./..."
|
||||||
|
|
||||||
[tasks.lint]
|
[tasks.lint]
|
||||||
description = "Run linters"
|
description = "Run linters"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user