VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/testbox.py@ 65040

Last change on this file since 65040 was 65040, checked in by vboxsync, 8 years ago

testmanager: More details in the system wide changelog.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 45.6 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: testbox.py 65040 2016-12-31 02:29:50Z vboxsync $
3
4"""
5Test Manager - TestBox.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2016 Oracle Corporation
11
12This file is part of VirtualBox Open Source Edition (OSE), as
13available from http://www.virtualbox.org. This file is free software;
14you can redistribute it and/or modify it under the terms of the GNU
15General Public License (GPL) as published by the Free Software
16Foundation, in version 2 as it comes in the "COPYING" file of the
17VirtualBox OSE distribution. VirtualBox OSE is distributed in the
18hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
19
20The contents of this file may alternatively be used under the terms
21of the Common Development and Distribution License Version 1.0
22(CDDL) only, as it comes in the "COPYING.CDDL" file of the
23VirtualBox OSE distribution, in which case the provisions of the
24CDDL are applicable instead of those of the GPL.
25
26You may elect to license modified versions of this file under the
27terms and conditions of either the GPL or the CDDL or both.
28"""
29__version__ = "$Revision: 65040 $"
30
31
32# Standard python imports.
33import copy;
34import unittest;
35
36# Validation Kit imports.
37from testmanager.core import db;
38from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMInFligthCollision, \
39 TMInvalidData, TMTooManyRows, TMRowNotFound, \
40 ChangeLogEntry, AttributeChangeEntry, AttributeChangeEntryPre;
41from testmanager.core.useraccount import UserAccountLogic;
42
43
44class TestBoxInSchedGroupData(ModelDataBase):
45 """
46 TestBox in SchedGroup data.
47 """
48
49 ksParam_idTestBox = 'TestBoxInSchedGroup_idTestBox';
50 ksParam_idSchedGroup = 'TestBoxInSchedGroup_idSchedGroup';
51 ksParam_tsEffective = 'TestBoxInSchedGroup_tsEffective';
52 ksParam_tsExpire = 'TestBoxInSchedGroup_tsExpire';
53 ksParam_uidAuthor = 'TestBoxInSchedGroup_uidAuthor';
54 ksParam_iSchedPriority = 'TestBoxInSchedGroup_iSchedPriority';
55
56 kasAllowNullAttributes = [ 'idTestBox', 'tsEffective', 'tsExpire', 'uidAuthor', ]
57
58 kiMin_iSchedPriority = 0;
59 kiMax_iSchedPriority = 32;
60
61 kcDbColumns = 6;
62
63 def __init__(self):
64 ModelDataBase.__init__(self);
65 self.idTestBox = None;
66 self.idSchedGroup = None;
67 self.tsEffective = None;
68 self.tsExpire = None;
69 self.uidAuthor = None;
70 self.iSchedPriority = 16;
71
72 def initFromDbRow(self, aoRow):
73 """
74 Expecting the result from a query like this:
75 SELECT * FROM TestBoxesInSchedGroups
76 """
77 if aoRow is None:
78 raise TMRowNotFound('TestBox/SchedGroup not found.');
79
80 self.idTestBox = aoRow[0];
81 self.idSchedGroup = aoRow[1];
82 self.tsEffective = aoRow[2];
83 self.tsExpire = aoRow[3];
84 self.uidAuthor = aoRow[4];
85 self.iSchedPriority = aoRow[5];
86
87 return self;
88
89class TestBoxInSchedGroupDataEx(TestBoxInSchedGroupData):
90 """
91 Extended version of TestBoxInSchedGroupData that contains the scheduling group.
92 """
93
94 def __init__(self):
95 TestBoxInSchedGroupData.__init__(self);
96 self.oSchedGroup = None; # type: SchedGroupData
97
98 def initFromDbRowEx(self, aoRow, oDb, tsNow = None, sPeriodBack = None):
99 """
100 Extended version of initFromDbRow that fills in the rest from the database.
101 """
102 from testmanager.core.schedgroup import SchedGroupData;
103 self.initFromDbRow(aoRow);
104 self.oSchedGroup = SchedGroupData().initFromDbWithId(oDb, self.idSchedGroup, tsNow, sPeriodBack);
105 return self;
106
107
108# pylint: disable=C0103
109class TestBoxData(ModelDataBase): # pylint: disable=R0902
110 """
111 TestBox Data.
112 """
113
114 ## LomKind_T
115 ksLomKind_None = 'none';
116 ksLomKind_ILOM = 'ilom';
117 ksLomKind_ELOM = 'elom';
118 ksLomKind_AppleXserveLom = 'apple-xserver-lom';
119 kasLomKindValues = [ ksLomKind_None, ksLomKind_ILOM, ksLomKind_ELOM, ksLomKind_AppleXserveLom];
120 kaoLomKindDescs = \
121 [
122 ( ksLomKind_None, 'None', ''),
123 ( ksLomKind_ILOM, 'ILOM', ''),
124 ( ksLomKind_ELOM, 'ELOM', ''),
125 ( ksLomKind_AppleXserveLom, 'Apple Xserve LOM', ''),
126 ];
127
128
129 ## TestBoxCmd_T
130 ksTestBoxCmd_None = 'none';
131 ksTestBoxCmd_Abort = 'abort';
132 ksTestBoxCmd_Reboot = 'reboot';
133 ksTestBoxCmd_Upgrade = 'upgrade';
134 ksTestBoxCmd_UpgradeAndReboot = 'upgrade-and-reboot';
135 ksTestBoxCmd_Special = 'special';
136 kasTestBoxCmdValues = [ ksTestBoxCmd_None, ksTestBoxCmd_Abort, ksTestBoxCmd_Reboot, ksTestBoxCmd_Upgrade,
137 ksTestBoxCmd_UpgradeAndReboot, ksTestBoxCmd_Special];
138 kaoTestBoxCmdDescs = \
139 [
140 ( ksTestBoxCmd_None, 'None', ''),
141 ( ksTestBoxCmd_Abort, 'Abort current test', ''),
142 ( ksTestBoxCmd_Reboot, 'Reboot TestBox', ''),
143 ( ksTestBoxCmd_Upgrade, 'Upgrade TestBox Script', ''),
144 ( ksTestBoxCmd_UpgradeAndReboot, 'Upgrade TestBox Script and reboot', ''),
145 ( ksTestBoxCmd_Special, 'Special (reserved)', ''),
146 ];
147
148
149 ksIdAttr = 'idTestBox';
150 ksIdGenAttr = 'idGenTestBox';
151
152 ksParam_idTestBox = 'TestBox_idTestBox';
153 ksParam_tsEffective = 'TestBox_tsEffective';
154 ksParam_tsExpire = 'TestBox_tsExpire';
155 ksParam_uidAuthor = 'TestBox_uidAuthor';
156 ksParam_idGenTestBox = 'TestBox_idGenTestBox';
157 ksParam_ip = 'TestBox_ip';
158 ksParam_uuidSystem = 'TestBox_uuidSystem';
159 ksParam_sName = 'TestBox_sName';
160 ksParam_sDescription = 'TestBox_sDescription';
161 ksParam_fEnabled = 'TestBox_fEnabled';
162 ksParam_enmLomKind = 'TestBox_enmLomKind';
163 ksParam_ipLom = 'TestBox_ipLom';
164 ksParam_pctScaleTimeout = 'TestBox_pctScaleTimeout';
165 ksParam_sComment = 'TestBox_sComment';
166 ksParam_sOs = 'TestBox_sOs';
167 ksParam_sOsVersion = 'TestBox_sOsVersion';
168 ksParam_sCpuVendor = 'TestBox_sCpuVendor';
169 ksParam_sCpuArch = 'TestBox_sCpuArch';
170 ksParam_sCpuName = 'TestBox_sCpuName';
171 ksParam_lCpuRevision = 'TestBox_lCpuRevision';
172 ksParam_cCpus = 'TestBox_cCpus';
173 ksParam_fCpuHwVirt = 'TestBox_fCpuHwVirt';
174 ksParam_fCpuNestedPaging = 'TestBox_fCpuNestedPaging';
175 ksParam_fCpu64BitGuest = 'TestBox_fCpu64BitGuest';
176 ksParam_fChipsetIoMmu = 'TestBox_fChipsetIoMmu';
177 ksParam_fRawMode = 'TestBox_fRawMode';
178 ksParam_cMbMemory = 'TestBox_cMbMemory';
179 ksParam_cMbScratch = 'TestBox_cMbScratch';
180 ksParam_sReport = 'TestBox_sReport';
181 ksParam_iTestBoxScriptRev = 'TestBox_iTestBoxScriptRev';
182 ksParam_iPythonHexVersion = 'TestBox_iPythonHexVersion';
183 ksParam_enmPendingCmd = 'TestBox_enmPendingCmd';
184
185 kasInternalAttributes = [ 'idStrDescription', 'idStrComment', 'idStrOs', 'idStrOsVersion', 'idStrCpuVendor',
186 'idStrCpuArch', 'idStrCpuName', 'idStrReport', ];
187 kasMachineSettableOnly = [ 'sOs', 'sOsVersion', 'sCpuVendor', 'sCpuArch', 'sCpuName', 'lCpuRevision', 'cCpus',
188 'fCpuHwVirt', 'fCpuNestedPaging', 'fCpu64BitGuest', 'fChipsetIoMmu', 'fRawMode',
189 'cMbMemory', 'cMbScratch', 'sReport', 'iTestBoxScriptRev', 'iPythonHexVersion', ];
190 kasAllowNullAttributes = ['idTestBox', 'tsEffective', 'tsExpire', 'uidAuthor', 'idGenTestBox', 'sDescription',
191 'ipLom', 'sComment', ] + kasMachineSettableOnly + kasInternalAttributes;
192
193 kasValidValues_enmLomKind = kasLomKindValues;
194 kasValidValues_enmPendingCmd = kasTestBoxCmdValues;
195 kiMin_pctScaleTimeout = 11;
196 kiMax_pctScaleTimeout = 19999;
197 kcchMax_sReport = 65535;
198
199 kcDbColumns = 40; # including the 7 string joins columns
200
201
202 def __init__(self):
203 ModelDataBase.__init__(self);
204
205 #
206 # Initialize with defaults.
207 # See the database for explanations of each of these fields.
208 #
209 self.idTestBox = None;
210 self.tsEffective = None;
211 self.tsExpire = None;
212 self.uidAuthor = None;
213 self.idGenTestBox = None;
214 self.ip = None;
215 self.uuidSystem = None;
216 self.sName = None;
217 self.idStrDescription = None;
218 self.fEnabled = False;
219 self.enmLomKind = self.ksLomKind_None;
220 self.ipLom = None;
221 self.pctScaleTimeout = 100;
222 self.idStrComment = None;
223 self.idStrOs = None;
224 self.idStrOsVersion = None;
225 self.idStrCpuVendor = None;
226 self.idStrCpuArch = None;
227 self.idStrCpuName = None;
228 self.lCpuRevision = None;
229 self.cCpus = 1;
230 self.fCpuHwVirt = False;
231 self.fCpuNestedPaging = False;
232 self.fCpu64BitGuest = False;
233 self.fChipsetIoMmu = False;
234 self.fRawMode = None;
235 self.cMbMemory = 1;
236 self.cMbScratch = 0;
237 self.idStrReport = None;
238 self.iTestBoxScriptRev = 0;
239 self.iPythonHexVersion = 0;
240 self.enmPendingCmd = self.ksTestBoxCmd_None;
241 # String table values.
242 self.sDescription = None;
243 self.sComment = None;
244 self.sOs = None;
245 self.sOsVersion = None;
246 self.sCpuVendor = None;
247 self.sCpuArch = None;
248 self.sCpuName = None;
249 self.sReport = None;
250
251 def initFromDbRow(self, aoRow):
252 """
253 Internal worker for initFromDbWithId and initFromDbWithGenId as well as
254 from TestBoxLogic. Expecting the result from a query like this:
255 SELECT TestBoxesWithStrings.* FROM TestBoxesWithStrings
256 """
257 if aoRow is None:
258 raise TMRowNotFound('TestBox not found.');
259
260 self.idTestBox = aoRow[0];
261 self.tsEffective = aoRow[1];
262 self.tsExpire = aoRow[2];
263 self.uidAuthor = aoRow[3];
264 self.idGenTestBox = aoRow[4];
265 self.ip = aoRow[5];
266 self.uuidSystem = aoRow[6];
267 self.sName = aoRow[7];
268 self.idStrDescription = aoRow[8];
269 self.fEnabled = aoRow[9];
270 self.enmLomKind = aoRow[10];
271 self.ipLom = aoRow[11];
272 self.pctScaleTimeout = aoRow[12];
273 self.idStrComment = aoRow[13];
274 self.idStrOs = aoRow[14];
275 self.idStrOsVersion = aoRow[15];
276 self.idStrCpuVendor = aoRow[16];
277 self.idStrCpuArch = aoRow[17];
278 self.idStrCpuName = aoRow[18];
279 self.lCpuRevision = aoRow[19];
280 self.cCpus = aoRow[20];
281 self.fCpuHwVirt = aoRow[21];
282 self.fCpuNestedPaging = aoRow[22];
283 self.fCpu64BitGuest = aoRow[23];
284 self.fChipsetIoMmu = aoRow[24];
285 self.fRawMode = aoRow[25];
286 self.cMbMemory = aoRow[26];
287 self.cMbScratch = aoRow[27];
288 self.idStrReport = aoRow[28];
289 self.iTestBoxScriptRev = aoRow[29];
290 self.iPythonHexVersion = aoRow[30];
291 self.enmPendingCmd = aoRow[31];
292
293 # String table values.
294 if len(aoRow) > 32:
295 self.sDescription = aoRow[32];
296 self.sComment = aoRow[33];
297 self.sOs = aoRow[34];
298 self.sOsVersion = aoRow[35];
299 self.sCpuVendor = aoRow[36];
300 self.sCpuArch = aoRow[37];
301 self.sCpuName = aoRow[38];
302 self.sReport = aoRow[39];
303
304 return self;
305
306 def initFromDbWithId(self, oDb, idTestBox, tsNow = None, sPeriodBack = None):
307 """
308 Initialize the object from the database.
309 """
310 oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
311 'SELECT TestBoxesWithStrings.*\n'
312 'FROM TestBoxesWithStrings\n'
313 'WHERE idTestBox = %s\n'
314 , ( idTestBox, ), tsNow, sPeriodBack));
315 aoRow = oDb.fetchOne()
316 if aoRow is None:
317 raise TMRowNotFound('idTestBox=%s not found (tsNow=%s sPeriodBack=%s)' % (idTestBox, tsNow, sPeriodBack,));
318 return self.initFromDbRow(aoRow);
319
320 def initFromDbWithGenId(self, oDb, idGenTestBox, tsNow = None):
321 """
322 Initialize the object from the database.
323 """
324 _ = tsNow; # Only useful for extended data classes.
325 oDb.execute('SELECT TestBoxesWithStrings.*\n'
326 'FROM TestBoxesWithStrings\n'
327 'WHERE idGenTestBox = %s\n'
328 , (idGenTestBox, ) );
329 return self.initFromDbRow(oDb.fetchOne());
330
331 def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other):
332 # Override to do extra ipLom checks.
333 dErrors = ModelDataBase._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor);
334 if self.ksParam_ipLom not in dErrors \
335 and self.ksParam_enmLomKind not in dErrors \
336 and self.enmLomKind != self.ksLomKind_None \
337 and self.ipLom is None:
338 dErrors[self.ksParam_ipLom] = 'Light-out-management IP is mandatory and a LOM is selected.'
339 return dErrors;
340
341 def formatPythonVersion(self):
342 """
343 Unbuttons the version number and formats it as a version string.
344 """
345 if self.iPythonHexVersion is None:
346 return 'N/A';
347 return 'v%d.%d.%d.%d' \
348 % ( self.iPythonHexVersion >> 24,
349 (self.iPythonHexVersion >> 16) & 0xff,
350 (self.iPythonHexVersion >> 8) & 0xff,
351 self.iPythonHexVersion & 0xff);
352
353 def getCpuFamily(self):
354 """ Returns the CPU family for a x86 or amd64 testboxes."""
355 if self.lCpuRevision is None:
356 return 0;
357 return (self.lCpuRevision >> 24 & 0xff);
358
359 def getCpuModel(self):
360 """ Returns the CPU model for a x86 or amd64 testboxes."""
361 if self.lCpuRevision is None:
362 return 0;
363 return (self.lCpuRevision >> 8 & 0xffff);
364
365 def getCpuStepping(self):
366 """ Returns the CPU stepping for a x86 or amd64 testboxes."""
367 if self.lCpuRevision is None:
368 return 0;
369 return (self.lCpuRevision & 0xff);
370
371 # The following is a translation of the g_aenmIntelFamily06 array in CPUMR3CpuId.cpp:
372 kdIntelFamily06 = {
373 0x00: 'P6',
374 0x01: 'P6',
375 0x03: 'P6_II',
376 0x05: 'P6_II',
377 0x06: 'P6_II',
378 0x07: 'P6_III',
379 0x08: 'P6_III',
380 0x09: 'P6_M_Banias',
381 0x0a: 'P6_III',
382 0x0b: 'P6_III',
383 0x0d: 'P6_M_Dothan',
384 0x0e: 'Core_Yonah',
385 0x0f: 'Core2_Merom',
386 0x15: 'P6_M_Dothan',
387 0x16: 'Core2_Merom',
388 0x17: 'Core2_Penryn',
389 0x1a: 'Core7_Nehalem',
390 0x1c: 'Atom_Bonnell',
391 0x1d: 'Core2_Penryn',
392 0x1e: 'Core7_Nehalem',
393 0x1f: 'Core7_Nehalem',
394 0x25: 'Core7_Westmere',
395 0x26: 'Atom_Lincroft',
396 0x27: 'Atom_Saltwell',
397 0x2a: 'Core7_SandyBridge',
398 0x2c: 'Core7_Westmere',
399 0x2d: 'Core7_SandyBridge',
400 0x2e: 'Core7_Nehalem',
401 0x2f: 'Core7_Westmere',
402 0x35: 'Atom_Saltwell',
403 0x36: 'Atom_Saltwell',
404 0x37: 'Atom_Silvermont',
405 0x3a: 'Core7_IvyBridge',
406 0x3c: 'Core7_Haswell',
407 0x3d: 'Core7_Broadwell',
408 0x3e: 'Core7_IvyBridge',
409 0x3f: 'Core7_Haswell',
410 0x45: 'Core7_Haswell',
411 0x46: 'Core7_Haswell',
412 0x47: 'Core7_Broadwell',
413 0x4a: 'Atom_Silvermont',
414 0x4c: 'Atom_Airmount',
415 0x4d: 'Atom_Silvermont',
416 0x4e: 'Core7_Skylake',
417 0x4f: 'Core7_Broadwell',
418 0x55: 'Core7_Skylake',
419 0x56: 'Core7_Broadwell',
420 0x5a: 'Atom_Silvermont',
421 0x5c: 'Atom_Goldmont',
422 0x5d: 'Atom_Silvermont',
423 0x5e: 'Core7_Skylake',
424 0x66: 'Core7_Cannonlake',
425 };
426 # Also from CPUMR3CpuId.cpp, but the switch.
427 kdIntelFamily15 = {
428 0x00: 'NB_Willamette',
429 0x01: 'NB_Willamette',
430 0x02: 'NB_Northwood',
431 0x03: 'NB_Prescott',
432 0x04: 'NB_Prescott2M',
433 0x05: 'NB_Unknown',
434 0x06: 'NB_CedarMill',
435 0x07: 'NB_Gallatin',
436 };
437
438 def queryCpuMicroarch(self):
439 """ Try guess the microarch name for the cpu. Returns None if we cannot. """
440 if self.lCpuRevision is None or self.sCpuVendor is None:
441 return None;
442 uFam = self.getCpuFamily();
443 uMod = self.getCpuModel();
444 if self.sCpuVendor == 'GenuineIntel':
445 if uFam == 6:
446 return self.kdIntelFamily06.get(uMod, None);
447 if uFam == 15:
448 return self.kdIntelFamily15.get(uMod, None);
449 elif self.sCpuVendor == 'AuthenticAMD':
450 if uFam == 0xf:
451 if uMod < 0x10: return 'K8_130nm';
452 if uMod >= 0x60 and uMod < 0x80: return 'K8_65nm';
453 if uMod >= 0x40: return 'K8_90nm_AMDV';
454 if uMod in [0x21, 0x23, 0x2b, 0x37, 0x3f]: return 'K8_90nm_DualCore';
455 return 'AMD_K8_90nm';
456 if uFam == 0x10: return 'K10';
457 if uFam == 0x11: return 'K10_Lion';
458 if uFam == 0x12: return 'K10_Llano';
459 if uFam == 0x14: return 'Bobcat';
460 if uFam == 0x15:
461 if uMod <= 0x01: return 'Bulldozer';
462 if uMod in [0x02, 0x10, 0x13]: return 'Piledriver';
463 return None;
464 if uFam == 0x16:
465 return 'Jaguar';
466 elif self.sCpuVendor == 'CentaurHauls':
467 if uFam == 0x05:
468 if uMod == 0x01: return 'Centaur_C6';
469 if uMod == 0x04: return 'Centaur_C6';
470 if uMod == 0x08: return 'Centaur_C2';
471 if uMod == 0x09: return 'Centaur_C3';
472 if uFam == 0x06:
473 if uMod == 0x05: return 'VIA_C3_M2';
474 if uMod == 0x06: return 'VIA_C3_C5A';
475 if uMod == 0x07: return 'VIA_C3_C5B' if self.getCpuStepping() < 8 else 'VIA_C3_C5C';
476 if uMod == 0x08: return 'VIA_C3_C5N';
477 if uMod == 0x09: return 'VIA_C3_C5XL' if self.getCpuStepping() < 8 else 'VIA_C3_C5P';
478 if uMod == 0x0a: return 'VIA_C7_C5J';
479 if uMod == 0x0f: return 'VIA_Isaiah';
480 return None;
481
482 def getPrettyCpuVersion(self):
483 """ Pretty formatting of the family/model/stepping with microarch optimizations. """
484 if self.lCpuRevision is None or self.sCpuVendor is None:
485 return u'<none>';
486 sMarch = self.queryCpuMicroarch();
487 if sMarch is not None:
488 return '%s m%02X s%02X' % (sMarch, self.getCpuModel(), self.getCpuStepping());
489 return 'fam%02X m%02X s%02X' % (self.getCpuFamily(), self.getCpuModel(), self.getCpuStepping());
490
491 def getArchBitString(self):
492 """ Returns 32-bit, 64-bit, <none>, or sCpuArch. """
493 if self.sCpuArch is None:
494 return '<none>';
495 if self.sCpuArch in [ 'x86',]:
496 return '32-bit';
497 if self.sCpuArch in [ 'amd64',]:
498 return '64-bit';
499 return self.sCpuArch;
500
501 def getPrettyCpuVendor(self):
502 """ Pretty vendor name."""
503 if self.sCpuVendor is None:
504 return '<none>';
505 if self.sCpuVendor == 'GenuineIntel': return 'Intel';
506 if self.sCpuVendor == 'AuthenticAMD': return 'AMD';
507 if self.sCpuVendor == 'CentaurHauls': return 'VIA';
508 return self.sCpuVendor;
509
510
511class TestBoxDataEx(TestBoxData):
512 """
513 TestBox data.
514 """
515
516 ksParam_aoInSchedGroups = 'TestBox_aoInSchedGroups';
517
518 # Use [] instead of None.
519 kasAltArrayNull = [ 'aoInSchedGroups', ];
520
521 ## Helper parameter containing the comma separated list with the IDs of
522 # potential members found in the parameters.
523 ksParam_aidSchedGroups = 'TestBoxDataEx_aidSchedGroups';
524
525 def __init__(self):
526 TestBoxData.__init__(self);
527 self.aoInSchedGroups = []; # type: list[TestBoxInSchedGroupData]
528
529 def _initExtraMembersFromDb(self, oDb, tsNow = None, sPeriodBack = None):
530 """
531 Worker shared by the initFromDb* methods.
532 Returns self. Raises exception if no row or database error.
533 """
534 oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
535 'SELECT *\n'
536 'FROM TestBoxesInSchedGroups\n'
537 'WHERE idTestBox = %s\n'
538 , (self.idTestBox,), tsNow, sPeriodBack)
539 + 'ORDER BY idSchedGroup\n' );
540 self.aoInSchedGroups = [];
541 for aoRow in oDb.fetchAll():
542 self.aoInSchedGroups.append(TestBoxInSchedGroupDataEx().initFromDbRowEx(aoRow, oDb, tsNow, sPeriodBack));
543 return self;
544
545 def initFromDbRowEx(self, aoRow, oDb, tsNow = None):
546 """
547 Reinitialize from a SELECT * FROM TestBoxesWithStrings row. Will query the
548 necessary additional data from oDb using tsNow.
549 Returns self. Raises exception if no row or database error.
550 """
551 TestBoxData.initFromDbRow(self, aoRow);
552 return self._initExtraMembersFromDb(oDb, tsNow);
553
554 def initFromDbWithId(self, oDb, idTestBox, tsNow = None, sPeriodBack = None):
555 """
556 Initialize the object from the database.
557 """
558 TestBoxData.initFromDbWithId(self, oDb, idTestBox, tsNow, sPeriodBack);
559 return self._initExtraMembersFromDb(oDb, tsNow, sPeriodBack);
560
561 def initFromDbWithGenId(self, oDb, idGenTestBox, tsNow = None):
562 """
563 Initialize the object from the database.
564 """
565 TestBoxData.initFromDbWithGenId(self, oDb, idGenTestBox);
566 if tsNow is None and not oDb.isTsInfinity(self.tsExpire):
567 tsNow = self.tsEffective;
568 return self._initExtraMembersFromDb(oDb, tsNow);
569
570 def getAttributeParamNullValues(self, sAttr): # Necessary?
571 if sAttr in ['aoInSchedGroups', ]:
572 return [[], ''];
573 return TestBoxData.getAttributeParamNullValues(self, sAttr);
574
575 def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict):
576 """
577 For dealing with the in-scheduling-group list.
578 """
579 if sAttr != 'aoInSchedGroups':
580 return TestBoxData.convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict);
581
582 aoNewValues = [];
583 aidSelected = oDisp.getListOfIntParams(sParam, iMin = 1, iMax = 0x7ffffffe, aiDefaults = []);
584 asIds = oDisp.getStringParam(self.ksParam_aidSchedGroups, sDefault = '').split(',');
585 for idSchedGroup in asIds:
586 try: idSchedGroup = int(idSchedGroup);
587 except: pass;
588 oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (TestBoxDataEx.ksParam_aoInSchedGroups, idSchedGroup,))
589 oMember = TestBoxInSchedGroupData().initFromParams(oDispWrapper, fStrict = False);
590 if idSchedGroup in aidSelected:
591 aoNewValues.append(oMember);
592 return aoNewValues;
593
594 def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): # pylint: disable=R0914
595 """
596 Validate special arrays and requirement expressions.
597
598 Some special needs for the in-scheduling-group list.
599 """
600 if sAttr != 'aoInSchedGroups':
601 return TestBoxData._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
602
603 asErrors = [];
604 aoNewValues = [];
605
606 # Note! We'll be returning an error dictionary instead of an string here.
607 dErrors = {};
608
609 for iInGrp, oInSchedGroup in enumerate(self.aoInSchedGroups):
610 oInSchedGroup = copy.copy(oInSchedGroup);
611 oInSchedGroup.idTestBox = self.idTestBox;
612 dCurErrors = oInSchedGroup.validateAndConvert(oDb, ModelDataBase.ksValidateFor_Other);
613 if len(dCurErrors) == 0:
614 pass; ## @todo figure out the ID?
615 else:
616 asErrors = [];
617 for sKey in dCurErrors:
618 asErrors.append('%s: %s' % (sKey[len('TestBoxInSchedGroup_'):], dCurErrors[sKey]));
619 dErrors[iInGrp] = '<br>\n'.join(asErrors)
620 aoNewValues.append(oInSchedGroup);
621
622 for iInGrp, oInSchedGroup in enumerate(self.aoInSchedGroups):
623 for iInGrp2 in xrange(iInGrp + 1, len(self.aoInSchedGroups)):
624 if self.aoInSchedGroups[iInGrp2].idSchedGroup == oInSchedGroup.idSchedGroup:
625 sMsg = 'Duplicate scheduling group #%s".' % (oInSchedGroup.idSchedGroup,);
626 if iInGrp in dErrors: dErrors[iInGrp] += '<br>\n' + sMsg;
627 else: dErrors[iInGrp] = sMsg;
628 if iInGrp2 in dErrors: dErrors[iInGrp2] += '<br>\n' + sMsg;
629 else: dErrors[iInGrp2] = sMsg;
630 break;
631
632 return (aoNewValues, dErrors if len(dErrors) > 0 else None);
633
634
635class TestBoxLogic(ModelLogicBase):
636 """
637 TestBox logic.
638 """
639
640
641 def __init__(self, oDb):
642 ModelLogicBase.__init__(self, oDb);
643 self.dCache = None;
644
645 def tryFetchTestBoxByUuid(self, sTestBoxUuid):
646 """
647 Tries to fetch a testbox by its UUID alone.
648 """
649 self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
650 'FROM TestBoxesWithStrings\n'
651 'WHERE uuidSystem = %s\n'
652 ' AND tsExpire = \'infinity\'::timestamp\n'
653 'ORDER BY tsEffective DESC\n',
654 (sTestBoxUuid,));
655 if self._oDb.getRowCount() == 0:
656 return None;
657 if self._oDb.getRowCount() != 1:
658 raise TMTooManyRows('Database integrity error: %u hits' % (self._oDb.getRowCount(),));
659 oData = TestBoxData();
660 oData.initFromDbRow(self._oDb.fetchOne());
661 return oData;
662
663 def fetchForListing(self, iStart, cMaxRows, tsNow):
664 """
665 Fetches testboxes for listing.
666
667 Returns an array (list) of TestBoxDataForListing items, empty list if none.
668 The TestBoxDataForListing instances are just TestBoxData with two extra
669 members, an extra oStatus member that is either None or a TestBoxStatusData
670 instance, and a member tsCurrent holding CURRENT_TIMESTAMP.
671
672 Raises exception on error.
673 """
674 class TestBoxDataForListing(TestBoxDataEx):
675 """ We add two members for the listing. """
676 def __init__(self):
677 TestBoxDataEx.__init__(self);
678 self.tsCurrent = None; # CURRENT_TIMESTAMP
679 self.oStatus = None; # type: TestBoxStatusData
680
681 from testmanager.core.testboxstatus import TestBoxStatusData;
682
683 if tsNow is None:
684 self._oDb.execute('SELECT TestBoxesWithStrings.*,\n'
685 ' TestBoxStatuses.*\n'
686 'FROM TestBoxesWithStrings\n'
687 ' LEFT OUTER JOIN TestBoxStatuses\n'
688 ' ON TestBoxStatuses.idTestBox = TestBoxesWithStrings.idTestBox\n'
689 'WHERE TestBoxesWithStrings.tsExpire = \'infinity\'::TIMESTAMP\n'
690 'ORDER BY TestBoxesWithStrings.sName\n'
691 'LIMIT %s OFFSET %s\n'
692 , (cMaxRows, iStart,));
693 else:
694 self._oDb.execute('SELECT TestBoxesWithStrings.*,\n'
695 ' TestBoxStatuses.*\n'
696 'FROM TestBoxesWithStrings\n'
697 ' LEFT OUTER JOIN TestBoxStatuses\n'
698 ' ON TestBoxStatuses.idTestBox = TestBoxesWithStrings.idTestBox\n'
699 'WHERE tsExpire > %s\n'
700 ' AND tsEffective <= %s\n'
701 'ORDER BY TestBoxesWithStrings.sName\n'
702 'LIMIT %s OFFSET %s\n'
703 , ( tsNow, tsNow, cMaxRows, iStart,));
704
705 aoRows = [];
706 for aoOne in self._oDb.fetchAll():
707 oTestBox = TestBoxDataForListing().initFromDbRowEx(aoOne, self._oDb, tsNow);
708 oTestBox.tsCurrent = self._oDb.getCurrentTimestamp();
709 if aoOne[TestBoxData.kcDbColumns] is not None:
710 oTestBox.oStatus = TestBoxStatusData().initFromDbRow(aoOne[TestBoxData.kcDbColumns:]);
711 aoRows.append(oTestBox);
712 return aoRows;
713
714 def fetchForChangeLog(self, idTestBox, iStart, cMaxRows, tsNow): # pylint: disable=R0914
715 """
716 Fetches change log entries for a testbox.
717
718 Returns an array of ChangeLogEntry instance and an indicator whether
719 there are more entries.
720 Raises exception on error.
721 """
722
723 ## @todo calc changes to scheduler group!
724
725 if tsNow is None:
726 tsNow = self._oDb.getCurrentTimestamp();
727
728 self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
729 'FROM TestBoxesWithStrings\n'
730 'WHERE TestBoxesWithStrings.tsEffective <= %s\n'
731 ' AND TestBoxesWithStrings.idTestBox = %s\n'
732 'ORDER BY TestBoxesWithStrings.tsExpire DESC\n'
733 'LIMIT %s OFFSET %s\n'
734 , (tsNow, idTestBox, cMaxRows + 1, iStart,));
735
736 aoRows = [];
737 for aoDbRow in self._oDb.fetchAll():
738 aoRows.append(TestBoxData().initFromDbRow(aoDbRow));
739
740 # Calculate the changes.
741 aoEntries = [];
742 for i in xrange(0, len(aoRows) - 1):
743 oNew = aoRows[i];
744 oOld = aoRows[i + 1];
745 aoChanges = [];
746 for sAttr in oNew.getDataAttributes():
747 if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
748 oOldAttr = getattr(oOld, sAttr);
749 oNewAttr = getattr(oNew, sAttr);
750 if oOldAttr != oNewAttr:
751 if sAttr == 'sReport':
752 aoChanges.append(AttributeChangeEntryPre(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
753 else:
754 aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
755 aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges));
756
757 # If we're at the end of the log, add the initial entry.
758 if len(aoRows) <= cMaxRows and len(aoRows) > 0:
759 oNew = aoRows[-1];
760 aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, []));
761
762 UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries);
763 return (aoEntries, len(aoRows) > cMaxRows);
764
765 def _validateAndConvertData(self, oData, enmValidateFor):
766 # type: (TestBoxDataEx, str) -> None
767 """
768 Helper for addEntry and editEntry that validates the scheduling group IDs in
769 addtion to what's covered by the default validateAndConvert of the data object.
770
771 Raises exception on invalid input.
772 """
773 dDataErrors = oData.validateAndConvert(self._oDb, enmValidateFor);
774 if len(dDataErrors) > 0:
775 raise TMInvalidData('TestBoxLogic.addEntry: %s' % (dDataErrors,));
776 if isinstance(oData, TestBoxDataEx):
777 if len(oData.aoInSchedGroups):
778 sSchedGrps = ', '.join('(%s)' % oCur.idSchedGroup for oCur in oData.aoInSchedGroups);
779 self._oDb.execute('SELECT SchedGroupIDs.idSchedGroup\n'
780 'FROM (VALUES ' + sSchedGrps + ' ) AS SchedGroupIDs(idSchedGroup)\n'
781 ' LEFT OUTER JOIN SchedGroups\n'
782 ' ON SchedGroupIDs.idSchedGroup = SchedGroups.idSchedGroup\n'
783 ' AND SchedGroups.tsExpire = \'infinity\'::TIMESTAMP\n'
784 'WHERE SchedGroups.idSchedGroup IS NULL\n');
785 aaoRows = self._oDb.fetchAll();
786 if len(aaoRows) > 0:
787 raise TMInvalidData('TestBoxLogic.addEntry missing scheduling groups: %s'
788 % (', '.join(str(aoRow[0]) for aoRow in aaoRows),));
789 return None;
790
791 def addEntry(self, oData, uidAuthor, fCommit = False):
792 # type: (TestBoxDataEx, int, bool) -> (int, int, datetime.datetime)
793 """
794 Creates a testbox in the database.
795 Returns the testbox ID, testbox generation ID and effective timestamp
796 of the created testbox on success. Throws error on failure.
797 """
798
799 #
800 # Validate. Extra work because of missing foreign key (due to history).
801 #
802 self._validateAndConvertData(oData, oData.ksValidateFor_Add);
803
804 #
805 # Do it.
806 #
807 self._oDb.callProc('TestBoxLogic_addEntry'
808 , ( uidAuthor,
809 oData.ip, # Should we allow setting the IP?
810 oData.uuidSystem,
811 oData.sName,
812 oData.sDescription,
813 oData.fEnabled,
814 oData.enmLomKind,
815 oData.ipLom,
816 oData.pctScaleTimeout,
817 oData.sComment,
818 oData.enmPendingCmd, ) );
819 (idTestBox, idGenTestBox, tsEffective) = self._oDb.fetchOne();
820
821 for oInSchedGrp in oData.aoInSchedGroups:
822 self._oDb.callProc('TestBoxLogic_addGroupEntry',
823 ( uidAuthor, idTestBox, oInSchedGrp.idSchedGroup, oInSchedGrp.iSchedPriority,) );
824
825 self._oDb.maybeCommit(fCommit);
826 return (idTestBox, idGenTestBox, tsEffective);
827
828
829 def editEntry(self, oData, uidAuthor, fCommit = False):
830 """
831 Data edit update, web UI is the primary user.
832
833 oData is either TestBoxDataEx or TestBoxData. The latter is for enabling
834 Returns the new generation ID and effective date.
835 """
836
837 #
838 # Validate.
839 #
840 self._validateAndConvertData(oData, oData.ksValidateFor_Edit);
841
842 #
843 # Get current data.
844 #
845 oOldData = TestBoxDataEx().initFromDbWithId(self._oDb, oData.idTestBox);
846
847 #
848 # Do it.
849 #
850 if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', 'aoInSchedGroups', ]
851 + TestBoxData.kasMachineSettableOnly ):
852 self._oDb.callProc('TestBoxLogic_editEntry'
853 , ( uidAuthor,
854 oData.idTestBox,
855 oData.ip, # Should we allow setting the IP?
856 oData.uuidSystem,
857 oData.sName,
858 oData.sDescription,
859 oData.fEnabled,
860 oData.enmLomKind,
861 oData.ipLom,
862 oData.pctScaleTimeout,
863 oData.sComment,
864 oData.enmPendingCmd, ));
865 (idGenTestBox, tsEffective) = self._oDb.fetchOne();
866 else:
867 idGenTestBox = oOldData.idGenTestBox;
868 tsEffective = oOldData.tsEffective;
869
870 if isinstance(oData, TestBoxDataEx):
871 # Calc in-group changes.
872 aoRemoved = list(oOldData.aoInSchedGroups);
873 aoNew = [];
874 aoUpdated = [];
875 for oNewInGroup in oData.aoInSchedGroups:
876 oOldInGroup = None;
877 for iCur, oCur in enumerate(aoRemoved):
878 if oCur.idSchedGroup == oNewInGroup.idSchedGroup:
879 oOldInGroup = aoRemoved.pop(iCur);
880 break;
881 if oOldInGroup is None:
882 aoNew.append(oNewInGroup);
883 elif oNewInGroup.iSchedPriority != oOldInGroup.iSchedPriority:
884 aoUpdated.append(oNewInGroup);
885
886 # Remove in-groups.
887 for oInGroup in aoRemoved:
888 self._oDb.callProc('TestBoxLogic_removeGroupEntry', (uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, ));
889
890 # Add new ones.
891 for oInGroup in aoNew:
892 self._oDb.callProc('TestBoxLogic_addGroupEntry',
893 ( uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, oInGroup.iSchedPriority, ) );
894
895 # Edit existing ones.
896 for oInGroup in aoUpdated:
897 self._oDb.callProc('TestBoxLogic_editGroupEntry',
898 ( uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, oInGroup.iSchedPriority, ) );
899 else:
900 assert isinstance(oData, TestBoxData);
901
902 self._oDb.maybeCommit(fCommit);
903 return (idGenTestBox, tsEffective);
904
905
906 def removeEntry(self, uidAuthor, idTestBox, fCascade = False, fCommit = False):
907 """
908 Delete test box and scheduling group associations.
909 """
910 self._oDb.callProc('TestBoxLogic_removeEntry'
911 , ( uidAuthor, idTestBox, fCascade,));
912 self._oDb.maybeCommit(fCommit);
913 return True;
914
915
916 def updateOnSignOn(self, idTestBox, idGenTestBox, sTestBoxAddr, sOs, sOsVersion, # pylint: disable=R0913,R0914
917 sCpuVendor, sCpuArch, sCpuName, lCpuRevision, cCpus, fCpuHwVirt, fCpuNestedPaging, fCpu64BitGuest,
918 fChipsetIoMmu, fRawMode, cMbMemory, cMbScratch, sReport, iTestBoxScriptRev, iPythonHexVersion):
919 """
920 Update the testbox attributes automatically on behalf of the testbox script.
921 Returns the new generation id on success, raises an exception on failure.
922 """
923 _ = idGenTestBox;
924 self._oDb.callProc('TestBoxLogic_updateOnSignOn'
925 , ( idTestBox,
926 sTestBoxAddr,
927 sOs,
928 sOsVersion,
929 sCpuVendor,
930 sCpuArch,
931 sCpuName,
932 lCpuRevision,
933 cCpus,
934 fCpuHwVirt,
935 fCpuNestedPaging,
936 fCpu64BitGuest,
937 fChipsetIoMmu,
938 fRawMode,
939 cMbMemory,
940 cMbScratch,
941 sReport,
942 iTestBoxScriptRev,
943 iPythonHexVersion,));
944 return self._oDb.fetchOne()[0];
945
946
947 def setCommand(self, idTestBox, sOldCommand, sNewCommand, uidAuthor = None, fCommit = False, sComment = None):
948 """
949 Sets or resets the pending command on a testbox.
950 Returns (idGenTestBox, tsEffective) of the new row.
951 """
952 ## @todo throw TMInFligthCollision again...
953 self._oDb.callProc('TestBoxLogic_setCommand'
954 , ( uidAuthor, idTestBox, sOldCommand, sNewCommand, sComment,));
955 aoRow = self._oDb.fetchOne();
956 self._oDb.maybeCommit(fCommit);
957 return (aoRow[0], aoRow[1]);
958
959
960 def getAll(self):
961 """
962 Retrieve list of all registered Test Box records from DB.
963 """
964 self._oDb.execute('SELECT *\n'
965 'FROM TestBoxesWithStrings\n'
966 'WHERE tsExpire=\'infinity\'::timestamp;')
967
968 aaoRows = self._oDb.fetchAll()
969 aoRet = []
970 for aoRow in aaoRows:
971 aoRet.append(TestBoxData().initFromDbRow(aoRow))
972 return aoRet
973
974
975 def cachedLookup(self, idTestBox):
976 # type: (int) -> TestBoxDataEx
977 """
978 Looks up the most recent TestBoxData object for idTestBox via
979 an object cache.
980
981 Returns a shared TestBoxDataEx object. None if not found.
982 Raises exception on DB error.
983 """
984 if self.dCache is None:
985 self.dCache = self._oDb.getCache('TestBoxData');
986 oEntry = self.dCache.get(idTestBox, None);
987 if oEntry is None:
988 fNeedNow = False;
989 self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
990 'FROM TestBoxesWithStrings\n'
991 'WHERE idTestBox = %s\n'
992 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
993 , (idTestBox, ));
994 if self._oDb.getRowCount() == 0:
995 # Maybe it was deleted, try get the last entry.
996 self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
997 'FROM TestBoxesWithStrings\n'
998 'WHERE idTestBox = %s\n'
999 'ORDER BY tsExpire DESC\n'
1000 'LIMIT 1\n'
1001 , (idTestBox, ));
1002 fNeedNow = True;
1003 elif self._oDb.getRowCount() > 1:
1004 raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestBox));
1005
1006 if self._oDb.getRowCount() == 1:
1007 aaoRow = self._oDb.fetchOne();
1008 if not fNeedNow:
1009 oEntry = TestBoxDataEx().initFromDbRowEx(aaoRow, self._oDb);
1010 else:
1011 oEntry = TestBoxDataEx().initFromDbRow(aaoRow);
1012 oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow = db.dbTimestampMinusOneTick(oEntry.tsExpire));
1013 self.dCache[idTestBox] = oEntry;
1014 return oEntry;
1015
1016
1017
1018 #
1019 # The virtual test sheriff interface.
1020 #
1021
1022 def hasTestBoxRecentlyBeenRebooted(self, idTestBox, cHoursBack = 2, tsNow = None):
1023 """
1024 Checks if the testbox has been rebooted in the specified time period.
1025
1026 This does not include already pending reboots, though under some
1027 circumstances it may. These being the test box entry being edited for
1028 other reasons.
1029
1030 Returns True / False.
1031 """
1032 if tsNow is None:
1033 tsNow = self._oDb.getCurrentTimestamp();
1034 self._oDb.execute('SELECT COUNT(idTestBox)\n'
1035 'FROM TestBoxes\n'
1036 'WHERE idTestBox = %s\n'
1037 ' AND tsExpire < %s\n'
1038 ' AND tsExpire >= %s - interval \'%s hours\'\n'
1039 ' AND enmPendingCmd IN (%s, %s)\n'
1040 , ( idTestBox, tsNow, tsNow, cHoursBack,
1041 TestBoxData.ksTestBoxCmd_Reboot, TestBoxData.ksTestBoxCmd_UpgradeAndReboot, ));
1042 return self._oDb.fetchOne()[0] > 0;
1043
1044
1045 def rebootTestBox(self, idTestBox, uidAuthor, sComment, sOldCommand = TestBoxData.ksTestBoxCmd_None, fCommit = False):
1046 """
1047 Issues a reboot command for the given test box.
1048 Return True on succes, False on in-flight collision.
1049 May raise DB exception on other trouble.
1050 """
1051 try:
1052 self.setCommand(idTestBox, sOldCommand, TestBoxData.ksTestBoxCmd_Reboot,
1053 uidAuthor = uidAuthor, fCommit = fCommit, sComment = sComment);
1054 except TMInFligthCollision:
1055 return False;
1056 except:
1057 raise;
1058 return True;
1059
1060
1061 def disableTestBox(self, idTestBox, uidAuthor, sComment, fCommit = False):
1062 """
1063 Disables the given test box.
1064
1065 Raises exception on trouble, without rollback.
1066 """
1067 oTestBox = TestBoxData().initFromDbWithId(self._oDb, idTestBox);
1068 if oTestBox.fEnabled:
1069 oTestBox.fEnabled = False;
1070 if sComment is not None:
1071 oTestBox.sComment = sComment;
1072 self.editEntry(oTestBox, uidAuthor = uidAuthor, fCommit = fCommit);
1073 return None;
1074
1075
1076#
1077# Unit testing.
1078#
1079
1080# pylint: disable=C0111
1081class TestBoxDataTestCase(ModelDataBaseTestCase):
1082 def setUp(self):
1083 self.aoSamples = [TestBoxData(),];
1084
1085if __name__ == '__main__':
1086 unittest.main();
1087 # not reached.
1088
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