From 3061edb7b02bc36ea90e0c983b5794c17f7fca62 Mon Sep 17 00:00:00 2001 From: jun0 Date: Thu, 17 Sep 2026 04:50:41 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[rustjava-adopt-indy-fixture-jdk-pin-and-sl?= =?UTF-8?q?ot-accounting-p0]=20test(fixtures):=20test-data=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=EC=9D=98=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B2=84=EC=A0=84=EC=9D=84=20=EB=8F=99=EA=B2=B0?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 제안은 「타깃을 정해 통일하라」를 전제하고 그 결정을 the work 라 불렀지만, 정작 산 이득은 드리프트 탐지다. 목표를 동결로 바꾸면 그 비용이 통째로 사라진다 — 재컴파일 0, 바이트 변경 0, 어느 타깃이 옳은가라는 결정 자체가 불요. 대가는 섞임을 고치지 않고 굳힌다는 것이다. 통일은 재컴파일이고 재컴파일은 그 픽스처가 무엇을 시험하는지를 바꿀 수 있어(제안 자신의 경고) 별 축으로 넘겼다. 세 방향을 전부 검사한다: 기록과 다름 · 기록에 없는 새 픽스처(없으면 핀이 옵트인이 되어 내일 추가분이 조용히 미보호) · 유령 기록. 개악 3종 전건 red · 복원 green · 기록기 멱등. --- REPORT.md | 21 +++ STATE.md | 24 +++ .../2026-09-17-test-data-version-freeze.json | 41 +++++ .../2026-09-17-test-data-version-freeze.md | 81 +++++++++ test-data/class-file-versions.txt | 167 ++++++++++++++++++ test-data/src/record-class-file-versions.py | 39 ++++ tests/test_fixture_pins.rs | 76 +++++++- 7 files changed, 448 insertions(+), 1 deletion(-) create mode 100644 docs/worklog/2026-09-17-test-data-version-freeze.json create mode 100644 docs/worklog/2026-09-17-test-data-version-freeze.md create mode 100644 test-data/class-file-versions.txt create mode 100755 test-data/src/record-class-file-versions.py diff --git a/REPORT.md b/REPORT.md index 7cc068db..d04f4fb1 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,4 +1,25 @@ # REPORT +## [2026-09-17] `test-data` 전체의 클래스 파일 버전을 «동결»했다 — 통일이 아니라 (rustjava-adopt-indy-fixture-jdk-pin-and-slot-accounting-p0) +- 무엇을: 커밋된 픽스처 **150개**의 `.` 를 표에 기록하고, ★**혼자 움직이면 red** 가 되게 했다. +- 왜: 채택 제안 `2026-09-16-indy-fixture-jdk-pin-and-slot-accounting#p0`. +- 사용자 영향: 없다(시험 위생). ★바뀐 것은 **「다른 JDK 로 재생성했는데 아무도 모르는」 경로가 닫힌 것**이다. +- ★★**제안의 «비용 산정»을 바꿨다 — 기각이 아니라 «설계 교체»**: 제안은 + 「**decide the intended target** per fixture … recompiled … **The decision is the work**」라며 **통일**을 전제했는데, + 정작 제안이 산 이득은 「a regenerated fixture **cannot quietly start testing a different Java version's** shapes」 = + ★**«드리프트 탐지»**다. ⇒ 목표를 **동결**로 바꾸면 ★**그 「work」가 통째로 사라진다** — 재컴파일 **0** · 바이트 변경 **0** · + 「어느 타깃이 옳은가」 **결정 불요**. +- ★**대가를 숨기지 않는다**: 이 축은 ★**섞임을 «고치지» 않고 «굳힌다»**. 통일하려면 재컴파일해야 하고, + ★**재컴파일은 바이트를 바꿔 그 픽스처가 «무엇을 시험하는지»를 바꿀 수 있다**(제안 자신의 경고) ⇒ 별 축으로 넘겼다(후속 L). +- ★**ⓑ 이미 핀하는 축이 둘 있었고, 그 둘이 못 덮는 곳이 대상이었다**: `test_fixture_pins.rs` 는 **indy 만** · + 생성기는 **자기 산출물 16개**의 버전을 소스에 박는다 ⇒ ★**남는 132개가 「javac 로 손수 재컴파일 가능」한 것**이고, + 루트의 **66×8 · 68×1 · 70×3** 이 그 사고의 지문이다. +- ★**세 방향을 전부 검사한다**: ⑴기록과 다름 ⑵★**기록에 없는 새 픽스처**(없으면 핀이 «옵트인»이 되어 내일 추가분이 조용히 미보호) ⑶유령 기록. + 개악 **3종 전건 red**(M1 버전 바이트 `65→70` · M2 미기록 픽스처 · M3 유령 행) · 복원 **green**. + ★실패 문면이 **파일·두 버전·해소 명령**을 함께 말한다(`Hello.class: recorded 65.0, found 70.0`). +- 검증: 재측 인구조사(총 150 · 루트 5버전) · 기록기 **멱등**(재실행 시 표 바이트 동일) · + `cargo test --all` **572 → 573 / 0 failed / 1 ignored** · DoD 7명령 rc=0. +- 후속 추천: 루트 픽스처를 **하나의 타깃으로 재컴파일할지 «판정»**(L) — ★동결은 「움직였나」를 답하지 「우리가 원하는 값인가」를 답하지 않는다. + 상세 = `docs/worklog/2026-09-17-test-data-version-freeze.md`. ## [2026-09-16] 부트스트랩 메서드의 «정적 인자» 인덱스를 경계 검사한다 (rustjava-adopt-bound-bootstrap-method-attr-index-p1) - 무엇을: JVMS 4.7.23 의 `bootstrap_arguments` 는 상수 풀 인덱스인데 ★**아무도 그것이 실재하는지 보지 않았다.** 이제 풀에 «없는» 인덱스를 가리키면 **거부**한다. diff --git a/STATE.md b/STATE.md index 3b4a97fe..d02f3e99 100644 --- a/STATE.md +++ b/STATE.md @@ -4,6 +4,30 @@ (없음 — 2026-09-16 실측: 착수 시 진행 티켓 0 · 열린 PR 0. ※「열린 PR 0」은 ★**이 회차 PR 착지 시점 기준**이다 — 회신 시점에는 그 PR 자신이 열려 있다) ## 완료 +- [rustjava-adopt-indy-fixture-jdk-pin-and-slot-accounting-p0] ★★**`test-data` 전체(150 클래스)의 클래스 파일 버전을 «동결»했다 — «통일»이 아니라.** + 채택 제안 `2026-09-16-indy-fixture-jdk-pin-and-slot-accounting#p0`(worklog json `adoptedProposals` 기록). + ★★**제안의 «비용 산정»을 바꿨다 — 기각이 아니라 «설계 교체»다.** 제안은 「**decide the intended target** per fixture or + per directory … recompiled … ★**The decision is the work**」라며 **통일**을 전제했는데, 정작 제안이 적은 이득은 + 「a regenerated fixture **cannot quietly start testing a different Java version's** bytecode shapes」 = ★**«드리프트 탐지»**다. + ⇒ ★**목표를 «동결»로 바꾸면 그 「work」가 통째로 사라진다**: 지금 값을 기록하고 혼자 움직이면 red — + ★**재컴파일 0 · 바이트 변경 0 · 「어느 타깃이 옳은가」 결정 자체가 불요.** + ★**대가는 정직하게**: 이 축은 ★**섞임을 «고치지» 않고 «굳힌다»**(루트에 52·65·66·68·70 이 그대로 남는다). + 그것이 옳은 이유는 ★**통일 = 재컴파일 = 바이트 변경 = 「그 픽스처가 무엇을 시험하는지」의 변경**이기 때문이고(제안 자신의 경고), + 그 판정은 **별 축(후속 L)** 으로 넘겼다. + ★★**ⓑ 이미 핀하는 축이 «둘» 있었고, 그 둘이 못 덮는 곳이 이 회차의 대상이었다**: + ⑴`tests/test_fixture_pins.rs` 는 **`test-data/indy` 만** ⑵**생성기 코드**가 자기 산출물 **16개**의 버전을 소스에 박는다 + (`make_ldc_fixtures.py` 의 `major=52` + 항목별 오버라이드) ⇒ ★**남는 132개 = 「`.java` 소스가 있어 손수 재컴파일 가능한 것」**이고, + ★**루트의 66×8 · 68×1 · 70×3 이 그 사고가 «이미 일어난» 지문**이다. + ★**만든 것**: `test-data/class-file-versions.txt`(150행 + 머리 주석에 「이것은 타깃이 아니라 동결이다」) · + `test-data/src/record-class-file-versions.py`(머리 주석 **보존** · 없으면 **거부** · docstring 이 「실패를 잠재우려고 돌리지 마라」를 못박는다) · + 테스트 `committed_fixtures_keep_their_recorded_class_file_version`. + ★★**세 방향을 «전부» 검사한다 — 이것이 급소다**: ⑴기록과 다름 ⑵★**기록에 없는 새 픽스처** + (★없으면 핀이 «옵트인»이 되어 **내일 추가되는 픽스처는 조용히 미보호** — 이 저장소가 반복해 잡은 그 형태의 변종) ⑶**유령 기록**. + ★**개악 3종 전건 red**: M1 `Hello.class` 버전 바이트 `65→70`(= 다른 JDK 재생성과 **같은 형상**) · + M2 미기록 `.class` 투입 · M3 표에만 있는 행 ⇒ 복원 **green**(`test_fixture_pins` **2 passed**). + ★실패 문면이 **파일·두 버전·해소 명령**을 함께 말한다(`Hello.class: recorded 65.0, found 70.0`). + ★**기록기 멱등**(재실행 시 표 **바이트 동일**) · `cargo test --all` **572 → 573 / 0 failed / 1 ignored** · DoD **7줄 전건 rc=0**. + ★**알고 남긴 값**: 생성기 산출물 16개는 **이중 잠금**(생성기 + 표)이다 — ★**예외 목록을 두는 규칙보다 «전건 단일 규칙»이 덜 썩는다.** - [rustjava-adopt-bound-bootstrap-method-attr-index-p1] ★★**부트스트랩 «정적 인자» 인덱스를 경계 검사한다 — 「감지되나 판정되지 않던」 자리를 닫았다.** 채택 제안 `2026-09-16-bound-bootstrap-method-attr-index#p1`(worklog json `adoptedProposals` 기록). ★**전/후**: `UnsupportedOperationException` → ★`ClassFormatError`. ★참조 JVM(OpenJDK 26.0.1) → diff --git a/docs/worklog/2026-09-17-test-data-version-freeze.json b/docs/worklog/2026-09-17-test-data-version-freeze.json new file mode 100644 index 00000000..3dd4dfa5 --- /dev/null +++ b/docs/worklog/2026-09-17-test-data-version-freeze.json @@ -0,0 +1,41 @@ +{ + "schema": "worklog/v1", + "date": "2026-09-17", + "taskId": "rustjava-adopt-indy-fixture-jdk-pin-and-slot-accounting-p0", + "summary": "Froze the class file version of all 150 committed fixtures in a checked-in table, rather than unifying them on a target. The proposal's stated benefit is drift detection, and a freeze buys that with zero recompiles — so the 'decide a target per directory' work it called the main cost is not needed.", + "changes": [ + "test-data/class-file-versions.txt: new, 150 rows of . , with a header saying it is a freeze rather than a target and why", + "test-data/src/record-class-file-versions.py: new, rewrites the table from disk; preserves the header and refuses to write without one", + "tests/test_fixture_pins.rs: committed_fixtures_keep_their_recorded_class_file_version, checking all three directions" + ], + "verification": [ + "census re-measured at start: root 52 x40, 65 x62, 66 x8, 68 x1, 70 x3; ldc 52 x7, 55 x6; indy 52 x1, 65 x6; 150 classes total — the proposal's numbers still hold in shape, 65 moved 61 -> 62 because this lineage added fixtures", + "existing coverage measured: tests/test_fixture_pins.rs pins only test-data/indy, and the generators pin their 16 outputs in source (make_ldc_fixtures.py has major=52 by default with per-fixture overrides)", + "the unpinned population is therefore the 132 javac-compiled fixtures that have .java sources — the ones a person can recompile by hand, which is exactly where the 66/68/70 outliers are", + "mutation M1, flipping Hello.class's version bytes 65 -> 70 (the same shape as a regeneration on another JDK): red, with the message naming the file, both versions and the fix", + "mutation M2, adding an unrecorded .class: red — without this direction the pin would be opt-in and new fixtures would be silently uncovered", + "mutation M3, adding a table row for a file that does not exist: red", + "restore: test_fixture_pins 2 passed", + "the recorder is idempotent: rerunning it leaves the table byte-identical", + "cargo test --all: 573 passed / 0 failed / 1 ignored (572 before; +1 test)", + "DoD 7 commands all rc=0" + ], + "issues": [ + "This freezes the heterogeneity rather than fixing it: five majors remain in the root. Unifying would require recompiling, and recompiling changes bytes, which can change what a fixture exercises — the proposal's own warning.", + "The 16 generator outputs are now double-locked (generator source plus the table). That is deliberate: one uniform rule is less likely to rot than a rule with an exemption list." + ], + "adoptedProposals": [ + "2026-09-16-indy-fixture-jdk-pin-and-slot-accounting#p0" + ], + "proposals": [ + { + "title": "Decide whether the root fixtures should be recompiled onto one target", + "plainSummary": "The main test-data directory holds fixtures built by five different Java versions. We froze that, which stops it drifting further, but did not decide whether it should be uniform.", + "userBenefit": "None directly. It would make a fixture's bytecode shapes predictable from one number instead of from its individual history.", + "why": "The freeze added in this round answers 'did it move', not 'is it what we want'. A fixture compiled at major 52 exercises pre-invokedynamic shapes; one at 70 may lower the same source differently. Nobody has decided which of those each test wants, and the current spread is an accident of when each file was last touched rather than a choice.", + "tradeoff": "Recompiling changes bytes, and a fixture's bytes are what its test asserts against — several compare output against a committed .txt. So this is not a bulk operation: it needs a per-fixture judgement about what that test is for, and the likely honest answer for most of them is 'leave it', which costs the review time and buys nothing.", + "effort": "L", + "target": "test-data/, test-data/src/" + } + ] +} diff --git a/docs/worklog/2026-09-17-test-data-version-freeze.md b/docs/worklog/2026-09-17-test-data-version-freeze.md new file mode 100644 index 00000000..a0a65c3f --- /dev/null +++ b/docs/worklog/2026-09-17-test-data-version-freeze.md @@ -0,0 +1,81 @@ +# 2026-09-17 — `test-data` 전체의 클래스 파일 버전을 «동결»한다 + +티켓 `rustjava-adopt-indy-fixture-jdk-pin-and-slot-accounting-p0` — 채택 제안 +`2026-09-16-indy-fixture-jdk-pin-and-slot-accounting#p0`. + +★**제안의 목적은 샀고, 제안이 「the work」라 부른 비용은 «치르지 않았다».** + +## ⓐ 제안이 «지금도» 참인가 — 참이다. 단 수는 움직였다 + +제안의 수: 루트 `65×61 · 52×40 · 66×8 · 70×3 · 68×1` · `ldc` `55×4 · 52×7`. +★**착수 시 재측**(그 회차 이후 픽스처가 늘었다): + +| 디렉터리 | 지금 | +|---|---| +| `test-data/`(루트) | **52×40 · 65×62 · 66×8 · 68×1 · 70×3** | +| `test-data/ldc` | **52×7 · 55×6** | +| `test-data/indy` | **52×1 · 65×6** | +| `cp` · `dispatch/*` · `loader` · `src/jar` | 52×3 · 52×10 · 52×2 · 65×1 | + +**총 150 클래스**. ⇒ 「다섯 버전이 섞여 있다」는 **여전히 참**이고, ★**65 가 61→62 로 는 것은 이 리니지가 그 사이 픽스처를 더했기 때문**이다. + +## ⓑ 이미 핀하는 축 — 둘 있다. 그리고 «그 둘이 못 덮는 것»이 이 회차의 대상이다 + +1. `tests/test_fixture_pins.rs` — **`test-data/indy` 의 javac 산출물**만(`.java` 짝이 있는 것) `65.0` 으로 핀. +2. **생성기 코드** — `make_ldc_fixtures.py` 는 `major=52` 기본값 + 항목별 오버라이드(55)를 **소스에 박아** 둔다. + `cp`·`indy` 합성분도 같다. ⇒ 그 **16개**는 재생성해도 버전이 안 흔들린다(생성기를 고치면 diff 에 보인다). + +⇒ ★**남는 것은 «javac 로 손수 재컴파일할 수 있는» 132개**다(`.java` 소스 보유 · 실측). +★**그 132개가 정확히 사고가 일어난 자리**다 — 루트의 66×8·68×1·70×3 이 그 지문이다. + +## ⓒ 제안의 «비용 산정»을 바꿨다 — 기각이 아니라 «설계 교체» + +제안의 tradeoff: 「pinning means first **deciding the intended target** per fixture or per directory — +and any fixture **recompiled** to match would change bytes … **The decision is the work**; the assertion is ten lines.」 + +★**그 문장은 «통일»을 전제한다.** 그런데 제안이 산 것으로 적은 이득은 +「A regenerated fixture **cannot quietly start testing a different Java version's** bytecode shapes」 — +즉 ★**«드리프트 탐지»**이지 «버전 통일»이 아니다. + +⇒ ★★**목표를 «동결»로 바꾸면 그 「work」가 통째로 사라진다**: +**지금 값을 그대로 기록하고, 혼자 움직이면 red.** 재컴파일 **0** · 바이트 변경 **0** · +「어느 타깃이 옳은가」라는 **결정 자체가 불요**다. +★**대가는 정직하게 적는다**: 이 축은 ★**「버전이 섞여 있다」를 «고치지» 않는다.** 섞인 채로 **굳힌다**. +그것이 옳은 이유는 — 통일하려면 재컴파일해야 하고, ★**재컴파일은 바이트를 바꾸며 그 픽스처가 «무엇을 시험하는지»를 바꿀 수 있다**(제안 자신의 경고다). +⇒ ★**섞임은 «관측된 사실»로 표에 남고, 그 사실이 보이는 자리에 놓인다.** + +## 만든 것 + +- **`test-data/class-file-versions.txt`** — 150행의 `. <경로>` + 머리 주석 + (★「이것은 «타깃»이 아니라 «동결»이다」·왜 통일하지 않는가·무엇을 잡는가·어떻게 갱신하는가). +- **`test-data/src/record-class-file-versions.py`** — 표 재기록기. ★머리 주석을 **보존**하고(없으면 **거부**), + docstring 이 ★**「실패를 잠재우려고 돌리지 마라 — 왜 움직였는지 보고, 재컴파일과 «같은 커밋»에 표를 넣어라」**를 못박는다. +- **`tests/test_fixture_pins.rs`** — 새 테스트 `committed_fixtures_keep_their_recorded_class_file_version`. + +## ★세 방향을 «전부» 검사한다 — 이것이 이 설계의 급소다 + +| 방향 | 없으면 생기는 구멍 | +|---|---| +| 기록과 **다르다** | 드리프트를 못 잡는다(본래 목적) | +| **기록에 없는** 픽스처가 있다 | ★**핀이 «옵트인»이 되어 내일 추가되는 픽스처는 조용히 미보호** | +| **파일이 없는** 기록이 남았다 | 표가 썩어 「무엇이 보호되는지」를 아무도 못 믿는다 | + +★**둘째가 특히 중요하다** — 이 저장소가 반복해 잡은 「통과하지만 아무것도 재지 않는」 형태의 정확한 변종이다. + +**개악 대조(3방향 · 전건 red · 복원 green)** +- **M1** `Hello.class` 의 버전 바이트만 `65 → 70`(= 다른 JDK 로 재생성한 것과 **같은 형상**) → ★**red**: + `Hello.class: recorded 65.0, found 70.0` + 「재컴파일이 의도였다면 기록기를 돌려 표를 «같은 커밋»에 넣어라」 +- **M2** 기록 없는 새 `.class` 투입 → ★**red**(`fixtures with no recorded version: [...]`) +- **M3** 표에만 있는 유령 항목 추가 → ★**red**(`the table records fixtures that are no longer here: [...]`) +- 복원 → **green**(`test_fixture_pins` **2 passed**) + +★기록기 **멱등** 확인: 다시 돌려도 표가 **바이트 동일**. + +## 잃는 것 / 안 하면 무엇이 나쁜가 + +⒜**잃는 것**: ⑴**150행짜리 데이터 파일**이 는다 — 픽스처를 **의도적으로** 재컴파일할 때마다 **한 줄을 같이 고쳐야** 한다 +(★그것이 비용이자 «목적»이다: 그 한 줄이 리뷰에 보이는 유일한 흔적이다) ⑵`cargo test --all` **572 → 573**(테스트 1개) · +`test_fixture_pins` 소요 **0.04s**(파일 150개의 8바이트만 읽는다 — 파싱 없음) ⑶★**«섞여 있음»을 고치지 않는다**(위 ⓒ). +⑷★**생성기 산출물 16개는 «이중 잠금»이 된다**(생성기 코드 + 표) — 규칙을 단순하게 유지하려고 **일부러** 전건을 넣었다. +⒝**안 하면**: 제안이 적은 그 사고가 계속 가능하다 — `javac Switch.java` 를 `--release` 없이 돌리면 **오늘의 JDK(70)** 가 찍히고, +★**이진 파일 안 2바이트 변화는 리뷰에서 아무도 못 본다.** ★그리고 그것은 가정이 아니다 — ★**루트의 66·68·70 이 그 사고가 «이미 일어난» 흔적**이다. diff --git a/test-data/class-file-versions.txt b/test-data/class-file-versions.txt new file mode 100644 index 00000000..c9165ef5 --- /dev/null +++ b/test-data/class-file-versions.txt @@ -0,0 +1,167 @@ +# Class file version of every committed fixture, one per line: . . +# +# This is a freeze, not a target. These fixtures were compiled at different times with whatever JDK +# was installed, which is why five different majors appear below — 52 x40, 65 x62, 66 x8, 68 x1, +# 70 x3 in the root alone. Unifying them would mean recompiling, and recompiling changes bytes, +# which can change what a fixture exercises. So the rule here is only "it must not move on its own". +# +# What that catches: regenerating a fixture with a different JDK. `javac Switch.java` with no +# --release stamps the running JDK's version, and a two-byte change inside a committed binary is +# not something anyone notices in review. The majors 66, 68 and 70 above are that accident already +# having happened. +# +# Changing a number here is allowed and sometimes correct — but it has to be a line in a diff that +# someone chose to write, next to the recompiled fixture, rather than something that slipped in. +# +# Regenerate this file (after a deliberate recompile) with: +# python3 test-data/src/record-class-file-versions.py +66.0 Abstract$Base.class +66.0 Abstract$Derived.class +66.0 Abstract.class +65.0 Array.class +70.0 ArrayCovariance.class +52.0 ArrayEdgeCases.class +70.0 ArrayStore.class +52.0 BooleanTest.class +52.0 CheckCast.class +52.0 ClassMetadata$ByteLoader.class +52.0 ClassMetadata$First.class +52.0 ClassMetadata$Payload.class +52.0 ClassMetadata$Sample.class +52.0 ClassMetadata$Second.class +52.0 ClassMetadata.class +65.0 ClinitFailure$Bad.class +65.0 ClinitFailure$BadError.class +65.0 ClinitFailure.class +52.0 CollectionsInvokeInterface.class +65.0 ConcurrentClinit$Failing.class +65.0 ConcurrentClinit$FailingReader.class +65.0 ConcurrentClinit$Successful.class +65.0 ConcurrentClinit$SuccessfulReader.class +65.0 ConcurrentClinit.class +65.0 Constants.class +65.0 ConstantsReader.class +65.0 ControlFlow.class +65.0 CurrentThread$1.class +65.0 CurrentThread.class +65.0 DoubleInit.class +66.0 Dup.class +65.0 Exception.class +65.0 Field.class +52.0 FieldTypes.class +65.0 FileErrors.class +65.0 FileLength.class +65.0 ForName.class +52.0 FormatterIntegration$1.class +52.0 FormatterIntegration$FailingAppendable.class +52.0 FormatterIntegration$SnapshotSequence.class +52.0 FormatterIntegration.class +65.0 Hello.class +65.0 IncompatibleStatic.class +65.0 Instanceof.class +52.0 IntegerOverflow.class +66.0 Interface$IInterface.class +66.0 Interface$Implementation.class +66.0 Interface.class +65.0 InterfaceCast$Base.class +65.0 InterfaceCast$Derived.class +65.0 InterfaceCast$IBase.class +65.0 InterfaceCast$IFace.class +65.0 InterfaceCast.class +65.0 LazyClinit$Base.class +65.0 LazyClinit$Derived.class +65.0 LazyClinit$GetstaticTarget.class +65.0 LazyClinit$IFace.class +65.0 LazyClinit$Impl.class +65.0 LazyClinit$NewTarget.class +65.0 LazyClinit$PutstaticTarget.class +65.0 LazyClinit$SelfRef.class +65.0 LazyClinit.class +52.0 LongDouble.class +65.0 Method.class +52.0 MonitorSemantics$1.class +52.0 MonitorSemantics$CounterWorker.class +52.0 MonitorSemantics$FailingWorker.class +52.0 MonitorSemantics.class +66.0 MultiArray.class +52.0 NativeMethod.class +52.0 NullArgGuards.class +52.0 NullBufferGuards.class +52.0 NullFileIoGuards.class +52.0 NullHandling.class +52.0 NullNetGuards$Handler.class +52.0 NullNetGuards$JarConn.class +52.0 NullNetGuards.class +52.0 NullSpecGuards.class +65.0 OddEven.class +52.0 Pop2Test.class +68.0 ReferenceCompare.class +65.0 StaleHolder.class +65.0 StaticFlag.class +65.0 StaticInherit$Base.class +65.0 StaticInherit$IConst.class +65.0 StaticInherit$Impl.class +65.0 StaticInherit$Sub.class +65.0 StaticInherit.class +65.0 StaticInitTrigger$Base.class +65.0 StaticInitTrigger$Sub.class +65.0 StaticInitTrigger.class +65.0 StaticOrder$Helper.class +65.0 StaticOrder.class +65.0 StringConcat.class +70.0 StringIntern.class +65.0 SuperClass$InnerClass.class +65.0 SuperClass$InnerDerivedClass.class +65.0 SuperClass.class +65.0 SurrogateChars.class +65.0 Switch.class +52.0 ThreadInterruption$DaemonWorker.class +52.0 ThreadInterruption$JoinWorker.class +52.0 ThreadInterruption$PreInterruptedSleepWorker.class +52.0 ThreadInterruption$TargetWorker.class +52.0 ThreadInterruption$WaitWorker.class +52.0 ThreadInterruption.class +65.0 ThrowableCause.class +65.0 TimeApi.class +52.0 TypeConversion.class +65.0 UnsupportedCharset.class +52.0 VirtualDispatchSemantics.class +65.0 WideLocals.class +65.0 ZipCorrupt.class +52.0 ZipGuards.class +52.0 cp/UnreferencedTag13.class +52.0 cp/UnreferencedTag14.class +52.0 cp/UnreferencedTag19.class +52.0 dispatch/base/PackageBase.class +52.0 dispatch/base/ProtectedBridge.class +52.0 dispatch/base/PublicBridge.class +52.0 dispatch/base/SamePackageChild.class +52.0 dispatch/foreign/ForeignChild.class +52.0 dispatch/foreign/ForeignProtectedGrandchild.class +52.0 dispatch/foreign/ForeignPublicGrandchild.class +52.0 dispatch/interfaces/InheritedOperation.class +52.0 dispatch/interfaces/InterfaceImplementation.class +52.0 dispatch/interfaces/InterfaceOwner.class +65.0 indy/ConstantKinds$Op.class +65.0 indy/ConstantKinds$Suit.class +65.0 indy/ConstantKinds.class +65.0 indy/Lambda$Op.class +65.0 indy/Lambda.class +52.0 indy/NotStringConcatFactory.class +65.0 indy/StringConcat.class +55.0 ldc/Ldc2WDynamic.class +52.0 ldc/Ldc2WMethodType.class +55.0 ldc/LdcDynamic.class +55.0 ldc/LdcDynamicBSMArgPastEnd.class +55.0 ldc/LdcDynamicBSMIndexPastEnd.class +55.0 ldc/LdcDynamicDuplicateBSM.class +55.0 ldc/LdcDynamicNoBSM.class +52.0 ldc/LdcDynamicOldMajor.class +52.0 ldc/LdcMethodHandle.class +52.0 ldc/LdcMethodType.class +52.0 ldc/LdcTag13.class +52.0 ldc/LdcTag14.class +52.0 ldc/LdcUnknownTag.class +52.0 loader/Base.class +52.0 loader/Payload.class +65.0 src/jar/JarTest.class diff --git a/test-data/src/record-class-file-versions.py b/test-data/src/record-class-file-versions.py new file mode 100755 index 00000000..78021a37 --- /dev/null +++ b/test-data/src/record-class-file-versions.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Rewrite `test-data/class-file-versions.txt` from what is on disk. + +Run this *after* deliberately recompiling a fixture, so the recorded version moves in the same +commit as the bytes it describes. Running it to silence a failing pin without looking at why the +version moved defeats the point — the check exists because a changed version is easy to miss +inside a binary diff. + +Usage: python3 test-data/src/record-class-file-versions.py +""" + +import struct +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +TABLE = ROOT / "class-file-versions.txt" + + +def main(): + old = TABLE.read_text().splitlines(keepends=True) if TABLE.exists() else [] + header = [line for line in old if line.startswith("#")] + if not header: + print(f"{TABLE} has no header comment to preserve; refusing to write", file=sys.stderr) + return 1 + + rows = [] + for path in sorted(ROOT.rglob("*.class")): + data = path.read_bytes() + minor, major = struct.unpack(">H", data[4:6])[0], struct.unpack(">H", data[6:8])[0] + rows.append(f"{major}.{minor} {path.relative_to(ROOT)}\n") + + TABLE.write_text("".join(header) + "".join(rows)) + print(f"recorded {len(rows)} fixtures in {TABLE}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_fixture_pins.rs b/tests/test_fixture_pins.rs index bf6f8caf..602ec026 100644 --- a/tests/test_fixture_pins.rs +++ b/tests/test_fixture_pins.rs @@ -5,7 +5,7 @@ //! check cannot ask the compiler what it would produce. The only thing that can notice a //! regeneration on a different toolchain is an assertion on the bytes that are actually committed. -use std::{ffi::OsStr, fs, path::Path}; +use std::{collections::BTreeMap, ffi::OsStr, fs, path::Path}; /// Fixtures under `test-data/indy` that have a `.java` source next to them were produced with: /// @@ -65,3 +65,77 @@ fn indy_javac_fixtures_keep_the_pinned_class_file_version() { // the source-pairing rule stops matching anything. assert!(!checked.is_empty(), "no javac-compiled fixture found under test-data/indy"); } + +/// Every committed fixture's class file version, frozen in `test-data/class-file-versions.txt`. +/// +/// The pin above says what `test-data/indy` *should* be and why; this one says only that nothing +/// moves without someone writing it down. That is the weaker claim on purpose: the rest of +/// `test-data` was never compiled to one target — 52, 65, 66, 68 and 70 all appear — and unifying +/// it would mean recompiling, which changes bytes and can change what a fixture exercises. +/// +/// All three directions are checked, because a pin that only compares the fixtures it already +/// knows about is opt-in, and opt-in pins rot: a fixture added tomorrow would simply not be +/// covered, silently. +#[test] +fn committed_fixtures_keep_their_recorded_class_file_version() { + let table = fs::read_to_string("test-data/class-file-versions.txt").unwrap(); + let mut recorded = table + .lines() + .filter(|line| !line.starts_with('#') && !line.trim().is_empty()) + .map(|line| { + let (version, path) = line.split_once(' ').expect("each row is `. `"); + (path.to_owned(), version.to_owned()) + }) + .collect::>(); + assert!(!recorded.is_empty(), "the version table is empty; it should list every fixture"); + + let mut unrecorded = Vec::new(); + let mut moved = Vec::new(); + for path in class_files(Path::new("test-data")) { + let relative = path.strip_prefix("test-data").unwrap().to_string_lossy().into_owned(); + let bytes = fs::read(&path).unwrap(); + let actual = format!( + "{}.{}", + u16::from_be_bytes([bytes[6], bytes[7]]), + u16::from_be_bytes([bytes[4], bytes[5]]) + ); + + match recorded.remove(&relative) { + None => unrecorded.push(relative), + Some(expected) if expected != actual => moved.push(format!("{relative}: recorded {expected}, found {actual}")), + Some(_) => {} + } + } + + assert!( + moved.is_empty(), + "class file version changed without the table changing with it: {moved:#?}\n\ + If the recompile was deliberate, run test-data/src/record-class-file-versions.py and \ + commit the table next to the fixture." + ); + assert!( + unrecorded.is_empty(), + "fixtures with no recorded version: {unrecorded:#?}\n\ + Run test-data/src/record-class-file-versions.py to add them." + ); + assert!( + recorded.is_empty(), + "the table records fixtures that are no longer here: {:#?}\n\ + Run test-data/src/record-class-file-versions.py to drop them.", + recorded.keys().collect::>() + ); +} + +fn class_files(dir: &Path) -> Vec { + let mut out = Vec::new(); + for entry in fs::read_dir(dir).unwrap() { + let path = entry.unwrap().path(); + if path.is_dir() { + out.extend(class_files(&path)); + } else if path.extension() == Some(OsStr::new("class")) { + out.push(path); + } + } + out.sort(); + out +} From 37d6a3748f58824ee9018ee21990f159cb32129b Mon Sep 17 00:00:00 2001 From: jun0 Date: Thu, 17 Sep 2026 09:54:22 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[rustjava-adopt-indy-fixture-jdk-pin-and-sl?= =?UTF-8?q?ot-accounting-p0-fix]=20docs(state):=20=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=E2=91=A2=20=EC=B0=A9=EC=A7=80=20=EA=B8=B0=EB=A1=9D=20?= =?UTF-8?q?=EB=8F=99=EB=B4=89=20(PR=20#57=20=C2=B7=20--merge)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- STATE.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/STATE.md b/STATE.md index 3c985ce0..ffbd7318 100644 --- a/STATE.md +++ b/STATE.md @@ -28,6 +28,14 @@ ★★**그래서 «문자열 치환»으로 구현했다** — `components()` join 이면 Unix 에서 역슬래시 입력이 **그대로 통과**해 ★**개악이 mac 에서 안 잡힌다**(대가: Unix 파일명의 진짜 역슬래시는 바뀐다 — 픽스처엔 없고 표는 우리 것이다). ★Python 축은 **윈도우 없이** 실증했다(`PureWindowsPath`: `str()` 역슬래시 ↔ `as_posix()` 슬래시). + ★★**게이트③ 착지 — PR #57 · `--merge`**(등재 repo · `merge_strategy: merge` 선언분). 게이트② **approve** · + 핀 `c56d43db` **불이동**(착수 실측 2026-09-17T00:52:46Z) · ★**`MERGEABLE/CLEAN` · base 뒤처짐 «0»** ⇒ 충돌 해소·base 당김 **둘 다 불요**. + ★핀에서 `ci-presence` **rc=0 CI_GREEN** · 이 형상에서 `cargo test --test test_fixture_pins` **3/3 green**(이 PR 이 만든 그 축). + ★**동봉은 이 기록 한 줄뿐**이다 — 원장(worklog 쌍·`STATE`·`REPORT`)은 구현 회차가 이미 실었다. + ★배포 **0**(이 저장소에 배포 워크플로 없음) · 자식 PR **0건** · 주기 자동 커밋 **0건** · 낡음 판별 도구 **0건**. + ★★**착지 순서 고지** — 이 PR 이 세운 규율(「`.class` 를 더하면 `test-data/class-file-versions.txt` 에 같은 커밋으로 행을 넣는다 · + 미등재 픽스처는 핀이 **실패**시킨다」)은 ★**열린 PR #60(신규 `.class` 3개)·#61(17개)에 «소급 적용»된다** — + 그쪽이 표에 행을 넣지 않고 착지하면 **main 이 red** 가 된다. 텍스트 충돌이 없어 `mergeable` 로는 보이지 않는 종류다. - [rustjava-adopt-indy-fixture-jdk-pin-and-slot-accounting-p0] ★★**`test-data` 전체(150 클래스)의 클래스 파일 버전을 «동결»했다 — «통일»이 아니라.** 채택 제안 `2026-09-16-indy-fixture-jdk-pin-and-slot-accounting#p0`(worklog json `adoptedProposals` 기록). ★★**제안의 «비용 산정»을 바꿨다 — 기각이 아니라 «설계 교체»다.** 제안은 「**decide the intended target** per fixture or From 403e1521a03203a014a787910e7a13126964f0e5 Mon Sep 17 00:00:00 2001 From: jun0 Date: Thu, 17 Sep 2026 15:22:40 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[rustjava-adopt-indy-fixture-jdk-pin-and-sl?= =?UTF-8?q?ot-accounting-p0-fix2]=20docs(state):=20=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=E2=91=A2=20=EC=B0=A9=EC=A7=80=20=EA=B8=B0=EB=A1=9D=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=20(=ED=95=80=20888d8821=20=C2=B7=202?= =?UTF-8?q?=ED=9A=8C=EC=B0=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- STATE.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/STATE.md b/STATE.md index d332df12..aa427e04 100644 --- a/STATE.md +++ b/STATE.md @@ -65,8 +65,12 @@ ★★**그래서 «문자열 치환»으로 구현했다** — `components()` join 이면 Unix 에서 역슬래시 입력이 **그대로 통과**해 ★**개악이 mac 에서 안 잡힌다**(대가: Unix 파일명의 진짜 역슬래시는 바뀐다 — 픽스처엔 없고 표는 우리 것이다). ★Python 축은 **윈도우 없이** 실증했다(`PureWindowsPath`: `str()` 역슬래시 ↔ `as_posix()` 슬래시). - ★★**게이트③ 착지 — PR #57 · `--merge`**(등재 repo · `merge_strategy: merge` 선언분). 게이트② **approve** · - 핀 `c56d43db` **불이동**(착수 실측 2026-09-17T00:52:46Z) · ★**`MERGEABLE/CLEAN` · base 뒤처짐 «0»** ⇒ 충돌 해소·base 당김 **둘 다 불요**. + ★★**게이트③ 착지 — PR #57 · `--merge`**(등재 repo · `merge_strategy: merge` 선언분). 게이트② **approve**(★`-fix2` 승계분) · + ★**핀 `888d8821` 불이동**(착수 실측 2026-09-17T06:21:43Z · `MERGEABLE/CLEAN` · base 뒤처짐 «0»). + ★★**게이트③이 «두 번» 돌았다 — 첫 회차는 «옳게» 막혔다**: base 를 당기면 이 PR 자신의 핀 테스트가 red 였다 + (형제 #59 가 들여온 `MakeConcat*` 3개가 표에 없었다) ⇒ 머지 티켓은 코드를 고치지 않으므로 `-fix2` 로 넘겼고, + 그 회차가 base 당김 + 표 **3행**(`record-class-file-versions.py` 1회 · 추가만 · 삭제 0)을 넣어 풀었다. + ⇒ ★**이 PR 이 세운 규율이 이 PR 에 처음 적용된 사례**이고, 검사기를 완화하지 않고 표를 고쳐 통과했다. ★핀에서 `ci-presence` **rc=0 CI_GREEN** · 이 형상에서 `cargo test --test test_fixture_pins` **3/3 green**(이 PR 이 만든 그 축). ★**동봉은 이 기록 한 줄뿐**이다 — 원장(worklog 쌍·`STATE`·`REPORT`)은 구현 회차가 이미 실었다. ★배포 **0**(이 저장소에 배포 워크플로 없음) · 자식 PR **0건** · 주기 자동 커밋 **0건** · 낡음 판별 도구 **0건**.