From 5a207ef92d81b671c9d0f30ddb245434cb49a2f8 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 31 May 2024 14:10:56 -0500 Subject: [PATCH] Command err handling (#480) * ignore .vscode * Improved error handling in command execution - Modified function signatures to return errors - Added error checks after command executions - Added specific error messages for different failures - Updated the handling of parser errors for source tape parsing - Altered evaluator to incorporate the revised error handling * Move waiting for "window.term" in VHS evaluator - Moved the logic for waiting until the "window.term" variable is accessible from VHS setup to evaluation phase - This ensures the proper timing and handling of errors related to the 'term' variable access (some SET commands access term) * ignore unused param --- .gitignore | 1 + command.go | 384 +++++++++++++++++++++++++++++++++++++-------------- evaluator.go | 31 ++++- testing.go | 27 +++- vhs.go | 3 - 5 files changed, 325 insertions(+), 121 deletions(-) diff --git a/.gitignore b/.gitignore index a76921bd4..cd18f3a65 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ completions dist vhs .idea/ +.vscode/ diff --git a/command.go b/command.go index 79832cc9f..4697eb1a1 100644 --- a/command.go +++ b/command.go @@ -19,21 +19,32 @@ import ( ) // Execute executes a command on a running instance of vhs. -func Execute(c parser.Command, v *VHS) { +func Execute(c parser.Command, v *VHS) error { if c.Type == token.SOURCE { - ExecuteSourceTape(c, v) + err := ExecuteSourceTape(c, v) + if err != nil { + return fmt.Errorf("failed to execute source tape: %w", err) + } } else { - CommandFuncs[c.Type](c, v) + err := CommandFuncs[c.Type](c, v) + if err != nil { + return fmt.Errorf("failed to execute command: %w", err) + } } if v.recording && v.Options.Test.Output != "" { - v.SaveOutput() + err := v.SaveOutput() + if err != nil { + return fmt.Errorf("failed to save output: %w", err) + } } + + return nil } // CommandFunc is a function that executes a command on a running // instance of vhs. -type CommandFunc func(c parser.Command, v *VHS) +type CommandFunc func(c parser.Command, v *VHS) error // CommandFuncs maps command types to their executable functions. var CommandFuncs = map[parser.CommandType]CommandFunc{ @@ -70,7 +81,7 @@ var CommandFuncs = map[parser.CommandType]CommandFunc{ // ExecuteNoop is a no-op command that does nothing. // Generally, this is used for Unknown commands when dealing with // commands that are not recognized. -func ExecuteNoop(_ parser.Command, _ *VHS) {} +func ExecuteNoop(_ parser.Command, _ *VHS) error { return nil } // ExecuteKey is a higher-order function that returns a CommandFunc to execute // a key press for a given key. This is so that the logic for key pressing @@ -79,7 +90,7 @@ func ExecuteNoop(_ parser.Command, _ *VHS) {} // i.e. ExecuteKey(input.ArrowDown) would return a CommandFunc that executes // the ArrowDown key press. func ExecuteKey(k input.Key) CommandFunc { - return func(c parser.Command, v *VHS) { + return func(c parser.Command, v *VHS) error { typingSpeed, err := time.ParseDuration(c.Options) if err != nil { typingSpeed = v.Options.TypingSpeed @@ -89,15 +100,20 @@ func ExecuteKey(k input.Key) CommandFunc { repeat = 1 } for i := 0; i < repeat; i++ { - _ = v.Page.Keyboard.Type(k) + err = v.Page.Keyboard.Type(k) + if err != nil { + return fmt.Errorf("failed to type key %c: %w", k, err) + } time.Sleep(typingSpeed) } + + return nil } } // ExecuteCtrl is a CommandFunc that presses the argument keys and/or modifiers // with the ctrl key held down on the running instance of vhs. -func ExecuteCtrl(c parser.Command, v *VHS) { +func ExecuteCtrl(c parser.Command, v *VHS) error { // Create key combination by holding ControlLeft action := v.Page.KeyActions().Press(input.ControlLeft) keys := strings.Split(c.Args, " ") @@ -134,102 +150,156 @@ func ExecuteCtrl(c parser.Command, v *VHS) { } } - action.MustDo() + err := action.Do() + if err != nil { + return fmt.Errorf("failed to type key %s: %w", c.Args, err) + } + + return nil } // ExecuteAlt is a CommandFunc that presses the argument key with the alt key // held down on the running instance of vhs. -func ExecuteAlt(c parser.Command, v *VHS) { - _ = v.Page.Keyboard.Press(input.AltLeft) +func ExecuteAlt(c parser.Command, v *VHS) error { + err := v.Page.Keyboard.Press(input.AltLeft) + if err != nil { + return fmt.Errorf("failed to press Alt key: %w", err) + } if k, ok := token.Keywords[c.Args]; ok { switch k { case token.ENTER: - _ = v.Page.Keyboard.Type(input.Enter) + err = v.Page.Keyboard.Type(input.Enter) + if err != nil { + return fmt.Errorf("failed to type Enter key: %w", err) + } case token.TAB: - _ = v.Page.Keyboard.Type(input.Tab) + err := v.Page.Keyboard.Type(input.Tab) + if err != nil { + return fmt.Errorf("failed to type Tab key: %w", err) + } } } else { for _, r := range c.Args { if k, ok := keymap[r]; ok { - _ = v.Page.Keyboard.Type(k) + err = v.Page.Keyboard.Type(k) + if err != nil { + return fmt.Errorf("failed to type key %c: %w", r, err) + } } } } - _ = v.Page.Keyboard.Release(input.AltLeft) + err = v.Page.Keyboard.Release(input.AltLeft) + if err != nil { + return fmt.Errorf("failed to release Alt key: %w", err) + } + + return nil } // ExecuteShift is a CommandFunc that presses the argument key with the shift // key held down on the running instance of vhs. -func ExecuteShift(c parser.Command, v *VHS) { - _ = v.Page.Keyboard.Press(input.ShiftLeft) +func ExecuteShift(c parser.Command, v *VHS) error { + err := v.Page.Keyboard.Press(input.ShiftLeft) + if err != nil { + return fmt.Errorf("failed to press Shift key: %w", err) + } + if k, ok := token.Keywords[c.Args]; ok { switch k { case token.ENTER: - _ = v.Page.Keyboard.Type(input.Enter) + err = v.Page.Keyboard.Type(input.Enter) + if err != nil { + return fmt.Errorf("failed to type Enter key: %w", err) + } case token.TAB: - _ = v.Page.Keyboard.Type(input.Tab) + err = v.Page.Keyboard.Type(input.Tab) + if err != nil { + return fmt.Errorf("failed to type Tab key: %w", err) + } } } else { for _, r := range c.Args { if k, ok := keymap[r]; ok { - _ = v.Page.Keyboard.Type(k) + err = v.Page.Keyboard.Type(k) + if err != nil { + return fmt.Errorf("failed to type key %c: %w", r, err) + } } } } - _ = v.Page.Keyboard.Release(input.ShiftLeft) + err = v.Page.Keyboard.Release(input.ShiftLeft) + if err != nil { + return fmt.Errorf("failed to release Shift key: %w", err) + } + + return nil } // ExecuteHide is a CommandFunc that starts or stops the recording of the vhs. -func ExecuteHide(_ parser.Command, v *VHS) { +func ExecuteHide(_ parser.Command, v *VHS) error { v.PauseRecording() + return nil } // ExecuteRequire is a CommandFunc that checks if all the binaries mentioned in the // Require command are present. If not, it exits with a non-zero error. -func ExecuteRequire(c parser.Command, v *VHS) { +func ExecuteRequire(c parser.Command, _ *VHS) error { _, err := exec.LookPath(c.Args) - if err != nil { - v.Errors = append(v.Errors, err) - } + return err } // ExecuteShow is a CommandFunc that resumes the recording of the vhs. -func ExecuteShow(_ parser.Command, v *VHS) { +func ExecuteShow(_ parser.Command, v *VHS) error { v.ResumeRecording() + return nil } // ExecuteSleep sleeps for the desired time specified through the argument of // the Sleep command. -func ExecuteSleep(c parser.Command, _ *VHS) { +func ExecuteSleep(c parser.Command, _ *VHS) error { dur, err := time.ParseDuration(c.Args) if err != nil { - return + return fmt.Errorf("failed to parse duration: %w", err) } time.Sleep(dur) + return nil } // ExecuteType types the argument string on the running instance of vhs. -func ExecuteType(c parser.Command, v *VHS) { - typingSpeed, err := time.ParseDuration(c.Options) - if err != nil { - typingSpeed = v.Options.TypingSpeed +func ExecuteType(c parser.Command, v *VHS) error { + typingSpeed := v.Options.TypingSpeed + if c.Options != "" { + var err error + typingSpeed, err = time.ParseDuration(c.Options) + if err != nil { + return fmt.Errorf("failed to parse typing speed: %w", err) + } } for _, r := range c.Args { k, ok := keymap[r] if ok { - _ = v.Page.Keyboard.Type(k) + err := v.Page.Keyboard.Type(k) + if err != nil { + return fmt.Errorf("failed to type key %c: %w", r, err) + } } else { - _ = v.Page.MustElement("textarea").Input(string(r)) + err := v.Page.MustElement("textarea").Input(string(r)) + if err != nil { + return fmt.Errorf("failed to input text: %w", err) + } + v.Page.MustWaitIdle() } time.Sleep(typingSpeed) } + + return nil } // ExecuteOutput applies the output on the vhs videos. -func ExecuteOutput(c parser.Command, v *VHS) { +func ExecuteOutput(c parser.Command, v *VHS) error { switch c.Options { case ".mp4": v.Options.Video.Output.MP4 = c.Args @@ -242,33 +312,43 @@ func ExecuteOutput(c parser.Command, v *VHS) { default: v.Options.Video.Output.GIF = c.Args } + + return nil } // ExecuteCopy copies text to the clipboard. -func ExecuteCopy(c parser.Command, _ *VHS) { - _ = clipboard.WriteAll(c.Args) +func ExecuteCopy(c parser.Command, _ *VHS) error { + return clipboard.WriteAll(c.Args) } // ExecuteEnv sets env with given key-value pair. -func ExecuteEnv(c parser.Command, _ *VHS) { - _ = os.Setenv(c.Options, c.Args) +func ExecuteEnv(c parser.Command, _ *VHS) error { + return os.Setenv(c.Options, c.Args) } // ExecutePaste pastes text from the clipboard. -func ExecutePaste(_ parser.Command, v *VHS) { +func ExecutePaste(_ parser.Command, v *VHS) error { clip, err := clipboard.ReadAll() if err != nil { - return + return fmt.Errorf("failed to read clipboard: %w", err) } for _, r := range clip { k, ok := keymap[r] if ok { - _ = v.Page.Keyboard.Type(k) + err = v.Page.Keyboard.Type(k) + if err != nil { + return fmt.Errorf("failed to type key %c: %w", r, err) + } } else { - _ = v.Page.MustElement("textarea").Input(string(r)) + err = v.Page.MustElement("textarea").Input(string(r)) + if err != nil { + return fmt.Errorf("failed to input text: %w", err) + } v.Page.MustWaitIdle() } } + + return nil } // Settings maps the Set commands to their respective functions. @@ -296,44 +376,76 @@ var Settings = map[string]CommandFunc{ // ExecuteSet applies the settings on the running vhs specified by the // option and argument pass to the command. -func ExecuteSet(c parser.Command, v *VHS) { - Settings[c.Options](c, v) +func ExecuteSet(c parser.Command, v *VHS) error { + return Settings[c.Options](c, v) } // ExecuteSetFontSize applies the font size on the vhs. -func ExecuteSetFontSize(c parser.Command, v *VHS) { - fontSize, _ := strconv.Atoi(c.Args) +func ExecuteSetFontSize(c parser.Command, v *VHS) error { + fontSize, err := strconv.Atoi(c.Args) + if err != nil { + return fmt.Errorf("failed to parse font size: %w", err) + } v.Options.FontSize = fontSize - _, _ = v.Page.Eval(fmt.Sprintf("() => term.options.fontSize = %d", fontSize)) + _, err = v.Page.Eval(fmt.Sprintf("() => term.options.fontSize = %d", fontSize)) + if err != nil { + return fmt.Errorf("failed to set font size: %w", err) + } // When changing the font size only the canvas dimensions change which are // scaled back during the render to fit the aspect ration and dimensions. // // We need to call term.fit to ensure that everything is resized properly. - _, _ = v.Page.Eval("term.fit") + _, err = v.Page.Eval("term.fit") + if err != nil { + return fmt.Errorf("failed to fit terminal: %w", err) + } + + return nil } // ExecuteSetFontFamily applies the font family on the vhs. -func ExecuteSetFontFamily(c parser.Command, v *VHS) { +func ExecuteSetFontFamily(c parser.Command, v *VHS) error { v.Options.FontFamily = c.Args - _, _ = v.Page.Eval(fmt.Sprintf("() => term.options.fontFamily = '%s'", withSymbolsFallback(c.Args))) + _, err := v.Page.Eval(fmt.Sprintf("() => term.options.fontFamily = '%s'", withSymbolsFallback(c.Args))) + if err != nil { + return fmt.Errorf("failed to set font family: %w", err) + } + + return nil } // ExecuteSetHeight applies the height on the vhs. -func ExecuteSetHeight(c parser.Command, v *VHS) { - v.Options.Video.Style.Height, _ = strconv.Atoi(c.Args) +func ExecuteSetHeight(c parser.Command, v *VHS) error { + height, err := strconv.Atoi(c.Args) + if err != nil { + return fmt.Errorf("failed to parse height: %w", err) + } + v.Options.Video.Style.Height = height + + return nil } // ExecuteSetWidth applies the width on the vhs. -func ExecuteSetWidth(c parser.Command, v *VHS) { - v.Options.Video.Style.Width, _ = strconv.Atoi(c.Args) +func ExecuteSetWidth(c parser.Command, v *VHS) error { + width, err := strconv.Atoi(c.Args) + if err != nil { + return fmt.Errorf("failed to parse width: %w", err) + } + v.Options.Video.Style.Width = width + + return nil } // ExecuteSetShell applies the shell on the vhs. -func ExecuteSetShell(c parser.Command, v *VHS) { - if s, ok := Shells[c.Args]; ok { - v.Options.Shell = s +func ExecuteSetShell(c parser.Command, v *VHS) error { + s, ok := Shells[c.Args] + if !ok { + return fmt.Errorf("invalid shell %s", c.Args) } + + v.Options.Shell = s + return nil } const ( @@ -343,113 +455,176 @@ const ( // ExecuteSetLetterSpacing applies letter spacing (also known as tracking) on // the vhs. -func ExecuteSetLetterSpacing(c parser.Command, v *VHS) { - letterSpacing, _ := strconv.ParseFloat(c.Args, bitSize) +func ExecuteSetLetterSpacing(c parser.Command, v *VHS) error { + letterSpacing, err := strconv.ParseFloat(c.Args, bitSize) + if err != nil { + return fmt.Errorf("failed to parse letter spacing: %w", err) + } + v.Options.LetterSpacing = letterSpacing - _, _ = v.Page.Eval(fmt.Sprintf("() => term.options.letterSpacing = %f", letterSpacing)) + _, err = v.Page.Eval(fmt.Sprintf("() => term.options.letterSpacing = %f", letterSpacing)) + if err != nil { + return fmt.Errorf("failed to set letter spacing: %w", err) + } + + return nil } // ExecuteSetLineHeight applies the line height on the vhs. -func ExecuteSetLineHeight(c parser.Command, v *VHS) { - lineHeight, _ := strconv.ParseFloat(c.Args, bitSize) +func ExecuteSetLineHeight(c parser.Command, v *VHS) error { + lineHeight, err := strconv.ParseFloat(c.Args, bitSize) + if err != nil { + return fmt.Errorf("failed to parse line height: %w", err) + } + v.Options.LineHeight = lineHeight - _, _ = v.Page.Eval(fmt.Sprintf("() => term.options.lineHeight = %f", lineHeight)) + _, err = v.Page.Eval(fmt.Sprintf("() => term.options.lineHeight = %f", lineHeight)) + if err != nil { + return fmt.Errorf("failed to set line height: %w", err) + } + + return nil } // ExecuteSetTheme applies the theme on the vhs. -func ExecuteSetTheme(c parser.Command, v *VHS) { +func ExecuteSetTheme(c parser.Command, v *VHS) error { var err error v.Options.Theme, err = getTheme(c.Args) if err != nil { - v.Errors = append(v.Errors, err) - return + return err + } + + bts, err := json.Marshal(v.Options.Theme) + if err != nil { + return fmt.Errorf("failed to marshal theme: %w", err) + } + + _, err = v.Page.Eval(fmt.Sprintf("() => term.options.theme = %s", string(bts))) + if err != nil { + return fmt.Errorf("failed to set theme: %w", err) } - bts, _ := json.Marshal(v.Options.Theme) - _, _ = v.Page.Eval(fmt.Sprintf("() => term.options.theme = %s", string(bts))) v.Options.Video.Style.BackgroundColor = v.Options.Theme.Background v.Options.Video.Style.WindowBarColor = v.Options.Theme.Background + + return nil } // ExecuteSetTypingSpeed applies the default typing speed on the vhs. -func ExecuteSetTypingSpeed(c parser.Command, v *VHS) { +func ExecuteSetTypingSpeed(c parser.Command, v *VHS) error { typingSpeed, err := time.ParseDuration(c.Args) if err != nil { - return + return fmt.Errorf("failed to parse typing speed: %w", err) } + v.Options.TypingSpeed = typingSpeed + return nil } // ExecuteSetPadding applies the padding on the vhs. -func ExecuteSetPadding(c parser.Command, v *VHS) { - v.Options.Video.Style.Padding, _ = strconv.Atoi(c.Args) +func ExecuteSetPadding(c parser.Command, v *VHS) error { + padding, err := strconv.Atoi(c.Args) + if err != nil { + return fmt.Errorf("failed to parse padding: %w", err) + } + + v.Options.Video.Style.Padding = padding + return nil } // ExecuteSetFramerate applies the framerate on the vhs. -func ExecuteSetFramerate(c parser.Command, v *VHS) { +func ExecuteSetFramerate(c parser.Command, v *VHS) error { framerate, err := strconv.ParseInt(c.Args, base, 0) if err != nil { - return + return fmt.Errorf("failed to parse framerate: %w", err) } + v.Options.Video.Framerate = int(framerate) + return nil } // ExecuteSetPlaybackSpeed applies the playback speed option on the vhs. -func ExecuteSetPlaybackSpeed(c parser.Command, v *VHS) { +func ExecuteSetPlaybackSpeed(c parser.Command, v *VHS) error { playbackSpeed, err := strconv.ParseFloat(c.Args, bitSize) if err != nil { - return + return fmt.Errorf("failed to parse playback speed: %w", err) } + v.Options.Video.PlaybackSpeed = playbackSpeed + return nil } // ExecuteLoopOffset applies the loop offset option on the vhs. -func ExecuteLoopOffset(c parser.Command, v *VHS) { +func ExecuteLoopOffset(c parser.Command, v *VHS) error { loopOffset, err := strconv.ParseFloat(strings.TrimRight(c.Args, "%"), bitSize) if err != nil { - return + return fmt.Errorf("failed to parse loop offset: %w", err) } + v.Options.LoopOffset = loopOffset + return nil } // ExecuteSetMarginFill sets vhs margin fill -func ExecuteSetMarginFill(c parser.Command, v *VHS) { +func ExecuteSetMarginFill(c parser.Command, v *VHS) error { v.Options.Video.Style.MarginFill = c.Args + return nil } // ExecuteSetMargin sets vhs margin size -func ExecuteSetMargin(c parser.Command, v *VHS) { - v.Options.Video.Style.Margin, _ = strconv.Atoi(c.Args) +func ExecuteSetMargin(c parser.Command, v *VHS) error { + margin, err := strconv.Atoi(c.Args) + if err != nil { + return fmt.Errorf("failed to parse margin: %w", err) + } + + v.Options.Video.Style.Margin = margin + return nil } // ExecuteSetWindowBar sets window bar type -func ExecuteSetWindowBar(c parser.Command, v *VHS) { +func ExecuteSetWindowBar(c parser.Command, v *VHS) error { v.Options.Video.Style.WindowBar = c.Args + return nil } // ExecuteSetWindowBar sets window bar size -func ExecuteSetWindowBarSize(c parser.Command, v *VHS) { - v.Options.Video.Style.WindowBarSize, _ = strconv.Atoi(c.Args) +func ExecuteSetWindowBarSize(c parser.Command, v *VHS) error { + windowBarSize, err := strconv.Atoi(c.Args) + if err != nil { + return fmt.Errorf("failed to parse window bar size: %w", err) + } + + v.Options.Video.Style.WindowBarSize = windowBarSize + return nil } // ExecuteSetBorderRadius sets corner radius -func ExecuteSetBorderRadius(c parser.Command, v *VHS) { - v.Options.Video.Style.BorderRadius, _ = strconv.Atoi(c.Args) +func ExecuteSetBorderRadius(c parser.Command, v *VHS) error { + borderRadius, err := strconv.Atoi(c.Args) + if err != nil { + return fmt.Errorf("failed to parse border radius: %w", err) + } + + v.Options.Video.Style.BorderRadius = borderRadius + return nil } // ExecuteSetCursorBlink sets cursor blinking -func ExecuteSetCursorBlink(c parser.Command, v *VHS) { +func ExecuteSetCursorBlink(c parser.Command, v *VHS) error { var err error v.Options.CursorBlink, err = strconv.ParseBool(c.Args) if err != nil { - return + return fmt.Errorf("failed to parse cursor blink: %w", err) } + + return nil } const sourceDisplayMaxLength = 10 // ExecuteSourceTape is a CommandFunc that executes all commands of source tape. -func ExecuteSourceTape(c parser.Command, v *VHS) { +func ExecuteSourceTape(c parser.Command, v *VHS) error { tapePath := c.Args var out io.Writer = os.Stdout if quietFlag { @@ -459,24 +634,15 @@ func ExecuteSourceTape(c parser.Command, v *VHS) { // read tape file tape, err := os.ReadFile(tapePath) if err != nil { - fmt.Println(err) - return + return fmt.Errorf("failed to read tape %s: %w", tapePath, err) } l := lexer.New(string(tape)) p := parser.New(l) cmds := p.Parse() - - errs := []error{} - for _, parsedErr := range p.Errors() { - errs = append(errs, parsedErr) - } - - if len(errs) != 0 { - fmt.Fprintln(out, ErrorStyle.Render(fmt.Sprintf("tape %s has errors", tapePath))) - printErrors(out, tapePath, errs) - return + if len(p.Errors()) != 0 { + return InvalidSyntaxError{p.Errors()} } displayPath := runewidth.Truncate(strings.TrimSuffix(tapePath, extension), sourceDisplayMaxLength, "…") @@ -489,13 +655,19 @@ func ExecuteSourceTape(c parser.Command, v *VHS) { continue } fmt.Fprintf(out, "%s %s\n", GrayStyle.Render(displayPath+":"), Highlight(cmd, false)) - CommandFuncs[cmd.Type](cmd, v) + err := CommandFuncs[cmd.Type](cmd, v) + if err != nil { + return fmt.Errorf("failed to execute command %s: %w", cmd.Type.String(), err) + } } + + return nil } // ExecuteScreenshot is a CommandFunc that indicates a new screenshot must be taken. -func ExecuteScreenshot(c parser.Command, v *VHS) { +func ExecuteScreenshot(c parser.Command, v *VHS) error { v.ScreenshotNextFrame(c.Args) + return nil } func getTheme(s string) (Theme, error) { diff --git a/evaluator.go b/evaluator.go index 3e10b5298..9c7a8448a 100644 --- a/evaluator.go +++ b/evaluator.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/vhs/lexer" "github.com/charmbracelet/vhs/parser" "github.com/charmbracelet/vhs/token" + "github.com/go-rod/rod" ) // EvaluatorOption is a function that can be used to modify the VHS instance. @@ -30,7 +31,10 @@ func Evaluate(ctx context.Context, tape string, out io.Writer, opts ...Evaluator v := New() for _, cmd := range cmds { if cmd.Type == token.SET && cmd.Options == "Shell" || cmd.Type == token.ENV { - Execute(cmd, &v) + err := Execute(cmd, &v) + if err != nil { + return []error{err} + } } } @@ -40,13 +44,23 @@ func Evaluate(ctx context.Context, tape string, out io.Writer, opts ...Evaluator } defer func() { _ = v.close() }() - // Run Output and Set commands as they only modify options on the VHS instance. + // Let's wait until we can access the window.term variable. + // + // This is necessary because some SET commands modify the terminal. + err := v.Page.Wait(rod.Eval("() => window.term != undefined")) + if err != nil { + return []error{err} + } + var offset int for i, cmd := range cmds { if cmd.Type == token.SET || cmd.Type == token.OUTPUT || cmd.Type == token.REQUIRE { fmt.Fprintln(out, Highlight(cmd, false)) if cmd.Options != "Shell" { - Execute(cmd, &v) + err := Execute(cmd, &v) + if err != nil { + return []error{err} + } } } else { offset = i @@ -88,7 +102,10 @@ func Evaluate(ctx context.Context, tape string, out io.Writer, opts ...Evaluator break } fmt.Fprintln(out, Highlight(cmd, true)) - Execute(cmd, &v) + err := Execute(cmd, &v) + if err != nil { + return []error{err} + } } } @@ -140,7 +157,11 @@ func Evaluate(ctx context.Context, tape string, out io.Writer, opts ...Evaluator continue } fmt.Fprintln(out, Highlight(cmd, !v.recording || cmd.Type == token.SHOW || cmd.Type == token.HIDE || isSetting)) - Execute(cmd, &v) + err := Execute(cmd, &v) + if err != nil { + teardown() + return []error{err} + } } // If running as an SSH server, the output file is a temporary file diff --git a/testing.go b/testing.go index a0c6fc8a3..ec7736178 100644 --- a/testing.go +++ b/testing.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" "path/filepath" "sync" @@ -28,27 +29,39 @@ var ( ) // SaveOutput saves the current buffer to the output file. -func (v *VHS) SaveOutput() { +func (v *VHS) SaveOutput() error { + var err error // Create output file (once) once.Do(func() { - err := os.MkdirAll(filepath.Dir(v.Options.Test.Output), os.ModePerm) + err = os.MkdirAll(filepath.Dir(v.Options.Test.Output), os.ModePerm) if err != nil { - file, _ = os.CreateTemp(os.TempDir(), "vhs-*.txt") + file, err = os.CreateTemp(os.TempDir(), "vhs-*.txt") return } - file, _ = os.Create(v.Options.Test.Output) + file, err = os.Create(v.Options.Test.Output) }) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } // Get the current buffer. buf, err := v.Page.Eval("() => Array(term.rows).fill(0).map((e, i) => term.buffer.active.getLine(i).translateToString().trimEnd())") if err != nil { - return + return fmt.Errorf("failed to get buffer: %w", err) } for _, line := range buf.Value.Arr() { str := line.Str() - _, _ = file.WriteString(str + "\n") + _, err = file.WriteString(str + "\n") + if err != nil { + return fmt.Errorf("failed to write buffer to file: %w", err) + } + } + + _, err = file.WriteString(separator + "\n") + if err != nil { + return fmt.Errorf("failed to write separator to file: %w", err) } - _, _ = file.WriteString(separator + "\n") + return nil } diff --git a/vhs.go b/vhs.go index 6660896fd..66a72e554 100644 --- a/vhs.go +++ b/vhs.go @@ -165,9 +165,6 @@ func (vhs *VHS) Setup() { height := vhs.Options.Video.Style.Height - double(padding) - double(margin) - bar vhs.Page = vhs.Page.MustSetViewport(width, height, 0, false) - // Let's wait until we can access the window.term variable. - vhs.Page = vhs.Page.MustWait("() => window.term != undefined") - // Find xterm.js canvases for the text and cursor layer for recording. vhs.TextCanvas, _ = vhs.Page.Element("canvas.xterm-text-layer") vhs.CursorCanvas, _ = vhs.Page.Element("canvas.xterm-cursor-layer")