diff --git a/doc/newsfragments/2751_change.no_parts_in_interactive.rst b/doc/newsfragments/2751_change.no_parts_in_interactive.rst new file mode 100644 index 000000000..301ddd1e9 --- /dev/null +++ b/doc/newsfragments/2751_change.no_parts_in_interactive.rst @@ -0,0 +1 @@ +Parts are disabled in interactive mode to fix a recent regression on this. diff --git a/testplan/common/utils/networking.py b/testplan/common/utils/networking.py index cb43490a0..61235a8b1 100644 --- a/testplan/common/utils/networking.py +++ b/testplan/common/utils/networking.py @@ -14,61 +14,5 @@ def port_to_int(port): return socket.getservbyname(port) -def format_access_urls(host, port, url_path, ssl=False): - """ - Format a string to log to give HTTP access to a URL endpoint listening - on a particular host/port. Handles special 0.0.0.0 IP and formats IPV6 - addresses. The returned string is indented by 4 spaces. - - :param host: hostname or IP address - :type host: ``str`` - :param port: port number - :type port: ``int`` - :param url_path: path to format after host:port part of URL. A leading / - will be inserted if there isn't already one present. - :type url_path: ``str`` - :return: a formatted string containing the connection URL(s) - :rtype: ``str`` - """ - # Strip any whitespace and insert a leading / to paths if required. - url_path = url_path.strip() - if not url_path.startswith("/"): - url_path = "/" + url_path - - scheme = "https" if ssl else "http" - - # Handle 0.0.0.0 as a special case: this means that the URL can be accessed - # both via localhost or on any IP address this host owns. - if host == "0.0.0.0": - local_url = "{scheme}://localhost:{port}{path}".format( - scheme=scheme, port=port, path=url_path - ) - - try: - local_ip = socket.gethostbyname(socket.getfqdn()) - network_url = "{scheme}://{host}:{port}{path}".format( - scheme=scheme, host=local_ip, port=port, path=url_path - ) - - return ( - " Local: {local}\n" - " On Your Network: {network}".format( - local=local_url, network=network_url - ) - ) - except socket.gaierror: - return " {}".format(local_url) - else: - # Check for an IPv6 address. Web browsers require IPv6 addresses - # to be enclosed in []. - try: - if ipaddress.ip_address(host).version == 6: - host = "[{}]".format(host) - except ValueError: - # Expected if the host is a host name instead of an IP address. - pass - - url = "{scheme}://{host}:{port}{path}".format( - scheme=scheme, host=host, port=port, path=url_path - ) - return " {}".format(url) +def get_hostname_access_url(port, url_path): + return f"http://{socket.getfqdn()}:{port}{url_path}" diff --git a/testplan/runnable/base.py b/testplan/runnable/base.py index 5fd2cae6a..e781f094a 100644 --- a/testplan/runnable/base.py +++ b/testplan/runnable/base.py @@ -394,7 +394,7 @@ def __init__(self, **options): # Before saving test report, recursively generate unique strings in # uuid4 format as report uid instead of original one. Skip this step # when executing unit/functional tests or running in interactive mode. - self._reset_report_uid = self.cfg.interactive_port is None + self._reset_report_uid = not self._is_interactive_run() self.scheduled_modules = [] # For interactive reload self.remote_services = {} self.runid_filename = uuid.uuid4().hex @@ -455,7 +455,7 @@ def get_default_exporters(self): test_exporters.WebServerExporter(ui_port=self.cfg.ui_port) ) if ( - self.cfg.interactive_port is None + not self._is_interactive_run() and self.cfg.tracing_tests is not None ): exporters.append(test_exporters.CoveredTestsExporter()) @@ -689,6 +689,12 @@ def discover( **task_target_info.task_kwargs, ) + multitest_parts = ( + None + if self._is_interactive_run() + else task_target_info.multitest_parts + ) + if task_target_info.target_params: for param in task_target_info.target_params: if isinstance(param, dict): @@ -707,7 +713,7 @@ def discover( tasks.extend( self._get_tasks( task_arguments, - task_target_info.multitest_parts, + multitest_parts, runtime_data, ) ) @@ -715,7 +721,7 @@ def discover( tasks.extend( self._get_tasks( task_arguments, - task_target_info.multitest_parts, + multitest_parts, runtime_data, ) ) @@ -860,7 +866,7 @@ def add( self.cfg.test_lister.log_test_info(task_info.materialized_test) return None - if self.cfg.interactive_port is not None: + if self._is_interactive_run(): self._register_task_for_interactive(task_info) # for interactive always use the local runner resource = local_runner @@ -875,6 +881,9 @@ def add( ) return uid + def _is_interactive_run(self): + return self.cfg.interactive_port is not None + def _register_task(self, resource, target, uid, metadata): self._tests[uid] = resource self._test_metadata.append(metadata) diff --git a/testplan/runnable/interactive/base.py b/testplan/runnable/interactive/base.py index 6d600a878..a3eda394d 100644 --- a/testplan/runnable/interactive/base.py +++ b/testplan/runnable/interactive/base.py @@ -11,6 +11,7 @@ from testplan.common import config, entity from testplan.common.report import Report +from testplan.common.utils.networking import get_hostname_access_url from testplan.runnable.interactive import http, reloader, resource_loader from testplan.report import ( TestReport, @@ -859,9 +860,8 @@ def _display_connection_info(self): ) self.logger.user_info( - "\nInteractive Testplan web UI is running. Access it at: %s:%s/interactive", - socket.getfqdn(), - str(port), + "\nInteractive Testplan web UI is running. Access it at: %s", + get_hostname_access_url(port, "/interactive"), ) def _initial_report(self): diff --git a/testplan/web_ui/server.py b/testplan/web_ui/server.py index 78cef9a62..419367965 100644 --- a/testplan/web_ui/server.py +++ b/testplan/web_ui/server.py @@ -8,7 +8,7 @@ from testplan import defaults from testplan.common.utils.thread import interruptible_join from testplan.common.utils.timing import wait -from testplan.common.utils.networking import format_access_urls +from testplan.common.utils.networking import get_hostname_access_url from testplan.common.utils.logger import TESTPLAN_LOGGER from .web_app import WebServer, TESTPLAN_UI_STATIC_DIR @@ -91,8 +91,8 @@ def display(self): self._report_url = f"http://localhost:{port}/testplan/local" TESTPLAN_LOGGER.user_info( - "View the JSON report in the browser:\n%s", - format_access_urls(host, port, "/testplan/local"), + "View the JSON report in the browser: %s", + get_hostname_access_url(port, "/testplan/local"), ) def wait_for_kb_interrupt(self): diff --git a/tests/functional/testplan/exporters/testing/test_webserver.py b/tests/functional/testplan/exporters/testing/test_webserver.py index 518820783..69587c045 100644 --- a/tests/functional/testplan/exporters/testing/test_webserver.py +++ b/tests/functional/testplan/exporters/testing/test_webserver.py @@ -19,7 +19,9 @@ _TIMEOUT = 120 _REQUEST_TIMEOUT = 0.5 -_URL_RE = re.compile(r"^\s*Local: (?P[^\s]+)\s*$") +_URL_RE = re.compile( + r"^View the JSON report in the browser: (?P[^\s]+)\s*$" +) @pytest.yield_fixture( diff --git a/tests/functional/testplan/runners/pools/test_auto_part.py b/tests/functional/testplan/runners/pools/test_auto_part.py index 462a68642..664bb899c 100755 --- a/tests/functional/testplan/runners/pools/test_auto_part.py +++ b/tests/functional/testplan/runners/pools/test_auto_part.py @@ -1,5 +1,6 @@ import os import tempfile +from pathlib import Path import pytest @@ -38,6 +39,36 @@ def test_auto_parts_discover(): assert pool.size == 2 +def test_auto_parts_discover_interactive(runpath): + mockplan = TestplanMock( + "plan", + runpath=runpath, + merge_scheduled_parts=True, + auto_part_runtime_limit=45, + plan_runtime_target=200, + interactive_port=0, + runtime_data={ + "Proj1-suite": { + "execution_time": 199.99, + "setup_time": 5, + } + }, + ) + pool = ProcessPool(name="MyPool", size="auto") + mockplan.add_resource(pool) + current_folder = Path(__file__).resolve().parent + mockplan.schedule_all( + path=current_folder / "discover_tasks", + name_pattern=r".*auto_parts_tasks\.py$", + resource="MyPool", + ) + + local_pool = mockplan.resources.get(mockplan.resources.first()) + # validate that only on etask added to the local pool without split + assert len(pool.added_items) == 0 + assert len(local_pool.added_items) == 1 + + def test_auto_weight_discover(): with tempfile.TemporaryDirectory() as runpath: mockplan = TestplanMock(