Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions docs-site/docs/contributor-guide/testing-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ often enough to name:
- **A tool found nothing**, and the result alone cannot say whether there was
nothing to find or whether the tool was not looking there. A repository lint
run from the wrong tree reports zero findings for a file it never opened.
Two ways a hand-built pattern misses what is in front of it. `gh` and
*The pattern only finds the notation you imagined.* `gh` and
`EXPLAIN` return JSON, and a name like `lint (backend)` or a plan node
nested under `SubPlan` does not match a pattern built without expecting
parentheses or nesting, so parentheses, brackets and dots inside a value
Expand All @@ -496,7 +496,20 @@ often enough to name:
compare exact strings; this repository already asserts over ASTs in several
places, so it is not a new tool to reach for.

A specific wrong answer is more dangerous than an empty one. Nobody trusts a
*A parse can be read as text again.* A census searched `ast.dump()` output
for `"alembic"` in double quotes and reported zero, since dump writes
apostrophes. Read the nodes.

*What you erase in order to compare is what counts as the same.* That
decision, not the comparison after it, produces the answer. Erasing string
literals to compare fixtures merged eight unrelated ones, the strings being
the payload; erasing decorators would have merged different lifetimes.
*A wider definition needs a fresh sample.* Counting a second way of
reaching the database swept in eighty-three modules that never open a
connection, so re-read what a census catches after every change to what it
counts. That one was wrong six times, five of them right after it grew.

*A specific wrong answer is more dangerous than an empty one.* Nobody trusts a
tool that returns nothing, but "13 of 17 checks never ran" carries a count
and a list, and the detail is what makes it credible: it looks like
arithmetic, and it is arithmetic on a broken premise. Counting is the common
Expand All @@ -508,12 +521,23 @@ often enough to name:
confirm it does. If the answer is a number, check what the number is
attached to.

Testing the checker has its own version of this. The input you plant has to
*The counterpart: a tool that says what it does not do.* `tools/static-checks`
names the CI steps it deliberately omits and why, because a local runner that
silently skips a check and reports success is worse than one admitting the
gap. Its contract test against the workflow is what caught a later change
adding a CI step without registering it - not review.

*Testing the checker has its own version of this.* The input you plant has to
be something the checker declares it catches, or "it did not fire" means
both "the checker is not running" and "that was never a violation". A
clumsy Korean sentence planted to prove a style lint was reading a new file
went uncaught because no rule in its catalogue covered that shape; a real
rule from the catalogue was caught immediately.
- **A rule was induced from a single example**, so that example's accidents are
inside it. An exemption list with one entry asserted that exempt modules have
no database gate, true of that one by coincidence; the next three keep their
gates deliberately and it failed for all three. A second instance is what
separates the essential from the incidental, in a rule as much as in data.
- **A fixture never populated the field**, so a guard over that field is blind
and reports success for every input.
- **Isolation removed a condition rather than removing noise.** Running one
Expand Down Expand Up @@ -550,6 +574,24 @@ substring, so the containment check passed and read as a weak contract. Assert
the state the mutation was supposed to produce, not that the text changed:
here, that the old name is absent.

Did it land is worth asking before a bulk change too. Declare how many files an
edit must touch and stop with nothing written on a mismatch, because a pattern
matching nothing reports "nothing to do", which reads as "not applicable". Over
193 modules that stopped three runs, all three the operator's arithmetic rather
than the tool - twice `wc -l` on a list with no trailing newline, which counts
newlines and is short by one - and never the transformation, which says which
input was weaker. Count independently of the tool, or the two always agree and
nothing is checked, and hold the same bar whatever the size: one file edited by
hand is not safer than a hundred edited by a transformation already run against
the whole tree.

A declared number can also disagree with you. A prediction that a batch would
shrink by one came back unchanged, the model of which list the file belonged to
being wrong; unwritten, the correct result would have read as confirmation. And
two identical failures mean the thing being changed is not the variable: three
repairs to a docstring each moved the insertion point and each failed the same
way, because the inserted fragment carried a stray terminator.

Two layers that can produce the same outcome hide each other: delete either
and the result is unchanged, so neither is verified. The reverse also
happens, where an outer check keeps the inner one unreachable, so what looks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,18 @@ def test_normalize_spdx_id(raw: str, expected: list[str]) -> None:

- **조작이 대상에 닿지 않은 경우.** 아무것도 바뀌지 않은 상태에서 통과합니다. 규칙 8을 참고하십시오.
- **쓰는 코드와 읽는 코드가 같은 경우.** 어떤 구현이든 자기 자신과는 일치합니다.
- **도구가 아무것도 찾지 못한 경우.** 결과만으로는 찾을 것이 없었는지 그곳을 보지 않았는지 구분되지 않습니다. 저장소 린트를 엉뚱한 트리에서 돌리면 열어 보지도 않은 파일에 대해 0건을 보고합니다. 손으로 만든 패턴이 눈앞의 것을 놓치는 방식이 둘입니다. `gh`도 `EXPLAIN`도 JSON을 돌려주는데 `lint (backend)` 같은 이름이나 `SubPlan` 아래 중첩된 노드는 괄호와 중첩을 예상하지 않은 패턴에 걸리지 않습니다. 값 안의 괄호·대괄호·점이 정규식에서는 다른 뜻입니다. 소스에서 세는 것도 같습니다. 테스트 파일의 5요소 튜플 다섯 중 `grep`은 셋만 찾았고, 놓친 둘은 패턴이 예상하지 못한 모양으로 쓰여 있었으며 그중 하나는 두 번째 헬퍼 안에 있었습니다. 패턴은 내가 상상한 표기를 찾고 파싱은 실제로 있는 것을 찾습니다. 구조를 파싱하거나 정확한 문자열로 비교하십시오. 이 저장소는 이미 여러 곳에서 AST로 단언하니 새로 꺼내는 도구도 아닙니다.
- **도구가 아무것도 찾지 못한 경우.** 결과만으로는 찾을 것이 없었는지 그곳을 보지 않았는지 구분되지 않습니다. 저장소 린트를 엉뚱한 트리에서 돌리면 열어 보지도 않은 파일에 대해 0건을 보고합니다. *패턴은 내가 상상한 표기만 찾습니다.* 손으로 만든 패턴이 눈앞의 것을 놓치는 방식이 둘입니다. `gh`도 `EXPLAIN`도 JSON을 돌려주는데 `lint (backend)` 같은 이름이나 `SubPlan` 아래 중첩된 노드는 괄호와 중첩을 예상하지 않은 패턴에 걸리지 않습니다. 값 안의 괄호·대괄호·점이 정규식에서는 다른 뜻입니다. 소스에서 세는 것도 같습니다. 테스트 파일의 5요소 튜플 다섯 중 `grep`은 셋만 찾았고, 놓친 둘은 패턴이 예상하지 못한 모양으로 쓰여 있었으며 그중 하나는 두 번째 헬퍼 안에 있었습니다. 패턴은 내가 상상한 표기를 찾고 파싱은 실제로 있는 것을 찾습니다. 구조를 파싱하거나 정확한 문자열로 비교하십시오. 이 저장소는 이미 여러 곳에서 AST로 단언하니 새로 꺼내는 도구도 아닙니다.

구체적인 오답은 빈 답보다 위험합니다. 아무것도 내지 않는 도구는 아무도 믿지 않지만, "17개 중 13개가 실행되지 않았다"에는 숫자와 목록이 있고 그 세부가 신뢰의 근거가 됩니다. 계산이 된 것처럼 보이는데 틀린 전제 위에서 된 계산입니다. 세는 것이 그런 답에 이르는 흔한 길입니다. 세면 그 수가 무엇들 사이의 관계였는지가 버려지기 때문입니다. 인증서를 마운트한 차트를 렌더했더니 마운트 경로가 넷이었고 그것이 기대한 수였는데, 그중 하나는 뒤에 볼륨이 없어 Kubernetes가 거부할 파드였습니다. 새로 만든 검사가 그럴듯한 답을 내면 잡혀야 하는 입력을 하나 넣어 실제로 잡는지 확인하십시오. 답이 숫자라면 그 숫자가 무엇에 붙어 있는지까지 봅니다.
*파싱한 결과를 다시 텍스트로 읽을 수 있습니다.* 그래서 파싱은 절반입니다. census가 `ast.dump()` 출력에서 `"alembic"`을 큰따옴표로 찾아 0건을 보고했는데 dump는 작은따옴표로 씁니다. 노드를 직접 읽으십시오.

검사기를 시험하는 데에도 같은 것이 있습니다. 심는 입력은 그 검사기가 잡겠다고 선언한 것이어야 합니다. 아니면 "안 걸렸다"가 검사기가 안 도는 것과 그것이 애초에 위반이 아닌 것을 동시에 뜻합니다. 한국어 문체 린트가 새 파일을 읽는지 보려고 어색한 문장을 심었는데 카탈로그에 그 형태의 규칙이 없어 안 걸렸고, 카탈로그에서 실제 규칙을 골라 심으니 바로 걸렸습니다.
*비교하려고 무엇을 지우는지가 곧 같음의 정의입니다.* 결과를 정하는 것은 뒤따르는 비교가 아니라 그 정의입니다. 픽스처 중복을 세며 문자열 상수를 지웠더니 서로 다른 여덟 개가 하나로 묶였습니다. 어느 환경변수인지 어느 팀인지가 그 문자열에 담기기 때문입니다. 데코레이터를 지웠다면 수명이 다른 것들이 하나로 묶였을 것입니다. *정의를 넓히면 표본을 다시 봐야 합니다.* 넓히기 전의 표본은 지금 걸리는 것을 덮지 못합니다. 데이터베이스에 닿는 두 번째 경로까지 세도록 넓혔더니 연결을 열지 않는 모듈 83개가 딸려 왔습니다. 세는 기준을 바꿀 때마다 무엇이 걸리는지 다시 읽으십시오. 그 census는 여섯 번 틀렸고 다섯 번이 넓힌 직후였습니다.

*구체적인 오답은 빈 답보다 위험합니다.* 아무것도 내지 않는 도구는 아무도 믿지 않지만, "17개 중 13개가 실행되지 않았다"에는 숫자와 목록이 있고 그 세부가 신뢰의 근거가 됩니다. 계산이 된 것처럼 보이는데 틀린 전제 위에서 된 계산입니다. 세는 것이 그런 답에 이르는 흔한 길입니다. 세면 그 수가 무엇들 사이의 관계였는지가 버려지기 때문입니다. 인증서를 마운트한 차트를 렌더했더니 마운트 경로가 넷이었고 그것이 기대한 수였는데, 그중 하나는 뒤에 볼륨이 없어 Kubernetes가 거부할 파드였습니다. 새로 만든 검사가 그럴듯한 답을 내면 잡혀야 하는 입력을 하나 넣어 실제로 잡는지 확인하십시오. 답이 숫자라면 그 숫자가 무엇에 붙어 있는지까지 봅니다.

*반대쪽은 자기가 하지 않는 것을 말하는 도구입니다.* `tools/static-checks`는 CI가 돌리는 것을 돌리고, 일부러 빼는 단계를 이유와 함께 이름으로 적어 둡니다. 검사를 조용히 빼고 성공을 보고하는 로컬 러너가 빠졌다고 밝히는 것보다 나쁘기 때문입니다. 계약 테스트가 그 목록을 워크플로와 대조하는데, 나중에 누가 CI에 단계를 더하면서 그 목록에 등록하지 않았을 때 그것을 알아챈 것이 검토가 아니라 그 테스트였습니다.

*검사기를 시험하는 데에도 같은 것이 있습니다.* 심는 입력은 그 검사기가 잡겠다고 선언한 것이어야 합니다. 아니면 "안 걸렸다"가 검사기가 안 도는 것과 그것이 애초에 위반이 아닌 것을 동시에 뜻합니다. 한국어 문체 린트가 새 파일을 읽는지 보려고 어색한 문장을 심었는데 카탈로그에 그 형태의 규칙이 없어 안 걸렸고, 카탈로그에서 실제 규칙을 골라 심으니 바로 걸렸습니다.
- **사례 하나에서 만든 규칙.** 그 사례의 우연한 성질이 규칙 안에 들어가고, 본질적인 것과 구분되지 않습니다. 항목이 하나뿐이던 예외 목록이 "예외 모듈은 데이터베이스 게이트가 없다"를 단언하고 있었는데, 그것은 그 하나가 우연히 그랬던 것입니다. 뒤에 추가된 예외 셋은 자기 게이트를 의도적으로 유지하고, 그 단언은 셋 다에 대해 실패했습니다. 예외의 뜻은 "자기 방식을 유지해도 된다"이지 "게이트가 없다"가 아닙니다. 본질과 우연을 가르는 것은 두 번째 사례이고, 이것은 데이터에서만이 아니라 규칙에서도 그렇습니다.
- **픽스처가 채우지 않은 필드.** 그 필드를 보는 가드는 눈이 먼 상태이고 모든 입력에 성공을 보고합니다.
- **격리가 잡음이 아니라 조건을 없앤 경우.** 파일 하나만 돌리기, 최소 픽스처 쓰기, 의존성을 목으로 대체하기, 함수를 직접 부르기가 모두 조건을 지우는 것이 아니라 바꾸는 것입니다. celery 태스크 등록 검사가 그 파일만 돌리면 실패하고 전체 스위트에서는 통과했는데, 단독 실행의 실패를 "깨끗한 조건에서의 결과"로 읽었습니다. 다른 테스트 모듈이 수행하는 import가 그 단언이 기대던 조건의 일부였습니다. 격리한 실행에서 결론을 내리기 전에, 그 격리가 무엇을 없앴는지 먼저 이름 붙입니다.
- **대조 실험을 그것이 답할 수 있는 범위 밖까지 읽은 경우.** 같은 테스트를 내 브랜치와 `main`에서 돌리면 내 변경이 그 결과를 만들었는지만 알 수 있고 무엇이 만들었는지는 알 수 없습니다. 양쪽이 공유하는 것은, 테스트를 부른 방식까지 포함해 대조에 보이지 않습니다. 같은 celery 검사가 두 브랜치에서 똑같이 실패했는데, 그것으로 "내 변경 때문은 아니다"까지 확인해 놓고 "내가 부른 방식 때문도 아니다"까지 읽었습니다. 브랜치 둘이든 환경 둘이든 설정 둘이든 모든 A/B에 같은 한계가 있습니다.
Expand All @@ -303,6 +310,10 @@ def test_normalize_spdx_id(raw: str, expected: list[str]) -> None:

변이 자체도 위 목록의 모든 것에 걸립니다. 변이가 시험하려는 그 문자열 매칭에도 걸립니다. 문서 계약이 변수가 페이지에서 사라진 것을 잡는지 보려고 `GIT_SSL_CAPATH`를 `GIT_SSL_CAPATH_REMOVED_FROM_DOCS`로 바꿨는데, 원래 이름이 부분 문자열로 남아 포함 검사가 그대로 통과했고 그것이 계약이 약하다는 근거로 읽혔습니다. 텍스트가 바뀌었다가 아니라 변이가 만들려던 상태를 단언하십시오. 여기서는 옛 이름이 없다는 것입니다.

적용됐는지를 묻는 것은 대량 변경 앞에서도 쓸모가 있습니다. 편집이 몇 개 파일에 닿아야 하는지를 먼저 선언하고 수가 어긋나면 아무것도 쓰지 않고 멈추게 하십시오. 아무것도 매칭하지 않은 패턴은 "할 일이 없다"로 보고하고 그것이 "해당 없음"으로 읽힙니다. 193개 모듈 변환에서 이 검사가 세 번 멈췄는데 세 번 다 도구가 아니라 작업자의 계산이었고, 그중 둘은 끝 개행이 없는 목록에 `wc -l`을 쓴 것입니다. 반대로 변환 자체가 틀린 것을 잡은 적은 없습니다. 어느 쪽 입력이 약한지를 말해 줍니다. 다만 그 수는 도구와 독립적으로 세야 하고, 도구가 세는 방식으로 예상도 세면 늘 일치해 아무것도 확인하지 못합니다. 기준은 크기가 아니라 방식을 따릅니다. 손으로 고친 파일 하나가 전 트리에 돌려 본 변환으로 고친 백 개보다 안전하지 않습니다.

선언한 수는 나와 어긋날 수도 있습니다. 어떤 배치가 하나 줄 것이라 예상했는데 그대로 나왔고, 그 파일이 어느 목록에 속하는지에 대한 모형이 틀렸던 것입니다. 적어 두지 않았으면 맞는 결과가 확인으로 읽혔을 것입니다. 그리고 두 번 바꿨는데 실패가 똑같으면 바꾸고 있는 그것이 변수가 아닙니다. 깨진 docstring을 세 번 고치면서 매번 삽입 위치를 옮겼고 매번 같은 오류가 났는데, 넣던 조각이 docstring을 닫는 따옴표를 달고 있었기 때문입니다.

같은 결과를 낼 수 있는 층이 둘이면 서로를 가립니다. 어느 쪽을 지워도 결과가 같으므로 둘 다 검증되지 않습니다. 반대 경우도 있습니다. 바깥 검사가 안쪽을 도달 불가로 만들고 있으면 겹쳐 보이는 방어가 실은 하나이고, 바깥을 완화하는 순간 안쪽이 조용히 사라집니다. 도입 준비도 점검에서 세 건이 나왔습니다. 매니페스트 사전 점검 때문에 도달할 수 없던 체크섬 검증, 라우터의 조기 거절과 카운터 가드가 서로를 덮던 로그인 잠금, GRANT 층에 가려 한 번도 시험된 적 없던 감사 트리거입니다. 각 층이 실제로 도달되는지와 혼자서도 작동하는지를 따로 단언합니다.

### 회귀망은 의도적으로 두 겹
Expand Down
Loading