import: move bib files to correct path
Moves the files imported from `bootc-image-builder` directly under `pkg` so they can be imported in reverse. Also fix up any import paths at the same time. Signed-off-by: Simon de Vlieger <supakeen@redhat.com>
This commit is contained in:
parent
7ac659490c
commit
f4bbd3e048
5 changed files with 2 additions and 2 deletions
47
pkg/progress/export_test.go
Normal file
47
pkg/progress/export_test.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package progress
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type (
|
||||
TerminalProgressBar = terminalProgressBar
|
||||
DebugProgressBar = debugProgressBar
|
||||
VerboseProgressBar = verboseProgressBar
|
||||
)
|
||||
|
||||
var (
|
||||
NewSyncedWriter = newSyncedWriter
|
||||
)
|
||||
|
||||
func MockOsStdout(w io.Writer) (restore func()) {
|
||||
saved := osStdout
|
||||
osStdout = func() io.Writer { return w }
|
||||
return func() {
|
||||
osStdout = saved
|
||||
}
|
||||
}
|
||||
|
||||
func MockOsStderr(w io.Writer) (restore func()) {
|
||||
saved := osStderr
|
||||
osStderr = func() io.Writer { return w }
|
||||
return func() {
|
||||
osStderr = saved
|
||||
}
|
||||
}
|
||||
|
||||
func MockIsattyIsTerminal(fn func(uintptr) bool) (restore func()) {
|
||||
saved := isattyIsTerminal
|
||||
isattyIsTerminal = fn
|
||||
return func() {
|
||||
isattyIsTerminal = saved
|
||||
}
|
||||
}
|
||||
|
||||
func MockOsbuildCmd(s string) (restore func()) {
|
||||
saved := osbuildCmd
|
||||
osbuildCmd = s
|
||||
return func() {
|
||||
osbuildCmd = saved
|
||||
}
|
||||
}
|
||||
506
pkg/progress/progress.go
Normal file
506
pkg/progress/progress.go
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
package progress
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/osbuild/images/pkg/osbuild"
|
||||
)
|
||||
|
||||
var (
|
||||
// This is only needed because pb.Pool require a real terminal.
|
||||
// It sets it into "raw-mode" but there is really no need for
|
||||
// this (see "func render()" below) so once this is fixed
|
||||
// upstream we should remove this.
|
||||
ESC = "\x1b"
|
||||
ERASE_LINE = ESC + "[2K"
|
||||
CURSOR_HIDE = ESC + "[?25l"
|
||||
CURSOR_SHOW = ESC + "[?25h"
|
||||
)
|
||||
|
||||
// Used for testing, this must be a function (instead of the usual
|
||||
// "var osStderr = os.Stderr" so that higher level libraries can test
|
||||
// this code by replacing "os.Stderr", e.g. testutil.CaptureStdio()
|
||||
var osStdout = func() io.Writer {
|
||||
return os.Stdout
|
||||
}
|
||||
var osStderr = func() io.Writer {
|
||||
return os.Stderr
|
||||
}
|
||||
|
||||
func cursorUp(i int) string {
|
||||
return fmt.Sprintf("%s[%dA", ESC, i)
|
||||
}
|
||||
|
||||
// ProgressBar is an interface for progress reporting when there is
|
||||
// an arbitrary amount of sub-progress information (like osbuild)
|
||||
type ProgressBar interface {
|
||||
// SetProgress sets the progress details at the given "level".
|
||||
// Levels should start with "0" and increase as the nesting
|
||||
// gets deeper.
|
||||
//
|
||||
// Note that reducing depth is currently not supported, once
|
||||
// a sub-progress is added it cannot be removed/hidden
|
||||
// (but if required it can be added, its a SMOP)
|
||||
SetProgress(level int, msg string, done int, total int) error
|
||||
|
||||
// The high-level message that is displayed in a spinner
|
||||
// that contains the current top level step, for bib this
|
||||
// is really just "Manifest generation step" and
|
||||
// "Image generation step". We could map this to a three-level
|
||||
// progress as well but we spend 90% of the time in the
|
||||
// "Image generation step" so the UI looks a bit odd.
|
||||
SetPulseMsgf(fmt string, args ...interface{})
|
||||
|
||||
// A high level message with the last operation status.
|
||||
// For us this usually comes from the stages and has information
|
||||
// like "Starting module org.osbuild.selinux"
|
||||
SetMessagef(fmt string, args ...interface{})
|
||||
|
||||
// Start will start rendering the progress information
|
||||
Start()
|
||||
|
||||
// Stop will stop rendering the progress information, the
|
||||
// screen is not cleared, the last few lines will be visible
|
||||
Stop()
|
||||
}
|
||||
|
||||
var isattyIsTerminal = isatty.IsTerminal
|
||||
|
||||
// New creates a new progressbar based on the requested type
|
||||
func New(typ string) (ProgressBar, error) {
|
||||
switch typ {
|
||||
case "", "auto":
|
||||
// autoselect based on if we are on an interactive
|
||||
// terminal, use verbose progress for scripts
|
||||
if isattyIsTerminal(os.Stdin.Fd()) {
|
||||
return NewTerminalProgressBar()
|
||||
}
|
||||
return NewVerboseProgressBar()
|
||||
case "verbose":
|
||||
return NewVerboseProgressBar()
|
||||
case "term":
|
||||
return NewTerminalProgressBar()
|
||||
case "debug":
|
||||
return NewDebugProgressBar()
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown progress type: %q", typ)
|
||||
}
|
||||
}
|
||||
|
||||
type terminalProgressBar struct {
|
||||
spinnerPb *pb.ProgressBar
|
||||
msgPb *pb.ProgressBar
|
||||
subLevelPbs []*pb.ProgressBar
|
||||
|
||||
shutdownCh chan bool
|
||||
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
// NewTerminalProgressBar creates a new default pb3 based progressbar suitable for
|
||||
// most terminals.
|
||||
func NewTerminalProgressBar() (ProgressBar, error) {
|
||||
b := &terminalProgressBar{
|
||||
out: osStderr(),
|
||||
}
|
||||
b.spinnerPb = pb.New(0)
|
||||
b.spinnerPb.SetTemplate(`[{{ (cycle . "|" "/" "-" "\\") }}] {{ string . "spinnerMsg" }}`)
|
||||
b.msgPb = pb.New(0)
|
||||
b.msgPb.SetTemplate(`Message: {{ string . "msg" }}`)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *terminalProgressBar) SetProgress(subLevel int, msg string, done int, total int) error {
|
||||
// auto-add as needed, requires sublevels to get added in order
|
||||
// i.e. adding 0 and then 2 will fail
|
||||
switch {
|
||||
case subLevel == len(b.subLevelPbs):
|
||||
apb := pb.New(0)
|
||||
progressBarTmpl := `[{{ counters . }}] {{ string . "prefix" }} {{ bar .}} {{ percent . }}`
|
||||
apb.SetTemplateString(progressBarTmpl)
|
||||
if err := apb.Err(); err != nil {
|
||||
return fmt.Errorf("error setting the progressbarTemplat: %w", err)
|
||||
}
|
||||
// workaround bug when running tests in tmt
|
||||
if apb.Width() == 0 {
|
||||
// this is pb.defaultBarWidth
|
||||
apb.SetWidth(100)
|
||||
}
|
||||
b.subLevelPbs = append(b.subLevelPbs, apb)
|
||||
case subLevel > len(b.subLevelPbs):
|
||||
return fmt.Errorf("sublevel added out of order, have %v sublevels but want level %v", len(b.subLevelPbs), subLevel)
|
||||
}
|
||||
apb := b.subLevelPbs[subLevel]
|
||||
apb.SetTotal(int64(total) + 1)
|
||||
apb.SetCurrent(int64(done) + 1)
|
||||
apb.Set("prefix", msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func shorten(msg string) string {
|
||||
msg = strings.Replace(msg, "\n", " ", -1)
|
||||
// XXX: make this smarter
|
||||
if len(msg) > 60 {
|
||||
return msg[:60] + "..."
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (b *terminalProgressBar) SetPulseMsgf(msg string, args ...interface{}) {
|
||||
b.spinnerPb.Set("spinnerMsg", shorten(fmt.Sprintf(msg, args...)))
|
||||
}
|
||||
|
||||
func (b *terminalProgressBar) SetMessagef(msg string, args ...interface{}) {
|
||||
b.msgPb.Set("msg", shorten(fmt.Sprintf(msg, args...)))
|
||||
}
|
||||
|
||||
func (b *terminalProgressBar) render() {
|
||||
var renderedLines int
|
||||
fmt.Fprintf(b.out, "%s%s\n", ERASE_LINE, b.spinnerPb.String())
|
||||
renderedLines++
|
||||
for _, prog := range b.subLevelPbs {
|
||||
fmt.Fprintf(b.out, "%s%s\n", ERASE_LINE, prog.String())
|
||||
renderedLines++
|
||||
}
|
||||
fmt.Fprintf(b.out, "%s%s\n", ERASE_LINE, b.msgPb.String())
|
||||
renderedLines++
|
||||
fmt.Fprint(b.out, cursorUp(renderedLines))
|
||||
}
|
||||
|
||||
// Workaround for the pb.Pool requiring "raw-mode" - see here how to avoid
|
||||
// it. Once fixes upstream we should remove this.
|
||||
func (b *terminalProgressBar) renderLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-b.shutdownCh:
|
||||
b.render()
|
||||
// finally move cursor down again
|
||||
fmt.Fprint(b.out, CURSOR_SHOW)
|
||||
fmt.Fprint(b.out, strings.Repeat("\n", 2+len(b.subLevelPbs)))
|
||||
// close last to avoid race with b.out
|
||||
close(b.shutdownCh)
|
||||
return
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
// break to redraw the screen
|
||||
}
|
||||
b.render()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *terminalProgressBar) Start() {
|
||||
// render() already running
|
||||
if b.shutdownCh != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(b.out, "%s", CURSOR_HIDE)
|
||||
b.shutdownCh = make(chan bool)
|
||||
go b.renderLoop()
|
||||
}
|
||||
|
||||
func (b *terminalProgressBar) Err() error {
|
||||
var errs []error
|
||||
if err := b.spinnerPb.Err(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("error on spinner progressbar: %w", err))
|
||||
}
|
||||
if err := b.msgPb.Err(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("error on spinner progressbar: %w", err))
|
||||
}
|
||||
for _, pb := range b.subLevelPbs {
|
||||
if err := pb.Err(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("error on spinner progressbar: %w", err))
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (b *terminalProgressBar) Stop() {
|
||||
if b.shutdownCh == nil {
|
||||
return
|
||||
}
|
||||
// request shutdown
|
||||
b.shutdownCh <- true
|
||||
// wait for ack
|
||||
select {
|
||||
case <-b.shutdownCh:
|
||||
// shudown complete
|
||||
case <-time.After(1 * time.Second):
|
||||
// I cannot think of how this could happen, i.e. why
|
||||
// closing would not work but lets be conservative -
|
||||
// without a timeout we hang here forever
|
||||
logrus.Warnf("no progress channel shutdown after 1sec")
|
||||
}
|
||||
b.shutdownCh = nil
|
||||
// This should never happen but be paranoid, this should
|
||||
// never happen but ensure we did not accumulate error while
|
||||
// running
|
||||
if err := b.Err(); err != nil {
|
||||
fmt.Fprintf(b.out, "error from pb.ProgressBar: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type verboseProgressBar struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
// NewVerboseProgressBar starts a new "verbose" progressbar that will just
|
||||
// prints message but does not show any progress.
|
||||
func NewVerboseProgressBar() (ProgressBar, error) {
|
||||
b := &verboseProgressBar{w: osStderr()}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *verboseProgressBar) SetPulseMsgf(msg string, args ...interface{}) {
|
||||
fmt.Fprintf(b.w, msg, args...)
|
||||
fmt.Fprintf(b.w, "\n")
|
||||
}
|
||||
|
||||
func (b *verboseProgressBar) SetMessagef(msg string, args ...interface{}) {
|
||||
fmt.Fprintf(b.w, msg, args...)
|
||||
fmt.Fprintf(b.w, "\n")
|
||||
}
|
||||
|
||||
func (b *verboseProgressBar) Start() {
|
||||
}
|
||||
|
||||
func (b *verboseProgressBar) Stop() {
|
||||
}
|
||||
|
||||
func (b *verboseProgressBar) SetProgress(subLevel int, msg string, done int, total int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type debugProgressBar struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
// NewDebugProgressBar will create a progressbar aimed to debug the
|
||||
// lower level osbuild/images message. It will never clear the screen
|
||||
// so "glitches/weird" messages from the lower-layers can be inspected
|
||||
// easier.
|
||||
func NewDebugProgressBar() (ProgressBar, error) {
|
||||
b := &debugProgressBar{w: osStderr()}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *debugProgressBar) SetPulseMsgf(msg string, args ...interface{}) {
|
||||
fmt.Fprintf(b.w, "pulse: ")
|
||||
fmt.Fprintf(b.w, msg, args...)
|
||||
fmt.Fprintf(b.w, "\n")
|
||||
}
|
||||
|
||||
func (b *debugProgressBar) SetMessagef(msg string, args ...interface{}) {
|
||||
fmt.Fprintf(b.w, "msg: ")
|
||||
fmt.Fprintf(b.w, msg, args...)
|
||||
fmt.Fprintf(b.w, "\n")
|
||||
}
|
||||
|
||||
func (b *debugProgressBar) Start() {
|
||||
fmt.Fprintf(b.w, "Start progressbar\n")
|
||||
}
|
||||
|
||||
func (b *debugProgressBar) Stop() {
|
||||
fmt.Fprintf(b.w, "Stop progressbar\n")
|
||||
}
|
||||
|
||||
func (b *debugProgressBar) SetProgress(subLevel int, msg string, done int, total int) error {
|
||||
fmt.Fprintf(b.w, "%s[%v / %v] %s", strings.Repeat(" ", subLevel), done, total, msg)
|
||||
fmt.Fprintf(b.w, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
type OSBuildOptions struct {
|
||||
StoreDir string
|
||||
OutputDir string
|
||||
ExtraEnv []string
|
||||
|
||||
// BuildLog writes the osbuild output to the given writer
|
||||
BuildLog io.Writer
|
||||
}
|
||||
|
||||
// XXX: merge variant back into images/pkg/osbuild/osbuild-exec.go
|
||||
func RunOSBuild(pb ProgressBar, manifest []byte, exports []string, opts *OSBuildOptions) error {
|
||||
if opts == nil {
|
||||
opts = &OSBuildOptions{}
|
||||
}
|
||||
|
||||
// To keep maximum compatibility keep the old behavior to run osbuild
|
||||
// directly and show all messages unless we have a "real" progress bar.
|
||||
//
|
||||
// This should ensure that e.g. "podman bootc" keeps working as it
|
||||
// is currently expecting the raw osbuild output. Once we double
|
||||
// checked with them we can remove the runOSBuildNoProgress() and
|
||||
// just run with the new runOSBuildWithProgress() helper.
|
||||
switch pb.(type) {
|
||||
case *terminalProgressBar, *debugProgressBar:
|
||||
return runOSBuildWithProgress(pb, manifest, exports, opts)
|
||||
default:
|
||||
return runOSBuildNoProgress(pb, manifest, exports, opts)
|
||||
}
|
||||
}
|
||||
|
||||
func runOSBuildNoProgress(pb ProgressBar, manifest []byte, exports []string, opts *OSBuildOptions) error {
|
||||
var stdout, stderr io.Writer
|
||||
|
||||
var writeMu sync.Mutex
|
||||
if opts.BuildLog == nil {
|
||||
// No external build log requested and we won't need an
|
||||
// internal one because all output goes directly to
|
||||
// stdout/stderr. This is for maximum compatibility with
|
||||
// the existing bootc-image-builder in "verbose" mode
|
||||
// where stdout, stderr come directly from osbuild.
|
||||
stdout = osStdout()
|
||||
stderr = osStderr()
|
||||
} else {
|
||||
// There is a slight wrinkle here: when requesting a
|
||||
// buildlog we can no longer write to separate
|
||||
// stdout/stderr streams without being racy and give
|
||||
// potential out-of-order output (which is very bad
|
||||
// and confusing in a log). The reason is that if
|
||||
// cmd.Std{out,err} are different "go" will start two
|
||||
// go-routine to monitor/copy those are racy when both
|
||||
// stdout,stderr output happens close together
|
||||
// (TestRunOSBuildWithBuildlog demos that). We cannot
|
||||
// have our cake and eat it so here we need to combine
|
||||
// osbuilds stderr into our stdout.
|
||||
mw := newSyncedWriter(&writeMu, io.MultiWriter(osStdout(), opts.BuildLog))
|
||||
stdout = mw
|
||||
stderr = mw
|
||||
}
|
||||
|
||||
cmd := exec.Command(
|
||||
osbuildCmd,
|
||||
"--store", opts.StoreDir,
|
||||
"--output-directory", opts.OutputDir,
|
||||
"-",
|
||||
)
|
||||
for _, export := range exports {
|
||||
cmd.Args = append(cmd.Args, "--export", export)
|
||||
}
|
||||
|
||||
cmd.Env = append(os.Environ(), opts.ExtraEnv...)
|
||||
cmd.Stdin = bytes.NewBuffer(manifest)
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("error running osbuild: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var osbuildCmd = "osbuild"
|
||||
|
||||
func runOSBuildWithProgress(pb ProgressBar, manifest []byte, exports []string, opts *OSBuildOptions) (err error) {
|
||||
rp, wp, err := os.Pipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create pipe for osbuild: %w", err)
|
||||
}
|
||||
defer rp.Close()
|
||||
defer wp.Close()
|
||||
|
||||
cmd := exec.Command(
|
||||
osbuildCmd,
|
||||
"--store", opts.StoreDir,
|
||||
"--output-directory", opts.OutputDir,
|
||||
"--monitor=JSONSeqMonitor",
|
||||
"--monitor-fd=3",
|
||||
"-",
|
||||
)
|
||||
for _, export := range exports {
|
||||
cmd.Args = append(cmd.Args, "--export", export)
|
||||
}
|
||||
|
||||
var stdio bytes.Buffer
|
||||
var mw, buildLog io.Writer
|
||||
var writeMu sync.Mutex
|
||||
if opts.BuildLog != nil {
|
||||
mw = newSyncedWriter(&writeMu, io.MultiWriter(&stdio, opts.BuildLog))
|
||||
buildLog = newSyncedWriter(&writeMu, opts.BuildLog)
|
||||
} else {
|
||||
mw = &stdio
|
||||
buildLog = io.Discard
|
||||
}
|
||||
|
||||
cmd.Env = append(os.Environ(), opts.ExtraEnv...)
|
||||
cmd.Stdin = bytes.NewBuffer(manifest)
|
||||
cmd.Stdout = mw
|
||||
cmd.Stderr = mw
|
||||
cmd.ExtraFiles = []*os.File{wp}
|
||||
|
||||
osbuildStatus := osbuild.NewStatusScanner(rp)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("error starting osbuild: %v", err)
|
||||
}
|
||||
wp.Close()
|
||||
defer func() {
|
||||
// Try to stop osbuild if we exit early, we are gentle
|
||||
// here to give osbuild the chance to release its
|
||||
// resources (like mounts in the buildroot). This is
|
||||
// best effort only (but also a pretty uncommon error
|
||||
// condition). If ProcessState is set the process has
|
||||
// already exited and we have nothing to do.
|
||||
if err != nil && cmd.Process != nil && cmd.ProcessState == nil {
|
||||
sigErr := cmd.Process.Signal(syscall.SIGINT)
|
||||
err = errors.Join(err, sigErr)
|
||||
}
|
||||
}()
|
||||
|
||||
var tracesMsgs []string
|
||||
for {
|
||||
st, err := osbuildStatus.Status()
|
||||
if err != nil {
|
||||
// This should never happen but if it does we try
|
||||
// to be helpful. We need to exit here (and kill
|
||||
// osbuild in the defer) or we would appear to be
|
||||
// handing as cmd.Wait() would wait to finish but
|
||||
// no progress or other message is reported. We
|
||||
// can also not (in the general case) recover as
|
||||
// the underlying osbuildStatus.scanner maybe in
|
||||
// an unrecoverable state (like ErrTooBig).
|
||||
return fmt.Errorf(`error parsing osbuild status, please report a bug and try with "--progress=verbose": %w`, err)
|
||||
}
|
||||
if st == nil {
|
||||
break
|
||||
}
|
||||
i := 0
|
||||
for p := st.Progress; p != nil; p = p.SubProgress {
|
||||
if err := pb.SetProgress(i, p.Message, p.Done, p.Total); err != nil {
|
||||
logrus.Warnf("cannot set progress: %v", err)
|
||||
}
|
||||
i++
|
||||
}
|
||||
// forward to user
|
||||
if st.Message != "" {
|
||||
pb.SetMessagef(st.Message)
|
||||
}
|
||||
|
||||
// keep internal log for error reporting, forward to
|
||||
// external build log
|
||||
if st.Message != "" {
|
||||
tracesMsgs = append(tracesMsgs, st.Message)
|
||||
fmt.Fprintln(buildLog, st.Message)
|
||||
}
|
||||
if st.Trace != "" {
|
||||
tracesMsgs = append(tracesMsgs, st.Trace)
|
||||
fmt.Fprintln(buildLog, st.Trace)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
return fmt.Errorf("error running osbuild: %w\nBuildLog:\n%s\nOutput:\n%s", err, strings.Join(tracesMsgs, "\n"), stdio.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
265
pkg/progress/progress_test.go
Normal file
265
pkg/progress/progress_test.go
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
package progress_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/osbuild/image-builder-cli/pkg/progress"
|
||||
)
|
||||
|
||||
func TestProgressNew(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
typ string
|
||||
expected interface{}
|
||||
expectedErr string
|
||||
}{
|
||||
{"term", &progress.TerminalProgressBar{}, ""},
|
||||
{"debug", &progress.DebugProgressBar{}, ""},
|
||||
{"verbose", &progress.VerboseProgressBar{}, ""},
|
||||
// unknown progress type
|
||||
{"bad", nil, `unknown progress type: "bad"`},
|
||||
} {
|
||||
pb, err := progress.New(tc.typ)
|
||||
if tc.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(pb), reflect.TypeOf(tc.expected), fmt.Sprintf("[%v] %T not the expected %T", tc.typ, pb, tc.expected))
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerboseProgress(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
restore := progress.MockOsStderr(&buf)
|
||||
defer restore()
|
||||
|
||||
// verbose progress never generates progress output
|
||||
pbar, err := progress.NewVerboseProgressBar()
|
||||
assert.NoError(t, err)
|
||||
err = pbar.SetProgress(0, "set-progress", 1, 100)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", buf.String())
|
||||
|
||||
// but it shows the messages
|
||||
pbar.SetPulseMsgf("pulse")
|
||||
assert.Equal(t, "pulse\n", buf.String())
|
||||
buf.Reset()
|
||||
|
||||
pbar.SetMessagef("message")
|
||||
assert.Equal(t, "message\n", buf.String())
|
||||
buf.Reset()
|
||||
|
||||
pbar.Start()
|
||||
assert.Equal(t, "", buf.String())
|
||||
pbar.Stop()
|
||||
assert.Equal(t, "", buf.String())
|
||||
}
|
||||
|
||||
func TestDebugProgress(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
restore := progress.MockOsStderr(&buf)
|
||||
defer restore()
|
||||
|
||||
pbar, err := progress.NewDebugProgressBar()
|
||||
assert.NoError(t, err)
|
||||
err = pbar.SetProgress(0, "set-progress-msg", 1, 100)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[1 / 100] set-progress-msg\n", buf.String())
|
||||
buf.Reset()
|
||||
|
||||
pbar.SetPulseMsgf("pulse-msg")
|
||||
assert.Equal(t, "pulse: pulse-msg\n", buf.String())
|
||||
buf.Reset()
|
||||
|
||||
pbar.SetMessagef("some-message")
|
||||
assert.Equal(t, "msg: some-message\n", buf.String())
|
||||
buf.Reset()
|
||||
|
||||
pbar.Start()
|
||||
assert.Equal(t, "Start progressbar\n", buf.String())
|
||||
buf.Reset()
|
||||
|
||||
pbar.Stop()
|
||||
assert.Equal(t, "Stop progressbar\n", buf.String())
|
||||
buf.Reset()
|
||||
}
|
||||
|
||||
func TestTermProgress(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
restore := progress.MockOsStderr(&buf)
|
||||
defer restore()
|
||||
|
||||
pbar, err := progress.NewTerminalProgressBar()
|
||||
assert.NoError(t, err)
|
||||
|
||||
pbar.Start()
|
||||
pbar.SetPulseMsgf("pulse-msg")
|
||||
pbar.SetMessagef("some-message")
|
||||
err = pbar.SetProgress(0, "set-progress-msg", 0, 5)
|
||||
assert.NoError(t, err)
|
||||
pbar.Stop()
|
||||
assert.NoError(t, pbar.(*progress.TerminalProgressBar).Err())
|
||||
|
||||
assert.Contains(t, buf.String(), "[1 / 6] set-progress-msg")
|
||||
assert.Contains(t, buf.String(), "[|] pulse-msg\n")
|
||||
assert.Contains(t, buf.String(), "Message: some-message\n")
|
||||
// check shutdown
|
||||
assert.Contains(t, buf.String(), progress.CURSOR_SHOW)
|
||||
}
|
||||
|
||||
func TestProgressNewAutoselect(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
onTerm bool
|
||||
expected interface{}
|
||||
}{
|
||||
{false, &progress.VerboseProgressBar{}},
|
||||
{true, &progress.TerminalProgressBar{}},
|
||||
} {
|
||||
restore := progress.MockIsattyIsTerminal(func(uintptr) bool {
|
||||
return tc.onTerm
|
||||
})
|
||||
defer restore()
|
||||
|
||||
pb, err := progress.New("auto")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(pb), reflect.TypeOf(tc.expected), fmt.Sprintf("[%v] %T not the expected %T", tc.onTerm, pb, tc.expected))
|
||||
}
|
||||
}
|
||||
|
||||
func makeFakeOsbuild(t *testing.T, content string) string {
|
||||
p := filepath.Join(t.TempDir(), "fake-osbuild")
|
||||
err := os.WriteFile(p, []byte("#!/bin/sh\n"+content), 0755)
|
||||
assert.NoError(t, err)
|
||||
return p
|
||||
}
|
||||
|
||||
func TestRunOSBuildWithProgressErrorReporting(t *testing.T) {
|
||||
restore := progress.MockOsStderr(io.Discard)
|
||||
defer restore()
|
||||
|
||||
restore = progress.MockOsbuildCmd(makeFakeOsbuild(t, `
|
||||
>&3 echo '{"message": "osbuild-stage-message"}'
|
||||
|
||||
echo osbuild-stdout-output
|
||||
>&2 echo osbuild-stderr-output
|
||||
exit 112
|
||||
`))
|
||||
defer restore()
|
||||
|
||||
pbar, err := progress.New("debug")
|
||||
assert.NoError(t, err)
|
||||
err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, nil)
|
||||
assert.EqualError(t, err, `error running osbuild: exit status 112
|
||||
BuildLog:
|
||||
osbuild-stage-message
|
||||
Output:
|
||||
osbuild-stdout-output
|
||||
osbuild-stderr-output
|
||||
`)
|
||||
}
|
||||
|
||||
func TestRunOSBuildWithProgressIncorrectJSON(t *testing.T) {
|
||||
signalDeliveredMarkerPath := filepath.Join(t.TempDir(), "sigint-delivered")
|
||||
|
||||
restore := progress.MockOsbuildCmd(makeFakeOsbuild(t, fmt.Sprintf(`
|
||||
trap 'touch "%s";exit 2' INT
|
||||
|
||||
>&3 echo invalid-json
|
||||
|
||||
# we cannot sleep infinity here or the shell script trap is never run
|
||||
while true; do
|
||||
sleep 0.1
|
||||
done
|
||||
`, signalDeliveredMarkerPath)))
|
||||
defer restore()
|
||||
|
||||
pbar, err := progress.New("debug")
|
||||
assert.NoError(t, err)
|
||||
err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, nil)
|
||||
assert.EqualError(t, err, `error parsing osbuild status, please report a bug and try with "--progress=verbose": cannot scan line "invalid-json": invalid character 'i' looking for beginning of value`)
|
||||
|
||||
// ensure the SIGINT got delivered
|
||||
var pathExists = func(p string) bool {
|
||||
_, err := os.Stat(p)
|
||||
return err == nil
|
||||
}
|
||||
for i := 0; i < 20; i++ {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if pathExists(signalDeliveredMarkerPath) {
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, pathExists(signalDeliveredMarkerPath))
|
||||
}
|
||||
|
||||
func TestRunOSBuildWithBuildlogTerm(t *testing.T) {
|
||||
restore := progress.MockOsbuildCmd(makeFakeOsbuild(t, `
|
||||
echo osbuild-stdout-output
|
||||
>&2 echo osbuild-stderr-output
|
||||
|
||||
# without the sleep this is racy as two different go routines poll
|
||||
# this does not matter (much) in practise because osbuild output and
|
||||
# stage output are using the syncedMultiWriter so output is not garbled
|
||||
sleep 0.1
|
||||
>&3 echo '{"message": "osbuild-stage-message"}'
|
||||
`))
|
||||
defer restore()
|
||||
|
||||
var fakeStdout, fakeStderr bytes.Buffer
|
||||
restore = progress.MockOsStdout(&fakeStdout)
|
||||
defer restore()
|
||||
restore = progress.MockOsStderr(&fakeStderr)
|
||||
defer restore()
|
||||
|
||||
pbar, err := progress.New("term")
|
||||
assert.NoError(t, err)
|
||||
|
||||
var buildLog bytes.Buffer
|
||||
opts := &progress.OSBuildOptions{
|
||||
BuildLog: &buildLog,
|
||||
}
|
||||
err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, opts)
|
||||
assert.NoError(t, err)
|
||||
expectedOutput := `osbuild-stdout-output
|
||||
osbuild-stderr-output
|
||||
osbuild-stage-message
|
||||
`
|
||||
assert.Equal(t, expectedOutput, buildLog.String())
|
||||
}
|
||||
|
||||
func TestRunOSBuildWithBuildlogVerbose(t *testing.T) {
|
||||
restore := progress.MockOsbuildCmd(makeFakeOsbuild(t, `
|
||||
echo osbuild-stdout-output
|
||||
>&2 echo osbuild-stderr-output
|
||||
`))
|
||||
defer restore()
|
||||
|
||||
var fakeStdout, fakeStderr bytes.Buffer
|
||||
restore = progress.MockOsStdout(&fakeStdout)
|
||||
defer restore()
|
||||
restore = progress.MockOsStderr(&fakeStderr)
|
||||
defer restore()
|
||||
|
||||
pbar, err := progress.New("verbose")
|
||||
assert.NoError(t, err)
|
||||
|
||||
var buildLog bytes.Buffer
|
||||
opts := &progress.OSBuildOptions{
|
||||
BuildLog: &buildLog,
|
||||
}
|
||||
err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, opts)
|
||||
assert.NoError(t, err)
|
||||
expectedOutput := `osbuild-stdout-output
|
||||
osbuild-stderr-output
|
||||
`
|
||||
assert.Equal(t, expectedOutput, buildLog.String())
|
||||
}
|
||||
22
pkg/progress/syncwriter.go
Normal file
22
pkg/progress/syncwriter.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package progress
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type syncedWriter struct {
|
||||
mu *sync.Mutex
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func newSyncedWriter(mu *sync.Mutex, w io.Writer) io.Writer {
|
||||
return &syncedWriter{mu: mu, w: w}
|
||||
}
|
||||
|
||||
func (sw *syncedWriter) Write(p []byte) (n int, err error) {
|
||||
sw.mu.Lock()
|
||||
defer sw.mu.Unlock()
|
||||
|
||||
return sw.w.Write(p)
|
||||
}
|
||||
44
pkg/progress/syncwriter_test.go
Normal file
44
pkg/progress/syncwriter_test.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package progress_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/osbuild/image-builder-cli/pkg/progress"
|
||||
)
|
||||
|
||||
func TestSyncWriter(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
var buf bytes.Buffer
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for id := 0; id < 100; id++ {
|
||||
wg.Add(1)
|
||||
w := progress.NewSyncedWriter(&mu, &buf)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 500; i++ {
|
||||
fmt.Fprintln(w, strings.Repeat(fmt.Sprintf("%v", id%10), 60))
|
||||
time.Sleep(10 * time.Nanosecond)
|
||||
}
|
||||
}(id)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
scanner := bufio.NewScanner(&buf)
|
||||
for {
|
||||
if !scanner.Scan() {
|
||||
break
|
||||
}
|
||||
line := scanner.Text()
|
||||
assert.True(t, len(line) == 60, fmt.Sprintf("len %v: line: %v", len(line), line))
|
||||
}
|
||||
assert.NoError(t, scanner.Err())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue