Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions cmd/spinner_test/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import (
"fmt"
"os"
"path/filepath"

"github.com/Azure/InnovationEngine/internal/testutil"
)

func main() {
// Find the root directory and set it as environment variable
execPath, err := os.Executable()
if err == nil {
dir := filepath.Dir(execPath)
// Go up two directories (from cmd/spinner_test to root)
rootDir := filepath.Dir(filepath.Dir(dir))
os.Setenv("INNOVATION_ENGINE_ROOT", rootDir)
}

fmt.Println("Running spinner test with real-time streaming...")
output := testutil.RunStreamingTest()
fmt.Println("\nTest result:", output)
}
6 changes: 5 additions & 1 deletion internal/engine/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []common.Step, env map[string]strin

var commandErr error
var frame int = 0
startTime := time.Now() // Record the start time for progress indicator

// If forwarding input/output, don't render the spinner.
if !interactiveCommand {
Expand Down Expand Up @@ -246,7 +247,10 @@ func (e *Engine) ExecuteAndRenderSteps(steps []common.Step, env map[string]strin
break renderingLoop
default:
frame = (frame + 1) % len(spinnerFrames)
fmt.Printf("\r %s", ui.SpinnerStyle.Render(string(spinnerFrames[frame])))
elapsedTime := time.Since(startTime)
minutes := int(elapsedTime.Minutes())
seconds := int(elapsedTime.Seconds()) % 60
fmt.Printf("\r %s [%02d:%02d elapsed]", ui.SpinnerStyle.Render(string(spinnerFrames[frame])), minutes, seconds)
time.Sleep(spinnerRefresh)
}
}
Expand Down
20 changes: 20 additions & 0 deletions internal/testutil/stream_output.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

# This script demonstrates real-time output with pauses to show the spinner
# It writes to both stdout and stderr to test both streams

echo "Starting long-running operation with streamed output..."
for i in {1..5}; do
echo -n "Processing step $i of 5... "
sleep 1
echo "done"

# Add a slight delay to allow spinner to be visible
sleep 0.5

# On step 3, output something to stderr to test error stream
if [ $i -eq 3 ]; then
echo "Note: Step $i added diagnostic info" >&2
fi
done
echo "Operation complete!"
149 changes: 149 additions & 0 deletions internal/testutil/streamer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package testutil

import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"strings"
"time"

"github.com/Azure/InnovationEngine/internal/ui"
)

const (
spinnerFrames = `-\|/`
spinnerRefresh = 100 * time.Millisecond
)

// StreamCommand executes a command and streams its output in real-time
// while showing a spinner with elapsed time between outputs
func StreamCommand(command string) error {
fmt.Println("Executing command with real-time output streaming:")
fmt.Println("$ " + command)

// Create the command
cmd := exec.Command("bash", "-c", command)

// Create pipes for stdout and stderr
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("error creating stdout pipe: %v", err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("error creating stderr pipe: %v", err)
}

// Start the command
if err := cmd.Start(); err != nil {
return fmt.Errorf("error starting command: %v", err)
}

// Create channels to signal when stdout/stderr reading is done
stdoutDone := make(chan struct{})
stderrDone := make(chan struct{})

// Track when the last output was printed
lastOutputTime := time.Now()
commandStartTime := time.Now()

// Function to read from a pipe and print to console
readFromPipe := func(pipe io.ReadCloser, isDone chan<- struct{}, prefix string) {
scanner := bufio.NewScanner(pipe)
for scanner.Scan() {
text := scanner.Text()
fmt.Printf("\r%s%s\n", prefix, text)
lastOutputTime = time.Now()
}
close(isDone)
}

// Read stdout and stderr concurrently
go readFromPipe(stdout, stdoutDone, "")
go readFromPipe(stderr, stderrDone, "[stderr] ")

// Spinner goroutine
spinnerDone := make(chan struct{})
go func() {
frame := 0
for {
select {
case <-spinnerDone:
return
default:
// Only show spinner if it's been a while since last output
if time.Since(lastOutputTime) > 200*time.Millisecond {
elapsedTime := time.Since(commandStartTime)
minutes := int(elapsedTime.Minutes())
seconds := int(elapsedTime.Seconds()) % 60

// Clear the current line and show spinner
fmt.Printf("\r %s [%02d:%02d elapsed]", ui.SpinnerStyle.Render(string(spinnerFrames[frame])), minutes, seconds)

frame = (frame + 1) % len(spinnerFrames)
}
time.Sleep(spinnerRefresh)
}
}
}()

// Wait for stdout and stderr to finish
<-stdoutDone
<-stderrDone

// Stop the spinner
close(spinnerDone)
fmt.Print("\r \r") // Clear the spinner line

// Wait for the command to finish
if err := cmd.Wait(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return fmt.Errorf("command exited with code %d", exitErr.ExitCode())
}
return fmt.Errorf("error waiting for command: %v", err)
}

return nil
}

// RunStreamingTest runs a command that demonstrates the spinner with elapsed time
// Returns a string with the captured output for test verification
func RunStreamingTest() string {
// Create a temporary file to capture output
outputCaptured := strings.Builder{}

// Get the path to the stream_output.sh script
scriptPath := os.Getenv("INNOVATION_ENGINE_ROOT")
if scriptPath == "" {
// If environment variable not set, use current directory
var err error
scriptPath, err = os.Getwd()
if err != nil {
outputCaptured.WriteString(fmt.Sprintf("Error getting working directory: %v\n", err))
return outputCaptured.String()
}
}

fullScriptPath := fmt.Sprintf("%s/internal/testutil/stream_output.sh", scriptPath)
outputCaptured.WriteString(fmt.Sprintf("Looking for script at: %s\n", fullScriptPath))

// Verify script exists
if _, err := os.Stat(fullScriptPath); os.IsNotExist(err) {
outputCaptured.WriteString(fmt.Sprintf("Error: Script not found at %s\n", fullScriptPath))
return outputCaptured.String()
}

outputCaptured.WriteString("Streaming test started\n")

// Run the command
err := StreamCommand(fullScriptPath)
if err != nil {
outputCaptured.WriteString(fmt.Sprintf("Error: %v\n", err))
} else {
outputCaptured.WriteString("Streaming test completed successfully\n")
}

return outputCaptured.String()
}
Loading