Skip to content

feat(cmd): send verbose make logs to stderr#115

Open
MeteorsLiu wants to merge 1 commit into
goplus:mainfrom
MeteorsLiu:codex/make-output-contract
Open

feat(cmd): send verbose make logs to stderr#115
MeteorsLiu wants to merge 1 commit into
goplus:mainfrom
MeteorsLiu:codex/make-output-contract

Conversation

@MeteorsLiu
Copy link
Copy Markdown
Collaborator

Summary

  • keep llar make stdout reserved for final metadata
  • redirect verbose build stdout to stderr during OnBuild
  • add regression coverage for stdout/stderr separation

Tests

  • go test -ldflags=-checklinkname=0 -short ./...

Note: non-short go test ./cmd/llar/internal still hits existing real integration tests that write to the user cache and failed locally with permission denied.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the make command to redirect verbose build logs to stderr while reserving stdout for the final metadata line, allowing callers to capture the output separately. The test suite has been updated to capture both streams concurrently, and a new test has been added to verify this behavior. The review feedback highlights critical issues in the tests: first, modifying global variables makeVerbose and makeOutput without restoring them can cause test pollution; second, ignoring errors from os.Pipe() and failing to close the read ends of the pipes in runMakeCmdStreams and captureBuildModuleStreams leads to potential nil pointer dereferences and file descriptor leaks.

Comment thread cmd/llar/internal/make_test.go Outdated
Comment thread cmd/llar/internal/make_test.go
Comment thread cmd/llar/internal/make_test.go Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

❌ Patch coverage is 87.87879% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.46%. Comparing base (f1a02d8) to head (56d8f94).

Files with missing lines Patch % Lines
cmd/llar/internal/make.go 87.87% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #115      +/-   ##
==========================================
+ Coverage   80.43%   80.46%   +0.02%     
==========================================
  Files          37       37              
  Lines        2116     2129      +13     
==========================================
+ Hits         1702     1713      +11     
- Misses        300      301       +1     
- Partials      114      115       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

fennoai[bot]

This comment was marked as outdated.

@MeteorsLiu MeteorsLiu force-pushed the codex/make-output-contract branch from 45a7ce7 to 56d8f94 Compare June 1, 2026 06:29
@MeteorsLiu
Copy link
Copy Markdown
Collaborator Author

/gemini review

@MeteorsLiu
Copy link
Copy Markdown
Collaborator Author

/review

@MeteorsLiu MeteorsLiu changed the title fix(cmd): send verbose make logs to stderr feat(cmd): send verbose make logs to stderr Jun 1, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors build output redirection in cmd/llar/internal/make.go by extracting the logic into a redirectBuildOutput helper, which redirects stdout to stderr in verbose mode and discards it in silent mode. It also updates the testing framework to capture both stdout and stderr concurrently using pipes and adds a new integration test for verbose output. The review feedback highlights several issues: 1) os.Stdout and os.Stderr should be restored before closing devNull to prevent panics from concurrent writes; 2) the read ends of the pipes in runMakeCmdStreams and captureProcessStreams should be closed to prevent file descriptor leaks; and 3) the global makeVerbose variable modified in tests should be restored using t.Cleanup to avoid test flakiness.

Comment thread cmd/llar/internal/make.go
Comment on lines +216 to +220
return func() {
_ = devNull.Close()
os.Stdout = savedStdout
os.Stderr = savedStderr
}, nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Restore os.Stdout and os.Stderr before closing devNull. If devNull is closed first, any concurrent or immediate writes to os.Stdout or os.Stderr before they are restored will fail or panic because they still point to the closed devNull file descriptor.

Suggested change
return func() {
_ = devNull.Close()
os.Stdout = savedStdout
os.Stderr = savedStderr
}, nil
return func() {
os.Stdout = savedStdout
os.Stderr = savedStderr
_ = devNull.Close()
}, nil

Comment on lines +339 to +347
oldStdout := os.Stdout
stdoutR, stdoutW, _ := os.Pipe()
os.Stdout = stdoutW
defer func() { os.Stdout = oldStdout }()

oldStderr := os.Stderr
stderrR, stderrW, _ := os.Pipe()
os.Stderr = stderrW
defer func() { os.Stderr = oldStderr }()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Close the read ends of the pipes (stdoutR and stderrR) when they are no longer needed to prevent file descriptor leaks during test execution.

Suggested change
oldStdout := os.Stdout
stdoutR, stdoutW, _ := os.Pipe()
os.Stdout = stdoutW
defer func() { os.Stdout = oldStdout }()
oldStderr := os.Stderr
stderrR, stderrW, _ := os.Pipe()
os.Stderr = stderrW
defer func() { os.Stderr = oldStderr }()
oldStdout := os.Stdout
stdoutR, stdoutW, _ := os.Pipe()
defer stdoutR.Close()
os.Stdout = stdoutW
defer func() { os.Stdout = oldStdout }()
oldStderr := os.Stderr
stderrR, stderrW, _ := os.Pipe()
defer stderrR.Close()
os.Stderr = stderrW
defer func() { os.Stderr = oldStderr }()

Comment on lines +405 to +417
return &stdoutBuf, &stderrBuf, func() {
_ = stdoutW.Close()
if copyErr := <-stdoutDone; copyErr != nil {
t.Fatalf("failed to capture stdout: %v", copyErr)
}
os.Stdout = oldStdout

_ = stderrW.Close()
if copyErr := <-stderrDone; copyErr != nil {
t.Fatalf("failed to capture stderr: %v", copyErr)
}
os.Stderr = oldStderr
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Close the read ends of the pipes (stdoutR and stderrR) in the returned cleanup function to prevent file descriptor leaks.

	return &stdoutBuf, &stderrBuf, func() {
		_ = stdoutW.Close()
		if copyErr := <-stdoutDone; copyErr != nil {
			t.Fatalf("failed to capture stdout: %v", copyErr)
		}
		_ = stdoutR.Close()
		os.Stdout = oldStdout

		_ = stderrW.Close()
		if copyErr := <-stderrDone; copyErr != nil {
			t.Fatalf("failed to capture stderr: %v", copyErr)
		}
		_ = stderrR.Close()
		os.Stderr = oldStderr
	}

t.Fatalf("modules.Load() failed: %v", err)
}

makeVerbose = true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Restore the global makeVerbose variable to its original value after the test finishes using t.Cleanup. Modifying global variables in tests without cleanup can cause side effects and flakiness in other tests running in the same package.

Suggested change
makeVerbose = true
origVerbose := makeVerbose
makeVerbose = true
t.Cleanup(func() { makeVerbose = origVerbose })

Copy link
Copy Markdown
Contributor

@fennoai fennoai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Clean refactoring that extracts output redirection into a well-structured helper with proper restore semantics. The new verbose-to-stderr behavior is a solid improvement for machine-parseable stdout. A few items below, mostly in the test code.

t.Fatalf("modules.Load() failed: %v", err)
}

makeVerbose = true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (test isolation): makeVerbose is set to true but never restored. If test execution order places another test after this one that assumes the default false, it will fail. Other tests in this file (e.g., TestBuildModule_SilentSuccess) properly defer-restore:

Suggested change
makeVerbose = true
makeVerbose = true
defer func() { makeVerbose = false }()


var buf bytes.Buffer
copyDone := make(chan error, 1)
// flows through process-wide os.Stdout/os.Stderr (including nested build
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (duplication): The pipe-capture-and-drain pattern in runMakeCmdStreams (lines 336-374) is nearly identical to captureProcessStreams (lines 382-418). Consider having runMakeCmdStreams delegate to captureProcessStreams internally to maintain the capture logic in one place.

// commands), redirect to pipes and drain concurrently to avoid blocking on
// full pipe buffers.
oldStdout := os.Stdout
stdoutR, stdoutW, _ := os.Pipe()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: os.Pipe() errors are silently discarded here (and in captureProcessStreams at similar lines). While unlikely to fail, a t.Fatal(err) on pipe creation error would give a clear failure message instead of a confusing nil-pointer panic on the write end.

Comment thread cmd/llar/internal/make.go
Comment on lines 195 to 197
return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (doc accuracy): "discarded until the metadata line is printed" implies this function itself watches for a metadata line and then switches behavior. In reality, the caller is responsible for calling the restore function before printing metadata. Consider:

Suggested change
return nil
}
// redirectBuildOutput reserves command stdout for final metadata. In verbose
// mode, build stdout is redirected to stderr; in silent mode, all build output
// is discarded. The caller must invoke the returned restore function before
// writing final results to stdout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant