sleepを使わず時刻境界を再現する
한국어 원문으로 표시합니다.
목표
가짜 시계로 제한 창·재시도 시각·사용자별 격리를 검증합니다.
왜 중요한가
요청 제한 테스트에 sleep을 넣었더니 개발 PC에서는 통과하고 CI에서는 실패했다. 느린 실행기가 시간 경계를 바꾸고 테스트 자체도 오래 걸렸다. 시간은 프로그램의 입력이므로 호출자가 제어할 수 있게 만들고 경계를 정확히 밟아야 한다.
단계
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: validate_limit(limit, window)는 bool 제외 양의 int limit와 양의 유한 int/float window만 허용해 (limit, float(window))를 반환합니다. 나머지는 ValueError입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.
처음 한 번 준비하세요. 기존 파일은 덮어쓰지 않습니다.
mkdir -p /root/work/test-clock-window-lab
test -e /root/work/test-clock-window-lab/service.py || cp /opt/fixtures/ten_labs/test-clock-window-lab/service.py /root/work/test-clock-window-lab/service.py
test -e /root/work/test-clock-window-lab/test_service.py || cp /opt/fixtures/ten_labs/test-clock-window-lab/test_service.py /root/work/test-clock-window-lab/test_service.py
cd /root/work/test-clock-window-lab
-
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: active(history, now, window)는 now-window보다 큰 시각만 원래 순서의 새 리스트로 반환합니다. history는 정렬된 비감소 시각입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요. -
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: retry_after(history, now, window)는 이미 정리된 비어 있지 않은 history의 첫 시각+window-now를 ceil한 값과 0 중 큰 정수입니다. 빈 리스트는 0입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요. -
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: history_for(state, key)는 없는 키면 빈 리스트, 있으면 그 기록의 사본을 반환합니다. 조회만으로 state를 수정하지 않습니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요. -
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: admit(state, key, now, limit, window)는 설정 검증 후 해당 키의 만료 기록을 정리합니다. 여유가 있으면 now를 추가하고 (True,0), 꽉 찼으면 추가하지 않고 (False,retry_after)를 반환합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요. -
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: client_key(value)는 1~40자 ASCII 영문·숫자·하이픈 문자열을 그대로 반환하고 나머지는 ValueError입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요. -
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: limited_response(wait)는 상태 429, 본문 {error:'rate_limited'}, Retry-After 헤더는 wait를 문자열로 만든 JSONResponse입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요. -
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: create_app(clock, limit=2, window=10)는 GET /work에서 X-Client-ID를 검사해 잘못된 키는 400 {error:'invalid_client'}, 허용은 200 {ok:True}, 초과는 limited_response입니다. state는 앱별로 분리합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.
참고
- 인터넷과 패키지 설치 없이 기존 lab-dev 환경에서 수행합니다.
- 각 단계는 45초 채점 예산 안에서 실행됩니다. 실제 sleep이나 네트워크 호출을 추가하지 마세요.
- 제출 테스트는 별도 임시 폴더에서 정상·결함 구현에 실행합니다. 정상에서는 실제 실행한 테스트가 모두 통과하고 결함에서는 테스트 본문이 실패해야 합니다. 수집 오류, 실행 0개, 전부 건너뜀, 강제 종료는 통과가 아닙니다. pytest 기본 기능과 제공 라이브러리만 사용하세요.
- FastAPI 공식 문서 · pytest 공식 문서 · Python sqlite3
- 한계: 프로세스 메모리에 있는 단일 워커용 예제다. 여러 파드가 공유하는 전역 한도나 악의적인 클라이언트의 신원을 보장하지 않는다. X-Client-ID는 테스트용 키이므로 운영에서는 인증된 주체에서 키를 얻어야 한다. 지속적인 시계 역행은 단조 시계 사용으로 피해야 하며 이 실습의 clock은 비감소한다. 제공 구현은 읽어도 되지만 채점은 별도 사본을 사용한다. 소스 문구 검사나 파일 수정으로 결함을 우회하지 말고 공개 인터페이스의 실행 결과를 검사한다.
설정을 검증한다 — 테스트
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: validate_limit(limit, window)는 bool 제외 양의 int limit와 양의 유한 int/float window만 허용해 (limit, float(window))를 반환합니다. 나머지는 ValueError입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.
처음 한 번 준비하세요. 기존 파일은 덮어쓰지 않습니다.
mkdir -p /root/work/test-clock-window-lab
test -e /root/work/test-clock-window-lab/service.py || cp /opt/fixtures/ten_labs/test-clock-window-lab/service.py /root/work/test-clock-window-lab/service.py
test -e /root/work/test-clock-window-lab/test_service.py || cp /opt/fixtures/ten_labs/test-clock-window-lab/test_service.py /root/work/test-clock-window-lab/test_service.py
cd /root/work/test-clock-window-lab
bool은 int의 하위 타입입니다. NaN과 무한대도 따로 거절해야 합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.
저장 후 bash /opt/lab/checks/test-clock-window-lab/01-contract.sh로 확인하세요.
창의 왼쪽 경계를 제외한다 — 테스트
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: active(history, now, window)는 now-window보다 큰 시각만 원래 순서의 새 리스트로 반환합니다. history는 정렬된 비감소 시각입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.
정확히 만료된 시각을 남기는 >=와 >의 차이를 확인합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.
저장 후 bash /opt/lab/checks/test-clock-window-lab/02-contract.sh로 확인하세요.
대기 시간을 올림한다 — 테스트
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: retry_after(history, now, window)는 이미 정리된 비어 있지 않은 history의 첫 시각+window-now를 ceil한 값과 0 중 큰 정수입니다. 빈 리스트는 0입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.
0.2초 남았다고 Retry-After를 0으로 내면 클라이언트가 즉시 재요청합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.
저장 후 bash /opt/lab/checks/test-clock-window-lab/03-contract.sh로 확인하세요.
키마다 기록을 나눈다 — 테스트
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: history_for(state, key)는 없는 키면 빈 리스트, 있으면 그 기록의 사본을 반환합니다. 조회만으로 state를 수정하지 않습니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.
공유 리스트를 반환하면 한 요청의 정리가 다른 요청의 기록을 바꿀 수 있습니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.
저장 후 bash /opt/lab/checks/test-clock-window-lab/04-contract.sh로 확인하세요.
허용 요청만 기록한다 — 테스트
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: admit(state, key, now, limit, window)는 설정 검증 후 해당 키의 만료 기록을 정리합니다. 여유가 있으면 now를 추가하고 (True,0), 꽉 찼으면 추가하지 않고 (False,retry_after)를 반환합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.
거절된 요청을 추가하면 재시도할 때마다 만료 시각이 밀립니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.
저장 후 bash /opt/lab/checks/test-clock-window-lab/05-contract.sh로 확인하세요.
클라이언트 키를 검증한다 — 테스트
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: client_key(value)는 1~40자 ASCII 영문·숫자·하이픈 문자열을 그대로 반환하고 나머지는 ValueError입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.
무제한 키 크기로 상태 메모리를 압박하지 않게 입력의 범위를 제한합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.
저장 후 bash /opt/lab/checks/test-clock-window-lab/06-contract.sh로 확인하세요.
거절 응답을 만든다 — 테스트
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: limited_response(wait)는 상태 429, 본문 {error:'rate_limited'}, Retry-After 헤더는 wait를 문자열로 만든 JSONResponse입니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.
클라이언트가 재시도 시간을 알 수 있게 상태와 헤더를 함께 보냅니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.
저장 후 bash /opt/lab/checks/test-clock-window-lab/07-contract.sh로 확인하세요.
가상 시간으로 요청 흐름을 끝낸다 — 테스트
/root/work/test-clock-window-lab/test_service.py에서 제공된 service.py의 다음 공개 계약을 테스트하세요: create_app(clock, limit=2, window=10)는 GET /work에서 X-Client-ID를 검사해 잘못된 키는 400 {error:'invalid_client'}, 허용은 200 {ok:True}, 초과는 limited_response입니다. state는 앱별로 분리합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.
실제로 sleep하지 말고 리스트에 담은 현재 시각을 clock 함수로 전달합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.
저장 후 bash /opt/lab/checks/test-clock-window-lab/08-contract.sh로 확인하세요.