Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement synchronization for using keys in testing #102

Merged
merged 3 commits into from
Oct 25, 2021
Merged
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
13 changes: 13 additions & 0 deletions gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ type Gui struct {
outputMode OutputMode
stop chan struct{}
blacklist []Key
testCounter int // used for testing synchronization
testNotify chan struct{}

// BgColor and FgColor allow to configure the background and foreground
// colors of the GUI.
Expand Down Expand Up @@ -485,6 +487,7 @@ func (g *Gui) MainLoop() error {
if err := g.flush(); err != nil {
return err
}
g.testCounter = 0
for {
select {
case ev := <-g.gEvents:
Expand All @@ -505,6 +508,13 @@ func (g *Gui) MainLoop() error {
if err := g.flush(); err != nil {
return err
}
// used during testing for synchronization
if g.testNotify != nil && g.testCounter > 0 {
for g.testCounter > 0 {
g.testCounter--
g.testNotify <- struct{}{}
}
}
}
}

Expand Down Expand Up @@ -532,6 +542,9 @@ func (g *Gui) handleEvent(ev *gocuiEvent) error {
switch ev.Type {
case eventKey, eventMouse:
return g.onKey(ev)
case eventTime:
g.testCounter++
return nil
case eventError:
return ev.Err
// Not sure if this should be handled. It acts weirder when it's here
Expand Down
3 changes: 3 additions & 0 deletions tcell_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const (
eventInterrupt
eventError
eventRaw
eventTime
)

var (
Expand Down Expand Up @@ -221,6 +222,8 @@ func pollEvent() gocuiEvent {
Ch: 0,
Mod: mouseMod,
}
case *tcell.EventTime:
return gocuiEvent{Type: eventTime}
default:
return gocuiEvent{Type: eventNone}
}
Expand Down
35 changes: 21 additions & 14 deletions testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ func (g *Gui) GetTestingScreen() TestingScreen {
// defer cleanup()
//
func (t *TestingScreen) StartGui() func() {
t.gui.testNotify = make(chan struct{})
go func() {
if err := t.gui.MainLoop(); err != nil && !errors.Is(err, ErrQuit) {
log.Panic(err)
}
}()
t.WaitSync()

t.started = true

Expand All @@ -76,6 +78,25 @@ func (t *TestingScreen) SendKey(key Key) {
t.screen.InjectKey(tcell.Key(key), rune(key), tcell.ModNone)
}

// SendsKeySync sends a key to gocui and wait until MainLoop process it.
func (t *TestingScreen) SendKeySync(key Key) {
if !t.started {
panic("TestingScreen must be started using 'StartGui' before injecting keys")
}
t.screen.InjectKey(tcell.Key(key), rune(key), tcell.ModNone)
t.WaitSync()
}

// WaitSync sends time event to gocui and awaits notification that it was received.
//
// Notification is sent from gocui at the end of MainLoop, so after this function returns,
// user has confirmation that all the keys sent to gocui before time event were processed.
func (t *TestingScreen) WaitSync() {
ev := &tcell.EventTime{}
t.screen.PostEvent(ev)
<-t.gui.testNotify
}
mjarkk marked this conversation as resolved.
Show resolved Hide resolved

// GetViewContent gets the current conent of a view from the simulated screen
func (t *TestingScreen) GetViewContent(viewName string) (string, error) {
if !t.started {
Expand Down Expand Up @@ -136,21 +157,7 @@ func (t *TestingScreen) injectString(str string) {
s := i * 10
e := i*10 + 10
simulationScreen.InjectKeyBytes([]byte(str[s:e]))
for i := 0; i < 10; i++ {
// Trigger GoCUI to update with new input
// Todo: Is this necessary?
t.gui.Update(func(*Gui) error {
return nil
})
}
}

simulationScreen.InjectKeyBytes([]byte(str[len(str)-extra:]))
for i := 0; i < extra; i++ {
// Trigger GoCUI to update with new input
// Todo: Is this necessary?
t.gui.Update(func(*Gui) error {
return nil
})
}
}
217 changes: 207 additions & 10 deletions testing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ package gocui
import (
"errors"
"fmt"
"log"
"strings"
"sync"
"testing"
"time"
)
Expand All @@ -22,7 +22,7 @@ func TestTestingScreenReturnsCorrectContent(t *testing.T) {
// Create a view specifying the "OutputSimulator" mode
g, err := NewGui(OutputSimulator, true)
if err != nil {
log.Panicln(err)
t.Error(err)
}
g.SetManagerFunc(func(g *Gui) error {
maxX, maxY := g.Size()
Expand All @@ -48,33 +48,230 @@ func TestTestingScreenReturnsCorrectContent(t *testing.T) {
return nil
}
if err := g.SetKeybinding("", KeyCtrlC, ModNone, exampleBindingToTest); err != nil {
log.Panicln(err)
t.Error(err)
}

// Create a test screen and start gocui
testingScreen := g.GetTestingScreen()
cleanup := testingScreen.StartGui()
defer cleanup()

// NOTE: This sequence can be replaced with `testingScreen.SendKeySync(KeyCtrlC)` (we keep it for covering the use case)
// Send a key to gocui
testingScreen.SendKey(KeyCtrlC)

// Wait for key to be processed
<-time.After(time.Millisecond * 50)
testingScreen.WaitSync()

mjarkk marked this conversation as resolved.
Show resolved Hide resolved
// Test that the keybinding fired and set "didCallCTRLC" to true
if !didCallCTRLC {
t.Error("Expect the simulator to invoke the key handler for CTRLC")
}

// Get the content from the testing screen
actualContent, err := testingScreen.GetViewContent(viewName)
// check view content
assertView(t, testingScreen, viewName, expectedViewContent)
}

func TestTestingScreenMultipleKeys(t *testing.T) {
// Track what happened in the view, we'll assert on these
didCallCTRLC := false
expectedViewContent := "Hello world!"
expectedViewContent1 := "Hello World!"
expectedViewContent2 := "HELLO WORLD!"
expectedViewContent3 := "Hello lord!!"
viewName := "testView1"

// Create a view specifying the "OutputSimulator" mode
g, err := NewGui(OutputSimulator, true)
if err != nil {
t.Error(err)
}
g.SetManagerFunc(func(g *Gui) error {
maxX, maxY := g.Size()
if v, err := g.SetView(viewName, maxX/2-7, maxY/2, maxX/2+7, maxY/2+2, 0); err != nil {
if !errors.Is(err, ErrUnknownView) {
return err
}

if _, err := g.SetCurrentView(viewName); err != nil {
return err
}

// Have the view draw "Hello world!"
fmt.Fprintln(v, expectedViewContent)
}

return nil
})

// Create a key binding which sets "didCallCTRLC" when triggered
exampleBindingToTest := func(g *Gui, v *View) error {
didCallCTRLC = true
return nil
}
if err := g.SetKeybinding("", KeyCtrlC, ModNone, exampleBindingToTest); err != nil {
t.Error(err)
}

if err := g.SetKeybinding("", KeyF1, ModNone, func(g *Gui, v *View) error {
v.Clear()
fmt.Fprintln(v, expectedViewContent1)
return nil
}); err != nil {
t.Error(err)
}
if err := g.SetKeybinding("", KeyF2, ModNone, func(g *Gui, v *View) error {
v.Clear()
<-time.After(time.Millisecond * 100)
fmt.Fprintln(v, expectedViewContent2)
return nil
}); err != nil {
t.Error(err)
}
if err := g.SetKeybinding("", KeyF3, ModNone, func(g *Gui, v *View) error {
v.Clear()
fmt.Fprintln(v, expectedViewContent3)
return nil
}); err != nil {
t.Error(err)
}

// Create a test screen and start gocui
testingScreen := g.GetTestingScreen()
cleanup := testingScreen.StartGui()
defer cleanup()

// check view content
dankox marked this conversation as resolved.
Show resolved Hide resolved
assertView(t, testingScreen, viewName, expectedViewContent)

// Send a key to gocui
testingScreen.SendKeySync(KeyCtrlC)

// Test that the keybinding fired and set "didCallCTRLC" to true
if !didCallCTRLC {
t.Error("Expect the simulator to invoke the key handler for CTRLC")
}

// Test that it contains the "Hello World!" we thought the view should draw
if strings.TrimSpace(actualContent) != expectedViewContent {
t.Error(fmt.Printf("Expected view content to be: %q got: %q", expectedViewContent, actualContent))
tests := []struct {
key Key
content string
}{
{KeyF1, expectedViewContent1},
{KeyF2, expectedViewContent2},
{KeyF3, expectedViewContent3},
}
for _, key := range tests {
// Send a key to gocui
testingScreen.SendKeySync(key.key)
// check view content
assertView(t, testingScreen, viewName, key.content)
}
}

func TestTestingScreenParallelKeys(t *testing.T) {
// Track what happened in the view, we'll assert on these
didCallCTRLC := false
didCallF1 := false
didCallF2 := false
didCallF3 := false
expectedViewContent := "Hello world!"
viewName := "testView1"

// Create a view specifying the "OutputSimulator" mode
g, err := NewGui(OutputSimulator, true)
if err != nil {
t.Error(err)
}
g.SetManagerFunc(func(g *Gui) error {
maxX, maxY := g.Size()
if v, err := g.SetView(viewName, maxX/2-7, maxY/2, maxX/2+7, maxY/2+2, 0); err != nil {
if !errors.Is(err, ErrUnknownView) {
return err
}

if _, err := g.SetCurrentView(viewName); err != nil {
return err
}

// Have the view draw "Hello world!"
fmt.Fprintln(v, expectedViewContent)
}

return nil
})

// Create a key bindings
if err := g.SetKeybinding("", KeyCtrlC, ModNone, func(g *Gui, v *View) error {
didCallCTRLC = true
return nil
}); err != nil {
t.Error(err)
}

if err := g.SetKeybinding("", KeyF1, ModNone, func(g *Gui, v *View) error {
didCallF1 = true
return nil
}); err != nil {
t.Error(err)
}
if err := g.SetKeybinding("", KeyF2, ModNone, func(g *Gui, v *View) error {
didCallF2 = true
return nil
}); err != nil {
t.Error(err)
}
if err := g.SetKeybinding("", KeyF3, ModNone, func(g *Gui, v *View) error {
didCallF3 = true
return nil
}); err != nil {
t.Error(err)
}

// Create a test screen and start gocui
testingScreen := g.GetTestingScreen()
cleanup := testingScreen.StartGui()
defer cleanup()

// check view content
assertView(t, testingScreen, viewName, expectedViewContent)

// Send a key to gocui
testingScreen.SendKeySync(KeyCtrlC)
var wg sync.WaitGroup
wg.Add(3)
go func() {
testingScreen.SendKeySync(KeyF1)
wg.Done()
}()
go func() {
testingScreen.SendKeySync(KeyF2)
wg.Done()
}()
go func() {
testingScreen.SendKeySync(KeyF3)
wg.Done()
}()

wg.Wait()

// Test that the keybinding fired
if !didCallCTRLC {
t.Error("Expect the simulator to invoke the key handler for CTRLC")
}
if !didCallF1 || !didCallF2 || !didCallF3 {
t.Error("Expect the simulator to invoke the key handler for F1, F2 and F3")
}
}

// assertView checks if view contains provided content.
func assertView(t *testing.T, ts TestingScreen, viewName, content string) {
t.Helper()
// Get the content from the testing screen
if actualContent, err := ts.GetViewContent(viewName); err != nil {
t.Error(err)
} else {
// Test that it contains the "Hello World!" we thought the view should draw
if strings.TrimSpace(actualContent) != content {
t.Error(fmt.Printf("Expected view content to be: %q got: %q", content, actualContent))
}
}
}