From 993df7f5a8778bee2f549352d1c4175548d98b77 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Tue, 13 Oct 2026 12:07:36 +0300 Subject: [PATCH 1/2] Add upstream release gates and manifest metadata The release workflow now blocks publishes that miss stale upstream PDFium commits, checks fork tags, layer mode, writable globals, and TSAN soak, and records the upstream base in release notes and manifest metadata. Added a script to report upstream freshness and cherry-pick requirements, updated the mirror job to use it, and documented the release gate policy and override flow. --- .github/workflows/release-libpdfium.yml | 88 +++++++++- .github/workflows/upstream-mirror.yml | 21 +-- docs/embedpdf/upstream-sync.md | 20 +++ .../check-upstream-freshness.py | 161 ++++++++++++++++++ scripts/embedpdf-runtime/write-manifest.sh | 15 +- 5 files changed, 278 insertions(+), 27 deletions(-) create mode 100755 scripts/embedpdf-runtime/check-upstream-freshness.py diff --git a/.github/workflows/release-libpdfium.yml b/.github/workflows/release-libpdfium.yml index ec2720c702..9ecd6fc1d0 100644 --- a/.github/workflows/release-libpdfium.yml +++ b/.github/workflows/release-libpdfium.yml @@ -15,13 +15,74 @@ on: type: boolean required: false default: false + upstream_gate_override_reason: + description: >- + Publish while missing upstream PDFium commits older than 2 days: why. + The release notes list them; likely security fixes must be cherry-picked + (docs/embedpdf/upstream-sync.md) + required: false + default: '' permissions: contents: read jobs: + # The release gates (docs/embedpdf/upstream-sync.md, "Release gates"). + upstream-gate: + name: Upstream freshness and fork tags + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref || github.ref_name }} + fetch-depth: 0 + + - name: Fetch upstream PDFium + shell: bash + run: | + for attempt in 1 2 3 4 5; do + if git fetch --no-tags https://pdfium.googlesource.com/pdfium main:refs/remotes/upstream/main; then + exit 0 + fi + sleep $((attempt * 30)) + done + exit 1 + + # Enforced only when publishing: sync branches are behind upstream on + # purpose until they merge. + - name: Check upstream freshness + shell: bash + env: + ENFORCE: ${{ inputs.release == true }} + OVERRIDE_REASON: ${{ inputs.upstream_gate_override_reason }} + run: | + mkdir -p out/upstream-gate + args=(--notes out/upstream-gate/notes.md --json out/upstream-gate/upstream.json) + if [[ "$ENFORCE" == true ]]; then + args+=(--enforce) + fi + python3 scripts/embedpdf-runtime/check-upstream-freshness.py "${args[@]}" \ + --override-reason "$OVERRIDE_REASON" + + - name: Check fork tags + shell: bash + run: python3 scripts/embedpdf-runtime/check-fork-tags.py --strict + + - uses: actions/upload-artifact@v6 + with: + name: upstream-gate + path: out/upstream-gate/ + + tsan-soak: + name: ThreadSanitizer soak + needs: upstream-gate + uses: ./.github/workflows/pdfium-tsan-thread-soak.yml + with: + ref: ${{ inputs.ref || github.ref_name }} + source-tests: name: Source tests (Linux x64) + needs: upstream-gate runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 @@ -51,6 +112,12 @@ jobs: shell: bash run: scripts/embedpdf-runtime/test-target.sh linux-x64 + - name: Run embedder tests in layer mode + shell: bash + run: | + python3 testing/tools/run_layer_mode.py out/embedpdf-runtime-tests-linux-x64 \ + --out out/embedpdf-runtime-test-results/linux-x64/layer-mode + - name: Upload PDFium test results if: always() uses: actions/upload-artifact@v6 @@ -168,6 +235,11 @@ jobs: - name: Build target shell: bash run: scripts/embedpdf-runtime/build-target.sh "${{ matrix.target }}" + # The native server runs this target on several threads in one process. + - name: Check writable globals + if: matrix.target == 'linux-x64' + shell: bash + run: python3 scripts/embedpdf-runtime/check-globals.py out/embedpdf-runtime/linux-x64/libembedpdf.so - name: Package target shell: bash run: scripts/embedpdf-runtime/package-target.sh "${{ matrix.target }}" @@ -179,7 +251,7 @@ jobs: release: name: Publish release runs-on: ubuntu-24.04 - needs: build + needs: [upstream-gate, tsan-soak, source-tests, build] if: github.event_name == 'workflow_dispatch' && inputs.release == true permissions: contents: write @@ -195,16 +267,24 @@ jobs: run: | mkdir -p out/embedpdf-runtime-artifacts find artifacts -name '*.tar.gz' -exec cp {} out/embedpdf-runtime-artifacts/ \; + # The built ref's commit, which can differ from the commit the workflow + # was dispatched from. + - name: Name the release + id: name + shell: bash + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Write manifest shell: bash run: | - tag="runtime-$(git rev-parse HEAD)" + tag="runtime-${{ steps.name.outputs.sha }}" export PDF_RUNTIME_ARTIFACT_DIR="$PWD/out/embedpdf-runtime-artifacts" export PDF_RUNTIME_RELEASE_BASE_URL="https://github.com/${{ github.repository }}/releases/download/$tag" + export PDF_RUNTIME_UPSTREAM_FILE="$PWD/artifacts/upstream-gate/upstream.json" scripts/embedpdf-runtime/write-manifest.sh "$PDF_RUNTIME_ARTIFACT_DIR/pdf-runtime-build.generated.json" - uses: ncipollo/release-action@v1 with: - tag: runtime-${{ github.sha }} - name: EmbedPDF PDF Runtime ${{ github.sha }} + tag: runtime-${{ steps.name.outputs.sha }} + name: EmbedPDF PDF Runtime ${{ steps.name.outputs.sha }} + bodyFile: artifacts/upstream-gate/notes.md artifacts: out/embedpdf-runtime-artifacts/* allowUpdates: true diff --git a/.github/workflows/upstream-mirror.yml b/.github/workflows/upstream-mirror.yml index 1f92f716e2..2dc4a645fa 100644 --- a/.github/workflows/upstream-mirror.yml +++ b/.github/workflows/upstream-mirror.yml @@ -35,23 +35,4 @@ jobs: - name: Report what embedpdf/main hasn't merged shell: bash - run: | - set -euo pipefail - base=$(git merge-base HEAD upstream/main) - count=$(git rev-list --count "$base..upstream/main") - oldest=$(git log --reverse --format=%ci "$base..upstream/main" | head -1) - { - echo "## Upstream PDFium not in embedpdf/main" - echo - echo "- Base: \`$(git log -1 --format='%h %ad %s' --date=short "$base")\`" - echo "- Unmerged upstream commits: **$count**${oldest:+, the oldest from $oldest}" - echo - echo "### Likely security fixes" - echo - } >> "$GITHUB_STEP_SUMMARY" - git log --format='%h%x09%ad%x09%s' --date=short "$base..upstream/main" | - grep -iE 'overflow|crash|out[- ]of[- ]bounds|use[- ]after[- ]free|heap|stack[- ]buffer|null deref|fuzz|security|sanitiz' | - while IFS=$'\t' read -r hash date subject; do - echo "- \`$hash\` $date $subject" >> "$GITHUB_STEP_SUMMARY" - echo "::warning title=Likely upstream security fix::$hash $subject" - done || true + run: python3 scripts/embedpdf-runtime/check-upstream-freshness.py diff --git a/docs/embedpdf/upstream-sync.md b/docs/embedpdf/upstream-sync.md index 5699e03a4b..a8bc04fe2e 100644 --- a/docs/embedpdf/upstream-sync.md +++ b/docs/embedpdf/upstream-sync.md @@ -75,6 +75,26 @@ All commands run from the fork's root. `BUILD.gn.orig`, so only the merge is committed. 7. **Open a PR into `embedpdf/main`** with the report. +## Release gates + +`release-libpdfium.yml` runs these before anything is published, on every run: + +- **Upstream freshness** (`check-upstream-freshness.py`): lists the upstream + commits the ref misses, likely security fixes marked. With `release` on it + fails when the oldest one landed more than 2 days ago. The release notes carry + the list, and the manifest the upstream commit the release is based on. +- **Fork tags** (`check-fork-tags.py --strict`). +- **Layer mode** on the Linux test build (`run_layer_mode.py`). +- **Writable globals** on the linux-x64 library (`check-globals.py`). +- **ThreadSanitizer soak** (`pdfium-tsan-thread-soak.yml`). + +**The override.** When a sync can't land before a release, set +`upstream_gate_override_reason`. The release then publishes with a warning, and +its notes give the reason and every upstream commit it misses. Every likely +security fix it holds back must be cherry-picked into the release first, with +`git cherry-pick -x` (the gate checks for the `cherry picked from commit` line). +The next release tries the full sync again. + ## Never silently - Take upstream's side of a tagged block without a line in the report. diff --git a/scripts/embedpdf-runtime/check-upstream-freshness.py b/scripts/embedpdf-runtime/check-upstream-freshness.py new file mode 100755 index 0000000000..86f25c855a --- /dev/null +++ b/scripts/embedpdf-runtime/check-upstream-freshness.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +# Copyright 2026 CloudPDF LTD +# SPDX-License-Identifier: Apache-2.0 + +"""Check that HEAD misses no upstream PDFium commit older than 2 days. + +The release gate of docs/embedpdf/upstream-sync.md: a release may not miss an +upstream commit that was more than 2 days old when the release started. It +lists the upstream commits HEAD doesn't contain (`git log ^HEAD`), +marks the ones that look like security fixes, and finds the upstream commit +HEAD is based on (the merge base), which the release manifest records. + +It fails, with --enforce, when the oldest missing commit is older than +--max-age-hours. With --override-reason it warns instead, but a missing commit +that looks like a security fix must still be in HEAD as a cherry-pick: a commit +since the base whose message says `(cherry picked from commit )` +(`git cherry-pick -x`). Without --enforce it only reports, as the mirror job and +release=false runs do. + +Commit times are committer dates: when the commit landed upstream. + +Usage: + check-upstream-freshness.py [--upstream REF] [--max-age-hours N] [--enforce] + [--override-reason TEXT] [--notes FILE] + [--json FILE] + + --upstream upstream main, fetched. Default: upstream/main. + --notes write the report as Markdown (release notes, run summary). + --json write {sha, date, subject} of the upstream base, plus the + missing count and the override, for the release manifest. + +In GitHub Actions the report also goes to the run summary, and each likely +security fix becomes a warning. +""" + +import argparse +import json +import os +import re +import subprocess +import sys +import time + +SOURCE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) + +# Subjects that look like security fixes; the mirror job runs this script too. +# Not "fuzz" (fuzzer build changes, fuzzy image matching: fixes a fuzzer found +# say crash or overflow) nor a bare "heap" (heap allocators). +SECURITY = re.compile( + r'overflow|crash|out[- ]of[- ]bounds|\boob\b|use[- ]after[- ]free|\buaf\b|' + r'(?:heap|stack)[- ]buffer|null(?:ptr)? deref|security|sanitiz|\bcve\b', + re.IGNORECASE) +CHERRY_PICK = re.compile(r'\(cherry picked from commit ([0-9a-f]{40})\)') + + +def git(*args): + return subprocess.run(['git', '-C', SOURCE_DIR, *args], + check=True, + capture_output=True, + text=True, + errors='replace').stdout + + +def commits(*rev_args): + """Returns [(sha, unix time, ISO date, subject)], newest first.""" + out = git('log', '--format=%H%x09%ct%x09%cI%x09%s', *rev_args) + result = [] + for line in out.splitlines(): + sha, stamp, date, subject = line.split('\t', 3) + result.append((sha, int(stamp), date, subject)) + return result + + +def cherry_picked(base): + return set(CHERRY_PICK.findall(git('log', '--format=%B', f'{base}..HEAD'))) + + +def age_text(seconds): + hours = seconds / 3600 + return f'{hours / 24:.1f} days' if hours >= 48 else f'{hours:.0f} hours' + + +def main(): + parser = argparse.ArgumentParser( + description=__doc__.split('\n\n')[0], + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('--upstream', default='upstream/main') + parser.add_argument('--max-age-hours', type=float, default=48) + parser.add_argument('--enforce', action='store_true') + parser.add_argument('--override-reason', default='') + parser.add_argument('--notes') + parser.add_argument('--json') + args = parser.parse_args() + + now = time.time() + base = git('merge-base', 'HEAD', args.upstream).strip() + base_sha, _, base_date, base_subject = commits('-1', base)[0] + missing = commits(args.upstream, '^HEAD') + security = [c for c in missing if SECURITY.search(c[3])] + picked = cherry_picked(base) + oldest = missing[-1] if missing else None + stale = bool(oldest) and now - oldest[1] > args.max_age_hours * 3600 + override = args.override_reason.strip() + unpicked = [c for c in security if c[0] not in picked] + + lines = [ + '## Upstream PDFium', + '', + f'- Based on upstream `{base_sha[:9]}` ({base_date[:10]}) {base_subject}', + ] + if oldest: + lines.append(f'- Missing {len(missing)} upstream commits; the oldest landed ' + f'{age_text(now - oldest[1])} ago (limit: ' + f'{age_text(args.max_age_hours * 3600)}).') + else: + lines.append('- Missing no upstream commit.') + if stale and override: + lines.append(f'- **Gate overridden:** {override}') + if missing: + lines += ['', '### Missing upstream commits', ''] + for commit in missing: + sha, _, date, subject = commit + mark = '' + if commit in security: + mark = (' (likely security fix; cherry-picked)' + if sha in picked else ' (**likely security fix**)') + lines.append(f'- `{sha[:9]}` {date[:10]} {subject}{mark}') + report = '\n'.join(lines) + '\n' + + print(report) + if os.environ.get('GITHUB_STEP_SUMMARY'): + with open(os.environ['GITHUB_STEP_SUMMARY'], 'a', encoding='utf-8') as f: + f.write(report) + if os.environ.get('GITHUB_ACTIONS'): + for sha, _, _, subject in security: + print(f'::warning title=Likely upstream security fix::{sha[:9]} {subject}') + if args.notes: + with open(args.notes, 'w', encoding='utf-8') as f: + f.write(report) + if args.json: + with open(args.json, 'w', encoding='utf-8') as f: + json.dump({'sha': base_sha, 'date': base_date, 'subject': base_subject, + 'missing': len(missing), 'override': override if stale else ''}, + f, indent=2) + f.write('\n') + + if not args.enforce or not stale: + return + if not override: + sys.exit(f'error: missing upstream commits older than ' + f'{age_text(args.max_age_hours * 3600)}: sync first ' + '(docs/embedpdf/upstream-sync.md), or pass an override reason.') + if unpicked: + sys.exit('error: an override still needs every likely security fix it ' + 'holds back, cherry-picked with -x: ' + + ', '.join(c[0][:9] for c in unpicked)) + print('warning: release gate overridden: ' + override) + + +if __name__ == '__main__': + main() diff --git a/scripts/embedpdf-runtime/write-manifest.sh b/scripts/embedpdf-runtime/write-manifest.sh index f53eb7c211..dee9a1ccf3 100755 --- a/scripts/embedpdf-runtime/write-manifest.sh +++ b/scripts/embedpdf-runtime/write-manifest.sh @@ -7,6 +7,9 @@ set -euo pipefail SOURCE_DIR="${PDF_RUNTIME_SOURCE_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" ARTIFACT_DIR="${PDF_RUNTIME_ARTIFACT_DIR:-$SOURCE_DIR/out/embedpdf-runtime-artifacts}" RELEASE_BASE_URL="${PDF_RUNTIME_RELEASE_BASE_URL:-}" +# The upstream PDFium commit the release is based on: the --json output of +# check-upstream-freshness.py, which the release gate writes. +UPSTREAM_FILE="${PDF_RUNTIME_UPSTREAM_FILE:-}" OUT_FILE="${1:-$ARTIFACT_DIR/pdf-runtime-build.generated.json}" SHA="$(git -C "$SOURCE_DIR" rev-parse HEAD)" @@ -14,13 +17,19 @@ if [[ -z "$RELEASE_BASE_URL" ]]; then echo "PDF_RUNTIME_RELEASE_BASE_URL is required" >&2 exit 1 fi +if [[ -z "$UPSTREAM_FILE" || ! -f "$UPSTREAM_FILE" ]]; then + echo "PDF_RUNTIME_UPSTREAM_FILE is required (check-upstream-freshness.py --json)" >&2 + exit 1 +fi -node - <<'NODE' "$ARTIFACT_DIR" "$RELEASE_BASE_URL" "$OUT_FILE" "$SHA" +node - <<'NODE' "$ARTIFACT_DIR" "$RELEASE_BASE_URL" "$OUT_FILE" "$SHA" "$UPSTREAM_FILE" const fs = require('node:fs'); const path = require('node:path'); const crypto = require('node:crypto'); -const [artifactDir, releaseBaseUrl, outFile, sha] = process.argv.slice(2); +const [artifactDir, releaseBaseUrl, outFile, sha, upstreamFile] = process.argv.slice(2); +const gate = JSON.parse(fs.readFileSync(upstreamFile, 'utf8')); +const upstream = { sha: gate.sha, date: gate.date }; const artifacts = {}; for (const file of fs.readdirSync(artifactDir)) { @@ -37,7 +46,7 @@ for (const file of fs.readdirSync(artifactDir)) { fs.writeFileSync( outFile, - JSON.stringify({ fork: 'embedpdf/runtime', sha, artifacts }, null, 2) + '\n', + JSON.stringify({ fork: 'embedpdf/runtime', sha, upstream, artifacts }, null, 2) + '\n', ); NODE From 942bb5cee9036573b91708147c0f0c15a1b6df61 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Tue, 13 Oct 2026 12:44:12 +0300 Subject: [PATCH 2/2] Fix globals check and layer-mode expectations This change fixes the ELF globals checker to recognize TLS symbols and ignore section-definition symbols, and expands the runtime allowlist for true singleton and runtime globals. It also updates the layer-mode test expectations and writer list to account for new platform- and form-related failures and recent test coverage. --- scripts/embedpdf-runtime/check-globals.py | 10 +++++++--- scripts/embedpdf-runtime/globals-allowlist.txt | 7 ++++++- testing/embedpdf_layer_mode_expectations.txt | 4 ++++ testing/embedpdf_layer_mode_writers.txt | 1 + testing/tools/run_layer_mode.py | 4 +++- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/scripts/embedpdf-runtime/check-globals.py b/scripts/embedpdf-runtime/check-globals.py index 66f0b281b8..53b9024ae9 100755 --- a/scripts/embedpdf-runtime/check-globals.py +++ b/scripts/embedpdf-runtime/check-globals.py @@ -51,8 +51,9 @@ THREAD_LOCAL_SECTIONS = ('.tdata', '.tbss', '__DATA,__thread_vars') # `
O
[] [.hidden] `; ELF has the size. -SYMBOL = re.compile(r'^[0-9a-f]+\s.*?\sO\s+(\S+)\s+(?:[0-9a-f]{8,}\s+)?' - r'(?:\.hidden\s+)?(.+)$') +# ELF thread-local symbols (STT_TLS) have no `O` flag: `
l .tbss ...`. +SYMBOL = re.compile(r'^[0-9a-f]+\s.*?\s(?:O\s+(\S+)|(\.tbss|\.tdata))\s+' + r'(?:[0-9a-f]{8,}\s+)?(?:\.hidden\s+)?(.+)$') def normalize(name, macho): @@ -76,7 +77,10 @@ def read_symbols(library): m = SYMBOL.match(line) if not m: continue - section, name = m.group(1), normalize(m.group(2).strip(), macho) + section = m.group(1) or m.group(2) + name = normalize(m.group(3).strip(), macho) + if name == section: # the section's own symbol + continue if section in SHARED_SECTIONS: shared.add(name) elif section in THREAD_LOCAL_SECTIONS: diff --git a/scripts/embedpdf-runtime/globals-allowlist.txt b/scripts/embedpdf-runtime/globals-allowlist.txt index 1104e5f5d4..0a8d56a54f 100644 --- a/scripts/embedpdf-runtime/globals-allowlist.txt +++ b/scripts/embedpdf-runtime/globals-allowlist.txt @@ -46,6 +46,7 @@ set-once | (anonymous namespace)::AddStandardFont(CPDF_Document*)::encoding | Up set-once | guard variable for (anonymous namespace)::AddStandardFont(CPDF_Document*)::encoding | Its initialization guard. set-once | fxcodec::(anonymous namespace)::Basis()::basis | The JPEG writer's DCT basis: a const table built once (thread-safe static initialization) and only read after. set-once | guard variable for fxcodec::(anonymous namespace)::Basis()::basis | Its initialization guard. +set-once | CStretchEngine::Expand1BppRow(*)::kBitsToBytes | Upstream: a table built at compile time (constinit) and only read; not const, so it sits in writable data. # --- Third-party code, shared by design. third-party | std::__Cr::* | libc++: locale and facet state behind its own locks. @@ -77,7 +78,8 @@ third-party | known_tables | HarfBuzz subsetter: a constant table. third-party | langs | HarfBuzz: the language list, behind atomics. third-party | endchar_str | HarfBuzz CFF: a constant. third-party | only_cleanup_pass_is_decoded | openjpeg: a one-time warning flag, set under a mutex. -third-party | Cr_z_arm_cpu_enable_* | zlib: CPU features, detected once. +third-party | Cr_z_*_cpu_enable_* | zlib: CPU features, detected once (under cpu_check_inited_once). +third-party | cpu_check_inited_once | zlib: the pthread_once flag of its CPU check. third-party | cpu_info_ | libyuv: CPU features, detected on first use; read and written with relaxed atomics, and every thread detects the same value. third-party | SkFibonacci47 | Skia path ops: a constant table. third-party | SkMatrix::I()::identity | Skia path ops: a constant. @@ -86,3 +88,6 @@ third-party | SkPathData::PeekEmptySingleton()::gEmpty | Skia path ops: a once-b third-party | guard variable for SkPath*::Peek* | Skia path ops: initialization guards. third-party | next_pathdata_unique_id()::nextID | Skia path ops: an atomic counter. third-party | fast_float::detail::rounds_to_nearest()::fmin | fast_float: a volatile constant. +third-party | __dso_handle | The C runtime (ELF): this library's handle for atexit, set when it loads. +third-party | DW.ref.__gxx_personality_v0 | The C++ runtime (ELF): the exception personality's address, set when the library loads. +third-party | completed.0 | The C runtime (ELF, crtstuff): set once, when the library's destructors run at unload. diff --git a/testing/embedpdf_layer_mode_expectations.txt b/testing/embedpdf_layer_mode_expectations.txt index a97488f8f0..c5b8177ff8 100644 --- a/testing/embedpdf_layer_mode_expectations.txt +++ b/testing/embedpdf_layer_mode_expectations.txt @@ -15,11 +15,15 @@ FPDFFormFill*.* Drives the form-fill environment. CPDFSDKBAAnnotTest.* Drives the form-fill environment. CPDFSDKAnnotIteratorTest.* Drives the form-fill environment. FPDFAnnotEmbedderTest.FocusableAnnotSubtypes Drives the form-fill environment. +FPDFAnnotEmbedderTest.FocusableAnnotRendering Drives the form-fill environment. FPDFProgressiveRenderEmbedderTest.RenderFormWithColorScheme Drives the form-fill environment. +FPDFProgressiveRenderEmbedderTest.RenderFormsWithPause Drives the form-fill environment. FPDFAnnotEmbedderTest.GetFontSize* Needs the form-fill page view (FPDF_FORMHANDLE APIs). FPDFAnnotEmbedderTest.GetFontColor Needs the form-fill page view (FPDF_FORMHANDLE APIs). FPDFAnnotEmbedderTest.IsChecked* Needs the form-fill page view (FPDF_FORMHANDLE APIs). FPDFAnnotEmbedderTest.GetFormFieldExportValue* Needs the form-fill page view (FPDF_FORMHANDLE APIs). +FPDFFlattenEmbedderTest.Bug861842 Pixel test of a form; layer mode doesn't draw with FPDF_FFLDraw. +FPDFFlattenEmbedderTest.Bug889099 Pixel test of a form; layer mode doesn't draw with FPDF_FFLDraw. FPDFFlattenEmbedderTest.Bug890322 Pixel test of a form; layer mode doesn't draw with FPDF_FFLDraw. FPDFFlattenEmbedderTest.Bug896366 Pixel test of a form; layer mode doesn't draw with FPDF_FFLDraw. diff --git a/testing/embedpdf_layer_mode_writers.txt b/testing/embedpdf_layer_mode_writers.txt index 14fa1a378d..aedb9a6406 100644 --- a/testing/embedpdf_layer_mode_writers.txt +++ b/testing/embedpdf_layer_mode_writers.txt @@ -268,6 +268,7 @@ FPDFTransformEmbedderTest.SetCropBox 1 FPDFTransformEmbedderTest.SetMediaBox 1 FPDFTransformEmbedderTest.TransFormWithClip 9 FPDFTransformEmbedderTest.TransFormWithClipAndSave 5 +FPDFTransformEmbedderTest.TransFormWithClipAndSaveWithLocale 5 PDFEditImgTest.GetAndSetMatrixForFormWithImage 3 RichTextParityTest.DecorationsFixtureMatchesAcrobat 2 RichTextParityTest.LinesFixtureMatchesAcrobat 2 diff --git a/testing/tools/run_layer_mode.py b/testing/tools/run_layer_mode.py index bc7e73706d..3295b0b5d6 100755 --- a/testing/tools/run_layer_mode.py +++ b/testing/tools/run_layer_mode.py @@ -199,7 +199,9 @@ def main(): ran = [r[0] for r in results] for pattern in expectations: matched = [t for t in ran if fnmatch.fnmatchcase(t, pattern)] - if matched and not any(t in layer_only for t in matched): + # A test that fails the plain run too (a platform's goldens) says nothing. + if matched and not any(t in layer_only or t in plain_failed + for t in matched): warnings.append(f'expected to fail in layer mode, but passes: {pattern}') if args.record_writers: