xref: /aosp_15_r20/external/autotest/client/site_tests/hardware_TrimIntegrity/hardware_TrimIntegrity.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import os, fcntl, logging, struct, random
7
8from autotest_lib.client.bin import test, utils
9from autotest_lib.client.common_lib import error
10
11
12class hardware_TrimIntegrity(test.test):
13    """
14    Performs data integrity trim test on an unmounted partition.
15
16    This test will write 1 GB of data and verify that trimmed data are gone and
17    untrimmed data are unaffected. The verification will be run in 5 passes with
18    0%, 25%, 50%, 75%, and 100% of data trimmed.
19
20    Also, perform 4K random read QD32 before and after trim. We should see some
21    speed / latency difference if the device firmware trim data properly.
22
23    Condition for test result:
24    - Trim command is not supported
25      -> Target disk is a harddisk           : TestNA
26      -> Target disk is SCSI disk w/o trim   : TestNA
27      -> Otherwise                           : TestFail
28    - Can not verify integrity of untrimmed data
29      -> All case                            : TestFail
30    - Trim data is not Zero
31      -> SSD with RZAT                       : TestFail
32      -> NVMe with dlfeat:1                  : TestFail
33      -> Otherwise                           : TestNA
34    """
35
36    version = 1
37    FILE_SIZE = 1024 * 1024 * 1024
38    CHUNK_SIZE = 192 * 1024
39    TRIM_RATIO = [0, 0.25, 0.5, 0.75, 1]
40
41    hdparm_trim = 'Data Set Management TRIM supported'
42    hdparm_rzat = 'Deterministic read ZEROs after TRIM'
43    nvme_dlfeat = 'dlfeat'
44
45    # Use hash value to check integrity of the random data.
46    HASH_CMD = 'sha256sum | cut -d" " -f 1'
47    # 0x1277 is ioctl BLKDISCARD command
48    IOCTL_TRIM_CMD = 0x1277
49    IOCTL_NOT_SUPPORT_ERRNO = 95
50
51    def _get_hash(self, chunk_count, chunk_size):
52        """
53        Get hash for every chunk of data.
54        """
55        cmd = str('for i in $(seq 0 %d); do dd if=%s of=/dev/stdout bs=%d'
56                  ' count=1 skip=$i iflag=direct | %s; done' %
57                  (chunk_count - 1, self._filename, chunk_size, self.HASH_CMD))
58        return utils.run(cmd).stdout.split()
59
60    def _do_trim(self, fd, offset, size):
61        """
62        Invoke ioctl to trim command.
63        """
64        fcntl.ioctl(fd, self.IOCTL_TRIM_CMD, struct.pack('QQ', offset, size))
65
66    def _verify_trim_support(self, size):
67        """
68        Check for trim support in ioctl. Raise TestNAError if not support.
69
70        @param size: size to try the trim command
71        """
72        try:
73            fd = os.open(self._filename, os.O_RDWR, 0o666)
74            self._do_trim(fd, 0, size)
75        except IOError as err:
76            if err.errno == self.IOCTL_NOT_SUPPORT_ERRNO:
77                reason = 'IOCTL Does not support trim.'
78                msg = utils.get_storage_error_msg(self._diskname, reason)
79
80                if utils.is_disk_scsi(self._diskname):
81                    if utils.is_disk_harddisk(self._diskname):
82                        msg += ' Disk is a hard disk.'
83                        raise error.TestNAError(msg)
84                    if utils.verify_hdparm_feature(self._diskname,
85                                                   self.hdparm_trim):
86                        msg += ' Disk claims trim supported.'
87                    else:
88                        msg += ' Disk does not claim trim supported.'
89                        raise error.TestNAError(msg)
90                # SSD with trim support / mmc / sd card
91                raise error.TestFail(msg)
92            else:
93                raise
94        finally:
95            os.close(fd)
96
97    def initialize(self):
98        self.job.use_sequence_number = True
99
100    def run_once(self, filename=None, file_size=FILE_SIZE,
101                 chunk_size=CHUNK_SIZE, trim_ratio=TRIM_RATIO):
102        """
103        Executes the test and logs the output.
104        @param file_name:  file/disk name to test
105                           default: spare partition of internal disk
106        @param file_size:  size of data to test. default: 1GB
107        @param chunk_size: size of chunk to calculate hash/trim. default: 64KB
108        @param trim_ratio: list of ratio of file size to trim data
109                           default: [0, 0.25, 0.5, 0.75, 1]
110        """
111
112        if not filename:
113            self._diskname = utils.get_fixed_dst_drive()
114            if self._diskname == utils.get_root_device():
115                self._filename = utils.get_free_root_partition()
116            else:
117                self._filename = self._diskname
118        else:
119            self._filename = filename
120            self._diskname = utils.get_disk_from_filename(filename)
121
122        if file_size == 0:
123            fulldisk = True
124            file_size = utils.get_disk_size(self._filename)
125            if file_size == 0:
126                cmd = ('%s seem to have 0 storage block. Is the media present?'
127                        % filename)
128                raise error.TestError(cmd)
129        else:
130            fulldisk = False
131
132        # Make file size multiple of 4 * chunk size
133        file_size -= file_size % (4 * chunk_size)
134
135        logging.info('filename: %s, filesize: %d', self._filename, file_size)
136
137        self._verify_trim_support(chunk_size)
138
139        # Calculate hash value for zero'ed and one'ed data
140        cmd = str('dd if=/dev/zero bs=%d count=1 | %s' %
141                  (chunk_size, self.HASH_CMD))
142        zero_hash = utils.run(cmd).stdout.strip()
143
144        cmd = str("dd if=/dev/zero bs=%d count=1 | tr '\\0' '\\xff' | %s" %
145                  (chunk_size, self.HASH_CMD))
146        one_hash = utils.run(cmd).stdout.strip()
147
148        trim_hash = ""
149
150        # Write random data to disk
151        chunk_count = file_size // chunk_size
152        cmd = str('dd if=/dev/urandom of=%s bs=%d count=%d oflag=direct' %
153                  (self._filename, chunk_size, chunk_count))
154        utils.run(cmd)
155
156        ref_hash = self._get_hash(chunk_count, chunk_size)
157
158        # Check read speed/latency when reading real data.
159        self.job.run_test('hardware_StorageFio',
160                          disable_sysinfo=True,
161                          filesize=file_size,
162                          blkdiscard=False,
163                          requirements=[('4k_read_qd32', [])],
164                          tag='before_trim')
165
166        # Generate random order of chunk to trim
167        trim_order = list(range(0, chunk_count))
168        random.shuffle(trim_order)
169        trim_status = [False] * chunk_count
170
171        # Init stat variable
172        data_verify_count = 0
173        data_verify_match = 0
174        trim_verify_count = 0
175        trim_verify_zero = 0
176        trim_verify_one = 0
177        trim_verify_non_delete = 0
178        trim_deterministic = True
179
180        last_ratio = 0
181        for ratio in trim_ratio:
182
183            # Do trim
184            begin_trim_chunk = int(last_ratio * chunk_count)
185            end_trim_chunk = int(ratio * chunk_count)
186            fd = os.open(self._filename, os.O_RDWR, 0o666)
187            for chunk in trim_order[begin_trim_chunk:end_trim_chunk]:
188                self._do_trim(fd, chunk * chunk_size, chunk_size)
189                trim_status[chunk] = True
190            os.close(fd)
191            last_ratio = ratio
192
193            cur_hash = self._get_hash(chunk_count, chunk_size)
194
195            trim_verify_count += int(ratio * chunk_count)
196            data_verify_count += chunk_count - int(ratio * chunk_count)
197
198            # Verify hash
199            for cur, ref, trim in zip(cur_hash, ref_hash, trim_status):
200                if trim:
201                    if not trim_hash:
202                        trim_hash = cur
203                    elif cur != trim_hash:
204                        trim_deterministic = False
205
206                    if cur == zero_hash:
207                        trim_verify_zero += 1
208                    elif cur == one_hash:
209                        trim_verify_one += 1
210                    elif cur == ref:
211                        trim_verify_non_delete += 1
212                else:
213                    if cur == ref:
214                        data_verify_match += 1
215
216        keyval = dict()
217        keyval['data_verify_count'] = data_verify_count
218        keyval['data_verify_match'] = data_verify_match
219        keyval['trim_verify_count'] = trim_verify_count
220        keyval['trim_verify_zero'] = trim_verify_zero
221        keyval['trim_verify_one'] = trim_verify_one
222        keyval['trim_verify_non_delete'] = trim_verify_non_delete
223        keyval['trim_deterministic'] = trim_deterministic
224        self.write_perf_keyval(keyval)
225
226        # Check read speed/latency when reading from trimmed data.
227        self.job.run_test('hardware_StorageFio',
228                          disable_sysinfo=True,
229                          filesize=file_size,
230                          blkdiscard=False,
231                          requirements=[('4k_read_qd32', [])],
232                          tag='after_trim')
233
234        if data_verify_match < data_verify_count:
235            reason = 'Fail to verify untrimmed data.'
236            msg = utils.get_storage_error_msg(self._diskname, reason)
237            raise error.TestFail(msg)
238
239        if trim_verify_zero <  trim_verify_count:
240            reason = 'Trimmed data are not zeroed.'
241            msg = utils.get_storage_error_msg(self._diskname, reason)
242            if utils.is_disk_scsi(self._diskname):
243                if utils.verify_hdparm_feature(self._diskname,
244                                               self.hdparm_rzat):
245                    msg += ' Disk claim deterministic read zero after trim.'
246                    raise error.TestFail(msg)
247            elif utils.is_disk_nvme(self._diskname):
248                dlfeat = utils.get_nvme_id_ns_feature(self._diskname,
249                                                      self.nvme_dlfeat)
250                if dlfeat == "None":
251                    msg += ' Expected values for trimmed data not reported.'
252                    raise error.TestNAError(msg)
253                elif int(dlfeat, 16) & 7 == 1:
254                    msg += ' Disk indicates values should be zero after trim.'
255                    raise error.TestFail(msg)
256                # TODO(asavery): NVMe 1.3 specification allows all bytes set
257                # to FF from a deallocated logical block
258                elif int(dlfeat, 16) & 7 == 2:
259                    msg += ' Unexpected values, test does not check for ones.'
260                    raise error.TestFail(msg)
261                else:
262                    msg += ' Expected values for trimmed data not specified.'
263                    raise error.TestNAError(msg)
264            raise error.TestNAError(msg)
265