def test_source_package_installation(): TestRun.LOGGER.info("Testing source package installation") iotrace: IotracePlugin = TestRun.plugins['iotrace'] work_path: str = f"{iotrace.working_dir}/standalone-linux-io-tracer" disk = TestRun.dut.disks[0] with TestRun.step("Building iotrace source package"): TestRun.executor.run_expect_success( f"cd {work_path} && " "make package_source -j`nproc --all`") iotrace_installed = check_if_installed() if iotrace_installed: with TestRun.step("Uninstall existing iotrace if needed"): uninstall_iotrace() with TestRun.step("Remove old source package"): TestRun.executor.run_expect_success(f"cd {work_path} && " f"rm -rf iotrace-*-Source") with TestRun.step("Unpack and install source package"): TestRun.executor.run_expect_success( f"tar -xvf {work_path}/build/release/iotrace-*.tar.gz -C {work_path} &&" f"cd {work_path}/iotrace-*-Source &&" "./setup_dependencies.sh &&" "make install -j`nproc --all`") with TestRun.step("Check if iotrace is installed"): iotrace.version() iotrace.start_tracing([disk.system_path]) stopped = iotrace.stop_tracing() if not stopped: raise Exception("Could not stop active tracing.") trace_path = IotracePlugin.get_latest_trace_path() summary_parsed = IotracePlugin.get_trace_summary(trace_path) if summary_parsed['state'] != "COMPLETE": TestRun.LOGGER.error("Trace state is not complete") with TestRun.step("Uninstall iotrace"): uninstall_iotrace() with TestRun.step("Check if iotrace was uninstalled"): TestRun.executor.run_expect_fail("iotrace -V") if iotrace_installed: with TestRun.step("Reinstall iotrace"): install_iotrace()
def test_fuzz_trace_file(): fuzzing_time_seconds = 20 * 60 iotrace: IotracePlugin = TestRun.plugins['iotrace'] repo_path: str = f"{iotrace.working_dir}/slit-afl" # Create trace files iotrace.start_tracing() iotrace.stop_tracing() trace_repo_path = IotracePlugin.get_trace_repository_path() trace_path = trace_repo_path + "/" + IotracePlugin.get_latest_trace_path() tracefile_path = f'{trace_path}/octf.trace.0' copied_tracefile_path = f'{repo_path}/rootfs/var/lib/octf/trace/' + \ f'{IotracePlugin.get_latest_trace_path()}' \ f'/octf.trace.0' # Create patch file for redirecting fuzzed stdin to trace file new_patch_path: str = f'{iotrace.working_dir}/redirect_to_tracefile.patch' create_patch_redirect_fuzz_to_file(f'{copied_tracefile_path}', new_patch_path) # Install iotrace locally with AFL support and redirect patch install_iotrace_with_afl_support(new_patch_path, [ 'modules/open-cas-telemetry-framework', 'modules/test-framework', 'source/kernel' ]) # Copy trace files to local instalation of iotrace TestRun.executor.run_expect_success( f'cp -r {trace_repo_path}/kernel ' f'{repo_path}/rootfs/var/lib/octf/trace/') TestRun.executor.run_expect_success( f'cd {repo_path} && mkdir -p afl-i afl-o') # Add input seed which shall be mutated TestRun.executor.run_expect_success( f'cd {repo_path} && echo "0" > afl-i/case0') TestRun.LOGGER.info( f'Starting fuzzing {tracefile_path} This should take ' + str(fuzzing_time_seconds / 60) + ' minutes') TestRun.executor.run(f'cd {repo_path} && ./tests/security/fuzzy/fuzz.sh ' '"rootfs/bin/iotrace --get-trace-statistics -p ' f'{IotracePlugin.get_latest_trace_path()}" --one-job') output = wait_for_completion(fuzzing_time_seconds, repo_path) TestRun.executor.run( f'cd {repo_path} && ./tests/security/fuzzy/fuzz.sh clean') TestRun.executor.run_expect_success(f'rm -rf {repo_path}/afl-i') detect_crashes(output, "trace-file")
def test_trace_start_stop(): TestRun.LOGGER.info("Testing starting and stopping of tracing") iotrace: IotracePlugin = TestRun.plugins['iotrace'] iotrace.start_tracing() stopped = iotrace.stop_tracing() if not stopped: raise Exception("Could not stop active tracing.") trace_path = IotracePlugin.get_latest_trace_path() summary_parsed = IotracePlugin.get_trace_summary(trace_path) if summary_parsed['state'] != "COMPLETE": raise Exception("Trace state is not complete")
def test_verify_flushes(): TestRun.LOGGER.info("Testing io events during tracing") iotrace = TestRun.plugins['iotrace'] for disk in TestRun.dut.disks: with TestRun.step("Start tracing"): iotrace.start_tracing([disk.system_path]) time.sleep(5) with TestRun.step("Run test workload with sync"): fio = (Fio().create_command().io_engine(IoEngine.sync).block_size( Size(4, Unit.KibiByte)).time_based().read_write( ReadWrite.randwrite).target( disk.system_path).direct(value=False).run_time( datetime.timedelta(seconds=10)).fsync(value=1)) fio.run() with TestRun.step("Stop tracing"): iotrace.stop_tracing() with TestRun.step("Verify trace correctness"): trace_path = IotracePlugin.get_latest_trace_path() events_parsed = IotracePlugin.get_trace_events(trace_path) result = any('io' in event and 'fua' in event['io'] and bool( event['io']['fua'] is True) for event in events_parsed) if not result: TestRun.fail("Could not find event with fua") result = any('io' in event and 'flush' in event['io'] and bool( event['io']['flush'] is True) for event in events_parsed) if not result: TestRun.fail("Could not find event with flush") result = all( 'flush' in event['io'] and bool(event['io']['flush']) is True for event in filter( lambda event: 'io' in event and 'lba' in event['io'] and int(event['io']['lba']) == 0 and int(event['io'][ 'len']) == 0, events_parsed)) if not result: TestRun.fail( "All events with lba 0 and len 0 should have flush")
def test_package_installation(): TestRun.LOGGER.info("Testing package installation") iotrace: IotracePlugin = TestRun.plugins['iotrace'] work_path: str = f"{iotrace.working_dir}/iotrace_package" disk = TestRun.dut.disks[0] with TestRun.step("Copying iotrace repository to DUT"): TestRun.executor.rsync_to(f"{iotrace.repo_dir}/", work_path, delete=True, symlinks=True, exclude_list=['build'] + ['*.pyc'], timeout=timedelta(minutes=2)) with TestRun.step("Setup dependencies"): TestRun.executor.run_expect_success(f"cd {work_path} && " "./setup_dependencies.sh") with TestRun.step("Remove old packages"): TestRun.executor.run_expect_success( f"rm -rf {work_path}/build/release/iotrace-*.deb && " f"rm -rf {work_path}/build/release/iotrace-*.rpm") with TestRun.step("Building iotrace package"): TestRun.executor.run_expect_success(f"cd {work_path} && " "make package -j`nproc --all`") iotrace_installed = check_if_installed() if iotrace_installed: with TestRun.step("Uninstall existing iotrace if needed"): uninstall_iotrace() with TestRun.step("Install from iotrace package"): if check_if_ubuntu(): TestRun.executor.run_expect_success( f"cd {work_path}/build/release && " "dpkg -i iotrace-*.deb") else: TestRun.executor.run_expect_success( f"cd {work_path}/build/release && " "rpm -i iotrace-*.rpm") with TestRun.step("Check if iotrace is installed"): iotrace.version() iotrace.start_tracing([disk.system_path]) time.sleep(1) stopped = iotrace.stop_tracing() if not stopped: raise Exception("Could not stop active tracing.") trace_path = IotracePlugin.get_latest_trace_path() summary_parsed = IotracePlugin.get_trace_summary(trace_path) if summary_parsed['state'] != "COMPLETE": TestRun.LOGGER.error("Trace state is not complete") with TestRun.step("Uninstall rpm package"): if check_if_ubuntu(): TestRun.executor.run_expect_success("apt remove -y iotrace") else: TestRun.executor.run_expect_success("rpm -e iotrace") with TestRun.step("Check if iotrace was uninstalled"): TestRun.executor.run_expect_fail("iotrace -V") if iotrace_installed: with TestRun.step("Reinstall iotrace"): install_iotrace()
def test_lba_histogram(): TestRun.LOGGER.info("Testing lba histogram") iotrace = TestRun.plugins['iotrace'] number_buckets = 32 bucket_size = Size(256) start_lba = 10240 end_lba = start_lba + number_buckets * bucket_size.value for disk in TestRun.dut.disks: io_len = Size(1, disk.block_size) with TestRun.step("Start tracing"): iotrace.start_tracing([disk.system_path]) time.sleep(5) with TestRun.step("Send write commands"): for bucket_index in range(number_buckets): for i in range(bucket_index + 1): # Generate IO for a given bucket. The LBA may need to be # translated from 512B (which iotrace always uses) to 4KiB # (if that's what the underlying disk surfaces) seek = ((start_lba + bucket_index * bucket_size.value + randrange(bucket_size.value)) * iotrace_lba_len / disk.block_size.get_value()) dd = (Dd().input("/dev/urandom").output( disk.system_path).count(1).block_size(io_len).oflag( 'direct,sync').seek(int(seek))) dd.run() with TestRun.step("Send read commands"): for bucket_index in range(number_buckets): for i in range(bucket_index + 1): seek = ((start_lba + bucket_index * bucket_size.value + randrange(bucket_size.value)) * iotrace_lba_len / disk.block_size.get_value()) dd = (Dd().input( disk.system_path).output("/dev/null").count( 1).block_size(io_len).iflag('direct,sync').skip( int(seek))) dd.run() with TestRun.step("Send discard commands"): for bucket_index in range(number_buckets): for i in range(bucket_index + 1): seek = (start_lba + bucket_index * bucket_size.value + randrange(bucket_size.value)) seek = round_down(seek * iotrace_lba_len, disk.block_size.get_value()) TestRun.executor.run_expect_success( f"blkdiscard -o {int(seek)} " f"-l {disk.block_size.get_value()} {disk.system_path}") with TestRun.step("Stop tracing"): iotrace.stop_tracing() with TestRun.step("Verify histogram correctness"): trace_path = IotracePlugin.get_latest_trace_path() json = IotracePlugin.get_lba_histogram(trace_path, bucket_size, start_lba, end_lba) TestRun.LOGGER.info(str(json[0]['histogram'][0])) TestRun.LOGGER.info( str(json[0]['histogram'][0]['total']['range'][4])) for bucket_index in range(number_buckets): read = json[0]['histogram'][0]['read']['range'][bucket_index] write = json[0]['histogram'][0]['write']['range'][bucket_index] discard = json[0]['histogram'][0]['discard']['range'][ bucket_index] total = json[0]['histogram'][0]['total']['range'][bucket_index] if int(read['begin'] ) != start_lba + bucket_index * bucket_size.value: TestRun.fail( f"Invalid read begin range: {read['begin']} for index {bucket_index}" ) if int(read['end']) != start_lba + (bucket_index + 1) * bucket_size.value - 1: TestRun.fail( f"Invalid read end range: {read['end']} for index {bucket_index}" ) if int(read['count']) != bucket_index + 1: TestRun.fail( f"Invalid read count: {read['count']} for index {bucket_index}" ) if int(write['begin'] ) != start_lba + bucket_index * bucket_size.value: TestRun.fail( f"Invalid write begin range: {write['begin']} for index {bucket_index}" ) if int( write['end'] ) != start_lba + (bucket_index + 1) * bucket_size.value - 1: TestRun.fail( f"Invalid write end range: {write['end']} for index {bucket_index}" ) if int(write['count']) != bucket_index + 1: TestRun.fail( f"Invalid write count: {write['count']} for index {bucket_index}" ) if int(discard['begin'] ) != start_lba + bucket_index * bucket_size.value: TestRun.fail( f"Invalid discard begin range: {discard['begin']} " f"for index {bucket_index}") if int( discard['end'] ) != start_lba + (bucket_index + 1) * bucket_size.value - 1: TestRun.fail( f"Invalid discard end range: {discard['end']} for index {bucket_index}" ) if int(discard['count']) != bucket_index + 1: TestRun.fail( f"Invalid discard count: {discard['count']} for index {bucket_index}" ) if int(total['begin'] ) != start_lba + bucket_index * bucket_size.value: TestRun.fail( f"Invalid total begin range: {total['begin']} for index {bucket_index}" ) if int( total['end'] ) != start_lba + (bucket_index + 1) * bucket_size.value - 1: TestRun.fail( f"Invalid total end range: {total['end']} for index {bucket_index}" ) if int(total['count']) != 3 * (bucket_index + 1): TestRun.fail( f"Invalid total count: {total['count']} for index {bucket_index}" )
def test_io_events(): TestRun.LOGGER.info("Testing io events during tracing") iotrace = TestRun.plugins['iotrace'] for disk in TestRun.dut.disks: with TestRun.step("Start tracing"): iotrace.start_tracing([disk.system_path]) time.sleep(5) with TestRun.step("Send write command"): write_length = Size(17, disk.block_size) write_offset = 2 * write_length.get_value() dd = (Dd().input("/dev/urandom").output(disk.system_path).count( 1).block_size(write_length).oflag('direct,sync').seek( int(write_offset / write_length.get_value()))) dd.run() with TestRun.step("Send read command"): read_length = Size(19, disk.block_size) read_offset = 2 * read_length.get_value() dd = (Dd().input(disk.system_path).output("/dev/null").count( 1).block_size(read_length).iflag('direct,sync').skip( int(read_offset / read_length.get_value()))) dd.run() with TestRun.step("Send discard command"): discard_length = Size(21, disk.block_size).get_value() discard_offset = int(2 * discard_length) TestRun.executor.run_expect_success( f"blkdiscard -o {discard_offset}" f" -l {int(discard_length)} {disk.system_path}") with TestRun.step("Stop tracing"): iotrace.stop_tracing() with TestRun.step("Verify trace correctness"): trace_path = IotracePlugin.get_latest_trace_path() events_parsed = IotracePlugin.get_trace_events(trace_path) result = any( 'io' in event and 'operation' in event['io'] and event['io']['operation'] == 'Write' # LBA 0 events don't have a lba field, so skip them and 'lba' in event['io'] and int(event['io']['lba']) == int( write_offset / iotrace_lba_len) and int(event['io']['len']) == int(write_length.get_value() / iotrace_lba_len) and f"/dev/{event['device']['name']}" == disk.system_path for event in events_parsed) if not result: TestRun.fail("Could not find write event") result = any( 'io' in event and 'operation' in event['io'] and event['io']['operation'] == 'Read' # LBA 0 events don't have a lba field, so skip them and 'lba' in event['io'] and int(event['io']['lba']) == int( read_offset / iotrace_lba_len) and int(event['io']['len']) == int(read_length.get_value() / iotrace_lba_len) and f"/dev/{event['device']['name']}" == disk.system_path for event in events_parsed) if not result: TestRun.fail("Could not find read event") result = any('io' in event and 'operation' in event['io'] and event['io']['operation'] == 'Discard' # LBA 0 events don't have a lba field, so skip them and 'lba' in event['io'] and int(event['io']['lba']) == int(discard_offset / iotrace_lba_len) and int(event['io']['len']) == int(discard_length / iotrace_lba_len) and f"/dev/{event['device']['name']}" == disk.system_path for event in events_parsed) if not result: TestRun.fail("Could not find discard event")
def test_iotracer_limits(): """ title: Check if io-tracer respects its own tracing limits . description: | Check if io-tracer respects the time limit and the trace file size limit set with start command and finishes tracing when one of two limits is reached. pass_criteria: - No system crash. - Tracing stops when one of the limits is reached. """ with TestRun.step("Generate workload on device."): disk = TestRun.dut.disks[0] fio_pid = fio_workload(disk.system_path, fio_runtime).run_in_background() with TestRun.step("Prepare io-tracer."): iotracer: IotracePlugin = TestRun.plugins['iotrace'] with TestRun.step("Run io-tracer with duration limit set."): iotracer.start_tracing([disk.system_path], timeout=runtime_short) with TestRun.step("Wait for tracing to finish."): wait(iotracer) output = IotracePlugin.get_trace_summary(IotracePlugin.get_latest_trace_path()) with TestRun.step("Check tracing duration."): if not is_time_almost_equal(runtime_short, output['traceDuration']): TestRun.LOGGER.error("Tracing duration is different than set.") with TestRun.step("Run io-tracer with file size limit set."): iotracer.start_tracing([disk.system_path], trace_file_size=size_limit_low) with TestRun.step("Wait for tracing to finish."): wait(iotracer) output = IotracePlugin.get_trace_summary(IotracePlugin.get_latest_trace_path()) with TestRun.step("Check trace file size."): if not is_size_almost_equal(size_limit_low, output['traceSize']): TestRun.LOGGER.error("Tracing file size is different than set.") with TestRun.step("Run io-tracer with low file size limit and high duration limit set."): iotracer.start_tracing([disk.system_path], timeout=runtime_long, trace_file_size=size_limit_low) with TestRun.step("Wait for tracing to finish."): wait(iotracer) output = IotracePlugin.get_trace_summary(IotracePlugin.get_latest_trace_path()) with TestRun.step("Check tracing duration and trace file size."): if not ((is_time_almost_equal(runtime_long, output['traceDuration']) and is_size_lower_or_equal(output['traceSize'], size_limit_low)) or (is_time_lower_or_equal(output['traceDuration'], runtime_long) and is_size_almost_equal(size_limit_low, output['traceSize']))): TestRun.LOGGER.error("Tracing did not stop after reaching size nor time limit.") with TestRun.step("Run io-tracer with high file size limit and low duration limit set."): iotracer.start_tracing([disk.system_path], timeout=runtime_short, trace_file_size=size_limit_high) with TestRun.step("Wait for tracing to finish."): wait(iotracer) output = IotracePlugin.get_trace_summary(IotracePlugin.get_latest_trace_path()) with TestRun.step("Check tracing duration and trace file size."): if not ((is_time_almost_equal(runtime_short, output['traceDuration']) and is_size_lower_or_equal(output['traceSize'], size_limit_high)) or (is_time_lower_or_equal(output['traceDuration'], runtime_short) and is_size_almost_equal(size_limit_high, output['traceSize']))): TestRun.LOGGER.error("Tracing did not stop after reaching size nor time limit.") with TestRun.step("Stop fio workload."): TestRun.executor.kill_process(fio_pid)
def test_fs_operations(): TestRun.LOGGER.info("Testing file system events during tracing") iotrace = TestRun.plugins['iotrace'] for disk in TestRun.dut.disks: try: with TestRun.step("Create file system"): disk.create_filesystem(Filesystem.ext4) with TestRun.step("Mount device"): disk.mount(mountpoint) with TestRun.step("Start tracing"): iotrace.start_tracing([disk.system_path]) time.sleep(5) with TestRun.step("Create test directory and file"): write_file(f"{mountpoint}/test_file", content="foo") sync() test_file_inode = get_inode(f"{mountpoint}/test_file") create_directory(f"{mountpoint}/test_dir") sync() with TestRun.step("Write to test file"): write_file(f"{mountpoint}/test_file", overwrite=False, content="bar") sync() with TestRun.step("Create new test file"): create_file(f"{mountpoint}/test_file2") test_file2_inode = get_inode(f"{mountpoint}/test_file2") sync() with TestRun.step("Move test file"): move(f"{mountpoint}/test_file", f"{mountpoint}/test_dir") sync() with TestRun.step("Delete test file"): remove(f"{mountpoint}/test_dir/test_file") sync() with TestRun.step("Stop tracing"): sync() iotrace.stop_tracing() with TestRun.step("Verify trace correctness"): trace_path = IotracePlugin.get_latest_trace_path() events_parsed = IotracePlugin.get_trace_events(trace_path) result = any( 'file' in event and event['file']['eventType'] == 'Create' and event['file']['id'] == test_file2_inode for event in events_parsed) if not result: raise Exception("Could not find Create event") result = any( 'file' in event and event['file']['eventType'] == 'Delete' and event['file']['id'] == test_file_inode for event in events_parsed) if not result: raise Exception("Could not find Delete event") result = any( 'file' in event and event['file']['eventType'] == 'MoveTo' and event['file']['id'] == test_file_inode for event in events_parsed) if not result: raise Exception("Could not find MoveTo event") result = any( 'file' in event and event['file']['eventType'] == 'MoveFrom' and event['file']['id'] == test_file_inode for event in events_parsed) if not result: raise Exception("Could not find MoveFrom event") result = any( 'file' in event and event['file']['eventType'] == 'Access' and event['file']['id'] == test_file_inode for event in events_parsed) if not result: raise Exception("Could not find Access event") finally: with TestRun.step("Unmount device"): disk.unmount()
def test_fs_statistics(fs): """ title: Test if FS statistics are properly calculated by iotrace description: | Create files on filesystem (relative to fs root): * test_dir/A.x * test_dir/B.y * A.y * B.x and execute known workload on them. Each file has different size and is written with different block size, also varying number of times. This way number of loops on given job is equal to WiF, and size of given file is equal to workset. Only workset and WiF stats are verified. pass_criteria: - Statistics for file prefixes A and B match with expected values - Statistics for file extensions x and y match with expected values - Statistics for root fs directory and test_dir directory match with expected values """ iotrace = TestRun.plugins["iotrace"] (disk, filesystem) = fs with TestRun.step("Prepare fio configuration"): test_dir = f"{filesystem}/test_dir" fio_cfg = (Fio().create_command().io_engine( IoEngine.libaio).sync(True).read_write(ReadWrite.write)) dir_Ax_size = Size(3, Unit.MebiByte) dir_Ax_WiF = 2 dir_Ax_written = dir_Ax_size * dir_Ax_WiF (fio_cfg.add_job("test_dir/A.x").target(f"{test_dir}/A.x").file_size( dir_Ax_size).block_size(Size(4, Unit.KibiByte)).loops(dir_Ax_WiF)) dir_By_size = Size(16, Unit.KibiByte) dir_By_WiF = 2 dir_By_written = dir_By_size * dir_By_WiF (fio_cfg.add_job("test_dir/B.y").target(f"{test_dir}/B.y").file_size( dir_By_size).block_size(Size(16, Unit.KibiByte)).loops(dir_By_WiF)) Ay_size = Size(2, Unit.MebiByte) Ay_WiF = 5 Ay_written = Ay_size * Ay_WiF (fio_cfg.add_job("A.y").target(f"{filesystem}/A.y").file_size( Ay_size).block_size(Size(64, Unit.KibiByte)).loops(Ay_WiF)) Bx_size = Size(5, Unit.MebiByte) Bx_WiF = 1 Bx_written = Bx_size * Bx_WiF (fio_cfg.add_job("B.x").target(f"{filesystem}/B.x").file_size( Bx_size).block_size(Size(128, Unit.KibiByte))) with TestRun.step("Prepare directory and files for test"): create_directory(test_dir) # In this run FIO will only create all the files needed by jobs and quit. # If we didn't do it WiF would be +1 for each of the files (one file write on creation). # For simplicity of calculations we create files first and after that start tracing. fio_cfg.edit_global().create_only(True) fio_cfg.run() with TestRun.step("Start tracing"): iotrace.start_tracing([disk.system_path], Size(1, Unit.GibiByte)) time.sleep(3) with TestRun.step("Run workload"): fio_cfg.edit_global().create_only(False) fio_cfg.run() with TestRun.step("Stop tracing"): iotrace.stop_tracing() with TestRun.step("Verify trace correctness"): A_prefix_workset = dir_Ax_size + Ay_size B_prefix_workset = dir_By_size + Bx_size A_prefix_WiF = (dir_Ax_written + Ay_written) / A_prefix_workset B_prefix_WiF = (dir_By_written + Bx_written) / B_prefix_workset x_extension_workset = dir_Ax_size + Bx_size y_extension_workset = dir_By_size + Ay_size x_extension_WiF = (dir_Ax_written + Bx_written) / x_extension_workset y_extension_WiF = (Ay_written + dir_By_written) / y_extension_workset test_dir_workset = dir_Ax_size + dir_By_size root_dir_workset = Ay_size + Bx_size test_dir_WiF = (dir_Ax_written + dir_By_written) / test_dir_workset root_dir_WiF = (Ay_written + Bx_written) / root_dir_workset trace_path = IotracePlugin.get_latest_trace_path() stats = parse_fs_stats( IotracePlugin.get_fs_statistics(trace_path)[0]['entries']) prefix_stats = { stat.file_name_prefix: stat for stat in stats if type(stat) == FileTraceStatistics } extension_stats = { stat.extension: stat for stat in stats if type(stat) == ExtensionTraceStatistics } dir_stats = { stat.directory: stat for stat in stats if type(stat) == DirectoryTraceStatistics } for (desc, (expect_workset, expect_WiF), got) in [ ("A file prefix", (A_prefix_workset, A_prefix_WiF), prefix_stats["A"]), ("B file prefix", (B_prefix_workset, B_prefix_WiF), prefix_stats["B"]), ("x file extension", (x_extension_workset, x_extension_WiF), extension_stats["x"]), ("y file extension", (y_extension_workset, y_extension_WiF), extension_stats["y"]), ("test_dir directory", (test_dir_workset, test_dir_WiF), dir_stats["/test_dir"]), ("root directory", (root_dir_workset, root_dir_WiF), dir_stats["/"]), ]: expect_equal(f"{desc} workset", expect_workset, got.statistics.total.metrics.workset) expect_equal(f"{desc} write invalidation factor", expect_WiF, got.statistics.write.metrics.wif)