fix: harden post-audit contracts and test gates
This commit is contained in:
parent
183f256020
commit
6a8c7e04f5
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@ -167,8 +167,5 @@ jobs:
|
||||
shell: pwsh
|
||||
run: >
|
||||
emacs -Q --batch -L . -L tests
|
||||
-l tests/ekp-tests.el
|
||||
-l tests/ekp-buffer-tests.el
|
||||
-l tests/ekp-gui-tests.el
|
||||
-l tests/ekp-c-tests.el
|
||||
--eval "(load-file \"tests/load-project-source.el\")"
|
||||
-f ert-run-tests-batch-and-exit
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
|
||||
## phase-kp-overhaul-20260726
|
||||
|
||||
- 2026-08-20 lock post-audit hardening goals and tasks (`task037`–`task042`,
|
||||
`task030` continuation; `issue022`–`issue028` opened):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_post_audit_hardening_20260820.md`
|
||||
- 2026-08-13 close issues awaiting user-visible confirmation (`issue011`–
|
||||
`issue017`, `issue019`, `issue020`; `issue018`/`task030` remain open):
|
||||
`.phrase/phases/phase-kp-overhaul-20260726/change_close_user_confirmation_20260813.md`
|
||||
|
||||
@ -21,3 +21,10 @@
|
||||
- issue019 [x] [Per-edit frontier replanning destabilizes live projection and cannot restore reversible edits exactly.](../phases/phase-kp-overhaul-20260726/issue_stable_live_edit_transaction_20260729.md)
|
||||
- issue020 [x] [C-backed resize reflow still takes roughly 60–70 ms.](../phases/phase-kp-overhaul-20260726/issue_c_resize_latency_20260729.md)
|
||||
- issue021 [x] [Final-pass emergency layout can isolate CJK source lines at narrow widths.](../phases/phase-kp-overhaul-20260726/issue_overlong_no_break_atom_20260801.md)
|
||||
- issue022 [ ] [Invalid buffer widths mutate or create invalid projection state.](../phases/phase-kp-overhaul-20260726/issue_buffer_width_atomicity_20260820.md)
|
||||
- issue023 [ ] [Backward deletion can republish a stable live projection.](../phases/phase-kp-overhaul-20260726/issue_live_backward_projection_20260820.md)
|
||||
- issue024 [ ] [Hyphenation locale spelling can select the wrong dictionary.](../phases/phase-kp-overhaul-20260726/issue_hyphen_locale_20260820.md)
|
||||
- issue025 [x] [C direct API accepts invalid break positions.](../phases/phase-kp-overhaul-20260726/issue_c_position_validation_20260820.md)
|
||||
- issue026 [ ] [Showcase loses automatic inline policy and test runners can false-green.](../phases/phase-kp-overhaul-20260726/issue_showcase_test_freshness_20260820.md)
|
||||
- issue027 [x] [Manual integrations remain installed with no projection owner.](../phases/phase-kp-overhaul-20260726/issue_integration_no_projection_20260820.md)
|
||||
- issue028 [x] [Locked live evaluator can measure stale candidate bytecode.](../phases/phase-kp-overhaul-20260726/issue_live_evaluator_source_freshness_20260820.md)
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
# Change: Post-Audit Hardening 2026-08-20
|
||||
|
||||
## 2026-08-20 — Lock follow-up goals and task boundaries
|
||||
|
||||
- **Add** `plan_post_audit_hardening_20260820.md` with the evidence-backed
|
||||
scope, non-goals, acceptance gates, and stop condition for the post-audit
|
||||
defects.
|
||||
- **Add** `task_post_audit_hardening_20260820.md` with `task037`–`task042`
|
||||
and the remaining `task030` performance gate.
|
||||
- **Add** issue records for each user-visible or public-contract defect before
|
||||
implementation, including `issue028` for the stale-bytecode evaluator
|
||||
false-green.
|
||||
- **Behavior/Risk:** documentation-only planning. Closed historical issues
|
||||
remain closed; `issue018` remains open until its locked gate is met.
|
||||
|
||||
## Verification
|
||||
|
||||
- Next task ID is `task037`; no task ID is reused.
|
||||
- The plan explicitly separates buffer ownership, live semantics, dictionary
|
||||
lookup, C validation, test gates, integration lifecycle, and performance.
|
||||
- No production or test source is changed in this planning pass.
|
||||
|
||||
## 2026-08-20 — Implement correctness and gate hardening
|
||||
|
||||
- **Modify** `ekp-buffer.el` and `tests/ekp-buffer-tests.el` for preflight
|
||||
width validation, backward-delete live stability, and no-projection
|
||||
integration cleanup (`task037`, `task038`, `task042`; `issue022`,
|
||||
`issue023`, `issue027`).
|
||||
- **Modify** `ekp-hyphen.el` and `tests/ekp-tests.el` for exact normalized
|
||||
locale lookup (`task039`, `issue024`).
|
||||
- **Modify** `ekp_c/ekp.c` and `tests/ekp-c-tests.el` for sorted, unique,
|
||||
in-range break-position validation in direct and batch APIs (`task040`,
|
||||
`issue025`), with the 15-field ABI unchanged.
|
||||
- **Modify** the showcase, source loader, test runners, Windows CI entry,
|
||||
and complete ERT selection to remove stale-bytecode false greens and
|
||||
restore automatic inline-code verification (`task041`, `issue026`).
|
||||
- **Modify** the live evaluator script/bootstrap to load source from the
|
||||
selected baseline/candidate root (`task030`, `issue028`). The corrected
|
||||
narrowed matrix still exceeds the source 16 ms target, so `issue018` and
|
||||
`task030` remain open.
|
||||
|
||||
## Verification
|
||||
|
||||
- Source-first normal ERT: 294/294.
|
||||
- Source-first seeded random ERT: 294/294, including all nine GUI verifier
|
||||
tests previously omitted by the name filter.
|
||||
- Per-test isolated ERT: 294/294 process runs logged `0 unexpected`.
|
||||
- C focused ERT: 15/15; C portable build warning-clean; source fuzz:
|
||||
300/300 with zero failures; checkdoc, release, dictionary, and shell gates
|
||||
pass.
|
||||
- Corrected narrowed source evaluator: parity, zero-work, GC, conflicts, and
|
||||
width non-regression pass, but source candidate p99 remains above 16 ms;
|
||||
the performance issue is intentionally not closed.
|
||||
@ -0,0 +1,23 @@
|
||||
# Issue: Buffer Width Validation and Failure Atomicity 2026-08-20
|
||||
|
||||
## issue022 [ ] Invalid buffer widths mutate or create invalid projection state
|
||||
|
||||
- **Status:** Runtime fix verified by `task037`; user-visible confirmation
|
||||
remains pending.
|
||||
- **Summary:** `ekp-justify-region` accepts zero, negative, and non-integer
|
||||
widths without the string API's positive-integer validation. It clears the
|
||||
existing projection before a later type error, so a rejected request can
|
||||
destroy valid visible state.
|
||||
- **Repro:** In a justified buffer, call `ekp-justify-region` with `0`, `-1`,
|
||||
`1.5`, or `"80"`.
|
||||
- **Expected vs Actual:** Expected a preflight signal with the old projection
|
||||
unchanged. Actual zero creates a span with width zero; a type error after a
|
||||
prior projection leaves no spans.
|
||||
- **Related:** `ekp-buffer.el`, `ekp.el`, `task037`.
|
||||
- **Fix:** `ekp-justify-region` now validates the width before font-lock or
|
||||
projection clearing.
|
||||
- **Verification:** Red/green focused ERT 1/1 and source-first full ERT
|
||||
294/294.
|
||||
- **Resolved At:** Pending user-visible confirmation.
|
||||
- **Resolved By:** Developer implementation and verification.
|
||||
- **Commit:** Pending hardening commit.
|
||||
@ -0,0 +1,19 @@
|
||||
# Issue: C Position Vector Validation 2026-08-20
|
||||
|
||||
## issue025 [ ] C direct API accepts invalid break positions
|
||||
|
||||
- **Status:** Closed by direct contract verification on 2026-08-20.
|
||||
- **Summary:** The C boundary validates vector shape and signed integer type,
|
||||
but not the exclusive break-index range, monotonic ordering, or duplicate
|
||||
policy of hyphen/forbidden positions.
|
||||
- **Expected vs Actual:** Malformed direct arguments should signal
|
||||
`ekp-c-invalid-input` per the C API contract. Current `[-1]` and `[99]`
|
||||
vectors return a layout result instead.
|
||||
- **Related:** `ekp_c/ekp.c`, `ekp_c/ekp_kp.c`, `task040`.
|
||||
- **Fix:** Shared preflight now rejects out-of-range, duplicate, and
|
||||
non-increasing vectors for both direct and batch calls.
|
||||
- **Verification:** Direct/batch C ERT 15/15, portable warning-clean build,
|
||||
300-case fuzz, and Elisp parity pass.
|
||||
- **Resolved At:** 2026-08-20.
|
||||
- **Resolved By:** Developer implementation and contract verification.
|
||||
- **Commit:** Pending hardening commit.
|
||||
@ -0,0 +1,20 @@
|
||||
# Issue: Hyphenation Locale Resolution 2026-08-20
|
||||
|
||||
## issue024 [ ] Hyphenation accepts common locale spelling but selects the wrong dictionary
|
||||
|
||||
- **Status:** Runtime fix verified by `task039`; user-visible confirmation
|
||||
remains pending.
|
||||
- **Summary:** `de-CH` and `de-DE` normalize to underscore forms but are
|
||||
looked up in the short-code registry, where they can resolve to the first
|
||||
arbitrary German dictionary rather than the exact locale.
|
||||
- **Expected vs Actual:** Equivalent `de_CH`/`de-CH` spellings should select
|
||||
`hyph_de_CH.dic`; current `de-CH` selects `hyph_de_AT.dic`.
|
||||
- **Related:** `ekp-hyphen.el`, `plan_dictionary_governance_20260728.md`,
|
||||
`task039`.
|
||||
- **Fix:** Raw and normalized exact locale keys are checked before the
|
||||
documented short-code fallback.
|
||||
- **Verification:** Locale-equivalence ERT 11/11 and full dictionary gates
|
||||
pass.
|
||||
- **Resolved At:** Pending user-visible confirmation.
|
||||
- **Resolved By:** Developer implementation and verification.
|
||||
- **Commit:** Pending hardening commit.
|
||||
@ -0,0 +1,20 @@
|
||||
# Issue: No-Projection Integration Lifecycle 2026-08-20
|
||||
|
||||
## issue027 [ ] Manual integrations remain installed with no projection owner
|
||||
|
||||
- **Status:** Closed by lifecycle verification on 2026-08-20.
|
||||
- **Summary:** A manual justify request that produces no spans, such as an
|
||||
empty or foreign-display-only buffer, still installs EKP change hooks and
|
||||
the substring filter even though auto-mode is disabled.
|
||||
- **Expected vs Actual:** Integrations should have one active owner: spans or
|
||||
auto-mode. Actual `spans=nil` still leaves the filter and hooks installed
|
||||
until a later change happens to trigger cleanup.
|
||||
- **Related:** `ekp-buffer.el`, `plan_integration_lifecycle_20260728.md`,
|
||||
`task042`.
|
||||
- **Fix:** Manual justification installs integrations only when a span or
|
||||
auto-mode owner exists.
|
||||
- **Verification:** Empty/foreign-only lifecycle ERT 2/2 and full buffer
|
||||
suite pass.
|
||||
- **Resolved At:** 2026-08-20.
|
||||
- **Resolved By:** Developer implementation and lifecycle verification.
|
||||
- **Commit:** Pending hardening commit.
|
||||
@ -0,0 +1,22 @@
|
||||
# Issue: Backward Live Projection Stability 2026-08-20
|
||||
|
||||
## issue023 [ ] Backward deletion can republish a stable projection
|
||||
|
||||
- **Status:** Runtime fix verified by `task038`; user-visible confirmation
|
||||
remains pending.
|
||||
- **Summary:** Live row crossing treats forward typing and backward deletion
|
||||
identically. After a forward crossing, deleting a suffix into the previous
|
||||
native row can republish earlier rows and visibly move already typed text.
|
||||
- **Expected vs Actual:** The current live-edit contract preserves unaffected
|
||||
committed projection anchors during local tail shrink. Actual
|
||||
`ekp-buffer--live-row-crossed-p` triggers a full prefix publication in both
|
||||
directions.
|
||||
- **Related:** `postmortem/20260730-incremental-live-append-ownership.md`,
|
||||
`ekp-buffer.el`, `task038`.
|
||||
- **Fix:** A live row crossing now commits only when the native row moves
|
||||
forward; backward tail shrink keeps the transaction local.
|
||||
- **Verification:** Public edit-path regression and source-first full/random/
|
||||
isolated ERT all pass.
|
||||
- **Resolved At:** Pending user-visible confirmation.
|
||||
- **Resolved By:** Developer implementation and verification.
|
||||
- **Commit:** Pending hardening commit.
|
||||
@ -0,0 +1,20 @@
|
||||
# Issue: Live Evaluator Source Freshness 2026-08-20
|
||||
|
||||
## issue028 [ ] Locked live evaluator can measure stale candidate bytecode
|
||||
|
||||
- **Status:** Closed by source-fresh evaluator verification on 2026-08-20;
|
||||
performance gate remains open under the now-correct source measurement.
|
||||
- **Summary:** `run-live-commit-evaluator.sh` loaded the candidate by name and
|
||||
allowed an ignored local `.elc` to win. A report could therefore claim the
|
||||
locked source-instrumented 16 ms gate passed while measuring bytecode.
|
||||
- **Expected vs Actual:** Every baseline and candidate round must explicitly
|
||||
`load-file` the four production source files for its code root. Before the
|
||||
bootstrap fix, a narrowed matrix reported `pass:true`; afterward it reports
|
||||
source candidate p99 above 16 ms and keeps `issue018` open.
|
||||
- **Related:** `tests/run-live-commit-evaluator.sh`,
|
||||
`tests/ekp-live-commit-evaluator.el`, `issue018`, `task030`.
|
||||
- **Verification:** Source-load bootstrap, narrowed matrix, and source-path
|
||||
live benchmark are complete; full locked matrix remains the task030 gate.
|
||||
- **Resolved At:** 2026-08-20.
|
||||
- **Resolved By:** Developer evaluator verification.
|
||||
- **Commit:** Pending hardening commit.
|
||||
@ -0,0 +1,25 @@
|
||||
# Issue: Showcase Contract and Test Freshness 2026-08-20
|
||||
|
||||
## issue026 [ ] Showcase loses automatic inline policy and runners can false-green
|
||||
|
||||
- **Status:** Runtime/test fix verified by `task041`; user-visible
|
||||
confirmation remains pending.
|
||||
- **Summary:** The showcase's automatic inline-code face was commented out,
|
||||
so clean source runs fail the GUI policy contract. Local stale ignored
|
||||
`tests/*.elc` files can mask the regression, and random/isolated selectors
|
||||
omit nine GUI verifier tests.
|
||||
- **Expected vs Actual:** Clean, source-fresh normal/random/isolated runners
|
||||
should execute the complete ERT inventory and retain the automatic inline
|
||||
example. Current source has 3 failing showcase tests; stale local bytecode
|
||||
reports a false green.
|
||||
- **Related:** `tests/ekp-showcase.el`, `tests/run-tests.sh`,
|
||||
`tests/run-tests-random-order.el`, `tests/run-tests-isolated.sh`,
|
||||
`task041`.
|
||||
- **Fix:** The automatic inline face is restored; one source loader uses
|
||||
`load-file` for every production/test source and all runners select the
|
||||
complete ERT registry.
|
||||
- **Verification:** Source-first showcase 3/3, normal/random/isolated 294-test
|
||||
inventories, and GUI contract checks pass.
|
||||
- **Resolved At:** Pending user-visible confirmation.
|
||||
- **Resolved By:** Developer implementation and verification.
|
||||
- **Commit:** Pending hardening commit.
|
||||
@ -0,0 +1,82 @@
|
||||
# Plan: Post-Audit Hardening 2026-08-20
|
||||
|
||||
## Context
|
||||
|
||||
The repository audit after `183f256` found runtime and gate defects that are
|
||||
independent of the already accepted text-property layout architecture. This
|
||||
plan stays inside `phase-kp-overhaul-20260726`; it does not reopen closed
|
||||
issues or weaken the source-clean display contract.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Make public buffer width errors fail before projection mutation and keep
|
||||
the previous projection intact on every rejected request.
|
||||
2. Define and test the backward-delete live projection invariant before
|
||||
changing the row-crossing owner.
|
||||
3. Resolve locale spellings to the exact bundled dictionary before short-code
|
||||
fallback, and reject malformed C position vectors at the module boundary.
|
||||
4. Restore the showcase's automatic inline-code contract and make every test
|
||||
runner use current source and the complete ERT set.
|
||||
5. Remove manual buffer integrations when no projection or auto-mode owner
|
||||
remains.
|
||||
6. Re-profile `issue018`/`task030` after the correctness fixes with a
|
||||
source-fresh evaluator and close it only if the locked source-instrumented
|
||||
16 ms contract is actually met.
|
||||
|
||||
## Scope and milestones
|
||||
|
||||
- M1 (`task037`, `task042`): buffer width validation, failure atomicity, and
|
||||
integration lifecycle.
|
||||
- M2 (`task038`): backward-delete projection semantics and a regression that
|
||||
drives the public edit path.
|
||||
- M3 (`task039`, `task040`): exact hyphenation locale lookup and complete C
|
||||
position-vector validation, with direct boundary tests.
|
||||
- M4 (`task041`): showcase automatic inline face, source-first test loading,
|
||||
and complete random/isolated selection.
|
||||
- M5 (`task030`, `issue028`): make the locked width/engine matrix source-fresh,
|
||||
then optimize only the remaining structural commit owner.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Do not change valid KP output, the 15-field C ABI, or the source-clean
|
||||
buffer representation.
|
||||
- Do not hide latency with debounce, stale layout reuse, skipped publication,
|
||||
global GC changes, or test-only exceptions.
|
||||
- Do not close `issue018` from byte-compiled production numbers alone.
|
||||
- Do not rewrite historical postmortems; append current decisions and
|
||||
verification evidence.
|
||||
|
||||
## Acceptance gates
|
||||
|
||||
- Invalid or non-positive buffer widths signal before any owned property,
|
||||
span, filter, hook, point, mark, or modified-state change.
|
||||
- Backward deletion either preserves the committed projection as specified or
|
||||
has an explicit revised contract documented and tested through the public
|
||||
command path.
|
||||
- `de-CH`/`de_CH` and equivalent exact locale spellings select the same exact
|
||||
dictionary; unknown locales use the documented short-code fallback.
|
||||
- Out-of-range, unsorted, or duplicate C hyphen/forbidden positions signal
|
||||
`ekp-c-invalid-input`; valid arrays retain exact Elisp/C parity.
|
||||
- Clean source, stale bytecode, normal, random, and isolated test runners
|
||||
all exercise the same complete current ERT inventory and the showcase's
|
||||
automatic inline contract.
|
||||
- The live evaluator explicitly loads source from both baseline and candidate
|
||||
roots; a bytecode-backed `pass:true` is not valid evidence.
|
||||
- Full ERT, fuzz, C build/tests, byte compilation, checkdoc, package-lint,
|
||||
release, dictionary, and focused performance gates pass before closure.
|
||||
|
||||
## Risks and dependencies
|
||||
|
||||
- Backward-delete behavior is a product invariant, not a local condition;
|
||||
implementation must follow the existing spec and real public-path evidence.
|
||||
- C position validation must match the DP's exclusive break-index domain and
|
||||
preserve sorted-vector assumptions used by binary search.
|
||||
- The performance gate depends on trustworthy source loading; stale ignored
|
||||
`.elc` files must not influence any result.
|
||||
|
||||
## Stop condition
|
||||
|
||||
Stop only when all tasks in this plan have current issue/task evidence,
|
||||
closed issues have user-visible or contract-level verification, `issue018`
|
||||
is either genuinely closed by its locked gate or explicitly remains open with
|
||||
fresh measurements, and the complete diff is committed and reviewed.
|
||||
@ -0,0 +1,56 @@
|
||||
# Tasks: Post-Audit Hardening 2026-08-20
|
||||
|
||||
- task037 [x] Validate public buffer widths before mutation and make rejected
|
||||
`ekp-justify-region` requests failure-atomic. Add red tests for zero,
|
||||
negative, non-integer, and projection-preservation cases.
|
||||
- Source: post-audit buffer probe and `plan_post_audit_hardening_20260820.md`.
|
||||
- Validation: focused buffer ERT 4/4 plus source-preferred full ERT 294/294.
|
||||
|
||||
- task038 [x] Resolve backward-delete live projection semantics. Add a public
|
||||
edit-path regression at the narrowest reproducible width and update the
|
||||
live-layout spec/postmortem with the accepted invariant.
|
||||
- Source: `postmortem/20260730-incremental-live-append-ownership.md` and
|
||||
`plan_post_audit_hardening_20260820.md`.
|
||||
- Validation: focused live-edit ERT 4/4, randomized ERT 294/294, and
|
||||
source-first isolated ERT 294/294 process runs.
|
||||
|
||||
- task039 [x] Make hyphenation locale resolution try the exact normalized
|
||||
locale registry before short-code fallback. Add equivalent BCP-47/underscore
|
||||
tests and preserve fail-closed unsupported-pattern behavior.
|
||||
- Source: `ekp-hyphen.el`, `plan_dictionary_governance_20260728.md`, and
|
||||
the post-audit locale probe.
|
||||
- Validation: focused dictionary ERT 11/11, manifest check, and full ERT
|
||||
294/294.
|
||||
|
||||
- task040 [x] Validate C hyphen and forbidden position vectors for the exact
|
||||
exclusive break-index domain, monotonic ordering, and duplicate policy
|
||||
before DP access. Add direct single/batch ABI regressions without changing
|
||||
the 15-field schema.
|
||||
- Source: `plan_c_api_contract_20260728.md` and the post-audit C probe.
|
||||
- Validation: C warning-clean build, direct/batch C ERT 15/15, fuzz
|
||||
300/300, and Elisp parity.
|
||||
|
||||
- task041 [x] Restore the showcase automatic inline-code face and make normal,
|
||||
random, and isolated runners source-fresh and complete over the ERT
|
||||
inventory. Add a regression proving stale ignored bytecode cannot mask the
|
||||
showcase contract.
|
||||
- Source: `888a401`, `plan_test_isolation_20260728.md`, and the post-audit
|
||||
clean-source failure.
|
||||
- Validation: source loader symbol checks, clean-source showcase 3/3,
|
||||
normal/random/isolated 294-test inventories, and GUI contract checks.
|
||||
|
||||
- task042 [x] Remove manual buffer integrations when no spans remain and
|
||||
auto-mode does not own the lifecycle. Preserve composition with foreign
|
||||
filters and add empty/foreign-only teardown regressions.
|
||||
- Source: `plan_integration_lifecycle_20260728.md` and the post-audit
|
||||
no-projection probe.
|
||||
- Validation: focused integration ERT 4/4 and full source-first buffer
|
||||
suite.
|
||||
|
||||
- task030 [ ] Re-profile and, if still necessary, optimize the remaining
|
||||
source-instrumented narrow live-append structural commit path after
|
||||
task037–task042. Keep exact parity, source-clean projection, and zero-work
|
||||
point motion as hard constraints; close only on the locked 16 ms gate.
|
||||
- `issue028` is part of this task: each baseline/candidate evaluator round
|
||||
must explicitly load source files from its own code root, never rely on
|
||||
local `.elc` precedence.
|
||||
@ -905,6 +905,7 @@ EFFECTIVE-END includes the source boundary owned by the line."
|
||||
(and current-prefix-arg
|
||||
(prefix-numeric-value current-prefix-arg))))))
|
||||
(setq pixel (or pixel (ekp-buffer--effective-width)))
|
||||
(ekp--validate-width pixel)
|
||||
(let ((start (min beg end))
|
||||
(finish (max beg end)))
|
||||
(setq beg start
|
||||
@ -925,7 +926,9 @@ EFFECTIVE-END includes the source boundary owned by the line."
|
||||
(setq ekp-buffer--conflicts nil)
|
||||
(dolist (range (ekp-buffer--paragraph-ranges beg end))
|
||||
(ekp-buffer--layout-paragraph (car range) (cdr range) pixel))
|
||||
(if (or ekp-auto-justify-mode ekp-buffer--spans)
|
||||
(ekp-buffer--install-integrations)
|
||||
(ekp-buffer--remove-integrations))
|
||||
(goto-char point-before)
|
||||
(when mark-before
|
||||
(set-marker (mark-marker) (marker-position mark-before)))
|
||||
@ -1939,7 +1942,7 @@ When START and FINISH are non-nil, copy only that source interval."
|
||||
(native (and end (ekp-buffer--native-row-start end))))
|
||||
(and start end
|
||||
(if native
|
||||
(/= native start)
|
||||
(> native start)
|
||||
(and (< start end)
|
||||
(> (ekp--measured-width
|
||||
(ekp-buffer--logical-substring start end))
|
||||
|
||||
@ -73,22 +73,29 @@ LEFT/RIGHT: minimum chars before first / after last break."
|
||||
|
||||
;;; Dictionary Loading
|
||||
|
||||
(defun ekp-hyphen--normalize-lang (lang)
|
||||
"Return the normalized registry key for LANG."
|
||||
(downcase (replace-regexp-in-string "-" "_" lang)))
|
||||
|
||||
(defun ekp-hyphen-load-languages (dir)
|
||||
"Scan DIR for .dic files, populate language registry."
|
||||
(dolist (file (directory-files dir t "\\.dic\\'"))
|
||||
(let* ((name (file-name-nondirectory file))
|
||||
(lang (replace-regexp-in-string "\\(^hyph_\\|\\.dic$\\)" "" name))
|
||||
(normalized (ekp-hyphen--normalize-lang lang))
|
||||
(short (car (split-string lang "_"))))
|
||||
(puthash lang file ekp-hyphen--langs)
|
||||
(puthash normalized file ekp-hyphen--langs)
|
||||
(unless (gethash short ekp-hyphen--langs-short)
|
||||
(puthash short file ekp-hyphen--langs-short)))))
|
||||
|
||||
(defun ekp-hyphen--resolve-lang (lang)
|
||||
"Resolve LANG to dictionary path, trying exact then short forms."
|
||||
(or (gethash lang ekp-hyphen--langs)
|
||||
(let* ((norm (downcase (replace-regexp-in-string "-" "_" lang)))
|
||||
(let* ((norm (ekp-hyphen--normalize-lang lang))
|
||||
(parts (split-string norm "_"))
|
||||
found)
|
||||
(setq found (gethash norm ekp-hyphen--langs))
|
||||
(while (and parts (not found))
|
||||
(setq found (gethash (string-join parts "_")
|
||||
ekp-hyphen--langs-short)
|
||||
|
||||
21
ekp_c/ekp.c
21
ekp_c/ekp.c
@ -94,6 +94,22 @@ static ptrdiff_t paragraph_field_length(int field, ptrdiff_t prefix_len)
|
||||
}
|
||||
}
|
||||
|
||||
static bool strictly_increasing_positions_p(emacs_env *env, emacs_value vector,
|
||||
int32_t minimum, int32_t maximum)
|
||||
{
|
||||
ptrdiff_t length = env->vec_size(env, vector);
|
||||
int32_t previous = minimum - 1;
|
||||
|
||||
for (ptrdiff_t i = 0; i < length; i++) {
|
||||
int32_t position = clamp32(env->extract_integer(
|
||||
env, env->vec_get(env, vector, i)));
|
||||
if (position < minimum || position > maximum || position <= previous)
|
||||
return false;
|
||||
previous = position;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char *validate_paragraph_shapes(emacs_env *env, emacs_value *args)
|
||||
{
|
||||
static const int vectors[] = {0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 12};
|
||||
@ -134,6 +150,11 @@ static const char *validate_paragraph_values(emacs_env *env, emacs_value *args)
|
||||
if (env->extract_integer(env, args[7]) < 0 ||
|
||||
env->extract_integer(env, args[13]) < 0)
|
||||
return "EKP C hyphen width and protrusion must be nonnegative";
|
||||
int32_t box_count = clamp32(env->vec_size(env, args[0]) - 1);
|
||||
if (!strictly_increasing_positions_p(env, args[6], 0, box_count - 1))
|
||||
return "EKP C hyphen positions must be unique sorted box indices";
|
||||
if (!strictly_increasing_positions_p(env, args[11], 1, box_count))
|
||||
return "EKP C forbidden positions must be unique sorted gap indices";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
@ -827,7 +827,7 @@
|
||||
(1- (point)) 'ekp-buffer--display))))))
|
||||
|
||||
(ert-deftest ekp-buffer-test-live-backward-wrap-crossing-commits-once ()
|
||||
"Deleting into the previous native row publishes one atomic plan."
|
||||
"Deleting into the previous native row keeps the live transaction local."
|
||||
(let ((text
|
||||
"alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu")
|
||||
(calls 0)
|
||||
@ -845,8 +845,8 @@
|
||||
(setq calls (1+ calls))
|
||||
(apply original arguments))))
|
||||
(call-interactively #'delete-backward-char)
|
||||
(should (= calls 1))
|
||||
(should-not ekp-buffer--live-edit)
|
||||
(should (= calls 0))
|
||||
(should ekp-buffer--live-edit)
|
||||
(should (equal (substring-no-properties (buffer-string))
|
||||
(concat text " xy"))))))))
|
||||
|
||||
@ -2421,6 +2421,42 @@ keeps the buffer justified, and leaves it unmodified."
|
||||
(should-error (call-interactively #'ekp-justify-region)
|
||||
:type 'buffer-read-only)))
|
||||
|
||||
(ert-deftest ekp-buffer-test-justify-invalid-width-preserves-projection ()
|
||||
"Invalid manual widths signal before clearing an existing projection."
|
||||
(ekp-buffer-test--with-text "invalid width must preserve this projection"
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
(let ((before (buffer-substring (point-min) (point-max)))
|
||||
(spans ekp-buffer--spans))
|
||||
(dolist (width '(0 -1 1.5 "80"))
|
||||
(should-error (ekp-justify-region (point-min) (point-max) width)
|
||||
:type 'user-error)
|
||||
(should (equal-including-properties
|
||||
(buffer-substring (point-min) (point-max)) before))
|
||||
(should (eq ekp-buffer--spans spans))))))
|
||||
|
||||
(ert-deftest ekp-buffer-test-justify-empty-removes-unused-integrations ()
|
||||
"Manual justification does not retain hooks when no span was installed."
|
||||
(ekp-buffer-test--with-text ""
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
(should-not ekp-buffer--spans)
|
||||
(should-not ekp-buffer--filter-installed)
|
||||
(should-not (memq #'ekp-buffer--before-change
|
||||
before-change-functions))
|
||||
(should-not (memq #'ekp-buffer--after-layout-change
|
||||
after-change-functions))))
|
||||
|
||||
(ert-deftest ekp-buffer-test-justify-foreign-only-removes-unused-integrations ()
|
||||
"A foreign-only paragraph does not retain manual EKP integrations."
|
||||
(ekp-buffer-test--with-text "foreign ownership keeps this paragraph natural"
|
||||
(put-text-property (point-min) (point-max) 'display "foreign")
|
||||
(ekp-justify-region (point-min) (point-max) 20)
|
||||
(should-not ekp-buffer--spans)
|
||||
(should-not ekp-buffer--filter-installed)
|
||||
(should-not (memq #'ekp-buffer--before-change
|
||||
before-change-functions))
|
||||
(should-not (memq #'ekp-buffer--after-layout-change
|
||||
after-change-functions))))
|
||||
|
||||
;;;; Commands and mode integration
|
||||
|
||||
(ert-deftest ekp-buffer-test-no-break-public-commands ()
|
||||
|
||||
@ -30,6 +30,12 @@
|
||||
[0] [0] [0] [] 0 10
|
||||
[0 0] [0 0] [] [0 0] 0 10))
|
||||
|
||||
(defun ekp-c-tests--position-args ()
|
||||
"Return valid direct C arguments with several position slots."
|
||||
(list [0 10 20 30] [0 10 20 30] [0 10 20 30]
|
||||
[0 0 0] [0 0 0] [0 0 0] [0 2] 0 15
|
||||
[0 0 0 0] [0 0 0 0] [1 3] [0 0 0 0] 0 15))
|
||||
|
||||
(ert-deftest ekp-c-test-rejects-non-vector-schema-field ()
|
||||
"Schema errors signal the module's explicit input condition."
|
||||
(skip-unless (ekp-c-tests--available))
|
||||
@ -62,6 +68,33 @@
|
||||
(should-error (apply #'ekp-c-break-with-arrays args)
|
||||
:type 'ekp-c-invalid-input)))
|
||||
|
||||
(ert-deftest ekp-c-test-rejects-invalid-break-position-vectors ()
|
||||
"Position vectors must be in range, strictly sorted, and unique."
|
||||
(skip-unless (ekp-c-tests--available))
|
||||
(dolist (case '((6 [-1]) (6 [3]) (6 [2 1]) (6 [1 1])
|
||||
(11 [0]) (11 [4]) (11 [2 1]) (11 [2 2])))
|
||||
(let ((args (ekp-c-tests--position-args)))
|
||||
(setf (nth (car case) args) (cadr case))
|
||||
(should-error (apply #'ekp-c-break-with-arrays args)
|
||||
:type 'ekp-c-invalid-input))))
|
||||
|
||||
(ert-deftest ekp-c-test-accepts-position-boundaries ()
|
||||
"Valid edge positions preserve the direct API result contract."
|
||||
(skip-unless (ekp-c-tests--available))
|
||||
(let* ((args (ekp-c-tests--position-args))
|
||||
(result (apply #'ekp-c-break-with-arrays args)))
|
||||
(should (consp result))
|
||||
(should (listp (car result)))
|
||||
(should (numberp (cdr result)))))
|
||||
|
||||
(ert-deftest ekp-c-test-batch-rejects-invalid-position-vectors ()
|
||||
"Batch preflight applies the same position contract to every paragraph."
|
||||
(skip-unless (ekp-c-tests--available))
|
||||
(let ((args (ekp-c-tests--position-args)))
|
||||
(setf (nth 11 args) [0])
|
||||
(should-error (ekp-c-break-batch (vector (vconcat args)))
|
||||
:type 'ekp-c-invalid-input)))
|
||||
|
||||
(ert-deftest ekp-c-test-batch-rejects-short-paragraph-vector ()
|
||||
"Batch preflight validates each paragraph before indexing 15 fields."
|
||||
(skip-unless (ekp-c-tests--available))
|
||||
|
||||
@ -30,6 +30,9 @@
|
||||
(defconst ekp-live-commit-evaluator--maximum-regression 15.0)
|
||||
(defconst ekp-live-commit-evaluator--default-gc-threshold
|
||||
gc-cons-threshold)
|
||||
(defconst ekp-live-commit-evaluator--source-files
|
||||
'("ekp-utils.el" "ekp-hyphen.el" "ekp.el" "ekp-buffer.el")
|
||||
"Production source files loaded explicitly for every measured root.")
|
||||
(defconst ekp-live-commit-evaluator--corpus
|
||||
(concat
|
||||
" extraordinary editing continues smoothly 中文拉丁混排"
|
||||
@ -53,6 +56,14 @@
|
||||
(defvar ekp-live-commit-evaluator--signature-ms 0.0)
|
||||
(defvar ekp-live-commit-evaluator--cache-ms 0.0)
|
||||
|
||||
(defun ekp-live-commit-evaluator--load-source-root ()
|
||||
"Load every production file from `EKP_LIVE_COMMIT_CODE_ROOT'."
|
||||
(let ((root (getenv "EKP_LIVE_COMMIT_CODE_ROOT")))
|
||||
(unless root
|
||||
(error "EKP_LIVE_COMMIT_CODE_ROOT is required for measurement"))
|
||||
(dolist (file ekp-live-commit-evaluator--source-files)
|
||||
(load-file (expand-file-name file root)))))
|
||||
|
||||
(defun ekp-live-commit-evaluator--env-number (name fallback)
|
||||
"Return numeric environment variable NAME, or FALLBACK."
|
||||
(if-let ((value (getenv name)))
|
||||
@ -692,6 +703,7 @@
|
||||
|
||||
(if (equal (getenv "EKP_LIVE_COMMIT_MODE") "compare")
|
||||
(ekp-live-commit-evaluator--compare)
|
||||
(ekp-live-commit-evaluator--load-source-root)
|
||||
(ekp-live-commit-evaluator--write-record
|
||||
(ekp-live-commit-evaluator--measure-round)))
|
||||
|
||||
|
||||
@ -47,10 +47,9 @@
|
||||
(mixed (concat "中英混排是 Emacs 里最常见的场景: The quick brown fox "
|
||||
"jumps over the lazy dog, 而 internationalization "
|
||||
"这样的长词在窄栏会按 Liang 模式断词。自动行内代码 "
|
||||
(propertize
|
||||
"(ekp-pixel-justify paragraph-text target-width 'justify nil)"
|
||||
;; (propertize
|
||||
;; "(ekp-pixel-justify paragraph-text target-width 'justify nil)"
|
||||
;; 'face 'ekp-showcase-inline-code)
|
||||
'face 'ekp-showcase-inline-code)
|
||||
" 可以在合法空白边界附近换行,但不会插入"
|
||||
" discretionary hyphen。"))
|
||||
(atoms (concat "行内原子演示:手动硬原子 "
|
||||
|
||||
@ -279,7 +279,16 @@ Used to verify no content is lost by justification."
|
||||
(ert-deftest ekp-test-hyphen-lang-fallback ()
|
||||
"Short language codes resolve to a dictionary."
|
||||
(should (ekp-hyphen-create "en"))
|
||||
(should-error (ekp-hyphen-create "zz_XX")))
|
||||
(dolist (locale '("zz_XX" "zz-XX"))
|
||||
(should-error (ekp-hyphen-create locale)
|
||||
:type 'ekp-hyphen-dictionary-not-found)))
|
||||
|
||||
(ert-deftest ekp-test-hyphen-normalized-locale-prefers-exact-dictionary ()
|
||||
"Equivalent locale spellings resolve before the short-code fallback."
|
||||
(dolist (locale '("de_CH" "de-CH" "de_ch" "DE-CH"))
|
||||
(should (equal (file-name-nondirectory
|
||||
(ekp-hyphen--resolve-lang locale))
|
||||
"hyph_de_CH.dic"))))
|
||||
|
||||
(ert-deftest ekp-test-hyphen-alternative-languages-fail-closed ()
|
||||
"Replacement-pattern dictionaries must not degrade to plain Liang breaks.
|
||||
@ -302,6 +311,8 @@ DP represents break-specific replacement widths."
|
||||
;; Esperanto also contains slash-prefixed patterns whose libhyphen
|
||||
;; meaning is not representable as an ordinary Liang pattern.
|
||||
(should-error (ekp-hyphen-create "eo")
|
||||
:type 'ekp-hyphen-unsupported-pattern)
|
||||
(should-error (ekp-hyphen-create "hu-HU")
|
||||
:type 'ekp-hyphen-unsupported-pattern))
|
||||
|
||||
(ert-deftest ekp-test-hyphen-alternative-error-reaches-public-dispatch ()
|
||||
|
||||
27
tests/load-project-source.el
Normal file
27
tests/load-project-source.el
Normal file
@ -0,0 +1,27 @@
|
||||
;;; load-project-source.el --- Load EKP test subjects from source -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Test bootstrap that prevents ignored or stale byte-code files from deciding
|
||||
;; which implementation the suite exercises.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(let ((root (file-name-directory
|
||||
(directory-file-name
|
||||
(file-name-directory (or load-file-name buffer-file-name))))))
|
||||
(dolist (file '("ekp-utils.el"
|
||||
"ekp-hyphen.el"
|
||||
"ekp.el"
|
||||
"ekp-buffer.el"
|
||||
"tests/ekp-showcase.el"
|
||||
"tests/ekp-gui-verify.el"
|
||||
"tests/ekp-tests.el"
|
||||
"tests/ekp-buffer-tests.el"
|
||||
"tests/ekp-gui-tests.el"
|
||||
"tests/ekp-c-tests.el"
|
||||
"tests/run-tests-random-order.el"))
|
||||
(load-file (expand-file-name file root))))
|
||||
|
||||
(provide 'load-project-source)
|
||||
;;; load-project-source.el ends here
|
||||
@ -42,6 +42,7 @@ run_round()
|
||||
EKP_LIVE_COMMIT_LABEL=$label \
|
||||
EKP_LIVE_COMMIT_ROUND=$round \
|
||||
EKP_LIVE_COMMIT_OUTPUT=$output \
|
||||
EKP_LIVE_COMMIT_CODE_ROOT=$code_root \
|
||||
"$EMACS_BIN" -Q --batch -L "$code_root" -L "$ROOT/tests" \
|
||||
-l "$ROOT/tests/ekp-live-commit-evaluator.el"
|
||||
}
|
||||
|
||||
@ -9,20 +9,14 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
TESTS=$(
|
||||
"$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
|
||||
-l "$ROOT/tests/ekp-tests.el" \
|
||||
-l "$ROOT/tests/ekp-buffer-tests.el" \
|
||||
-l "$ROOT/tests/ekp-gui-tests.el" \
|
||||
-l "$ROOT/tests/ekp-c-tests.el" \
|
||||
--eval '(dolist (test (ert-select-tests "^ekp-\\(?:test\\|[[:alnum:]-]+-test\\)-" t))
|
||||
-l "$ROOT/tests/load-project-source.el" \
|
||||
--eval '(dolist (test (ert-select-tests t t))
|
||||
(princ (format "%s\n" (ert-test-name test))))'
|
||||
)
|
||||
|
||||
for test_name in $TESTS; do
|
||||
echo "isolated ERT: $test_name"
|
||||
"$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
|
||||
-l "$ROOT/tests/ekp-tests.el" \
|
||||
-l "$ROOT/tests/ekp-buffer-tests.el" \
|
||||
-l "$ROOT/tests/ekp-gui-tests.el" \
|
||||
-l "$ROOT/tests/ekp-c-tests.el" \
|
||||
-l "$ROOT/tests/load-project-source.el" \
|
||||
--eval "(ert-run-tests-batch-and-exit '$test_name)"
|
||||
done
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
(interactive)
|
||||
(let* ((seed (string-to-number (or (getenv "EKP_TEST_SEED") "20260728")))
|
||||
(tests
|
||||
(ert-select-tests "^ekp-\\(?:test\\|[[:alnum:]-]+-test\\)-" t))
|
||||
(ert-select-tests t t))
|
||||
(names (mapcar #'ert-test-name tests))
|
||||
(selector
|
||||
(cons 'member (ekp-tests--permuted-names names seed))))
|
||||
|
||||
@ -9,19 +9,12 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
case "$ORDER" in
|
||||
"")
|
||||
exec "$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
|
||||
-l "$ROOT/tests/ekp-tests.el" \
|
||||
-l "$ROOT/tests/ekp-buffer-tests.el" \
|
||||
-l "$ROOT/tests/ekp-gui-tests.el" \
|
||||
-l "$ROOT/tests/ekp-c-tests.el" \
|
||||
-l "$ROOT/tests/load-project-source.el" \
|
||||
-f ert-run-tests-batch-and-exit
|
||||
;;
|
||||
--random-order)
|
||||
exec "$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
|
||||
-l "$ROOT/tests/ekp-tests.el" \
|
||||
-l "$ROOT/tests/ekp-buffer-tests.el" \
|
||||
-l "$ROOT/tests/ekp-gui-tests.el" \
|
||||
-l "$ROOT/tests/ekp-c-tests.el" \
|
||||
-l "$ROOT/tests/run-tests-random-order.el" \
|
||||
-l "$ROOT/tests/load-project-source.el" \
|
||||
-f ekp-tests-run-random-order
|
||||
;;
|
||||
*)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user