From 6546b12a747a7a428dcd7381a370a0b9a6092b5a Mon Sep 17 00:00:00 2001
From: Terry Howe <terrylhowe@gmail.com>
Date: Thu, 15 Aug 2024 18:19:44 -0600
Subject: [PATCH] feature: create discard console

Signed-off-by: Terry Howe <terrylhowe@gmail.com>
---
 .../display/status/console/console.go         | 75 +++++++++-------
 .../display/status/console/console_test.go    | 20 ++---
 .../display/status/console/discard.go         | 85 +++++++++++++++++++
 .../display/status/console/discard_test.go    | 65 ++++++++++++++
 .../display/status/progress/manager.go        |  6 +-
 .../display/status/progress/manager_test.go   |  9 +-
 internal/testutils/mock_console.go            | 65 --------------
 7 files changed, 214 insertions(+), 111 deletions(-)
 create mode 100644 cmd/oras/internal/display/status/console/discard.go
 create mode 100644 cmd/oras/internal/display/status/console/discard_test.go
 delete mode 100644 internal/testutils/mock_console.go

diff --git a/cmd/oras/internal/display/status/console/console.go b/cmd/oras/internal/display/status/console/console.go
index 7c29492b5..29cd27fc7 100644
--- a/cmd/oras/internal/display/status/console/console.go
+++ b/cmd/oras/internal/display/status/console/console.go
@@ -16,11 +16,9 @@ limitations under the License.
 package console
 
 import (
-	"os"
-
-	"github.com/containerd/console"
+	containerd "github.com/containerd/console"
 	"github.com/morikuni/aec"
-	"oras.land/oras/internal/testutils"
+	"os"
 )
 
 const (
@@ -34,55 +32,70 @@ const (
 	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()
+}
+
+type console struct {
+	containerd.Console
+}
+
+// NewConsole generates a console from a file.
+func NewConsole(f *os.File) (Console, error) {
+	if f != nil && f.Name() == os.DevNull {
+		return NewDiscardConsole(f), nil
+	}
+	c, err := containerd.ConsoleFromFile(f)
+	if err != nil {
+		return nil, err
+	}
+	return &console{c}, nil
 }
 
 // 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)
+func (c *console) Size() (size containerd.WinSize, err error) {
+	size, err = c.Console.Size()
+	if err != nil {
+		size.Height = MinHeight
+		size.Width = MinWidth
+	} else {
+		if size.Height < MinHeight {
+			size.Height = MinHeight
 		}
-		if size.Width > MinWidth {
-			width = int(size.Width)
+		if size.Width < MinWidth {
+			size.Width = MinWidth
 		}
 	}
 	return
 }
 
-// New generates a Console from a file.
-func New(f *os.File) (*Console, error) {
-	if f != nil && f.Name() == os.DevNull {
-		return &Console{testutils.NewMockConsole(f)}, nil
-	}
-	c, err := console.ConsoleFromFile(f)
-	if err != nil {
-		return nil, err
-	}
-	return &Console{c}, nil
+// GetHeightWidth returns the width and height of the console.
+func (c *console) GetHeightWidth() (height, width int) {
+	windowSize, _ := c.Size()
+	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"))
@@ -90,7 +103,7 @@ func (c *Console) OutputTo(upCnt uint, str 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).
diff --git a/cmd/oras/internal/display/status/console/console_test.go b/cmd/oras/internal/display/status/console/console_test.go
index 00e1f694c..ded00e4fb 100644
--- a/cmd/oras/internal/display/status/console/console_test.go
+++ b/cmd/oras/internal/display/status/console/console_test.go
@@ -20,7 +20,7 @@ package console
 import (
 	"testing"
 
-	"github.com/containerd/console"
+	containerd "github.com/containerd/console"
 )
 
 func validateSize(t *testing.T, gotWidth, gotHeight, wantWidth, wantHeight int) {
@@ -34,30 +34,30 @@ func validateSize(t *testing.T, gotWidth, gotHeight, wantWidth, wantHeight int)
 }
 
 func TestConsole_Size(t *testing.T) {
-	pty, _, err := console.NewPty()
+	pty, _, err := containerd.NewPty()
 	if err != nil {
 		t.Fatal(err)
 	}
-	c := &Console{
+	c := &console{
 		Console: pty,
 	}
 
 	// 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)
 }
diff --git a/cmd/oras/internal/display/status/console/discard.go b/cmd/oras/internal/display/status/console/discard.go
new file mode 100644
index 000000000..52b335022
--- /dev/null
+++ b/cmd/oras/internal/display/status/console/discard.go
@@ -0,0 +1,85 @@
+/*
+Copyright The ORAS Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package console
+
+import (
+	"os"
+
+	containerd "github.com/containerd/console"
+)
+
+type discardConsole struct {
+	*os.File
+}
+
+func NewDiscardConsole(f *os.File) Console {
+	dc := discardConsole{
+		File: f,
+	}
+	return &dc
+}
+
+// Fd returns its file descriptor
+func (mc *discardConsole) Fd() uintptr {
+	return os.Stderr.Fd()
+}
+
+// Name returns its file name
+func (mc *discardConsole) Name() string {
+	return mc.File.Name()
+}
+
+func (mc *discardConsole) Resize(_ containerd.WinSize) error {
+	return nil
+}
+
+func (mc *discardConsole) ResizeFrom(containerd.Console) error {
+	return nil
+}
+func (mc *discardConsole) SetRaw() error {
+	return nil
+}
+func (mc *discardConsole) DisableEcho() error {
+	return nil
+}
+func (mc *discardConsole) Reset() error {
+	return nil
+}
+func (mc *discardConsole) Size() (containerd.WinSize, error) {
+	ws := containerd.WinSize{
+		Width:  80,
+		Height: 24,
+	}
+	return ws, nil
+}
+
+// GetHeightWidth returns the width and height of the console.
+func (mc *discardConsole) GetHeightWidth() (height, width int) {
+	windowSize, _ := mc.Size()
+	return int(windowSize.Height), int(windowSize.Width)
+}
+
+func (mc *discardConsole) Save() {
+}
+
+func (mc *discardConsole) NewRow() {
+}
+
+func (mc *discardConsole) OutputTo(_ uint, _ string) {
+}
+
+func (mc *discardConsole) Restore() {
+}
diff --git a/cmd/oras/internal/display/status/console/discard_test.go b/cmd/oras/internal/display/status/console/discard_test.go
new file mode 100644
index 000000000..f8420e381
--- /dev/null
+++ b/cmd/oras/internal/display/status/console/discard_test.go
@@ -0,0 +1,65 @@
+/*
+Copyright The ORAS Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package console
+
+import (
+	"os"
+	"testing"
+
+	containerd "github.com/containerd/console"
+)
+
+func TestConsole_New(t *testing.T) {
+	mockFile, err := os.OpenFile(os.DevNull, os.O_RDWR, 0666)
+	if err != nil {
+		t.Fatalf("Unexpected error %v", err)
+	}
+
+	sut, err := NewConsole(mockFile)
+	if err != nil {
+		t.Errorf("Unexpected error %v", err)
+	}
+
+	if err = sut.Resize(containerd.WinSize{}); err != nil {
+		t.Errorf("Unexpected erro for Resize: %v", err)
+	}
+	if err = sut.ResizeFrom(nil); err != nil {
+		t.Errorf("Unexpected erro for Resize: %v", err)
+	}
+	if err = sut.SetRaw(); err != nil {
+		t.Errorf("Unexpected erro for Resize: %v", err)
+	}
+	if err = sut.DisableEcho(); err != nil {
+		t.Errorf("Unexpected erro for Resize: %v", err)
+	}
+	if err = sut.Reset(); err != nil {
+		t.Errorf("Unexpected erro for Resize: %v", err)
+	}
+	windowSize, _ := sut.Size()
+	if windowSize.Height != 24 {
+		t.Errorf("Expected size 24 actual %d", windowSize.Height)
+	}
+	if windowSize.Width != 80 {
+		t.Errorf("Expected size 80 actual %d", windowSize.Width)
+	}
+	h, w := sut.GetHeightWidth()
+	if h != 24 {
+		t.Errorf("Expected size 24 actual %d", h)
+	}
+	if w != 80 {
+		t.Errorf("Expected size 80 actual %d", w)
+	}
+}
diff --git a/cmd/oras/internal/display/status/progress/manager.go b/cmd/oras/internal/display/status/progress/manager.go
index d0a3186dd..a3c182650 100644
--- a/cmd/oras/internal/display/status/progress/manager.go
+++ b/cmd/oras/internal/display/status/progress/manager.go
@@ -44,7 +44,7 @@ type Manager interface {
 type manager struct {
 	status       []*status
 	statusLock   sync.RWMutex
-	console      *console.Console
+	console      console.Console
 	actionPrompt string
 	donePrompt   string
 	updating     sync.WaitGroup
@@ -54,7 +54,7 @@ type manager struct {
 
 // NewManager initialized a new progress manager.
 func NewManager(actionPrompt string, donePrompt string, tty *os.File) (Manager, error) {
-	c, err := console.New(tty)
+	c, err := console.NewConsole(tty)
 	if err != nil {
 		return nil, err
 	}
@@ -92,7 +92,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 {
diff --git a/cmd/oras/internal/display/status/progress/manager_test.go b/cmd/oras/internal/display/status/progress/manager_test.go
index 516e4aaf2..9a76aa2d4 100644
--- a/cmd/oras/internal/display/status/progress/manager_test.go
+++ b/cmd/oras/internal/display/status/progress/manager_test.go
@@ -32,10 +32,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)
diff --git a/internal/testutils/mock_console.go b/internal/testutils/mock_console.go
deleted file mode 100644
index 3c5e108b4..000000000
--- a/internal/testutils/mock_console.go
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
-Copyright The ORAS Authors.
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package testutils
-
-import (
-	"github.com/containerd/console"
-	"os"
-)
-
-type MockConsole struct {
-	*os.File
-}
-
-func NewMockConsole(f *os.File) MockConsole {
-	return MockConsole{
-		File: f,
-	}
-}
-
-// Fd returns its file descriptor
-func (mc MockConsole) Fd() uintptr {
-	return os.Stderr.Fd()
-}
-
-// Name returns its file name
-func (mc MockConsole) Name() string {
-	return mc.File.Name()
-}
-
-func (mc MockConsole) Resize(_ console.WinSize) error {
-	return nil
-}
-
-func (mc MockConsole) ResizeFrom(console.Console) error {
-	return nil
-}
-func (mc MockConsole) SetRaw() error {
-	return nil
-}
-func (mc MockConsole) DisableEcho() error {
-	return nil
-}
-func (mc MockConsole) Reset() error {
-	return nil
-}
-func (mc MockConsole) Size() (console.WinSize, error) {
-	ws := console.WinSize{
-		Width:  80,
-		Height: 24,
-	}
-	return ws, nil
-}