VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/common/utils.py@ 52936

Last change on this file since 52936 was 52776, checked in by vboxsync, 11 years ago

fix OSE

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 44.4 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: utils.py 52776 2014-09-17 14:51:43Z vboxsync $
3# pylint: disable=C0302
4
5"""
6Common Utility Functions.
7"""
8
9__copyright__ = \
10"""
11Copyright (C) 2012-2014 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: 52776 $"
31
32
33# Standard Python imports.
34import datetime;
35import os;
36import platform;
37import re;
38import stat;
39import subprocess;
40import sys;
41import tarfile;
42import time;
43import traceback;
44import unittest;
45import zipfile
46
47if sys.platform == 'win32':
48 import win32api; # pylint: disable=F0401
49 import win32con; # pylint: disable=F0401
50 import win32console; # pylint: disable=F0401
51 import win32process; # pylint: disable=F0401
52else:
53 import signal;
54
55# Python 3 hacks:
56if sys.version_info[0] >= 3:
57 long = int; # pylint: disable=W0622,C0103
58
59
60#
61# Host OS and CPU.
62#
63
64def getHostOs():
65 """
66 Gets the host OS name (short).
67
68 See the KBUILD_OSES variable in kBuild/header.kmk for possible return values.
69 """
70 sPlatform = platform.system();
71 if sPlatform in ('Linux', 'Darwin', 'Solaris', 'FreeBSD', 'NetBSD', 'OpenBSD'):
72 sPlatform = sPlatform.lower();
73 elif sPlatform == 'Windows':
74 sPlatform = 'win';
75 elif sPlatform == 'SunOS':
76 sPlatform = 'solaris';
77 else:
78 raise Exception('Unsupported platform "%s"' % (sPlatform,));
79 return sPlatform;
80
81g_sHostArch = None;
82
83def getHostArch():
84 """
85 Gets the host CPU architecture.
86
87 See the KBUILD_ARCHES variable in kBuild/header.kmk for possible return values.
88 """
89 global g_sHostArch;
90 if g_sHostArch is None:
91 sArch = platform.machine();
92 if sArch in ('i386', 'i486', 'i586', 'i686', 'i786', 'i886', 'x86'):
93 sArch = 'x86';
94 elif sArch in ('AMD64', 'amd64', 'x86_64'):
95 sArch = 'amd64';
96 elif sArch == 'i86pc': # SunOS
97 if platform.architecture()[0] == '64bit':
98 sArch = 'amd64';
99 else:
100 try:
101 sArch = processOutputChecked(['/usr/bin/isainfo', '-n',]);
102 except:
103 pass;
104 sArch = sArch.strip();
105 if sArch != 'amd64':
106 sArch = 'x86';
107 else:
108 raise Exception('Unsupported architecture/machine "%s"' % (sArch,));
109 g_sHostArch = sArch;
110 return g_sHostArch;
111
112
113def getHostOsDotArch():
114 """
115 Gets the 'os.arch' for the host.
116 """
117 return '%s.%s' % (getHostOs(), getHostArch());
118
119
120def isValidOs(sOs):
121 """
122 Validates the OS name.
123 """
124 if sOs in ('darwin', 'dos', 'dragonfly', 'freebsd', 'haiku', 'l4', 'linux', 'netbsd', 'nt', 'openbsd', \
125 'os2', 'solaris', 'win', 'os-agnostic'):
126 return True;
127 return False;
128
129
130def isValidArch(sArch):
131 """
132 Validates the CPU architecture name.
133 """
134 if sArch in ('x86', 'amd64', 'sparc32', 'sparc64', 's390', 's390x', 'ppc32', 'ppc64', \
135 'mips32', 'mips64', 'ia64', 'hppa32', 'hppa64', 'arm', 'alpha'):
136 return True;
137 return False;
138
139def isValidOsDotArch(sOsDotArch):
140 """
141 Validates the 'os.arch' string.
142 """
143
144 asParts = sOsDotArch.split('.');
145 if asParts.length() != 2:
146 return False;
147 return isValidOs(asParts[0]) \
148 and isValidArch(asParts[1]);
149
150def getHostOsVersion():
151 """
152 Returns the host OS version. This is platform.release with additional
153 distro indicator on linux.
154 """
155 sVersion = platform.release();
156 sOs = getHostOs();
157 if sOs == 'linux':
158 sDist = '';
159 try:
160 # try /etc/lsb-release first to distinguish between Debian and Ubuntu
161 oFile = open('/etc/lsb-release');
162 for sLine in oFile:
163 oMatch = re.search(r'(?:DISTRIB_DESCRIPTION\s*=)\s*"*(.*)"', sLine);
164 if oMatch is not None:
165 sDist = oMatch.group(1).strip();
166 except:
167 pass;
168 if sDist:
169 sVersion += ' / ' + sDist;
170 else:
171 asFiles = \
172 [
173 [ '/etc/debian_version', 'Debian v'],
174 [ '/etc/gentoo-release', '' ],
175 [ '/etc/redhat-release', '' ],
176 [ '/etc/SuSE-release', '' ],
177 ];
178 for sFile, sPrefix in asFiles:
179 if os.path.isfile(sFile):
180 try:
181 oFile = open(sFile);
182 sLine = oFile.readline();
183 oFile.close();
184 except:
185 continue;
186 sLine = sLine.strip()
187 if len(sLine) > 0:
188 sVersion += ' / ' + sPrefix + sLine;
189 break;
190
191 elif sOs == 'solaris':
192 sVersion = platform.version();
193 if os.path.isfile('/etc/release'):
194 try:
195 oFile = open('/etc/release');
196 sLast = oFile.readlines()[-1];
197 oFile.close();
198 sLast = sLast.strip();
199 if len(sLast) > 0:
200 sVersion += ' (' + sLast + ')';
201 except:
202 pass;
203
204 return sVersion;
205
206#
207# File system.
208#
209
210def openNoInherit(sFile, sMode = 'r'):
211 """
212 Wrapper around open() that tries it's best to make sure the file isn't
213 inherited by child processes.
214
215 This is a best effort thing at the moment as it doesn't synchronizes with
216 child process spawning in any way. Thus it can be subject to races in
217 multithreaded programs.
218 """
219
220 try:
221 from fcntl import FD_CLOEXEC, F_GETFD, F_SETFD, fcntl; # pylint: disable=F0401
222 except:
223 return open(sFile, sMode);
224
225 oFile = open(sFile, sMode)
226 #try:
227 fcntl(oFile, F_SETFD, fcntl(oFile, F_GETFD) | FD_CLOEXEC);
228 #except:
229 # pass;
230 return oFile;
231
232def noxcptReadLink(sPath, sXcptRet):
233 """
234 No exceptions os.readlink wrapper.
235 """
236 try:
237 sRet = os.readlink(sPath); # pylint: disable=E1101
238 except:
239 sRet = sXcptRet;
240 return sRet;
241
242def readFile(sFile, sMode = 'rb'):
243 """
244 Reads the entire file.
245 """
246 oFile = open(sFile, sMode);
247 sRet = oFile.read();
248 oFile.close();
249 return sRet;
250
251def noxcptReadFile(sFile, sXcptRet, sMode = 'rb'):
252 """
253 No exceptions common.readFile wrapper.
254 """
255 try:
256 sRet = readFile(sFile, sMode);
257 except:
258 sRet = sXcptRet;
259 return sRet;
260
261def noxcptRmDir(sDir, oXcptRet = False):
262 """
263 No exceptions os.rmdir wrapper.
264 """
265 oRet = True;
266 try:
267 os.rmdir(sDir);
268 except:
269 oRet = oXcptRet;
270 return oRet;
271
272def noxcptDeleteFile(sFile, oXcptRet = False):
273 """
274 No exceptions os.remove wrapper.
275 """
276 oRet = True;
277 try:
278 os.remove(sFile);
279 except:
280 oRet = oXcptRet;
281 return oRet;
282
283
284#
285# SubProcess.
286#
287
288def _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs):
289 """
290 If the "executable" is a python script, insert the python interpreter at
291 the head of the argument list so that it will work on systems which doesn't
292 support hash-bang scripts.
293 """
294
295 asArgs = dKeywordArgs.get('args');
296 if asArgs is None:
297 asArgs = aPositionalArgs[0];
298
299 if asArgs[0].endswith('.py'):
300 if sys.executable is not None and len(sys.executable) > 0:
301 asArgs.insert(0, sys.executable);
302 else:
303 asArgs.insert(0, 'python');
304
305 # paranoia...
306 if dKeywordArgs.get('args') is not None:
307 dKeywordArgs['args'] = asArgs;
308 else:
309 aPositionalArgs = (asArgs,) + aPositionalArgs[1:];
310 return None;
311
312def processCall(*aPositionalArgs, **dKeywordArgs):
313 """
314 Wrapper around subprocess.call to deal with its absense in older
315 python versions.
316 Returns process exit code (see subprocess.poll).
317 """
318 assert dKeywordArgs.get('stdout') == None;
319 assert dKeywordArgs.get('stderr') == None;
320 _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs);
321 oProcess = subprocess.Popen(*aPositionalArgs, **dKeywordArgs);
322 return oProcess.wait();
323
324def processOutputChecked(*aPositionalArgs, **dKeywordArgs):
325 """
326 Wrapper around subprocess.check_output to deal with its absense in older
327 python versions.
328 """
329 _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs);
330 oProcess = subprocess.Popen(stdout=subprocess.PIPE, *aPositionalArgs, **dKeywordArgs);
331
332 sOutput, _ = oProcess.communicate();
333 iExitCode = oProcess.poll();
334
335 if iExitCode is not 0:
336 asArgs = dKeywordArgs.get('args');
337 if asArgs is None:
338 asArgs = aPositionalArgs[0];
339 print(sOutput);
340 raise subprocess.CalledProcessError(iExitCode, asArgs);
341
342 return str(sOutput); # str() make pylint happy.
343
344g_fOldSudo = None;
345def _sudoFixArguments(aPositionalArgs, dKeywordArgs, fInitialEnv = True):
346 """
347 Adds 'sudo' (or similar) to the args parameter, whereever it is.
348 """
349
350 # Are we root?
351 fIsRoot = True;
352 try:
353 fIsRoot = os.getuid() == 0; # pylint: disable=E1101
354 except:
355 pass;
356
357 # If not, prepend sudo (non-interactive, simulate initial login).
358 if fIsRoot is not True:
359 asArgs = dKeywordArgs.get('args');
360 if asArgs is None:
361 asArgs = aPositionalArgs[0];
362
363 # Detect old sudo.
364 global g_fOldSudo;
365 if g_fOldSudo is None:
366 try:
367 sVersion = processOutputChecked(['sudo', '-V']);
368 except:
369 sVersion = '1.7.0';
370 sVersion = sVersion.strip().split('\n')[0];
371 sVersion = sVersion.replace('Sudo version', '').strip();
372 g_fOldSudo = len(sVersion) >= 4 \
373 and sVersion[0] == '1' \
374 and sVersion[1] == '.' \
375 and sVersion[2] <= '6' \
376 and sVersion[3] == '.';
377
378 asArgs.insert(0, 'sudo');
379 if not g_fOldSudo:
380 asArgs.insert(1, '-n');
381 if fInitialEnv and not g_fOldSudo:
382 asArgs.insert(1, '-i');
383
384 # paranoia...
385 if dKeywordArgs.get('args') is not None:
386 dKeywordArgs['args'] = asArgs;
387 else:
388 aPositionalArgs = (asArgs,) + aPositionalArgs[1:];
389 return None;
390
391
392def sudoProcessCall(*aPositionalArgs, **dKeywordArgs):
393 """
394 sudo (or similar) + subprocess.call
395 """
396 _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs);
397 _sudoFixArguments(aPositionalArgs, dKeywordArgs);
398 return processCall(*aPositionalArgs, **dKeywordArgs);
399
400def sudoProcessOutputChecked(*aPositionalArgs, **dKeywordArgs):
401 """
402 sudo (or similar) + subprocess.check_output.
403 """
404 _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs);
405 _sudoFixArguments(aPositionalArgs, dKeywordArgs);
406 return processOutputChecked(*aPositionalArgs, **dKeywordArgs);
407
408def sudoProcessOutputCheckedNoI(*aPositionalArgs, **dKeywordArgs):
409 """
410 sudo (or similar) + subprocess.check_output, except '-i' isn't used.
411 """
412 _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs);
413 _sudoFixArguments(aPositionalArgs, dKeywordArgs, False);
414 return processOutputChecked(*aPositionalArgs, **dKeywordArgs);
415
416def sudoProcessPopen(*aPositionalArgs, **dKeywordArgs):
417 """
418 sudo (or similar) + subprocess.Popen.
419 """
420 _processFixPythonInterpreter(aPositionalArgs, dKeywordArgs);
421 _sudoFixArguments(aPositionalArgs, dKeywordArgs);
422 return subprocess.Popen(*aPositionalArgs, **dKeywordArgs);
423
424
425#
426# Generic process stuff.
427#
428
429def processInterrupt(uPid):
430 """
431 Sends a SIGINT or equivalent to interrupt the specified process.
432 Returns True on success, False on failure.
433
434 On Windows hosts this may not work unless the process happens to be a
435 process group leader.
436 """
437 if sys.platform == 'win32':
438 try:
439 win32console.GenerateConsoleCtrlEvent(win32con.CTRL_BREAK_EVENT, uPid); # pylint
440 fRc = True;
441 except:
442 fRc = False;
443 else:
444 try:
445 os.kill(uPid, signal.SIGINT);
446 fRc = True;
447 except:
448 fRc = False;
449 return fRc;
450
451def sendUserSignal1(uPid):
452 """
453 Sends a SIGUSR1 or equivalent to nudge the process into shutting down
454 (VBoxSVC) or something.
455 Returns True on success, False on failure or if not supported (win).
456
457 On Windows hosts this may not work unless the process happens to be a
458 process group leader.
459 """
460 if sys.platform == 'win32':
461 fRc = False;
462 else:
463 try:
464 os.kill(uPid, signal.SIGUSR1); # pylint: disable=E1101
465 fRc = True;
466 except:
467 fRc = False;
468 return fRc;
469
470def processTerminate(uPid):
471 """
472 Terminates the process in a nice manner (SIGTERM or equivalent).
473 Returns True on success, False on failure.
474 """
475 fRc = False;
476 if sys.platform == 'win32':
477 try:
478 hProcess = win32api.OpenProcess(win32con.PROCESS_TERMINATE, False, uPid);
479 except:
480 pass;
481 else:
482 try:
483 win32process.TerminateProcess(hProcess, 0x40010004); # DBG_TERMINATE_PROCESS
484 fRc = True;
485 except:
486 pass;
487 win32api.CloseHandle(hProcess)
488 else:
489 try:
490 os.kill(uPid, signal.SIGTERM);
491 fRc = True;
492 except:
493 pass;
494 return fRc;
495
496def processKill(uPid):
497 """
498 Terminates the process with extreme prejudice (SIGKILL).
499 Returns True on success, False on failure.
500 """
501 if sys.platform == 'win32':
502 fRc = processTerminate(uPid);
503 else:
504 try:
505 os.kill(uPid, signal.SIGKILL); # pylint: disable=E1101
506 fRc = True;
507 except:
508 fRc = False;
509 return fRc;
510
511def processKillWithNameCheck(uPid, sName):
512 """
513 Like processKill(), but checks if the process name matches before killing
514 it. This is intended for killing using potentially stale pid values.
515
516 Returns True on success, False on failure.
517 """
518
519 if processCheckPidAndName(uPid, sName) is not True:
520 return False;
521 return processKill(uPid);
522
523
524def processExists(uPid):
525 """
526 Checks if the specified process exits.
527 This will only work if we can signal/open the process.
528
529 Returns True if it positively exists, False otherwise.
530 """
531 if sys.platform == 'win32':
532 fRc = False;
533 try:
534 hProcess = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, False, uPid);
535 except:
536 pass;
537 else:
538 win32api.CloseHandle(hProcess)
539 fRc = True;
540 else:
541 try:
542 os.kill(uPid, 0);
543 fRc = True;
544 except:
545 fRc = False;
546 return fRc;
547
548def processCheckPidAndName(uPid, sName):
549 """
550 Checks if a process PID and NAME matches.
551 """
552 fRc = processExists(uPid);
553 if fRc is not True:
554 return False;
555
556 if sys.platform == 'win32':
557 try:
558 from win32com.client import GetObject; # pylint: disable=F0401
559 oWmi = GetObject('winmgmts:');
560 aoProcesses = oWmi.InstancesOf('Win32_Process');
561 for oProcess in aoProcesses:
562 if long(oProcess.Properties_("ProcessId").Value) == uPid:
563 sCurName = oProcess.Properties_("Name").Value;
564 #reporter.log2('uPid=%s sName=%s sCurName=%s' % (uPid, sName, sCurName));
565 sName = sName.lower();
566 sCurName = sCurName.lower();
567 if os.path.basename(sName) == sName:
568 sCurName = os.path.basename(sCurName);
569
570 if sCurName == sName \
571 or sCurName + '.exe' == sName \
572 or sCurName == sName + '.exe':
573 fRc = True;
574 break;
575 except:
576 #reporter.logXcpt('uPid=%s sName=%s' % (uPid, sName));
577 pass;
578 else:
579 if sys.platform in ('linux2', ):
580 asPsCmd = ['/bin/ps', '-p', '%u' % (uPid,), '-o', 'fname='];
581 elif sys.platform in ('sunos5',):
582 asPsCmd = ['/usr/bin/ps', '-p', '%u' % (uPid,), '-o', 'fname='];
583 elif sys.platform in ('darwin',):
584 asPsCmd = ['/bin/ps', '-p', '%u' % (uPid,), '-o', 'ucomm='];
585 else:
586 asPsCmd = None;
587
588 if asPsCmd is not None:
589 try:
590 oPs = subprocess.Popen(asPsCmd, stdout=subprocess.PIPE);
591 sCurName = oPs.communicate()[0];
592 iExitCode = oPs.wait();
593 except:
594 #reporter.logXcpt();
595 return False;
596
597 # ps fails with non-zero exit code if the pid wasn't found.
598 if iExitCode is not 0:
599 return False;
600 if sCurName is None:
601 return False;
602 sCurName = sCurName.strip();
603 if sCurName is '':
604 return False;
605
606 if os.path.basename(sName) == sName:
607 sCurName = os.path.basename(sCurName);
608 elif os.path.basename(sCurName) == sCurName:
609 sName = os.path.basename(sName);
610
611 if sCurName != sName:
612 return False;
613
614 fRc = True;
615 return fRc;
616
617
618class ProcessInfo(object):
619 """Process info."""
620 def __init__(self, iPid):
621 self.iPid = iPid;
622 self.iParentPid = None;
623 self.sImage = None;
624 self.sName = None;
625 self.asArgs = None;
626 self.sCwd = None;
627 self.iGid = None;
628 self.iUid = None;
629 self.iProcGroup = None;
630 self.iSessionId = None;
631
632 def loadAll(self):
633 """Load all the info."""
634 sOs = getHostOs();
635 if sOs == 'linux':
636 sProc = '/proc/%s/' % (self.iPid,);
637 if self.sImage is None: self.sImage = noxcptReadLink(sProc + 'exe', None);
638 if self.sCwd is None: self.sCwd = noxcptReadLink(sProc + 'cwd', None);
639 if self.asArgs is None: self.asArgs = noxcptReadFile(sProc + 'cmdline', '').split('\x00');
640 elif sOs == 'solaris':
641 sProc = '/proc/%s/' % (self.iPid,);
642 if self.sImage is None: self.sImage = noxcptReadLink(sProc + 'path/a.out', None);
643 if self.sCwd is None: self.sCwd = noxcptReadLink(sProc + 'path/cwd', None);
644 else:
645 pass;
646 if self.sName is None and self.sImage is not None:
647 self.sName = self.sImage;
648
649 def windowsGrabProcessInfo(self, oProcess):
650 """Windows specific loadAll."""
651 try: self.sName = oProcess.Properties_("Name").Value;
652 except: pass;
653 try: self.sImage = oProcess.Properties_("ExecutablePath").Value;
654 except: pass;
655 try: self.asArgs = oProcess.Properties_("CommandLine").Value; ## @todo split it.
656 except: pass;
657 try: self.iParentPid = oProcess.Properties_("ParentProcessId").Value;
658 except: pass;
659 try: self.iSessionId = oProcess.Properties_("SessionId").Value;
660 except: pass;
661 if self.sName is None and self.sImage is not None:
662 self.sName = self.sImage;
663
664 def getBaseImageName(self):
665 """
666 Gets the base image name if available, use the process name if not available.
667 Returns image/process base name or None.
668 """
669 sRet = self.sImage if self.sName is None else self.sName;
670 if sRet is None:
671 self.loadAll();
672 sRet = self.sImage if self.sName is None else self.sName;
673 if sRet is None:
674 if self.asArgs is None or len(self.asArgs) == 0:
675 return None;
676 sRet = self.asArgs[0];
677 if len(sRet) == 0:
678 return None;
679 return os.path.basename(sRet);
680
681 def getBaseImageNameNoExeSuff(self):
682 """
683 Same as getBaseImageName, except any '.exe' or similar suffix is stripped.
684 """
685 sRet = self.getBaseImageName();
686 if sRet is not None and len(sRet) > 4 and sRet[-4] == '.':
687 if (sRet[-4:]).lower() in [ '.exe', '.com', '.msc', '.vbs', '.cmd', '.bat' ]:
688 sRet = sRet[:-4];
689 return sRet;
690
691
692def processListAll(): # pylint: disable=R0914
693 """
694 Return a list of ProcessInfo objects for all the processes in the system
695 that the current user can see.
696 """
697 asProcesses = [];
698
699 sOs = getHostOs();
700 if sOs == 'win':
701 from win32com.client import GetObject; # pylint: disable=F0401
702 oWmi = GetObject('winmgmts:');
703 aoProcesses = oWmi.InstancesOf('Win32_Process');
704 for oProcess in aoProcesses:
705 try:
706 iPid = int(oProcess.Properties_("ProcessId").Value);
707 except:
708 continue;
709 oMyInfo = ProcessInfo(iPid);
710 oMyInfo.windowsGrabProcessInfo(oProcess);
711 asProcesses.append(oMyInfo);
712
713 elif sOs in [ 'linux', 'solaris' ]:
714 try:
715 asDirs = os.listdir('/proc');
716 except:
717 asDirs = [];
718 for sDir in asDirs:
719 if sDir.isdigit():
720 asProcesses.append(ProcessInfo(int(sDir),));
721
722 elif sOs == 'darwin':
723 # Try our best to parse ps output. (Not perfect but does the job most of the time.)
724 try:
725 sRaw = processOutputChecked([ '/bin/ps', '-A',
726 '-o', 'pid=',
727 '-o', 'ppid=',
728 '-o', 'pgid=',
729 '-o', 'sess=',
730 '-o', 'uid=',
731 '-o', 'gid=',
732 '-o', 'comm=' ]);
733 except:
734 return asProcesses;
735
736 for sLine in sRaw.split('\n'):
737 sLine = sLine.lstrip();
738 if len(sLine) < 7 or not sLine[0].isdigit():
739 continue;
740
741 iField = 0;
742 off = 0;
743 aoFields = [None, None, None, None, None, None, None];
744 while iField < 7:
745 # Eat whitespace.
746 while off < len(sLine) and (sLine[off] == ' ' or sLine[off] == '\t'):
747 off += 1;
748
749 # Final field / EOL.
750 if iField == 6:
751 aoFields[6] = sLine[off:];
752 break;
753 if off >= len(sLine):
754 break;
755
756 # Generic field parsing.
757 offStart = off;
758 off += 1;
759 while off < len(sLine) and sLine[off] != ' ' and sLine[off] != '\t':
760 off += 1;
761 try:
762 if iField != 3:
763 aoFields[iField] = int(sLine[offStart:off]);
764 else:
765 aoFields[iField] = long(sLine[offStart:off], 16); # sess is a hex address.
766 except:
767 pass;
768 iField += 1;
769
770 if aoFields[0] is not None:
771 oMyInfo = ProcessInfo(aoFields[0]);
772 oMyInfo.iParentPid = aoFields[1];
773 oMyInfo.iProcGroup = aoFields[2];
774 oMyInfo.iSessionId = aoFields[3];
775 oMyInfo.iUid = aoFields[4];
776 oMyInfo.iGid = aoFields[5];
777 oMyInfo.sName = aoFields[6];
778 asProcesses.append(oMyInfo);
779
780 return asProcesses;
781
782
783def processCollectCrashInfo(uPid, fnLog, fnCrashFile):
784 """
785 Looks for information regarding the demise of the given process.
786 """
787 sOs = getHostOs();
788 if sOs == 'darwin':
789 #
790 # On darwin we look for crash and diagnostic reports.
791 #
792 asLogDirs = [
793 u'/Library/Logs/DiagnosticReports/',
794 u'/Library/Logs/CrashReporter/',
795 u'~/Library/Logs/DiagnosticReports/',
796 u'~/Library/Logs/CrashReporter/',
797 ];
798 for sDir in asLogDirs:
799 sDir = os.path.expanduser(sDir);
800 if not os.path.isdir(sDir):
801 continue;
802 try:
803 asDirEntries = os.listdir(sDir);
804 except:
805 continue;
806 for sEntry in asDirEntries:
807 # Only interested in .crash files.
808 _, sSuff = os.path.splitext(sEntry);
809 if sSuff != '.crash':
810 continue;
811
812 # The pid can be found at the end of the first line.
813 sFull = os.path.join(sDir, sEntry);
814 try:
815 oFile = open(sFull, 'r');
816 sFirstLine = oFile.readline();
817 oFile.close();
818 except:
819 continue;
820 if len(sFirstLine) <= 4 or sFirstLine[-2] != ']':
821 continue;
822 offPid = len(sFirstLine) - 3;
823 while offPid > 1 and sFirstLine[offPid - 1].isdigit():
824 offPid -= 1;
825 try: uReportPid = int(sFirstLine[offPid:-2]);
826 except: continue;
827
828 # Does the pid we found match?
829 if uReportPid == uPid:
830 fnLog('Found crash report for %u: %s' % (uPid, sFull,));
831 fnCrashFile(sFull, False);
832 elif sOs == 'win':
833 #
834 # Getting WER reports would be great, however we have trouble match the
835 # PID to those as they seems not to mention it in the brief reports.
836 # Instead we'll just look for crash dumps in C:\CrashDumps (our custom
837 # location - see the windows readme for the testbox script) and what
838 # the MSDN article lists for now.
839 #
840 # It's been observed on Windows server 2012 that the dump files takes
841 # the form: <processimage>.<decimal-pid>.dmp
842 #
843 asDmpDirs = [
844 u'%SystemDrive%/CrashDumps/', # Testboxes.
845 u'%LOCALAPPDATA%/CrashDumps/', # MSDN example.
846 u'%WINDIR%/ServiceProfiles/LocalServices/', # Local and network service.
847 u'%WINDIR%/ServiceProfiles/NetworkSerices/',
848 u'%WINDIR%/ServiceProfiles/',
849 u'%WINDIR%/System32/Config/SystemProfile/', # System services.
850 ];
851 sMatchSuffix = '.%u.dmp' % (uPid,);
852
853 for sDir in asDmpDirs:
854 sDir = os.path.expandvars(sDir);
855 if not os.path.isdir(sDir):
856 continue;
857 try:
858 asDirEntries = os.listdir(sDir);
859 except:
860 continue;
861 for sEntry in asDirEntries:
862 if sEntry.endswith(sMatchSuffix):
863 sFull = os.path.join(sDir, sEntry);
864 fnLog('Found crash dump for %u: %s' % (uPid, sFull,));
865 fnCrashFile(sFull, True);
866
867 else:
868 pass; ## TODO
869 return None;
870
871
872#
873# Time.
874#
875
876def timestampNano():
877 """
878 Gets a nanosecond timestamp.
879 """
880 if sys.platform == 'win32':
881 return long(time.clock() * 1000000000);
882 return long(time.time() * 1000000000);
883
884def timestampMilli():
885 """
886 Gets a millisecond timestamp.
887 """
888 if sys.platform == 'win32':
889 return long(time.clock() * 1000);
890 return long(time.time() * 1000);
891
892def timestampSecond():
893 """
894 Gets a second timestamp.
895 """
896 if sys.platform == 'win32':
897 return long(time.clock());
898 return long(time.time());
899
900def getTimePrefix():
901 """
902 Returns a timestamp prefix, typically used for logging. UTC.
903 """
904 try:
905 oNow = datetime.datetime.utcnow();
906 sTs = '%02u:%02u:%02u.%06u' % (oNow.hour, oNow.minute, oNow.second, oNow.microsecond);
907 except:
908 sTs = 'getTimePrefix-exception';
909 return sTs;
910
911def getTimePrefixAndIsoTimestamp():
912 """
913 Returns current UTC as log prefix and iso timestamp.
914 """
915 try:
916 oNow = datetime.datetime.utcnow();
917 sTsPrf = '%02u:%02u:%02u.%06u' % (oNow.hour, oNow.minute, oNow.second, oNow.microsecond);
918 sTsIso = formatIsoTimestamp(oNow);
919 except:
920 sTsPrf = sTsIso = 'getTimePrefix-exception';
921 return (sTsPrf, sTsIso);
922
923def formatIsoTimestamp(oNow):
924 """Formats the datetime object as an ISO timestamp."""
925 assert oNow.tzinfo is None;
926 sTs = '%s.%09uZ' % (oNow.strftime('%Y-%m-%dT%H:%M:%S'), oNow.microsecond * 1000);
927 return sTs;
928
929def getIsoTimestamp():
930 """Returns the current UTC timestamp as a string."""
931 return formatIsoTimestamp(datetime.datetime.utcnow());
932
933
934def getLocalHourOfWeek():
935 """ Local hour of week (0 based). """
936 oNow = datetime.datetime.now();
937 return (oNow.isoweekday() - 1) * 24 + oNow.hour;
938
939
940def formatIntervalSeconds(cSeconds):
941 """ Format a seconds interval into a nice 01h 00m 22s string """
942 # Two simple special cases.
943 if cSeconds < 60:
944 return '%ss' % (cSeconds,);
945 if cSeconds < 3600:
946 cMins = cSeconds / 60;
947 cSecs = cSeconds % 60;
948 if cSecs == 0:
949 return '%sm' % (cMins,);
950 return '%sm %ss' % (cMins, cSecs,);
951
952 # Generic and a bit slower.
953 cDays = cSeconds / 86400;
954 cSeconds %= 86400;
955 cHours = cSeconds / 3600;
956 cSeconds %= 3600;
957 cMins = cSeconds / 60;
958 cSecs = cSeconds % 60;
959 sRet = '';
960 if cDays > 0:
961 sRet = '%sd ' % (cDays,);
962 if cHours > 0:
963 sRet += '%sh ' % (cHours,);
964 if cMins > 0:
965 sRet += '%sm ' % (cMins,);
966 if cSecs > 0:
967 sRet += '%ss ' % (cSecs,);
968 assert len(sRet) > 0; assert sRet[-1] == ' ';
969 return sRet[:-1];
970
971def formatIntervalSeconds2(oSeconds):
972 """
973 Flexible input version of formatIntervalSeconds for use in WUI forms where
974 data is usually already string form.
975 """
976 if isinstance(oSeconds, int) or isinstance(oSeconds, long):
977 return formatIntervalSeconds(oSeconds);
978 if not isString(oSeconds):
979 try:
980 lSeconds = long(oSeconds);
981 except:
982 pass;
983 else:
984 if lSeconds >= 0:
985 return formatIntervalSeconds2(lSeconds);
986 return oSeconds;
987
988def parseIntervalSeconds(sString):
989 """
990 Reverse of formatIntervalSeconds.
991
992 Returns (cSeconds, sError), where sError is None on success.
993 """
994
995 # We might given non-strings, just return them without any fuss.
996 if not isString(sString):
997 if isinstance(sString, int) or isinstance(sString, long) or sString is None:
998 return (sString, None);
999 ## @todo time/date objects?
1000 return (int(sString), None);
1001
1002 # Strip it and make sure it's not empty.
1003 sString = sString.strip();
1004 if len(sString) == 0:
1005 return (0, 'Empty interval string.');
1006
1007 #
1008 # Split up the input into a list of 'valueN, unitN, ...'.
1009 #
1010 # Don't want to spend too much time trying to make re.split do exactly what
1011 # I need here, so please forgive the extra pass I'm making here.
1012 #
1013 asRawParts = re.split(r'\s*([0-9]+)\s*([^0-9,;]*)[\s,;]*', sString);
1014 asParts = [];
1015 for sPart in asRawParts:
1016 sPart = sPart.strip();
1017 if len(sPart) > 0:
1018 asParts.append(sPart);
1019 if len(asParts) == 0:
1020 return (0, 'Empty interval string or something?');
1021
1022 #
1023 # Process them one or two at the time.
1024 #
1025 cSeconds = 0;
1026 asErrors = [];
1027 i = 0;
1028 while i < len(asParts):
1029 sNumber = asParts[i];
1030 i += 1;
1031 if sNumber.isdigit():
1032 iNumber = int(sNumber);
1033
1034 sUnit = 's';
1035 if i < len(asParts) and not asParts[i].isdigit():
1036 sUnit = asParts[i];
1037 i += 1;
1038
1039 sUnitLower = sUnit.lower();
1040 if sUnitLower in [ 's', 'se', 'sec', 'second', 'seconds' ]:
1041 pass;
1042 elif sUnitLower in [ 'm', 'mi', 'min', 'minute', 'minutes' ]:
1043 iNumber *= 60;
1044 elif sUnitLower in [ 'h', 'ho', 'hou', 'hour', 'hours' ]:
1045 iNumber *= 3600;
1046 elif sUnitLower in [ 'd', 'da', 'day', 'days' ]:
1047 iNumber *= 86400;
1048 elif sUnitLower in [ 'w', 'week', 'weeks' ]:
1049 iNumber *= 7 * 86400;
1050 else:
1051 asErrors.append('Unknown unit "%s".' % (sUnit,));
1052 cSeconds += iNumber;
1053 else:
1054 asErrors.append('Bad number "%s".' % (sNumber,));
1055 return (cSeconds, None if len(asErrors) == 0 else ' '.join(asErrors));
1056
1057def formatIntervalHours(cHours):
1058 """ Format a hours interval into a nice 1w 2d 1h string. """
1059 # Simple special cases.
1060 if cHours < 24:
1061 return '%sh' % (cHours,);
1062
1063 # Generic and a bit slower.
1064 cWeeks = cHours / (7 * 24);
1065 cHours %= 7 * 24;
1066 cDays = cHours / 24;
1067 cHours %= 24;
1068 sRet = '';
1069 if cWeeks > 0:
1070 sRet = '%sw ' % (cWeeks,);
1071 if cDays > 0:
1072 sRet = '%sd ' % (cDays,);
1073 if cHours > 0:
1074 sRet += '%sh ' % (cHours,);
1075 assert len(sRet) > 0; assert sRet[-1] == ' ';
1076 return sRet[:-1];
1077
1078def parseIntervalHours(sString):
1079 """
1080 Reverse of formatIntervalHours.
1081
1082 Returns (cHours, sError), where sError is None on success.
1083 """
1084
1085 # We might given non-strings, just return them without any fuss.
1086 if not isString(sString):
1087 if isinstance(sString, int) or isinstance(sString, long) or sString is None:
1088 return (sString, None);
1089 ## @todo time/date objects?
1090 return (int(sString), None);
1091
1092 # Strip it and make sure it's not empty.
1093 sString = sString.strip();
1094 if len(sString) == 0:
1095 return (0, 'Empty interval string.');
1096
1097 #
1098 # Split up the input into a list of 'valueN, unitN, ...'.
1099 #
1100 # Don't want to spend too much time trying to make re.split do exactly what
1101 # I need here, so please forgive the extra pass I'm making here.
1102 #
1103 asRawParts = re.split(r'\s*([0-9]+)\s*([^0-9,;]*)[\s,;]*', sString);
1104 asParts = [];
1105 for sPart in asRawParts:
1106 sPart = sPart.strip();
1107 if len(sPart) > 0:
1108 asParts.append(sPart);
1109 if len(asParts) == 0:
1110 return (0, 'Empty interval string or something?');
1111
1112 #
1113 # Process them one or two at the time.
1114 #
1115 cHours = 0;
1116 asErrors = [];
1117 i = 0;
1118 while i < len(asParts):
1119 sNumber = asParts[i];
1120 i += 1;
1121 if sNumber.isdigit():
1122 iNumber = int(sNumber);
1123
1124 sUnit = 'h';
1125 if i < len(asParts) and not asParts[i].isdigit():
1126 sUnit = asParts[i];
1127 i += 1;
1128
1129 sUnitLower = sUnit.lower();
1130 if sUnitLower in [ 'h', 'ho', 'hou', 'hour', 'hours' ]:
1131 pass;
1132 elif sUnitLower in [ 'd', 'da', 'day', 'days' ]:
1133 iNumber *= 24;
1134 elif sUnitLower in [ 'w', 'week', 'weeks' ]:
1135 iNumber *= 7 * 24;
1136 else:
1137 asErrors.append('Unknown unit "%s".' % (sUnit,));
1138 cHours += iNumber;
1139 else:
1140 asErrors.append('Bad number "%s".' % (sNumber,));
1141 return (cHours, None if len(asErrors) == 0 else ' '.join(asErrors));
1142
1143
1144#
1145# Introspection.
1146#
1147
1148def getCallerName(oFrame=None, iFrame=2):
1149 """
1150 Returns the name of the caller's caller.
1151 """
1152 if oFrame is None:
1153 try:
1154 raise Exception();
1155 except:
1156 oFrame = sys.exc_info()[2].tb_frame.f_back;
1157 while iFrame > 1:
1158 if oFrame is not None:
1159 oFrame = oFrame.f_back;
1160 iFrame = iFrame - 1;
1161 if oFrame is not None:
1162 sName = '%s:%u' % (oFrame.f_code.co_name, oFrame.f_lineno);
1163 return sName;
1164 return "unknown";
1165
1166
1167def getXcptInfo(cFrames = 1):
1168 """
1169 Gets text detailing the exception. (Good for logging.)
1170 Returns list of info strings.
1171 """
1172
1173 #
1174 # Try get exception info.
1175 #
1176 try:
1177 oType, oValue, oTraceback = sys.exc_info();
1178 except:
1179 oType = oValue = oTraceback = None;
1180 if oType is not None:
1181
1182 #
1183 # Try format the info
1184 #
1185 asRet = [];
1186 try:
1187 try:
1188 asRet = asRet + traceback.format_exception_only(oType, oValue);
1189 asTraceBack = traceback.format_tb(oTraceback);
1190 if cFrames is not None and cFrames <= 1:
1191 asRet.append(asTraceBack[-1]);
1192 else:
1193 asRet.append('Traceback:')
1194 for iFrame in range(min(cFrames, len(asTraceBack))):
1195 asRet.append(asTraceBack[-iFrame - 1]);
1196 asRet.append('Stack:')
1197 asRet = asRet + traceback.format_stack(oTraceback.tb_frame.f_back, cFrames);
1198 except:
1199 asRet.append('internal-error: Hit exception #2! %s' % (traceback.format_exc(),));
1200
1201 if len(asRet) == 0:
1202 asRet.append('No exception info...');
1203 except:
1204 asRet.append('internal-error: Hit exception! %s' % (traceback.format_exc(),));
1205 else:
1206 asRet = ['Couldn\'t find exception traceback.'];
1207 return asRet;
1208
1209
1210#
1211# TestSuite stuff.
1212#
1213
1214def isRunningFromCheckout(cScriptDepth = 1):
1215 """
1216 Checks if we're running from the SVN checkout or not.
1217 """
1218
1219 try:
1220 sFile = __file__;
1221 cScriptDepth = 1;
1222 except:
1223 sFile = sys.argv[0];
1224
1225 sDir = os.path.abspath(sFile);
1226 while cScriptDepth >= 0:
1227 sDir = os.path.dirname(sDir);
1228 if os.path.exists(os.path.join(sDir, 'Makefile.kmk')) \
1229 or os.path.exists(os.path.join(sDir, 'Makefile.kup')):
1230 return True;
1231 cScriptDepth -= 1;
1232
1233 return False;
1234
1235
1236#
1237# Bourne shell argument fun.
1238#
1239
1240
1241def argsSplit(sCmdLine):
1242 """
1243 Given a bourne shell command line invocation, split it up into arguments
1244 assuming IFS is space.
1245 Returns None on syntax error.
1246 """
1247 ## @todo bourne shell argument parsing!
1248 return sCmdLine.split(' ');
1249
1250def argsGetFirst(sCmdLine):
1251 """
1252 Given a bourne shell command line invocation, get return the first argument
1253 assuming IFS is space.
1254 Returns None on invalid syntax, otherwise the parsed and unescaped argv[0] string.
1255 """
1256 asArgs = argsSplit(sCmdLine);
1257 if asArgs is None or len(asArgs) == 0:
1258 return None;
1259
1260 return asArgs[0];
1261
1262#
1263# String helpers.
1264#
1265
1266def stricmp(sFirst, sSecond):
1267 """
1268 Compares to strings in an case insensitive fashion.
1269
1270 Python doesn't seem to have any way of doing the correctly, so this is just
1271 an approximation using lower.
1272 """
1273 if sFirst == sSecond:
1274 return 0;
1275 sLower1 = sFirst.lower();
1276 sLower2 = sSecond.lower();
1277 if sLower1 == sLower2:
1278 return 0;
1279 if sLower1 < sLower2:
1280 return -1;
1281 return 1;
1282
1283
1284#
1285# Misc.
1286#
1287
1288def versionCompare(sVer1, sVer2):
1289 """
1290 Compares to version strings in a fashion similar to RTStrVersionCompare.
1291 """
1292
1293 ## @todo implement me!!
1294
1295 if sVer1 == sVer2:
1296 return 0;
1297 if sVer1 < sVer2:
1298 return -1;
1299 return 1;
1300
1301
1302def formatNumber(lNum, sThousandSep = ' '):
1303 """
1304 Formats a decimal number with pretty separators.
1305 """
1306 sNum = str(lNum);
1307 sRet = sNum[-3:];
1308 off = len(sNum) - 3;
1309 while off > 0:
1310 off -= 3;
1311 sRet = sNum[(off if off >= 0 else 0):(off + 3)] + sThousandSep + sRet;
1312 return sRet;
1313
1314
1315def formatNumberNbsp(lNum):
1316 """
1317 Formats a decimal number with pretty separators.
1318 """
1319 sRet = formatNumber(lNum);
1320 return unicode(sRet).replace(' ', u'\u00a0');
1321
1322
1323def isString(oString):
1324 """
1325 Checks if the object is a string object, hiding difference between python 2 and 3.
1326
1327 Returns True if it's a string of some kind.
1328 Returns False if not.
1329 """
1330 if sys.version_info[0] >= 3:
1331 return isinstance(oString, str);
1332 return isinstance(oString, basestring);
1333
1334
1335def hasNonAsciiCharacters(sText):
1336 """
1337 Returns True is specified string has non-ASCII characters.
1338 """
1339 sTmp = unicode(sText, errors='ignore') if isinstance(sText, str) else sText
1340 return not all(ord(cChar) < 128 for cChar in sTmp)
1341
1342
1343def chmodPlusX(sFile):
1344 """
1345 Makes the specified file or directory executable.
1346 Returns success indicator, no exceptions.
1347
1348 Note! Symbolic links are followed and the target will be changed.
1349 """
1350 try:
1351 oStat = os.stat(sFile);
1352 except:
1353 return False;
1354 try:
1355 os.chmod(sFile, oStat.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH);
1356 except:
1357 return False;
1358 return True;
1359
1360
1361def unpackFile(sArchive, sDstDir, fnLog, fnError = None):
1362 """
1363 Unpacks the given file if it has a know archive extension, otherwise do
1364 nothing.
1365
1366 Returns list of the extracted files (full path) on success.
1367 Returns empty list if not a supported archive format.
1368 Returns None on failure. Raises no exceptions.
1369 """
1370 if fnError is None:
1371 fnError = fnLog;
1372
1373 asMembers = [];
1374
1375 sBaseNameLower = os.path.basename(sArchive).lower();
1376 if sBaseNameLower.endswith('.zip'):
1377 fnLog('Unzipping "%s" to "%s"...' % (sArchive, sDstDir));
1378 try:
1379 oZipFile = zipfile.ZipFile(sArchive, 'r')
1380 asMembers = oZipFile.namelist();
1381 for sMember in asMembers:
1382 if sMember.endswith('/'):
1383 os.makedirs(os.path.join(sDstDir, sMember.replace('/', os.path.sep)), 0775);
1384 else:
1385 oZipFile.extract(sMember, sDstDir);
1386 oZipFile.close();
1387 except Exception, oXcpt:
1388 fnError('Error unpacking "%s" into "%s": %s' % (sArchive, sDstDir, oXcpt));
1389 return None;
1390
1391 elif sBaseNameLower.endswith('.tar') \
1392 or sBaseNameLower.endswith('.tar.gz') \
1393 or sBaseNameLower.endswith('.tgz') \
1394 or sBaseNameLower.endswith('.tar.bz2'):
1395 fnLog('Untarring "%s" to "%s"...' % (sArchive, sDstDir));
1396 try:
1397 oTarFile = tarfile.open(sArchive, 'r:*');
1398 asMembers = [oTarInfo.name for oTarInfo in oTarFile.getmembers()];
1399 oTarFile.extractall(sDstDir);
1400 oTarFile.close();
1401 except Exception, oXcpt:
1402 fnError('Error unpacking "%s" into "%s": %s' % (sArchive, sDstDir, oXcpt));
1403 return None;
1404
1405 else:
1406 fnLog('Not unpacking "%s".' % (sArchive,));
1407 return [];
1408
1409 #
1410 # Change asMembers to local slashes and prefix with path.
1411 #
1412 asMembersRet = [];
1413 for sMember in asMembers:
1414 asMembersRet.append(os.path.join(sDstDir, sMember.replace('/', os.path.sep)));
1415
1416 return asMembersRet;
1417
1418
1419def getDiskUsage(sPath):
1420 """
1421 Get free space of a partition that corresponds to specified sPath in MB.
1422
1423 Returns partition free space value in MB.
1424 """
1425 if platform.system() == 'Windows':
1426 import ctypes
1427 oCTypeFreeSpace = ctypes.c_ulonglong(0);
1428 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(sPath), None, None,
1429 ctypes.pointer(oCTypeFreeSpace));
1430 cbFreeSpace = oCTypeFreeSpace.value;
1431 else:
1432 oStats = os.statvfs(sPath); # pylint: disable=E1101
1433 cbFreeSpace = long(oStats.f_frsize) * oStats.f_bfree;
1434
1435 # Convert to MB
1436 cMbFreeSpace = long(cbFreeSpace) / (1024 * 1024);
1437
1438 return cMbFreeSpace;
1439
1440
1441#
1442# Unit testing.
1443#
1444
1445# pylint: disable=C0111
1446class BuildCategoryDataTestCase(unittest.TestCase):
1447 def testIntervalSeconds(self):
1448 self.assertEqual(parseIntervalSeconds(formatIntervalSeconds(3600)), (3600, None));
1449 self.assertEqual(parseIntervalSeconds(formatIntervalSeconds(1209438593)), (1209438593, None));
1450 self.assertEqual(parseIntervalSeconds('123'), (123, None));
1451 self.assertEqual(parseIntervalSeconds(123), (123, None));
1452 self.assertEqual(parseIntervalSeconds(99999999999), (99999999999, None));
1453 self.assertEqual(parseIntervalSeconds(''), (0, 'Empty interval string.'));
1454 self.assertEqual(parseIntervalSeconds('1X2'), (3, 'Unknown unit "X".'));
1455 self.assertEqual(parseIntervalSeconds('1 Y3'), (4, 'Unknown unit "Y".'));
1456 self.assertEqual(parseIntervalSeconds('1 Z 4'), (5, 'Unknown unit "Z".'));
1457 self.assertEqual(parseIntervalSeconds('1 hour 2m 5second'), (3725, None));
1458 self.assertEqual(parseIntervalSeconds('1 hour,2m ; 5second'), (3725, None));
1459
1460if __name__ == '__main__':
1461 unittest.main();
1462 # not reached.
1463
Note: See TracBrowser for help on using the repository browser.

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