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))))