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

refactor: create an interface for status console #1481

Merged
merged 3 commits into from
Sep 30, 2024
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
63 changes: 35 additions & 28 deletions cmd/oras/internal/display/status/console/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import (
"os"

"github.com/containerd/console"
containerd "github.com/containerd/console"
"github.com/morikuni/aec"
)

Expand All @@ -33,60 +33,67 @@
Restore = "\0338"
)

// Console is a wrapper around containerd's console.Console and ANSI escape
// codes.
type Console struct {
console.Console
// Console is a wrapper around containerd's Console and ANSI escape codes.
type Console interface {
containerd.Console
GetHeightWidth() (height, width int)
Save()
NewRow()
OutputTo(upCnt uint, str string)
Restore()
}

// Size returns the width and height of the console.
// If the console size cannot be determined, returns a default value of 80x10.
func (c *Console) Size() (width, height int) {
width = MinWidth
height = MinHeight
size, err := c.Console.Size()
if err == nil {
if size.Height > MinHeight {
height = int(size.Height)
}
if size.Width > MinWidth {
width = int(size.Width)
}
}
return
type console struct {
containerd.Console
}

// New generates a Console from a file.
func New(f *os.File) (*Console, error) {
c, err := console.ConsoleFromFile(f)
// NewConsole generates a console from a file.
func NewConsole(f *os.File) (Console, error) {
c, err := containerd.ConsoleFromFile(f)
if err != nil {
return nil, err
}
return &Console{c}, nil
return &console{c}, nil
}

// GetHeightWidth returns the width and height of the console.
// If the console size cannot be determined, returns a default value of 80x10.
func (c *console) GetHeightWidth() (height, width int) {
windowSize, err := c.Console.Size()
if err != nil {
return MinHeight, MinWidth

Check warning on line 64 in cmd/oras/internal/display/status/console/console.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/internal/display/status/console/console.go#L64

Added line #L64 was not covered by tests
}
if windowSize.Height < MinHeight {
windowSize.Height = MinHeight
}
if windowSize.Width < MinWidth {
windowSize.Width = MinWidth
}
return int(windowSize.Height), int(windowSize.Width)
}

// Save saves the current cursor position.
func (c *Console) Save() {
func (c *console) Save() {
_, _ = c.Write([]byte(aec.Hide.Apply(Save)))
}

// NewRow allocates a horizontal space to the output area with scroll if needed.
func (c *Console) NewRow() {
func (c *console) NewRow() {
_, _ = c.Write([]byte(Restore))
_, _ = c.Write([]byte("\n"))
_, _ = c.Write([]byte(Save))
}

// OutputTo outputs a string to a specific line.
func (c *Console) OutputTo(upCnt uint, str string) {
func (c *console) OutputTo(upCnt uint, str string) {
_, _ = c.Write([]byte(Restore))
_, _ = c.Write([]byte(aec.PreviousLine(upCnt).Apply(str)))
_, _ = c.Write([]byte("\n"))
_, _ = c.Write([]byte(aec.EraseLine(aec.EraseModes.Tail).String()))
}

// Restore restores the saved cursor position.
func (c *Console) Restore() {
func (c *console) Restore() {
// cannot use aec.Restore since DEC has better compatibility than SCO
_, _ = c.Write([]byte(Restore))
_, _ = c.Write([]byte(aec.Column(0).
Expand Down
103 changes: 88 additions & 15 deletions cmd/oras/internal/display/status/console/console_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,38 @@ limitations under the License.
package console

import (
"os"
"testing"

"github.com/containerd/console"
containerd "github.com/containerd/console"
"oras.land/oras/internal/testutils"
)

func givenConsole(t *testing.T) (c Console, pty containerd.Console) {
pty, _, err := containerd.NewPty()
if err != nil {
t.Fatal(err)
}

c = &console{
Console: pty,
}
return c, pty
}

func givenTestConsole(t *testing.T) (c Console, pty containerd.Console, tty *os.File) {
var err error
pty, tty, err = testutils.NewPty()
if err != nil {
t.Fatal(err)
}

c = &console{
Console: pty,
}
return c, pty, tty
}

func validateSize(t *testing.T, gotWidth, gotHeight, wantWidth, wantHeight int) {
t.Helper()
if gotWidth != wantWidth {
Expand All @@ -33,31 +60,77 @@ func validateSize(t *testing.T, gotWidth, gotHeight, wantWidth, wantHeight int)
}
}

func TestConsole_Size(t *testing.T) {
pty, _, err := console.NewPty()
if err != nil {
t.Fatal(err)
}
c := &Console{
Console: pty,
func TestNewConsole(t *testing.T) {
_, err := NewConsole(os.Stdin)
if err == nil {
t.Error("expected error creating bogus console")
}
}

func TestConsole_GetHeightWidth(t *testing.T) {
c, pty := givenConsole(t)

// minimal width and height
gotWidth, gotHeight := c.Size()
gotHeight, gotWidth := c.GetHeightWidth()
validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight)

// zero width
_ = pty.Resize(console.WinSize{Width: 0, Height: MinHeight})
gotWidth, gotHeight = c.Size()
_ = pty.Resize(containerd.WinSize{Width: 0, Height: MinHeight})
gotHeight, gotWidth = c.GetHeightWidth()
validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight)

// zero height
_ = pty.Resize(console.WinSize{Width: MinWidth, Height: 0})
gotWidth, gotHeight = c.Size()
_ = pty.Resize(containerd.WinSize{Width: MinWidth, Height: 0})
gotHeight, gotWidth = c.GetHeightWidth()
validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight)

// valid zero and height
_ = pty.Resize(console.WinSize{Width: 200, Height: 100})
gotWidth, gotHeight = c.Size()
_ = pty.Resize(containerd.WinSize{Width: 200, Height: 100})
gotHeight, gotWidth = c.GetHeightWidth()
validateSize(t, gotWidth, gotHeight, 200, 100)

}

func TestConsole_NewRow(t *testing.T) {
c, pty, tty := givenTestConsole(t)

c.NewRow()

err := testutils.MatchPty(pty, tty, "^[8\r\n^[7")
if err != nil {
t.Fatalf("NewRow output error: %v", err)
}
}

func TestConsole_OutputTo(t *testing.T) {
c, pty, tty := givenTestConsole(t)

c.OutputTo(1, "test string")

err := testutils.MatchPty(pty, tty, "^[8^[[1Ftest string^[[0m\r\n^[[0K")
if err != nil {
t.Fatalf("OutputTo output error: %v", err)
}
}

func TestConsole_Restore(t *testing.T) {
c, pty, tty := givenTestConsole(t)

c.Restore()

err := testutils.MatchPty(pty, tty, "^[8^[[0G^[[2K^[[?25h")
if err != nil {
t.Fatalf("Restore output error: %v", err)
}
}

func TestConsole_Save(t *testing.T) {
c, pty, tty := givenTestConsole(t)

c.Save()

err := testutils.MatchPty(pty, tty, "^[[?25l^[7^[[0m")
if err != nil {
t.Fatalf("Save output error: %v", err)
}
}
8 changes: 4 additions & 4 deletions cmd/oras/internal/display/status/progress/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ type Manager interface {
type manager struct {
status []*status
statusLock sync.RWMutex
console *console.Console
console console.Console
updating sync.WaitGroup
renderDone chan struct{}
renderClosed chan struct{}
}

// NewManager initialized a new progress manager.
func NewManager(f *os.File) (Manager, error) {
c, err := console.New(f)
func NewManager(tty *os.File) (Manager, error) {
c, err := console.NewConsole(tty)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -88,7 +88,7 @@ func (m *manager) render() {
m.statusLock.RLock()
defer m.statusLock.RUnlock()
// todo: update size in another routine
width, height := m.console.Size()
height, width := m.console.GetHeightWidth()
lineCount := len(m.status) * 2
offset := 0
if lineCount > height {
Expand Down
9 changes: 7 additions & 2 deletions cmd/oras/internal/display/status/progress/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,15 @@ func Test_manager_render(t *testing.T) {
t.Fatal(err)
}
defer device.Close()
sole, err := console.NewConsole(device)
if err != nil {
t.Fatal(err)
}

m := &manager{
console: &console.Console{Console: pty},
console: sole,
}
_, height := m.console.Size()
height, _ := m.console.GetHeightWidth()
for i := 0; i < height; i++ {
if _, err := m.Add(); err != nil {
t.Fatal(err)
Expand Down
8 changes: 4 additions & 4 deletions internal/testutils/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ import (
"strings"
"sync"

"github.com/containerd/console"
containerd "github.com/containerd/console"
)

// NewPty creates a new pty pair for testing, caller is responsible for closing
// the returned device file if err is not nil.
func NewPty() (console.Console, *os.File, error) {
pty, devicePath, err := console.NewPty()
func NewPty() (containerd.Console, *os.File, error) {
pty, devicePath, err := containerd.NewPty()
if err != nil {
return nil, nil, err
}
Expand All @@ -42,7 +42,7 @@ func NewPty() (console.Console, *os.File, error) {

// MatchPty checks that the output matches the expected strings in specified
// order.
func MatchPty(pty console.Console, device *os.File, expected ...string) error {
func MatchPty(pty containerd.Console, device *os.File, expected ...string) error {
var wg sync.WaitGroup
wg.Add(1)
var buffer bytes.Buffer
Expand Down
Loading