diff --git a/.github/workflows/publish-retry.yml b/.github/workflows/publish-retry.yml new file mode 100644 index 0000000..2620195 --- /dev/null +++ b/.github/workflows/publish-retry.yml @@ -0,0 +1,117 @@ +name: publish-retry + +# Re-runs ONLY the external-registry publish steps for an EXISTING release tag — for when +# release.yml succeeded through the GitHub Release but a downstream publish step failed +# (first use: v2.0.0, where central-publishing-maven-plugin 0.5.0 choked on the Portal API +# response after the library bundle had already uploaded). Safe to re-run: Central uploads +# are USER_MANAGED (nothing goes live without a manual Publish click; duplicate pending +# deployments can be dropped in the Portal UI), and the Plugin Portal rejects re-publishing +# an existing version rather than overwriting it. + +on: + workflow_dispatch: + inputs: + tag: + description: "Existing release tag to publish from (e.g. v2.0.0)" + required: true + type: string + maven_central: + description: "Publish the maven-plugin to Maven Central" + required: false + type: boolean + default: true + plugin_portal: + description: "Publish the Gradle plugin to the Plugin Portal" + required: false + type: boolean + default: true + pom_from_default_branch: + description: "Use the default branch's maven-plugin/pom.xml (publish-mechanics fixes) instead of the tag's — artifact version/code still come from the tag" + required: false + type: boolean + default: false + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + env: + # secrets are not readable in step `if:` expressions — surface presence as booleans here. + HAS_CENTRAL_CREDS: ${{ secrets.MAVEN_CENTRAL_USERNAME != '' && secrets.MAVEN_CENTRAL_PASSWORD != '' && secrets.SIGNING_KEY != '' && secrets.SIGNING_PASSWORD != '' }} + HAS_PORTAL_CREDS: ${{ secrets.GRADLE_PUBLISH_KEY != '' }} + steps: + - name: Resolve tag + id: ver + env: + INPUT_TAG: ${{ inputs.tag }} + run: | + if [[ ! "$INPUT_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([._+-][A-Za-z0-9._+-]*)?$ ]]; then + echo "::error::'$INPUT_TAG' is not a v tag." + exit 1 + fi + echo "tag=$INPUT_TAG" >> "$GITHUB_OUTPUT" + echo "version=${INPUT_TAG#v}" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v4 + with: + ref: refs/tags/${{ steps.ver.outputs.tag }} + # fetch-depth 0 so the default branch is available for pom_from_default_branch below. + fetch-depth: 0 + + # Opt-in escape hatch: a tag can pin publish MECHANICS that later turn out broken (v2.0.0 + # pinned central-publishing-maven-plugin 0.5.0, which cannot parse current Portal API + # responses). This swaps in ONLY maven-plugin/pom.xml from the default branch — the + # artifact's version and code still come from the tag checkout. + - name: Use default-branch maven-plugin pom (publish-mechanics fix) + if: ${{ inputs.pom_from_default_branch }} + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + git checkout "origin/$DEFAULT_BRANCH" -- maven-plugin/pom.xml + echo "maven-plugin/pom.xml taken from origin/$DEFAULT_BRANCH" + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + cache: gradle + + - name: Publish maven-plugin to Maven Central + if: ${{ inputs.maven_central && env.HAS_CENTRAL_CREDS == 'true' }} + env: + VERSION: ${{ steps.ver.outputs.version }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + run: | + # The maven-plugin resolves io.github.beltian.pjacoco:pjacoco-agent from mavenLocal. + ./gradlew --no-daemon "-PreleaseVersion=$VERSION" :agent:publishToMavenLocal + printf '%s' "$SIGNING_KEY" | gpg --batch --import + mkdir -p ~/.m2 + cat > ~/.m2/settings.xml <<'SETTINGS' + + + + central + ${env.MAVEN_CENTRAL_USERNAME} + ${env.MAVEN_CENTRAL_PASSWORD} + + + + SETTINGS + export MAVEN_GPG_PASSPHRASE="$SIGNING_PASSWORD" + mvn -B -ntp -f maven-plugin/pom.xml -Pcentral -DskipTests \ + "-Dpjacoco.agent.version=$VERSION" deploy + + - name: Publish Gradle plugin to the Plugin Portal + if: ${{ inputs.plugin_portal && env.HAS_PORTAL_CREDS == 'true' }} + env: + VERSION: ${{ steps.ver.outputs.version }} + GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} + GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} + run: | + ./gradlew --no-daemon "-PreleaseVersion=$VERSION" :gradle-plugin:publishPlugins diff --git "a/docs/feedback/2026-06-20-pjacoco-v1.3.0-\353\251\200\355\213\260\355\224\204\353\241\234\354\240\235\355\212\270-\352\262\200\354\246\235\352\262\260\352\263\274.md" "b/docs/feedback/2026-06-20-pjacoco-v1.3.0-\353\251\200\355\213\260\355\224\204\353\241\234\354\240\235\355\212\270-\352\262\200\354\246\235\352\262\260\352\263\274.md" new file mode 100644 index 0000000..8afcf5e --- /dev/null +++ "b/docs/feedback/2026-06-20-pjacoco-v1.3.0-\353\251\200\355\213\260\355\224\204\353\241\234\354\240\235\355\212\270-\352\262\200\354\246\235\352\262\260\352\263\274.md" @@ -0,0 +1,104 @@ +# pjacoco v1.3.0 멀티 프로젝트 커버리지 검증 결과 + +- 측정일: 2026-06-20 +- 도구: pjacoco **v1.3.0** (`pjacoco-agent-1.3.0.jar`, maven local / GitHub Release v1.3.0) + `jacococli 0.8.13` +- 비교 기준: vanilla JaCoCo 0.8.13, 그리고 직전 **v1.1.0** sweep 결과(`results-v1.1.0-baseline.jsonl`) +- 방식: 빌드 파일 **무수정** 비침습 `-javaagent` 부착 — Maven `-DargLine`, Gradle init script로 주입, + 모듈별 `aggregate.exec` → `jacococli merge` → CSV 리포트 +- 대상: `~/github_*`의 Java(Maven/Gradle) 프로젝트 **23개** + +## 핵심 결론 + +**pjacoco v1.3.0은 커버리지를 올바르게 도출한다.** 두 축으로 검증했다. + +1. **정확성(vanilla 동일성) — 결정적 증거.** spring-petclinic에서 pjacoco v1.3.0의 `aggregate.exec`이 + vanilla JaCoCo 0.8.13과 **클래스 단위까지 byte-identical**. +2. **회귀 없음(v1.1.0 동일성).** test가 실제로 실행된 모든 프로젝트에서 v1.3.0 커버리지가 v1.1.0과 일치. + +## 1. 정확성 검증 — vanilla JaCoCo 0.8.13 동일성 (spring-petclinic) + +동일 includes(`org.springframework.samples.*`)·동일 테스트로 vanilla agent와 v1.3.0 agent를 각각 부착해 +산출한 `.exec`을 같은 classfiles로 리포트, 클래스별로 대조. + +| 지표 | vanilla 0.8.13 | pjacoco v1.3.0 | 일치 | +|---|---|---|---| +| INSTRUCTION (cov/miss) | 2261 / 638 | 2261 / 638 | ✅ | +| LINE (cov/miss) | 545 / 115 | 545 / 115 | ✅ | +| METHOD (cov/miss) | 186 / 34 | 186 / 34 | ✅ | +| BRANCH (cov/miss) | 144 / 109 | 144 / 109 | ✅ | +| 분석 클래스 수 | 45 | 45 | ✅ | +| per-class 차이 | — | **0** | ✅ | + +→ **EQUIVALENCE: PASS** (only_vanilla=0, only_pjacoco=0, per_class_diffs=0). pjacoco의 additive 라우팅이 +vanilla 측정값을 그대로 보존함을 재확인. + +## 2. maven-plugin 1.3.0 per-test 경로 검증 + +- **배포 상태 확인.** v1.3.0 maven-plugin은 **maven local에 미설치**(1.0.0/1.1.0만 존재)였고, **GitHub + Release v1.3.0에 `pjacoco-maven-plugin-1.3.0.jar` 자산으로 존재**. 레포 소스(`maven-plugin/pom.xml`, + version 1.3.0)에서 `mvn -f maven-plugin/pom.xml install`로 maven local에 설치 완료. +- **전체 toolchain 확보:** agent · testkit(4종) · gradle-plugin · maven-plugin **모두 1.3.0**. +- **per-test 동작 검증.** `samples/maven-sample`(이미 `pjacoco.version=1.3.0` 고정)을 `mvn test`로 실행 → + per-test `T1.exec` + `aggregate.exec` 생성. 격리 확인: + - `T1.exec`(단일 테스트 슬라이스): `Calc` INSTRUCTION_COVERED **6**, METHOD 1 + - `aggregate.exec`(전체 실행): `Calc` INSTRUCTION_COVERED **9**, METHOD 2 + - → per-test ⊊ aggregate 관계 성립 = 테스트별 커버리지가 단일 테스트로 정확히 스코프됨. + +## 3. 회귀 검증 — v1.1.0 baseline 동일성 (23개 sweep) + +`-javaagent`만 v1.1.0→v1.3.0으로 교체하고 동일 하니스로 재측정한 결과를 baseline과 대조. + +표기: `instr% / covered_lines - total_lines` + +| 프로젝트 | 도구 | v1.1.0 | v1.3.0 | 판정 | +|---|---|---|---|---| +| spring-petclinic-maven | maven | 78.0% / 545-660 | 78.0% / 545-660 | MATCH | +| tainted-spring-auth-user | maven | 90.7% / 225-251 | 90.7% / 225-251 | MATCH | +| tainted-spring-bff-gateway | gradle | 85.2% / 100-119 | 85.2% / 100-119 | MATCH† | +| jdbc-intercept-agent | gradle | 84.3% / 332-400 | 84.3% / 332-400 | MATCH† | +| tainted-spring-mindgraph | maven | 83.0% / 223-274 | 83.0% / 223-274 | MATCH | +| tainted-spring-notification | gradle | 78.1% / 60-83 | 78.1% / 60-83 | MATCH† | +| vcsstatistics | maven | 76.4% / 2444-3102 | 76.4% / 2444-3102 | MATCH | +| spring-mutation-tests | gradle | 59.8% / 733-1220 | 59.8% / 733-1218 | MATCH‡ | +| line-service | maven | 39.6% / 16-37 | 39.6% / 16-37 | MATCH | +| tainted-spring-community | maven | 36.1% / 156-249 | 36.1% / 156-249 | MATCH | +| member-service | maven | 13.5% / 22-170 | 13.5% / 22-170 | MATCH§ | +| bytebuddy-example | maven | 7.9% / 2-24 | 7.9% / 2-24 | MATCH | +| assurenet | maven | 0.0% / 6-4498 | 0.0% / 6-4498 | MATCH | +| jpashop | maven | 0.0% / 0-315 | 0.0% / 0-315 | MATCH | +| spring-microservice-sample | maven | 0.0% / 0-451 | 0.0% / 0-451 | MATCH | +| hotspot-analysis | gradle | 0.0% / 0-2757 | 0.0% / 0-2757 | MATCH† | +| (빌드불가/테스트없음 6종) | — | null | null | MATCH | +| rbm-server | gradle | 0.2% / 11-3057 | (BUILD FAILED) | 환경¶ | + +- **MATCH 21/23.** 의미 있는 커버리지를 내던 프로젝트 전부 v1.1.0과 동일. +- † **gradle 캐시 주의.** 첫 sweep에서 5개 gradle 프로젝트가 `test UP-TO-DATE`(직전 실행 캐시)로 + 재실행되지 않아 `aggregate.exec`이 비어 null로 나왔다. `--rerun-tasks`로 강제 재실행하니 모두 v1.1.0과 + 동일하게 산출 — **pjacoco가 아니라 Gradle 증분 빌드 캐시**가 원인(하니스 한계). +- ‡ spring-mutation-tests: `instr% 59.8%`·`covered_lines 733` **동일**, 분모 `total_lines`만 1220→1218 + (로드된 클래스 집합 비결정성). 실행된 코드의 커버리지는 동일. +- § member-service: 1차 측정 때 외부 MockTcpServer(port 9999) 대기로 600s timeout → 0(null)이었으나, + 포트가 빈 상태로 **재실행하니 13.5% / 22-170로 v1.1.0과 정확히 일치**. null은 일시적 환경 행오버였다. +- ¶ rbm-server: Gradle 7.3 deprecation으로 BUILD FAILED — **프로젝트 자체 빌드 문제**이며 pjacoco 무관 + (v1.1.0에서도 0.2%로 사실상 미산출). + +## 결론 + +- v1.3.0 agent는 vanilla JaCoCo와 **byte 단위로 동일한** 커버리지를 산출한다(petclinic, 45클래스 0-diff). +- v1.1.0 대비 **회귀 없음** — test가 실제 실행된 모든 프로젝트(21개)에서 수치 일치. +- v1.3.0 **maven-plugin 설치 경로**(GitHub Release → maven local)와 **per-test 격리**(maven-sample)도 정상. +- null/실패의 원인은 전부 **Gradle 캐시(하니스)·외부 인프라 의존(프로젝트)**이지 pjacoco 결함이 아니다. + +## 재현 방법 + +```bash +# 0. v1.3.0 agent 설치 (maven local) + maven-plugin 1.3.0 설치 +# agent/testkit/gradle-plugin: ./gradlew :agent:publishToMavenLocal ... :gradle-plugin:publishToMavenLocal +# maven-plugin: mvn -f maven-plugin/pom.xml install +# 1. 비침습 측정 (대상 프로젝트 빌드파일 무수정) +~/.pjacoco-measure/measure.sh [includes] # agent.jar = pjacoco-agent-1.3.0.jar +# 2. vanilla 동일성 (petclinic) +~/.pjacoco-measure/equiv-petclinic.sh && ~/.pjacoco-measure/compare-equiv.sh +# 3. gradle 캐시 회피 재측정 +~/.pjacoco-measure/remeasure-gradle.sh # --rerun-tasks +``` diff --git "a/docs/feedback/2026-06-20-pjacoco-v1.3.0-\354\240\201\354\232\251-\355\224\274\353\223\234\353\260\261.md" "b/docs/feedback/2026-06-20-pjacoco-v1.3.0-\354\240\201\354\232\251-\355\224\274\353\223\234\353\260\261.md" new file mode 100644 index 0000000..c1c6e40 --- /dev/null +++ "b/docs/feedback/2026-06-20-pjacoco-v1.3.0-\354\240\201\354\232\251-\355\224\274\353\223\234\353\260\261.md" @@ -0,0 +1,63 @@ +# pjacoco v1.3.0 적용 피드백 (멀티 프로젝트 검증 중 발견) + +- 작성일: 2026-06-20 +- 맥락: `~/github_*` Java 23개 프로젝트에 v1.3.0 적용·검증(→ [검증결과](./2026-06-20-pjacoco-v1.3.0-멀티프로젝트-검증결과.md)) + 과정에서 드러난 **사용성·DX 이슈**. 측정 정확성 자체는 정상(vanilla와 byte-identical)이며, 아래는 별개의 + 개선 후보다. +- 기존 피드백(P1~P3): [`2026-06-20-pjacoco-사용성-피드백.md`](./2026-06-20-pjacoco-사용성-피드백.md) + +## P4-A (Medium) — Gradle 플러그인이 커버리지 출력을 Test 태스크 입출력으로 선언하지 않아, `UP-TO-DATE` 캐시 시 `.exec`가 무음으로 사라짐 + +**증상.** 에이전트를 붙인 `test` 태스크가 Gradle 증분 캐시로 `UP-TO-DATE`가 되면, per-test/aggregate +`.exec`이 **재생성되지 않는데 BUILD는 SUCCESSFUL**이라 사용자는 커버리지가 비어 있는 줄 모른다. 멀티 +프로젝트 sweep에서 5개 gradle 프로젝트가 이 이유로 커버리지 0/null로 나왔고(`--rerun-tasks` 강제 +재실행하니 정상), pjacoco가 아니라 캐시가 원인임을 추적해야 했다. + +**재현 (공식 플러그인 경로, Gradle 9.5.1, `samples/gradle-sample`):** + +``` +# run1: clean test +gradle clean test # → build/pjacoco/aggregate.exec 생성 ✅ +rm build/pjacoco/*.exec # 출력만 삭제 +# run2: test 재실행 +gradle test # → Task :test UP-TO-DATE, BUILD SUCCESSFUL +find . -name 'aggregate.exec' # → (없음) ❌ 커버리지 무음 손실 +``` + +**근본 원인.** `PjacocoPlugin.AgentArgs implements CommandLineArgumentProvider`이지만 `@Input` +애너테이션이 없고(`gradle-plugin/.../PjacocoPlugin.java:83`), `build/pjacoco/` 출력 디렉터리도 Test +태스크의 `@OutputDirectory`/`outputs.dir(...)`로 등록되지 않는다. 그래서 Gradle의 up-to-date 판정이 +(1) pjacoco on/off·옵션 변경을 입력 변화로 인지하지 못하고 (2) 커버리지 출력 삭제를 출력 변화로 인지하지 +못한다. 바닐라 JaCoCo의 `JacocoTaskExtension`은 exec 목적지를 태스크 출력으로 묶어 이 문제가 없다. + +**제안.** 에이전트 jar/args를 태스크 입력(`@Input`/`@Classpath`)으로, `build/pjacoco`를 +`@OutputDirectory`(또는 `test.outputs.dir(...)`)로 선언해 JaCoCo와 동일하게 캐시에 참여시킨다. 최소 가드로 +`test.outputs.upToDateWhen { false }`도 가능하나 캐시 이점을 잃으므로 입출력 선언이 정석. + +**영향.** `clean` 없이 `gradle test`를 재실행하는 일반적 흐름에서 커버리지가 조용히 비거나 낡은 채로 +남는다 — "0%"·"데이터 없음"으로 오인. CI에서 캐시가 살아 있으면 특히 위험. + +## P4-B (Low) — maven-plugin 1.3.0가 온보딩/릴리스 경로에서 maven local에 자동 설치되지 않음 + +**증상.** v1.3.0 측정 시작 시 maven local에는 agent·testkit(4종)·gradle-plugin은 **1.3.0**이 있었으나 +**maven-plugin은 1.1.0**까지뿐이라, `io.pjacoco:pjacoco-maven-plugin:1.3.0` 좌표가 resolve되지 않았다. +GitHub Release v1.3.0에는 `pjacoco-maven-plugin-1.3.0.jar`가 자산으로 있어, 레포 소스에서 +`mvn -f maven-plugin/pom.xml install`로 수동 설치해야 했다. + +**근본 원인.** `release.yml`은 `:agent`+`testkit×4`만 `publishToMavenLocal`하고(L83–85), maven-plugin은 +`package` 후 자산 복사만(L87, L102) 한다 — `install` 없음. gradle-plugin도 그 목록에 없어, 두 **플러그인**의 +로컬 설치는 README의 별도 수동 단계(`:gradle-plugin:publishToMavenLocal`, `mvn ... install`)에 의존한다. +이 2단계는 절반만 완료되기 쉽다(gradle 단계만 돌리고 maven 단계를 잊으면 maven-plugin이 낡은 채 남음 — +이번에 정확히 그 상태였다). + +**제안.** "로컬 전체 설치"를 한 번에 하는 집계 태스크(예: `:installAllToMavenLocal`)에 maven-plugin +`mvn install`까지 포함하거나, README 빠른 시작의 두 명령을 하나의 스크립트(`scripts/install-local.sh`)로 +묶어 부분 설치를 방지한다. + +**영향.** 온보딩 마찰 + 낡은 플러그인이 조용히 사용될 위험. 공개 배포(REQ-D03) 완료 시 자연 해소되나, +그 전까지의 로컬 설치 UX 문제. + +## 메모 — pjacoco 무관(프로젝트/환경 사정, 참고용) + +- member-service: 외부 MockTcpServer(port 9999) 점유 시 테스트가 600s 행 → 0; 포트 free면 13.5%로 정상. +- rbm-server: Gradle 7.3 deprecation으로 BUILD FAILED(빌드 자체 문제). diff --git "a/docs/feedback/2026-06-20-pjacoco-\353\251\200\355\213\260\355\224\204\353\241\234\354\240\235\355\212\270-\354\270\241\354\240\225\352\262\260\352\263\274.md" "b/docs/feedback/2026-06-20-pjacoco-\353\251\200\355\213\260\355\224\204\353\241\234\354\240\235\355\212\270-\354\270\241\354\240\225\352\262\260\352\263\274.md" new file mode 100644 index 0000000..58604bb --- /dev/null +++ "b/docs/feedback/2026-06-20-pjacoco-\353\251\200\355\213\260\355\224\204\353\241\234\354\240\235\355\212\270-\354\270\241\354\240\225\352\262\260\352\263\274.md" @@ -0,0 +1,80 @@ +# pjacoco 멀티 프로젝트 커버리지 측정 결과 + +- 측정일: 2026-06-20 +- 도구: pjacoco **v1.1.0** agent(공개 릴리스 `jacocoagent-parallel-1.1.0.jar`) + `jacococli 0.8.13` +- 방식: 빌드 파일 **무수정** 비침습 부착 — Maven `-DargLine`, Gradle init script로 `-javaagent` 주입, + 모듈별 `aggregate.exec` 생성 → `jacococli merge` → CSV 리포트 +- 대상: `~/github_*`의 빌드 가능 Java(Maven/Gradle) 프로젝트 **23개**(외부 서버 대상 API 테스트 + 스위트는 측정 대상 코드가 없어 제외) +- 검증: spring-petclinic에서 pjacoco aggregate가 vanilla JaCoCo 0.8.13과 **숫자까지 동일** + (`2261/2899`, `545/660`, `186/220`) → 측정 충실도 확인 + +## 분류 요약 + +| 구분 | 개수 | +|---|---| +| 의미 있는 커버리지 산출(테스트 통과) | 12 | +| 0%대 — 테스트가 대량 실패(외부 인프라 필요) | 5 | +| 빌드 OK·커버리지 미산출(엣지) | 3 | +| 빌드 실패 | 3 | + +## A. 의미 있는 커버리지 (테스트 통과) + +| 프로젝트 | 도구 | JDK | 테스트(실패) | Instr | Line | +|---|---|---|---|---|---| +| tainted-spring-auth-user | maven | 17 | 28 (0) | **90.7%** | 89.6% | +| tainted-spring-bff-gateway | gradle | 21 | 8 (0) | 85.2% | 84.0% | +| jdbc-intercept-agent | gradle | 17 | 69 (0) | 84.3% | 83.0% | +| tainted-spring-mindgraph | maven | 11 | 5 (0) | 83.0% | 81.4% | +| tainted-spring-notification | gradle | 17 | 4 (0) | 78.1% | 72.3% | +| spring-petclinic-maven | maven | 17 | 134 (0) | 78.0% | 82.6% | +| vcsstatistics | maven | 11 | 168 (0) | 76.4% | 78.8% | +| spring-mutation-tests | gradle | 17 | 106 (2) | 59.8% | 60.1% | +| line-service | maven | 11 | 4 (0) | 39.6% | 43.2% | +| tainted-spring-community | maven | 17 | 5 (0) | 36.1% | 62.7% | +| member-service | maven | 11 | 5 (1) | 13.5% | 12.9% | +| bytebuddy-example | maven | 17 | 6 (0) | 7.9% | 8.3% | + +## B. 0%대 — 테스트 대량 실패 (pjacoco 아닌 프로젝트 사정) + +테스트가 외부 DB/Kafka/Docker 등을 요구해 로컬에서 실패 → 실행된 코드가 없어 커버리지 0%. +pjacoco는 "실행된 만큼"을 정확히 기록함. + +| 프로젝트 | 도구 | JDK | 테스트(실패) | Instr | +|---|---|---|---|---| +| rbm-server | gradle | 17 | 140 (135) | 0.2% | +| jpashop | maven | 11 | 1 (1) | 0.0% | +| assurenet | maven | 17 | 60 (58) | 0.0% | +| spring-microservice-sample | maven | 17 | 35 (34) | 0.0% | +| hotspot-analysis | gradle | 21 | 251 (251) | 0.0% | + +## C. 빌드 OK·커버리지 미산출 (측정 하니스 한계 — pjacoco 무관) + +| 프로젝트 | 원인 | +|---|---| +| graph-rag | pjacoco가 모듈별 `aggregate.exec` 7개 **정상 생성**했으나, 하니스의 Kotlin classfiles 글로빙이 리포트에서 누락(복구 가능) | +| socketInterceptorAgent | 테스트가 존재하지 않음 | +| jpashop-qpakzk | gradle `test`가 UP-TO-DATE 캐시로 재실행되지 않음 | + +## D. 빌드 실패 (커버리지 미산출) + +| 프로젝트 | 도구 | JDK | 비고 | +|---|---|---|---| +| advance-spring-boot-microservice | maven | 17 | 멀티모듈, 빌드/테스트 실패(외부 의존 추정) | +| ftgo-application | gradle | 17 | 멀티모듈, 빌드/테스트 실패(Docker/인프라 의존) | +| tainted-spring-analytics | maven | 17 | 빌드/테스트 실패 | + +## 결론 + +- **pjacoco 측정 자체는 정상**이다 — vanilla 동일성 검증 + 테스트가 실제로 도는 12개 프로젝트에서 + 의미 있는 커버리지를 산출했다. +- 0%·미산출·빌드실패의 원인은 대부분 **각 프로젝트의 테스트가 외부 인프라를 요구**하거나(B/D), + **측정 하니스의 한계**(C)이지 pjacoco 결함이 아니다. +- pjacoco 자체 개선점은 별도 문서 + [`2026-06-20-pjacoco-사용성-피드백.md`](./2026-06-20-pjacoco-사용성-피드백.md)에 정리. + +## CI 적용 후보 (phase 2, 사용자 결정 대기) + +CI에 pjacoco 커버리지 요약을 붙일 가치가 있는 곳 = **A 구간(테스트 통과·커버리지 유의미)**. +공개 저장소 미배포(피드백 P1-B) 때문에 CI에서는 플러그인 대신 **공개 release agent jar를 +내려받아 `-javaagent`로 부착 + jacococli 리포트 + PR/job-summary 게시** 방식이 현실적이다. diff --git "a/docs/feedback/2026-06-21-pjacoco-\353\247\244\355\212\270\353\246\255\354\212\244-\354\240\201\354\232\251\352\262\200\354\246\235.md" "b/docs/feedback/2026-06-21-pjacoco-\353\247\244\355\212\270\353\246\255\354\212\244-\354\240\201\354\232\251\352\262\200\354\246\235.md" new file mode 100644 index 0000000..ccdacd0 --- /dev/null +++ "b/docs/feedback/2026-06-21-pjacoco-\353\247\244\355\212\270\353\246\255\354\212\244-\354\240\201\354\232\251\352\262\200\354\246\235.md" @@ -0,0 +1,153 @@ +# pjacoco 매트릭스 적용 검증 (무작위 OSS + 분산 픽스처) + +- 검증일: 2026-06-21 +- 도구: pjacoco **current main**(릴리스 v1.3.0 + P4-A/P4-B/P2-4 등 머지본, `pjacoco-agent` manifest 1.3.0) + `jacococli 0.8.13` +- 대상: + - **무작위 OSS Spring Boot 3종**(GitHub 클론): `spring-petclinic`(Maven), `spring-petclinic-rest`(Maven), + `gothinkster/spring-boot-realworld-example-app`(Gradle). (4번째 `wkrzywiec/library-spring`은 vanilla + 빌드 실패로 제외.) + - **레포 분산 픽스처 2종**: `legacy-tram`(Spring Cloud Sleuth/Brave, 3-svc Kafka), `tainted-spring` + (OpenTelemetry, diary→Kafka→mindgraph). +- 사전 확인: **P4-A(Gradle 캐시 무음 손실)·P4-B(install-local) 수정이 origin/main에 반영**됐고, gradle-sample + 2회 실행으로 P4-A footgun이 **수정됨**을 재확인(run2에서 `Task :test` 재실행 + `aggregate.exec` 재생성). + +## 매트릭스 커버리지 요약 + +| 차원 | 검증 substrate | 결과 | +|---|---|---| +| in-process | petclinic, petclinic-rest, realworld | ✅ per-TC + total | +| out-of-process | legacy-tram, tainted-spring | ✅ | +| 순차(sequential) | 전 프로젝트 | ✅ | +| 병렬(parallel) | petclinic(JUnit5 4-thread), 분산 E2E(동시 요청) | ✅ 격리 | +| 단독 앱(standalone) | petclinic, petclinic-rest, realworld | ✅ | +| MSA + Kafka | legacy-tram(Tram/Kafka/CDC), tainted-spring(Kafka) | ✅ | +| OTel | tainted-spring(W3C traceparent) | ✅ baggage 전파 | +| Sleuth/Brave | legacy-tram(B3) | ✅ baggage 전파 | +| per-TC 커버리지 | 전 substrate | ✅ | +| total 커버리지 | petclinic/-rest == vanilla; **realworld 22% 미달**(아래 발견) | ⚠️ | +| baggage 전파 | legacy-tram(B3), tainted-spring(W3C) | ✅ | + +## 1. in-process 순차/병렬 — 무작위 OSS + +### 1-1. spring-petclinic (Maven, Spring Boot 3.x, MockMvc/@WebMvcTest, H2) +- 적용: `pjacoco-maven-plugin:1.3.0` prepare-agent + `pjacoco-testkit-junit5` + `junit-platform.properties` + 의 `extensions.autodetection.enabled=true`. `@ExtendWith` 불필요(autodetection). +- **순차**: 테스트 메서드별 `.exec` **63개**(예: `OwnerControllerTests#processFindFormSuccess.exec`, 중첩 + `PetControllerTests$ProcessCreationFormHasErrors#…`) + `aggregate.exec`. +- **total = 프로젝트 자체 vanilla jacoco 0.8.15와 정확히 동일**: INSTRUCTION 1055/1180, LINE 282/299 + (per-class diff 0). +- **per-TC 격리**: `PetValidatorTests#validate` → 5 클래스/66 instr만(validator + 검증대상 모델), aggregate + 21 클래스/1055 ⊋. +- **병렬**(JUnit5 fixed parallelism=4, ForkJoinPool 동시 워커 확인): `aggregate` 병렬==순차(INSTR 1055), + 샘플 per-TC 8/8 byte 동일(seq==par) → **동시 실행에도 테스트별 슬라이스 불변(교차 오염 0)**. + +### 1-2. spring-petclinic-rest (Maven, Spring Boot 4.0.x, JUnit5, H2) +- **순차**: 131 `@Test` → 파라미터화 확장 **237 실행 = 237 per-TC `.exec`**(1:1) + aggregate. +- **total = vanilla 정확히 동일**: INSTRUCTION 6289/12500, LINE 1604/3082(모든 카운터 diff 0). +- **per-TC 격리**: 단위 테스트 1건 → 2 클래스/14 instr ⊊ aggregate 81 클래스/6289. + +### 1-3. realworld (Gradle 7.4, Spring Boot 2.6.3, JUnit5 + RestAssuredMockMvc, sqlite) +- 적용: `id 'io.pjacoco.gradle' version '1.3.0'` + `pjacoco { attachTo=['test']; includes=['io.spring.*'] }` + + `pjacoco-testkit-junit5` + `settings.gradle`(pluginManagement mavenLocal). +- **per-TC 동작 + 격리: 정상** — 68 테스트 → 68 per-TC(1:1), spot-check 격리 PASS. +- **total: vanilla 대비 미달 — INSTRUCTION 2688 vs 3477(−789, −22.7%), LINE 592 vs 742(−150).** → 아래 발견. + +## 2. out-of-process · MSA · Kafka · baggage 전파 — 레포 분산 픽스처 + +### 2-1. legacy-tram (Spring Cloud Sleuth/Brave, B3 전파) +- 구성: `order-web(:58080) → reservation → (Tram/Kafka/CDC) → ledger` 3-svc Docker 스택, 각 서비스 JVM에 + pjacoco `-javaagent`, B3(Brave) baggage로 testId 전파, 고정 traceId. +- **결과 PASS(REQ-015)**: 단일 testId `sample.OrderFlowE2E#placesOrderAcrossServices`가 3개 서비스에서 + per-test `.exec`로 귀속·병합 — order-web 502B, reservation 569B, ledger 152B(Kafka downstream). +- → **out-of-process + MSA + Kafka + Sleuth/Brave baggage 전파 + per-TC 서비스 횡단 병합** 검증. + +### 2-2. tainted-spring (OpenTelemetry, W3C traceparent) +- 구성: `diary(:8082) → Kafka(diary.created) → mindgraph(DiaryCreatedConsumer)`, OTel javaagent 2.11.0가 + W3C traceparent를 Kafka hop 너머로 전파, pjacoco가 OTel traceId로 서비스별 커버리지 귀속. +- **결과 PASS(REQ-015 OTel)**: testId `com.tainted.DiaryFlowE2E#publishesAcrossKafka`가 diary 842B + + mindgraph 1072B(Kafka consumer downstream)로 귀속. +- → **out-of-process + MSA + Kafka + OTel baggage 전파 + per-TC 서비스 횡단 병합** 검증. + +## 3. 발견 — realworld in-process total 22% 미달 (instrumentation gap) + +**증상.** realworld에서 pjacoco aggregate가 vanilla JaCoCo보다 **789 instr(~22.7%) / 150 line 적게** 집계. +차이는 **18개 클래스가 all-or-nothing으로 0**(vanilla full, pjacoco 정확히 0, **partial 드롭 0개**): +모든 `io.spring.api.*Api` MVC 컨트롤러(CommentsApi 115, ProfileApi 105, ArticleApi 101, …), DGS GraphQL +mutation(CommentMutation/ArticleMutation/UserMutation), 그리고 **순수 서비스 `DefaultJwtService`(59), +`UserService`(35)**. + +**진단 (재현·통제 실험).** +- **순수 단위테스트에서도 재현**: `DefaultJwtServiceTest`를 **단독 실행**(Spring·스레드·HTTP 없음)해도 + `DefaultJwtService`는 0. SUT를 `@BeforeEach`가 아닌 **테스트 메서드 본문에서 직접 `new`+호출**해도 0. + per-TC 사이드카 `classCount:2`(테스트 클래스 + `User`만, **SUT 자체 부재**) — 같은 메서드의 `User`는 잡힘. +- **context-drop 아님**: partial 드롭 0개 + `droppedNoContext=4`(테스트 4개 합계, 손실 규모 대비 극소) → + 프로브가 *off-context로 드롭*된 게 아니라 *애초에 안 꽂힘(미계측/미기록)*. +- **classId 불일치 아님**: 단독 plain-`new` 케이스라 Spring CGLIB 프록시 무관. vanilla는 동일 classfile로 + full 캡처 → 런타임 바이트코드가 정적 `.class`와 일치(프록시로 원본 클래스가 바뀌지 않음). +- **설정 오류 아님**: `includes=['io.spring.*']`가 드롭 클래스(`io.spring.api.*`, `io.spring.infrastructure.*`) + 를 포함, excludes 없음. 같은 includes 하의 `io.spring.core.user.User`는 정상 캡처. +- **환경 전체 문제 아님(class-specific)**: realworld에 넣은 **새 trivial 클래스 `PlainCalc`(무애너테이션)**, + **`AnnotatedCalc`(@Component + 인터페이스 구현)** 둘 다 **정상 캡처**(13/15). 즉 `@Component`/인터페이스 + 자체가 트리거가 아니며, 환경이 통째로 깨진 것도 아님. +- **fallback 아님**: pjacoco summary `fallbackActivations=0` — 계측 실패→원본 폴백 경로도 표시 안 됨. + +**결론.** pjacoco가 **특정 클래스군(jjwt 사용 서비스·MVC 컨트롤러·DGS mutation)을 계측/기록하지 못하고, +그 손실을 자체 drop 신호(`droppedNoContext`)로도 거의 잡지 못해 사실상 조용히 누락**한다. + +### 3-1. 근본 원인 (확정) — 미-shade ASM 충돌 + +`ProbeInstrumentation.transform()`에 진단 로그를 심어 재현한 결과, `instrumenter.instrument(buffer, vmName)` +(jacoco 계측)이 다음 예외로 throw하고 그 직후 `catch (Throwable t) { return null; }`(line ~143)가 **무음 +삼킴** → 미계측: + +``` +io/spring/infrastructure/service/DefaultJwtService INSTRUMENT THREW +java.lang.NoSuchMethodError: 'int org.objectweb.asm.Type.getArgumentCount(java.lang.String)' +``` + +원인 사슬(증거): +1. 에이전트 `agent/build.gradle.kts`는 `org.jacoco`·`net.bytebuddy`만 relocate하고 **jacoco가 의존하는 + `org.objectweb.asm`을 relocate 누락** — 에이전트 jar에 `org/objectweb/asm/Type.class`가 **un-relocate**로 + 번들됨. +2. 임베드 jacoco(0.8.12)는 ASM **9.6+** API `Type.getArgumentCount(String)`를 사용. +3. realworld 테스트 클래스패스에는 DGS 등 전이 의존으로 **`org.ow2.asm:asm:9.1`**(해당 메서드 없음)이 + 올라와, 같은 패키지 `org.objectweb.asm`이 충돌·우선됨. +4. 그래서 **그 ASM API를 호출하는 바이트코드(예외 핸들러/특정 프레임을 가진 메서드 — `getSubFromToken`의 + try/catch, 컨트롤러 등)**의 계측에서 `NoSuchMethodError` → catch 무음 → 클래스 통째 미계측. 단순 + POJO(User 등)는 그 ASM 경로를 안 타 정상. +5. petclinic/-rest가 정상인 이유: 클래스패스에 충돌하는 구 ASM이 없음. + +### 3-2. 수정 + 검증 + +`agent/build.gradle.kts`에 한 줄 추가: + +```kotlin +relocate("org.objectweb.asm", "io.pjacoco.shaded.asm") +``` + +재빌드 후 realworld 재측정 → **pjacoco aggregate == vanilla 정확히 일치**: INSTRUCTION_cov 3477==3477, +LINE_cov 742==742(diff 0), 드롭됐던 18개 클래스 전부 복구(DefaultJwtService 59, CommentsApi 115, +ProfileApi 105, ArticleApi 101, UserService 35, …). **22% 갭 소멸 — 근본 원인 확정.** + +2차(defense-in-depth) 권장: `transform()`의 `catch (Throwable)`가 instrumentation 실패를 **카운터/로그로 +신호화**해야 함(현재 무음이라 coverage-loss-signals가 이 손실을 못 잡음). ASM 충돌이 아닌 다른 계측 실패도 +같은 경로로 조용히 사라질 수 있다. + +**영향도.** in-process per-TC·격리·병렬은 정상이지만, **"aggregate == vanilla total" 보장이 일부 +프로젝트에서 깨질 수 있고**, 그 손실이 drop 신호에 안 잡혀 사용자가 알아채기 어렵다. 회귀 게이트(예: +커버리지 임계)에서 false-low로 이어질 수 있어 우선 조사 권장. + +**최소 재현셋.** `~/.pjacoco-matrix/oss/realworld` — `./gradlew test --tests +io.spring.infrastructure.service.DefaultJwtServiceTest -x spotlessCheck` 후 `build/pjacoco/… +DefaultJwtServiceTest#should_generate_and_parse_token.json`의 `classCount`와 `.exec`에 `DefaultJwtService` +부재 확인. (진단용으로 추가한 `io.spring.probe.PlainCalc/AnnotatedCalc` 및 diag 테스트는 검증 목적 산출물.) + +## 결론 + +- pjacoco current main은 **in-process(순차/병렬, per-TC 격리)·out-of-process·MSA/Kafka·OTel·Sleuth/Brave· + baggage 전파**의 요청 조합 전반에서 동작하며, petclinic/petclinic-rest에서 **total == vanilla(byte 동일)**, + 분산 픽스처 2종에서 **서비스 횡단 per-TC 귀속**을 검증했다. +- 단, **realworld에서 18개 클래스가 조용히 누락(total −22%)** 되는 재현 가능한 결함을 발견하고 **근본 + 원인을 확정**: 에이전트의 **미-shade `org.objectweb.asm`이 타깃 앱의 구 ASM(9.1)과 충돌 → jacoco 계측 + `NoSuchMethodError` → 무음 catch**. **수정(ASM relocate 1줄) 검증 완료** — realworld total이 vanilla와 + 정확히 일치. PR로 반영 권장(+ 무음 catch 신호화). diff --git a/maven-plugin/pom.xml b/maven-plugin/pom.xml index 8bd2d5a..588aaa5 100644 --- a/maven-plugin/pom.xml +++ b/maven-plugin/pom.xml @@ -138,7 +138,9 @@ org.sonatype.central central-publishing-maven-plugin - 0.5.0 + + 0.11.0 true central