diff --git a/cmd/oras/internal/display/status/console/console.go b/cmd/oras/internal/display/status/console/console.go index 419c63a3d..fd9c06ec3 100644 --- a/cmd/oras/internal/display/status/console/console.go +++ b/cmd/oras/internal/display/status/console/console.go @@ -18,7 +18,7 @@ package console import ( "os" - "github.com/containerd/console" + containerd "github.com/containerd/console" "github.com/morikuni/aec" ) @@ -33,52 +33,59 @@ 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() } -// 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 + } + 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")) @@ -86,7 +93,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..65549e9e2 100644 --- a/cmd/oras/internal/display/status/console/console_test.go +++ b/cmd/oras/internal/display/status/console/console_test.go @@ -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 { @@ -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) + } } diff --git a/cmd/oras/internal/display/status/progress/manager.go b/cmd/oras/internal/display/status/progress/manager.go index 2b526e47e..28e61d5e0 100644 --- a/cmd/oras/internal/display/status/progress/manager.go +++ b/cmd/oras/internal/display/status/progress/manager.go @@ -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 } @@ -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 { diff --git a/cmd/oras/internal/display/status/progress/manager_test.go b/cmd/oras/internal/display/status/progress/manager_test.go index 01d2e4835..43d0f2104 100644 --- a/cmd/oras/internal/display/status/progress/manager_test.go +++ b/cmd/oras/internal/display/status/progress/manager_test.go @@ -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) diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 92ad30e5e..4c7ff4d22 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -216,7 +216,7 @@ func runPush(cmd *cobra.Command, opts *pushOptions) error { copyOptions.CopyGraphOptions.OnCopySkipped = displayStatus.OnCopySkipped copyOptions.CopyGraphOptions.PreCopy = displayStatus.PreCopy copyOptions.CopyGraphOptions.PostCopy = displayStatus.PostCopy - copy := func(root ocispec.Descriptor) error { + copyWithScopeHint := func(root ocispec.Descriptor) error { // add both pull and push scope hints for dst repository // to save potential push-scope token requests during copy ctx = registryutil.WithScopeHint(ctx, dst, auth.ActionPull, auth.ActionPush) @@ -230,7 +230,7 @@ func runPush(cmd *cobra.Command, opts *pushOptions) error { } // Push - root, err := doPush(dst, stopTrack, pack, copy) + root, err := doPush(dst, stopTrack, pack, copyWithScopeHint) if err != nil { return err } @@ -272,7 +272,7 @@ func doPush(dst oras.Target, stopTrack status.StopTrackTargetFunc, pack packFunc type packFunc func() (ocispec.Descriptor, error) type copyFunc func(desc ocispec.Descriptor) error -func pushArtifact(dst oras.Target, pack packFunc, copy copyFunc) (ocispec.Descriptor, error) { +func pushArtifact(_ oras.Target, pack packFunc, copy copyFunc) (ocispec.Descriptor, error) { root, err := pack() if err != nil { return ocispec.Descriptor{}, err diff --git a/go.mod b/go.mod index 46c9173ee..c7acf2c17 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 golang.org/x/sync v0.8.0 - golang.org/x/term v0.24.0 + golang.org/x/term v0.25.0 gopkg.in/yaml.v3 v3.0.1 oras.land/oras-go/v2 v2.5.0 ) @@ -29,5 +29,5 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.0 // indirect golang.org/x/crypto v0.26.0 // indirect - golang.org/x/sys v0.25.0 // indirect + golang.org/x/sys v0.26.0 // indirect ) diff --git a/go.sum b/go.sum index 467cc7dfc..c0001d698 100644 --- a/go.sum +++ b/go.sum @@ -60,10 +60,10 @@ golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/testutils/console.go b/internal/testutils/console.go index 8a262340b..5d26674c7 100644 --- a/internal/testutils/console.go +++ b/internal/testutils/console.go @@ -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 } @@ -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