Skip to content

Commit

Permalink
feat: support progressed output in blob push and blob get (#1113)
Browse files Browse the repository at this point in the history
Signed-off-by: Billy Zha <jinzha1@microsoft.com>
  • Loading branch information
qweeah authored Oct 13, 2023
1 parent 20c06d6 commit f9bb6d8
Show file tree
Hide file tree
Showing 20 changed files with 1,282 additions and 50 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ LDFLAGS += -X $(PROJECT_PKG)/internal/version.GitTreeState=${GIT_DIRTY}

.PHONY: test
test: tidy vendor check-encoding ## tidy and run tests
$(GO_EXE) test -race -v -coverprofile=coverage.txt -covermode=atomic ./...
$(GO_EXE) test -race -v -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./...

.PHONY: teste2e
teste2e: ## run end to end tests
Expand Down
95 changes: 95 additions & 0 deletions cmd/oras/internal/display/console/console.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
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"

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

const (
// MinWidth is the minimal width of supported console.
MinWidth = 80
// MinHeight is the minimal height of supported console.
MinHeight = 10
// cannot use aec.Save since DEC has better compatilibity than SCO
Save = "\0337"
// cannot use aec.Restore since DEC has better compatilibity than SCO
Restore = "\0338"
)

// Console is a wrapper around containerd's console.Console and ANSI escape
// codes.
type Console struct {
console.Console
}

// 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
}

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

// Save saves the current cursor position.
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() {
_, _ = 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) {
_, _ = c.Write([]byte(Restore))
_, _ = c.Write([]byte(aec.PreviousLine(upCnt).Apply(str)))
_, _ = c.Write([]byte(" "))
_, _ = c.Write([]byte(aec.EraseLine(aec.EraseModes.Tail).String()))
}

// Restore restores the saved cursor position.
func (c *Console) Restore() {
// cannot use aec.Restore since DEC has better compatilibity than SCO
_, _ = c.Write([]byte(Restore))
_, _ = c.Write([]byte(aec.Column(0).
With(aec.EraseLine(aec.EraseModes.All)).
With(aec.Show).String()))
}
63 changes: 63 additions & 0 deletions cmd/oras/internal/display/console/console_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//go:build darwin || freebsd || linux || netbsd || openbsd || solaris

/*
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 (
"testing"

"github.com/containerd/console"
)

func validateSize(t *testing.T, gotWidth, gotHeight, wantWidth, wantHeight int) {
t.Helper()
if gotWidth != wantWidth {
t.Errorf("Console.Size() gotWidth = %v, want %v", gotWidth, wantWidth)
}
if gotHeight != wantHeight {
t.Errorf("Console.Size() gotHeight = %v, want %v", gotHeight, wantHeight)
}
}

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

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

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

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

// valid zero and height
_ = pty.Resize(console.WinSize{Width: 200, Height: 100})
gotWidth, gotHeight = c.Size()
validateSize(t, gotWidth, gotHeight, 200, 100)
}
71 changes: 71 additions & 0 deletions cmd/oras/internal/display/console/testutils/testutils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//go:build darwin || freebsd || linux || netbsd || openbsd || solaris

/*
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 (
"bytes"
"fmt"
"io"
"os"
"strings"
"sync"

"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()
if err != nil {
return nil, nil, err
}
device, err := os.OpenFile(devicePath, os.O_RDWR, 0)
if err != nil {
return nil, nil, err
}
return pty, device, nil
}

// MatchPty checks that the output matches the expected strings in specified
// order.
func MatchPty(pty console.Console, device *os.File, expected ...string) error {
var wg sync.WaitGroup
wg.Add(1)
var buffer bytes.Buffer
go func() {
defer wg.Done()
_, _ = io.Copy(&buffer, pty)
}()
device.Close()
wg.Wait()

return OrderedMatch(buffer.String(), expected...)
}

// OrderedMatch matches the got with the expected strings in order.
func OrderedMatch(got string, want ...string) error {
for _, e := range want {
i := strings.Index(got, e)
if i < 0 {
return fmt.Errorf("failed to find %q in %q", e, got)
}
got = got[i+len(e):]
}
return nil
}
60 changes: 60 additions & 0 deletions cmd/oras/internal/display/progress/humanize/bytes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
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 humanize

import (
"fmt"
"math"
)

const base = 1024.0

var units = []string{"B", "kB", "MB", "GB", "TB"}

type Bytes struct {
Size float64
Unit string
}

// ToBytes converts size in bytes to human readable format.
func ToBytes(sizeInBytes int64) Bytes {
f := float64(sizeInBytes)
if f < base {
return Bytes{f, units[0]}
}
e := int(math.Floor(math.Log(f) / math.Log(base)))
if e >= len(units) {
// only support up to TB
e = len(units) - 1
}
p := f / math.Pow(base, float64(e))
return Bytes{RoundTo(p), units[e]}
}

// String returns the string representation of Bytes.
func (b Bytes) String() string {
return fmt.Sprintf("%v %2s", b.Size, b.Unit)
}

// RoundTo makes length of the size string to less than or equal to 4.
func RoundTo(size float64) float64 {
if size < 10 {
return math.Round(size*100) / 100
} else if size < 100 {
return math.Round(size*10) / 10
}
return math.Round(size)
}
72 changes: 72 additions & 0 deletions cmd/oras/internal/display/progress/humanize/bytes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
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 humanize

import (
"reflect"
"testing"
)

func TestRoundTo(t *testing.T) {
type args struct {
quantity float64
}
tests := []struct {
name string
args args
want float64
}{
{"round to 2 digit", args{1.223}, 1.22},
{"round to 1 digit", args{12.23}, 12.2},
{"round to no digit", args{122.6}, 123},
{"round to no digit", args{1223.123}, 1223},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := RoundTo(tt.args.quantity); got != tt.want {
t.Errorf("RoundTo() = %v, want %v", got, tt.want)
}
})
}
}

func TestToBytes(t *testing.T) {
type args struct {
sizeInBytes int64
}
tests := []struct {
name string
args args
want Bytes
}{
{"0 bytes", args{0}, Bytes{0, "B"}},
{"1023 bytes", args{1023}, Bytes{1023, "B"}},
{"1 kB", args{1024}, Bytes{1, "kB"}},
{"1.5 kB", args{1024 + 512}, Bytes{1.5, "kB"}},
{"12.5 kB", args{1024 * 12.5}, Bytes{12.5, "kB"}},
{"512.5 kB", args{1024 * 512.5}, Bytes{513, "kB"}},
{"1 MB", args{1024 * 1024}, Bytes{1, "MB"}},
{"1 GB", args{1024 * 1024 * 1024}, Bytes{1, "GB"}},
{"1 TB", args{1024 * 1024 * 1024 * 1024}, Bytes{1, "TB"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ToBytes(tt.args.sizeInBytes); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToBytes() = %v, want %v", got, tt.want)
}
})
}
}
Loading

0 comments on commit f9bb6d8

Please sign in to comment.