Transfer completed Go output without copies
This commit is contained in:
@@ -105,7 +105,7 @@ func PrepareBuildDependencies(ctx context.Context, directory string) error {
|
||||
Stderr: captured,
|
||||
})
|
||||
if err != nil {
|
||||
if output := strings.TrimSpace(string(captured.Bytes())); output != "" {
|
||||
if output := strings.TrimSpace(string(captured.take())); output != "" {
|
||||
return fmt.Errorf("go mod download: %w: %s", err, output)
|
||||
}
|
||||
return fmt.Errorf("go mod download: %w", err)
|
||||
@@ -121,8 +121,19 @@ func (tool *Tool) HostTarget() Target {
|
||||
// Run executes a Go command with ReleaseKit's shared offline baseline. Native
|
||||
// commands use the host target and effective cgo configuration. Static commands
|
||||
// use target and disable cgo. Output, when supplied, receives stdout and stderr
|
||||
// as the command runs; the returned bytes retain both streams for diagnostics.
|
||||
func (tool *Tool) Run(ctx context.Context, directory string, mode Mode, target Target, output io.Writer, args ...string) ([]byte, error) {
|
||||
// as the command runs.
|
||||
func (tool *Tool) Run(ctx context.Context, directory string, mode Mode, target Target, output io.Writer, args ...string) error {
|
||||
_, err := tool.execute(ctx, directory, mode, target, output, false, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Output executes a Go command and returns its combined stdout and stderr.
|
||||
// Output, when supplied, also receives the bytes as the command runs.
|
||||
func (tool *Tool) Output(ctx context.Context, directory string, mode Mode, target Target, output io.Writer, args ...string) ([]byte, error) {
|
||||
return tool.execute(ctx, directory, mode, target, output, true, args...)
|
||||
}
|
||||
|
||||
func (tool *Tool) execute(ctx context.Context, directory string, mode Mode, target Target, output io.Writer, keepOutput bool, args ...string) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, errors.New("Go command requires arguments")
|
||||
}
|
||||
@@ -136,14 +147,24 @@ func (tool *Tool) Run(ctx context.Context, directory string, mode Mode, target T
|
||||
Stdout: captured,
|
||||
Stderr: captured,
|
||||
})
|
||||
contents := captured.Bytes()
|
||||
if forwardErr := captured.forwardingError(); forwardErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("forward Go output: %w", forwardErr))
|
||||
}
|
||||
contents := captured.take()
|
||||
if err != nil {
|
||||
if len(contents) != 0 {
|
||||
return contents, fmt.Errorf("go %s: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(string(contents)))
|
||||
return outputBytes(contents, keepOutput), fmt.Errorf("go %s: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(string(contents)))
|
||||
}
|
||||
return contents, fmt.Errorf("go %s: %w", strings.Join(args, " "), err)
|
||||
return outputBytes(contents, keepOutput), fmt.Errorf("go %s: %w", strings.Join(args, " "), err)
|
||||
}
|
||||
return contents, nil
|
||||
return outputBytes(contents, keepOutput), nil
|
||||
}
|
||||
|
||||
func outputBytes(contents []byte, keep bool) []byte {
|
||||
if keep {
|
||||
return contents
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tool *Tool) environment(mode Mode, target Target) map[string]string {
|
||||
@@ -206,9 +227,10 @@ func discover(ctx context.Context, directory string, environment map[string]stri
|
||||
}
|
||||
|
||||
type combinedWriter struct {
|
||||
mu sync.Mutex
|
||||
output io.Writer
|
||||
buffer bytes.Buffer
|
||||
mu sync.Mutex
|
||||
output io.Writer
|
||||
buffer bytes.Buffer
|
||||
forwardErr error
|
||||
}
|
||||
|
||||
func (writer *combinedWriter) Write(contents []byte) (int, error) {
|
||||
@@ -217,11 +239,29 @@ func (writer *combinedWriter) Write(contents []byte) (int, error) {
|
||||
if writer.output == nil {
|
||||
return writer.buffer.Write(contents)
|
||||
}
|
||||
return io.MultiWriter(&writer.buffer, writer.output).Write(contents)
|
||||
if _, err := writer.buffer.Write(contents); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
written, err := writer.output.Write(contents)
|
||||
if err == nil && written != len(contents) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
if err != nil && writer.forwardErr == nil {
|
||||
writer.forwardErr = err
|
||||
}
|
||||
return written, err
|
||||
}
|
||||
|
||||
func (writer *combinedWriter) Bytes() []byte {
|
||||
func (writer *combinedWriter) forwardingError() error {
|
||||
writer.mu.Lock()
|
||||
defer writer.mu.Unlock()
|
||||
return append([]byte(nil), writer.buffer.Bytes()...)
|
||||
return writer.forwardErr
|
||||
}
|
||||
|
||||
// take transfers the completed capture to the caller. It must only be used
|
||||
// after the child process has stopped writing.
|
||||
func (writer *combinedWriter) take() []byte {
|
||||
writer.mu.Lock()
|
||||
defer writer.mu.Unlock()
|
||||
return writer.buffer.Bytes()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gotool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -62,6 +63,14 @@ func runGoPreparationFixture() int {
|
||||
case "fail":
|
||||
fmt.Fprintln(os.Stderr, "fixture dependency failure")
|
||||
return 23
|
||||
case "output":
|
||||
fmt.Fprintln(os.Stdout, "fixture stdout")
|
||||
fmt.Fprintln(os.Stderr, "fixture stderr")
|
||||
return 0
|
||||
case "large-fail":
|
||||
_, _ = fmt.Fprint(os.Stdout, strings.Repeat("x", 8<<20))
|
||||
fmt.Fprintln(os.Stderr, "fixture large failure")
|
||||
return 23
|
||||
case "wait":
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
@@ -295,7 +304,7 @@ func TestPersistentSettingsPreserveCachesAndNativeCTools(t *testing.T) {
|
||||
func readSettings(t *testing.T, tool *Tool, mode Mode, target Target, names ...string) map[string]string {
|
||||
t.Helper()
|
||||
args := append([]string{"env", "-json"}, names...)
|
||||
output, err := tool.Run(context.Background(), t.TempDir(), mode, target, nil, args...)
|
||||
output, err := tool.Output(context.Background(), t.TempDir(), mode, target, nil, args...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -306,6 +315,122 @@ func readSettings(t *testing.T, tool *Tool, mode Mode, target Target, names ...s
|
||||
return settings
|
||||
}
|
||||
|
||||
func TestOutputReturnsAndForwardsCombinedOutput(t *testing.T) {
|
||||
repository, _ := installGoPreparationFixture(t)
|
||||
t.Setenv("RELEASEKIT_GO_FIXTURE_MODE", "output")
|
||||
tool := fixtureTool()
|
||||
var forwarded bytes.Buffer
|
||||
|
||||
output, err := tool.Output(context.Background(), repository, Native, Target{}, &forwarded, "version")
|
||||
if err != nil {
|
||||
t.Fatalf("Output() error = %v", err)
|
||||
}
|
||||
if !bytes.Contains(output, []byte("fixture stdout")) || !bytes.Contains(output, []byte("fixture stderr")) {
|
||||
t.Fatalf("combined output = %q", output)
|
||||
}
|
||||
if !bytes.Equal(forwarded.Bytes(), output) {
|
||||
t.Fatalf("forwarded output differs from capture: forwarded %d bytes, captured %d", forwarded.Len(), len(output))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunForwardsOutputWithoutReturningCapture(t *testing.T) {
|
||||
repository, _ := installGoPreparationFixture(t)
|
||||
t.Setenv("RELEASEKIT_GO_FIXTURE_MODE", "output")
|
||||
tool := fixtureTool()
|
||||
var forwarded bytes.Buffer
|
||||
|
||||
if err := tool.Run(context.Background(), repository, Native, Target{}, &forwarded, "version"); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
if !bytes.Contains(forwarded.Bytes(), []byte("fixture stdout")) || !bytes.Contains(forwarded.Bytes(), []byte("fixture stderr")) {
|
||||
t.Fatalf("forwarded output = %q", forwarded.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPreservesFailureDiagnosticsAndCause(t *testing.T) {
|
||||
repository, _ := installGoPreparationFixture(t)
|
||||
t.Setenv("RELEASEKIT_GO_FIXTURE_MODE", "fail")
|
||||
tool := fixtureTool()
|
||||
|
||||
err := tool.Run(context.Background(), repository, Native, Target{}, nil, "version")
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) || exitError.ExitCode() != 23 || !strings.Contains(err.Error(), "fixture dependency failure") {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputReturnsLargeFailureDiagnosticsAndCause(t *testing.T) {
|
||||
repository, _ := installGoPreparationFixture(t)
|
||||
t.Setenv("RELEASEKIT_GO_FIXTURE_MODE", "large-fail")
|
||||
tool := fixtureTool()
|
||||
|
||||
output, err := tool.Output(context.Background(), repository, Native, Target{}, nil, "version")
|
||||
var exitError *exec.ExitError
|
||||
wantSize := (8 << 20) + len("fixture large failure\n")
|
||||
if len(output) != wantSize {
|
||||
t.Fatalf("captured output = %d bytes, want %d", len(output), wantSize)
|
||||
}
|
||||
if !errors.As(err, &exitError) || exitError.ExitCode() != 23 || !strings.Contains(err.Error(), "fixture large failure") {
|
||||
t.Fatalf("Output() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputReturnsCancellation(t *testing.T) {
|
||||
repository, recordPath := installGoPreparationFixture(t)
|
||||
t.Setenv("RELEASEKIT_GO_FIXTURE_MODE", "wait")
|
||||
tool := fixtureTool()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := tool.Output(ctx, repository, Native, Target{}, nil, "version")
|
||||
done <- err
|
||||
}()
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for {
|
||||
if _, err := os.Stat(recordPath); err == nil {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("Go fixture did not start")
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case err := <-done:
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Output() error = %v; want cancellation", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Go fixture did not stop after cancellation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputPreservesForwardingWriterFailure(t *testing.T) {
|
||||
repository, _ := installGoPreparationFixture(t)
|
||||
t.Setenv("RELEASEKIT_GO_FIXTURE_MODE", "output")
|
||||
tool := fixtureTool()
|
||||
writerErr := errors.New("fixture output writer failed")
|
||||
|
||||
output, err := tool.Output(context.Background(), repository, Native, Target{}, failingOutputWriter{err: writerErr}, "version")
|
||||
if !errors.Is(err, writerErr) || !strings.Contains(err.Error(), "go version") {
|
||||
t.Fatalf("Output() error = %v; want wrapped writer error and command", err)
|
||||
}
|
||||
if len(output) == 0 {
|
||||
t.Fatal("Output() returned no captured bytes after forwarding failed")
|
||||
}
|
||||
}
|
||||
|
||||
type failingOutputWriter struct{ err error }
|
||||
|
||||
func (writer failingOutputWriter) Write([]byte) (int, error) { return 0, writer.err }
|
||||
|
||||
func fixtureTool() *Tool {
|
||||
return &Tool{host: Target{OS: runtime.GOOS, Arch: runtime.GOARCH}, settings: map[string]string{}}
|
||||
}
|
||||
|
||||
func clearGoSettings(t *testing.T) {
|
||||
t.Helper()
|
||||
for _, key := range []string{
|
||||
|
||||
@@ -28,7 +28,7 @@ func BenchmarkPerformanceCombinedOutput(b *testing.B) {
|
||||
if written, err := writer.Write(input); err != nil || written != len(input) {
|
||||
b.Fatalf("write captured output = %d, err %v; want %d bytes", written, err, len(input))
|
||||
}
|
||||
captured := writer.Bytes()
|
||||
captured := writer.take()
|
||||
if len(captured) != len(input) {
|
||||
b.Fatalf("captured output = %d bytes, want %d", len(captured), len(input))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user