test(api): race the pdf/docmark temp-copy path too
PR #156 round 4: TextIn's pdf/docmark branch copies the file through the root to a temp file, and pointing the extractor back at the raw/ path by name stayed green (race tests used only r.md); a stub docmark then got the outside file's bytes 34k times in 4s. TestDocmarkExtractionLeaksNothingUnderASymlinkSwapRace races a symlink into raw/r.docx through ReadIngestFile while a fake docmark counts the sentinel in every document it receives. Flipped: the by-name revert is red. Refs #153 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
package api_test
|
package api_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -110,3 +114,59 @@ func TestRawReadersDoNotHangUnderAFIFOSwapRace(t *testing.T) {
|
|||||||
t.Fatalf("a raw/ reader hung on a swapped-in FIFO after %d iterations", iters.Load())
|
t.Fatalf("a raw/ reader hung on a swapped-in FIFO after %d iterations", iters.Load())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PR #156 round 4: the pdf/docmark branch of TextIn copies the file through
|
||||||
|
// the root to a temp file; pointing the extractor back at the raw/ path by
|
||||||
|
// name stayed green because the race tests only used r.md (a stub docmark
|
||||||
|
// then received the outside file's bytes 34k times in 4s). This races a
|
||||||
|
// symlink into raw/r.docx and has a fake docmark count the sentinel in every
|
||||||
|
// document it is sent.
|
||||||
|
func TestDocmarkExtractionLeaksNothingUnderASymlinkSwapRace(t *testing.T) {
|
||||||
|
var seen atomic.Int64
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Params struct {
|
||||||
|
Arguments struct {
|
||||||
|
ContentBase64 string `json:"content_base64"`
|
||||||
|
} `json:"arguments"`
|
||||||
|
} `json:"params"`
|
||||||
|
}
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if b, err := base64.StdEncoding.DecodeString(req.Params.Arguments.ContentBase64); err == nil &&
|
||||||
|
strings.Contains(string(b), raceSentinel) {
|
||||||
|
seen.Add(1)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"converted"}]}}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
t.Setenv("DOCMARK_URL", srv.URL)
|
||||||
|
|
||||||
|
brain, raw, outside := raceBrain(t)
|
||||||
|
tgt := filepath.Join(raw, "r.docx")
|
||||||
|
if err := os.WriteFile(tgt, []byte("inside docx bytes"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var stop atomic.Bool
|
||||||
|
go func() {
|
||||||
|
for i := 0; !stop.Load(); i++ {
|
||||||
|
tmp := filepath.Join(raw, ".swapdocx")
|
||||||
|
_ = os.Remove(tmp)
|
||||||
|
if i%2 == 0 {
|
||||||
|
_ = os.WriteFile(tmp, []byte("inside docx bytes"), 0o644)
|
||||||
|
} else {
|
||||||
|
_ = os.Symlink(outside, tmp)
|
||||||
|
}
|
||||||
|
_ = os.Rename(tmp, tgt)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer stop.Store(true)
|
||||||
|
iters := 0
|
||||||
|
for deadline := time.Now().Add(1500 * time.Millisecond); time.Now().Before(deadline); iters++ {
|
||||||
|
_, _ = api.ReadIngestFile(brain, tgt)
|
||||||
|
}
|
||||||
|
if n := seen.Load(); n > 0 {
|
||||||
|
t.Fatalf("docmark received the outside file %d times in %d iterations", n, iters)
|
||||||
|
}
|
||||||
|
t.Logf("%d iterations, 0 leaks to docmark", iters)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user