diff --git a/book/docs/12_0_standard_library.md b/book/docs/12_0_standard_library.md index a2e2fc1..c77a9d2 100644 --- a/book/docs/12_0_standard_library.md +++ b/book/docs/12_0_standard_library.md @@ -1 +1,126 @@ # 12. 표준 라이브러리 + +CPython 표준 라이브러리 모듈은 두 가지 종류로 나뉜다. + +1. 유틸리티를 제공하는 순수한 파이썬 모듈 +2. C로 작성됐고 파이썬 래퍼를 제공하는 모듈 + +## 12.1 파이썬 모듈 + +순수한 파이썬 모듈들은 `Lib` 디렉토리에 위치한다. + +아래의 email 모듈과 같은 큰 모듈 중 일부는 하위 모듈이 있는 경우도 있다. + +![library_directory](../images/12_standard_library/00_library_directory.png) + +파이썬 배포판을 설치하면 표준 라이브러리 모듈은 배포판 폴더로 복사된다. 해당 폴더는 파이썬을 시작할 때 항상 경로에 포함되어 모듈을 임포트할 때 모듈 위치를 신경 쓰지 않아도 된다. + +→ `sys.path`에 포함된다. + +아래는 파이썬 모듈 중 하나인 `colorsys`의 함수이다. + +```python +def rgb_to_hls(r, g, b): + maxc = max(r, g, b) + minc = min(r, g, b) + # XXX Can optimize (maxc+minc) and (maxc-minc) + l = (minc+maxc)/2.0 + if minc == maxc: + return 0.0, l, 0.0 + if l <= 0.5: + s = (maxc-minc) / (maxc+minc) + else: + s = (maxc-minc) / (2.0-maxc-minc) + rc = (maxc-r) / (maxc-minc) + gc = (maxc-g) / (maxc-minc) + bc = (maxc-b) / (maxc-minc) + if r == maxc: + h = bc-gc + elif g == maxc: + h = 2.0+rc-bc + else: + h = 4.0+gc-rc + h = (h/6.0) % 1.0 + return h, l, s +``` + +위 함수를 사용하는 방법은 아래와 같다. + +```python +import colorsys + +colorsys.rgb_to_hls(255, 0, 0) +``` + +모듈의 위치를 신경 쓰지 않고 바로 임포트할 수 있다. + +모든 파이썬 모듈은 위 함수와 같이 단순한 파이썬 코드로 이루어져 있으며 이해하기 어렵지 않다. 따라서 표준 라이브러리 코드에서 버그나 개선 사항을 찾아 파이썬 배포판에 기여할 수도 있다. + +## 12.2 파이썬과 C가 혼용된 모듈 + +순수한 파이썬 모듈을 제외한 나머지 모듈은 모두 C로 작성됐거나 C와 파이썬이 혼용되어 있다. + +C로 작성된 부분은 `Modules` 디렉토리에 위치하며 파이썬으로 작성된 부분은 `Lib` 디렉토리에 위치한다. + +아래 사항은 예외이다. + +1. `sys` 모듈은 `Python/sysmodule.c`에 위치한다. + - CPython의 내부와 강하게 연관되어 있기 때문에 `Python` 디렉토리에 존재한다. +2. `__builtins__` 모듈은 `Python/bltinmodule.c`에 위치한다. + - 인터프리터를 인스턴스화할 때 내장 함수들을 __builtins__로부터 임포트하므로 `Python` 디렉토리에 존재한다. + - `print()`, `chr()`, `format()`과 같은 모든 내장 함수를 해당 파일에서 찾을 수 있다. + +C로 작성된 일부 모듈의 내용은 운영 체제 기능을 외부에 노출한다. 따라서 운영 체제에 따라 모듈이 다르게 동작하는 특수한 케이스가 존재한다. + +예를 들어 `time` 모듈은 윈도우가 시간을 유지하고 저장하는 방법이 리눅스, macOS와 전혀 다르기 때문에 운영 체제별로 시간 함수의 정확도가 다르다. + +스레딩 모듈, 파일 시스템 모듈, 네트워킹 모듈도 동일한 API를 운영 체제별로 여러 번 구현한다. + +운영 체제별로 동작이 다르기 때문에 CPython 소스 코드는 최대한 같은 방식으로 동작을 구현한 다음 일관성 있고 추상화된 API만 제공한다. + +이러한 구현 방식을 `time` 모듈에서도 확인할 수 있다. + +아래는 `time` 모듈의 함수로 프로세스의 CPU 사용 시간을 나노초 단위로 반환하는 함수이다. + +```c +static PyObject * +time_process_time_ns(PyObject *self, PyObject *unused) +{ + _PyTime_t t; + if (_PyTime_GetProcessTimeWithInfo(&t, NULL) < 0) { + return NULL; + } + return _PyTime_AsNanosecondsObject(t); +} +``` + +아래 함수는 `time_process_time_ns()`에서 호출하는 함수로 운영 체제에 따라 다른 방식으로 시스템 프로세스 시간을 가져오는 기능을 수행한다. + +```c +static int +_PyTime_GetProcessTimeWithInfo(_PyTime_t *tp, _Py_clock_info_t *info) +{ +#if defined(MS_WINDOWS) + HANDLE process; + FILETIME creation_time, exit_time, kernel_time, user_time; + +... + + t = _PyTime_FromNanoseconds((ktime + utime) * 100); + *tp = t; + return 0; +#else + + /* clock_gettime */ +#if defined(HAVE_CLOCK_GETTIME) \ + && (defined(CLOCK_PROCESS_CPUTIME_ID) || defined(CLOCK_PROF)) + struct timespec ts; + +... + + /* clock */ + /* Currently, Python 3 requires clock() to build: see issue #22624 */ + return _PyTime_GetClockWithInfo(tp, info); +#endif +} +``` diff --git a/book/docs/13_0_test_suite.md b/book/docs/13_0_test_suite.md index eda7197..4a0212f 100644 --- a/book/docs/13_0_test_suite.md +++ b/book/docs/13_0_test_suite.md @@ -1 +1,336 @@ # 13. 테스트 스위트 + +CPython은 코어 인터프리터, 표준 라이브러리, 툴링을 비롯해 +리눅스, macOS용 배포까지 포함하는 강력한 테스트 스위트를 가지고 있다. + +테스트 스위트는 `Lib/tests`에 위치해 있고 대부분 파이썬으로 작성되어있다. + +전체 테스트 스위트가 파이썬 패키지이기 때문에 +컴파일한 파이썬 인터프리터로 테스트를 실행해 볼 수 있다. + +--- + +## 용어 + +테스트를 처음 접하는 경우 아래의 용어를 알고 있다면 이해하는데 도움이 될 것 같다. + +### 테스트 하네스 + +테스트 수행 도구를 의미한다. +Postman, unittest, Pytest, JUnit5, Robot Framework 등이 여기에 해당한다. + +![test_harness](../images/13_test_suite/00_test_harness.png) + +### 테스트 스위트, 테스트 케이스 + +테스트 케이스의 모음을 테스트 스위트라고 부른다. + +### 확인 테스트, 회귀 테스트 + +**확인 테스트**는 발생한 이슈에 대한 수정 검증 테스트를 의미한다. +만약 확인 테스트가 이후 릴리즈 할 버전에도 영향을 미칠 것 같다고 판단된다면 +수행한 확인 테스트 케이스를 회귀 테스트 스위트에 포함시킨다. + +**회귀 테스트**는 테스트 버전에 대해 +릴리즈 버전에서 수행한 테스트 케이스들을 다시 수행하는 것을 의미한다. + +## 일반적인 테스트 코드 구조 + +일반적으로는 테스트 코드는 다음과 같이 구성되어 있다. (프로젝트 별로 관리 방식이 상이할 수 있다) + +pytest를 사용할 경우의 디렉토리 구조이다 + +``` +Project + |- src + |- __init__.py + |- main.py + |- data_loader + |- ... + |- models + |- ... + |- tests + |- __init__.py + |- test_{suites1} + |- ... + |- test_{suites2} + |- __init__.py + |- conftest.py + |- test_{suite1}.py + |- test_{suite2}.py +``` + +### Mock + +Mock은 '가짜 데이터'를 의미한다. +예를 들어 테스트 코드를 작성할 때, 실제 모듈과 유사하게 동작하는 가짜 데이터를 만들어 사용할 수 있다. + +mock를 사용하면 함수의 실제 실행 없이, 미리 정해진 값을 반환하도록 할 수 있다. + +성능 저하, 비용 등 불필요한 리소스 발생이 예상될 때, 일반적으로 mock 사용을 권장한다. + +```python +# src/example.py + +def func(): + return "fail" +``` + +위와 같은 함수가 있다고 해보자. +원래라면 “fail”만 리턴하는 함수이다. + +그리고 테스트를 위한 `test_example.py`를 만들어보자. + +```python +# tests/test_example.py + +import unittest +from unittest import mock +from src import example + +class TestExample(unittest.TestCase): + + @mock.patch("src.example.func", return_value="success") + def test_func(self, mock_func): + actual = example.func() + expected = "success" + + self.assertEqual(actual, expected) + +if __name__ == "__main__": + unittest.main() +``` + +mock를 사용하기 위해서는 test 함수 위에 다음과 같은 데코레이터를 추가해야 한다. + +```python +@mock.patch("src.example.func", return_value="success") +``` + +`patch`메소드의 첫 번째 인자는 target 함수의 경로이다. +`return_value` 인자에는 target 함수의 return 값을 지정할 수 있다. + +`return_value`를 통해 함수의 return 값을 강제할 수 있다. + +`func()` 는 "fail"을 return 하지만, +`return_value` 파라미터를 통해 "success"로 강제할 수 있다. + +### Hook + +각 테스트 케이스마다 반복되는 작업이 있을 경우 Hook을 수행할 수 있다. + +`unittest`에서는 테스트 케이스의 설정 및 정리를 위해 +`setUp`, `tearDown`, `setUpClass`, `tearDownClass` 메소드를 사용한다. + +- `setUp`: 각 테스트 케이스가 호출되기 전 먼저 호출되는 메소드 +- `setUpClass`: 테스트 스위트가 실행된 직후 가장 먼저 호출되는 메소드 +- `tearDown`: 각 테스트 케이스의 종료 후 호출되는 메소드 +- `tearDownClass`: 테스트 스위트의 종료 후 호출되는 메소드 + +```python +# tests/test_example.py + +import unittest +from unittest import mock +from src import example + +class TestExample(unittest.TestCase): + def setUp(self): + self.mock_func = mock.patch("src.example.func", return_value="success").start() + + def tearDown(self): + mock.patch.stopall() + + def test_func(self): + actual = example.func() + expected = "success" + self.assertEqual(actual, expected) + +if __name__ == "__main__": + unittest.main() +``` + +--- + +## **리눅스와 macOS에서 테스트 스위트 실행하기** + +리눅스와 macOS에서 `make`로 `test` 타겟을 실행하면 컴파일 후 테스트가 실행된다. + +```python +$ make test +== CPython 3.9 +== macOS-14.4.1-arm64-arm-64bit little-endian +== cwd: /Users/wooy0ng/Desktop/cpython/build/test_python_3006æ +== CPU count: 8 +== encodings: locale=UTF-8, FS=utf-8 +0:00:00 load avg: 5.40 [  1/425] test_wave passed +0:00:00 load avg: 5.40 [  2/425] test_richcmp passed +0:00:00 load avg: 7.93 [  3/425] test_future5 passed +``` + +또는 컴파일된 바이너리인 `python`이나 `python.exe`로 `test` 패키지를 실행할 수도 있다. + +```python +$ ./python -m test +== CPython 3.9 +== macOS-14.4.1-arm64-arm-64bit little-endian +== cwd: /Users/wooy0ng/Desktop/cpython/build/test_python_3006æ +== CPU count: 8 +== encodings: locale=UTF-8, FS=utf-8 +0:00:00 load avg: 5.20 [  1/425] test_wave passed +0:00:00 load avg: 5.20 [  2/425] test_richcmp passed +0:00:00 load avg: 7.68 [  3/425] test_future5 passed +``` + +다음은 cpython에서 테스트를 위한 `make` 타겟 목록이다. + +| 타깃 | 용도 | +| --- | --- | +| test | 기본적인 회귀 테스트를 실행한다. | +| quicktest | 오래 걸리는 테스트를 제외하고 빠른 회귀 테스트만 실행한다. | +| testall | .pyc 파일이 없는 상태로 한 번, 있는 상태로 한 번 전체 테스트 스위트를 실행한다. | +| testuniversal | macOS 유니버셜 빌드에서 여러 아키텍처에 대한 테스트 스위트를 실행한다. | +| coverage | 컴파일 후 gcov로 테스트를 실행한다. | +| coverage-lcov | HTML 커버리지 보고를 생성한다. | + +--- + +## 테스트 플래그 + +GUI가 필요한 IDLE에 대한 테스트처럼 +일부 테스트는 특정한 플래그가 없으면 자동으로 건너뛴다. + +- `-list-tests` 플래그로 구성에서 테스트 스위트 목록을 볼 수 있다. + +```python +$ ./python -m test --list-tests + +test_grammer +test_opcodes +test_dict +test_builtin +test_exceptions +... +``` + +--- + +## 특정 테스트만 실행하기 + +테스트를 실행할 때 첫 번째 인자에 실행할 테스트 스위트를 명시해서 특정 테스트만 실행할 수 있다. + +아래는 리눅스와 macOS에서 실행할 테스트 스위트를 명시하는 방법이다. + +```python +$ ./python -m test test_webbrowser +Raised RLIMIT_NOFILE: 256 -> 1024 + +0:00:00 load avg: 1.70 Run tests sequentially +0:00:00 load avg: 1.70 [1/1] test_webbrowser + +== Tests result: SUCCESS == +1 test OK. + +Total duration: 37 ms +Tests result: SUCCESS +``` + +CPython을 변경하려면 테스트 스위트를 사용하는 방법과 +직접 컴파일한 바이너리 상태를 확인하는 방법을 이해하는 것이 매우 중요하다. + +소스 코드를 변경하기 전에 전체 테스트 세트를 실행하고 모두 통과되는지 확인해야 한다. + +--- + +## 테스트 모듈 + +C 확장과 파이썬 모듈은 `unittest` 모듈로 임포트하고 테스트한다. +테스트는 모듈이나 패키지 단위로 구성된다. + +예를 들어 파이썬 유니코드 문자열 타입의 테스트는 `Lib/test/test_unicode.py`에서, +`asyncio` 패키지의 테스트 패키지는 `Lib/test/test_asyncio`에서 찾을 수 있다. + +아래는 UnicodeTest 클래스 중 일부이다. + +```python + +class UnicodeTest(string_tests.CommonTest, + string_tests.MixinStrUnicodeUserStringTest, + string_tets.MixinStrUnicodeTest, + unittest.TestCase +): +... + def test_casefold(self): + self.assertEqual('hello'.casefold(), 'hello') + self.assertEqual('hELlo'.casefold(), 'hello') + self.assertEqual('ß'.casefold(), 'ss') + self.assertEqual('fi'.casefold(), 'fi') +``` + +이전 장에서 유니코드 문자열에 대해 구현한 '거의 같음' 연산자에 대한 테스트를 +UnicodeTest 클래스의 새 메소드로 추가해보자. + +```python +def test_almost_equals(self): + self.assertTrue('hello' ~= 'hello') + self.assertTrue('heLlo' ~= 'hello) + self.assertFalse('hELlo!' ~= 'hello') +``` + +새로 추가한 테스트를 아래의 커멘드로 실행해보자 + +- Windows + + ```python + > rc.bat -q -d -x64 test_unicode + ``` + + +- MacOS, 리눅스 + + ```python + $ ./python -m test test_unicode -v + ``` + + +--- + +## 테스트 유틸리티 + +`test.support.script_helper` 모듈은 +파이썬 런타임 테스트를 사용할 수 있는 helper 함수를 제공한다. + +- `assert_python_ok(*args, **env_vars)` + + > 지정된 인수와 함께 파이썬 프로세스를 실행하고 반환 코드와 + stdout, stderr를 담은 튜플을 반환한다. + > +- `assert_python_failure(*args, **env_vars)` + + > assert_python_ok()와 비슷하지만 실패를 가정하는 경우에 사용한다. + > +- `make_script(script_dir, script_basename, source)` + + > script_basename과 source를 사용해 script_dir에 스크립트를 생성하고 + 스크립트에 대한 경로를 반환한다. + > + + +모듈이 빌드되지 않았을 경우 테스트도 건너뛰게 하고 싶다면 +유틸리티 함수 `test.support.import_module()`을 사용할 수 있다. + +이 유틸리티는 테스트할 모듈이 빌드되지 않았다면 `SkipTest`를 발생시켜 +이 테스트 패키지를 건너뛰라는 신호를 Test Runner에 보낸다. + +`import_module()`을 사용하는 방법은 아래와 같다. + +```python +import test.support + +_multiprocessing = test.support.import_module('_multiprocessing') + +# test 작성 +... +``` + +--- diff --git a/book/images/12_standard_library/00_library_directory.png b/book/images/12_standard_library/00_library_directory.png new file mode 100644 index 0000000..010789c Binary files /dev/null and b/book/images/12_standard_library/00_library_directory.png differ diff --git a/book/images/13_test_suite/00_test_harness.png b/book/images/13_test_suite/00_test_harness.png new file mode 100644 index 0000000..5af3295 Binary files /dev/null and b/book/images/13_test_suite/00_test_harness.png differ