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
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
2722from connection .utils .output import CmdException
2823from core .test_run import TestRun
2924from storage_devices .disk import DiskType , DiskTypeSet , DiskTypeLowerThan
3833mountpoint = "/tmp/cas1-1"
3934cache_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