charmed-kubernetes / pytest-operator

Apache License 2.0
6 stars 13 forks source link

Model deployment fails if test file structure contains __init__.py files #30

Closed mkalcok closed 2 years ago

mkalcok commented 2 years ago

I'm working on a charm which has following structure:

├── LICENSE
├── ...
├── src
│   ├── charm.py
│   └── resource.py
├── tests
│   ├── __init__.py
│   ├── functional
│   │   ├── __init__.py
│   │   ├── bundle.yaml
│   │   ├── conftest.py
│   │   ├── test_ceph_csi.py
│   │   └── utils
│   │       ├── __init__.py
│   │       └── utils.py
│   └── unit
│       ├── ...
├── tox.ini

I'm not sure if it's important but I run functional tests via tox. All it really does is execute pytest {toxinidir}/tests/functional.

Problem is that due to the presence of __init__.py files in my structure, when the pytest-operator tries to deploy my bundle, it constructs name of the model with dot-separated names of the directories. Final model name looks like this:

tests.functional.test-ceph-csi-8gzx

and I get the following error/traceback:

/usr/lib/python3.6/asyncio/base_events.py:484: in run_until_complete
    return future.result()
.tox/func/lib/python3.6/site-packages/pytest_asyncio/plugin.py:123: in setup
    res = await gen_obj.__anext__()
.tox/func/lib/python3.6/site-packages/pytest_operator/plugin.py:144: in ops_test
    await ops_test._setup_model()
.tox/func/lib/python3.6/site-packages/pytest_operator/plugin.py:223: in _setup_model
    self.model_name, cloud_name=self.cloud_name
.tox/func/lib/python3.6/site-packages/juju/controller.py:360: in add_model
    region=region
.tox/func/lib/python3.6/site-packages/juju/client/facade.py:480: in wrapper
    reply = await f(*args, **kwargs)
.tox/func/lib/python3.6/site-packages/juju/client/_client5.py:5515: in CreateModel
    reply = await self.rpc(msg)
.tox/func/lib/python3.6/site-packages/juju/client/facade.py:623: in rpc
    result = await self.connection.rpc(msg, encoder=TypeEncoder)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <juju.client.connection.Connection object at 0x7f68dd1b26a0>
msg = {'params': {'cloud-tag': 'cloud-openstack', 'config': {'authorized-keys': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDyfPI...: 'tests.functional.test-ceph-csi-8gzx', ...}, 'request': 'CreateModel', 'request-id': 10, 'type': 'ModelManager', ...}
encoder = <class 'juju.client.facade.TypeEncoder'>

    async def rpc(self, msg, encoder=None):
        '''Make an RPC to the API. The message is encoded as JSON
        using the given encoder if any.
        :param msg: Parameters for the call (will be encoded as JSON).
        :param encoder: Encoder to be used when encoding the message.
        :return: The result of the call.
        :raises JujuAPIError: When there's an error returned.
        :raises JujuError:
        '''
        self.__request_id__ += 1
        msg['request-id'] = self.__request_id__
        if'params' not in msg:
            msg['params'] = {}
        if "version" not in msg:
            msg['version'] = self.facades[msg['type']]
        outgoing = json.dumps(msg, indent=2, cls=encoder)
        log.debug('connection {} -> {}'.format(id(self), outgoing))
        for attempt in range(3):
            if self.monitor.status == Monitor.DISCONNECTED:
                # closed cleanly; shouldn't try to reconnect
                raise websockets.exceptions.ConnectionClosed(
                    0, 'websocket closed')
            try:
                await self.ws.send(outgoing)
                break
            except websockets.ConnectionClosed:
                if attempt == 2:
                    raise
                log.warning('RPC: Connection closed, reconnecting')
                # the reconnect has to be done in a separate task because,
                # if it is triggered by the pinger, then this RPC call will
                # be cancelled when the pinger is cancelled by the reconnect,
                # and we don't want the reconnect to be aborted halfway through
                await asyncio.wait([self.reconnect()], loop=self.loop)
                if self.monitor.status != Monitor.CONNECTED:
                    # reconnect failed; abort and shutdown
                    log.error('RPC: Automatic reconnect failed')
                    raise
        result = await self._recv(msg['request-id'])
        log.debug('connection {} <- {}'.format(id(self), result))

        if not result:
            return result

        if 'error' in result:
            # API Error Response
>           raise errors.JujuAPIError(result)
E           juju.errors.JujuAPIError: failed to create config: creating config from values failed: "tests.functional.test-ceph-csi-8gzx" is not a valid name: model names may only contain lowercase letters, digits and hyphens

.tox/func/lib/python3.6/site-packages/juju/client/connection.py:495: JujuAPIError

Removing the __init__.py files gets rid of this problem but then pytest complains about relative imports. Also I think that main point is that algorithm which creates new model name uses forbidden characters to concatenate directories.

EDIT: Including test_build_and_deploy step 'cause I forgot originaly.

@pytest.mark.abort_on_fail
async def test_build_and_deploy(ops_test):
    """Build ceph-csi charm and deploy testing model."""
    logger.info("Building ceph-csi charm.")
    ceph_csi_charm = await ops_test.build_charm(".")
    logger.debug("Deploying ceph-csi functional test bundle.")
    await ops_test.model.deploy(
        ops_test.render_bundle("tests/functional/bundle.yaml", master_charm=ceph_csi_charm)
    )
    await ops_test.model.wait_for_idle(
        wait_for_active=True, timeout=60 * 60, check_freq=5, raise_on_error=False
    )
techalchemy commented 2 years ago

This seems to be the same issue I ran into in #26 -- I have a workaround mentioned there, which is just to add

_, _, __name__ = __name__.rpartition(".")

in the relevant file (in my case tests/test_integration.py)