4.6 KiB
title, source_type, status, related_branches, related_projects, tags, created, status_label
| title | source_type | status | related_branches | related_projects | tags | created | status_label | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| error / archunit-importpackages-empty-vacuous-stale-build-2026-06-20 | error-note | raw |
|
|
|
2026-06-20 | resolved |
error: archunit-importpackages-empty-vacuous-stale-build-2026-06-20
Layer:
raw/errors/— 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw에 영구 보관한다.
Parent / 부모
- raw/branch-notes/feature-test-taxonomy-fixture-contract — Task 2/3/4 의
TestTaxonomyArchitectureTest가 전체 스위트에서 비결정적으로 실패한 사건.
증상 / Symptom
TestTaxonomyArchitectureTest 는 단독(--tests '*TestTaxonomyArchitectureTest') 실행 시 6/6 PASS 이지만, 전체 :app-bootstrap:test 한 번에서 5건 실패(positive-control 3건 Expecting true but was false + OperationalContractRuntimeTest 2건). 같은 코드로 이후 5회 전체 실행은 모두 PASS → 비결정적(flaky)·재현율 낮음. 실패는 전체 스위트의 첫 실행(리소스 edit 직후, --rerun-tasks 없이)에서만 관측.
근본 원인 / Root cause
각 positive-control 의 코퍼스가 static final JavaClasses = new ClassFileImporter().importPackages("dev.caskeleton.bootstrap.integration") 형태였다. importPackages(String) 은 패키지를 thread-context classloader / 클래스패스 location enumeration 으로 해석하는데, 대형 멀티-컨텍스트 스위트 + 불완전한 incremental build 상태에서 빈 코퍼스를 돌려줄 수 있다. 빈 코퍼스의 영향:
- positive-control(
hasViolation()==true기대): 빈 코퍼스 → 위반 0 → loud FAIL (이게 사건을 잡아줌). - clean-check(
hasViolation()==false기대): 빈 코퍼스 → 위반 0 → silently PASS (vacuous) — 규칙이 아무것도 검사 안 했는데 통과. 더 위험.
즉 이 테스트 자체가 (a) flaky 하고 (b) clean-check 가 vacuous-pass 할 수 있는, test-taxonomy 계약이 금지하는 바로 그 안티패턴이었다.
진단 / Diagnosis
--tests단독 vs 전체 스위트 대조 → 단독 PASS, 전체 FAIL(간헐) → 상호작용/순서 의존.OperationalContractRuntimeTest + TestTaxonomy둘만 함께 실행 → PASS → 특정 클래스 쌍이 아님.- 코퍼스 size + TCCL 을 assertion 메시지에 심어 전체 스위트 반복 실행으로 포착 시도 → 이후 5회 모두 PASS(재현 안 됨) → stale-build 일회성 가능성 높음. 단 vacuous-pass 위험 자체는 설계 결함이라 재현 여부와 무관하게 수정.
해결 / Fix
- positive-control / over-block 코퍼스를
importClasses(SomeFixture.class)클래스 리터럴로 전환 — 특정 클래스 바이트코드만 읽고 패키지 enumeration 을 안 하므로 classloader 상태와 무관하게 결정적.- 슬라이스 fixture(
MixedSliceAnnotationsFixture,SingleSliceWebMvcFixture)는 cross-package.class참조를 위해public으로 승격. - Testcontainers positive-control 은 전용
public TestcontainersUsingFixture(PostgreSQLContainer 필드)를..contract../..architecture..밖(..taxonomyfixtures..)에 두어 clean-check 코퍼스 오염 없이 importClasses.
- 슬라이스 fixture(
- clean-check 2건(contract/architecture 실 패키지 스캔)은 auto-coverage 위해
importPackages유지하되, non-vacuity 가드assertThat(corpus.size()).isGreaterThan(0)추가 → 빈 스캔이면 silent-pass 대신 loud-fail.
검증: TestTaxonomyArchitectureTest 단독 PASS + 전체 :app-bootstrap:test --rerun-tasks 3회 연속 PASS.
교훈 / Lesson
- ArchUnit 규칙의 대상이 test 클래스면
@AnalyzeClasses(DoNotIncludeTests)로는 못 보고 manual importer 가 필요한데(raw/interviews/archunit-manual-importer-vs-analyzeclasses), 그 manual importer 를importPackages(String)static 필드로 쓰면 대형 스위트에서 빈 코퍼스 → vacuous/flaky 위험. - 결정성이 필요한 positive-control 은
importClasses(Class…)(클래스 리터럴) 가 정석 — repo 의ArchitectureViolationFixtureTest가 같은 이유로 isolation 케이스에 importClasses 사용. - clean-check 처럼 패키지 스캔이 불가피하면 non-vacuity 가드(코퍼스 비어있지 않음) 를 반드시 동반 — “규칙이 실제로 무언가를 검사했다”를 보장. 관련: raw/errors/archunit-empty-should-anchor-2026-05-27, raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28.