This is an automated email from the ASF dual-hosted git repository.

janhoy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-orbit.git


The following commit(s) were added to refs/heads/main by this push:
     new 5b8d58ff Repair the integration test suite and make the Solr port 
configurable (#105)
5b8d58ff is described below

commit 5b8d58ff10bf08d45659cb3c8d7a66937bd1a86b
Author: Jan Høydahl <[email protected]>
AuthorDate: Fri Oct 16 16:08:00 2026 +0200

    Repair the integration test suite and make the Solr port configurable (#105)
---
 .github/workflows/integration-test.yml             | 95 ++++++++++++++++++++++
 it/__init__.py                                     | 89 ++++++++------------
 it/distribution_test.py                            | 10 +--
 it/info_test.py                                    |  8 +-
 it/list_test.py                                    |  4 +-
 it/sources_test.py                                 | 19 +++--
 it/tracker_test.py                                 | 19 +++--
 solrorbit/benchmark.py                             | 10 +++
 solrorbit/builder/launcher.py                      |  4 +
 .../builder/launchers/local_process_launcher.py    |  4 +
 solrorbit/builder/provisioner.py                   | 23 ++++--
 solrorbit/workload/loader.py                       | 15 ++--
 12 files changed, 212 insertions(+), 88 deletions(-)

diff --git a/.github/workflows/integration-test.yml 
b/.github/workflows/integration-test.yml
new file mode 100644
index 00000000..ede2eea8
--- /dev/null
+++ b/.github/workflows/integration-test.yml
@@ -0,0 +1,95 @@
+name: Run Integration Tests
+
+# Manual only. The suite downloads Solr distributions, builds from source and
+# runs real benchmarks -- minutes to tens of minutes -- which is too slow and
+# too failure-prone to gate every push. Manual is enough to make rot visible:
+# a maintainer runs it before a release, or when touching the builder or it/.
+on:
+  workflow_dispatch:
+    inputs:
+      tests:
+        description: "pytest targets (e.g. 'it/info_test.py it/list_test.py')"
+        required: false
+        default: "it"
+      distribution:
+        description: "Override the Solr versions under test (comma-separated)"
+        required: false
+        default: ""
+
+concurrency:
+  # Manual dispatches are never cancelled: they are not superseded by anything,
+  # and a half-finished run hides breakage.
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: false
+
+permissions:
+  contents: read
+
+jobs:
+  Integration-Tests:
+    # Linux only, deliberately. The two hangs that make this suite unrunnable 
on
+    # macOS -- the CPython os.stat recursion during corpus download, and a
+    # Thespian stall (#106) -- are both absent here.
+    runs-on: ubuntu-latest
+    timeout-minutes: 90
+
+    steps:
+      - name: Install bz2 development package
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y libbz2-dev
+
+      - name: Check out repository code
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 
v7.0.1
+
+      - name: Set up JDK 21
+        # Solr 10 will not start on anything lower, and left to itself the
+        # resolver picks the lowest allowed JDK.
+        uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # 
v6.1.0
+        with:
+          distribution: temurin
+          java-version: "21"
+
+      - name: Install uv
+        uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # 
v10.2.0
+        with:
+          version: "0.11.32"
+          enable-cache: true
+
+      - name: Record the toolchain
+        run: |
+          java -version
+          docker --version
+          uv --version
+
+      - name: Run the integration suite
+        env:
+          # Inputs reach the shell as environment variables rather than being
+          # interpolated into the script, which is both correct quoting and the
+          # documented way to keep a dispatch input out of the command text.
+          TESTS: ${{ inputs.tests }}
+          IT_DISTRIBUTIONS: ${{ inputs.distribution }}
+        run: |
+          # jvm.resolve_path looks for JAVA<major>_HOME before JAVA_HOME, and
+          # the tests ask for --runtime-jdk=21. setup-java exports JAVA_HOME.
+          export JAVA21_HOME="$JAVA_HOME"
+          # Unquoted on purpose: the input may name several pytest targets.
+          # shellcheck disable=SC2086
+          uv run --extra develop pytest -s -v --junitxml=it-results.xml $TESTS
+
+      - name: Upload test results
+        if: always()
+        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 
# v7.0.2
+        with:
+          name: integration-test-results
+          path: it-results.xml
+          if-no-files-found: warn
+
+      - name: Upload Solr Orbit logs
+        # These failures are usually only diagnosable from the logs.
+        if: always()
+        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 
# v7.0.2
+        with:
+          name: solr-orbit-logs
+          path: ~/.solr-orbit/logs/
+          if-no-files-found: warn
diff --git a/it/__init__.py b/it/__init__.py
index e438811b..789f9fbe 100644
--- a/it/__init__.py
+++ b/it/__init__.py
@@ -25,12 +25,13 @@
 # specific language governing permissions and limitations
 # under the License.
 
-import errno
 import functools
 import json
 import os
 import random
+import shlex
 import socket
+import sys
 import time
 import datetime
 
@@ -40,9 +41,15 @@ from solrorbit import client, config, version, paths
 from solrorbit.utils import process
 
 CONFIG_NAMES = ["in-memory-it"]
-DISTRIBUTIONS = ["9.10.1", "10.1.0"]
+# Overridable so a CI run can pin a single version without a code change.
+DISTRIBUTIONS = [v.strip() for v in
+                 os.environ.get("IT_DISTRIBUTIONS", 
"9.11.0,10.0.0").split(",") if v.strip()]
 WORKLOADS = ["geonames", "nyc_taxis"]
-BASE_COMMANDS = ["solr-orbit"]
+# Invoke the Solr Orbit under test, not whatever "solr-orbit" happens to be on
+# PATH. A bare command name resolves to a globally installed copy -- a pyenv 
shim,
+# say -- so the integration tests exercised an installation that had nothing 
to do
+# with the working tree.
+BASE_COMMANDS = [f"{shlex.quote(sys.executable)} -m solrorbit.benchmark"]
 ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
 
 
@@ -124,24 +131,16 @@ def command_in_docker(command_line, python_version):
     return shell_cmd(docker_command)
 
 
-def wait_until_port_is_free(port_number=39200, timeout=120):
-    start = time.perf_counter()
-    end = start + timeout
-    while time.perf_counter() < end:
-        c = socket.socket()
-        connect_result = c.connect_ex(("127.0.0.1", port_number))
-        # noinspection PyBroadException
-        try:
-            if connect_result == errno.ECONNREFUSED:
-                c.close()
-                return
-            else:
-                c.close()
-                time.sleep(0.5)
-        except Exception:
-            pass
+def free_port():
+    """Pick a free TCP port.
 
-    raise TimeoutError(f"Port [{port_number}] is occupied after [{timeout}] 
seconds")
+    The integration tests must not assume 8983: a Solr developer very likely 
has
+    their own Solr listening there, and the pipelines refuse to start when the
+    port is busy.
+    """
+    with socket.socket() as s:
+        s.bind(("127.0.0.1", 0))
+        return s.getsockname()[1]
 
 
 def check_prerequisites():
@@ -173,16 +172,16 @@ class TestCluster:
         self.http_port = http_port
         transport_port = http_port + 100
         try:
-            err, retcode = process.run_subprocess_with_stderr(
-                "solr-orbit install --configuration-name={cfg} 
--distribution-version={dist} --build-type=tar "
-                "--http-port={http_port} --node={node_name} --master-nodes="
+            err, retcode = 
process.run_subprocess_with_stderr(solrorbit_command_line_for(
+                self.cfg,
+                "install --distribution-version={dist} --build-type=tar "
+                "--http-port={http_port} --node-name={node_name} 
--master-nodes="
                 "{node_name} --cluster-config={cluster_config} "
-                
"--seed-hosts=\"127.0.0.1:{transport_port}\"".format(cfg=self.cfg,
-                                                                     
dist=distribution_version,
-                                                                     
http_port=http_port,
-                                                                     
node_name=node_name,
-                                                                     
cluster_config=cluster_config,
-                                                                     
transport_port=transport_port))
+                
"--seed-hosts=\"127.0.0.1:{transport_port}\"".format(dist=distribution_version,
+                                                                      
http_port=http_port,
+                                                                      
node_name=node_name,
+                                                                      
cluster_config=cluster_config,
+                                                                      
transport_port=transport_port)))
             if retcode != 0:
                 raise AssertionError("Failed to install node 
{}.".format(distribution_version), err)
             self.installation_id = json.loads(err)["installation-id"]
@@ -190,11 +189,17 @@ class TestCluster:
             raise AssertionError("Failed to install node 
{}.".format(distribution_version), e)
 
     def start(self, test_run_id):
-        cmd = "start --runtime-jdk=\"bundled\" --installation-id={} 
--test-run-id={}".format(self.installation_id, test_run_id)
+        # Java 21: "bundled" is an OpenSearch notion (its distributions ship a 
JDK,
+        # Solr's do not) and resolved to a nonexistent JAVAbundled_HOME. Left 
to
+        # itself the resolver picks the lowest allowed JDK, which is 11 and 
cannot
+        # start Solr 10.
+        cmd = "start --runtime-jdk=21 --installation-id={} 
--test-run-id={}".format(self.installation_id, test_run_id)
         if solrorbit(self.cfg, cmd) != 0:
             raise AssertionError("Failed to start test cluster.")
         solr_client = client.ClientFactory(hosts=[{"host": "127.0.0.1", 
"port": self.http_port}], client_options={}).create()
-        client.wait_for_rest_layer(solr_client)
+        # client.wait_for_rest_layer was an OpenSearch helper; SolrClient 
carries its
+        # own readiness check.
+        solr_client.wait_for_cluster_ready()
 
     def stop(self):
         if self.installation_id:
@@ -205,23 +210,6 @@ class TestCluster:
         return f"TestCluster[installation-id={self.installation_id}]"
 
 
-class MetricsStore:
-    VERSION = "1.3.9"
-
-    def __init__(self):
-        self.cluster = TestCluster("in-memory-it")
-
-    def start(self):
-        self.cluster.install(distribution_version=MetricsStore.VERSION,
-                             node_name="metrics-store",
-                             cluster_config="defaults",
-                             http_port=10200)
-        self.cluster.start(test_run_id="metrics-store")
-
-    def stop(self):
-        self.cluster.stop()
-
-
 def install_integration_test_config():
     def copy_config(name):
         source_path = os.path.join(os.path.dirname(__file__), "resources", 
f"benchmark-{name}.ini")
@@ -237,9 +225,6 @@ def remove_integration_test_config():
         os.remove(config.ConfigFile(config_name).location)
 
 
-METRICS_STORE = MetricsStore()
-
-
 def get_license():
     with open(os.path.join(ROOT_DIR, 'LICENSE')) as license_file:
         return license_file.readlines()[1].strip()
@@ -263,9 +248,7 @@ def build_docker_image():
 def setup_module():
     check_prerequisites()
     install_integration_test_config()
-    METRICS_STORE.start()
 
 
 def teardown_module():
-    METRICS_STORE.stop()
     remove_integration_test_config()
diff --git a/it/distribution_test.py b/it/distribution_test.py
index 9030950d..95996e49 100644
--- a/it/distribution_test.py
+++ b/it/distribution_test.py
@@ -30,20 +30,18 @@ import it
 
 @it.random_benchmark_config
 def test_tar_distributions(cfg):
-    port = 19200
     for dist in it.DISTRIBUTIONS:
         for workload in it.WORKLOADS:
-            it.wait_until_port_is_free(port_number=port)
+            port = it.free_port()
             assert it.run_test(cfg, f"--distribution-version=\"{dist}\" 
--workload=\"{workload}\" "
-                                f"--test-mode --cluster-config=4gheap 
--target-hosts=127.0.0.1:{port}") == 0
+                                f"--test-mode --cluster-config=4gheap 
--solr-port={port}") == 0
 
 
 @it.random_benchmark_config
 def test_docker_distribution(cfg):
-    port = 19200
     # only test the most recent Docker distribution
     dist = it.DISTRIBUTIONS[-1]
-    it.wait_until_port_is_free(port_number=port)
+    port = it.free_port()
     assert it.run_test(cfg, f"--pipeline=\"docker\" 
--distribution-version=\"{dist}\" "
                         f"--workload=\"geonames\" 
--test-procedure=\"append-no-conflicts-index-only\" --test-mode "
-                        f"--cluster-config=4gheap 
--target-hosts=127.0.0.1:{port}") == 0
+                        f"--cluster-config=4gheap --solr-port={port}") == 0
diff --git a/it/info_test.py b/it/info_test.py
index 624922ba..dc02409b 100644
--- a/it/info_test.py
+++ b/it/info_test.py
@@ -47,10 +47,10 @@ def test_workload_info_with_task_filter(cfg):
 @it.benchmark_in_mem
 def test_workload_info_fails_with_wrong_workload_params(cfg):
     # simulate a typo in workload parameter
-    cmd = it.solrorbit_command_line_for(cfg, "info --workload=geonames 
--workload-params='conflict_probability:5,number-of-replicas:1'")
+    cmd = it.solrorbit_command_line_for(cfg, "info --workload=geonames 
--workload-params='conflict_probability:5,conflict_probabilty:1'")
     output = process.run_subprocess_with_output(cmd)
-    expected = "Some of your workload parameter(s) \"number-of-replicas\" are 
not used by this workload; " \
-               "perhaps you intend to use \"number_of_replicas\" 
instead.\n\nAll workload parameters you " \
-               "provided are:\n- conflict_probability\n- 
number-of-replicas\n\nAll parameters exposed by this workload"
+    expected = "Some of your workload parameter(s) \"conflict_probabilty\" are 
not used by this workload; " \
+               "perhaps you intend to use \"conflict_probability\" 
instead.\n\nAll workload parameters you " \
+               "provided are:\n- conflict_probability\n- 
conflict_probabilty\n\nAll parameters exposed by this workload"
 
     assert expected in "\n".join(output)
diff --git a/it/list_test.py b/it/list_test.py
index 4455f239..94573c46 100644
--- a/it/list_test.py
+++ b/it/list_test.py
@@ -43,8 +43,10 @@ def test_list_cluster_configs(cfg):
 @it.benchmark_in_mem
 def test_list_workloads(cfg):
     assert it.solrorbit(cfg, "list workloads") == 0
+    # The workloads repository's initial commit: a revision that exists and, 
short
+    # of a history rewrite, cannot stop existing.
     assert it.solrorbit(cfg, "list workloads --workload-repository=default "
-                           
"--workload-revision=cba4e45dda37ac03abbd3c9dd4532475dac355e9") == 0
+                           
"--workload-revision=89403d1fa7a78670fbf45710d97500414b231a66") == 0
 
 
 @it.benchmark_in_mem
diff --git a/it/sources_test.py b/it/sources_test.py
index dcc28012..213e8bc8 100644
--- a/it/sources_test.py
+++ b/it/sources_test.py
@@ -29,13 +29,16 @@ import it
 
 @it.random_benchmark_config
 def test_sources(cfg):
-    port = 19200
-    it.wait_until_port_is_free(port_number=port)
-    assert it.run_test(cfg, f"--pipeline=from-sources --revision=latest \
-                           --workload=geonames --test-mode  
--target-hosts=127.0.0.1:{port} "
+    port = it.free_port()
+    # analysis-extras is Solr's home for the ICU analysis components that the
+    # OpenSearch analysis-icu plugin provided, so this keeps the test's intent:
+    # build from source with an extra module enabled.
+    assert it.run_test(cfg, f"--pipeline=from-sources --revision=latest "
+                        f"--workload=geonames --test-mode "
                         f"--test-procedure=append-no-conflicts 
--cluster-config=4gheap "
-                        f"--opensearch-plugins=analysis-icu") == 0
+                        f"--solr-modules=analysis-extras --solr-port={port}") 
== 0
 
-    it.wait_until_port_is_free(port_number=port)
-    assert it.run_test(cfg, f"--pipeline=from-sources --workload=geonames 
--test-mode --target-hosts=127.0.0.1:{port} "
-                        f"--test-procedure=append-no-conflicts-index-only 
--cluster-config=\"4gheap,ea\"") == 0
+    port = it.free_port()
+    assert it.run_test(cfg, f"--pipeline=from-sources --workload=geonames 
--test-mode "
+                        f"--test-procedure=append-no-conflicts-index-only 
--cluster-config=\"4gheap,ea\" "
+                        f"--solr-port={port}") == 0
diff --git a/it/tracker_test.py b/it/tracker_test.py
index 994652e2..897339f0 100644
--- a/it/tracker_test.py
+++ b/it/tracker_test.py
@@ -37,14 +37,21 @@ def test_cluster():
     cluster = it.TestCluster("in-memory-it")
     # test with a recent distribution
     dist = it.DISTRIBUTIONS[-1]
-    port = 19200
     test_run_id = str(uuid.uuid4())
 
-    it.wait_until_port_is_free(port_number=port)
-    cluster.install(distribution_version=dist, node_name="benchmark-node", 
cluster_config="4gheap", http_port=port)
-    cluster.start(test_run_id=test_run_id)
-    yield cluster
-    cluster.stop()
+    cluster.install(distribution_version=dist, node_name="benchmark-node", 
cluster_config="4gheap",
+                    http_port=it.free_port())
+    try:
+        cluster.start(test_run_id=test_run_id)
+    except BaseException:
+        # Without this a failed start leaves a Solr node holding the port, and
+        # every later test that provisions Solr fails on "port already in use".
+        cluster.stop()
+        raise
+    try:
+        yield cluster
+    finally:
+        cluster.stop()
 
 
 @it.benchmark_in_mem
diff --git a/solrorbit/benchmark.py b/solrorbit/benchmark.py
index 89e92e2d..140ee8c1 100644
--- a/solrorbit/benchmark.py
+++ b/solrorbit/benchmark.py
@@ -595,6 +595,13 @@ def create_arg_parser():
         help="Comma-separated list of Solr modules to enable (sets 
SOLR_MODULES). "
              "Example: --solr-modules=analytics,extraction",
         default="")
+    test_run_parser.add_argument(
+        "--solr-port",
+        type=positive_number,
+        help="The HTTP port Solr should listen on for the pipelines that 
provision it "
+             "(docker, from-distribution, from-sources). Useful when the 
default is "
+             "already taken (default: 8983).",
+        default=None)
     test_run_parser.add_argument(
         "--plugin-params",
         help="Define a comma-separated list of key:value pairs that are 
injected verbatim to all plugins as variables.",
@@ -1086,6 +1093,9 @@ def configure_builder_params(args, cfg, 
command_requires_cluster_config=True):
         "cluster_config.params", opts.to_dict(
             args.cluster_config_params))
         cfg.add(config.Scope.applicationOverride, "solr", "modules", 
getattr(args, "solr_modules", ""))
+        solr_port = getattr(args, "solr_port", None)
+        if solr_port:
+            cfg.add(config.Scope.applicationOverride, "solr", "port", 
solr_port)
         pipeline = getattr(args, "pipeline", None)
         if pipeline == "benchmark-only" and args.cluster_config != "defaults":
             raise SystemExit(
diff --git a/solrorbit/builder/launcher.py b/solrorbit/builder/launcher.py
index a17e76c1..cc41ff4d 100644
--- a/solrorbit/builder/launcher.py
+++ b/solrorbit/builder/launcher.py
@@ -170,6 +170,9 @@ class ProcessLauncher:
 
         t = telemetry.Telemetry(enabled_devices, devices=node_telemetry)
         env = self._prepare_env(node_name, java_home, t)
+        # The port the node was provisioned for. Without this Solr starts on 
its
+        # own default and the pid-file lookup below looks in the wrong place.
+        env["SOLR_PORT"] = str(node_configuration.http_port)
         t.on_pre_node_start(node_name)
         # Get Solr version for version-specific startup command
         distribution_version = self.cfg.opts("builder", 
"distribution.version", mandatory=False)
@@ -235,6 +238,7 @@ class ProcessLauncher:
         # - Solr 10.x+: just "start" enables SolrCloud mode with embedded 
ZooKeeper by default
         # The bin/solr script handles daemonization and PID file creation
         cmd.append("start")
+        cmd += ["-p", env.get("SOLR_PORT", "8983")]
 
         # Determine if we need --cloud flag based on version
         if distribution_version:
diff --git a/solrorbit/builder/launchers/local_process_launcher.py 
b/solrorbit/builder/launchers/local_process_launcher.py
index 850cd23b..f6a2e330 100644
--- a/solrorbit/builder/launchers/local_process_launcher.py
+++ b/solrorbit/builder/launchers/local_process_launcher.py
@@ -68,6 +68,9 @@ class LocalProcessLauncher(Launcher):
 
         telemetry = self._prepare_telemetry(node_configuration, 
node_count_on_host, java_major_version)
         env = self._prepare_env(node_name, java_home, telemetry)
+        # The port the node was provisioned for. Without this Solr starts on 
its
+        # own default and the pid-file lookup below looks in the wrong place.
+        env["SOLR_PORT"] = str(node_configuration.http_port)
         telemetry.on_pre_node_start(node_name)
 
         node_pid = self._start_process(host, binary_path, env)
@@ -130,6 +133,7 @@ class LocalProcessLauncher(Launcher):
 
         cmd = [io.escape_path(os.path.join(binary_path, "bin", "solr"))]
         cmd.append("start")
+        cmd += ["-p", env.get("SOLR_PORT", "8983")]
 
         # Solr 9.x requires --cloud flag for SolrCloud mode; Solr 10+ uses it 
by default
         distribution_version = 
self.cluster_config.variables.get("distribution", {}).get("version")
diff --git a/solrorbit/builder/provisioner.py b/solrorbit/builder/provisioner.py
index 903be94e..c5390258 100644
--- a/solrorbit/builder/provisioner.py
+++ b/solrorbit/builder/provisioner.py
@@ -63,8 +63,14 @@ def docker(cfg, cluster_config, ip, http_port, target_root, 
node_name):
     return DockerProvisioner(cluster_config, node_name, ip, http_port, 
node_root_dir, distribution_version, benchmark_root)
 
 
+#: Solr's own default HTTP port, used when a node configuration predates the
+#: http-port field.
+DEFAULT_HTTP_PORT = 8983
+
+
 class NodeConfiguration:
-    def __init__(self, build_type, cluster_config_runtime_jdks, ip, node_name, 
node_root_path, binary_path, data_paths):
+    def __init__(self, build_type, cluster_config_runtime_jdks, ip, node_name, 
node_root_path, binary_path, data_paths,
+                 http_port=DEFAULT_HTTP_PORT):
         self.build_type = build_type
         self.cluster_config_runtime_jdks = cluster_config_runtime_jdks
         self.ip = ip
@@ -72,6 +78,9 @@ class NodeConfiguration:
         self.node_root_path = node_root_path
         self.binary_path = binary_path
         self.data_paths = data_paths
+        # The port the node was provisioned for. Without it the launcher cannot
+        # start Solr anywhere but its built-in default.
+        self.http_port = int(http_port)
 
     def as_dict(self):
         return {
@@ -81,14 +90,17 @@ class NodeConfiguration:
             "node-name": self.node_name,
             "node-root-path": self.node_root_path,
             "binary-path": self.binary_path,
-            "data-paths": self.data_paths
+            "data-paths": self.data_paths,
+            "http-port": self.http_port
         }
 
     @staticmethod
     def from_dict(d):
         return NodeConfiguration(
             d["build-type"], d["cluster-config-instance-runtime-jdks"], 
d["ip"],
-            d["node-name"], d["node-root-path"], d["binary-path"], 
d["data-paths"])
+            d["node-name"], d["node-root-path"], d["binary-path"], 
d["data-paths"],
+            # Tolerate a node-config.json written before this field existed.
+            d.get("http-port", DEFAULT_HTTP_PORT))
 
 
 def save_node_configuration(path, n):
@@ -195,7 +207,7 @@ class BareProvisioner:
         return NodeConfiguration("tar", 
self.os_installer.cluster_config.mandatory_var("runtime.jdk"),
                                  self.os_installer.node_ip, 
self.os_installer.node_name,
                                  self.os_installer.node_root_dir, 
self.os_installer.os_home_path,
-                                 self.os_installer.data_paths)
+                                 self.os_installer.data_paths, 
self.os_installer.http_port)
 
     def _provisioner_variables(self):
         provisioner_vars = {}
@@ -377,7 +389,8 @@ class DockerProvisioner:
             f.write(docker_cfg)
 
         return NodeConfiguration("docker", 
self.cluster_config.mandatory_var("runtime.jdk"),
-                                 self.node_ip, self.node_name, 
self.node_root_dir, self.binary_path, self.data_paths)
+                                 self.node_ip, self.node_name, 
self.node_root_dir, self.binary_path,
+                                 self.data_paths, self.http_port)
 
     def docker_vars(self, mounts):
         # Determine Docker image based on version type
diff --git a/solrorbit/workload/loader.py b/solrorbit/workload/loader.py
index 5e6a8e5e..88d8b43f 100644
--- a/solrorbit/workload/loader.py
+++ b/solrorbit/workload/loader.py
@@ -1399,17 +1399,22 @@ class WorkloadFileReader:
 
         unused_user_defined_workload_params = 
self.complete_workload_params.unused_user_defined_workload_params()
         if len(unused_user_defined_workload_params) > 0:
+            close_matches = sorted(opts.make_list_of_close_matches(
+                unused_user_defined_workload_params,
+                self.complete_workload_params.workload_defined_params))
+            # Only suggest alternatives when there are any. The suggestion 
carries its
+            # own punctuation, so without it the sentence still ends in a full 
stop
+            # rather than "...by this workload; perhaps you intend to use  
instead."
+            suggestion = "; perhaps you intend to use {} instead.".format(
+                ",".join(opts.double_quoted_list_of(close_matches))) if 
close_matches else "."
             err_msg = (
-                "Some of your workload parameter(s) {} are not used by this 
workload; perhaps you intend to use {} instead.\n\n"
+                "Some of your workload parameter(s) {} are not used by this 
workload{}\n\n"
                 "All workload parameters you provided are:\n"
                 "{}\n\n"
                 "All parameters exposed by this workload:\n"
                 "{}".format(
                     
",".join(opts.double_quoted_list_of(sorted(unused_user_defined_workload_params))),
-                    
",".join(opts.double_quoted_list_of(sorted(opts.make_list_of_close_matches(
-                        unused_user_defined_workload_params,
-                        self.complete_workload_params.workload_defined_params
-                    )))),
+                    suggestion,
                     
"\n".join(opts.bulleted_list_of(sorted(list(self.workload_params.keys())))),
                     
"\n".join(opts.bulleted_list_of(self.complete_workload_params.sorted_workload_defined_params))))
 

Reply via email to