VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testdriver/testfileset.py@ 79208

Last change on this file since 79208 was 79208, checked in by vboxsync, 5 years ago

ValKit/tdAddGuestCtrl.py: More cleanups in the copyto area. bugref:9320

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 18.9 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: testfileset.py 79208 2019-06-18 12:00:44Z vboxsync $
3# pylint: disable=too-many-lines
4
5"""
6Test File Set
7"""
8
9__copyright__ = \
10"""
11Copyright (C) 2010-2019 Oracle Corporation
12
13This file is part of VirtualBox Open Source Edition (OSE), as
14available from http://www.virtualbox.org. This file is free software;
15you can redistribute it and/or modify it under the terms of the GNU
16General Public License (GPL) as published by the Free Software
17Foundation, in version 2 as it comes in the "COPYING" file of the
18VirtualBox OSE distribution. VirtualBox OSE is distributed in the
19hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
20
21The contents of this file may alternatively be used under the terms
22of the Common Development and Distribution License Version 1.0
23(CDDL) only, as it comes in the "COPYING.CDDL" file of the
24VirtualBox OSE distribution, in which case the provisions of the
25CDDL are applicable instead of those of the GPL.
26
27You may elect to license modified versions of this file under the
28terms and conditions of either the GPL or the CDDL or both.
29"""
30__version__ = "$Revision: 79208 $"
31
32
33# Standard Python imports.
34import os;
35import random;
36import string;
37import sys;
38import tarfile;
39import unittest;
40
41# Validation Kit imports.
42from common import utils;
43from common import pathutils;
44from testdriver import reporter;
45
46# Python 3 hacks:
47if sys.version_info[0] >= 3:
48 xrange = range; # pylint: disable=redefined-builtin,invalid-name
49
50
51
52class TestFsObj(object):
53 """ A file system object we created in for test purposes. """
54 def __init__(self, oParent, sPath, sName = None):
55 self.oParent = oParent # type: TestDir
56 self.sPath = sPath # type: str
57 self.sName = sName # type: str
58 if oParent:
59 assert sPath.startswith(oParent.sPath);
60 assert sName is None;
61 self.sName = sPath[len(oParent.sPath) + 1:];
62 # Add to parent.
63 oParent.aoChildren.append(self);
64 oParent.dChildrenUpper[self.sName.upper()] = self;
65
66 def buildPath(self, sRoot, sSep):
67 """
68 Build the path from sRoot using sSep.
69
70 This is handy for getting the path to an object in a different context
71 (OS, path) than what it was generated for.
72 """
73 if self.oParent:
74 return self.oParent.buildPath(sRoot, sSep) + sSep + self.sName;
75 return sRoot + sSep + self.sName;
76
77
78class TestFile(TestFsObj):
79 """ A file object in the guest. """
80 def __init__(self, oParent, sPath, abContent):
81 TestFsObj.__init__(self, oParent, sPath);
82 self.abContent = abContent # type: bytearray
83 self.cbContent = len(abContent);
84 self.off = 0;
85
86 def read(self, cbToRead):
87 """ read() emulation. """
88 assert self.off <= self.cbContent;
89 cbLeft = self.cbContent - self.off;
90 if cbLeft < cbToRead:
91 cbToRead = cbLeft;
92 abRet = self.abContent[self.off:(self.off + cbToRead)];
93 assert len(abRet) == cbToRead;
94 self.off += cbToRead;
95 if sys.version_info[0] < 3:
96 return bytes(abRet);
97 return abRet;
98
99 def equalFile(self, oFile):
100 """ Compares the content of oFile with self.abContent. """
101
102 # Check the size first.
103 try:
104 cbFile = os.fstat(oFile.fileno()).st_size;
105 except:
106 return reporter.errorXcpt();
107 if cbFile != self.cbContent:
108 return reporter.error('file size differs: %s, cbContent=%s' % (cbFile, self.cbContent));
109
110 # Compare the bytes next.
111 offFile = 0;
112 try:
113 oFile.seek(offFile);
114 except:
115 return reporter.error('seek error');
116 while offFile < self.cbContent:
117 cbToRead = self.cbContent - offFile;
118 if cbToRead > 256*1024:
119 cbToRead = 256*1024;
120 try:
121 abRead = oFile.read(cbToRead);
122 except:
123 return reporter.error('read error at offset %s' % (offFile,));
124 cbRead = len(abRead);
125 if cbRead == 0:
126 return reporter.error('premature end of file at offset %s' % (offFile,));
127 if not utils.areBytesEqual(abRead, self.abContent[offFile:(offFile + cbRead)]):
128 return reporter.error('%s byte block at offset %s differs' % (cbRead, offFile,));
129 # Advance:
130 offFile += cbRead;
131
132 return True;
133
134
135class TestDir(TestFsObj):
136 """ A file object in the guest. """
137 def __init__(self, oParent, sPath, sName = None):
138 TestFsObj.__init__(self, oParent, sPath, sName);
139 self.aoChildren = [] # type: list(TestFsObj)
140 self.dChildrenUpper = {} # type: dict(str, TestFsObj)
141
142 def contains(self, sName):
143 """ Checks if the directory contains the given name. """
144 return sName.upper() in self.dChildrenUpper
145
146
147class TestFileSet(object):
148 """
149 A generated set of files and directories for use in a test.
150
151 Can be wrapped up into a tarball or written directly to the file system.
152 """
153
154 ksReservedWinOS2 = '/\\"*:<>?|\t\v\n\r\f\a\b';
155 ksReservedUnix = '/';
156 ksReservedTrailingWinOS2 = ' .';
157 ksReservedTrailingUnix = '';
158
159 ## @name Path style.
160 ## @{
161
162 ## @}
163
164 def __init__(self, fDosStyle, sBasePath, sSubDir, # pylint: disable=too-many-arguments
165 asCompatibleWith = None, # List of getHostOs values to the names must be compatible with.
166 oRngFileSizes = xrange(0, 16384),
167 oRngManyFiles = xrange(128, 512),
168 oRngTreeFiles = xrange(128, 384),
169 oRngTreeDepth = xrange(92, 256),
170 oRngTreeDirs = xrange(2, 16),
171 cchMaxPath = 230,
172 cchMaxName = 230,
173 uSeed = None):
174 ## @name Parameters
175 ## @{
176 self.fDosStyle = fDosStyle;
177 self.sMinStyle = 'win' if fDosStyle else 'linux';
178 if asCompatibleWith is not None:
179 for sOs in asCompatibleWith:
180 assert sOs in ('win', 'os2', 'darwin', 'linux', 'solaris',), sOs;
181 if 'os2' in asCompatibleWith:
182 self.sMinStyle = 'os2';
183 elif 'win' in asCompatibleWith:
184 self.sMinStyle = 'win';
185 self.sBasePath = sBasePath;
186 self.sSubDir = sSubDir;
187 self.oRngFileSizes = oRngFileSizes;
188 self.oRngManyFiles = oRngManyFiles;
189 self.oRngTreeFiles = oRngTreeFiles;
190 self.oRngTreeDepth = oRngTreeDepth;
191 self.oRngTreeDirs = oRngTreeDirs;
192 self.cchMaxPath = cchMaxPath;
193 self.cchMaxName = cchMaxName
194 ## @}
195
196 ## @name Charset stuff
197 ## @todo allow more chars for unix hosts + guests.
198 ## @todo include unicode stuff, except on OS/2 and DOS.
199 ## @{
200 ## The filename charset.
201 self.sFileCharset = string.printable;
202 ## Set of characters that should not trail a guest filename.
203 self.sReservedTrailing = self.ksReservedTrailingWinOS2;
204 if self.sMinStyle in ('win', 'os2'):
205 for ch in self.ksReservedWinOS2:
206 self.sFileCharset = self.sFileCharset.replace(ch, '');
207 else:
208 self.sReservedTrailing = self.ksReservedTrailingUnix;
209 for ch in self.ksReservedUnix:
210 self.sFileCharset = self.sFileCharset.replace(ch, '');
211 # More spaces and dot:
212 self.sFileCharset += ' ...';
213 ## @}
214
215 ## The root directory.
216 self.oRoot = None # type: TestDir;
217 ## An empty directory (under root).
218 self.oEmptyDir = None # type: TestDir;
219
220 ## A directory with a lot of files in it.
221 self.oManyDir = None # type: TestDir;
222
223 ## A directory with a mixed tree structure under it.
224 self.oTreeDir = None # type: TestDir;
225 ## Number of files in oTreeDir.
226 self.cTreeFiles = 0;
227 ## Number of directories under oTreeDir.
228 self.cTreeDirs = 0;
229 ## Number of other file types under oTreeDir.
230 self.cTreeOthers = 0;
231
232 ## All directories in creation order.
233 self.aoDirs = [] # type: list(TestDir);
234 ## All files in creation order.
235 self.aoFiles = [] # type: list(TestFile);
236 ## Path to object lookup.
237 self.dPaths = {} # type: dict(str, TestFsObj);
238
239 #
240 # Do the creating.
241 #
242 self.uSeed = uSeed if uSeed is not None else utils.timestampMilli();
243 self.oRandom = random.Random();
244 self.oRandom.seed(self.uSeed);
245 reporter.log('prepareGuestForTesting: random seed %s' % (self.uSeed,));
246
247 self.__createTestStuff();
248
249 def __createFilename(self, oParent, sCharset, sReservedTrailing):
250 """
251 Creates a filename contains random characters from sCharset and together
252 with oParent.sPath doesn't exceed the given max chars in length.
253 """
254 ## @todo Consider extending this to take UTF-8 and UTF-16 encoding so we
255 ## can safely use the full unicode range. Need to check how
256 ## RTZipTarCmd handles file name encoding in general...
257
258 if oParent:
259 cchMaxName = self.cchMaxPath - len(oParent.sPath) - 1;
260 else:
261 cchMaxName = self.cchMaxPath - 4;
262 if cchMaxName > self.cchMaxName:
263 cchMaxName = self.cchMaxName;
264 if cchMaxName <= 1:
265 cchMaxName = 2;
266
267 while True:
268 cchName = self.oRandom.randrange(1, cchMaxName);
269 sName = ''.join(self.oRandom.choice(sCharset) for _ in xrange(cchName));
270 if oParent is None or not oParent.contains(sName):
271 if sName[-1] not in sReservedTrailing:
272 if sName not in ('.', '..',):
273 return sName;
274 return ''; # never reached, but makes pylint happy.
275
276 def generateFilenameEx(self, cchMax = -1, cchMin = -1):
277 """
278 Generates a filename according to the given specs.
279
280 This is for external use, whereas __createFilename is for internal.
281
282 Returns generated filename.
283 """
284 assert cchMax == -1 or (cchMax >= 1 and cchMax > cchMin);
285 if cchMin <= 0:
286 cchMin = 1;
287 if cchMax < cchMin:
288 cchMax = self.cchMaxName;
289
290 while True:
291 cchName = self.oRandom.randrange(cchMin, cchMax + 1);
292 sName = ''.join(self.oRandom.choice(self.sFileCharset) for _ in xrange(cchName));
293 if sName[-1] not in self.sReservedTrailing:
294 if sName not in ('.', '..',):
295 return sName;
296 return ''; # never reached, but makes pylint happy.
297
298 def __createTestDir(self, oParent, sDir, sName = None):
299 """
300 Creates a test directory.
301 """
302 oDir = TestDir(oParent, sDir, sName);
303 self.aoDirs.append(oDir);
304 self.dPaths[sDir] = oDir;
305 return oDir;
306
307 def __createTestFile(self, oParent, sFile):
308 """
309 Creates a test file with random size up to cbMaxContent and random content.
310 """
311 cbFile = self.oRandom.choice(self.oRngFileSizes);
312 abContent = bytearray(self.oRandom.getrandbits(8) for _ in xrange(cbFile));
313
314 oFile = TestFile(oParent, sFile, abContent);
315 self.aoFiles.append(oFile);
316 self.dPaths[sFile] = oFile;
317 return oFile;
318
319 def __createTestStuff(self):
320 """
321 Create a random file set that we can work on in the tests.
322 Returns True/False.
323 """
324
325 #
326 # Create the root test dir.
327 #
328 sRoot = pathutils.joinEx(self.fDosStyle, self.sBasePath, self.sSubDir);
329 self.oRoot = self.__createTestDir(None, sRoot, self.sSubDir);
330 self.oEmptyDir = self.__createTestDir(self.oRoot, pathutils.joinEx(self.fDosStyle, sRoot, 'empty'));
331
332 #
333 # Create a directory with lots of files in it:
334 #
335 oDir = self.__createTestDir(self.oRoot, pathutils.joinEx(self.fDosStyle, sRoot, 'many'));
336 self.oManyDir = oDir;
337 cManyFiles = self.oRandom.choice(self.oRngManyFiles);
338 for _ in xrange(cManyFiles):
339 sName = self.__createFilename(oDir, self.sFileCharset, self.sReservedTrailing);
340 self.__createTestFile(oDir, pathutils.joinEx(self.fDosStyle, oDir.sPath, sName));
341
342 #
343 # Generate a tree of files and dirs.
344 #
345 oDir = self.__createTestDir(self.oRoot, pathutils.joinEx(self.fDosStyle, sRoot, 'tree'));
346 uMaxDepth = self.oRandom.choice(self.oRngTreeDepth);
347 cMaxFiles = self.oRandom.choice(self.oRngTreeFiles);
348 cMaxDirs = self.oRandom.choice(self.oRngTreeDirs);
349 self.oTreeDir = oDir;
350 self.cTreeFiles = 0;
351 self.cTreeDirs = 0;
352 uDepth = 0;
353 while self.cTreeFiles < cMaxFiles and self.cTreeDirs < cMaxDirs:
354 iAction = self.oRandom.randrange(0, 2+1);
355 # 0: Add a file:
356 if iAction == 0 and self.cTreeFiles < cMaxFiles and len(oDir.sPath) < 230 - 2:
357 sName = self.__createFilename(oDir, self.sFileCharset, self.sReservedTrailing);
358 self.__createTestFile(oDir, pathutils.joinEx(self.fDosStyle, oDir.sPath, sName));
359 self.cTreeFiles += 1;
360 # 1: Add a subdirector and descend into it:
361 elif iAction == 1 and self.cTreeDirs < cMaxDirs and uDepth < uMaxDepth and len(oDir.sPath) < 220:
362 sName = self.__createFilename(oDir, self.sFileCharset, self.sReservedTrailing);
363 oDir = self.__createTestDir(oDir, pathutils.joinEx(self.fDosStyle, oDir.sPath, sName));
364 self.cTreeDirs += 1;
365 uDepth += 1;
366 # 2: Ascend to parent dir:
367 elif iAction == 2 and uDepth > 0:
368 oDir = oDir.oParent;
369 uDepth -= 1;
370
371 return True;
372
373 def createTarball(self, sTarFileHst):
374 """
375 Creates a tarball on the host.
376 Returns success indicator.
377 """
378 reporter.log('Creating tarball "%s" with test files for the guest...' % (sTarFileHst,));
379
380 cchSkip = len(self.sBasePath) + 1;
381
382 # Open the tarball:
383 try:
384 oTarFile = tarfile.open(sTarFileHst, 'w:gz');
385 except:
386 return reporter.errorXcpt('Failed to open new tar file: %s' % (sTarFileHst,));
387
388 # Directories:
389 for oDir in self.aoDirs:
390 sPath = oDir.sPath[cchSkip:];
391 if self.fDosStyle:
392 sPath = sPath.replace('\\', '/');
393 oTarInfo = tarfile.TarInfo(sPath + '/');
394 oTarInfo.mode = 0o777;
395 oTarInfo.type = tarfile.DIRTYPE;
396 try:
397 oTarFile.addfile(oTarInfo);
398 except:
399 return reporter.errorXcpt('Failed adding directory tarfile: %s' % (oDir.sPath,));
400
401 # Files:
402 for oFile in self.aoFiles:
403 sPath = oFile.sPath[cchSkip:];
404 if self.fDosStyle:
405 sPath = sPath.replace('\\', '/');
406 oTarInfo = tarfile.TarInfo(sPath);
407 oTarInfo.size = len(oFile.abContent);
408 oFile.off = 0;
409 try:
410 oTarFile.addfile(oTarInfo, oFile);
411 except:
412 return reporter.errorXcpt('Failed adding directory tarfile: %s' % (oFile.sPath,));
413
414 # Complete the tarball.
415 try:
416 oTarFile.close();
417 except:
418 return reporter.errorXcpt('Error closing new tar file: %s' % (sTarFileHst,));
419 return True;
420
421 def writeToDisk(self, sAltBase = None):
422 """
423 Writes out the files to disk.
424 Returns True on success, False + error logging on failure.
425 """
426
427 # We only need to flip DOS slashes to unix ones, since windows & OS/2 can handle unix slashes.
428 fDosToUnix = self.fDosStyle and os.path.sep != '\\';
429
430 # The directories:
431 for oDir in self.aoDirs:
432 sPath = oDir.sPath;
433 if sAltBase:
434 if fDosToUnix:
435 sPath = sAltBase + sPath[len(self.sBasePath):].replace('\\', os.path.sep);
436 else:
437 sPath = sAltBase + sPath[len(self.sBasePath):];
438 elif fDosToUnix:
439 sPath = sPath.replace('\\', os.path.sep);
440
441 try:
442 os.mkdir(sPath, 0o770);
443 except:
444 return reporter.errorXcpt('mkdir(%s) failed' % (sPath,));
445
446 # The files:
447 for oFile in self.aoFiles:
448 sPath = oFile.sPath;
449 if sAltBase:
450 if fDosToUnix:
451 sPath = sAltBase + sPath[len(self.sBasePath):].replace('\\', os.path.sep);
452 else:
453 sPath = sAltBase + sPath[len(self.sBasePath):];
454 elif fDosToUnix:
455 sPath = sPath.replace('\\', os.path.sep);
456
457 try:
458 oOutFile = open(sPath, 'wb');
459 except:
460 return reporter.errorXcpt('open(%s, "wb") failed' % (sPath,));
461 try:
462 if sys.version_info[0] < 3:
463 oOutFile.write(bytes(oFile.abContent));
464 else:
465 oOutFile.write(oFile.abContent);
466 except:
467 try: oOutFile.close();
468 except: pass;
469 return reporter.errorXcpt('%s: write(%s bytes) failed' % (sPath, oFile.cbContent,));
470 try:
471 oOutFile.close();
472 except:
473 return reporter.errorXcpt('%s: close() failed' % (sPath,));
474
475 return True;
476
477
478 def chooseRandomFile(self):
479 """
480 Returns a random file.
481 """
482 return self.aoFiles[self.oRandom.choice(xrange(len(self.aoFiles)))];
483
484 def chooseRandomDirFromTree(self, fLeaf = False, fNonEmpty = False):
485 """
486 Returns a random directory from the tree (self.oTreeDir).
487 """
488 while True:
489 oDir = self.aoDirs[self.oRandom.choice(xrange(len(self.aoDirs)))];
490 # Check fNonEmpty requirement:
491 if not fNonEmpty or oDir.aoChildren:
492 # Check leaf requirement:
493 if not fLeaf:
494 for oChild in oDir.aoChildren:
495 if isinstance(oChild, TestDir):
496 continue; # skip it.
497
498 # Return if in the tree:
499 oParent = oDir.oParent;
500 while oParent is not None:
501 if oParent is self.oTreeDir:
502 return oDir;
503 oParent = oParent.oParent;
504 return None; # make pylint happy
505
506#
507# Unit testing.
508#
509
510# pylint: disable=missing-docstring
511# pylint: disable=undefined-variable
512class TestFileSetUnitTests(unittest.TestCase):
513 def testGeneral(self):
514 oSet = TestFileSet(False, '/tmp', 'unittest');
515 self.assertTrue(isinstance(oSet.chooseRandomDirFromTree(), TestDir));
516 self.assertTrue(isinstance(oSet.chooseRandomFile(), TestFile));
517
518
519if __name__ == '__main__':
520 unittest.main();
521 # not reached.
522
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette