[base] Add nocompile tests for SequenceBound

MERGED2026base
2026. 8. 24.Indigochi1d 프로필 이미지Indigochi1d

base::SequenceBound의 AsyncCall() 빌더 체인이 강제하는 호출 순서 계약을 검증하는 nocompile 테스트를 추가하고,
그 자리를 차지하고 있던 빈 껍데기 테스트를 제거했습니다. 2020년부터 6년간 방치된 TODO를 구현한 작업입니다.

문제 설명

SequenceBound의 메서드 호출은 AsyncCall → WithArgs → Then 순서를 지켜야 한다. AsyncCallBuilderImpl의 부분 특수화 3종이 각 단계에서 유효한 메서드만 노출해서 이를 강제한다.

  • sequence_bound_unittest.ccTYPED_TEST(SequenceBoundTest, NoCompileTests)는 본문 33줄이 전부 주석이었다. 검증하는 게 없었다.
  • TYPED_TEST라 두 변형으로 등록되어 항상 통과했다. 테스트 목록에는 검증이 있는 것처럼 보였다.
  • 검증 대상이 "컴파일되면 안 된다"인데 gtest로는 표현할 수 없다. // TODO(crbug.com/40245687): Maybe use the nocompile harness here instead of being "clever"... 라고 적어둔 문제를 해결한 경우이다.

해결 내용

어떻게 문제를 해결했는지 설명하세요.

  1. .nc 파일로 이전. clang -verify가 expected-error 주석과 실제 진단을 대조한다. 기대한 에러가 안 나도, 예상 못 한 에러가 나도 실패한다.
  2. base/threading/sequence_bound_nocompile.nc 신규 (43줄), base/BUILD.gn에 등록, sequence_bound_unittest.cc 1083~1116행 삭제.
  3. sequence_bound.h는 수정하지 않았다. Sequence를 강제하는 장치는 이미 정상 동작한다.
삭제한 것
TYPED_TEST(SequenceBoundTest, NoCompileTests) {
  // TODO: Test calling WithArgs() on a method that takes no arguments.
  // ... 주석 33줄, 코드 0줄
}
#undef SequenceBound   // 이 줄은 유지. 지우면 아래 death test가 깨진다.
추가한 것

base/threading/sequence_bound_nocompile.nc 를 신규로 생성하여 chromium의 nocomplie 규칙에 따라 파일을 만들었다.
아래의 코드는 주요내용만 표시하였고 주석이나 파일 컨벤션 내용은 제외하였다.

// `WithArgs()` may only be used with methods that accept arguments.
class NoArgs {
 public:
  void Method();
};

void CallWithArgsOnMethodTakingNoArgs() {
  SequenceBound<NoArgs> sq(SequencedTaskRunner::GetCurrentDefault());
  sq.AsyncCall(&NoArgs::Method).WithArgs(1);  // expected-error@*:* {{no member named 'WithArgs' in}}
}

// `Then()` may only be used after `WithArgs()`.
class HasArgs {
 public:
  void Method(int);
};

void CallThenBeforeWithArgs() {
  SequenceBound<HasArgs> sq(SequencedTaskRunner::GetCurrentDefault());
  sq.AsyncCall(&HasArgs::Method)
      .Then(DoNothing())  // expected-error@*:* {{no member named 'Then' in}}
      .WithArgs(1);
}

테스트 방법

.nc 파일은 바이너리에 링크되지 않는다. wrapper.py가 clang++ -fsyntax-only -Xclang -verify를 돌리고 빈 .o를 만들어 source_set처럼 위장한다. 빌드가 곧 테스트다.

autoninja -C out/Default base_nocompile_tests

배운 점

기술

  • 디스크 쓰기 같은 느린 작업을 UI 스레드에서 하면 브라우저 화면이 멈춘다. 백그라운드로 보내야 하는데 이 작업을 담당하는 객체를 두 스레드가 동시에 만지면 내부 상태가 깨진다.
    Chromium은 이걸 락이 아니라 시퀀스로 푼다. 시퀀스는 작업이 순서대로 실행되는 논리적 흐름이다. 시퀀스 하나에만 갇힌 객체는 락 없이 안전하다. 그 작업순서 실행보장을 AsyncCall(필수) → WithArgs(인자있을때 필요) → Then(선택) 로 하고 있다.
  • .Then(...).WithArgs(...)는 두 군데가 잘못돼 보이지만 에러는 1개다. .Then()에서 표현식이 error type이 되면 뒤는 진단을 내지 않는다. expected-error를 2개 쓰면 테스트가 실패한다.
  • 에러 메시지는 추측하지 않는다. expected-error 없이 먼저 빌드하면 clang이 실제 메시지를 출력한다.

프로세스

  • git blame으로 CL을 찾아 리뷰 코멘트를 읽은 게 도움이 됐다. CL:4004169 에서 dcheng의 description과 patchset을 보고 이 작업의 맥락을 파악하며 나의 작업범위가 무엇인지 알 수 있었다.
  • 처음엔 타입 안정성 문제로 봤는데, 직접 컴파일해보니 BindOnce의 arity 검사가 조용히 통과하는 경우는 없었다. 실제 가치는 진단 품질이고 원저자도 리뷰에서 "Better error messages" 라고 해서 작업범위를 결정할 수 있었다.
  • 처음엔 SequenceBound<std::unique_ptr<T>> 형태까지 4케이스로 썼다. 근거는 "기존 TYPED_TEST가 두 형태를 커버했다"였는데 틀렸었다. 삭제한 테스트는 비어 있어서 어느 형태도 커버하지 않았고, AsyncCall이 빌더를 고를 때 쓰는 템플릿 인자는 전부 메서드 시그니처에서 나온다(sequence_bound.h:227). 두 형태의 UnwrappedT도 같다. 중복이라 패치셋 2에서 제거했다.

향후

  • sequence_bound.h:635의 변환 제약을 구현하면 이번에 이관한 TODO를 따라 나머지 테스트를 채울 수 있다.

참고 자료

참고 문서
참고 CL 및 코드