3주차 과제 - 정지안

테스트 실행을 아래와 같은 흐름으로 최적화

  1. tox가 너무 느려, tox -e py3 로 테스트 수행

  2. tox -e py3 또한 너무 느려, export PIP_CACHE_DIR=~/.cache/pip 를 추가하여 캐싱 시도

  3. 이 또한 너무 느려, tox가 stestr을 실행하기위해 다양한 의존성을 가상환경에 설치하는 것이므로, 이를 생략하고자 stestr run 'openstackclient.tests.unit.compute.v2.test_server' 실행으로 테스트 최적화

기존 ListServer를 아래와 같이 수정하여 openstack server list 를 구현했었음.


# 25.08.05 - jian
columns += ('project_name', 'user_name')
column_headers += ('Project_name', 'User_name')
...
...
...

# Add project_name & User_name to serverlist - 2025.07.28 jian
project_map = {p.id: p.name for p in identity_client.projects.list()}
user_map = {u.id: u.name for u in identity_client.users.list()}

# Add project_name & User_name to serverlist - 2025.07.28 jian
for s in data:
    s.project_name = project_map.get(s.project_id)
    s.user_name = user_map.get(s.user_id)
    
...
...
...

이에 아래와 같은 에러 발생

==============================
Failed 26 tests - output below:
==============================

openstackclient.tests.unit.compute.v2.test_server.TestServerList.test_server_list_all_projects_option  
-----------------------------------------------------------------------------------------------------  

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

      File "/mnt/c/git/project/OSSCA/python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py", line 4790, in test_server_list_all_projects_option
    columns, data = self.cmd.take_action(parsed_args)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

      File "/mnt/c/git/project/OSSCA/python-openstackclient/openstackclient/compute/v2/server.py", line 2932, in take_action
    project_map = {p.id: p.name for p in identity_client.projects.list()}
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    TypeError: 'Mock' object is not iterable

openstackclient.tests.unit.compute.v2.test_server.TestServerList.test_server_list_column_option        
-----------------------------------------------------------------------------------------------        
...
...
...

목 객체에 이터러블이 적용되어 있지 않은 문제를 발견.

방법1. 목객체에 이터러블 타입 추가

방법2. 목객체를 이터러블로 조회하지 않고, 가짜 값을 반환하기

방법2가 쉬워보여 방법 2를 택했음.

class _TestServerList(TestServer):
    # Columns to be listed up.
    columns = (
        'ID',
        'Name',
        'Status',
        'Networks',
        'Image',
        'Flavor',
        'Project_name', # - 2025.08.04 jian
        'User_name' # - 2025.08.04 jian
    )
    columns_long = (
        'ID',
        'Name',
        'Status',
        'Task State',
        'Power State',
        'Networks',
        'Image Name',
        'Image ID',
        'Flavor Name',
        'Flavor ID',
        'Availability Zone',
        'Pinned Availability Zone',
        'Host',
        'Properties',
        'Scheduler Hints',
        'Project_name', # - 2025.08.04 jian
        'User_name' # - 2025.08.04 jian
    )
    columns_all_projects = (
        'ID',
        'Name',
        'Status',
        'Networks',
        'Image',
        'Flavor',
        'Project ID',
        'Project_name', # - 2025.08.04 jian
        'User_name' # - 2025.08.04 jian
    )
...

PN, UN을 달아주고

setup()에서 객체에 proj,user id 셋업 후 Mock 이터러블 에러 해결을 위해 가짜값 반환

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,
        }

        # The fake servers' attributes. Use the original attributes names in
        # nova, not the ones printed by "server list" command.
        self.attrs = {
            '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

        # 서버들이 keystone 맵핑에 걸리도록 id를 통일
        for s in self.servers: # - 2025.08.04 jian
            s.project_id = 'proj1'# - 2025.08.04 jian
            s.user_id = 'user1'# - 2025.08.04 jian

        # Get the command object to test
        self.cmd = server.ListServer(self.app, None)
        
        from collections import namedtuple# - 2025.08.04 jian
        Project = namedtuple('Project', 'id name')# - 2025.08.04 jian
        User    = namedtuple('User', 'id name')# - 2025.08.04 jian

        self.identity_client.projects.list.return_value = [Project('proj1', 'Project 1')]# - 2025.08.04 jian
        self.identity_client.users.list.return_value    = [User('user1', 'User 1')]# - 2025.08.04 jian

이후 모든 함수들의 self.data()에서 가짜값을 통일시켜줌

def test_server_list_long_with_host_status_v216(self):
        self.set_compute_api_version('2.16')
        self.data1 = tuple(
            (
                s.id,
                s.name,
                s.status,
                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'],
                getattr(s, 'availability_zone'),
                getattr(s, 'pinned_availability_zone', ''),
                server.HostColumn(getattr(s, 'hypervisor_hostname')),
                format_columns.DictColumn(s.metadata),
                format_columns.DictListColumn(s.scheduler_hints),
                'Project 1', 'User 1', # - 25.08.04 jian
            )
            for s in self.servers
        )

TestServerListV273 클래스에서는 expedcted_row튜플에서 PN/UN을 None설정

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',
            server.AddressesColumn(None),
            '',
            '',
            None, None, # - 25.08.04 jian
        )
        self.assertEqual(expected_row, partial_server)

다시 stestr run 돌렸을때 테스트 값이 같지 않는 에러발생.

에러: AssertionError: Element counts were not equal:


==============================
Failed 4 tests - output below:
==============================

openstackclient.tests.unit.compute.v2.test_server.TestServerListV273.test_server_list_with_changes_before
---------------------------------------------------------------------------------------------------------

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

      File "/mnt/c/git/project/OSSCA/python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py", line 5503, in test_server_list_with_changes_before
    self.assertCountEqual(self.data, tuple(data))

      File "/usr/lib/python3.12/unittest/case.py", line 1216, in assertCountEqual
    self.fail(msg)

      File "/usr/lib/python3.12/unittest/case.py", line 715, in fail
    raise self.failureException(msg)

    AssertionError: Element counts were not equal:
First has 1, Second has 0:  ('server-id-bc189e87868a48f2b25f05b3bac201f2', 'server-name-9855255ec1ef41d1b3a0efe4d69b332e', 'ACTIVE', AddressesColumn({}), 'image-name7e04e951297a4d3a973a9f35b6ad806a', 'flavor-name-8c066c28121b4f0eacfe96dafc4abc8e', 'Project 1', 'User 1')
First has 1, Second has 0:  ('server-id-6f516480a8db4f4d9874bf8c67394932', 'server-name-42cc232aefd94a6dac6ebf95b80862af', 'ACTIVE', AddressesColumn({}), 'image-name7e04e951297a4d3a973a9f35b6ad806a', 'flavor-name-8c066c28121b4f0eacfe96dafc4abc8e', 'Project 1', 'User 1')
First has 1, Second has 0:  ('server-id-cc84b533eec245c09f5de7e949c80329', 'server-name-501ba90a89db479f9f7839a8c204f9e9', 'ACTIVE', AddressesColumn({}), 'image-name7e04e951297a4d3a973a9f35b6ad806a', 'flavor-name-8c066c28121b4f0eacfe96dafc4abc8e', 'Project 1', 'User 1')
First has 0, Second has 1:  ('server-id-bc189e87868a48f2b25f05b3bac201f2', 'server-name-9855255ec1ef41d1b3a0efe4d69b332e', 'ACTIVE', AddressesColumn({}), 'image-name7e04e951297a4d3a973a9f35b6ad806a', 'flavor-name-8c066c28121b4f0eacfe96dafc4abc8e', None, None)
First has 0, Second has 1:  ('server-id-6f516480a8db4f4d9874bf8c67394932', 'server-name-42cc232aefd94a6dac6ebf95b80862af', 'ACTIVE', AddressesColumn({}), 'image-name7e04e951297a4d3a973a9f35b6ad806a', 'flavor-name-8c066c28121b4f0eacfe96dafc4abc8e', None, None)
First has 0, Second has 1:  ('server-id-cc84b533eec245c09f5de7e949c80329', 'server-name-501ba90a89db479f9f7839a8c204f9e9', 'ACTIVE', AddressesColumn({}), 'image-name7e04e951297a4d3a973a9f35b6ad806a', 'flavor-name-8c066c28121b4f0eacfe96dafc4abc8e', None, None)

openstackclient.tests.unit.compute.v2.test_server.TestServerListV273.test_server_list_with_locked
-------------------------------------------------------------------------------------------------

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

      File "/mnt/c/git/project/OSSCA/python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py", line 5455, in test_server_list_with_locked
    self.assertCountEqual(self.data, tuple(data))

      File "/usr/lib/python3.12/unittest/case.py", line 1216, in assertCountEqual
    self.fail(msg)

      File "/usr/lib/python3.12/unittest/case.py", line 715, in fail
    raise self.failureException(msg)

    AssertionError: Element counts were not equal:
First has 1, Second has 0:  ('server-id-77400a0
...
...
...

TestServerList273 에도 setup()에서 id주입

class TestServerListV273(_TestServerList):
...
...

    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)
        
        for s in self.servers: # - 25.08.05 -jian
            s.project_id = 'proj1'# - 25.08.05 -jian
            s.user_id = 'user1'# - 25.08.05 -jian

아직 Fail 1개


==============================
Failed 1 tests - output below:
==============================

openstackclient.tests.unit.compute.v2.test_server.TestServerList.test_server_list_long_with_host_status_v216
------------------------------------------------------------------------------------------------------------

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

      File "/mnt/c/git/project/OSSCA/python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py", line 5354, in test_server_list_long_with_host_status_v216
    self.assertEqual(tuple(self.data2), tuple(data))

      File "/mnt/c/git/project/OSSCA/.venv/lib/python3.12/site-packages/testtools/testcase.py", line 419, in assertEqual
    self.assertThat(observed, matcher, message)

      File "/mnt/c/git/project/OSSCA/.venv/lib/python3.12/site-packages/testtools/testcase.py", line 509, in assertThat
    raise mismatch_error

    testtools.matchers._impl.MismatchError: !=:
...
...

216쪽에서 미스매치 에러.

또 id를 셋업해줬다.

def test_server_list_long_with_host_status_v216(self):
        self.set_compute_api_version('2.16')
        self.data1 = tuple(
            (
                s.id,
                s.name,
                s.status,
                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'],
                getattr(s, 'availability_zone'),
                getattr(s, 'pinned_availability_zone', ''),
                server.HostColumn(getattr(s, 'hypervisor_hostname')),
                format_columns.DictColumn(s.metadata),
                format_columns.DictListColumn(s.scheduler_hints),
                'Project 1', 'User 1', # - 25.08.04 jian
            )
            for s in self.servers
        )

        arglist = ['--long']
        verifylist = [
            ('long', True),
        ]

        # First test without host_status in the data -- the column should not
        # be present in this case.
        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
        columns, data = self.cmd.take_action(parsed_args)

        self.compute_client.servers.assert_called_with(**self.kwargs)

        self.assertEqual(self.columns_long, columns)
        self.assertEqual(tuple(self.data1), tuple(data))

        # Next test with host_status in the data -- the column should be
        # present in this case.
        self.compute_client.servers.reset_mock()

        self.attrs['host_status'] = 'UP'
        servers = self.setup_sdk_servers_mock(3)
        
        for s in servers: # - 25.08.04 - jian
            s.project_id = 'proj1'# - 25.08.04 - jian
            s.user_id = 'user1'# - 25.08.04 - jian

하면서 너무 난잡한것같아서 다른사람들 걸 봤는데 이렇게 하는게 아닌 것 같다..

take_action부터 이터러블 아닌걸로 고치고 다시 해보겠습니다.