- fix: ekp-param-set silently reset after first justify (now persists;
ekp-param-reset added)
- fix: narrow-width CJK returned empty string (data loss); two-pass
emergency-break strategy guarantees output for any input
- fix: K-P penalties never synced to C module; space-box metrics
divergence between C and Elisp engines
- fix: para cache ignored ekp-latin-lang (stale hyphenation after
language switch) and used collision-prone sxhash keys
- fix: fullwidth letters/digits misclassified as CJK punctuation
- fix: combining chars split from their base char in the tokenizer
- fix: punctuation-wrapped words (word!/(word)/word;) never hyphenated
- fix: renderer double-counted stripped space widths; negative glue
clamped; batch/tty font detection no longer crashes
- feat: real looseness support via (position × line-count) DP
- perf: O(1) line metrics and gap counts via prefix arrays (inner loop
previously allocated O(n) subsequences → O(n³) total); box measurement
dedupe; eq fast-path para lookup; prebuilt per-para glue arrays
→ zh justify 7547ms → 96ms (compiled elisp) / 57ms (C);
range-justify 68.5s → 0.48s / 34ms; C module itself 3–19× faster
- test: 36 batch-safe ERT tests + 300-case property fuzz (C/elisp
byte-identical output, zero content loss) replacing ad-hoc suite
- docs: readme/readme_zh/DEVELOPER/DEVELOPER_ZH/ekp_c-README rewritten
to match the implementation; phase handoff in .phrase/phases/
BREAKING: requires Emacs 29.1+; C module must be rebuilt (v1.1, new
arities); ekp-threshold-factor / ekp-flagged-penalty /
ekp-forced-break-penalty removed; Rust module stubs removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
72 lines
2.9 KiB
EmacsLisp
72 lines
2.9 KiB
EmacsLisp
;;; ekp-fuzz.el --- property-based stress test for ekp -*- lexical-binding: t; -*-
|
||
|
||
(require 'ekp)
|
||
(require 'cl-lib)
|
||
|
||
(ekp-c-module-load)
|
||
(unless ekp-c-module-loaded (error "C module required for parity fuzz"))
|
||
|
||
(defvar fuzz--seed 42)
|
||
(defun fuzz--rand (n) ; deterministic LCG so failures are reproducible
|
||
(setq fuzz--seed (mod (+ (* fuzz--seed 1103515245) 12345) 2147483648))
|
||
(mod fuzz--seed n))
|
||
|
||
(defconst fuzz--cjk "中文排版是门艺术需要考虑标点悬挂避头尾规则同时兼顾美观")
|
||
(defconst fuzz--words '("the" "quick" "hyphenation" "emergency" "extraordinary"
|
||
"a" "of" "supercalifragilisticexpialidocious"
|
||
"bcdfghjklmnpqrstvwxz" "word!" "(paren)" "don't"
|
||
"test," "end." "«quoted»" "naïve" "Full" "123"))
|
||
(defconst fuzz--puncts '("," "。" "、" "《" "》" "「" "」" "!" "?"))
|
||
|
||
(defun fuzz--gen-string ()
|
||
"Random mixed paragraph of 5-60 tokens."
|
||
(let ((n (+ 5 (fuzz--rand 56))) (parts nil))
|
||
(dotimes (_ n)
|
||
(pcase (fuzz--rand 10)
|
||
;; latin word
|
||
((or 0 1 2 3) (push (nth (fuzz--rand (length fuzz--words)) fuzz--words) parts)
|
||
(push " " parts))
|
||
;; CJK run
|
||
((or 4 5 6 7) (let ((len (1+ (fuzz--rand 6)))
|
||
(start (fuzz--rand (- (length fuzz--cjk) 7))))
|
||
(push (substring fuzz--cjk start (+ start len)) parts)))
|
||
;; CJK punct
|
||
(8 (push (nth (fuzz--rand (length fuzz--puncts)) fuzz--puncts) parts))
|
||
;; spaces / zwsp
|
||
(9 (push (if (= 0 (fuzz--rand 3)) "" " ") parts))))
|
||
(string-trim (apply #'concat (nreverse parts)))))
|
||
|
||
(defun fuzz--content (s)
|
||
(replace-regexp-in-string "[ \t\n-]+" "" (substring-no-properties s)))
|
||
|
||
(ekp-param-set 5 2 1 4 2 1 0 3 0)
|
||
(let ((cases 300) (fails 0))
|
||
(dotimes (i cases)
|
||
(let* ((s (fuzz--gen-string))
|
||
(w (+ 1 (fuzz--rand 300))))
|
||
(unless (string-blank-p s)
|
||
(condition-case err
|
||
(let (el cr)
|
||
(setq ekp-use-c-module nil)
|
||
(ekp-clear-caches)
|
||
(setq el (ekp-pixel-justify s w))
|
||
(setq ekp-use-c-module t)
|
||
(ekp-clear-caches)
|
||
(setq cr (ekp-pixel-justify s w))
|
||
;; ① parity
|
||
(unless (equal el cr)
|
||
(cl-incf fails)
|
||
(message "PARITY FAIL #%d w=%d s=%S" i w s))
|
||
;; ② content preservation
|
||
(unless (equal (fuzz--content el) (fuzz--content s))
|
||
(cl-incf fails)
|
||
(message "CONTENT FAIL #%d w=%d s=%S" i w s))
|
||
;; ③ finite cost
|
||
(unless (numberp (ekp-total-cost s w))
|
||
(cl-incf fails)
|
||
(message "COST FAIL #%d w=%d" i w)))
|
||
(error (cl-incf fails)
|
||
(message "ERROR #%d w=%d s=%S err=%S" i w s err))))))
|
||
(message "fuzz done: %d cases, %d failures" cases fails)
|
||
(kill-emacs (if (> fails 0) 1 0)))
|