From 082584822b635fd575faa695583b099409b5c0a2 Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sat, 5 Sep 2026 07:01:33 +0900 Subject: [PATCH 1/3] Fold what the gate conversion taught into rules 7 and 8 No new rules. Eight lessons from the ER66 series go where they belong: switching to a parse does not help if its output is read as text again; what a comparison normalises away is its definition of sameness; widening what a census counts invalidates the sample taken before; a rule induced from one example carries that example's accidents. And in rule 8, that 'did it land' applies before a bulk change too: declare the count, keep it independent of the tool, hold the same bar whatever the size, and treat two identical failures as evidence that the thing being changed is not the variable. --- .../docs/contributor-guide/testing-guide.md | 64 +++++++++++++++++++ .../contributor-guide/testing-guide.md | 15 +++++ 2 files changed, 79 insertions(+) diff --git a/docs-site/docs/contributor-guide/testing-guide.md b/docs-site/docs/contributor-guide/testing-guide.md index 3ec72580..89a5e0ef 100644 --- a/docs-site/docs/contributor-guide/testing-guide.md +++ b/docs-site/docs/contributor-guide/testing-guide.md @@ -496,6 +496,26 @@ 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. + Reaching for a parse is only half of it, because the parse's output can be + read as text again. A census of database gates searched `ast.dump()` output + for `"alembic"` in double quotes and reported zero, since dump writes + apostrophes: the string matching had moved up a level rather than gone away. + Read the nodes. + + What such a tool normalises away is its definition of "the same", and that + decision, not the comparison that follows it, produces the answer. Comparing + test fixtures for duplication, erasing string literals merged eight unrelated + fixtures into one group because for a fixture the strings are the payload - + which variable, which team - and erasing decorators would have merged + fixtures with different lifetimes into one recommendation. Whenever the + definition widens, the sample taken before it no longer covers what it now + catches: the same census, widened to count a second way of reaching the + database, swept in eighty-three modules that skip over a missing binary and + never open a connection. It was wrong six times while being written, once by + counting too few and five times by counting too many, and each of the five + arrived immediately after the definition grew. Re-read what it catches after + every change to what it counts. + 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 @@ -508,12 +528,27 @@ often enough to name: confirm it does. If the answer is a number, check what the number is attached to. + The counterpart is to make a tool say what it does not do. `tools/static-checks` + runs what CI runs, and names the steps it deliberately leaves out along with + the reason for each, because a local runner that silently omits a check and + then reports success is worse than one that admits the gap. A contract test + holds that list against the workflow, and when a later change added a CI step + without registering it there, that test is what noticed - 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 the example's incidental + properties are inside the rule and nothing distinguishes them from its + essential ones. An exemption list with one entry asserted that exempt + modules have no database gate, which was true of that one module by + accident; the next three exemptions keep their own gates deliberately, and + the assertion failed for all three. What an exemption means is "may keep its + own arrangement", not "has none". A second instance is what separates the + essential from the incidental, in a rule as much as in a fixture. - **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 @@ -550,6 +585,35 @@ 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. +The same question - did it land - is worth asking before a bulk change as +well as after a mutation. Declare how many files an edit must touch and make a +mismatch stop the run with nothing written, because a pattern matching nothing +otherwise reports "nothing to do", which reads as "not applicable here". On the +193-module gate conversion that check stopped three runs, and all three were +arithmetic on the operator's side rather than a fault in the tool: a file +counted from the wrong directory, and twice `wc -l` on a list with no trailing +newline, which counts newlines and so is short by one. It never once caught the +transformation being wrong. That is worth stating plainly, because it says which +input was the weaker one. + +A declared number has a second use that is easy to miss: it can disagree with +you. A prediction that one batch would shrink by one came back unchanged, and +the reason was a wrong model of which list the file belonged to. Without the +prediction written down, the correct result would have read as confirmation and +the wrong model would have survived intact. + +Keep the count independent of the tool, though - if the prediction is computed +the way the tool computes it, 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 that has already run against the whole tree; +parsing every file afterwards costs one command either way. + +And if changing something twice produces an identical failure, the thing being +changed is not the variable. Three attempts to repair a mangled docstring each +moved the insertion point and each produced the same error, because the fragment +being inserted carried a stray docstring terminator. The second identical result +was already enough to say the insertion point was not the problem. + 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 diff --git a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/contributor-guide/testing-guide.md b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/contributor-guide/testing-guide.md index a441a19f..ef7159d0 100644 --- a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/contributor-guide/testing-guide.md +++ b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/contributor-guide/testing-guide.md @@ -290,9 +290,16 @@ def test_normalize_spdx_id(raw: str, expected: list[str]) -> None: - **쓰는 코드와 읽는 코드가 같은 경우.** 어떤 구현이든 자기 자신과는 일치합니다. - **도구가 아무것도 찾지 못한 경우.** 결과만으로는 찾을 것이 없었는지 그곳을 보지 않았는지 구분되지 않습니다. 저장소 린트를 엉뚱한 트리에서 돌리면 열어 보지도 않은 파일에 대해 0건을 보고합니다. 손으로 만든 패턴이 눈앞의 것을 놓치는 방식이 둘입니다. `gh`도 `EXPLAIN`도 JSON을 돌려주는데 `lint (backend)` 같은 이름이나 `SubPlan` 아래 중첩된 노드는 괄호와 중첩을 예상하지 않은 패턴에 걸리지 않습니다. 값 안의 괄호·대괄호·점이 정규식에서는 다른 뜻입니다. 소스에서 세는 것도 같습니다. 테스트 파일의 5요소 튜플 다섯 중 `grep`은 셋만 찾았고, 놓친 둘은 패턴이 예상하지 못한 모양으로 쓰여 있었으며 그중 하나는 두 번째 헬퍼 안에 있었습니다. 패턴은 내가 상상한 표기를 찾고 파싱은 실제로 있는 것을 찾습니다. 구조를 파싱하거나 정확한 문자열로 비교하십시오. 이 저장소는 이미 여러 곳에서 AST로 단언하니 새로 꺼내는 도구도 아닙니다. + 파싱으로 바꾸는 것은 절반입니다. 파싱한 결과를 다시 텍스트로 읽을 수 있기 때문입니다. 데이터베이스 게이트를 세는 census가 `ast.dump()` 출력에서 `"alembic"`을 큰따옴표로 찾아 0건을 보고했는데, dump는 작은따옴표로 씁니다. 문자열 매칭이 없어진 것이 아니라 한 층 위로 옮겨 간 것입니다. 노드를 직접 읽으십시오. + + 그런 도구가 무엇을 지우고 비교하는지가 곧 무엇을 같다고 볼 것인지의 정의이고, 결과를 정하는 것은 뒤따르는 비교가 아니라 그 정의입니다. 테스트 픽스처의 중복을 세면서 문자열 상수를 지웠더니 서로 다른 여덟 개가 한 몸통으로 묶였습니다. 픽스처에서는 어느 환경변수인지 어느 팀인지가 그 문자열에 담기므로 그것이 알맹이입니다. 데코레이터를 지웠다면 수명이 다른 픽스처들이 하나의 권고로 묶였을 것입니다. 그리고 정의를 넓힐 때마다 넓히기 전에 본 표본은 지금 걸리는 것을 덮지 못합니다. 같은 census를 데이터베이스에 닿는 두 번째 경로까지 세도록 넓혔더니, 없는 바이너리를 이유로 건너뛸 뿐 연결을 열지 않는 모듈 83개가 함께 딸려 왔습니다. 이 census는 쓰는 동안 여섯 번 틀렸고 적게 센 것이 한 번, 많이 센 것이 다섯 번이며, 다섯 번 모두 정의를 넓힌 직후였습니다. 세는 기준을 바꿀 때마다 무엇이 걸리는지 다시 읽으십시오. + 구체적인 오답은 빈 답보다 위험합니다. 아무것도 내지 않는 도구는 아무도 믿지 않지만, "17개 중 13개가 실행되지 않았다"에는 숫자와 목록이 있고 그 세부가 신뢰의 근거가 됩니다. 계산이 된 것처럼 보이는데 틀린 전제 위에서 된 계산입니다. 세는 것이 그런 답에 이르는 흔한 길입니다. 세면 그 수가 무엇들 사이의 관계였는지가 버려지기 때문입니다. 인증서를 마운트한 차트를 렌더했더니 마운트 경로가 넷이었고 그것이 기대한 수였는데, 그중 하나는 뒤에 볼륨이 없어 Kubernetes가 거부할 파드였습니다. 새로 만든 검사가 그럴듯한 답을 내면 잡혀야 하는 입력을 하나 넣어 실제로 잡는지 확인하십시오. 답이 숫자라면 그 숫자가 무엇에 붙어 있는지까지 봅니다. + 반대쪽은 도구가 자기가 하지 않는 것을 말하게 만드는 것입니다. `tools/static-checks`는 CI가 돌리는 것을 돌리고, 일부러 빼는 단계를 이유와 함께 이름으로 적어 둡니다. 검사를 조용히 빼고 성공을 보고하는 로컬 러너가 빠졌다고 밝히는 것보다 나쁘기 때문입니다. 계약 테스트가 그 목록을 워크플로와 대조하는데, 나중에 누가 CI에 단계를 더하면서 그 목록에 등록하지 않았을 때 그것을 알아챈 것이 검토가 아니라 그 테스트였습니다. + 검사기를 시험하는 데에도 같은 것이 있습니다. 심는 입력은 그 검사기가 잡겠다고 선언한 것이어야 합니다. 아니면 "안 걸렸다"가 검사기가 안 도는 것과 그것이 애초에 위반이 아닌 것을 동시에 뜻합니다. 한국어 문체 린트가 새 파일을 읽는지 보려고 어색한 문장을 심었는데 카탈로그에 그 형태의 규칙이 없어 안 걸렸고, 카탈로그에서 실제 규칙을 골라 심으니 바로 걸렸습니다. +- **사례 하나에서 만든 규칙.** 그 사례의 우연한 성질이 규칙 안에 들어가고, 본질적인 것과 구분되지 않습니다. 항목이 하나뿐이던 예외 목록이 "예외 모듈은 데이터베이스 게이트가 없다"를 단언하고 있었는데, 그것은 그 하나가 우연히 그랬던 것입니다. 뒤에 추가된 예외 셋은 자기 게이트를 의도적으로 유지하고, 그 단언은 셋 다에 대해 실패했습니다. 예외의 뜻은 "자기 방식을 유지해도 된다"이지 "게이트가 없다"가 아닙니다. 본질과 우연을 가르는 것은 두 번째 사례이고, 이것은 데이터에서만이 아니라 규칙에서도 그렇습니다. - **픽스처가 채우지 않은 필드.** 그 필드를 보는 가드는 눈이 먼 상태이고 모든 입력에 성공을 보고합니다. - **격리가 잡음이 아니라 조건을 없앤 경우.** 파일 하나만 돌리기, 최소 픽스처 쓰기, 의존성을 목으로 대체하기, 함수를 직접 부르기가 모두 조건을 지우는 것이 아니라 바꾸는 것입니다. celery 태스크 등록 검사가 그 파일만 돌리면 실패하고 전체 스위트에서는 통과했는데, 단독 실행의 실패를 "깨끗한 조건에서의 결과"로 읽었습니다. 다른 테스트 모듈이 수행하는 import가 그 단언이 기대던 조건의 일부였습니다. 격리한 실행에서 결론을 내리기 전에, 그 격리가 무엇을 없앴는지 먼저 이름 붙입니다. - **대조 실험을 그것이 답할 수 있는 범위 밖까지 읽은 경우.** 같은 테스트를 내 브랜치와 `main`에서 돌리면 내 변경이 그 결과를 만들었는지만 알 수 있고 무엇이 만들었는지는 알 수 없습니다. 양쪽이 공유하는 것은, 테스트를 부른 방식까지 포함해 대조에 보이지 않습니다. 같은 celery 검사가 두 브랜치에서 똑같이 실패했는데, 그것으로 "내 변경 때문은 아니다"까지 확인해 놓고 "내가 부른 방식 때문도 아니다"까지 읽었습니다. 브랜치 둘이든 환경 둘이든 설정 둘이든 모든 A/B에 같은 한계가 있습니다. @@ -303,6 +310,14 @@ def test_normalize_spdx_id(raw: str, expected: list[str]) -> None: 변이 자체도 위 목록의 모든 것에 걸립니다. 변이가 시험하려는 그 문자열 매칭에도 걸립니다. 문서 계약이 변수가 페이지에서 사라진 것을 잡는지 보려고 `GIT_SSL_CAPATH`를 `GIT_SSL_CAPATH_REMOVED_FROM_DOCS`로 바꿨는데, 원래 이름이 부분 문자열로 남아 포함 검사가 그대로 통과했고 그것이 계약이 약하다는 근거로 읽혔습니다. 텍스트가 바뀌었다가 아니라 변이가 만들려던 상태를 단언하십시오. 여기서는 옛 이름이 없다는 것입니다. +적용됐는지를 묻는 것은 변이 뒤만이 아니라 대량 변경 앞에서도 쓸모가 있습니다. 편집이 몇 개 파일에 닿아야 하는지를 먼저 선언하고, 수가 어긋나면 아무것도 쓰지 않고 멈추게 하십시오. 아무것도 매칭하지 않은 패턴은 "할 일이 없다"로 보고하고 그것이 "해당 없음"으로 읽힙니다. 193개 모듈 게이트 변환에서 이 검사가 세 번 멈췄는데 세 번 다 도구가 아니라 작업자의 계산이 원인이었습니다. 다른 디렉터리의 파일을 하나 빼서 센 것 한 번, 끝 개행이 없는 목록에 `wc -l`을 쓴 것 두 번입니다. `wc -l`은 개행을 세므로 하나가 빕니다. 반대로 변환 자체가 틀린 것을 잡은 적은 한 번도 없습니다. 어느 쪽 입력이 약한 고리였는지를 말해 주므로 적어 둘 만합니다. + +선언한 수에는 놓치기 쉬운 두 번째 쓸모가 있습니다. 그 수가 나와 어긋날 수 있다는 것입니다. 어떤 배치가 하나 줄어들 것이라고 예상했는데 그대로 나왔고, 원인은 그 파일이 어느 목록에 속하는지에 대한 제 모형이 틀렸던 것입니다. 예상을 적어 두지 않았으면 맞는 결과가 확인으로 읽히고 틀린 모형이 그대로 남았을 것입니다. + +다만 그 수는 도구와 독립적으로 세야 합니다. 도구가 세는 방식으로 예상도 세면 둘이 늘 일치해서 아무것도 확인하지 못합니다. 그리고 기준은 변경 크기가 아니라 방식을 따릅니다. 손으로 고친 파일 하나가 이미 전 트리에 돌려 본 변환으로 고친 백 개보다 안전하지 않고, 전부 파싱해 보는 것은 어느 쪽이든 명령 하나입니다. + +그리고 무언가를 두 번 바꿨는데 실패가 똑같으면, 바꾸고 있는 그것이 변수가 아닙니다. 깨진 docstring을 고치려고 세 번 시도하면서 매번 삽입 위치를 옮겼고 매번 같은 오류가 났습니다. 넣고 있던 조각이 docstring을 닫는 따옴표를 달고 있었기 때문입니다. 두 번째 같은 결과에서 이미 삽입 위치가 문제가 아니라는 것을 알 수 있었습니다. + 같은 결과를 낼 수 있는 층이 둘이면 서로를 가립니다. 어느 쪽을 지워도 결과가 같으므로 둘 다 검증되지 않습니다. 반대 경우도 있습니다. 바깥 검사가 안쪽을 도달 불가로 만들고 있으면 겹쳐 보이는 방어가 실은 하나이고, 바깥을 완화하는 순간 안쪽이 조용히 사라집니다. 도입 준비도 점검에서 세 건이 나왔습니다. 매니페스트 사전 점검 때문에 도달할 수 없던 체크섬 검증, 라우터의 조기 거절과 카운터 가드가 서로를 덮던 로그인 잠금, GRANT 층에 가려 한 번도 시험된 적 없던 감사 트리거입니다. 각 층이 실제로 도달되는지와 혼자서도 작동하는지를 따로 단언합니다. ### 회귀망은 의도적으로 두 겹 From 9e7c7480bc534431e97440b2f8173e1d9f43ffff Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sat, 5 Sep 2026 07:06:03 +0900 Subject: [PATCH 2/3] Tighten the additions Same eight lessons, about a third fewer words: the concrete cases stay, the sentences around them go. --- .../docs/contributor-guide/testing-guide.md | 96 +++++++------------ .../contributor-guide/testing-guide.md | 12 +-- 2 files changed, 41 insertions(+), 67 deletions(-) diff --git a/docs-site/docs/contributor-guide/testing-guide.md b/docs-site/docs/contributor-guide/testing-guide.md index 89a5e0ef..4c04917e 100644 --- a/docs-site/docs/contributor-guide/testing-guide.md +++ b/docs-site/docs/contributor-guide/testing-guide.md @@ -496,25 +496,18 @@ 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. - Reaching for a parse is only half of it, because the parse's output can be - read as text again. A census of database gates searched `ast.dump()` output - for `"alembic"` in double quotes and reported zero, since dump writes - apostrophes: the string matching had moved up a level rather than gone away. - Read the nodes. + Parsing is only half of it, because a parse's output 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 such a tool normalises away is its definition of "the same", and that - decision, not the comparison that follows it, produces the answer. Comparing - test fixtures for duplication, erasing string literals merged eight unrelated - fixtures into one group because for a fixture the strings are the payload - - which variable, which team - and erasing decorators would have merged - fixtures with different lifetimes into one recommendation. Whenever the - definition widens, the sample taken before it no longer covers what it now - catches: the same census, widened to count a second way of reaching the - database, swept in eighty-three modules that skip over a missing binary and - never open a connection. It was wrong six times while being written, once by - counting too few and five times by counting too many, and each of the five - arrived immediately after the definition grew. Re-read what it catches after - every change to what it counts. + 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. + Widening also voids the sample you took before it - 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 @@ -528,12 +521,11 @@ often enough to name: confirm it does. If the answer is a number, check what the number is attached to. - The counterpart is to make a tool say what it does not do. `tools/static-checks` - runs what CI runs, and names the steps it deliberately leaves out along with - the reason for each, because a local runner that silently omits a check and - then reports success is worse than one that admits the gap. A contract test - holds that list against the workflow, and when a later change added a CI step - without registering it there, that test is what noticed - not review. + The counterpart is 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 @@ -541,14 +533,11 @@ often enough to name: 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 the example's incidental - properties are inside the rule and nothing distinguishes them from its - essential ones. An exemption list with one entry asserted that exempt - modules have no database gate, which was true of that one module by - accident; the next three exemptions keep their own gates deliberately, and - the assertion failed for all three. What an exemption means is "may keep its - own arrangement", not "has none". A second instance is what separates the - essential from the incidental, in a rule as much as in a fixture. +- **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 @@ -585,34 +574,23 @@ 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. -The same question - did it land - is worth asking before a bulk change as -well as after a mutation. Declare how many files an edit must touch and make a -mismatch stop the run with nothing written, because a pattern matching nothing -otherwise reports "nothing to do", which reads as "not applicable here". On the -193-module gate conversion that check stopped three runs, and all three were -arithmetic on the operator's side rather than a fault in the tool: a file -counted from the wrong directory, and twice `wc -l` on a list with no trailing -newline, which counts newlines and so is short by one. It never once caught the -transformation being wrong. That is worth stating plainly, because it says which -input was the weaker one. - -A declared number has a second use that is easy to miss: it can disagree with -you. A prediction that one batch would shrink by one came back unchanged, and -the reason was a wrong model of which list the file belonged to. Without the -prediction written down, the correct result would have read as confirmation and -the wrong model would have survived intact. - -Keep the count independent of the tool, though - if the prediction is computed -the way the tool computes it, 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 that has already run against the whole tree; -parsing every file afterwards costs one command either way. - -And if changing something twice produces an identical failure, the thing being -changed is not the variable. Three attempts to repair a mangled docstring each -moved the insertion point and each produced the same error, because the fragment -being inserted carried a stray docstring terminator. The second identical result -was already enough to say the insertion point was not the problem. +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 diff --git a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/contributor-guide/testing-guide.md b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/contributor-guide/testing-guide.md index ef7159d0..c0d829a7 100644 --- a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/contributor-guide/testing-guide.md +++ b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/contributor-guide/testing-guide.md @@ -290,9 +290,9 @@ def test_normalize_spdx_id(raw: str, expected: list[str]) -> None: - **쓰는 코드와 읽는 코드가 같은 경우.** 어떤 구현이든 자기 자신과는 일치합니다. - **도구가 아무것도 찾지 못한 경우.** 결과만으로는 찾을 것이 없었는지 그곳을 보지 않았는지 구분되지 않습니다. 저장소 린트를 엉뚱한 트리에서 돌리면 열어 보지도 않은 파일에 대해 0건을 보고합니다. 손으로 만든 패턴이 눈앞의 것을 놓치는 방식이 둘입니다. `gh`도 `EXPLAIN`도 JSON을 돌려주는데 `lint (backend)` 같은 이름이나 `SubPlan` 아래 중첩된 노드는 괄호와 중첩을 예상하지 않은 패턴에 걸리지 않습니다. 값 안의 괄호·대괄호·점이 정규식에서는 다른 뜻입니다. 소스에서 세는 것도 같습니다. 테스트 파일의 5요소 튜플 다섯 중 `grep`은 셋만 찾았고, 놓친 둘은 패턴이 예상하지 못한 모양으로 쓰여 있었으며 그중 하나는 두 번째 헬퍼 안에 있었습니다. 패턴은 내가 상상한 표기를 찾고 파싱은 실제로 있는 것을 찾습니다. 구조를 파싱하거나 정확한 문자열로 비교하십시오. 이 저장소는 이미 여러 곳에서 AST로 단언하니 새로 꺼내는 도구도 아닙니다. - 파싱으로 바꾸는 것은 절반입니다. 파싱한 결과를 다시 텍스트로 읽을 수 있기 때문입니다. 데이터베이스 게이트를 세는 census가 `ast.dump()` 출력에서 `"alembic"`을 큰따옴표로 찾아 0건을 보고했는데, dump는 작은따옴표로 씁니다. 문자열 매칭이 없어진 것이 아니라 한 층 위로 옮겨 간 것입니다. 노드를 직접 읽으십시오. + 파싱은 절반입니다. 파싱한 결과를 다시 텍스트로 읽을 수 있기 때문입니다. census가 `ast.dump()` 출력에서 `"alembic"`을 큰따옴표로 찾아 0건을 보고했는데 dump는 작은따옴표로 씁니다. 노드를 직접 읽으십시오. - 그런 도구가 무엇을 지우고 비교하는지가 곧 무엇을 같다고 볼 것인지의 정의이고, 결과를 정하는 것은 뒤따르는 비교가 아니라 그 정의입니다. 테스트 픽스처의 중복을 세면서 문자열 상수를 지웠더니 서로 다른 여덟 개가 한 몸통으로 묶였습니다. 픽스처에서는 어느 환경변수인지 어느 팀인지가 그 문자열에 담기므로 그것이 알맹이입니다. 데코레이터를 지웠다면 수명이 다른 픽스처들이 하나의 권고로 묶였을 것입니다. 그리고 정의를 넓힐 때마다 넓히기 전에 본 표본은 지금 걸리는 것을 덮지 못합니다. 같은 census를 데이터베이스에 닿는 두 번째 경로까지 세도록 넓혔더니, 없는 바이너리를 이유로 건너뛸 뿐 연결을 열지 않는 모듈 83개가 함께 딸려 왔습니다. 이 census는 쓰는 동안 여섯 번 틀렸고 적게 센 것이 한 번, 많이 센 것이 다섯 번이며, 다섯 번 모두 정의를 넓힌 직후였습니다. 세는 기준을 바꿀 때마다 무엇이 걸리는지 다시 읽으십시오. + 그런 도구가 비교하려고 무엇을 지우는지가 곧 같음의 정의이고, 결과를 정하는 것은 뒤따르는 비교가 아니라 그 정의입니다. 픽스처 중복을 세며 문자열 상수를 지웠더니 서로 다른 여덟 개가 하나로 묶였습니다. 어느 환경변수인지 어느 팀인지가 그 문자열에 담기기 때문입니다. 데코레이터를 지웠다면 수명이 다른 것들이 하나로 묶였을 것입니다. 정의를 넓히면 넓히기 전의 표본도 무효가 됩니다. 데이터베이스에 닿는 두 번째 경로까지 세도록 넓혔더니 연결을 열지 않는 모듈 83개가 딸려 왔습니다. 세는 기준을 바꿀 때마다 무엇이 걸리는지 다시 읽으십시오. 그 census는 여섯 번 틀렸고 다섯 번이 넓힌 직후였습니다. 구체적인 오답은 빈 답보다 위험합니다. 아무것도 내지 않는 도구는 아무도 믿지 않지만, "17개 중 13개가 실행되지 않았다"에는 숫자와 목록이 있고 그 세부가 신뢰의 근거가 됩니다. 계산이 된 것처럼 보이는데 틀린 전제 위에서 된 계산입니다. 세는 것이 그런 답에 이르는 흔한 길입니다. 세면 그 수가 무엇들 사이의 관계였는지가 버려지기 때문입니다. 인증서를 마운트한 차트를 렌더했더니 마운트 경로가 넷이었고 그것이 기대한 수였는데, 그중 하나는 뒤에 볼륨이 없어 Kubernetes가 거부할 파드였습니다. 새로 만든 검사가 그럴듯한 답을 내면 잡혀야 하는 입력을 하나 넣어 실제로 잡는지 확인하십시오. 답이 숫자라면 그 숫자가 무엇에 붙어 있는지까지 봅니다. @@ -310,13 +310,9 @@ def test_normalize_spdx_id(raw: str, expected: list[str]) -> None: 변이 자체도 위 목록의 모든 것에 걸립니다. 변이가 시험하려는 그 문자열 매칭에도 걸립니다. 문서 계약이 변수가 페이지에서 사라진 것을 잡는지 보려고 `GIT_SSL_CAPATH`를 `GIT_SSL_CAPATH_REMOVED_FROM_DOCS`로 바꿨는데, 원래 이름이 부분 문자열로 남아 포함 검사가 그대로 통과했고 그것이 계약이 약하다는 근거로 읽혔습니다. 텍스트가 바뀌었다가 아니라 변이가 만들려던 상태를 단언하십시오. 여기서는 옛 이름이 없다는 것입니다. -적용됐는지를 묻는 것은 변이 뒤만이 아니라 대량 변경 앞에서도 쓸모가 있습니다. 편집이 몇 개 파일에 닿아야 하는지를 먼저 선언하고, 수가 어긋나면 아무것도 쓰지 않고 멈추게 하십시오. 아무것도 매칭하지 않은 패턴은 "할 일이 없다"로 보고하고 그것이 "해당 없음"으로 읽힙니다. 193개 모듈 게이트 변환에서 이 검사가 세 번 멈췄는데 세 번 다 도구가 아니라 작업자의 계산이 원인이었습니다. 다른 디렉터리의 파일을 하나 빼서 센 것 한 번, 끝 개행이 없는 목록에 `wc -l`을 쓴 것 두 번입니다. `wc -l`은 개행을 세므로 하나가 빕니다. 반대로 변환 자체가 틀린 것을 잡은 적은 한 번도 없습니다. 어느 쪽 입력이 약한 고리였는지를 말해 주므로 적어 둘 만합니다. +적용됐는지를 묻는 것은 대량 변경 앞에서도 쓸모가 있습니다. 편집이 몇 개 파일에 닿아야 하는지를 먼저 선언하고 수가 어긋나면 아무것도 쓰지 않고 멈추게 하십시오. 아무것도 매칭하지 않은 패턴은 "할 일이 없다"로 보고하고 그것이 "해당 없음"으로 읽힙니다. 193개 모듈 변환에서 이 검사가 세 번 멈췄는데 세 번 다 도구가 아니라 작업자의 계산이었고, 그중 둘은 끝 개행이 없는 목록에 `wc -l`을 쓴 것입니다. 반대로 변환 자체가 틀린 것을 잡은 적은 없습니다. 어느 쪽 입력이 약한지를 말해 줍니다. 다만 그 수는 도구와 독립적으로 세야 하고, 도구가 세는 방식으로 예상도 세면 늘 일치해 아무것도 확인하지 못합니다. 기준은 크기가 아니라 방식을 따릅니다. 손으로 고친 파일 하나가 전 트리에 돌려 본 변환으로 고친 백 개보다 안전하지 않습니다. -선언한 수에는 놓치기 쉬운 두 번째 쓸모가 있습니다. 그 수가 나와 어긋날 수 있다는 것입니다. 어떤 배치가 하나 줄어들 것이라고 예상했는데 그대로 나왔고, 원인은 그 파일이 어느 목록에 속하는지에 대한 제 모형이 틀렸던 것입니다. 예상을 적어 두지 않았으면 맞는 결과가 확인으로 읽히고 틀린 모형이 그대로 남았을 것입니다. - -다만 그 수는 도구와 독립적으로 세야 합니다. 도구가 세는 방식으로 예상도 세면 둘이 늘 일치해서 아무것도 확인하지 못합니다. 그리고 기준은 변경 크기가 아니라 방식을 따릅니다. 손으로 고친 파일 하나가 이미 전 트리에 돌려 본 변환으로 고친 백 개보다 안전하지 않고, 전부 파싱해 보는 것은 어느 쪽이든 명령 하나입니다. - -그리고 무언가를 두 번 바꿨는데 실패가 똑같으면, 바꾸고 있는 그것이 변수가 아닙니다. 깨진 docstring을 고치려고 세 번 시도하면서 매번 삽입 위치를 옮겼고 매번 같은 오류가 났습니다. 넣고 있던 조각이 docstring을 닫는 따옴표를 달고 있었기 때문입니다. 두 번째 같은 결과에서 이미 삽입 위치가 문제가 아니라는 것을 알 수 있었습니다. +선언한 수는 나와 어긋날 수도 있습니다. 어떤 배치가 하나 줄 것이라 예상했는데 그대로 나왔고, 그 파일이 어느 목록에 속하는지에 대한 모형이 틀렸던 것입니다. 적어 두지 않았으면 맞는 결과가 확인으로 읽혔을 것입니다. 그리고 두 번 바꿨는데 실패가 똑같으면 바꾸고 있는 그것이 변수가 아닙니다. 깨진 docstring을 세 번 고치면서 매번 삽입 위치를 옮겼고 매번 같은 오류가 났는데, 넣던 조각이 docstring을 닫는 따옴표를 달고 있었기 때문입니다. 같은 결과를 낼 수 있는 층이 둘이면 서로를 가립니다. 어느 쪽을 지워도 결과가 같으므로 둘 다 검증되지 않습니다. 반대 경우도 있습니다. 바깥 검사가 안쪽을 도달 불가로 만들고 있으면 겹쳐 보이는 방어가 실은 하나이고, 바깥을 완화하는 순간 안쪽이 조용히 사라집니다. 도입 준비도 점검에서 세 건이 나왔습니다. 매니페스트 사전 점검 때문에 도달할 수 없던 체크섬 검증, 라우터의 조기 거절과 카운터 가드가 서로를 덮던 로그인 잠금, GRANT 층에 가려 한 번도 시험된 적 없던 감사 트리거입니다. 각 층이 실제로 도달되는지와 혼자서도 작동하는지를 따로 단언합니다. From 02b5a5cdb2575e9cfaf062fb11e471a395170f78 Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sat, 5 Sep 2026 07:08:32 +0900 Subject: [PATCH 3/3] Name the mechanisms inside the long bullet The additions landed almost entirely in one bullet that was already the longest, so somebody arriving with a specific problem has to read the whole block to find their case. Same rule number, same single item, but each mechanism now leads with what it is: the pattern finds only the notation you imagined, a parse can be read as text again, what you erase in order to compare is what counts as the same, and a wider definition needs a fresh sample. --- .../docs/contributor-guide/testing-guide.md | 20 +++++++++---------- .../contributor-guide/testing-guide.md | 12 +++++------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs-site/docs/contributor-guide/testing-guide.md b/docs-site/docs/contributor-guide/testing-guide.md index 4c04917e..1162e352 100644 --- a/docs-site/docs/contributor-guide/testing-guide.md +++ b/docs-site/docs/contributor-guide/testing-guide.md @@ -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 @@ -496,20 +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. - Parsing is only half of it, because a parse's output 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. + *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 such a tool normalises away is its definition of "the same", and that + *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. - Widening also voids the sample you took before it - counting a second way of + *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 + 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 + *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 @@ -521,13 +521,13 @@ often enough to name: confirm it does. If the answer is a number, check what the number is attached to. - The counterpart is a tool that says what it does not do. `tools/static-checks` + *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 + *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 diff --git a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/contributor-guide/testing-guide.md b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/contributor-guide/testing-guide.md index c0d829a7..b62333fe 100644 --- a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/contributor-guide/testing-guide.md +++ b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/contributor-guide/testing-guide.md @@ -288,17 +288,17 @@ 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로 단언하니 새로 꺼내는 도구도 아닙니다. - 파싱은 절반입니다. 파싱한 결과를 다시 텍스트로 읽을 수 있기 때문입니다. census가 `ast.dump()` 출력에서 `"alembic"`을 큰따옴표로 찾아 0건을 보고했는데 dump는 작은따옴표로 씁니다. 노드를 직접 읽으십시오. + *파싱한 결과를 다시 텍스트로 읽을 수 있습니다.* 그래서 파싱은 절반입니다. census가 `ast.dump()` 출력에서 `"alembic"`을 큰따옴표로 찾아 0건을 보고했는데 dump는 작은따옴표로 씁니다. 노드를 직접 읽으십시오. - 그런 도구가 비교하려고 무엇을 지우는지가 곧 같음의 정의이고, 결과를 정하는 것은 뒤따르는 비교가 아니라 그 정의입니다. 픽스처 중복을 세며 문자열 상수를 지웠더니 서로 다른 여덟 개가 하나로 묶였습니다. 어느 환경변수인지 어느 팀인지가 그 문자열에 담기기 때문입니다. 데코레이터를 지웠다면 수명이 다른 것들이 하나로 묶였을 것입니다. 정의를 넓히면 넓히기 전의 표본도 무효가 됩니다. 데이터베이스에 닿는 두 번째 경로까지 세도록 넓혔더니 연결을 열지 않는 모듈 83개가 딸려 왔습니다. 세는 기준을 바꿀 때마다 무엇이 걸리는지 다시 읽으십시오. 그 census는 여섯 번 틀렸고 다섯 번이 넓힌 직후였습니다. + *비교하려고 무엇을 지우는지가 곧 같음의 정의입니다.* 결과를 정하는 것은 뒤따르는 비교가 아니라 그 정의입니다. 픽스처 중복을 세며 문자열 상수를 지웠더니 서로 다른 여덟 개가 하나로 묶였습니다. 어느 환경변수인지 어느 팀인지가 그 문자열에 담기기 때문입니다. 데코레이터를 지웠다면 수명이 다른 것들이 하나로 묶였을 것입니다. *정의를 넓히면 표본을 다시 봐야 합니다.* 넓히기 전의 표본은 지금 걸리는 것을 덮지 못합니다. 데이터베이스에 닿는 두 번째 경로까지 세도록 넓혔더니 연결을 열지 않는 모듈 83개가 딸려 왔습니다. 세는 기준을 바꿀 때마다 무엇이 걸리는지 다시 읽으십시오. 그 census는 여섯 번 틀렸고 다섯 번이 넓힌 직후였습니다. - 구체적인 오답은 빈 답보다 위험합니다. 아무것도 내지 않는 도구는 아무도 믿지 않지만, "17개 중 13개가 실행되지 않았다"에는 숫자와 목록이 있고 그 세부가 신뢰의 근거가 됩니다. 계산이 된 것처럼 보이는데 틀린 전제 위에서 된 계산입니다. 세는 것이 그런 답에 이르는 흔한 길입니다. 세면 그 수가 무엇들 사이의 관계였는지가 버려지기 때문입니다. 인증서를 마운트한 차트를 렌더했더니 마운트 경로가 넷이었고 그것이 기대한 수였는데, 그중 하나는 뒤에 볼륨이 없어 Kubernetes가 거부할 파드였습니다. 새로 만든 검사가 그럴듯한 답을 내면 잡혀야 하는 입력을 하나 넣어 실제로 잡는지 확인하십시오. 답이 숫자라면 그 숫자가 무엇에 붙어 있는지까지 봅니다. + *구체적인 오답은 빈 답보다 위험합니다.* 아무것도 내지 않는 도구는 아무도 믿지 않지만, "17개 중 13개가 실행되지 않았다"에는 숫자와 목록이 있고 그 세부가 신뢰의 근거가 됩니다. 계산이 된 것처럼 보이는데 틀린 전제 위에서 된 계산입니다. 세는 것이 그런 답에 이르는 흔한 길입니다. 세면 그 수가 무엇들 사이의 관계였는지가 버려지기 때문입니다. 인증서를 마운트한 차트를 렌더했더니 마운트 경로가 넷이었고 그것이 기대한 수였는데, 그중 하나는 뒤에 볼륨이 없어 Kubernetes가 거부할 파드였습니다. 새로 만든 검사가 그럴듯한 답을 내면 잡혀야 하는 입력을 하나 넣어 실제로 잡는지 확인하십시오. 답이 숫자라면 그 숫자가 무엇에 붙어 있는지까지 봅니다. - 반대쪽은 도구가 자기가 하지 않는 것을 말하게 만드는 것입니다. `tools/static-checks`는 CI가 돌리는 것을 돌리고, 일부러 빼는 단계를 이유와 함께 이름으로 적어 둡니다. 검사를 조용히 빼고 성공을 보고하는 로컬 러너가 빠졌다고 밝히는 것보다 나쁘기 때문입니다. 계약 테스트가 그 목록을 워크플로와 대조하는데, 나중에 누가 CI에 단계를 더하면서 그 목록에 등록하지 않았을 때 그것을 알아챈 것이 검토가 아니라 그 테스트였습니다. + *반대쪽은 자기가 하지 않는 것을 말하는 도구입니다.* `tools/static-checks`는 CI가 돌리는 것을 돌리고, 일부러 빼는 단계를 이유와 함께 이름으로 적어 둡니다. 검사를 조용히 빼고 성공을 보고하는 로컬 러너가 빠졌다고 밝히는 것보다 나쁘기 때문입니다. 계약 테스트가 그 목록을 워크플로와 대조하는데, 나중에 누가 CI에 단계를 더하면서 그 목록에 등록하지 않았을 때 그것을 알아챈 것이 검토가 아니라 그 테스트였습니다. - 검사기를 시험하는 데에도 같은 것이 있습니다. 심는 입력은 그 검사기가 잡겠다고 선언한 것이어야 합니다. 아니면 "안 걸렸다"가 검사기가 안 도는 것과 그것이 애초에 위반이 아닌 것을 동시에 뜻합니다. 한국어 문체 린트가 새 파일을 읽는지 보려고 어색한 문장을 심었는데 카탈로그에 그 형태의 규칙이 없어 안 걸렸고, 카탈로그에서 실제 규칙을 골라 심으니 바로 걸렸습니다. + *검사기를 시험하는 데에도 같은 것이 있습니다.* 심는 입력은 그 검사기가 잡겠다고 선언한 것이어야 합니다. 아니면 "안 걸렸다"가 검사기가 안 도는 것과 그것이 애초에 위반이 아닌 것을 동시에 뜻합니다. 한국어 문체 린트가 새 파일을 읽는지 보려고 어색한 문장을 심었는데 카탈로그에 그 형태의 규칙이 없어 안 걸렸고, 카탈로그에서 실제 규칙을 골라 심으니 바로 걸렸습니다. - **사례 하나에서 만든 규칙.** 그 사례의 우연한 성질이 규칙 안에 들어가고, 본질적인 것과 구분되지 않습니다. 항목이 하나뿐이던 예외 목록이 "예외 모듈은 데이터베이스 게이트가 없다"를 단언하고 있었는데, 그것은 그 하나가 우연히 그랬던 것입니다. 뒤에 추가된 예외 셋은 자기 게이트를 의도적으로 유지하고, 그 단언은 셋 다에 대해 실패했습니다. 예외의 뜻은 "자기 방식을 유지해도 된다"이지 "게이트가 없다"가 아닙니다. 본질과 우연을 가르는 것은 두 번째 사례이고, 이것은 데이터에서만이 아니라 규칙에서도 그렇습니다. - **픽스처가 채우지 않은 필드.** 그 필드를 보는 가드는 눈이 먼 상태이고 모든 입력에 성공을 보고합니다. - **격리가 잡음이 아니라 조건을 없앤 경우.** 파일 하나만 돌리기, 최소 픽스처 쓰기, 의존성을 목으로 대체하기, 함수를 직접 부르기가 모두 조건을 지우는 것이 아니라 바꾸는 것입니다. celery 태스크 등록 검사가 그 파일만 돌리면 실패하고 전체 스위트에서는 통과했는데, 단독 실행의 실패를 "깨끗한 조건에서의 결과"로 읽었습니다. 다른 테스트 모듈이 수행하는 import가 그 단언이 기대던 조건의 일부였습니다. 격리한 실행에서 결론을 내리기 전에, 그 격리가 무엇을 없앴는지 먼저 이름 붙입니다.