| 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | # random_seed.py |
| 4 | # |
| 5 | # Test fio's random seed options. |
| 6 | # |
| 7 | # - make sure that randseed overrides randrepeat and allrandrepeat |
| 8 | # - make sure that seeds differ across invocations when [all]randrepeat=0 and randseed is not set |
| 9 | # - make sure that seeds are always the same when [all]randrepeat=1 and randseed is not set |
| 10 | # |
| 11 | # USAGE |
| 12 | # see python3 random_seed.py --help |
| 13 | # |
| 14 | # EXAMPLES |
| 15 | # python3 t/random_seed.py |
| 16 | # python3 t/random_seed.py -f ./fio |
| 17 | # |
| 18 | # REQUIREMENTS |
| 19 | # Python 3.6 |
| 20 | # |
| 21 | """ |
| 22 | import os |
| 23 | import sys |
| 24 | import time |
| 25 | import locale |
| 26 | import logging |
| 27 | import argparse |
| 28 | from pathlib import Path |
| 29 | from fiotestlib import FioJobCmdTest, run_fio_tests |
| 30 | |
| 31 | class FioRandTest(FioJobCmdTest): |
| 32 | """fio random seed test.""" |
| 33 | |
| 34 | def setup(self, parameters): |
| 35 | """Setup the test.""" |
| 36 | |
| 37 | fio_args = [ |
| 38 | "--debug=random", |
| 39 | "--name=random_seed", |
| 40 | "--ioengine=null", |
| 41 | "--filesize=32k", |
| 42 | "--rw=randread", |
| 43 | f"--output={self.filenames['output']}", |
| 44 | ] |
| 45 | for opt in ['randseed', 'randrepeat', 'allrandrepeat']: |
| 46 | if opt in self.fio_opts: |
| 47 | option = f"--{opt}={self.fio_opts[opt]}" |
| 48 | fio_args.append(option) |
| 49 | |
| 50 | super().setup(fio_args) |
| 51 | |
| 52 | def get_rand_seeds(self): |
| 53 | """Collect random seeds from --debug=random output.""" |
| 54 | with open(self.filenames['output'], "r", |
| 55 | encoding=locale.getpreferredencoding()) as out_file: |
| 56 | file_data = out_file.read() |
| 57 | |
| 58 | offsets = 0 |
| 59 | for line in file_data.split('\n'): |
| 60 | if 'random' in line and 'FIO_RAND_NR_OFFS=' in line: |
| 61 | tokens = line.split('=') |
| 62 | offsets = int(tokens[len(tokens)-1]) |
| 63 | break |
| 64 | |
| 65 | if offsets == 0: |
| 66 | pass |
| 67 | # find an exception to throw |
| 68 | |
| 69 | seed_list = [] |
| 70 | for line in file_data.split('\n'): |
| 71 | if 'random' not in line: |
| 72 | continue |
| 73 | if 'rand_seeds[' in line: |
| 74 | tokens = line.split('=') |
| 75 | seed = int(tokens[-1]) |
| 76 | seed_list.append(seed) |
| 77 | # assume that seeds are in order |
| 78 | |
| 79 | return seed_list |
| 80 | |
| 81 | |
| 82 | class TestRR(FioRandTest): |
| 83 | """ |
| 84 | Test object for [all]randrepeat. If run for the first time just collect the |
| 85 | seeds. For later runs make sure the seeds match or do not match those |
| 86 | previously collected. |
| 87 | """ |
| 88 | # one set of seeds is for randrepeat=0 and the other is for randrepeat=1 |
| 89 | seeds = { 0: None, 1: None } |
| 90 | |
| 91 | def check_result(self): |
| 92 | """Check output for allrandrepeat=1.""" |
| 93 | |
| 94 | super().check_result() |
| 95 | if not self.passed: |
| 96 | return |
| 97 | |
| 98 | opt = 'randrepeat' if 'randrepeat' in self.fio_opts else 'allrandrepeat' |
| 99 | rr = self.fio_opts[opt] |
| 100 | rand_seeds = self.get_rand_seeds() |
| 101 | |
| 102 | if not TestRR.seeds[rr]: |
| 103 | TestRR.seeds[rr] = rand_seeds |
| 104 | logging.debug("TestRR: saving rand_seeds for [a]rr=%d", rr) |
| 105 | else: |
| 106 | if rr: |
| 107 | if TestRR.seeds[1] != rand_seeds: |
| 108 | self.passed = False |
| 109 | print(f"TestRR: unexpected seed mismatch for [a]rr={rr}") |
| 110 | else: |
| 111 | logging.debug("TestRR: seeds correctly match for [a]rr=%d", rr) |
| 112 | if TestRR.seeds[0] == rand_seeds: |
| 113 | self.passed = False |
| 114 | print("TestRR: seeds unexpectedly match those from system RNG") |
| 115 | else: |
| 116 | if TestRR.seeds[0] == rand_seeds: |
| 117 | self.passed = False |
| 118 | print(f"TestRR: unexpected seed match for [a]rr={rr}") |
| 119 | else: |
| 120 | logging.debug("TestRR: seeds correctly don't match for [a]rr=%d", rr) |
| 121 | if TestRR.seeds[1] == rand_seeds: |
| 122 | self.passed = False |
| 123 | print("TestRR: random seeds unexpectedly match those from [a]rr=1") |
| 124 | |
| 125 | |
| 126 | class TestRS(FioRandTest): |
| 127 | """ |
| 128 | Test object when randseed=something controls the generated seeds. If run |
| 129 | for the first time for a given randseed just collect the seeds. For later |
| 130 | runs with the same seed make sure the seeds are the same as those |
| 131 | previously collected. |
| 132 | """ |
| 133 | seeds = {} |
| 134 | |
| 135 | def check_result(self): |
| 136 | """Check output for randseed=something.""" |
| 137 | |
| 138 | super().check_result() |
| 139 | if not self.passed: |
| 140 | return |
| 141 | |
| 142 | rand_seeds = self.get_rand_seeds() |
| 143 | randseed = self.fio_opts['randseed'] |
| 144 | |
| 145 | logging.debug("randseed = %s", randseed) |
| 146 | |
| 147 | if randseed not in TestRS.seeds: |
| 148 | TestRS.seeds[randseed] = rand_seeds |
| 149 | logging.debug("TestRS: saving rand_seeds") |
| 150 | else: |
| 151 | if TestRS.seeds[randseed] != rand_seeds: |
| 152 | self.passed = False |
| 153 | print("TestRS: seeds don't match when they should") |
| 154 | else: |
| 155 | logging.debug("TestRS: seeds correctly match") |
| 156 | |
| 157 | # Now try to find seeds generated using a different randseed and make |
| 158 | # sure they *don't* match |
| 159 | for key, value in TestRS.seeds.items(): |
| 160 | if key != randseed: |
| 161 | if value == rand_seeds: |
| 162 | self.passed = False |
| 163 | print("TestRS: randseeds differ but generated seeds match.") |
| 164 | else: |
| 165 | logging.debug("TestRS: randseeds differ and generated seeds also differ.") |
| 166 | |
| 167 | |
| 168 | def parse_args(): |
| 169 | """Parse command-line arguments.""" |
| 170 | |
| 171 | parser = argparse.ArgumentParser() |
| 172 | parser.add_argument('-f', '--fio', help='path to file executable (e.g., ./fio)') |
| 173 | parser.add_argument('-a', '--artifact-root', help='artifact root directory') |
| 174 | parser.add_argument('-d', '--debug', help='enable debug output', action='store_true') |
| 175 | parser.add_argument('-s', '--skip', nargs='+', type=int, |
| 176 | help='list of test(s) to skip') |
| 177 | parser.add_argument('-o', '--run-only', nargs='+', type=int, |
| 178 | help='list of test(s) to run, skipping all others') |
| 179 | args = parser.parse_args() |
| 180 | |
| 181 | return args |
| 182 | |
| 183 | |
| 184 | def main(): |
| 185 | """Run tests of fio random seed options""" |
| 186 | |
| 187 | args = parse_args() |
| 188 | |
| 189 | if args.debug: |
| 190 | logging.basicConfig(level=logging.DEBUG) |
| 191 | else: |
| 192 | logging.basicConfig(level=logging.INFO) |
| 193 | |
| 194 | artifact_root = args.artifact_root if args.artifact_root else \ |
| 195 | f"random-seed-test-{time.strftime('%Y%m%d-%H%M%S')}" |
| 196 | os.mkdir(artifact_root) |
| 197 | print(f"Artifact directory is {artifact_root}") |
| 198 | |
| 199 | if args.fio: |
| 200 | fio_path = str(Path(args.fio).absolute()) |
| 201 | else: |
| 202 | fio_path = 'fio' |
| 203 | print(f"fio path is {fio_path}") |
| 204 | |
| 205 | test_list = [ |
| 206 | { |
| 207 | "test_id": 1, |
| 208 | "fio_opts": { |
| 209 | "randrepeat": 0, |
| 210 | }, |
| 211 | "test_class": TestRR, |
| 212 | }, |
| 213 | { |
| 214 | "test_id": 2, |
| 215 | "fio_opts": { |
| 216 | "randrepeat": 0, |
| 217 | }, |
| 218 | "test_class": TestRR, |
| 219 | }, |
| 220 | { |
| 221 | "test_id": 3, |
| 222 | "fio_opts": { |
| 223 | "randrepeat": 1, |
| 224 | }, |
| 225 | "test_class": TestRR, |
| 226 | }, |
| 227 | { |
| 228 | "test_id": 4, |
| 229 | "fio_opts": { |
| 230 | "randrepeat": 1, |
| 231 | }, |
| 232 | "test_class": TestRR, |
| 233 | }, |
| 234 | { |
| 235 | "test_id": 5, |
| 236 | "fio_opts": { |
| 237 | "allrandrepeat": 0, |
| 238 | }, |
| 239 | "test_class": TestRR, |
| 240 | }, |
| 241 | { |
| 242 | "test_id": 6, |
| 243 | "fio_opts": { |
| 244 | "allrandrepeat": 0, |
| 245 | }, |
| 246 | "test_class": TestRR, |
| 247 | }, |
| 248 | { |
| 249 | "test_id": 7, |
| 250 | "fio_opts": { |
| 251 | "allrandrepeat": 1, |
| 252 | }, |
| 253 | "test_class": TestRR, |
| 254 | }, |
| 255 | { |
| 256 | "test_id": 8, |
| 257 | "fio_opts": { |
| 258 | "allrandrepeat": 1, |
| 259 | }, |
| 260 | "test_class": TestRR, |
| 261 | }, |
| 262 | { |
| 263 | "test_id": 9, |
| 264 | "fio_opts": { |
| 265 | "randrepeat": 0, |
| 266 | "randseed": "12345", |
| 267 | }, |
| 268 | "test_class": TestRS, |
| 269 | }, |
| 270 | { |
| 271 | "test_id": 10, |
| 272 | "fio_opts": { |
| 273 | "randrepeat": 0, |
| 274 | "randseed": "12345", |
| 275 | }, |
| 276 | "test_class": TestRS, |
| 277 | }, |
| 278 | { |
| 279 | "test_id": 11, |
| 280 | "fio_opts": { |
| 281 | "randrepeat": 1, |
| 282 | "randseed": "12345", |
| 283 | }, |
| 284 | "test_class": TestRS, |
| 285 | }, |
| 286 | { |
| 287 | "test_id": 12, |
| 288 | "fio_opts": { |
| 289 | "allrandrepeat": 0, |
| 290 | "randseed": "12345", |
| 291 | }, |
| 292 | "test_class": TestRS, |
| 293 | }, |
| 294 | { |
| 295 | "test_id": 13, |
| 296 | "fio_opts": { |
| 297 | "allrandrepeat": 1, |
| 298 | "randseed": "12345", |
| 299 | }, |
| 300 | "test_class": TestRS, |
| 301 | }, |
| 302 | { |
| 303 | "test_id": 14, |
| 304 | "fio_opts": { |
| 305 | "randrepeat": 0, |
| 306 | "randseed": "67890", |
| 307 | }, |
| 308 | "test_class": TestRS, |
| 309 | }, |
| 310 | { |
| 311 | "test_id": 15, |
| 312 | "fio_opts": { |
| 313 | "randrepeat": 1, |
| 314 | "randseed": "67890", |
| 315 | }, |
| 316 | "test_class": TestRS, |
| 317 | }, |
| 318 | { |
| 319 | "test_id": 16, |
| 320 | "fio_opts": { |
| 321 | "allrandrepeat": 0, |
| 322 | "randseed": "67890", |
| 323 | }, |
| 324 | "test_class": TestRS, |
| 325 | }, |
| 326 | { |
| 327 | "test_id": 17, |
| 328 | "fio_opts": { |
| 329 | "allrandrepeat": 1, |
| 330 | "randseed": "67890", |
| 331 | }, |
| 332 | "test_class": TestRS, |
| 333 | }, |
| 334 | ] |
| 335 | |
| 336 | test_env = { |
| 337 | 'fio_path': fio_path, |
| 338 | 'fio_root': str(Path(__file__).absolute().parent.parent), |
| 339 | 'artifact_root': artifact_root, |
| 340 | 'basename': 'random', |
| 341 | } |
| 342 | |
| 343 | _, failed, _ = run_fio_tests(test_list, test_env, args) |
| 344 | sys.exit(failed) |
| 345 | |
| 346 | |
| 347 | if __name__ == '__main__': |
| 348 | main() |