Merge ~sylvain-pineau/checkbox-support:usb_read_write into checkbox-support:master

Proposed by Sylvain Pineau
Status: Merged
Approved by: Sylvain Pineau
Approved revision: 6d8deed3e1f94cb6ab1d6a6ad22149abdedbb396
Merged at revision: 512270690e5a309ca9d8ebf3bde520f889a678ae
Proposed branch: ~sylvain-pineau/checkbox-support:usb_read_write
Merge into: checkbox-support:master
Diff against target: 377 lines (+354/-0)
3 files modified
checkbox_support/scripts/network.py (+0/-0)
checkbox_support/scripts/usb_read_write.py (+352/-0)
setup.py (+2/-0)
Reviewer Review Type Date Requested Status
Jonathan Cave (community) Approve
Maciej Kisielewski Approve
Review via email: mp+325373@code.launchpad.net

Description of the change

New console script entry point for usb_read_write, to use the same version for checkbox-plano and checkbox-snappy.

It's really just a copy of https://git.launchpad.net/plainbox-provider-snappy/tree/bin/usb_read_write

tested using:

sylvain@sylvain-ThinkPad-T430s:~/lp/checkbox-support$ sudo !!
sudo USB_RWTEST_PARTITIONS=sdb1 checkbox-support-usb_read_write
PASS: WRITING TEST: /tmp/tmpd5_03uve/tmpzm6djfsu0
PASS: WRITING TEST: /tmp/tmpd5_03uve/tmpzm6djfsu1
[...]

To post a comment you must log in.
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

Goodies, thanks! +1

review: Approve
Revision history for this message
Jonathan Cave (jocave) wrote :

Looks like a straight import to me +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/checkbox_support/scripts/network.py b/checkbox_support/scripts/network.py
2old mode 100755
3new mode 100644
4index f66c4ad..f66c4ad
5--- a/checkbox_support/scripts/network.py
6+++ b/checkbox_support/scripts/network.py
7diff --git a/checkbox_support/scripts/usb_read_write.py b/checkbox_support/scripts/usb_read_write.py
8new file mode 100644
9index 0000000..c589def
10--- /dev/null
11+++ b/checkbox_support/scripts/usb_read_write.py
12@@ -0,0 +1,352 @@
13+#!/usr/bin/env python3
14+# Copyright 2015 - 2016 Canonical Ltd.
15+# All rights reserved.
16+#
17+# Written by:
18+# Taihsiang Ho <taihsiang.ho@canonical.com>
19+
20+"""
21+Script to test the performance of USB read/write.
22+
23+This script is intended be used as part of a plainbox provider.
24+It is expected to find the partition of the usb storage
25+inserted in the previous insertion test
26+and use the info to mount, read and write the USB.
27+
28+The test is performed by the following steps:
29+ 1. create a random file, say a "source file"
30+ 2. mount the USB storage with the folder FOLDER_TO_MOUNT
31+ 3. copy the source file into FOLDER_TO_MOUNT by dd REPETITION_NUM times.
32+ 4. access the md5sum numbers of the files copied into FOLDER_TO_MOUNT
33+ 5. compare the md5sum numbers with the md5sum of the source file.
34+ 6. report the result and return associated values back to plainbox.
35+"""
36+
37+import sys
38+import subprocess
39+import os
40+import collections
41+import tempfile
42+import logging
43+import errno
44+import contextlib
45+
46+
47+PLAINBOX_SESSION_SHARE = os.environ.get('PLAINBOX_SESSION_SHARE', '')
48+FOLDER_TO_MOUNT = tempfile.mkdtemp()
49+REPETITION_NUM = 5 # number to repeat the read/write test units.
50+# Prepare a random file which size is RANDOM_FILE_SIZE.
51+# Default 1048576 = 1MB to perform writing test
52+RANDOM_FILE_SIZE = 104857600
53+USB_INSERT_INFO = "usb_insert_info"
54+
55+log_path = os.path.join(PLAINBOX_SESSION_SHARE, 'usb-rw.log')
56+logging.basicConfig(level=logging.DEBUG, filename=log_path)
57+
58+
59+class RandomData():
60+ """
61+ Class to create data files.
62+
63+ This class is ported from the original checkbox script.
64+ """
65+
66+ def __init__(self, size):
67+ """
68+ init method of class RandomData.
69+
70+ :param size:
71+ an integer to decide the size of the generated random file in byte.
72+ """
73+ self.tfile = tempfile.NamedTemporaryFile(delete=False)
74+ self.path = ''
75+ self.name = ''
76+ self.path, self.name = os.path.split(self.tfile.name)
77+ self._write_test_data_file(size)
78+
79+ def _generate_test_data(self):
80+ seed = "104872948765827105728492766217823438120"
81+ phrase = '''
82+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam
83+ nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat
84+ volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation
85+ ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
86+ Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse
87+ molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero
88+ eros et accumsan et iusto odio dignissim qui blandit praesent luptatum
89+ zzril delenit augue duis dolore te feugait nulla facilisi.
90+ '''
91+ words = phrase.replace('\n', '').split()
92+ word_deque = collections.deque(words)
93+ seed_deque = collections.deque(seed)
94+ while True:
95+ yield ' '.join(list(word_deque))
96+ word_deque.rotate(int(seed_deque[0]))
97+ seed_deque.rotate(1)
98+
99+ def _write_test_data_file(self, size):
100+ data = self._generate_test_data()
101+ while os.path.getsize(self.tfile.name) < size:
102+ self.tfile.write(next(data).encode('UTF-8'))
103+ return self
104+
105+
106+def get_partition_info():
107+ """
108+ get partition info.
109+
110+ use a cache file provided by usb insertion test
111+ to get the partition to mount later
112+
113+ return: a string which is a partition name. e.g. sdb1
114+ """
115+ if not PLAINBOX_SESSION_SHARE:
116+ logging.error("no PLAINBOX_SESSION_SHARE is defined.")
117+ sys.exit(1)
118+ file_lines = ""
119+ info_path = os.path.join(PLAINBOX_SESSION_SHARE, USB_INSERT_INFO)
120+ try:
121+ with open(info_path, "r") as file_usb_insert_info:
122+ file_lines = file_usb_insert_info.readlines()
123+ except OSError as e:
124+ if e.errno == errno.ENOENT:
125+ logging.error("%s info file was not found. \
126+ Did the insertion test was run successfully?"
127+ % USB_INSERT_INFO)
128+ sys.exit(1)
129+ # TODO: need to be smarter
130+ if len(file_lines) == 1:
131+ partition = file_lines[0].strip()
132+ else:
133+ logging.error("has no idea which partition to mount or not found")
134+ sys.exit(1)
135+
136+ return partition
137+
138+
139+def run_read_write_test():
140+ """try to mount the partition candidates."""
141+ # random file as a benchmark, a "source" file
142+ with gen_random_file() as random_file:
143+ # initialize the necessary tasks before performing read/write test
144+ partitions = os.environ.get('USB_RWTEST_PARTITIONS', '').split()
145+ if not partitions:
146+ partitions = [get_partition_info()]
147+ for partition in partitions:
148+ with mount_usb_storage(partition):
149+ # write test
150+ write_test(random_file)
151+ # already write some data into the target
152+ # so let's read it to perform the read test
153+ # and validate the writing correctness
154+ read_test(random_file)
155+
156+
157+@contextlib.contextmanager
158+def mount_usb_storage(partition):
159+ """
160+ initialize the configuration so we could get ready to test jobs.
161+
162+ get everything ready to have the read/write test, including
163+ 1. create a temporary folder used for mounting devices
164+ 2. un-mount the target device at the very beginning if it was mounted
165+
166+ TODO: this function should be smarter and not completed enough
167+ """
168+ logging.debug("try to mount usb storage for testing")
169+
170+ try:
171+ device_to_mount = os.path.join("/dev", partition)
172+ # use pipe so I could hide message like
173+ # "umount: /tmp/tmpjzwb6lys: not mounted"
174+ subprocess.call(['umount', FOLDER_TO_MOUNT], stderr=subprocess.PIPE)
175+
176+ # mount the target device/partition
177+ # if the return code of the shell command is non-zero,
178+ # means something wrong.
179+ # quit this script and return a non-zero value to plainbox
180+ if subprocess.call(['mount', device_to_mount, FOLDER_TO_MOUNT]):
181+ logging.error("mount %s on %s failed."
182+ % (device_to_mount, FOLDER_TO_MOUNT))
183+ sys.exit(1)
184+ else:
185+ logging.debug("mount %s on %s successfully."
186+ % (device_to_mount, FOLDER_TO_MOUNT))
187+ yield
188+
189+ finally:
190+ logging.info("context manager exit: unmount USB storage")
191+ if subprocess.call(['umount', FOLDER_TO_MOUNT]):
192+ logging.warning("umount %s failed." % FOLDER_TO_MOUNT)
193+ else:
194+ logging.info("umount %s successfully." % FOLDER_TO_MOUNT)
195+
196+
197+def read_test(random_file):
198+ """perform the read test."""
199+ logging.debug("===================")
200+ logging.debug("reading test begins")
201+ logging.debug("===================")
202+
203+ read_test_list = []
204+ for idx in range(REPETITION_NUM):
205+ read_test_list.append(read_test_unit(random_file, str(idx)))
206+ print('PASS: all reading tests passed.')
207+
208+
209+def read_test_unit(random_source_file, idx=""):
210+ """
211+ perform the read test.
212+
213+ :param random_source_file: a RandomData object
214+ :param idx: a idx to label the files to be compared with the source file.
215+ It is an int string, "1", "2", "3", ......etc.
216+ """
217+ # access the temporary file
218+ path_random_file = os.path.join(
219+ FOLDER_TO_MOUNT, os.path.basename(random_source_file.tfile.name)) + idx
220+ # get the md5sum of the temp random files to compare
221+ process = subprocess.Popen(['md5sum', path_random_file],
222+ stdout=subprocess.PIPE)
223+ tfile_md5sum = process.communicate()[0].decode().split(" ")[0]
224+ # get the md5sum of the source random file
225+ process = subprocess.Popen(['md5sum', random_source_file.tfile.name],
226+ stdout=subprocess.PIPE)
227+ source_md5sum = process.communicate()[0].decode().split(" ")[0]
228+
229+ logging.debug("%s %s (verified)" % (tfile_md5sum, path_random_file))
230+ logging.debug("%s %s (source)"
231+ % (source_md5sum, random_source_file.tfile.name))
232+
233+ # Clean the target file
234+ os.remove(path_random_file)
235+
236+ # verify the md5sum
237+ if tfile_md5sum == source_md5sum:
238+ print("PASS: READING TEST: %s passes md5sum comparison."
239+ % path_random_file)
240+ else:
241+ # failed in the reading test
242+ # tell plainbox the failure code
243+ logging.warning("FAIL: READING TEST: %s failed in md5sum comparison."
244+ % path_random_file)
245+ sys.exit(1)
246+
247+
248+def write_test(random_file):
249+ """perform a writing test."""
250+ logging.debug("===================")
251+ logging.debug("writing test begins")
252+ logging.debug("===================")
253+
254+ write_speed_list = []
255+ for idx in range(REPETITION_NUM):
256+ write_speed_list.append(write_test_unit(random_file, str(idx)))
257+ average_speed = sum(write_speed_list)/REPETITION_NUM
258+ file_size_in_mb = RANDOM_FILE_SIZE / (1024*1024)
259+ print("Average writing speed is: {:.3f} MB/s "
260+ "({}x{} MB files were written)".format(
261+ average_speed, REPETITION_NUM, file_size_in_mb))
262+
263+
264+def write_test_unit(random_file, idx=""):
265+ """
266+ perform the writing test.
267+
268+ :param random_file: a RndomData object created to be written
269+ :return: a float in MB/s to denote writing speed
270+ """
271+ target_file = os.path.join(
272+ FOLDER_TO_MOUNT, os.path.basename(random_file.tfile.name)) + idx
273+ process = subprocess.Popen([
274+ 'dd', 'if=' + random_file.tfile.name, 'of=' + target_file],
275+ stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
276+ logging.debug("Apply command: %s" % process.args)
277+ # will get something like
278+ # ['2048+1 records in', '2048+1 records out',
279+ # '1049076 bytes (1.0 MB) copied, 0.00473357 s, 222 MB/s', '']
280+ list_dd_message = process.communicate()[0].decode().split("\n")
281+ logging.debug(list_dd_message)
282+ logging.debug(get_md5sum(target_file))
283+
284+ try:
285+ dd_speed = float(list_dd_message[2].split(" ")[-2])
286+ print("PASS: WRITING TEST: %s" % target_file)
287+ except:
288+ # Example:
289+ # ['dd: writing to ‘/tmp/tmp08osy45j/tmpnek46on30’: Input/output error'
290+ # , '38913+0 records in', '38912+0 records out', '19922944 bytes
291+ # (20 MB) copied, 99.647 s, 200 kB/s', '']
292+ print("ERROR: {}".format(list_dd_message))
293+ sys.exit(1)
294+
295+ return dd_speed
296+
297+
298+@contextlib.contextmanager
299+def gen_random_file():
300+ """
301+ generate a random file which size is RANDOM_FILE_SIZE.
302+
303+ :return: a RandomData object
304+ """
305+ logging.debug("generating a random file")
306+
307+ try:
308+ # 1048576 = 1024 * 1024
309+ # we are going to generate a 1M file
310+ random_file = RandomData(RANDOM_FILE_SIZE)
311+ # flush the remaining data in the memory buffer
312+ # otherwise the md5sum will be different if you
313+ # check it manually from your shell command md5sum
314+ random_file.tfile.file.flush()
315+
316+ yield random_file
317+
318+ finally:
319+ logging.info("Remove temporary folders and files.")
320+ # delete the mount folder
321+ try:
322+ os.rmdir(FOLDER_TO_MOUNT)
323+ except OSError:
324+ logging.warning("Failed to remove %s (mount folder not empty)."
325+ % FOLDER_TO_MOUNT)
326+ # delete the random file (source file of a writing test)
327+ os.unlink(random_file.tfile.name)
328+
329+
330+def get_md5sum(file_to_check):
331+ """
332+ get md5sum of file_to_check.
333+
334+ :param file_to_check: the absolute path of a file to access its md5sum
335+ :return: md5sum as a string
336+ """
337+ try:
338+ # return the md5sum of the temp file
339+ process = subprocess.Popen(['md5sum', file_to_check],
340+ stdout=subprocess.PIPE)
341+ # something like
342+ # (b'07bc8f96b7c7dba2c1f3eb2f7dd50541 /tmp/tmp9jnuv329\n', None)
343+ # will be returned by communicate() in this case
344+ md5sum = process.communicate()[0].decode().split(" ")[0]
345+
346+ if md5sum:
347+ logging.debug("MD5SUM: of %s \t\t\t\t\t%s"
348+ % (file_to_check, md5sum))
349+ return md5sum
350+ else:
351+ logging.error("Could not found file to check its MD5SUM. \
352+ Check the folder permission?")
353+ sys.exit(1)
354+
355+ except OSError as e:
356+ if e.errno == errno.ENOENT:
357+ logging.error("%s info file was not found. \
358+ Did the insertion test was run successfully?"
359+ % USB_INSERT_INFO)
360+ sys.exit(1)
361+
362+
363+if __name__ == "__main__":
364+ run_read_write_test()
365diff --git a/setup.py b/setup.py
366index c95ea76..dc1aa37 100755
367--- a/setup.py
368+++ b/setup.py
369@@ -82,6 +82,8 @@ setup(
370 "checkbox_support.scripts.fwts_test:main"),
371 ("checkbox-support-network="
372 "checkbox_support.scripts.network:main"),
373+ ("checkbox-support-usb_read_write="
374+ "checkbox_support.scripts.usb_read_write:run_read_write_test"),
375 ],
376 },
377 )

Subscribers

People subscribed via source and target branches