İçeriğe geç
getnextpdf.com

Oluşturulan PDF'leri CI'da test etme

Bu tarif, NextPDF ile PDF oluşturan ve kendi çıktısını test altında tutmak isteyen uygulama geliştiricileri içindir. Bu, motorun kendi test disiplininin tüketici tarafıdır: NextPDF’i yeniden test etmezsiniz; belgenizin hâlâ söylemesi gerekeni söylediğini ve hâlâ eskisi gibi göründüğünü onaylarsınız.

İki onay biçimi neredeyse her şeyi kapsar:

  • Çıkarılan metin üzerindeki anlamsal onaylar — oluşturun, Unicode metni geri kazanın ve beklediğiniz dizeleri içerdiğini onaylayın. Bu, yerleşim düzenlemelerine ve yazı tipi değişikliklerine dayanır.
  • Baytlar üzerindeki altın (anlık görüntü) onaylar — bir yeniden oluşturma bayt düzeyinde özdeş olsun diye DeterministicSettings’i sabitleyin, sonra yeni baytları, gönderime alınmış bir referans dosyasıyla karşılaştırın. Bu, her türlü istenmeyen değişikliği yakalar.

İçerik doğruluğu için anlamsal onayları ve bir gerileme tetikleyicisi olarak altın onayları kullanın. Çalıştırıcı, iş istasyonunuzun ürettiği baytların aynısını ürettiğinde, ikisi de CI’da değiştirilmeden çalışır.

Terminal window
composer require --dev phpunit/phpunit
composer require nextpdf/core:^3

Bir bayt farkı üzerinde değil, çıkarılan metin üzerinde onaylayın

“Bir bayt farkı üzerinde değil, çıkarılan metin üzerinde onaylayın” başlıklı bölüm

İki PDF’in ham bayt farkı kırılgandır: yeni bir zaman damgası, yeniden alt kümelenmiş bir yazı tipi ya da yeniden sıralanmış bir nesne; bir okuyucunun gördüğünü değiştirmeden baytları değiştirir. Bunun yerine içerik üzerinde onaylayın.

NextPDF Core bir üreticidir; bu yüzden önce metni çıkarılabilir kılın. Bunlar tek değil, iki ayrı mekanizmadır. Metin çıkarımı, glif kodlarını Unicode’a geri eşleyen doğru bir /ToUnicode CMap’ine (ISO 32000-2 §9.10.2) dayanır — motor onu gömülü yazı tipleri için yayar; böylece çıkarıcılar ham glif dizinleri yerine gerçek karakterleri geri kazanır. Etiketli PDF ayrıdır: enableTaggedPdf() ve setLanguage(), okuma sırasını ve erişilebilirliği kaydeden yapı ağacını ekler; ki bu, /ToUnicode CMap’ini oluşturan şey değildir. İçerik yazmadan önce ikisini de etkinleştirin: temiz metin geri kazanımı için CMap’i, okuma sırası için etiketlemeyi. Üretici ayrıntıları için bkz. Çıkarılabilir metin içeriği üretme. Sonra metni geri kazanın ve onun üzerinde onaylayın.

Sayfa sayısı ve yapısal gerçekler için, Inspect modülünün Quick derinliğinin, hiçbir Spectrum yan hizmeti yokken süreç içinde çalışan saf PHP bir yedeği vardır — bir CI çalıştırıcısında kullanışlıdır ama bozulmuş bir taramadır. Bir INSPECT-FALLBACK-001 “accuracy may be limited” sorunu işaretler ve sayfa sayısını, tam bir nesne ağacı ayrıştırmasından değil, ham baytlar üzerindeki kaba bir /Type /Page regex’inden türetir. Bir Spectrum yan hizmeti yapılandırılmış olduğunda, Quick derinliği bile onu kullanır — InspectDepth, yan hizmetin ne kadar analiz yaptığını denetler; bu yüzden Quick doğası gereği yan hizmetsiz değildir.

<?php
declare(strict_types=1);
use NextPDF\Inspect\Inspector;
use NextPDF\Inspect\InspectConfig;
$result = (new Inspector())->inspect($pdfBytes, InspectConfig::quick());
// With no sidecar injected, Quick depth takes the in-process PHP fallback:
// a degraded scan (page count from a regex) that flags INSPECT-FALLBACK-001.
// If a Spectrum sidecar is available, Inspector uses it even at Quick depth.
$pageCount = $result->pageCount; // int (regex-derived in the fallback)
$version = $result->pdfVersion; // e.g. "2.0"
$encrypted = $result->isEncrypted; // bool

Inspector::inspect(), değişmez bir InspectResult döndürür. Tam metin geri kazanımı için, baytlar üzerinde bir alt akış çıkarıcısı (pdftotext ya da Standard derinliğindeki Inspect Spectrum yan hizmeti) çalıştırın ve çıktısı üzerinde onaylayın — geri kazanılan metin üzerinde onaylayın, asla üreticinin tam baytları üzerinde değil.

Altın anlık görüntüler için çıktıyı bayt düzeyinde özdeş kılın

“Altın anlık görüntüler için çıktıyı bayt düzeyinde özdeş kılın” başlıklı bölüm

Bir altın testi yalnızca bir yeniden oluşturma aynı baytları ürettiğinde işe yarar. PDF’in yerleşik iki belirsizlik kaynağı vardır: tarih alanları (CreationDate / ModDate) ve fragmandaki dosya tanımlayıcısı (ISO 32000-2 §7.5.5). NextPDF, ikisini de DeterministicSettings aracılığıyla kaldırır; bu, bir test kıvırması değil, birinci sınıf bir yapılandırma değeridir.

DeterministicSettings, sabit bir DateTimeImmutable ve 32 karakterlik onaltılık bir fileIdSeed alır. Onu Config’te geçirin, sonra belgenizi o yapılandırmadan oluşturun. Belirleyimci profil sabitlendiğinde (sabit zaman damgası ve /ID), aynı girdi aynı sabitlenmiş araç zincirinde çalıştırmalar arasında bayt düzeyinde özdeş çıktı verir — PHP yaması, uzantı ile sıkıştırma kitaplığı sürümleri ve yazı tipi dosyaları sabit tutulur. Bunlardan herhangi birinde farklılaşan makineler arasında baytlar yine de ayrışabilir; orada metin çıkarımı onaylarını tercih edin ve altın anlık görüntüyü, sabit, sabitlenmiş bir ortam için ayırın.

<?php
declare(strict_types=1);
use DateTimeImmutable;
use NextPDF\Core\Config;
use NextPDF\Core\Document;
use NextPDF\Core\DeterministicSettings;
function buildInvoice(int $invoiceId): string
{
$config = new Config(
deterministic: new DeterministicSettings(
timestamp: new DateTimeImmutable('2026-01-01T00:00:00+00:00'),
fileIdSeed: '00000000000000000000000000000000', // exactly 32 hex chars
),
);
$document = Document::createStandalone($config);
$document->setLanguage('en');
$document->enableTaggedPdf('en'); // structure tree for reading order; /ToUnicode is emitted separately
$document->addPage();
$document->setFont('helvetica', '', 12);
$document->multiCell(0, 7, "Invoice #{$invoiceId}");
return $document->getPdfData();
}

fileIdSeed tam olarak 32 onaltılık karakter olmalıdır; yoksa kurucu InvalidConfigException fırlatır. Zaten bir Config tutuyorsanız, onu yeniden oluşturmak yerine $config->withDeterministic($settings) ile belirleyimci bir kopya türetebilirsiniz.

Bu test sınıfı, aynı oluşturucuya karşı bir anlamsal onayı ve bir altın onayı çalıştırır. Altın dosya bir kez oluşturulur, bir insan tarafından gözden geçirilir ve gönderime alınır; ondan sonra test, her bayt değişikliğinde başarısız olur.

<?php
declare(strict_types=1);
namespace App\Tests\Pdf;
use PHPUnit\Framework\TestCase;
use function App\Pdf\buildInvoice; // the deterministic builder above
final class InvoicePdfTest extends TestCase
{
private const GOLDEN = __DIR__ . '/__snapshots__/invoice-42.pdf';
public function testInvoiceTextIsPresent(): void
{
$pdf = buildInvoice(42);
// Recover text with an external extractor (installed in CI, see below).
$text = self::extractText($pdf);
self::assertStringContainsString('Invoice #42', $text);
}
public function testInvoiceBytesMatchGolden(): void
{
$pdf = buildInvoice(42);
// First run: write the golden, then review and commit it by hand.
if (! \is_file(self::GOLDEN)) {
\file_put_contents(self::GOLDEN, $pdf);
self::markTestIncomplete('Golden file created — review and commit it.');
}
self::assertSame(
\file_get_contents(self::GOLDEN),
$pdf,
'Generated PDF bytes drifted from the committed golden snapshot.',
);
}
private static function extractText(string $pdf): string
{
// tempnam() creates a zero-byte file; track it so the finally block
// removes both it and the .pdf path, leaking neither.
$tmp = \tempnam(\sys_get_temp_dir(), 'pdf');
$tmpPdf = $tmp . '.pdf';
try {
\file_put_contents($tmpPdf, $pdf);
// Run pdftotext via proc_open so we can read the exit code AND
// stderr. shell_exec() returns "" on a missing/failed binary, which
// would silently turn a broken runner into a passing assertion —
// the opposite of a reliable CI test. pdftotext writes UTF-8 to "-"
// (stdout). Requires poppler-utils on the runner (see workflow).
$descriptors = [
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'], // stderr
];
$process = \proc_open(
['pdftotext', $tmpPdf, '-'],
$descriptors,
$pipes,
);
if (! \is_resource($process)) {
throw new \RuntimeException(
'Could not start pdftotext. Install poppler-utils on the runner.',
);
}
$text = \stream_get_contents($pipes[1]);
$stderr = \stream_get_contents($pipes[2]);
\fclose($pipes[1]);
\fclose($pipes[2]);
$exitCode = \proc_close($process);
if ($exitCode !== 0) {
throw new \RuntimeException(\sprintf(
'pdftotext failed (exit %d): %s. Is poppler-utils installed on the runner?',
$exitCode,
\trim((string) $stderr) !== '' ? \trim((string) $stderr) : '(no stderr)',
));
}
return (string) $text;
} finally {
// Remove both the original tempnam() file and the .pdf we wrote.
@\unlink($tmp);
@\unlink($tmpPdf);
}
}
}

Bayt onayı yalnızca buildInvoice(), DeterministicSettings’i sabitlediği için anlamlıdır. Onsuz, tek başına CreationDate her çalıştırmada altın testi başarısız kılardı.

CI’nın aynı baytları üretmesi için yazı tiplerini sabitleyin

“CI’nın aynı baytları üretmesi için yazı tiplerini sabitleyin” başlıklı bölüm

Bayt düzeyinde özdeş çıktı, her makinede aynı yazı tipi baytlarının alt kümelenmesine bağlıdır. Çalıştırıcıda iş istasyonunuzdakinden farklı çözümlenen bir yazı tipi, gömülü alt kümeyi değiştirir ve — DeterministicSettings sabitlenmiş olsa bile — altın testi bozar.

İki kural yazı tiplerini kararlı tutar:

  • Belirli bir yazı tipine ihtiyaç duymadığınız altın testler için Base 14 standart yazı tiplerini (örneğin helvetica) kullanın. Bunlar özel yazı tipi baytları gömmekten kaçınır — kararlı yerleşik metriklere dayanırlar; ancak tam oluşturulan görünüm yine de görüntüleyicinin yazı tipi değişimine bağlı olabilir.
  • Sistemler arasında farklılaşan bir sistem yazı tipi yoluna güvenmek yerine, her özel yazı tipini depoya tedarik edin ve NextPDF’i açıkça ona yönlendirin. Config(fontsDirectory: ...) ayarlayın ya da gönderime alınmış dizinle addFontDirectory() çağırın:
<?php
declare(strict_types=1);
use NextPDF\Core\Config;
use NextPDF\Core\Document;
$config = new Config(fontsDirectory: __DIR__ . '/fonts'); // committed to the repo
$document = Document::createStandalone($config);
$document->addFontDirectory(__DIR__ . '/fonts'); // or add it imperatively
$document->addPage();
$document->setFont('dejavusans', '', 12); // resolved from the repo

Altın testler için yazı tiplerini işletim sistemi paket yöneticisinden kurmayın: dağıtım yazı tipi paketleri sürüm ve hinting bakımından farklılaşır; bu yüzden bir çalıştırıcı yükseltmesi baytlarınızı sessizce değiştirir. Tedarik edilmiş bir yazı tipleri dizini o değişkeni kaldırır.

Bu iş akışı, PHP’yi NextPDF’in ihtiyaç duyduğu uzantılarla kurar, anlamsal onaylar için bir metin çıkarıcısı kurar ve PHPUnit çalıştırır. php-version: "8.4" satırı, yamayı değil, PHP minör sürümünü (8.4) sabitler — setup-php onu mevcut en son 8.4.x sürümüne çözer. Bayt düzeyinde yeniden üretilebilirlik için, desteklediğiniz somut bir yamayı sabitleyin (örneğin php-version: "8.4.8"); böylece bir çalıştırıcı görüntüsü yükseltmesi, altın anlık görüntülerinizin altındaki PHP yapısını kaydıramaz.

name: PDF tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
extensions: curl, gd, intl, mbstring, openssl, zlib
coverage: none
- name: Install text extractor for PDF assertions
run: sudo apt-get update && sudo apt-get install -y poppler-utils
- name: Install dependencies
run: composer install --no-interaction --no-progress --prefer-dist
- name: Run the test suite
run: vendor/bin/phpunit --testsuite=pdf

poppler-utils, metin onayları için pdftotext’i sağlar. Uzantı listesi, NextPDF Core’un katı biçimde gerektirdiğiyle eşleşir: curl, gd, intl, mbstring, openssl ve zlib; ağ iletişimini, raster görüntü işlemeyi, uluslararasılaştırılmış metin ile harmanlamayı, çok baytlı metni, şifreleme/imzalama için kriptografiyi ve akış sıkıştırmasını kapsar. Hepsini kurun — Core’un composer.json’u her birini gerektirir; bu yüzden eksik bir uzantı yalnızca tek bir özelliği değil, composer install’u başarısız kılar. Sonraki bir onay adımı HTML ya da XML çıktısını ayrıştırırsa, o adım için dom ekleyin; bu bir Core gereksinimi değildir. Yazı tipleri depoya tedarik edildiği için hiçbir yazı tipi paketi kurulumu gerekmez — çalıştırıcının baytlarını sizinkilere eşit tutan da budur.

  • Altın testler DeterministicSettings gerektirir. Sabitlenmiş bir zaman damgası ve fileIdSeed olmadan; CreationDate, ModDate ve fragman dosya tanımlayıcısı her çalıştırmada değişir ve bayt onayı asla geçmez.
  • fileIdSeed tam olarak 32 onaltılık karakterdir. Başka herhangi bir uzunluk ya da onaltılık olmayan bir karakter, kuruluşta InvalidConfigException fırlatır.
  • Yazı tipleri baytların parçasıdır. Çalıştırıcıda farklı bir yazı tipi sürümü, glifleri yeniden alt kümeler ve altın testi başarısız kılar. Yazı tipini tedarik edin ya da Base 14 kullanın.
  • Core hiçbir extractText() göndermez. Onaylar için metin geri kazanımı tüketici işidir: pdftotext ya da Inspect Spectrum yan hizmetini kullanın. Üreticinin işi, çıkarıcıların gerçek Unicode’u geri kazanması için doğru bir /ToUnicode CMap’i yaymaktır (gömülü yazı tipleri için otomatik); enableTaggedPdf(), üzerine yapı ağacını ekler ama CMap’i üreten o değildir.
  • Inspect Quick derinliğinin, hiçbir yan hizmet yokken süreç içi bir PHP yedeği vardır (sınırlı doğruluk — INSPECT-FALLBACK-001 işaretler); Standard ve Full her zaman yan hizmeti gerektirir. Yan hizmetsiz CI için, Quick yedeği sayfa sayısını, sürümü ve şifreleme bayrağını verir — sonuçlarını yaklaşık olarak ele alın ve içerik doğruluğu için çıkarılan metne yaslanın.
  • Altın dosyaları kasıtlı olarak yeniden oluşturun. Bir değişiklik amaçlandığında, anlık görüntüyü silin, taze bir tane yazmak için yeniden çalıştırın ve gönderime almadan önce farkı gözden geçirin. Bir altını CI’da asla otomatik üzerine yazmayın.

Her iki onay biçimi de ucuzdur. Bir altın karşılaştırması, bir oluşturma artı bir dize karşılaştırmasıdır. Anlamsal yol, belge başına süreç dışı bir pdftotext çağrısı ekler; bunları, metnini gerçekten onayladığınız belgelerle sınırlı tutun. Inspect Quick PHP yedeği (yan hizmet yok), baytların tek geçişli bir taramasıdır; bu yüzden bir teste ihmal edilebilir süre ekler; bir yan hizmet yapılandırıldığında, Quick derinliği bunun yerine bir yan hizmet gidiş-dönüşü yapar.

  • Çıkarılan metni makine tarafından okunabilir olarak ele alın: bir gizli bilginin baytlardan yoksun olduğunu asla bir gizlilik denetimi olarak onaylamayın. Etiketli metin, dosyaya sahip herkes tarafından okunabilir. Gizlilik için şifreleyin.
  • Çıkarıcı için geçici dosya yolunu tempnam() ile oluşturun ve temizleyin; test fikstürlerini öngörülebilir, paylaşılan bir yoldan geçirmeyin.
  • Bir tedarik zinciri sıçramasının altın baytlarınızı ya da araç zincirinizi sessizce değiştiremeyeceğinden emin olmak için araç ve eylem sürümlerini sabitleyin (yalnızca 8.4 minörünü değil, 8.4.8 gibi somut bir PHP yaması; dağıtım üzerinden poppler-utils; eylem SHA’ları ya da etiketleri).

Bu kılavuz hiçbir normatif standart iddiasında bulunmaz. Dayandığı belirleyimcilik, ISO 32000-2’de adı geçen iki belirsiz olmayan alanın — fragman dosya tanımlayıcısı (/ID, §7.5.5) ve belge bilgisi tarih alanları (CreationDate / ModDate, fragmandan ayrı bir konum olan belge bilgisi sözlüğünde taşınan) — DeterministicSettings aracılığıyla kaldırılmasıdır. Metin onayları, motorun gömülü yazı tipleri için yaydığı /ToUnicode CMap’ine (§9.10.2) dayanır; enableTaggedPdf(), yapı ağacını ayrıca ekler ve o CMap’i oluşturmaz. Gösterilen her NextPDF çağrısı doğrulanmış genel API’dir.