//go:build !windows package main // Gates for four office items: slide ORDER, docx COMMENTS, HIDDEN // sheets and slides, and the workbook that stops before its last sheet. // // Each test asserts TWICE, the way contextoffice_test.go's defect gates do: // first that the fixture still carries the structure the behaviour needs, read // out of the package's own XML, then that the reader does the right thing with // it. The first half is not ceremony — three earlier defects all sat under a green suite // because the fixtures did not contain the shapes involved, and the reordering // case below cannot be built with LibreOffice at all (it renumbers the parts). import ( "encoding/base64" "fmt" "strings" "testing" ) // ── 1. slide order ────────────────────────────────────────────────────────── // A DECK COMES OUT IN THE ORDER IT IS PRESENTED IN. // // ppt/slides/slideN.xml is numbered in CREATION order. The order a person sees — // the order the deck plays in, and the numbers PowerPoint puts under the slides — // is p:sldIdLst in ppt/presentation.xml, resolved through that part's // relationships. Drag a slide to a new position and the list is rewritten while // the parts keep their old numbers, so the two disagree on any deck anyone has // edited. // // Reading the file numbers instead returns the deck in the wrong sequence AND // labels every slide with a number the user cannot find in their own deck. A // citation to "slide 1" that lands on the deck's third slide is a wrong answer // delivered as a fact. func TestOfficeTextEmitsSlidesInPresentationOrder(t *testing.T) { setupBox(t) // THE FIXTURE STILL DIVERGES. If a regeneration ever renumbers the parts to // match the list — which is exactly what LibreOffice does on write — this // file stops testing anything and the test must say so rather than pass. pres := officePart(t, fxPPTXReordered, "ppt/presentation.xml") lst := pres[strings.Index(pres, ""); i >= 0 { lst = lst[:i] } rels := officePart(t, fxPPTXReordered, "ppt/_rels/presentation.xml.rels") // The first entry of the list must NOT be the first slide part. firstID := relIDAt(t, lst, 0) if got := relTarget(t, rels, firstID); got != "slides/slide3.xml" { t.Fatalf("the fixture's first presented slide is now %q, not slides/slide3.xml — "+ "its file numbering and its p:sldIdLst no longer disagree, so it cannot "+ "exercise the defect:\n sldIdLst: %s", got, lst) } if !strings.Contains(officePart(t, fxPPTXReordered, "ppt/slides/slide1.xml"), "FIRST slide alpha") { t.Fatalf("the fixture's slide1.xml is no longer the deck's created-first slide") } resp, errOut, code := driveOffice(t, fxPPTXReordered) if code != 0 { t.Fatalf("ctx_extract failed: %s", errOut) } // THE SEQUENCE. The deck reads THIRD, FIRST, SECOND. third := strings.Index(resp.Text, "THIRD slide charlie") first := strings.Index(resp.Text, "FIRST slide alpha") second := strings.Index(resp.Text, "SECOND slide bravo") if third < 0 || first < 0 || second < 0 { t.Fatalf("slide text missing:\n%s", resp.Text) } if !(third < first && first < second) { t.Fatalf("the deck came back in the order its slides were CREATED, not the order "+ "it is presented in — this deck plays THIRD, FIRST, SECOND:\n%s", resp.Text) } // THE LABELS. "--- Slide 1 ---" must name the slide the user sees first. if s := officeSection(t, resp.Text, 1); !strings.Contains(s, "THIRD slide charlie") { t.Fatalf("slide 3's text came back labelled slide 1 is the OLD bug; this is its "+ "mirror — the deck's first slide is not under \"--- Slide 1 ---\":\n%s", s) } if s := officeSection(t, resp.Text, 3); !strings.Contains(s, "SECOND slide bravo") { t.Fatalf("the deck's third slide is not under \"--- Slide 3 ---\":\n%s", s) } } // relIDAt returns the r:id of the nth in a sldIdLst fragment. func relIDAt(t *testing.T, lst string, n int) string { t.Helper() rest, seen := lst, 0 for { i := strings.Index(rest, "") if j < 0 { break } if seen == n { return attrValue(t, rest[:j], "r:id") } seen++ } t.Fatalf("the fixture's sldIdLst has no entry %d: %s", n, lst) return "" } // relTarget returns the Target of the Relationship with this Id. func relTarget(t *testing.T, rels, id string) string { t.Helper() for _, chunk := range strings.Split(rels, " head { t.Fatalf("the document's own paragraphs are missing or come after its comments:\n%s", resp.Text) } } // ── 3. hidden sheets and hidden slides ────────────────────────────────────── // A HIDDEN SHEET IS READ AND SAID TO BE HIDDEN. // // The call is written out on the sheetRef.heading comment. In one line: dropping // a hidden sheet loses content silently and a silent loss is the one outcome this // lane never chooses, so it is read — and marked, because a search hit a user // cannot find anywhere in the file they have open is the thing that makes them // distrust the whole corpus. func TestOfficeTextMarksAHiddenSheet(t *testing.T) { setupBox(t) // THE FIXTURE STILL HIDES THEM, and the state lives in the workbook part // rather than in the worksheet — which is why reading the sheets alone can // never tell. wb := officePart(t, fxXLSXHiddenSheet, "xl/workbook.xml") for _, want := range []string{`state="hidden"`, `state="veryHidden"`} { if !strings.Contains(wb, want) { t.Fatalf("the fixture's workbook no longer declares %s — it cannot exercise "+ "the case:\n%s", want, wb) } } if s := officePart(t, fxXLSXHiddenSheet, "xl/worksheets/sheet2.xml"); strings.Contains(s, `state="`) { t.Fatalf("the fixture's hidden worksheet part now carries a state of its own; the " + "point of this case is that only the workbook knows") } resp, errOut, code := driveOffice(t, fxXLSXHiddenSheet) if code != 0 { t.Fatalf("ctx_extract failed: %s", errOut) } // READ: the hidden sheets' contents are there. for _, want := range []string{ "HIDDENPROBE this sheet is hidden scratch delta", "HIDDENPROBE this sheet is veryHidden echo", } { if !strings.Contains(resp.Text, want) { t.Fatalf("a hidden sheet's contents were dropped: %q\n%s", want, resp.Text) } } // MARKED: and the user can tell which ones they were. if !strings.Contains(resp.Text, "=== Sheet: Scratch working (hidden) ===") { t.Fatalf("a hidden sheet's contents went into the corpus with nothing saying it is "+ "hidden — a hit on it sends the user looking for a sheet they cannot see:\n%s", resp.Text) } if !strings.Contains(resp.Text, "=== Sheet: Locked assumptions (very hidden) ===") { t.Fatalf("the veryHidden sheet — the one Excel's own interface cannot unhide — is "+ "not marked:\n%s", resp.Text) } // AND AN ORDINARY SHEET IS NOT MARKED. Marking everything says nothing. if !strings.Contains(resp.Text, "=== Sheet: Summary ===") { t.Fatalf("a visible sheet's heading changed:\n%s", resp.Text) } } // A HIDDEN SLIDE IS READ AND SAID TO BE HIDDEN, for the same reason and with the // same shape. show="0" on the p:sld root is what "Hide Slide" writes: the slide // stays in the deck and in the file, and is skipped when the deck is played. func TestOfficeTextMarksAHiddenSlide(t *testing.T) { setupBox(t) sld := officePart(t, fxPPTXHiddenSlide, "ppt/slides/slide2.xml") if !strings.Contains(sld, `show="0"`) { t.Fatalf("the fixture's slide 2 is no longer hidden — it cannot exercise the case") } if s := officePart(t, fxPPTXHiddenSlide, "ppt/slides/slide1.xml"); strings.Contains(s, `show="0"`) { t.Fatalf("the fixture's slide 1 is hidden too, so the test cannot tell a marked " + "heading from a blanket one") } resp, errOut, code := driveOffice(t, fxPPTXHiddenSlide) if code != 0 { t.Fatalf("ctx_extract failed: %s", errOut) } if !strings.Contains(resp.Text, "HIDSLIDEPROBE second slide is HIDDEN bravo") { t.Fatalf("the hidden slide's text was dropped:\n%s", resp.Text) } if !strings.Contains(resp.Text, "--- Slide 2 (hidden) ---") { t.Fatalf("the hidden slide went into the corpus looking exactly like a slide the "+ "deck actually shows:\n%s", resp.Text) } if !strings.Contains(resp.Text, "--- Slide 1 ---\n") { t.Fatalf("a visible slide's heading changed:\n%s", resp.Text) } } // ── 4. the workbook that stopped ──────────────────────────────────────────── // bigWorkbook builds a workbook whose text runs past ctxExtractMaxText part-way // through its FIRST sheet, with named sheets behind it. // // IT IS GENERATED RATHER THAN EMBEDDED because the shape under test is SIZE — // 4 MiB of emitted text — and a base64 constant of a real writer's 15 MB of sheet // XML would be a multi-hundred-kilobyte source file to assert a property that // has nothing to do with the writer. The row shape it emits is not invented: it // is openpyxl 3.1.5's, copied from a workbook written with it and read back, // which is `… // ` exactly as below. func bigWorkbook(t *testing.T, rows int, sheets ...string) []byte { t.Helper() var sb strings.Builder sb.WriteString(``) for i := 1; i <= rows; i++ { fmt.Fprintf(&sb, `BIGROW short cell value abc`, i, i) } sb.WriteString(``) var decl, rels strings.Builder parts := map[string]string{"xl/worksheets/sheet1.xml": sb.String()} for i, name := range sheets { fmt.Fprintf(&decl, ``, name, i+1, i+1) fmt.Fprintf(&rels, ``, i+1, i+1) if i > 0 { parts[fmt.Sprintf("xl/worksheets/sheet%d.xml", i+1)] = `` + `TAILSHEET ` + name + `` } } parts["xl/workbook.xml"] = `` + decl.String() + `` parts["xl/_rels/workbook.xml.rels"] = `` + rels.String() + `` return officeZip(t, parts) } // A WORKBOOK THAT STOPPED SAYS WHAT IT NEVER GOT TO. // // Emission stops at ctxExtractMaxText, and the sheet it stopped inside says so. // The sheets BEHIND it said nothing at all: a user whose workbook holds // "Assumptions" and "Notes" after a huge data sheet got a reply that read as a // complete extraction of a workbook with one sheet in it. That is the blind // answer readPPTX already refuses to give for a slide with no words on it, and it // is the same answer here. // // This is also where the `_ = stopped` dead variable went: see readXLSX. func TestOfficeTextNamesTheSheetsItNeverReached(t *testing.T) { setupBox(t) pkg := bigWorkbook(t, 200000, "Big data", "Assumptions", "Notes") resp, errOut, code := driveOffice(t, base64.StdEncoding.EncodeToString(pkg)) if code != 0 { t.Fatalf("ctx_extract failed: %s", errOut) } // THE INPUT REALLY IS OVER THE WALL. Without this the rest passes vacuously // the day ctxExtractMaxText moves or the row shape shrinks. Either signal // proves it: the sheet said it stopped, or the reply itself was cut. if !strings.Contains(resp.Text, "this workbook is larger") && !resp.Truncated { t.Fatalf("the generated workbook no longer overruns one extraction, so it cannot "+ "exercise the case — %d chars, cap %d", len(resp.Text), ctxExtractMaxText) } if strings.Contains(resp.Text, "TAILSHEET Assumptions") { t.Fatalf("the generated workbook is small enough to reach its later sheets") } // THE ASSERTION: the reply names the sheets it never opened. for _, want := range []string{"Assumptions", "Notes"} { if !strings.Contains(resp.Text, want) { t.Fatalf("the extraction stopped inside the first sheet and never said that "+ "%q exists — the reply reads as a complete extraction of a one-sheet "+ "workbook.\n tail: %q", want, tailOf(resp.Text, 400)) } } if !strings.Contains(resp.Text, "2 more sheets in this workbook were not read") { t.Fatalf("the reply does not say how many sheets went unread:\n tail: %q", tailOf(resp.Text, 400)) } // AND IT SURVIVES THE REPLY'S OWN TRUNCATION. resp.Text is what the host // receives, after contextextract.go cuts anything past ctxExtractMaxText — // so a marker written past the cap would be the one sentence thrown away. if resp.Truncated { t.Fatalf("the workbook's closing markers were written past the reply's own cap and "+ "cut back off: chars=%d of %d", resp.Chars, resp.CharsAll) } } func tailOf(s string, n int) string { if len(s) <= n { return s } return s[len(s)-n:] }