Files
wehub-resource-sync fed8b2eed7
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

312 lines
12 KiB
Python

#!/usr/bin/env python3
"""Regenerate e2e fixture documents.
Do not edit generated fixtures by hand. Run this script.
Usage (from repo root):
.venv/bin/python tests/e2e/fixtures/docs/_generate.py
The script is deterministic and idempotent: running it twice produces
byte-identical output. No dates, UUIDs, or randomness are included in any
fixture. Content is intentionally stable so chunk-level assertions in e2e
specs remain reliable across runs.
Stdlib only. If ``reportlab`` is available we could use it for ``small.pdf``,
but we prefer the raw-bytes path to keep the fixture byte-stable regardless
of reportlab version drift.
"""
from __future__ import annotations
import sys
from pathlib import Path
FIXTURES_DIR = Path(__file__).resolve().parent
# ---------------------------------------------------------------------------
# PDF helpers (raw bytes)
# ---------------------------------------------------------------------------
def _escape_pdf_text(text: str) -> str:
"""Escape characters that are special inside a PDF literal string."""
return (
text.replace("\\", "\\\\")
.replace("(", "\\(")
.replace(")", "\\)")
)
def _build_minimal_pdf(body_lines: list[str], extra_bytes: bytes = b"") -> bytes:
"""Build a valid single-page PDF containing ``body_lines`` of Helvetica text.
Structure (5 objects + catalog/pages/page/font/contents):
1 0 obj Catalog
2 0 obj Pages
3 0 obj Page
4 0 obj Font (Helvetica)
5 0 obj Contents stream
``extra_bytes`` are injected as a PDF comment block right after the
header. PDF comments (lines beginning with ``%`` up to EOL) are legal
anywhere outside streams/strings and are treated as whitespace by every
compliant parser. This is how we pad ``oversize.pdf`` without breaking
validity.
"""
# Build the content stream (page drawing instructions).
# 72pt = 1 inch. Letter page = 612 x 792.
y = 740
stream_parts = ["BT", "/F1 12 Tf"]
for i, line in enumerate(body_lines):
if i == 0:
stream_parts.append(f"72 {y} Td")
else:
stream_parts.append("0 -16 Td")
stream_parts.append(f"({_escape_pdf_text(line)}) Tj")
stream_parts.append("ET")
content_stream = ("\n".join(stream_parts) + "\n").encode("ascii")
# Build each object's bytes.
objs: list[bytes] = [
b"<< /Type /Catalog /Pages 2 0 R >>",
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
(
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] "
b"/Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>"
),
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
(
b"<< /Length " + str(len(content_stream)).encode("ascii") + b" >>\n"
b"stream\n" + content_stream + b"endstream"
),
]
out = bytearray()
out += b"%PDF-1.4\n"
# Binary marker comment recommended by the PDF spec for "binary" PDFs.
out += b"%\xe2\xe3\xcf\xd3\n"
if extra_bytes:
out += extra_bytes
offsets: list[int] = []
for i, body in enumerate(objs, start=1):
offsets.append(len(out))
out += f"{i} 0 obj\n".encode("ascii")
out += body
out += b"\nendobj\n"
xref_offset = len(out)
out += f"xref\n0 {len(objs) + 1}\n".encode("ascii")
out += b"0000000000 65535 f \n"
for off in offsets:
out += f"{off:010d} 00000 n \n".encode("ascii")
out += b"trailer\n"
out += f"<< /Size {len(objs) + 1} /Root 1 0 R >>\n".encode("ascii")
out += b"startxref\n"
out += f"{xref_offset}\n".encode("ascii")
out += b"%%EOF\n"
return bytes(out)
# ---------------------------------------------------------------------------
# Fixture generators
# ---------------------------------------------------------------------------
SMALL_PDF_LINES = [
"The Unicode Consortium was founded in January 1991.",
"Its mission is to enable people around the world to use computers in any language.",
"The consortium maintains the Unicode Standard, a text-encoding specification.",
"Unicode assigns a unique code point to every character across modern scripts.",
"Today the standard covers more than one hundred forty thousand characters.",
]
README_MD = """# Introduction to UTF-8 Encoding
UTF-8 is a variable-width character encoding capable of representing every
character in the Unicode standard. It was designed as a backward-compatible
replacement for ASCII, and it has become the dominant encoding for text on
the web and in most modern file formats.
## How UTF-8 Works
UTF-8 encodes each Unicode code point as one to four bytes. The number of
bytes used depends on the numeric value of the code point. Characters in the
ASCII range use a single byte identical to the ASCII byte, which is why any
valid ASCII text is also a valid UTF-8 text. Higher code points use a lead
byte that signals how many continuation bytes follow, and continuation bytes
always begin with the bit pattern ten.
The design has a number of useful properties. Byte boundaries cannot be
mistaken for character boundaries, because continuation bytes never look like
lead bytes. A corrupted or truncated stream can be resynchronised by scanning
forward to the next byte that is not a continuation byte. Sorting UTF-8
strings lexicographically by byte value produces the same order as sorting by
Unicode code point.
## Example
Below is a short Python snippet that encodes and decodes a UTF-8 string.
```python
text = "hello"
data = text.encode("utf-8")
again = data.decode("utf-8")
assert again == text
```
## Key Advantages
- Compact for Latin-script text, because ASCII characters use only one byte.
- Self-synchronising, which makes error recovery straightforward.
- A strict superset of ASCII, so legacy ASCII tools handle it gracefully.
- Well supported by every major programming language and operating system.
- Avoids the byte-order ambiguity that affects UTF-16 and UTF-32.
UTF-8 is recommended by the Internet Engineering Task Force as the default
encoding for web content, email headers, and most other text-based protocols.
Using it consistently across an application removes an entire class of
encoding-related bugs.
"""
NOTES_TXT = (
"Notes on multilingual text handling.\n"
"\n"
"These notes illustrate a handful of typographic and script considerations\n"
"that software dealing with international text must keep in mind. The\n"
"examples intentionally mix several writing systems so that a parser can be\n"
"exercised end to end.\n"
"\n"
"Latin script covers English, French, German, Spanish and many more. The\n"
"em-dash \u2014 together with the en-dash \u2013 is used for pauses and\n"
"ranges. Curly quotes such as \u201chello\u201d and \u2018world\u2019 are\n"
"preferred in polished prose, while straight quotes remain common in\n"
"source code. An ellipsis character \u2026 compresses three dots into a\n"
"single glyph.\n"
"\n"
"Cyrillic script is used for Russian, Ukrainian, Bulgarian and several\n"
"other languages. A short sample: \u041f\u0440\u0438\u0432\u0435\u0442,\n"
"\u043c\u0438\u0440. \u042d\u0442\u043e \u043f\u0440\u043e\u0441\u0442\u043e\u0439\n"
"\u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0439 \u0442\u0435\u043a\u0441\u0442.\n"
"\n"
"CJK ideographs cover written Chinese, Japanese and Korean. A short\n"
"sample of each: \u4f60\u597d\u4e16\u754c. \u3053\u3093\u306b\u3061\u306f\u4e16\u754c.\n"
"\uc548\ub155\ud558\uc138\uc694 \uc138\uacc4.\n"
"\n"
"Punctuation worth noting includes the middle dot \u00b7, the section sign\n"
"\u00a7, the pilcrow \u00b6 and the interrobang \u203d. A well-behaved text\n"
"pipeline preserves each of these exactly, normalises line endings to a\n"
"single convention, and never silently replaces characters it does not\n"
"understand.\n"
)
def write_small_pdf(path: Path) -> None:
path.write_bytes(_build_minimal_pdf(SMALL_PDF_LINES))
def write_readme(path: Path) -> None:
path.write_text(README_MD, encoding="utf-8")
def write_notes(path: Path) -> None:
path.write_text(NOTES_TXT, encoding="utf-8")
def write_corrupt_pdf(path: Path) -> None:
# Header bytes that look like a PDF, then deterministic non-PDF garbage.
# Do NOT include real PDF objects — this file must fail to parse.
header = b"%PDF-1.4\n"
garbage = (
b"this is intentionally not a valid pdf body -- used for error-path "
b"testing in the docsgpt e2e suite. no xref table, no objects, no "
b"trailer. a compliant parser must reject this file.\n"
) * 8
path.write_bytes(header + garbage)
def write_oversize_pdf(path: Path, target_bytes: int = 55 * 1024 * 1024) -> None:
"""Write a valid PDF padded to roughly ``target_bytes`` (default ~55 MB).
The padding lives inside a PDF comment block placed immediately after the
header. Comments are legal whitespace in PDF, so the file still parses.
"""
# Build an unpadded reference PDF to measure overhead. We pad the
# comment block so that the final file size matches target_bytes exactly.
reference = _build_minimal_pdf(SMALL_PDF_LINES)
overhead = len(reference)
# Reserve space for the comment frame itself: "%" + "\n".
# Each comment line is at most ~80 chars to stay friendly to parsers.
# We compute a deterministic filler string of the exact needed length.
#
# NOTE: adding ``extra_bytes`` shifts every object offset, but since
# _build_minimal_pdf recomputes the xref from actual offsets, the final
# file remains valid. The only subtlety is that xref/startxref digit
# widths change as the file grows; for a 55 MB file we're well within
# the 10-digit xref offset format, so no length drift occurs.
needed = max(0, target_bytes - overhead)
# Build the filler as repeating 79-char comment lines + newline = 80 bytes.
line_body = b"%" + b"x" * 78 # 79 bytes of comment content
line = line_body + b"\n" # 80 bytes per line
num_full_lines = needed // len(line)
remainder = needed - num_full_lines * len(line)
filler = line * num_full_lines
if remainder > 0:
# Pad the tail with a shorter comment line so total byte count matches.
# remainder is at least 2 here? Not necessarily — but it's always >= 0.
# Shortest valid comment line is "%\n" (2 bytes). If remainder == 1,
# we emit one extra byte into a preceding line instead.
if remainder == 1:
# Replace the last full line with an 81-byte version.
if num_full_lines > 0:
filler = line * (num_full_lines - 1) + b"%" + b"x" * 79 + b"\n"
else:
# Degenerate: just drop the last byte from the target.
filler = b""
else:
tail_body_len = remainder - 2 # minus "%" and "\n"
tail = b"%" + b"x" * tail_body_len + b"\n"
filler += tail
blob = _build_minimal_pdf(SMALL_PDF_LINES, extra_bytes=filler)
path.write_bytes(blob)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main() -> int:
FIXTURES_DIR.mkdir(parents=True, exist_ok=True)
generators = [
("small.pdf", write_small_pdf),
("readme.md", write_readme),
("notes.txt", write_notes),
("corrupt.pdf", write_corrupt_pdf),
("oversize.pdf", write_oversize_pdf),
]
for name, fn in generators:
target = FIXTURES_DIR / name
fn(target)
size = target.stat().st_size
print(f"wrote {name:<14} {size:>10} bytes", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())