Skip to content

Commit 2f541b7

Browse files
committed
tests: test_ioclass_stats: Fix after API changes
Signed-off-by: Robert Baldyga <robert.baldyga@unvertical.com>
1 parent 8978e1f commit 2f541b7

File tree

1 file changed

+184
-101
lines changed

1 file changed

+184
-101
lines changed

test/functional/tests/stats/test_ioclass_stats.py

Lines changed: 184 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#
22
# Copyright(c) 2019-2021 Intel Corporation
33
# Copyright(c) 2024-2025 Huawei Technologies Co., Ltd.
4+
# Copyright(c) 2026 Unvertical
45
# SPDX-License-Identifier: BSD-3-Clause
56
#
67

@@ -17,13 +18,7 @@
1718
get_stats_ioclass_id_not_configured,
1819
get_stats_ioclass_id_out_of_range
1920
)
20-
from api.cas.statistics import (
21-
config_stats_ioclass,
22-
usage_stats_ioclass,
23-
request_stats,
24-
block_stats_core,
25-
block_stats_cache
26-
)
21+
from api.cas.statistics import RequestStats, BlockStats, ErrorStats, get_stats_dict
2722
from connection.utils.output import CmdException
2823
from core.test_run import TestRun
2924
from storage_devices.disk import DiskType, DiskTypeSet, DiskTypeLowerThan
@@ -38,6 +33,87 @@
3833
mountpoint = "/tmp/cas1-1"
3934
cache_id = 1
4035

36+
config_stats_ioclass = [
37+
"IO class ID",
38+
"IO class name",
39+
"Eviction priority",
40+
"Max size"
41+
]
42+
43+
usage_stats_ioclass = [
44+
"Occupancy [4KiB Blocks]",
45+
"Occupancy [%]",
46+
"Clean [4KiB Blocks]",
47+
"Clean [%]",
48+
"Dirty [4KiB Blocks]",
49+
"Dirty [%]"
50+
]
51+
52+
request_stats = [
53+
"Read hits [Requests]",
54+
"Read hits [%]",
55+
"Read partial misses [Requests]",
56+
"Read partial misses [%]",
57+
"Read full misses [Requests]",
58+
"Read full misses [%]",
59+
"Read total [Requests]",
60+
"Read total [%]",
61+
"Write hits [Requests]",
62+
"Write hits [%]",
63+
"Write partial misses [Requests]",
64+
"Write partial misses [%]",
65+
"Write full misses [Requests]",
66+
"Write full misses [%]",
67+
"Write total [Requests]",
68+
"Write total [%]",
69+
"Pass-Through reads [Requests]",
70+
"Pass-Through reads [%]",
71+
"Pass-Through writes [Requests]",
72+
"Pass-Through writes [%]",
73+
"Serviced requests [Requests]",
74+
"Serviced requests [%]",
75+
"Total requests [Requests]",
76+
"Total requests [%]"
77+
]
78+
79+
block_stats = [
80+
"Reads from core [4KiB Blocks]",
81+
"Reads from core [%]",
82+
"Writes to core [4KiB Blocks]",
83+
"Writes to core [%]",
84+
"Total to/from core [4KiB Blocks]",
85+
"Total to/from core [%]",
86+
"Reads from cache [4KiB Blocks]",
87+
"Reads from cache [%]",
88+
"Writes to cache [4KiB Blocks]",
89+
"Writes to cache [%]",
90+
"Total to/from cache [4KiB Blocks]",
91+
"Total to/from cache [%]",
92+
"Reads from exported object [4KiB Blocks]",
93+
"Reads from exported object [%]",
94+
"Writes to exported object [4KiB Blocks]",
95+
"Writes to exported object [%]",
96+
"Total to/from exported object [4KiB Blocks]",
97+
"Total to/from exported object [%]"
98+
]
99+
100+
error_stats = [
101+
"Cache read errors [Requests]",
102+
"Cache read errors [%]",
103+
"Cache write errors [Requests]",
104+
"Cache write errors [%]",
105+
"Cache total errors [Requests]",
106+
"Cache total errors [%]",
107+
"Core read errors [Requests]",
108+
"Core read errors [%]",
109+
"Core write errors [Requests]",
110+
"Core write errors [%]",
111+
"Core total errors [Requests]",
112+
"Core total errors [%]",
113+
"Total errors [Requests]",
114+
"Total errors [%]"
115+
]
116+
41117

42118
@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand]))
43119
@pytest.mark.require_disk("core", DiskTypeLowerThan("cache"))
@@ -82,7 +158,7 @@ def test_ioclass_stats_basic(random_cls):
82158
casadm.print_statistics(
83159
cache_id=cache_id,
84160
io_class_id=class_id,
85-
per_io_class=True)
161+
io_class=True)
86162
if not expected:
87163
TestRun.LOGGER.error(
88164
f"Stats retrieved for not configured IO class {class_id}")
@@ -118,6 +194,8 @@ def test_ioclass_stats_sum(random_cls):
118194
with TestRun.step("Test prepare"):
119195
caches, cores = prepare(random_cls)
120196
cache, core = caches[0], cores[0]
197+
# Include mandatory io class 0
198+
ioclass_id_list = [0] + list(range(min_ioclass_id, max_ioclass_id))
121199

122200
with TestRun.step("Prepare IO class config file"):
123201
ioclass_list = []
@@ -152,74 +230,99 @@ def test_ioclass_stats_sum(random_cls):
152230
core.unmount()
153231
sync()
154232

155-
with TestRun.step("Check if per class cache IO class statistics sum up to cache statistics"):
156-
# Name of stats, which should not be compared
157-
not_compare_stats = ["clean", "occupancy", "free"]
158-
ioclass_id_list = list(range(min_ioclass_id, max_ioclass_id))
159-
# Append default IO class id
160-
ioclass_id_list.append(0)
161-
162-
cache_stats = cache.get_statistics_flat(
163-
stat_filter=[StatsFilter.usage, StatsFilter.req, StatsFilter.blk]
164-
)
233+
with TestRun.step("Accumulate IO class statistics"):
234+
occupancy = Size.zero()
235+
dirty = Size.zero()
236+
request_stats = RequestStats.zero()
237+
block_stats = BlockStats.zero()
165238
for ioclass_id in ioclass_id_list:
166-
ioclass_stats = cache.get_statistics_flat(
239+
ioclass_stats = cache.get_io_class_statistics(
167240
stat_filter=[StatsFilter.usage, StatsFilter.req, StatsFilter.blk],
168241
io_class_id=ioclass_id,
169242
)
170-
for stat_name in cache_stats:
171-
if stat_name in not_compare_stats:
172-
continue
173-
cache_stats[stat_name] -= ioclass_stats[stat_name]
174-
175-
for stat_name in cache_stats:
176-
if stat_name in not_compare_stats:
177-
continue
178-
stat_val = (
179-
cache_stats[stat_name].get_value()
180-
if isinstance(cache_stats[stat_name], Size)
181-
else cache_stats[stat_name]
182-
)
183-
if stat_val != 0:
184-
TestRun.LOGGER.error(f"{stat_name} diverged for cache!\n")
243+
occupancy += ioclass_stats.usage_stats.occupancy
244+
dirty += ioclass_stats.usage_stats.dirty
245+
request_stats += ioclass_stats.request_stats
246+
block_stats += ioclass_stats.block_stats
247+
248+
with TestRun.step("Check if per class cache IO class statistics sum up to cache statistics"):
249+
cache_stats = cache.get_statistics(
250+
stat_filter=[StatsFilter.usage, StatsFilter.req, StatsFilter.blk]
251+
)
252+
if occupancy != cache_stats.usage_stats.occupancy:
253+
TestRun.LOGGER.error("Occupancy diverged for cache!")
254+
if dirty != cache_stats.usage_stats.dirty:
255+
TestRun.LOGGER.error("Dirty diverged for cache!")
256+
if request_stats != cache_stats.request_stats:
257+
TestRun.LOGGER.error("Request statistics diverged for cache!\n")
258+
if block_stats != cache_stats.block_stats:
259+
TestRun.LOGGER.error("Block statistics diverged for cache!\n")
185260

186261
with TestRun.step("Check if per class core IO class statistics sum up to core statistics"):
187-
core_stats = core.get_statistics_flat(
262+
core_stats = core.get_statistics(
188263
stat_filter=[StatsFilter.usage, StatsFilter.req, StatsFilter.blk]
189264
)
190-
for ioclass_id in ioclass_id_list:
191-
ioclass_stats = core.get_statistics_flat(
192-
stat_filter=[StatsFilter.usage, StatsFilter.req, StatsFilter.blk],
193-
io_class_id=ioclass_id,
194-
)
195-
for stat_name in core_stats:
196-
if stat_name in not_compare_stats:
197-
continue
198-
core_stats[stat_name] -= ioclass_stats[stat_name]
199-
200-
for stat_name in core_stats:
201-
if stat_name in not_compare_stats:
202-
continue
203-
stat_val = (
204-
core_stats[stat_name].get_value()
205-
if isinstance(core_stats[stat_name], Size)
206-
else core_stats[stat_name]
207-
)
208-
if stat_val != 0:
209-
TestRun.LOGGER.error(f"{stat_name} diverged for core!\n")
265+
if occupancy != core_stats.usage_stats.occupancy:
266+
TestRun.LOGGER.error("Occupancy diverged for core!")
267+
if dirty != core_stats.usage_stats.dirty:
268+
TestRun.LOGGER.error("Dirty diverged for core!")
269+
if request_stats != core_stats.request_stats:
270+
TestRun.LOGGER.error("Request statistics diverged for core!\n")
271+
if block_stats != core_stats.block_stats:
272+
TestRun.LOGGER.error("Block statistics diverged for core!\n")
273+
274+
with TestRun.step("Test cleanup"):
275+
for f in files_list:
276+
f.remove()
210277

211-
with TestRun.step("Test cleanup"):
212-
for f in files_list:
213-
f.remove()
278+
279+
@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand]))
280+
@pytest.mark.require_disk("core", DiskTypeLowerThan("cache"))
281+
@pytest.mark.parametrize(
282+
"stat_filter",
283+
[StatsFilter.usage, StatsFilter.conf, StatsFilter.req, StatsFilter.blk, StatsFilter.err])
284+
@pytest.mark.parametrize("random_cls", [random.choice(list(CacheLineSize))])
285+
def test_ioclass_stats_sections_cache(stat_filter, random_cls):
286+
"""
287+
title: Test for cache/core IO class statistics sections.
288+
description: |
289+
Check if IO class statistics sections for cache/core print all required entries and
290+
no additional ones.
291+
pass_criteria:
292+
- Section statistics contain all required entries.
293+
- Section statistics do not contain any additional entries.
294+
"""
295+
with TestRun.step("Test prepare"):
296+
caches, cores = prepare(random_cls, cache_count=4, cores_per_cache=1)
297+
298+
with TestRun.group(f"Default IO class config statistics"):
299+
for cache in caches:
300+
with TestRun.step(f"Cache {cache.cache_id}"):
301+
statistics = get_stats_dict(
302+
filter=[stat_filter], cache_id=cache.cache_id, io_class_id=0)
303+
validate_statistics(statistics, stat_filter)
304+
305+
with TestRun.step("Load random IO class configuration for each cache"):
306+
for cache in caches:
307+
random_list = IoClass.generate_random_ioclass_list(ioclass_config.MAX_IO_CLASS_ID + 1)
308+
IoClass.save_list_to_config_file(random_list, add_default_rule=False)
309+
cache.load_io_class(ioclass_config.default_config_file_path)
310+
311+
with TestRun.group(f"Random IO class config statistics"):
312+
for cache in caches:
313+
with TestRun.step(f"Cache {cache.cache_id}"):
314+
for class_id in range(ioclass_config.MAX_IO_CLASS_ID + 1):
315+
statistics = get_stats_dict(
316+
filter=[stat_filter], cache_id=cache.cache_id, io_class_id=class_id)
317+
validate_statistics(statistics, stat_filter)
214318

215319

216320
@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand]))
217321
@pytest.mark.require_disk("core", DiskTypeLowerThan("cache"))
218322
@pytest.mark.parametrize("stat_filter", [StatsFilter.req, StatsFilter.usage, StatsFilter.conf,
219323
StatsFilter.blk])
220-
@pytest.mark.parametrize("per_core", [True, False])
221324
@pytest.mark.parametrize("random_cls", [random.choice(list(CacheLineSize))])
222-
def test_ioclass_stats_sections(stat_filter, per_core, random_cls):
325+
def test_ioclass_stats_sections_core(stat_filter, random_cls):
223326
"""
224327
title: Test for cache/core IO class statistics sections.
225328
description: |
@@ -232,65 +335,45 @@ def test_ioclass_stats_sections(stat_filter, per_core, random_cls):
232335
with TestRun.step("Test prepare"):
233336
caches, cores = prepare(random_cls, cache_count=4, cores_per_cache=3)
234337

235-
with TestRun.step(f"Validate displayed {stat_filter.name} statistics for default IO class for "
236-
f"{'cores' if per_core else 'caches'}"):
338+
with TestRun.group(f"Default IO class config statistics"):
237339
for cache in caches:
238-
with TestRun.group(f"Cache {cache.cache_id}"):
239-
for core in cache.get_cores():
240-
if per_core:
241-
TestRun.LOGGER.info(f"Core {core.cache_id}-{core.core_id}")
242-
statistics = (
243-
core.get_statistics_flat(
244-
io_class_id=0, stat_filter=[stat_filter]) if per_core
245-
else cache.get_statistics_flat(
246-
io_class_id=0, stat_filter=[stat_filter]))
247-
validate_statistics(statistics, stat_filter, per_core)
248-
if not per_core:
249-
break
340+
for core in cache.get_cores():
341+
with TestRun.step(f"Core {core.cache_id}-{core.core_id}"):
342+
statistics = get_stats_dict(
343+
[stat_filter], core.cache_id, core.core_id, 0)
344+
validate_statistics(statistics, stat_filter)
250345

251346
with TestRun.step("Load random IO class configuration for each cache"):
252347
for cache in caches:
253348
random_list = IoClass.generate_random_ioclass_list(ioclass_config.MAX_IO_CLASS_ID + 1)
254349
IoClass.save_list_to_config_file(random_list, add_default_rule=False)
255350
cache.load_io_class(ioclass_config.default_config_file_path)
256351

257-
with TestRun.step(f"Validate displayed {stat_filter.name} statistics for every configured IO "
258-
f"class for all {'cores' if per_core else 'caches'}"):
352+
with TestRun.group(f"Random IO class config statistics"):
259353
for cache in caches:
260-
with TestRun.group(f"Cache {cache.cache_id}"):
261-
for core in cache.get_cores():
262-
core_info = f"Core {core.cache_id}-{core.core_id} ," if per_core else ""
354+
for core in cache.get_cores():
355+
with TestRun.step(f"Core {core.cache_id}-{core.core_id}"):
263356
for class_id in range(ioclass_config.MAX_IO_CLASS_ID + 1):
264-
with TestRun.group(core_info + f"IO class id {class_id}"):
265-
statistics = (
266-
core.get_statistics_flat(class_id, [stat_filter]) if per_core
267-
else cache.get_statistics_flat(class_id, [stat_filter]))
268-
validate_statistics(statistics, stat_filter, per_core)
269-
if stat_filter == StatsFilter.conf: # no percentage statistics for conf
270-
continue
271-
statistics_percents = (
272-
core.get_statistics_flat(
273-
class_id, [stat_filter], percentage_val=True) if per_core
274-
else cache.get_statistics_flat(
275-
class_id, [stat_filter], percentage_val=True))
276-
validate_statistics(statistics_percents, stat_filter, per_core)
277-
if not per_core:
278-
break
279-
280-
281-
def get_checked_statistics(stat_filter: StatsFilter, per_core: bool):
357+
statistics = get_stats_dict(
358+
[stat_filter], core.cache_id, core.core_id, class_id)
359+
validate_statistics(statistics, stat_filter)
360+
361+
362+
def get_checked_statistics(stat_filter: StatsFilter):
282363
if stat_filter == StatsFilter.conf:
283364
return config_stats_ioclass
284365
if stat_filter == StatsFilter.usage:
285366
return usage_stats_ioclass
286-
if stat_filter == StatsFilter.blk:
287-
return block_stats_core if per_core else block_stats_cache
288367
if stat_filter == StatsFilter.req:
289368
return request_stats
369+
if stat_filter == StatsFilter.blk:
370+
return block_stats
371+
if stat_filter == StatsFilter.err:
372+
return error_stats
290373

291374

292-
def validate_statistics(statistics: dict, stat_filter: StatsFilter, per_core: bool):
293-
for stat_name in get_checked_statistics(stat_filter, per_core):
375+
def validate_statistics(statistics: dict, stat_filter: StatsFilter):
376+
for stat_name in get_checked_statistics(stat_filter):
294377
if stat_name not in statistics.keys():
295378
TestRun.LOGGER.error(f"Value for {stat_name} not displayed in output")
296379
else:

0 commit comments

Comments
 (0)