Commit 48f81435 authored by kzangeli's avatar kzangeli
Browse files

fix(portability): honour `console`, and run mosquitto where docker is absent



Two things that make the suite unusable in a container, both fixable without
touching a single test.

logUtils' `Output` keyword documents a `console` argument - "If false, the JSON is
not written to terminal" - and ignored it, passing also_console=True on every
line. A full run is ~4400 request/response pairs, so the terminal receives about
175,000 lines and the one line per test that says what happened is lost in it. The
argument is honoured now, and ETSI_QUIET_IO=1 switches the echo off globally
without editing tests. Nothing is lost: logger.info still writes to log.html,
which is where a failure gets investigated.

Start Mqtt Server ran `docker run eclipse-mosquitto`, which cannot work where
there is no docker daemon - a CI job that is itself a container, for one. All
twelve 058_* tests then failed at setup with "Connection refused", which reads as
a broker fault and is not one. Where docker is absent but mosquitto is present, it
now starts the daemon directly with the same configuration: the listener port and
the password-file path are rewritten out of the container's paths, and the
password file is made readable because mosquitto drops privileges when started as
root and cannot otherwise read a root-owned file. Stop does whichever matches.

Both are candidates for upstream: the first is a documented argument that did not
work, the second is portability with no behaviour change where docker exists.

Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent 75ba471d
Loading
Loading
Loading
Loading
+31 −8
Original line number Diff line number Diff line
from __future__ import unicode_literals
from __future__ import division
from json import dumps, JSONDecodeError, loads
from os import environ
from robot.api import logger
from robot.api.deco import keyword


def _to_console(console):
    """Should this be echoed to the terminal?

    The `console` argument of the keywords below is documented ("If false, the
    JSON is not written to terminal") and was ignored - every request and every
    response went to the terminal unconditionally. On a full run that is around
    4400 request/response pairs and 175,000 lines, which buries the one line per
    test that tells you what happened, and makes the output unusable in CI.

    ETSI_QUIET_IO=1 switches the echo off globally without touching a single test.
    Nothing is lost: logger.info always writes to log.html, which is where a
    failure is investigated anyway.
    """
    if environ.get("ETSI_QUIET_IO") == "1":
        return False
    return bool(console)


@keyword(name="Output", tags=("I/O",))
def output(response, description, console=True):
    """*Request and response are output to terminal and file (in JSON).*
@@ -34,11 +53,13 @@ def output(response, description, console=True):
    pretty_request_json = dumps(request_json, indent=4, sort_keys=False, separators=(",", ": "))
    pretty_response_json = dumps(response_json, indent=4, sort_keys=False, separators=(",", ": "))

    logger.info("\n" + description, also_console=True)
    logger.info("Request ->", also_console=True)
    logger.info(pretty_request_json, also_console=True)
    logger.info("Response ->", also_console=True)
    logger.info(pretty_response_json, also_console=True)
    echo = _to_console(console)

    logger.info("\n" + description, also_console=echo)
    logger.info("Request ->", also_console=echo)
    logger.info(pretty_request_json, also_console=echo)
    logger.info("Response ->", also_console=echo)
    logger.info(pretty_response_json, also_console=echo)


@keyword(name="Output Notification", tags=("I/O",))
@@ -54,6 +75,8 @@ def output_notification(body, headers, description, console=True):

    pretty_request_json = dumps(request_json, indent=4, sort_keys=False, separators=(",", ": "))

    logger.info("\n" + description, also_console=True)
    logger.info("Notification ->", also_console=True)
    logger.info(pretty_request_json, also_console=True)
    echo = _to_console(console)

    logger.info("\n" + description, also_console=echo)
    logger.info("Notification ->", also_console=echo)
    logger.info(pretty_request_json, also_console=echo)
+50 −2
Original line number Diff line number Diff line
@@ -14,6 +14,22 @@ Start Mqtt Server
    [Tags]    actor_notification-receiver
    [Arguments]    ${conf_file_name}    ${port}

    # docker is not available everywhere the suite runs - a CI job that is itself
    # a container has no docker daemon, and every MQTT test then fails at setup
    # with "Connection refused", which reads as a broker fault and is not one.
    # Where there is no docker but there IS a mosquitto, run it directly: the same
    # configuration, the same password file, the same port.
    ${docker} =    Run Process    command -v docker    shell=yes
    IF    ${docker.rc} == 0
        Start Mqtt Server With Docker    ${conf_file_name}    ${port}
    ELSE
        Start Mqtt Server Natively    ${conf_file_name}    ${port}
    END

Start Mqtt Server With Docker
    [Tags]    actor_notification-receiver
    [Arguments]    ${conf_file_name}    ${port}

    ${request} =    Set Variable
    ...    docker run -d -p ${port}:1883 -v "${CURDIR}/mosquitto/${conf_file_name}:/mosquitto/config/mosquitto.conf" -v "${CURDIR}/mosquitto/mosquitto_pwd:/mosquitto/config/mosquitto_pwd" --name "${container_name}" eclipse-mosquitto:2.0.18-openssl

@@ -27,7 +43,39 @@ Start Mqtt Server
        Log To Console    ${result.stderr}
    END

Start Mqtt Server Natively
    [Tags]    actor_notification-receiver
    [Arguments]    ${conf_file_name}    ${port}

    # The configuration is written for the container: it listens on 1883 and
    # refers to the password file by its path inside the image. Both are rewritten
    # here. The password file is copied and made world-readable because mosquitto
    # drops privileges when started as root - as it is in a container - and cannot
    # otherwise read a file owned by root.
    ${script} =    Set Variable
    ...    set -e;
    ...    d=/tmp/etsi-mosquitto-${port}; rm -rf $d; mkdir -p $d;
    ...    cp "${CURDIR}/mosquitto/mosquitto_pwd" $d/mosquitto_pwd; chmod 644 $d/mosquitto_pwd;
    ...    sed -e "s|^listener .*|listener ${port}|" -e "s|/mosquitto/config/mosquitto_pwd|$d/mosquitto_pwd|" "${CURDIR}/mosquitto/${conf_file_name}" > $d/mosquitto.conf;
    ...    chmod 644 $d/mosquitto.conf;
    ...    mosquitto -d -c $d/mosquitto.conf;
    ...    for i in $(seq 1 40); do (exec 3<>/dev/tcp/127.0.0.1/${port}) 2>/dev/null && exit 0; sleep 0.25; done; exit 1

    ${result} =    Run Process    ${script}    shell=yes
    IF    ${result.rc} > 0
        Log To Console    mosquitto could not be started on port ${port} without docker
        Log To Console    ${result.stdout}
        Log To Console    ${result.stderr}
    END

Stop Mqtt Server
    [Tags]    actor_notification-receiver
    ${request} =    Set Variable    docker container rm -f "${container_name}"
    Run Process    ${request}    shell=yes

    # Stop whichever was started - the container if docker is here, otherwise the
    # local daemon. Both are harmless when the other was used.
    ${docker} =    Run Process    command -v docker    shell=yes
    IF    ${docker.rc} == 0
        Run Process    docker container rm -f "${container_name}"    shell=yes
    ELSE
        Run Process    pkill -x mosquitto    shell=yes
    END