From afde4bed9eb846701603b9d4cde44080122b9fec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 07:17:08 +0000 Subject: [PATCH 01/12] tests: support NVME_BIN env var and nvme-bin meson option for e2e tests Add two mechanisms to run end-to-end tests against a binary other than the one built from the current source tree: 1. NVME_BIN environment variable (highest priority): NVME_BIN=/usr/bin/nvme python3 tests/tap_runner.py ... 2. -Dnvme-bin meson option (for meson-driven runs): meson setup .build -Dnvme-bin=/usr/bin/nvme meson test -C .build The NVME_BIN env var is also explicitly set by meson when running tests, pointing to the build-tree binary by default (or the -Dnvme-bin value when specified). This makes the binary selection deterministic regardless of PATH. Priority order: NVME_BIN env var > config.json nvme_bin > build-tree binary Update the README to document all three selection mechanisms. --- meson_options.txt | 6 ++++++ tests/README | 18 ++++++++++++++++++ tests/meson.build | 17 ++++++++++++++++- tests/nvme_test.py | 10 ++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/meson_options.txt b/meson_options.txt index f1978d17df..53ec80b0e8 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -77,6 +77,12 @@ option( value : false, description: 'Run tests against real hardware' ) +option( + 'nvme-bin', + type : 'string', + value : '', + description: 'Path to nvme binary used for end-to-end tests (default: use the binary from the build tree)' +) option( 'tests', type : 'boolean', diff --git a/tests/README b/tests/README index 1ca4bd6db3..a4c6d2fb75 100644 --- a/tests/README +++ b/tests/README @@ -49,3 +49,21 @@ Running testcases with framework 2. Running all the testcases (in the build root directory) :- $ meson test -C .build + +Selecting the nvme binary +-------------------------- + By default the tests use the nvme binary built in the current build tree. + To test against a different version (e.g. a distribution-provided binary) + two mechanisms are available: + + a. Environment variable (highest priority, overrides config.json): + $ NVME_BIN=/usr/bin/nvme python3 tests/tap_runner.py --start-dir tests nvme_id_ctrl_test + + b. Meson option (for meson-driven test runs): + $ meson setup .build -Dnvme-bin=/usr/bin/nvme + $ meson test -C .build + + c. config.json entry (lower priority than NVME_BIN): + Add "nvme_bin": "/usr/bin/nvme" to tests/config.json. + + Priority order: NVME_BIN env var > config.json nvme_bin > build-tree binary diff --git a/tests/meson.build b/tests/meson.build index 12c1501920..4ba987adac 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -41,6 +41,18 @@ python_module = import('python') python = python_module.find_installation('python3') +# Determine which nvme binary to expose to the end-to-end tests. +# The -Dnvme-bin option lets callers point at a distribution-provided (or +# any other) binary without touching config.json: +# meson setup .build -Dnvme-bin=/usr/bin/nvme +# When the option is not set the binary produced by the current build is used. +nvme_bin_opt = get_option('nvme-bin') +if nvme_bin_opt != '' + nvme_bin_for_test = nvme_bin_opt +else + nvme_bin_for_test = meson.project_build_root() / 'nvme' +endif + foreach t : tests t_name = t.split('.')[0] test( @@ -51,7 +63,10 @@ foreach t : tests '--start-dir', meson.current_source_dir(), t_name, ], - env: ['PATH=' + meson.project_build_root() + ':/usr/bin:/usr/sbin'], + env: [ + 'PATH=' + meson.project_build_root() + ':/usr/bin:/usr/sbin', + 'NVME_BIN=' + nvme_bin_for_test, + ], timeout: 500, protocol: 'tap', is_parallel: false, diff --git a/tests/nvme_test.py b/tests/nvme_test.py index 98f6b2daea..b5bc3b09d3 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -168,6 +168,16 @@ def load_config(self): if not logging.getLogger().handlers: logging.basicConfig(format='%(message)s', stream=sys.stdout) logging.getLogger().setLevel(log_level) + + # Environment variable takes the highest priority, overriding both + # the default and any value from config.json. This allows running + # the tests against a distribution-provided (or any other) binary + # without modifying config.json: + # NVME_BIN=/usr/bin/nvme python3 tests/tap_runner.py ... + env_nvme_bin = os.environ.get('NVME_BIN') + if env_nvme_bin: + self.nvme_bin = env_nvme_bin + logger.debug("Using nvme binary '%s'", self.nvme_bin) if self.clear_log_dir is True: From 3ce939b7d1cd1ba349b6f2b0ce32519bd82a49b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 07:30:59 +0000 Subject: [PATCH 02/12] Handle legacy create-ns output in e2e tests --- tests/nvme_test.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/nvme_test.py b/tests/nvme_test.py index b5bc3b09d3..cdf09a7687 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -493,6 +493,22 @@ def create_ns(self, nsze, ncap, flbas, dps): result = self.run_cmd(create_ns_cmd) return result.returncode, result.stdout + def _get_created_nsid(self, stdout): + """Extract the namespace id from create-ns output.""" + try: + json_output = json.loads(stdout) + except json.JSONDecodeError: + match = re.search(r"created nsid:\s*(\d+)", stdout) + self.assertIsNotNone( + match, + f"ERROR : expected create-ns output with nsid, got: {stdout!r}", + ) + return int(match.group(1)) + + self.assertIn('nsid', json_output, + f"ERROR : unexpected create-ns JSON output: {json_output}") + return int(json_output['nsid']) + def create_and_validate_ns(self, nsid, nsze, ncap, flbas, dps): """ Wrapper for creating and validating a namespace. - Args: @@ -506,8 +522,8 @@ def create_and_validate_ns(self, nsid, nsze, ncap, flbas, dps): """ err, stdout = self.create_ns(nsze, ncap, flbas, dps) if err == 0: - json_output = json.loads(stdout) - self.assertEqual(int(json_output['nsid']), nsid, + created_nsid = self._get_created_nsid(stdout) + self.assertEqual(created_nsid, nsid, "ERROR : create namespace failed") id_ns_cmd = f"{self.nvme_bin} id-ns {self.ctrl} " + \ f"--namespace-id={str(nsid)}" From a3a0cd3463c8c841d92a135b52b2325da513f94b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 07:31:41 +0000 Subject: [PATCH 03/12] tests: accept legacy create-ns success output --- tests/nvme_test.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/nvme_test.py b/tests/nvme_test.py index cdf09a7687..cce169cf24 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -501,12 +501,16 @@ def _get_created_nsid(self, stdout): match = re.search(r"created nsid:\s*(\d+)", stdout) self.assertIsNotNone( match, - f"ERROR : expected create-ns output with nsid, got: {stdout!r}", + "ERROR : expected create-ns output with nsid, " + f"got: {stdout!r}", ) return int(match.group(1)) - self.assertIn('nsid', json_output, - f"ERROR : unexpected create-ns JSON output: {json_output}") + self.assertIn( + 'nsid', + json_output, + f"ERROR : unexpected create-ns JSON output: {json_output}", + ) return int(json_output['nsid']) def create_and_validate_ns(self, nsid, nsze, ncap, flbas, dps): From 65cb0eac2c96c8fe1350ba1434d3e6f18cd5e941 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 07:32:22 +0000 Subject: [PATCH 04/12] tests: match legacy create-ns output case-insensitively --- tests/nvme_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/nvme_test.py b/tests/nvme_test.py index cce169cf24..96398c0507 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -498,7 +498,9 @@ def _get_created_nsid(self, stdout): try: json_output = json.loads(stdout) except json.JSONDecodeError: - match = re.search(r"created nsid:\s*(\d+)", stdout) + match = re.search( + r"created nsid:\s*(\d+)", stdout, re.IGNORECASE, + ) self.assertIsNotNone( match, "ERROR : expected create-ns output with nsid, " From 01f7caf107883009972b9fdae8389bfd93919f83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:45:10 +0000 Subject: [PATCH 05/12] Harden JSON parsing in Python tests --- tests/nvme_copy_test.py | 13 +++--- tests/nvme_format_test.py | 11 +++-- tests/nvme_test.py | 93 +++++++++++++++++++++++++++++++-------- 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/tests/nvme_copy_test.py b/tests/nvme_copy_test.py index f63df24647..575fc6bf9f 100644 --- a/tests/nvme_copy_test.py +++ b/tests/nvme_copy_test.py @@ -22,8 +22,6 @@ """ -import json - from nvme_test import TestNVMe, to_decimal @@ -101,14 +99,16 @@ def _get_current_ns_pif(self): result = self.run_cmd(id_ns_cmd) if result.returncode != 0: return 0 - flbas = int(json.loads(result.stdout).get("flbas", 0)) + id_ns_data = self.parse_json_output(result.stdout, "nvme id-ns") + flbas = int(id_ns_data.get("flbas", 0)) lbaf_idx = (flbas & 0xF) | (((flbas >> 5) & 0x3) << 4) nvm_id_ns_cmd = f"{self.nvme_bin} nvm-id-ns {self.ns1} --output-format=json" result = self.run_cmd(nvm_id_ns_cmd) if result.returncode != 0: return 0 - elbafs = json.loads(result.stdout).get("elbafs", []) + nvm_id_ns_data = self.parse_json_output(result.stdout, "nvme nvm-id-ns") + elbafs = nvm_id_ns_data.get("elbafs", []) if lbaf_idx < len(elbafs): return elbafs[lbaf_idx].get("pif", 0) return 0 @@ -144,7 +144,8 @@ def _find_64b_guard_lbaf_index(self): result = self.run_cmd(nvm_id_ns_cmd) if result.returncode != 0: return None - elbafs = json.loads(result.stdout).get("elbafs", []) + nvm_id_ns_data = self.parse_json_output(result.stdout, "nvme nvm-id-ns") + elbafs = nvm_id_ns_data.get("elbafs", []) for i, elbaf in enumerate(elbafs): if elbaf.get("pif", 0) == 2: # NVME_NVM_PIF_64B_GUARD = 2 return i @@ -215,7 +216,7 @@ def _enable_cdfe_for_format(self, desc_format): result = self.run_cmd(get_features_cmd) self.assertEqual(result.returncode, 0, "ERROR : nvme feat host-behavior-support failed") - data = json.loads(result.stdout) + data = self.parse_json_output(result.stdout, "nvme feat host-behavior-support") fields = data.get("Feature: 0x16", [{}])[0] current_cdfe = ( (0x4 if fields.get("Copy Descriptor Format 2h Enable (CDF2E)") == "True" else 0) | diff --git a/tests/nvme_format_test.py b/tests/nvme_format_test.py index 82d31731fb..8148e82694 100644 --- a/tests/nvme_format_test.py +++ b/tests/nvme_format_test.py @@ -37,7 +37,6 @@ - Delete Namespace. """ -import json import logging import math @@ -111,8 +110,8 @@ def attach_detach_primary_ns(self): f"--namespace-id={self.default_nsid} --output-format=json" result = self.run_cmd(id_ns_cmd) self.assertEqual(result.returncode, 0, "ERROR : nvme id-ns failed") - json_output = json.loads(result.stdout) - self.lba_format_list = json_output['lbafs'] + json_output = self.parse_json_output(result.stdout, "nvme id-ns") + self.lba_format_list = json_output.get('lbafs', []) self.assertTrue(len(self.lba_format_list) > 0, "ERROR : nvme id-ns could not find any lba formats") self.assertEqual(self.detach_ns(self.ctrl_id, self.default_nsid), 0) @@ -127,6 +126,12 @@ def test_format_ns(self): print("##### Testing lba formats:") # iterate through all supported format for flbas, lba_format in enumerate(self.lba_format_list): + self.assertIsInstance(lba_format, dict, + f"ERROR : invalid lba format entry: {lba_format!r}") + self.assertIn('ds', lba_format, + f"ERROR : lba format entry missing ds: {lba_format!r}") + self.assertIn('ms', lba_format, + f"ERROR : lba format entry missing ms: {lba_format!r}") ds = lba_format['ds'] ms = lba_format['ms'] print(f"\nlba format {str(flbas)}" diff --git a/tests/nvme_test.py b/tests/nvme_test.py index 96398c0507..e39ea32a2e 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -218,6 +218,28 @@ def run_cmd(self, cmd, stdin_data=None): logger.debug(result.stderr) return result + def parse_json_output(self, output, context, expected_type=dict): + """Parse JSON output and fail the testcase gracefully on malformed data.""" + try: + data = json.loads(output) + except (TypeError, json.JSONDecodeError) as exc: + self.fail(f"ERROR : invalid JSON from {context}: {exc}; output={output!r}") + + if expected_type is not None and not isinstance(data, expected_type): + self.fail( + "ERROR : unexpected JSON type from " + f"{context}: expected {expected_type.__name__}, got {type(data).__name__}" + ) + return data + + def json_get(self, data, key, default=None, context="JSON output"): + """Safely read a key from a JSON object, failing gracefully if shape is wrong.""" + if not isinstance(data, dict): + self.fail( + f"ERROR : expected JSON object for {context}, got {type(data).__name__}" + ) + return data.get(key, default) + def exec_cmd(self, cmd): """ Wrapper for executing a shell command and return the result. """ return self.run_cmd(cmd).returncode @@ -247,10 +269,18 @@ def get_ctrl_id(self): "--output-format=json" result = self.run_cmd(get_ctrl_id) self.assertEqual(result.returncode, 0, "ERROR : nvme list-ctrl failed") - json_output = json.loads(result.stdout) - self.assertTrue(len(json_output['ctrl_list']) > 0, + json_output = self.parse_json_output(result.stdout, "nvme list-ctrl") + ctrl_list = self.json_get(json_output, 'ctrl_list', [], "nvme list-ctrl") + self.assertIsInstance(ctrl_list, list, + "ERROR : nvme list-ctrl returned invalid ctrl_list type") + self.assertTrue(len(ctrl_list) > 0, "ERROR : nvme list-ctrl could not find ctrl") - return str(json_output['ctrl_list'][0]['ctrl_id']) + first_ctrl = ctrl_list[0] if ctrl_list else {} + self.assertIsInstance(first_ctrl, dict, + "ERROR : nvme list-ctrl returned invalid controller entry") + self.assertIn('ctrl_id', first_ctrl, + f"ERROR : nvme list-ctrl missing ctrl_id: {first_ctrl!r}") + return str(first_ctrl['ctrl_id']) def get_ns_mgmt_support(self): """ @@ -288,9 +318,16 @@ def get_nsid_list(self): "--output-format=json" result = self.run_cmd(ns_list_cmd) self.assertEqual(result.returncode, 0, "ERROR : nvme list namespace failed") - json_output = json.loads(result.stdout) - - for ns in json_output['nsid_list']: + json_output = self.parse_json_output(result.stdout, "nvme list-ns") + + nsid_list = self.json_get(json_output, 'nsid_list', [], "nvme list-ns") + self.assertIsInstance(nsid_list, list, + "ERROR : nvme list-ns returned invalid nsid_list type") + for ns in nsid_list: + self.assertIsInstance(ns, dict, + f"ERROR : nvme list-ns returned invalid namespace entry: {ns!r}") + self.assertIn('nsid', ns, + f"ERROR : nvme list-ns entry missing nsid: {ns!r}") ns_list.append(ns['nsid']) return ns_list @@ -306,8 +343,10 @@ def get_max_ns(self): "--output-format=json" result = self.run_cmd(max_ns_cmd) self.assertEqual(result.returncode, 0, "ERROR : reading maximum namespace count failed") - json_output = json.loads(result.stdout) - return int(json_output['nn']) + json_output = self.parse_json_output(result.stdout, "nvme id-ctrl") + nn = self.json_get(json_output, 'nn', None, "nvme id-ctrl") + self.assertIsNotNone(nn, "ERROR : reading maximum namespace count failed") + return int(nn) def get_lba_status_supported(self): """ Check if 'Get LBA Status' command is supported by the device @@ -330,9 +369,13 @@ def _get_active_lbaf_index(self): "--output-format=json" result = self.run_cmd(nvme_id_ns_cmd) self.assertEqual(result.returncode, 0, "ERROR : reading id-ns") - json_output = json.loads(result.stdout) + json_output = self.parse_json_output(result.stdout, "nvme id-ns") for lbaf in json_output.get('lbafs', []): + if not isinstance(lbaf, dict): + continue if lbaf.get('in_use') == 1: + self.assertIn('lbaf', lbaf, + f"ERROR : id-ns lbaf entry missing lbaf index: {lbaf!r}") return int(lbaf['lbaf']) return 0 @@ -349,7 +392,7 @@ def _get_ns_dps(self): "--output-format=json" result = self.run_cmd(nvme_id_ns_cmd) self.assertEqual(result.returncode, 0, "ERROR : reading id-ns") - json_output = json.loads(result.stdout) + json_output = self.parse_json_output(result.stdout, "nvme id-ns") return int(json_output.get('dps', 0)) def _get_pif(self): @@ -371,7 +414,7 @@ def _get_pif(self): "--output-format=json" result = self.run_cmd(nvme_id_ns_cmd) self.assertEqual(result.returncode, 0, "ERROR : reading id-ns") - json_output = json.loads(result.stdout) + json_output = self.parse_json_output(result.stdout, "nvme id-ns") dps = int(json_output.get('dps', 0)) return (dps >> 3) & 0x7 @@ -385,7 +428,7 @@ def _is_metadata_ext(self): "--output-format=json" result = self.run_cmd(nvme_id_ns_cmd) self.assertEqual(result.returncode, 0, "ERROR : reading id-ns") - json_output = json.loads(result.stdout) + json_output = self.parse_json_output(result.stdout, "nvme id-ns") flbas = int(json_output.get('flbas', 0)) return bool(flbas & (1 << 4)) @@ -400,10 +443,17 @@ def get_lba_format_size(self): "--output-format=json" result = self.run_cmd(nvme_id_ns_cmd) self.assertEqual(result.returncode, 0, "ERROR : reading id-ns") - json_output = json.loads(result.stdout) - self.assertTrue(len(json_output['lbafs']) > self.flbas, + json_output = self.parse_json_output(result.stdout, "nvme id-ns") + lbafs = self.json_get(json_output, 'lbafs', [], "nvme id-ns") + self.assertIsInstance(lbafs, list, + "Error : id-ns returned invalid lbafs type") + self.assertTrue(len(lbafs) > self.flbas, "Error : could not match the given flbas to an existing lbaf") - lbaf_json = json_output['lbafs'][int(self.flbas)] + lbaf_json = lbafs[int(self.flbas)] + self.assertIsInstance(lbaf_json, dict, + "Error : id-ns returned invalid lbaf entry") + self.assertIn('ms', lbaf_json, "Error : id-ns lbaf missing 'ms'") + self.assertIn('ds', lbaf_json, "Error : id-ns lbaf missing 'ds'") ms = int(lbaf_json['ms']) ds_expo = int(lbaf_json['ds']) ds = (1 << ds_expo) if ds_expo > 0 else 0 @@ -429,7 +479,7 @@ def get_id_ctrl_field_value(self, field): "--output-format=json" result = self.run_cmd(id_ctrl_cmd) self.assertEqual(result.returncode, 0, "ERROR : reading id-ctrl failed") - json_output = json.loads(result.stdout) + json_output = self.parse_json_output(result.stdout, "nvme id-ctrl") self.assertTrue(field in json_output, f"ERROR : reading field '{field}' failed") return str(json_output[field]) @@ -445,7 +495,7 @@ def get_id_ns_field_value(self, field): "--output-format=json" result = self.run_cmd(id_ns_cmd) self.assertEqual(result.returncode, 0, "ERROR : reading id-ns failed") - json_output = json.loads(result.stdout) + json_output = self.parse_json_output(result.stdout, "nvme id-ns") self.assertTrue(field in json_output, f"ERROR : reading field '{field}' failed") return str(json_output[field]) @@ -473,8 +523,11 @@ def delete_all_ns(self): "--output-format=json" result = self.run_cmd(list_ns_cmd) self.assertEqual(result.returncode, 0, "ERROR : nvme list-ns failed") - json_output = json.loads(result.stdout) - self.assertEqual(len(json_output['nsid_list']), 0, + json_output = self.parse_json_output(result.stdout, "nvme list-ns") + nsid_list = self.json_get(json_output, 'nsid_list', [], "nvme list-ns") + self.assertIsInstance(nsid_list, list, + "ERROR : nvme list-ns returned invalid nsid_list type") + self.assertEqual(len(nsid_list), 0, "ERROR : deleting all namespace failed") def create_ns(self, nsze, ncap, flbas, dps): @@ -508,6 +561,8 @@ def _get_created_nsid(self, stdout): ) return int(match.group(1)) + self.assertIsInstance(json_output, dict, + f"ERROR : unexpected create-ns JSON output type: {type(json_output).__name__}") self.assertIn( 'nsid', json_output, From 26e13fcb19d12b51ff22d47bf4cedaea21f93148 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:45:46 +0000 Subject: [PATCH 06/12] Address review nits in JSON test hardening --- tests/nvme_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/nvme_test.py b/tests/nvme_test.py index e39ea32a2e..9bddb8ae94 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -275,7 +275,7 @@ def get_ctrl_id(self): "ERROR : nvme list-ctrl returned invalid ctrl_list type") self.assertTrue(len(ctrl_list) > 0, "ERROR : nvme list-ctrl could not find ctrl") - first_ctrl = ctrl_list[0] if ctrl_list else {} + first_ctrl = ctrl_list[0] self.assertIsInstance(first_ctrl, dict, "ERROR : nvme list-ctrl returned invalid controller entry") self.assertIn('ctrl_id', first_ctrl, @@ -552,7 +552,7 @@ def _get_created_nsid(self, stdout): json_output = json.loads(stdout) except json.JSONDecodeError: match = re.search( - r"created nsid:\s*(\d+)", stdout, re.IGNORECASE, + r"created nsid:\s*(\d+)", stdout, re.IGNORECASE ) self.assertIsNotNone( match, From 23b92e43568d3baba5d2ae2fe916caf652c4b377 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:46:27 +0000 Subject: [PATCH 07/12] Improve JSON helper docs in tests --- tests/nvme_test.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/nvme_test.py b/tests/nvme_test.py index 9bddb8ae94..69691fee79 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -219,7 +219,10 @@ def run_cmd(self, cmd, stdin_data=None): return result def parse_json_output(self, output, context, expected_type=dict): - """Parse JSON output and fail the testcase gracefully on malformed data.""" + """Parse JSON output and fail test clearly on malformed or wrong-typed data. + + Pass expected_type=None to skip type validation. + """ try: data = json.loads(output) except (TypeError, json.JSONDecodeError) as exc: @@ -233,7 +236,7 @@ def parse_json_output(self, output, context, expected_type=dict): return data def json_get(self, data, key, default=None, context="JSON output"): - """Safely read a key from a JSON object, failing gracefully if shape is wrong.""" + """Return key from a JSON object, with default and graceful shape validation.""" if not isinstance(data, dict): self.fail( f"ERROR : expected JSON object for {context}, got {type(data).__name__}" @@ -547,7 +550,7 @@ def create_ns(self, nsze, ncap, flbas, dps): return result.returncode, result.stdout def _get_created_nsid(self, stdout): - """Extract the namespace id from create-ns output.""" + """Extract namespace id from create-ns output (JSON or legacy text).""" try: json_output = json.loads(stdout) except json.JSONDecodeError: @@ -561,8 +564,11 @@ def _get_created_nsid(self, stdout): ) return int(match.group(1)) - self.assertIsInstance(json_output, dict, - f"ERROR : unexpected create-ns JSON output type: {type(json_output).__name__}") + self.assertIsInstance( + json_output, + dict, + f"ERROR : unexpected create-ns JSON output type: {type(json_output).__name__}", + ) self.assertIn( 'nsid', json_output, From 754af2a6ab58341eef4b539beb1928aa1fdd8242 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:47:11 +0000 Subject: [PATCH 08/12] Clarify JSON assertion failure messages --- tests/nvme_format_test.py | 2 +- tests/nvme_test.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/nvme_format_test.py b/tests/nvme_format_test.py index 8148e82694..8e8bd636d2 100644 --- a/tests/nvme_format_test.py +++ b/tests/nvme_format_test.py @@ -127,7 +127,7 @@ def test_format_ns(self): # iterate through all supported format for flbas, lba_format in enumerate(self.lba_format_list): self.assertIsInstance(lba_format, dict, - f"ERROR : invalid lba format entry: {lba_format!r}") + f"ERROR : lba format entry must be dict, got {type(lba_format).__name__}: {lba_format!r}") self.assertIn('ds', lba_format, f"ERROR : lba format entry missing ds: {lba_format!r}") self.assertIn('ms', lba_format, diff --git a/tests/nvme_test.py b/tests/nvme_test.py index 69691fee79..3ab6290f9e 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -449,12 +449,12 @@ def get_lba_format_size(self): json_output = self.parse_json_output(result.stdout, "nvme id-ns") lbafs = self.json_get(json_output, 'lbafs', [], "nvme id-ns") self.assertIsInstance(lbafs, list, - "Error : id-ns returned invalid lbafs type") + f"Error : id-ns returned invalid lbafs type, expected list, got {type(lbafs).__name__}") self.assertTrue(len(lbafs) > self.flbas, "Error : could not match the given flbas to an existing lbaf") lbaf_json = lbafs[int(self.flbas)] self.assertIsInstance(lbaf_json, dict, - "Error : id-ns returned invalid lbaf entry") + f"Error : id-ns returned invalid lbaf entry, expected dict, got {type(lbaf_json).__name__}") self.assertIn('ms', lbaf_json, "Error : id-ns lbaf missing 'ms'") self.assertIn('ds', lbaf_json, "Error : id-ns lbaf missing 'ds'") ms = int(lbaf_json['ms']) @@ -572,7 +572,7 @@ def _get_created_nsid(self, stdout): self.assertIn( 'nsid', json_output, - f"ERROR : unexpected create-ns JSON output: {json_output}", + f"ERROR : create-ns JSON output missing nsid field: {json_output}", ) return int(json_output['nsid']) From 082a420fdbb1a8cf3ea5a141c043fb8e5c2cb559 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:47:59 +0000 Subject: [PATCH 09/12] Refine JSON helper consistency in tests --- tests/nvme_test.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/nvme_test.py b/tests/nvme_test.py index 3ab6290f9e..bcca202802 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -221,6 +221,7 @@ def run_cmd(self, cmd, stdin_data=None): def parse_json_output(self, output, context, expected_type=dict): """Parse JSON output and fail test clearly on malformed or wrong-typed data. + context should identify the command/action that produced output. Pass expected_type=None to skip type validation. """ try: @@ -236,7 +237,7 @@ def parse_json_output(self, output, context, expected_type=dict): return data def json_get(self, data, key, default=None, context="JSON output"): - """Return key from a JSON object, with default and graceful shape validation.""" + """Return key from a JSON object and fail the test if data is not a dict.""" if not isinstance(data, dict): self.fail( f"ERROR : expected JSON object for {context}, got {type(data).__name__}" @@ -374,8 +375,8 @@ def _get_active_lbaf_index(self): self.assertEqual(result.returncode, 0, "ERROR : reading id-ns") json_output = self.parse_json_output(result.stdout, "nvme id-ns") for lbaf in json_output.get('lbafs', []): - if not isinstance(lbaf, dict): - continue + self.assertIsInstance(lbaf, dict, + f"ERROR : id-ns returned invalid lbaf entry: {lbaf!r}") if lbaf.get('in_use') == 1: self.assertIn('lbaf', lbaf, f"ERROR : id-ns lbaf entry missing lbaf index: {lbaf!r}") @@ -449,12 +450,12 @@ def get_lba_format_size(self): json_output = self.parse_json_output(result.stdout, "nvme id-ns") lbafs = self.json_get(json_output, 'lbafs', [], "nvme id-ns") self.assertIsInstance(lbafs, list, - f"Error : id-ns returned invalid lbafs type, expected list, got {type(lbafs).__name__}") + f"ERROR : id-ns returned invalid lbafs type, expected list, got {type(lbafs).__name__}") self.assertTrue(len(lbafs) > self.flbas, "Error : could not match the given flbas to an existing lbaf") lbaf_json = lbafs[int(self.flbas)] self.assertIsInstance(lbaf_json, dict, - f"Error : id-ns returned invalid lbaf entry, expected dict, got {type(lbaf_json).__name__}") + f"ERROR : id-ns returned invalid lbaf entry, expected dict, got {type(lbaf_json).__name__}") self.assertIn('ms', lbaf_json, "Error : id-ns lbaf missing 'ms'") self.assertIn('ds', lbaf_json, "Error : id-ns lbaf missing 'ds'") ms = int(lbaf_json['ms']) From 2b1207a4df2ea6a8221d9c0e666dab06c8255f27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:49:04 +0000 Subject: [PATCH 10/12] Strengthen JSON shape checks in tests --- tests/nvme_copy_test.py | 8 ++++++++ tests/nvme_format_test.py | 2 ++ tests/nvme_test.py | 7 ++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/nvme_copy_test.py b/tests/nvme_copy_test.py index 575fc6bf9f..415efc6315 100644 --- a/tests/nvme_copy_test.py +++ b/tests/nvme_copy_test.py @@ -109,7 +109,11 @@ def _get_current_ns_pif(self): return 0 nvm_id_ns_data = self.parse_json_output(result.stdout, "nvme nvm-id-ns") elbafs = nvm_id_ns_data.get("elbafs", []) + self.assertIsInstance(elbafs, list, + f"ERROR : nvm-id-ns returned invalid elbafs type: {type(elbafs).__name__}") if lbaf_idx < len(elbafs): + self.assertIsInstance(elbafs[lbaf_idx], dict, + f"ERROR : invalid elbaf entry: {elbafs[lbaf_idx]!r}") return elbafs[lbaf_idx].get("pif", 0) return 0 @@ -146,7 +150,11 @@ def _find_64b_guard_lbaf_index(self): return None nvm_id_ns_data = self.parse_json_output(result.stdout, "nvme nvm-id-ns") elbafs = nvm_id_ns_data.get("elbafs", []) + self.assertIsInstance(elbafs, list, + f"ERROR : nvm-id-ns returned invalid elbafs type: {type(elbafs).__name__}") for i, elbaf in enumerate(elbafs): + self.assertIsInstance(elbaf, dict, + f"ERROR : invalid elbaf entry: {elbaf!r}") if elbaf.get("pif", 0) == 2: # NVME_NVM_PIF_64B_GUARD = 2 return i return None diff --git a/tests/nvme_format_test.py b/tests/nvme_format_test.py index 8e8bd636d2..1f32f45d4a 100644 --- a/tests/nvme_format_test.py +++ b/tests/nvme_format_test.py @@ -112,6 +112,8 @@ def attach_detach_primary_ns(self): self.assertEqual(result.returncode, 0, "ERROR : nvme id-ns failed") json_output = self.parse_json_output(result.stdout, "nvme id-ns") self.lba_format_list = json_output.get('lbafs', []) + self.assertIsInstance(self.lba_format_list, list, + f"ERROR : lbafs must be a list, got {type(self.lba_format_list).__name__}") self.assertTrue(len(self.lba_format_list) > 0, "ERROR : nvme id-ns could not find any lba formats") self.assertEqual(self.detach_ns(self.ctrl_id, self.default_nsid), 0) diff --git a/tests/nvme_test.py b/tests/nvme_test.py index bcca202802..a9d17cd709 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -551,7 +551,12 @@ def create_ns(self, nsze, ncap, flbas, dps): return result.returncode, result.stdout def _get_created_nsid(self, stdout): - """Extract namespace id from create-ns output (JSON or legacy text).""" + """Extract namespace id from create-ns output (JSON or legacy text). + + Returns: + int: Namespace ID from `{"nsid": ...}` JSON output or from + legacy text output like "created nsid: X". + """ try: json_output = json.loads(stdout) except json.JSONDecodeError: From 70d92b95dbde8b91c8f4273b2250f796dc1c99c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:50:02 +0000 Subject: [PATCH 11/12] Require critical JSON keys in test helpers --- tests/nvme_test.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/nvme_test.py b/tests/nvme_test.py index a9d17cd709..c4d2edd697 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -236,12 +236,14 @@ def parse_json_output(self, output, context, expected_type=dict): ) return data - def json_get(self, data, key, default=None, context="JSON output"): - """Return key from a JSON object and fail the test if data is not a dict.""" + def json_get(self, data, key, default=None, context="JSON output", required=False): + """Return key from JSON dict and optionally fail if key is missing.""" if not isinstance(data, dict): self.fail( f"ERROR : expected JSON object for {context}, got {type(data).__name__}" ) + if required and key not in data: + self.fail(f"ERROR : missing key '{key}' in {context}: {data!r}") return data.get(key, default) def exec_cmd(self, cmd): @@ -274,7 +276,7 @@ def get_ctrl_id(self): result = self.run_cmd(get_ctrl_id) self.assertEqual(result.returncode, 0, "ERROR : nvme list-ctrl failed") json_output = self.parse_json_output(result.stdout, "nvme list-ctrl") - ctrl_list = self.json_get(json_output, 'ctrl_list', [], "nvme list-ctrl") + ctrl_list = self.json_get(json_output, 'ctrl_list', context="nvme list-ctrl", required=True) self.assertIsInstance(ctrl_list, list, "ERROR : nvme list-ctrl returned invalid ctrl_list type") self.assertTrue(len(ctrl_list) > 0, @@ -324,7 +326,7 @@ def get_nsid_list(self): self.assertEqual(result.returncode, 0, "ERROR : nvme list namespace failed") json_output = self.parse_json_output(result.stdout, "nvme list-ns") - nsid_list = self.json_get(json_output, 'nsid_list', [], "nvme list-ns") + nsid_list = self.json_get(json_output, 'nsid_list', context="nvme list-ns", required=True) self.assertIsInstance(nsid_list, list, "ERROR : nvme list-ns returned invalid nsid_list type") for ns in nsid_list: @@ -348,7 +350,7 @@ def get_max_ns(self): result = self.run_cmd(max_ns_cmd) self.assertEqual(result.returncode, 0, "ERROR : reading maximum namespace count failed") json_output = self.parse_json_output(result.stdout, "nvme id-ctrl") - nn = self.json_get(json_output, 'nn', None, "nvme id-ctrl") + nn = self.json_get(json_output, 'nn', context="nvme id-ctrl", required=True) self.assertIsNotNone(nn, "ERROR : reading maximum namespace count failed") return int(nn) @@ -448,7 +450,7 @@ def get_lba_format_size(self): result = self.run_cmd(nvme_id_ns_cmd) self.assertEqual(result.returncode, 0, "ERROR : reading id-ns") json_output = self.parse_json_output(result.stdout, "nvme id-ns") - lbafs = self.json_get(json_output, 'lbafs', [], "nvme id-ns") + lbafs = self.json_get(json_output, 'lbafs', context="nvme id-ns", required=True) self.assertIsInstance(lbafs, list, f"ERROR : id-ns returned invalid lbafs type, expected list, got {type(lbafs).__name__}") self.assertTrue(len(lbafs) > self.flbas, @@ -528,7 +530,7 @@ def delete_all_ns(self): result = self.run_cmd(list_ns_cmd) self.assertEqual(result.returncode, 0, "ERROR : nvme list-ns failed") json_output = self.parse_json_output(result.stdout, "nvme list-ns") - nsid_list = self.json_get(json_output, 'nsid_list', [], "nvme list-ns") + nsid_list = self.json_get(json_output, 'nsid_list', context="nvme list-ns", required=True) self.assertIsInstance(nsid_list, list, "ERROR : nvme list-ns returned invalid nsid_list type") self.assertEqual(len(nsid_list), 0, From 41a9a2930aa4e2a3b52a3e17ce42a4ed2f5ae138 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:51:02 +0000 Subject: [PATCH 12/12] Standardize JSON error messages in tests --- tests/nvme_test.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/nvme_test.py b/tests/nvme_test.py index c4d2edd697..ba0938ade7 100644 --- a/tests/nvme_test.py +++ b/tests/nvme_test.py @@ -454,12 +454,12 @@ def get_lba_format_size(self): self.assertIsInstance(lbafs, list, f"ERROR : id-ns returned invalid lbafs type, expected list, got {type(lbafs).__name__}") self.assertTrue(len(lbafs) > self.flbas, - "Error : could not match the given flbas to an existing lbaf") + "ERROR : could not match the given flbas to an existing lbaf") lbaf_json = lbafs[int(self.flbas)] self.assertIsInstance(lbaf_json, dict, f"ERROR : id-ns returned invalid lbaf entry, expected dict, got {type(lbaf_json).__name__}") - self.assertIn('ms', lbaf_json, "Error : id-ns lbaf missing 'ms'") - self.assertIn('ds', lbaf_json, "Error : id-ns lbaf missing 'ds'") + self.assertIn('ms', lbaf_json, "ERROR : id-ns lbaf missing 'ms'") + self.assertIn('ds', lbaf_json, "ERROR : id-ns lbaf missing 'ds'") ms = int(lbaf_json['ms']) ds_expo = int(lbaf_json['ds']) ds = (1 << ds_expo) if ds_expo > 0 else 0 @@ -567,7 +567,8 @@ def _get_created_nsid(self, stdout): ) self.assertIsNotNone( match, - "ERROR : expected create-ns output with nsid, " + "ERROR : create-ns output missing nsid " + "(expected format: 'created nsid: '), " f"got: {stdout!r}", ) return int(match.group(1))