t/io_uring: use char * for name arg in detect_node
[fio.git] / t / nvmept_trim.py
CommitLineData
66cbbb18
VF
1#!/usr/bin/env python3
2#
3# Copyright 2024 Samsung Electronics Co., Ltd All Rights Reserved
4#
5# For conditions of distribution and use, see the accompanying COPYING file.
6#
7"""
8# nvmept_trim.py
9#
10# Test fio's io_uring_cmd ioengine with NVMe pass-through dataset management
11# commands that trim multiple ranges.
12#
13# USAGE
14# see python3 nvmept_trim.py --help
15#
16# EXAMPLES
17# python3 t/nvmept_trim.py --dut /dev/ng0n1
18# python3 t/nvmept_trim.py --dut /dev/ng1n1 -f ./fio
19#
20# REQUIREMENTS
21# Python 3.6
22#
23"""
24import os
25import sys
26import time
27import logging
28import argparse
29from pathlib import Path
30from fiotestlib import FioJobCmdTest, run_fio_tests
31from fiotestcommon import SUCCESS_NONZERO
32
33
34class TrimTest(FioJobCmdTest):
35 """
36 NVMe pass-through test class. Check to make sure output for selected data
37 direction(s) is non-zero and that zero data appears for other directions.
38 """
39
40 def setup(self, parameters):
41 """Setup a test."""
42
43 fio_args = [
44 "--name=nvmept-trim",
45 "--ioengine=io_uring_cmd",
46 "--cmd_type=nvme",
47 f"--filename={self.fio_opts['filename']}",
48 f"--rw={self.fio_opts['rw']}",
49 f"--output={self.filenames['output']}",
50 f"--output-format={self.fio_opts['output-format']}",
51 ]
52 for opt in ['fixedbufs', 'nonvectored', 'force_async', 'registerfiles',
53 'sqthread_poll', 'sqthread_poll_cpu', 'hipri', 'nowait',
54 'time_based', 'runtime', 'verify', 'io_size', 'num_range',
55 'iodepth', 'iodepth_batch', 'iodepth_batch_complete',
56 'size', 'rate', 'bs', 'bssplit', 'bsrange', 'randrepeat',
57 'buffer_pattern', 'verify_pattern', 'verify', 'offset']:
58 if opt in self.fio_opts:
59 option = f"--{opt}={self.fio_opts[opt]}"
60 fio_args.append(option)
61
62 super().setup(fio_args)
63
64
65 def check_result(self):
66
67 super().check_result()
68
69 if 'rw' not in self.fio_opts or \
70 not self.passed or \
71 'json' not in self.fio_opts['output-format']:
72 return
73
74 job = self.json_data['jobs'][0]
75
76 if self.fio_opts['rw'] in ['read', 'randread']:
77 self.passed = self.check_all_ddirs(['read'], job)
78 elif self.fio_opts['rw'] in ['write', 'randwrite']:
79 if 'verify' not in self.fio_opts:
80 self.passed = self.check_all_ddirs(['write'], job)
81 else:
82 self.passed = self.check_all_ddirs(['read', 'write'], job)
83 elif self.fio_opts['rw'] in ['trim', 'randtrim']:
84 self.passed = self.check_all_ddirs(['trim'], job)
85 elif self.fio_opts['rw'] in ['readwrite', 'randrw']:
86 self.passed = self.check_all_ddirs(['read', 'write'], job)
87 elif self.fio_opts['rw'] in ['trimwrite', 'randtrimwrite']:
88 self.passed = self.check_all_ddirs(['trim', 'write'], job)
89 else:
90 logging.error("Unhandled rw value %s", self.fio_opts['rw'])
91 self.passed = False
92
93 if 'iodepth' in self.fio_opts:
94 # We will need to figure something out if any test uses an iodepth
95 # different from 8
96 if job['iodepth_level']['8'] < 95:
97 logging.error("Did not achieve requested iodepth")
98 self.passed = False
99 else:
100 logging.debug("iodepth 8 target met %s", job['iodepth_level']['8'])
101
102
103class RangeTrimTest(TrimTest):
104 """
105 Multi-range trim test class.
106 """
107
108 def get_bs(self):
109 """Calculate block size and determine whether bs will be an average or exact."""
110
111 if 'bs' in self.fio_opts:
112 exact_size = True
113 bs = self.fio_opts['bs']
114 elif 'bssplit' in self.fio_opts:
115 exact_size = False
116 bs = 0
117 total = 0
118 for split in self.fio_opts['bssplit'].split(':'):
119 [blocksize, share] = split.split('/')
120 total += int(share)
121 bs += int(blocksize) * int(share) / 100
122 if total != 100:
123 logging.error("bssplit '%s' total percentage is not 100", self.fio_opts['bssplit'])
124 self.passed = False
125 else:
126 logging.debug("bssplit: average block size is %d", int(bs))
127 # The only check we do here for bssplit is to calculate an average
128 # blocksize and see if the IOPS and bw are consistent
129 elif 'bsrange' in self.fio_opts:
130 exact_size = False
131 [minbs, maxbs] = self.fio_opts['bsrange'].split('-')
132 minbs = int(minbs)
133 maxbs = int(maxbs)
134 bs = int((minbs + maxbs) / 2)
135 logging.debug("bsrange: average block size is %d", int(bs))
136 # The only check we do here for bsrange is to calculate an average
137 # blocksize and see if the IOPS and bw are consistent
138 else:
139 exact_size = True
140 bs = 4096
141
142 return bs, exact_size
143
144
145 def check_result(self):
146 """
147 Make sure that the number of IO requests is consistent with the
148 blocksize and num_range values. In other words, if the blocksize is
149 4KiB and num_range is 2, we should have 128 IO requests to trim 1MiB.
150 """
151 # TODO Enable debug output to check the actual offsets
152
153 super().check_result()
154
155 if not self.passed or 'json' not in self.fio_opts['output-format']:
156 return
157
158 job = self.json_data['jobs'][0]['trim']
159 bs, exact_size = self.get_bs()
160
161 # make sure bw and IOPS are consistent
162 bw = job['bw_bytes']
163 iops = job['iops']
164 runtime = job['runtime']
165
166 calculated = int(bw*runtime/1000)
167 expected = job['io_bytes']
168 if abs(calculated - expected) / expected > 0.05:
169 logging.error("Total bytes %d from bw does not match reported total bytes %d",
170 calculated, expected)
171 self.passed = False
172 else:
173 logging.debug("Total bytes %d from bw matches reported total bytes %d", calculated,
174 expected)
175
176 calculated = int(iops*runtime/1000*bs*self.fio_opts['num_range'])
177 if abs(calculated - expected) / expected > 0.05:
178 logging.error("Total bytes %d from IOPS does not match reported total bytes %d",
179 calculated, expected)
180 self.passed = False
181 else:
182 logging.debug("Total bytes %d from IOPS matches reported total bytes %d", calculated,
183 expected)
184
185 if 'size' in self.fio_opts:
186 io_count = self.fio_opts['size'] / self.fio_opts['num_range'] / bs
187 if exact_size:
188 delta = 0.1
189 else:
190 delta = 0.05*job['total_ios']
191
192 if abs(job['total_ios'] - io_count) > delta:
193 logging.error("Expected numbers of IOs %d does not match actual value %d",
194 io_count, job['total_ios'])
195 self.passed = False
196 else:
197 logging.debug("Expected numbers of IOs %d matches actual value %d", io_count,
198 job['total_ios'])
199
200 if 'rate' in self.fio_opts:
201 if abs(bw - self.fio_opts['rate']) / self.fio_opts['rate'] > 0.05:
202 logging.error("Actual rate %f does not match expected rate %f", bw,
203 self.fio_opts['rate'])
204 self.passed = False
205 else:
206 logging.debug("Actual rate %f matches expeected rate %f", bw, self.fio_opts['rate'])
207
208
209
210TEST_LIST = [
211 # The group of tests below checks existing use cases to make sure there are
212 # no regressions.
213 {
214 "test_id": 1,
215 "fio_opts": {
216 "rw": 'trim',
217 "time_based": 1,
218 "runtime": 3,
219 "output-format": "json",
220 },
221 "test_class": TrimTest,
222 },
223 {
224 "test_id": 2,
225 "fio_opts": {
226 "rw": 'randtrim',
227 "time_based": 1,
228 "runtime": 3,
229 "output-format": "json",
230 },
231 "test_class": TrimTest,
232 },
233 {
234 "test_id": 3,
235 "fio_opts": {
236 "rw": 'trim',
237 "time_based": 1,
238 "runtime": 3,
239 "iodepth": 8,
240 "iodepth_batch": 4,
241 "iodepth_batch_complete": 4,
242 "output-format": "json",
243 },
244 "test_class": TrimTest,
245 },
246 {
247 "test_id": 4,
248 "fio_opts": {
249 "rw": 'randtrim',
250 "time_based": 1,
251 "runtime": 3,
252 "iodepth": 8,
253 "iodepth_batch": 4,
254 "iodepth_batch_complete": 4,
255 "output-format": "json",
256 },
257 "test_class": TrimTest,
258 },
259 {
260 "test_id": 5,
261 "fio_opts": {
262 "rw": 'trimwrite',
263 "time_based": 1,
264 "runtime": 3,
265 "output-format": "json",
266 },
267 "test_class": TrimTest,
268 },
269 {
270 "test_id": 6,
271 "fio_opts": {
272 "rw": 'randtrimwrite',
273 "time_based": 1,
274 "runtime": 3,
275 "output-format": "json",
276 },
277 "test_class": TrimTest,
278 },
279 {
280 "test_id": 7,
281 "fio_opts": {
282 "rw": 'randtrim',
283 "time_based": 1,
284 "runtime": 3,
285 "fixedbufs": 0,
286 "nonvectored": 1,
287 "force_async": 1,
288 "registerfiles": 1,
289 "sqthread_poll": 1,
290 "fixedbuffs": 1,
291 "output-format": "json",
292 },
293 "test_class": TrimTest,
294 },
295 # The group of tests below try out the new functionality
296 {
297 "test_id": 100,
298 "fio_opts": {
299 "rw": 'trim',
300 "num_range": 2,
301 "size": 16*1024*1024,
302 "output-format": "json",
303 },
304 "test_class": RangeTrimTest,
305 },
306 {
307 "test_id": 101,
308 "fio_opts": {
309 "rw": 'randtrim',
310 "num_range": 2,
311 "size": 16*1024*1024,
312 "output-format": "json",
313 },
314 "test_class": RangeTrimTest,
315 },
316 {
317 "test_id": 102,
318 "fio_opts": {
319 "rw": 'randtrim',
320 "num_range": 256,
321 "size": 64*1024*1024,
322 "output-format": "json",
323 },
324 "test_class": RangeTrimTest,
325 },
326 {
327 "test_id": 103,
328 "fio_opts": {
329 "rw": 'trim',
330 "num_range": 2,
331 "bs": 16*1024,
332 "size": 32*1024*1024,
333 "output-format": "json",
334 },
335 "test_class": RangeTrimTest,
336 },
337 {
338 "test_id": 104,
339 "fio_opts": {
340 "rw": 'randtrim',
341 "num_range": 2,
342 "bs": 16*1024,
343 "size": 32*1024*1024,
344 "output-format": "json",
345 },
346 "test_class": RangeTrimTest,
347 },
348 {
349 "test_id": 105,
350 "fio_opts": {
351 "rw": 'randtrim',
352 "num_range": 2,
353 "bssplit": "4096/50:16384/50",
354 "size": 80*1024*1024,
355 "output-format": "json",
356 "randrepeat": 0,
357 },
358 "test_class": RangeTrimTest,
359 },
360 {
361 "test_id": 106,
362 "fio_opts": {
363 "rw": 'randtrim',
364 "num_range": 4,
365 "bssplit": "4096/25:8192/25:12288/25:16384/25",
366 "size": 80*1024*1024,
367 "output-format": "json",
368 "randrepeat": 0,
369 },
370 "test_class": RangeTrimTest,
371 },
372 {
373 "test_id": 107,
374 "fio_opts": {
375 "rw": 'randtrim',
376 "num_range": 4,
377 "bssplit": "4096/20:8192/20:12288/20:16384/20:20480/20",
378 "size": 72*1024*1024,
379 "output-format": "json",
380 "randrepeat": 0,
381 },
382 "test_class": RangeTrimTest,
383 },
384 {
385 "test_id": 108,
386 "fio_opts": {
387 "rw": 'randtrim',
388 "num_range": 2,
389 "bsrange": "4096-16384",
390 "size": 80*1024*1024,
391 "output-format": "json",
392 "randrepeat": 0,
393 },
394 "test_class": RangeTrimTest,
395 },
396 {
397 "test_id": 109,
398 "fio_opts": {
399 "rw": 'randtrim',
400 "num_range": 4,
401 "bsrange": "4096-20480",
402 "size": 72*1024*1024,
403 "output-format": "json",
404 "randrepeat": 0,
405 },
406 "test_class": RangeTrimTest,
407 },
408 {
409 "test_id": 110,
410 "fio_opts": {
411 "rw": 'randtrim',
412 "time_based": 1,
413 "runtime": 10,
414 "rate": 1024*1024,
415 "num_range": 2,
416 "output-format": "json",
417 },
418 "test_class": RangeTrimTest,
419 },
420 # All of the tests below should fail
421 # TODO check the error messages resulting from the jobs below
422 {
423 "test_id": 200,
424 "fio_opts": {
425 "rw": 'randtrimwrite',
426 "time_based": 1,
427 "runtime": 10,
428 "rate": 1024*1024,
429 "num_range": 2,
430 "output-format": "normal",
431 },
432 "test_class": RangeTrimTest,
433 "success": SUCCESS_NONZERO,
434 },
435 {
436 "test_id": 201,
437 "fio_opts": {
438 "rw": 'trimwrite',
439 "time_based": 1,
440 "runtime": 10,
441 "rate": 1024*1024,
442 "num_range": 2,
443 "output-format": "normal",
444 },
445 "test_class": RangeTrimTest,
446 "success": SUCCESS_NONZERO,
447 },
448 {
449 "test_id": 202,
450 "fio_opts": {
451 "rw": 'trim',
452 "time_based": 1,
453 "runtime": 10,
454 "num_range": 257,
455 "output-format": "normal",
456 },
457 "test_class": RangeTrimTest,
458 "success": SUCCESS_NONZERO,
459 },
460 # The sequence of jobs below constitute a single test with multiple steps
461 # - write a data pattern
462 # - verify the data pattern
463 # - trim the first half of the LBA space
464 # - verify that the trim'd LBA space no longer returns the original data pattern
465 # - verify that the remaining LBA space has the expected pattern
466 {
467 "test_id": 300,
468 "fio_opts": {
469 "rw": 'write',
470 "output-format": 'json',
471 "buffer_pattern": 0x0f,
472 "size": 256*1024*1024,
473 },
474 "test_class": TrimTest,
475 },
476 {
477 "test_id": 301,
478 "fio_opts": {
479 "rw": 'read',
480 "output-format": 'json',
481 "verify_pattern": 0x0f,
482 "verify": "pattern",
483 "size": 256*1024*1024,
484 },
485 "test_class": TrimTest,
486 },
487 {
488 "test_id": 302,
489 "fio_opts": {
490 "rw": 'randtrim',
491 "num_range": 8,
492 "output-format": 'json',
493 "size": 128*1024*1024,
494 },
495 "test_class": TrimTest,
496 },
497 # The identify namespace data structure has a DLFEAT field which specifies
498 # what happens when reading data from deallocated blocks. There are three
499 # options:
500 # - read behavior not reported
501 # - deallocated logical block returns all bytes 0x0
502 # - deallocated logical block returns all bytes 0xff
503 # The test below merely checks that the original data pattern is not returned.
504 # Source: Figure 97 from
505 # https://nvmexpress.org/wp-content/uploads/NVM-Express-NVM-Command-Set-Specification-1.0c-2022.10.03-Ratified.pdf
506 {
507 "test_id": 303,
508 "fio_opts": {
509 "rw": 'read',
510 "output-format": 'json',
511 "verify_pattern": 0x0f,
512 "verify": "pattern",
513 "size": 128*1024*1024,
514 },
515 "test_class": TrimTest,
516 "success": SUCCESS_NONZERO,
517 },
518 {
519 "test_id": 304,
520 "fio_opts": {
521 "rw": 'read',
522 "output-format": 'json',
523 "verify_pattern": 0x0f,
524 "verify": "pattern",
525 "offset": 128*1024*1024,
526 "size": 128*1024*1024,
527 },
528 "test_class": TrimTest,
529 },
530]
531
532def parse_args():
533 """Parse command-line arguments."""
534
535 parser = argparse.ArgumentParser()
536 parser.add_argument('-d', '--debug', help='Enable debug messages', action='store_true')
537 parser.add_argument('-f', '--fio', help='path to file executable (e.g., ./fio)')
538 parser.add_argument('-a', '--artifact-root', help='artifact root directory')
539 parser.add_argument('-s', '--skip', nargs='+', type=int,
540 help='list of test(s) to skip')
541 parser.add_argument('-o', '--run-only', nargs='+', type=int,
542 help='list of test(s) to run, skipping all others')
543 parser.add_argument('--dut', help='target NVMe character device to test '
544 '(e.g., /dev/ng0n1). WARNING: THIS IS A DESTRUCTIVE TEST', required=True)
545 args = parser.parse_args()
546
547 return args
548
549
550def main():
551 """Run tests using fio's io_uring_cmd ioengine to send NVMe pass through commands."""
552
553 args = parse_args()
554
555 if args.debug:
556 logging.basicConfig(level=logging.DEBUG)
557 else:
558 logging.basicConfig(level=logging.INFO)
559
560 artifact_root = args.artifact_root if args.artifact_root else \
561 f"nvmept-trim-test-{time.strftime('%Y%m%d-%H%M%S')}"
562 os.mkdir(artifact_root)
563 print(f"Artifact directory is {artifact_root}")
564
565 if args.fio:
566 fio_path = str(Path(args.fio).absolute())
567 else:
568 fio_path = 'fio'
569 print(f"fio path is {fio_path}")
570
571 for test in TEST_LIST:
572 test['fio_opts']['filename'] = args.dut
573
574 test_env = {
575 'fio_path': fio_path,
576 'fio_root': str(Path(__file__).absolute().parent.parent),
577 'artifact_root': artifact_root,
578 'basename': 'nvmept-trim',
579 }
580
581 _, failed, _ = run_fio_tests(TEST_LIST, test_env, args)
582 sys.exit(failed)
583
584
585if __name__ == '__main__':
586 main()