Files
kyleandClaude Opus 5.5 aafc1d1c28 P5a review: outlines from named destinations; pool race; fast over-limit; revalidation under the limiter; Accept pdf
- pdftext/outline: resolve named destinations through the catalog /Dests and the /Names /Dests
  tree, resolve indirect /Dest, /A and /Title, and treat reader.Null (what Dict.Get returns for
  a missing key) as absent. Before this every LaTeX and Acrobat PDF had an empty outline.
  pdftest gains NamedDests; TestOutlineNamedDestinations.
- api: pool() always goes through its Once (data race on concurrent first use);
  TestDefaultPoolConcurrentFirstUse. Revalidation of a stale cache entry counts against the
  caller/host limit.
- pdftext/runner: output over MaxText kills the child at once instead of waiting for the
  deadline; TestRunnerTooMuchTextFailsFast.
- fetch: Accept includes application/pdf;q=0.8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 04:48:17 -07:00

61 lines
2.1 KiB
Go

package pdftext_test
// Owner review (P5a): output over MaxText fails fast, not at the deadline. The child writes more
// than a pipe buffer (64 KiB), so it blocks on stdout until killed; Run must not wait for it to
// exit on its own before checking the size.
import (
"bytes"
"context"
"encoding/json"
"errors"
"testing"
"time"
"git.wntrmute.dev/kyle/sift/internal/pdftest"
"git.wntrmute.dev/kyle/sift/internal/pdftext"
)
func TestRunnerTooMuchTextFailsFast(t *testing.T) {
var pages []string
for p := 1; p <= 80; p++ {
pages = append(pages, pdftest.Lorem(p, 40)) // ~2,700 characters a page, ~200 KiB in all
}
r := runner()
r.MaxText = 1000
r.Deadline = 30 * time.Second
start := time.Now()
_, err := r.Run(context.Background(), pdftest.Build(pdftest.Doc{Pages: pages}))
if !errors.Is(err, pdftext.ErrTooMuchText) {
t.Fatalf("err %v, want ErrTooMuchText", err)
}
if d := time.Since(start); d > 5*time.Second {
t.Errorf("ErrTooMuchText after %v; it waited for the deadline instead of killing the child", d)
}
}
// Owner review (P5a): LaTeX (hyperref) and Acrobat outlines use named destinations through
// the /Names tree and indirect titles; the first run returned no outline for any arXiv paper
// or the NIST PDF. reader's Dict.Get answers reader.Null{} for a missing key, which hid /A.
func TestOutlineNamedDestinations(t *testing.T) {
b := pdftest.Build(pdftest.Doc{
Title: "Named",
Pages: []string{"one", "two", "three"},
Outline: []pdftest.Outline{{Title: "1 Intro", Page: 1, Level: 1}, {Title: "2 Method", Page: 3, Level: 1}},
NamedDests: true,
})
var out bytes.Buffer
if err := pdftext.Extract(b, 10, &out); err != nil {
t.Fatal(err)
}
var l struct{ Meta pdftext.Meta }
line, _ := out.ReadBytes('\n')
if err := json.Unmarshal(line, &l); err != nil {
t.Fatal(err)
}
want := []pdftext.OutlineItem{{Title: "1 Intro", Page: 1, Level: 1}, {Title: "2 Method", Page: 3, Level: 1}}
if len(l.Meta.Outline) != 2 || l.Meta.Outline[0] != want[0] || l.Meta.Outline[1] != want[1] {
t.Errorf("outline = %+v, want %+v", l.Meta.Outline, want)
}
}