Remove unnecessary output restrictions and simplify report tests
This commit is contained in:
@@ -16,11 +16,13 @@ const (
|
||||
|
||||
// OutputSelection contains only active destinations. ProtectedPaths are inputs
|
||||
// and loaded configuration/profile sources that must never be overwritten.
|
||||
// InputTranscriptPath may be replaced by final transcript publication, but not reports.
|
||||
type OutputSelection struct {
|
||||
TranscriptPath string
|
||||
ReportDir string
|
||||
ReportingEnabled bool
|
||||
ProtectedPaths []string
|
||||
InputTranscriptPath string
|
||||
TranscriptPath string
|
||||
ReportDir string
|
||||
ReportingEnabled bool
|
||||
ProtectedPaths []string
|
||||
}
|
||||
|
||||
type ReportBundle struct {
|
||||
@@ -88,7 +90,11 @@ func PrepareOutput(selection OutputSelection) (*OutputSession, error) {
|
||||
return nil, fmt.Errorf("report output %q: %w", path, err)
|
||||
}
|
||||
}
|
||||
for _, source := range protected {
|
||||
sources := protected
|
||||
if path != session.transcript && selection.InputTranscriptPath != "" {
|
||||
sources = append(append([]string(nil), protected...), selection.InputTranscriptPath)
|
||||
}
|
||||
for _, source := range sources {
|
||||
collision, err := sameDestination(path, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -157,7 +163,7 @@ func (s *OutputSession) Finalize(transcript []byte, reports ReportBundle, failed
|
||||
}
|
||||
failures := make([]FileFailure, 0)
|
||||
if transcript != nil && s.transcript != "" {
|
||||
if err := s.replace(s.transcript, transcript); err != nil {
|
||||
if err := s.writeTranscript(transcript); err != nil {
|
||||
failures = append(failures, FileFailure{Path: s.transcript, Kind: "transcript write", Err: err})
|
||||
}
|
||||
}
|
||||
@@ -176,6 +182,24 @@ func (s *OutputSession) Finalize(transcript []byte, reports ReportBundle, failed
|
||||
return failures
|
||||
}
|
||||
|
||||
// Stream destinations use ordinary writes; replacing their directory entry would
|
||||
// destroy the device or pipe. Regular files retain per-file replacement.
|
||||
func (s *OutputSession) writeTranscript(payload []byte) error {
|
||||
info, err := os.Stat(s.transcript)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
if err == nil && !info.Mode().IsRegular() {
|
||||
file, err := os.OpenFile(s.transcript, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, writeErr := file.Write(payload)
|
||||
return errors.Join(writeErr, file.Close())
|
||||
}
|
||||
return s.replace(s.transcript, payload)
|
||||
}
|
||||
|
||||
func bundleParts(bundle ReportBundle) [3][]byte {
|
||||
return [3][]byte{bundle.Errors, bundle.Recoveries, bundle.Markdown}
|
||||
}
|
||||
@@ -183,8 +207,12 @@ func bundleParts(bundle ReportBundle) [3][]byte {
|
||||
func preflightTranscript(path string) error {
|
||||
info, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if info.IsDir() {
|
||||
return fmt.Errorf("transcript output %q is a directory", path)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return fmt.Errorf("transcript output %q is not a regular file", path)
|
||||
// Opening a pipe here can block before its reader is ready.
|
||||
return nil
|
||||
}
|
||||
file, openErr := os.OpenFile(path, os.O_WRONLY, 0)
|
||||
if openErr != nil {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPrepareOutputRejectsDirectAndAliasCollisions(t *testing.T) {
|
||||
func TestPrepareOutputAllowsInPlaceTranscriptButRejectsReportCollisions(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
input := filepath.Join(dir, "input.json")
|
||||
if err := os.WriteFile(input, []byte("source"), 0600); err != nil {
|
||||
@@ -23,8 +23,8 @@ func TestPrepareOutputRejectsDirectAndAliasCollisions(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, output := range []string{input, alias, hardlink} {
|
||||
if _, err := PrepareOutput(OutputSelection{TranscriptPath: output, ProtectedPaths: []string{input}}); err == nil {
|
||||
t.Fatalf("collision accepted: %s", output)
|
||||
if _, err := PrepareOutput(OutputSelection{TranscriptPath: output, InputTranscriptPath: input}); err != nil {
|
||||
t.Fatalf("in-place output rejected: %s: %v", output, err)
|
||||
}
|
||||
}
|
||||
if _, err := PrepareOutput(OutputSelection{TranscriptPath: filepath.Join(dir, ErrorsFile), ReportDir: dir, ReportingEnabled: true}); err == nil {
|
||||
@@ -33,6 +33,15 @@ func TestPrepareOutputRejectsDirectAndAliasCollisions(t *testing.T) {
|
||||
if _, err := PrepareOutput(OutputSelection{ReportDir: dir, ReportingEnabled: true, ProtectedPaths: []string{filepath.Join(dir, ErrorsFile)}}); err == nil {
|
||||
t.Fatal("report/source collision accepted")
|
||||
}
|
||||
if err := os.Symlink(input, filepath.Join(dir, ErrorsFile)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := PrepareOutput(OutputSelection{ReportDir: dir, ReportingEnabled: true, InputTranscriptPath: input}); err == nil {
|
||||
t.Fatal("report/input alias accepted")
|
||||
}
|
||||
if err := os.Remove(filepath.Join(dir, ErrorsFile)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(dir, MarkdownFile), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
69
internal/core/diagnostics/output_stream_test.go
Normal file
69
internal/core/diagnostics/output_stream_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
//go:build linux || darwin
|
||||
|
||||
package diagnostics
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTranscriptStreamDestinations(t *testing.T) {
|
||||
fifo := filepath.Join(t.TempDir(), "transcript.pipe")
|
||||
if err := syscall.Mkfifo(fifo, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, path := range []string{os.DevNull, fifo} {
|
||||
t.Run(filepath.Base(path), func(t *testing.T) {
|
||||
before, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Preparation must finish even though the pipe has no reader yet.
|
||||
session, err := PrepareOutput(OutputSelection{TranscriptPath: path})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var reader *os.File
|
||||
if path == fifo {
|
||||
// Keep both ends open in the test so publication and reading can be
|
||||
// sequential; the small payload fits in the pipe buffer.
|
||||
reader, err = os.OpenFile(path, os.O_RDWR|syscall.O_NONBLOCK, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer reader.Close()
|
||||
}
|
||||
payload := []byte("transcript")
|
||||
if failures := session.Finalize(payload, ReportBundle{}, nil); len(failures) != 0 {
|
||||
t.Fatal(failures)
|
||||
}
|
||||
after, err := os.Stat(path)
|
||||
if err != nil || !os.SameFile(before, after) || before.Mode() != after.Mode() {
|
||||
t.Fatalf("stream replaced: %v", err)
|
||||
}
|
||||
if reader != nil {
|
||||
got := make([]byte, len(payload))
|
||||
if _, err := io.ReadFull(reader, got); err != nil || string(got) != string(payload) {
|
||||
t.Fatalf("pipe delivery: %q %v", got, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranscriptStreamWriteFailure(t *testing.T) {
|
||||
if _, err := os.Stat("/dev/full"); os.IsNotExist(err) {
|
||||
t.Skip("/dev/full unavailable")
|
||||
}
|
||||
session, err := PrepareOutput(OutputSelection{TranscriptPath: "/dev/full"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
failures := session.Finalize([]byte("transcript"), ReportBundle{}, nil)
|
||||
if len(failures) != 1 || failures[0].Kind != "transcript write" {
|
||||
t.Fatalf("write failure lost: %+v", failures)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user