Files
llm-wiki/raw/errors/archunit-importpackages-empty-vacuous-stale-build-2026-06-20.md

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
feature-test-taxonomy-fixture-contract
ca-skeleton
error
ca-skeleton
archunit
testing
flaky-test
vacuous-pass
importpackages
classloader
2026-06-20 resolved

error: archunit-importpackages-empty-vacuous-stale-build-2026-06-20

Layer: raw/errors/ — 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw에 영구 보관한다.

Parent / 부모

증상 / 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

  1. 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.
  2. 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.