3주차 과제 - 민상연

3주차 과제

[필수 과제]

2주차에 진행한 server list 확장 코드에서 tox를 통해 unit test를 실행시키면 Test Failed가 발생합니다.
openstackclient.tests.unit.compute 경로에 있는 server list에 대한 unit test를 수정하여 tox 실행시 모든 unit test에서 성공하도록 해주세요

제출 내용 1: 수정된 unit test code (스크린샷 또는 실제 코드)
제출 내용 2: 아래의 예시와 같이 tox를 통해 모든 unit test 통과한 화면

[TroubleShooting] ImportError: Start directory is not importable

$ tox
...
py3: commands[0]> stestr run
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\.tox\py3\Lib\site-packages\stestr\subunit_runner\run.py", line 88, in <module>
    main()
  File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\.tox\py3\Lib\site-packages\stestr\subunit_runner\run.py", line 82, in main
    program.TestProgram(
  File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\.tox\py3\Lib\site-packages\stestr\subunit_runner\program.py", line 179, in __init__
    self.parseArgs(argv)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\unittest\main.py", line 130, in parseArgs
    self._do_discovery(argv[2:])
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\unittest\main.py", line 253, in _do_discovery
    self.createTests(from_discovery=True, Loader=Loader)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\unittest\main.py", line 160, in createTests
    self.test = loader.discover(self.start, self.pattern, self.top)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\unittest\loader.py", line 308, in discover
    raise ImportError('Start directory is not importable: %r' % start_dir)
ImportError: Start directory is not importable: '${OS_TEST_PATH:-./openstackclient/tests/unit}'

tox 명령어를 실행했을때 위와 같은 오류가 발생하였습니다.

이는 tox가 테스트를 실행할 때, 테스트 파일의 위치를 잘못 읽어오면서 발생한 문제로

# python-openstackclient/.stestr.conf
[DEFAULT]
# test_path=${OS_TEST_PATH:-./openstackclient/tests/unit}
test_path=./openstackclient/tests/unit
top_dir=./
group_regex=([^\.]+\.)+

.stestr.conf 파일은 stestr의 핵심 설정 파일이며 stestrOpenStack 프로젝트에서 널리 사용되는 Test Runner 입니다.

stestr 설정 파일은 셸이 아니므로, ${...} 같은 변수 확장 문법을 해석하지 못합니다.

따라서 test_path의 값을 openstackclient/tests/unit이라는 실제 경로가 아닌, ${OS_TEST_PATH:-./openstackclient/tests/unit} 이라는 문자열 그대로 인식하여 존재하지 않는 디렉토리를 찾으려고 시도하기 때문에 계속해서 ImportError가 발생하였다고 판단하였습니다.

따라서 .stestr.conf 파일에서 변수 확장 문법을 제거하고 실제 경로만 남겨 문제를 해결하였습니다.

$ tox
...
openstackclient.tests.unit.compute.v2.test_server.TestServerListV273.test_server_list_with_unlocked_v273
--------------------------------------------------------------------------------------------------------

Captured traceback:
~~~~~~~~~~~~~~~~~~~
    Traceback (most recent call last):

      File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\openstackclient\tests\unit\compute\v2\test_server.py", line 5440, in test_server_list_with_unlocked_v273
    self.assertCountEqual(self.columns, columns)

      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\unittest\case.py", line 1216, in assertCountEqual
    self.fail(msg)

      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\unittest\case.py", line 715, in fail
    raise self.failureException(msg)

    AssertionError: Element counts were not equal:
First has 0, Second has 1:  'Project Name'
First has 0, Second has 1:  'User Name'


Captured pythonlogging:
~~~~~~~~~~~~~~~~~~~~~~~
    Could not pre-fetch projects or users: 'Mock' object is not iterable

또한 이후 정상적으로 tox를 실행시켰을때 강의에서 안내주신 내용처럼 위와 같은 테스트 실패가 발생하였습니다.

# python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py
from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes

이후 identity_fakes를 통해 ProjectUser에 대한 Mock 데이터를 임의로 만들어주기 위해 위와 같이 패키지를 import하였습니다.

class _TestServerList(TestServer):
    # Columns to be listed up.
    columns = (
        'ID',
        'Name',
        'Status',
        # columns 튜플에 Project Name과 User Name 추가
        'Project Name',
        'User Name',
        'Networks',
        'Image',
        'Flavor',
    )
    columns_long = (
        'ID',
        'Name',
        'Status',
        # columns_long 튜플에 Project Name과 User Name 추가
        'Project Name',
        'User Name',
        'Flavor ID',
        'Task State',
        'Power State',
        'Networks',
        'Image Name',
        'Image ID',
        'Flavor Name',
        'Flavor ID',
        'Availability Zone',
        'Pinned Availability Zone',
        'Host',
        'Properties',
        'Scheduler Hints',
    )
    columns_all_projects = (
        'ID',
        'Name',
        'Status',
        # columns_all_projects 튜플에 Project Name과 User Name 추가
        'Project Name',
        'User Name',
        'Networks',
        'Image',
        'Flavor',
        'Project ID',
    )
    
    def setUp(self):
        super().setUp()

        # Default params of the core function of the command in the case of no
        # commandline option specified.
        self.kwargs = {
            'reservation_id': None,
            'ip': None,
            'ip6': None,
            'name': None,
            'status': None,
            'flavor': None,
            'image': None,
            'compute_host': None,
            'project_id': None,
            'all_projects': False,
            'user_id': None,
            'deleted': False,
            'changes-since': None,
            'changes-before': None,
        }

        # 테스트를 위한 project, user id 설정
        self.project_id = 'project-id-for-testing'
        self.user_id = 'user-id-for-testing'

        # The fake servers' attributes. Use the original attributes names in
        # nova, not the ones printed by "server list" command.
        self.attrs = {
            # Mock 서버 생성 시의 속성 명시적으로 추가
            'project_id': self.project_id,
            'user_id': self.user_id,
            'status': 'ACTIVE',
            'OS-EXT-STS:task_state': 'None',
            'OS-EXT-STS:power_state': 0x01,  # Running
            'networks': {'public': ['10.20.30.40', '2001:db8::5']},
            'OS-EXT-AZ:availability_zone': 'availability-zone-xxx',
            'OS-EXT-SRV-ATTR:host': 'host-name-xxx',
            'Metadata': format_columns.DictColumn({}),
        }

        self.image = image_fakes.create_one_image()

        self.image_client.find_image.return_value = self.image
        self.image_client.get_image.return_value = self.image

        self.flavor = compute_fakes.create_one_flavor()
        self.compute_client.find_flavor.return_value = self.flavor
        self.attrs['flavor'] = {'original_name': self.flavor.name}

        # The servers to be listed.
        self.servers = self.setup_sdk_servers_mock(3)
        self.compute_client.servers.return_value = self.servers

        # 정의한 ID와 동일한 ID를 가진 Fake 프로젝트/사용자 객체를 생성
        self.project = identity_fakes.FakeProject.create_one_project(
            attrs={'id': self.project_id}
        )
        self.user = identity_fakes.FakeUser.create_one_user(
            attrs={'id': self.user_id}
        )

이후 위와 같이 테스트의 기본 설정을 담당하는 _TestServerList 클래스에서 예상 컬럼 목록을 수정합니다.

또한 임의로 정의한 ID와 동일한 ID를 가진 Fake 프로젝트/사용자 객체를 생성하도록 setUp()을 수정합니다.

# python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py
class TestServerList(_TestServerList):
       def test_server_list_long_option(self):
        self.data = tuple(
            (
                s.id,
                s.name,
                s.status,
                # project name과 user name 컬럼 추가
                # 이때 추가하는 순서에 주의해야 함
                self.project.name,
                self.user.name,
                getattr(s, 'task_state'),
                server.PowerStateColumn(getattr(s, 'power_state')),
                server.AddressesColumn(s.addresses),
                # Image will be an empty string if boot-from-volume
                self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
                s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV,
                self.flavor.name,
                s.flavor['id'],
                ...

또한 실제로 테스트를 수행하는 test_server_list_long_option() 등의 메소드에 대해 self.data 튜플에 컬럼들을 모두 추가해줍니다.

이때, 추가하는 순서에 주의해야 합니다.

# python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py
class TestServerListV273(_TestServerList):
    # Columns to be listed up.
    columns = (
        'ID',
        'Name',
        'Status',
        'Project Name',
        'User Name',
        'Networks',
        'Image',
        'Flavor',
    )
    columns_long = (
        'ID',
        'Name',
        'Status',
        'Project Name',
        'User Name',
        'Task State',
        'Power State',
        'Networks',
        'Image Name',
        'Image ID',
        'Flavor',
        'Availability Zone',
        'Pinned Availability Zone',
        'Host',
        'Properties',
        'Scheduler Hints',
    )

    def setUp(self):
        super().setUp()

        # The fake servers' attributes. Use the original attributes names in
        # nova, not the ones printed by "server list" command.
        self.attrs['flavor'] = {
            'vcpus': self.flavor.vcpus,
            'ram': self.flavor.ram,
            'disk': self.flavor.disk,
            'ephemeral': self.flavor.ephemeral,
            'swap': self.flavor.swap,
            'original_name': self.flavor.name,
            'extra_specs': self.flavor.extra_specs,
        }

        # The servers to be listed.
        self.servers = self.setup_sdk_servers_mock(3)
        self.compute_client.servers.return_value = self.servers

        Image = collections.namedtuple('Image', 'id name')
        self.image_client.images.return_value = [
            Image(id=s.image['id'], name=self.image.name)
            # Image will be an empty string if boot-from-volume
            for s in self.servers
            if s.image
        ]

        # The flavor information is embedded, so now reason for this to be
        # called
        self.compute_client.flavors = mock.NonCallableMock()

        self.data = tuple(
            (
                s.id,
                s.name,
                s.status,
                # 순서에 맞게 project name과 user name 추가
                self.project.name,
                self.user.name,
                server.AddressesColumn(s.addresses),
                # Image will be an empty string if boot-from-volume
                self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
                self.flavor.name,
            )
            for s in self.servers
        )

TestServerListV273 클래스에 대해서도 동일하게 columns의 정보를 추가합니다.

해당 클래스의 경우 이미 _TestServerList를 상속받고 있기 때문에 새로운 ID와 Mock Identity를 생성하지는 않고, self.data만 수정합니다.

# python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py
class TestServerListV273(_TestServerList):
    def test_server_list_v269_with_partial_constructs(self):
        self.set_compute_api_version('2.69')
        arglist = []
        verifylist = []
        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
        # include "partial results" from non-responsive part of
        # infrastructure.
        server_dict = {
            "id": "server-id-95a56bfc4xxxxxx28d7e418bfd97813a",
            "status": "UNKNOWN",
            "tenant_id": "6f70656e737461636b20342065766572",
            "created": "2018-12-03T21:06:18Z",
            "links": [
                {"href": "http://fake/v2.1/", "rel": "self"},
                {"href": "http://fake", "rel": "bookmark"},
            ],
            "networks": {},
        }
        fake_server = _server.Server(**server_dict)
        self.servers.append(fake_server)
        columns, data = self.cmd.take_action(parsed_args)
        # get the first three servers out since our interest is in the partial
        # server.
        next(data)
        next(data)
        next(data)
        partial_server = next(data)
        expected_row = (
            'server-id-95a56bfc4xxxxxx28d7e418bfd97813a',
            None,
            'UNKNOWN',
            # self.project.name 대신, 위 server_dict에 하드코딩된 값 사용
            '6f70656e737461636b20342065766572',
            # self.user.name 대신, user_id가 없으므로 None 기대
            None,
            server.AddressesColumn(None),
            '',
            '',
        )
        self.assertEqual(expected_row, partial_server)

test_server_list_v269_with_partial_constructs() 메소드의 경우 하드코딩된 테스트 데이터를 사용하기 때문에 이에 맞게 tox의 출력결과를 기반으로 expected_row를 수정해줍니다.

==============================

Failed 1 tests - output below:

==============================



openstackclient.tests.unit.compute.v2.test_server.TestServerCreate.test_server_create_with_block_device_from_file

-----------------------------------------------------------------------------------------------------------------



Captured traceback:

~~~~~~~~~~~~~~~~~~~

    Traceback (most recent call last):



      File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\openstackclient\tests\unit\compute\v2\test_server.py", line 2736, in test_server_create_with_block_device_from_file

    parsed_args = self.check_parser(self.cmd, arglist, verifylist)

                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^



      File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\openstackclient\tests\unit\utils.py", line 86, in check_parser

    parsed_args = cmd_parser.parse_args(args)

                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^



      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\argparse.py", line 1904, in parse_args

    args, argv = self.parse_known_args(args, namespace)

                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^



      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\argparse.py", line 1914, in parse_known_args

    return self._parse_known_args2(args, namespace, intermixed=False)

           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^



      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\argparse.py", line 1943, in _parse_known_args2

    namespace, args = self._parse_known_args(args, namespace, intermixed)

                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^



      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\argparse.py", line 2184, in _parse_known_args

    start_index = consume_optional(start_index)

                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^



      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\argparse.py", line 2113, in consume_optional

    take_action(action, args, option_string)



      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\argparse.py", line 2018, in take_action

    action(self, namespace, argument_values, option_string)



      File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\openstackclient\compute\v2\server.py", line 1068, in __call__

    with open(values) as fh:

         ^^^^^^^^^^^^



    PermissionError: [Errno 13] Permission denied: 'C:\\Users\\lenovo\\AppData\\Local\\Temp\\tmpgr_188ci'

또한 현재 저는 Window 환경에서 테스트를 수행하고 있기 때문에 위와 같이 temp 파일을 다루는 테스트 로직 상에서 PermissionError가 발생하였습니다.

# python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py
class 
import os
...
TestServerCreate(TestServer):
...
    def test_server_create_with_block_device_from_file(self):
        self.set_compute_api_version('2.67')

        block_device = {
            'uuid': self.volume.id,
            'source_type': 'volume',
            'destination_type': 'volume',
            'disk_bus': 'ide',
            'device_type': 'disk',
            'device_name': 'sdb',
            'guest_format': 'ext4',
            'volume_size': 64,
            'volume_type': 'foo',
            'boot_index': 1,
            'delete_on_termination': True,
            'tag': 'foo',
        }

        # with 문을 제거하고 파일을 직접 열기
        fp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
        # 테스트 종료 후 파일을 자동으로 삭제하도록 예약
        self.addCleanup(os.remove, fp.name)

        json.dump(block_device, fp=fp)
        fp.flush()

        # 파일 핸들을 명시적으로 닫아줌
        fp.close()

        # with tempfile.NamedTemporaryFile(mode='w+') as fp:
        #     json.dump(block_device, fp=fp)
        #     fp.flush()

        arglist = [
            '--image',
            self.image.id,
            '--flavor',
            self.flavor.id,
            '--block-device',
            fp.name,
            self.server.name,
        ]
        verifylist = [
            ('image', self.image.id),
            ('flavor', self.flavor.id),
            ('block_devices', [block_device]),
            ('server_name', self.server.name),
        ]
        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
        columns, data = self.cmd.take_action(parsed_args)

이는 Windows 환경에서 tempfile로 생성된 임시 파일에 접근할 때 발생하는 전형적인 권한 문제로 test_server_create_with_block_device_from_file() 메소드에서 with 문을 제거하고 직접 파일을 생성하고 테스트 종료 후 자동으로 삭제하도록 예약합니다.

최종적으로 위와 같이 새롭게 만들어진 Fake Identity에 대해서 모든 테스트가 정상적으로 수행됨을 볼 수 있었습니다.

멘토님께서 말씀주신 내용에 따라 새로운 Fake Identity가 아니라, 기존에 생성된 Fake Identity를 사용하는 방식으로 변경을 시도해볼 예정입니다.

2개의 좋아요